diff --git a/.dockerignore b/.dockerignore index 76aae0c402..17211b25eb 100644 --- a/.dockerignore +++ b/.dockerignore @@ -8,6 +8,7 @@ .gitconfig .github .gitignore +.pnpm-store .husky .next .prettierignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d0437b5e90..93443e34af 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,24 +35,25 @@ jobs: NUMBER: ${{ github.event.pull_request.number }} steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - name: Pnpm Setup - uses: pnpm/action-setup@903f9c1a6ebcba6cf41d87230be49611ac97822e # v6.0.3 + - parallel: + - name: Pnpm Setup + uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 - - name: Set up Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version-file: 'package.json' + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version-file: 'package.json' - name: Get pnpm store directory shell: bash run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - name: Setup pnpm cache - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ env.STORE_PATH }} key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} @@ -93,12 +94,12 @@ jobs: container: node:22.22.2-alpine3.23@sha256:8ea2348b068a9544dae7317b4f3aafcdc032df1647bb7d768a05a5cad1a7683f steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Pnpm Setup - uses: pnpm/action-setup@903f9c1a6ebcba6cf41d87230be49611ac97822e # v6.0.3 + uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 - name: Get pnpm store directory shell: sh @@ -106,7 +107,7 @@ jobs: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - name: Setup pnpm cache - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ env.STORE_PATH }} key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} @@ -118,14 +119,15 @@ jobs: CI: true run: pnpm install - - name: Lint - run: pnpm lint + - parallel: + - name: Lint + run: pnpm lint - - name: Formatting - run: pnpm format:check + - name: Formatting + run: pnpm format:check - - name: Build - run: pnpm build + - name: Build + run: pnpm build unit-test: name: Unit Tests @@ -136,12 +138,12 @@ jobs: checks: write steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Pnpm Setup - uses: pnpm/action-setup@903f9c1a6ebcba6cf41d87230be49611ac97822e # v6.0.3 + uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 - name: Get pnpm store directory shell: sh @@ -149,7 +151,7 @@ jobs: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - name: Setup pnpm cache - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ env.STORE_PATH }} key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} @@ -167,7 +169,7 @@ jobs: run: pnpm test - name: Publish test report - uses: mikepenz/action-junit-report@bccf2e31636835cf0874589931c4116687171386 # v6.4.0 + uses: mikepenz/action-junit-report@a9170d5795813c01ab4901ffb045b52bab4ab09d # v6.5.0 if: success() || failure() # always run even if the previous step fails with: report_paths: 'report.xml' @@ -186,20 +188,21 @@ jobs: arch: arm64 runs-on: ${{ matrix.runner }} steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false + - parallel: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4 - name: Commit timestamp id: ts run: echo "TIMESTAMP=$(git log -1 --pretty=%ct)" >> "$GITHUB_OUTPUT" - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 - - name: Warm cache (no push) — ${{ matrix.platform }} - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: context: . file: ./Dockerfile @@ -222,34 +225,35 @@ jobs: packages: write id-token: write steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - name: Commit timestamp - id: ts - run: echo "TIMESTAMP=$(git log -1 --pretty=%ct)" >> "$GITHUB_OUTPUT" + - parallel: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4 - name: Log in to Docker Hub - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_TOKEN }} - name: Log in to GitHub Container Registry - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Commit timestamp + id: ts + run: echo "TIMESTAMP=$(git log -1 --pretty=%ct)" >> "$GITHUB_OUTPUT" + - name: Extract metadata id: meta - uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6 + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6 with: images: | ${{ env.DOCKER_HUB }} @@ -261,7 +265,7 @@ jobs: org.opencontainers.image.created=${{ steps.ts.outputs.TIMESTAMP }} - name: Build & Push (multi-arch, single tag) - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: context: . file: ./Dockerfile diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 9e0f15b49c..8f1d8f8aee 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -36,21 +36,22 @@ jobs: matrix: language: [actions, javascript] steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false + - parallel: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - - name: Initialize CodeQL - uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 - with: - languages: ${{ matrix.language }} - queries: +security-and-quality + - name: Initialize CodeQL + uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 + with: + languages: ${{ matrix.language }} + queries: +security-and-quality - name: Autobuild - uses: github/codeql-action/autobuild@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 + uses: github/codeql-action/autobuild@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 + uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 with: category: '/language:${{ matrix.language }}' diff --git a/.github/workflows/conflict_labeler.yml b/.github/workflows/conflict_labeler.yml index 61e5d29a8d..220caa2196 100644 --- a/.github/workflows/conflict_labeler.yml +++ b/.github/workflows/conflict_labeler.yml @@ -28,7 +28,7 @@ jobs: pull-requests: write steps: - name: Apply label - uses: eps1lon/actions-label-merge-conflict@1df065ebe6e3310545d4f4c4e862e43bdca146f0 # v3.0.3 + uses: eps1lon/actions-label-merge-conflict@0273be72a0bbd58fcd71d0d6c02c209b50d1e5e1 # v3.1.0 with: dirtyLabel: 'merge conflict' commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.' diff --git a/.github/workflows/create-tag.yml b/.github/workflows/create-tag.yml index 35ef124c9f..eabb687dac 100644 --- a/.github/workflows/create-tag.yml +++ b/.github/workflows/create-tag.yml @@ -22,16 +22,17 @@ jobs: outputs: tag_version: ${{ steps.git-cliff.outputs.tag_version }} steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 - persist-credentials: false + - parallel: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false - - name: Install git-cliff - uses: taiki-e/install-action@481c34c1cf3a84c68b5e46f4eccfc82af798415a # v2.75.23 - with: - tool: git-cliff + - name: Install git-cliff + uses: taiki-e/install-action@742a3317eac7bd62f91cd888b4eead5e784ba833 # v2.87.1 + with: + tool: git-cliff - name: Get tag version id: git-cliff @@ -55,25 +56,26 @@ jobs: TAG_VERSION: ${{ needs.determine-tag-version.outputs.tag_version }} steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ssh-key: '${{ secrets.COMMIT_KEY }}' - - name: Pnpm Setup - uses: pnpm/action-setup@903f9c1a6ebcba6cf41d87230be49611ac97822e # v6.0.3 + - parallel: + - name: Pnpm Setup + uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 - - name: Set up Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version-file: 'package.json' - # For workflows with elevated privileges we recommend disabling automatic caching. - # https://github.com/actions/setup-node - package-manager-cache: false + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version-file: 'package.json' + # For workflows with elevated privileges we recommend disabling automatic caching. + # https://github.com/actions/setup-node + package-manager-cache: false - - name: Configure git - run: | - git config --global user.name "${{ github.actor }}" - git config --global user.email "${{ github.actor }}@users.noreply.github.com" + - name: Configure git + run: | + git config --global user.name "${{ github.actor }}" + git config --global user.email "${{ github.actor }}@users.noreply.github.com" - name: Bump package.json run: npm version ${TAG_VERSION} --no-commit-hooks --no-git-tag-version diff --git a/.github/workflows/cypress.yml b/.github/workflows/cypress.yml index e86bd917f5..a95f1aeedd 100644 --- a/.github/workflows/cypress.yml +++ b/.github/workflows/cypress.yml @@ -10,7 +10,7 @@ on: - 'cypress.config.ts' - 'package.json' - 'pnpm-lock.yaml' - - 'next.config.js' + - 'next.config.ts' - 'tsconfig.json' - '.github/workflows/cypress.yml' push: @@ -20,7 +20,7 @@ on: - 'cypress.config.ts' - 'package.json' - 'pnpm-lock.yaml' - - 'next.config.js' + - 'next.config.ts' - 'tsconfig.json' - '.github/workflows/cypress.yml' @@ -37,24 +37,24 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false + - parallel: + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version-file: package.json + package-manager-cache: false - - name: Set up Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version-file: package.json - package-manager-cache: false - - - name: Pnpm Setup - uses: pnpm/action-setup@903f9c1a6ebcba6cf41d87230be49611ac97822e # v6.0.3 + - name: Pnpm Setup + uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 - name: Install dependencies run: pnpm install --frozen-lockfile - name: Setup cypress cache - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/.cache/Cypress key: ${{ runner.os }}-cypress-store-${{ hashFiles('**/pnpm-lock.yaml') }} @@ -67,7 +67,7 @@ jobs: run: pnpm exec cypress install - name: Cypress run - uses: cypress-io/github-action@c495c3ddffba403ba11be95fffb67e25203b3799 # v7.1.10 + uses: cypress-io/github-action@09090944bd8aaa2a517cefb6e38bf1aae336b42b # v7.4.3 with: install: false build: pnpm cypress:build diff --git a/.github/workflows/detect-duplicate.yml b/.github/workflows/detect-duplicate.yml index 842fbc5b66..5b46ef6125 100644 --- a/.github/workflows/detect-duplicate.yml +++ b/.github/workflows/detect-duplicate.yml @@ -8,7 +8,6 @@ on: permissions: {} env: - EMBEDDING_MODEL: ${{ vars.EMBEDDING_MODEL }} GROQ_MODEL: ${{ vars.GROQ_MODEL }} GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }} @@ -23,22 +22,23 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Pnpm Setup - uses: pnpm/action-setup@903f9c1a6ebcba6cf41d87230be49611ac97822e # v6.0.3 + - parallel: + - name: Pnpm Setup + uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 - - name: Set up Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version-file: 'package.json' + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version-file: 'package.json' - name: Get pnpm store directory shell: bash run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - name: Setup pnpm cache - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ env.STORE_PATH }} key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} @@ -51,14 +51,25 @@ jobs: CI: true run: pnpm install --frozen-lockfile - - name: Download issue index - uses: dawidd6/action-download-artifact@8305c0f1062bb0d184d09ef4493ecb9288447732 # v20 + - name: Get latest successful run id + id: get-latest-run-id + run: | + RUN_ID=$(gh api \ + /repos/${{ github.repository }}/actions/workflows/rebuild-issue-index.yml/runs \ + --jq '[.workflow_runs[] | select(.status=="completed" and .conclusion=="success")][0].id // empty') + echo "run_id=$RUN_ID" >> "$GITHUB_OUTPUT" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Download artifact + if: steps.get-latest-run-id.outputs.run_id != '' + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: issue-index - workflow: rebuild-issue-index.yml path: bin/duplicate-detector - search_artifacts: true - if_no_artifact_found: warn + run-id: ${{ steps.get-latest-run-id.outputs.run_id }} + github-token: ${{ secrets.GITHUB_TOKEN }} - name: Build index if missing working-directory: bin/duplicate-detector diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml index 32c85a4b2b..8363a48095 100644 --- a/.github/workflows/docs-deploy.yml +++ b/.github/workflows/docs-deploy.yml @@ -24,19 +24,19 @@ jobs: name: Build Docusaurus runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false + - parallel: + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version-file: package.json + package-manager-cache: false - - name: Set up Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version-file: package.json - package-manager-cache: false - - - name: Pnpm Setup - uses: pnpm/action-setup@903f9c1a6ebcba6cf41d87230be49611ac97822e # v6.0.3 + - name: Pnpm Setup + uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 - name: Get pnpm store directory shell: sh @@ -44,7 +44,7 @@ jobs: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - name: Setup pnpm cache - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ env.STORE_PATH }} key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} @@ -58,7 +58,7 @@ jobs: - name: Build website working-directory: gen-docs - run: pnpm gen-api-docs all && pnpm build + run: pnpm gen-api-docs && pnpm build - name: Upload Build Artifact uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b6 # v4.0.0 diff --git a/.github/workflows/docs-link-check.yml b/.github/workflows/docs-link-check.yml index 8c988c878d..724c7487a7 100644 --- a/.github/workflows/docs-link-check.yml +++ b/.github/workflows/docs-link-check.yml @@ -36,13 +36,13 @@ jobs: steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false - name: Run Lychee link checker - uses: lycheeverse/lychee-action@8646ba30535128ac92d33dfc9133794bfdd9b411 # v2.8.0 + uses: lycheeverse/lychee-action@e7477775783ea5526144ba13e8db5eec57747ce8 # v2.9.0 with: fail: false args: >- diff --git a/.github/workflows/helm.yml b/.github/workflows/helm.yml index e7c9a599dc..84937a8056 100644 --- a/.github/workflows/helm.yml +++ b/.github/workflows/helm.yml @@ -27,24 +27,25 @@ jobs: outputs: has_artifacts: ${{ steps.check-artifacts.outputs.has_artifacts }} steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 - persist-credentials: false - - - name: Install helm - uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5 - - - name: Install Oras - uses: oras-project/setup-oras@38de303aac69abb66f3e6255b7198bff35f323e3 # v2 - - - name: Login to GitHub Container Registry - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} + - parallel: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Install helm + uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5 + + - name: Install Oras + uses: oras-project/setup-oras@1d808f7d7f6995cc68b7bf507bfe5c5446e1dc9d # v2 + + - name: Login to GitHub Container Registry + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Package helm charts run: | @@ -92,33 +93,34 @@ jobs: needs: [package-helm-chart] if: needs.package-helm-chart.outputs.has_artifacts == 'true' steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 - persist-credentials: false - - - name: Install helm - uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5 - - - name: Install Oras - uses: oras-project/setup-oras@38de303aac69abb66f3e6255b7198bff35f323e3 # v2 - - - name: Install Cosign - uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1 - - - name: Downloads artifacts - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: artifacts - path: .cr-release-packages/ - - - name: Login to GitHub Container Registry - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} + - parallel: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Install helm + uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5 + + - name: Install Oras + uses: oras-project/setup-oras@1d808f7d7f6995cc68b7bf507bfe5c5446e1dc9d # v2 + + - name: Install Cosign + uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 + + - name: Downloads artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: artifacts + path: .cr-release-packages/ + + - name: Login to GitHub Container Registry + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Push charts to GHCR env: @@ -150,27 +152,28 @@ jobs: permissions: contents: read steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 - persist-credentials: false - - - name: Install Cosign - uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1 - - - name: Downloads artifacts - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: artifacts - path: .cr-release-packages/ - - - name: Login to GitHub Container Registry - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} + - parallel: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Install Cosign + uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 + + - name: Downloads artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: artifacts + path: .cr-release-packages/ + + - name: Login to GitHub Container Registry + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Verify signatures for each chart tag run: | diff --git a/.github/workflows/lint-helm-charts.yml b/.github/workflows/lint-helm-charts.yml index 283d1e09f8..a428a3abae 100644 --- a/.github/workflows/lint-helm-charts.yml +++ b/.github/workflows/lint-helm-charts.yml @@ -27,29 +27,31 @@ jobs: permissions: contents: read steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 - persist-credentials: false - - - name: Set up Helm - uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5 - - - name: Set up chart-testing - uses: helm/chart-testing-action@6ec842c01de15ebb84c8627d2744a0c2f2755c9f # v2.8.0 - - - name: Ensure documentation is updated - uses: docker://jnorwood/helm-docs:v1.14.2@sha256:7e562b49ab6b1dbc50c3da8f2dd6ffa8a5c6bba327b1c6335cc15ce29267979c - - - name: Run chart-testing (list-changed) - id: list-changed - run: | - changed=$(ct list-changed --target-branch ${{ github.event.repository.default_branch }}) - if [[ -n "$changed" ]]; then - echo "changed=true" >> "$GITHUB_OUTPUT" - echo "$changed" - fi + - parallel: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Set up Helm + uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5 + + - name: Set up chart-testing + uses: helm/chart-testing-action@6ec842c01de15ebb84c8627d2744a0c2f2755c9f # v2.8.0 + + - parallel: + - name: Ensure documentation is updated + uses: docker://jnorwood/helm-docs:v1.14.2@sha256:7e562b49ab6b1dbc50c3da8f2dd6ffa8a5c6bba327b1c6335cc15ce29267979c + + - name: Run chart-testing (list-changed) + id: list-changed + run: | + changed=$(ct list-changed --target-branch ${{ github.event.repository.default_branch }}) + if [[ -n "$changed" ]]; then + echo "changed=true" >> "$GITHUB_OUTPUT" + echo "$changed" + fi - name: Run chart-testing if: steps.list-changed.outputs.changed == 'true' diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index 57353cdd90..01446ac568 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -31,82 +31,83 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 - if: always() && steps.lint_pr_title.outputs.error_message != null - env: - ERROR_MESSAGE: ${{ steps.lint_pr_title.outputs.error_message }} - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const message = process.env.ERROR_MESSAGE; - const prNumber = context.payload.pull_request.number; + - parallel: + - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + if: always() && steps.lint_pr_title.outputs.error_message != null + env: + ERROR_MESSAGE: ${{ steps.lint_pr_title.outputs.error_message }} + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const message = process.env.ERROR_MESSAGE; + const prNumber = context.payload.pull_request.number; - const body = [ - `### PR Title Validation Failed\n`, - message, - `\n---\n`, - `PR titles must follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).`, - `*This check will re-run when you update your PR title.*`, - ].join('\n'); + const body = [ + `### PR Title Validation Failed\n`, + message, + `\n---\n`, + `PR titles must follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).`, + `*This check will re-run when you update your PR title.*`, + ].join('\n'); - const allComments = await github.paginate( - github.rest.issues.listComments, - { - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - per_page: 100, - } - ); + const allComments = await github.paginate( + github.rest.issues.listComments, + { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + per_page: 100, + } + ); - const botComment = allComments.find( - c => c.user.type === 'Bot' && c.body && c.body.includes('### PR Title Validation Failed') - ); + const botComment = allComments.find( + c => c.user.type === 'Bot' && c.body && c.body.includes('### PR Title Validation Failed') + ); - if (botComment) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: botComment.id, - body, - }); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - body, - }); - } + if (botComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body, + }); + } - - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 - if: always() && steps.lint_pr_title.outputs.error_message == null - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const prNumber = context.payload.pull_request.number; + - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + if: always() && steps.lint_pr_title.outputs.error_message == null + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const prNumber = context.payload.pull_request.number; - const allComments = await github.paginate( - github.rest.issues.listComments, - { - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - per_page: 100, - } - ); + const allComments = await github.paginate( + github.rest.issues.listComments, + { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + per_page: 100, + } + ); - const botComment = allComments.find( - c => c.user.type === 'Bot' && c.body && c.body.includes('### PR Title Validation Failed') - ); + const botComment = allComments.find( + c => c.user.type === 'Bot' && c.body && c.body.includes('### PR Title Validation Failed') + ); - if (botComment) { - await github.rest.issues.deleteComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: botComment.id, - }); - } + if (botComment) { + await github.rest.issues.deleteComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + }); + } template-check: name: Validate PR Template @@ -117,22 +118,23 @@ jobs: issues: write pull-requests: write steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false + - parallel: + - name: Skip bot PRs + id: bot-check + if: github.event.pull_request.user.type == 'Bot' + run: echo "skip=true" >> "$GITHUB_OUTPUT" + + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Set up Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version-file: 'package.json' package-manager-cache: false - - name: Skip bot PRs - id: bot-check - if: github.event.pull_request.user.type == 'Bot' - run: echo "skip=true" >> "$GITHUB_OUTPUT" - - name: Write PR body to file if: steps.bot-check.outputs.skip != 'true' env: @@ -156,130 +158,131 @@ jobs: } >> "$GITHUB_OUTPUT" exit 0 - - name: Label and comment on failure - if: steps.bot-check.outputs.skip != 'true' && steps.check.outputs.exit_code != '0' - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 - env: - ISSUES_JSON: ${{ steps.check.outputs.issues }} - PR_AUTHOR: ${{ github.event.pull_request.user.login }} - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const issues = JSON.parse(process.env.ISSUES_JSON); - const author = process.env.PR_AUTHOR; - const prNumber = context.payload.pull_request.number; - const LABEL = 'blocked:template'; + - parallel: + - name: Label and comment on failure + if: steps.bot-check.outputs.skip != 'true' && steps.check.outputs.exit_code != '0' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + ISSUES_JSON: ${{ steps.check.outputs.issues }} + PR_AUTHOR: ${{ github.event.pull_request.user.login }} + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const issues = JSON.parse(process.env.ISSUES_JSON); + const author = process.env.PR_AUTHOR; + const prNumber = context.payload.pull_request.number; + const LABEL = 'blocked:template'; - const issueList = issues.map(i => `- ${i}`).join('\n'); + const issueList = issues.map(i => `- ${i}`).join('\n'); - const commentBody = [ - `Hey @${author}, thanks for submitting this PR! However, it looks like the PR template hasn't been fully filled out.\n`, - `### Issues found:\n`, - issueList, - `\n---\n`, - `**Please update your PR description to follow the [PR template](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/develop/.github/PULL_REQUEST_TEMPLATE.md).**`, - `Incomplete or missing PR descriptions may indicate insufficient review of the changes, and PRs that do not follow the template **may be closed without review**.`, - `See our [Contributing Guide](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/develop/CONTRIBUTING.md) for more details.\n`, - `*This check will automatically re-run when you edit your PR description.*`, - ].join('\n'); + const commentBody = [ + `Hey @${author}, thanks for submitting this PR! However, it looks like the PR template hasn't been fully filled out.\n`, + `### Issues found:\n`, + issueList, + `\n---\n`, + `**Please update your PR description to follow the [PR template](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/develop/.github/PULL_REQUEST_TEMPLATE.md).**`, + `Incomplete or missing PR descriptions may indicate insufficient review of the changes, and PRs that do not follow the template **may be closed without review**.`, + `See our [Contributing Guide](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/develop/CONTRIBUTING.md) for more details.\n`, + `*This check will automatically re-run when you edit your PR description.*`, + ].join('\n'); - const allComments = await github.paginate( - github.rest.issues.listComments, - { - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - per_page: 100, - } - ); + const allComments = await github.paginate( + github.rest.issues.listComments, + { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + per_page: 100, + } + ); - const botComment = allComments.find( - c => c.user.type === 'Bot' && c.body && c.body.includes('### Issues found:') - ); + const botComment = allComments.find( + c => c.user.type === 'Bot' && c.body && c.body.includes('### Issues found:') + ); - if (botComment) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: botComment.id, - body: commentBody, - }); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - body: commentBody, - }); - } + if (botComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: commentBody, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: commentBody, + }); + } - try { - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - labels: [LABEL], - }); - } catch (e) { - try { - await github.rest.issues.createLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - name: LABEL, - color: 'B60205', - description: 'PR template not properly filled out', - }); - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - labels: [LABEL], - }); - } catch (e2) { - console.log('Could not create/add label:', e2.message); - } - } + try { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + labels: [LABEL], + }); + } catch (e) { + try { + await github.rest.issues.createLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: LABEL, + color: 'B60205', + description: 'PR template not properly filled out', + }); + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + labels: [LABEL], + }); + } catch (e2) { + console.log('Could not create/add label:', e2.message); + } + } - core.setFailed('PR template is not properly filled out.'); + core.setFailed('PR template is not properly filled out.'); - - name: Remove label on success - if: steps.bot-check.outputs.skip != 'true' && steps.check.outputs.exit_code == '0' - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const prNumber = context.payload.pull_request.number; - const LABEL = 'blocked:template'; + - name: Remove label on success + if: steps.bot-check.outputs.skip != 'true' && steps.check.outputs.exit_code == '0' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const prNumber = context.payload.pull_request.number; + const LABEL = 'blocked:template'; - try { - await github.rest.issues.removeLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - name: LABEL, - }); - } catch (e) { - console.log('Could not remove label', e.message); - } + try { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + name: LABEL, + }); + } catch (e) { + console.log('Could not remove label', e.message); + } - const allComments = await github.paginate( - github.rest.issues.listComments, - { - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - per_page: 100, - } - ); + const allComments = await github.paginate( + github.rest.issues.listComments, + { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + per_page: 100, + } + ); - const botComment = allComments.find( - c => c.user.type === 'Bot' && c.body && c.body.includes('### Issues found:') - ); + const botComment = allComments.find( + c => c.user.type === 'Bot' && c.body && c.body.includes('### Issues found:') + ); - if (botComment) { - await github.rest.issues.deleteComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: botComment.id, - }); - } + if (botComment) { + await github.rest.issues.deleteComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + }); + } diff --git a/.github/workflows/preview-books.yml b/.github/workflows/preview-books.yml new file mode 100644 index 0000000000..a01d1e4700 --- /dev/null +++ b/.github/workflows/preview-books.yml @@ -0,0 +1,115 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json +name: Seerr Books Preview + +on: + push: + tags: + - 'preview-books' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: Build (per-arch, native runners) + strategy: + matrix: + include: + - runner: ubuntu-24.04 + platform: linux/amd64 + arch: amd64 + - runner: ubuntu-24.04-arm + platform: linux/arm64 + arch: arm64 + runs-on: ${{ matrix.runner }} + steps: + - name: Checkout + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + with: + persist-credentials: false + + - name: Commit timestamp + id: ts + run: echo "TIMESTAMP=$(git log -1 --pretty=%ct)" >> "$GITHUB_OUTPUT" + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + + - name: Warm cache (no push) — ${{ matrix.platform }} + uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 + with: + context: . + file: ./Dockerfile + platforms: ${{ matrix.platform }} + push: false + build-args: | + COMMIT_TAG=${{ github.sha }} + BUILD_VERSION=preview-books + SOURCE_DATE_EPOCH=${{ steps.ts.outputs.TIMESTAMP }} + cache-from: type=gha,scope=${{ matrix.platform }} + cache-to: type=gha,mode=max,scope=${{ matrix.platform }} + provenance: false + + publish: + name: Publish multi-arch image + needs: build + runs-on: ubuntu-24.04 + permissions: + contents: read + packages: write + steps: + - name: Checkout + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + with: + persist-credentials: false + + - name: Commit timestamp + id: ts + run: echo "TIMESTAMP=$(git log -1 --pretty=%ct)" >> "$GITHUB_OUTPUT" + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 + with: + images: | + ghcr.io/${{ github.repository }} + tags: | + type=raw,value=preview-books + labels: | + org.opencontainers.image.version=preview-books + org.opencontainers.image.created=${{ steps.ts.outputs.TIMESTAMP }} + + - name: Build & Push (multi-arch, single tag) + uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + build-args: | + COMMIT_TAG=${{ github.sha }} + BUILD_VERSION=preview-books + SOURCE_DATE_EPOCH=${{ steps.ts.outputs.TIMESTAMP }} + labels: ${{ steps.meta.outputs.labels }} + tags: ${{ steps.meta.outputs.tags }} + cache-from: | + type=gha,scope=linux/amd64 + type=gha,scope=linux/arm64 + cache-to: type=gha,mode=max + provenance: false diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 16452b9873..f70c3332a5 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -32,30 +32,31 @@ jobs: arch: arm64 runs-on: ${{ matrix.runner }} steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false + - parallel: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4 + + - name: Derive preview version from tag + id: ver + shell: bash + run: | + TAG="${GITHUB_REF_NAME}" + VER="${TAG#preview-}" + VER="${VER#v}" + echo "version=${VER}" >> "$GITHUB_OUTPUT" + echo "Building preview version: ${VER}" - name: Commit timestamp id: ts run: echo "TIMESTAMP=$(git log -1 --pretty=%ct)" >> "$GITHUB_OUTPUT" - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 - - - name: Derive preview version from tag - id: ver - shell: bash - run: | - TAG="${GITHUB_REF_NAME}" - VER="${TAG#preview-}" - VER="${VER#v}" - echo "version=${VER}" >> "$GITHUB_OUTPUT" - echo "Building preview version: ${VER}" - - name: Warm cache (no push) — ${{ matrix.platform }} - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: context: . file: ./Dockerfile @@ -78,44 +79,44 @@ jobs: packages: write id-token: write steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - name: Commit timestamp - id: ts - run: echo "TIMESTAMP=$(git log -1 --pretty=%ct)" >> "$GITHUB_OUTPUT" - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 + - parallel: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4 + + - name: Derive preview version from tag + id: ver + shell: bash + run: | + TAG="${GITHUB_REF_NAME}" + VER="${TAG#preview-}" + VER="${VER#v}" + echo "version=${VER}" >> "$GITHUB_OUTPUT" + echo "Publishing preview version: ${VER}" - name: Log in to Docker Hub - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_TOKEN }} - name: Log in to GitHub Container Registry - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - - - name: Derive preview version from tag - id: ver - shell: bash - run: | - TAG="${GITHUB_REF_NAME}" - VER="${TAG#preview-}" - VER="${VER#v}" - echo "version=${VER}" >> "$GITHUB_OUTPUT" - echo "Publishing preview version: ${VER}" + - name: Commit timestamp + id: ts + run: echo "TIMESTAMP=$(git log -1 --pretty=%ct)" >> "$GITHUB_OUTPUT" - name: Extract metadata id: meta - uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6 + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6 with: images: | ${{ env.DOCKER_HUB }} @@ -127,7 +128,7 @@ jobs: org.opencontainers.image.created=${{ steps.ts.outputs.TIMESTAMP }} - name: Build & Push (multi-arch, single tag) - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: context: . file: ./Dockerfile diff --git a/.github/workflows/rebuild-issue-index.yml b/.github/workflows/rebuild-issue-index.yml index dc39c1df76..a1aa0ba34d 100644 --- a/.github/workflows/rebuild-issue-index.yml +++ b/.github/workflows/rebuild-issue-index.yml @@ -3,14 +3,11 @@ name: Rebuild Issue Index on: schedule: - - cron: "0 3 * * *" + - cron: '0 3 * * *' workflow_dispatch: permissions: {} -env: - EMBEDDING_MODEL: ${{ vars.EMBEDDING_MODEL }} - jobs: build-index: runs-on: ubuntu-24.04 @@ -21,22 +18,23 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Pnpm Setup - uses: pnpm/action-setup@903f9c1a6ebcba6cf41d87230be49611ac97822e # v6.0.3 + - parallel: + - name: Pnpm Setup + uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 - - name: Set up Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version-file: 'package.json' + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version-file: 'package.json' - name: Get pnpm store directory shell: bash run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - name: Setup pnpm cache - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ env.STORE_PATH }} key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} diff --git a/.github/workflows/release-books.yml b/.github/workflows/release-books.yml new file mode 100644 index 0000000000..2d74cdc806 --- /dev/null +++ b/.github/workflows/release-books.yml @@ -0,0 +1,161 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json +name: Seerr Books Release + +on: + push: + tags: + - 'v*' + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + changelog: + name: Generate changelog + runs-on: ubuntu-24.04 + permissions: + contents: read + outputs: + release_body: ${{ steps.git-cliff.outputs.content }} + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Generate changelog + id: git-cliff + uses: orhun/git-cliff-action@f50e11560dce63f7c33227798f90b924471a88b5 # v4.8.0 + with: + config: .github/cliff.toml + args: -vv --current + env: + OUTPUT: CHANGELOG.md + GITHUB_REPO: ${{ github.repository }} + + create-draft-release: + name: Create draft release + runs-on: ubuntu-24.04 + permissions: + contents: write + needs: changelog + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + + - name: Draft Release + run: gh release create ${GITHUB_REF_NAME} -t "Release ${GITHUB_REF_NAME}" -n "${RELEASE_BODY}" --draft + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASE_BODY: ${{ needs.changelog.outputs.release_body }} + + build: + name: Build (per-arch, native runners) + strategy: + matrix: + include: + - runner: ubuntu-24.04 + platform: linux/amd64 + arch: amd64 + - runner: ubuntu-24.04-arm + platform: linux/arm64 + arch: arm64 + runs-on: ${{ matrix.runner }} + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + + - name: Commit timestamp + id: ts + run: echo "TIMESTAMP=$(git log -1 --pretty=%ct)" >> "$GITHUB_OUTPUT" + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4 + + - name: Warm cache (no push) — ${{ matrix.platform }} + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 + with: + context: . + file: ./Dockerfile + platforms: ${{ matrix.platform }} + push: false + build-args: | + COMMIT_TAG=${{ github.sha }} + BUILD_VERSION=${{ github.ref_name }} + SOURCE_DATE_EPOCH=${{ steps.ts.outputs.TIMESTAMP }} + cache-from: type=gha,scope=${{ matrix.platform }} + cache-to: type=gha,mode=max,scope=${{ matrix.platform }} + provenance: false + + publish: + name: Publish multi-arch image + needs: build + runs-on: ubuntu-24.04 + permissions: + contents: read + packages: write + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + + - name: Commit timestamp + id: ts + run: echo "TIMESTAMP=$(git log -1 --pretty=%ct)" >> "$GITHUB_OUTPUT" + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6 + with: + images: | + ghcr.io/${{ github.repository }} + # latest=auto also adds :latest for non-prerelease semver tags + flavor: | + latest=auto + tags: | + type=semver,pattern={{raw}} + type=semver,pattern=v{{major}}.{{minor}} + type=semver,pattern=v{{major}} + labels: | + org.opencontainers.image.version=${{ github.ref_name }} + org.opencontainers.image.created=${{ steps.ts.outputs.TIMESTAMP }} + + - name: Build & Push (multi-arch) + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + build-args: | + COMMIT_TAG=${{ github.sha }} + BUILD_VERSION=${{ github.ref_name }} + SOURCE_DATE_EPOCH=${{ steps.ts.outputs.TIMESTAMP }} + labels: ${{ steps.meta.outputs.labels }} + tags: ${{ steps.meta.outputs.tags }} + cache-from: | + type=gha,scope=linux/amd64 + type=gha,scope=linux/arm64 + cache-to: type=gha,mode=max + provenance: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 574154322d..0000000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,308 +0,0 @@ ---- -# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json -name: Seerr Release - -on: - push: - tags: - - 'v*' - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -env: - DOCKER_HUB: seerr/seerr - -jobs: - changelog: - name: Generate changelog - runs-on: ubuntu-24.04 - permissions: - contents: read - outputs: - release_body: ${{ steps.git-cliff.outputs.content }} - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 - persist-credentials: false - - - name: Generate changelog - id: git-cliff - uses: orhun/git-cliff-action@f50e11560dce63f7c33227798f90b924471a88b5 # v4.8.0 - with: - config: .github/cliff.toml - args: -vv --current - env: - OUTPUT: CHANGELOG.md - GITHUB_REPO: ${{ github.repository }} - - create-draft-release: - name: Create draft release - runs-on: ubuntu-24.04 - permissions: - contents: write - needs: changelog - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - name: Draft Release - run: gh release create ${GITHUB_REF_NAME} -t "Release ${GITHUB_REF_NAME}" -n "${RELEASE_BODY}" --draft - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RELEASE_BODY: ${{ needs.changelog.outputs.release_body }} - - build: - name: Build (${{ matrix.arch }}) - strategy: - matrix: - include: - - runner: ubuntu-24.04 - platform: linux/amd64 - arch: amd64 - - runner: ubuntu-24.04-arm - platform: linux/arm64 - arch: arm64 - runs-on: ${{ matrix.runner }} - env: - VERSION: ${{ github.ref_name }} - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - name: Commit timestamp - id: ts - run: echo "TIMESTAMP=$(git log -1 --pretty=%ct)" >> "$GITHUB_OUTPUT" - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 - - - name: Warm cache [${{ matrix.platform }}] - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 - with: - context: . - file: ./Dockerfile - platforms: ${{ matrix.platform }} - push: false - build-args: | - COMMIT_TAG=${{ github.sha }} - BUILD_VERSION=${{ env.VERSION }} - SOURCE_DATE_EPOCH=${{ steps.ts.outputs.TIMESTAMP }} - cache-from: type=gha,scope=${{ matrix.platform }} - cache-to: type=gha,mode=max,scope=${{ matrix.platform }} - provenance: false - - publish: - name: Publish multi-arch manifests - needs: build - runs-on: ubuntu-24.04 - permissions: - contents: read - packages: write - outputs: - image_digest: ${{ steps.digests.outputs.IMAGE_DIGEST }} - env: - VERSION: ${{ github.ref_name }} - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - name: Commit timestamp - id: ts - run: echo "TIMESTAMP=$(git log -1 --pretty=%ct)" >> "$GITHUB_OUTPUT" - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 - - - name: Log in to Docker Hub - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_TOKEN }} - - - name: Log in to GitHub Container Registry - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata - id: meta - uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6 - with: - images: | - ${{ env.DOCKER_HUB }} - ghcr.io/${{ github.repository }} - tags: | - type=semver,pattern={{raw}} - type=semver,pattern=v{{major}}.{{minor}} - type=semver,pattern=v{{major}} - labels: | - org.opencontainers.image.created=${{ steps.ts.outputs.TIMESTAMP }} - - - name: Build & Push (multi-arch) - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 - with: - context: . - file: ./Dockerfile - platforms: linux/amd64,linux/arm64 - push: true - build-args: | - COMMIT_TAG=${{ github.sha }} - BUILD_VERSION=${{ env.VERSION }} - SOURCE_DATE_EPOCH=${{ steps.ts.outputs.TIMESTAMP }} - labels: ${{ steps.meta.outputs.labels }} - tags: ${{ steps.meta.outputs.tags }} - cache-from: | - type=gha,scope=linux/amd64 - type=gha,scope=linux/arm64 - cache-to: type=gha,mode=max - provenance: false - - - name: Resolve manifest digest - id: digests - run: | - DIGEST=$(docker buildx imagetools inspect "${{ env.DOCKER_HUB }}:${{ env.VERSION }}" --format '{{json .Manifest.Digest}}' | tr -d '"') - echo "IMAGE_DIGEST=$DIGEST" >> $GITHUB_OUTPUT - - - name: Also tag :latest (non-pre-release only) - shell: bash - if: ${{ !contains(env.VERSION, '-') }} - run: | - docker buildx imagetools create \ - -t ${{ env.DOCKER_HUB }}:latest \ - ${{ env.DOCKER_HUB }}:${{ env.VERSION }} - - docker buildx imagetools create \ - -t ghcr.io/${{ github.repository }}:latest \ - ghcr.io/${{ github.repository }}:${{ env.VERSION }} - - sign: - name: Sign images and create SBOM attestations - needs: publish - runs-on: ubuntu-24.04 - permissions: - contents: read - id-token: write - packages: write - env: - VERSION: ${{ github.ref_name }} - COSIGN_YES: 'true' - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - name: Install Cosign - uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1 - - - name: Install Trivy - uses: aquasecurity/setup-trivy@3fb12ec12f41e471780db15c232d5dd185dcb514 # v0.2.6 - - - name: Log in to Docker Hub - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_TOKEN }} - - - name: Log in to GitHub Container Registry - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Sign images - run: | - cosign sign --recursive "ghcr.io/${{ github.repository }}@${{ needs.publish.outputs.image_digest }}" - cosign sign --recursive "${{ env.DOCKER_HUB }}@${{ needs.publish.outputs.image_digest }}" - - - name: Generate SBOMs - run: | - trivy image --format cyclonedx --output seerr-ghcr-image-${{ env.VERSION }}.sbom \ - "ghcr.io/${{ github.repository }}@${{ needs.publish.outputs.image_digest }}" - - trivy image --format cyclonedx --output seerr-dockerhub-image-${{ env.VERSION }}.sbom \ - "${{ env.DOCKER_HUB }}@${{ needs.publish.outputs.image_digest }}" - - - name: Attest SBOMs - run: | - cosign attest \ - --type cyclonedx \ - --predicate seerr-ghcr-image-${{ env.VERSION }}.sbom \ - "ghcr.io/${{ github.repository }}@${{ needs.publish.outputs.image_digest }}" - - cosign attest \ - --type cyclonedx \ - --predicate seerr-dockerhub-image-${{ env.VERSION }}.sbom \ - "${{ env.DOCKER_HUB }}@${{ needs.publish.outputs.image_digest }}" - - - name: Upload SBOMs - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: sboms-${{ env.VERSION }} - path: '*.sbom' - if-no-files-found: error - retention-days: 1 - - verify: - name: Verify signatures and attestations - needs: [publish, sign] - runs-on: ubuntu-24.04 - permissions: - contents: read - env: - VERSION: ${{ github.ref_name }} - steps: - - name: Install Cosign - uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1 - - - name: Verify signatures - run: | - cosign verify "ghcr.io/${{ github.repository }}@${{ needs.publish.outputs.image_digest }}" \ - --certificate-identity "https://github.com/${{ github.workflow_ref }}" \ - --certificate-oidc-issuer "https://token.actions.githubusercontent.com" - - cosign verify "${{ env.DOCKER_HUB }}@${{ needs.publish.outputs.image_digest }}" \ - --certificate-identity "https://github.com/${{ github.workflow_ref }}" \ - --certificate-oidc-issuer "https://token.actions.githubusercontent.com" - - # - name: Verify attestations - # run: | - # cosign verify-attestation "ghcr.io/${{ github.repository }}@${{ needs.publish.outputs.image_digest }}" \ - # --type cyclonedx \ - # --certificate-identity "https://github.com/${{ github.workflow_ref }}" \ - # --certificate-oidc-issuer "https://token.actions.githubusercontent.com" > /dev/null - - # cosign verify-attestation "${{ env.DOCKER_HUB }}@${{ needs.publish.outputs.image_digest }}" \ - # --type cyclonedx \ - # --certificate-identity "https://github.com/${{ github.workflow_ref }}" \ - # --certificate-oidc-issuer "https://token.actions.githubusercontent.com" > /dev/null - - publish-release: - name: Publish release - needs: [create-draft-release, verify] - runs-on: ubuntu-24.04 - permissions: - contents: write - env: - VERSION: ${{ github.ref_name }} - steps: - - name: Publish release - run: gh release edit "${{ env.VERSION }}" --draft=false --repo "${{ github.repository }}" - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/renovate-helm-custom-hooks.yml b/.github/workflows/renovate-helm-custom-hooks.yml index b3cde18da4..534a467b14 100644 --- a/.github/workflows/renovate-helm-custom-hooks.yml +++ b/.github/workflows/renovate-helm-custom-hooks.yml @@ -24,20 +24,21 @@ jobs: pull-requests: write if: github.actor == 'renovate[bot]' steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 - persist-credentials: false - - - uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 - id: app-token - with: - app-id: 2138788 - private-key: ${{ secrets.APP_SEERR_HELM_PRIVATE_KEY }} - - - name: Set up chart-testing - uses: helm/chart-testing-action@6ec842c01de15ebb84c8627d2744a0c2f2755c9f # v2.8.0 + - parallel: + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + + - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + id: app-token + with: + client-id: ${{ secrets.APP_SEERR_HELM_CLIENT_ID }} + private-key: ${{ secrets.APP_SEERR_HELM_PRIVATE_KEY }} + + - name: Set up chart-testing + uses: helm/chart-testing-action@6ec842c01de15ebb84c8627d2744a0c2f2755c9f # v2.8.0 - name: Run chart-testing (list-changed) id: list-changed diff --git a/.github/workflows/seerr-labeller.yml b/.github/workflows/seerr-labeller.yml index 7a8330555a..e7e8f7b8d1 100644 --- a/.github/workflows/seerr-labeller.yml +++ b/.github/workflows/seerr-labeller.yml @@ -27,39 +27,40 @@ jobs: NUMBER: ${{ github.event.pull_request.number }} PR_AUTHOR: ${{ github.event.pull_request.user.login }} steps: - - name: Label added, comment and close pull request - if: github.event.action == 'labeled' && github.event.label.name == 'ai-generated' - shell: bash - env: - BODY: > - :wave: @${{ env.PR_AUTHOR }}, thank you for your contribution! + - parallel: + - name: Label added, comment and close pull request + if: github.event.action == 'labeled' && github.event.label.name == 'ai-generated' + shell: bash + env: + BODY: > + :wave: @${{ env.PR_AUTHOR }}, thank you for your contribution! - However, this pull request has been closed because it appears to contain a significant amount of AI-generated code without sufficient human review or supervision. + However, this pull request has been closed because it appears to contain a significant amount of AI-generated code without sufficient human review or supervision. - AI-generated code can often introduce subtle bugs, poor design patterns, or inconsistent styles that make long-term maintenance difficult and reduce overall code quality. For the sake of the project's future stability and readability, we require that all contributions meet our established coding standards and demonstrate clear developer oversight. + AI-generated code can often introduce subtle bugs, poor design patterns, or inconsistent styles that make long-term maintenance difficult and reduce overall code quality. For the sake of the project's future stability and readability, we require that all contributions meet our established coding standards and demonstrate clear developer oversight. - This pull request is also too large for effective human review. Please discuss with us on how to break down these changes into smaller, more focused PRs to ensure a thorough and efficient review process. - If you'd like to revise and resubmit your changes with careful review and cleanup, we'd be happy to take another look. - run: | - retry() { n=0; until "$@"; do n=$((n+1)); [ $n -ge 3 ] && break; echo "retry $n: $*" >&2; sleep 2; done; } - retry gh pr comment "$NUMBER" -R "$GH_REPO" -b "$BODY" || true - retry gh pr close "$NUMBER" -R "$GH_REPO" || true - gh pr lock "$NUMBER" -R "$GH_REPO" -r "spam" || true + Please read our [AI Assistance Notice](https://github.com/seerr-team/seerr/blob/develop/CONTRIBUTING.md#ai-assistance-notice) for what we expect from AI-assisted contributions. + If you'd like to revise and resubmit your changes with genuine review, testing, and an accurate disclosure, we'd be happy to take another look. + run: | + retry() { n=0; until "$@"; do n=$((n+1)); [ $n -ge 3 ] && break; echo "retry $n: $*" >&2; sleep 2; done; } + retry gh pr comment "$NUMBER" -R "$GH_REPO" -b "$BODY" || true + retry gh pr close "$NUMBER" -R "$GH_REPO" || true + gh pr lock "$NUMBER" -R "$GH_REPO" -r "spam" || true - - name: Label removed, reopen and unlock pull request - if: github.event.action == 'unlabeled' && github.event.label.name == 'ai-generated' - shell: bash - run: | - retry() { n=0; until "$@"; do n=$((n+1)); [ $n -ge 3 ] && break; echo "retry $n: $*" >&2; sleep 2; done; } - retry gh pr reopen "$NUMBER" -R "$GH_REPO" || true - gh pr unlock "$NUMBER" -R "$GH_REPO" || true + - name: Label removed, reopen and unlock pull request + if: github.event.action == 'unlabeled' && github.event.label.name == 'ai-generated' + shell: bash + run: | + retry() { n=0; until "$@"; do n=$((n+1)); [ $n -ge 3 ] && break; echo "retry $n: $*" >&2; sleep 2; done; } + retry gh pr reopen "$NUMBER" -R "$GH_REPO" || true + gh pr unlock "$NUMBER" -R "$GH_REPO" || true - - name: Remove AI-generated label on manual reopen - if: github.event.action == 'reopened' - shell: bash - run: | - gh pr edit "$NUMBER" -R "$GH_REPO" --remove-label "ai-generated" || true - gh pr unlock "$NUMBER" -R "$GH_REPO" || true + - name: Remove AI-generated label on manual reopen + if: github.event.action == 'reopened' + shell: bash + run: | + gh pr edit "$NUMBER" -R "$GH_REPO" --remove-label "ai-generated" || true + gh pr unlock "$NUMBER" -R "$GH_REPO" || true support: if: > @@ -78,34 +79,35 @@ jobs: NUMBER: ${{ github.event.issue.number }} ISSUE_AUTHOR: ${{ github.event.issue.user.login }} steps: - - name: Label added, comment and close issue - if: github.event.action == 'labeled' && github.event.label.name == 'support' - shell: bash - env: - BODY: > - :wave: @${{ env.ISSUE_AUTHOR }}, we use the issue tracker exclusively - for bug reports and feature requests. However, this issue appears - to be a support request. Please use our support channels - to get help with Seerr. + - parallel: + - name: Label added, comment and close issue + if: github.event.action == 'labeled' && github.event.label.name == 'support' + shell: bash + env: + BODY: > + :wave: @${{ env.ISSUE_AUTHOR }}, we use the issue tracker exclusively + for bug reports and feature requests. However, this issue appears + to be a support request. Please use our support channels + to get help with Seerr. - - [Discord](https://discord.gg/seerr) - run: | - retry() { n=0; until "$@"; do n=$((n+1)); [ $n -ge 3 ] && break; echo "retry $n: $*" >&2; sleep 2; done; } - retry gh issue comment "$NUMBER" -R "$GH_REPO" -b "$BODY" || true - retry gh issue close "$NUMBER" -R "$GH_REPO" || true - gh issue lock "$NUMBER" -R "$GH_REPO" -r "off_topic" || true + - [Discord](https://discord.gg/seerr) + run: | + retry() { n=0; until "$@"; do n=$((n+1)); [ $n -ge 3 ] && break; echo "retry $n: $*" >&2; sleep 2; done; } + retry gh issue comment "$NUMBER" -R "$GH_REPO" -b "$BODY" || true + retry gh issue close "$NUMBER" -R "$GH_REPO" || true + gh issue lock "$NUMBER" -R "$GH_REPO" -r "off_topic" || true - - name: Label removed, reopen and unlock issue - if: github.event.action == 'unlabeled' && github.event.label.name == 'support' - shell: bash - run: | - retry() { n=0; until "$@"; do n=$((n+1)); [ $n -ge 3 ] && break; echo "retry $n: $*" >&2; sleep 2; done; } - retry gh issue reopen "$NUMBER" -R "$GH_REPO" || true - gh issue unlock "$NUMBER" -R "$GH_REPO" || true + - name: Label removed, reopen and unlock issue + if: github.event.action == 'unlabeled' && github.event.label.name == 'support' + shell: bash + run: | + retry() { n=0; until "$@"; do n=$((n+1)); [ $n -ge 3 ] && break; echo "retry $n: $*" >&2; sleep 2; done; } + retry gh issue reopen "$NUMBER" -R "$GH_REPO" || true + gh issue unlock "$NUMBER" -R "$GH_REPO" || true - - name: Remove support label on manual reopen - if: github.event.action == 'reopened' - shell: bash - run: | - gh issue edit "$NUMBER" -R "$GH_REPO" --remove-label "support" || true - gh issue unlock "$NUMBER" -R "$GH_REPO" || true + - name: Remove support label on manual reopen + if: github.event.action == 'reopened' + shell: bash + run: | + gh issue edit "$NUMBER" -R "$GH_REPO" --remove-label "support" || true + gh issue unlock "$NUMBER" -R "$GH_REPO" || true diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index e78886dd12..ce1b169990 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -21,7 +21,7 @@ jobs: issues: write pull-requests: write steps: - - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0 + - uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0 with: any-of-labels: "pending author's response" exempt-issue-labels: 'confirmed' diff --git a/.github/workflows/test-docs.yml b/.github/workflows/test-docs.yml index c4dc8bdf5e..d97c53d77c 100644 --- a/.github/workflows/test-docs.yml +++ b/.github/workflows/test-docs.yml @@ -24,19 +24,20 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false - - name: Set up Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version-file: package.json - package-manager-cache: false + - parallel: + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version-file: package.json + package-manager-cache: false - - name: Pnpm Setup - uses: pnpm/action-setup@903f9c1a6ebcba6cf41d87230be49611ac97822e # v6.0.3 + - name: Pnpm Setup + uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 - name: Get pnpm store directory shell: sh @@ -44,7 +45,7 @@ jobs: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - name: Setup pnpm cache - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ env.STORE_PATH }} key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} @@ -59,4 +60,4 @@ jobs: - name: Build website run: | cd gen-docs - pnpm build + pnpm gen-api-docs && pnpm build diff --git a/.github/workflows/trivy-scan.yml b/.github/workflows/trivy-scan.yml index 0b419a377c..ba8190076f 100644 --- a/.github/workflows/trivy-scan.yml +++ b/.github/workflows/trivy-scan.yml @@ -34,13 +34,13 @@ jobs: steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false - name: Cache Trivy DB - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: .trivycache key: trivy-${{ runner.os }}-${{ hashFiles('**/Dockerfile') }} @@ -56,6 +56,6 @@ jobs: ignore-unfixed: true - name: Upload SARIF to code scanning - uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 + uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 with: sarif_file: trivy.sarif diff --git a/.gitignore b/.gitignore index b8ab627578..7a2221a325 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ lcov.info # next.js /.next/ /out/ +/next-env.d.ts # production /build @@ -79,3 +80,14 @@ config/cache # Docker compose compose.override.yaml + +# Devenv +.devenv* +devenv.local.nix +devenv.local.yaml + +# direnv +.direnv + +# pre-commit +.pre-commit-config.yaml diff --git a/.husky/commit-msg b/.husky/commit-msg index e2f1f07714..522117da14 100755 --- a/.husky/commit-msg +++ b/.husky/commit-msg @@ -1,4 +1 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - [ -n "$HUSKY_BYPASS" ] || npx commitlint --edit $1 diff --git a/.husky/pre-commit b/.husky/pre-commit index 36af219892..2312dc587f 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - npx lint-staged diff --git a/.husky/prepare-commit-msg b/.husky/prepare-commit-msg index 17e2764cdf..2d9f99924c 100755 --- a/.husky/prepare-commit-msg +++ b/.husky/prepare-commit-msg @@ -1,4 +1,2 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" exec < /dev/tty && npx cz --hook || true diff --git a/.prettierignore b/.prettierignore index d6464bf14b..91fd263c98 100644 --- a/.prettierignore +++ b/.prettierignore @@ -7,6 +7,7 @@ pnpm-lock.yaml cypress/config/settings.cypress.json .github .vscode +next-env.d.ts # assets src/assets/ diff --git a/.prettierrc.js b/.prettierrc.js index 035c9a84f4..5a46b64562 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -9,6 +9,12 @@ module.exports = { rangeEnd: 0, // default: Infinity }, }, + { + files: 'next-env.d.ts', + options: { + rangeEnd: 0, // default: Infinity + }, + }, { files: 'gen-docs/pnpm-lock.yaml', options: { diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000..995e62b28b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,1959 @@ +## [2.7.3](https://github.com/fallenbagel/jellyseerr/compare/v2.7.2...v2.7.3) (2025-08-14) + + +### Bug Fixes + +* **api:** add missing user settings' api docs ([#1820](https://github.com/fallenbagel/jellyseerr/issues/1820)) ([e52c631](https://github.com/fallenbagel/jellyseerr/commit/e52c63164fcf0fa1d35b61e4a9dedfae92764bdd)) +* **api:** make username field nullable in UserSettings API schema ([#1835](https://github.com/fallenbagel/jellyseerr/issues/1835)) ([c86ee0d](https://github.com/fallenbagel/jellyseerr/commit/c86ee0ddb1b1e24c296a2935aa964e7e2fb2b905)) +* **api:** update Plex Watchlist URL ([#1847](https://github.com/fallenbagel/jellyseerr/issues/1847)) ([17d4f13](https://github.com/fallenbagel/jellyseerr/commit/17d4f13afe389a9d0edd6eaa9a0728380a80d892)) +* **blacklist:** handle invalid keywords gracefully ([#1815](https://github.com/fallenbagel/jellyseerr/issues/1815)) ([ca16864](https://github.com/fallenbagel/jellyseerr/commit/ca1686425bcd34b05ebd3aa0b52ae939d2becc9d)) +* **MediaRequestSubscriber:** use event manager to get fresh media state for MEDIA_AVAILABLE notifications ([#1825](https://github.com/fallenbagel/jellyseerr/issues/1825)) ([3292f11](https://github.com/fallenbagel/jellyseerr/commit/3292f113081cf83aa01d522c9d19c3b5ce0e281a)) +* **media:** update delete media file logic to include is4k parameter ([#1832](https://github.com/fallenbagel/jellyseerr/issues/1832)) ([e02ee24](https://github.com/fallenbagel/jellyseerr/commit/e02ee24f70bae47731ddf445057703ce273b42ef)) +* **proxy:** initialize image proxies after the proxy is set up ([#1794](https://github.com/fallenbagel/jellyseerr/issues/1794)) ([e98f31e](https://github.com/fallenbagel/jellyseerr/commit/e98f31e66cd2c9836a24169be0b3446d0923d9f9)), closes [#1787](https://github.com/fallenbagel/jellyseerr/issues/1787) + +## [2.7.2](https://github.com/fallenbagel/jellyseerr/compare/v2.7.1...v2.7.2) (2025-07-21) + + +### Bug Fixes + +* **proxy:** modify the registration of the axios interceptors ([#1791](https://github.com/fallenbagel/jellyseerr/issues/1791)) ([75a7279](https://github.com/fallenbagel/jellyseerr/commit/75a7279ea24874548fece12bb5e7c97d78d088a9)), closes [#1787](https://github.com/fallenbagel/jellyseerr/issues/1787) + +## [2.7.1](https://github.com/fallenbagel/jellyseerr/compare/v2.7.0...v2.7.1) (2025-07-15) + + +### Bug Fixes + +* allow setting IPv6 as an IP address in hostname field ([#1782](https://github.com/fallenbagel/jellyseerr/issues/1782)) ([844b1ab](https://github.com/fallenbagel/jellyseerr/commit/844b1abad9589c57ea6f56717212d9219b2aa954)) +* **gotify:** notifications blocked when priority set to 0 ([#1763](https://github.com/fallenbagel/jellyseerr/issues/1763)) ([8c43db2](https://github.com/fallenbagel/jellyseerr/commit/8c43db2abf3b504dbb789369c9a9ac92bb820722)) +* **proxy:** apply all proxy settings to Axios ([#1741](https://github.com/fallenbagel/jellyseerr/issues/1741)) ([b83367c](https://github.com/fallenbagel/jellyseerr/commit/b83367cbf2e0470cc1ad4eed8ec6eafaafafdbad)) +* remove LunaSea ([#1759](https://github.com/fallenbagel/jellyseerr/issues/1759)) ([510108f](https://github.com/fallenbagel/jellyseerr/commit/510108f9bbec9651a5d91e11ea411e688b5043fe)), closes [#1756](https://github.com/fallenbagel/jellyseerr/issues/1756) + +# [2.7.0](https://github.com/fallenbagel/jellyseerr/compare/v2.6.0...v2.7.0) (2025-06-20) + + +### Bug Fixes + +* **blacklist:** hide items from MediaSliders when hideBlacklisted is enabled ([#1713](https://github.com/fallenbagel/jellyseerr/issues/1713)) ([d4a6cb2](https://github.com/fallenbagel/jellyseerr/commit/d4a6cb268a33d96c03f1f76c207b5597e4eae6e7)) +* correct typing issue ([#1715](https://github.com/fallenbagel/jellyseerr/issues/1715)) ([bb95c70](https://github.com/fallenbagel/jellyseerr/commit/bb95c7009faaf22103c1c8e84e3403823377ce0f)) +* **jellyfin:** use the same deviceId for admins ([#1710](https://github.com/fallenbagel/jellyseerr/issues/1710)) ([c7284f4](https://github.com/fallenbagel/jellyseerr/commit/c7284f473c43634b3a324f3b11a9a60990b3c0da)) +* **proxy:** apply http proxy settings to axios ([#1716](https://github.com/fallenbagel/jellyseerr/issues/1716)) ([7c969f4](https://github.com/fallenbagel/jellyseerr/commit/7c969f4235aa052234084c3cb951d485c6fff9cd)) +* redirect the 'Request' button to the right page ([#1711](https://github.com/fallenbagel/jellyseerr/issues/1711)) ([9cb7e14](https://github.com/fallenbagel/jellyseerr/commit/9cb7e1495ab2860cea614d10f6f7b62cf77b4def)), closes [#1588](https://github.com/fallenbagel/jellyseerr/issues/1588) +* **settings:** add a tip for youtube URL setting ([#1714](https://github.com/fallenbagel/jellyseerr/issues/1714)) ([fb8677f](https://github.com/fallenbagel/jellyseerr/commit/fb8677f29cfe2a7f0e0c465a1a742be119517886)) + + +### Features + +* add force ipv4 first setting ([#1719](https://github.com/fallenbagel/jellyseerr/issues/1719)) ([0357d17](https://github.com/fallenbagel/jellyseerr/commit/0357d172058ceda7d49a0c18c13009e0031e034d)) + +# [2.6.0](https://github.com/fallenbagel/jellyseerr/compare/v2.5.2...v2.6.0) (2025-06-09) + + +### Bug Fixes + +* add missing cache for some tmdb images ([#1656](https://github.com/fallenbagel/jellyseerr/issues/1656)) ([8949ede](https://github.com/fallenbagel/jellyseerr/commit/8949edea7edcb64546f2c8b9363120102509a2a5)) +* **entity:** use TIMESTAMPTZ in Postgres and sort issue comments oldest-first ([#1654](https://github.com/fallenbagel/jellyseerr/issues/1654)) ([8da1c92](https://github.com/fallenbagel/jellyseerr/commit/8da1c9292391a39b8c08ed9f7cd7a2bb10217588)), closes [#1569](https://github.com/fallenbagel/jellyseerr/issues/1569) [#1568](https://github.com/fallenbagel/jellyseerr/issues/1568) +* **filters:** display the right value when resetting the filter options ([#1695](https://github.com/fallenbagel/jellyseerr/issues/1695)) ([c0dd2e5](https://github.com/fallenbagel/jellyseerr/commit/c0dd2e5e27ad9927c52c54cd66bfb2b3cf7890d0)), closes [#1693](https://github.com/fallenbagel/jellyseerr/issues/1693) +* **imagecache:** fix avatar cache folder creation ([#1581](https://github.com/fallenbagel/jellyseerr/issues/1581)) ([355b76d](https://github.com/fallenbagel/jellyseerr/commit/355b76de5cc029a76708bb754c6c4fd72ce99e3d)), closes [#1520](https://github.com/fallenbagel/jellyseerr/issues/1520) +* **issuecomment:** fix issue display lists in IssueComment ([#1638](https://github.com/fallenbagel/jellyseerr/issues/1638)) ([515124b](https://github.com/fallenbagel/jellyseerr/commit/515124bab4b5e13759cb9497489dea828a6cef52)), closes [#1328](https://github.com/fallenbagel/jellyseerr/issues/1328) [#1328](https://github.com/fallenbagel/jellyseerr/issues/1328) [#1328](https://github.com/fallenbagel/jellyseerr/issues/1328) +* **jellyfin:** clean up Jellyfin sessions on Jellyseerr logout ([#1651](https://github.com/fallenbagel/jellyseerr/issues/1651)) ([b27dbd7](https://github.com/fallenbagel/jellyseerr/commit/b27dbd7a155bed9490afdd9dc25ef8a7ca9311eb)) +* **mediarequests:** properly sort season numbers in media requests ([#1688](https://github.com/fallenbagel/jellyseerr/issues/1688)) ([6b8c0bd](https://github.com/fallenbagel/jellyseerr/commit/6b8c0bd8f3ac7a5c74bc0cb2c81edd2c22a7a618)), closes [#1336](https://github.com/fallenbagel/jellyseerr/issues/1336) +* **pushover notificatons:** the sound setting will now be stored correctly ([#1630](https://github.com/fallenbagel/jellyseerr/issues/1630)) ([5fd65eb](https://github.com/fallenbagel/jellyseerr/commit/5fd65eb1ba4e8f383e8a11cb1004acc3bace26c0)), closes [#1614](https://github.com/fallenbagel/jellyseerr/issues/1614) +* **requestlist:** remove unnecessary semicolon ([#1647](https://github.com/fallenbagel/jellyseerr/issues/1647)) ([6b9aedb](https://github.com/fallenbagel/jellyseerr/commit/6b9aedb97062e6b551cc7e023f699b8b26f730f0)) +* **ui:** correct seasons badge order ([#1648](https://github.com/fallenbagel/jellyseerr/issues/1648)) ([123894b](https://github.com/fallenbagel/jellyseerr/commit/123894b475fae421b416781587432ecc4e50acc2)) +* **ui:** make person media type filter consistent on mobile ([#1669](https://github.com/fallenbagel/jellyseerr/issues/1669)) ([24e1e94](https://github.com/fallenbagel/jellyseerr/commit/24e1e94747c9a070bc84a64ace47166efe52f7f1)) +* **url validation:** correct URL validation for empty fields ([#1657](https://github.com/fallenbagel/jellyseerr/issues/1657)) ([d226dbb](https://github.com/fallenbagel/jellyseerr/commit/d226dbb9b4279c3d649fb0c72656d35ba065491c)) +* **usediscover hook:** detect end of pagination when totalSize is a multiple of pageSize ([#1649](https://github.com/fallenbagel/jellyseerr/issues/1649)) ([45f2540](https://github.com/fallenbagel/jellyseerr/commit/45f25408c62f87e4a2b73e270644fb2f1f79a290)), closes [#1623](https://github.com/fallenbagel/jellyseerr/issues/1623) +* **usersettings:** exclude current user when checking for existing email ([#1689](https://github.com/fallenbagel/jellyseerr/issues/1689)) ([ea7e68f](https://github.com/fallenbagel/jellyseerr/commit/ea7e68fc99bbc783277891b3408f07b7a70765be)) + + +### Features + +* add caching for TVDB images ([#1655](https://github.com/fallenbagel/jellyseerr/issues/1655)) ([e69649d](https://github.com/fallenbagel/jellyseerr/commit/e69649d71d19d23cafac363792a7e89c897bcad0)) +* add content certification/age-rating filter ([#1418](https://github.com/fallenbagel/jellyseerr/issues/1418)) ([149d79e](https://github.com/fallenbagel/jellyseerr/commit/149d79e5404cae48217806079b0ac0a34fbaeb35)), closes [#501](https://github.com/fallenbagel/jellyseerr/issues/501) [#501](https://github.com/fallenbagel/jellyseerr/issues/501) +* allow changing YouTube host for trailers ([#643](https://github.com/fallenbagel/jellyseerr/issues/643)) ([d01f9a0](https://github.com/fallenbagel/jellyseerr/commit/d01f9a058042657ae1ce39537cdf0029ef878c8e)) +* **blacklist:** Automatically add media with blacklisted tags to the blacklist ([#1306](https://github.com/fallenbagel/jellyseerr/issues/1306)) ([4a5ac3c](https://github.com/fallenbagel/jellyseerr/commit/4a5ac3cc42fd924e8c83d64e5b01de44bed6b8ab)) +* **blacklist:** hide blacklisted items from discover pages for admins ([#1601](https://github.com/fallenbagel/jellyseerr/issues/1601)) ([185167a](https://github.com/fallenbagel/jellyseerr/commit/185167a0a75eb710a8696b58f7bc0a3f5939cee8)) +* **discord.ts:** adds a link to the pending approval discord notification ([#436](https://github.com/fallenbagel/jellyseerr/issues/436)) ([14ee52e](https://github.com/fallenbagel/jellyseerr/commit/14ee52e93e7e6b2c1c5ec6cf05abcac3c0d8163a)) +* filter by media type on PersonDetails ([#1566](https://github.com/fallenbagel/jellyseerr/issues/1566)) ([a19dcaf](https://github.com/fallenbagel/jellyseerr/commit/a19dcaf5e5c5cc4072a7e4733bbbf149a352915b)), closes [#1513](https://github.com/fallenbagel/jellyseerr/issues/1513) [#1513](https://github.com/fallenbagel/jellyseerr/issues/1513) +* **gotify:** added priority input for gotify ([#1410](https://github.com/fallenbagel/jellyseerr/issues/1410)) ([21400ce](https://github.com/fallenbagel/jellyseerr/commit/21400cecdc1b964023087bce479bb7d141049080)) +* **issuecomment:** fix translation issue ([#1635](https://github.com/fallenbagel/jellyseerr/issues/1635)) ([8a42fe1](https://github.com/fallenbagel/jellyseerr/commit/8a42fe16b5ebca80e76d98fb05006cf41f5a953a)), closes [#1604](https://github.com/fallenbagel/jellyseerr/issues/1604) +* make chart probes configurable ([#1574](https://github.com/fallenbagel/jellyseerr/issues/1574)) ([c3b8574](https://github.com/fallenbagel/jellyseerr/commit/c3b8574515de4f5a2c13f36b3a00708d0858966e)) +* now uses markdown linebreaks instead of relying purely on newlines ([#1514](https://github.com/fallenbagel/jellyseerr/issues/1514)) ([7d36dc1](https://github.com/fallenbagel/jellyseerr/commit/7d36dc182b462c12a31833ee08e74255d7964c24)) +* **ntfy:** add native ntfy notification support ([#1599](https://github.com/fallenbagel/jellyseerr/issues/1599)) ([fc4db7f](https://github.com/fallenbagel/jellyseerr/commit/fc4db7fa002acbb31f1fd8d20da019291d8096d8)), closes [#499](https://github.com/fallenbagel/jellyseerr/issues/499) +* **requestlist:** add requests list media type filtering ([#1511](https://github.com/fallenbagel/jellyseerr/issues/1511)) ([e8f1edc](https://github.com/fallenbagel/jellyseerr/commit/e8f1edc0621387921dca73a8c9ad1d15f6b3a847)) + +## [2.5.2](https://github.com/fallenbagel/jellyseerr/compare/v2.5.1...v2.5.2) (2025-04-03) + + +### Bug Fixes + +* **auth:** Bitwarden autofill fix on local/Jellyfin login (2) ([#1487](https://github.com/fallenbagel/jellyseerr/issues/1487)) ([85bbc85](https://github.com/fallenbagel/jellyseerr/commit/85bbc857141d38bcf5244078437ed6a3318bba67)) +* **avatar:** fix avatar cache busting by using avatarVersion ([#1537](https://github.com/fallenbagel/jellyseerr/issues/1537)) ([29034b3](https://github.com/fallenbagel/jellyseerr/commit/29034b350d35ebaed52556448e46436aeb644e77)) +* correct "Remove from *arr" button ([#1544](https://github.com/fallenbagel/jellyseerr/issues/1544)) ([8dc1d81](https://github.com/fallenbagel/jellyseerr/commit/8dc1d8196c67bee0e772941445c294f0ca367961)), closes [#1476](https://github.com/fallenbagel/jellyseerr/issues/1476) [#1494](https://github.com/fallenbagel/jellyseerr/issues/1494) +* **helm:** apply annotations to pvc ([#1489](https://github.com/fallenbagel/jellyseerr/issues/1489)) ([e5ab847](https://github.com/fallenbagel/jellyseerr/commit/e5ab847547564869c3aa6443b1e22208c09a7810)) +* **jellyfin:** ensure deviceID is never empty ([#1538](https://github.com/fallenbagel/jellyseerr/issues/1538)) ([7438042](https://github.com/fallenbagel/jellyseerr/commit/7438042757cb0e81534cf9f766d84dd3ff57fd84)) +* **job:** handle media removal for 4k on the same server ([#1543](https://github.com/fallenbagel/jellyseerr/issues/1543)) ([63dc27d](https://github.com/fallenbagel/jellyseerr/commit/63dc27d400ecc80a18442fc42dd417cc03c3f9e1)) +* **job:** rename Plex Sync to Jellyfin Sync ([#1549](https://github.com/fallenbagel/jellyseerr/issues/1549)) ([2f6be95](https://github.com/fallenbagel/jellyseerr/commit/2f6be955b51e8920c8954413286577e6fea4aee2)) +* **migrations:** add missing Postgres migration and fix SQLite migration ([#1532](https://github.com/fallenbagel/jellyseerr/issues/1532)) ([0b0b76e](https://github.com/fallenbagel/jellyseerr/commit/0b0b76e58c583fc7c31d7821e7825e32065f7944)), closes [#1466](https://github.com/fallenbagel/jellyseerr/issues/1466) +* **ui:** handle import-from-plex response as array ([#1510](https://github.com/fallenbagel/jellyseerr/issues/1510)) ([4cd02ba](https://github.com/fallenbagel/jellyseerr/commit/4cd02babbace98c01bcef153a50d34cb36dd1d4b)) +* **ui:** resolve discover language dropdown overlap ([#1497](https://github.com/fallenbagel/jellyseerr/issues/1497)) ([f5b3a52](https://github.com/fallenbagel/jellyseerr/commit/f5b3a526cb9b12c19e5ff6a79240e3d85685ff9b)), closes [#1475](https://github.com/fallenbagel/jellyseerr/issues/1475) + +## [2.5.1](https://github.com/fallenbagel/jellyseerr/compare/v2.5.0...v2.5.1) (2025-03-17) + + +### Bug Fixes + +* **auth:** Bitwarden autofill fix on local/Jellyfin login ([#1459](https://github.com/fallenbagel/jellyseerr/issues/1459)) ([b085e12](https://github.com/fallenbagel/jellyseerr/commit/b085e12ff9df9f57d71ca1fe27fefa8319229a2a)) +* **blacklist:** add back the blacklist button on TitleCard for Plex ([#1463](https://github.com/fallenbagel/jellyseerr/issues/1463)) ([4d1163c](https://github.com/fallenbagel/jellyseerr/commit/4d1163c34384efa59fe9b5401c5bd42d7f0435fc)), closes [#1398](https://github.com/fallenbagel/jellyseerr/issues/1398) +* check if the file still exists in the service before deleting ([#1476](https://github.com/fallenbagel/jellyseerr/issues/1476)) ([f773e0f](https://github.com/fallenbagel/jellyseerr/commit/f773e0fb2a62f4f316ca7f8fe3d8dabdebae2ab7)) +* **job:** resolve edge case issue with season availability updates ([#1483](https://github.com/fallenbagel/jellyseerr/issues/1483)) ([77a36f9](https://github.com/fallenbagel/jellyseerr/commit/77a36f971444ee5dc0d15b2d34a8daaf4e1f28b5)) +* **mediarequest:** correct download sync for Radarr ([#1484](https://github.com/fallenbagel/jellyseerr/issues/1484)) ([c2d9d00](https://github.com/fallenbagel/jellyseerr/commit/c2d9d00b415fecbb5a8d7ca28a6ed76ea3ba3c19)), closes [#1376](https://github.com/fallenbagel/jellyseerr/issues/1376) +* **proxy:** update http proxy to accept bypass list with undici v7 ([#1456](https://github.com/fallenbagel/jellyseerr/issues/1456)) ([9891a75](https://github.com/fallenbagel/jellyseerr/commit/9891a7577cc0874f41c38ff0e6e5a6b4d8315281)), closes [#1454](https://github.com/fallenbagel/jellyseerr/issues/1454) +* **requestlist:** hide the remove from *arr button when no service exists ([#1457](https://github.com/fallenbagel/jellyseerr/issues/1457)) ([33e7a15](https://github.com/fallenbagel/jellyseerr/commit/33e7a153aa64461a715595d070fba53d52b34767)), closes [#1449](https://github.com/fallenbagel/jellyseerr/issues/1449) +* **smtp-notification-test:** missing allowSelfSigned option in test function ([#1461](https://github.com/fallenbagel/jellyseerr/issues/1461)) ([b8425d6](https://github.com/fallenbagel/jellyseerr/commit/b8425d6388003322edd7b4b2473aeb24c06e4802)) +* **ui:** correct seasons badge order ([#1485](https://github.com/fallenbagel/jellyseerr/issues/1485)) ([f884ac9](https://github.com/fallenbagel/jellyseerr/commit/f884ac9c660d1931c8b3815dcaefd109da249f2a)) +* **ui:** move watch trailer button above the 4k request button ([#1465](https://github.com/fallenbagel/jellyseerr/issues/1465)) ([a6dd4a8](https://github.com/fallenbagel/jellyseerr/commit/a6dd4a8fedb9af9810581b1cc18cfea53b3cfd39)), closes [#1462](https://github.com/fallenbagel/jellyseerr/issues/1462) +* **ui:** resolve streaming region dropdown overlap ([#1477](https://github.com/fallenbagel/jellyseerr/issues/1477)) ([767a241](https://github.com/fallenbagel/jellyseerr/commit/767a24164d6c9d101e613c53960985f4fbe2ce93)), closes [#1475](https://github.com/fallenbagel/jellyseerr/issues/1475) + + +### Reverts + +* **airdate:** reverts airdate offset & changes relative time to only display date (not time) ([#1467](https://github.com/fallenbagel/jellyseerr/issues/1467)) ([8394eb5](https://github.com/fallenbagel/jellyseerr/commit/8394eb5ad405a90e840952d5977712e1ab890530)), closes [#1390](https://github.com/fallenbagel/jellyseerr/issues/1390) + +# [2.5.0](https://github.com/fallenbagel/jellyseerr/compare/v2.4.0...v2.5.0) (2025-03-11) + + +### Bug Fixes + +* **ui:** correct media action icon size ([#1444](https://github.com/fallenbagel/jellyseerr/issues/1444)) ([771ecdf](https://github.com/fallenbagel/jellyseerr/commit/771ecdf7812004eec0f516cc424f9982936c8a2a)), closes [#1440](https://github.com/fallenbagel/jellyseerr/issues/1440) +* **users:** correct user list for Postgres ([#1443](https://github.com/fallenbagel/jellyseerr/issues/1443)) ([5b998be](https://github.com/fallenbagel/jellyseerr/commit/5b998bef82388dccaaa462ff2ff3a526dd03338c)), closes [#1333](https://github.com/fallenbagel/jellyseerr/issues/1333) + + +### Features + +* **helm:** upgrade jellyseerr to 2.4.0 ([#1438](https://github.com/fallenbagel/jellyseerr/issues/1438)) ([077e355](https://github.com/fallenbagel/jellyseerr/commit/077e355c775af92ff4dd2341543555d473c1abbb)) + + +### Reverts + +* reverts csrf-csrf back to csurf ([#1442](https://github.com/fallenbagel/jellyseerr/issues/1442)) ([21ab20b](https://github.com/fallenbagel/jellyseerr/commit/21ab20bba97102fe9eb9d4af4213a604c05e0acc)), closes [#1393](https://github.com/fallenbagel/jellyseerr/issues/1393) + +# [2.4.0](https://github.com/fallenbagel/jellyseerr/compare/v2.3.0...v2.4.0) (2025-03-10) + + +### Bug Fixes + +* add email requirement for local users ([#1389](https://github.com/fallenbagel/jellyseerr/issues/1389)) ([f0a6055](https://github.com/fallenbagel/jellyseerr/commit/f0a605577469248a2a7c2170be8310e106131c59)), closes [#900](https://github.com/fallenbagel/jellyseerr/issues/900) [#1367](https://github.com/fallenbagel/jellyseerr/issues/1367) +* **api:** make item endpoints user-independent ([#1413](https://github.com/fallenbagel/jellyseerr/issues/1413)) ([9cc6930](https://github.com/fallenbagel/jellyseerr/commit/9cc6930fed31c834201fe4e8a2a2f456b878dec6)) +* assign the keep-alive value explicitly ([#1368](https://github.com/fallenbagel/jellyseerr/issues/1368)) ([438ccfe](https://github.com/fallenbagel/jellyseerr/commit/438ccfe9c37f4848b84e60a2ce64687e0b4e4dc0)), closes [#1365](https://github.com/fallenbagel/jellyseerr/issues/1365) +* corrected spelling errors in function names ([#1366](https://github.com/fallenbagel/jellyseerr/issues/1366)) ([e035cd8](https://github.com/fallenbagel/jellyseerr/commit/e035cd84ae24502f43cf842d6d10621f28719682)) +* disable first page revalidation in useSWRInfinite ([#1386](https://github.com/fallenbagel/jellyseerr/issues/1386)) ([d563b36](https://github.com/fallenbagel/jellyseerr/commit/d563b361869d8183041cb6aea91279e17a513070)), closes [#1380](https://github.com/fallenbagel/jellyseerr/issues/1380) +* disallow admins to edit other admins in bulk edit ([#1340](https://github.com/fallenbagel/jellyseerr/issues/1340)) ([2dbd109](https://github.com/fallenbagel/jellyseerr/commit/2dbd1096d2756a7213209419d1d4da36e7267959)), closes [#1309](https://github.com/fallenbagel/jellyseerr/issues/1309) +* **emby:** throw the right error message if no library exists ([#1415](https://github.com/fallenbagel/jellyseerr/issues/1415)) ([67bd639](https://github.com/fallenbagel/jellyseerr/commit/67bd639a432d724bb34b7d6fed76c0bb66d94147)) +* fix remove from *arr in item details ([#1387](https://github.com/fallenbagel/jellyseerr/issues/1387)) ([9712f56](https://github.com/fallenbagel/jellyseerr/commit/9712f5605471a673edb3d25048dc08d1addd58db)) +* **helm:** no change, fixing OCI manifest corruption ([#1310](https://github.com/fallenbagel/jellyseerr/issues/1310)) ([418f0c2](https://github.com/fallenbagel/jellyseerr/commit/418f0c2eb844e8814aca0d280292e9fb372cc118)) +* **jobs:** run plex/jellyfin jobs only for the relevant media server ([#1331](https://github.com/fallenbagel/jellyseerr/issues/1331)) ([2b7974f](https://github.com/fallenbagel/jellyseerr/commit/2b7974fa06f196b40de270ad24e54b227143b081)), closes [#1329](https://github.com/fallenbagel/jellyseerr/issues/1329) +* make watchlist buttons consistent ([#1272](https://github.com/fallenbagel/jellyseerr/issues/1272)) ([f247642](https://github.com/fallenbagel/jellyseerr/commit/f247642b76ebefd9eeb8aed485573b5d6b133673)), closes [#1270](https://github.com/fallenbagel/jellyseerr/issues/1270) +* **mediarequest:** optimise more typeorm lifecycle triggers ([#1376](https://github.com/fallenbagel/jellyseerr/issues/1376)) ([80927b9](https://github.com/fallenbagel/jellyseerr/commit/80927b97058a219fca9fa580243cb3f966fb0b37)), closes [#513](https://github.com/fallenbagel/jellyseerr/issues/513) +* missing plex.tv url in images remotePatterns ([#1356](https://github.com/fallenbagel/jellyseerr/issues/1356)) ([b29959b](https://github.com/fallenbagel/jellyseerr/commit/b29959b0637fd8add9598d2a3d05f9a0972b65df)) +* **overriderules:** allows every user to be added to the override rules ([#1333](https://github.com/fallenbagel/jellyseerr/issues/1333)) ([af8d6b4](https://github.com/fallenbagel/jellyseerr/commit/af8d6b475c0040f7b96f04e3783ac8b4c702b3db)) +* **overriderules:** correct disabled condition for override rule creation ([#1419](https://github.com/fallenbagel/jellyseerr/issues/1419)) ([1de518d](https://github.com/fallenbagel/jellyseerr/commit/1de518d9154ea7809688c73ebefdcac66d27bdf8)) +* **overriderules:** enable override rules only when a service exists ([#1417](https://github.com/fallenbagel/jellyseerr/issues/1417)) ([4e44282](https://github.com/fallenbagel/jellyseerr/commit/4e44282387e7b511daecd961cdc9da98cb4b0139)) +* resolve a vulnerability with admin token ([#1345](https://github.com/fallenbagel/jellyseerr/issues/1345)) ([620135a](https://github.com/fallenbagel/jellyseerr/commit/620135aeac6d9fc284a3daddcafd1964474d2789)) +* **settings:** remove dns server option ([#1416](https://github.com/fallenbagel/jellyseerr/issues/1416)) ([ada467e](https://github.com/fallenbagel/jellyseerr/commit/ada467ecf40c7c27d57ae69ad515bd245d7bb639)), closes [#1266](https://github.com/fallenbagel/jellyseerr/issues/1266) +* **setup:** resolve looping library validation error message ([#1316](https://github.com/fallenbagel/jellyseerr/issues/1316)) ([6ab4632](https://github.com/fallenbagel/jellyseerr/commit/6ab463285d566c18ef0b4034fbfd0b5863a4f7a5)) +* **watchlist:** disable Jellyseerr's watchlist for Plex users ([#1398](https://github.com/fallenbagel/jellyseerr/issues/1398)) ([4eddbaa](https://github.com/fallenbagel/jellyseerr/commit/4eddbaa71b7972b6db33976102501fb8b6333206)), closes [#1344](https://github.com/fallenbagel/jellyseerr/issues/1344) + + +### Features + +* add a robots.txt file ([#1335](https://github.com/fallenbagel/jellyseerr/issues/1335)) ([24d3f52](https://github.com/fallenbagel/jellyseerr/commit/24d3f523fc07ff4b28d041b2a74cfb5ab0a788a7)), closes [#1323](https://github.com/fallenbagel/jellyseerr/issues/1323) +* add linked accounts page ([#883](https://github.com/fallenbagel/jellyseerr/issues/883)) ([64f05bc](https://github.com/fallenbagel/jellyseerr/commit/64f05bcad6956f7e8cbe3fdf5f430af1f30ddd6d)) +* **airdatebadge:** convert airDate from UTC to local timezone ([#1390](https://github.com/fallenbagel/jellyseerr/issues/1390)) ([a790b1a](https://github.com/fallenbagel/jellyseerr/commit/a790b1abccfa9c3f8272ade8cd055017905dd87f)), closes [#1373](https://github.com/fallenbagel/jellyseerr/issues/1373) +* **api:** make rottentomatoes matching more robust ([#1265](https://github.com/fallenbagel/jellyseerr/issues/1265)) ([907ba6f](https://github.com/fallenbagel/jellyseerr/commit/907ba6fdea0341e8d0f429eaf6aaa404dbc7daff)) +* **helm:** Add possibility to pass volumes and volume mounts ([#1291](https://github.com/fallenbagel/jellyseerr/issues/1291)) ([62c1a70](https://github.com/fallenbagel/jellyseerr/commit/62c1a70b373ee574ad9ff98d322085976dbc7868)) +* revamp login page and support disabling media server login ([#1286](https://github.com/fallenbagel/jellyseerr/issues/1286)) ([73d8efa](https://github.com/fallenbagel/jellyseerr/commit/73d8efaa54888b5282624e618c1461c23653f0b9)) +* **settings:** add a disclaimer for dns servers and ipv4 first settings ([#1375](https://github.com/fallenbagel/jellyseerr/issues/1375)) ([1176171](https://github.com/fallenbagel/jellyseerr/commit/117617188ed988bd8a90e9fbe8bada08d5b14513)) +* **ui:** prevent password manager interference & improve service links ([#1396](https://github.com/fallenbagel/jellyseerr/issues/1396)) ([e97a13e](https://github.com/fallenbagel/jellyseerr/commit/e97a13e1e46298be9f334c8e6c6028fb8a99c53d)), closes [#3989](https://github.com/fallenbagel/jellyseerr/issues/3989) +* update Jellyfin logo ([#1359](https://github.com/fallenbagel/jellyseerr/issues/1359)) ([c181cee](https://github.com/fallenbagel/jellyseerr/commit/c181cee328eb867f90d906757b8bddaeb74ba9f2)) +* upgrade chart to 2.0.0 ([#1268](https://github.com/fallenbagel/jellyseerr/issues/1268)) ([0ee3e69](https://github.com/fallenbagel/jellyseerr/commit/0ee3e69a6101f5a8818b6d4c5654d84f6aac322b)) + +# [2.3.0](https://github.com/fallenbagel/jellyseerr/compare/v2.2.3...v2.3.0) (2025-01-16) + + +### Bug Fixes + +* correct typos for the special episodes setting ([#1209](https://github.com/fallenbagel/jellyseerr/issues/1209)) ([ebe7d11](https://github.com/fallenbagel/jellyseerr/commit/ebe7d11a5393f3d444dd9613854d6054af1ec58b)), closes [#1193](https://github.com/fallenbagel/jellyseerr/issues/1193) [#1208](https://github.com/fallenbagel/jellyseerr/issues/1208) +* **externalapi:** clear cache after a request is made ([#1217](https://github.com/fallenbagel/jellyseerr/issues/1217)) ([f718cec](https://github.com/fallenbagel/jellyseerr/commit/f718cec23fccbfd16fdb792c2778cd543b751799)), closes [#1207](https://github.com/fallenbagel/jellyseerr/issues/1207) +* **jellyfinlogin:** add proper error message when no admin user exists ([#1216](https://github.com/fallenbagel/jellyseerr/issues/1216)) ([ac90802](https://github.com/fallenbagel/jellyseerr/commit/ac908026dbb7ca06c0fb520bbb360120d6b87feb)) +* optimize media status update to avoid lifecycle hook triggers ([#1218](https://github.com/fallenbagel/jellyseerr/issues/1218)) ([656cd91](https://github.com/fallenbagel/jellyseerr/commit/656cd91c9c90e57914b7fedb097f29e21fb18090)) +* **overriderules:** allow override rules only when the service is created ([#1259](https://github.com/fallenbagel/jellyseerr/issues/1259)) ([ce1b39f](https://github.com/fallenbagel/jellyseerr/commit/ce1b39f73b953b6fa0a00948e72d24c43476bc5f)) +* prevent TypeORM subscribers from calling itself over and over ([#1215](https://github.com/fallenbagel/jellyseerr/issues/1215)) ([d67ec57](https://github.com/fallenbagel/jellyseerr/commit/d67ec571c5950f04b85f5a268b38eb026a156320)) +* resolve plex user mismatch due to caching issues ([#1242](https://github.com/fallenbagel/jellyseerr/issues/1242)) ([131a5a2](https://github.com/fallenbagel/jellyseerr/commit/131a5a2b0b1a235599940affc183b93c36f12ade)), closes [#1227](https://github.com/fallenbagel/jellyseerr/issues/1227) +* **settingsmigrator:** prevent region migration from running multiple times ([#1255](https://github.com/fallenbagel/jellyseerr/issues/1255)) ([1c6f536](https://github.com/fallenbagel/jellyseerr/commit/1c6f5362d773c850a5e58b5013f0d65474467e9c)), closes [#1251](https://github.com/fallenbagel/jellyseerr/issues/1251) +* **setup:** fix continue button disabled on refresh in setup 3 ([#1211](https://github.com/fallenbagel/jellyseerr/issues/1211)) ([0b331ca](https://github.com/fallenbagel/jellyseerr/commit/0b331ca579c75e546dcdbf0f1896e0f0ec3a89f1)) +* **setup:** plex library setting validation ([#1233](https://github.com/fallenbagel/jellyseerr/issues/1233)) ([b8dbfaa](https://github.com/fallenbagel/jellyseerr/commit/b8dbfaaed083734b05a28a05bf100941dc673ea7)) +* specify cached image type ([#1237](https://github.com/fallenbagel/jellyseerr/issues/1237)) ([d71ee58](https://github.com/fallenbagel/jellyseerr/commit/d71ee58302fe95c9c79e27b4edf317a98faf6f5c)) +* **ui:** resolve streaming region dropdown overlap ([#1210](https://github.com/fallenbagel/jellyseerr/issues/1210)) ([2f0e493](https://github.com/fallenbagel/jellyseerr/commit/2f0e4932572497322df0d7d7f4377aeb9cc35d5b)), closes [#1206](https://github.com/fallenbagel/jellyseerr/issues/1206) +* **users:** correct request count query for PostgreSQL compatibility ([#1213](https://github.com/fallenbagel/jellyseerr/issues/1213)) ([f3ebf60](https://github.com/fallenbagel/jellyseerr/commit/f3ebf6028b23f803a1c8801b1541a444e8856421)) + + +### Features + +* Add latest tag to ghcr container image ([#1224](https://github.com/fallenbagel/jellyseerr/issues/1224)) ([b9dc9bc](https://github.com/fallenbagel/jellyseerr/commit/b9dc9bceb5805889c1ea3157c3ace880865eaf9c)) +* Add release charts workflow ([#1140](https://github.com/fallenbagel/jellyseerr/issues/1140)) ([3cc34b0](https://github.com/fallenbagel/jellyseerr/commit/3cc34b0db6b868a6133408a69a60b7eab69d9ea3)) +* **settings:** add settings for custom DNS servers and IPv4 resolution first ([#1266](https://github.com/fallenbagel/jellyseerr/issues/1266)) ([7fcc0eb](https://github.com/fallenbagel/jellyseerr/commit/7fcc0eb66d907e74b72197d6abee511150ab5e1e)) + +# [2.3.0](https://github.com/fallenbagel/jellyseerr/compare/v2.2.3...v2.3.0) (2025-01-16) + + +### Bug Fixes + +* correct typos for the special episodes setting ([#1209](https://github.com/fallenbagel/jellyseerr/issues/1209)) ([ebe7d11](https://github.com/fallenbagel/jellyseerr/commit/ebe7d11a5393f3d444dd9613854d6054af1ec58b)), closes [#1193](https://github.com/fallenbagel/jellyseerr/issues/1193) [#1208](https://github.com/fallenbagel/jellyseerr/issues/1208) +* **externalapi:** clear cache after a request is made ([#1217](https://github.com/fallenbagel/jellyseerr/issues/1217)) ([f718cec](https://github.com/fallenbagel/jellyseerr/commit/f718cec23fccbfd16fdb792c2778cd543b751799)), closes [#1207](https://github.com/fallenbagel/jellyseerr/issues/1207) +* **jellyfinlogin:** add proper error message when no admin user exists ([#1216](https://github.com/fallenbagel/jellyseerr/issues/1216)) ([ac90802](https://github.com/fallenbagel/jellyseerr/commit/ac908026dbb7ca06c0fb520bbb360120d6b87feb)) +* optimize media status update to avoid lifecycle hook triggers ([#1218](https://github.com/fallenbagel/jellyseerr/issues/1218)) ([656cd91](https://github.com/fallenbagel/jellyseerr/commit/656cd91c9c90e57914b7fedb097f29e21fb18090)) +* **overriderules:** allow override rules only when the service is created ([#1259](https://github.com/fallenbagel/jellyseerr/issues/1259)) ([ce1b39f](https://github.com/fallenbagel/jellyseerr/commit/ce1b39f73b953b6fa0a00948e72d24c43476bc5f)) +* prevent TypeORM subscribers from calling itself over and over ([#1215](https://github.com/fallenbagel/jellyseerr/issues/1215)) ([d67ec57](https://github.com/fallenbagel/jellyseerr/commit/d67ec571c5950f04b85f5a268b38eb026a156320)) +* resolve plex user mismatch due to caching issues ([#1242](https://github.com/fallenbagel/jellyseerr/issues/1242)) ([131a5a2](https://github.com/fallenbagel/jellyseerr/commit/131a5a2b0b1a235599940affc183b93c36f12ade)), closes [#1227](https://github.com/fallenbagel/jellyseerr/issues/1227) +* **settingsmigrator:** prevent region migration from running multiple times ([#1255](https://github.com/fallenbagel/jellyseerr/issues/1255)) ([1c6f536](https://github.com/fallenbagel/jellyseerr/commit/1c6f5362d773c850a5e58b5013f0d65474467e9c)), closes [#1251](https://github.com/fallenbagel/jellyseerr/issues/1251) +* **setup:** fix continue button disabled on refresh in setup 3 ([#1211](https://github.com/fallenbagel/jellyseerr/issues/1211)) ([0b331ca](https://github.com/fallenbagel/jellyseerr/commit/0b331ca579c75e546dcdbf0f1896e0f0ec3a89f1)) +* **setup:** plex library setting validation ([#1233](https://github.com/fallenbagel/jellyseerr/issues/1233)) ([b8dbfaa](https://github.com/fallenbagel/jellyseerr/commit/b8dbfaaed083734b05a28a05bf100941dc673ea7)) +* specify cached image type ([#1237](https://github.com/fallenbagel/jellyseerr/issues/1237)) ([d71ee58](https://github.com/fallenbagel/jellyseerr/commit/d71ee58302fe95c9c79e27b4edf317a98faf6f5c)) +* **ui:** resolve streaming region dropdown overlap ([#1210](https://github.com/fallenbagel/jellyseerr/issues/1210)) ([2f0e493](https://github.com/fallenbagel/jellyseerr/commit/2f0e4932572497322df0d7d7f4377aeb9cc35d5b)), closes [#1206](https://github.com/fallenbagel/jellyseerr/issues/1206) +* **users:** correct request count query for PostgreSQL compatibility ([#1213](https://github.com/fallenbagel/jellyseerr/issues/1213)) ([f3ebf60](https://github.com/fallenbagel/jellyseerr/commit/f3ebf6028b23f803a1c8801b1541a444e8856421)) + + +### Features + +* Add latest tag to ghcr container image ([#1224](https://github.com/fallenbagel/jellyseerr/issues/1224)) ([b9dc9bc](https://github.com/fallenbagel/jellyseerr/commit/b9dc9bceb5805889c1ea3157c3ace880865eaf9c)) +* Add release charts workflow ([#1140](https://github.com/fallenbagel/jellyseerr/issues/1140)) ([3cc34b0](https://github.com/fallenbagel/jellyseerr/commit/3cc34b0db6b868a6133408a69a60b7eab69d9ea3)) +* **settings:** add settings for custom DNS servers and IPv4 resolution first ([#1266](https://github.com/fallenbagel/jellyseerr/issues/1266)) ([7fcc0eb](https://github.com/fallenbagel/jellyseerr/commit/7fcc0eb66d907e74b72197d6abee511150ab5e1e)) + +## [2.2.3](https://github.com/fallenbagel/jellyseerr/compare/v2.2.2...v2.2.3) (2024-12-30) + + +### Bug Fixes + +* properly fetch sonarr/radarr specific override rules ([#1199](https://github.com/fallenbagel/jellyseerr/issues/1199)) ([814a735](https://github.com/fallenbagel/jellyseerr/commit/814a7357c0c7418091e8d3e911adc403811c9dfe)) +* **usersettings:** fix the streaming region setting toggling itself ([#1203](https://github.com/fallenbagel/jellyseerr/issues/1203)) ([7e94ad7](https://github.com/fallenbagel/jellyseerr/commit/7e94ad721026a03d3ae640ee2deb60e321cabf10)), closes [#1200](https://github.com/fallenbagel/jellyseerr/issues/1200) + +## [2.2.2](https://github.com/fallenbagel/jellyseerr/compare/v2.2.1...v2.2.2) (2024-12-30) + + +### Bug Fixes + +* **overriderules:** apply override rules to tv shows during request ([#1198](https://github.com/fallenbagel/jellyseerr/issues/1198)) ([f8a8ebd](https://github.com/fallenbagel/jellyseerr/commit/f8a8ebdf76f939ccc28ce7b39343e3a606c90b33)), closes [#1197](https://github.com/fallenbagel/jellyseerr/issues/1197) [#1195](https://github.com/fallenbagel/jellyseerr/issues/1195) + +## [2.2.1](https://github.com/fallenbagel/jellyseerr/compare/v2.2.0...v2.2.1) (2024-12-30) + + +### Bug Fixes + +* **overriderules:** apply override rules during request only for non-admin/non-auto-approve users ([#1197](https://github.com/fallenbagel/jellyseerr/issues/1197)) ([8da4870](https://github.com/fallenbagel/jellyseerr/commit/8da48709977fa0111225c3519f9128bea41867fc)), closes [#1195](https://github.com/fallenbagel/jellyseerr/issues/1195) + +# [2.2.0](https://github.com/fallenbagel/jellyseerr/compare/v2.1.0...v2.2.0) (2024-12-29) + + +### Bug Fixes + +* **avatarproxy:** add support for Emby avatars ([#1128](https://github.com/fallenbagel/jellyseerr/issues/1128)) ([17418f8](https://github.com/fallenbagel/jellyseerr/commit/17418f82af53362338aebe9602373a3c8fa027f7)), closes [#1101](https://github.com/fallenbagel/jellyseerr/issues/1101) +* **blacklist:** remove a "undefined" appearing when the blacklist modal closes ([#1142](https://github.com/fallenbagel/jellyseerr/issues/1142)) ([b01f98f](https://github.com/fallenbagel/jellyseerr/commit/b01f98f7e280a037eba303eeaa836f6623daa440)) +* **discover:** display recent requests even if there is an error with *arr ([#1141](https://github.com/fallenbagel/jellyseerr/issues/1141)) ([fa443c0](https://github.com/fallenbagel/jellyseerr/commit/fa443c05bedfca8208bfb05ab02c3b0e678e4ca0)) +* **discover:** resolve a typing issue with the WatchlistItem interface ([#1156](https://github.com/fallenbagel/jellyseerr/issues/1156)) ([de6e591](https://github.com/fallenbagel/jellyseerr/commit/de6e591baedacb33704216842dddaa2b96bfae19)) +* **emby:** change default value of Accept-Encoding header ([#1157](https://github.com/fallenbagel/jellyseerr/issues/1157)) ([7c734bc](https://github.com/fallenbagel/jellyseerr/commit/7c734bc8732a511e62edfcc371028ead6b6f1b12)) +* fix PostgreSQL migrations and TelegramMessageThreadId migration ([#1171](https://github.com/fallenbagel/jellyseerr/issues/1171)) ([0491a04](https://github.com/fallenbagel/jellyseerr/commit/0491a04ef1816e81bb495746cc529fc621e4e147)) +* handle non-existent rottentomatoes rating for movies ([#1169](https://github.com/fallenbagel/jellyseerr/issues/1169)) ([347a24a](https://github.com/fallenbagel/jellyseerr/commit/347a24a97b354725c4ccb3b5a07793b96ff60b80)) +* remove non-null requirement for some fields ([#1175](https://github.com/fallenbagel/jellyseerr/issues/1175)) ([13d15d1](https://github.com/fallenbagel/jellyseerr/commit/13d15d1dcf4a80bc0b544fecbeced706f2dbd816)), closes [#628](https://github.com/fallenbagel/jellyseerr/issues/628) +* **requestlist:** use default value of sort direction only if valid ([#1174](https://github.com/fallenbagel/jellyseerr/issues/1174)) ([59c22cc](https://github.com/fallenbagel/jellyseerr/commit/59c22ccc089c960b523ccfb69efc680b2687c353)), closes [#1147](https://github.com/fallenbagel/jellyseerr/issues/1147) +* **server/settings:** write settings to a temp file then move to avoid corruption ([#1067](https://github.com/fallenbagel/jellyseerr/issues/1067)) ([01bbece](https://github.com/fallenbagel/jellyseerr/commit/01bbeced65b82f5041462cd7a6c9016274acade4)) +* **ui:** allow thetvdb images for unmatched series ([#1105](https://github.com/fallenbagel/jellyseerr/issues/1105)) ([9b151fe](https://github.com/fallenbagel/jellyseerr/commit/9b151feb4f44d631b44c88c089f184c4c93161c5)), closes [#1075](https://github.com/fallenbagel/jellyseerr/issues/1075) +* **ui:** display Rotten Tomatoes for 0% ratings ([#1178](https://github.com/fallenbagel/jellyseerr/issues/1178)) ([5345207](https://github.com/fallenbagel/jellyseerr/commit/534520794071d8530d6325460e61dabfcb46fbf0)), closes [#1166](https://github.com/fallenbagel/jellyseerr/issues/1166) +* **ui:** resize streaming service logos ([#1106](https://github.com/fallenbagel/jellyseerr/issues/1106)) ([fe5d016](https://github.com/fallenbagel/jellyseerr/commit/fe5d016929d18c38aef7a3d48e4828188131e025)), closes [#1103](https://github.com/fallenbagel/jellyseerr/issues/1103) +* use less strict validation for external URLs ([#1104](https://github.com/fallenbagel/jellyseerr/issues/1104)) ([14f316a](https://github.com/fallenbagel/jellyseerr/commit/14f316a9a6d91c25c43e07ae66923785f90b1fdf)), closes [#1068](https://github.com/fallenbagel/jellyseerr/issues/1068) +* use links instead of buttons for external links in movie/tv details page ([#923](https://github.com/fallenbagel/jellyseerr/issues/923)) ([5776715](https://github.com/fallenbagel/jellyseerr/commit/57767156f79cb0bcb761f6fc0907d747f126e146)) +* use tmdb first as metadata provider and fallback to tvdb ([#1138](https://github.com/fallenbagel/jellyseerr/issues/1138)) ([84fd884](https://github.com/fallenbagel/jellyseerr/commit/84fd884052ea2177c92d144367c4b4ed1dde3b73)), closes [#1137](https://github.com/fallenbagel/jellyseerr/issues/1137) +* **usediscover hook:** fixing duplicate movies ([#708](https://github.com/fallenbagel/jellyseerr/issues/708)) ([39dbb7f](https://github.com/fallenbagel/jellyseerr/commit/39dbb7f7e59cf4b1b5f029089c6b1ea6a0d7e5f5)) +* **usersettings:** allow unset email and add more explicit email error message ([#1096](https://github.com/fallenbagel/jellyseerr/issues/1096)) ([39a5ccb](https://github.com/fallenbagel/jellyseerr/commit/39a5ccb7f3a6ed4e93b12e11021bb30515936ce7)) + + +### Features + +* add a setting for special episodes ([#1193](https://github.com/fallenbagel/jellyseerr/issues/1193)) ([b6e2e6c](https://github.com/fallenbagel/jellyseerr/commit/b6e2e6ce615cb94cea8d2335140fe245a0ca2d8a)) +* add postgres support + migrations ([#628](https://github.com/fallenbagel/jellyseerr/issues/628)) ([44a9221](https://github.com/fallenbagel/jellyseerr/commit/44a9221a9dca501fa57c0bcbd743aed9889059ff)), closes [#186](https://github.com/fallenbagel/jellyseerr/issues/186) +* **helm:** add base helm chart ([#1116](https://github.com/fallenbagel/jellyseerr/issues/1116)) ([27e3d46](https://github.com/fallenbagel/jellyseerr/commit/27e3d465bd7eaa3f382c961220f8af1860a15c7f)) +* **notifications:** added telegram thread id's ([#1145](https://github.com/fallenbagel/jellyseerr/issues/1145)) ([d76d794](https://github.com/fallenbagel/jellyseerr/commit/d76d79441142ccc6fe2357549f39a1fba3546ff9)) +* **notifications:** improve discord notifications ([#1102](https://github.com/fallenbagel/jellyseerr/issues/1102)) ([5c24e79](https://github.com/fallenbagel/jellyseerr/commit/5c24e79b1dddc3c8421e57e67302fa3dc064f87f)) +* override rules ([#945](https://github.com/fallenbagel/jellyseerr/issues/945)) ([9a59529](https://github.com/fallenbagel/jellyseerr/commit/9a595296dbdd00bb3477052b53412e6019667740)) +* **requestlist:** sort direction ([#1147](https://github.com/fallenbagel/jellyseerr/issues/1147)) ([66a5ab4](https://github.com/fallenbagel/jellyseerr/commit/66a5ab41ab646501f72a658782e8a89f9faf939f)) +* **usersettings:** add separate setting for streaming region ([#993](https://github.com/fallenbagel/jellyseerr/issues/993)) ([89831f7](https://github.com/fallenbagel/jellyseerr/commit/89831f70909df0a76dfa8a027702e4e5f9b57be8)), closes [#890](https://github.com/fallenbagel/jellyseerr/issues/890) + +# [2.1.0](https://github.com/fallenbagel/jellyseerr/compare/v2.0.1...v2.1.0) (2024-11-12) + + +### Bug Fixes + +* **blacklist:** request data only when modal is shown, remove useless ratelimit and lazy load blacklist ([#1084](https://github.com/fallenbagel/jellyseerr/issues/1084)) ([694913c](https://github.com/fallenbagel/jellyseerr/commit/694913c767c558147f413e2375b2512567541127)) +* cache Jellyfin/Emby avatars from API ([#1045](https://github.com/fallenbagel/jellyseerr/issues/1045)) ([0bbcfcb](https://github.com/fallenbagel/jellyseerr/commit/0bbcfcbd5e03137aba35ceb07e42f623aefa41d7)) +* **externalapi:** extract basic auth and pass it through header ([#1062](https://github.com/fallenbagel/jellyseerr/issues/1062)) ([cf59102](https://github.com/fallenbagel/jellyseerr/commit/cf59102ef91fa0e907cc6369b0fe60b503c823ca)), closes [#1027](https://github.com/fallenbagel/jellyseerr/issues/1027) +* fixes wrong avatar rendered for the modifiedBy user in request list ([#1028](https://github.com/fallenbagel/jellyseerr/issues/1028)) ([cbb1a74](https://github.com/fallenbagel/jellyseerr/commit/cbb1a74526ef5c003b7081c31146c52e7e551d60)), closes [#1017](https://github.com/fallenbagel/jellyseerr/issues/1017) +* **i18n:** update extractMessages function for better escaping of characters ([#1079](https://github.com/fallenbagel/jellyseerr/issues/1079)) ([a2d2fd3](https://github.com/fallenbagel/jellyseerr/commit/a2d2fd3c2a53fc98d6288bd049fd8e37a1914280)) +* remove language profiles dropdown for Sonarr v4 ([#1000](https://github.com/fallenbagel/jellyseerr/issues/1000)) ([d331798](https://github.com/fallenbagel/jellyseerr/commit/d331798b28a7bd32a27fc0ccbad2354be2e15b02)), closes [#207](https://github.com/fallenbagel/jellyseerr/issues/207) +* resolve error when setup on second attempt ([#1061](https://github.com/fallenbagel/jellyseerr/issues/1061)) ([64f4610](https://github.com/fallenbagel/jellyseerr/commit/64f4610b9ffcad01c24ecdd81b8b3a2f3db4c98d)) +* **setup:** add leading slash validation for baseUrl ([#1083](https://github.com/fallenbagel/jellyseerr/issues/1083)) ([2829c25](https://github.com/fallenbagel/jellyseerr/commit/2829c2548aa0cd03f92433d3bc3b9b2739e98486)) +* update i18n translations ([#1090](https://github.com/fallenbagel/jellyseerr/issues/1090)) ([f25b32a](https://github.com/fallenbagel/jellyseerr/commit/f25b32aec8ec3c2fd40ccfc6a83f18ddc99c1a15)) +* use fs/promises for settings ([#1057](https://github.com/fallenbagel/jellyseerr/issues/1057)) ([f2ed101](https://github.com/fallenbagel/jellyseerr/commit/f2ed101e522561dab8563b744d908ff036c957c5)) + + +### Features + +* add a warning if permissions are missing from config folder ([#1030](https://github.com/fallenbagel/jellyseerr/issues/1030)) ([f2b6315](https://github.com/fallenbagel/jellyseerr/commit/f2b63156d1d4aa903eb261d2c80c059c39d9091b)) +* add bypass list, bypass local addresses and username/password to proxy setting ([#1059](https://github.com/fallenbagel/jellyseerr/issues/1059)) ([ca838a0](https://github.com/fallenbagel/jellyseerr/commit/ca838a00fa4acb0ccdfbac8be4cf7fde493346f7)) +* add more logs to migrations and create a settings backup ([#1036](https://github.com/fallenbagel/jellyseerr/issues/1036)) ([326001c](https://github.com/fallenbagel/jellyseerr/commit/326001c3ecc92dc730f327130a71e797882a62b9)) +* exit Jellyseerr when migration fails ([#1026](https://github.com/fallenbagel/jellyseerr/issues/1026)) ([a2b3408](https://github.com/fallenbagel/jellyseerr/commit/a2b3408c9aa5e22e1193f535c969325254f08193)) +* proxy setting ([#1031](https://github.com/fallenbagel/jellyseerr/issues/1031)) ([4b4eeb6](https://github.com/fallenbagel/jellyseerr/commit/4b4eeb6ec707e0971fe8745910edbfb546bf25fe)) + +## [2.0.1](https://github.com/fallenbagel/jellyseerr/compare/v2.0.0...v2.0.1) (2024-10-17) + + +### Bug Fixes + +* fetch override to attach XSRF token to fix csrfProtection issue ([#1014](https://github.com/fallenbagel/jellyseerr/issues/1014)) ([4945b54](https://github.com/fallenbagel/jellyseerr/commit/4945b5429848b36fc0ee41cf0277ed79f53d8286)), closes [#1011](https://github.com/fallenbagel/jellyseerr/issues/1011) +* handle non-existent rottentomatoes rating ([#1018](https://github.com/fallenbagel/jellyseerr/issues/1018)) ([a351264](https://github.com/fallenbagel/jellyseerr/commit/a351264b878b2660ae7a6415f26d38b52015c591)) +* rewrite avatarproxy and CachedImage ([#1016](https://github.com/fallenbagel/jellyseerr/issues/1016)) ([4e48fdf](https://github.com/fallenbagel/jellyseerr/commit/4e48fdf2cb9f76ae5c25073b585718650abd3288)), closes [#1012](https://github.com/fallenbagel/jellyseerr/issues/1012) [#1013](https://github.com/fallenbagel/jellyseerr/issues/1013) +* use jellyfinMediaId4k for mediaUrl4k ([#1006](https://github.com/fallenbagel/jellyseerr/issues/1006)) ([a0f80fe](https://github.com/fallenbagel/jellyseerr/commit/a0f80fe7647ef4a9025ca93407cd21ddc640fed1)), closes [#520](https://github.com/fallenbagel/jellyseerr/issues/520) + +# [2.0.0](https://github.com/fallenbagel/jellyseerr/compare/v1.9.2...v2.0.0) (2024-10-15) + + +### Bug Fixes + +* abort availability sync job if auth token invalid/connection lost ([#845](https://github.com/fallenbagel/jellyseerr/issues/845)) ([bdee340](https://github.com/fallenbagel/jellyseerr/commit/bdee34053080c8975a88ba16a9e8f402e10fe7e1)) +* add an error message to say when an email is already taken ([#947](https://github.com/fallenbagel/jellyseerr/issues/947)) ([89e0a83](https://github.com/fallenbagel/jellyseerr/commit/89e0a831ec85a6905f539f59b7523bb1feb90bcf)) +* add missing brackets ([#888](https://github.com/fallenbagel/jellyseerr/issues/888)) ([6cea8bb](https://github.com/fallenbagel/jellyseerr/commit/6cea8bba592b8db566b4d8147630385f5c377f1b)) +* add missing content-type header ([#887](https://github.com/fallenbagel/jellyseerr/issues/887)) ([2be9c7d](https://github.com/fallenbagel/jellyseerr/commit/2be9c7dcc1f418726a19e99cfdb3933257a03c6f)) +* add missing header when creating an issue ([#879](https://github.com/fallenbagel/jellyseerr/issues/879)) ([084e1b2](https://github.com/fallenbagel/jellyseerr/commit/084e1b224e109f0f8279741b9a5ead138396d7f8)) +* add missing parameter to delete requests from ExternalAPI ([#904](https://github.com/fallenbagel/jellyseerr/issues/904)) ([36d98a2](https://github.com/fallenbagel/jellyseerr/commit/36d98a2681921a8770027b78878688f2782e8b77)), closes [#903](https://github.com/fallenbagel/jellyseerr/issues/903) +* **api:** fix nextjs error handler ([#882](https://github.com/fallenbagel/jellyseerr/issues/882)) ([0116c13](https://github.com/fallenbagel/jellyseerr/commit/0116c13e0632d1ccec43299fbb10cd71db45bc29)) +* **api:** handle non-existent ratings on IMDb ([#822](https://github.com/fallenbagel/jellyseerr/issues/822)) ([74a2d25](https://github.com/fallenbagel/jellyseerr/commit/74a2d25f153b07a0cae5b44adca5fa1fed5a3b9e)) +* **api:** save new password when reset password of local account ([#886](https://github.com/fallenbagel/jellyseerr/issues/886)) ([5cc4389](https://github.com/fallenbagel/jellyseerr/commit/5cc43898256b130c2576f34a3d4e7ce6a3940d3e)) +* **blacklist:** add blacklist to mobile menu ([#980](https://github.com/fallenbagel/jellyseerr/issues/980)) ([f390da4](https://github.com/fallenbagel/jellyseerr/commit/f390da486625a22951956ba96867de63f73bfc2b)), closes [#979](https://github.com/fallenbagel/jellyseerr/issues/979) +* change SeriesSearch to MissingEpisodeSearch for season requests ([#711](https://github.com/fallenbagel/jellyseerr/issues/711)) ([ee7e91c](https://github.com/fallenbagel/jellyseerr/commit/ee7e91c7c948b17b556a625919eb1252a721bb6e)) +* **docker:** add postinstall script ([#839](https://github.com/fallenbagel/jellyseerr/issues/839)) ([f714132](https://github.com/fallenbagel/jellyseerr/commit/f7141329094d88eb0940b1db1f21376142cb8893)) +* enhance error messages when Fetch API fails ([#893](https://github.com/fallenbagel/jellyseerr/issues/893)) ([fccfca6](https://github.com/fallenbagel/jellyseerr/commit/fccfca6ed06c8dc599e1ea4b1b3dbac48eb3a7f6)) +* handle status badge for season packs ([#927](https://github.com/fallenbagel/jellyseerr/issues/927)) ([80f6301](https://github.com/fallenbagel/jellyseerr/commit/80f63017ac5e9b1720a19c761dbef4dd517f1c2c)) +* length of undefined on users warnings ([#875](https://github.com/fallenbagel/jellyseerr/issues/875)) ([c600566](https://github.com/fallenbagel/jellyseerr/commit/c600566ac0045c2314f9013b063007b087ee4327)) +* remove DNS caching ([#837](https://github.com/fallenbagel/jellyseerr/issues/837)) ([268c7df](https://github.com/fallenbagel/jellyseerr/commit/268c7df28eea8b911d6a53297f5ce296983067ce)) +* remove email requirement for the user, and use the username if no email provided ([#900](https://github.com/fallenbagel/jellyseerr/issues/900)) ([d5f817e](https://github.com/fallenbagel/jellyseerr/commit/d5f817e734131cdacc229361d9498a095af57950)) +* remove protocol-relative URLs from next/image ([#889](https://github.com/fallenbagel/jellyseerr/issues/889)) ([c80d9a8](https://github.com/fallenbagel/jellyseerr/commit/c80d9a853a2a3451293a5382ef183c18add0c040)) +* resize episode preview image ([#842](https://github.com/fallenbagel/jellyseerr/issues/842)) ([96ba53f](https://github.com/fallenbagel/jellyseerr/commit/96ba53fecc7b9d269f0d974051ab62836b0102bc)) +* resize header image in network and studio pages ([#902](https://github.com/fallenbagel/jellyseerr/issues/902)) ([4220855](https://github.com/fallenbagel/jellyseerr/commit/422085523e5dfc132f3c3ca19eaa87117828b7be)) +* rewrite request from axios to Fetch ([#920](https://github.com/fallenbagel/jellyseerr/issues/920)) ([9aee888](https://github.com/fallenbagel/jellyseerr/commit/9aee8887d3cca6e018f4be1c8400c22e86bf8dab)) +* rewrite the rate limit utility ([#896](https://github.com/fallenbagel/jellyseerr/issues/896)) ([3fc14c9](https://github.com/fallenbagel/jellyseerr/commit/3fc14c9e2262463afec666e7f54e38d0d36cff68)) +* **session:** set the correct TTL for the cookie store ([#992](https://github.com/fallenbagel/jellyseerr/issues/992)) ([96e1d40](https://github.com/fallenbagel/jellyseerr/commit/96e1d40304749ce00d2ff7359efc39a1d9724358)), closes [#991](https://github.com/fallenbagel/jellyseerr/issues/991) +* set correct user type when importing from emby ([#949](https://github.com/fallenbagel/jellyseerr/issues/949)) ([e57d265](https://github.com/fallenbagel/jellyseerr/commit/e57d2654d1c634a91649722d3a2bf4d73c4a02ca)), closes [#948](https://github.com/fallenbagel/jellyseerr/issues/948) +* **setup:** page display when homepage is loading ([#940](https://github.com/fallenbagel/jellyseerr/issues/940)) ([7423bbb](https://github.com/fallenbagel/jellyseerr/commit/7423bbbffc5bee2e52e3348254f035dc8527d973)) +* **tmdb:** fallback movie/show overview to English when none is available in requested locale ([#928](https://github.com/fallenbagel/jellyseerr/issues/928)) ([12f908d](https://github.com/fallenbagel/jellyseerr/commit/12f908de7f5fbd717a5f151858b6edee3be13ed9)), closes [#925](https://github.com/fallenbagel/jellyseerr/issues/925) +* update the filter removing existing users from Jellyfin import modal ([#924](https://github.com/fallenbagel/jellyseerr/issues/924)) ([61dcd8e](https://github.com/fallenbagel/jellyseerr/commit/61dcd8e487d7886773ccb12501623c17838476e5)) + + +### Code Refactoring + +* **jellyfin:** abstract jellyfin hostname, updated ui to reflect it, better validation ([#773](https://github.com/fallenbagel/jellyseerr/issues/773)) ([38ad875](https://github.com/fallenbagel/jellyseerr/commit/38ad875dd7848b4e92ac3ccdd16dbf785f6a5c4d)) + + +### Features + +* add environment variable for API key ([#831](https://github.com/fallenbagel/jellyseerr/issues/831)) ([45ef150](https://github.com/fallenbagel/jellyseerr/commit/45ef150e36944d456cc9440574b5ac75f2e4bbc1)) +* adds status filter for tv shows ([#796](https://github.com/fallenbagel/jellyseerr/issues/796)) ([cfd1bc2](https://github.com/fallenbagel/jellyseerr/commit/cfd1bc253557d6e19725743b8aa9a2fa33bbe760)), closes [#605](https://github.com/fallenbagel/jellyseerr/issues/605) +* allow request managers to delete data from sonarr/radarr ([#644](https://github.com/fallenbagel/jellyseerr/issues/644)) ([a5d22ba](https://github.com/fallenbagel/jellyseerr/commit/a5d22ba5b83dd0e812b16f06476d993b5d59cb2a)) +* blacklist items from Discover page ([#632](https://github.com/fallenbagel/jellyseerr/issues/632)) ([818aa60](https://github.com/fallenbagel/jellyseerr/commit/818aa60aac185da07bfb71b08e0448939b63a736)), closes [#490](https://github.com/fallenbagel/jellyseerr/issues/490) +* Jellyfin/Emby server type setup ([#685](https://github.com/fallenbagel/jellyseerr/issues/685)) ([15cb949](https://github.com/fallenbagel/jellyseerr/commit/15cb949f1f2e617853f90ae7bb8ae5d6622f610e)) +* **jellyfinapi:** switch to API tokens instead of auth tokens ([#868](https://github.com/fallenbagel/jellyseerr/issues/868)) ([bd4da6d](https://github.com/fallenbagel/jellyseerr/commit/bd4da6d5fc8cb55c2bc3d9a8336787cbd30814d0)) +* Option on item's page to add/remove from watchlist ([#781](https://github.com/fallenbagel/jellyseerr/issues/781)) ([2348f23](https://github.com/fallenbagel/jellyseerr/commit/2348f23f433195d64dee3e6eeede296fca5fdbc9)), closes [#730](https://github.com/fallenbagel/jellyseerr/issues/730) +* refresh monitored downloads before getting queue items ([#994](https://github.com/fallenbagel/jellyseerr/issues/994)) ([92ba262](https://github.com/fallenbagel/jellyseerr/commit/92ba26207dcb1ddd696e0f01931d2609c521ae45)), closes [#866](https://github.com/fallenbagel/jellyseerr/issues/866) +* show quality profile on request ([#847](https://github.com/fallenbagel/jellyseerr/issues/847)) ([6445332](https://github.com/fallenbagel/jellyseerr/commit/64453320d36595e75dcb710dfd43997bf2d2acd5)) +* **translation:** added full Hebrew translation ([#871](https://github.com/fallenbagel/jellyseerr/issues/871)) ([c96ca67](https://github.com/fallenbagel/jellyseerr/commit/c96ca6742e0a6d5685319c52f995fe06e439a450)) +* update Plex logo ([#884](https://github.com/fallenbagel/jellyseerr/issues/884)) ([3a363ae](https://github.com/fallenbagel/jellyseerr/commit/3a363ae1ffa7f384be6f7d25f8558b1e55a73fb3)) + + +### Reverts + +* fix(api): fix nextjs error handler ([#882](https://github.com/fallenbagel/jellyseerr/issues/882)) ([#892](https://github.com/fallenbagel/jellyseerr/issues/892)) ([62dbde4](https://github.com/fallenbagel/jellyseerr/commit/62dbde448c7f7d530de8534bb8538452d0f91276)) + + +### BREAKING CHANGES + +* This commit deprecates the JELLYFIN_TYPE variable to identify Emby media server and +instead rely on the mediaServerType that is set in the `settings.json`. Existing environment +variable users can log out and log back in to set the mediaServerType to `3` (Emby). + +* feat(api): add severType to the api +* This adds a serverType to the `/auth/jellyfin` which requires a serverType to be +set (`jellyfin`/`emby`) + +* refactor: use enums for serverType and rename selectedservice to serverType + +* refactor(auth): jellyfin/emby authentication to set MediaServerType + +* fix: issue page formatMessage for 4k media + +* refactor: cleaner way of handling serverType change using MediaServerType instead of strings + +instead of using strings now it will use MediaServerType enums for serverType + +* revert: removed conditional render of the auto-request permission + +reverts the conditional render toshow the auto-request permission if the mediaServerType was set to +Plex as this should be handled in a different PR and Cypress tests should be modified +accordingly(currently cypress test would fail if this conditional check is there) + +* feat: add server type step to setup + +* feat: migrate existing emby setups to use emby mediaServerType + +* fix: scan jobs not running when media server type is emby + +* fix: emby media server type migration + +* refactor: change emby logo to full logo + +* style: decrease emby logo size in setup screen + +* refactor: use title case for servertype i18n message + +* refactor(i18n): fix a typo + +* refactor: use enums instead of numbers + +* fix: remove old references to JELLYFIN_TYPE environment variable + +* fix: go back to the last step when refresh the setup page + +* fix: move "scanning in background" tip next to the scanning section + +* fix: redirect the setup page when Jellyseerr is already setup +* **jellyfin:** Jellyfin settings now does not include a hostname. Instead it abstracted it to ip, +port, useSsl, and urlBase. However, migration of old settings to new settings should work +automatically. + +* refactor: remove console logs and use getHostname and ApiErrorCodes + +* fix: store req.body jellyfin settings temporarily and store only if valid + +This should fix the issue where settings are saved even if the url +was invalid. Now the settings will only be saved if the url is +valid. Sort of like a test connection. + +* refactor: clean up commented out code + +* refactor(i18n): extract translation keys + +* fix(auth): auth failing with jellyfin login is disabled + +* fix(settings): jellyfin migrations replacing the rest of the settings + +* fix(settings): jellyfin hostname should be carried out if hostname exists + +* fix(settings): merging the wrong settings source + +* refactor(settings): use migrator for dynamic settings migrations + +* refactor(settingsmigrator): settings migration handler and the migrations + +* test(cypress): fix cypress tests failing + +cypress settings were lacking some of the jobs so when the startJobs() is called when the app +starts, it was failing to schedule the jobs where their cron timings were not specified in the +cypress settings. Therefore, this commit adds those jobs back. In addition, other setting options +were added to keep cypress settings consistent with a normal user. + +* chore(prettierignore): ignore cypress/config/settings.cypress.json as it does not need prettier + +* chore(prettier): ran formatter on cypress config to fix format check error + +format check locally passes on this file. However, it fails during the github actions format check. +Therefore, json language features formatter was run instead of prettier to see if that fixes the +issue. + +* test(cypress): add only missing jobs to the cypress settings + +* ci: attempt at trying to get formatter to pass on cypress config json file + +* refactor: revert the changes brought to try and fix formatter + +added back the rest of the cypress settings and removed cypress settings from .prettierignore + +* refactor(settings): better erorr logging when jellyfin connection test fails in settings page + +## [1.9.2](https://github.com/fallenbagel/jellyseerr/compare/v1.9.1...v1.9.2) (2024-06-13) + + +### Bug Fixes + +* **auth:** improve login resilience with headerless fallback authentication ([#814](https://github.com/fallenbagel/jellyseerr/issues/814)) ([a9741fa](https://github.com/fallenbagel/jellyseerr/commit/a9741fa36d06710aa00d28db3dd2c29f2b0973d3)) +* **auth:** validation of ipv6/ipv4 ([#812](https://github.com/fallenbagel/jellyseerr/issues/812)) ([9aeb360](https://github.com/fallenbagel/jellyseerr/commit/9aeb3604e6498c388df1d30dd0b613ba84160fc0)), closes [#795](https://github.com/fallenbagel/jellyseerr/issues/795) +* bypass cache-able lookups when resolving localhost ([#813](https://github.com/fallenbagel/jellyseerr/issues/813)) ([b5a0699](https://github.com/fallenbagel/jellyseerr/commit/b5a069901a9545772deaa9c491f2075261da0189)) + +## [1.9.1](https://github.com/fallenbagel/jellyseerr/compare/v1.9.0...v1.9.1) (2024-06-12) + + +### Bug Fixes + +* **api:** add DNS caching ([#810](https://github.com/fallenbagel/jellyseerr/issues/810)) ([46ee8a4](https://github.com/fallenbagel/jellyseerr/commit/46ee8a4ca13b026bd929b4027eb001cc74064bb8)), closes [#387](https://github.com/fallenbagel/jellyseerr/issues/387) [#657](https://github.com/fallenbagel/jellyseerr/issues/657) [#728](https://github.com/fallenbagel/jellyseerr/issues/728) +* empty email in user settings ([#807](https://github.com/fallenbagel/jellyseerr/issues/807)) ([20863d4](https://github.com/fallenbagel/jellyseerr/commit/20863d4a8dabe78fb5c52995b5bcb2da557a804e)), closes [#803](https://github.com/fallenbagel/jellyseerr/issues/803) +* **jellyfinscanner:** assign only 4k available badge for a 4k request instead of both badges ([#805](https://github.com/fallenbagel/jellyseerr/issues/805)) ([d31a2c3](https://github.com/fallenbagel/jellyseerr/commit/d31a2c37e639c1126b446277fa5d666d8102fef5)) +* remove the settings button of media when useless ([#809](https://github.com/fallenbagel/jellyseerr/issues/809)) ([f52939e](https://github.com/fallenbagel/jellyseerr/commit/f52939e4cdcbee94fc35165f613f6b3e21599e3c)) + + +### Reverts + +* Revert "ci: update format check command to ignore .prettierignore files (#787)" (#788) ([4757f1c](https://github.com/fallenbagel/jellyseerr/commit/4757f1c3e599304410a737c11f97db92a2bfcefd)), closes [#787](https://github.com/fallenbagel/jellyseerr/issues/787) [#788](https://github.com/fallenbagel/jellyseerr/issues/788) + +# [1.9.0](https://github.com/fallenbagel/jellyseerr/compare/v1.8.1...v1.9.0) (2024-05-29) + + +### Bug Fixes + +* **api:** save user email on the first try ([#760](https://github.com/fallenbagel/jellyseerr/issues/760)) ([0bbcfdc](https://github.com/fallenbagel/jellyseerr/commit/0bbcfdc4f9ff9735f45232a2412ac8444f525de9)), closes [#227](https://github.com/fallenbagel/jellyseerr/issues/227) [#748](https://github.com/fallenbagel/jellyseerr/issues/748) +* **api:** small errors on overseerr-api.yaml ([#721](https://github.com/fallenbagel/jellyseerr/issues/721)) ([0eea109](https://github.com/fallenbagel/jellyseerr/commit/0eea1090dfdba4333646280c84b09b0197fefa74)) +* **auth:** case-sensitive logins not updating authtokens ([#778](https://github.com/fallenbagel/jellyseerr/issues/778)) ([2bd125d](https://github.com/fallenbagel/jellyseerr/commit/2bd125d9a55d15a398ceb5f2996105a5e861b6e0)) +* **jellyfinapi:** use external api class for jellyfin api requests ([#762](https://github.com/fallenbagel/jellyseerr/issues/762)) ([650c339](https://github.com/fallenbagel/jellyseerr/commit/650c339d74d4fe85ef7f76184901e86f4eeada85)), closes [#728](https://github.com/fallenbagel/jellyseerr/issues/728) [#387](https://github.com/fallenbagel/jellyseerr/issues/387) +* **logging:** handle media server connection refused error/toast ([#748](https://github.com/fallenbagel/jellyseerr/issues/748)) ([f486fb5](https://github.com/fallenbagel/jellyseerr/commit/f486fb5e75f9ea21456952b6a52cb841e30f3556)) +* use UTF8 encoding for webhook JSON ([#714](https://github.com/fallenbagel/jellyseerr/issues/714)) ([c0a0b9c](https://github.com/fallenbagel/jellyseerr/commit/c0a0b9c8a8b0c2eeaf3fa9159f10742baa9f6c1f)) + + +### Features + +* add Latin American Spanish translation ([#725](https://github.com/fallenbagel/jellyseerr/issues/725)) ([783fda9](https://github.com/fallenbagel/jellyseerr/commit/783fda9621aef8ffd46e5f036136de82ed502ccc)), closes [#677](https://github.com/fallenbagel/jellyseerr/issues/677) +* add merge conflict labeler workflow ([#719](https://github.com/fallenbagel/jellyseerr/issues/719)) ([d9d07c7](https://github.com/fallenbagel/jellyseerr/commit/d9d07c705a24d5c49905066aac45a3c6a2e36a53)) +* **auth:** send real information on login ([#470](https://github.com/fallenbagel/jellyseerr/issues/470)) ([d765055](https://github.com/fallenbagel/jellyseerr/commit/d765055da83ee94546399f6348aee14d8427d462)) +* **settings:** stores jellyfin/emby server name in the settings ([#763](https://github.com/fallenbagel/jellyseerr/issues/763)) ([7a5e8d6](https://github.com/fallenbagel/jellyseerr/commit/7a5e8d69bf620c8e7bf5f284840b1a5fe757ae5f)) + +## [1.8.1](https://github.com/fallenbagel/jellyseerr/compare/v1.8.0...v1.8.1) (2024-04-17) + + +### Reverts + +* Revert "fix: disable seasonfolder option in sonarr for jellyfin/Emby users" (#718) ([cd0fa3e](https://github.com/fallenbagel/jellyseerr/commit/cd0fa3e2232dcb522673143f113fc382fb2ff0a3)), closes [#718](https://github.com/fallenbagel/jellyseerr/issues/718) + +# [1.8.0](https://github.com/fallenbagel/jellyseerr/compare/v1.7.0...v1.8.0) (2024-04-15) + + +### Bug Fixes + +* correct width issue in datepicker of filterSliderOver ([f564cdd](https://github.com/fallenbagel/jellyseerr/commit/f564cddff4525ccebffbf304672d49c57aefe635)), closes [#415](https://github.com/fallenbagel/jellyseerr/issues/415) +* disable seasonfolder option in sonarr for jellyfin/Emby users ([8ec8f2a](https://github.com/fallenbagel/jellyseerr/commit/8ec8f2ac5730aad3b12dcd8ed95bb553b46b399c)), closes [#126](https://github.com/fallenbagel/jellyseerr/issues/126) [#575](https://github.com/fallenbagel/jellyseerr/issues/575) +* **embyauth:** remove the accidentally added mediaServerType change code from another PR ([#684](https://github.com/fallenbagel/jellyseerr/issues/684)) ([c2e8771](https://github.com/fallenbagel/jellyseerr/commit/c2e87714b4c4aa11bf68dcd82b76979f82990f3c)) +* ensure watchlist updates are immediately reflected ([b85d7f3](https://github.com/fallenbagel/jellyseerr/commit/b85d7f37b931735ca2ad955dccb6599bf445fc73)) +* fix german translation for "components.Discover.FilterSlideover.tmdbuservotecount" ([e032c02](https://github.com/fallenbagel/jellyseerr/commit/e032c02f5f84dc4b6b470eecb18ba2c376c55f37)) +* fix the translations for watchlist permissions and userSettings page ([8c82a61](https://github.com/fallenbagel/jellyseerr/commit/8c82a61450a7525c0e2f1b64e6939da47a7c715d)) +* **i18n:** fixed jellyfin jobs ([7eed236](https://github.com/fallenbagel/jellyseerr/commit/7eed23637ddfb10bdcb19698e7ae171f07299502)) +* **jellyfin.ts:** process virtual seasons if they have non virtual episodes ([#639](https://github.com/fallenbagel/jellyseerr/issues/639)) ([db84f65](https://github.com/fallenbagel/jellyseerr/commit/db84f6529ab285be26c96daaab065dfabf347417)) +* **jellyfinapi:** refactors jellyfin library sync to support automatic grouping and collections ([#700](https://github.com/fallenbagel/jellyseerr/issues/700)) ([3856061](https://github.com/fallenbagel/jellyseerr/commit/3856061fe1ee4d3457996586b4979ad9dd60765a)), closes [#450](https://github.com/fallenbagel/jellyseerr/issues/450) [#524](https://github.com/fallenbagel/jellyseerr/issues/524) [#256](https://github.com/fallenbagel/jellyseerr/issues/256) [#489](https://github.com/fallenbagel/jellyseerr/issues/489) [#450](https://github.com/fallenbagel/jellyseerr/issues/450) [#524](https://github.com/fallenbagel/jellyseerr/issues/524) [#515](https://github.com/fallenbagel/jellyseerr/issues/515) [#474](https://github.com/fallenbagel/jellyseerr/issues/474) [#473](https://github.com/fallenbagel/jellyseerr/issues/473) +* **jellyfinlogin:** use externalHostname if set for forgetpassword link ([405f6bb](https://github.com/fallenbagel/jellyseerr/commit/405f6bbb7ffc390327c99dcef2cbbf9b3bc75f01)), closes [#199](https://github.com/fallenbagel/jellyseerr/issues/199) [#424](https://github.com/fallenbagel/jellyseerr/issues/424) [#212](https://github.com/fallenbagel/jellyseerr/issues/212) +* **jellyfinscanner:** conditionally assign the jellyfinMediaId and jellyfinMediaId4k ([#686](https://github.com/fallenbagel/jellyseerr/issues/686)) ([530be42](https://github.com/fallenbagel/jellyseerr/commit/530be4272cce1b0d74d7f4156b8d794cda6ea03f)), closes [#681](https://github.com/fallenbagel/jellyseerr/issues/681) +* **langcode:** fixes the ukranian language code ([dc67aaa](https://github.com/fallenbagel/jellyseerr/commit/dc67aaaf53eae86ba20c6c2798c92ec40962d85f)), closes [#504](https://github.com/fallenbagel/jellyseerr/issues/504) +* nullable type for jellyfinMediaId(4k) ([#702](https://github.com/fallenbagel/jellyseerr/issues/702)) ([0900a95](https://github.com/fallenbagel/jellyseerr/commit/0900a95532501b6f4d9698de7530a771512924fc)), closes [#668](https://github.com/fallenbagel/jellyseerr/issues/668) +* request watchlist items sequentially to prevent bypassing quota ([#3667](https://github.com/fallenbagel/jellyseerr/issues/3667)) ([b40ba07](https://github.com/fallenbagel/jellyseerr/commit/b40ba07a4de5857b8392f667038eeb0b22aa5d9a)) +* resolved issue with region selector and all regions value ([#3652](https://github.com/fallenbagel/jellyseerr/issues/3652)) ([28a2c50](https://github.com/fallenbagel/jellyseerr/commit/28a2c50495d0ce531da7f8c442bd488a54b1e84c)) +* typos on readme ([#655](https://github.com/fallenbagel/jellyseerr/issues/655)) ([eee9a02](https://github.com/fallenbagel/jellyseerr/commit/eee9a025d246c72bcd3aca753d9e49c1f8f064ea)) +* **watchlist:** added missing prop for watchlist item removal button in watchlist page ([a0ec992](https://github.com/fallenbagel/jellyseerr/commit/a0ec992028093257e9fa043622e236014f02dea3)) +* **watchlist:** discover local watchlist item display and profile local watchlist slider visibility ([3cb9494](https://github.com/fallenbagel/jellyseerr/commit/3cb9494e6210151716587d8c4b22e0a21692cf88)) + + +### Features + +* add ko language ([#3619](https://github.com/fallenbagel/jellyseerr/issues/3619)) ([9250735](https://github.com/fallenbagel/jellyseerr/commit/92507359b48db08b0066047d6505660b8c8b0b12)) +* add Peacock to Network Slider ([#3545](https://github.com/fallenbagel/jellyseerr/issues/3545)) ([0c39057](https://github.com/fallenbagel/jellyseerr/commit/0c39057ca58743697e9dcc3b678440ac3688c65a)) +* add tooltips to tautulli avatars ([#3601](https://github.com/fallenbagel/jellyseerr/issues/3601)) ([c484810](https://github.com/fallenbagel/jellyseerr/commit/c484810f965f8d04643c25c6d283dd83f4bd4a23)) +* added Letterboxd links for the external link blocks for movies ([981f5e6](https://github.com/fallenbagel/jellyseerr/commit/981f5e679c4c707e119741240a58de8bb07f9d6c)) +* check if first jellyfin user is admin ([#635](https://github.com/fallenbagel/jellyseerr/issues/635)) ([010df62](https://github.com/fallenbagel/jellyseerr/commit/010df62776191fe4c195e590df338f8d8523f55b)), closes [#610](https://github.com/fallenbagel/jellyseerr/issues/610) +* jellyseerr makeover ([#715](https://github.com/fallenbagel/jellyseerr/issues/715)) ([0c27132](https://github.com/fallenbagel/jellyseerr/commit/0c2713213c56de342f76300d12ce01fd543d2ce3)) +* **job:** media availability support for jellyfin/emby ([#522](https://github.com/fallenbagel/jellyseerr/issues/522)) ([3eb1bb3](https://github.com/fallenbagel/jellyseerr/commit/3eb1bb3d8ff22391acb2e629bbec7b6e4b65ca95)), closes [#406](https://github.com/fallenbagel/jellyseerr/issues/406) [#193](https://github.com/fallenbagel/jellyseerr/issues/193) [#516](https://github.com/fallenbagel/jellyseerr/issues/516) [#362](https://github.com/fallenbagel/jellyseerr/issues/362) [#84](https://github.com/fallenbagel/jellyseerr/issues/84) +* **notif:** add Pushover sound options ([#2403](https://github.com/fallenbagel/jellyseerr/issues/2403)) ([3ea5076](https://github.com/fallenbagel/jellyseerr/commit/3ea5076053359b518b1b4d537e7b61580d9275a3)) +* select default seriesType for anime ([#3627](https://github.com/fallenbagel/jellyseerr/issues/3627)) ([f628635](https://github.com/fallenbagel/jellyseerr/commit/f6286359cfd2ed93fc692aa2efda37310e02c11c)), closes [#3626](https://github.com/fallenbagel/jellyseerr/issues/3626) +* standard series type selector ([#3628](https://github.com/fallenbagel/jellyseerr/issues/3628)) ([7bdd25e](https://github.com/fallenbagel/jellyseerr/commit/7bdd25e5a45843a3e530d3fa2b0887664b53eec8)) +* translations update from Hosted Weblate ([#3258](https://github.com/fallenbagel/jellyseerr/issues/3258)) ([e62a078](https://github.com/fallenbagel/jellyseerr/commit/e62a078298ced7dec627fb3ff9fc8f99a39d5e1b)) +* update SameSite policy of session cookie to Lax ([#3650](https://github.com/fallenbagel/jellyseerr/issues/3650)) ([c84ca43](https://github.com/fallenbagel/jellyseerr/commit/c84ca4307465af4278f3dad5cf9c2b8cbae3fada)) + + +### Reverts + +* **jellyfinapi:** reverts [#450](https://github.com/fallenbagel/jellyseerr/issues/450) as it broke library sync support for local accounts using LDAP ([b5acc09](https://github.com/fallenbagel/jellyseerr/commit/b5acc09ba98e2dd9b61e6b78721e4dd9f42a996c)), closes [#489](https://github.com/fallenbagel/jellyseerr/issues/489) + +# [1.7.0](https://github.com/fallenbagel/jellyseerr/compare/v1.6.0...v1.7.0) (2023-09-14) + + +### Bug Fixes + +* adjust the plex watchlist sync schedule to have fuzziness ([#3502](https://github.com/fallenbagel/jellyseerr/issues/3502)) ([2c3f533](https://github.com/fallenbagel/jellyseerr/commit/2c3f5330764492e1323afd2d1f25e28ad78a2f2f)) +* handle issue causing incorrect media to change to unknown ([#3516](https://github.com/fallenbagel/jellyseerr/issues/3516)) ([83b008c](https://github.com/fallenbagel/jellyseerr/commit/83b008c8391459bd02dc74bcdb0d8caf27207bdf)) +* improved handling of edge case that could cause availability sync to fail ([#3497](https://github.com/fallenbagel/jellyseerr/issues/3497)) ([d0836ce](https://github.com/fallenbagel/jellyseerr/commit/d0836ce0efd55fccf2546087a0c4f94f7cb2e82a)) +* Include all defaults in payload ([#3538](https://github.com/fallenbagel/jellyseerr/issues/3538)) ([cb63bf2](https://github.com/fallenbagel/jellyseerr/commit/cb63bf217b9e8810a5210b4bf475b2a96583cc84)) +* multiple notifications for available media ([048fa96](https://github.com/fallenbagel/jellyseerr/commit/048fa967f2e5b23831ac9917c703934c50ef75f0)) +* repeat notifications for available 4k media ([30361f2](https://github.com/fallenbagel/jellyseerr/commit/30361f2ab751d9a882a9120e0f3df28dc42cc2cd)) +* resolved issue with create slider causing incorrect form submission ([#3514](https://github.com/fallenbagel/jellyseerr/issues/3514)) ([a761b7d](https://github.com/fallenbagel/jellyseerr/commit/a761b7dd35a5bd61bb4eb0275b75d1e0977e6a2d)) +* resolved user access check issue ([#3551](https://github.com/fallenbagel/jellyseerr/issues/3551)) ([2816c66](https://github.com/fallenbagel/jellyseerr/commit/2816c66300bf870d493c0665b0e984d60f707dfd)) +* **server/api/jellyfin.ts:** use /Library/VirtualFolders Jellyfin API call to fetch Jellyfin libs ([8685f57](https://github.com/fallenbagel/jellyseerr/commit/8685f5796a99d9700146bae9892319db10508d68)), closes [#256](https://github.com/fallenbagel/jellyseerr/issues/256) +* **statusbadge:** handle missing season/episode number ([#3526](https://github.com/fallenbagel/jellyseerr/issues/3526)) ([01de972](https://github.com/fallenbagel/jellyseerr/commit/01de972a8fe2ea3c18d5b2f426d01b5b14d142d4)) +* **tautulli:** only test connection if hostname is defined ([#3573](https://github.com/fallenbagel/jellyseerr/issues/3573)) ([f7b4dfc](https://github.com/fallenbagel/jellyseerr/commit/f7b4dfcac472d08c54779a14fc1ad3c90927df26)) +* **ui:** corrected issues icon color ([#3498](https://github.com/fallenbagel/jellyseerr/issues/3498)) ([c1a47bd](https://github.com/fallenbagel/jellyseerr/commit/c1a47bd9de332cb4925974690f5a33448b5cc2e6)) + + +### Features + +* **rating:** added IMDB Radarr proxy ([#3496](https://github.com/fallenbagel/jellyseerr/issues/3496)) ([b4191f9](https://github.com/fallenbagel/jellyseerr/commit/b4191f9c65b7ff08764e61d18e7a75bc8d4b3325)) + +# [1.6.0](https://github.com/fallenbagel/jellyseerr/compare/v1.5.0...v1.6.0) (2023-08-04) + + +### Bug Fixes + +* availability sync file detection ([#3371](https://github.com/fallenbagel/jellyseerr/issues/3371)) ([7522aa3](https://github.com/fallenbagel/jellyseerr/commit/7522aa31743b169c903ebdf9d4d698645d27514c)) +* corrected initial fallback data load on details page ([#3395](https://github.com/fallenbagel/jellyseerr/issues/3395)) ([4bd8764](https://github.com/fallenbagel/jellyseerr/commit/4bd87647d0551c20e13589a62690a6f3e5ad8ff7)) +* correctly load series fallback modal with sonarr v4 ([#3451](https://github.com/fallenbagel/jellyseerr/issues/3451)) ([e051b1d](https://github.com/fallenbagel/jellyseerr/commit/e051b1dfea9c9320cc9dd420c475ae74cff0d901)) +* **deps:** update all non-major dependencies ([#3223](https://github.com/fallenbagel/jellyseerr/issues/3223)) ([f5191ad](https://github.com/fallenbagel/jellyseerr/commit/f5191aded680357522a65bbdcc40d162b8fbf594)) +* error deleting users with over 1000 requests ([#3376](https://github.com/fallenbagel/jellyseerr/issues/3376)) ([ac77b03](https://github.com/fallenbagel/jellyseerr/commit/ac77b037d5fb0c54f5edf4b29d04adb57aef388f)) +* external url regex is now consistent with internal url ([33ec443](https://github.com/fallenbagel/jellyseerr/commit/33ec4436fb82e1eb1bc97dd650088c27785e9d94)) +* externalLinkBlock ([46cd4d0](https://github.com/fallenbagel/jellyseerr/commit/46cd4d01d9a3cf17d79350c5e678202820272299)) +* fix regex for internal url to use a more effecient one ([e848386](https://github.com/fallenbagel/jellyseerr/commit/e848386d10f05f157e7a6dde8847ecab50c169ac)) +* fixes RT ratings for tv shows ([#3492](https://github.com/fallenbagel/jellyseerr/issues/3492)) ([04fbd00](https://github.com/fallenbagel/jellyseerr/commit/04fbd00d4ac29045592588ef8b664d1916991e37)), closes [#3491](https://github.com/fallenbagel/jellyseerr/issues/3491) +* **genreselector:** fix searching in Genre filter ([#3468](https://github.com/fallenbagel/jellyseerr/issues/3468)) ([d7fa35e](https://github.com/fallenbagel/jellyseerr/commit/d7fa35e066cf371797aaa46ca464aa531ba8fb35)) +* handle search results with collections ([#3393](https://github.com/fallenbagel/jellyseerr/issues/3393)) ([70b1540](https://github.com/fallenbagel/jellyseerr/commit/70b1540ae23e83e01013856a9e06ad39e600922d)) +* lock body scroll when using webkit ([#3399](https://github.com/fallenbagel/jellyseerr/issues/3399)) ([c27f960](https://github.com/fallenbagel/jellyseerr/commit/c27f96096ac8cc6c387f9d1dde5b263576ac2132)) +* **logs:** jellyfin auth error now has the severity warn consistent with local login ([cc041b5](https://github.com/fallenbagel/jellyseerr/commit/cc041b5e0aa2b67573edba5919772b77a5111162)), closes [#224](https://github.com/fallenbagel/jellyseerr/issues/224) +* make a (shallow) copy of radarr/sonarr tags into a request before adding user tags ([#3485](https://github.com/fallenbagel/jellyseerr/issues/3485)) ([48f7666](https://github.com/fallenbagel/jellyseerr/commit/48f76662d5c08156f1da3f47e216c5f02668f64b)) +* **ui:** corrected default badge hover opacity ([#3369](https://github.com/fallenbagel/jellyseerr/issues/3369)) ([a4d07f5](https://github.com/fallenbagel/jellyseerr/commit/a4d07f5afab613317d96c9c6e9b47157a5a28986)) +* **ui:** corrected mobile menu spacing in collection details ([#3432](https://github.com/fallenbagel/jellyseerr/issues/3432)) ([77a33cb](https://github.com/fallenbagel/jellyseerr/commit/77a33cb74d744bb747b791785799b632af8c7862)) +* **ui:** Make play symbol white ([1fe4bb8](https://github.com/fallenbagel/jellyseerr/commit/1fe4bb8a0415a72791ced75a2fba1027287398d5)) +* **ui:** Resize Emby icon and add margins ([ad69d67](https://github.com/fallenbagel/jellyseerr/commit/ad69d6715e976630092bfbbb1843886523551014)) +* **watchlist:** add validation for creation request ([03316c6](https://github.com/fallenbagel/jellyseerr/commit/03316c642d1ecf89753789af08caf6e3aac80113)) +* **watchlist:** fix github code scanning ([c08897b](https://github.com/fallenbagel/jellyseerr/commit/c08897bdc1cff65862c62347572bbbd01b6c36ac)) + + +### Features + +* **add watchlist:** adding midding functionality from overserr ([5f1c10d](https://github.com/fallenbagel/jellyseerr/commit/5f1c10d50aaa430bcda96218ef2cc12a0eb926f3)) +* adds streaming services custom slider ([#3361](https://github.com/fallenbagel/jellyseerr/issues/3361)) ([2520d8f](https://github.com/fallenbagel/jellyseerr/commit/2520d8f739abfde608f3ef66a9fbe6b7b5c6647a)) +* auto tagging requested media with username ([#3338](https://github.com/fallenbagel/jellyseerr/issues/3338)) ([24f268b](https://github.com/fallenbagel/jellyseerr/commit/24f268b6cb67d9a8d8675cd6e09dd83a7f499add)) +* **discover:** support filtering by tmdb user vote count on discover page ([#3407](https://github.com/fallenbagel/jellyseerr/issues/3407)) ([aa84977](https://github.com/fallenbagel/jellyseerr/commit/aa849776809dfe891e67ff4db6861ef44df1a774)) +* **settings:** add internal url to jellyfin settings form ([0a30cd3](https://github.com/fallenbagel/jellyseerr/commit/0a30cd356d217a39546c016cc8bfa6ff6ad75e3e)), closes [#194](https://github.com/fallenbagel/jellyseerr/issues/194) +* **src/components/externallinkblock/index.tsx:** support Emby icon ([672061c](https://github.com/fallenbagel/jellyseerr/commit/672061cd646c97c9954790c8e50eac88ea2666e9)) +* **tooltip:** email tooltip now appears when hovered over info icon ([cd7930e](https://github.com/fallenbagel/jellyseerr/commit/cd7930eef98451a781e5c9dc5ec223600a379f42)) +* translations update ([47287c3](https://github.com/fallenbagel/jellyseerr/commit/47287c368885d14bd1a56e3e8318ce22dd0f6ddf)), closes [#381](https://github.com/fallenbagel/jellyseerr/issues/381) +* **watchlist:** add translation for en ([b7e3d28](https://github.com/fallenbagel/jellyseerr/commit/b7e3d285ed35b623062eceb0d99035cafbf075a6)) + +# [1.5.0](https://github.com/fallenbagel/jellyseerr/compare/v1.4.1...v1.5.0) (2023-04-20) + + +### Bug Fixes + +* add better checks on 4k detection of series ([bc9017f](https://github.com/fallenbagel/jellyseerr/commit/bc9017f54d84ec24c4d74d38e1b4e24219425d41)) +* added a refresh interval if download status is in progress ([#3275](https://github.com/fallenbagel/jellyseerr/issues/3275)) ([1e2c6f4](https://github.com/fallenbagel/jellyseerr/commit/1e2c6f46ab66c836f321b5d8e34f1e8124c0b542)) +* **build:** increase threshold for amount of data to be fetched when SSR'ing ([#3320](https://github.com/fallenbagel/jellyseerr/issues/3320)) ([d7b83d2](https://github.com/fallenbagel/jellyseerr/commit/d7b83d22cee3d20db564cc0564d42802b02327e3)) +* disable availability sync temporarily ([2e5cf22](https://github.com/fallenbagel/jellyseerr/commit/2e5cf226265686012329248e7f729fec324c3deb)) +* hide remove button when default service is not configured ([7d4455b](https://github.com/fallenbagel/jellyseerr/commit/7d4455ba6bfd12e2730f7085cbb87df246f01d22)) +* **jellyfin scan:** temporary workaround fix for jellyfin scan when display specials within season ([38fb66d](https://github.com/fallenbagel/jellyseerr/commit/38fb66d31e41232c01898d0d362af8338eb7b960)), closes [#215](https://github.com/fallenbagel/jellyseerr/issues/215) [#176](https://github.com/fallenbagel/jellyseerr/issues/176) [#246](https://github.com/fallenbagel/jellyseerr/issues/246) +* lint issues ([bcd2bb7](https://github.com/fallenbagel/jellyseerr/commit/bcd2bb7c96810f5a6932f42468a628d2db1bc771)) +* logger was set to info for the wrong logs ([#3354](https://github.com/fallenbagel/jellyseerr/issues/3354)) ([c36a4ba](https://github.com/fallenbagel/jellyseerr/commit/c36a4ba2b8df05873f5dfd0946a9bc3dc4ecfd1d)) +* remove unnecessary parenthesis from api key generation ([#3336](https://github.com/fallenbagel/jellyseerr/issues/3336)) ([6bd3f01](https://github.com/fallenbagel/jellyseerr/commit/6bd3f015d65507efca60279007bd2b86ee860643)) +* **snapcraft:** use the correct config folder for image cache ([#3302](https://github.com/fallenbagel/jellyseerr/issues/3302)) ([c93467b](https://github.com/fallenbagel/jellyseerr/commit/c93467b3acf2c256324297e7e8f21e9944005dd4)) +* **ui:** hide mini status badge if non-4K media status is unknown ([#3346](https://github.com/fallenbagel/jellyseerr/issues/3346)) ([50f06da](https://github.com/fallenbagel/jellyseerr/commit/50f06dabbffc693f0843584a64d1d96e77982820)) +* **ui:** hide search bar behind slideover when opened ([#3348](https://github.com/fallenbagel/jellyseerr/issues/3348)) ([b3882de](https://github.com/fallenbagel/jellyseerr/commit/b3882de8930a70adb2f93a27be6370bfa1826587)) +* **ui:** prevent title cards from flickering when quickly hovering across them ([#3349](https://github.com/fallenbagel/jellyseerr/issues/3349)) ([eb5502a](https://github.com/fallenbagel/jellyseerr/commit/eb5502a16f86e37a933f6beca0678c2d228e77d5)) +* **watchlist:** correctly load more than 20 watchlist items ([#3351](https://github.com/fallenbagel/jellyseerr/issues/3351)) ([af880a6](https://github.com/fallenbagel/jellyseerr/commit/af880a6c839794b34bddcd7e0fe56353aa48ba36)) + + +### Features + +* add a button in ManageSlideOver to remove the movie and the file from Radarr/Sonarr ([2e74584](https://github.com/fallenbagel/jellyseerr/commit/2e7458457e995dd3ec6dd96035fe997646cdd446)) +* availability sync rework ([#3219](https://github.com/fallenbagel/jellyseerr/issues/3219)) ([ae38183](https://github.com/fallenbagel/jellyseerr/commit/ae3818304b2f75222d1bd223ece94f829a3b42d0)), closes [#377](https://github.com/fallenbagel/jellyseerr/issues/377) +* full title of download item on hover with tooltip ([#3296](https://github.com/fallenbagel/jellyseerr/issues/3296)) ([33e7691](https://github.com/fallenbagel/jellyseerr/commit/33e7691b94d7d369a0a1410e434850bc51e5572e)) + + +### Performance Improvements + +* **imageproxy:** do not set cookies to image proxy so CDNs can cache images ([#3332](https://github.com/fallenbagel/jellyseerr/issues/3332)) ([966639d](https://github.com/fallenbagel/jellyseerr/commit/966639df430d32f6bfebdb16314dc4590d21caf8)) + +## [1.4.1](https://github.com/fallenbagel/jellyseerr/compare/v1.4.0...v1.4.1) (2023-01-31) + + +### Bug Fixes + +* pass in library type when scanning recently added items ([#3287](https://github.com/fallenbagel/jellyseerr/issues/3287)) ([8942eb8](https://github.com/fallenbagel/jellyseerr/commit/8942eb8b7c4fa1d16aa2e72e8ba7120a653c9aa2)) +* **ui:** air date will use UTC for timezone ([#3297](https://github.com/fallenbagel/jellyseerr/issues/3297)) ([3e43586](https://github.com/fallenbagel/jellyseerr/commit/3e43586acc0804c3fff524509caa890a104e132b)) +* **ui:** correct range slider styling in chrome ([#3299](https://github.com/fallenbagel/jellyseerr/issues/3299)) ([d954328](https://github.com/fallenbagel/jellyseerr/commit/d9543289111d72245564d25d300a71b0ea3954ba)) +* **ui:** show 5 icons when possible on mobile menu ([#3298](https://github.com/fallenbagel/jellyseerr/issues/3298)) ([7040da1](https://github.com/fallenbagel/jellyseerr/commit/7040da1334f6d18e19a494c73caa17f7df552dfe)) +* **ui:** style range thumbs correctly for firefox ([#3294](https://github.com/fallenbagel/jellyseerr/issues/3294)) ([9d10e6a](https://github.com/fallenbagel/jellyseerr/commit/9d10e6a88c0996671f1d9d20792e1930dbc82329)) + +# [1.4.0](https://github.com/fallenbagel/jellyseerr/compare/v1.3.0...v1.4.0) (2023-01-29) + + +### Bug Fixes + +* add bg-opacity to in-progress status badges ([#3190](https://github.com/fallenbagel/jellyseerr/issues/3190)) ([68223f4](https://github.com/fallenbagel/jellyseerr/commit/68223f4b1e98b01825516dcba39cbb2d3df31a70)) +* added download status and title to request card/item error components ([#3186](https://github.com/fallenbagel/jellyseerr/issues/3186)) ([3309f77](https://github.com/fallenbagel/jellyseerr/commit/3309f77aa4be1d70b27693531c119a8e26822518)) +* arrow icons were misplaced on mobile in slider edit ([#3260](https://github.com/fallenbagel/jellyseerr/issues/3260)) ([d328485](https://github.com/fallenbagel/jellyseerr/commit/d328485161b9cae6a70ef0713b4878207bc6015e)) +* **build:** update usage of publish snap action ([#3272](https://github.com/fallenbagel/jellyseerr/issues/3272)) ([51b05cd](https://github.com/fallenbagel/jellyseerr/commit/51b05cd8fbb5d332807d8c00b2ffb7b10c3d0179)) +* changed overflow scroll to only if necessary ([#3184](https://github.com/fallenbagel/jellyseerr/issues/3184)) ([27feeea](https://github.com/fallenbagel/jellyseerr/commit/27feeea69121336557deda1f32b65a5daa146f82)) +* convert genre/studio to string in create slider ([#3201](https://github.com/fallenbagel/jellyseerr/issues/3201)) ([93afead](https://github.com/fallenbagel/jellyseerr/commit/93afead92e497f2e5bce67a34fffdaa08d20c7f2)) +* correct checkbox position (again) for slider edits ([#3227](https://github.com/fallenbagel/jellyseerr/issues/3227)) ([3ba6df1](https://github.com/fallenbagel/jellyseerr/commit/3ba6df1a41c084c4a6a90354338047623abef521)) +* correct grid sizing for webkit on streaming services ([#3248](https://github.com/fallenbagel/jellyseerr/issues/3248)) ([6fd11cf](https://github.com/fallenbagel/jellyseerr/commit/6fd11cf4254e1a19310592bec78a6de52bc073a8)) +* correct issue detail bottom padding on mobile displays ([#3268](https://github.com/fallenbagel/jellyseerr/issues/3268)) ([3db010b](https://github.com/fallenbagel/jellyseerr/commit/3db010b9eaec62aa08d973a61caf1801471bbf3e)) +* correct link to correct keyword results for series ([#3208](https://github.com/fallenbagel/jellyseerr/issues/3208)) ([4e9be7a](https://github.com/fallenbagel/jellyseerr/commit/4e9be7a3f7304ee7be5ee6fd34b1ea8f6c0cf399)) +* correct spacing between sliders ([#3225](https://github.com/fallenbagel/jellyseerr/issues/3225)) ([62e2de7](https://github.com/fallenbagel/jellyseerr/commit/62e2de70bf37b72d5f63370b662d4103a642775b)) +* correctly check mobile menu permissions ([#3271](https://github.com/fallenbagel/jellyseerr/issues/3271)) ([f4a22dc](https://github.com/fallenbagel/jellyseerr/commit/f4a22dc437404558f301ccfc195cf0a300dd1ff2)) +* correctly restore selected streaming service filters ([#3249](https://github.com/fallenbagel/jellyseerr/issues/3249)) ([154f3e7](https://github.com/fallenbagel/jellyseerr/commit/154f3e72efbf0b663358b3029156f54516f01a2f)) +* create shared class to add bottom spacing ([#3269](https://github.com/fallenbagel/jellyseerr/issues/3269)) ([5d1c6f7](https://github.com/fallenbagel/jellyseerr/commit/5d1c6f706555613d97ed9e61d8b665543c2f239b)) +* **deps:** pin dependency @headlessui/react to 1.7.7 ([#3194](https://github.com/fallenbagel/jellyseerr/issues/3194)) [skip ci] ([c4b16ab](https://github.com/fallenbagel/jellyseerr/commit/c4b16abc62647c74215155942a4230a31a238677)) +* **deps:** update dependency @heroicons/react to v2 ([#2970](https://github.com/fallenbagel/jellyseerr/issues/2970)) ([dd48d59](https://github.com/fallenbagel/jellyseerr/commit/dd48d59b20e2d1800ea30912116f4a4f1bb7928f)) +* **deps:** update dependency axios to v1 ([#3202](https://github.com/fallenbagel/jellyseerr/issues/3202)) ([421029e](https://github.com/fallenbagel/jellyseerr/commit/421029ebab66c9a6622ba47e56d7f6473524cce4)) +* **deps:** update dependency swr to v2 ([#3212](https://github.com/fallenbagel/jellyseerr/issues/3212)) ([7b6db50](https://github.com/fallenbagel/jellyseerr/commit/7b6db50ae55b1fc60d19a5cff62dd46bb989fa51)) +* **experimental:** use new RT API (sorta) ([#3179](https://github.com/fallenbagel/jellyseerr/issues/3179)) ([357cab8](https://github.com/fallenbagel/jellyseerr/commit/357cab87ac7752b8e119b51c938b343c661d83c2)) +* improve small screen layout for discover editing ([#3221](https://github.com/fallenbagel/jellyseerr/issues/3221)) ([d23b213](https://github.com/fallenbagel/jellyseerr/commit/d23b2132de05f072f7f9daad83d81421d747cf99)) +* include new package calendar css in build ([#3235](https://github.com/fallenbagel/jellyseerr/issues/3235)) ([c2a1a20](https://github.com/fallenbagel/jellyseerr/commit/c2a1a20a3bb20039a1936c7fe0ecb9e8311a0aea)) +* issues with issues ([#3267](https://github.com/fallenbagel/jellyseerr/issues/3267)) ([fd21971](https://github.com/fallenbagel/jellyseerr/commit/fd219717c01c558814d7a80de6304272b5a7944e)) +* multiple genre filtering now works ([#3282](https://github.com/fallenbagel/jellyseerr/issues/3282)) ([5076938](https://github.com/fallenbagel/jellyseerr/commit/507693881b939819413f0959df5ef6b7a357eb5c)) +* prevent double encode if we are on /search endpoint ([#3238](https://github.com/fallenbagel/jellyseerr/issues/3238)) ([a343f8a](https://github.com/fallenbagel/jellyseerr/commit/a343f8ad915491a9c81512c7e541a1dac8906025)) +* **request:** approve request when retrying request ([#3234](https://github.com/fallenbagel/jellyseerr/issues/3234)) ([b515701](https://github.com/fallenbagel/jellyseerr/commit/b5157010c46cd9083993d5ee0172007b83d631da)) +* **request:** mark request as approved if media is already available when retrying failed request ([#3244](https://github.com/fallenbagel/jellyseerr/issues/3244)) ([cb65074](https://github.com/fallenbagel/jellyseerr/commit/cb650745f6a33e69391a633e6d272831f314e098)) +* restore border to ghost button and fix discover slider visibility toggle position ([#3226](https://github.com/fallenbagel/jellyseerr/issues/3226)) ([2eebb7f](https://github.com/fallenbagel/jellyseerr/commit/2eebb7fd3941b34fe9472aaf9d28265df8cce311)) +* restore status badges on titles on actors page when hide available media enabled ([#3206](https://github.com/fallenbagel/jellyseerr/issues/3206)) ([9d3446d](https://github.com/fallenbagel/jellyseerr/commit/9d3446d370499c3251159393e5c791b01225e05c)) +* screen would zoom on mobile if date picker input was selected ([#3241](https://github.com/fallenbagel/jellyseerr/issues/3241)) ([3aefddd](https://github.com/fallenbagel/jellyseerr/commit/3aefddd48834d86150d5f5cceb2d08af3a78847b)) +* series displayed an empty season with series list/request modal ([#3147](https://github.com/fallenbagel/jellyseerr/issues/3147)) ([2179637](https://github.com/fallenbagel/jellyseerr/commit/2179637d437999290eaa4152f6f37c71fc3d8ba3)) +* tooltip shows properly if not in progress ([#3185](https://github.com/fallenbagel/jellyseerr/issues/3185)) ([6face8c](https://github.com/fallenbagel/jellyseerr/commit/6face8cc4564b978fb98af32659b326d8c5cede8)) +* **ui:** series first air date sorting ([#3283](https://github.com/fallenbagel/jellyseerr/issues/3283)) ([374c78c](https://github.com/fallenbagel/jellyseerr/commit/374c78c989cc86bb144a954a91d5d183c4b591c0)) +* update StatusBadgeMini to shrink on title cards (and remove ring) ([#3210](https://github.com/fallenbagel/jellyseerr/issues/3210)) ([042a1a9](https://github.com/fallenbagel/jellyseerr/commit/042a1a950fdd4d4a61edf4bc19657f9b7a526da8)) + + +### Features + +* add discover customization ([#3182](https://github.com/fallenbagel/jellyseerr/issues/3182)) ([cd35748](https://github.com/fallenbagel/jellyseerr/commit/cd3574851a12517cbfadc109e6412a7a9e44c114)) +* add keywords to movie/series detail pages ([#3204](https://github.com/fallenbagel/jellyseerr/issues/3204)) ([e084649](https://github.com/fallenbagel/jellyseerr/commit/e084649878a58c296786141d12dd69a69a27ee85)) +* add streaming services filter ([#3247](https://github.com/fallenbagel/jellyseerr/issues/3247)) ([1154156](https://github.com/fallenbagel/jellyseerr/commit/1154156459403494e8daf0c89a3ba356aeea1d97)) +* discover inline customization ([#3220](https://github.com/fallenbagel/jellyseerr/issues/3220)) ([8bd10b5](https://github.com/fallenbagel/jellyseerr/commit/8bd10b5bf3d1b8069872b616c7c8596caeb4937e)) +* discover overhaul (filters!) ([#3232](https://github.com/fallenbagel/jellyseerr/issues/3232)) ([dd00e48](https://github.com/fallenbagel/jellyseerr/commit/dd00e48f59054b44bef6b32a2c169e59f6175051)) +* discover slider edit arrow buttons for reordering ([#3259](https://github.com/fallenbagel/jellyseerr/issues/3259)) ([da00d45](https://github.com/fallenbagel/jellyseerr/commit/da00d454e17e8b00d04f6e26f6dd5153ed6ced81)) +* **lang:** translations update from Hosted Weblate ([#3030](https://github.com/fallenbagel/jellyseerr/issues/3030)) ([0d8b390](https://github.com/fallenbagel/jellyseerr/commit/0d8b390b678731e76bd1f0f8a0a4952c11e77f4d)) +* new mobile menu ([#3251](https://github.com/fallenbagel/jellyseerr/issues/3251)) ([fcbca17](https://github.com/fallenbagel/jellyseerr/commit/fcbca1722f31f32633a57bc5048f46c9da057d87)) +* translations update from Hosted Weblate ([#3218](https://github.com/fallenbagel/jellyseerr/issues/3218)) ([5940ff7](https://github.com/fallenbagel/jellyseerr/commit/5940ff7f5f62eed9ac5aa6f02803418aaa09813a)) +* **ui:** add episode number to front of episode name in season details ([#3086](https://github.com/fallenbagel/jellyseerr/issues/3086)) ([a672b32](https://github.com/fallenbagel/jellyseerr/commit/a672b324ec391a20f6f3a1daed82a8d276a52c2c)) +* **ui:** request card progress bar ([#3123](https://github.com/fallenbagel/jellyseerr/issues/3123)) ([03853a1](https://github.com/fallenbagel/jellyseerr/commit/03853a1b9155c8a2153c8885022a74619af1bc15)) + +# [1.3.0](https://github.com/fallenbagel/jellyseerr/compare/v1.2.1...v1.3.0) (2023-01-02) + +### Bug Fixes + +- added deep links to issues and status badges ([#3065](https://github.com/fallenbagel/jellyseerr/issues/3065)) ([bfe56c3](https://github.com/fallenbagel/jellyseerr/commit/bfe56c347073001795b1c3e917eb7a5afcc4462c)) +- **api:** handle auth for accounts where the plex id may have been set to null ([#3125](https://github.com/fallenbagel/jellyseerr/issues/3125)) ([15e2469](https://github.com/fallenbagel/jellyseerr/commit/15e246929bdbc2b7b5bdab7a84bd7882b79d5cb1)) +- **api:** ignore Music,Books,Photos,MusicVideo libraries ([d9ca3c6](https://github.com/fallenbagel/jellyseerr/commit/d9ca3c6e52c118698ca71021217f6ca409e71974)) +- count combined episodes ([64339e5](https://github.com/fallenbagel/jellyseerr/commit/64339e5f0374f8490e685e5c086e088bb7fd737e)) +- improved PTR scrolling performance ([#3095](https://github.com/fallenbagel/jellyseerr/issues/3095)) ([07ec3ef](https://github.com/fallenbagel/jellyseerr/commit/07ec3efbcaf669de7ccde4421c1112bfd23675d6)) +- **locale:** fix the duplicated wording in the Clear Media Warning message ([7e20c7c](https://github.com/fallenbagel/jellyseerr/commit/7e20c7cb78a44c32ab8a5f21203e285f23f402ab)) +- **ui:** adds mediaServerName to statusBadge and manageSlideOver ([d0cdce9](https://github.com/fallenbagel/jellyseerr/commit/d0cdce9e90fba642d2bf934a4266e1421424bc73)), closes [#254](https://github.com/fallenbagel/jellyseerr/issues/254) +- update API docs to allow 'all' seasons value ([#3073](https://github.com/fallenbagel/jellyseerr/issues/3073)) ([1dfa943](https://github.com/fallenbagel/jellyseerr/commit/1dfa9431a95e7e2a1843746c2473d8a06f03e184)) + +### Features + +- **api:** adds support for Mixed Libraries ([ba82ece](https://github.com/fallenbagel/jellyseerr/commit/ba82ecec5c994e79d7c9b658372041522b58a120)), closes [#95](https://github.com/fallenbagel/jellyseerr/issues/95) +- custom image proxy ([#3056](https://github.com/fallenbagel/jellyseerr/issues/3056)) ([500cd1f](https://github.com/fallenbagel/jellyseerr/commit/500cd1f872942923d2b9c3b835e6329e335d4a3f)) +- **lang:** add Croatian display language ([#3041](https://github.com/fallenbagel/jellyseerr/issues/3041)) ([64aab6d](https://github.com/fallenbagel/jellyseerr/commit/64aab6dd8240e191026512733b34cc046b6e508a)) + +## [1.29.1](https://github.com/sct/overseerr/compare/v1.29.0...v1.29.1) (2022-04-06) + +### Bug Fixes + +- **auth:** resolve local/password authentication issues ([#2677](https://github.com/sct/overseerr/issues/2677)) ([b75fc7b](https://github.com/sct/overseerr/commit/b75fc7b2384ce760432620faaa92277dcd42b8e1)) + +# [1.29.0](https://github.com/sct/overseerr/compare/v1.28.0...v1.29.0) (2022-04-01) + +### Bug Fixes + +- add Discord ID setting to general user settings page ([#2406](https://github.com/sct/overseerr/issues/2406)) ([eff665e](https://github.com/sct/overseerr/commit/eff665ef4b688aac881408790304b77bd9a31ddb)) +- address unhandled promise rejections & bump node to v16.13 ([#2398](https://github.com/sct/overseerr/issues/2398)) ([8cba486](https://github.com/sct/overseerr/commit/8cba486249fed88232e93a688c8bfe0f6179c589)) +- **css:** rename form-input to form-input-area ([#2613](https://github.com/sct/overseerr/issues/2613)) ([086f0b6](https://github.com/sct/overseerr/commit/086f0b6ce23f607d20c2cec3c73b2e4d1ce9b426)) +- **email:** enclose PGP encryption logic in try/catch ([#2519](https://github.com/sct/overseerr/issues/2519)) ([a76b608](https://github.com/sct/overseerr/commit/a76b608ab796944c0c660e3296a7aca6615d69f3)) +- **frontend:** disable autocomplete on search field ([#2592](https://github.com/sct/overseerr/issues/2592)) ([82d1617](https://github.com/sct/overseerr/commit/82d16177bf763fe8097b4aae326793e3e21e847d)) +- **frontend:** theme-color meta tag ([#2420](https://github.com/sct/overseerr/issues/2420)) ([ff28c9b](https://github.com/sct/overseerr/commit/ff28c9bfebf4a930e2542ee3b3c35f8af4e1b97e)) +- **frontend:** various fixes ([#2524](https://github.com/sct/overseerr/issues/2524)) ([c3dbd0d](https://github.com/sct/overseerr/commit/c3dbd0d6913946e0e1b5308edfbb5ca744740223)) +- **lang:** rename 'Media' notification types for clarity ([#2400](https://github.com/sct/overseerr/issues/2400)) ([399b037](https://github.com/sct/overseerr/commit/399b0379186ed34dcc436bd95330fd1a05fef4b3)) +- **lang:** translations update from Hosted Weblate ([#2625](https://github.com/sct/overseerr/issues/2625)) ([19cdedd](https://github.com/sct/overseerr/commit/19cdedd2a6656b1a852e1cc653bbdb140e978b51)) +- **lang:** translations update from Hosted Weblate ([#2639](https://github.com/sct/overseerr/issues/2639)) ([418a533](https://github.com/sct/overseerr/commit/418a533588bbbdbbbb4caee1ef91d57c1ca35717)) +- **logs:** handle log message nested extra properties ([#2459](https://github.com/sct/overseerr/issues/2459)) ([d777940](https://github.com/sct/overseerr/commit/d7779408d162949b2eafcacefc8eabe53fae229f)) +- **notif:** duplicate notification check logic ([#2424](https://github.com/sct/overseerr/issues/2424)) ([10651ba](https://github.com/sct/overseerr/commit/10651baa675993f7109989bbac67f54661c8693f)) +- **notif:** show event in pop up notification for slack ([#2413](https://github.com/sct/overseerr/issues/2413)) ([d4438c8](https://github.com/sct/overseerr/commit/d4438c82e3753c9b29b6269ad406d263b3fcef4c)), closes [#2408](https://github.com/sct/overseerr/issues/2408) +- **plex:** correctly generate uuid for safari ([#2614](https://github.com/sct/overseerr/issues/2614)) ([d06f2cd](https://github.com/sct/overseerr/commit/d06f2cdb08bfa6f05cf7cec2c408a258fa926b09)) +- **plex:** find TV series in addition to movies from IMDb IDs ([#1830](https://github.com/sct/overseerr/issues/1830)) ([30644f6](https://github.com/sct/overseerr/commit/30644f65ea2e8437676422ae0b083c642a836887)) +- **plex:** include 'Overseerr' in X-Plex-Device-Name header ([#2635](https://github.com/sct/overseerr/issues/2635)) ([d4f9650](https://github.com/sct/overseerr/commit/d4f9650cd07704a97f8b591b7de7351c1e85b825)) +- **plex:** use unique client identifier ([#2602](https://github.com/sct/overseerr/issues/2602)) ([648b346](https://github.com/sct/overseerr/commit/648b346cbe5a941c7e1ec4ddfb276fb0e27ed502)) +- **plex:** user import ([#2442](https://github.com/sct/overseerr/issues/2442)) ([86dff12](https://github.com/sct/overseerr/commit/86dff12cdeef6dca92527dd31757a3a4c7f921bf)) +- **radarr:** correctly check for existing movies ([#2490](https://github.com/sct/overseerr/issues/2490)) ([5d4b06b](https://github.com/sct/overseerr/commit/5d4b06bbcc6cf6d328f6b4a86c4c0f9b0f3aff3e)) +- **radarr:** remove PreDB minimum availability option ([#2386](https://github.com/sct/overseerr/issues/2386)) ([3e5eb4e](https://github.com/sct/overseerr/commit/3e5eb4e148a9f88b871abc4ee1784b870f691534)) +- **requests:** check for existing media of same type when requesting ([#2445](https://github.com/sct/overseerr/issues/2445)) ([eb9ca2e](https://github.com/sct/overseerr/commit/eb9ca2e86f3be3f4ff8ee2e7c4aecdf337d8976d)) +- **sonarr:** monitor existing series upon request approval ([#2553](https://github.com/sct/overseerr/issues/2553)) ([aa062d9](https://github.com/sct/overseerr/commit/aa062d921c425d4b64bfdb28a5f102b0c92f7d87)) +- **sonarr:** only scan seasons that exist in TMDb ([#2523](https://github.com/sct/overseerr/issues/2523)) ([6168185](https://github.com/sct/overseerr/commit/61681857b123802aaeff02a8f61b1ba046c5d333)) +- **tautulli:** fetch additional user history as necessary to return 20 unique media ([#2446](https://github.com/sct/overseerr/issues/2446)) ([7d19de6](https://github.com/sct/overseerr/commit/7d19de6a4af6297be18140ca59402b40f7bbb30b)) + +### Features + +- **about:** show config directory ([#2600](https://github.com/sct/overseerr/issues/2600)) ([0c7373c](https://github.com/sct/overseerr/commit/0c7373c7e89a4ff717efaa7d6a5854f7ccd6a8d3)) +- **api:** add additional request counts ([#2426](https://github.com/sct/overseerr/issues/2426)) ([2535edc](https://github.com/sct/overseerr/commit/2535edcc7fd6ec66fd45ad754c03929f1fe94871)) +- **discord:** add 'Enable Mentions' setting ([#1779](https://github.com/sct/overseerr/issues/1779)) ([5f7538a](https://github.com/sct/overseerr/commit/5f7538ae2bf9c6e2feea385cc299bd08df071218)) +- **frontend:** open media management slideover on status badge click ([#2407](https://github.com/sct/overseerr/issues/2407)) ([1f5785d](https://github.com/sct/overseerr/commit/1f5785d6c53b2ca2da67a8ccee72165c052c61a1)) +- **lang:** add Albanian display language ([#2605](https://github.com/sct/overseerr/issues/2605)) ([3d32462](https://github.com/sct/overseerr/commit/3d32462f50b4ced0d9205b79003c35d6d1c948a3)) +- **lang:** translations update from Hosted Weblate ([#2379](https://github.com/sct/overseerr/issues/2379)) ([bd93168](https://github.com/sct/overseerr/commit/bd93168ba1ed650baf4024569bb6a76811a99820)) +- **lang:** translations update from Hosted Weblate ([#2389](https://github.com/sct/overseerr/issues/2389)) ([d2241a4](https://github.com/sct/overseerr/commit/d2241a41877d126a802fc53c925d258af31f34fd)) +- **lang:** translations update from Hosted Weblate ([#2404](https://github.com/sct/overseerr/issues/2404)) ([1b29b15](https://github.com/sct/overseerr/commit/1b29b15d7c9a7ec918cb59116d60e1ae2e797dc4)) +- **lang:** translations update from Hosted Weblate ([#2405](https://github.com/sct/overseerr/issues/2405)) ([879df20](https://github.com/sct/overseerr/commit/879df20022c8c5d9b32858ac5499d3e4369fc064)) +- **lang:** translations update from Hosted Weblate ([#2414](https://github.com/sct/overseerr/issues/2414)) ([88536b1](https://github.com/sct/overseerr/commit/88536b1f9d6e8c1a11e1adf91b85bab4f34b751c)) +- **lang:** translations update from Hosted Weblate ([#2425](https://github.com/sct/overseerr/issues/2425)) ([e9d4b63](https://github.com/sct/overseerr/commit/e9d4b6327b50a005ee6c2c3292b6f107e90fc50c)) +- **lang:** translations update from Hosted Weblate ([#2428](https://github.com/sct/overseerr/issues/2428)) ([f8b1bcc](https://github.com/sct/overseerr/commit/f8b1bccda44371bb6f3f8f4ceeab900b1df3de31)) +- **lang:** translations update from Hosted Weblate ([#2436](https://github.com/sct/overseerr/issues/2436)) ([99c0407](https://github.com/sct/overseerr/commit/99c04072e9f7be8191f25cbcfd5103017b8796eb)) +- **lang:** translations update from Hosted Weblate ([#2452](https://github.com/sct/overseerr/issues/2452)) ([b5bd6ee](https://github.com/sct/overseerr/commit/b5bd6ee78f3d4aa14f0c440d1f2a8323dccfa399)) +- **lang:** translations update from Hosted Weblate ([#2457](https://github.com/sct/overseerr/issues/2457)) ([92b2d32](https://github.com/sct/overseerr/commit/92b2d32d2e1e1d319410a9e357e1304065a77598)) +- **lang:** translations update from Hosted Weblate ([#2489](https://github.com/sct/overseerr/issues/2489)) ([ec08fa6](https://github.com/sct/overseerr/commit/ec08fa67934715ff4a4d618d5b9ff97853913b78)) +- **lang:** translations update from Hosted Weblate ([#2508](https://github.com/sct/overseerr/issues/2508)) ([9f4ae34](https://github.com/sct/overseerr/commit/9f4ae34da76707a40e2c89a50c722ffa1c0327c0)) +- **lang:** translations update from Hosted Weblate ([#2531](https://github.com/sct/overseerr/issues/2531)) ([54b32eb](https://github.com/sct/overseerr/commit/54b32ebfd6b2eb6aeeea98c25939166eda8cc17f)) +- **lang:** translations update from Hosted Weblate ([#2541](https://github.com/sct/overseerr/issues/2541)) ([4549ed3](https://github.com/sct/overseerr/commit/4549ed389e4f25c0946dc01526387e5ac000c3cf)) +- **lang:** translations update from Hosted Weblate ([#2611](https://github.com/sct/overseerr/issues/2611)) ([81c75c8](https://github.com/sct/overseerr/commit/81c75c800edf6d36a1082a291ef7e308f338d005)) +- **lang:** translations update from Hosted Weblate ([#2629](https://github.com/sct/overseerr/issues/2629)) ([1d0cbd2](https://github.com/sct/overseerr/commit/1d0cbd2e761072be0b4b3de461397ad9f9f681f3)) +- **lang:** translations update from Hosted Weblate ([#2645](https://github.com/sct/overseerr/issues/2645)) ([341e3b8](https://github.com/sct/overseerr/commit/341e3b8f0657e09f53ad0b813b051290947343c0)) +- **logs:** use separate json file to parse logs for log viewer ([#2399](https://github.com/sct/overseerr/issues/2399)) ([ce31bef](https://github.com/sct/overseerr/commit/ce31bef8a125c5492f2a1cfef0dcf3d8a4e9ee11)) +- **notif:** add Gotify agent ([#2196](https://github.com/sct/overseerr/issues/2196)) ([e0b6abe](https://github.com/sct/overseerr/commit/e0b6abe4796f5a324c0ff78cff317fcaead671f1)), closes [#2183](https://github.com/sct/overseerr/issues/2183) [#2183](https://github.com/sct/overseerr/issues/2183) [#2077](https://github.com/sct/overseerr/issues/2077) [#2183](https://github.com/sct/overseerr/issues/2183) [#2183](https://github.com/sct/overseerr/issues/2183) [#2183](https://github.com/sct/overseerr/issues/2183) [#2077](https://github.com/sct/overseerr/issues/2077) [#2183](https://github.com/sct/overseerr/issues/2183) [#2183](https://github.com/sct/overseerr/issues/2183) [#2183](https://github.com/sct/overseerr/issues/2183) +- **notif:** add Pushbullet channel tag ([#2198](https://github.com/sct/overseerr/issues/2198)) ([f9200b7](https://github.com/sct/overseerr/commit/f9200b7977208f9b8267ce3a74bd8a86d6f28f7b)) +- **plex:** selective user import ([#2188](https://github.com/sct/overseerr/issues/2188)) ([9cb97db](https://github.com/sct/overseerr/commit/9cb97db13ced5df2dc595cd9033470b1a0750093)) +- **search:** filter search results by year ([#2460](https://github.com/sct/overseerr/issues/2460)) ([72c825d](https://github.com/sct/overseerr/commit/72c825d2a5109688bcc1991a30249284bf281500)) +- **search:** search by id ([#2082](https://github.com/sct/overseerr/issues/2082)) ([b31cdbf](https://github.com/sct/overseerr/commit/b31cdbf074d5dbecbbf6da135a9b686aea9e3c0e)) +- Tautulli integration ([#2230](https://github.com/sct/overseerr/issues/2230)) ([0842c23](https://github.com/sct/overseerr/commit/0842c233d0fc56d44824cad18749492cd52cbed5)) +- **tautulli:** validate upon saving settings ([#2511](https://github.com/sct/overseerr/issues/2511)) ([1dc900d](https://github.com/sct/overseerr/commit/1dc900d5ce9689d179c9d2f554abc74ca50bd9cb)) +- **ui:** add trakt external link ([#2367](https://github.com/sct/overseerr/issues/2367)) ([4e56bae](https://github.com/sct/overseerr/commit/4e56bae98508c1a60aeb3a08560ba1c00acce7e7)) +- verify Plex server access during auth for existing users with Plex IDs ([#2458](https://github.com/sct/overseerr/issues/2458)) ([85bb30e](https://github.com/sct/overseerr/commit/85bb30e252c27047ae367491f0e5bb92a7d52605)) + +# [1.28.0](https://github.com/sct/overseerr/compare/v1.27.0...v1.28.0) (2022-01-01) + +### Bug Fixes + +- add missing route guards to issues pages ([#2235](https://github.com/sct/overseerr/issues/2235)) ([c79dc9f](https://github.com/sct/overseerr/commit/c79dc9f70f512dbec0e3460ee78dbc9feccfbbb1)) +- allow basic HTTP auth in hostname validation ([#2307](https://github.com/sct/overseerr/issues/2307)) ([d48a7ba](https://github.com/sct/overseerr/commit/d48a7ba518f9c79d70e499037cb730eb3efe2c08)) +- **docker:** explicitly install python3 ([#2273](https://github.com/sct/overseerr/issues/2273)) [skip ci] ([f1cd087](https://github.com/sct/overseerr/commit/f1cd0878a5c74bddc864f5f8ce9e2f041bdde5ec)) +- **email:** use decrypted private key ([#2232](https://github.com/sct/overseerr/issues/2232)) ([8d29685](https://github.com/sct/overseerr/commit/8d2968572a569ed77a4d7c14ae1dc69935fa847e)) +- **frontend:** more issues-related fixes ([#2234](https://github.com/sct/overseerr/issues/2234)) ([3ec4a9c](https://github.com/sct/overseerr/commit/3ec4a9c76e1f31bee5c8801b389721bf8e5884e0)) +- **frontend:** setup page backdrops ([#2251](https://github.com/sct/overseerr/issues/2251)) ([78a8091](https://github.com/sct/overseerr/commit/78a8091bcd29a7cf50cc7c493c28710389817adf)) +- **frontend:** use consistent formatting & strings ([#2231](https://github.com/sct/overseerr/issues/2231)) ([2164471](https://github.com/sct/overseerr/commit/216447121b686b6d01a31b95ec0c8eb005f6b103)) +- handle Plex library settings migration failure gracefully ([#2254](https://github.com/sct/overseerr/issues/2254)) ([ed53810](https://github.com/sct/overseerr/commit/ed53810fb33f70722361c67d176ff4edf531ba45)) +- **issues:** only allow edit of own comments & do not allow non-admin delete of issues with comments ([#2248](https://github.com/sct/overseerr/issues/2248)) ([bba09d6](https://github.com/sct/overseerr/commit/bba09d69c1bc55c2f35db5a7986e7c935cc9619c)) +- **lang:** add missing string ([#2370](https://github.com/sct/overseerr/issues/2370)) ([d36c1d2](https://github.com/sct/overseerr/commit/d36c1d29295020efb76bac21a443b6f9049802f3)) +- **lang:** string edits ([#2229](https://github.com/sct/overseerr/issues/2229)) ([ab20c21](https://github.com/sct/overseerr/commit/ab20c21184639e1c7725f7cae96249c6fa157351)) +- **lang:** translations update from Weblate ([#2212](https://github.com/sct/overseerr/issues/2212)) ([85aec4f](https://github.com/sct/overseerr/commit/85aec4f8925746ebae9bcc99d8480b78ccfd851e)) +- **logs:** handle unexpected log messages ([#2303](https://github.com/sct/overseerr/issues/2303)) ([f284e4a](https://github.com/sct/overseerr/commit/f284e4ab978e502d2cc08e76226a8ebac91bb48f)) +- **logs:** lazily parse log message label ([#2359](https://github.com/sct/overseerr/issues/2359)) ([5af06bd](https://github.com/sct/overseerr/commit/5af06bd87226fbc6176b0c5e362824793165a34e)) +- **notif:** correct issue notif action URLs ([#2333](https://github.com/sct/overseerr/issues/2333)) ([dc7f959](https://github.com/sct/overseerr/commit/dc7f959cb422a8d89bcebc78377f1513412e542c)) +- **notif:** only send MEDIA_AVAILABLE notifications for non-declined requests ([#2343](https://github.com/sct/overseerr/issues/2343)) ([fcb0dcf](https://github.com/sct/overseerr/commit/fcb0dcf5be64bf9ca814bfe119586908922099c5)) +- **requests:** do not fail request edits if acting user lacks Manage Users permission ([#2338](https://github.com/sct/overseerr/issues/2338)) ([91bfff7](https://github.com/sct/overseerr/commit/91bfff71b7c05c9b9aad2c95282533eefbb6b2e7)) +- secure session cookie ([#2308](https://github.com/sct/overseerr/issues/2308)) ([7f330af](https://github.com/sct/overseerr/commit/7f330aff2e1d3546e8dd1a3e4b037b9beb1cc7f0)) +- **servarr:** handle baseurl error when testing connection ([#2294](https://github.com/sct/overseerr/issues/2294)) ([93b5ea2](https://github.com/sct/overseerr/commit/93b5ea20ca590996f6dc90713a76800180d0621c)) +- **servarr:** handle servaarr server being unavailable when scanning downloads ([#2358](https://github.com/sct/overseerr/issues/2358)) ([488874f](https://github.com/sct/overseerr/commit/488874fc17e4e4719e90d383b83b1e1a5217213b)) +- sort collection parts by release date ([#2368](https://github.com/sct/overseerr/issues/2368)) ([1b3797c](https://github.com/sct/overseerr/commit/1b3797cf6e6ef6b3d8c81e644382f6e3f68cfaaa)) +- **ui:** request badge styling in request list ([#2302](https://github.com/sct/overseerr/issues/2302)) ([f2375c9](https://github.com/sct/overseerr/commit/f2375c902b79dcb1f349500862775ae57ea7d406)) + +### Features + +- add production countries to movie/TV detail pages ([#2170](https://github.com/sct/overseerr/issues/2170)) ([30b20df](https://github.com/sct/overseerr/commit/30b20df37a9604ba1c066f89e54a5482a09575ea)) +- add quotas, advanced options, and toggles to collection request modal ([#1742](https://github.com/sct/overseerr/issues/1742)) ([af40212](https://github.com/sct/overseerr/commit/af40212a738f8d6d9a5bf26dc20c0c87780d6020)) +- **frontend:** add Discovery+ to network slider ([#2345](https://github.com/sct/overseerr/issues/2345)) ([2ded8f5](https://github.com/sct/overseerr/commit/2ded8f5484168bd7b8f45124d9ebdd296a5708d5)) +- issues ([#2180](https://github.com/sct/overseerr/issues/2180)) ([e402c42](https://github.com/sct/overseerr/commit/e402c42aaa7d795cd724856a2e23615bb1a3695d)) +- **lang:** add Polish display language ([#2261](https://github.com/sct/overseerr/issues/2261)) ([c760cea](https://github.com/sct/overseerr/commit/c760ceaa5f36c77fa3ce320fae1b4597d2d8b976)) +- **lang:** translated using Weblate (Chinese (Traditional)) ([#2272](https://github.com/sct/overseerr/issues/2272)) ([d401e33](https://github.com/sct/overseerr/commit/d401e33249cbbca6e707479e5f0207e298ef3248)) +- **lang:** translations update from Hosted Weblate ([#2277](https://github.com/sct/overseerr/issues/2277)) ([92732fc](https://github.com/sct/overseerr/commit/92732fcb42c56242d16daab00e2d38740b92dea0)) +- **lang:** translations update from Hosted Weblate ([#2315](https://github.com/sct/overseerr/issues/2315)) ([6245be1](https://github.com/sct/overseerr/commit/6245be1e10dda67c869b59522c1290e7c100145f)) +- **lang:** translations update from Hosted Weblate ([#2320](https://github.com/sct/overseerr/issues/2320)) ([68112fa](https://github.com/sct/overseerr/commit/68112faefbd64d5c71d3eff21620767f88ccfc34)) +- **lang:** translations update from Hosted Weblate ([#2325](https://github.com/sct/overseerr/issues/2325)) ([febf067](https://github.com/sct/overseerr/commit/febf0677b880d2fed2822ce510db7cbb0826a920)) +- **lang:** translations update from Hosted Weblate ([#2336](https://github.com/sct/overseerr/issues/2336)) ([3f7ef7a](https://github.com/sct/overseerr/commit/3f7ef7af97a807ef38041f4f2642b565aa33d066)) +- **lang:** translations update from Hosted Weblate ([#2341](https://github.com/sct/overseerr/issues/2341)) ([33fe0bd](https://github.com/sct/overseerr/commit/33fe0bdd1e00da40e85b4e4b4780134b31a105d2)) +- **lang:** translations update from Hosted Weblate ([#2346](https://github.com/sct/overseerr/issues/2346)) ([50dc934](https://github.com/sct/overseerr/commit/50dc9341dd98cb2d8ef3ef6471882a5a9b060afa)) +- **lang:** translations update from Hosted Weblate ([#2364](https://github.com/sct/overseerr/issues/2364)) ([d437cc2](https://github.com/sct/overseerr/commit/d437cc25392e9c0881888371ffabc82892a1b15c)) +- **lang:** translations update from Hosted Weblate ([#2366](https://github.com/sct/overseerr/issues/2366)) ([cc2b2bc](https://github.com/sct/overseerr/commit/cc2b2bc7a8ecd89e1feb38a907596b16df9bf0fc)) +- **lang:** translations update from Hosted Weblate ([#2374](https://github.com/sct/overseerr/issues/2374)) ([b9bedac](https://github.com/sct/overseerr/commit/b9bedac7d7ba85223ecf1d9b93b96e2a490d571a)) +- **lang:** translations update from Weblate ([#2226](https://github.com/sct/overseerr/issues/2226)) ([62b3dc5](https://github.com/sct/overseerr/commit/62b3dc5471c28f4d0e4399cb3bc8bfab94cff5ea)) +- **lang:** translations update from Weblate ([#2241](https://github.com/sct/overseerr/issues/2241)) ([2b0b8e0](https://github.com/sct/overseerr/commit/2b0b8e05d9c95ff9218cea858a920a2815871186)) +- **lang:** translations update from Weblate ([#2244](https://github.com/sct/overseerr/issues/2244)) ([0828b00](https://github.com/sct/overseerr/commit/0828b008badc8b512316799a6787bb7c403658d5)) +- **lang:** translations update from Weblate ([#2247](https://github.com/sct/overseerr/issues/2247)) ([8c49309](https://github.com/sct/overseerr/commit/8c49309c35c31f7bcd0b84b0a307febc16842f68)) +- **lang:** translations update from Weblate ([#2252](https://github.com/sct/overseerr/issues/2252)) ([99d5000](https://github.com/sct/overseerr/commit/99d50004e58f6b4594df0a171f6bc668635ec50c)) +- **lang:** translations update from Weblate ([#2265](https://github.com/sct/overseerr/issues/2265)) ([b1b367a](https://github.com/sct/overseerr/commit/b1b367aac625ed3eb865832c94c2352e5a5c40f5)) +- **notif:** 4K media notifications ([#2324](https://github.com/sct/overseerr/issues/2324)) ([88a8c1a](https://github.com/sct/overseerr/commit/88a8c1aa596e1113d6da52e5e8cbe443abc6384f)) +- **notif:** add Pushbullet and Pushover agents to user notification settings ([#1740](https://github.com/sct/overseerr/issues/1740)) ([aeb7a48](https://github.com/sct/overseerr/commit/aeb7a48d72cec3fa2b857030aad3eaa0a457a896)) +- **notif:** issue notifications ([#2242](https://github.com/sct/overseerr/issues/2242)) ([c9ffac3](https://github.com/sct/overseerr/commit/c9ffac33f7c04d926f8c45295703689d42fe87af)) +- **search:** close search bar when hitting return ([#2260](https://github.com/sct/overseerr/issues/2260)) ([b423dc1](https://github.com/sct/overseerr/commit/b423dc167d12f0ba49f902876bceb2e876e35f58)) +- **ui:** allow admins to edit & approve request from advanced request modal ([#2067](https://github.com/sct/overseerr/issues/2067)) ([340f1a2](https://github.com/sct/overseerr/commit/340f1a211952bd2e8f40f0ea4622b52dbe934e85)) + +# [1.27.0](https://github.com/sct/overseerr/compare/v1.26.1...v1.27.0) (2021-10-19) + +### Bug Fixes + +- **api:** return queried user's requests instead of own requests ([#2174](https://github.com/sct/overseerr/issues/2174)) ([0edb1f4](https://github.com/sct/overseerr/commit/0edb1f452b6ff4a49ae2bde15f7273769788cf4f)) +- **api:** use query builder for user requests endpoint ([#2119](https://github.com/sct/overseerr/issues/2119)) ([a20f395](https://github.com/sct/overseerr/commit/a20f395c94c97dd7ddbc25590f15def2c9bf13c9)) +- apply request overrides iff override & selected servers match ([#2164](https://github.com/sct/overseerr/issues/2164)) ([50ce198](https://github.com/sct/overseerr/commit/50ce198471b1a3777a183d68904bbfb39ebd4523)) +- **email:** do not attempt to display logo if app URL not configured ([#2125](https://github.com/sct/overseerr/issues/2125)) ([b3b421a](https://github.com/sct/overseerr/commit/b3b421a67408a4a48d23c15341fcdf7aaf19b25a)) +- **frontend:** notification type validation ([#2207](https://github.com/sct/overseerr/issues/2207)) ([2f204b9](https://github.com/sct/overseerr/commit/2f204b995269a53ae36f2a8733f27ae6ab70da5a)) +- **scripts:** update migration scripts ([#2208](https://github.com/sct/overseerr/issues/2208)) [skip ci] ([d0ac74e](https://github.com/sct/overseerr/commit/d0ac74ea4bbfcf3d25d30cbd422d9df1c1259a18)) +- **ui:** refinements for 'About' page ([#2173](https://github.com/sct/overseerr/issues/2173)) ([084a842](https://github.com/sct/overseerr/commit/084a842a4f9b6caaed22edbe77bc9e414bc1f387)) + +### Features + +- display release dates for theatrical, digital, and physical release types ([#1492](https://github.com/sct/overseerr/issues/1492)) ([a4dca23](https://github.com/sct/overseerr/commit/a4dca2356b7605026f7bc45b691496e765c3328c)) +- dynamically fetch login screen backdrop images ([#2206](https://github.com/sct/overseerr/issues/2206)) ([3486d0b](https://github.com/sct/overseerr/commit/3486d0bf5520cbdff60bd8fd023caed76c452973)) +- **frontend:** add Hulu to network slider ([#2204](https://github.com/sct/overseerr/issues/2204)) ([1e402f7](https://github.com/sct/overseerr/commit/1e402f710b53c11855aab0abdb4b12c51c30b022)) +- **jobs:** allow modifying job schedules ([#1440](https://github.com/sct/overseerr/issues/1440)) ([82614ca](https://github.com/sct/overseerr/commit/82614ca4410782a12d65b4c0a6526ff064be1241)) +- **lang:** add Czech and Danish display languages ([#2176](https://github.com/sct/overseerr/issues/2176)) ([8d8db6c](https://github.com/sct/overseerr/commit/8d8db6cf5d98d4e498a31db339d02f8a98057c8d)) +- **lang:** translations update from Weblate ([#2101](https://github.com/sct/overseerr/issues/2101)) ([c73cf7b](https://github.com/sct/overseerr/commit/c73cf7b19cbc19e97a777c0facb9264fb0113093)) +- **lang:** translations update from Weblate ([#2179](https://github.com/sct/overseerr/issues/2179)) ([e3312ce](https://github.com/sct/overseerr/commit/e3312cef33821c8cb76a4a63bd565c78d67b3e0b)) +- **lang:** translations update from Weblate ([#2185](https://github.com/sct/overseerr/issues/2185)) ([dce10f7](https://github.com/sct/overseerr/commit/dce10f743f52cb04036e2cdaee280e26a81b253b)) +- **lang:** translations update from Weblate ([#2202](https://github.com/sct/overseerr/issues/2202)) ([492d8e3](https://github.com/sct/overseerr/commit/492d8e3daa5fb99aa9df2a18978085d5ddd581e7)) +- **lang:** translations update from Weblate ([#2210](https://github.com/sct/overseerr/issues/2210)) ([0a6ef6c](https://github.com/sct/overseerr/commit/0a6ef6cc81376f7a02f1483109be7ae4ab851c48)) +- **plex-scan:** plex scanner improvements ([#2105](https://github.com/sct/overseerr/issues/2105)) ([afda9c7](https://github.com/sct/overseerr/commit/afda9c7dc222137b0e6654a6beb4737cf2c1752e)) +- **servarr:** auto fill base url when testing service if missing ([#1995](https://github.com/sct/overseerr/issues/1995)) ([739f667](https://github.com/sct/overseerr/commit/739f667b54d8dec258b74d0cd8fd8b3b88dcf8d5)) +- **ui:** link processing/requested status badges to service URL ([#1761](https://github.com/sct/overseerr/issues/1761)) ([032c14a](https://github.com/sct/overseerr/commit/032c14a22680f62f8106943297b081b68645ce61)) + +## [1.26.1](https://github.com/sct/overseerr/compare/v1.26.0...v1.26.1) (2021-09-20) + +### Bug Fixes + +- **rt-api:** correctly format movie urls ([4c6009b](https://github.com/sct/overseerr/commit/4c6009bc2c3ff5f657a806363e3bdf7cd83d4261)) + +# [1.26.0](https://github.com/sct/overseerr/compare/v1.25.0...v1.26.0) (2021-09-19) + +### Bug Fixes + +- **email:** omit links when application URL is not configured ([#1806](https://github.com/sct/overseerr/issues/1806)) ([1133a34](https://github.com/sct/overseerr/commit/1133a34ffdf95c4d036be0264fe7f94f64007e8f)) +- **lang:** minor changes to password reset strings ([#1798](https://github.com/sct/overseerr/issues/1798)) ([a41245c](https://github.com/sct/overseerr/commit/a41245c703688743ec24f9b4a53e70f3340daa0f)) +- **notif:** truncate media overviews ([#1800](https://github.com/sct/overseerr/issues/1800)) ([42e45f3](https://github.com/sct/overseerr/commit/42e45f38e5ede7df0fc4bdb20a970917b2361569)) +- **plex:** do not fail to scan empty libraries ([#1771](https://github.com/sct/overseerr/issues/1771)) ([6789b87](https://github.com/sct/overseerr/commit/6789b8701cb644d9a3f1384f30b3dff707201ef7)) +- **quota:** block multi-season requests that would exceed a user's quota ([#1874](https://github.com/sct/overseerr/issues/1874)) ([8a55f85](https://github.com/sct/overseerr/commit/8a55f85d3ef14ccb83b139acb35d0746431637be)) +- **rt-api:** use rotten-tomatoes 2.0 search api for movies ([a11bb49](https://github.com/sct/overseerr/commit/a11bb49663ec345332c4dd70ddbb49ce230b5c3c)) +- **ui:** center logo on password reset pages ([#1807](https://github.com/sct/overseerr/issues/1807)) ([b8e82b5](https://github.com/sct/overseerr/commit/b8e82b5b4d3cb49ec372e3dce3cd89dff440ffd0)) +- **ui:** change sidebar breakpoint to lg ([#1972](https://github.com/sct/overseerr/issues/1972)) ([70bd9e9](https://github.com/sct/overseerr/commit/70bd9e9308b607206b60a2a36a511de6e397a3db)) +- **ui:** do not allow submission of invalid form inputs ([#1799](https://github.com/sct/overseerr/issues/1799)) ([910d00c](https://github.com/sct/overseerr/commit/910d00c19522a70125bfb5e5081a7ef4000e7f54)) +- **ui:** do not display negative remaining quota ([#1859](https://github.com/sct/overseerr/issues/1859)) ([3841fb0](https://github.com/sct/overseerr/commit/3841fb06ebe1e09250362cc6cb401fdca12eef7f)) +- **ui:** fix notifications settings buttons overflowing ([#1911](https://github.com/sct/overseerr/issues/1911)) ([0ce18b2](https://github.com/sct/overseerr/commit/0ce18b21ca547af6c083c3f248e22b7daf92aef0)) +- **ui:** sort 'Request As' user dropdown by display name ([#2099](https://github.com/sct/overseerr/issues/2099)) ([bb09f8e](https://github.com/sct/overseerr/commit/bb09f8eaf70f6d0c981f31bd5f3c8afb2fe101ab)) +- **webpush:** load user in push sub query ([#1894](https://github.com/sct/overseerr/issues/1894)) ([6f2db6a](https://github.com/sct/overseerr/commit/6f2db6a6ccf299262cf86d91acf639b921f28286)) +- correct logo filename ([#1805](https://github.com/sct/overseerr/issues/1805)) ([f95be83](https://github.com/sct/overseerr/commit/f95be832f95a68b114ff24a65ffa0ebbd71b4121)) + +### Features + +- list streaming providers on movie/TV detail pages ([#1778](https://github.com/sct/overseerr/issues/1778)) ([98ece67](https://github.com/sct/overseerr/commit/98ece67655a5dffe894974e337a3603afeed0236)) +- **lang:** add Simplified Chinese display language ([#2032](https://github.com/sct/overseerr/issues/2032)) ([590ea7e](https://github.com/sct/overseerr/commit/590ea7e40460e381377b212d00869f191908b41f)) +- **lang:** translated using Weblate (German) ([#1791](https://github.com/sct/overseerr/issues/1791)) ([15f7941](https://github.com/sct/overseerr/commit/15f7941269075b7e12de8bbc0f98418af70df380)) +- **lang:** translations update from Weblate ([#1772](https://github.com/sct/overseerr/issues/1772)) ([6a75a05](https://github.com/sct/overseerr/commit/6a75a05c2348455d5374132a2574d988879d543a)) +- **lang:** translations update from Weblate ([#1796](https://github.com/sct/overseerr/issues/1796)) ([57b52fc](https://github.com/sct/overseerr/commit/57b52fc9cccd3fac93cdb68e36cf652ddbcdf86c)) +- **lang:** translations update from Weblate ([#1910](https://github.com/sct/overseerr/issues/1910)) ([fe89fd5](https://github.com/sct/overseerr/commit/fe89fd5f12460cb1b3acb09fb16b62497ef50f5f)) +- **lang:** translations update from Weblate ([#2058](https://github.com/sct/overseerr/issues/2058)) ([db42c46](https://github.com/sct/overseerr/commit/db42c4678145d2a9676aa71b6773607b696f7cea)) +- **notif:** Restyle HTML email notifications Part 2 ([#1917](https://github.com/sct/overseerr/issues/1917)) ([376149d](https://github.com/sct/overseerr/commit/376149d6ebb4db28d949391115f475afdd4e7d48)) +- **ui:** add 'show more/less...' for studios on movie details page ([#1770](https://github.com/sct/overseerr/issues/1770)) ([680ea0c](https://github.com/sct/overseerr/commit/680ea0c87a9ae143413354680c421d62bccd869d)) +- new logo, who dis? ([#1802](https://github.com/sct/overseerr/issues/1802)) ([beb5637](https://github.com/sct/overseerr/commit/beb5637d9f5c01d773eaee93035b7c195c2ae5f2)) + +# [1.25.0](https://github.com/sct/overseerr/compare/v1.24.0...v1.25.0) (2021-06-10) + +### Bug Fixes + +- **frontend:** add missing route guards to settings pages ([#1700](https://github.com/sct/overseerr/issues/1700)) ([78fc1f7](https://github.com/sct/overseerr/commit/78fc1f7b7d9ef912077066a3605fed6237fb4c8a)) +- **locale:** set locale based on user settings upon login ([#1584](https://github.com/sct/overseerr/issues/1584)) ([f48312e](https://github.com/sct/overseerr/commit/f48312e833ed5d48c41179d0eadbc66d45486d8a)) +- **notif:** include year in Media Available notifications ([#1672](https://github.com/sct/overseerr/issues/1672)) ([11aa712](https://github.com/sct/overseerr/commit/11aa712eb0e8796874c96fbcc9b51b523108e2d4)) +- **plex:** disable library sync if Plex not configured, and disable scan if no libraries ([#1764](https://github.com/sct/overseerr/issues/1764)) ([22238fe](https://github.com/sct/overseerr/commit/22238fe4f711267d001be95942b3151c536e0c18)) +- **plex:** do not fail to import Plex users when Plex Home has managed users ([#1699](https://github.com/sct/overseerr/issues/1699)) ([310cdb3](https://github.com/sct/overseerr/commit/310cdb36df1601bca5e57f0bc796c44111b8435f)) +- **plex:** sync libraries after saving settings ([#1592](https://github.com/sct/overseerr/issues/1592)) ([9749d72](https://github.com/sct/overseerr/commit/9749d723fc0a282b291c06ee68a6e174dcec1c5b)) +- **requests:** appropriately set modifiedBy user for new requests ([#1684](https://github.com/sct/overseerr/issues/1684)) ([a3f04b3](https://github.com/sct/overseerr/commit/a3f04b3f3522d46dc65178bddd1e986426e48050)) +- **requests:** do not prevent duplicate requests if other requests are declined ([de0759c](https://github.com/sct/overseerr/commit/de0759c26a9e857e2b8d7244673625fc79ee4660)) +- **requests:** prevent duplicate movie requests ([126d866](https://github.com/sct/overseerr/commit/126d8665ee2808fc0bc37df4ca61f3e63be096e2)) +- check that application URL and email agent are configured for password reset/generation ([#1724](https://github.com/sct/overseerr/issues/1724)) ([091d66a](https://github.com/sct/overseerr/commit/091d66a1928d3c69a11eab2a789b4639b5ba9817)) +- correctly display error messages ([#1653](https://github.com/sct/overseerr/issues/1653)) ([31cb717](https://github.com/sct/overseerr/commit/31cb7176d286e706575a2dc8003df13f3e737106)) +- handle null values in User email transform ([#1712](https://github.com/sct/overseerr/issues/1712)) ([4a042f1](https://github.com/sct/overseerr/commit/4a042f12be6510ee47de3a7e025497f8d132d6a1)) +- **lang:** only set locale once at page load and move subsequent updates back into Layout ([14756f4](https://github.com/sct/overseerr/commit/14756f4b208c5b201a6e632b43e7a21c5bec6f9c)), closes [#1662](https://github.com/sct/overseerr/issues/1662) +- **locale:** properly restore display language upon page refresh ([#1646](https://github.com/sct/overseerr/issues/1646)) ([e85d1ce](https://github.com/sct/overseerr/commit/e85d1ce94ec45d8f5d086722cfd88e0e2c5b4bb6)) +- **notifications:** default webpush notification agent to enabled for users for settings response ([7520e24](https://github.com/sct/overseerr/commit/7520e24e9287e214dd31224f1201e9b6385fd567)), closes [#1663](https://github.com/sct/overseerr/issues/1663) +- **quotas:** do not count already-requested seasons when editing TV request ([#1649](https://github.com/sct/overseerr/issues/1649)) ([808ccf1](https://github.com/sct/overseerr/commit/808ccf1c6975f853db6dc89f4d9f1f5488dbaae3)) +- **requests:** remove requestedBy user param from existing movie request check ([#1569](https://github.com/sct/overseerr/issues/1569)) ([788f3dc](https://github.com/sct/overseerr/commit/788f3dc435ae224fcc4d4cb2890b1b9b494c64e8)) +- **sensitiveinput:** do not capture enter key input ([#1650](https://github.com/sct/overseerr/issues/1650)) ([bb8d14b](https://github.com/sct/overseerr/commit/bb8d14b5ffd840eff0c2a00e1b5d318677a5ca5f)) +- **sonarr:** do not mark media as failed if there is no season data on TVDB ([#1691](https://github.com/sct/overseerr/issues/1691)) ([0cd7fa0](https://github.com/sct/overseerr/commit/0cd7fa0f1a00d129339be13550a4f694c820a0e9)) +- **tv:** don't show duplicate air date ([#1666](https://github.com/sct/overseerr/issues/1666)) ([e1f5feb](https://github.com/sct/overseerr/commit/e1f5febe7bbf27e77b6f5d057c2c3f7e22898734)) +- **ui:** add clarification to user settings ([#1644](https://github.com/sct/overseerr/issues/1644)) ([2ef57e9](https://github.com/sct/overseerr/commit/2ef57e9b1a5b4d0a1499921f4e26b0b0712d7ded)) +- **ui:** correct horizontal overflow behavior of settings tabs ([#1667](https://github.com/sct/overseerr/issues/1667)) ([e6d5f0a](https://github.com/sct/overseerr/commit/e6d5f0abfebdc24f25d08822b57a8eb7bc48e137)) +- **ui:** hide advanced request options when there is only one choice ([#1591](https://github.com/sct/overseerr/issues/1591)) ([6b26188](https://github.com/sct/overseerr/commit/6b26188d888a1f80bd36a1968e41333bab2af794)) +- **ui:** improve QuotaSelector display of unlimited and singular values ([#1704](https://github.com/sct/overseerr/issues/1704)) ([59b2ec1](https://github.com/sct/overseerr/commit/59b2ec11fa8868bf6873ffa80f4999ae10d65637)) +- perform case-insensitive match for local user email addresses ([#1633](https://github.com/sct/overseerr/issues/1633)) ([928b8a7](https://github.com/sct/overseerr/commit/928b8a71cf361b7bc2b8957c621f5b66c4657b1e)) +- **ui:** apply pointer cursor style for clickable status badges ([#1632](https://github.com/sct/overseerr/issues/1632)) ([6968caa](https://github.com/sct/overseerr/commit/6968caa35a70c172bdd57c984fde6cb6a04a1470)) +- **ui:** remove delete button from request cards ([#1635](https://github.com/sct/overseerr/issues/1635)) ([6b37242](https://github.com/sct/overseerr/commit/6b37242a3f5a3b332d259f4814d235d751ae2491)) +- switch PGP regex to span multiple lines ([#1598](https://github.com/sct/overseerr/issues/1598)) ([d0703aa](https://github.com/sct/overseerr/commit/d0703aa37772759e8e28b5da7187e97e7aadc495)) +- **ui:** hide Plex alert after setup and add local login warning to local user modal ([#1600](https://github.com/sct/overseerr/issues/1600)) ([694d0ff](https://github.com/sct/overseerr/commit/694d0ffcf6b3e3fa00175400fa4217a7d6eb787f)) + +### Features + +- **lang:** add Greek display language ([#1605](https://github.com/sct/overseerr/issues/1605)) ([2241564](https://github.com/sct/overseerr/commit/22415642e8602809e3507e5b13dc2f8de3000003)) +- **lang:** translations update from Weblate ([#1585](https://github.com/sct/overseerr/issues/1585)) ([361ea77](https://github.com/sct/overseerr/commit/361ea77588db3dc04a51dd3a62c73ae1297cdce2)) +- **lang:** translations update from Weblate ([#1603](https://github.com/sct/overseerr/issues/1603)) ([2efa7fa](https://github.com/sct/overseerr/commit/2efa7faf20d05a5fc423e0151c6b46fe6212d096)) +- **lang:** translations update from Weblate ([#1639](https://github.com/sct/overseerr/issues/1639)) ([d22400d](https://github.com/sct/overseerr/commit/d22400dbc9320743498eeb8e6a4dcbccf1a4d52d)) +- **lang:** translations update from Weblate ([#1676](https://github.com/sct/overseerr/issues/1676)) ([8a80571](https://github.com/sct/overseerr/commit/8a805716e3e34ae8d081ad47f9d4cd68f88b0116)) +- **lang:** translations update from Weblate ([#1703](https://github.com/sct/overseerr/issues/1703)) ([6a3649f](https://github.com/sct/overseerr/commit/6a3649f620e518ff07a48c17ce1182aaedff398a)) +- **lang:** translations update from Weblate ([#1727](https://github.com/sct/overseerr/issues/1727)) ([60c3ced](https://github.com/sct/overseerr/commit/60c3ced9e2466568eecde93c88410c87ff0b796f)) +- **lang:** translations update from Weblate ([#1746](https://github.com/sct/overseerr/issues/1746)) ([37a4df6](https://github.com/sct/overseerr/commit/37a4df646cc3e3101360037f1b6f061a734eb5e2)) +- **lang:** translations update from Weblate ([#1768](https://github.com/sct/overseerr/issues/1768)) ([dedf95e](https://github.com/sct/overseerr/commit/dedf95e574a15a708866c381353e58ce3b3a1a61)) +- add display name to create local user modal ([#1631](https://github.com/sct/overseerr/issues/1631)) ([44c3edb](https://github.com/sct/overseerr/commit/44c3edb98568ba15eb525e665115429cfb15d28b)) +- allow users to select notification types ([#1512](https://github.com/sct/overseerr/issues/1512)) ([e605989](https://github.com/sct/overseerr/commit/e60598905b2d6eef7c1872d0c9e92e6d70508ae8)) +- **notif:** prevent manage-request users receiving auto-approve notif from their requests ([#1707](https://github.com/sct/overseerr/issues/1707)) ([#1709](https://github.com/sct/overseerr/issues/1709)) ([9ead8bb](https://github.com/sct/overseerr/commit/9ead8bb1f1680b522550f963502c83e2f99d1e96)) +- **plex:** add support for custom Plex Web App URLs ([#1581](https://github.com/sct/overseerr/issues/1581)) ([a640a91](https://github.com/sct/overseerr/commit/a640a91390f1411637ad379a8253002fdf60480f)) +- **pwa:** add notification badge icon ([#1695](https://github.com/sct/overseerr/issues/1695)) ([9b3b6a9](https://github.com/sct/overseerr/commit/9b3b6a9170b25209e54c74aa9e96659bc2d19edd)) +- **ui:** request list item & request card improvements ([#1532](https://github.com/sct/overseerr/issues/1532)) ([d7b9b1a](https://github.com/sct/overseerr/commit/d7b9b1a525ec6d1d81ad6fe4e55994dd8428988f)) +- **webpush:** add warning to web push settings re: HTTPS requirement ([#1599](https://github.com/sct/overseerr/issues/1599)) ([0c4fb64](https://github.com/sct/overseerr/commit/0c4fb6446be425905a120df5be9a28b052e884c0)) + +### Reverts + +- **deps:** revert back to typeorm 0.2.32 ([4368c3a](https://github.com/sct/overseerr/commit/4368c3aa4f88425ec08f3b555419e572cfa320e3)) +- **deps:** use 10.1.3 until css import issue is resolved ([2254248](https://github.com/sct/overseerr/commit/2254248abc0f2051a9dd28d9663c7ab1d0b547b6)) +- **requests:** go back to old modifiedBy request values for now ([0918b25](https://github.com/sct/overseerr/commit/0918b254132b0541999486e1f0679d0c0cd65864)) + +# [1.24.0](https://github.com/sct/overseerr/compare/v1.23.2...v1.24.0) (2021-05-05) + +### Bug Fixes + +- **api:** do not try to transform empty values passed to user notificationTypes ([ef3f977](https://github.com/sct/overseerr/commit/ef3f9778aa81f8ed39dcd835d63d94f2248e0204)), closes [#1501](https://github.com/sct/overseerr/issues/1501) +- **backend:** properly set request media status ([#1541](https://github.com/sct/overseerr/issues/1541)) ([b7b55e2](https://github.com/sct/overseerr/commit/b7b55e275cb2f1f61c3057cb8ab4cb1027f6356d)) +- **css:** don't target button globally ([#1510](https://github.com/sct/overseerr/issues/1510)) ([f78b9c1](https://github.com/sct/overseerr/commit/f78b9c1ca9648eb10b010e526d9b9db09648b154)) +- **css:** fix cog icon size on media detail pages ([#1520](https://github.com/sct/overseerr/issues/1520)) ([26ddc03](https://github.com/sct/overseerr/commit/26ddc03b2c01b343c24f1c359b78c587310cc747)) +- **email:** parse sender hostname from application URL ([#1518](https://github.com/sct/overseerr/issues/1518)) ([3baa55c](https://github.com/sct/overseerr/commit/3baa55c690dd9ba39768b8b271595cb6b09fe6da)) +- **lang:** correct overwritten email toast strings ([11a5e8d](https://github.com/sct/overseerr/commit/11a5e8d95bc2a2f16adf1e48d2ef38b508a6ace5)) +- **locale:** default user locale should be the server setting ([#1574](https://github.com/sct/overseerr/issues/1574)) ([549103f](https://github.com/sct/overseerr/commit/549103f6f6d5624201e425df7d7814f0f67863b9)) +- **pwa:** add Discover shortcut and fix/optimize icons ([#1525](https://github.com/sct/overseerr/issues/1525)) ([e1dc62b](https://github.com/sct/overseerr/commit/e1dc62b0a5b64202701aff821837ed11dd3f12db)) +- **radarr:** only process Radarr movies which are either monitored or downloaded ([#1511](https://github.com/sct/overseerr/issues/1511)) ([85899ab](https://github.com/sct/overseerr/commit/85899ab49a27542390e91443531905737224338d)) +- **ui:** add missing margins on button SVGs on Plex Settings page ([#1546](https://github.com/sct/overseerr/issues/1546)) ([5e588be](https://github.com/sct/overseerr/commit/5e588be8127b50dd83477f7f3a65f18de774e8af)) +- **ui:** add user profile links to RequestBlock and change 'ETA' string in DownloadBlock ([#1551](https://github.com/sct/overseerr/issues/1551)) ([e4d0029](https://github.com/sct/overseerr/commit/e4d0029f7b4245b8606e2447c54629def40c7761)) +- **ui:** apply rounded-l-only to SensitiveInput textareas and increase visible text input area ([#1561](https://github.com/sct/overseerr/issues/1561)) ([1123fce](https://github.com/sct/overseerr/commit/1123fce089b86251dcafebf77743d60a6e396bee)) +- **ui:** correct RegionSelector z-index ([#1567](https://github.com/sct/overseerr/issues/1567)) ([e912a00](https://github.com/sct/overseerr/commit/e912a00880f856fa9621e8587ef1cc6513a3d49c)) +- **ui:** correct toasts being in the wrong position on smaller screens ([2ecd9d7](https://github.com/sct/overseerr/commit/2ecd9d7b1391b8fc83e9c12a18bab105e7148f0f)) +- **ui:** default to text input type for SensitiveInputs ([#1568](https://github.com/sct/overseerr/issues/1568)) ([e2acf88](https://github.com/sct/overseerr/commit/e2acf8887cb0456c80308bd1b7f3bbe1930e8cff)) +- **ui:** explicitly specify width/height of Listbox dropdown icon ([#1514](https://github.com/sct/overseerr/issues/1514)) ([802e40a](https://github.com/sct/overseerr/commit/802e40a5dfa00f897f9d5a741718a319f74ff030)) +- **ui:** improve form usability ([#1563](https://github.com/sct/overseerr/issues/1563)) ([26580ea](https://github.com/sct/overseerr/commit/26580eaa218702bc5841718310e340d049c50332)) +- **ui:** show warning if user has both a default non-4K server and a non-default 4K server ([#1478](https://github.com/sct/overseerr/issues/1478)) ([4faddf3](https://github.com/sct/overseerr/commit/4faddf3810e20851c7ae1251ff0187fa13d7b0f6)) +- **webpush:** only prompt user to allow notifications if enabled in user settings ([#1552](https://github.com/sct/overseerr/issues/1552)) ([b05b177](https://github.com/sct/overseerr/commit/b05b177776a5d22bf3b5e93bad4358f4007b879a)) +- correctly fall back to English name in LanguageSelector ([#1537](https://github.com/sct/overseerr/issues/1537)) ([189313e](https://github.com/sct/overseerr/commit/189313e94a16e694d192d157642d77f664fd709b)) +- do not set locale when modifying other users ([#1499](https://github.com/sct/overseerr/issues/1499)) ([4858771](https://github.com/sct/overseerr/commit/48587719e9474139c7bbc2970b1c7d1d17b78a81)) + +### Features + +- **email:** replace 'Enable SSL' setting with more descriptive/clear 'Encryption Method' setting ([#1549](https://github.com/sct/overseerr/issues/1549)) ([69ab7cc](https://github.com/sct/overseerr/commit/69ab7cc660bea43b70bdb646eabd3866c1b5a90f)) +- **inputs:** add support for toggling security on input fields ([#1404](https://github.com/sct/overseerr/issues/1404)) ([4fd452d](https://github.com/sct/overseerr/commit/4fd452dd1880f597a0acda812d567e7cb6c16d83)) +- **lang:** translated using Weblate (Spanish) ([#1553](https://github.com/sct/overseerr/issues/1553)) ([e3d5e33](https://github.com/sct/overseerr/commit/e3d5e33ec3e43d36ec832d6ca47f330fc7675088)) +- **lang:** translations update from Weblate ([#1497](https://github.com/sct/overseerr/issues/1497)) ([9a95a07](https://github.com/sct/overseerr/commit/9a95a073916c9968b8ef348d0805d77400ea203a)) +- **lang:** translations update from Weblate ([#1527](https://github.com/sct/overseerr/issues/1527)) ([1a6d4bd](https://github.com/sct/overseerr/commit/1a6d4bddc016f4aaad83b945e103b19be4d0da31)) +- **lang:** translations update from Weblate ([#1558](https://github.com/sct/overseerr/issues/1558)) ([6c9991d](https://github.com/sct/overseerr/commit/6c9991d474a5cd95d9a0a10104bd79d8a9f3ada9)) +- **lang:** translations update from Weblate ([#1566](https://github.com/sct/overseerr/issues/1566)) ([93c441e](https://github.com/sct/overseerr/commit/93c441ef6665291ca3698368e4b093c843726036)) +- add server default locale setting ([#1536](https://github.com/sct/overseerr/issues/1536)) ([f256a44](https://github.com/sct/overseerr/commit/f256a444c57f2d92c1c4918d4ff6e223ef85ecd2)) +- **notif:** add LunaSea agent ([#1495](https://github.com/sct/overseerr/issues/1495)) ([4e6fb00](https://github.com/sct/overseerr/commit/4e6fb00a4a59545817add1544c0b1555078809a4)) +- **notif:** show success/failure toast for test notifications ([#1442](https://github.com/sct/overseerr/issues/1442)) ([079645c](https://github.com/sct/overseerr/commit/079645c2c74edfb7e4f583de2ac72bb9824f6524)) +- **perms:** add separate REQUEST_MOVIE and REQUEST_TV permissions ([#1474](https://github.com/sct/overseerr/issues/1474)) ([91b9e0f](https://github.com/sct/overseerr/commit/91b9e0f67996a442b5c0117fe09e2d69c163fafb)) +- **pwa:** add shortcuts to PWA ([#1509](https://github.com/sct/overseerr/issues/1509)) ([ed99e49](https://github.com/sct/overseerr/commit/ed99e4976dc2700fe84c70af4887c1a431bba92c)) +- add option to only allow Plex sign-in from existing users ([#1496](https://github.com/sct/overseerr/issues/1496)) ([db49b20](https://github.com/sct/overseerr/commit/db49b2024d399d90f2d1500b262374efc42f333c)) +- PWA Support ([#1488](https://github.com/sct/overseerr/issues/1488)) ([28830d4](https://github.com/sct/overseerr/commit/28830d4ef809efa92a5879a81cac11ff52ea3d1f)) + +## [1.23.2](https://github.com/sct/overseerr/compare/v1.23.1...v1.23.2) (2021-04-21) + +### Bug Fixes + +- **lang:** add missing '4K' from singular case of approve/deny 4K request strings ([#1481](https://github.com/sct/overseerr/issues/1481)) ([a822b01](https://github.com/sct/overseerr/commit/a822b019220e86e362a2570e7024289450b4ed46)) +- **ui:** change 'Disable Auto-Search' checkbox to 'Enable Automatic Search' ([#1476](https://github.com/sct/overseerr/issues/1476)) ([1a311d2](https://github.com/sct/overseerr/commit/1a311d211d78731c9089e66ed5387c1b5afe33c0)) +- better error message when creating a user with an existing email ([f13f1c9](https://github.com/sct/overseerr/commit/f13f1c94515b5bd51382fa18ad96a2ccfd06e50d)), closes [#1441](https://github.com/sct/overseerr/issues/1441) +- set editRequest attribute as necessary, allow users to edit their own pending requests, and show 'View Request' button on series pages ([#1446](https://github.com/sct/overseerr/issues/1446)) ([89455ad](https://github.com/sct/overseerr/commit/89455ad9b783d04d993a0009c351b1096f2b222e)) +- **api:** add check for 4K request perms to request creation endpoint ([#1450](https://github.com/sct/overseerr/issues/1450)) ([4449241](https://github.com/sct/overseerr/commit/4449241a8f63fdaeaa4995aa7ec34127c322b9dd)) +- **notif:** include year in notifications ([#1439](https://github.com/sct/overseerr/issues/1439)) ([4e98f56](https://github.com/sct/overseerr/commit/4e98f567534a650e26b0244990b7ca549cecbe89)) +- **plex:** add support for plex.direct URLs ([#1437](https://github.com/sct/overseerr/issues/1437)) ([db07770](https://github.com/sct/overseerr/commit/db077700e42ab1d2c870213fd55bbdee74002775)) +- **radarr:** search in addition to monitoring existing movies ([#1449](https://github.com/sct/overseerr/issues/1449)) ([3ae7d00](https://github.com/sct/overseerr/commit/3ae7d0098b225562499d7c8a74b8b6c3e8893ad9)) +- **ui:** adjust user list buttons on mobile ([#1452](https://github.com/sct/overseerr/issues/1452)) ([5d1b741](https://github.com/sct/overseerr/commit/5d1b741f55665c528e299a09464dff6d66f72666)) +- **ui:** align icons in user dropdown ([eb5d152](https://github.com/sct/overseerr/commit/eb5d1528869959cdf642e6fefc1a8f4dcf51b84e)) + +## [1.23.1](https://github.com/sct/overseerr/compare/v1.23.0...v1.23.1) (2021-04-16) + +### Bug Fixes + +- **api:** correctly check if update is available for release versions ([190cbd6](https://github.com/sct/overseerr/commit/190cbd6559c51a02ec09b267891f3033add6afc8)) + +# [1.23.0](https://github.com/sct/overseerr/compare/v1.22.0...v1.23.0) (2021-04-16) + +### Bug Fixes + +- **api:** allow server owner to delete other admin accounts ([2ac6fe7](https://github.com/sct/overseerr/commit/2ac6fe7f6d666d64228d11cde24865acc54c7ce7)) +- **backend:** do not log error when user has no server access ([#1419](https://github.com/sct/overseerr/issues/1419)) ([fc14037](https://github.com/sct/overseerr/commit/fc14037ec1c0b7450d892fa9be8176f5b9ff9d73)) +- **frontend:** add crossorigin attribute to webmanifest link ([#1376](https://github.com/sct/overseerr/issues/1376)) ([82ca2f5](https://github.com/sct/overseerr/commit/82ca2f59349407e3b1b5cd4f321e196f37044df0)) +- **frontend:** autofill with Plex server address ([#1381](https://github.com/sct/overseerr/issues/1381)) ([d9e314b](https://github.com/sct/overseerr/commit/d9e314bad295463d26d8ffe92728f3b5eee4ad05)) +- **frontend:** handle media items/requests no longer having a valid tmdb id ([b5ac2f5](https://github.com/sct/overseerr/commit/b5ac2f5a2c5dda808eca177359f125d6e03d1b0f)), closes [#517](https://github.com/sct/overseerr/issues/517) +- **lang:** remove unused strings & correct manageModalNoRequests strings ([#1413](https://github.com/sct/overseerr/issues/1413)) ([190a5c0](https://github.com/sct/overseerr/commit/190a5c0723d4aeafc4ad6103d52c2042a4eaed0e)) +- **plex:** do not use SSL for local servers ([#1418](https://github.com/sct/overseerr/issues/1418)) ([9233fc0](https://github.com/sct/overseerr/commit/9233fc078579df8a193344ba45bafb0d5c2cb9af)) +- **plex:** use server 'address' returned by Plex API ([#1379](https://github.com/sct/overseerr/issues/1379)) ([33542c9](https://github.com/sct/overseerr/commit/33542c9b2dc53b1e036a7d9571cf467c3d3dc8af)) +- **quotas:** Time value of a quota was being ignored ([d3c6bc1](https://github.com/sct/overseerr/commit/d3c6bc1619c39b1e6225d405efaad5df99a27406)) +- **ui:** allow canceling from request list & hide edit button for own requests ([#1401](https://github.com/sct/overseerr/issues/1401)) ([bed850d](https://github.com/sct/overseerr/commit/bed850dce9ad0d0b52c3c628225aea938164c38b)) +- **ui:** close sidebar on mobile when clicking version status ([ad67381](https://github.com/sct/overseerr/commit/ad673813976669797202c2cefc50274aca84989d)) +- **ui:** correctly set autocomplete attribute for password fields ([#1430](https://github.com/sct/overseerr/issues/1430)) ([4b5e355](https://github.com/sct/overseerr/commit/4b5e355df9e291a5cb550483c7dad6c43f03d3a7)) +- **ui:** dim password field when password generation option is selected ([#1427](https://github.com/sct/overseerr/issues/1427)) ([e8bbd44](https://github.com/sct/overseerr/commit/e8bbd4497a5eab6357fa7b37c9906285b3d1f64f)) +- **ui:** hide alert when email notifs are already configured ([#1335](https://github.com/sct/overseerr/issues/1335)) ([5117987](https://github.com/sct/overseerr/commit/5117987feaed21ccc19e64b04a15f2b77c22b880)) +- fall back to English genre names ([#1352](https://github.com/sct/overseerr/issues/1352)) ([e43106a](https://github.com/sct/overseerr/commit/e43106a434548840acecaf1276a5cebdc30e1345)) +- fix outofdate string & display version status badge in Settings > About ([#1417](https://github.com/sct/overseerr/issues/1417)) ([4eb9209](https://github.com/sct/overseerr/commit/4eb92098ba1f141bf74875ce76816a615763de5f)) +- various fixes for new tags feature ([#1369](https://github.com/sct/overseerr/issues/1369)) ([b4450a3](https://github.com/sct/overseerr/commit/b4450a308c56f767fbaa769d574a1b3f8e221d59)) +- **ui:** link request card status badge to Plex media URL ([#1361](https://github.com/sct/overseerr/issues/1361)) ([7a5c4a3](https://github.com/sct/overseerr/commit/7a5c4a30b5735fe6fbe821a8fcfdb4bcbeca68b3)) + +### Features + +- **lang:** Translations update from Weblate ([#1429](https://github.com/sct/overseerr/issues/1429)) ([a54241c](https://github.com/sct/overseerr/commit/a54241c775705fadc7c044f5312307f28f9a854b)) +- change alpha warning to beta warning ([03fd21b](https://github.com/sct/overseerr/commit/03fd21bebc3ffa34ce983b524d09e74b8ab2d057)) +- **lang:** translated using Weblate (Catalan) ([#1351](https://github.com/sct/overseerr/issues/1351)) ([35c13a8](https://github.com/sct/overseerr/commit/35c13a87467b4deabab3cb2cd1cab1b24ab51875)) +- **lang:** translations update from Weblate ([#1360](https://github.com/sct/overseerr/issues/1360)) ([8ee7693](https://github.com/sct/overseerr/commit/8ee7693a1f00a2f735b2555c7f8180c8a2c6144f)) +- **lang:** translations update from Weblate ([#1416](https://github.com/sct/overseerr/issues/1416)) ([dceca4d](https://github.com/sct/overseerr/commit/dceca4dd97f78f2e3aef678edcd5755c781f5249)) +- add overseerr version and update availability status to sidebar ([ecf1312](https://github.com/sct/overseerr/commit/ecf13123d21d765d67bfa7f9b6509b0f2af62cee)) +- **lang:** translations update from Weblate ([#1388](https://github.com/sct/overseerr/issues/1388)) ([9b199b2](https://github.com/sct/overseerr/commit/9b199b27d806e290cf0551e2d2ede6add61770aa)) +- **lang:** translations update from Weblate ([#1396](https://github.com/sct/overseerr/issues/1396)) ([3daf57e](https://github.com/sct/overseerr/commit/3daf57e9a12e4973dbc56656379ab2dbcb3c2619)) +- **notif:** allow users to enable/disable specific agents ([#1172](https://github.com/sct/overseerr/issues/1172)) ([46c4ee1](https://github.com/sct/overseerr/commit/46c4ee1625cf3e74bd885ecfc254b1e46cf44f29)) +- **webhook:** include requestedBy user in payload ([#1385](https://github.com/sct/overseerr/issues/1385)) ([e605687](https://github.com/sct/overseerr/commit/e60568758097d07f9d4b201ffdf34f0c32ba9cf3)) +- radarr/sonarr tag support ([#1366](https://github.com/sct/overseerr/issues/1366)) ([a306ebc](https://github.com/sct/overseerr/commit/a306ebc2d18317d8dbe4ccd3f24c22f55ffcd6a6)) + +# [1.22.0](https://github.com/sct/overseerr/compare/v1.21.1...v1.22.0) (2021-04-01) + +### Bug Fixes + +- **android:** adaptive icons for Android devices ([#1274](https://github.com/sct/overseerr/issues/1274)) ([a65e3d5](https://github.com/sct/overseerr/commit/a65e3d5bb6924cbde30b26ff8acf535e5274efee)) +- **backend:** fix getShowByTvdbId() error message ([#1314](https://github.com/sct/overseerr/issues/1314)) [skip ci] ([fe8d346](https://github.com/sct/overseerr/commit/fe8d34607b07095dce51b29ef7aaae0485573f14)) +- **db:** enable WAL journal mode ([aa205ff](https://github.com/sct/overseerr/commit/aa205ffa975d02ef0be30626e7c946a42679a847)) +- **frontend:** 'Recent Requests' slider should link to request list w/ same filter ([#1235](https://github.com/sct/overseerr/issues/1235)) ([49782c0](https://github.com/sct/overseerr/commit/49782c0b730cce9f0bad14e9c83842b5b0bfe11e)) +- **frontend:** call mutate after changing public settings ([#1302](https://github.com/sct/overseerr/issues/1302)) ([c8f67cf](https://github.com/sct/overseerr/commit/c8f67cf866ada791e4129a0bbae16b9eac41f32e)) +- **frontend:** include language parameter in TMDb links ([#1344](https://github.com/sct/overseerr/issues/1344)) ([1d88be9](https://github.com/sct/overseerr/commit/1d88be9341a8ff9e1f39b02556b489cdbd06392b)) +- **frontend:** redirect from /setup if already initialized ([#1238](https://github.com/sct/overseerr/issues/1238)) ([8016503](https://github.com/sct/overseerr/commit/80165038fd214897e3520a420f971341e7b94865)) +- **frontend:** use correct path to user profile in request modal quota dropdown ([#1307](https://github.com/sct/overseerr/issues/1307)) ([f990585](https://github.com/sct/overseerr/commit/f9905859148088afec53549b81611b07bf19d3b9)) +- **frontend:** use HTTPS to fetch TMDb assets for network/studio sliders ([#1343](https://github.com/sct/overseerr/issues/1343)) ([c886ea6](https://github.com/sct/overseerr/commit/c886ea6c0578cb7532d6c09266a76bfad8598b9d)) +- **frontend:** use next/image to serve login page images ([cbf4519](https://github.com/sct/overseerr/commit/cbf45196b023f60c8e4cf7602c0295f886fe610c)), closes [#1207](https://github.com/sct/overseerr/issues/1207) +- **lang:** allow proper localization of comma-delimited lists ([#1264](https://github.com/sct/overseerr/issues/1264)) ([173408a](https://github.com/sct/overseerr/commit/173408a1f269f09c724843ba087ef3f85b2832ad)) +- **lang:** change 'Extra Data' string to 'Additional Data' ([#1226](https://github.com/sct/overseerr/issues/1226)) ([665e164](https://github.com/sct/overseerr/commit/665e16475f3fa2ea6118340d9ea2d30b98abb238)) +- **lang:** correct mismatched language strings ([#1246](https://github.com/sct/overseerr/issues/1246)) ([8ebc829](https://github.com/sct/overseerr/commit/8ebc8292504cdc57a148ab69bcb4e1514ef018c6)) +- **lang:** correct strings for library sync button & user import toast ([#1252](https://github.com/sct/overseerr/issues/1252)) ([cb5ca7a](https://github.com/sct/overseerr/commit/cb5ca7acf38dcc2e27ec31d88434a11757cdb469)) +- **lang:** edit setting label strings for verb tense consistency ([#1214](https://github.com/sct/overseerr/issues/1214)) ([6d7671d](https://github.com/sct/overseerr/commit/6d7671dd80fea632e5cef29fc0b4968bffe231b0)) +- **lang:** fix overwritten/shared string ([#1212](https://github.com/sct/overseerr/issues/1212)) ([dfd4ff9](https://github.com/sct/overseerr/commit/dfd4ff9229822b0ce79ba322376194cbb6fd233d)) +- **lang:** remove 'requires and' ([#1215](https://github.com/sct/overseerr/issues/1215)) ([cb852fd](https://github.com/sct/overseerr/commit/cb852fded18f53806c23ec6f215385072b2a867b)) +- **lang:** remove unused strings ([#1330](https://github.com/sct/overseerr/issues/1330)) ([13e1595](https://github.com/sct/overseerr/commit/13e1595c6ebff32ca905d9bd3dd781e241545e83)) +- **lang:** UI string edits, round 2 ([#1202](https://github.com/sct/overseerr/issues/1202)) ([ea1863a](https://github.com/sct/overseerr/commit/ea1863ac3a5d3051e07815d07df0d3f2abd9166f)) +- **log:** fix typo in base scanner logging ([#1329](https://github.com/sct/overseerr/issues/1329)) [skip ci] ([b0b04ca](https://github.com/sct/overseerr/commit/b0b04ca1c7218ad5b67d9ec8b3fac5af78a4c132)) +- **logs:** add i18n strings for new log page changes ([8c51c28](https://github.com/sct/overseerr/commit/8c51c28f546b9c2d38ff7f20d59bb08a599e8146)) +- **notifications:** correctly send notifications for users that do not have any user settings yet ([d3a25b9](https://github.com/sct/overseerr/commit/d3a25b935aae35dd97ef0f168ac7e2898126a9a5)), closes [#1324](https://github.com/sct/overseerr/issues/1324) +- **overseerr-api.yml:** fixed pushbullet & webhook API definition refs and descriptions ([#1288](https://github.com/sct/overseerr/issues/1288)) [skip ci] ([3b003b7](https://github.com/sct/overseerr/commit/3b003b770120f7d150c64ff098b626015c030794)) +- **plex:** always send Overseerr for the device name to the plex.tv api ([f7146e4](https://github.com/sct/overseerr/commit/f7146e41899a59f75b963e1cc9dac9eddf24aebe)), closes [#1244](https://github.com/sct/overseerr/issues/1244) +- **ui:** add validation to hostname/IP fields ([#1206](https://github.com/sct/overseerr/issues/1206)) ([f49a024](https://github.com/sct/overseerr/commit/f49a02449c4928aef56cecbf908cf585ea0d4fca)) +- **ui:** better regex matching when parsing logs ([#1225](https://github.com/sct/overseerr/issues/1225)) ([2d737f2](https://github.com/sct/overseerr/commit/2d737f276095a8ca9abea360ef29134e9f639a39)) +- **ui:** button w/ dropdown z-indices ([#1230](https://github.com/sct/overseerr/issues/1230)) ([015671f](https://github.com/sct/overseerr/commit/015671f5be7a9f0f5c38db5a11a4b3c788dfaade)) +- **ui:** center role under title cards on person detail pages ([#1205](https://github.com/sct/overseerr/issues/1205)) ([4a61518](https://github.com/sct/overseerr/commit/4a6151873a3a3c5e45f9817131774a2c52957138)) +- **ui:** correctly enable the request button when partial requests are disabled with no quota ([16a611b](https://github.com/sct/overseerr/commit/16a611b9dfc3c66483640f4f5364646f41d37159)) +- **ui:** correctly paginate request list ([67fbb40](https://github.com/sct/overseerr/commit/67fbb401ac6ba05e58b8dfefd5954b28316254f2)) +- **ui:** correctly show quota display on tv request modal when only series quota is set ([3f1f85a](https://github.com/sct/overseerr/commit/3f1f85a80edfd2a4e9627162ff29ca6bcf2d8583)) +- **ui:** display asterisk indicator on required field labels ([#1236](https://github.com/sct/overseerr/issues/1236)) ([380d361](https://github.com/sct/overseerr/commit/380d36119f19a20ad67f79b3fb5db4036a093cac)) +- **ui:** do not check isValid on Sonarr/Radarr modals for the test button ([0974a4c](https://github.com/sct/overseerr/commit/0974a4c971358b7a64668f9a63fc356234a656c9)) +- **ui:** do not require numeric value in FormattedRelativeTime ([#1234](https://github.com/sct/overseerr/issues/1234)) ([3642b1e](https://github.com/sct/overseerr/commit/3642b1e84a20fef72428b3e240c86d35be8be8a2)) +- **ui:** filter out server options that do not match request type (non-4K or 4K) ([#1183](https://github.com/sct/overseerr/issues/1183)) ([28a6a70](https://github.com/sct/overseerr/commit/28a6a70e1ecc125f4cf4900e599ad0d4d7b55e3b)) +- **ui:** fix label formatting in general user settings ([#1275](https://github.com/sct/overseerr/issues/1275)) ([8546b0e](https://github.com/sct/overseerr/commit/8546b0ef53d232256b62cf08466e692a6971c16b)) +- **ui:** fix regex matching when parsing label from logs ([#1231](https://github.com/sct/overseerr/issues/1231)) ([4a00617](https://github.com/sct/overseerr/commit/4a00617fe47064ea50f95a02f29832a419ab13a3)) +- **ui:** gracefully handle lengthy titles & long words in overviews ([#1338](https://github.com/sct/overseerr/issues/1338)) ([d8bcb99](https://github.com/sct/overseerr/commit/d8bcb99b2fd3b24a5119ba5ff213a640425ff553)) +- **ui:** hide 'show details' button if there are no additional details ([#1254](https://github.com/sct/overseerr/issues/1254)) ([6210f12](https://github.com/sct/overseerr/commit/6210f12e8e9f593d629d22278d78310482ca0cfa)) +- **ui:** increase page size dropdown width when necessary ([#1216](https://github.com/sct/overseerr/issues/1216)) ([75c72b9](https://github.com/sct/overseerr/commit/75c72b987eb52b907ffd8af33f15ecc58213fc12)) +- **ui:** restore saved states of quota override checkboxes ([#1282](https://github.com/sct/overseerr/issues/1282)) ([2059fc1](https://github.com/sct/overseerr/commit/2059fc1cd4d48c7d80e761b7d41b7ec122d82769)) +- **ui:** sort regions & languages by their localized names rather than their TMDb English names ([#1157](https://github.com/sct/overseerr/issues/1157)) ([d76bf32](https://github.com/sct/overseerr/commit/d76bf32c9dcc83ebd0bae979726b1456a9028d8b)) +- **ui:** tweak request list design ([#1201](https://github.com/sct/overseerr/issues/1201)) ([d226fc7](https://github.com/sct/overseerr/commit/d226fc79b8d5f1263d4b80a7a1772074020ec94f)) +- **ui:** use appropriate cursor type for disabled UI elements ([#1184](https://github.com/sct/overseerr/issues/1184)) ([b767a58](https://github.com/sct/overseerr/commit/b767a58b011cc317a889cb8c2889b3210bec5fae)) +- **ui:** use appropriate cursor type for readonly input fields ([#1208](https://github.com/sct/overseerr/issues/1208)) ([9ec2c46](https://github.com/sct/overseerr/commit/9ec2c468cbbcbd41b94bbf9f3cfeb43eed09f36e)) +- **ui:** use correct colspan for 'No results.' message in Settings > Logs ([#1325](https://github.com/sct/overseerr/issues/1325)) ([5c135c9](https://github.com/sct/overseerr/commit/5c135c9974ebfcbdb434dafd459d1035624df6ed)) +- fetch localized person details from TMDb ([#1243](https://github.com/sct/overseerr/issues/1243)) ([1d7a938](https://github.com/sct/overseerr/commit/1d7a938ef8b0b8c20fda5024121de2a217ef4127)) + +### Features + +- **frontend:** add apple splash for pwa ([232def9](https://github.com/sct/overseerr/commit/232def972b9156afcbd83592708dbf8b5866ee24)) +- **frontend:** add apple tv+ to network slider ([3dc27ff](https://github.com/sct/overseerr/commit/3dc27ffd9bb054e6cda58872939dbc352877d184)), closes [#1219](https://github.com/sct/overseerr/issues/1219) +- **frontend:** allow selecting multiple original languages ([a908c07](https://github.com/sct/overseerr/commit/a908c07670532b0ca7f766065bb4653ce2376e6f)) +- **lang:** add Catalan to language picker ([#1309](https://github.com/sct/overseerr/issues/1309)) ([77911c0](https://github.com/sct/overseerr/commit/77911c03e98aa3c2c6c062a01c22b030704309c2)) +- **lang:** translations update from Weblate ([#1178](https://github.com/sct/overseerr/issues/1178)) ([3c89010](https://github.com/sct/overseerr/commit/3c89010629bc16f225f1d3936abe9f4e47a0d7c7)) +- **lang:** translations update from Weblate ([#1224](https://github.com/sct/overseerr/issues/1224)) ([c1975b3](https://github.com/sct/overseerr/commit/c1975b33f1115a95068be000b7f479a401f0f0ae)) +- **lang:** translations update from Weblate ([#1237](https://github.com/sct/overseerr/issues/1237)) ([dabd32a](https://github.com/sct/overseerr/commit/dabd32a18b42980059c7a7a7450514ca827a5d3b)) +- **lang:** translations update from Weblate ([#1256](https://github.com/sct/overseerr/issues/1256)) ([e9b1a9e](https://github.com/sct/overseerr/commit/e9b1a9e80e6b8285fa451a8551c5832a850c1746)) +- **lang:** translations update from Weblate ([#1281](https://github.com/sct/overseerr/issues/1281)) ([bec1d3d](https://github.com/sct/overseerr/commit/bec1d3dde834b9a50e24c5894c362e5982ff3bd5)) +- **lang:** translations update from Weblate ([#1305](https://github.com/sct/overseerr/issues/1305)) ([1b129c0](https://github.com/sct/overseerr/commit/1b129c0b3863ea3c5ad34c66b3ace5d09cd4e391)) +- **lang:** translations update from Weblate ([#1313](https://github.com/sct/overseerr/issues/1313)) ([18ce349](https://github.com/sct/overseerr/commit/18ce349faac6ee560b9c92374039954f2365a8d1)) +- **logs:** add copy to clipboard button to logs page ([e2b8745](https://github.com/sct/overseerr/commit/e2b8745fdc192f3d49872625652184005a760885)) +- **notif:** include requested season numbers in notifications ([#1211](https://github.com/sct/overseerr/issues/1211)) ([4ee78ab](https://github.com/sct/overseerr/commit/4ee78ab2fe0359df6baa58f0986687f05a8392a2)) +- **requests:** add request quotas ([#1277](https://github.com/sct/overseerr/issues/1277)) ([6c75c88](https://github.com/sct/overseerr/commit/6c75c8822842514ffd31864992e8d3ce686fea1b)) +- **settings:** logs viewer ([#997](https://github.com/sct/overseerr/issues/997)) ([54429bb](https://github.com/sct/overseerr/commit/54429bbc1d765d0e50486a42749f9bbd4e5b3386)) +- **ui:** add movie/series genre list pages ([#1194](https://github.com/sct/overseerr/issues/1194)) ([6f1a31d](https://github.com/sct/overseerr/commit/6f1a31de473d1a25bc77e0961a52b07050b64c51)) +- **ui:** add option to only allow complete series requests ([#1164](https://github.com/sct/overseerr/issues/1164)) ([36c00fd](https://github.com/sct/overseerr/commit/36c00fde273799a56ec42ce6177ff44fed0904c3)) +- **ui:** Add user requests page ([#936](https://github.com/sct/overseerr/issues/936)) ([a9461f7](https://github.com/sct/overseerr/commit/a9461f760d8112f2ae16183e796f706d3392f8ec)) +- **ui:** allow any value 1-100 for quota limit/days ([#1337](https://github.com/sct/overseerr/issues/1337)) ([f4bed9a](https://github.com/sct/overseerr/commit/f4bed9a63b6b856ebedca9eb7662cd00038d7f7c)) +- **ui:** display movie/series original title ([#1240](https://github.com/sct/overseerr/issues/1240)) ([7230915](https://github.com/sct/overseerr/commit/723091509414465e98d870b3dc943f41b9ac590d)) +- **ui:** experimental status bar style change for ios pwa app ([958cdf9](https://github.com/sct/overseerr/commit/958cdf98fd1cb7c1bdb33aebb6c061750e9ab331)) +- **ui:** store sort order and page size of userlist in localstorage ([#1262](https://github.com/sct/overseerr/issues/1262)) ([f5f8269](https://github.com/sct/overseerr/commit/f5f8269cd28ee792120060f4f38ef09d571fb8d5)) +- add option to cache images locally ([#1213](https://github.com/sct/overseerr/issues/1213)) ([0ca3d43](https://github.com/sct/overseerr/commit/0ca3d4374942b54b59a19d017ab4ae14ba7019c1)) +- genre sliders (experiment) ([#1182](https://github.com/sct/overseerr/issues/1182)) ([1c4515a](https://github.com/sct/overseerr/commit/1c4515a1ae6097f3948aaa0d0ed210831581fd98)) + +### Reverts + +- **ui:** remove local image cache option from settings page ([911faef](https://github.com/sct/overseerr/commit/911faeff562b737a2d18a395fcd90bf354af0cc4)) +- remove experimental tailwind jit compiler until title card hover is fixed ([1df67ba](https://github.com/sct/overseerr/commit/1df67baf9e7cdabc4045a0c115735797e8081bca)) +- **deps:** revert react-intl to 5.13.5 ([e16277c](https://github.com/sct/overseerr/commit/e16277c07d58ddbb749f4a60bc05924f4a5af146)) + +## [1.21.1](https://github.com/sct/overseerr/compare/v1.21.0...v1.21.1) (2021-03-15) + +### Bug Fixes + +- **lang:** translations update from Weblate ([#1155](https://github.com/sct/overseerr/issues/1155)) ([ebc285c](https://github.com/sct/overseerr/commit/ebc285c758f69846e4a5cb74bb42ca5924d166d4)) + +# [1.21.0](https://github.com/sct/overseerr/compare/v1.20.1...v1.21.0) (2021-03-15) + +### Bug Fixes + +- do not allow editing of user settings under certain conditions ([#1168](https://github.com/sct/overseerr/issues/1168)) ([001dcd3](https://github.com/sct/overseerr/commit/001dcd328c8d3b1c417fd7c7ee2aa20183b08eef)) +- **frontend:** check for ID instead of email after initial setup Plex login ([#1097](https://github.com/sct/overseerr/issues/1097)) ([778dda6](https://github.com/sct/overseerr/commit/778dda67d54df87347dd79577ef1bdc88d3c1d3f)) +- **frontend:** check if swr is validating to determine if we should fetch new data ([e5f5bdb](https://github.com/sct/overseerr/commit/e5f5bdb95c62eba31a3321a7457d354f0226bf85)), closes [#719](https://github.com/sct/overseerr/issues/719) +- **frontend:** never hide available content in search results ([d48edeb](https://github.com/sct/overseerr/commit/d48edeb5a9bd8e2edce8bca0fea50e300bb7a1ae)) +- **lang:** add missing i18n strings ([6072e8a](https://github.com/sct/overseerr/commit/6072e8aa9a0f84e50c44a92af303aad15b5f3021)) +- **lang:** edit new Telegram-related strings to conform to style guide ([#1093](https://github.com/sct/overseerr/issues/1093)) ([bdf67e7](https://github.com/sct/overseerr/commit/bdf67e732b6c77cbae768a25edfc9a663ef0108b)) +- **notif:** loosen input validation on Pushover settings ([#1166](https://github.com/sct/overseerr/issues/1166)) ([3148d31](https://github.com/sct/overseerr/commit/3148d312141248653c5d1e42cd2882a67a339163)) +- **notif:** set URL for Discord embeds rather than adding a field for the link ([#1167](https://github.com/sct/overseerr/issues/1167)) ([0bd0912](https://github.com/sct/overseerr/commit/0bd0912613f0db24bd0da4ec956b5119133e35d4)) +- correctly send auto-approval notifictions for series ([8634081](https://github.com/sct/overseerr/commit/8634081c869a2078793ecf06b1b7e249bba0a2f8)) +- **lang:** fix singular form of season count ([#1080](https://github.com/sct/overseerr/issues/1080)) ([b57645d](https://github.com/sct/overseerr/commit/b57645d382361c856281e7a74295afe16c5390f2)) +- **requests:** add plex url to request item ([#1088](https://github.com/sct/overseerr/issues/1088)) ([420038d](https://github.com/sct/overseerr/commit/420038d5ffdd4070df03e5c5cb6ef8d6208fddb5)) +- **sonarr:** correctly search when updating existing sonarr series ([ed0a7fb](https://github.com/sct/overseerr/commit/ed0a7fbdf5122a26fa936e83b76a97c55781782d)), closes [#588](https://github.com/sct/overseerr/issues/588) +- **ui:** add alt prop to studio/network logos & fix blinking text cursor ([#1095](https://github.com/sct/overseerr/issues/1095)) ([0c4637f](https://github.com/sct/overseerr/commit/0c4637f779d8904037b9cbd5fe9166cf05a891c5)) +- **ui:** add link to poster image on request items ([7289872](https://github.com/sct/overseerr/commit/7289872937d5bb94d027424760ee1ceb94095604)) +- **ui:** correct language usage re: "sync" vs. "scan" ([#1079](https://github.com/sct/overseerr/issues/1079)) ([e98f2b9](https://github.com/sct/overseerr/commit/e98f2b96058fb9c5af77be2e8a1bd07fb8fcca06)) +- **ui:** display "Season" vs. "Seasons" as appropriate, and fix request block "Seasons" formatting ([#1127](https://github.com/sct/overseerr/issues/1127)) ([45886cc](https://github.com/sct/overseerr/commit/45886ccef1bee57dc555060a491834567e45b59c)) +- **ui:** request list button sizes ([#1152](https://github.com/sct/overseerr/issues/1152)) ([fc73592](https://github.com/sct/overseerr/commit/fc73592b69c38191f91a68a020868b8e5ec2e2e2)) +- fix language filter link on movie detail pages ([#1142](https://github.com/sct/overseerr/issues/1142)) ([60d453b](https://github.com/sct/overseerr/commit/60d453b0bbba5e2060f72f40d1dde85ec6b05af4)) +- remove language/region filtering on studio/network results ([#1129](https://github.com/sct/overseerr/issues/1129)) ([109aca8](https://github.com/sct/overseerr/commit/109aca8229dc7b81cac314d84591f1c04c12ac2e)) +- **api:** check correct permissions for auto approve when requests are created ([3c1a72b](https://github.com/sct/overseerr/commit/3c1a72b038fd178b4be4dc082cd1496474148d7e)) +- **frontend:** status, requested by, and modified alignment fix ([#1109](https://github.com/sct/overseerr/issues/1109)) ([1a7dc1a](https://github.com/sct/overseerr/commit/1a7dc1acf57888d3d0285b58c1c97a824a232216)) +- **ui:** don't show "Password" user settings tab if current user lacks perms to modify the password ([#1063](https://github.com/sct/overseerr/issues/1063)) ([b146d11](https://github.com/sct/overseerr/commit/b146d11e2ffecedae76472b0491a4662ca4a4a4e)) +- **ui:** fix Radarr logo alignment ([#1068](https://github.com/sct/overseerr/issues/1068)) ([0fa005a](https://github.com/sct/overseerr/commit/0fa005a99cd868b5a235ae9ce65b4c64b05d0f47)) +- **ui:** fix request list UI behavior when season list is too long ([#1106](https://github.com/sct/overseerr/issues/1106)) ([8507691](https://github.com/sct/overseerr/commit/85076919c6ccbf052699b7d5f4ba8b6e5e5af74d)) +- **ui:** improve responsive design on new request list UI ([#1105](https://github.com/sct/overseerr/issues/1105)) ([1f8b03f](https://github.com/sct/overseerr/commit/1f8b03ff6f67ce76051667de05166da54ed3dc89)) +- **ui:** list all movie studios instead of just the first result ([#1110](https://github.com/sct/overseerr/issues/1110)) ([239202d](https://github.com/sct/overseerr/commit/239202d9c11f27410b0fa084bcc4c824b7136081)) +- add correct permission checks to modifying user password/permissions ([ddfc5e6](https://github.com/sct/overseerr/commit/ddfc5e6aa8fc636931f495d6f23d56367466e3b5)) + +### Features + +- add tagline, episode runtime, genres list to media details & clean/refactor CSS into globals ([#1160](https://github.com/sct/overseerr/issues/1160)) ([2f2e002](https://github.com/sct/overseerr/commit/2f2e00237d43bdab85bfadc3c4f2fbcdde4c2e90)) +- **docker:** add tini to docker image ([#1017](https://github.com/sct/overseerr/issues/1017)) ([1629d02](https://github.com/sct/overseerr/commit/1629d02f3d8368bfd5f6fed05382974ae6fce51f)) +- **email:** add pgp support ([#1138](https://github.com/sct/overseerr/issues/1138)) ([9e5adeb](https://github.com/sct/overseerr/commit/9e5adeb610bdc4800ff536412d0ae8a11fb4338d)) +- **frontend:** add loading bar indicator ([#1170](https://github.com/sct/overseerr/issues/1170)) ([3d6b343](https://github.com/sct/overseerr/commit/3d6b3434138fec49c58f2bf74f781d5e2fc2911f)) +- **lang:** localize job names ([#1043](https://github.com/sct/overseerr/issues/1043)) ([594aad9](https://github.com/sct/overseerr/commit/594aad9d3ae9b323677f3af8c434d7664526593d)) +- **lang:** translations update from Weblate ([#1051](https://github.com/sct/overseerr/issues/1051)) ([69bf817](https://github.com/sct/overseerr/commit/69bf817f598babed99964f073259f827b60bd014)) +- **lang:** Translations update from Weblate ([#1131](https://github.com/sct/overseerr/issues/1131)) ([e4686d6](https://github.com/sct/overseerr/commit/e4686d664b52448e32488ff1c4236f72e01e9a29)) +- **notif:** add "Media Automatically Approved" notification type ([#1137](https://github.com/sct/overseerr/issues/1137)) ([f7d2723](https://github.com/sct/overseerr/commit/f7d2723fab2c30564fd23945709cd39b178a6eef)) +- **notif:** add settings for Discord bot username & avatar URL ([#1113](https://github.com/sct/overseerr/issues/1113)) ([3384eb1](https://github.com/sct/overseerr/commit/3384eb1c479114c0246cb22f9a933aa79fb95fcf)) +- **notif:** include poster image in Telegram notifications ([#1112](https://github.com/sct/overseerr/issues/1112)) ([48387e5](https://github.com/sct/overseerr/commit/48387e5b2f26c0c33acd436c6e1cf902d6c32101)) +- **scan:** add support for new plex tv agent ([#1144](https://github.com/sct/overseerr/issues/1144)) ([a51d2a2](https://github.com/sct/overseerr/commit/a51d2a24d51d092a0c6da608e3322f19a37c2d28)) +- **ui:** add user ID to profile header ([6e95c8b](https://github.com/sct/overseerr/commit/6e95c8b7a10e3467bfd2c3df84ccf886fe01ca5c)) +- add genre/studio/network view to Discover results ([#1067](https://github.com/sct/overseerr/issues/1067)) ([f28112f](https://github.com/sct/overseerr/commit/f28112f057df2589f31ae0d0b14e8b50e479fdb7)) +- add language-filtered Discover pages ([#1111](https://github.com/sct/overseerr/issues/1111)) ([7501161](https://github.com/sct/overseerr/commit/75011610e57f03098c8be9375d0c9ba1e3647e9b)) +- add studio/network sliders to discover ([1c6914f](https://github.com/sct/overseerr/commit/1c6914f5ce5c0d171c4609813915b50233a8e3ad)) +- **telegram:** add support for individual chat notifications ([#1027](https://github.com/sct/overseerr/issues/1027)) ([f6d00d8](https://github.com/sct/overseerr/commit/f6d00d8d1559879189f83739193c6e2acafde51d)) +- **ui:** display "Owner" role instead of "Admin" for user ID 1 ([#1050](https://github.com/sct/overseerr/issues/1050)) ([1b55d2d](https://github.com/sct/overseerr/commit/1b55d2dfbc06d900e7370a4ddfd81789a25bf00c)) +- **ui:** display season count on TV details page ([#1078](https://github.com/sct/overseerr/issues/1078)) ([4365231](https://github.com/sct/overseerr/commit/436523139e8f1594c352b17032734b4498d3994f)) +- **ui:** in Settings > Services, make Radarr/Sonarr server names and logos clickable links ([#1008](https://github.com/sct/overseerr/issues/1008)) ([6a1e389](https://github.com/sct/overseerr/commit/6a1e3891aa5f84b6adb1e475a6658a8cd4e34c22)) +- **ui:** request list redesign ([#1099](https://github.com/sct/overseerr/issues/1099)) ([cd21865](https://github.com/sct/overseerr/commit/cd21865c4d5be00c13c372e0b7a058f61ec855a2)) + +## [1.20.1](https://github.com/sct/overseerr/compare/v1.20.0...v1.20.1) (2021-02-28) + +### Bug Fixes + +- **notif:** escape application title in Telegram notifications ([#1012](https://github.com/sct/overseerr/issues/1012)) ([5560abf](https://github.com/sct/overseerr/commit/5560abf459b0350ff30b5e71d4208418fc8f3b3e)) +- **notif:** fixed typo in pushover hint ([#1029](https://github.com/sct/overseerr/issues/1029)) ([e9f2fe9](https://github.com/sct/overseerr/commit/e9f2fe910d72fa41bc27673ed43291211c3cac65)) +- **notifications:** correctly send tv auto approval notifications ([537850f](https://github.com/sct/overseerr/commit/537850f414a88df24c78794a2fd68e1e24ff73d1)), closes [#1041](https://github.com/sct/overseerr/issues/1041) +- **plex-sync:** no longer incorrectly sets 4k availability when there isnt any ([3f9a116](https://github.com/sct/overseerr/commit/3f9a116b17d78eeb04f0f125a4f3af6f907c83dd)), closes [#990](https://github.com/sct/overseerr/issues/990) +- **ui:** for server default options, display "All" region/language option instead of empty string ([#1042](https://github.com/sct/overseerr/issues/1042)) ([3fed26c](https://github.com/sct/overseerr/commit/3fed26cfbe74cb662ca531fd37b69f159a051ac1)) +- **ui:** show translated string on sonarr sucesss/failure toast messages ([#1035](https://github.com/sct/overseerr/issues/1035)) ([eefcbcd](https://github.com/sct/overseerr/commit/eefcbcd3ddfa5258ee24dbbbd79de5bf50310f27)) +- **ui:** use country-flag-icons instead of country-flag-emoji for RegionSelector ([#1011](https://github.com/sct/overseerr/issues/1011)) ([abcd7c9](https://github.com/sct/overseerr/commit/abcd7c997584c1310bd8b313ac38f30e335af8d7)) +- add missing default value for settings context ([084917f](https://github.com/sct/overseerr/commit/084917f02d399e2d29bb9927e033c2e6533f586c)) +- added missing language default for ssr context defaults ([9ce88ab](https://github.com/sct/overseerr/commit/9ce88abcc85d744d77172cd2357fdb4ff60dc5e4)) +- allow users to override language/region settings ([69294a7](https://github.com/sct/overseerr/commit/69294a7c4c5bbe55c5cd276786cdfd48ddbff889)), closes [#1013](https://github.com/sct/overseerr/issues/1013) + +# [1.20.0](https://github.com/sct/overseerr/compare/v1.19.1...v1.20.0) (2021-02-23) + +### Bug Fixes + +- **api:** add isAuthenticated middleware to base user route ([8a27c70](https://github.com/sct/overseerr/commit/8a27c7062599ea23dca115e6e6e95a594e1b219a)) +- **api:** sort users requests by most recent ([1798383](https://github.com/sct/overseerr/commit/17983837fc10661a59d29fc1531530fca0d77825)) +- **api:** Use POST instead of GET for API endpoints that mutate state ([#877](https://github.com/sct/overseerr/issues/877)) ([ff0b5ed](https://github.com/sct/overseerr/commit/ff0b5ed44132cc5a0cd178035796d042ba735a8d)) +- **auth:** handle sign-in attempts from emails with no password ([#933](https://github.com/sct/overseerr/issues/933)) ([5e37a96](https://github.com/sct/overseerr/commit/5e37a96bc017471f8dc4cbdd57f2e8c3568bd97f)) +- **frontend:** changed plex, request, and cog buttons to align properly on smaller mobile UIs ([#928](https://github.com/sct/overseerr/issues/928)) ([f1c3358](https://github.com/sct/overseerr/commit/f1c335815f2f17465cdd36ceb223e78a58149b3b)) +- **frontend:** check for id instead of email after logging in ([c4af4c4](https://github.com/sct/overseerr/commit/c4af4c42ab00f1a63a2f5326c9cd8b26c19f4f14)) +- **frontend:** Do not allow user w/ ID 1 to disable 'Admin' permission ([#965](https://github.com/sct/overseerr/issues/965)) ([77b2d9e](https://github.com/sct/overseerr/commit/77b2d9ea22a2f70cff58ac9421f3f6231bc93059)) +- **frontend:** handle empty array of media attributes ([#922](https://github.com/sct/overseerr/issues/922)) ([04fa9f7](https://github.com/sct/overseerr/commit/04fa9f79e2ec90082b3fa15590dd170f7d68ad52)) +- **frontend:** request and cog button would be misaligned without play on plex/watch trailer button ([#956](https://github.com/sct/overseerr/issues/956)) ([e28dfad](https://github.com/sct/overseerr/commit/e28dfadaf57d47887013c31dc5006332473156e3)) +- **frontend:** Update AdvancedRequester to reflect new /user API response ([#970](https://github.com/sct/overseerr/issues/970)) ([b4bac6a](https://github.com/sct/overseerr/commit/b4bac6a9157119a4f234933245944e133c127bd0)) +- **frontend:** use region settings instead of hardcoded 'US' value for movie/TV ratings ([#1006](https://github.com/sct/overseerr/issues/1006)) ([6ecd202](https://github.com/sct/overseerr/commit/6ecd202607cb48d559440da810ecc585e740542b)) +- **lang:** formatMessage should not use an object spread ([8a7fa00](https://github.com/sct/overseerr/commit/8a7fa00164fd5c5501da525baa29be97bac7e7c4)) +- **lang:** Remove unused strings and correct spelling of 'canceling'/'canceled' ([#981](https://github.com/sct/overseerr/issues/981)) ([5b64655](https://github.com/sct/overseerr/commit/5b646557765d1ad75e44e1c0e60e0291313c7746)) +- **login:** fix the gap when 'use your overseer account' was selected ([#870](https://github.com/sct/overseerr/issues/870)) ([d163e29](https://github.com/sct/overseerr/commit/d163e294599c4bd9bdc0a148db15c8e8541410d8)) +- **notif:** Do not HTML-escape email subjects ([#931](https://github.com/sct/overseerr/issues/931)) ([019622a](https://github.com/sct/overseerr/commit/019622aab1b94cc4d71cacbf0dc5cf64b62c8623)) +- **notif:** Remove extra newlines from Telegram notifications ([#973](https://github.com/sct/overseerr/issues/973)) ([bbea522](https://github.com/sct/overseerr/commit/bbea52249950eb98a8d3886f2bd7648a7d669bf4)) +- **plex:** Check Plex server access on user import ([#955](https://github.com/sct/overseerr/issues/955)) ([bdb3cb2](https://github.com/sct/overseerr/commit/bdb3cb202550e34d8951ac2b5015f97f6a5c1ebf)) +- **plex-sync:** get correct Plex metadata for Hama movie items ([#901](https://github.com/sct/overseerr/issues/901)) ([03cecb3](https://github.com/sct/overseerr/commit/03cecb33559e27199c5a174fc86de0c4550fe666)), closes [#898](https://github.com/sct/overseerr/issues/898) +- **requests:** correctly filter requests out for users without view requests permission ([e118501](https://github.com/sct/overseerr/commit/e118501bf1dfa8dada2c57090e62631de620f3dd)) +- **requests:** correctly handle when tvdbid is missing ([#891](https://github.com/sct/overseerr/issues/891)) ([e037ba4](https://github.com/sct/overseerr/commit/e037ba48f173c06b0c9c8b03085edf832d770c06)) +- **search:** Handle search errors and escape \* ([#893](https://github.com/sct/overseerr/issues/893)) ([034968e](https://github.com/sct/overseerr/commit/034968e4370eaea726c94730274349c083856813)) +- **services:** update all radarr/sonarr endpoints to use v3 ([da5ca02](https://github.com/sct/overseerr/commit/da5ca02f81fe91070afbda3e1ebc8d869fe39a8f)) +- **sonarr:** use qualityProfileId instad of profileId when adding series ([552a7e3](https://github.com/sct/overseerr/commit/552a7e30da5fc2cc0bd43b5aef79a0225c75d233)) +- **sync:** fix sonarr/plex sync fighting over availability ([9b73423](https://github.com/sct/overseerr/commit/9b73423d49e1e799cd82764a9ade8c75d92a28a2)), closes [#872](https://github.com/sct/overseerr/issues/872) +- **ui:** add fallback for region display name ([f9c83e1](https://github.com/sct/overseerr/commit/f9c83e14e52a57d6865307b3324a61c04a77a541)) +- **ui:** add missing string for default Discover Language & edit string for default Discover Region ([#1004](https://github.com/sct/overseerr/issues/1004)) ([0acad8e](https://github.com/sct/overseerr/commit/0acad8e9fa65a9de6cecac9b6a4a5b2313ba8f06)) +- **ui:** Add tip & validation for Discord ID input ([#966](https://github.com/sct/overseerr/issues/966)) ([e70a4ec](https://github.com/sct/overseerr/commit/e70a4ecae613e045977e262fd7f9643f30985ab7)) +- **ui:** also allow 17 digit discord ids ([57c00c1](https://github.com/sct/overseerr/commit/57c00c1ea71c1229d5a59e1b8dadd84a646772b9)), closes [#971](https://github.com/sct/overseerr/issues/971) +- **ui:** Automatically disable and uncheck user permissions with unmet requirements ([#941](https://github.com/sct/overseerr/issues/941)) ([c9a150b](https://github.com/sct/overseerr/commit/c9a150b1db2adbb305cf1a448489d7a8c14cf1cb)) +- **ui:** change font size in request list/user list dropdowns to prevent zoom on mobile ([fb9c878](https://github.com/sct/overseerr/commit/fb9c878db49c01d13773e8d2f94c93f840be0b82)) +- **ui:** Display 4K download status on 4K status badge ([#988](https://github.com/sct/overseerr/issues/988)) ([40b07c3](https://github.com/sct/overseerr/commit/40b07c35d40c03039e4bfa5ed1e73af7e8aa6a7d)) +- **ui:** Fix card sizes on person detail pages ([#881](https://github.com/sct/overseerr/issues/881)) ([a3042f8](https://github.com/sct/overseerr/commit/a3042f8e1b05a91d98f48a4aecb08e831a48fc56)) +- **ui:** Fix settings navigation horizontal scroll issues ([#987](https://github.com/sct/overseerr/issues/987)) ([8701fb2](https://github.com/sct/overseerr/commit/8701fb20d07773f4cc32e857b68575a813cf7e21)) +- **ui:** fix webhook URL validation regex ([baad19a](https://github.com/sct/overseerr/commit/baad19a2c94728313ee996fe1a0ffc64fbd9aaa3)) +- **ui:** fixed anime language profile typo ([#879](https://github.com/sct/overseerr/issues/879)) ([ee50761](https://github.com/sct/overseerr/commit/ee5076146ef3c5e8baba197a5b397d3c3f575262)) +- **ui:** Handle missing movie/series data ([#862](https://github.com/sct/overseerr/issues/862)) ([7c0ddad](https://github.com/sct/overseerr/commit/7c0ddad653393327226a877692f046d8693ddc66)) +- **ui:** Notification-related string/UI edits and field validation ([#985](https://github.com/sct/overseerr/issues/985)) ([c88fcb2](https://github.com/sct/overseerr/commit/c88fcb2e2d1c4b84527844a80680c15337626e72)) +- **ui:** rename global group class to form-group ([8056187](https://github.com/sct/overseerr/commit/8056187c3c0ea464a8f751aa6347ea1d35c01aac)) +- **ui:** Size cards appropriately based on base font size ([#871](https://github.com/sct/overseerr/issues/871)) ([282f28f](https://github.com/sct/overseerr/commit/282f28f2b9d0cc8c9105d01b43d4e1f730320b8b)) +- **ui/notif:** Custom application title in password-related emails and UI messages ([#979](https://github.com/sct/overseerr/issues/979)) ([4e2706b](https://github.com/sct/overseerr/commit/4e2706b4211b06f364910c327d84c2ceb45b2fe3)) + +### Features + +- **lang:** translated using Weblate (French) ([#1007](https://github.com/sct/overseerr/issues/1007)) ([970da66](https://github.com/sct/overseerr/commit/970da664b2700b8cd9ad8dce0cbca1d37820eceb)) +- **lang:** translations update from Weblate ([#853](https://github.com/sct/overseerr/issues/853)) ([e156acc](https://github.com/sct/overseerr/commit/e156acc1ae2fa86b4441faacc0b58e1e993e0edc)) +- **lang:** translations update from Weblate ([#986](https://github.com/sct/overseerr/issues/986)) ([4296765](https://github.com/sct/overseerr/commit/4296765ad61bac09c2317b71b763366d328733e4)) +- **notif:** Add Pushbullet notification agent ([#950](https://github.com/sct/overseerr/issues/950)) ([29b97ef](https://github.com/sct/overseerr/commit/29b97ef6d85bbea31dd59b7ad857b0d8ab30bff0)) +- **notif:** Notification improvements ([#914](https://github.com/sct/overseerr/issues/914)) ([2768155](https://github.com/sct/overseerr/commit/2768155bbabe121a4c51fc1472461cd5114c4300)) +- **regions:** add region/original language setting for filtering Discover ([#732](https://github.com/sct/overseerr/issues/732)) ([#942](https://github.com/sct/overseerr/issues/942)) ([b557c06](https://github.com/sct/overseerr/commit/b557c06b0a78f5df5f64a05dc1e4511dae72df4f)) +- **requests:** add language profile support ([#860](https://github.com/sct/overseerr/issues/860)) ([53f6f59](https://github.com/sct/overseerr/commit/53f6f59798fa7e3f95959990a3df555db3c1c51e)) +- **ui:** Add 'Available' filter to request list and remove unused MediaRequestStatus.AVAILABLE enum value ([#905](https://github.com/sct/overseerr/issues/905)) ([9757e3a](https://github.com/sct/overseerr/commit/9757e3ae0c572fb46177e25154b29e0ceced665f)) +- **ui:** Add 'Page Size' setting for request/user list pages ([#957](https://github.com/sct/overseerr/issues/957)) ([621db89](https://github.com/sct/overseerr/commit/621db893281f0280fe773ac7dbdc44434895242c)) +- **ui:** Add separate permissions for 4K auto approval ([#908](https://github.com/sct/overseerr/issues/908)) ([53b7425](https://github.com/sct/overseerr/commit/53b7425f6711e250935e7bb024c38ff6c62e07d9)) +- **ui:** Add sort options to user list ([#913](https://github.com/sct/overseerr/issues/913)) ([ef5d019](https://github.com/sct/overseerr/commit/ef5d019c18d7f6cdbbb1e1b7f8ff7816ed9b117b)) +- **ui:** Add support for requesting collections in 4K ([#968](https://github.com/sct/overseerr/issues/968)) ([139341b](https://github.com/sct/overseerr/commit/139341b0434b41e7c31af36baacd8d65566a6a0c)) +- user profile/settings pages ([#958](https://github.com/sct/overseerr/issues/958)) ([bbb683e](https://github.com/sct/overseerr/commit/bbb683e637386ad8bbeb44dca97aac9cdaf11349)) +- **ui:** added content ratings for tv shows and movie ratings ([#878](https://github.com/sct/overseerr/issues/878)) ([c8b2a57](https://github.com/sct/overseerr/commit/c8b2a57721a51adcc7f90ec1acb48b127991d467)) +- **users:** add reset password flow ([#772](https://github.com/sct/overseerr/issues/772)) ([e5966bd](https://github.com/sct/overseerr/commit/e5966bd3fbfe172f264f4e986ad2aecf29ae1510)) + +## [1.19.1](https://github.com/sct/overseerr/compare/v1.19.0...v1.19.1) (2021-02-06) + +### Bug Fixes + +- **ui:** Fix webhook URL validation regex ([#864](https://github.com/sct/overseerr/issues/864)) ([726f62b](https://github.com/sct/overseerr/commit/726f62b9b69b5078e718f129e26abdf358f5cb06)) + +# [1.19.0](https://github.com/sct/overseerr/compare/v1.18.0...v1.19.0) (2021-02-05) + +### Bug Fixes + +- **api:** filter out adult content from combined credits ([3052f12](https://github.com/sct/overseerr/commit/3052f12c91b3ce86128324e3698fff61bbce3f2a)) +- **cache:** use formatted numbers for displaying cache counts ([6c437c5](https://github.com/sct/overseerr/commit/6c437c515fc01b9fe4461968875e23542bae7542)) +- **email:** make image a link to the action url in request template ([ee0a7bd](https://github.com/sct/overseerr/commit/ee0a7bd8c0b3a79c292b0abceb2f780f3889e49f)), closes [#834](https://github.com/sct/overseerr/issues/834) +- **frontend:** add github sponsor link to about page ([7c192d5](https://github.com/sct/overseerr/commit/7c192d54f422a5f2b55750535d2382e313f1d011)) +- **frontend:** correctly show 4k download tracker activity ([a7314f8](https://github.com/sct/overseerr/commit/a7314f876ea528fdec0fb0a2adaa36a01afcdf38)) +- **frontend:** fix possible division by zero in download status ([#839](https://github.com/sct/overseerr/issues/839)) ([c97c96a](https://github.com/sct/overseerr/commit/c97c96a30c50db7735f06c6d2d2f6193fb7da55e)) +- **frontend:** match request button color on titlecards to other request buttons ([5b39911](https://github.com/sct/overseerr/commit/5b39911e024513fab7a62948e653cee08fd166c7)) +- **frontend:** set 4k status on RequestItem when request is for 4k ([a3b00c3](https://github.com/sct/overseerr/commit/a3b00c3458b868506d4158fb24f0369fa5daefc5)) +- **frontend:** use consistent spinner style on TitleCard/Plex Presets ([cf7ebc4](https://github.com/sct/overseerr/commit/cf7ebc488db33725444c428b4244d780ab9d123b)) +- **html:** th elements should be nested under tr, not directly under thead ([#801](https://github.com/sct/overseerr/issues/801)) ([6e9ac27](https://github.com/sct/overseerr/commit/6e9ac275e19d56de8c7a366db970c7321f26fc8a)) +- **lang:** Add missing source strings & remove local user sign-in setting tip ([#828](https://github.com/sct/overseerr/issues/828)) ([c0769d4](https://github.com/sct/overseerr/commit/c0769d4f8f2bad88e4638d8c3cbcc0414b3ef6fb)) +- **lang:** Edit English language strings ([#820](https://github.com/sct/overseerr/issues/820)) ([f54df21](https://github.com/sct/overseerr/commit/f54df214af86d90ea8d7cfcd4e39022215c3568c)) +- **lang:** translate language names & change zh-Hant language code to zh-TW ([#793](https://github.com/sct/overseerr/issues/793)) ([3c5ae36](https://github.com/sct/overseerr/commit/3c5ae360fd179d794a78cc918fe97a09216ca6b2)) +- **notif/ui:** Use custom application title in notifications & sign-in page ([#849](https://github.com/sct/overseerr/issues/849)) ([38c76b5](https://github.com/sct/overseerr/commit/38c76b55e0039c489cb6a4a0a298aa6385406db4)) +- **radarr:** correctly set requested status after sending to radarr (with auto approve) ([ec44841](https://github.com/sct/overseerr/commit/ec448413569ddc2f24bb856d29084169979f9f05)) +- **sonarr-sync:** sonarr sync will no longer set shows with no episodes to partially available ([d20bd53](https://github.com/sct/overseerr/commit/d20bd530edaadc5887b0361358da80153e36505c)), closes [#796](https://github.com/sct/overseerr/issues/796) +- **ui:** Add additional URL & email input validation ([#843](https://github.com/sct/overseerr/issues/843)) ([3f9bfeb](https://github.com/sct/overseerr/commit/3f9bfeb01a67b2b587c7548b02ee826722e65c0f)) +- **ui:** Don't display empty dropdown when no trailer available ([#804](https://github.com/sct/overseerr/issues/804)) ([95c2a21](https://github.com/sct/overseerr/commit/95c2a2169799d96413b47ab24506b330435643eb)) +- **ui:** dont show bulk edit options on user list if there is only one user ([b658ddf](https://github.com/sct/overseerr/commit/b658ddf5cf61b2bb9b93cb1a4ca716cd75e18bb4)) +- **ui:** Dynamically generate path to config in warning message ([#851](https://github.com/sct/overseerr/issues/851)) ([b531a64](https://github.com/sct/overseerr/commit/b531a642f601f4ef9bf39c2f5915402157e55372)) +- **ui:** fix tables extending outside viewport in mobile formats ([e270999](https://github.com/sct/overseerr/commit/e270999745f97c2860f6a5b84e897dc6da8d6001)) +- **ui:** Hide 'Mark 4k as Available' button if 4k not enabled ([#833](https://github.com/sct/overseerr/issues/833)) ([e4a50c3](https://github.com/sct/overseerr/commit/e4a50c33f105b440243885d72a9e96595a525447)) +- **ui:** Limit max width of forms & lists ([#845](https://github.com/sct/overseerr/issues/845)) ([b9d14a9](https://github.com/sct/overseerr/commit/b9d14a9fd0f3c94d8267755147a87fe3b77fa2c3)) +- **ui:** prevent names from getting squished in AdvancedRequester user selector ([06e9411](https://github.com/sct/overseerr/commit/06e941171a1d019fbb178624167c026f6df5271c)) +- **ui:** remove yup validation from display name on user edit page ([63d7e2b](https://github.com/sct/overseerr/commit/63d7e2b39858fcb1cc0819a680eebccded7f4451)) +- **ui:** Restore original port input size ([#814](https://github.com/sct/overseerr/issues/814)) ([1ccafc0](https://github.com/sct/overseerr/commit/1ccafc0ebd368d798f9571b83910336efa317e37)) +- **ui:** show request as option even if there are no radarr/sonarr servers ([b116281](https://github.com/sct/overseerr/commit/b116281196c264b4ec35b07f1b4ffa717e50ade5)) +- **ui:** uniform-size checkboxes, vertically-aligned form labels, and fixes for other UI imperfections/inconsistencies ([#737](https://github.com/sct/overseerr/issues/737)) ([e34fbf7](https://github.com/sct/overseerr/commit/e34fbf72fda34d69b9f25563fa81f88b3c20912a)) +- **ui:** Use minimum char validation message ([#850](https://github.com/sct/overseerr/issues/850)) ([7456bea](https://github.com/sct/overseerr/commit/7456bea2ae600a28cb933278ffb310b63a474d6a)) +- **ui:** validate application url and service external urls ([026795d](https://github.com/sct/overseerr/commit/026795d4c940cb4797d3e68089456a4c3defbb21)) +- **ui:** when PersonCard has no profilePath, correctly position name/role content ([3ffd5ab](https://github.com/sct/overseerr/commit/3ffd5ab0ee8ffa63199d1428e37206f9b59fb7a5)) + +### Features + +- **cache:** add cache table and flush cache option to settings ([996bd9f](https://github.com/sct/overseerr/commit/996bd9f14ed0f56767892c169b071be4f0f628d0)) +- **cache:** external API cache ([#786](https://github.com/sct/overseerr/issues/786)) ([20289b5](https://github.com/sct/overseerr/commit/20289b5960a93545cdff9331a1a7b613f382e702)) +- **docker:** Check for /app/config volume mount during setup ([#826](https://github.com/sct/overseerr/issues/826)) ([1e5f88f](https://github.com/sct/overseerr/commit/1e5f88f462b0c69db5f6ab8e0249a5905bc6952a)) +- **frontend:** add TheTVDB external link ([#800](https://github.com/sct/overseerr/issues/800)) ([72cffd7](https://github.com/sct/overseerr/commit/72cffd74a75984ba98c456c0ec006ec378a8dcec)) +- **lang:** add support for Hungarian language ([cfacb15](https://github.com/sct/overseerr/commit/cfacb151b52d08e19d2fcd603fb4bbcd78707cdf)) +- **lang:** translations update from Weblate ([#791](https://github.com/sct/overseerr/issues/791)) ([42295e0](https://github.com/sct/overseerr/commit/42295e076a7579b226d57407a20cb0ba044e9ec1)) +- **lang:** translations update from Weblate ([#819](https://github.com/sct/overseerr/issues/819)) ([9e5e4c2](https://github.com/sct/overseerr/commit/9e5e4c22f5b25df96f47875d599ed8685791382a)) +- **lang:** translations update from Weblate ([#841](https://github.com/sct/overseerr/issues/841)) ([e4f9b8a](https://github.com/sct/overseerr/commit/e4f9b8a9848f3af00e86fc7108c823ed0584609f)) +- **lang:** translations update from Weblate ([#852](https://github.com/sct/overseerr/issues/852)) ([c5be00e](https://github.com/sct/overseerr/commit/c5be00eebfd2b0e65295edbe282cbba22fffa660)) +- **ui:** Add local login setting ([#817](https://github.com/sct/overseerr/issues/817)) ([9d0d5b8](https://github.com/sct/overseerr/commit/9d0d5b86aae025e4647bb664c6412d42192e2fe7)) +- **ui:** added next airing date to TV Shows ([#842](https://github.com/sct/overseerr/issues/842)) ([4eae02a](https://github.com/sct/overseerr/commit/4eae02a7e14e377fd69ddd4a43774cb7e3d1855b)) +- new permission to allow users to see other users requests ([033ba9d](https://github.com/sct/overseerr/commit/033ba9d41bddf6dc1c4512d8404f747e57923bca)), closes [#840](https://github.com/sct/overseerr/issues/840) +- request as another user ([59150f9](https://github.com/sct/overseerr/commit/59150f955f7003672ef19eb9d37156e93b79c97d)) +- **tv:** show cast for the entire show instead of only the last season ([#778](https://github.com/sct/overseerr/issues/778)) ([b239598](https://github.com/sct/overseerr/commit/b239598e64d33b78dc5d7972878840149aff360a)), closes [#775](https://github.com/sct/overseerr/issues/775) +- **ui:** Add custom title functionality ([#825](https://github.com/sct/overseerr/issues/825)) ([35c6bfc](https://github.com/sct/overseerr/commit/35c6bfc0216bf879353b3ee546b439a06c8e6121)) + +# [1.18.0](https://github.com/sct/overseerr/compare/v1.17.2...v1.18.0) (2021-01-30) + +### Bug Fixes + +- **api:** prevent duplicate movie requests ([421f4c1](https://github.com/sct/overseerr/commit/421f4c17f0f206bbe7bfcbf2819014b8c7f55b6a)), closes [#705](https://github.com/sct/overseerr/issues/705) +- **build:** fix sqlite3 build error ([#691](https://github.com/sct/overseerr/issues/691)) ([3a1f6d5](https://github.com/sct/overseerr/commit/3a1f6d5706c8fc100e88425f3d89a26a0325af79)) +- **frontend:** add poster not found image to request card and request list item ([ae9a1b3](https://github.com/sct/overseerr/commit/ae9a1b3e940ac2abf6e842d91f458daab3dd0f0d)) +- **frontend:** add poster not found image to tv details page ([0b05545](https://github.com/sct/overseerr/commit/0b055458d0ddbfd4c87ebf9b0562f161fa3445a3)) +- **frontend:** dont show external links unless slug is set ([946bd2d](https://github.com/sct/overseerr/commit/946bd2db5ecde0748b2e9bc5edfe7ca6000ec3d5)) +- **frontend:** fix server name position on plex settings page ([86efcd8](https://github.com/sct/overseerr/commit/86efcd82c34ad6490f2899ebf6f84cdd1bffc498)) +- **frontend:** fixed mismatched rounded sizing on new login ([5e352c2](https://github.com/sct/overseerr/commit/5e352c201fc2f731ca5f713ecb6901527ef354da)), closes [#721](https://github.com/sct/overseerr/issues/721) +- **ip logging:** add env var for proxy to fix ip logging on failed logins ([#756](https://github.com/sct/overseerr/issues/756)) ([9342a40](https://github.com/sct/overseerr/commit/9342a40bbc03f7fdda23e3876b3a4a81ea8532c0)) +- **lang:** add missing i18n strings for notification settings ([2f75c4c](https://github.com/sct/overseerr/commit/2f75c4c6aed42a15bb47d3652272de8f852ec79f)) +- **notifications:** only send a single notification when standard media becomes available ([b5fd1d5](https://github.com/sct/overseerr/commit/b5fd1d520cd2a7be6e6356a25129e93af1caf542)), closes [#770](https://github.com/sct/overseerr/issues/770) +- **permissions:** use default user permissions when creating a local user ([#713](https://github.com/sct/overseerr/issues/713)) ([660ada0](https://github.com/sct/overseerr/commit/660ada0b2025eb2c06d9054fd0a7b5a632af6af2)) +- **radarr:** fix request bug which made it unable to be added to radarr ([#760](https://github.com/sct/overseerr/issues/760)) ([45a2779](https://github.com/sct/overseerr/commit/45a277964b0c39346d7216873812e0ebe505cb79)) +- **radarr:** return the updated data when updating radarr request ([#765](https://github.com/sct/overseerr/issues/765)) ([0c6d478](https://github.com/sct/overseerr/commit/0c6d4780c355ffe1a951268fb6949491d435bbf1)) +- **requests:** handle when tvdbid is null ([#657](https://github.com/sct/overseerr/issues/657)) ([2da0da8](https://github.com/sct/overseerr/commit/2da0da826ae1d73467bc8a671fda7cc5ca1f14c9)) +- **sonarr-sync:** correctly set series with no seasons to requested status ([3812989](https://github.com/sct/overseerr/commit/3812989a1ce1e07d4af09149008043a6e2e94060)), closes [#762](https://github.com/sct/overseerr/issues/762) +- **sync:** do not update series status if already available and no new seasons ([136d874](https://github.com/sct/overseerr/commit/136d874cba37babf9c0670844b002871710e6d99)), closes [#777](https://github.com/sct/overseerr/issues/777) +- **ui:** Capitalization, punctuation, and grammar inconsistences & errors ([#731](https://github.com/sct/overseerr/issues/731)) ([f05d4a0](https://github.com/sct/overseerr/commit/f05d4a0d0b42905fcaee49b2471bb1f4ee77fffe)) +- lookup movie by imdbid if tmdbid does not exits for plex movie agent ([#711](https://github.com/sct/overseerr/issues/711)) ([e972288](https://github.com/sct/overseerr/commit/e97228899a5936b2525c8060abfa14b5ce31658d)) +- show recently added series even if they are not complete ([d0c830e](https://github.com/sct/overseerr/commit/d0c830e80d389f9e0f48a9b83659331f54630d03)) + +### Features + +- **lang:** translated using Weblate (Dutch) ([059995e](https://github.com/sct/overseerr/commit/059995e0ef3370a3192bd386fa6875ca0f58690a)) +- **lang:** translated using Weblate (French) ([4789583](https://github.com/sct/overseerr/commit/4789583d66305ac7b3d393659b2f3604c0acc576)) +- **lang:** translations update from Weblate ([#727](https://github.com/sct/overseerr/issues/727)) ([71875ef](https://github.com/sct/overseerr/commit/71875efb48246dbb0139ad15a4261a5661fcfe17)) +- **lang:** update languages and fix merge conflict ([083a74a](https://github.com/sct/overseerr/commit/083a74a686d202cce5775bf9752caaa9a626cf45)) +- **ui:** Move PROXY setting to UI ([#782](https://github.com/sct/overseerr/issues/782)) ([f1dd5e7](https://github.com/sct/overseerr/commit/f1dd5e7e12c1f602449c4769173dbce71e3569d0)) +- add manual availability buttons to manage slideover ([67f8aef](https://github.com/sct/overseerr/commit/67f8aef00d98c834b60cb6152ccd5cb7b5709d12)), closes [#672](https://github.com/sct/overseerr/issues/672) +- **media:** add link to the item on plex ([#735](https://github.com/sct/overseerr/issues/735)) ([1d7150c](https://github.com/sct/overseerr/commit/1d7150c24ec5ad347093889bfceab61b664900d5)) +- Radarr & Sonarr Sync ([#734](https://github.com/sct/overseerr/issues/734)) ([ec5fb83](https://github.com/sct/overseerr/commit/ec5fb836785855eb4846fd33b49faeb94c40506a)) +- **frontend:** add option to hide all available items from discovery ([#699](https://github.com/sct/overseerr/issues/699)) ([6c1742e](https://github.com/sct/overseerr/commit/6c1742e94ccfc6c13cf1d25fd9e893ee1f431aae)) +- **lang:** add support for Portuguese (Portugal) language ([e044146](https://github.com/sct/overseerr/commit/e044146aa55109a1eccfde9650b26beb0d5ec9a6)) +- **lang:** translated using Weblate (Dutch) ([6d0f7d4](https://github.com/sct/overseerr/commit/6d0f7d4b50370c420c1017f32d48313074543743)) +- **lang:** translated using Weblate (Italian) ([9aa5c12](https://github.com/sct/overseerr/commit/9aa5c121644518c1fbb308a487c26d8998bb5a36)) +- **lang:** translated using Weblate (Portuguese (Portugal)) ([f001fb3](https://github.com/sct/overseerr/commit/f001fb3b33d4fb749acb70c45b8a55a5bbef570c)) +- **lang:** translated using Weblate (Spanish) ([4f94d22](https://github.com/sct/overseerr/commit/4f94d227fc3096bcb8a1e5cf12fe9222d6c6b711)) +- **login:** add request ip to the failed request log ([#714](https://github.com/sct/overseerr/issues/714)) ([2d31ea9](https://github.com/sct/overseerr/commit/2d31ea940ac0a1a84d2150743798b41ff6490317)) +- **users:** add editable usernames ([#715](https://github.com/sct/overseerr/issues/715)) ([20ca3f2](https://github.com/sct/overseerr/commit/20ca3f2f5fcf4a9eb0d6a8be671bb4fb1f5e6178)) +- pre-populate server info from plex.tv API ([#563](https://github.com/sct/overseerr/issues/563)) ([82ac76b](https://github.com/sct/overseerr/commit/82ac76b0540ba1133cb5384744d2499c2488a4e8)) +- **auth:** Add optional CSRF protection ([#697](https://github.com/sct/overseerr/issues/697)) ([6e25891](https://github.com/sct/overseerr/commit/6e2589178b99f8f32f0ded9a7cfd9921c33e9b60)) +- ability to edit user settings in bulk ([#597](https://github.com/sct/overseerr/issues/597)) ([4b0241c](https://github.com/sct/overseerr/commit/4b0241c3b34d4229f928c21defb10a1c051264d1)) +- **lang:** translated using Weblate (English) ([9bb11af](https://github.com/sct/overseerr/commit/9bb11afc6b4a109ae1e14d41c9fe2b71f19c470a)) +- **lang:** translated using Weblate (German) ([c2a3e8e](https://github.com/sct/overseerr/commit/c2a3e8ed5243925dce991ec7995ae831702dbc7b)) +- **lang:** translated using Weblate (Portuguese (Brazil)) ([32f4916](https://github.com/sct/overseerr/commit/32f4916c4a926097f31ed472aee031536b847bb7)) +- **lang:** translated using Weblate (Portuguese (Brazil)) ([98570c9](https://github.com/sct/overseerr/commit/98570c920e4904a594bb7464161b985094958f84)) +- **notifications:** add option to send notifications for auto-approved requests ([21db367](https://github.com/sct/overseerr/commit/21db3676d1464b63384b04c0c2926cb2a6252e9b)), closes [#267](https://github.com/sct/overseerr/issues/267) + +## [1.17.2](https://github.com/sct/overseerr/compare/v1.17.1...v1.17.2) (2021-01-20) + +### Bug Fixes + +- **requests:** allow declined season requests to be re-requested ([e1032ff](https://github.com/sct/overseerr/commit/e1032ff5dfac4a8c9d4da9cf2788c19822343ad9)), closes [#690](https://github.com/sct/overseerr/issues/690) +- **requests:** update requests to approved when parent media is set as available ([78444a9](https://github.com/sct/overseerr/commit/78444a9e643829823162389dee60cca70da56bff)), closes [#688](https://github.com/sct/overseerr/issues/688) + +## [1.17.1](https://github.com/sct/overseerr/compare/v1.17.0...v1.17.1) (2021-01-19) + +### Bug Fixes + +- **frontend:** show auto approval on series request modal only with correct permissions ([8927c6d](https://github.com/sct/overseerr/commit/8927c6d2e39dbda2b1121095a7273f5cab1c9b74)), closes [#687](https://github.com/sct/overseerr/issues/687) + +# [1.17.0](https://github.com/sct/overseerr/compare/v1.16.0...v1.17.0) (2021-01-19) + +### Bug Fixes + +- **api:** improve rottentomatoes rating matching for movies ([7db62ab](https://github.com/sct/overseerr/commit/7db62ab824eefc42e6db16e42d52f4266b136f82)), closes [#494](https://github.com/sct/overseerr/issues/494) +- **build:** remove cross import from client to server for UserType ([23624bd](https://github.com/sct/overseerr/commit/23624bd144af5df4c31995b68ce48105b95b20f6)) +- **frontend:** clarify which fields are required in radarr/sonarr modals ([860d71e](https://github.com/sct/overseerr/commit/860d71ed69a69a1a3f74b79290ef471e04f57a6b)), closes [#575](https://github.com/sct/overseerr/issues/575) +- **frontend:** do not show failed media status on request list for declined requests ([00944b1](https://github.com/sct/overseerr/commit/00944b1ec2db8ddc5742448f6448f7364c473a98)), closes [#664](https://github.com/sct/overseerr/issues/664) +- **frontend:** fix button styling on details page on small screen sizes ([d9e0c90](https://github.com/sct/overseerr/commit/d9e0c90e76d80aef0c67318e00e997804805f46e)) +- **frontend:** fix request button height ([a262727](https://github.com/sct/overseerr/commit/a2627270784bdef8644875fa5c5a7349a0b7fd81)) +- **frontend:** request dropdown menu now properly shows up over collection button ([b491be1](https://github.com/sct/overseerr/commit/b491be1b1e7f6aa588274230e695e4c5302b961e)) +- **frontend:** show correct request status on request cards for 4k requests ([1aa0005](https://github.com/sct/overseerr/commit/1aa0005b4298fc1af9c1d0bf1f357738c0fa2673)) +- **lang:** add missing see more i18n string for SeeMoreCard ([d9919ab](https://github.com/sct/overseerr/commit/d9919abb8998d28558ddec35b8e60ab2af75d5b7)) +- **lang:** change email auth user/pass strings to SMTP Username/Password ([a77a2aa](https://github.com/sct/overseerr/commit/a77a2aa3ebb1be353d534db5b07647ac26c60e15)) +- **notifications:** correctly compare seasons before sending series notifications ([f17fa2a](https://github.com/sct/overseerr/commit/f17fa2a2db8144bac89936f588627e8dd37bf54a)) +- **notifications:** only send one available notification for standard media ([fc6f7cc](https://github.com/sct/overseerr/commit/fc6f7ccea586165a30022b6d5554911c66ece6df)) +- **notifications:** send media declined email ([eb6fc8a](https://github.com/sct/overseerr/commit/eb6fc8a19099469794d471db0b48a258c2866633)), closes [#679](https://github.com/sct/overseerr/issues/679) +- **plex-sync:** improve plex sync error handling. add session id to fix stuck runs ([a740b07](https://github.com/sct/overseerr/commit/a740b07f06f892b72a651b928af28ce71cb495ee)) +- **plex-sync:** store plex added date and sort recently added by it ([d688a96](https://github.com/sct/overseerr/commit/d688a967596afcba9799b8133089bebb5add27cf)) +- **requests:** select the correct radarr/sonarr server when sending request to service ([e0d9f89](https://github.com/sct/overseerr/commit/e0d9f891e797c3839f976b75a871903b6f2e55f1)) +- **server:** support absolute paths for CONFIG_DIRECTORY ([51d8fba](https://github.com/sct/overseerr/commit/51d8fba9162b9e148a35ced69e7e035438c8b0f1)) +- **user edit:** fix user edit not being able to be saved ([#651](https://github.com/sct/overseerr/issues/651)) ([b04d00e](https://github.com/sct/overseerr/commit/b04d00ef509d6f13c1f9677b3f318331782c0086)) + +### Features + +- **api:** /request/count endpoint ([#682](https://github.com/sct/overseerr/issues/682)) ([192cfd8](https://github.com/sct/overseerr/commit/192cfd8a8ea9ab942d5bb265d42050917a2f5a04)) +- **frontend:** add see more card to media sliders ([587e8db](https://github.com/sct/overseerr/commit/587e8db15e9c19b4c58406e3e4215d8bf87d8762)) +- **frontend:** add template variable help button to custom webhook settings page ([29c5bc4](https://github.com/sct/overseerr/commit/29c5bc40975e7ab0a2e08bb77294f164f0c60769)) +- **lang:** add support for Chinese (Traditional) language ([686c4f7](https://github.com/sct/overseerr/commit/686c4f71bf930625af082ac5e14dc5f79f5c42eb)) +- **lang:** Translations update from Weblate ([#604](https://github.com/sct/overseerr/issues/604)) ([801e765](https://github.com/sct/overseerr/commit/801e76524d6ea0887249f1630402e9c3a3430b44)) +- **login:** add local users functionality ([#591](https://github.com/sct/overseerr/issues/591)) ([492e19d](https://github.com/sct/overseerr/commit/492e19df4014e67dc6a2de5903a33c25e13fcf45)) +- **notifications:** add notification for declined requests ([2f97f61](https://github.com/sct/overseerr/commit/2f97f61a6e8846975774aa16950a39ada2b1a016)), closes [#663](https://github.com/sct/overseerr/issues/663) +- **notifications:** Webhook Notifications ([#632](https://github.com/sct/overseerr/issues/632)) ([a7cc7c5](https://github.com/sct/overseerr/commit/a7cc7c59753dd9649b2ec37eb9d46fe4fa8e1e1c)) +- **requests:** Request Overrides & Request Editing ([#653](https://github.com/sct/overseerr/issues/653)) ([bdb3372](https://github.com/sct/overseerr/commit/bdb33722e6df09dd6d8caa36b104b61c6b8dc00d)) +- **server:** add CONFIG_DIRECTORY env var to control config directory location ([fa8f112](https://github.com/sct/overseerr/commit/fa8f112c31ccb5ee6244f776bc97e76d81958539)) +- 4K Requests ([#559](https://github.com/sct/overseerr/issues/559)) ([6b2df24](https://github.com/sct/overseerr/commit/6b2df24a2e8f96dd2277a814d7e02015d1f80cdc)) +- map AniDB IDs from Hama agent to tvdb/tmdb/imdb IDs ([#538](https://github.com/sct/overseerr/issues/538)) ([0600ac7](https://github.com/sct/overseerr/commit/0600ac7c3a1bc0cdd906634d5f77ea3e99b10e94)), closes [#453](https://github.com/sct/overseerr/issues/453) + +### Reverts + +- **deps:** revert back to next@10.0.3 until sharp optional dependency bug is fixed ([7962964](https://github.com/sct/overseerr/commit/79629645aacc1a042919834da79bff0c1f69c9d6)) + +# [1.16.0](https://github.com/sct/overseerr/compare/v1.15.0...v1.16.0) (2021-01-07) + +### Bug Fixes + +- **frontend:** adjust titlecard badge styling ([effc809](https://github.com/sct/overseerr/commit/effc80977a4ed732092254248f82363e52233171)) +- **frontend:** apply same titlecard hover effect to personcard ([67f2b57](https://github.com/sct/overseerr/commit/67f2b57f00216ded3b34965629d6fdd2f16bc25f)) +- **frontend:** only animate titlecard when showDetail is true ([0ab4c3c](https://github.com/sct/overseerr/commit/0ab4c3c36fe2c1ded142b6931111516f7f990a41)) +- **frontend:** use hardware acceleration for titlecard scale ([88810bf](https://github.com/sct/overseerr/commit/88810bf0a4ef74299f6541b60fa91cea3610f99c)) +- **plex-sync:** do not run plex sync if no admin exists ([493d82b](https://github.com/sct/overseerr/commit/493d82b6b066d77609cf66e005fd1f1472b8e011)) + +### Features + +- **lang:** translations update from Weblate ([#495](https://github.com/sct/overseerr/issues/495)) ([b04eda6](https://github.com/sct/overseerr/commit/b04eda6c8a3bfcaa2a14b8a29612fdf690c9fba0)) +- **lang:** Translations update from Weblate ([#580](https://github.com/sct/overseerr/issues/580)) ([2bfe0f2](https://github.com/sct/overseerr/commit/2bfe0f2bf66956763ab26d5c54f26e6c456f59f7)) +- **notifications:** add pushover integration ([#574](https://github.com/sct/overseerr/issues/574)) ([ee5d018](https://github.com/sct/overseerr/commit/ee5d0181fc9a673b27aefd1d09b0a78c3d2e4f55)) + +# [1.15.0](https://github.com/sct/overseerr/compare/v1.14.1...v1.15.0) (2021-01-04) + +### Bug Fixes + +- **api:** return 202 when same seasons are requested again ([5c84702](https://github.com/sct/overseerr/commit/5c847026aad79fcac4d020786ded9f867696c226)) +- **build:** fixes build to include commit tag for app build step ([289864a](https://github.com/sct/overseerr/commit/289864af1a995ce04834bf8a220cc238e1954d19)) +- **docs:** fix typo in build instructions ([#503](https://github.com/sct/overseerr/issues/503)) ([2b27a71](https://github.com/sct/overseerr/commit/2b27a715b07c27200ba1e5e9623629a34389276d)) +- **frontend:** add i18n for request text on titlecard ([a524b9c](https://github.com/sct/overseerr/commit/a524b9c4c8968f6823d33eb270dc26069fe4a725)) +- **frontend:** add localized strings for status checker ([2dcda39](https://github.com/sct/overseerr/commit/2dcda39d40d820419e098bd6f1101eb820e5b42d)) +- **frontend:** center text in movie auto-approve modal on small screens ([#510](https://github.com/sct/overseerr/issues/510)) ([1438b08](https://github.com/sct/overseerr/commit/1438b08cf0b358d79c6688c64be99f1718ec2d23)), closes [#507](https://github.com/sct/overseerr/issues/507) +- **frontend:** change titlecard to only have a request button ([b5a3a7a](https://github.com/sct/overseerr/commit/b5a3a7a89fcaf86dd794dc419711677b53646577)) +- **frontend:** combine duplicate credits on a persons detail page ([d188f6f](https://github.com/sct/overseerr/commit/d188f6ffadff1564c47d5f33138e35498bed29fd)), closes [#504](https://github.com/sct/overseerr/issues/504) +- **frontend:** disable pointer-events on titlecard badges ([ce06879](https://github.com/sct/overseerr/commit/ce0687922a94588b3492e8ddf2e84f54dd1a0d4e)) +- **frontend:** fix count of requests in request list ([f124d73](https://github.com/sct/overseerr/commit/f124d732a2911abdccb5abc11471efe61cc20f7a)) +- **frontend:** fix sliders overflowing on firefox ([67ac9e0](https://github.com/sct/overseerr/commit/67ac9e075f0ca1cfe7e4766d9168815d7ab600fa)), closes [#566](https://github.com/sct/overseerr/issues/566) +- **frontend:** full season request modal fits on a smaller mobile UI ([#535](https://github.com/sct/overseerr/issues/535)) ([12db7a0](https://github.com/sct/overseerr/commit/12db7a065ad566b47d46de4b949343290894f153)) +- **frontend:** handle currentLibrary possibly being null on first manual sync ([93b57a7](https://github.com/sct/overseerr/commit/93b57a76f10a823615ca11ff59f523b67aa30fad)) +- **frontend:** increase titlecard status badge size on larger screens ([ba106c4](https://github.com/sct/overseerr/commit/ba106c447d76db2f9ac70a60c5b38cc60ab554fe)) +- **frontend:** search clear button now correctly triggers routing ([343f466](https://github.com/sct/overseerr/commit/343f466788abc308b91a414ef61bba816ac8875c)) +- **frontend:** set locale cookie expiration to be much longer ([fae4818](https://github.com/sct/overseerr/commit/fae481895736eab81d52eb93788beb00669fb355)) +- **frontend:** show movie/series badges always ([8cbf39a](https://github.com/sct/overseerr/commit/8cbf39a9d12eaee7720fa4721c350c1ef9dee856)) +- **frontend:** update login/setup images ([058fb65](https://github.com/sct/overseerr/commit/058fb65495baa08a0bd4c9e0aef320c6fc7d017b)) +- **holiday:** remove special holiday slider ([8c09033](https://github.com/sct/overseerr/commit/8c0903393cf2cb2a929ba70a8ab6ddcc4cba0574)) +- correctly deal with tmdb id duplicates between movies/series ([721ed9a](https://github.com/sct/overseerr/commit/721ed9a93087a57ae749388bddcacf26022e3df6)), closes [#526](https://github.com/sct/overseerr/issues/526) +- use new commit tag file for app version as well ([d00e470](https://github.com/sct/overseerr/commit/d00e470b55327489b49d770144b7cfdb24045be6)) + +### Features + +- **email:** add sendername to email notification ([#506](https://github.com/sct/overseerr/issues/506)) ([0185bb1](https://github.com/sct/overseerr/commit/0185bb1a7084c1faeb61fb1c63e34e26732711c8)) +- **frontend:** add clear-field-icon to search field ([#498](https://github.com/sct/overseerr/issues/498)) ([7434a26](https://github.com/sct/overseerr/commit/7434a26f76b5e9f74918f3e1a34443d20ecfcbe4)) +- **frontend:** add documentation link to about page ([c034496](https://github.com/sct/overseerr/commit/c034496f557a031aed35cd28dc7221d8cdf36643)) +- **frontend:** add telegram integration ([#491](https://github.com/sct/overseerr/issues/491)) ([c8d4d67](https://github.com/sct/overseerr/commit/c8d4d674f412082ad9e9da09abd79660365cf728)) +- **frontend:** filter/sorting for request list ([5add44c](https://github.com/sct/overseerr/commit/5add44cfb0379aa6fed7c3b867230292feacc684)), closes [#431](https://github.com/sct/overseerr/issues/431) +- **notifications:** control notifcation types per agent ([8af6a1f](https://github.com/sct/overseerr/commit/8af6a1f566769c583af7dd9e18d162717835b7cc)), closes [#513](https://github.com/sct/overseerr/issues/513) +- status checker to prompt users to reload their frontend when app version changes ([75a4264](https://github.com/sct/overseerr/commit/75a426437a4182e21da13684066966dd5bf8fc5e)) + +## [1.14.1](https://github.com/sct/overseerr/compare/v1.14.0...v1.14.1) (2021-01-02) + +### Bug Fixes + +- **holiday:** remove special holiday slider ([22f2037](https://github.com/sct/overseerr/commit/22f2037ea6c5a0ba2ffa4d69f2b7cf42bdcf8575)) + +# [1.14.0](https://github.com/sct/overseerr/compare/v1.13.0...v1.14.0) (2020-12-25) + +### Bug Fixes + +- **frontend:** add margin to ButtonWithDropdown component on movie/tv details page ([06fc98b](https://github.com/sct/overseerr/commit/06fc98b6b958221fa180f57f702c348f15b31f1c)) +- **frontend:** correctly position title card hover section ([#486](https://github.com/sct/overseerr/issues/486)) ([4b7af86](https://github.com/sct/overseerr/commit/4b7af86111a0300e1a137f23fa4ad1639fa55feb)) +- **frontend:** fix missing styles for alert component ([de3d288](https://github.com/sct/overseerr/commit/de3d288949b60d3a3af889d69a62bea2bc799ed7)) +- **frontend:** fix mobile dropdown in notifications settings ([6353cda](https://github.com/sct/overseerr/commit/6353cda5825f442dd539886c7b9ba437edf27ac4)) +- **frontend:** fix scaling titlecard content position ([bd94740](https://github.com/sct/overseerr/commit/bd947409e6e8ff313011b77adc76ccd5f9112c78)) +- **frontend:** improve flex header on movie/tv details page ([d7b1c28](https://github.com/sct/overseerr/commit/d7b1c2840690c144ebf29a360defcbd6fdb21354)) +- **frontend:** invalid dom-nesting title card fix ([#482](https://github.com/sct/overseerr/issues/482)) ([f2ebba7](https://github.com/sct/overseerr/commit/f2ebba7b1df775d33d2af6abc3ee2c9de5f2e57a)), closes [#476](https://github.com/sct/overseerr/issues/476) +- **frontend:** remove vote permission for now ([5d06a34](https://github.com/sct/overseerr/commit/5d06a347311bd10c05d8f58068ca7104e265dcca)) +- **frontend:** sort person detail credits by tmdb votes ([17518db](https://github.com/sct/overseerr/commit/17518dbe7f545100770a892d03d1f8508adc3650)) +- **frontend:** status badge Unavailable renamed to Requested ([ed94a0f](https://github.com/sct/overseerr/commit/ed94a0f335c59de526dd812aea7616313fe002fd)), closes [#374](https://github.com/sct/overseerr/issues/374) +- **frontend:** update titlecard status badge to new requested colors ([8f292d5](https://github.com/sct/overseerr/commit/8f292d538b937ea133175089979ef02599f6fef4)) +- **logs:** rotate logs on a daily basis instead of incrementing log filename ([395cbb2](https://github.com/sct/overseerr/commit/395cbb2be6c62f1d7573593e49a93615eaf22853)) +- improve apple-touch-icon and android app icons ([329a814](https://github.com/sct/overseerr/commit/329a814a8fb791122266c0b04b05848c71d68ba1)) + +### Features + +- **lang:** translations update from Weblate ([#479](https://github.com/sct/overseerr/issues/479)) ([c8c74b0](https://github.com/sct/overseerr/commit/c8c74b0ae54fcc524aa8b2edf5a5c5e5db6c1638)) +- **notifications:** add slack notification agent ([1163e81](https://github.com/sct/overseerr/commit/1163e81adc7da1e8334155ebee5b4672a22143db)), closes [#365](https://github.com/sct/overseerr/issues/365) +- add collections ([#484](https://github.com/sct/overseerr/issues/484)) ([a333a09](https://github.com/sct/overseerr/commit/a333a095820ce3f10857026ba4770a2fffeed7cb)), closes [#418](https://github.com/sct/overseerr/issues/418) +- add separate auto approve permissions for Movies/Series ([4809257](https://github.com/sct/overseerr/commit/480925781691de456abc427fbbba161be11a3a8a)), closes [#268](https://github.com/sct/overseerr/issues/268) +- simple failed request handling ([#474](https://github.com/sct/overseerr/issues/474)) ([02969d5](https://github.com/sct/overseerr/commit/02969d5426245062a2f53475d83c4a8639632c9d)) +- YouTube Movie/TV Trailers ([#454](https://github.com/sct/overseerr/issues/454)) ([e88dc83](https://github.com/sct/overseerr/commit/e88dc83aeba0475e3ad421d5ab130cea4fc9a806)) + +# [1.13.0](https://github.com/sct/overseerr/compare/v1.12.1...v1.13.0) (2020-12-23) + +### Bug Fixes + +- **api:** correctly return firstAirDate for series in search endpoints ([32b4c99](https://github.com/sct/overseerr/commit/32b4c99950659d9e1da2ffa93c22383c54d0d904)), closes [#462](https://github.com/sct/overseerr/issues/462) +- **email:** correctly log errors when emails fail to send ([0980fa5](https://github.com/sct/overseerr/commit/0980fa54f9fc3bdfae6c57fa5a20ce3b2a88a677)) +- **frontend:** added new Radarr v3 logo ([#471](https://github.com/sct/overseerr/issues/471)) ([3bbc716](https://github.com/sct/overseerr/commit/3bbc716434dc04bfe6b55de9898eb2c0ecb03baa)) +- **frontend:** approve and decline button (in manage panel) will now fit on mobile ([#441](https://github.com/sct/overseerr/issues/441)) ([66ef72d](https://github.com/sct/overseerr/commit/66ef72dd42912d83ea8f86aabb75fbee547f8de9)) +- **frontend:** filter out undefined backdrop paths for person details page ([2e0e4d5](https://github.com/sct/overseerr/commit/2e0e4d5129ed4912415f61eb8d1da41e88ddcaff)) +- **frontend:** show backdrops instead of posters for new person detail design ([9f5f920](https://github.com/sct/overseerr/commit/9f5f920c23007363aa7f53ebef0b61236d4f53ea)) +- clarify full sync runs every 24 hours ([0c8a180](https://github.com/sct/overseerr/commit/0c8a180189b2610bab2fa977d458743d8a60343e)) +- **plex-sync:** match correct tmdb format for movies ([4205e32](https://github.com/sct/overseerr/commit/4205e32ae71bc18c07209f1c82e6af1cb5f01335)) + +### Features + +- **email:** option to allow self signed certificates ([6898357](https://github.com/sct/overseerr/commit/6898357b13a6aa53a55709ea95819c2b3df6784c)) +- **frontend:** adjust person details design and add improved truncate ([1fb7ea7](https://github.com/sct/overseerr/commit/1fb7ea72589d2908ae80a2a688881d4eb3c050e5)) +- **frontend:** first air date added to TV details page ([#470](https://github.com/sct/overseerr/issues/470)) ([a7db01f](https://github.com/sct/overseerr/commit/a7db01fba483ca633a6eb9d39eb085ab9939d4d2)) +- **lang:** translations update from Weblate ([#410](https://github.com/sct/overseerr/issues/410)) ([941fe19](https://github.com/sct/overseerr/commit/941fe1990454439cf05b48ef92bd3493432f8ed8)) +- **logs:** rotate log files if they reach 20MB in size ([22002ab](https://github.com/sct/overseerr/commit/22002ab4c76aace2bb202ac58da605b7a6f75d6d)), closes [#438](https://github.com/sct/overseerr/issues/438) +- **notifications:** include direct links to media in notifications ([659fa50](https://github.com/sct/overseerr/commit/659fa505f0db32262ad0041cddb4daea893e6d65)), closes [#437](https://github.com/sct/overseerr/issues/437) +- **plex-sync:** add support for hama guid's ([ffe9e19](https://github.com/sct/overseerr/commit/ffe9e19c3b99de6af1185900e292da641ff44320)), closes [#453](https://github.com/sct/overseerr/issues/453) + +## [1.12.1](https://github.com/sct/overseerr/compare/v1.12.0...v1.12.1) (2020-12-22) + +### Bug Fixes + +- **migration:** fixes issue migrating away from the unique imdbId constraint ([69fd7a5](https://github.com/sct/overseerr/commit/69fd7a5511215674a5c22ba48627f221da900229)) + +# [1.12.0](https://github.com/sct/overseerr/compare/v1.11.0...v1.12.0) (2020-12-22) + +### Bug Fixes + +- **api:** fix cross-imported type crashing build ([f35dae5](https://github.com/sct/overseerr/commit/f35dae56a583a5545375318fa5be994ae1f2557f)) +- **api:** prevent checking first admin account for plex server access ([22006e9](https://github.com/sct/overseerr/commit/22006e9dbde82609440f89bde9a40887b4742682)) +- **frontend:** add name, short_name and start_url to manifest ([#424](https://github.com/sct/overseerr/issues/424)) ([c6836e0](https://github.com/sct/overseerr/commit/c6836e02c810e8adb12c3a4b110f9604cf5b7b81)) +- **frontend:** adjust person card layout to deal with overflowing content ([4891298](https://github.com/sct/overseerr/commit/48912988915ae40606a900a6f1dd23fc25ed567f)), closes [#416](https://github.com/sct/overseerr/issues/416) +- **frontend:** allow more special characters in search input ([5deb64a](https://github.com/sct/overseerr/commit/5deb64a87fd70e97da27a025ad11fb8ace0e0b57)), closes [#430](https://github.com/sct/overseerr/issues/430) +- **logs:** improve logging when adding to sonarr/radarr ([4b50522](https://github.com/sct/overseerr/commit/4b505223b881a750007e3fbc7d4bcb9677d4d412)) +- only run migrations in production ([ab9cef3](https://github.com/sct/overseerr/commit/ab9cef3624b5db1ec03507553a69d33b87857e29)) +- **notifications:** always update the media table when seasons become available ([0916b58](https://github.com/sct/overseerr/commit/0916b58594a00db98c6701fdcaee4f3c3e08904e)) +- **plex-sync:** fixes processing movies using TMDB agent ([764db94](https://github.com/sct/overseerr/commit/764db94f1bd7866309684d5bd56033b21cbc2e0c)), closes [#363](https://github.com/sct/overseerr/issues/363) + +### Features + +- **frontend:** add crew related movies/shows to person details page ([12127a7](https://github.com/sct/overseerr/commit/12127a77633f0e92ae88cbafd49581296f559c33)) +- **frontend:** add full crew page for movies/shows ([604ba2a](https://github.com/sct/overseerr/commit/604ba2a92f1d59489e7fc6dfc011347f8595c123)) +- default user permissions added to settings ([e7ee85c](https://github.com/sct/overseerr/commit/e7ee85c29b5d25c6bff58717eae5e62de4dcef0c)), closes [#388](https://github.com/sct/overseerr/issues/388) +- import users from plex ([#428](https://github.com/sct/overseerr/issues/428)) ([7e8f361](https://github.com/sct/overseerr/commit/7e8f361af711001cfc4dcc06a384b76f9846f90f)), closes [#281](https://github.com/sct/overseerr/issues/281) +- **frontend:** add prioritized crew under overview ([6753d9d](https://github.com/sct/overseerr/commit/6753d9daaafb18672f14fd86f2c1675dcec39b13)), closes [#406](https://github.com/sct/overseerr/issues/406) +- **notifications:** added ability to send test notifications ([44a3054](https://github.com/sct/overseerr/commit/44a305426f3e9829c167a4a73095d0d248641f47)), closes [#309](https://github.com/sct/overseerr/issues/309) + +### Reverts + +- **deps:** revert react-use-clipboard to 1.0.2 ([7083ddf](https://github.com/sct/overseerr/commit/7083ddf18121716e3442acab3506c395fdc351ac)) + +# [1.11.0](https://github.com/sct/overseerr/compare/v1.10.0...v1.11.0) (2020-12-20) + +### Features + +- **frontend:** add language picker to setup/login ([ff2ab29](https://github.com/sct/overseerr/commit/ff2ab29491a80c421525b9a394d6fbbf54914dc2)) +- **frontend:** add support overseerr block to about page ([c128898](https://github.com/sct/overseerr/commit/c128898206d6cbb482de4d8dca53f70b87e4911a)) +- **frontend:** releases added to about page ([b7f5739](https://github.com/sct/overseerr/commit/b7f573903500cc8a62e39afd787bc1da8c09d88b)), closes [#303](https://github.com/sct/overseerr/issues/303) +- **lang:** add support for Italian, Portuguese (Brazil) and Serbian ([108dfc4](https://github.com/sct/overseerr/commit/108dfc4afd31388cb6c9e07deccd168ade8b1574)) +- **lang:** add support for swedish language ([c9fe6cb](https://github.com/sct/overseerr/commit/c9fe6cb0b7ea984d8e4e1cb3f284935c9da7cc2b)) +- **lang:** translations update from Weblate ([#400](https://github.com/sct/overseerr/issues/400)) ([1bd0e64](https://github.com/sct/overseerr/commit/1bd0e646e313ddf77ef331e818e03401fbf64a72)) +- **lang:** translations update from Weblate ([#403](https://github.com/sct/overseerr/issues/403)) ([3778ad8](https://github.com/sct/overseerr/commit/3778ad829c0897de178212b3bde4c0d3b5089161)) + +# [1.10.0](https://github.com/sct/overseerr/compare/v1.9.1...v1.10.0) (2020-12-19) + +### Bug Fixes + +- **email:** fix link to Overseerr in email templates ([816fec1](https://github.com/sct/overseerr/commit/816fec1a83a53edb3b65c3e5e7d0e6e1bd49726d)), closes [#392](https://github.com/sct/overseerr/issues/392) +- **frontend:** adjust padding of search box so placeholder text fits on mobile ([3601d44](https://github.com/sct/overseerr/commit/3601d442db32d3f98f7b050365c11ea8ef9bc4ae)), closes [#393](https://github.com/sct/overseerr/issues/393) +- **frontend:** changed request block for slideover on mobile UI ([#387](https://github.com/sct/overseerr/issues/387)) ([549567a](https://github.com/sct/overseerr/commit/549567a7e9db01933546d9970fc06f17218dfab1)) +- **frontend:** hide Request More button if all current seasons are available ([2a4dd52](https://github.com/sct/overseerr/commit/2a4dd52275007e48f946c3b9e29f1d78da57bdaa)), closes [#343](https://github.com/sct/overseerr/issues/343) +- **frontend:** try not to render broken rottentomatoes data ([a0c5608](https://github.com/sct/overseerr/commit/a0c5608aa0b6c7a4294300589efa9a662163ce48)) + +### Features + +- **lang:** translations update from Weblate ([#391](https://github.com/sct/overseerr/issues/391)) ([5f71fb7](https://github.com/sct/overseerr/commit/5f71fb7ee280714275d2ac045c472fcdddd5a2ea)) +- add missing tzdata package to image ([53bede6](https://github.com/sct/overseerr/commit/53bede692d4f0e940dededa63015fe1908129914)), closes [#394](https://github.com/sct/overseerr/issues/394) +- **frontend:** add external links to movie and tv detail pages ([a0024a0](https://github.com/sct/overseerr/commit/a0024a0cbe717d78f53413bb78644c829f143c4d)) +- **lang:** translations update from Weblate ([#380](https://github.com/sct/overseerr/issues/380)) ([8408e19](https://github.com/sct/overseerr/commit/8408e19568b2f239c57e11e2946c75f193d1c22e)) + +## [1.9.1](https://github.com/sct/overseerr/compare/v1.9.0...v1.9.1) (2020-12-18) + +### Bug Fixes + +- change default internal port to 5055 ([#389](https://github.com/sct/overseerr/issues/389)) ([5e5ba40](https://github.com/sct/overseerr/commit/5e5ba4050563f07bff367d2fb31ed7e7fca4291e)) + +# [1.9.0](https://github.com/sct/overseerr/compare/v1.8.0...v1.9.0) (2020-12-18) + +### Features + +- api key regeneration ([6beac73](https://github.com/sct/overseerr/commit/6beac736efcf7b9102e02e43b75d91a9a158cd22)) +- **api:** add movie keyword search ([f88c4a6](https://github.com/sct/overseerr/commit/f88c4a6d4a49f8f3451ba6c85153677f33b7f5f6)) +- **frontend:** add studio/networks to movie/tv details ([4b6ad8a](https://github.com/sct/overseerr/commit/4b6ad8a3871957db4192b603abf38404250cea5d)), closes [#370](https://github.com/sct/overseerr/issues/370) +- **frontend:** added user deletion to the user list ([727fa06](https://github.com/sct/overseerr/commit/727fa06c18febb2a97ca219cc6bf0277ff462acd)), closes [#348](https://github.com/sct/overseerr/issues/348) +- **holiday:** special seasonal slider added to discover :) ([908f635](https://github.com/sct/overseerr/commit/908f63557ca03a1da8b16809ffa2c3acd782d94e)) +- allow to listen server on specific host interface ([#381](https://github.com/sct/overseerr/issues/381)) ([086183b](https://github.com/sct/overseerr/commit/086183b5636aa8d075d01fe59492c3eab0d1345b)), closes [#273](https://github.com/sct/overseerr/issues/273) +- anime profile support ([#384](https://github.com/sct/overseerr/issues/384)) ([0972f40](https://github.com/sct/overseerr/commit/0972f40a4e1fb3b5f02b07ae46b997d71aab9bfb)), closes [#266](https://github.com/sct/overseerr/issues/266) + +# [1.8.0](https://github.com/sct/overseerr/compare/v1.7.0...v1.8.0) (2020-12-17) + +### Features + +- **lang:** translations update from Weblate ([#336](https://github.com/sct/overseerr/issues/336)) ([ee84f74](https://github.com/sct/overseerr/commit/ee84f74f8a3558875b41daa539f42d00b949898a)) + +# [1.7.0](https://github.com/sct/overseerr/compare/v1.6.0...v1.7.0) (2020-12-17) + +### Bug Fixes + +- **email:** do not pass auth object to transport if no auth data present ([d5eb4d8](https://github.com/sct/overseerr/commit/d5eb4d8d438a159266b2de66b6bcdd9440a0c8ef)), closes [#312](https://github.com/sct/overseerr/issues/312) +- **frontend:** add http/https prefix to hostname fields for plex/radarr/sonarr ([ce0266f](https://github.com/sct/overseerr/commit/ce0266f74ea3979b291ff962271a928682892788)), closes [#357](https://github.com/sct/overseerr/issues/357) +- **frontend:** clarify that radarr/sonnarr servers must be tested before profiles/folders appear ([fc12ab8](https://github.com/sct/overseerr/commit/fc12ab84d9482eb3a11f117f8cab6fd48a9401cd)), closes [#326](https://github.com/sct/overseerr/issues/326) [#328](https://github.com/sct/overseerr/issues/328) +- **frontend:** correctly show an unauthorized error when a user fails to login ([18925de](https://github.com/sct/overseerr/commit/18925decafdac518f52a354c594cc378d2529022)), closes [#322](https://github.com/sct/overseerr/issues/322) +- **frontend:** fix tv shows failing to open when firstAirDate is undefined ([c21fa5b](https://github.com/sct/overseerr/commit/c21fa5b5350abdd8e03c077fde7246fa398e176e)), closes [#347](https://github.com/sct/overseerr/issues/347) +- **frontend:** make minimum availability required for Radarr servers ([2fe53ec](https://github.com/sct/overseerr/commit/2fe53ec5a8534e75c7d0cef31a8b46065111e0a7)), closes [#345](https://github.com/sct/overseerr/issues/345) +- **plex-sync:** bundle duplicate ratingKeys to speed up recently added sync ([67146c3](https://github.com/sct/overseerr/commit/67146c33ef7f28d520ba2c50b32673d43f4525c8)), closes [#360](https://github.com/sct/overseerr/issues/360) +- **sonarr.ts, mediarequest.ts:** add missing seasonFolder option ([#358](https://github.com/sct/overseerr/issues/358)) ([e9c899c](https://github.com/sct/overseerr/commit/e9c899ce419d149dde2ad9a0f7d5a2f2545b3ebf)) + +### Features + +- **frontend:** show alert when there are no default radarr/sonarr servers ([0d088e0](https://github.com/sct/overseerr/commit/0d088e085e68d39455fda21d1fd08ebcaef2c06b)), closes [#344](https://github.com/sct/overseerr/issues/344) + +# [1.6.0](https://github.com/sct/overseerr/compare/v1.5.0...v1.6.0) (2020-12-16) + +### Bug Fixes + +- **api:** accept the api key to perform actions on the api with X-API-Key header ([33f8831](https://github.com/sct/overseerr/commit/33f8831e880dc7fd3f69d951246cada5c6c0ffe7)) +- **api:** filter out libraries that do not have any metadata agent or are not movie/show ([01c179f](https://github.com/sct/overseerr/commit/01c179f762e686a1e5a3d4dab3a5bea53425b575)) +- **api:** only run recently added sync on enabled libraries ([e08fa35](https://github.com/sct/overseerr/commit/e08fa35548bb8644afa8df3124e6f9cc3a2c8f4a)), closes [#259](https://github.com/sct/overseerr/issues/259) +- **api:** set plex libraries to disabled if the name changes ([675060b](https://github.com/sct/overseerr/commit/675060bcdf23acbfd4de2900a65f95e74f4966a5)), closes [#324](https://github.com/sct/overseerr/issues/324) +- **frontend:** adds a tip to plex setup to clarify that syncing runs in the background ([df4ac83](https://github.com/sct/overseerr/commit/df4ac8361f82971ee845f3be217408a9123a0bf3)), closes [#325](https://github.com/sct/overseerr/issues/325) +- **frontend:** aligned movie and tv details ([#331](https://github.com/sct/overseerr/issues/331)) ([db0a5c4](https://github.com/sct/overseerr/commit/db0a5c44f678e76eee7f5582381016306d1f46a2)) +- **frontend:** close sidebar when clicking outside ([#333](https://github.com/sct/overseerr/issues/333)) ([6d7907e](https://github.com/sct/overseerr/commit/6d7907e844a909993d185759d660632f55aeaa35)) +- spelling mistake on the word 'requested' fixed ([#319](https://github.com/sct/overseerr/issues/319)) ([961d110](https://github.com/sct/overseerr/commit/961d1107208069a6fc820a1ba97ffda7336677cb)) + +### Features + +- add version to startup logs ([2948f93](https://github.com/sct/overseerr/commit/2948f9360eb484d1d6c0740a840135ca97e7240a)) +- **frontend:** temporary logs page to clear up confusion about it 404ing ([d9788c4](https://github.com/sct/overseerr/commit/d9788c4aa9f87e2eda3f7e3f1adc985f16039552)), closes [#272](https://github.com/sct/overseerr/issues/272) +- **lang:** add support for Spanish language ([6cd2049](https://github.com/sct/overseerr/commit/6cd20491d2a0ceb995c4744eeb92a6e2f57a4893)) +- **lang:** Translations update from Weblate ([#291](https://github.com/sct/overseerr/issues/291)) ([fddbb3c](https://github.com/sct/overseerr/commit/fddbb3cdfe3d50b2835c248556139c769dc2b805)) + +# [1.5.0](https://github.com/sct/overseerr/compare/v1.4.0...v1.5.0) (2020-12-15) + +### Bug Fixes + +- **api:** require package.json directly so typescript doesnt compile it into dist folder ([b9faa64](https://github.com/sct/overseerr/commit/b9faa6486b35aa865019aa8af9d307531054bc1d)) +- **frontend:** add validation for Radarr/Sonarr server name ([b5988f9](https://github.com/sct/overseerr/commit/b5988f9a5ff274e97f208c2726abe76c22c858ee)) +- **frontend:** only show alpha notice to admins ([ff61895](https://github.com/sct/overseerr/commit/ff618956b5d9cf933d867ea979b612c3d8a6f30b)) +- add support for ssl when connecting to plex ([3ba09d0](https://github.com/sct/overseerr/commit/3ba09d07eb0367c41603cd55e7ff41c66fb641c4)), closes [#275](https://github.com/sct/overseerr/issues/275) +- **services:** improve logging for when Radarr movie already exists ([#285](https://github.com/sct/overseerr/issues/285)) ([f998873](https://github.com/sct/overseerr/commit/f998873fc5669a547901f2733c9c785d744d27ca)), closes [#260](https://github.com/sct/overseerr/issues/260) + +### Features + +- **lang:** add i18n strings for new about page ([900827b](https://github.com/sct/overseerr/commit/900827be97845688e4bea72a8c5d9611a3e9d069)) +- about page initial version ([3f2a04c](https://github.com/sct/overseerr/commit/3f2a04c881bf06b73a952181fa463af84454b0dd)) + +# [1.4.0](https://github.com/sct/overseerr/compare/v1.3.2...v1.4.0) (2020-12-15) + +### Bug Fixes + +- changing parameter name to use correct 'port' [#276](https://github.com/sct/overseerr/issues/276) ([#277](https://github.com/sct/overseerr/issues/277)) ([6d08b10](https://github.com/sct/overseerr/commit/6d08b108200177ca3068c852e60a0df75ce2232a)) +- **services:** include radarr/sonarr baseUrl when adding media ([78af1a3](https://github.com/sct/overseerr/commit/78af1a3e6d00a5645a05e7bf3cf56a59439b6cc9)) + +### Features + +- **lang:** Translations update from Weblate ([#240](https://github.com/sct/overseerr/issues/240)) ([e17c637](https://github.com/sct/overseerr/commit/e17c63748362b6a480693e003ef5eec614dcec43)) + +## [1.3.2](https://github.com/sct/overseerr/compare/v1.3.1...v1.3.2) (2020-12-14) + +### Bug Fixes + +- **frontend:** convert plex port to a number before posting to the api ([8cb05c4](https://github.com/sct/overseerr/commit/8cb05c413a15a4b74e37ece5e24367d115995b32)) +- **frontend:** converts email smtp port to a number before posting to the api ([2098a2d](https://github.com/sct/overseerr/commit/2098a2d3d2981fd2ae54392aec3ef81327f2858e)), closes [#251](https://github.com/sct/overseerr/issues/251) +- **frontend:** encode special characters in search input to prevent crashing router ([15013d6](https://github.com/sct/overseerr/commit/15013d6c5dbff15704c7c30d261d68a265e7f2d7)), closes [#252](https://github.com/sct/overseerr/issues/252) +- **plex sync:** catch errors that occur during processMovie ([edbbccf](https://github.com/sct/overseerr/commit/edbbccf3ae623430294f1a5c3fd2728dbd42e555)), closes [#244](https://github.com/sct/overseerr/issues/244) [#246](https://github.com/sct/overseerr/issues/246) [#250](https://github.com/sct/overseerr/issues/250) +- **services:** improve logging for adding movies to Radarr ([6c1ee83](https://github.com/sct/overseerr/commit/6c1ee830a183f89bb1fe96a181a7d61684e23b22)) +- **services:** radarr/sonarr will use the correct default server ([0658b79](https://github.com/sct/overseerr/commit/0658b7943e1ab25816db9da34d4c9ea808d9203d)) + +## [1.3.1](https://github.com/sct/overseerr/compare/v1.3.0...v1.3.1) (2020-12-14) + +### Bug Fixes + +- **frontend:** also convert activeProfileId to a number for radarr/sonarr submissions ([7bf924f](https://github.com/sct/overseerr/commit/7bf924f7e94a0e0834f41b4ec067ed277c652766)) +- **frontend:** also convert ports to numbers when saving radarr/sonarr servers ([c53dc3b](https://github.com/sct/overseerr/commit/c53dc3b15da522c6e6ab76bbc9d15008a8a9fb9d)) +- **frontend:** new radarr/sonarr ports will be converted to a number before posting ([92c9001](https://github.com/sct/overseerr/commit/92c9001c9d1f2cbd272a5897ea1157d2cadbce2d)) + +# [1.3.0](https://github.com/sct/overseerr/compare/v1.2.0...v1.3.0) (2020-12-14) + +### Bug Fixes + +- **api:** correctly generate clientId on first startup ([5f09e83](https://github.com/sct/overseerr/commit/5f09e83ed870336638d3e9d94fcf55ead928e737)) + +### Features + +- **frontend:** add full cast page for movies and series ([051f1b3](https://github.com/sct/overseerr/commit/051f1b3e899bf749e632743e5c8d45a02b621998)) +- **lang:** translated using Weblate (Dutch) ([1ab3a4b](https://github.com/sct/overseerr/commit/1ab3a4b80a081d7e4a201f1290cd270ed5b38ac7)) +- **lang:** translated using Weblate (English) ([0949c9b](https://github.com/sct/overseerr/commit/0949c9b334b3a4b6c342517a157a9e2b7596f2f0)) +- **lang:** translated using Weblate (French) ([f943701](https://github.com/sct/overseerr/commit/f943701e13c7f0de5a711302597858cc898b16e2)) +- **lang:** translated using Weblate (French) ([30d04ce](https://github.com/sct/overseerr/commit/30d04ce35adc21070cce37ab10384154afda191b)) +- **lang:** translated using Weblate (German) ([7bf9add](https://github.com/sct/overseerr/commit/7bf9addd13a707aac23b64ef3f1733e491d40a4e)) +- **lang:** translated using Weblate (German) ([b6e60a4](https://github.com/sct/overseerr/commit/b6e60a412b30907aea751a4cf1ce0cc8230f9814)) +- **lang:** translated using Weblate (Japanese) ([08e968f](https://github.com/sct/overseerr/commit/08e968fd0097ec7b2a65de064ed5b07e7c49ef39)) +- **lang:** translated using Weblate (Norwegian Bokmål) ([83efb0e](https://github.com/sct/overseerr/commit/83efb0e3d4d96b6a2d2ebdd85d36c9d78c1717b2)) +- **lang:** translated using Weblate (Russian) ([0d8e0d0](https://github.com/sct/overseerr/commit/0d8e0d0352f72fdb65ee8f054371eae08c39fe33)) + +# [1.2.0](https://github.com/sct/overseerr/compare/v1.1.0...v1.2.0) (2020-12-11) + +### Bug Fixes + +- **frontend:** person cards now show correctly in ListView's ([ccb9855](https://github.com/sct/overseerr/commit/ccb98553f104c1aebd33796b7090cc9bbe964bd7)) +- **frontend:** properly remove site overlay when closing modals ([3fa7ff9](https://github.com/sct/overseerr/commit/3fa7ff9858d14d132151f3329164d55d74638f53)) +- **frontend:** switch to using Transition component for modals ([b16fbaf](https://github.com/sct/overseerr/commit/b16fbafa1f3d5e105c0a4ba6f1d66aa064019636)), closes [#220](https://github.com/sct/overseerr/issues/220) +- fix missing personid in Discover ([d8060af](https://github.com/sct/overseerr/commit/d8060afe02574337f51b88cab0a0f824976ac721)) +- missing personId in ListView component ([6502feb](https://github.com/sct/overseerr/commit/6502feb1a5be3c6daab33230814fe74632c87f7e)) +- **frontend:** update overflow issues with seasons + email ([#217](https://github.com/sct/overseerr/issues/217)) ([2d0afb2](https://github.com/sct/overseerr/commit/2d0afb29d37798a626e3f182571ccce43d80063c)), closes [#216](https://github.com/sct/overseerr/issues/216) +- **lang:** fix missing i18n string for agent enabled in email notification page ([42788ad](https://github.com/sct/overseerr/commit/42788adb75f7d23e68327688b1c542dd047e9609)) + +### Features + +- **lang:** update language files ([8cd067b](https://github.com/sct/overseerr/commit/8cd067b6e9df1a3c8f4056789436a31177703986)) +- person details page ([d6eb3ae](https://github.com/sct/overseerr/commit/d6eb3ae64ef46bd62145010d3029e272676487c3)) +- **lang:** add nb-NO and de language support to app ([d38b28d](https://github.com/sct/overseerr/commit/d38b28d2061b38366989ff412957a5dee5766c6f)) +- **lang:** add support for dutch language ([df94db0](https://github.com/sct/overseerr/commit/df94db050bf68a925118e0ce865d27178b702f9e)) +- **lang:** add support for russian languge ([8d8e750](https://github.com/sct/overseerr/commit/8d8e7509826514eebc859374d2e1ab212cc442d1)) +- **lang:** added translation using Weblate (Russian) ([887f5dd](https://github.com/sct/overseerr/commit/887f5dd487b61676029652d99cbc5b40213aa22e)) +- **lang:** translated using Weblate (French) ([30a8934](https://github.com/sct/overseerr/commit/30a8934626fa2d47e95b5925d7e4227a0d0aa728)) +- **lang:** translated using Weblate (German) ([44dbb74](https://github.com/sct/overseerr/commit/44dbb745b6216ce19fab4740520785c6414cf367)) +- **lang:** translated using Weblate (Japanese) ([a494507](https://github.com/sct/overseerr/commit/a494507dfeafb0cfd2bd66fb01138522e0e80737)) +- **lang:** translated using Weblate (Russian) ([86cadb8](https://github.com/sct/overseerr/commit/86cadb8283fcab8745b4c09f8429fd9e46708813)) +- **lang:** translations update from Weblate ([#201](https://github.com/sct/overseerr/issues/201)) ([b0c663b](https://github.com/sct/overseerr/commit/b0c663baccd994e234b4d41d86486c3af4906344)) + +# [1.1.0](https://github.com/sct/overseerr/compare/v1.0.0...v1.1.0) (2020-12-08) + +### Bug Fixes + +- fix a few misc unused imports and useless assignments/conditionals ([8e6daf7](https://github.com/sct/overseerr/commit/8e6daf7bd271ce5bebf4a00f5bb1144bd6b60aa5)) +- **frontend:** dont show delete button in request list for users without correct permission ([83fde46](https://github.com/sct/overseerr/commit/83fde46a59c6f1910806a6106b5526b8adbc386c)) +- **frontend:** push updated i18n locale files ([b4002d7](https://github.com/sct/overseerr/commit/b4002d71323a04e7991198cedc263660e872df8d)) + +### Features + +- generate real api key ([a839370](https://github.com/sct/overseerr/commit/a8393707fec85a9262af5ba8c03d205190b2235b)) +- **frontend:** add i18n strings for request list and request item ([6c4022f](https://github.com/sct/overseerr/commit/6c4022fb236583ad20d4c4c6693c1339e165b4af)) +- **frontend:** initial version of the requests page (no filtering/sorting) ([1ba027b](https://github.com/sct/overseerr/commit/1ba027b4357e078c3f177d9d07208049f0c1ce65)) +- **frontend:** only load request/tmdb cards when in the browser view ([2d51efd](https://github.com/sct/overseerr/commit/2d51efd71612ec969b83c62d6aa0dac6df9391a3)) + +# 1.0.0 (2020-12-06) + +### Bug Fixes + +- **api:** fix scheduling for plex full sync (maybe) ([7287a6a](https://github.com/sct/overseerr/commit/7287a6a95703b23acc0c4f6eb3beb9ec2295e33f)) +- **frontend:** always show request modal option for tv ([2b46268](https://github.com/sct/overseerr/commit/2b462688243531b4be620a942f59defd4e0534d0)) +- **frontend:** canceled movie request should set parent movie status back to unknown ([#198](https://github.com/sct/overseerr/issues/198)) ([139871f](https://github.com/sct/overseerr/commit/139871f218812a15f742aa66408db12704e0b9b5)) +- **frontend:** close request modals when complete ([85ae499](https://github.com/sct/overseerr/commit/85ae4998f0ba8d4869b9b244f2c440b9df1310d2)) +- **frontend:** dont show runtime if there is no runtime data ([e0c39ae](https://github.com/sct/overseerr/commit/e0c39aeca119b822f2a54ff05a97f91780ddd052)) +- **frontend:** fix missing data for request modal title i18n ([a56fd16](https://github.com/sct/overseerr/commit/a56fd16ab6638d4649fe9f8b9d75e7cae7742f73)) +- **frontend:** fix missing import for ReactNode type in Slider ([b26a234](https://github.com/sct/overseerr/commit/b26a2347e7b0f7ff8720a204d9faefd501ba886c)) +- **frontend:** fix modal design and rename some text for adding servers ([46d99b0](https://github.com/sct/overseerr/commit/46d99b02b1c992c7b8dde2150217ed9ce326b7a5)) +- **frontend:** fix opening popups on safari ([364d9d1](https://github.com/sct/overseerr/commit/364d9d105ca3690fcd5f635485d7c025353bb9f1)) +- **frontend:** fix request card placeholder sizes for mobile ([ef62c67](https://github.com/sct/overseerr/commit/ef62c67480ed52d753ea6db8205f035b2e9da272)) +- **frontend:** show a badge on requestcard for partially available status ([59056c4](https://github.com/sct/overseerr/commit/59056c44f942a37df536ff947b5faccc27f32246)) +- dont cross import SyncStatus type ([e032e38](https://github.com/sct/overseerr/commit/e032e385a5253d215490255c676f42ee48f39428)) +- fix type import from server side crashing build process ([89be56d](https://github.com/sct/overseerr/commit/89be56d8403ebc60c411e7cb357593edd9c79bb2)) +- **frontend:** fix title detail background image to be centered ([b92f64f](https://github.com/sct/overseerr/commit/b92f64fa6e167bc89168d8f5c0f2eb12efa0b6f0)) +- **frontend:** fixed similar/recommendations showing when empty ([#180](https://github.com/sct/overseerr/issues/180)) ([a3ca9b4](https://github.com/sct/overseerr/commit/a3ca9b40c552e6cc5effc2f57f7562ff6f723e42)) +- **frontend:** have tvDetail use the new RequestModal ([6aca826](https://github.com/sct/overseerr/commit/6aca82607b97d4a4ad74e2ea843d52fba4689e6a)) +- **frontend:** reinitalize plex form after data loads ([97e3036](https://github.com/sct/overseerr/commit/97e30367fb5d2d27efc42c1d76b0d051b6f1da76)) +- **frontend:** remove requestId from tilecard request modal component ([61b6152](https://github.com/sct/overseerr/commit/61b6152e8915c99585b944756a61d33b8c8a0307)) +- **frontend:** run initial props for children components after getting the user ([fdf9f38](https://github.com/sct/overseerr/commit/fdf9f38776b6d4c08b3505c03b354639cebb011f)) +- **frontend:** when there were no results in the list view, it would call fetch more infinitely ([c0ce87b](https://github.com/sct/overseerr/commit/c0ce87b6f65bf0ab1301c7ca61090d779709529f)) +- fixed an issue with eslint-prettier on windows ([#32](https://github.com/sct/overseerr/issues/32)) ([b673ea1](https://github.com/sct/overseerr/commit/b673ea1b18ca0f432996bb9e4e5d148af0247170)) +- fixes next.js build to not include server files ([de8ee9b](https://github.com/sct/overseerr/commit/de8ee9ba85e0160b0b472cab44f92c01796efec8)) + +### Features + +- add migration for delete cascades on season requests/seasons ([c688cf6](https://github.com/sct/overseerr/commit/c688cf60c710f0cf0b2da5ba6b0c18a2d137e7f9)) +- **api:** email notification agent ([0962392](https://github.com/sct/overseerr/commit/0962392e3930c7fdcb3164b9143cc8faca38bdfa)) +- **frontend:** add french language file ([cd6d8a8](https://github.com/sct/overseerr/commit/cd6d8a8216e7ae183b046d26cd22f3c1dc1d2b35)) +- **frontend:** add translatable strings for request card ([0d2f360](https://github.com/sct/overseerr/commit/0d2f360c22cd9bb50ae04f00a25e5fcc6c21bcdd)) +- **frontend:** added more localized strings ([659a601](https://github.com/sct/overseerr/commit/659a6018777718f7a90141307678d8dadcfd77f8)) +- actually include email templates in built server files ([a28a8b3](https://github.com/sct/overseerr/commit/a28a8b37b0afc79583e4a7191a91f73ff6d3adad)) +- add application url config to main settings ui ([a359672](https://github.com/sct/overseerr/commit/a359672ebafffef742858814f0faa918e0341aa3)) +- add filtering for requests api ([cb9ae25](https://github.com/sct/overseerr/commit/cb9ae25d94f21e97113dfea3ca45c7002089e344)) +- add trending to discover page ([ff8b9d8](https://github.com/sct/overseerr/commit/ff8b9d8e7ed228a153c2da4d237f7a4f99a79321)) +- force setup if app is not initialized ([a99705f](https://github.com/sct/overseerr/commit/a99705f6a5674b436ae28cbc558f4ee6e99ac910)) +- initial user list (no edit/delete yet) and job schedules ([24a0423](https://github.com/sct/overseerr/commit/24a0423f3b14303cfb0e83aef6e9e3bb273c5ba9)) +- manage series slideover added (and approve/decline/delete hooked up) ([236c4e5](https://github.com/sct/overseerr/commit/236c4e5e6126d2424a4badc08b7f7e6d1d70f401)) +- media delete option in manage media slideover ([250f484](https://github.com/sct/overseerr/commit/250f48492c95d74e40d95d3f026d2952157bc6e1)) +- other email notifications for approved/available ([0d73d88](https://github.com/sct/overseerr/commit/0d73d88f35b03e993f305873dc72672003c7d9e5)) +- radarr edit/create modal/backend functionality ([c4ac357](https://github.com/sct/overseerr/commit/c4ac357ef4cdd7a2c610260db46a4f0c325cd785)) +- season creation migration ([978f92a](https://github.com/sct/overseerr/commit/978f92a1c589ac404a3cb1103a68a8a5ffb0dd7d)) +- sonarr edit/delete modal ([3204326](https://github.com/sct/overseerr/commit/320432657e6ccf4d255238098e03590f28267bdb)) +- throw 404 when movie/tv show doesnt exist ([0601b44](https://github.com/sct/overseerr/commit/0601b446873e2eaf042044dd6a995b713586b0cc)) +- **api:** sonarr api wrapper / send to sonarr ([9385592](https://github.com/sct/overseerr/commit/9385592362eeba1dba05c5aa8fc7a2de1d054d74)) +- **frontend:** add header styling to movie/tv recommendation and similar list views ([f5f2545](https://github.com/sct/overseerr/commit/f5f2545520a43daa23e1276d24ff60d794ebbc6e)) +- **frontend:** add links to detail pages from new request card ([6ad3384](https://github.com/sct/overseerr/commit/6ad3384a78f7bcb03f409cce8b35cc61d634d6b2)) +- **frontend:** new design for request card ([93738e1](https://github.com/sct/overseerr/commit/93738e154c41fd11d5c6cf3d35573daf54ead471)) +- **frontend:** update favicon ([886389a](https://github.com/sct/overseerr/commit/886389a361da54c616da3bdfeee9a85e9d12bcf3)) +- notification framework ([d8e542e](https://github.com/sct/overseerr/commit/d8e542e5fe2ed76dcb20fb6dfc5f59430cd4245d)) +- notifications for media_available and media_approved ([a6c5e65](https://github.com/sct/overseerr/commit/a6c5e65bbfc196545471e99fe2e5b7194f9dd387)) +- rotten tomatoes scores on movie/tv details pages ([1694f60](https://github.com/sct/overseerr/commit/1694f60e8aa475ceeb7f170a783ec0ba70bd4bce)) +- upcoming movies on discover ([67290dd](https://github.com/sct/overseerr/commit/67290dd502571a22dcf8559ac07f42e855275bd0)) +- upcoming/trending list views and larger title cards ([94eaaf9](https://github.com/sct/overseerr/commit/94eaaf96b4302a832c52ccb72009b3593452c779)) +- upgrade tailwindcss to 2.0.1 ([fb5c791](https://github.com/sct/overseerr/commit/fb5c791b0b6b7593a472bf01713999a001f92dc7)) +- user edit functionality (managing permissions) ([185ac26](https://github.com/sct/overseerr/commit/185ac2648fd21c4bf9692ac5ac055e9c740065ca)) +- **api:** plex tv sync and recently added sync ([1390cc1](https://github.com/sct/overseerr/commit/1390cc1f130bb3975996e84b12ac833f55f2f753)) +- **frontend:** allow permission check for showing nav items ([0b239f0](https://github.com/sct/overseerr/commit/0b239f0bdfb1394897bce5c50b0d112abfbb4ad7)) +- **frontend:** alpha notice ([33da7e9](https://github.com/sct/overseerr/commit/33da7e9df3a2546b0f208bd3b1d1f268e343cead)) +- **frontend:** buttonWithDropdown component added (no hookups yet) ([4975841](https://github.com/sct/overseerr/commit/4975841b5d4ba4ed1ba8cacaa5a063eeb3b8c311)) +- **frontend:** cancel movie request modal ([1f9cbbf](https://github.com/sct/overseerr/commit/1f9cbbfdf1ac98e54de5b8777c52c7bfc69c7e20)) +- **frontend:** improved settings menu design for mobile ([16221a4](https://github.com/sct/overseerr/commit/16221a46a7d57c77f53aa0186263aa27267d9863)) +- **frontend:** initial Settings design ([8742da0](https://github.com/sct/overseerr/commit/8742da0ebb92d2f78309a998de0f67e788e14376)) +- **frontend:** plex library scan ([1bc3f7b](https://github.com/sct/overseerr/commit/1bc3f7be4b07211563a1e254c28ce51e1bc337a2)) +- **frontend:** plex settings page ([47714b6](https://github.com/sct/overseerr/commit/47714b698cf4351c1ee38bdf0b672d9f0baed03a)) +- **frontend:** radarr delete modal ([877a518](https://github.com/sct/overseerr/commit/877a5184158fb4aa371fa2ea2107032543c9aa37)) +- **frontend:** recently added on discover ([06dc606](https://github.com/sct/overseerr/commit/06dc606bcfeb50b7be1c35ac180c10738bade458)) +- **frontend:** slideover initial work ([14b9cb6](https://github.com/sct/overseerr/commit/14b9cb610c0dcfef939ebec328f371e1cdfb689d)) +- tv request modal status hookup ([5f8114f](https://github.com/sct/overseerr/commit/5f8114f730b067eb710704952824057e7b5b8fbf)) +- **.editorconfig:** add .editorconfig ([b982066](https://github.com/sct/overseerr/commit/b982066327525156f8dd0d32818d3fe7cb28f9c8)) +- **api:** add external ids to movie/tv response ([4aa7431](https://github.com/sct/overseerr/commit/4aa74319e0adcc19041239e57a00bc40fb127826)) +- **api:** add movie details endpoint ([b176148](https://github.com/sct/overseerr/commit/b1761484cb2861329763d51a868f37dd3098760d)) +- **api:** add tmdb discover api wrapper ([#67](https://github.com/sct/overseerr/issues/67)) ([839448f](https://github.com/sct/overseerr/commit/839448fcc8cc14ea83092af82e2ba3d0d92c9b73)) +- **api:** allow plex logins from users who have access to the server ([5147140](https://github.com/sct/overseerr/commit/514714071dfe4be04e607fe6412f5b3f0ef74dd4)) +- **api:** decouple media requests from media info ([8577db1](https://github.com/sct/overseerr/commit/8577db1be16f099d92c6649bbfb15f15e09a2f73)) +- **api:** discover endpoint for movie/tv ([#73](https://github.com/sct/overseerr/issues/73)) ([258bb93](https://github.com/sct/overseerr/commit/258bb93be2acc2ca32eaaefb617a5c326c5943ba)) +- **api:** initial implementation of the auth system ([#30](https://github.com/sct/overseerr/issues/30)) ([5343f35](https://github.com/sct/overseerr/commit/5343f35e5b572fe366a8712b24bd735de30e6170)) +- **api:** plex Sync (Movies) ([1be8b18](https://github.com/sct/overseerr/commit/1be8b183617c3a44ab8d4454a64b43dfe1d877fe)) +- **api:** public settings route ([#57](https://github.com/sct/overseerr/issues/57)) ([c0166e7](https://github.com/sct/overseerr/commit/c0166e7ecb5df110a4167f33338ed6406bf47f41)) +- **api:** radarr api wrapper / send to radarr when requests approved ([#93](https://github.com/sct/overseerr/issues/93)) ([48d62c3](https://github.com/sct/overseerr/commit/48d62c3178488d0d51831155ddd35cc31867db2b)) +- **api:** request api ([#80](https://github.com/sct/overseerr/issues/80)) ([f4c2c47](https://github.com/sct/overseerr/commit/f4c2c47e569e7faea7f99664966cb98b321ce952)) +- **api:** tmdb api wrapper / multi search route ([#62](https://github.com/sct/overseerr/issues/62)) ([c702c17](https://github.com/sct/overseerr/commit/c702c17cee00a52b23f685206e2d5d0c2eddf5a2)) +- **api:** tmdb trending api wrapper ([#68](https://github.com/sct/overseerr/issues/68)) ([ba34e54](https://github.com/sct/overseerr/commit/ba34e54d77d142d211df58d6ce9f53b6e673e004)) +- **api:** tv details endpoint ([a3beeed](https://github.com/sct/overseerr/commit/a3beeede7e72e99c7595673a27e38611ca4bb0cd)) +- **api:** validate plex when settings are saved ([8f6247d](https://github.com/sct/overseerr/commit/8f6247d82160704a3cfb76262696957b27641e87)) +- **api-user:** add basic User Entity and basic routing to fetch all users ([d902ef7](https://github.com/sct/overseerr/commit/d902ef72770712f2f71f33c09bca9ba99a30fc64)) +- **components/plexloginbutton:** added PlexLoginButton ([0abf743](https://github.com/sct/overseerr/commit/0abf743b17c664b58da18bdbf176f4a55ddc4179)) +- **extensions.json:** added recommended extensions for VSCode ([5dc9b51](https://github.com/sct/overseerr/commit/5dc9b510b8049516ad889c9d76a2f84daa0d2718)) +- **frontend:** add cancel request modal for titlecards ([f22f8c5](https://github.com/sct/overseerr/commit/f22f8c5d734be5cc0b1dcca869458a7321cd43a2)) +- **frontend:** approve/decline request well added to movie detail ([8f21358](https://github.com/sct/overseerr/commit/8f21358f797ed55923d90ba43acf1126856e9dfd)) +- **frontend:** basic discover page (only movies) ([#74](https://github.com/sct/overseerr/issues/74)) ([bbfe349](https://github.com/sct/overseerr/commit/bbfe349b52d308620796b37aaf986a0ed1ff0006)) +- **frontend:** design updates for responsive titlecards ([31809d9](https://github.com/sct/overseerr/commit/31809d952c8bafde3f63e2c1d952cc013149940e)) +- **frontend:** discover tv/movies full page ([be0003a](https://github.com/sct/overseerr/commit/be0003a85dc4e91799e85019aeb1110bd524a026)) +- **frontend:** initial search functionality ([#78](https://github.com/sct/overseerr/issues/78)) ([342d1a3](https://github.com/sct/overseerr/commit/342d1a3c75b32b172a51ca7d82fdfde8510abedf)) +- **frontend:** loading spinner ([de84658](https://github.com/sct/overseerr/commit/de84658b48985e24b0f92a1690387f6d59d0bc16)) +- **frontend:** logo updates ([5a43ec5](https://github.com/sct/overseerr/commit/5a43ec5405855deb244e8085484a9d2b743caba6)) +- **frontend:** modal component and basic request hookup ([#91](https://github.com/sct/overseerr/issues/91)) ([626099a](https://github.com/sct/overseerr/commit/626099a2c98fb30d0cb53d8ccf79a6bf75a00059)) +- **frontend:** new dashboard concept ([#82](https://github.com/sct/overseerr/issues/82)) ([eae38bb](https://github.com/sct/overseerr/commit/eae38bb9ec8588856f319387d2f262d7ee3f7e9c)) +- **frontend:** refresh indicator for titlecards / toasts ([4638fae](https://github.com/sct/overseerr/commit/4638fae336edc62a539796b3f55277a238683603)) +- **frontend:** request card / recent requests ([371e433](https://github.com/sct/overseerr/commit/371e43356d2c057e52368c32ffe2af1744311d91)) +- **frontend:** title detail (movie) initial version ([73ce24a](https://github.com/sct/overseerr/commit/73ce24a37bda3713e8cedc44e1ed065bdbc4ee4f)) +- **frontend/api:** beginning of new request modal ([2bf7e10](https://github.com/sct/overseerr/commit/2bf7e10e32718b36799be2feb0a7f9ff54d85744)) +- **frontend/api:** cast included with movie request and cast list on detail page ([04252f8](https://github.com/sct/overseerr/commit/04252f88bbdf51949923586feda582f86ac668ce)) +- **frontend/api:** i18n support ([9131254](https://github.com/sct/overseerr/commit/9131254f3371f12a17de44b6fa8f9bfb0e5c002e)) +- **frontend/api:** movie recommendations/similar request and frontend detail page update ([6398e36](https://github.com/sct/overseerr/commit/6398e3645a1e4ddbb9de9f4fda0a0659b4cac4d0)) +- **frontend/api:** tv details page ([02cbb5b](https://github.com/sct/overseerr/commit/02cbb5b030a3af5d62ab6c4cafdd4d800b4f61f4)) +- **frontend/api:** tv request modal (no status. only request) ([608b966](https://github.com/sct/overseerr/commit/608b96600a926adf16331b36e77789afa5d67069)) +- logout route/sign out button ([#54](https://github.com/sct/overseerr/issues/54)) ([cb9098f](https://github.com/sct/overseerr/commit/cb9098f457f79b71734959fd924b6c72ca77d61d)) +- user avatars from plex ([#53](https://github.com/sct/overseerr/issues/53)) ([e6349c1](https://github.com/sct/overseerr/commit/e6349c13a0eb0489289aa7663fcc64fa7d2906e6)) +- **layout:** created Layout component ([1f497e8](https://github.com/sct/overseerr/commit/1f497e8913146ceb9748d667e638141b2ca4612a)) +- **login component/route:** add: Login Component and Route ([6e47be2](https://github.com/sct/overseerr/commit/6e47be2fa865bcd51582ce30ebee6fd820c5f9dd)) +- **login route conditional:** on login route, do not display layout ([7d179ae](https://github.com/sct/overseerr/commit/7d179ae3b42d8ffae5e1b6e266038793260f1bbe)) +- **pass pageprops to loginpage:** pass page props to loginPage ([1597188](https://github.com/sct/overseerr/commit/159718891fb363001c650ac8b7e1446a1520ce4a)) +- **plex/utils:** added Plex OAuth class ([72f9624](https://github.com/sct/overseerr/commit/72f9624f1db721fe0324b7be9f0f811d2ae02389)) +- bootstrap the basic app structure ([89a6017](https://github.com/sct/overseerr/commit/89a6017c7f6f7637fe249ac0d667a652f44e02bb)) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 609c9d727c..b343448d06 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,53 +6,50 @@ All help is welcome and greatly appreciated! If you would like to contribute to > [!IMPORTANT] > -> Automated AI-generated contributions without human review are not allowed and will be rejected. -> This is an open-source project maintained by volunteers. -> We do not have the resources to review pull requests that could have been avoided with proper human oversight. -> While we have no issue with contributors using AI tools as an aid, it is your responsibility as a contributor to ensure that all submissions are carefully reviewed and meet our quality standards. +> Automated AI-generated contributions without human review are not allowed and will be rejected. This is an open-source project maintained by volunteers. We do not have the resources to review pull requests that could have been avoided with proper human oversight. While we have no issue with contributors using AI tools as an aid, it is your responsibility as a contributor to ensure that all submissions are carefully reviewed and meet our quality standards. > -> **We expect AI-assisted development, not AI-driven development.** -> Use AI as a tool to help you write code. Do not let an AI agent -> autonomously generate an entire contribution and submit it on your behalf. -> We have been increasingly receiving low-effort, fully AI-generated PRs -> and will not tolerate them. Contributors who repeatedly submit unreviewed -> AI output may result in a ban. +> **We expect AI-assisted development, not AI-driven development.** Use AI as a tool to help you write code. Do not let an AI agent autonomously generate an entire contribution and submit it on your behalf. It does not matter who clicks submit, an agent acting on its own or a human relaying its output unread. What matters is whether you reviewed, tested, and understood the change before it went up. We have been increasingly receiving low-effort, fully AI-generated PRs and will not tolerate them. Contributors who repeatedly submit unreviewed AI output may be banned. > -> **Submissions that appear to be unreviewed AI output will be considered low-effort and may result in a ban.** Signs of unreviewed AI output include but are not limited to: +> **Submissions that appear to be unreviewed AI output will be considered low-effort and may result in a ban if repeated even after several warnings.** Signs of unreviewed AI output include but are not limited to: > > - Blank or template-default PR descriptions > - AI-generated PR descriptions that replace our template with their own structure (e.g., "Summary / What changed / Root cause / Test plan" instead of following the PR template; this is the default output format of tools like Claude Code and is an immediate indicator that the PR was not reviewed by a human) +> - AI-generated content moved under our template's headers without being rewritten in your own words. Matching our structure is not the same as writing the description yourself > - Unchecked checklists or missing checklist entirely > - Failing CI checks that would have been caught by running `pnpm build` > - Code that does not match the described changes > - Inability to answer questions about the submitted code > > **Read and follow the [Contributing Guide](CONTRIBUTING.md) before submitting.** -> If your AI tool generates its own PR description format, it is your -> responsibility to rewrite it to follow our template before submitting. -> An incomplete PR template tells maintainers that insufficient review has -> been performed on the submission, regardless of the actual code quality. -> We may close such PRs without review. +> If your AI tool generates its own PR description format, it is your responsibility to rewrite it to follow our template before submitting, and rewrite means write the content yourself, not relabel the AI's output under our section headers. An incomplete PR template tells maintainers that insufficient review has been performed on the submission, regardless of the actual code quality. We may close such PRs without review. > > If you are using **any kind of AI assistance** to contribute to Seerr, it must be disclosed in the pull request. ### Disclosure Requirements -If you are using any kind of AI assistance while contributing to Seerr, -**this must be disclosed in the pull request description**, along with -the extent to which AI assistance was used (e.g., docs only vs. code generation). -If PR responses (comments, review replies) are being generated by AI, -disclose that as well. +If you are using any kind of AI assistance while contributing to Seerr, **this must be disclosed in the pull request description**, along with the extent to which AI assistance was used (e.g., docs only vs. code generation). -As a small exception, trivial tab-completion doesn't need to be disclosed, -so long as it is limited to single keywords or short phrases. +PR responses (comments, review replies) must be written in your own words, per the Expectations below. The one exception is AI-assisted translation, if you use AI to translate a response into English, disclose that here too. -Example disclosures: +As a small exception, trivial tab-completion doesn't need to be disclosed, so long as it is limited to single keywords or short phrases. -> **AI Disclosure:** This PR was written primarily by Claude Code. -> **AI Disclosure:** I consulted ChatGPT to understand the codebase but the solution +**Disclosure does not exempt a PR from the AI-driven development policy above.** +If the implementation was primarily generated by an AI tool in one or two prompts, and your own involvement was limited to testing or a small edit, that is AI-driven development whether or not you disclosed it. Disclosure tells us how much scrutiny to apply to your PR. It does not turn an AI-driven contribution into an AI-assisted one. + +Example disclosures that meet our bar: + +> - **AI Disclosure:** I designed the approach and wrote the implementation myself. +> I used Claude to scaffold the initial component and help debug a TypeScript +> generics issue. +> - **AI Disclosure:** I consulted ChatGPT to understand the codebase but the solution > was fully authored manually by myself. -> **AI Disclosure:** None. +> - **AI Disclosure:** None. + +A disclosure like the one below is honest, but the PR will still be closed as +AI-driven: + +> **AI Disclosure:** This PR was written primarily by Claude Code, then tested +> and lightly tweaked. When using AI assistance, we expect contributors to: @@ -62,51 +59,29 @@ When using AI assistance, we expect contributors to: filling out the PR template, testing section, and checklist. - **Run the build and tests** before submitting. -Failure to disclose AI assistance is first and foremost disrespectful to the -human maintainers on the other end of the pull request, but it also makes it -difficult to determine how much scrutiny to apply to the contribution. +Failure to disclose AI assistance is first and foremost disrespectful to the human maintainers on the other end of the pull request, but it also makes it difficult to determine how much scrutiny to apply to the contribution. -In a perfect world, AI assistance would produce equal or higher quality -work than any human. That is not the world we live in today, and in most cases -it is generating slop. +In a perfect world, AI assistance would produce equal or higher quality work than any human. That is not the world we live in today, and in most cases it is generating slop. -When using AI assistance, we expect contributors to understand the code -that is produced and be able to answer critical questions about it. It -is not a maintainer's job to review a PR so broken that it requires -significant rework to be acceptable. +When using AI assistance, we expect contributors to understand the code that is produced and be able to answer critical questions about it. It is not a maintainer's job to review a PR so broken that it requires significant rework to be acceptable. Please be respectful to maintainers and disclose AI assistance. ### Expectations for AI-Assisted Contributions -1. **PR descriptions and all comments must be your own words.** Do not paste - LLM output as your PR description, review response, or issue comment. - We want *your* understanding and explanation of the changes, not a - machine-generated summary. An exception is made for LLM-assisted - translation, however, note it explicitly if used. - -2. **Contributions must be concise and focused.** A PR that claims to fix one thing - but touches a bunch of unrelated code will be rejected. This is a - common side effect of broad AI prompts and makes review unnecessarily - difficult. - -3. **You must be able to handle review feedback yourself.** If you cannot discuss or - implement requested changes without round-tripping reviewer comments - through an AI, that tells us you don't understand the code you - submitted. We will close the PR. - -4. **Don't commit non-project files.** Editor configs, AI tool - directories, and other local tooling files do not belong in the - repository. Keep your commits clean. - -5. **Changes must be tested.** Build the project, run the tests, and - manually verify the functionality you modified. Don't just assume - CI will catch everything. - -6. **Final discretion lies with the reviewers.** If a PR cannot be - reasonably reviewed for any reason due to over-complexity, size, or poor - structure, it will be rejected. This applies equally to AI-assisted - and non-AI-assisted contributions. +1. **PR descriptions and all comments must be your own words.** Do not paste LLM output as your PR description, review response, or issue comment. We want *your* understanding and explanation of the changes, not a machine-generated summary. An exception is made for LLM-assisted translation, however, note it explicitly if used. + + **Don't be a meat proxy** (the term is borrowed from [this post](https://gruhn.me/blog/2026-08-03/)). If a maintainer asks you a question and you paste it into an AI, then paste the answer back without reading it, checking it against the code, and putting it in your own words, you haven't added anything to the exchange. We could have asked the AI ourselves and skipped the wait. This project is maintained by volunteers reviewing your contribution on their own time, and relaying answers you haven't read wastes that time rather than saving it. + +2. **Contributions must be concise and focused.** A PR that claims to fix one thing but touches a bunch of unrelated code will be rejected. This is a common side effect of broad AI prompts and makes review unnecessarily difficult. + +3. **You must be able to handle review feedback yourself.** If you cannot discuss or implement requested changes without round-tripping reviewer comments through an AI, that tells us you don't understand the code you submitted. We will close the PR. + +4. **Don't commit non-project files.** Editor configs, AI tool directories, and other local tooling files do not belong in the repository. Keep your commits clean. + +5. **Changes must be tested.** Build the project, run the tests, and manually verify the functionality you modified. Don't just assume CI will catch everything. + +6. **Final discretion lies with the reviewers.** If a PR cannot be reasonably reviewed for any reason due to over-complexity, size, or poor structure, it will be rejected. This applies equally to AI-assisted and non-AI-assisted contributions. ## Development diff --git a/Dockerfile b/Dockerfile index cceace779e..4e7fa1110d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:22.22.2-alpine3.23@sha256:8ea2348b068a9544dae7317b4f3aafcdc032df1647bb7d768a05a5cad1a7683f AS base +FROM node:22.23.2-alpine3.23@sha256:46825fbbd4e996a78b7a2cdc08d75e38a5a505bdab95dcda55605359bf124bc6 AS base ARG SOURCE_DATE_EPOCH ARG TARGETPLATFORM ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64} @@ -50,7 +50,7 @@ RUN pnpm build RUN rm -rf .next/cache -FROM node:22.22.2-alpine3.23@sha256:8ea2348b068a9544dae7317b4f3aafcdc032df1647bb7d768a05a5cad1a7683f +FROM node:22.23.2-alpine3.23@sha256:46825fbbd4e996a78b7a2cdc08d75e38a5a505bdab95dcda55605359bf124bc6 ARG SOURCE_DATE_EPOCH ARG COMMIT_TAG ENV NODE_ENV=production diff --git a/Dockerfile.local b/Dockerfile.local index da7e5710d3..a39feb8a5f 100644 --- a/Dockerfile.local +++ b/Dockerfile.local @@ -1,4 +1,4 @@ -FROM node:22.22.2-alpine3.23@sha256:8ea2348b068a9544dae7317b4f3aafcdc032df1647bb7d768a05a5cad1a7683f +FROM node:22.23.2-alpine3.23@sha256:46825fbbd4e996a78b7a2cdc08d75e38a5a505bdab95dcda55605359bf124bc6 ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PATH" diff --git a/README.md b/README.md index aafdc0224d..301020d17c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,31 @@

Seerr

+ +> [!IMPORTANT] +> A pull request to Seerr is on hold until Lidarr support is fully merged. + +> [!WARNING] +> +> ### Book Support Fork +> +> This fork adds book support to Seerr using [Hardcover](https://hardcover.app). It is currently in testing and under active development. +> +> **Before you begin, please make a backup of your database and settings.** +> +> #### Requirements +> +> - You must be running Seerr v3.1.0 or later. If you are coming from an older version, please update to v3.1.0 first. +> - A [Hardcover](https://hardcover.app) account is required. You will need to generate a personal API key from your [account settings](https://hardcover.app/account/api) and enter it in the Seerr settings page. +> +> #### Migration +> +> - If you are already running Seerr v3.1.0, simply replace your installation with this fork. Your existing database and settings will be migrated automatically on first start. +> +> #### Notes +> +> - Image caching is enabled for Hardcover content. +> - Chaptarr support is planned for a future release.

Seerr Release Seerr CI diff --git a/SECURITY.md b/SECURITY.md index 5f20209c32..b0a6eb00c0 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -12,44 +12,35 @@ To report a security issue, please use the GitHub Security Advisory ["Report a V > [!IMPORTANT] > -> Automated AI-generated contributions without human review are not allowed and will be rejected. -> This is an open-source project maintained by volunteers. -> We do not have the resources to review pull requests that could have been avoided with proper human oversight. -> While we have no issue with contributors using AI tools as an aid, it is your responsibility as a contributor to ensure that all submissions are carefully reviewed and meet our quality standards. -> Submissions that appear to be unreviewed AI output will be considered low-effort and may result in a ban. +> Automated AI-generated reports without human review are not allowed and will be rejected. This is an open-source project maintained by volunteers. We do not have the resources to review reports that could have been avoided with proper human oversight. > -> If you are using **any kind of AI assistance** to contribute to Seerr, -> it must be disclosed in the pull request. +> While we have no issue with reporters using AI tools as an aid, it is your responsibility as a reporter to ensure that all reports are carefully reviewed and meet our quality standards. Contributors who repeatedly submit unreviewed, low-effort AI output may be banned. +> +> We expect AI-assisted work, not AI-driven work. Use AI as a tool, not as a stand-in for your own review. It does not matter who submits it. Whether it's an agent acting on its own or a human relaying its output unread. What matters is whether you reviewed, verified, and understood your report or response before you sent it. +> +> If you are using **any kind of AI assistance** for a security report or response, it must be disclosed in your report. -If you are using any kind of AI assistance while contributing to Seerr, -**this must be disclosed in the pull request**, along with the extent to -which AI assistance was used (e.g. docs only vs. code generation). -If security advisory responses are being generated by an AI, disclose that as well. -As a small exception, trivial tab-completion doesn't need to be disclosed, -so long as it is limited to single keywords or short phrases. +If you are using any kind of AI assistance for a security report or response, this must be disclosed, along with the extent to which AI assistance was used (e.g. used to help investigate the issue vs. used to write the report itself). -An example disclosure: +Security advisory responses must be written in your own words, same as described below for the meat proxy problem. The one exception is AI-assisted translation, if you use AI to translate a response into English, disclose that here too. As a small exception, trivial tab-completion doesn't need to be disclosed, so long as it is limited to single keywords or short phrases. -> This security advisory was written primarily by Claude Code. +**Disclosure does not exempt a report or response from this policy.** If a security report or advisory response was primarily generated by an AI tool with little more than a copy and paste on your end, that is still AI-driven, whether or not you disclosed it. + +A more detailed disclosure that meets our bar: -Or a more detailed disclosure: +> I consulted ChatGPT to understand the codebase but the solution was fully authored manually by myself. + +A disclosure like the one below is honest, but the response will still be treated as AI-driven: + +> This security advisory was written primarily by Claude Code. -> I consulted ChatGPT to understand the codebase but the solution -> was fully authored manually by myself. +Failure to disclose this is first and foremost rude to the human operators on the other end of the report, but it also makes it difficult to determine how much scrutiny to apply to the submission. -Failure to disclose this is first and foremost rude to the human operators -on the other end of the pull request, but it also makes it difficult to -determine how much scrutiny to apply to the contribution. +Don't be a meat proxy between an AI and the security team either (the term is borrowed from [this post](https://gruhn.me/blog/2026-08-03/)). If we ask a follow-up question about your report and you relay it to an AI, then paste back an answer you haven't read or checked against the actual code, you haven't added anything, we could have asked the AI ourselves. We are volunteers handling this on our own time. Read the response, verify it, and write your own answer. -In a perfect world, AI assistance would produce equal or higher quality -work than any human. That isn't the world we live in today, and in most cases -it's generating slop. I say this despite being a fan of and using them -successfully myself (with heavy supervision)! +In a perfect world, AI assistance would produce equal or higher quality work than any human. That isn't the world we live in today, and in most cases it's generating slop. I say this despite being a fan of and using them successfully myself (with heavy supervision)! -When using AI assistance, we expect contributors to understand the code -that is produced and be able to answer critical questions about it. It -isn't a maintainers job to review a PR so broken that it requires -significant rework to be acceptable. +When using AI assistance, we expect reporters to understand the findings they're submitting and be able to answer critical questions about them. It isn't a maintainer's job to review a report so broken that it requires significant rework to be acceptable. Please be respectful to maintainers and disclose AI assistance. diff --git a/bin/duplicate-detector/.gitignore b/bin/duplicate-detector/.gitignore index c2658d7d1b..7aa28f880f 100644 --- a/bin/duplicate-detector/.gitignore +++ b/bin/duplicate-detector/.gitignore @@ -1 +1,2 @@ node_modules/ +issue_index.json diff --git a/bin/duplicate-detector/build-index.mjs b/bin/duplicate-detector/build-index.mjs index 6fb5c8091e..a6c4a58ae8 100644 --- a/bin/duplicate-detector/build-index.mjs +++ b/bin/duplicate-detector/build-index.mjs @@ -10,9 +10,8 @@ import { pipeline } from '@huggingface/transformers'; import { mkdirSync, writeFileSync } from 'node:fs'; import { dirname } from 'node:path'; -import { fetchIssues, issueText } from './utils.mjs'; +import { EMBEDDING, fetchIssues, issueText } from './utils.mjs'; -const MODEL_NAME = process.env.EMBEDDING_MODEL || 'Xenova/all-MiniLM-L6-v2'; const OUTPUT_PATH = 'issue_index.json'; const INCLUDE_CLOSED_DAYS = 90; const MAX_ISSUES = 5000; @@ -56,8 +55,8 @@ async function main() { return; } - console.log(`Loading model: ${MODEL_NAME}`); - const extractor = await pipeline('feature-extraction', MODEL_NAME, { + console.log(`Loading model: ${EMBEDDING.model}`); + const extractor = await pipeline('feature-extraction', EMBEDDING.model, { dtype: 'fp32', }); @@ -68,7 +67,7 @@ async function main() { for (let i = 0; i < texts.length; i += BATCH_SIZE) { const batch = texts.slice(i, i + BATCH_SIZE); const output = await extractor(batch, { - pooling: 'mean', + pooling: EMBEDDING.pooling, normalize: true, }); @@ -96,7 +95,7 @@ async function main() { const indexData = { issues: issueMetadata, embeddings: allEmbeddings, - model: MODEL_NAME, + model: EMBEDDING.model, issue_count: issueMetadata.length, built_at: new Date().toISOString(), }; diff --git a/bin/duplicate-detector/detect.mjs b/bin/duplicate-detector/detect.mjs index 1b002b4a4f..aa9fde31ea 100644 --- a/bin/duplicate-detector/detect.mjs +++ b/bin/duplicate-detector/detect.mjs @@ -10,6 +10,7 @@ import { pipeline } from '@huggingface/transformers'; import { existsSync, readFileSync } from 'node:fs'; import { + EMBEDDING, addLabel, dotProduct, fetchIssues, @@ -18,10 +19,8 @@ import { postComment, } from './utils.mjs'; -const SIMILARITY_THRESHOLD = 0.55; const TOP_K = 5; const MAX_COMMENT_CANDIDATES = 3; -const MODEL_NAME = process.env.EMBEDDING_MODEL || 'Xenova/all-MiniLM-L6-v2'; const GROQ_MODEL = process.env.GROQ_MODEL || 'llama-3.3-70b-versatile'; const INDEX_PATH = 'issue_index.json'; const LABEL_NAME = 'possible-duplicate'; @@ -45,7 +44,7 @@ function loadIndex(path) { function findSimilar( queryEmbedding, index, - { topK = TOP_K, threshold = SIMILARITY_THRESHOLD, excludeNumber } = {} + { topK = TOP_K, threshold = EMBEDDING.threshold, excludeNumber } = {} ) { const { issues, embeddings } = index; if (!issues.length) return []; @@ -80,8 +79,8 @@ Example: async function confirmWithLlm(newIssue, candidates) { if (!GROQ_API_KEY) { - console.warn('GROQ_API_KEY not set — skipping LLM confirmation'); - return candidates; + console.warn('GROQ_API_KEY not set - posting unconfirmed candidates'); + return candidates.map((c) => ({ ...c, unconfirmed: true })); } const candidateText = candidates @@ -161,17 +160,20 @@ async function confirmWithLlm(newIssue, candidates) { return confirmed; } catch (err) { console.warn( - `LLM confirmation failed: ${err.message} - falling back to all candidates` + `LLM confirmation failed: ${err.message} - posting unconfirmed candidates` ); - return candidates; + return candidates.map((c) => ({ ...c, unconfirmed: true })); } } function formatComment(candidates) { + const method = candidates.some((c) => c.unconfirmed) + ? 'semantic similarity only, LLM review was unavailable' + : 'semantic similarity + LLM review'; const lines = [ '**Possible duplicate detected**', '', - 'This issue may be a duplicate of the following (detected via semantic similarity + LLM review):', + `This issue may be a duplicate of the following (detected via ${method}):`, '', ]; @@ -227,19 +229,29 @@ async function main() { return; } - console.log(`Loading model: ${MODEL_NAME}`); - const extractor = await pipeline('feature-extraction', MODEL_NAME, { + const index = loadIndex(INDEX_PATH); + if (index.model !== EMBEDDING.model) { + console.log( + `Index was built with ${index.model}, expected ${EMBEDDING.model} - skipping` + ); + return; + } + + console.log(`Loading model: ${EMBEDDING.model}`); + const extractor = await pipeline('feature-extraction', EMBEDDING.model, { dtype: 'fp32', }); - const index = loadIndex(INDEX_PATH); const text = issueText(issue.title, issue.body); - const output = await extractor(text, { pooling: 'mean', normalize: true }); + const output = await extractor([text], { + pooling: EMBEDDING.pooling, + normalize: true, + }); const queryEmbedding = output.tolist()[0]; let candidates = findSimilar(queryEmbedding, index, { topK: TOP_K, - threshold: SIMILARITY_THRESHOLD, + threshold: EMBEDDING.threshold, excludeNumber: issue.number, }); diff --git a/bin/duplicate-detector/package.json b/bin/duplicate-detector/package.json index f61aa7093c..18d39bba82 100644 --- a/bin/duplicate-detector/package.json +++ b/bin/duplicate-detector/package.json @@ -9,7 +9,7 @@ "detect": "node detect.mjs" }, "dependencies": { - "@huggingface/transformers": "^3.8.1" + "@huggingface/transformers": "^4.0.0" }, "engines": { "node": ">=22.0" diff --git a/bin/duplicate-detector/pnpm-lock.yaml b/bin/duplicate-detector/pnpm-lock.yaml index 5610d9b179..69070f427e 100644 --- a/bin/duplicate-detector/pnpm-lock.yaml +++ b/bin/duplicate-detector/pnpm-lock.yaml @@ -9,162 +9,170 @@ importers: .: dependencies: '@huggingface/transformers': - specifier: ^3.8.1 - version: 3.8.1 + specifier: ^4.0.0 + version: 4.3.0 packages: - '@emnapi/runtime@1.8.1': - resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} + '@emnapi/runtime@1.11.3': + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} - '@huggingface/jinja@0.5.5': - resolution: {integrity: sha512-xRlzazC+QZwr6z4ixEqYHo9fgwhTZ3xNSdljlKfUFGZSdlvt166DljRELFUfFytlYOYvo3vTisA/AFOuOAzFQQ==} + '@huggingface/jinja@0.5.10': + resolution: {integrity: sha512-SgS1D1bglQ94ceD4ZCL6eayUDy9uV1xuyk61OjgSxU5GJh7upqZCKII0JoTYMc6wWsg3JUgaPRX0ElQzXPk3Cw==} engines: {node: '>=18'} - '@huggingface/transformers@3.8.1': - resolution: {integrity: sha512-tsTk4zVjImqdqjS8/AOZg2yNLd1z9S5v+7oUPpXaasDRwEDhB+xnglK1k5cad26lL5/ZIaeREgWWy0bs9y9pPA==} + '@huggingface/tokenizers@0.2.0': + resolution: {integrity: sha512-LidMHe1FpcSYH4vcSjXooda34pC0M8m1gmDOv7SQi6Iv+ib1zX7J3sHBCC9/3FPCkn2k1b55FneVqMRxrr99pg==} - '@img/colour@1.0.0': - resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} + '@huggingface/transformers@4.3.0': + resolution: {integrity: sha512-fL1A/WUZwouPrOlYxU5dzIwD2T5J781JiB2jDR8bFe5DwCj0Gfudq+NEXCMno49kQgajHA7xQkrRLJlqG1veEA==} + + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} - '@img/sharp-darwin-arm64@0.34.5': - resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-darwin-arm64@0.35.4': + resolution: {integrity: sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==} + engines: {node: '>=20.9.0'} cpu: [arm64] os: [darwin] - '@img/sharp-darwin-x64@0.34.5': - resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-darwin-x64@0.35.4': + resolution: {integrity: sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==} + engines: {node: '>=20.9.0'} cpu: [x64] os: [darwin] - '@img/sharp-libvips-darwin-arm64@1.2.4': - resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + '@img/sharp-freebsd-wasm32@0.35.4': + resolution: {integrity: sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==} + engines: {node: '>=20.9.0'} + os: [freebsd] + + '@img/sharp-libvips-darwin-arm64@1.3.3': + resolution: {integrity: sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==} cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.2.4': - resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + '@img/sharp-libvips-darwin-x64@1.3.3': + resolution: {integrity: sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==} cpu: [x64] os: [darwin] - '@img/sharp-libvips-linux-arm64@1.2.4': - resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + '@img/sharp-libvips-linux-arm64@1.3.3': + resolution: {integrity: sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==} cpu: [arm64] os: [linux] - '@img/sharp-libvips-linux-arm@1.2.4': - resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + '@img/sharp-libvips-linux-arm@1.3.3': + resolution: {integrity: sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==} cpu: [arm] os: [linux] - '@img/sharp-libvips-linux-ppc64@1.2.4': - resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + '@img/sharp-libvips-linux-ppc64@1.3.3': + resolution: {integrity: sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==} cpu: [ppc64] os: [linux] - '@img/sharp-libvips-linux-riscv64@1.2.4': - resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + '@img/sharp-libvips-linux-riscv64@1.3.3': + resolution: {integrity: sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==} cpu: [riscv64] os: [linux] - '@img/sharp-libvips-linux-s390x@1.2.4': - resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + '@img/sharp-libvips-linux-s390x@1.3.3': + resolution: {integrity: sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==} cpu: [s390x] os: [linux] - '@img/sharp-libvips-linux-x64@1.2.4': - resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + '@img/sharp-libvips-linux-x64@1.3.3': + resolution: {integrity: sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==} cpu: [x64] os: [linux] - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': - resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + '@img/sharp-libvips-linuxmusl-arm64@1.3.3': + resolution: {integrity: sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==} cpu: [arm64] os: [linux] - '@img/sharp-libvips-linuxmusl-x64@1.2.4': - resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + '@img/sharp-libvips-linuxmusl-x64@1.3.3': + resolution: {integrity: sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==} cpu: [x64] os: [linux] - '@img/sharp-linux-arm64@0.34.5': - resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-arm64@0.35.4': + resolution: {integrity: sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==} + engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] - '@img/sharp-linux-arm@0.34.5': - resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-arm@0.35.4': + resolution: {integrity: sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==} + engines: {node: '>=20.9.0'} cpu: [arm] os: [linux] - '@img/sharp-linux-ppc64@0.34.5': - resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-ppc64@0.35.4': + resolution: {integrity: sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==} + engines: {node: '>=20.9.0'} cpu: [ppc64] os: [linux] - '@img/sharp-linux-riscv64@0.34.5': - resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-riscv64@0.35.4': + resolution: {integrity: sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==} + engines: {node: '>=20.9.0'} cpu: [riscv64] os: [linux] - '@img/sharp-linux-s390x@0.34.5': - resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-s390x@0.35.4': + resolution: {integrity: sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==} + engines: {node: '>=20.9.0'} cpu: [s390x] os: [linux] - '@img/sharp-linux-x64@0.34.5': - resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-x64@0.35.4': + resolution: {integrity: sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==} + engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] - '@img/sharp-linuxmusl-arm64@0.34.5': - resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linuxmusl-arm64@0.35.4': + resolution: {integrity: sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==} + engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] - '@img/sharp-linuxmusl-x64@0.34.5': - resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linuxmusl-x64@0.35.4': + resolution: {integrity: sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==} + engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] - '@img/sharp-wasm32@0.34.5': - resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-wasm32@0.35.4': + resolution: {integrity: sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==} + engines: {node: '>=20.9.0'} + + '@img/sharp-webcontainers-wasm32@0.35.4': + resolution: {integrity: sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==} + engines: {node: '>=20.9.0'} cpu: [wasm32] - '@img/sharp-win32-arm64@0.34.5': - resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-win32-arm64@0.35.4': + resolution: {integrity: sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==} + engines: {node: '>=20.9.0'} cpu: [arm64] os: [win32] - '@img/sharp-win32-ia32@0.34.5': - resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-win32-ia32@0.35.4': + resolution: {integrity: sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==} + engines: {node: ^20.9.0} cpu: [ia32] os: [win32] - '@img/sharp-win32-x64@0.34.5': - resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-win32-x64@0.35.4': + resolution: {integrity: sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==} + engines: {node: '>=20.9.0'} cpu: [x64] os: [win32] - '@isaacs/fs-minipass@4.0.1': - resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} - engines: {node: '>=18.0.0'} - '@protobufjs/aspromise@1.1.2': resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} @@ -198,13 +206,9 @@ packages: '@types/node@25.2.2': resolution: {integrity: sha512-BkmoP5/FhRYek5izySdkOneRyXYN35I860MFAGupTdebyE66uZaR+bXLHq8k4DirE5DwQi3NuhvRU1jqTVwUrQ==} - boolean@3.2.0: - resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} - deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. - - chownr@3.0.0: - resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} - engines: {node: '>=18'} + adm-zip@0.6.1: + resolution: {integrity: sha512-Xwrja8nx9e5o2N1my4DsKCeKpdrnACyr1wtbPxBDgGzKzKyE9kRtBFA8mWldI+RVlD7CBZNWY/wQ2+ydwOR6kQ==} + engines: {node: '>=14.0'} define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} @@ -218,9 +222,6 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - detect-node@2.1.0: - resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} - es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -229,9 +230,6 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} - es6-error@4.1.1: - resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} - escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -239,8 +237,8 @@ packages: flatbuffers@25.9.23: resolution: {integrity: sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==} - global-agent@3.0.0: - resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==} + global-agent@4.1.3: + resolution: {integrity: sha512-KUJEViiuFT3I97t+GYMikLPJS2Lfo/S2F+DQuBWzuzaMPnvt5yyZePzArx36fBzpGTxZjIpDbXLeySLgh+k76g==} engines: {node: '>=10.0'} globalthis@1.0.4: @@ -257,40 +255,29 @@ packages: has-property-descriptors@1.0.2: resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} - json-stringify-safe@5.0.1: - resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} - long@5.3.2: resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} - matcher@3.0.0: - resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} + matcher@4.0.0: + resolution: {integrity: sha512-S6x5wmcDmsDRRU/c2dkccDwQPXoFczc5+HpQ2lON8pnvHlnvHAHj5WlLVvw6n6vNyHuVugYrFohYxbS+pvFpKQ==} engines: {node: '>=10'} - minipass@7.1.2: - resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} - engines: {node: '>=16 || 14 >=14.17'} - - minizlib@3.1.0: - resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} - engines: {node: '>= 18'} - object-keys@1.1.1: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} engines: {node: '>= 0.4'} - onnxruntime-common@1.21.0: - resolution: {integrity: sha512-Q632iLLrtCAVOTO65dh2+mNbQir/QNTVBG3h/QdZBpns7mZ0RYbLRBgGABPbpU9351AgYy7SJf1WaeVwMrBFPQ==} + onnxruntime-common@1.30.0: + resolution: {integrity: sha512-7fdVWjAID1dVhH/G8qK3APARunV4VkBFoCQAP7qp4Wkab0mrorvmc+sqiT+mKXOzDqdjN5j+/Z9nb4gzNPWcyA==} - onnxruntime-common@1.22.0-dev.20250409-89f8206ba4: - resolution: {integrity: sha512-vDJMkfCfb0b1A836rgHj+ORuZf4B4+cc2bASQtpeoJLueuFc5DuYwjIZUBrSvx/fO5IrLjLz+oTrB3pcGlhovQ==} + onnxruntime-common@1.31.0-dev.20260911-2a43ec07e: + resolution: {integrity: sha512-gBuF6U32YErKIAt+yD7DeGBpjRxhJ1uJwako6ygjokSZULDU6Pd+KWComX8Tumk1hYVHRxjPj7mdMnD0ryAPOw==} - onnxruntime-node@1.21.0: - resolution: {integrity: sha512-NeaCX6WW2L8cRCSqy3bInlo5ojjQqu2fD3D+9W5qb5irwxhEyWKXeH2vZ8W9r6VxaMPUan+4/7NDwZMtouZxEw==} + onnxruntime-node@1.30.0: + resolution: {integrity: sha512-twhs1C2C/BFkz1yc5OY0KIU2GUq6DURO7hD4bx5Q2Qy3nAMJwRXW8xU3NVczE29VA9lolLOYepoD8fjTGOfIqw==} os: [win32, darwin, linux] - onnxruntime-web@1.22.0-dev.20250409-89f8206ba4: - resolution: {integrity: sha512-0uS76OPgH0hWCPrFKlL8kYVV7ckM7t/36HfbgoFw6Nd0CZVVbQC4PkrR8mBX8LtNUFZO25IQBqV2Hx2ho3FlbQ==} + onnxruntime-web@1.31.0-dev.20260914-8d85527a0: + resolution: {integrity: sha512-Iy7rtadoBgxS/LLvDr3QW38DB1PNXRnr0GJMcL0TAt7c9qjgVQl83UlGCVyeAnK2InpmW8Uc3PL8XIuqtDeF6g==} platform@1.3.6: resolution: {integrity: sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==} @@ -299,162 +286,160 @@ packages: resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} engines: {node: '>=12.0.0'} - roarr@2.15.4: - resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} - engines: {node: '>=8.0'} - - semver-compare@1.0.0: - resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} - - semver@7.7.4: - resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} hasBin: true - serialize-error@7.0.1: - resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} + serialize-error@8.1.0: + resolution: {integrity: sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==} engines: {node: '>=10'} - sharp@0.34.5: - resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - - sprintf-js@1.1.3: - resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} - - tar@7.5.7: - resolution: {integrity: sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==} - engines: {node: '>=18'} + sharp@0.35.4: + resolution: {integrity: sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==} + engines: {node: '>=20.9.0'} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - type-fest@0.13.1: - resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} engines: {node: '>=10'} undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} - yallist@5.0.0: - resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} - engines: {node: '>=18'} - snapshots: - '@emnapi/runtime@1.8.1': + '@emnapi/runtime@1.11.3': dependencies: tslib: 2.8.1 optional: true - '@huggingface/jinja@0.5.5': {} + '@huggingface/jinja@0.5.10': {} - '@huggingface/transformers@3.8.1': + '@huggingface/tokenizers@0.2.0': {} + + '@huggingface/transformers@4.3.0': dependencies: - '@huggingface/jinja': 0.5.5 - onnxruntime-node: 1.21.0 - onnxruntime-web: 1.22.0-dev.20250409-89f8206ba4 - sharp: 0.34.5 + '@huggingface/jinja': 0.5.10 + '@huggingface/tokenizers': 0.2.0 + onnxruntime-node: 1.30.0 + onnxruntime-web: 1.31.0-dev.20260914-8d85527a0 + sharp: 0.35.4 + transitivePeerDependencies: + - '@types/node' - '@img/colour@1.0.0': {} + '@img/colour@1.1.0': {} - '@img/sharp-darwin-arm64@0.34.5': + '@img/sharp-darwin-arm64@0.35.4': optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-arm64': 1.3.3 optional: true - '@img/sharp-darwin-x64@0.34.5': + '@img/sharp-darwin-x64@0.35.4': optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.3.3 optional: true - '@img/sharp-libvips-darwin-arm64@1.2.4': + '@img/sharp-freebsd-wasm32@0.35.4': + dependencies: + '@img/sharp-wasm32': 0.35.4 optional: true - '@img/sharp-libvips-darwin-x64@1.2.4': + '@img/sharp-libvips-darwin-arm64@1.3.3': optional: true - '@img/sharp-libvips-linux-arm64@1.2.4': + '@img/sharp-libvips-darwin-x64@1.3.3': optional: true - '@img/sharp-libvips-linux-arm@1.2.4': + '@img/sharp-libvips-linux-arm64@1.3.3': optional: true - '@img/sharp-libvips-linux-ppc64@1.2.4': + '@img/sharp-libvips-linux-arm@1.3.3': optional: true - '@img/sharp-libvips-linux-riscv64@1.2.4': + '@img/sharp-libvips-linux-ppc64@1.3.3': optional: true - '@img/sharp-libvips-linux-s390x@1.2.4': + '@img/sharp-libvips-linux-riscv64@1.3.3': optional: true - '@img/sharp-libvips-linux-x64@1.2.4': + '@img/sharp-libvips-linux-s390x@1.3.3': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + '@img/sharp-libvips-linux-x64@1.3.3': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.2.4': + '@img/sharp-libvips-linuxmusl-arm64@1.3.3': optional: true - '@img/sharp-linux-arm64@0.34.5': + '@img/sharp-libvips-linuxmusl-x64@1.3.3': + optional: true + + '@img/sharp-linux-arm64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.3.3 optional: true - '@img/sharp-linux-arm@0.34.5': + '@img/sharp-linux-arm@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.3.3 optional: true - '@img/sharp-linux-ppc64@0.34.5': + '@img/sharp-linux-ppc64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.3.3 optional: true - '@img/sharp-linux-riscv64@0.34.5': + '@img/sharp-linux-riscv64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.3.3 optional: true - '@img/sharp-linux-s390x@0.34.5': + '@img/sharp-linux-s390x@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.3.3 optional: true - '@img/sharp-linux-x64@0.34.5': + '@img/sharp-linux-x64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.3.3 optional: true - '@img/sharp-linuxmusl-arm64@0.34.5': + '@img/sharp-linuxmusl-arm64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.3 optional: true - '@img/sharp-linuxmusl-x64@0.34.5': + '@img/sharp-linuxmusl-x64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.3.3 optional: true - '@img/sharp-wasm32@0.34.5': + '@img/sharp-wasm32@0.35.4': dependencies: - '@emnapi/runtime': 1.8.1 + '@emnapi/runtime': 1.11.3 optional: true - '@img/sharp-win32-arm64@0.34.5': + '@img/sharp-webcontainers-wasm32@0.35.4': + dependencies: + '@img/sharp-wasm32': 0.35.4 optional: true - '@img/sharp-win32-ia32@0.34.5': + '@img/sharp-win32-arm64@0.35.4': optional: true - '@img/sharp-win32-x64@0.34.5': + '@img/sharp-win32-ia32@0.35.4': optional: true - '@isaacs/fs-minipass@4.0.1': - dependencies: - minipass: 7.1.2 + '@img/sharp-win32-x64@0.35.4': + optional: true '@protobufjs/aspromise@1.1.2': {} @@ -483,9 +468,7 @@ snapshots: dependencies: undici-types: 7.16.0 - boolean@3.2.0: {} - - chownr@3.0.0: {} + adm-zip@0.6.1: {} define-data-property@1.1.4: dependencies: @@ -501,26 +484,20 @@ snapshots: detect-libc@2.1.2: {} - detect-node@2.1.0: {} - es-define-property@1.0.1: {} es-errors@1.3.0: {} - es6-error@4.1.1: {} - escape-string-regexp@4.0.0: {} flatbuffers@25.9.23: {} - global-agent@3.0.0: + global-agent@4.1.3: dependencies: - boolean: 3.2.0 - es6-error: 4.1.1 - matcher: 3.0.0 - roarr: 2.15.4 - semver: 7.7.4 - serialize-error: 7.0.1 + globalthis: 1.0.4 + matcher: 4.0.0 + semver: 7.8.5 + serialize-error: 8.1.0 globalthis@1.0.4: dependencies: @@ -535,38 +512,30 @@ snapshots: dependencies: es-define-property: 1.0.1 - json-stringify-safe@5.0.1: {} - long@5.3.2: {} - matcher@3.0.0: + matcher@4.0.0: dependencies: escape-string-regexp: 4.0.0 - minipass@7.1.2: {} - - minizlib@3.1.0: - dependencies: - minipass: 7.1.2 - object-keys@1.1.1: {} - onnxruntime-common@1.21.0: {} + onnxruntime-common@1.30.0: {} - onnxruntime-common@1.22.0-dev.20250409-89f8206ba4: {} + onnxruntime-common@1.31.0-dev.20260911-2a43ec07e: {} - onnxruntime-node@1.21.0: + onnxruntime-node@1.30.0: dependencies: - global-agent: 3.0.0 - onnxruntime-common: 1.21.0 - tar: 7.5.7 + adm-zip: 0.6.1 + global-agent: 4.1.3 + onnxruntime-common: 1.30.0 - onnxruntime-web@1.22.0-dev.20250409-89f8206ba4: + onnxruntime-web@1.31.0-dev.20260914-8d85527a0: dependencies: flatbuffers: 25.9.23 guid-typescript: 1.0.9 long: 5.3.2 - onnxruntime-common: 1.22.0-dev.20250409-89f8206ba4 + onnxruntime-common: 1.31.0-dev.20260911-2a43ec07e platform: 1.3.6 protobufjs: 7.5.4 @@ -587,69 +556,47 @@ snapshots: '@types/node': 25.2.2 long: 5.3.2 - roarr@2.15.4: - dependencies: - boolean: 3.2.0 - detect-node: 2.1.0 - globalthis: 1.0.4 - json-stringify-safe: 5.0.1 - semver-compare: 1.0.0 - sprintf-js: 1.1.3 + semver@7.8.5: {} - semver-compare@1.0.0: {} - - semver@7.7.4: {} - - serialize-error@7.0.1: + serialize-error@8.1.0: dependencies: - type-fest: 0.13.1 + type-fest: 0.20.2 - sharp@0.34.5: + sharp@0.35.4: dependencies: - '@img/colour': 1.0.0 + '@img/colour': 1.1.0 detect-libc: 2.1.2 - semver: 7.7.4 + semver: 7.8.5 optionalDependencies: - '@img/sharp-darwin-arm64': 0.34.5 - '@img/sharp-darwin-x64': 0.34.5 - '@img/sharp-libvips-darwin-arm64': 1.2.4 - '@img/sharp-libvips-darwin-x64': 1.2.4 - '@img/sharp-libvips-linux-arm': 1.2.4 - '@img/sharp-libvips-linux-arm64': 1.2.4 - '@img/sharp-libvips-linux-ppc64': 1.2.4 - '@img/sharp-libvips-linux-riscv64': 1.2.4 - '@img/sharp-libvips-linux-s390x': 1.2.4 - '@img/sharp-libvips-linux-x64': 1.2.4 - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 - '@img/sharp-linux-arm': 0.34.5 - '@img/sharp-linux-arm64': 0.34.5 - '@img/sharp-linux-ppc64': 0.34.5 - '@img/sharp-linux-riscv64': 0.34.5 - '@img/sharp-linux-s390x': 0.34.5 - '@img/sharp-linux-x64': 0.34.5 - '@img/sharp-linuxmusl-arm64': 0.34.5 - '@img/sharp-linuxmusl-x64': 0.34.5 - '@img/sharp-wasm32': 0.34.5 - '@img/sharp-win32-arm64': 0.34.5 - '@img/sharp-win32-ia32': 0.34.5 - '@img/sharp-win32-x64': 0.34.5 - - sprintf-js@1.1.3: {} - - tar@7.5.7: - dependencies: - '@isaacs/fs-minipass': 4.0.1 - chownr: 3.0.0 - minipass: 7.1.2 - minizlib: 3.1.0 - yallist: 5.0.0 + '@img/sharp-darwin-arm64': 0.35.4 + '@img/sharp-darwin-x64': 0.35.4 + '@img/sharp-freebsd-wasm32': 0.35.4 + '@img/sharp-libvips-darwin-arm64': 1.3.3 + '@img/sharp-libvips-darwin-x64': 1.3.3 + '@img/sharp-libvips-linux-arm': 1.3.3 + '@img/sharp-libvips-linux-arm64': 1.3.3 + '@img/sharp-libvips-linux-ppc64': 1.3.3 + '@img/sharp-libvips-linux-riscv64': 1.3.3 + '@img/sharp-libvips-linux-s390x': 1.3.3 + '@img/sharp-libvips-linux-x64': 1.3.3 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.3 + '@img/sharp-libvips-linuxmusl-x64': 1.3.3 + '@img/sharp-linux-arm': 0.35.4 + '@img/sharp-linux-arm64': 0.35.4 + '@img/sharp-linux-ppc64': 0.35.4 + '@img/sharp-linux-riscv64': 0.35.4 + '@img/sharp-linux-s390x': 0.35.4 + '@img/sharp-linux-x64': 0.35.4 + '@img/sharp-linuxmusl-arm64': 0.35.4 + '@img/sharp-linuxmusl-x64': 0.35.4 + '@img/sharp-webcontainers-wasm32': 0.35.4 + '@img/sharp-win32-arm64': 0.35.4 + '@img/sharp-win32-ia32': 0.35.4 + '@img/sharp-win32-x64': 0.35.4 tslib@2.8.1: optional: true - type-fest@0.13.1: {} + type-fest@0.20.2: {} undici-types@7.16.0: {} - - yallist@5.0.0: {} diff --git a/bin/duplicate-detector/utils.mjs b/bin/duplicate-detector/utils.mjs index 4f20b85f9f..6117f12511 100644 --- a/bin/duplicate-detector/utils.mjs +++ b/bin/duplicate-detector/utils.mjs @@ -1,3 +1,9 @@ +export const EMBEDDING = { + model: 'Xenova/bge-small-en-v1.5', + pooling: 'cls', + threshold: 0.82, +}; + const GITHUB_API = 'https://api.github.com'; const GITHUB_TOKEN = process.env.GITHUB_TOKEN; const GITHUB_REPOSITORY = process.env.GITHUB_REPOSITORY; @@ -12,6 +18,7 @@ function ghHeaders() { export async function fetchIssues({ state = 'open', since, + creator, maxIssues = 5000, } = {}) { const issues = []; @@ -27,6 +34,7 @@ export async function fetchIssues({ direction: 'desc', }); if (since) params.set('since', since); + if (creator) params.set('creator', creator); const url = `${GITHUB_API}/repos/${GITHUB_REPOSITORY}/issues?${params}`; const resp = await fetch(url, { headers: ghHeaders() }); @@ -101,8 +109,12 @@ export async function addLabel(issueNumber, label) { console.log(`Added label '${label}' to #${issueNumber}`); } +const FORM_NOISE = [/^###\s.*$/gm, /^_No response_$/gm, /^- \[[ xX]\] .*$/gm]; + export function issueText(title, body) { body = (body || '').trim(); + for (const pattern of FORM_NOISE) body = body.replace(pattern, ''); + body = body.replace(/\n{3,}/g, '\n\n').trim(); if (body.length > 2000) body = body.slice(0, 2000) + '...'; return body ? `${title}\n\n${body}` : title; } diff --git a/bin/prepare.js b/bin/prepare.mjs similarity index 63% rename from bin/prepare.js rename to bin/prepare.mjs index 5f19878a1d..3d195c4e23 100644 --- a/bin/prepare.js +++ b/bin/prepare.mjs @@ -5,5 +5,6 @@ */ const isCi = process.env.CI !== undefined; if (!isCi) { - require('husky').install(); + const husky = (await import('husky')).default; + console.log(husky()); } diff --git a/charts/seerr-chart/Chart.yaml b/charts/seerr-chart/Chart.yaml index 88d156b4bf..7a568eac6a 100644 --- a/charts/seerr-chart/Chart.yaml +++ b/charts/seerr-chart/Chart.yaml @@ -3,9 +3,9 @@ kubeVersion: '>=1.23.0-0' name: seerr-chart description: Seerr helm chart for Kubernetes type: application -version: 3.6.0 +version: 3.9.1 # renovate: image=ghcr.io/seerr-team/seerr -appVersion: 'v3.2.0' +appVersion: 'v3.4.1' maintainers: - name: Seerr Team url: https://github.com/orgs/seerr-team/people diff --git a/charts/seerr-chart/README.md b/charts/seerr-chart/README.md index a41c6e7ace..b3df682520 100644 --- a/charts/seerr-chart/README.md +++ b/charts/seerr-chart/README.md @@ -1,6 +1,6 @@ # seerr-chart -![Version: 3.6.0](https://img.shields.io/badge/Version-3.6.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v3.2.0](https://img.shields.io/badge/AppVersion-v3.2.0-informational?style=flat-square) +![Version: 3.9.1](https://img.shields.io/badge/Version-3.9.1-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v3.4.1](https://img.shields.io/badge/AppVersion-v3.4.1-informational?style=flat-square) Seerr helm chart for Kubernetes @@ -53,9 +53,12 @@ If `replicaCount` value was used - remove it. Helm update should work fine after | config.persistence.storageClass | string | `""` | Storage class for the PVC. Set to "-" to disable dynamic provisioning. Uses default storage class if no value is provided | | config.persistence.subPath | string | `""` | Subpath of the pvc which should be mounted | | config.persistence.volumeName | string | `""` | Name of the permanent volume to reference in the claim. Can be used to bind to existing volumes. | +| dnsConfig | object | `{}` | docs: https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#pod-dns-config | +| dnsPolicy | string | `""` | docs: https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#pod-s-dns-policy | | extraEnv | list | `[]` | Environment variables to add to the seerr pods | | extraEnvFrom | list | `[]` | Environment variables from secrets or configmaps to add to the seerr pods | | fullnameOverride | string | `""` | | +| hostUsers | bool | `true` | docs: https://kubernetes.io/docs/concepts/workloads/pods/user-namespaces/ | | image.pullPolicy | string | `"IfNotPresent"` | | | image.registry | string | `"ghcr.io"` | | | image.repository | string | `"seerr-team/seerr"` | | @@ -75,6 +78,7 @@ If `replicaCount` value was used - remove it. Helm update should work fine after | podLabels | object | `{}` | | | podSecurityContext.fsGroup | int | `1000` | | | podSecurityContext.fsGroupChangePolicy | string | `"OnRootMismatch"` | | +| priorityClassName | string | `nil` | Specify a priorityclass, or use default if unset. | | probes.livenessProbe | object | `{"initialDelaySeconds":20,"periodSeconds":15,"timeoutSeconds":3}` | Configure liveness probe | | probes.readinessProbe | object | `{"initialDelaySeconds":60,"periodSeconds":15,"timeoutSeconds":3}` | Configure readiness probe | | probes.startupProbe | string | `nil` | Configure startup probe | @@ -99,9 +103,9 @@ If `replicaCount` value was used - remove it. Helm update should work fine after | securityContext.runAsNonRoot | bool | `true` | | | securityContext.runAsUser | int | `1000` | | | securityContext.seccompProfile.type | string | `"RuntimeDefault"` | | +| service.annotations | object | `{}` | | | service.port | int | `80` | | | service.type | string | `"ClusterIP"` | | -| service.annotations | object | {} | | | serviceAccount.annotations | object | `{}` | Annotations to add to the service account | | serviceAccount.automount | bool | `true` | Automatically mount a ServiceAccount's API credentials? | | serviceAccount.create | bool | `true` | Specifies whether a service account should be created | diff --git a/charts/seerr-chart/templates/statefulset.yaml b/charts/seerr-chart/templates/statefulset.yaml index 1888ea4199..1ce545fff2 100644 --- a/charts/seerr-chart/templates/statefulset.yaml +++ b/charts/seerr-chart/templates/statefulset.yaml @@ -28,6 +28,19 @@ spec: serviceAccountName: {{ include "seerr.serviceAccountName" . }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} + {{- with .Values.priorityClassName }} + priorityClassName: {{ . }} + {{- end }} + {{- if kindIs "bool" .Values.hostUsers }} + hostUsers: {{ .Values.hostUsers }} + {{- end }} + {{- with .Values.dnsPolicy }} + dnsPolicy: {{ . }} + {{- end }} + {{- with .Values.dnsConfig }} + dnsConfig: + {{- toYaml . | nindent 8 }} + {{- end }} containers: - name: {{ .Chart.Name }} securityContext: diff --git a/charts/seerr-chart/values.yaml b/charts/seerr-chart/values.yaml index 5321f44a7a..adc8a97651 100644 --- a/charts/seerr-chart/values.yaml +++ b/charts/seerr-chart/values.yaml @@ -67,6 +67,31 @@ securityContext: seccompProfile: type: RuntimeDefault +# -- Set a DNS policy, likely not needed unless seerr only interacts with external services. +# -- docs: https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#pod-s-dns-policy +dnsPolicy: '' + +# -- Set DNS config options +# -- Setting ndots lower can prevent queries like `raw.githubusercontent.com.seerr.svc.cluster.local.` +# -- docs: https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#pod-dns-config +dnsConfig: {} +# nameservers: +# - 1.2.3.4 +# searches: +# - ns1.svc.cluster-domain.example +# - my.dns.search.suffix +# options: +# - name: ndots +# value: "2" + +# -- Set to false to run in an idmaped user namespace for additional isolation +# -- requires cluster support, and PV support of idmaped volumes. +# -- docs: https://kubernetes.io/docs/concepts/workloads/pods/user-namespaces/ +hostUsers: true + +# -- Specify a priorityclass, or use default if unset. +priorityClassName: null + service: type: ClusterIP port: 80 diff --git a/cypress.config.ts b/cypress.config.ts index 0fa88d2f9a..5db5e200ba 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -9,7 +9,7 @@ export default defineConfig({ env: { ADMIN_EMAIL: 'admin@seerr.dev', ADMIN_PASSWORD: 'test1234', - USER_EMAIL: 'friend@seerr.dev', + USER_EMAIL: 'demo@seerr.dev', USER_PASSWORD: 'test1234', }, retries: { diff --git a/cypress/e2e/discover.cy.ts b/cypress/e2e/discover.cy.ts index 52a5e883a1..66bc7713c9 100644 --- a/cypress/e2e/discover.cy.ts +++ b/cypress/e2e/discover.cy.ts @@ -145,7 +145,7 @@ describe('Discover', () => { requestedBy: { permissions: 4194336, id: 18, - email: 'friend@seerr.dev', + email: 'demo@seerr.dev', plexUsername: null, username: '', recoveryLinkExpirationDate: null, @@ -159,7 +159,7 @@ describe('Discover', () => { createdAt: '2022-08-17T04:55:28.000Z', updatedAt: '2022-08-17T04:55:28.000Z', requestCount: 1, - displayName: 'friend@seerr.dev', + displayName: 'demo@seerr.dev', }, seasonCount: 0, }, diff --git a/cypress/e2e/movie-details.cy.ts b/cypress/e2e/movie-details.cy.ts index 1d3ecf3f13..0c2eeea56d 100644 --- a/cypress/e2e/movie-details.cy.ts +++ b/cypress/e2e/movie-details.cy.ts @@ -9,4 +9,20 @@ describe('Movie Details', () => { 'Minions: The Rise of Gru (2022)' ); }); + + it('does not reopen the manager panel after closing and going back', () => { + cy.loginAsAdmin(); + + cy.visit('/movie/438148'); + cy.visit('/movie/438148?manage=1'); + + cy.get('button[aria-label="Close panel"]').should('be.visible').click(); + cy.location('search').should('eq', ''); + cy.get('button[aria-label="Close panel"]').should('not.exist'); + + cy.go('back'); + + cy.location('search').should('eq', ''); + cy.get('button[aria-label="Close panel"]').should('not.exist'); + }); }); diff --git a/cypress/e2e/settings/general-settings.cy.ts b/cypress/e2e/settings/general-settings.cy.ts index 59649ed6b1..114e37a5f9 100644 --- a/cypress/e2e/settings/general-settings.cy.ts +++ b/cypress/e2e/settings/general-settings.cy.ts @@ -3,6 +3,11 @@ describe('General Settings', () => { cy.loginAsAdmin(); }); + // A mid-test failure skips the in-test revert, leaving the restart modal up for every later spec. + afterEach(() => { + cy.request('POST', '/api/v1/settings/network', { trustProxy: false }); + }); + it('opens the settings page from the home page', () => { cy.visit('/'); diff --git a/devenv.lock b/devenv.lock new file mode 100644 index 0000000000..0a451c2da9 --- /dev/null +++ b/devenv.lock @@ -0,0 +1,65 @@ +{ + "nodes": { + "devenv": { + "locked": { + "dir": "src/modules", + "lastModified": 1780991472, + "narHash": "sha256-5SglTZTbq5xyrhgH0+1xIZ92lC+W1aNMm9tEvFt4k9U=", + "owner": "cachix", + "repo": "devenv", + "rev": "822871663d1568432b8eef86a714352519b158b8", + "type": "github" + }, + "original": { + "dir": "src/modules", + "owner": "cachix", + "repo": "devenv", + "type": "github" + } + }, + "nixpkgs": { + "inputs": { + "nixpkgs-src": "nixpkgs-src" + }, + "locked": { + "lastModified": 1778507786, + "narHash": "sha256-HzSQCKMsMr8r55LwM1JuzIOB+8bzk0FEv6sItKvsfoY=", + "owner": "cachix", + "repo": "devenv-nixpkgs", + "rev": "8f24a228a782e24576b155d1e39f0d914b380691", + "type": "github" + }, + "original": { + "owner": "cachix", + "ref": "rolling", + "repo": "devenv-nixpkgs", + "type": "github" + } + }, + "nixpkgs-src": { + "flake": false, + "locked": { + "lastModified": 1778274207, + "narHash": "sha256-I4puXmX1iovcCHZlRmztO3vW0mAbbRvq4F8wgIMQ1MM=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "b3da656039dc7a6240f27b2ef8cc6a3ef3bccae7", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "devenv": "devenv", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} \ No newline at end of file diff --git a/devenv.nix b/devenv.nix new file mode 100644 index 0000000000..7c1c502af8 --- /dev/null +++ b/devenv.nix @@ -0,0 +1,24 @@ +{ + pkgs, + lib, + config, + ... +}: +{ + packages = [ + pkgs.prettier + pkgs.husky + pkgs.helm-docs + ]; + + languages.javascript = { + enable = true; + package = pkgs.nodejs_22; + nodejs.enable = true; + pnpm = { + enable = true; + package = pkgs.pnpm_10; + install.enable = true; + }; + }; +} diff --git a/devenv.yaml b/devenv.yaml new file mode 100644 index 0000000000..6bf1e6c17d --- /dev/null +++ b/devenv.yaml @@ -0,0 +1,3 @@ +inputs: + nixpkgs: + url: github:cachix/devenv-nixpkgs/rolling diff --git a/docs/api/_category_.json b/docs/api/_category_.json deleted file mode 100644 index 7543e68319..0000000000 --- a/docs/api/_category_.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "label": "REST API documentation", - "position": 4, - "link": { - "type": "generated-index", - "title": "REST API documentation", - "description": "Create integrations with the Seerr REST API." - } -} diff --git a/docs/extending-seerr/database-config.mdx b/docs/extending-seerr/database-config.mdx index edf101afd6..0d3dc8e687 100644 --- a/docs/extending-seerr/database-config.mdx +++ b/docs/extending-seerr/database-config.mdx @@ -36,6 +36,7 @@ DB_USER= # (required) Username used to connect to the database. DB_PASS= # (required) Password of the user used to connect to the database. DB_NAME="seerr" # (optional) The name of the database to connect to. The default is "seerr". DB_POOL_SIZE="10" # (optional) The number of connections to maintain in the connection pool. The default is "10". +DB_CONNECT_TIMEOUT_MS="30000" # (optional) How long to wait for a free connection from the pool before erroring, in milliseconds. The default is "30000". Set to "0" to wait indefinitely. DB_LOG_QUERIES="false" # (optional) Whether to log the DB queries for debugging. The default is "false". ``` @@ -50,6 +51,7 @@ DB_USER= # (required) Username used to connect to the database. DB_PASS= # (optional) Password of the user used to connect to the database, depending on the server's authentication configuration. DB_NAME="seerr" # (optional) The name of the database to connect to. The default is "seerr". DB_POOL_SIZE="10" # (optional) The number of connections to maintain in the connection pool. The default is "10". +DB_CONNECT_TIMEOUT_MS="30000" # (optional) How long to wait for a free connection from the pool before erroring, in milliseconds. The default is "30000". Set to "0" to wait indefinitely. DB_LOG_QUERIES="false" # (optional) Whether to log the DB queries for debugging. The default is "false". ``` diff --git a/docs/getting-started/docker.mdx b/docs/getting-started/docker.mdx index b6a12ec290..d4dba819cc 100644 --- a/docs/getting-started/docker.mdx +++ b/docs/getting-started/docker.mdx @@ -68,6 +68,8 @@ docker run -d \ --health-timeout 3s \ --health-interval 15s \ --health-retries 3 \ + --security-opt no-new-privileges \ + --cap-drop ALL \ ghcr.io/seerr-team/seerr:latest ``` @@ -123,6 +125,10 @@ services: timeout: 3s interval: 15s retries: 3 + security_opt: + - no-new-privileges + cap_drop: + - ALL restart: unless-stopped ``` @@ -186,6 +192,8 @@ docker run -d ` --health-timeout 3s ` --health-interval 15s ` --health-retries 3 ` + --security-opt no-new-privileges ` + --cap-drop ALL ` ghcr.io/seerr-team/seerr:latest ``` @@ -224,6 +232,8 @@ docker run -d ^ --health-timeout 3s ^ --health-interval 15s ^ --health-retries 3 ^ + --security-opt no-new-privileges ^ + --cap-drop ALL ^ ghcr.io/seerr-team/seerr:latest ``` @@ -267,6 +277,10 @@ services: timeout: 3s interval: 15s retries: 3 + security_opt: + - no-new-privileges + cap_drop: + - ALL restart: unless-stopped volumes: diff --git a/docs/getting-started/kubernetes.mdx b/docs/getting-started/kubernetes.mdx index c55e3051d7..b92e767b3a 100644 --- a/docs/getting-started/kubernetes.mdx +++ b/docs/getting-started/kubernetes.mdx @@ -19,8 +19,3 @@ To confirm that the chart you are using is authentic and unmodified, please refe helm install seerr oci://ghcr.io/seerr-team/seerr/seerr-chart ``` Helm values can be found in the Seerr repository under [charts/seerr-chart/README.md](https://github.com/seerr-team/seerr/tree/develop/charts/seerr-chart). - -Verify the signature with [cosign](https://docs.sigstore.dev/cosign/system_config/installation/) (replace [tag], with the TAG you want to verify) : -```console -cosign verify ghcr.io/seerr-team/seerr/seerr-chart:[tag] --certificate-identity=https://github.com/seerr-team/seerr/.github/workflows/helm.yml@refs/heads/main --certificate-oidc-issuer=https://token.actions.githubusercontent.com -``` diff --git a/docs/getting-started/nixpkg.mdx b/docs/getting-started/nixpkg.mdx index 43cee97cbb..f8d0c0fee6 100644 --- a/docs/getting-started/nixpkg.mdx +++ b/docs/getting-started/nixpkg.mdx @@ -12,15 +12,11 @@ import TabItem from '@theme/TabItem'; # Nix Package Manager :::warning This method is not recommended for most users. It is intended for advanced users who are using NixOS distribution. +The Seerr service and package are available as of the 26.05 release. ::: -:::danger -The seerr service and package are available in the unstable channel only and will be officially included in the 26.05 release. -::: - -Refer to [NixOS documentation](https://search.nixos.org/options?channel=unstable&query=seerr) +Refer to [NixOS documentation](https://search.nixos.org/options?channel=26.05&query=seerr) - ## Installation To get up and running with seerr using Nix, you can add the following to your `configuration.nix`: diff --git a/docs/migration-guide.mdx b/docs/migration-guide.mdx index c15667ad0c..3486f38703 100644 --- a/docs/migration-guide.mdx +++ b/docs/migration-guide.mdx @@ -222,7 +222,7 @@ Summary of changes : Refer to [Seerr Documentation](/getting-started/nixpkg), all of our examples have been updated to reflect the below change. -The seerr service and package are available in the unstable channel and will be officially included in the 26.05 release. +The Seerr service and package are available as of the 26.05 release. ## Third-party installation methods @@ -238,6 +238,8 @@ See https://aur.archlinux.org/packages/seerr Refer to [Seerr TrueNAS Documentation](/getting-started/third-parties/truenas), all of our examples have been updated to reflect the below change. +For detailed community-maintained TrueNAS SCALE walkthroughs, see the [Host Path setup](https://github.com/truenas/apps/issues/3374#issuecomment-3916418872) and [ixVolume setup](https://github.com/truenas/apps/issues/3374#issuecomment-3916700870) guides. + **This guide describes how to migrate from Host Path storage (not ixVolume).** @@ -265,7 +267,7 @@ Refer to [Seerr TrueNAS Documentation](/getting-started/third-parties/truenas), 3. Copy ixVolume Data Open System Settings → Shell, or SSH into your TrueNAS server as root and run : ```bash - rsync -av /mnt/.ix-apps/app_mounts/jellyseerr/ /mnt/storage/seerr/ + rsync -av --chown=568:568 /mnt/.ix-apps/app_mounts/jellyseerr/ /mnt/storage/seerr/ ``` 4. Install Seerr and use the same Host Path storage that was created before (`/mnt/storage/seerr/config` in our example) 5. Start Seerr app diff --git a/docs/using-seerr/advanced/verifying-signed-artifacts.mdx b/docs/using-seerr/advanced/verifying-signed-artifacts.mdx index 3ed328676f..8c2d6ea1fd 100644 --- a/docs/using-seerr/advanced/verifying-signed-artifacts.mdx +++ b/docs/using-seerr/advanced/verifying-signed-artifacts.mdx @@ -298,7 +298,7 @@ sha256:abcd1234... ```bash cosign verify ghcr.io/seerr-team/seerr/seerr-chart@sha256:abcd1234... \ - --certificate-identity "https://github.com/seerr-team/seerr/.github/workflows/helm.yml@refs/heads/main" \ + --certificate-identity "https://github.com/seerr-team/seerr/.github/workflows/helm.yml@refs/heads/develop" \ --certificate-oidc-issuer "https://token.actions.githubusercontent.com" ``` @@ -316,13 +316,13 @@ The following checks were performed: ### Expected Certificate Identity -The expected certificate identity for all signed Seerr images is: +The expected certificate identity for all signed Seerr charts is: ``` -https://github.com/seerr-team/seerr/.github/workflows/helm.yml@refs/heads/main +https://github.com/seerr-team/seerr/.github/workflows/helm.yml@refs/heads/develop ``` -This confirms that the image was: +This confirms that the chart was: - Built by the official Seerr Release workflow - Produced from the seerr-team/seerr repository @@ -339,12 +339,12 @@ This confirms that the image was: DIGEST=$(docker buildx imagetools inspect ghcr.io/seerr-team/seerr/seerr-chart:3.0.0 --format '{{json .Manifest.Digest}}' | tr -d '"') cosign verify ghcr.io/seerr-team/seerr/seerr-chart@"$DIGEST" \ - --certificate-identity-regexp "https://github.com/seerr-team/seerr/.github/workflows/helm.yml@refs/heads/main" \ + --certificate-identity-regexp "https://github.com/seerr-team/seerr/.github/workflows/helm.yml@refs/heads/develop" \ --certificate-oidc-issuer "https://token.actions.githubusercontent.com" cosign verify-attestation ghcr.io/seerr-team/seerr/seerr-chart@"$DIGEST" \ --type cyclonedx \ - --certificate-identity-regexp "https://github.com/seerr-team/seerr/.github/workflows/helm.yml@refs/heads/main" \ + --certificate-identity-regexp "https://github.com/seerr-team/seerr/.github/workflows/helm.yml@refs/heads/develop" \ --certificate-oidc-issuer "https://token.actions.githubusercontent.com" ``` @@ -355,7 +355,7 @@ cosign verify-attestation ghcr.io/seerr-team/seerr/seerr-chart@"$DIGEST" \ DIGEST=$(skopeo inspect docker://ghcr.io/seerr-team/seerr/seerr-chart:3.0.0 --format '{{.Digest}}') cosign verify ghcr.io/seerr-team/seerr/seerr-chart@"$DIGEST" \ - --certificate-identity-regexp "https://github.com/seerr-team/seerr/.github/workflows/helm.yml@refs/heads/main" \ + --certificate-identity-regexp "https://github.com/seerr-team/seerr/.github/workflows/helm.yml@refs/heads/develop" \ --certificate-oidc-issuer "https://token.actions.githubusercontent.com" ``` diff --git a/docs/using-seerr/notifications/discord.md b/docs/using-seerr/notifications/discord.md index 3988d93ce3..680d9b1b64 100644 --- a/docs/using-seerr/notifications/discord.md +++ b/docs/using-seerr/notifications/discord.md @@ -22,6 +22,10 @@ You can find the webhook URL in the Discord application, at **Server Settings &r If a role ID is specified, it will be included in the webhook message. See [Discord role ID](https://support.discord.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID). +### Thread ID (optional) + +If a thread ID is specified, the notification will be sent to a specific thread, instead of the webhook channel. Leave it blank to send it directly to the channel. + ### Bot Username (optional) If you would like to override the name you configured for your bot in Discord, you may set this value to whatever you like! @@ -36,4 +40,6 @@ When enabled, notifications will be sent in the language of the user who trigger ### Notification Language -Sets the language for all notifications sent to this Discord channel. This option is only available when **Use Notification Recipient Locale** is disabled. \ No newline at end of file +Sets the language for all notifications sent to this Discord channel. This option is only available when **Use Notification Recipient Locale** is disabled. + +The thread must belong to the webhook's channel. A thread from a different channel will cause the request to fail and the notification will not be delivered. diff --git a/docs/using-seerr/notifications/ntfy.md b/docs/using-seerr/notifications/ntfy.md index 6aa4664923..2b153a2f94 100644 --- a/docs/using-seerr/notifications/ntfy.md +++ b/docs/using-seerr/notifications/ntfy.md @@ -16,6 +16,10 @@ Set this to the URL of your ntfy.sh server. Set this to the topic you want to send notifications to. +### Tags (optional) + +Set this to a comma-separated list of [ntfy tags](https://docs.ntfy.sh/publish/#tags-emojis) to apply to notifications (for example, `eyes` or `eyes,warning`). + ### Username + Password authentication (optional) Set this to the username and password for your ntfy.sh server. diff --git a/docs/using-seerr/notifications/webhook.md b/docs/using-seerr/notifications/webhook.md index c359c9262f..263058f9ae 100644 --- a/docs/using-seerr/notifications/webhook.md +++ b/docs/using-seerr/notifications/webhook.md @@ -53,13 +53,13 @@ Customize the JSON payload to suit your needs. Seerr provides several [template These variables are for the target recipient of the notification. -| Variable | Value | -| ---------------------------------------- | ------------------------------------------------------------- | -| `{{notifyuser_username}}` | The target notification recipient's username | -| `{{notifyuser_email}}` | The target notification recipient's email address | -| `{{notifyuser_avatar}}` | The target notification recipient's avatar URL | -| `{{notifyuser_settings_discordId}}` | The target notification recipient's Discord ID (if set) | -| `{{notifyuser_settings_telegramChatId}}` | The target notification recipient's Telegram Chat ID (if set) | +| Variable | Value | +| ---------------------------------------- | -------------------------------------------------------------------------------------------- | +| `{{notifyuser_username}}` | The target notification recipient's username | +| `{{notifyuser_email}}` | The target notification recipient's email address | +| `{{notifyuser_avatar}}` | The target notification recipient's avatar URL | +| `{{notifyuser_settings_discordIds}}` | The target notification recipient's Discord ID(s) as a JSON array (if set) | +| `{{notifyuser_settings_telegramChatId}}` | The target notification recipient's Telegram Chat ID (if set) | :::info The `notifyuser` variables are not defined for the following request notification types, as they are intended for application administrators rather than end users: @@ -113,15 +113,15 @@ The `{{request}}` will be `null` if there is no relevant media object for the no The following special variables are only included in request-related notifications. -| Variable | Value | -| ----------------------------------------- | ----------------------------------------------- | -| `{{request_id}}` | The request ID | -| `{{requestedBy_username}}` | The requesting user's username | -| `{{requestedBy_email}}` | The requesting user's email address | -| `{{requestedBy_avatar}}` | The requesting user's avatar URL | -| `{{requestedBy_jellyfinUserId}}` | The requesting user's Jellyfin User ID | -| `{{requestedBy_settings_discordId}}` | The requesting user's Discord ID (if set) | -| `{{requestedBy_settings_telegramChatId}}` | The requesting user's Telegram Chat ID (if set) | +| Variable | Value | +| ----------------------------------------- | ------------------------------------------------------------------------------ | +| `{{request_id}}` | The request ID | +| `{{requestedBy_username}}` | The requesting user's username | +| `{{requestedBy_email}}` | The requesting user's email address | +| `{{requestedBy_avatar}}` | The requesting user's avatar URL | +| `{{requestedBy_jellyfinUserId}}` | The requesting user's Jellyfin User ID | +| `{{requestedBy_settings_discordIds}}` | The requesting user's Discord ID(s) as a JSON array (if set) | +| `{{requestedBy_settings_telegramChatId}}` | The requesting user's Telegram Chat ID (if set) | #### Issue @@ -129,14 +129,14 @@ The `{{issue}}` will be `null` if there is no relevant media object for the noti The following special variables are only included in issue-related notifications. -| Variable | Value | -| ---------------------------------------- | ----------------------------------------------- | -| `{{issue_id}}` | The issue ID | -| `{{reportedBy_username}}` | The requesting user's username | -| `{{reportedBy_email}}` | The requesting user's email address | -| `{{reportedBy_avatar}}` | The requesting user's avatar URL | -| `{{reportedBy_settings_discordId}}` | The requesting user's Discord ID (if set) | -| `{{reportedBy_settings_telegramChatId}}` | The requesting user's Telegram Chat ID (if set) | +| Variable | Value | +| ---------------------------------------- | ------------------------------------------------------------------------------ | +| `{{issue_id}}` | The issue ID | +| `{{reportedBy_username}}` | The requesting user's username | +| `{{reportedBy_email}}` | The requesting user's email address | +| `{{reportedBy_avatar}}` | The requesting user's avatar URL | +| `{{reportedBy_settings_discordIds}}` | The reporting user's Discord ID(s) as a JSON array (if set) | +| `{{reportedBy_settings_telegramChatId}}` | The requesting user's Telegram Chat ID (if set) | #### Comment @@ -144,11 +144,11 @@ The `{{comment}}` will be `null` if there is no relevant media object for the no The following special variables are only included in issue comment-related notifications. -| Variable | Value | -| ----------------------------------------- | ----------------------------------------------- | -| `{{comment_message}}` | The comment message | -| `{{commentedBy_username}}` | The commenting user's username | -| `{{commentedBy_email}}` | The commenting user's email address | -| `{{commentedBy_avatar}}` | The commenting user's avatar URL | -| `{{commentedBy_settings_discordId}}` | The commenting user's Discord ID (if set) | -| `{{commentedBy_settings_telegramChatId}}` | The commenting user's Telegram Chat ID (if set) | +| Variable | Value | +| ----------------------------------------- | ------------------------------------------------------------------------------ | +| `{{comment_message}}` | The comment message | +| `{{commentedBy_username}}` | The commenting user's username | +| `{{commentedBy_email}}` | The commenting user's email address | +| `{{commentedBy_avatar}}` | The commenting user's avatar URL | +| `{{commentedBy_settings_discordIds}}` | The commenting user's Discord ID(s) as a JSON array (if set) | +| `{{commentedBy_settings_telegramChatId}}` | The commenting user's Telegram Chat ID (if set) | diff --git a/docs/using-seerr/plex/index.md b/docs/using-seerr/plex/index.md index e699392657..68b4d1f549 100644 --- a/docs/using-seerr/plex/index.md +++ b/docs/using-seerr/plex/index.md @@ -10,7 +10,7 @@ Seerr provides integration features that connect with your Plex media server to ## Available Features -- [Watchlist Auto Request](./plex/watchlist-auto-request) - Automatically request media from your Plex Watchlist +- [Watchlist Auto Request](./watchlist-auto-request) - Automatically request media from your Plex Watchlist - More features coming soon! ## Prerequisites diff --git a/docs/using-seerr/settings/general.md b/docs/using-seerr/settings/general.md index 65b337495f..24bf19daa9 100644 --- a/docs/using-seerr/settings/general.md +++ b/docs/using-seerr/settings/general.md @@ -71,6 +71,14 @@ When enabled, media that has been blocklisted will not appear on the "Discover" This setting is **disabled** by default. +## Hide Requested Media + +When enabled, media that has been requested (but not yet available) will not appear on the "Discover" home page, or in the "Recommended" or "Similar" categories or other links on media detail pages. + +Requested media will still appear in search results, however, so it is possible to locate and view hidden items by searching for them by title. + +This setting is **disabled** by default. + ## Allow Partial Series Requests When enabled, users will be able to submit requests for specific seasons of TV series. If disabled, users will only be able to submit requests for all unavailable seasons. diff --git a/gen-docs/blog/2026-02-28-seerr-security-fix-release.md b/gen-docs/blog/2026-02-28-seerr-security-fix-release.md index bfe5077ad0..db5d5e01fd 100644 --- a/gen-docs/blog/2026-02-28-seerr-security-fix-release.md +++ b/gen-docs/blog/2026-02-28-seerr-security-fix-release.md @@ -2,7 +2,7 @@ title: "Seerr v3.1.0: Critical Security Release" description: "Seerr v3.1.0 addresses three CVEs, including a high-priority vulnerability affecting Plex-configured instances. Upgrade immediately." slug: seerr-3-1-0-security-release -authors: [seerr-team] +authors: [fallenbagel] image: https://raw.githubusercontent.com/seerr-team/seerr/refs/heads/develop/gen-docs/static/img/logo_full.svg hide_table_of_contents: false --- @@ -69,4 +69,4 @@ Many thanks to those making their first contribution to Seerr in this release: Now that the post-merger feature freeze has ended, the team is resuming active feature development. Stay tuned to our blog for upcoming releases and in-depth looks at what we're building next. -In the meantime, please upgrade to **v3.1.0** right away, especially if you are using a Plex Media Server configuration. See our [migration guide](https://docs.seerr.dev/migration-guide) if you need help upgrading from Overseerr/Jellyseerr. \ No newline at end of file +In the meantime, please upgrade to **v3.1.0** right away, especially if you are using a Plex Media Server configuration. See our [migration guide](https://docs.seerr.dev/migration-guide) if you need help upgrading from Overseerr/Jellyseerr. diff --git a/gen-docs/blog/2026-06-01/blocklist-region-language.png b/gen-docs/blog/2026-06-01/blocklist-region-language.png new file mode 100644 index 0000000000..2df2e2ccf6 Binary files /dev/null and b/gen-docs/blog/2026-06-01/blocklist-region-language.png differ diff --git a/gen-docs/blog/2026-06-01/discord-multiple-ids.png b/gen-docs/blog/2026-06-01/discord-multiple-ids.png new file mode 100644 index 0000000000..0312266f2f Binary files /dev/null and b/gen-docs/blog/2026-06-01/discord-multiple-ids.png differ diff --git a/gen-docs/blog/2026-06-01/seerr-3-2-0-and-3-3-0-release-notes.md b/gen-docs/blog/2026-06-01/seerr-3-2-0-and-3-3-0-release-notes.md new file mode 100644 index 0000000000..c47971eeb1 --- /dev/null +++ b/gen-docs/blog/2026-06-01/seerr-3-2-0-and-3-3-0-release-notes.md @@ -0,0 +1,98 @@ +--- +title: "Seerr v3.2.0 and v3.3.0 Release Notes" +description: "Seerr v3.2.0 and v3.3.0" +slug: seerr-3-2-0-and-3-3-0-release-notes +authors: [gauthier-th] +image: https://raw.githubusercontent.com/seerr-team/seerr/refs/heads/develop/gen-docs/static/img/logo_full.svg +hide_table_of_contents: false +--- + + +This release note covers both **v3.2.0** and **v3.3.0** releases, which include a host of new features, improvements, and bug fixes to enhance your Seerr experience. + +We have been a bit busy recently and forgot to release the notes for **v3.2.0**, so this release note also includes the previous release notes for v3.2.0, which was released in the interim. Better late than never, right? + + + +Beyond the usual bug fixes and optimizations, here are some of the highlights from these releases: + +## What's New? + +### Blocklist Upgrades + +With the new region and language settings, you can now tailor your blocklist to filter content based on specific regions or languages. + +Moreover, you can now blocklist entire collections of media items, allowing you to filter out specific franchises, series, or groups of related content with ease. + +![Blocklist Region and Language Settings](./blocklist-region-language.png) + +### Expanded Language Support + +In this release, we added a long-awaited feature: server-side internationalization (i18n) for all notification agents. +Notifications now automatically translate at send-time based on the recipient's language, with dedicated locale settings for Discord, Slack, Gotify, and ntfy—including a Discord toggle to match the requesting user's language. + +![Server-Side Internationalization](./server-side-i18n.png) + +We are continuously working to make the platform accessible to everyone, everywhere. This update expands our internationalization efforts by officially introducing full language support for **Estonian**, **Luxembourgish**, and **Vietnamese**. + +### Notifications & Webhooks + +Webhooks are also more powerful with support for custom headers and the inclusion of `imdbid` in payloads. + +![Webhook Custom Headers](./webhook-custom-headers.png) + +Additionally, **ntfy** users can now utilize rich Markdown formatting and custom priority settings. + +Discord users can now also include multiple user or role IDs in the `User IDs` field, allowing for more flexible and targeted notifications. + +![Discord Multiple IDs](./discord-multiple-ids.png) + +### Other Enhancements & Quality of Life Improvements + +- You can now choose to enable or disable the `monitorNewItems` option for Sonarr in the service settings. +- We added the ability to set an unlimited time for quota resets, allowing you to add a global request per user. +- In the user list, you can now sort your users by name, email, role, requests, and more to find the users you are looking for more easily. +- Filters on the trending page have been added to see only trending movies or TV shows, with the option to filter by the daily or weekly trending lists. +- Handy IMDb and TMDB links have been added directly on the person detail pages. + +We've also officially promoted ourselves and removed the "BETA software" banner from the About Page. We are well past the beta phase and running with the big kids now. + +You can find the full list of the new features and bug fixes in the [v3.2.0 release notes](https://github.com/seerr-team/seerr/releases/tag/v3.2.0) and the [v3.3.0 release notes](https://github.com/seerr-team/seerr/releases/tag/v3.3.0). + +## New Contributors + +Many thanks to those making their first contribution to Seerr in v3.2.0 and v3.3.0: + +* [@aslafy-z](https://github.com/aslafy-z) +* [@leereilly](https://github.com/leereilly) +* [@jisef](https://github.com/jisef) +* [@dougrathbone](https://github.com/dougrathbone) +* [@bobziroll](https://github.com/bobziroll) +* [@v3DJG6GL](https://github.com/v3DJG6GL) +* [@Roboroads](https://github.com/Roboroads) +* [@costajohnt](https://github.com/costajohnt) +* [@tiagodefendi](https://github.com/tiagodefendi) +* [@Jyasapara](https://github.com/Jyasapara) +* [@Sym-jay](https://github.com/Sym-jay) +* [@bibi0019](https://github.com/bibi0019) +* [@redondos](https://github.com/redondos) +* [@bogo22](https://github.com/bogo22) +* [@jabloink](https://github.com/jabloink) +* [@YakGravity](https://github.com/YakGravity) +* [@dj0024javia](https://github.com/dj0024javia) +* [@Jerra94](https://github.com/Jerra94) +* [@its-wizza](https://github.com/its-wizza) +* [@ventiph](https://github.com/ventiph) +* [@RinZ27](https://github.com/RinZ27) +* [@aldoeliacim](https://github.com/aldoeliacim) +* [@danjuv](https://github.com/danjuv) +* [@kyle-engler](https://github.com/kyle-engler) +* [@Finchow](https://github.com/Finchow) +* [@Josh-Archer](https://github.com/Josh-Archer) +* [@marcinjurczak](https://github.com/marcinjurczak) +* [@fredrikburmester](https://github.com/fredrikburmester) +* [@death2all110](https://github.com/death2all110) +* [@burakemirsezen](https://github.com/burakemirsezen) +* [@felixschndr](https://github.com/felixschndr) +* [@haribo-hyung](https://github.com/haribo-hyung) +* [@defaultdino](https://github.com/defaultdino) diff --git a/gen-docs/blog/2026-06-01/server-side-i18n.png b/gen-docs/blog/2026-06-01/server-side-i18n.png new file mode 100644 index 0000000000..5f9e844389 Binary files /dev/null and b/gen-docs/blog/2026-06-01/server-side-i18n.png differ diff --git a/gen-docs/blog/2026-06-01/webhook-custom-headers.png b/gen-docs/blog/2026-06-01/webhook-custom-headers.png new file mode 100644 index 0000000000..142c0c79d9 Binary files /dev/null and b/gen-docs/blog/2026-06-01/webhook-custom-headers.png differ diff --git a/gen-docs/blog/2026-06-03-why-seerr-runs-rootless.md b/gen-docs/blog/2026-06-03-why-seerr-runs-rootless.md new file mode 100644 index 0000000000..8c6e858158 --- /dev/null +++ b/gen-docs/blog/2026-06-03-why-seerr-runs-rootless.md @@ -0,0 +1,84 @@ +--- +title: "Why Seerr Doesn't Support PUID/PGID" +description: "Seerr runs rootless by design. Here's why we won't be adding PUID/PGID support, and why chown is the right approach." +slug: why-seerr-doesnt-support-puid-pgid +authors: [fallenbagel] +image: https://raw.githubusercontent.com/seerr-team/seerr/refs/heads/develop/gen-docs/static/img/logo_full.svg +hide_table_of_contents: false +--- + +A common question we get from users migrating from Overseerr or Jellyseerr is why Seerr doesn't support `PUID`/`PGID` environment variables for setting the user the container runs as. This post explains the reasoning behind that decision and why using chown on the host is the correct approach. + + + +## What PUID/PGID actually does under the hood + +The `PUID`/`PGID` pattern, popularised by images like LinuxServer.io, works by running the container entrypoint **as root**, executing a script that calls `chown`/`chmod` at runtime to reassign file ownership, then using a privilege-dropping tool to switch to the target user before starting the app. + +So it's still running `chown`; just inside the container, as root, on every single startup. The critical detail here is that **the container must start as root (UID 0) for this to work at all**. That single requirement undermines the whole security model. + +## Running as root in a container is a real risk + +Docker containers share the host kernel and they are not VMs. If a process inside a root container escapes the namespace via a kernel vulnerability, a container runtime bug, or a compromised dependency, it lands on your host **as root**. + +This is well documented: + +- Docker's security documentation explicitly warns against it: https://docs.docker.com/engine/security/#linux-kernel-capabilities +- The official Docker best practices guide recommends using the `USER` instruction in your Dockerfile to avoid running as root: https://docs.docker.com/build/building/best-practices/#user +- OWASP's Docker Security Cheat Sheet lists it as Rule #2: https://cheatsheetseries.owasp.org/cheatsheets/Docker_Security_Cheat_Sheet.html#rule-2-set-a-user + +:::note +There is one legitimate exception: Docker's [user namespace remapping](https://docs.docker.com/engine/security/userns-remap/) (`userns-remap`), where UID 0 inside the container is mapped to an unprivileged high-number UID on the host via a kernel-level isolation mechanism. However,`userns-remap` is **not enabled by default**, requires explicit host-level configuration, and is not what the `PUID`/`PGID` environment variable pattern does. Without it configured on the host, a container starting as root is simply running as root. +::: + +## How we ship Seerr instead + +Seerr runs as the `node` (UID `1000`) user baked into the official Node.js Docker image. This is exactly how the Node.js Docker team documents it in their own examples. Their own official README uses `user: "node"` in the example Docker Compose configuration: https://github.com/nodejs/docker-node/blob/main/README.md. + +We also use [pnpm](https://pnpm.io) as our package manager, which takes a stricter and more secure approach to dependency management than npm. Unlike npm, pnpm uses a content-addressable store with hard links and a non-hoisted, symlinked `node_modules` layout that prevents phantom dependencies (packages silently relying on undeclared transitive dependencies). pnpm also ships with hardened install defaults, including integrity verification and lifecycle script controls, that reduce the supply-chain attack surface. + +## Understanding Docker's `USER` directive + +We often hear users assume that the `USER` directive in a Dockerfile is a strict, unchangeable rule. + +In reality, it simply sets the *default* user for the container. It can be overridden at runtime using [`--user` flag in docker cli](https://docs.docker.com/reference/cli/docker/container/run/) or by specifying [`user:` in your docker compose file](https://docs.docker.com/reference/compose-file/services/#user). + +However, when you do switch users, it’s important to ensure that any mounted volumes (such as a `/path/to/appdata/config` directory in our case) have the correct permissions for the new user to avoid access issues. + +## Why does Seerr default to UID/GID `1000:1000`? + +A common question is whether the UID/GID needs to exist on the host machine to run the container. It doesn't; it only needs to exist inside the container. This means you can use any UID/GID, even one that doesn't correspond to a user on your host system. + +However, if a security vulnerability allows an attacker to escape the container, they inherit the permissions of that UID/GID on the host, if it exists. This is exactly the scenario we're avoiding by not running as root. If Seerr (or any of its dependencies) ran as root, as Overseerr and Jellyseerr did, a container escape would hand an attacker root on your host. The same applies to the `PUID`/`PGID` pattern: since it requires the container to start as root before dropping privileges, a vulnerability triggered before that drop would have the same effect. To mitigate this, Docker offers [user namespace remapping](https://docs.docker.com/engine/security/userns-remap/), which lets you map container UIDs to different, non-conflicting UIDs on the host for stronger isolation. + +You might also wonder why `1000:1000` specifically. While `1000` is often the first non-root user created on many Linux systems, our reason is simpler: it's the default user created in the [base Node.js Docker image](https://github.com/nodejs/docker-node/blob/c94b4249a1a1bfb2ebb05fac9012cd34fecc2db7/22/alpine3.23/Dockerfile#L5). + +As for why we don't go into more depth on this in our docs: we focus on the basics and the most common use cases for Seerr itself. Docker's permission model, including how the `USER` directive works, is already well documented by Docker. Our goal is to get you up and running smoothly, while encouraging you to explore the tools you're using. + +## So what do you actually need to do? + +If you're mounting a data directory into the Seerr container, that directory needs to be owned by UID `1000` (the `node` user) on the host. You do this once: + +```bash +chown -R 1000:1000 /your/seerr/data +``` + +That's it. A common misconception we've encountered from users unfamiliar with this setup is that this is a "workaround" or a "hack", but it isn't. It is basic Linux file permissions and it works as intended. If you're unfamiliar with how Linux file ownership works, DigitalOcean has a solid primer: https://www.digitalocean.com/community/tutorials/how-to-set-permissions-linux. + +Docker's own documentation recommends this exact approach for bind mount permissions: https://docs.docker.com/engine/storage/bind-mounts/. + +:::note +Unraid users may prefer to match the container user to their existing share permissions rather than chowning their data directory. Our [Unraid documentation](/getting-started/third-parties/unraid) covers both approaches. Note that third-party platform support is community-maintained and not officially supported by the Seerr team. +::: + +## Why we won't add PUID/PGID support + +Adding `PUID`/`PGID` support would require us to deliberately ship an image that starts as root. In a project with hundreds of npm dependencies, a single compromised package in a supply chain attack could, under that setup, gain root-level access to your server. The convenience of skipping one `chown` command does not come close to justifying that tradeoff. + +:::tip +For additional hardening, you can mount the container filesystem as [read-only](https://docs.docker.com/reference/compose-file/services/#read_only) and grant write access only to your data directory. Since all runtime writes go through `CONFIG_DIRECTORY`, nothing outside that volume needs to be writable. +::: + +If you need the PUID/PGID pattern, third-party images that implement it already exist. But it is not something we will maintain here, because it would mean knowingly reintroducing a security regression we deliberately moved away from. + +**Seerr's container setup is more secure than what Overseerr and Jellyseerr shipped, and we intend to keep it that way.** \ No newline at end of file diff --git a/gen-docs/docusaurus.config.ts b/gen-docs/docusaurus.config.ts index d94c4939ba..d2a6c07155 100644 --- a/gen-docs/docusaurus.config.ts +++ b/gen-docs/docusaurus.config.ts @@ -10,7 +10,13 @@ const config: Config = { url: 'https://docs.seerr.dev', baseUrl: '/', - trailingSlash: false, + trailingSlash: true, + + future: { + faster: { + swcJsMinimizer: true, + }, + }, organizationName: 'seerr-team', projectName: 'seerr', @@ -39,6 +45,19 @@ const config: Config = { path: '../docs', editUrl: 'https://github.com/seerr-team/seerr/edit/develop/docs/', docItemComponent: '@theme/ApiItem', + async sidebarItemsGenerator({ + defaultSidebarItemsGenerator, + ...args + }) { + const items = await defaultSidebarItemsGenerator(args); + return items.filter( + (item) => + !( + item.type === 'category' && + item.label?.toLowerCase() === 'api' + ) + ); + }, }, pages: false, theme: { @@ -97,6 +116,11 @@ const config: Config = { src: 'img/logo_full.svg', }, items: [ + { + to: '/api/seerr-api', + label: 'REST API', + position: 'right', + }, { to: 'blog', label: 'Blog', @@ -124,6 +148,10 @@ const config: Config = { label: 'Documentation', to: '/', }, + { + label: 'REST API', + to: '/api/seerr-api', + }, ], }, { diff --git a/gen-docs/package.json b/gen-docs/package.json index a2f010d8d0..6857e54ac6 100644 --- a/gen-docs/package.json +++ b/gen-docs/package.json @@ -7,7 +7,7 @@ "docusaurus": "docusaurus", "start": "docusaurus start", "build": "docusaurus build", - "gen-api-docs": "docusaurus gen-api-docs", + "gen-api-docs": "docusaurus gen-api-docs all", "clean-api-docs": "node ./scripts/clean-api-docs.cjs", "swizzle": "docusaurus swizzle", "deploy": "docusaurus deploy", @@ -19,12 +19,13 @@ }, "dependencies": { "@docusaurus/core": "3.10.1", + "@docusaurus/faster": "^3.10.1", "@docusaurus/preset-classic": "3.10.1", "@easyops-cn/docusaurus-search-local": "^0.55.1", "@mdx-js/react": "^3.0.0", "clsx": "^2.1.1", - "docusaurus-plugin-openapi-docs": "^5.0.1", - "docusaurus-theme-openapi-docs": "^5.0.1", + "docusaurus-plugin-openapi-docs": "5.0.2", + "docusaurus-theme-openapi-docs": "5.0.2", "prism-react-renderer": "^2.4.1", "react": "^18.0.0", "react-dom": "^18.0.0", diff --git a/gen-docs/pnpm-lock.yaml b/gen-docs/pnpm-lock.yaml index 423b2b6d74..610bcf106a 100644 --- a/gen-docs/pnpm-lock.yaml +++ b/gen-docs/pnpm-lock.yaml @@ -10,13 +10,16 @@ importers: dependencies: '@docusaurus/core': specifier: 3.10.1 - version: 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) + version: 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) + '@docusaurus/faster': + specifier: ^3.10.1 + version: 3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) '@docusaurus/preset-classic': specifier: 3.10.1 - version: 3.10.1(@algolia/client-search@5.39.0)(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@6.0.3) + version: 3.10.1(@algolia/client-search@5.39.0)(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@6.0.3) '@easyops-cn/docusaurus-search-local': specifier: ^0.55.1 - version: 0.55.1(@docusaurus/theme-common@3.10.1(@docusaurus/plugin-content-docs@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) + version: 0.55.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@docusaurus/theme-common@3.10.1(@docusaurus/plugin-content-docs@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) '@mdx-js/react': specifier: ^3.0.0 version: 3.1.1(@types/react@19.2.14)(react@18.3.1) @@ -24,11 +27,11 @@ importers: specifier: ^2.1.1 version: 2.1.1 docusaurus-plugin-openapi-docs: - specifier: ^5.0.1 - version: 5.0.1(@docusaurus/plugin-content-docs@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(@docusaurus/utils-validation@3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@docusaurus/utils@3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/json-schema@7.0.15)(react@18.3.1) + specifier: 5.0.2 + version: 5.0.2(@docusaurus/plugin-content-docs@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(@docusaurus/utils-validation@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@docusaurus/utils@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/json-schema@7.0.15)(react@18.3.1) docusaurus-theme-openapi-docs: - specifier: ^5.0.1 - version: 5.0.1(@docusaurus/theme-common@3.10.1(@docusaurus/plugin-content-docs@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@19.2.14)(docusaurus-plugin-openapi-docs@5.0.1(@docusaurus/plugin-content-docs@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(@docusaurus/utils-validation@3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@docusaurus/utils@3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/json-schema@7.0.15)(react@18.3.1))(docusaurus-plugin-sass@0.2.6(@docusaurus/core@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(sass@1.99.0)(webpack@5.101.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(redux@5.0.1)(webpack@5.101.3) + specifier: 5.0.2 + version: 5.0.2(e20b839675fe7ee566ace2729a870ef0) prism-react-renderer: specifier: ^2.4.1 version: 2.4.1(react@18.3.1) @@ -44,13 +47,13 @@ importers: devDependencies: '@docusaurus/module-type-aliases': specifier: 3.10.1 - version: 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/tsconfig': specifier: 3.10.1 version: 3.10.1 '@docusaurus/types': specifier: 3.10.1 - version: 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) typescript: specifier: ~6.0.3 version: 6.0.3 @@ -1054,6 +1057,12 @@ packages: resolution: {integrity: sha512-eNfHGcTKCSq6xmcavAkX3RRclHaE2xRCMParlDXLdXVP01/a2e/jKXMj/0ULnLFQSNwwuI62L0Ge8J+nZsR7UQ==} engines: {node: '>=20.0'} + '@docusaurus/faster@3.10.1': + resolution: {integrity: sha512-XTZhE5C1gZ/DaYYMlSk02dwP5vhpQON5QHVz1s3892mSESAywgWanURpXEDAvt4GvGuq7s+XP8rTWHZvfaJmdQ==} + engines: {node: '>=20.0'} + peerDependencies: + '@docusaurus/types': '*' + '@docusaurus/logger@3.10.1': resolution: {integrity: sha512-oPjNFnfJsRCkePVjkGrxWGq4MvJKRQT0r9jOP0eRBTZ7Wr9FAbzdP/Gjs0I2Ss6YRkPoEgygKG112OkE6skvJw==} engines: {node: '>=20.0'} @@ -1317,9 +1326,30 @@ packages: '@types/react': '>=16' react: '>=16' + '@module-federation/error-codes@0.22.0': + resolution: {integrity: sha512-xF9SjnEy7vTdx+xekjPCV5cIHOGCkdn3pIxo9vU7gEZMIw0SvAEdsy6Uh17xaCpm8V0FWvR0SZoK9Ik6jGOaug==} + + '@module-federation/runtime-core@0.22.0': + resolution: {integrity: sha512-GR1TcD6/s7zqItfhC87zAp30PqzvceoeDGYTgF3Vx2TXvsfDrhP6Qw9T4vudDQL3uJRne6t7CzdT29YyVxlgIA==} + + '@module-federation/runtime-tools@0.22.0': + resolution: {integrity: sha512-4ScUJ/aUfEernb+4PbLdhM/c60VHl698Gn1gY21m9vyC1Ucn69fPCA1y2EwcCB7IItseRMoNhdcWQnzt/OPCNA==} + + '@module-federation/runtime@0.22.0': + resolution: {integrity: sha512-38g5iPju2tPC3KHMPxRKmy4k4onNp6ypFPS1eKGsNLUkXgHsPMBFqAjDw96iEcjri91BrahG4XcdyKi97xZzlA==} + + '@module-federation/sdk@0.22.0': + resolution: {integrity: sha512-x4aFNBKn2KVQRuNVC5A7SnrSCSqyfIWmm1DvubjbO9iKFe7ith5niw8dqSFBekYBg2Fwy+eMg4sEFNVvCAdo6g==} + + '@module-federation/webpack-bundler-runtime@0.22.0': + resolution: {integrity: sha512-aM8gCqXu+/4wBmJtVeMeeMN5guw3chf+2i6HajKtQv7SJfxV/f4IyNQJUeUQu9HfiAZHjqtMV5Lvq/Lvh8LdyA==} + '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + '@napi-rs/wasm-runtime@1.0.7': + resolution: {integrity: sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==} + '@node-rs/jieba-android-arm-eabi@1.10.4': resolution: {integrity: sha512-MhyvW5N3Fwcp385d0rxbCWH42kqDBatQTyP8XbnYbju2+0BO/eTeCCLYj7Agws4pwxn2LtdldXRSKavT7WdzNA==} engines: {node: '>= 10'} @@ -1540,6 +1570,70 @@ packages: react-redux: optional: true + '@rspack/binding-darwin-arm64@1.7.11': + resolution: {integrity: sha512-oduECiZVqbO5zlVw+q7Vy65sJFth99fWPTyucwvLJJtJkPL5n17Uiql2cYP6Ijn0pkqtf1SXgK8WjiKLG5bIig==} + cpu: [arm64] + os: [darwin] + + '@rspack/binding-darwin-x64@1.7.11': + resolution: {integrity: sha512-a1+TtTE9ap6RalgFi7FGIgkJP6O4Vy6ctv+9WGJy53E4kuqHR0RygzaiVxCI/GMc/vBT9vY23hyrpWb3d1vtXA==} + cpu: [x64] + os: [darwin] + + '@rspack/binding-linux-arm64-gnu@1.7.11': + resolution: {integrity: sha512-P0QrGRPbTWu6RKWfN0bDtbnEps3rXH0MWIMreZABoUrVmNQKtXR6e73J3ub6a+di5s2+K0M2LJ9Bh2/H4UsDUA==} + cpu: [arm64] + os: [linux] + + '@rspack/binding-linux-arm64-musl@1.7.11': + resolution: {integrity: sha512-6ky7R43VMjWwmx3Yx7Jl7faLBBMAgMDt+/bN35RgwjiPgsIByz65EwytUVuW9rikB43BGHvA/eqlnjLrUzNBqw==} + cpu: [arm64] + os: [linux] + + '@rspack/binding-linux-x64-gnu@1.7.11': + resolution: {integrity: sha512-cuOJMfCOvb2Wgsry5enXJ3iT1FGUjdPqtGUBVupQlEG4ntSYsQ2PtF4wIDVasR3wdxC5nQbipOrDiN/u6fYsdQ==} + cpu: [x64] + os: [linux] + + '@rspack/binding-linux-x64-musl@1.7.11': + resolution: {integrity: sha512-CoK37hva4AmHGh3VCsQXmGr40L36m1/AdnN5LEjUX6kx5rEH7/1nEBN6Ii72pejqDVvk9anEROmPDiPw10tpFg==} + cpu: [x64] + os: [linux] + + '@rspack/binding-wasm32-wasi@1.7.11': + resolution: {integrity: sha512-OtrmnPUVJMxjNa3eDMfHyPdtlLRmmp/aIm0fQHlAOATbZvlGm12q7rhPW5BXTu1yh+1rQ1/uqvz+SzKEZXuJaQ==} + cpu: [wasm32] + + '@rspack/binding-win32-arm64-msvc@1.7.11': + resolution: {integrity: sha512-lObFW6e5lCWNgTBNwT//yiEDbsxm9QG4BYUojqeXxothuzJ/L6ibXz6+gLMvbOvLGV3nKgkXmx8GvT9WDKR0mA==} + cpu: [arm64] + os: [win32] + + '@rspack/binding-win32-ia32-msvc@1.7.11': + resolution: {integrity: sha512-0pYGnZd8PPqNR68zQ8skamqNAXEA1sUfXuAdYcknIIRq2wsbiwFzIc0Pov1cIfHYab37G7sSIPBiOUdOWF5Ivw==} + cpu: [ia32] + os: [win32] + + '@rspack/binding-win32-x64-msvc@1.7.11': + resolution: {integrity: sha512-EeQXayoQk/uBkI3pdoXfQBXNIUrADq56L3s/DFyM2pJeUDrWmhfIw2UFIGkYPTMSCo8F2JcdcGM32FGJrSnU0Q==} + cpu: [x64] + os: [win32] + + '@rspack/binding@1.7.11': + resolution: {integrity: sha512-2MGdy2s2HimsDT444Bp5XnALzNRxuBNc7y0JzyuqKbHBywd4x2NeXyhWXXoxufaCFu5PBc9Qq9jyfjW2Aeh06Q==} + + '@rspack/core@1.7.11': + resolution: {integrity: sha512-rsD9b+Khmot5DwCMiB3cqTQo53ioPG3M/A7BySu8+0+RS7GCxKm+Z+mtsjtG/vsu4Tn2tcqCdZtA3pgLoJB+ew==} + engines: {node: '>=18.12.0'} + peerDependencies: + '@swc/helpers': '>=0.5.1' + peerDependenciesMeta: + '@swc/helpers': + optional: true + + '@rspack/lite-tapable@1.1.0': + resolution: {integrity: sha512-E2B0JhYFmVAwdDiG14+DW0Di4Ze4Jg10Pc4/lILUrd5DRCaklduz2OvJ5HYQ6G+hd+WTzqQb3QnDNfK4yvAFYw==} + '@sideway/address@4.1.5': resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==} @@ -1653,6 +1747,169 @@ packages: resolution: {integrity: sha512-LnhVjMWyMQV9ZmeEy26maJk+8HTIbd59cH4F2MJ439k9DqejRisfFNGAPvRYlKETuh9LrImlS8aKsBgKjMA8WA==} engines: {node: '>=14'} + '@swc/core-darwin-arm64@1.15.33': + resolution: {integrity: sha512-N+L0uXhuO7FIfzqwgxmzv0zIpV0qEp8wPX3QQs2p4atjMoywup2JTeDlXPw+z9pWJGCae3JjM+tZ6myclI+2gA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + + '@swc/core-darwin-x64@1.15.33': + resolution: {integrity: sha512-/Il4QHSOhV4FekbsDtkrNmKbsX26oSysvgrRswa/RYOHXAkwXDbB4jaeKq6PsJLSPkzJ2KzQ061gtBnk0vNHfA==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + + '@swc/core-linux-arm-gnueabihf@1.15.33': + resolution: {integrity: sha512-C64hBnBxq4viOPQ8hlx+2lJ23bzZBGnjw7ryALmS+0Q3zHmwO8lw1/DArLENw4Q18/0w5wdEO1k3m1wWNtKGqQ==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + + '@swc/core-linux-arm64-gnu@1.15.33': + resolution: {integrity: sha512-TRJfnJbX3jqpxRDRoieMzRiCBS5jOmXNb3iQXmcgjFEHKLnAgK1RZRU8Cq1MsPqO4jAJp/ld1G4O3fXuxv85uw==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-arm64-musl@1.15.33': + resolution: {integrity: sha512-il7tYM+CpUNzieQbwAjFT1P8zqAhmGWNAGhQZBnxurXZ0aNn+5nqYFTEUKNZl7QibtT0uQXzTZrNGHCIj6Y1Og==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-ppc64-gnu@1.15.33': + resolution: {integrity: sha512-ZtNBwN0Z7CFj9Il0FcPaKdjgP7URyKu/3RfH46vq+0paOBqLj4NYldD6Qo//Duif/7IOtAraUfDOmp0PLAufog==} + engines: {node: '>=10'} + cpu: [ppc64] + os: [linux] + + '@swc/core-linux-s390x-gnu@1.15.33': + resolution: {integrity: sha512-De1IyajoOmhOYYjw/lx66bKlyDpHZTueqwpDrWgf5O7T6d1ODeJJO9/OqMBmrBQc5C+dNnlmIufHsp4QVCWufA==} + engines: {node: '>=10'} + cpu: [s390x] + os: [linux] + + '@swc/core-linux-x64-gnu@1.15.33': + resolution: {integrity: sha512-mGTH0YxmUN+x6vRN/I6NOk5X0ogNktkwPnJ94IMvR7QjhRDwL0O8RXEDhyUM0YtwWrryBOqaJQBX4zruxEPRGw==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-linux-x64-musl@1.15.33': + resolution: {integrity: sha512-hj628ZkSEJf6zMf5VMbYrG2O6QqyTIp2qwY6VlCjvIa9lAEZ5c2lfPblCLVGYubTeLJDxadLB/CxqQYOQABeEQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-win32-arm64-msvc@1.15.33': + resolution: {integrity: sha512-GV2oohtN2/5+KSccl86VULu3aT+LrISC8uzgSq0FRnikpD+Zwc+sBlXmoKQ+Db6jI57ITUOIB8jRkdGMABC29g==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + + '@swc/core-win32-ia32-msvc@1.15.33': + resolution: {integrity: sha512-gtyvzSNR8DHKfFEA2uqb8Ld1myqi6uEg2jyeUq3ikn5ytYs7H8RpZYC8mdy4NXr8hfcdJfCLXPlYaqqfBXpoEQ==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + + '@swc/core-win32-x64-msvc@1.15.33': + resolution: {integrity: sha512-d6fRqQSkJI+kmMEBWaDQ7TMl8+YjLYbwRUPZQ9DY0ORBJeTzOrG0twvfvlZ2xgw6jA0ScQKgfBm4vHLSLl5Hqg==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@swc/core@1.15.33': + resolution: {integrity: sha512-jOlwnFV2xhuuZeAUILGFULeR6vDPfijEJ57evfocwznQldLU3w2cZ9bSDryY9ip+AsM3r1NJKzf47V2NXebkeQ==} + engines: {node: '>=10'} + peerDependencies: + '@swc/helpers': '>=0.5.17' + peerDependenciesMeta: + '@swc/helpers': + optional: true + + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + + '@swc/html-darwin-arm64@1.15.33': + resolution: {integrity: sha512-zyO6uMBfLyCh55wundAxKX+8P/f98ecuyir4VX6nTmn6y7x37ndB8f01LUrd9Tiq6eEAvDXLiqEUvuGjEc7Pmg==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + + '@swc/html-darwin-x64@1.15.33': + resolution: {integrity: sha512-MaGunsY/J5l7Rb5OmoztEWh+ikooydT7nWkjiDovj7UfkB9HLk5sLr9O7ZdNGJ2u9dD6FX89SzMdA0Psm9NJrQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + + '@swc/html-linux-arm-gnueabihf@1.15.33': + resolution: {integrity: sha512-CrbUDjVl6/hQ1C5KPMiK4vxk/eOMjxkVELqwnOxsZ+aFVTv3L3YrGMaJ5H47vvIihkPhqiSOUPmMEFqxvqKmXg==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + + '@swc/html-linux-arm64-gnu@1.15.33': + resolution: {integrity: sha512-7tZ0IgmUslI9Extu/TpxJS0GjJoDx0j9zeq2cIidPdM/njSBpyRB7n4B292Q5WFVh7PcZl7WXqqqMczibQ27aA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/html-linux-arm64-musl@1.15.33': + resolution: {integrity: sha512-gYi2ainYZV2z+jwjp9UKuPVOf3c5q+NkH3QRDjqDrIPLagqDsYNjobi8p5oajGcPGFLNTcVw08VTcubJGChReA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/html-linux-ppc64-gnu@1.15.33': + resolution: {integrity: sha512-6CfzyVQSdD8ezFdxFve4J/b6qTgXIwYFWEvSdaJvXSgwTy976uUV5Ff1LOF86mt2zWMhZJX9DqmkGyIhepbyWw==} + engines: {node: '>=10'} + cpu: [ppc64] + os: [linux] + + '@swc/html-linux-s390x-gnu@1.15.33': + resolution: {integrity: sha512-Msx1eniw95lhMHUSe3D5FXweKHtkHtzJLsHJDj920uL4Dm7UHqzwaCuZdCmzbkHnO96YjjQvAm266djg8wupmQ==} + engines: {node: '>=10'} + cpu: [s390x] + os: [linux] + + '@swc/html-linux-x64-gnu@1.15.33': + resolution: {integrity: sha512-JDNb4Uq+7g+23QuOtwWnP0/EqztWIHFFdQdeBIS5zx83YBG2dYRMdPAjnHJWh2YRZxdepd8q6S9MUIxpSrouAg==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/html-linux-x64-musl@1.15.33': + resolution: {integrity: sha512-NSpZdbz4dj0pu1A0Z9l68Bll5HAzEMtBAeMe6jc4GEVfpIw6eeafQHm2/yMUEh09tgl8t9LzM9DycfdTZDjM4g==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/html-win32-arm64-msvc@1.15.33': + resolution: {integrity: sha512-w7iho3/zS3lCDqgUZMDLMBO0ElX7j+KgvMb8BOrKqLDOSTDDj3lY/BClNJ7vBpAliI2kPQs/mUikdZyzi4MBjQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + + '@swc/html-win32-ia32-msvc@1.15.33': + resolution: {integrity: sha512-6hJ2pBweSfZ38trYHXmzTBDpRNvqJgFl2PkIWdy4IXbV/Fv0v9Dqe0t9Gi2ZVEBpgI7PD6pF42AT4HmrNTVFyQ==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + + '@swc/html-win32-x64-msvc@1.15.33': + resolution: {integrity: sha512-eaY/vNE7rkPKluJYjhOiQOA1tto5VbJOoD1C1xFTBmr9t7WsqYUfbQhYQy5A26/z83NNgtDwELM85rkMB+/vWA==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@swc/html@1.15.33': + resolution: {integrity: sha512-PZIfmj5zYpAJ2eMptf0My2q9Bl8bkraW28+FD1pRnxOiYMrKrP5vL2tB2PdxMRjS0ziLFVM5HEuGFw8PxEDOaw==} + engines: {node: '>=14'} + + '@swc/types@0.1.26': + resolution: {integrity: sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==} + '@szmarczak/http-timer@5.0.1': resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} engines: {node: '>=14.16'} @@ -2619,8 +2876,8 @@ packages: resolution: {integrity: sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==} engines: {node: '>=6'} - docusaurus-plugin-openapi-docs@5.0.1: - resolution: {integrity: sha512-OVfoDovRdiS78DQYWmr2BjuOF2A6kVmJ43mgkQaAEZxASyHbUft4zUIhvfa7gqema6KNL9pVKejDievZdZ3wGQ==} + docusaurus-plugin-openapi-docs@5.0.2: + resolution: {integrity: sha512-WCC2m6PpylXZfNga+ScelTG0a7jUGtbB9+AmbR9lUj93FPryTs8VHTMJ3fKtO0senJTWgOU3MDvZw0v+mE3ztA==} engines: {node: '>=14'} peerDependencies: '@docusaurus/plugin-content-docs': ^3.10.0 @@ -2634,8 +2891,8 @@ packages: '@docusaurus/core': ^2.0.0-beta || ^3.0.0-alpha sass: ^1.30.0 - docusaurus-theme-openapi-docs@5.0.1: - resolution: {integrity: sha512-bVeb7hOqog9LKVrJzYXdNJ7/0N22lk0VE22QK+naAn5GuAvYo41JmpXW9hqLIPkEp2UbexTHoPW9SYVdUsyvvw==} + docusaurus-theme-openapi-docs@5.0.2: + resolution: {integrity: sha512-BD6WhbunR6kXqtoUUDlhxO4HlCNM2nYENGr/TbiTEknkgXYKQz+FEIhY4Hyz5GSLpuhPih0CDuNl7Xkfpcz0Yw==} engines: {node: '>=14'} peerDependencies: '@docusaurus/theme-common': ^3.10.0 @@ -3554,6 +3811,76 @@ packages: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -3895,9 +4222,6 @@ packages: minimalistic-assert@1.0.1: resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} - minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - minimatch@3.1.5: resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} @@ -5334,6 +5658,12 @@ packages: resolution: {integrity: sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g==} hasBin: true + swc-loader@0.2.7: + resolution: {integrity: sha512-nwYWw3Fh9ame3Rtm7StS9SBLpHRRnYcK7bnpF3UKZmesAK0gw2/ADvlURFAINmPvKtDLzp+GBiP9yLoEjg6S9w==} + peerDependencies: + '@swc/core': ^1.2.147 + webpack: '>=2' + tailwindcss@3.4.19: resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==} engines: {node: '>=14.0.0'} @@ -6983,7 +7313,7 @@ snapshots: - '@algolia/client-search' - algoliasearch - '@docusaurus/babel@3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@docusaurus/babel@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/core': 7.28.4 '@babel/generator': 7.28.3 @@ -6995,7 +7325,7 @@ snapshots: '@babel/runtime': 7.29.2 '@babel/traverse': 7.28.4 '@docusaurus/logger': 3.10.1 - '@docusaurus/utils': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) babel-plugin-dynamic-import-node: 2.3.3 fs-extra: 11.3.2 tslib: 2.8.1 @@ -7008,32 +7338,34 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/bundler@3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3)': + '@docusaurus/bundler@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@rspack/core@1.7.11)(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3)': dependencies: '@babel/core': 7.28.4 - '@docusaurus/babel': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/babel': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/cssnano-preset': 3.10.1 '@docusaurus/logger': 3.10.1 - '@docusaurus/types': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - babel-loader: 9.2.1(@babel/core@7.28.4)(webpack@5.101.3) + '@docusaurus/types': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + babel-loader: 9.2.1(@babel/core@7.28.4)(webpack@5.101.3(@swc/core@1.15.33)) clean-css: 5.3.3 - copy-webpack-plugin: 11.0.0(webpack@5.101.3) - css-loader: 6.11.0(webpack@5.101.3) - css-minimizer-webpack-plugin: 5.0.1(clean-css@5.3.3)(webpack@5.101.3) + copy-webpack-plugin: 11.0.0(webpack@5.101.3(@swc/core@1.15.33)) + css-loader: 6.11.0(@rspack/core@1.7.11)(webpack@5.101.3(@swc/core@1.15.33)) + css-minimizer-webpack-plugin: 5.0.1(clean-css@5.3.3)(webpack@5.101.3(@swc/core@1.15.33)) cssnano: 6.1.2(postcss@8.5.9) - file-loader: 6.2.0(webpack@5.101.3) + file-loader: 6.2.0(webpack@5.101.3(@swc/core@1.15.33)) html-minifier-terser: 7.2.0 - mini-css-extract-plugin: 2.9.4(webpack@5.101.3) - null-loader: 4.0.1(webpack@5.101.3) + mini-css-extract-plugin: 2.9.4(webpack@5.101.3(@swc/core@1.15.33)) + null-loader: 4.0.1(webpack@5.101.3(@swc/core@1.15.33)) postcss: 8.5.9 - postcss-loader: 7.3.4(postcss@8.5.9)(typescript@6.0.3)(webpack@5.101.3) + postcss-loader: 7.3.4(postcss@8.5.9)(typescript@6.0.3)(webpack@5.101.3(@swc/core@1.15.33)) postcss-preset-env: 10.4.0(postcss@8.5.9) - terser-webpack-plugin: 5.3.14(webpack@5.101.3) + terser-webpack-plugin: 5.3.14(@swc/core@1.15.33)(webpack@5.101.3(@swc/core@1.15.33)) tslib: 2.8.1 - url-loader: 4.1.1(file-loader@6.2.0(webpack@5.101.3))(webpack@5.101.3) - webpack: 5.101.3 - webpackbar: 7.0.0(webpack@5.101.3) + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.101.3(@swc/core@1.15.33)))(webpack@5.101.3(@swc/core@1.15.33)) + webpack: 5.101.3(@swc/core@1.15.33) + webpackbar: 7.0.0(@rspack/core@1.7.11)(webpack@5.101.3(@swc/core@1.15.33)) + optionalDependencies: + '@docusaurus/faster': 3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) transitivePeerDependencies: - '@parcel/css' - '@rspack/core' @@ -7049,15 +7381,15 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/core@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3)': + '@docusaurus/core@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3)': dependencies: - '@docusaurus/babel': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/bundler': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) + '@docusaurus/babel': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/bundler': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@rspack/core@1.7.11)(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) '@docusaurus/logger': 3.10.1 - '@docusaurus/mdx-loader': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils-common': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils-validation': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/mdx-loader': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-common': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mdx-js/react': 3.1.1(@types/react@19.2.14)(react@18.3.1) boxen: 6.2.1 chalk: 4.1.2 @@ -7073,7 +7405,7 @@ snapshots: execa: 5.1.1 fs-extra: 11.3.2 html-tags: 3.3.1 - html-webpack-plugin: 5.6.4(webpack@5.101.3) + html-webpack-plugin: 5.6.4(@rspack/core@1.7.11)(webpack@5.101.3(@swc/core@1.15.33)) leven: 3.1.0 lodash: 4.17.23 open: 8.4.2 @@ -7083,7 +7415,7 @@ snapshots: react-dom: 18.3.1(react@18.3.1) react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)' react-loadable: '@docusaurus/react-loadable@6.0.0(react@18.3.1)' - react-loadable-ssr-addon-v5-slorber: 1.0.3(@docusaurus/react-loadable@6.0.0(react@18.3.1))(webpack@5.101.3) + react-loadable-ssr-addon-v5-slorber: 1.0.3(@docusaurus/react-loadable@6.0.0(react@18.3.1))(webpack@5.101.3(@swc/core@1.15.33)) react-router: 5.3.4(react@18.3.1) react-router-config: 5.1.1(react-router@5.3.4(react@18.3.1))(react@18.3.1) react-router-dom: 5.3.4(react@18.3.1) @@ -7092,10 +7424,12 @@ snapshots: tinypool: 1.1.1 tslib: 2.8.1 update-notifier: 6.0.2 - webpack: 5.101.3 + webpack: 5.101.3(@swc/core@1.15.33) webpack-bundle-analyzer: 4.10.2 - webpack-dev-server: 5.2.2(debug@4.4.3)(webpack@5.101.3) + webpack-dev-server: 5.2.2(debug@4.4.3)(webpack@5.101.3(@swc/core@1.15.33)) webpack-merge: 6.0.1 + optionalDependencies: + '@docusaurus/faster': 3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) transitivePeerDependencies: - '@parcel/css' - '@rspack/core' @@ -7119,21 +7453,39 @@ snapshots: postcss-sort-media-queries: 5.2.0(postcss@8.5.9) tslib: 2.8.1 + '@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))': + dependencies: + '@docusaurus/types': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@rspack/core': 1.7.11 + '@swc/core': 1.15.33 + '@swc/html': 1.15.33 + browserslist: 4.26.2 + lightningcss: 1.32.0 + semver: 7.7.2 + swc-loader: 0.2.7(@swc/core@1.15.33)(webpack@5.101.3(@swc/core@1.15.33)) + tslib: 2.8.1 + webpack: 5.101.3(@swc/core@1.15.33) + transitivePeerDependencies: + - '@swc/helpers' + - esbuild + - uglify-js + - webpack-cli + '@docusaurus/logger@3.10.1': dependencies: chalk: 4.1.2 tslib: 2.8.1 - '@docusaurus/mdx-loader@3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@docusaurus/mdx-loader@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@docusaurus/logger': 3.10.1 - '@docusaurus/utils': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils-validation': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mdx-js/mdx': 3.1.1 '@slorber/remark-comment': 1.0.0 escape-html: 1.0.3 estree-util-value-to-estree: 3.4.0 - file-loader: 6.2.0(webpack@5.101.3) + file-loader: 6.2.0(webpack@5.101.3(@swc/core@1.15.33)) fs-extra: 11.3.2 image-size: 2.0.2 mdast-util-mdx: 3.0.0 @@ -7149,9 +7501,9 @@ snapshots: tslib: 2.8.1 unified: 11.0.5 unist-util-visit: 5.0.0 - url-loader: 4.1.1(file-loader@6.2.0(webpack@5.101.3))(webpack@5.101.3) + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.101.3(@swc/core@1.15.33)))(webpack@5.101.3(@swc/core@1.15.33)) vfile: 6.0.3 - webpack: 5.101.3 + webpack: 5.101.3(@swc/core@1.15.33) transitivePeerDependencies: - '@swc/core' - esbuild @@ -7159,9 +7511,9 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/module-type-aliases@3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@docusaurus/module-type-aliases@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@docusaurus/types': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/types': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/history': 4.7.11 '@types/react': 19.2.14 '@types/react-router-config': 5.0.11 @@ -7177,17 +7529,17 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/plugin-content-blog@3.10.1(@docusaurus/plugin-content-docs@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3)': + '@docusaurus/plugin-content-blog@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@docusaurus/plugin-content-docs@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3)': dependencies: - '@docusaurus/core': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) + '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) '@docusaurus/logger': 3.10.1 - '@docusaurus/mdx-loader': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/plugin-content-docs': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) - '@docusaurus/theme-common': 3.10.1(@docusaurus/plugin-content-docs@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/types': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils-common': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils-validation': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/mdx-loader': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/plugin-content-docs': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) + '@docusaurus/theme-common': 3.10.1(@docusaurus/plugin-content-docs@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/types': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-common': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) cheerio: 1.0.0-rc.12 combine-promises: 1.2.0 feed: 4.2.2 @@ -7200,7 +7552,7 @@ snapshots: tslib: 2.8.1 unist-util-visit: 5.0.0 utility-types: 3.11.0 - webpack: 5.101.3 + webpack: 5.101.3(@swc/core@1.15.33) transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -7219,17 +7571,17 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-content-docs@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3)': + '@docusaurus/plugin-content-docs@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3)': dependencies: - '@docusaurus/core': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) + '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) '@docusaurus/logger': 3.10.1 - '@docusaurus/mdx-loader': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/module-type-aliases': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/theme-common': 3.10.1(@docusaurus/plugin-content-docs@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/types': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils-common': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils-validation': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/mdx-loader': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/module-type-aliases': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/theme-common': 3.10.1(@docusaurus/plugin-content-docs@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/types': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-common': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/react-router-config': 5.0.11 combine-promises: 1.2.0 fs-extra: 11.3.2 @@ -7240,7 +7592,7 @@ snapshots: schema-dts: 1.1.5 tslib: 2.8.1 utility-types: 3.11.0 - webpack: 5.101.3 + webpack: 5.101.3(@swc/core@1.15.33) transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -7259,18 +7611,18 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-content-pages@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3)': + '@docusaurus/plugin-content-pages@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3)': dependencies: - '@docusaurus/core': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) - '@docusaurus/mdx-loader': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/types': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils-validation': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) + '@docusaurus/mdx-loader': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/types': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) fs-extra: 11.3.2 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) tslib: 2.8.1 - webpack: 5.101.3 + webpack: 5.101.3(@swc/core@1.15.33) transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -7289,12 +7641,12 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-css-cascade-layers@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3)': + '@docusaurus/plugin-css-cascade-layers@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3)': dependencies: - '@docusaurus/core': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) - '@docusaurus/types': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils-validation': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) + '@docusaurus/types': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) tslib: 2.8.1 transitivePeerDependencies: - '@docusaurus/faster' @@ -7316,11 +7668,11 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-debug@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3)': + '@docusaurus/plugin-debug@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3)': dependencies: - '@docusaurus/core': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) - '@docusaurus/types': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) + '@docusaurus/types': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) fs-extra: 11.3.2 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -7344,11 +7696,11 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-google-analytics@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3)': + '@docusaurus/plugin-google-analytics@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3)': dependencies: - '@docusaurus/core': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) - '@docusaurus/types': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils-validation': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) + '@docusaurus/types': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) tslib: 2.8.1 @@ -7370,11 +7722,11 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-google-gtag@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3)': + '@docusaurus/plugin-google-gtag@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3)': dependencies: - '@docusaurus/core': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) - '@docusaurus/types': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils-validation': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) + '@docusaurus/types': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/gtag.js': 0.0.20 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -7397,11 +7749,11 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-google-tag-manager@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3)': + '@docusaurus/plugin-google-tag-manager@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3)': dependencies: - '@docusaurus/core': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) - '@docusaurus/types': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils-validation': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) + '@docusaurus/types': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) tslib: 2.8.1 @@ -7423,14 +7775,14 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-sitemap@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3)': + '@docusaurus/plugin-sitemap@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3)': dependencies: - '@docusaurus/core': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) + '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) '@docusaurus/logger': 3.10.1 - '@docusaurus/types': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils-common': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils-validation': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/types': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-common': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) fs-extra: 11.3.2 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -7454,18 +7806,18 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-svgr@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3)': + '@docusaurus/plugin-svgr@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3)': dependencies: - '@docusaurus/core': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) - '@docusaurus/types': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils-validation': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) + '@docusaurus/types': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@svgr/core': 8.1.0(typescript@6.0.3) '@svgr/webpack': 8.1.0(typescript@6.0.3) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) tslib: 2.8.1 - webpack: 5.101.3 + webpack: 5.101.3(@swc/core@1.15.33) transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -7484,23 +7836,23 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/preset-classic@3.10.1(@algolia/client-search@5.39.0)(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@6.0.3)': - dependencies: - '@docusaurus/core': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) - '@docusaurus/plugin-content-blog': 3.10.1(@docusaurus/plugin-content-docs@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) - '@docusaurus/plugin-content-docs': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) - '@docusaurus/plugin-content-pages': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) - '@docusaurus/plugin-css-cascade-layers': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) - '@docusaurus/plugin-debug': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) - '@docusaurus/plugin-google-analytics': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) - '@docusaurus/plugin-google-gtag': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) - '@docusaurus/plugin-google-tag-manager': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) - '@docusaurus/plugin-sitemap': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) - '@docusaurus/plugin-svgr': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) - '@docusaurus/theme-classic': 3.10.1(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) - '@docusaurus/theme-common': 3.10.1(@docusaurus/plugin-content-docs@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/theme-search-algolia': 3.10.1(@algolia/client-search@5.39.0)(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@6.0.3) - '@docusaurus/types': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/preset-classic@3.10.1(@algolia/client-search@5.39.0)(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@6.0.3)': + dependencies: + '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) + '@docusaurus/plugin-content-blog': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@docusaurus/plugin-content-docs@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) + '@docusaurus/plugin-content-docs': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) + '@docusaurus/plugin-content-pages': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) + '@docusaurus/plugin-css-cascade-layers': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) + '@docusaurus/plugin-debug': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) + '@docusaurus/plugin-google-analytics': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) + '@docusaurus/plugin-google-gtag': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) + '@docusaurus/plugin-google-tag-manager': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) + '@docusaurus/plugin-sitemap': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) + '@docusaurus/plugin-svgr': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) + '@docusaurus/theme-classic': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@rspack/core@1.7.11)(@swc/core@1.15.33)(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) + '@docusaurus/theme-common': 3.10.1(@docusaurus/plugin-content-docs@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/theme-search-algolia': 3.10.1(@algolia/client-search@5.39.0)(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@6.0.3) + '@docusaurus/types': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) transitivePeerDependencies: @@ -7529,21 +7881,21 @@ snapshots: '@types/react': 19.2.14 react: 18.3.1 - '@docusaurus/theme-classic@3.10.1(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3)': + '@docusaurus/theme-classic@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@rspack/core@1.7.11)(@swc/core@1.15.33)(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3)': dependencies: - '@docusaurus/core': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) + '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) '@docusaurus/logger': 3.10.1 - '@docusaurus/mdx-loader': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/module-type-aliases': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/plugin-content-blog': 3.10.1(@docusaurus/plugin-content-docs@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) - '@docusaurus/plugin-content-docs': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) - '@docusaurus/plugin-content-pages': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) - '@docusaurus/theme-common': 3.10.1(@docusaurus/plugin-content-docs@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/mdx-loader': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/module-type-aliases': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/plugin-content-blog': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@docusaurus/plugin-content-docs@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) + '@docusaurus/plugin-content-docs': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) + '@docusaurus/plugin-content-pages': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) + '@docusaurus/theme-common': 3.10.1(@docusaurus/plugin-content-docs@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/theme-translations': 3.10.1 - '@docusaurus/types': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils-common': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils-validation': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/types': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-common': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mdx-js/react': 3.1.1(@types/react@19.2.14)(react@18.3.1) clsx: 2.1.1 copy-text-to-clipboard: 3.2.2 @@ -7577,13 +7929,13 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/theme-common@3.10.1(@docusaurus/plugin-content-docs@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@docusaurus/theme-common@3.10.1(@docusaurus/plugin-content-docs@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@docusaurus/mdx-loader': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/module-type-aliases': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/plugin-content-docs': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) - '@docusaurus/utils': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils-common': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/mdx-loader': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/module-type-aliases': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/plugin-content-docs': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) + '@docusaurus/utils': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-common': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/history': 4.7.11 '@types/react': 19.2.14 '@types/react-router-config': 5.0.11 @@ -7601,17 +7953,17 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/theme-search-algolia@3.10.1(@algolia/client-search@5.39.0)(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@6.0.3)': + '@docusaurus/theme-search-algolia@3.10.1(@algolia/client-search@5.39.0)(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@6.0.3)': dependencies: '@algolia/autocomplete-core': 1.19.2(@algolia/client-search@5.39.0)(algoliasearch@5.39.0)(search-insights@2.17.3) '@docsearch/react': 4.6.3(@algolia/client-search@5.39.0)(@types/react@19.2.14)(algoliasearch@5.39.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3) - '@docusaurus/core': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) + '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) '@docusaurus/logger': 3.10.1 - '@docusaurus/plugin-content-docs': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) - '@docusaurus/theme-common': 3.10.1(@docusaurus/plugin-content-docs@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/plugin-content-docs': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) + '@docusaurus/theme-common': 3.10.1(@docusaurus/plugin-content-docs@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/theme-translations': 3.10.1 - '@docusaurus/utils': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils-validation': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) algoliasearch: 5.39.0 algoliasearch-helper: 3.26.0(algoliasearch@5.39.0) clsx: 2.1.1 @@ -7650,7 +8002,7 @@ snapshots: '@docusaurus/tsconfig@3.10.1': {} - '@docusaurus/types@3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@mdx-js/mdx': 3.1.1 '@types/history': 4.7.11 @@ -7662,7 +8014,7 @@ snapshots: react-dom: 18.3.1(react@18.3.1) react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)' utility-types: 3.11.0 - webpack: 5.101.3 + webpack: 5.101.3(@swc/core@1.15.33) webpack-merge: 5.10.0 transitivePeerDependencies: - '@swc/core' @@ -7671,9 +8023,9 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/utils-common@3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@docusaurus/utils-common@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@docusaurus/types': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/types': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) tslib: 2.8.1 transitivePeerDependencies: - '@swc/core' @@ -7684,11 +8036,11 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/utils-validation@3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@docusaurus/utils-validation@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@docusaurus/logger': 3.10.1 - '@docusaurus/utils': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils-common': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-common': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) fs-extra: 11.3.2 joi: 17.13.3 js-yaml: 4.1.1 @@ -7703,14 +8055,14 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/utils@3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@docusaurus/utils@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@docusaurus/logger': 3.10.1 - '@docusaurus/types': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils-common': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/types': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-common': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) escape-string-regexp: 4.0.0 execa: 5.1.1 - file-loader: 6.2.0(webpack@5.101.3) + file-loader: 6.2.0(webpack@5.101.3(@swc/core@1.15.33)) fs-extra: 11.3.2 github-slugger: 1.5.0 globby: 11.1.0 @@ -7723,9 +8075,9 @@ snapshots: prompts: 2.4.2 resolve-pathname: 3.0.0 tslib: 2.8.1 - url-loader: 4.1.1(file-loader@6.2.0(webpack@5.101.3))(webpack@5.101.3) + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.101.3(@swc/core@1.15.33)))(webpack@5.101.3(@swc/core@1.15.33)) utility-types: 3.11.0 - webpack: 5.101.3 + webpack: 5.101.3(@swc/core@1.15.33) transitivePeerDependencies: - '@swc/core' - esbuild @@ -7740,14 +8092,14 @@ snapshots: cssesc: 3.0.0 immediate: 3.3.0 - '@easyops-cn/docusaurus-search-local@0.55.1(@docusaurus/theme-common@3.10.1(@docusaurus/plugin-content-docs@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3)': + '@easyops-cn/docusaurus-search-local@0.55.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@docusaurus/theme-common@3.10.1(@docusaurus/plugin-content-docs@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3)': dependencies: - '@docusaurus/plugin-content-docs': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) - '@docusaurus/theme-common': 3.10.1(@docusaurus/plugin-content-docs@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/plugin-content-docs': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) + '@docusaurus/theme-common': 3.10.1(@docusaurus/plugin-content-docs@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/theme-translations': 3.10.1 - '@docusaurus/utils': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils-common': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils-validation': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-common': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@easyops-cn/autocomplete.js': 0.38.1 '@node-rs/jieba': 1.10.4 cheerio: 1.1.2 @@ -7921,6 +8273,31 @@ snapshots: '@types/react': 19.2.14 react: 18.3.1 + '@module-federation/error-codes@0.22.0': {} + + '@module-federation/runtime-core@0.22.0': + dependencies: + '@module-federation/error-codes': 0.22.0 + '@module-federation/sdk': 0.22.0 + + '@module-federation/runtime-tools@0.22.0': + dependencies: + '@module-federation/runtime': 0.22.0 + '@module-federation/webpack-bundler-runtime': 0.22.0 + + '@module-federation/runtime@0.22.0': + dependencies: + '@module-federation/error-codes': 0.22.0 + '@module-federation/runtime-core': 0.22.0 + '@module-federation/sdk': 0.22.0 + + '@module-federation/sdk@0.22.0': {} + + '@module-federation/webpack-bundler-runtime@0.22.0': + dependencies: + '@module-federation/runtime': 0.22.0 + '@module-federation/sdk': 0.22.0 + '@napi-rs/wasm-runtime@0.2.12': dependencies: '@emnapi/core': 1.5.0 @@ -7928,6 +8305,13 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true + '@napi-rs/wasm-runtime@1.0.7': + dependencies: + '@emnapi/core': 1.5.0 + '@emnapi/runtime': 1.5.0 + '@tybys/wasm-util': 0.10.1 + optional: true + '@node-rs/jieba-android-arm-eabi@1.10.4': optional: true @@ -8119,6 +8503,59 @@ snapshots: react: 18.3.1 react-redux: 9.2.0(@types/react@19.2.14)(react@18.3.1)(redux@5.0.1) + '@rspack/binding-darwin-arm64@1.7.11': + optional: true + + '@rspack/binding-darwin-x64@1.7.11': + optional: true + + '@rspack/binding-linux-arm64-gnu@1.7.11': + optional: true + + '@rspack/binding-linux-arm64-musl@1.7.11': + optional: true + + '@rspack/binding-linux-x64-gnu@1.7.11': + optional: true + + '@rspack/binding-linux-x64-musl@1.7.11': + optional: true + + '@rspack/binding-wasm32-wasi@1.7.11': + dependencies: + '@napi-rs/wasm-runtime': 1.0.7 + optional: true + + '@rspack/binding-win32-arm64-msvc@1.7.11': + optional: true + + '@rspack/binding-win32-ia32-msvc@1.7.11': + optional: true + + '@rspack/binding-win32-x64-msvc@1.7.11': + optional: true + + '@rspack/binding@1.7.11': + optionalDependencies: + '@rspack/binding-darwin-arm64': 1.7.11 + '@rspack/binding-darwin-x64': 1.7.11 + '@rspack/binding-linux-arm64-gnu': 1.7.11 + '@rspack/binding-linux-arm64-musl': 1.7.11 + '@rspack/binding-linux-x64-gnu': 1.7.11 + '@rspack/binding-linux-x64-musl': 1.7.11 + '@rspack/binding-wasm32-wasi': 1.7.11 + '@rspack/binding-win32-arm64-msvc': 1.7.11 + '@rspack/binding-win32-ia32-msvc': 1.7.11 + '@rspack/binding-win32-x64-msvc': 1.7.11 + + '@rspack/core@1.7.11': + dependencies: + '@module-federation/runtime-tools': 0.22.0 + '@rspack/binding': 1.7.11 + '@rspack/lite-tapable': 1.1.0 + + '@rspack/lite-tapable@1.1.0': {} + '@sideway/address@4.1.5': dependencies: '@hapi/hoek': 9.3.0 @@ -8246,6 +8683,119 @@ snapshots: - supports-color - typescript + '@swc/core-darwin-arm64@1.15.33': + optional: true + + '@swc/core-darwin-x64@1.15.33': + optional: true + + '@swc/core-linux-arm-gnueabihf@1.15.33': + optional: true + + '@swc/core-linux-arm64-gnu@1.15.33': + optional: true + + '@swc/core-linux-arm64-musl@1.15.33': + optional: true + + '@swc/core-linux-ppc64-gnu@1.15.33': + optional: true + + '@swc/core-linux-s390x-gnu@1.15.33': + optional: true + + '@swc/core-linux-x64-gnu@1.15.33': + optional: true + + '@swc/core-linux-x64-musl@1.15.33': + optional: true + + '@swc/core-win32-arm64-msvc@1.15.33': + optional: true + + '@swc/core-win32-ia32-msvc@1.15.33': + optional: true + + '@swc/core-win32-x64-msvc@1.15.33': + optional: true + + '@swc/core@1.15.33': + dependencies: + '@swc/counter': 0.1.3 + '@swc/types': 0.1.26 + optionalDependencies: + '@swc/core-darwin-arm64': 1.15.33 + '@swc/core-darwin-x64': 1.15.33 + '@swc/core-linux-arm-gnueabihf': 1.15.33 + '@swc/core-linux-arm64-gnu': 1.15.33 + '@swc/core-linux-arm64-musl': 1.15.33 + '@swc/core-linux-ppc64-gnu': 1.15.33 + '@swc/core-linux-s390x-gnu': 1.15.33 + '@swc/core-linux-x64-gnu': 1.15.33 + '@swc/core-linux-x64-musl': 1.15.33 + '@swc/core-win32-arm64-msvc': 1.15.33 + '@swc/core-win32-ia32-msvc': 1.15.33 + '@swc/core-win32-x64-msvc': 1.15.33 + + '@swc/counter@0.1.3': {} + + '@swc/html-darwin-arm64@1.15.33': + optional: true + + '@swc/html-darwin-x64@1.15.33': + optional: true + + '@swc/html-linux-arm-gnueabihf@1.15.33': + optional: true + + '@swc/html-linux-arm64-gnu@1.15.33': + optional: true + + '@swc/html-linux-arm64-musl@1.15.33': + optional: true + + '@swc/html-linux-ppc64-gnu@1.15.33': + optional: true + + '@swc/html-linux-s390x-gnu@1.15.33': + optional: true + + '@swc/html-linux-x64-gnu@1.15.33': + optional: true + + '@swc/html-linux-x64-musl@1.15.33': + optional: true + + '@swc/html-win32-arm64-msvc@1.15.33': + optional: true + + '@swc/html-win32-ia32-msvc@1.15.33': + optional: true + + '@swc/html-win32-x64-msvc@1.15.33': + optional: true + + '@swc/html@1.15.33': + dependencies: + '@swc/counter': 0.1.3 + optionalDependencies: + '@swc/html-darwin-arm64': 1.15.33 + '@swc/html-darwin-x64': 1.15.33 + '@swc/html-linux-arm-gnueabihf': 1.15.33 + '@swc/html-linux-arm64-gnu': 1.15.33 + '@swc/html-linux-arm64-musl': 1.15.33 + '@swc/html-linux-ppc64-gnu': 1.15.33 + '@swc/html-linux-s390x-gnu': 1.15.33 + '@swc/html-linux-x64-gnu': 1.15.33 + '@swc/html-linux-x64-musl': 1.15.33 + '@swc/html-win32-arm64-msvc': 1.15.33 + '@swc/html-win32-ia32-msvc': 1.15.33 + '@swc/html-win32-x64-msvc': 1.15.33 + + '@swc/types@0.1.26': + dependencies: + '@swc/counter': 0.1.3 + '@szmarczak/http-timer@5.0.1': dependencies: defer-to-connect: 2.0.1 @@ -8647,12 +9197,12 @@ snapshots: postcss: 8.5.9 postcss-value-parser: 4.2.0 - babel-loader@9.2.1(@babel/core@7.28.4)(webpack@5.101.3): + babel-loader@9.2.1(@babel/core@7.28.4)(webpack@5.101.3(@swc/core@1.15.33)): dependencies: '@babel/core': 7.28.4 find-cache-dir: 4.0.0 schema-utils: 4.3.2 - webpack: 5.101.3 + webpack: 5.101.3(@swc/core@1.15.33) babel-plugin-dynamic-import-node@2.3.3: dependencies: @@ -9027,7 +9577,7 @@ snapshots: copy-text-to-clipboard@3.2.2: {} - copy-webpack-plugin@11.0.0(webpack@5.101.3): + copy-webpack-plugin@11.0.0(webpack@5.101.3(@swc/core@1.15.33)): dependencies: fast-glob: 3.3.3 glob-parent: 6.0.2 @@ -9035,7 +9585,7 @@ snapshots: normalize-path: 3.0.0 schema-utils: 4.3.2 serialize-javascript: 6.0.2 - webpack: 5.101.3 + webpack: 5.101.3(@swc/core@1.15.33) core-js-compat@3.45.1: dependencies: @@ -9082,7 +9632,7 @@ snapshots: postcss-selector-parser: 7.1.1 postcss-value-parser: 4.2.0 - css-loader@6.11.0(webpack@5.101.3): + css-loader@6.11.0(@rspack/core@1.7.11)(webpack@5.101.3(@swc/core@1.15.33)): dependencies: icss-utils: 5.1.0(postcss@8.5.9) postcss: 8.5.9 @@ -9093,9 +9643,10 @@ snapshots: postcss-value-parser: 4.2.0 semver: 7.7.2 optionalDependencies: - webpack: 5.101.3 + '@rspack/core': 1.7.11 + webpack: 5.101.3(@swc/core@1.15.33) - css-minimizer-webpack-plugin@5.0.1(clean-css@5.3.3)(webpack@5.101.3): + css-minimizer-webpack-plugin@5.0.1(clean-css@5.3.3)(webpack@5.101.3(@swc/core@1.15.33)): dependencies: '@jridgewell/trace-mapping': 0.3.31 cssnano: 6.1.2(postcss@8.5.9) @@ -9103,7 +9654,7 @@ snapshots: postcss: 8.5.9 schema-utils: 4.3.2 serialize-javascript: 6.0.2 - webpack: 5.101.3 + webpack: 5.101.3(@swc/core@1.15.33) optionalDependencies: clean-css: 5.3.3 @@ -9259,8 +9810,7 @@ snapshots: destroy@1.2.0: {} - detect-libc@2.1.2: - optional: true + detect-libc@2.1.2: {} detect-node@2.1.0: {} @@ -9291,12 +9841,12 @@ snapshots: dependencies: '@leichtgewicht/ip-codec': 2.0.5 - docusaurus-plugin-openapi-docs@5.0.1(@docusaurus/plugin-content-docs@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(@docusaurus/utils-validation@3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@docusaurus/utils@3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/json-schema@7.0.15)(react@18.3.1): + docusaurus-plugin-openapi-docs@5.0.2(@docusaurus/plugin-content-docs@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(@docusaurus/utils-validation@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@docusaurus/utils@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/json-schema@7.0.15)(react@18.3.1): dependencies: '@apidevtools/json-schema-ref-parser': 15.3.5(@types/json-schema@7.0.15) - '@docusaurus/plugin-content-docs': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) - '@docusaurus/utils': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils-validation': 3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/plugin-content-docs': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) + '@docusaurus/utils': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@redocly/openapi-core': 2.30.3 allof-merge: 0.6.8 chalk: 5.6.2 @@ -9316,20 +9866,20 @@ snapshots: - '@types/json-schema' - encoding - docusaurus-plugin-sass@0.2.6(@docusaurus/core@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(sass@1.99.0)(webpack@5.101.3): + docusaurus-plugin-sass@0.2.6(@docusaurus/core@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(@rspack/core@1.7.11)(sass@1.99.0)(webpack@5.101.3(@swc/core@1.15.33)): dependencies: - '@docusaurus/core': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) + '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) sass: 1.99.0 - sass-loader: 16.0.7(sass@1.99.0)(webpack@5.101.3) + sass-loader: 16.0.7(@rspack/core@1.7.11)(sass@1.99.0)(webpack@5.101.3(@swc/core@1.15.33)) transitivePeerDependencies: - '@rspack/core' - node-sass - sass-embedded - webpack - docusaurus-theme-openapi-docs@5.0.1(@docusaurus/theme-common@3.10.1(@docusaurus/plugin-content-docs@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@19.2.14)(docusaurus-plugin-openapi-docs@5.0.1(@docusaurus/plugin-content-docs@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(@docusaurus/utils-validation@3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@docusaurus/utils@3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/json-schema@7.0.15)(react@18.3.1))(docusaurus-plugin-sass@0.2.6(@docusaurus/core@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(sass@1.99.0)(webpack@5.101.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(redux@5.0.1)(webpack@5.101.3): + docusaurus-theme-openapi-docs@5.0.2(e20b839675fe7ee566ace2729a870ef0): dependencies: - '@docusaurus/theme-common': 3.10.1(@docusaurus/plugin-content-docs@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/theme-common': 3.10.1(@docusaurus/plugin-content-docs@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@hookform/error-message': 2.0.1(react-dom@18.3.1(react@18.3.1))(react-hook-form@7.74.0(react@18.3.1))(react@18.3.1) '@reduxjs/toolkit': 2.11.2(react-redux@9.2.0(@types/react@19.2.14)(react@18.3.1)(redux@5.0.1))(react@18.3.1) allof-merge: 0.6.8 @@ -9337,8 +9887,8 @@ snapshots: clsx: 2.1.1 copy-text-to-clipboard: 3.2.2 crypto-js: 4.2.0 - docusaurus-plugin-openapi-docs: 5.0.1(@docusaurus/plugin-content-docs@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(@docusaurus/utils-validation@3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@docusaurus/utils@3.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/json-schema@7.0.15)(react@18.3.1) - docusaurus-plugin-sass: 0.2.6(@docusaurus/core@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(sass@1.99.0)(webpack@5.101.3) + docusaurus-plugin-openapi-docs: 5.0.2(@docusaurus/plugin-content-docs@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(@docusaurus/utils-validation@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@docusaurus/utils@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/json-schema@7.0.15)(react@18.3.1) + docusaurus-plugin-sass: 0.2.6(@docusaurus/core@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@rspack/core@1.7.11)(@swc/core@1.15.33)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(@rspack/core@1.7.11)(sass@1.99.0)(webpack@5.101.3(@swc/core@1.15.33)) file-saver: 2.0.5 lodash: 4.17.23 pako: 2.1.0 @@ -9358,7 +9908,7 @@ snapshots: rehype-raw: 7.0.0 remark-gfm: 4.0.1 sass: 1.99.0 - sass-loader: 16.0.7(sass@1.99.0)(webpack@5.101.3) + sass-loader: 16.0.7(@rspack/core@1.7.11)(sass@1.99.0)(webpack@5.101.3(@swc/core@1.15.33)) unist-util-visit: 5.0.0 url: 0.11.4 xml-formatter: 3.7.0 @@ -9661,11 +10211,11 @@ snapshots: dependencies: xml-js: 1.6.11 - file-loader@6.2.0(webpack@5.101.3): + file-loader@6.2.0(webpack@5.101.3(@swc/core@1.15.33)): dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.101.3 + webpack: 5.101.3(@swc/core@1.15.33) file-saver@2.0.5: {} @@ -9781,7 +10331,7 @@ snapshots: fs.realpath: 1.0.0 inflight: 1.0.6 inherits: 2.0.4 - minimatch: 3.1.2 + minimatch: 3.1.5 once: 1.4.0 path-is-absolute: 1.0.1 @@ -10001,7 +10551,7 @@ snapshots: html-void-elements@3.0.0: {} - html-webpack-plugin@5.6.4(webpack@5.101.3): + html-webpack-plugin@5.6.4(@rspack/core@1.7.11)(webpack@5.101.3(@swc/core@1.15.33)): dependencies: '@types/html-minifier-terser': 6.1.0 html-minifier-terser: 6.1.0 @@ -10009,7 +10559,8 @@ snapshots: pretty-error: 4.0.0 tapable: 2.2.3 optionalDependencies: - webpack: 5.101.3 + '@rspack/core': 1.7.11 + webpack: 5.101.3(@swc/core@1.15.33) htmlparser2@10.0.0: dependencies: @@ -10352,6 +10903,55 @@ snapshots: leven@3.1.0: {} + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} @@ -10951,18 +11551,14 @@ snapshots: mimic-response@4.0.0: {} - mini-css-extract-plugin@2.9.4(webpack@5.101.3): + mini-css-extract-plugin@2.9.4(webpack@5.101.3(@swc/core@1.15.33)): dependencies: schema-utils: 4.3.2 tapable: 2.2.3 - webpack: 5.101.3 + webpack: 5.101.3(@swc/core@1.15.33) minimalistic-assert@1.0.1: {} - minimatch@3.1.2: - dependencies: - brace-expansion: 1.1.12 - minimatch@3.1.5: dependencies: brace-expansion: 1.1.12 @@ -11045,11 +11641,11 @@ snapshots: dependencies: boolbase: 1.0.0 - null-loader@4.0.1(webpack@5.101.3): + null-loader@4.0.1(webpack@5.101.3(@swc/core@1.15.33)): dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.101.3 + webpack: 5.101.3(@swc/core@1.15.33) oas-kit-common@1.0.8: dependencies: @@ -11452,13 +12048,13 @@ snapshots: postcss: 8.5.9 yaml: 2.8.1 - postcss-loader@7.3.4(postcss@8.5.9)(typescript@6.0.3)(webpack@5.101.3): + postcss-loader@7.3.4(postcss@8.5.9)(typescript@6.0.3)(webpack@5.101.3(@swc/core@1.15.33)): dependencies: cosmiconfig: 8.3.6(typescript@6.0.3) jiti: 1.21.7 postcss: 8.5.9 semver: 7.7.2 - webpack: 5.101.3 + webpack: 5.101.3(@swc/core@1.15.33) transitivePeerDependencies: - typescript @@ -11885,11 +12481,11 @@ snapshots: sucrase: 3.35.1 use-editable: 2.3.3(react@18.3.1) - react-loadable-ssr-addon-v5-slorber@1.0.3(@docusaurus/react-loadable@6.0.0(react@18.3.1))(webpack@5.101.3): + react-loadable-ssr-addon-v5-slorber@1.0.3(@docusaurus/react-loadable@6.0.0(react@18.3.1))(webpack@5.101.3(@swc/core@1.15.33)): dependencies: '@babel/runtime': 7.29.2 react-loadable: '@docusaurus/react-loadable@6.0.0(react@18.3.1)' - webpack: 5.101.3 + webpack: 5.101.3(@swc/core@1.15.33) react-magic-dropzone@1.0.1: {} @@ -12199,12 +12795,13 @@ snapshots: safer-buffer@2.1.2: {} - sass-loader@16.0.7(sass@1.99.0)(webpack@5.101.3): + sass-loader@16.0.7(@rspack/core@1.7.11)(sass@1.99.0)(webpack@5.101.3(@swc/core@1.15.33)): dependencies: neo-async: 2.6.2 optionalDependencies: + '@rspack/core': 1.7.11 sass: 1.99.0 - webpack: 5.101.3 + webpack: 5.101.3(@swc/core@1.15.33) sass@1.99.0: dependencies: @@ -12592,6 +13189,12 @@ snapshots: transitivePeerDependencies: - encoding + swc-loader@0.2.7(@swc/core@1.15.33)(webpack@5.101.3(@swc/core@1.15.33)): + dependencies: + '@swc/core': 1.15.33 + '@swc/counter': 0.1.3 + webpack: 5.101.3(@swc/core@1.15.33) + tailwindcss@3.4.19(yaml@2.8.1): dependencies: '@alloc/quick-lru': 5.2.0 @@ -12622,14 +13225,16 @@ snapshots: tapable@2.2.3: {} - terser-webpack-plugin@5.3.14(webpack@5.101.3): + terser-webpack-plugin@5.3.14(@swc/core@1.15.33)(webpack@5.101.3(@swc/core@1.15.33)): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.2 serialize-javascript: 6.0.2 terser: 5.44.0 - webpack: 5.101.3 + webpack: 5.101.3(@swc/core@1.15.33) + optionalDependencies: + '@swc/core': 1.15.33 terser@5.44.0: dependencies: @@ -12791,14 +13396,14 @@ snapshots: dependencies: punycode: 2.3.1 - url-loader@4.1.1(file-loader@6.2.0(webpack@5.101.3))(webpack@5.101.3): + url-loader@4.1.1(file-loader@6.2.0(webpack@5.101.3(@swc/core@1.15.33)))(webpack@5.101.3(@swc/core@1.15.33)): dependencies: loader-utils: 2.0.4 mime-types: 2.1.35 schema-utils: 3.3.0 - webpack: 5.101.3 + webpack: 5.101.3(@swc/core@1.15.33) optionalDependencies: - file-loader: 6.2.0(webpack@5.101.3) + file-loader: 6.2.0(webpack@5.101.3(@swc/core@1.15.33)) url@0.11.4: dependencies: @@ -12892,7 +13497,7 @@ snapshots: - bufferutil - utf-8-validate - webpack-dev-middleware@7.4.5(webpack@5.101.3): + webpack-dev-middleware@7.4.5(webpack@5.101.3(@swc/core@1.15.33)): dependencies: colorette: 2.0.20 memfs: 4.47.0 @@ -12901,9 +13506,9 @@ snapshots: range-parser: 1.2.1 schema-utils: 4.3.2 optionalDependencies: - webpack: 5.101.3 + webpack: 5.101.3(@swc/core@1.15.33) - webpack-dev-server@5.2.2(debug@4.4.3)(webpack@5.101.3): + webpack-dev-server@5.2.2(debug@4.4.3)(webpack@5.101.3(@swc/core@1.15.33)): dependencies: '@types/bonjour': 3.5.13 '@types/connect-history-api-fallback': 1.5.4 @@ -12931,10 +13536,10 @@ snapshots: serve-index: 1.9.1 sockjs: 0.3.24 spdy: 4.0.2 - webpack-dev-middleware: 7.4.5(webpack@5.101.3) + webpack-dev-middleware: 7.4.5(webpack@5.101.3(@swc/core@1.15.33)) ws: 8.18.3 optionalDependencies: - webpack: 5.101.3 + webpack: 5.101.3(@swc/core@1.15.33) transitivePeerDependencies: - bufferutil - debug @@ -12955,7 +13560,7 @@ snapshots: webpack-sources@3.3.3: {} - webpack@5.101.3: + webpack@5.101.3(@swc/core@1.15.33): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.8 @@ -12979,7 +13584,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.2 tapable: 2.2.3 - terser-webpack-plugin: 5.3.14(webpack@5.101.3) + terser-webpack-plugin: 5.3.14(@swc/core@1.15.33)(webpack@5.101.3(@swc/core@1.15.33)) watchpack: 2.4.4 webpack-sources: 3.3.3 transitivePeerDependencies: @@ -12987,14 +13592,15 @@ snapshots: - esbuild - uglify-js - webpackbar@7.0.0(webpack@5.101.3): + webpackbar@7.0.0(@rspack/core@1.7.11)(webpack@5.101.3(@swc/core@1.15.33)): dependencies: ansis: 3.17.0 consola: 3.4.2 pretty-time: 1.1.0 std-env: 3.9.0 optionalDependencies: - webpack: 5.101.3 + '@rspack/core': 1.7.11 + webpack: 5.101.3(@swc/core@1.15.33) websocket-driver@0.7.4: dependencies: diff --git a/gen-docs/sidebars.ts b/gen-docs/sidebars.ts index bad106a1f5..9e08e5f084 100644 --- a/gen-docs/sidebars.ts +++ b/gen-docs/sidebars.ts @@ -1,4 +1,6 @@ import type { SidebarsConfig } from '@docusaurus/plugin-content-docs'; +// eslint-disable-next-line no-relative-import-paths/no-relative-import-paths +import apiSidebar from '../docs/api/sidebar'; /** * Creating a sidebar enables you to: @@ -13,6 +15,7 @@ import type { SidebarsConfig } from '@docusaurus/plugin-content-docs'; const sidebars: SidebarsConfig = { // By default, Docusaurus generates a sidebar from the docs folder structure documentationSidebar: [{ type: 'autogenerated', dirName: '.' }], + apisidebar: apiSidebar, // But you can create a sidebar manually /* diff --git a/next-env.d.ts b/next-env.d.ts deleted file mode 100644 index a4a7b3f5cf..0000000000 --- a/next-env.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -/// -/// - -// NOTE: This file should not be edited -// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/next.config.js b/next.config.ts similarity index 59% rename from next.config.js rename to next.config.ts index 9f15b7ff46..a3b5414f06 100644 --- a/next.config.js +++ b/next.config.ts @@ -1,9 +1,9 @@ -/** - * @type {import('next').NextConfig} - */ -module.exports = { +import type { NextConfig } from 'next'; + +const nextConfig: NextConfig = { env: { commitTag: process.env.COMMIT_TAG || 'local', + unsafeDoNotUseDemo: process.env.UNSAFE_DO_NOT_USE_DEMO, }, images: { remotePatterns: [ @@ -14,17 +14,18 @@ module.exports = { ], }, transpilePackages: ['country-flag-icons'], - webpack(config) { - config.module.rules.push({ - test: /\.svg$/, - issuer: /\.(js|ts)x?$/, - use: ['@svgr/webpack'], - }); - - return config; + turbopack: { + rules: { + '*.svg': { + loaders: ['@svgr/webpack'], + as: '*.js', + }, + }, }, experimental: { scrollRestoration: true, largePageDataBytes: 512 * 1000, }, }; + +export default nextConfig; diff --git a/package.json b/package.json index fc94c76635..9e16cf4ed2 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { "name": "seerr", - "version": "0.1.0", + "version": "3.5.0", "private": true, "packageManager": "pnpm@10.24.0", "scripts": { "preinstall": "npx only-allow pnpm", "postinstall": "next telemetry disable", "dev": "nodemon -e ts,json,yml --watch server --watch seerr-api.yml --exec 'ts-node -r tsconfig-paths/register --files --project server/tsconfig.json server/index.ts'", - "build:server": "tsc --project server/tsconfig.json && copyfiles -u 2 server/templates/**/*.{html,pug} dist/templates && copyfiles -u 2 \"server/i18n/locale/*.json\" dist/i18n && tsc-alias -p server/tsconfig.json", + "build:server": "tsc --project server/tsconfig.build.json && copyfiles -u 2 server/templates/**/*.{html,pug} dist/templates && copyfiles -u 2 \"server/i18n/locale/*.json\" dist/i18n && tsc-alias -p server/tsconfig.build.json", "build:next": "next build", "build": "pnpm build:next && pnpm build:server", "lint": "eslint \"./server/**/*.{ts,tsx}\" \"./src/**/*.{ts,tsx}\" --cache", @@ -23,7 +23,7 @@ "typecheck": "pnpm typecheck:server && pnpm typecheck:client", "typecheck:server": "tsc --project server/tsconfig.json --noEmit", "typecheck:client": "tsc --noEmit", - "prepare": "node bin/prepare.js", + "prepare": "node bin/prepare.mjs", "cypress:open": "cypress open", "cypress:prepare": "ts-node -r tsconfig-paths/register --files --project server/tsconfig.json server/scripts/prepareTestDb.ts", "cypress:build": "pnpm build && pnpm cypress:prepare" @@ -38,7 +38,7 @@ "@fontsource-variable/inter": "^5.2.8", "@formatjs/intl": "^4.1.4", "@formatjs/intl-locale": "5.3.2", - "@headlessui/react": "1.7.19", + "@headlessui/react": "2.2.10", "@heroicons/react": "2.2.0", "@react-spring/web": "^10.0.3", "@seerr-team/react-tailwindcss-datepicker": "^1.3.4", @@ -68,21 +68,22 @@ "humanize-duration": "^3.33.2", "js-yaml": "^4.1.1", "lodash": "4.18.1", + "lru-cache": "11.2.1", "mime": "^4.1.0", "nanoid": "^5.1.7", - "next": "^14.2.35", - "node-cache": "5.1.2", + "next": "16.2.6", + "ip-address": "^10.2.0", "node-gyp": "12.2.0", "node-schedule": "2.1.1", "nodemailer": "8.0.5", "openpgp": "6.3.0", - "pg": "8.20.0", + "pg": "8.23.0", "pug": "3.0.4", - "react": "^18.3.1", + "react": "19.2.6", "react-ace": "14.0.1", "react-animate-height": "3.2.3", "react-aria": "3.47.0", - "react-dom": "^18.3.1", + "react-dom": "19.2.6", "react-hot-toast": "^2.6.0", "react-intersection-observer": "10.0.3", "react-intl": "^7.1.14", @@ -99,7 +100,7 @@ "swagger-ui-express": "5.0.1", "swr": "2.4.1", "tailwind-merge": "^2.6.1", - "typeorm": "0.3.28", + "typeorm": "0.3.31", "ua-parser-js": "^2.0.9", "undici": "^8.1.0", "validator": "^13.15.35", @@ -115,7 +116,7 @@ "@commitlint/cli": "20.5.0", "@commitlint/config-conventional": "20.5.0", "@eslint/js": "9.39.3", - "@next/eslint-plugin-next": "^16.2.3", + "@next/eslint-plugin-next": "16.2.6", "@tailwindcss/aspect-ratio": "^0.4.2", "@tailwindcss/forms": "^0.5.11", "@tailwindcss/typography": "^0.5.19", @@ -130,12 +131,11 @@ "@types/humanize-duration": "^3.27.4", "@types/js-yaml": "^4.0.9", "@types/lodash": "4.17.24", - "@types/mime": "^3.0.4", "@types/node": "22.19.0", "@types/node-schedule": "2.1.8", "@types/nodemailer": "^8.0.0", - "@types/react": "^18.3.28", - "@types/react-dom": "^18.3.7", + "@types/react": "19.2.14", + "@types/react-dom": "19.2.3", "@types/react-transition-group": "4.4.12", "@types/semver": "7.7.1", "@types/supertest": "^7.2.0", @@ -154,6 +154,7 @@ "cypress": "15.13.1", "cz-conventional-changelog": "3.3.0", "eslint": "9.39.3", + "eslint-config-next": "16.2.6", "eslint-config-prettier": "10.1.8", "eslint-plugin-formatjs": "6.4.5", "eslint-plugin-jsx-a11y": "6.10.2", @@ -162,7 +163,7 @@ "eslint-plugin-react": "7.37.5", "eslint-plugin-react-hooks": "7.1.1", "globals": "^17.5.0", - "husky": "8.0.3", + "husky": "9.1.7", "jiti": "^2.6.1", "lint-staged": "16.4.0", "nodemon": "3.1.14", @@ -211,7 +212,9 @@ ], "overrides": { "sqlite3>node-gyp": "8.4.1", - "@types/express-session": "1.18.2" + "@types/express-session": "1.18.2", + "@types/react": "19.2.14", + "@types/react-dom": "19.2.3" } }, "scarfSettings": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3143a9e1cb..829703b19a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,8 @@ settings: overrides: sqlite3>node-gyp: 8.4.1 '@types/express-session': 1.18.2 + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3 importers: @@ -25,17 +27,17 @@ importers: specifier: 5.3.2 version: 5.3.2 '@headlessui/react': - specifier: 1.7.19 - version: 1.7.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 2.2.10 + version: 2.2.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@heroicons/react': specifier: 2.2.0 - version: 2.2.0(react@18.3.1) + version: 2.2.0(react@19.2.6) '@react-spring/web': specifier: ^10.0.3 - version: 10.0.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 10.0.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@seerr-team/react-tailwindcss-datepicker': specifier: ^1.3.4 - version: 1.3.4(dayjs@1.11.20)(react@18.3.1) + version: 1.3.4(dayjs@1.11.23)(react@19.2.6) '@supercharge/request-ip': specifier: 1.2.0 version: 1.2.0 @@ -44,7 +46,7 @@ importers: version: 8.1.0(typescript@5.4.5) '@tanem/react-nprogress': specifier: 6.0.3 - version: 6.0.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 6.0.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6) ace-builds: specifier: 1.43.6 version: 1.43.6 @@ -62,7 +64,7 @@ importers: version: 2.14.1 connect-typeorm: specifier: 2.0.0 - version: 2.0.0(typeorm@0.3.28(babel-plugin-macros@3.1.0)(pg@8.20.0)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.19.0)(typescript@5.4.5))) + version: 2.0.0(typeorm@0.3.31(babel-plugin-macros@3.1.0)(pg@8.23.0)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.15))(@types/node@22.19.0)(typescript@5.4.5))) cookie-parser: specifier: 1.4.7 version: 1.4.7 @@ -80,7 +82,7 @@ importers: version: 0.2.9 email-templates: specifier: 13.0.1 - version: 13.0.1(@babel/core@7.29.0)(handlebars@4.7.8)(mustache@4.2.0)(pug@3.0.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(underscore@1.13.7) + version: 13.0.1(@babel/core@7.29.0)(handlebars@4.7.8)(mustache@4.2.0)(pug@3.0.4)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(underscore@1.13.7) express: specifier: 5.2.1 version: 5.2.1 @@ -95,7 +97,7 @@ importers: version: 1.19.0 formik: specifier: ^2.4.9 - version: 2.4.9(react@18.3.1) + version: 2.4.9(react@19.2.6) gravatar-url: specifier: 4.0.1 version: 4.0.1 @@ -108,24 +110,27 @@ importers: humanize-duration: specifier: ^3.33.2 version: 3.33.2 + ip-address: + specifier: ^10.2.0 + version: 10.2.0 js-yaml: specifier: ^4.1.1 version: 4.1.1 lodash: specifier: 4.18.1 version: 4.18.1 + lru-cache: + specifier: 11.2.1 + version: 11.2.1 mime: specifier: ^4.1.0 version: 4.1.0 nanoid: specifier: ^5.1.7 - version: 5.1.9 + version: 5.1.11 next: - specifier: ^14.2.35 - version: 14.2.35(@babel/core@7.29.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - node-cache: - specifier: 5.1.2 - version: 5.1.2 + specifier: 16.2.6 + version: 16.2.6(@babel/core@7.29.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) node-gyp: specifier: 12.2.0 version: 12.2.0 @@ -139,53 +144,53 @@ importers: specifier: 6.3.0 version: 6.3.0 pg: - specifier: 8.20.0 - version: 8.20.0 + specifier: 8.23.0 + version: 8.23.0 pug: specifier: 3.0.4 version: 3.0.4 react: - specifier: ^18.3.1 - version: 18.3.1 + specifier: 19.2.6 + version: 19.2.6 react-ace: specifier: 14.0.1 - version: 14.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 14.0.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) react-animate-height: specifier: 3.2.3 - version: 3.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 3.2.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6) react-aria: specifier: 3.47.0 - version: 3.47.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 3.47.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) react-dom: - specifier: ^18.3.1 - version: 18.3.1(react@18.3.1) + specifier: 19.2.6 + version: 19.2.6(react@19.2.6) react-hot-toast: specifier: ^2.6.0 - version: 2.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 2.6.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) react-intersection-observer: specifier: 10.0.3 - version: 10.0.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 10.0.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6) react-intl: specifier: ^7.1.14 - version: 7.1.14(react@18.3.1)(typescript@5.4.5) + version: 7.1.14(react@19.2.6)(typescript@5.4.5) react-markdown: specifier: 10.1.0 - version: 10.1.0(@types/react@18.3.28)(react@18.3.1) + version: 10.1.0(@types/react@19.2.14)(react@19.2.6) react-popper-tooltip: specifier: 4.4.2 - version: 4.4.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 4.4.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) react-select: specifier: 5.10.2 - version: 5.10.2(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 5.10.2(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) react-transition-group: specifier: ^4.4.5 - version: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 4.4.5(react-dom@19.2.6(react@19.2.6))(react@19.2.6) react-truncate-markup: specifier: 5.1.2 - version: 5.1.2(react@18.3.1) + version: 5.1.2(react@19.2.6) react-use-clipboard: specifier: 1.0.9 - version: 1.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 1.0.9(react-dom@19.2.6(react@19.2.6))(react@19.2.6) reflect-metadata: specifier: ^0.2.2 version: 0.2.2 @@ -203,13 +208,13 @@ importers: version: 5.0.1(express@5.2.1) swr: specifier: 2.4.1 - version: 2.4.1(react@18.3.1) + version: 2.4.1(react@19.2.6) tailwind-merge: specifier: ^2.6.1 version: 2.6.1 typeorm: - specifier: 0.3.28 - version: 0.3.28(babel-plugin-macros@3.1.0)(pg@8.20.0)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.19.0)(typescript@5.4.5)) + specifier: 0.3.31 + version: 0.3.31(babel-plugin-macros@3.1.0)(pg@8.23.0)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.15))(@types/node@22.19.0)(typescript@5.4.5)) ua-parser-js: specifier: ^2.0.9 version: 2.0.9 @@ -251,8 +256,8 @@ importers: specifier: 9.39.3 version: 9.39.3 '@next/eslint-plugin-next': - specifier: ^16.2.3 - version: 16.2.4 + specifier: 16.2.6 + version: 16.2.6 '@tailwindcss/aspect-ratio': specifier: ^0.4.2 version: 0.4.2(tailwindcss@3.4.19(yaml@2.8.3)) @@ -295,9 +300,6 @@ importers: '@types/lodash': specifier: 4.17.24 version: 4.17.24 - '@types/mime': - specifier: ^3.0.4 - version: 3.0.4 '@types/node': specifier: 22.19.0 version: 22.19.0 @@ -308,14 +310,14 @@ importers: specifier: ^8.0.0 version: 8.0.0 '@types/react': - specifier: ^18.3.28 - version: 18.3.28 + specifier: 19.2.14 + version: 19.2.14 '@types/react-dom': - specifier: ^18.3.7 - version: 18.3.7(@types/react@18.3.28) + specifier: 19.2.3 + version: 19.2.3(@types/react@19.2.14) '@types/react-transition-group': specifier: 4.4.12 - version: 4.4.12(@types/react@18.3.28) + version: 4.4.12(@types/react@19.2.14) '@types/semver': specifier: 7.7.1 version: 7.7.1 @@ -367,12 +369,15 @@ importers: eslint: specifier: 9.39.3 version: 9.39.3(jiti@2.6.1) + eslint-config-next: + specifier: 16.2.6 + version: 16.2.6(@typescript-eslint/parser@8.59.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.4.5))(eslint@9.39.3(jiti@2.6.1))(typescript@5.4.5) eslint-config-prettier: specifier: 10.1.8 version: 10.1.8(eslint@9.39.3(jiti@2.6.1)) eslint-plugin-formatjs: specifier: 6.4.5 - version: 6.4.5(eslint@9.39.3(jiti@2.6.1))(ts-jest@29.4.6(@babel/core@7.29.0)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.29.0))(jest-util@30.2.0)(jest@30.2.0(@types/node@22.19.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.19.0)(typescript@5.4.5)))(typescript@5.4.5)) + version: 6.4.5(eslint@9.39.3(jiti@2.6.1))(ts-jest@29.4.6(@babel/core@7.29.0)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.29.0))(jest-util@30.2.0)(jest@30.2.0(@types/node@22.19.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.15))(@types/node@22.19.0)(typescript@5.4.5)))(typescript@5.4.5)) eslint-plugin-jsx-a11y: specifier: 6.10.2 version: 6.10.2(eslint@9.39.3(jiti@2.6.1)) @@ -392,8 +397,8 @@ importers: specifier: ^17.5.0 version: 17.5.0 husky: - specifier: 8.0.3 - version: 8.0.3 + specifier: 9.1.7 + version: 9.1.7 jiti: specifier: ^2.6.1 version: 2.6.1 @@ -423,7 +428,7 @@ importers: version: 3.4.19(yaml@2.8.3) ts-node: specifier: 10.9.2 - version: 10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.19.0)(typescript@5.4.5) + version: 10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.15))(@types/node@22.19.0)(typescript@5.4.5) tsc-alias: specifier: 1.8.16 version: 1.8.16 @@ -1269,14 +1274,26 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@floating-ui/core@1.7.3': - resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} + '@floating-ui/core@1.8.0': + resolution: {integrity: sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==} - '@floating-ui/dom@1.7.4': - resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==} + '@floating-ui/dom@1.8.0': + resolution: {integrity: sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==} - '@floating-ui/utils@0.2.10': - resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + '@floating-ui/react-dom@2.1.9': + resolution: {integrity: sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/react@0.26.28': + resolution: {integrity: sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.12': + resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==} '@fontsource-variable/inter@5.2.8': resolution: {integrity: sha512-kOfP2D+ykbcX/P3IFnokOhVRNoTozo5/JxhAIVYLpea/UBmCQ/YWPBfWIDuBImXX/15KH+eKh4xpEUyS2sQQGQ==} @@ -1347,12 +1364,12 @@ packages: '@gar/promisify@1.1.3': resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} - '@headlessui/react@1.7.19': - resolution: {integrity: sha512-Ll+8q3OlMJfJbAKM/+/Y2q6PPYbryqNTXDbryx7SXLIDamkF6iQFbriYHga0dY44PvDhvvBWCx1Xj4U5+G4hOw==} + '@headlessui/react@2.2.10': + resolution: {integrity: sha512-5pVLNK9wlpxTUTy9GpgbX/SdcRh+HBnPktjM2wbiLTH4p+2EPHBO1aoSryUCuKUIItdDWO9ITlhUL8UnUN/oIA==} engines: {node: '>=10'} peerDependencies: - react: ^16 || ^17 || ^18 - react-dom: ^16 || ^17 || ^18 + react: ^18 || ^19 || ^19.0.0-rc + react-dom: ^18 || ^19 || ^19.0.0-rc '@heroicons/react@2.2.0': resolution: {integrity: sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==} @@ -1823,62 +1840,56 @@ packages: '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} - '@next/env@14.2.35': - resolution: {integrity: sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ==} + '@next/env@16.2.6': + resolution: {integrity: sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==} - '@next/eslint-plugin-next@16.2.4': - resolution: {integrity: sha512-tOX826JJ96gYK/go18sPUgMq9FK1tqxBFfUCEufJb5XIkWFFmpgU7mahJANKGkHs7F41ir3tReJ3Lv5La0RvhA==} + '@next/eslint-plugin-next@16.2.6': + resolution: {integrity: sha512-Z8l6o4JWKUl755x4R+wogD86KPeU+Ckw4K+SYG4kHeOJtRenDeK+OSbGcqZpDtbwn9DsJVdir2UxmwXuinUbUw==} - '@next/swc-darwin-arm64@14.2.33': - resolution: {integrity: sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA==} + '@next/swc-darwin-arm64@16.2.6': + resolution: {integrity: sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@14.2.33': - resolution: {integrity: sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA==} + '@next/swc-darwin-x64@16.2.6': + resolution: {integrity: sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@14.2.33': - resolution: {integrity: sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw==} + '@next/swc-linux-arm64-gnu@16.2.6': + resolution: {integrity: sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@14.2.33': - resolution: {integrity: sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==} + '@next/swc-linux-arm64-musl@16.2.6': + resolution: {integrity: sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@14.2.33': - resolution: {integrity: sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==} + '@next/swc-linux-x64-gnu@16.2.6': + resolution: {integrity: sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@14.2.33': - resolution: {integrity: sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==} + '@next/swc-linux-x64-musl@16.2.6': + resolution: {integrity: sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@14.2.33': - resolution: {integrity: sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==} + '@next/swc-win32-arm64-msvc@16.2.6': + resolution: {integrity: sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-ia32-msvc@14.2.33': - resolution: {integrity: sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==} - engines: {node: '>= 10'} - cpu: [ia32] - os: [win32] - - '@next/swc-win32-x64-msvc@14.2.33': - resolution: {integrity: sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg==} + '@next/swc-win32-x64-msvc@16.2.6': + resolution: {integrity: sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -1899,6 +1910,10 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@nolyfill/is-core-module@1.0.39': + resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} + engines: {node: '>=12.4.0'} + '@npmcli/agent@4.0.0': resolution: {integrity: sha512-kAQTcEN9E8ERLVg5AsGwLNoFb+oEG6engbqAU2P43gD4JEIkNGMHdVQ096FsOAAYpZPB0RSt0zgInKIAS1l5QA==} engines: {node: ^20.17.0 || >=22.9.0} @@ -2205,6 +2220,9 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + '@rtsao/scc@1.1.0': + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + '@seerr-team/react-tailwindcss-datepicker@1.3.4': resolution: {integrity: sha512-KZrnl6WL1lvUnAG4RZIkReJ+E0vSpOtMEuatobMqiWAa5Y+Z3d0ZcOOJWHoeRNtF19sIzzBkuLyhFNFlNtXg3A==} peerDependencies: @@ -2390,11 +2408,8 @@ packages: '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} - '@swc/helpers@0.5.11': - resolution: {integrity: sha512-YNlnKRWF2sVojTpIyzwou9XoTNbzbzONwRhOoniEioF1AtaitTvVZblaQRrAzChWQ1bLYyYSWzM18y4WwgzJ+A==} - - '@swc/helpers@0.5.5': - resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} + '@swc/helpers@0.5.15': + resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} '@swc/types@0.1.26': resolution: {integrity: sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==} @@ -2501,9 +2516,6 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - '@types/express-serve-static-core@4.19.5': - resolution: {integrity: sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg==} - '@types/express-serve-static-core@5.1.1': resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==} @@ -2543,6 +2555,9 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/lodash@4.17.24': resolution: {integrity: sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==} @@ -2555,9 +2570,6 @@ packages: '@types/mime@1.3.5': resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} - '@types/mime@3.0.4': - resolution: {integrity: sha512-iJt33IQnVRkqeqC7PzBHPTC6fDlRNRW8vjrgqtScAhrmMwe8c4Eo7+fUGTa+XdWrpEgpyKWMYmi2dIwMAYRzPw==} - '@types/minimatch@3.0.5': resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==} @@ -2582,27 +2594,24 @@ packages: '@types/picomatch@4.0.2': resolution: {integrity: sha512-qHHxQ+P9PysNEGbALT8f8YOSHW0KJu6l2xU8DYY0fu/EmGxXdVnuTLvFUvBgPJMSqXq29SYHveejeAha+4AYgA==} - '@types/prop-types@15.7.15': - resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} - '@types/qs@6.9.15': resolution: {integrity: sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==} '@types/range-parser@1.2.7': resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} - '@types/react-dom@18.3.7': - resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: - '@types/react': ^18.0.0 + '@types/react': 19.2.14 '@types/react-transition-group@4.4.12': resolution: {integrity: sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==} peerDependencies: - '@types/react': '*' + '@types/react': 19.2.14 - '@types/react@18.3.28': - resolution: {integrity: sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==} + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} '@types/semver@7.7.1': resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} @@ -2610,9 +2619,6 @@ packages: '@types/send@0.17.4': resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} - '@types/serve-static@1.15.7': - resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} - '@types/serve-static@2.2.0': resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} @@ -2731,6 +2737,7 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + deprecated: Potential CWE-502 - Update to 1.3.1 or higher '@unicode/unicode-17.0.0@1.6.16': resolution: {integrity: sha512-advq5p36zZ+PDRUpDkWcHHR++R19kx0LYB5iG3bj0KB8mYVKg0ywS996e2bXeXxDb8XdOF7KTivcx7VkYie1pg==} @@ -2941,8 +2948,8 @@ packages: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} - ansis@4.2.0: - resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} + ansis@4.3.1: + resolution: {integrity: sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA==} engines: {node: '>=14'} any-promise@1.3.0: @@ -3001,8 +3008,8 @@ packages: array-ify@1.0.0: resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} - array-includes@3.1.8: - resolution: {integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==} + array-includes@3.1.9: + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} engines: {node: '>= 0.4'} array-union@2.1.0: @@ -3013,8 +3020,12 @@ packages: resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} engines: {node: '>= 0.4'} - array.prototype.flat@1.3.2: - resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==} + array.prototype.findlastindex@1.2.6: + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} engines: {node: '>= 0.4'} array.prototype.flatmap@1.3.3: @@ -3426,10 +3437,6 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} - clone@2.1.2: - resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} - engines: {node: '>=0.8'} - clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -3661,6 +3668,7 @@ packages: cron-parser@4.9.0: resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} engines: {node: '>=12.0.0'} + deprecated: v4 is no longer maintained, upgrade to v5 cronstrue@3.14.0: resolution: {integrity: sha512-XnW4vuK/jPJjmTyDWiej1Zq36Od7ITwxaV2O1pzHZuyMVvdy7NAvyvIBzybt+idqSpfqYuoDG7uf/ocGtJVWxA==} @@ -3735,8 +3743,8 @@ packages: resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} engines: {node: '>= 0.4'} - dayjs@1.11.20: - resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==} + dayjs@1.11.23: + resolution: {integrity: sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==} debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} @@ -4031,8 +4039,9 @@ packages: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} - es-shim-unscopables@1.0.2: - resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==} + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} es-to-primitive@1.3.0: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} @@ -4065,17 +4074,73 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + eslint-config-next@16.2.6: + resolution: {integrity: sha512-z2ELYSkyrrJ6cuunTU8vhsT/RpouPkjaSah06nVW6Rg2Hpg0Vs8s497/e5s8G8qtdp4ccsiovz5P1rv+5VSW2Q==} + peerDependencies: + eslint: '>=9.0.0' + typescript: '>=3.3.1' + peerDependenciesMeta: + typescript: + optional: true + eslint-config-prettier@10.1.8: resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} hasBin: true peerDependencies: eslint: '>=7.0.0' + eslint-import-resolver-node@0.3.10: + resolution: {integrity: sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==} + + eslint-import-resolver-typescript@3.10.1: + resolution: {integrity: sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '*' + eslint-plugin-import: '*' + eslint-plugin-import-x: '*' + peerDependenciesMeta: + eslint-plugin-import: + optional: true + eslint-plugin-import-x: + optional: true + + eslint-module-utils@2.12.1: + resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + eslint-plugin-formatjs@6.4.5: resolution: {integrity: sha512-a42E0lErGJMy/cz57JdM6zdLuqBHF3i6Pv2j8Pw9XIBBkSIvJ1s98axTq2CznJ4ukmR0QZ1P+3upEx3kxSkbJQ==} peerDependencies: eslint: 9 || 10 + eslint-plugin-import@2.32.0: + resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint-plugin-jsx-a11y@6.10.2: resolution: {integrity: sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==} engines: {node: '>=4.0'} @@ -4130,6 +4195,7 @@ packages: eslint@9.39.3: resolution: {integrity: sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true peerDependencies: jiti: '*' @@ -4505,6 +4571,7 @@ packages: git-raw-commits@5.0.1: resolution: {integrity: sha512-Y+csSm2GD/PCSh6Isd/WiMjNAydu0VBiG9J7EdQsNA5P9uXvLayqjmTsNlK5Gs9IhblFZqOU0yid5Il5JPoLiQ==} engines: {node: '>=18'} + deprecated: Deprecated and no longer maintained. Use @conventional-changelog/git-client instead. hasBin: true github-from-package@0.0.0: @@ -4551,6 +4618,10 @@ packages: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} + globals@16.4.0: + resolution: {integrity: sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==} + engines: {node: '>=18'} + globals@17.5.0: resolution: {integrity: sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==} engines: {node: '>=18'} @@ -4711,9 +4782,9 @@ packages: humanize-ms@1.2.1: resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} - husky@8.0.3: - resolution: {integrity: sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==} - engines: {node: '>=14'} + husky@9.1.7: + resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} + engines: {node: '>=18'} hasBin: true i18n-locales@0.0.5: @@ -4811,6 +4882,10 @@ packages: resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} engines: {node: '>= 12'} + ip-address@10.2.0: + resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} + engines: {node: '>= 12'} + ip-address@9.0.5: resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==} engines: {node: '>= 12'} @@ -4848,6 +4923,9 @@ packages: resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} engines: {node: '>= 0.4'} + is-bun-module@2.0.0: + resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==} + is-callable@1.2.7: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} @@ -5248,6 +5326,10 @@ packages: json-stringify-safe@5.0.1: resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -5798,13 +5880,13 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - nanoid@5.1.9: - resolution: {integrity: sha512-ZUvP7KeBLe3OZ1ypw6dI/TzYJuvHP77IM4Ry73waSQTLn8/g8rpdjfyVAh7t1/+FjBtG4lCP42MEbDxOsRpBMw==} + nanoid@5.1.11: + resolution: {integrity: sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg==} engines: {node: ^18 || >=20} hasBin: true @@ -5830,21 +5912,24 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - next@14.2.35: - resolution: {integrity: sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig==} - engines: {node: '>=18.17.0'} + next@16.2.6: + resolution: {integrity: sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==} + engines: {node: '>=20.9.0'} hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 - '@playwright/test': ^1.41.2 - react: ^18.2.0 - react-dom: ^18.2.0 + '@playwright/test': ^1.51.1 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 sass: ^1.3.0 peerDependenciesMeta: '@opentelemetry/api': optional: true '@playwright/test': optional: true + babel-plugin-react-compiler: + optional: true sass: optional: true @@ -5865,9 +5950,9 @@ packages: resolution: {integrity: sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==} engines: {node: ^18 || ^20 || >= 21} - node-cache@5.1.2: - resolution: {integrity: sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==} - engines: {node: '>= 8.0.0'} + node-exports-info@1.6.0: + resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==} + engines: {node: '>= 0.4'} node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} @@ -5984,6 +6069,10 @@ packages: resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} engines: {node: '>= 0.4'} + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + object.values@1.2.1: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} @@ -6167,30 +6256,30 @@ packages: performance-now@2.1.0: resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} - pg-cloudflare@1.3.0: - resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==} + pg-cloudflare@1.4.0: + resolution: {integrity: sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==} - pg-connection-string@2.12.0: - resolution: {integrity: sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==} + pg-connection-string@2.14.0: + resolution: {integrity: sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==} pg-int8@1.0.1: resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} engines: {node: '>=4.0.0'} - pg-pool@3.13.0: - resolution: {integrity: sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==} + pg-pool@3.14.0: + resolution: {integrity: sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==} peerDependencies: pg: '>=8.0' - pg-protocol@1.13.0: - resolution: {integrity: sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==} + pg-protocol@1.16.0: + resolution: {integrity: sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==} pg-types@2.2.0: resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} engines: {node: '>=4'} - pg@8.20.0: - resolution: {integrity: sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==} + pg@8.23.0: + resolution: {integrity: sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==} engines: {node: '>= 16.0.0'} peerDependencies: pg-native: '>=3.0.1' @@ -6554,10 +6643,10 @@ packages: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom@18.3.1: - resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + react-dom@19.2.6: + resolution: {integrity: sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==} peerDependencies: - react: ^18.3.1 + react: ^19.2.6 react-fast-compare@2.0.4: resolution: {integrity: sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==} @@ -6599,7 +6688,7 @@ packages: react-markdown@10.1.0: resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} peerDependencies: - '@types/react': '>=18' + '@types/react': 19.2.14 react: '>=18' react-popper-tooltip@4.4.2: @@ -6643,8 +6732,8 @@ packages: react: ^16.8.0 || ^17 || ^18 react-dom: ^16.8.0 || ^17 || ^18 - react@18.3.1: - resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + react@19.2.6: + resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==} engines: {node: '>=0.10.0'} read-cache@1.0.0: @@ -6740,8 +6829,9 @@ packages: engines: {node: '>= 0.4'} hasBin: true - resolve@2.0.0-next.5: - resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} + resolve@2.0.0-next.6: + resolution: {integrity: sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==} + engines: {node: '>= 0.4'} hasBin: true restore-cursor@3.1.0: @@ -6821,8 +6911,8 @@ packages: resolution: {integrity: sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==} engines: {node: '>=11.0.0'} - scheduler@0.23.2: - resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} selderee@0.11.0: resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==} @@ -7017,6 +7107,9 @@ packages: resolution: {integrity: sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==} engines: {node: '>= 8'} + stable-hash@0.0.5: + resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} + stack-trace@0.0.10: resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} @@ -7133,13 +7226,13 @@ packages: style-to-object@1.0.14: resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} - styled-jsx@5.1.1: - resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} engines: {node: '>= 12.0.0'} peerDependencies: '@babel/core': '*' babel-plugin-macros: '*' - react: '>= 16.8.0 || 17.x.x || ^18.0.0-0' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' peerDependenciesMeta: '@babel/core': optional: true @@ -7210,6 +7303,9 @@ packages: os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android] hasBin: true + tabbable@6.5.0: + resolution: {integrity: sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==} + tailwind-merge@2.6.1: resolution: {integrity: sha512-Oo6tHdpZsGpkKG88HJ8RR1rg/RdnEkQEfMoEk2x1XRI3F1AxeU+ijRXpiVUF4UbLfcxxRGw6TbUINKYdWVsQTQ==} @@ -7402,6 +7498,9 @@ packages: engines: {node: '>=16.20.2'} hasBin: true + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + tsconfig-paths@4.2.0: resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} engines: {node: '>=6'} @@ -7477,8 +7576,8 @@ packages: typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} - typeorm@0.3.28: - resolution: {integrity: sha512-6GH7wXhtfq2D33ZuRXYwIsl/qM5685WZcODZb7noOOcRMteM9KF2x2ap3H0EBjnSV0VO4gNAfJT5Ukp0PkOlvg==} + typeorm@0.3.31: + resolution: {integrity: sha512-6u9EFtdLBgHjnPm78NStVeM+I/1MolTzKykDDcydzKUkh6E++YS6XViU/fePJbvDvEGU4Xq34KOM/CLeer9I2A==} engines: {node: '>=16.13.0'} hasBin: true peerDependencies: @@ -7489,13 +7588,13 @@ packages: mongodb: ^5.8.0 || ^6.0.0 mssql: ^9.1.1 || ^10.0.0 || ^11.0.0 || ^12.0.0 mysql2: ^2.2.5 || ^3.0.1 - oracledb: ^6.3.0 + oracledb: ^6.3.0 || ^7.0.0 pg: ^8.5.1 pg-native: ^3.0.0 pg-query-stream: ^4.0.0 redis: ^3.1.1 || ^4.0.0 || ^5.0.14 sql.js: ^1.4.0 - sqlite3: ^5.0.3 + sqlite3: ^5.0.3 || ^6.0.0 ts-node: ^10.7.0 typeorm-aurora-data-api-driver: ^2.0.0 || ^3.0.0 peerDependenciesMeta: @@ -7676,16 +7775,18 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - uuid@11.1.0: - resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + uuid@11.1.1: + resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==} hasBin: true uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true uuid@9.0.1: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true v8-compile-cache-lib@3.0.1: @@ -7889,8 +7990,8 @@ packages: resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} engines: {node: '>=10'} - yargs@17.7.2: - resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + yargs@17.7.3: + resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==} engines: {node: '>=12'} yauzl@2.10.0: @@ -8762,7 +8863,7 @@ snapshots: '@commitlint/read': 20.5.0(conventional-commits-parser@6.4.0) '@commitlint/types': 20.5.0 tinyexec: 1.1.1 - yargs: 17.7.2 + yargs: 17.7.3 transitivePeerDependencies: - '@types/node' - conventional-commits-filter @@ -8968,19 +9069,19 @@ snapshots: '@emotion/memoize@0.9.0': {} - '@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1)': + '@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6)': dependencies: '@babel/runtime': 7.29.2 '@emotion/babel-plugin': 11.13.5 '@emotion/cache': 11.14.0 '@emotion/serialize': 1.3.3 - '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@18.3.1) + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.6) '@emotion/utils': 1.4.2 '@emotion/weak-memoize': 0.4.0 hoist-non-react-statics: 3.3.2 - react: 18.3.1 + react: 19.2.6 optionalDependencies: - '@types/react': 18.3.28 + '@types/react': 19.2.14 transitivePeerDependencies: - supports-color @@ -8996,9 +9097,9 @@ snapshots: '@emotion/unitless@0.10.0': {} - '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@18.3.1)': + '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@19.2.6)': dependencies: - react: 18.3.1 + react: 19.2.6 '@emotion/utils@1.4.2': {} @@ -9050,16 +9151,30 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 - '@floating-ui/core@1.7.3': + '@floating-ui/core@1.8.0': + dependencies: + '@floating-ui/utils': 0.2.12 + + '@floating-ui/dom@1.8.0': dependencies: - '@floating-ui/utils': 0.2.10 + '@floating-ui/core': 1.8.0 + '@floating-ui/utils': 0.2.12 - '@floating-ui/dom@1.7.4': + '@floating-ui/react-dom@2.1.9(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@floating-ui/core': 1.7.3 - '@floating-ui/utils': 0.2.10 + '@floating-ui/dom': 1.8.0 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) - '@floating-ui/utils@0.2.10': {} + '@floating-ui/react@0.26.28(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@floating-ui/react-dom': 2.1.9(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@floating-ui/utils': 0.2.12 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + tabbable: 6.5.0 + + '@floating-ui/utils@0.2.12': {} '@fontsource-variable/inter@5.2.8': {} @@ -9130,30 +9245,33 @@ snapshots: '@formatjs/icu-messageformat-parser': 3.5.5 intl-messageformat: 11.2.2 - '@formatjs/ts-transformer@4.4.4(ts-jest@29.4.6(@babel/core@7.29.0)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.29.0))(jest-util@30.2.0)(jest@30.2.0(@types/node@22.19.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.19.0)(typescript@5.4.5)))(typescript@5.4.5))': + '@formatjs/ts-transformer@4.4.4(ts-jest@29.4.6(@babel/core@7.29.0)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.29.0))(jest-util@30.2.0)(jest@30.2.0(@types/node@22.19.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.15))(@types/node@22.19.0)(typescript@5.4.5)))(typescript@5.4.5))': dependencies: '@formatjs/icu-messageformat-parser': 3.5.4 '@types/node': 22.19.0 json-stable-stringify: 1.3.0 typescript: 5.9.3 optionalDependencies: - ts-jest: 29.4.6(@babel/core@7.29.0)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.29.0))(jest-util@30.2.0)(jest@30.2.0(@types/node@22.19.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.19.0)(typescript@5.4.5)))(typescript@5.4.5) + ts-jest: 29.4.6(@babel/core@7.29.0)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.29.0))(jest-util@30.2.0)(jest@30.2.0(@types/node@22.19.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.15))(@types/node@22.19.0)(typescript@5.4.5)))(typescript@5.4.5) '@gar/promise-retry@1.0.3': {} '@gar/promisify@1.1.3': optional: true - '@headlessui/react@1.7.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@headlessui/react@2.2.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@tanstack/react-virtual': 3.13.24(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - client-only: 0.0.1 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + '@floating-ui/react': 0.26.28(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@react-aria/focus': 3.22.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@react-aria/interactions': 3.28.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@tanstack/react-virtual': 3.13.24(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + use-sync-external-store: 1.6.0(react@19.2.6) - '@heroicons/react@2.2.0(react@18.3.1)': + '@heroicons/react@2.2.0(react@19.2.6)': dependencies: - react: 18.3.1 + react: 19.2.6 '@humanfs/core@0.19.1': {} @@ -9264,20 +9382,20 @@ snapshots: '@internationalized/date@3.12.1': dependencies: - '@swc/helpers': 0.5.11 + '@swc/helpers': 0.5.15 '@internationalized/message@3.1.9': dependencies: - '@swc/helpers': 0.5.11 + '@swc/helpers': 0.5.15 intl-messageformat: 10.7.18 '@internationalized/number@3.6.6': dependencies: - '@swc/helpers': 0.5.11 + '@swc/helpers': 0.5.15 '@internationalized/string@3.2.8': dependencies: - '@swc/helpers': 0.5.11 + '@swc/helpers': 0.5.15 '@isaacs/cliui@8.0.2': dependencies: @@ -9314,7 +9432,7 @@ snapshots: slash: 3.0.0 optional: true - '@jest/core@30.2.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.19.0)(typescript@5.4.5))': + '@jest/core@30.2.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.15))(@types/node@22.19.0)(typescript@5.4.5))': dependencies: '@jest/console': 30.2.0 '@jest/pattern': 30.0.1 @@ -9329,7 +9447,7 @@ snapshots: exit-x: 0.2.2 graceful-fs: 4.2.11 jest-changed-files: 30.2.0 - jest-config: 30.2.0(@types/node@22.19.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.19.0)(typescript@5.4.5)) + jest-config: 30.2.0(@types/node@22.19.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.15))(@types/node@22.19.0)(typescript@5.4.5)) jest-haste-map: 30.2.0 jest-message-util: 30.2.0 jest-regex-util: 30.0.1 @@ -9527,15 +9645,15 @@ snapshots: '@jsdevtools/ono@7.1.3': {} - '@ladjs/consolidate@1.0.4(@babel/core@7.29.0)(handlebars@4.7.8)(lodash@4.18.1)(mustache@4.2.0)(pug@3.0.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(underscore@1.13.7)': + '@ladjs/consolidate@1.0.4(@babel/core@7.29.0)(handlebars@4.7.8)(lodash@4.18.1)(mustache@4.2.0)(pug@3.0.4)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(underscore@1.13.7)': optionalDependencies: '@babel/core': 7.29.0 handlebars: 4.7.8 lodash: 4.18.1 mustache: 4.2.0 pug: 3.0.4 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) underscore: 1.13.7 '@ladjs/country-language@0.2.1': @@ -9587,37 +9705,34 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true - '@next/env@14.2.35': {} + '@next/env@16.2.6': {} - '@next/eslint-plugin-next@16.2.4': + '@next/eslint-plugin-next@16.2.6': dependencies: fast-glob: 3.3.1 - '@next/swc-darwin-arm64@14.2.33': + '@next/swc-darwin-arm64@16.2.6': optional: true - '@next/swc-darwin-x64@14.2.33': + '@next/swc-darwin-x64@16.2.6': optional: true - '@next/swc-linux-arm64-gnu@14.2.33': + '@next/swc-linux-arm64-gnu@16.2.6': optional: true - '@next/swc-linux-arm64-musl@14.2.33': + '@next/swc-linux-arm64-musl@16.2.6': optional: true - '@next/swc-linux-x64-gnu@14.2.33': + '@next/swc-linux-x64-gnu@16.2.6': optional: true - '@next/swc-linux-x64-musl@14.2.33': + '@next/swc-linux-x64-musl@16.2.6': optional: true - '@next/swc-win32-arm64-msvc@14.2.33': + '@next/swc-win32-arm64-msvc@16.2.6': optional: true - '@next/swc-win32-ia32-msvc@14.2.33': - optional: true - - '@next/swc-win32-x64-msvc@14.2.33': + '@next/swc-win32-x64-msvc@16.2.6': optional: true '@noble/hashes@1.8.0': {} @@ -9634,6 +9749,8 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.17.1 + '@nolyfill/is-core-module@1.0.39': {} + '@npmcli/agent@4.0.0': dependencies: agent-base: 7.1.3 @@ -9673,336 +9790,338 @@ snapshots: '@popperjs/core@2.11.8': {} - '@react-aria/breadcrumbs@3.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/breadcrumbs@3.6.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@swc/helpers': 0.5.11 - react: 18.3.1 - react-aria: 3.48.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-dom: 18.3.1(react@18.3.1) + '@swc/helpers': 0.5.15 + react: 19.2.6 + react-aria: 3.48.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-dom: 19.2.6(react@19.2.6) - '@react-aria/button@3.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/button@3.15.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@swc/helpers': 0.5.11 - react: 18.3.1 - react-aria: 3.48.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-dom: 18.3.1(react@18.3.1) + '@swc/helpers': 0.5.15 + react: 19.2.6 + react-aria: 3.48.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-dom: 19.2.6(react@19.2.6) - '@react-aria/calendar@3.10.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/calendar@3.10.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@swc/helpers': 0.5.11 - react: 18.3.1 - react-aria: 3.48.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-dom: 18.3.1(react@18.3.1) - react-stately: 3.46.0(react@18.3.1) + '@swc/helpers': 0.5.15 + react: 19.2.6 + react-aria: 3.48.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-dom: 19.2.6(react@19.2.6) + react-stately: 3.46.0(react@19.2.6) - '@react-aria/checkbox@3.17.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/checkbox@3.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@swc/helpers': 0.5.11 - react: 18.3.1 - react-aria: 3.48.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-dom: 18.3.1(react@18.3.1) + '@swc/helpers': 0.5.15 + react: 19.2.6 + react-aria: 3.48.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-dom: 19.2.6(react@19.2.6) - '@react-aria/color@3.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/color@3.2.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@swc/helpers': 0.5.11 - react: 18.3.1 - react-aria: 3.48.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-dom: 18.3.1(react@18.3.1) + '@swc/helpers': 0.5.15 + react: 19.2.6 + react-aria: 3.48.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-dom: 19.2.6(react@19.2.6) - '@react-aria/combobox@3.16.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/combobox@3.16.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@swc/helpers': 0.5.11 - react: 18.3.1 - react-aria: 3.48.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-dom: 18.3.1(react@18.3.1) + '@swc/helpers': 0.5.15 + react: 19.2.6 + react-aria: 3.48.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-dom: 19.2.6(react@19.2.6) - '@react-aria/datepicker@3.17.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/datepicker@3.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@swc/helpers': 0.5.11 - react: 18.3.1 - react-aria: 3.48.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-dom: 18.3.1(react@18.3.1) - react-stately: 3.46.0(react@18.3.1) + '@swc/helpers': 0.5.15 + react: 19.2.6 + react-aria: 3.48.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-dom: 19.2.6(react@19.2.6) + react-stately: 3.46.0(react@19.2.6) - '@react-aria/dialog@3.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/dialog@3.6.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@swc/helpers': 0.5.11 - react: 18.3.1 - react-aria: 3.48.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-dom: 18.3.1(react@18.3.1) + '@swc/helpers': 0.5.15 + react: 19.2.6 + react-aria: 3.48.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-dom: 19.2.6(react@19.2.6) - '@react-aria/disclosure@3.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/disclosure@3.2.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@swc/helpers': 0.5.11 - react: 18.3.1 - react-aria: 3.48.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-dom: 18.3.1(react@18.3.1) + '@swc/helpers': 0.5.15 + react: 19.2.6 + react-aria: 3.48.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-dom: 19.2.6(react@19.2.6) - '@react-aria/dnd@3.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/dnd@3.12.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@react-types/shared': 3.34.0(react@18.3.1) - '@swc/helpers': 0.5.11 - react: 18.3.1 - react-aria: 3.48.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-dom: 18.3.1(react@18.3.1) + '@react-types/shared': 3.34.0(react@19.2.6) + '@swc/helpers': 0.5.15 + react: 19.2.6 + react-aria: 3.48.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-dom: 19.2.6(react@19.2.6) - '@react-aria/focus@3.22.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/focus@3.22.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@swc/helpers': 0.5.11 - react: 18.3.1 - react-aria: 3.48.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-dom: 18.3.1(react@18.3.1) + '@swc/helpers': 0.5.15 + react: 19.2.6 + react-aria: 3.48.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-dom: 19.2.6(react@19.2.6) - '@react-aria/gridlist@3.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/gridlist@3.15.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@swc/helpers': 0.5.11 - react: 18.3.1 - react-aria: 3.48.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-dom: 18.3.1(react@18.3.1) + '@swc/helpers': 0.5.15 + react: 19.2.6 + react-aria: 3.48.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-dom: 19.2.6(react@19.2.6) - '@react-aria/i18n@3.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/i18n@3.13.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@internationalized/date': 3.12.1 '@internationalized/message': 3.1.9 '@internationalized/string': 3.2.8 - '@swc/helpers': 0.5.11 - react: 18.3.1 - react-aria: 3.48.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-dom: 18.3.1(react@18.3.1) + '@swc/helpers': 0.5.15 + react: 19.2.6 + react-aria: 3.48.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-dom: 19.2.6(react@19.2.6) - '@react-aria/interactions@3.28.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/interactions@3.28.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@react-types/shared': 3.34.0(react@18.3.1) - '@swc/helpers': 0.5.11 - react: 18.3.1 - react-aria: 3.48.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-dom: 18.3.1(react@18.3.1) + '@react-types/shared': 3.34.0(react@19.2.6) + '@swc/helpers': 0.5.15 + react: 19.2.6 + react-aria: 3.48.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-dom: 19.2.6(react@19.2.6) - '@react-aria/label@3.8.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/label@3.8.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@swc/helpers': 0.5.11 - react: 18.3.1 - react-aria: 3.48.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-dom: 18.3.1(react@18.3.1) + '@swc/helpers': 0.5.15 + react: 19.2.6 + react-aria: 3.48.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-dom: 19.2.6(react@19.2.6) - '@react-aria/landmark@3.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/landmark@3.1.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@swc/helpers': 0.5.11 - react: 18.3.1 - react-aria: 3.48.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-dom: 18.3.1(react@18.3.1) + '@swc/helpers': 0.5.15 + react: 19.2.6 + react-aria: 3.48.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-dom: 19.2.6(react@19.2.6) - '@react-aria/link@3.9.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/link@3.9.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@swc/helpers': 0.5.11 - react: 18.3.1 - react-aria: 3.48.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-dom: 18.3.1(react@18.3.1) + '@swc/helpers': 0.5.15 + react: 19.2.6 + react-aria: 3.48.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-dom: 19.2.6(react@19.2.6) - '@react-aria/listbox@3.16.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/listbox@3.16.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@swc/helpers': 0.5.11 - react: 18.3.1 - react-aria: 3.48.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-dom: 18.3.1(react@18.3.1) + '@swc/helpers': 0.5.15 + react: 19.2.6 + react-aria: 3.48.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-dom: 19.2.6(react@19.2.6) - '@react-aria/menu@3.22.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/menu@3.22.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@swc/helpers': 0.5.11 - react: 18.3.1 - react-aria: 3.48.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-dom: 18.3.1(react@18.3.1) + '@swc/helpers': 0.5.15 + react: 19.2.6 + react-aria: 3.48.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-dom: 19.2.6(react@19.2.6) - '@react-aria/meter@3.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/meter@3.5.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@swc/helpers': 0.5.11 - react: 18.3.1 - react-aria: 3.48.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-dom: 18.3.1(react@18.3.1) + '@swc/helpers': 0.5.15 + react: 19.2.6 + react-aria: 3.48.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-dom: 19.2.6(react@19.2.6) - '@react-aria/numberfield@3.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/numberfield@3.13.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@swc/helpers': 0.5.11 - react: 18.3.1 - react-aria: 3.48.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-dom: 18.3.1(react@18.3.1) + '@swc/helpers': 0.5.15 + react: 19.2.6 + react-aria: 3.48.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-dom: 19.2.6(react@19.2.6) - '@react-aria/overlays@3.32.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/overlays@3.32.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@swc/helpers': 0.5.11 - react: 18.3.1 - react-aria: 3.48.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-dom: 18.3.1(react@18.3.1) + '@swc/helpers': 0.5.15 + react: 19.2.6 + react-aria: 3.48.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-dom: 19.2.6(react@19.2.6) - '@react-aria/progress@3.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/progress@3.5.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@swc/helpers': 0.5.11 - react: 18.3.1 - react-aria: 3.48.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-dom: 18.3.1(react@18.3.1) + '@swc/helpers': 0.5.15 + react: 19.2.6 + react-aria: 3.48.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-dom: 19.2.6(react@19.2.6) - '@react-aria/radio@3.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/radio@3.13.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@react-types/shared': 3.34.0(react@18.3.1) - '@swc/helpers': 0.5.11 - react: 18.3.1 - react-aria: 3.48.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-dom: 18.3.1(react@18.3.1) + '@react-types/shared': 3.34.0(react@19.2.6) + '@swc/helpers': 0.5.15 + react: 19.2.6 + react-aria: 3.48.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-dom: 19.2.6(react@19.2.6) - '@react-aria/searchfield@3.9.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/searchfield@3.9.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@swc/helpers': 0.5.11 - react: 18.3.1 - react-aria: 3.48.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-dom: 18.3.1(react@18.3.1) + '@swc/helpers': 0.5.15 + react: 19.2.6 + react-aria: 3.48.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-dom: 19.2.6(react@19.2.6) - '@react-aria/select@3.18.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/select@3.18.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@swc/helpers': 0.5.11 - react: 18.3.1 - react-aria: 3.48.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-dom: 18.3.1(react@18.3.1) + '@swc/helpers': 0.5.15 + react: 19.2.6 + react-aria: 3.48.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-dom: 19.2.6(react@19.2.6) - '@react-aria/selection@3.28.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/selection@3.28.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@swc/helpers': 0.5.11 - react: 18.3.1 - react-aria: 3.48.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-dom: 18.3.1(react@18.3.1) + '@swc/helpers': 0.5.15 + react: 19.2.6 + react-aria: 3.48.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-dom: 19.2.6(react@19.2.6) - '@react-aria/separator@3.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/separator@3.5.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@swc/helpers': 0.5.11 - react: 18.3.1 - react-aria: 3.48.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-dom: 18.3.1(react@18.3.1) + '@swc/helpers': 0.5.15 + react: 19.2.6 + react-aria: 3.48.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-dom: 19.2.6(react@19.2.6) - '@react-aria/slider@3.9.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/slider@3.9.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@react-types/shared': 3.34.0(react@18.3.1) - '@swc/helpers': 0.5.11 - react: 18.3.1 - react-aria: 3.48.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-dom: 18.3.1(react@18.3.1) + '@react-types/shared': 3.34.0(react@19.2.6) + '@swc/helpers': 0.5.15 + react: 19.2.6 + react-aria: 3.48.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-dom: 19.2.6(react@19.2.6) - '@react-aria/ssr@3.9.10(react@18.3.1)': + '@react-aria/ssr@3.9.10(react@19.2.6)': dependencies: - '@swc/helpers': 0.5.11 - react: 18.3.1 + '@swc/helpers': 0.5.15 + react: 19.2.6 - '@react-aria/switch@3.8.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/switch@3.8.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@swc/helpers': 0.5.11 - react: 18.3.1 - react-aria: 3.48.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-dom: 18.3.1(react@18.3.1) + '@swc/helpers': 0.5.15 + react: 19.2.6 + react-aria: 3.48.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-dom: 19.2.6(react@19.2.6) - '@react-aria/table@3.18.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/table@3.18.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@swc/helpers': 0.5.11 - react: 18.3.1 - react-aria: 3.48.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-dom: 18.3.1(react@18.3.1) + '@swc/helpers': 0.5.15 + react: 19.2.6 + react-aria: 3.48.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-dom: 19.2.6(react@19.2.6) - '@react-aria/tabs@3.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/tabs@3.12.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@react-types/shared': 3.34.0(react@18.3.1) - '@swc/helpers': 0.5.11 - react: 18.3.1 - react-aria: 3.48.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-dom: 18.3.1(react@18.3.1) + '@react-types/shared': 3.34.0(react@19.2.6) + '@swc/helpers': 0.5.15 + react: 19.2.6 + react-aria: 3.48.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-dom: 19.2.6(react@19.2.6) - '@react-aria/tag@3.9.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/tag@3.9.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@swc/helpers': 0.5.11 - react: 18.3.1 - react-aria: 3.48.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-dom: 18.3.1(react@18.3.1) + '@swc/helpers': 0.5.15 + react: 19.2.6 + react-aria: 3.48.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-dom: 19.2.6(react@19.2.6) - '@react-aria/textfield@3.19.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/textfield@3.19.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@swc/helpers': 0.5.11 - react: 18.3.1 - react-aria: 3.48.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-dom: 18.3.1(react@18.3.1) + '@swc/helpers': 0.5.15 + react: 19.2.6 + react-aria: 3.48.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-dom: 19.2.6(react@19.2.6) - '@react-aria/toast@3.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/toast@3.1.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@swc/helpers': 0.5.11 - react: 18.3.1 - react-aria: 3.48.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-dom: 18.3.1(react@18.3.1) + '@swc/helpers': 0.5.15 + react: 19.2.6 + react-aria: 3.48.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-dom: 19.2.6(react@19.2.6) - '@react-aria/tooltip@3.10.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/tooltip@3.10.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@swc/helpers': 0.5.11 - react: 18.3.1 - react-aria: 3.48.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-dom: 18.3.1(react@18.3.1) - react-stately: 3.46.0(react@18.3.1) + '@swc/helpers': 0.5.15 + react: 19.2.6 + react-aria: 3.48.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-dom: 19.2.6(react@19.2.6) + react-stately: 3.46.0(react@19.2.6) - '@react-aria/tree@3.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/tree@3.2.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@swc/helpers': 0.5.11 - react: 18.3.1 - react-aria: 3.48.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-dom: 18.3.1(react@18.3.1) + '@swc/helpers': 0.5.15 + react: 19.2.6 + react-aria: 3.48.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-dom: 19.2.6(react@19.2.6) - '@react-aria/utils@3.34.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/utils@3.34.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@swc/helpers': 0.5.11 - react: 18.3.1 - react-aria: 3.48.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-dom: 18.3.1(react@18.3.1) - react-stately: 3.46.0(react@18.3.1) + '@swc/helpers': 0.5.15 + react: 19.2.6 + react-aria: 3.48.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-dom: 19.2.6(react@19.2.6) + react-stately: 3.46.0(react@19.2.6) - '@react-aria/visually-hidden@3.9.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/visually-hidden@3.9.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@swc/helpers': 0.5.11 - react: 18.3.1 - react-aria: 3.48.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-dom: 18.3.1(react@18.3.1) + '@swc/helpers': 0.5.15 + react: 19.2.6 + react-aria: 3.48.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-dom: 19.2.6(react@19.2.6) - '@react-spring/animated@10.0.3(react@18.3.1)': + '@react-spring/animated@10.0.3(react@19.2.6)': dependencies: - '@react-spring/shared': 10.0.3(react@18.3.1) + '@react-spring/shared': 10.0.3(react@19.2.6) '@react-spring/types': 10.0.3 - react: 18.3.1 + react: 19.2.6 - '@react-spring/core@10.0.3(react@18.3.1)': + '@react-spring/core@10.0.3(react@19.2.6)': dependencies: - '@react-spring/animated': 10.0.3(react@18.3.1) - '@react-spring/shared': 10.0.3(react@18.3.1) + '@react-spring/animated': 10.0.3(react@19.2.6) + '@react-spring/shared': 10.0.3(react@19.2.6) '@react-spring/types': 10.0.3 - react: 18.3.1 + react: 19.2.6 '@react-spring/rafz@10.0.3': {} - '@react-spring/shared@10.0.3(react@18.3.1)': + '@react-spring/shared@10.0.3(react@19.2.6)': dependencies: '@react-spring/rafz': 10.0.3 '@react-spring/types': 10.0.3 - react: 18.3.1 + react: 19.2.6 '@react-spring/types@10.0.3': {} - '@react-spring/web@10.0.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-spring/web@10.0.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@react-spring/animated': 10.0.3(react@18.3.1) - '@react-spring/core': 10.0.3(react@18.3.1) - '@react-spring/shared': 10.0.3(react@18.3.1) + '@react-spring/animated': 10.0.3(react@19.2.6) + '@react-spring/core': 10.0.3(react@19.2.6) + '@react-spring/shared': 10.0.3(react@19.2.6) '@react-spring/types': 10.0.3 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) - '@react-types/shared@3.34.0(react@18.3.1)': + '@react-types/shared@3.34.0(react@19.2.6)': dependencies: - react: 18.3.1 + react: 19.2.6 + + '@rtsao/scc@1.1.0': {} - '@seerr-team/react-tailwindcss-datepicker@1.3.4(dayjs@1.11.20)(react@18.3.1)': + '@seerr-team/react-tailwindcss-datepicker@1.3.4(dayjs@1.11.23)(react@19.2.6)': dependencies: - dayjs: 1.11.20 - react: 18.3.1 + dayjs: 1.11.23 + react: 19.2.6 '@selderee/plugin-htmlparser2@0.11.0': dependencies: @@ -10160,7 +10279,7 @@ snapshots: '@swc/core-win32-x64-msvc@1.6.5': optional: true - '@swc/core@1.6.5(@swc/helpers@0.5.11)': + '@swc/core@1.6.5(@swc/helpers@0.5.15)': dependencies: '@swc/counter': 0.1.3 '@swc/types': 0.1.26 @@ -10175,18 +10294,14 @@ snapshots: '@swc/core-win32-arm64-msvc': 1.6.5 '@swc/core-win32-ia32-msvc': 1.6.5 '@swc/core-win32-x64-msvc': 1.6.5 - '@swc/helpers': 0.5.11 + '@swc/helpers': 0.5.15 optional: true - '@swc/counter@0.1.3': {} - - '@swc/helpers@0.5.11': - dependencies: - tslib: 2.8.1 + '@swc/counter@0.1.3': + optional: true - '@swc/helpers@0.5.5': + '@swc/helpers@0.5.15': dependencies: - '@swc/counter': 0.1.3 tslib: 2.8.1 '@swc/types@0.1.26': @@ -10208,18 +10323,18 @@ snapshots: postcss-selector-parser: 6.0.10 tailwindcss: 3.4.19(yaml@2.8.3) - '@tanem/react-nprogress@6.0.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@tanem/react-nprogress@6.0.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@babel/runtime': 7.29.2 hoist-non-react-statics: 3.3.2 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) - '@tanstack/react-virtual@3.13.24(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@tanstack/react-virtual@3.13.24(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@tanstack/virtual-core': 3.14.0 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) '@tanstack/virtual-core@3.14.0': {} @@ -10287,7 +10402,7 @@ snapshots: '@types/csurf@1.11.5': dependencies: - '@types/express-serve-static-core': 4.19.5 + '@types/express-serve-static-core': 5.1.1 '@types/debug@0.0.31': {} @@ -10316,13 +10431,6 @@ snapshots: '@types/estree@1.0.8': {} - '@types/express-serve-static-core@4.19.5': - dependencies: - '@types/node': 22.19.0 - '@types/qs': 6.9.15 - '@types/range-parser': 1.2.7 - '@types/send': 0.17.4 - '@types/express-serve-static-core@5.1.1': dependencies: '@types/node': 22.19.0 @@ -10346,7 +10454,7 @@ snapshots: '@types/hoist-non-react-statics@3.3.5': dependencies: - '@types/react': 18.3.28 + '@types/react': 19.2.14 hoist-non-react-statics: 3.3.2 '@types/html-to-text@9.0.4': {} @@ -10372,6 +10480,8 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/json5@0.0.29': {} + '@types/lodash@4.17.24': {} '@types/mdast@4.0.4': @@ -10382,8 +10492,6 @@ snapshots: '@types/mime@1.3.5': {} - '@types/mime@3.0.4': {} - '@types/minimatch@3.0.5': {} '@types/ms@0.7.34': {} @@ -10408,23 +10516,20 @@ snapshots: '@types/picomatch@4.0.2': {} - '@types/prop-types@15.7.15': {} - '@types/qs@6.9.15': {} '@types/range-parser@1.2.7': {} - '@types/react-dom@18.3.7(@types/react@18.3.28)': + '@types/react-dom@19.2.3(@types/react@19.2.14)': dependencies: - '@types/react': 18.3.28 + '@types/react': 19.2.14 - '@types/react-transition-group@4.4.12(@types/react@18.3.28)': + '@types/react-transition-group@4.4.12(@types/react@19.2.14)': dependencies: - '@types/react': 18.3.28 + '@types/react': 19.2.14 - '@types/react@18.3.28': + '@types/react@19.2.14': dependencies: - '@types/prop-types': 15.7.15 csstype: 3.2.3 '@types/semver@7.7.1': {} @@ -10434,12 +10539,6 @@ snapshots: '@types/mime': 1.3.5 '@types/node': 22.19.0 - '@types/serve-static@1.15.7': - dependencies: - '@types/http-errors': 2.0.4 - '@types/node': 22.19.0 - '@types/send': 0.17.4 - '@types/serve-static@2.2.0': dependencies: '@types/http-errors': 2.0.4 @@ -10467,7 +10566,7 @@ snapshots: '@types/swagger-ui-express@4.1.8': dependencies: '@types/express': 5.0.6 - '@types/serve-static': 1.15.7 + '@types/serve-static': 2.2.0 '@types/tmp@0.2.6': {} @@ -10756,7 +10855,7 @@ snapshots: ansi-styles@6.2.3: {} - ansis@4.2.0: {} + ansis@4.3.1: {} any-promise@1.3.0: {} @@ -10806,14 +10905,16 @@ snapshots: array-ify@1.0.0: {} - array-includes@3.1.8: + array-includes@3.1.9: dependencies: call-bind: 1.0.8 + call-bound: 1.0.4 define-properties: 1.2.1 es-abstract: 1.24.0 es-object-atoms: 1.1.1 get-intrinsic: 1.3.0 is-string: 1.1.1 + math-intrinsics: 1.1.0 array-union@2.1.0: {} @@ -10824,21 +10925,31 @@ snapshots: es-abstract: 1.24.0 es-errors: 1.3.0 es-object-atoms: 1.1.1 - es-shim-unscopables: 1.0.2 + es-shim-unscopables: 1.1.0 - array.prototype.flat@1.3.2: + array.prototype.findlastindex@1.2.6: dependencies: call-bind: 1.0.8 + call-bound: 1.0.4 define-properties: 1.2.1 es-abstract: 1.24.0 - es-shim-unscopables: 1.0.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-shim-unscopables: 1.1.0 array.prototype.flatmap@1.3.3: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 es-abstract: 1.24.0 - es-shim-unscopables: 1.0.2 + es-shim-unscopables: 1.1.0 array.prototype.tosorted@1.1.4: dependencies: @@ -10846,7 +10957,7 @@ snapshots: define-properties: 1.2.1 es-abstract: 1.24.0 es-errors: 1.3.0 - es-shim-unscopables: 1.0.2 + es-shim-unscopables: 1.1.0 arraybuffer.prototype.slice@1.0.4: dependencies: @@ -11345,8 +11456,6 @@ snapshots: clone@1.0.4: {} - clone@2.1.2: {} - clsx@2.1.1: {} co@4.6.0: @@ -11448,13 +11557,13 @@ snapshots: readable-stream: 3.6.2 typedarray: 0.0.6 - connect-typeorm@2.0.0(typeorm@0.3.28(babel-plugin-macros@3.1.0)(pg@8.20.0)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.19.0)(typescript@5.4.5))): + connect-typeorm@2.0.0(typeorm@0.3.31(babel-plugin-macros@3.1.0)(pg@8.23.0)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.15))(@types/node@22.19.0)(typescript@5.4.5))): dependencies: '@types/debug': 0.0.31 '@types/express-session': 1.18.2 debug: 4.4.3(supports-color@5.5.0) express-session: 1.19.0 - typeorm: 0.3.28(babel-plugin-macros@3.1.0)(pg@8.20.0)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.19.0)(typescript@5.4.5)) + typeorm: 0.3.31(babel-plugin-macros@3.1.0)(pg@8.23.0)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.15))(@types/node@22.19.0)(typescript@5.4.5)) transitivePeerDependencies: - supports-color @@ -11644,7 +11753,7 @@ snapshots: cli-table3: 0.6.1 commander: 6.2.1 common-tags: 1.8.2 - dayjs: 1.11.20 + dayjs: 1.11.23 debug: 4.4.3(supports-color@8.1.1) enquirer: 2.4.1 eventemitter2: 6.4.7 @@ -11710,7 +11819,7 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.2 - dayjs@1.11.20: {} + dayjs@1.11.23: {} debug@2.6.9: dependencies: @@ -11906,9 +12015,9 @@ snapshots: electron-to-chromium@1.5.344: {} - email-templates@13.0.1(@babel/core@7.29.0)(handlebars@4.7.8)(mustache@4.2.0)(pug@3.0.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(underscore@1.13.7): + email-templates@13.0.1(@babel/core@7.29.0)(handlebars@4.7.8)(mustache@4.2.0)(pug@3.0.4)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(underscore@1.13.7): dependencies: - '@ladjs/consolidate': 1.0.4(@babel/core@7.29.0)(handlebars@4.7.8)(lodash@4.18.1)(mustache@4.2.0)(pug@3.0.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(underscore@1.13.7) + '@ladjs/consolidate': 1.0.4(@babel/core@7.29.0)(handlebars@4.7.8)(lodash@4.18.1)(mustache@4.2.0)(pug@3.0.4)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(underscore@1.13.7) '@ladjs/i18n': 10.0.0 get-paths: 0.0.7 html-to-text: 9.0.5 @@ -12110,7 +12219,7 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.2 - es-shim-unscopables@1.0.2: + es-shim-unscopables@1.1.0: dependencies: hasown: 2.0.2 @@ -12136,14 +12245,68 @@ snapshots: escape-string-regexp@4.0.0: {} + eslint-config-next@16.2.6(@typescript-eslint/parser@8.59.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.4.5))(eslint@9.39.3(jiti@2.6.1))(typescript@5.4.5): + dependencies: + '@next/eslint-plugin-next': 16.2.6 + eslint: 9.39.3(jiti@2.6.1) + eslint-import-resolver-node: 0.3.10 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.3(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.4.5))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3(jiti@2.6.1)) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.3(jiti@2.6.1)) + eslint-plugin-react: 7.37.5(eslint@9.39.3(jiti@2.6.1)) + eslint-plugin-react-hooks: 7.1.1(eslint@9.39.3(jiti@2.6.1)) + globals: 16.4.0 + typescript-eslint: 8.59.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.4.5) + optionalDependencies: + typescript: 5.4.5 + transitivePeerDependencies: + - '@typescript-eslint/parser' + - eslint-import-resolver-webpack + - eslint-plugin-import-x + - supports-color + eslint-config-prettier@10.1.8(eslint@9.39.3(jiti@2.6.1)): dependencies: eslint: 9.39.3(jiti@2.6.1) - eslint-plugin-formatjs@6.4.5(eslint@9.39.3(jiti@2.6.1))(ts-jest@29.4.6(@babel/core@7.29.0)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.29.0))(jest-util@30.2.0)(jest@30.2.0(@types/node@22.19.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.19.0)(typescript@5.4.5)))(typescript@5.4.5)): + eslint-import-resolver-node@0.3.10: + dependencies: + debug: 3.2.7(supports-color@8.1.1) + is-core-module: 2.16.1 + resolve: 2.0.0-next.6 + transitivePeerDependencies: + - supports-color + + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.3(jiti@2.6.1)): + dependencies: + '@nolyfill/is-core-module': 1.0.39 + debug: 4.4.3(supports-color@5.5.0) + eslint: 9.39.3(jiti@2.6.1) + get-tsconfig: 4.13.0 + is-bun-module: 2.0.0 + stable-hash: 0.0.5 + tinyglobby: 0.2.15 + unrs-resolver: 1.11.1 + optionalDependencies: + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.4.5))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3(jiti@2.6.1)) + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.59.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.4.5))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3(jiti@2.6.1)): + dependencies: + debug: 3.2.7(supports-color@8.1.1) + optionalDependencies: + '@typescript-eslint/parser': 8.59.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.4.5) + eslint: 9.39.3(jiti@2.6.1) + eslint-import-resolver-node: 0.3.10 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.3(jiti@2.6.1)) + transitivePeerDependencies: + - supports-color + + eslint-plugin-formatjs@6.4.5(eslint@9.39.3(jiti@2.6.1))(ts-jest@29.4.6(@babel/core@7.29.0)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.29.0))(jest-util@30.2.0)(jest@30.2.0(@types/node@22.19.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.15))(@types/node@22.19.0)(typescript@5.4.5)))(typescript@5.4.5)): dependencies: '@formatjs/icu-messageformat-parser': 3.5.4 - '@formatjs/ts-transformer': 4.4.4(ts-jest@29.4.6(@babel/core@7.29.0)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.29.0))(jest-util@30.2.0)(jest@30.2.0(@types/node@22.19.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.19.0)(typescript@5.4.5)))(typescript@5.4.5)) + '@formatjs/ts-transformer': 4.4.4(ts-jest@29.4.6(@babel/core@7.29.0)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.29.0))(jest-util@30.2.0)(jest@30.2.0(@types/node@22.19.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.15))(@types/node@22.19.0)(typescript@5.4.5)))(typescript@5.4.5)) '@types/picomatch': 4.0.2 '@unicode/unicode-17.0.0': 1.6.16 eslint: 9.39.3(jiti@2.6.1) @@ -12152,10 +12315,39 @@ snapshots: transitivePeerDependencies: - ts-jest + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.4.5))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3(jiti@2.6.1)): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.9 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7(supports-color@8.1.1) + doctrine: 2.1.0 + eslint: 9.39.3(jiti@2.6.1) + eslint-import-resolver-node: 0.3.10 + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.59.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.4.5))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3(jiti@2.6.1)) + hasown: 2.0.2 + is-core-module: 2.16.1 + is-glob: 4.0.3 + minimatch: 3.1.5 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.9 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.59.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.4.5) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.3(jiti@2.6.1)): dependencies: aria-query: 5.3.2 - array-includes: 3.1.8 + array-includes: 3.1.9 array.prototype.flatmap: 1.3.3 ast-types-flow: 0.0.8 axe-core: 4.11.0 @@ -12195,7 +12387,7 @@ snapshots: eslint-plugin-react@7.37.5(eslint@9.39.3(jiti@2.6.1)): dependencies: - array-includes: 3.1.8 + array-includes: 3.1.9 array.prototype.findlast: 1.2.5 array.prototype.flatmap: 1.3.3 array.prototype.tosorted: 1.1.4 @@ -12210,7 +12402,7 @@ snapshots: object.fromentries: 2.0.8 object.values: 1.2.1 prop-types: 15.8.1 - resolve: 2.0.0-next.5 + resolve: 2.0.0-next.6 semver: 6.3.1 string.prototype.matchall: 4.0.12 string.prototype.repeat: 1.0.0 @@ -12608,14 +12800,14 @@ snapshots: dezalgo: 1.0.4 once: 1.4.0 - formik@2.4.9(react@18.3.1): + formik@2.4.9(react@19.2.6): dependencies: '@types/hoist-non-react-statics': 3.3.5 deepmerge: 2.2.1 hoist-non-react-statics: 3.3.2 lodash: 4.18.1 lodash-es: 4.17.23 - react: 18.3.1 + react: 19.2.6 react-fast-compare: 2.0.4 tiny-warning: 1.0.3 tslib: 2.8.1 @@ -12797,6 +12989,8 @@ snapshots: globals@14.0.0: {} + globals@16.4.0: {} + globals@17.5.0: {} globalthis@1.0.4: @@ -13011,7 +13205,7 @@ snapshots: ms: 2.1.3 optional: true - husky@8.0.3: {} + husky@9.1.7: {} i18n-locales@0.0.5: dependencies: @@ -13121,6 +13315,8 @@ snapshots: ip-address@10.1.0: {} + ip-address@10.2.0: {} + ip-address@9.0.5: dependencies: jsbn: 1.1.0 @@ -13160,6 +13356,10 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 + is-bun-module@2.0.0: + dependencies: + semver: 7.7.4 + is-callable@1.2.7: {} is-core-module@2.16.1: @@ -13399,18 +13599,18 @@ snapshots: - supports-color optional: true - jest-cli@30.2.0(@types/node@22.19.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.19.0)(typescript@5.4.5)): + jest-cli@30.2.0(@types/node@22.19.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.15))(@types/node@22.19.0)(typescript@5.4.5)): dependencies: - '@jest/core': 30.2.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.19.0)(typescript@5.4.5)) + '@jest/core': 30.2.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.15))(@types/node@22.19.0)(typescript@5.4.5)) '@jest/test-result': 30.2.0 '@jest/types': 30.2.0 chalk: 4.1.2 exit-x: 0.2.2 import-local: 3.2.0 - jest-config: 30.2.0(@types/node@22.19.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.19.0)(typescript@5.4.5)) + jest-config: 30.2.0(@types/node@22.19.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.15))(@types/node@22.19.0)(typescript@5.4.5)) jest-util: 30.2.0 jest-validate: 30.2.0 - yargs: 17.7.2 + yargs: 17.7.3 transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -13419,7 +13619,7 @@ snapshots: - ts-node optional: true - jest-config@30.2.0(@types/node@22.19.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.19.0)(typescript@5.4.5)): + jest-config@30.2.0(@types/node@22.19.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.15))(@types/node@22.19.0)(typescript@5.4.5)): dependencies: '@babel/core': 7.29.0 '@jest/get-type': 30.1.0 @@ -13447,7 +13647,7 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 22.19.0 - ts-node: 10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.19.0)(typescript@5.4.5) + ts-node: 10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.15))(@types/node@22.19.0)(typescript@5.4.5) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -13688,12 +13888,12 @@ snapshots: supports-color: 8.1.1 optional: true - jest@30.2.0(@types/node@22.19.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.19.0)(typescript@5.4.5)): + jest@30.2.0(@types/node@22.19.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.15))(@types/node@22.19.0)(typescript@5.4.5)): dependencies: - '@jest/core': 30.2.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.19.0)(typescript@5.4.5)) + '@jest/core': 30.2.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.15))(@types/node@22.19.0)(typescript@5.4.5)) '@jest/types': 30.2.0 import-local: 3.2.0 - jest-cli: 30.2.0(@types/node@22.19.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.19.0)(typescript@5.4.5)) + jest-cli: 30.2.0(@types/node@22.19.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.15))(@types/node@22.19.0)(typescript@5.4.5)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -13748,6 +13948,10 @@ snapshots: json-stringify-safe@5.0.1: {} + json5@1.0.2: + dependencies: + minimist: 1.2.8 + json5@2.2.3: {} jsonfile@6.1.0: @@ -13772,8 +13976,8 @@ snapshots: jsx-ast-utils@3.3.5: dependencies: - array-includes: 3.1.8 - array.prototype.flat: 1.3.2 + array-includes: 3.1.9 + array.prototype.flat: 1.3.3 object.assign: 4.1.7 object.values: 1.2.1 @@ -14470,14 +14674,13 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 - nanoid@3.3.11: {} + nanoid@3.3.12: {} - nanoid@5.1.9: {} + nanoid@5.1.11: {} napi-build-utils@2.0.0: {} - napi-postinstall@0.3.4: - optional: true + napi-postinstall@0.3.4: {} natural-compare@1.4.0: {} @@ -14489,27 +14692,26 @@ snapshots: neo-async@2.6.2: optional: true - next@14.2.35(@babel/core@7.29.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next@16.2.6(@babel/core@7.29.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6): dependencies: - '@next/env': 14.2.35 - '@swc/helpers': 0.5.5 - busboy: 1.6.0 + '@next/env': 16.2.6 + '@swc/helpers': 0.5.15 + baseline-browser-mapping: 2.10.23 caniuse-lite: 1.0.30001791 - graceful-fs: 4.2.11 postcss: 8.4.31 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - styled-jsx: 5.1.1(@babel/core@7.29.0)(babel-plugin-macros@3.1.0)(react@18.3.1) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + styled-jsx: 5.1.6(@babel/core@7.29.0)(babel-plugin-macros@3.1.0)(react@19.2.6) optionalDependencies: - '@next/swc-darwin-arm64': 14.2.33 - '@next/swc-darwin-x64': 14.2.33 - '@next/swc-linux-arm64-gnu': 14.2.33 - '@next/swc-linux-arm64-musl': 14.2.33 - '@next/swc-linux-x64-gnu': 14.2.33 - '@next/swc-linux-x64-musl': 14.2.33 - '@next/swc-win32-arm64-msvc': 14.2.33 - '@next/swc-win32-ia32-msvc': 14.2.33 - '@next/swc-win32-x64-msvc': 14.2.33 + '@next/swc-darwin-arm64': 16.2.6 + '@next/swc-darwin-x64': 16.2.6 + '@next/swc-linux-arm64-gnu': 16.2.6 + '@next/swc-linux-arm64-musl': 16.2.6 + '@next/swc-linux-x64-gnu': 16.2.6 + '@next/swc-linux-x64-musl': 16.2.6 + '@next/swc-win32-arm64-msvc': 16.2.6 + '@next/swc-win32-x64-msvc': 16.2.6 + sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros @@ -14530,9 +14732,12 @@ snapshots: node-addon-api@8.5.0: {} - node-cache@5.1.2: + node-exports-info@1.6.0: dependencies: - clone: 2.1.2 + array.prototype.flatmap: 1.3.3 + es-errors: 1.3.0 + object.entries: 1.1.9 + semver: 6.3.1 node-fetch@2.7.0(encoding@0.1.13): dependencies: @@ -14676,6 +14881,12 @@ snapshots: es-abstract: 1.24.0 es-object-atoms: 1.1.1 + object.groupby@1.0.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + object.values@1.2.1: dependencies: call-bind: 1.0.8 @@ -14875,18 +15086,18 @@ snapshots: performance-now@2.1.0: {} - pg-cloudflare@1.3.0: + pg-cloudflare@1.4.0: optional: true - pg-connection-string@2.12.0: {} + pg-connection-string@2.14.0: {} pg-int8@1.0.1: {} - pg-pool@3.13.0(pg@8.20.0): + pg-pool@3.14.0(pg@8.23.0): dependencies: - pg: 8.20.0 + pg: 8.23.0 - pg-protocol@1.13.0: {} + pg-protocol@1.16.0: {} pg-types@2.2.0: dependencies: @@ -14896,15 +15107,15 @@ snapshots: postgres-date: 1.0.7 postgres-interval: 1.2.0 - pg@8.20.0: + pg@8.23.0: dependencies: - pg-connection-string: 2.12.0 - pg-pool: 3.13.0(pg@8.20.0) - pg-protocol: 1.13.0 + pg-connection-string: 2.14.0 + pg-pool: 3.14.0(pg@8.23.0) + pg-protocol: 1.16.0 pg-types: 2.2.0 pgpass: 1.0.5 optionalDependencies: - pg-cloudflare: 1.3.0 + pg-cloudflare: 1.4.0 pgpass@1.0.5: dependencies: @@ -14972,13 +15183,13 @@ snapshots: postcss@8.4.31: dependencies: - nanoid: 3.3.11 + nanoid: 3.3.12 picocolors: 1.1.1 source-map-js: 1.2.1 postcss@8.5.12: dependencies: - nanoid: 3.3.11 + nanoid: 3.3.12 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -15196,115 +15407,114 @@ snapshots: minimist: 1.2.8 strip-json-comments: 2.0.1 - react-ace@14.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-ace@14.0.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6): dependencies: ace-builds: 1.43.6 diff-match-patch: 1.0.5 lodash.get: 4.4.2 lodash.isequal: 4.5.0 prop-types: 15.8.1 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) - react-animate-height@3.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-animate-height@3.2.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6): dependencies: - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) - react-aria@3.47.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-aria@3.47.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6): dependencies: '@internationalized/string': 3.2.8 - '@react-aria/breadcrumbs': 3.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/button': 3.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/calendar': 3.10.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/checkbox': 3.17.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/color': 3.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/combobox': 3.16.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/datepicker': 3.17.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/dialog': 3.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/disclosure': 3.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/dnd': 3.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/focus': 3.22.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/gridlist': 3.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/i18n': 3.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/interactions': 3.28.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/label': 3.8.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/landmark': 3.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/link': 3.9.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/listbox': 3.16.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/menu': 3.22.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/meter': 3.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/numberfield': 3.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/overlays': 3.32.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/progress': 3.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/radio': 3.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/searchfield': 3.9.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/select': 3.18.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/selection': 3.28.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/separator': 3.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/slider': 3.9.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/ssr': 3.9.10(react@18.3.1) - '@react-aria/switch': 3.8.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/table': 3.18.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/tabs': 3.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/tag': 3.9.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/textfield': 3.19.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/toast': 3.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/tooltip': 3.10.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/tree': 3.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/utils': 3.34.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/visually-hidden': 3.9.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-types/shared': 3.34.0(react@18.3.1) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - - react-aria@3.48.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + '@react-aria/breadcrumbs': 3.6.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@react-aria/button': 3.15.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@react-aria/calendar': 3.10.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@react-aria/checkbox': 3.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@react-aria/color': 3.2.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@react-aria/combobox': 3.16.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@react-aria/datepicker': 3.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@react-aria/dialog': 3.6.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@react-aria/disclosure': 3.2.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@react-aria/dnd': 3.12.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@react-aria/focus': 3.22.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@react-aria/gridlist': 3.15.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@react-aria/i18n': 3.13.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@react-aria/interactions': 3.28.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@react-aria/label': 3.8.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@react-aria/landmark': 3.1.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@react-aria/link': 3.9.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@react-aria/listbox': 3.16.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@react-aria/menu': 3.22.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@react-aria/meter': 3.5.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@react-aria/numberfield': 3.13.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@react-aria/overlays': 3.32.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@react-aria/progress': 3.5.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@react-aria/radio': 3.13.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@react-aria/searchfield': 3.9.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@react-aria/select': 3.18.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@react-aria/selection': 3.28.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@react-aria/separator': 3.5.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@react-aria/slider': 3.9.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@react-aria/ssr': 3.9.10(react@19.2.6) + '@react-aria/switch': 3.8.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@react-aria/table': 3.18.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@react-aria/tabs': 3.12.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@react-aria/tag': 3.9.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@react-aria/textfield': 3.19.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@react-aria/toast': 3.1.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@react-aria/tooltip': 3.10.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@react-aria/tree': 3.2.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@react-aria/utils': 3.34.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@react-aria/visually-hidden': 3.9.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@react-types/shared': 3.34.0(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + + react-aria@3.48.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6): dependencies: '@internationalized/date': 3.12.1 '@internationalized/number': 3.6.6 '@internationalized/string': 3.2.8 - '@react-types/shared': 3.34.0(react@18.3.1) - '@swc/helpers': 0.5.11 + '@react-types/shared': 3.34.0(react@19.2.6) + '@swc/helpers': 0.5.15 aria-hidden: 1.2.6 clsx: 2.1.1 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - react-stately: 3.46.0(react@18.3.1) - use-sync-external-store: 1.6.0(react@18.3.1) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + react-stately: 3.46.0(react@19.2.6) + use-sync-external-store: 1.6.0(react@19.2.6) - react-dom@18.3.1(react@18.3.1): + react-dom@19.2.6(react@19.2.6): dependencies: - loose-envify: 1.4.0 - react: 18.3.1 - scheduler: 0.23.2 + react: 19.2.6 + scheduler: 0.27.0 react-fast-compare@2.0.4: {} react-fast-compare@3.2.2: {} - react-hot-toast@2.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-hot-toast@2.6.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6): dependencies: csstype: 3.2.3 goober: 2.1.18(csstype@3.2.3) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) - react-intersection-observer@10.0.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-intersection-observer@10.0.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6): dependencies: - react: 18.3.1 + react: 19.2.6 optionalDependencies: - react-dom: 18.3.1(react@18.3.1) + react-dom: 19.2.6(react@19.2.6) - react-intl@7.1.14(react@18.3.1)(typescript@5.4.5): + react-intl@7.1.14(react@19.2.6)(typescript@5.4.5): dependencies: '@formatjs/ecma402-abstract': 2.3.6 '@formatjs/icu-messageformat-parser': 2.11.4 '@formatjs/intl': 3.1.8(typescript@5.4.5) '@types/hoist-non-react-statics': 3.3.5 - '@types/react': 18.3.28 + '@types/react': 19.2.14 hoist-non-react-statics: 3.3.2 intl-messageformat: 10.7.18 - react: 18.3.1 + react: 19.2.6 tslib: 2.8.1 optionalDependencies: typescript: 5.4.5 @@ -15314,16 +15524,16 @@ snapshots: react-is@18.3.1: optional: true - react-markdown@10.1.0(@types/react@18.3.28)(react@18.3.1): + react-markdown@10.1.0(@types/react@19.2.14)(react@19.2.6): dependencies: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 - '@types/react': 18.3.28 + '@types/react': 19.2.14 devlop: 1.1.0 hast-util-to-jsx-runtime: 2.3.6 html-url-attributes: 3.0.1 mdast-util-to-hast: 13.2.1 - react: 18.3.1 + react: 19.2.6 remark-parse: 11.0.0 remark-rehype: 11.1.2 unified: 11.0.5 @@ -15332,75 +15542,73 @@ snapshots: transitivePeerDependencies: - supports-color - react-popper-tooltip@4.4.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-popper-tooltip@4.4.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6): dependencies: '@babel/runtime': 7.29.2 '@popperjs/core': 2.11.8 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - react-popper: 2.3.0(@popperjs/core@2.11.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + react-popper: 2.3.0(@popperjs/core@2.11.8)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - react-popper@2.3.0(@popperjs/core@2.11.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-popper@2.3.0(@popperjs/core@2.11.8)(react-dom@19.2.6(react@19.2.6))(react@19.2.6): dependencies: '@popperjs/core': 2.11.8 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) react-fast-compare: 3.2.2 warning: 4.0.3 - react-select@5.10.2(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-select@5.10.2(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6): dependencies: '@babel/runtime': 7.29.2 '@emotion/cache': 11.14.0 - '@emotion/react': 11.14.0(@types/react@18.3.28)(react@18.3.1) - '@floating-ui/dom': 1.7.4 - '@types/react-transition-group': 4.4.12(@types/react@18.3.28) + '@emotion/react': 11.14.0(@types/react@19.2.14)(react@19.2.6) + '@floating-ui/dom': 1.8.0 + '@types/react-transition-group': 4.4.12(@types/react@19.2.14) memoize-one: 6.0.0 prop-types: 15.8.1 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - use-isomorphic-layout-effect: 1.2.1(@types/react@18.3.28)(react@18.3.1) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + react-transition-group: 4.4.5(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + use-isomorphic-layout-effect: 1.2.1(@types/react@19.2.14)(react@19.2.6) transitivePeerDependencies: - '@types/react' - supports-color - react-stately@3.46.0(react@18.3.1): + react-stately@3.46.0(react@19.2.6): dependencies: '@internationalized/date': 3.12.1 '@internationalized/number': 3.6.6 '@internationalized/string': 3.2.8 - '@react-types/shared': 3.34.0(react@18.3.1) - '@swc/helpers': 0.5.11 - react: 18.3.1 - use-sync-external-store: 1.6.0(react@18.3.1) + '@react-types/shared': 3.34.0(react@19.2.6) + '@swc/helpers': 0.5.15 + react: 19.2.6 + use-sync-external-store: 1.6.0(react@19.2.6) - react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-transition-group@4.4.5(react-dom@19.2.6(react@19.2.6))(react@19.2.6): dependencies: '@babel/runtime': 7.29.2 dom-helpers: 5.2.1 loose-envify: 1.4.0 prop-types: 15.8.1 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) - react-truncate-markup@5.1.2(react@18.3.1): + react-truncate-markup@5.1.2(react@19.2.6): dependencies: line-height: 0.3.1 memoize-one: 5.2.1 prop-types: 15.8.1 - react: 18.3.1 + react: 19.2.6 resize-observer-polyfill: 1.5.1 - react-use-clipboard@1.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-use-clipboard@1.0.9(react-dom@19.2.6(react@19.2.6))(react@19.2.6): dependencies: copy-to-clipboard: 3.3.3 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) - react@18.3.1: - dependencies: - loose-envify: 1.4.0 + react@19.2.6: {} read-cache@1.0.0: dependencies: @@ -15529,9 +15737,12 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - resolve@2.0.0-next.5: + resolve@2.0.0-next.6: dependencies: + es-errors: 1.3.0 is-core-module: 2.16.1 + node-exports-info: 1.6.0 + object-keys: 1.1.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 @@ -15615,9 +15826,7 @@ snapshots: sax@1.5.0: {} - scheduler@0.23.2: - dependencies: - loose-envify: 1.4.0 + scheduler@0.27.0: {} selderee@0.11.0: dependencies: @@ -15891,6 +16100,8 @@ snapshots: minipass: 3.3.6 optional: true + stable-hash@0.0.5: {} + stack-trace@0.0.10: {} stack-utils@2.0.6: @@ -16032,10 +16243,10 @@ snapshots: dependencies: inline-style-parser: 0.2.7 - styled-jsx@5.1.1(@babel/core@7.29.0)(babel-plugin-macros@3.1.0)(react@18.3.1): + styled-jsx@5.1.6(@babel/core@7.29.0)(babel-plugin-macros@3.1.0)(react@19.2.6): dependencies: client-only: 0.0.1 - react: 18.3.1 + react: 19.2.6 optionalDependencies: '@babel/core': 7.29.0 babel-plugin-macros: 3.1.0 @@ -16107,11 +16318,11 @@ snapshots: express: 5.2.1 swagger-ui-dist: 5.17.14 - swr@2.4.1(react@18.3.1): + swr@2.4.1(react@19.2.6): dependencies: dequal: 2.0.3 - react: 18.3.1 - use-sync-external-store: 1.6.0(react@18.3.1) + react: 19.2.6 + use-sync-external-store: 1.6.0(react@19.2.6) synckit@0.11.12: dependencies: @@ -16119,6 +16330,8 @@ snapshots: systeminformation@5.31.5: {} + tabbable@6.5.0: {} + tailwind-merge@2.6.1: {} tailwindcss@3.4.19(yaml@2.8.3): @@ -16280,12 +16493,12 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-jest@29.4.6(@babel/core@7.29.0)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.29.0))(jest-util@30.2.0)(jest@30.2.0(@types/node@22.19.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.19.0)(typescript@5.4.5)))(typescript@5.4.5): + ts-jest@29.4.6(@babel/core@7.29.0)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.29.0))(jest-util@30.2.0)(jest@30.2.0(@types/node@22.19.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.15))(@types/node@22.19.0)(typescript@5.4.5)))(typescript@5.4.5): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 handlebars: 4.7.8 - jest: 30.2.0(@types/node@22.19.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.19.0)(typescript@5.4.5)) + jest: 30.2.0(@types/node@22.19.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.15))(@types/node@22.19.0)(typescript@5.4.5)) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 @@ -16301,7 +16514,7 @@ snapshots: jest-util: 30.2.0 optional: true - ts-node@10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.19.0)(typescript@5.4.5): + ts-node@10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.15))(@types/node@22.19.0)(typescript@5.4.5): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 @@ -16319,7 +16532,7 @@ snapshots: v8-compile-cache-lib: 3.0.1 yn: 3.1.1 optionalDependencies: - '@swc/core': 1.6.5(@swc/helpers@0.5.11) + '@swc/core': 1.6.5(@swc/helpers@0.5.15) tsc-alias@1.8.16: dependencies: @@ -16331,6 +16544,13 @@ snapshots: normalize-path: 3.0.0 plimit-lit: 1.6.1 + tsconfig-paths@3.15.0: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + tsconfig-paths@4.2.0: dependencies: json5: 2.2.3 @@ -16413,13 +16633,13 @@ snapshots: typedarray@0.0.6: {} - typeorm@0.3.28(babel-plugin-macros@3.1.0)(pg@8.20.0)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.19.0)(typescript@5.4.5)): + typeorm@0.3.31(babel-plugin-macros@3.1.0)(pg@8.23.0)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.15))(@types/node@22.19.0)(typescript@5.4.5)): dependencies: '@sqltools/formatter': 1.2.5 - ansis: 4.2.0 + ansis: 4.3.1 app-root-path: 3.1.0 buffer: 6.0.3 - dayjs: 1.11.20 + dayjs: 1.11.23 debug: 4.4.3(supports-color@5.5.0) dedent: 1.7.2(babel-plugin-macros@3.1.0) dotenv: 16.6.1 @@ -16428,12 +16648,12 @@ snapshots: sha.js: 2.4.12 sql-highlight: 6.1.0 tslib: 2.8.1 - uuid: 11.1.0 - yargs: 17.7.2 + uuid: 11.1.1 + yargs: 17.7.3 optionalDependencies: - pg: 8.20.0 + pg: 8.23.0 sqlite3: 5.1.7 - ts-node: 10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.19.0)(typescript@5.4.5) + ts-node: 10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.15))(@types/node@22.19.0)(typescript@5.4.5) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -16573,7 +16793,6 @@ snapshots: '@unrs/resolver-binding-win32-arm64-msvc': 1.11.1 '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 - optional: true untildify@4.0.0: {} @@ -16587,19 +16806,19 @@ snapshots: dependencies: punycode: 2.3.1 - use-isomorphic-layout-effect@1.2.1(@types/react@18.3.28)(react@18.3.1): + use-isomorphic-layout-effect@1.2.1(@types/react@19.2.14)(react@19.2.6): dependencies: - react: 18.3.1 + react: 19.2.6 optionalDependencies: - '@types/react': 18.3.28 + '@types/react': 19.2.14 - use-sync-external-store@1.6.0(react@18.3.1): + use-sync-external-store@1.6.0(react@19.2.6): dependencies: - react: 18.3.1 + react: 19.2.6 util-deprecate@1.0.2: {} - uuid@11.1.0: {} + uuid@11.1.1: {} uuid@8.3.2: {} @@ -16861,7 +17080,7 @@ snapshots: y18n: 5.0.8 yargs-parser: 20.2.9 - yargs@17.7.2: + yargs@17.7.3: dependencies: cliui: 8.0.1 escalade: 3.2.0 diff --git a/seerr-api.yml b/seerr-api.yml index e517722609..34d17b690b 100644 --- a/seerr-api.yml +++ b/seerr-api.yml @@ -148,10 +148,6 @@ components: email: type: string example: 'user@example.com' - discordId: - type: string - nullable: true - example: '123456789' locale: type: string nullable: true @@ -236,6 +232,9 @@ components: hideAvailable: type: boolean example: false + hideRequested: + type: boolean + example: false partialRequestsEnabled: type: boolean example: false @@ -254,6 +253,9 @@ components: enableSpecialEpisodes: type: boolean example: false + versionCheck: + type: boolean + example: false NetworkSettings: type: object properties: @@ -695,6 +697,73 @@ components: - is4k - enableSeasonFolders - isDefault + ReadarrSettings: + type: object + properties: + id: + type: number + example: 0 + readOnly: true + name: + type: string + example: 'Readarr Main' + hostname: + type: string + example: '127.0.0.1' + port: + type: number + example: 7878 + apiKey: + type: string + example: 'exampleapikey' + useSsl: + type: boolean + example: false + baseUrl: + type: string + activeProfileId: + type: number + example: 1 + activeProfileName: + type: string + example: ebook/Spoken + activeMetadataProfileId: + type: number + example: 1 + activeMetadataProfileName: + type: string + example: Standard + activeDirectory: + type: string + example: '/books' + is4k: + type: boolean + example: false + isDefault: + type: boolean + example: false + externalUrl: + type: string + example: http://readarr.example.com + syncEnabled: + type: boolean + example: false + preventSearch: + type: boolean + example: false + required: + - name + - hostname + - port + - apiKey + - useSsl + - activeProfileId + - activeProfileName + - activeMetadataProfileId + - activeMetadataProfileName + - activeDirectory + - isDefault + - is4k ServarrTag: type: object properties: @@ -1271,6 +1340,10 @@ components: type: number rootFolder: type: string + ignoreQuota: + type: boolean + example: false + description: If true, this request will not count against the user's quota. Requires MANAGE_REQUESTS permission. required: - id - status @@ -1551,6 +1624,8 @@ components: type: string topic: type: string + tags: + type: string authMethodUsernamePassword: type: boolean username: @@ -1561,6 +1636,10 @@ components: type: boolean token: type: string + priority: + type: number + locale: + type: string NotificationEmailSettings: type: object properties: @@ -1952,8 +2031,10 @@ components: discordEnabledTypes: type: number nullable: true - discordId: - type: string + discordIds: + type: array + items: + type: string nullable: true pushbulletAccessToken: type: string @@ -2131,6 +2212,195 @@ components: - certification: 'PG' meaning: 'Some material may not be suitable for children under 10.' order: 2 + + BookDetails: + type: object + properties: + id: + type: number + title: + type: string + description: + type: string + releaseDate: + type: string + image: + type: object + properties: + url: + type: string + mediaInfo: + $ref: '#/components/schemas/MediaInfo' + author: + type: array + items: + $ref: '#/components/schemas/AuthorSummary' + pages: + type: number + tags: + $ref: '#/components/schemas/BookTags' + headline: + type: string + subtitle: + type: string + series: + type: array + items: + type: object + properties: + position: + type: number + series_id: + type: number + series: + $ref: '#/components/schemas/SeriesSummary' + + BookResult: + type: object + properties: + id: + type: number + mediaType: + type: string + enum: [book] + title: + type: string + overview: + type: string + releaseDate: + type: string + posterPath: + type: string + nullable: true + position: + type: number + nullable: true + mediaInfo: + $ref: '#/components/schemas/MediaInfo' + + AuthorDetails: + type: object + properties: + id: + type: number + name: + type: string + bio: + type: string + born_date: + type: string + books_count: + type: number + posterPath: + type: string + nullable: true + books: + type: array + items: + $ref: '#/components/schemas/BookResult' + + AuthorResult: + type: object + properties: + id: + type: number + name: + type: string + image: + type: string + nullable: true + mediaType: + type: string + enum: [author] + + AuthorSummary: + type: object + properties: + id: + type: number + name: + type: string + bio: + type: string + nullable: true + born_date: + type: string + nullable: true + image: + type: object + nullable: true + properties: + url: + type: string + + Series: + type: object + properties: + id: + type: number + name: + type: string + overview: + type: string + nullable: true + posterPath: + type: string + nullable: true + backdropPath: + type: string + nullable: true + books: + type: array + items: + $ref: '#/components/schemas/BookResult' + + SeriesSummary: + type: object + properties: + id: + type: number + name: + type: string + description: + type: string + nullable: true + + BookTags: + type: object + properties: + Genre: + type: array + items: + $ref: '#/components/schemas/BookTag' + Mood: + type: array + items: + $ref: '#/components/schemas/BookTag' + 'Content Warning': + type: array + items: + $ref: '#/components/schemas/BookTag' + Tag: + type: array + items: + $ref: '#/components/schemas/BookTag' + + BookTag: + type: object + properties: + tag: + type: string + tagSlug: + type: string + category: + type: string + categorySlug: + type: string + spoilerRatio: + type: number + count: + type: number + securitySchemes: cookieAuth: type: apiKey @@ -2145,10 +2415,18 @@ paths: /status: get: summary: Get Seerr status - description: Returns the current Seerr status in a JSON object. + description: Returns the current Seerr status in a JSON object. updateAvailable and commitsBehind are omitted when checkUpdateAvailable is false. security: [] tags: - public + parameters: + - in: query + name: checkUpdateAvailable + description: If false, updateAvailable and commitsBehind will be omitted from the response. Defaults to the versionCheck setting. + required: false + example: false + schema: + type: boolean responses: '200': description: Returned status @@ -2302,23 +2580,58 @@ paths: get: summary: Get Jellyfin libraries description: Returns a list of Jellyfin libraries in a JSON array. + tags: + - settings + responses: + '200': + description: 'Jellyfin libraries returned' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/JellyfinLibrary' + /settings/jellyfin/library/{libraryId}: + put: + summary: Update a single Jellyfin library + description: Updates a single Jellyfin library with the provided values. tags: - settings parameters: - - in: query - name: sync - description: Syncs the current libraries with the current Jellyfin server - schema: - type: string - nullable: true - - in: query - name: enable - explode: false - allowReserved: true - description: Comma separated list of libraries to enable. Any libraries not passed will be disabled! + - in: path + name: libraryId + required: true schema: type: string - nullable: true + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - enabled + properties: + enabled: + type: boolean + example: true + responses: + '200': + description: 'Jellyfin library returned' + content: + application/json: + schema: + $ref: '#/components/schemas/JellyfinLibrary' + '400': + description: 'Invalid request body' + '404': + description: 'Library does not exist' + /settings/jellyfin/library/sync: + post: + summary: Sync Jellyfin libraries + description: Syncs the current libraries with the current Jellyfin server. Enabled flags are preserved. + tags: + - settings responses: '200': description: 'Jellyfin libraries returned' @@ -2328,6 +2641,10 @@ paths: type: array items: $ref: '#/components/schemas/JellyfinLibrary' + '404': + description: 'No libraries found' + '501': + description: 'Automatic library grouping is not supported' /settings/jellyfin/users: get: summary: Get Jellyfin Users @@ -2457,23 +2774,58 @@ paths: get: summary: Get Plex libraries description: Returns a list of Plex libraries in a JSON array. + tags: + - settings + responses: + '200': + description: 'Plex libraries returned' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PlexLibrary' + /settings/plex/library/{libraryId}: + put: + summary: Update a single Plex library + description: Updates a single Plex library with the provided values. tags: - settings parameters: - - in: query - name: sync - description: Syncs the current libraries with the current Plex server - schema: - type: string - nullable: true - - in: query - name: enable - explode: false - allowReserved: true - description: Comma separated list of libraries to enable. Any libraries not passed will be disabled! + - in: path + name: libraryId + required: true schema: type: string - nullable: true + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - enabled + properties: + enabled: + type: boolean + example: true + responses: + '200': + description: 'Plex library returned' + content: + application/json: + schema: + $ref: '#/components/schemas/PlexLibrary' + '400': + description: 'Invalid request body' + '404': + description: 'Library does not exist' + /settings/plex/library/sync: + post: + summary: Sync Plex libraries + description: Syncs the current libraries with the current Plex server. Enabled flags are preserved. + tags: + - settings responses: '200': description: 'Plex libraries returned' @@ -2955,6 +3307,173 @@ paths: application/json: schema: $ref: '#/components/schemas/SonarrSettings' + /settings/readarr: + get: + summary: Get Readarr settings + description: Returns all Readarr settings in a JSON array. + tags: + - settings + responses: + '200': + description: 'Values were returned' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ReadarrSettings' + post: + summary: Create Readarr instance + description: Creates a new Readarr instance from the request body. + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ReadarrSettings' + responses: + '201': + description: 'New Readarr instance created' + content: + application/json: + schema: + $ref: '#/components/schemas/ReadarrSettings' + /settings/readarr/test: + post: + summary: Test Readarr configuration + description: Tests if the Readarr configuration is valid. Returns profiles and root folders on success. + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + hostname: + type: string + example: '127.0.0.1' + port: + type: number + example: 7878 + apiKey: + type: string + example: yourapikey + useSsl: + type: boolean + example: false + baseUrl: + type: string + required: + - hostname + - port + - apiKey + - useSsl + responses: + '200': + description: Succesfully connected to Readarr instance + content: + application/json: + schema: + type: object + properties: + profiles: + type: array + items: + $ref: '#/components/schemas/ServiceProfile' + /settings/readarr/{readarrId}: + put: + summary: Update Readarr instance + description: Updates an existing Readarr instance with the provided values. + tags: + - settings + parameters: + - in: path + name: readarrId + required: true + schema: + type: integer + description: Readarr instance ID + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ReadarrSettings' + responses: + '200': + description: 'Readarr instance updated' + content: + application/json: + schema: + $ref: '#/components/schemas/ReadarrSettings' + delete: + summary: Delete Readarr instance + description: Deletes an existing Readarr instance based on the readarrId parameter. + tags: + - settings + parameters: + - in: path + name: readarrId + required: true + schema: + type: integer + description: Readarr instance ID + responses: + '200': + description: 'Readarr instance deleted' + content: + application/json: + schema: + $ref: '#/components/schemas/ReadarrSettings' + /settings/readarr/{readarrId}/profiles: + get: + summary: Get available Readarr profiles + description: Returns a list of profiles available on the Readarr server instance in a JSON array. + tags: + - settings + parameters: + - in: path + name: readarrId + required: true + schema: + type: integer + description: Readarr instance ID + responses: + '200': + description: Returned list of profiles + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ServiceProfile' + /settings/readarr/{readarrId}/metadataprofiles: + get: + summary: Get available Readarr metadata profiles + description: Returns a list of metadata profiles available on the Readarr server instance in a JSON array. + tags: + - settings + parameters: + - in: path + name: readarrId + required: true + schema: + type: integer + description: Readarr instance ID + responses: + '200': + description: Returned list of metadata profiles + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ServiceProfile' + /settings/public: get: summary: Get public settings @@ -3965,13 +4484,90 @@ paths: security: [] tags: - auth - responses: - '200': - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/User' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/User' + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + username: + type: string + password: + type: string + hostname: + type: string + email: + type: string + serverType: + type: number + required: + - username + - password + /auth/jellyfin/quickconnect/initiate: + post: + summary: Initiate Jellyfin Quick Connect + description: Initiates a Quick Connect session and returns a code for the user to authorize on their Jellyfin server. + security: [] + tags: + - auth + responses: + '200': + description: Quick Connect session initiated + content: + application/json: + schema: + type: object + properties: + code: + type: string + example: '123456' + secret: + type: string + example: 'abc123def456' + '500': + description: Failed to initiate Quick Connect + /auth/jellyfin/quickconnect/check: + get: + summary: Check Quick Connect authorization status + description: Checks if the Quick Connect code has been authorized by the user. + security: [] + tags: + - auth + parameters: + - in: query + name: secret + required: true + schema: + type: string + description: The secret returned from the initiate endpoint + responses: + '200': + description: Authorization status returned + content: + application/json: + schema: + type: object + properties: + authenticated: + type: boolean + example: false + '404': + description: Quick Connect session not found or expired + /auth/jellyfin/quickconnect/authenticate: + post: + summary: Authenticate with Quick Connect + description: Completes the Quick Connect authentication flow and creates a user session. + security: [] + tags: + - auth requestBody: required: true content: @@ -3979,19 +4575,21 @@ paths: schema: type: object properties: - username: - type: string - password: - type: string - hostname: - type: string - email: + secret: type: string - serverType: - type: number required: - - username - - password + - secret + responses: + '200': + description: Successfully authenticated + content: + application/json: + schema: + $ref: '#/components/schemas/User' + '403': + description: Quick Connect not authorized or access denied + '500': + description: Authentication failed /auth/local: post: summary: Sign in using a local account @@ -4667,6 +5265,7 @@ paths: enum: - movie - tv + - book responses: '200': description: Blocklist details in JSON @@ -4690,6 +5289,7 @@ paths: enum: - movie - tv + - book responses: '204': description: Succesfully removed media item @@ -4800,6 +5400,7 @@ paths: enum: - movie - tv + - book responses: '200': description: Blocklist details in JSON @@ -4826,6 +5427,7 @@ paths: enum: - movie - tv + - book responses: '204': description: Succesfully removed media item @@ -4912,6 +5514,7 @@ paths: enum: - movie - tv + - book responses: '204': description: Succesfully removed watchlist item @@ -5155,6 +5758,40 @@ paths: description: Unlink request invalid '404': description: User does not exist + /user/{userId}/settings/linked-accounts/jellyfin/quickconnect: + post: + summary: Link Jellyfin/Emby account with Quick Connect + description: Links a Jellyfin/Emby account to the user's profile using Quick Connect authentication + tags: + - users + parameters: + - in: path + name: userId + required: true + schema: + type: number + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + secret: + type: string + required: + - secret + responses: + '400': + description: Invalid Quick Connect secret + '204': + description: Account successfully linked + '401': + description: Unauthorized + '422': + description: Account already linked + '500': + description: Server error /user/{userId}/settings/notifications: get: summary: Get notification settings for a user @@ -5307,6 +5944,12 @@ paths: schema: type: string example: en + - in: query + name: type + required: false + schema: + type: string + example: 'Book' responses: '200': description: Results @@ -5411,6 +6054,58 @@ paths: type: array items: $ref: '#/components/schemas/Company' + + /discover/books: + get: + summary: Discover books + description: Returns a list of books in a JSON object. + tags: + - search + parameters: + - in: query + name: page + schema: + type: number + example: 1 + default: 1 + - in: query + name: sortBy + schema: + type: string + - in: query + name: genre + schema: + type: string + - in: query + name: primaryReleaseDateGte + schema: + type: string + - in: query + name: primaryReleaseDateLte + schema: + type: string + responses: + '200': + description: Results + content: + application/json: + schema: + type: object + properties: + page: + type: number + example: 1 + totalPages: + type: number + example: 20 + totalResults: + type: number + example: 200 + results: + type: array + items: + $ref: '#/components/schemas/BookResult' + /discover/movies: get: summary: Discover movies @@ -6338,7 +7033,7 @@ paths: name: mediaType schema: type: string - enum: [movie, tv, all] + enum: [movie, tv, book, all] nullable: true default: all responses: @@ -6372,7 +7067,7 @@ paths: properties: mediaType: type: string - enum: [movie, tv] + enum: [movie, tv, book] example: movie mediaId: type: number @@ -6395,6 +7090,8 @@ paths: type: number profileId: type: number + metadataProfileId: + type: number rootFolder: type: string languageProfileId: @@ -6402,6 +7099,10 @@ paths: userId: type: number nullable: true + ignoreQuota: + type: boolean + example: false + description: If true, this request will not count against the user's quota. Requires MANAGE_REQUESTS permission. required: - mediaType - mediaId @@ -6488,7 +7189,7 @@ paths: properties: mediaType: type: string - enum: [movie, tv] + enum: [movie, tv, book] seasons: type: array items: @@ -6501,6 +7202,8 @@ paths: type: number profileId: type: number + metadataProfileId: + type: number rootFolder: type: string languageProfileId: @@ -6517,6 +7220,8 @@ paths: application/json: schema: $ref: '#/components/schemas/MediaRequest' + '409': + description: Only pending requests can be modified delete: summary: Delete request description: Removes a request. If the user has the `MANAGE_REQUESTS` permission, any request can be removed. Otherwise, only pending requests can be removed. @@ -6557,6 +7262,8 @@ paths: application/json: schema: $ref: '#/components/schemas/MediaRequest' + '409': + description: Only failed requests can be retried /request/{requestId}/{status}: post: summary: Update a request's status @@ -6588,6 +7295,30 @@ paths: application/json: schema: $ref: '#/components/schemas/MediaRequest' + '400': + description: Status must be approve or decline + '409': + description: Only pending requests can be approved or declined + /book/{bookId}: + get: + summary: Get book details + description: Returns full book details in a JSON object. + tags: + - books + parameters: + - in: path + name: bookId + required: true + schema: + type: number + example: 337401 + responses: + '200': + description: Book details + content: + application/json: + schema: + $ref: '#/components/schemas/BookDetails' /movie/{movieId}: get: summary: Get movie details @@ -6984,6 +7715,26 @@ paths: criticsRating: type: string enum: ['Rotten', 'Fresh'] + /author/{authorId}: + get: + summary: Get author details + description: Returns author details based on provided authorId in a JSON object. + tags: + - person + parameters: + - in: path + name: authorId + required: true + schema: + type: number + example: 287 + responses: + '200': + description: Returned author + content: + application/json: + schema: + $ref: '#/components/schemas/AuthorDetails' /person/{personId}: get: summary: Get person details @@ -7119,7 +7870,7 @@ paths: /media/{mediaId}/file: delete: summary: Delete media file - description: Removes a media file from radarr/sonarr. The `ADMIN` permission is required to perform this action. + description: Removes a media file from radarr/sonarr/readarr. The `ADMIN` permission is required to perform this action. tags: - media parameters: @@ -7132,7 +7883,7 @@ paths: type: string - in: query name: is4k - description: Whether to remove from 4K service instance (true) or regular service instance (false) + description: Whether to remove from 4K/audiobook service instance (true) or regular service instance (false) required: false example: false schema: @@ -7140,6 +7891,42 @@ paths: responses: '204': description: Successfully removed media item + /media/lookup/{id}: + get: + summary: Lookup internal media ID by external ID + description: | + Returns the internal media ID for a given external identifier (TMDB ID for movies/TV, Hardcover ID for books). + tags: + - media + parameters: + - in: path + name: id + description: tmdbId ID or hardcover ID + required: true + schema: + type: string + - in: query + name: mediaType + description: movie, tv, or book + required: true + schema: + type: string + enum: [movie, tv, book] + responses: + '200': + description: Internal media ID lookup result + content: + application/json: + schema: + type: object + properties: + id: + type: number + nullable: true + description: Internal media ID, or null if not found + example: 123 + '500': + description: Internal server error /media/{mediaId}/{status}: post: summary: Update media status @@ -7258,6 +8045,26 @@ paths: application/json: schema: $ref: '#/components/schemas/Collection' + /series/{seriesId}: + get: + summary: Get series details + description: Returns full series details in a JSON object. + tags: + - series + parameters: + - in: path + name: seriesId + required: true + schema: + type: number + example: 537982 + responses: + '200': + description: Series details + content: + application/json: + schema: + $ref: '#/components/schemas/Series' /service/radarr: get: summary: Get non-sensitive Radarr server list @@ -7360,6 +8167,47 @@ paths: type: array items: $ref: '#/components/schemas/SonarrSeries' + /service/readarr: + get: + summary: Get non-sensitive Readarr server list + description: Returns a list of Readarr server IDs and names in a JSON object. + tags: + - service + responses: + '200': + description: Request successful + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ReadarrSettings' + /service/readarr/{readarrId}: + get: + summary: Get Readarr server quality profiles, metadata profiles and root folders + description: Returns a Readarr server's quality profile, metadata profiles and root folder details in a JSON object. + tags: + - service + parameters: + - in: path + name: readarrId + required: true + schema: + type: number + example: 0 + responses: + '200': + description: Request successful + content: + application/json: + schema: + type: object + properties: + server: + $ref: '#/components/schemas/ReadarrSettings' + profiles: + $ref: '#/components/schemas/ServiceProfile' + /regions: get: summary: Regions supported by TMDB @@ -7590,6 +8438,9 @@ paths: type: string mediaId: type: number + userId: + type: number + nullable: true responses: '201': description: Succesfully created the issue @@ -8014,6 +8865,70 @@ paths: application/json: schema: $ref: '#/components/schemas/OverrideRule' + /overrideRule/advancedRequest: + post: + summary: Advanced override rule request + description: Processes an advanced override rule request. + tags: + - overriderule + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - mediaType + - tmdbId + properties: + mediaType: + type: string + tmdbId: + type: number + example: 1 + is4k: + type: boolean + example: false + requestId: + type: number + nullable: true + requestUser: + type: number + nullable: true + tags: + type: array + items: + type: number + nullable: true + serviceId: + type: number + nullable: true + responses: + '200': + description: Advanced override rule request processed + content: + application/json: + schema: + type: object + properties: + rootFolder: + type: string + nullable: true + profileId: + type: number + nullable: true + tags: + type: array + items: + type: number + nullable: true + '403': + description: User does not have permission to modify the request user + '404': + description: User, media or request not found + '500': + description: Unable to evaluate override rules + security: - cookieAuth: [] - apiKey: [] diff --git a/server/api/externalapi.ts b/server/api/externalapi.ts index db9addcdbc..185c49dc65 100644 --- a/server/api/externalapi.ts +++ b/server/api/externalapi.ts @@ -1,8 +1,9 @@ -import { requestInterceptorFunction } from '@server/utils/customProxyAgent'; +import type { CacheStore } from '@server/lib/cache'; +import { proxyRequestInterceptor } from '@server/utils/customProxyAgent'; +import { userAgentRequestInterceptor } from '@server/utils/userAgent'; import type { AxiosInstance, AxiosRequestConfig } from 'axios'; import axios from 'axios'; import rateLimit from 'axios-rate-limit'; -import type NodeCache from 'node-cache'; // 5 minute default TTL (in seconds) const DEFAULT_TTL = 300; @@ -11,7 +12,7 @@ const DEFAULT_TTL = 300; const DEFAULT_ROLLING_BUFFER = 10000; export interface ExternalAPIOptions { - nodeCache?: NodeCache; + nodeCache?: CacheStore; headers?: Record; timeout?: number; rateLimit?: { @@ -23,7 +24,7 @@ export interface ExternalAPIOptions { class ExternalAPI { protected axios: AxiosInstance; private baseUrl: string; - private cache?: NodeCache; + private cache?: CacheStore; constructor( baseUrl: string, @@ -40,7 +41,8 @@ class ExternalAPI { ...options.headers, }, }); - this.axios.interceptors.request.use(requestInterceptorFunction); + this.axios.interceptors.request.use(proxyRequestInterceptor); + this.axios.interceptors.request.use(userAgentRequestInterceptor); if (options.rateLimit) { this.axios = rateLimit(this.axios, { @@ -53,27 +55,33 @@ class ExternalAPI { this.cache = options.nodeCache; } + // transform runs before the cache write. protected async get( endpoint: string, config?: AxiosRequestConfig, - ttl?: number + ttl?: number, + options?: { cache?: CacheStore; transform?: (data: T) => T } ): Promise { + const cache = options?.cache ?? this.cache; const cacheKey = this.serializeCacheKey(endpoint, { ...config?.params, headers: config?.headers, }); - const cachedItem = this.cache?.get(cacheKey); + const cachedItem = cache?.get(cacheKey); if (cachedItem) { return cachedItem; } const response = await this.axios.get(endpoint, config); + const data = options?.transform + ? options.transform(response.data) + : response.data; - if (this.cache && ttl !== 0) { - this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL); + if (cache && ttl !== 0) { + cache.set(cacheKey, data, ttl ?? DEFAULT_TTL); } - return response.data; + return data; } protected async post( diff --git a/server/api/hardcover/constants.ts b/server/api/hardcover/constants.ts new file mode 100644 index 0000000000..ee216632d4 --- /dev/null +++ b/server/api/hardcover/constants.ts @@ -0,0 +1,124 @@ +export const VALID_BOOK_HEADERS = [ + 'bookstore', + 'adventure', + 'classics', + 'dystopian', + 'fantasy', + 'fiction', + 'history', + 'lgbtq', + 'mystery', + 'nonfiction', + 'science-fiction', + 'war', + 'young-adult', +]; + +export const BOOK = ` + id + pages + title + subtitle + headline + description + users_count + release_date + rating + ratings_count + reviews_count + slug + cached_tags + image { + url + height + width + } + default_physical_edition_id + default_ebook_edition_id + default_audio_edition_id + default_physical_edition { + id + pages + isbn_10 + isbn_13 + asin + physical_format + publisher { + name + } + language { + language + } + } + default_ebook_edition { + id + isbn_13 + asin + } + default_audio_edition { + id + asin + audio_seconds + contributions(where: { contribution: { _eq: "Narrator" } }, limit: 3) { + author { + name + } + } + } +`; + +export const BOOK_RESULT = ` + id + title + description + release_date + cached_tags + image { + url + height + width + } +`; + +export const SERIES = ` + name + id + slug + description + books_count + primary_books_count + book_series { + book { + ${BOOK_RESULT} + } + } +`; + +export const AUTHOR = ` + bio + books_count + id + identifiers + name + user_id + users_count + born_date + image { + url + height + width + } +`; + +export const BOOK_SERIES = ` + book_series( + order_by: [{position: asc}, {book: {users_count: desc}}] + distinct_on: position + where: {book: {book_status_id: {_eq: "1"}, compilation: {_eq: false}, release_date: {_is_null: false}}, position: {_gte: "0"}} + ) { + series_id + position + book { + ${BOOK_RESULT} + } + }`; diff --git a/server/api/hardcover/hardcover.test.ts b/server/api/hardcover/hardcover.test.ts new file mode 100644 index 0000000000..13696a4809 --- /dev/null +++ b/server/api/hardcover/hardcover.test.ts @@ -0,0 +1,76 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; + +import type { AxiosAdapter, AxiosInstance } from 'axios'; +import axios, { AxiosError } from 'axios'; + +import Hardcover, { retryOnRateLimit } from '@server/api/hardcover'; + +function statusSequence(statuses: number[], retryAfter = '0.01') { + let calls = 0; + const adapter: AxiosAdapter = async (config) => { + const status = statuses[Math.min(calls, statuses.length - 1)]; + calls++; + const response = { + data: { ok: status === 200 }, + status, + statusText: String(status), + headers: { 'retry-after': retryAfter }, + config, + }; + + if (status !== 200) { + throw new AxiosError( + `Request failed with status code ${status}`, + AxiosError.ERR_BAD_REQUEST, + config, + null, + response + ); + } + + return response; + }; + + return { adapter, calls: () => calls }; +} + +describe('Hardcover rate limiting', () => { + it('retries a request that Hardcover rate limited', async () => { + const sequence = statusSequence([429, 200]); + const instance = axios.create({ adapter: sequence.adapter }); + retryOnRateLimit(instance); + + const response = await instance.post('/', {}); + + assert.strictEqual(response.status, 200); + assert.strictEqual(sequence.calls(), 2); + }); + + it('gives up after two retries', async () => { + const sequence = statusSequence([429]); + const instance = axios.create({ adapter: sequence.adapter }); + retryOnRateLimit(instance); + + await assert.rejects(() => instance.post('/', {}), /status code 429/); + assert.strictEqual(sequence.calls(), 3); + }); + + it('does not retry other errors', async () => { + const sequence = statusSequence([500]); + const instance = axios.create({ adapter: sequence.adapter }); + retryOnRateLimit(instance); + + await assert.rejects(() => instance.post('/', {}), /status code 500/); + assert.strictEqual(sequence.calls(), 1); + }); + + it('shares one rate-limited client between instances', () => { + const first = (new Hardcover() as unknown as { axios: AxiosInstance }) + .axios; + const second = (new Hardcover() as unknown as { axios: AxiosInstance }) + .axios; + + assert.strictEqual(first, second); + }); +}); diff --git a/server/api/hardcover/index.ts b/server/api/hardcover/index.ts new file mode 100644 index 0000000000..39dd716555 --- /dev/null +++ b/server/api/hardcover/index.ts @@ -0,0 +1,483 @@ +import ExternalAPI from '@server/api/externalapi'; +import { + AUTHOR, + BOOK, + BOOK_RESULT, + SERIES, + VALID_BOOK_HEADERS, +} from '@server/api/hardcover/constants'; +import cacheManager from '@server/lib/cache'; +import { getSettings } from '@server/lib/settings'; +import type { AxiosInstance, InternalAxiosRequestConfig } from 'axios'; + +import type { + AuthorByPKResponse, + BookByPKResponse, + BookResponse, + Books, + BookTrendingResponse, + HardcoverAuthorDetails, + HardcoverAuthorResult, + HardcoverBookDetails, + HardcoverBookResult, + HardcoverSearchHit, + HardcoverSearchMultiResponse, + HardcoverSearchResponse, + HardcoverSeries, + SeriesByPKResponse, + Tags, +} from './interfaces'; + +export const BookSortOptionsIterable = [ + 'popularity.desc', + 'popularity.asc', + 'release_date.desc', + 'release_date.asc', + 'original_title.asc', + 'original_title.desc', +] as const; + +export type BookSortOptions = (typeof BookSortOptionsIterable)[number]; + +interface DiscoverBookOptions { + genres?: string[]; + releaseDateGte?: string; + releaseDateLte?: string; + sortBy?: BookSortOptions; + page?: number; +} + +interface SearchOptions { + query?: string; + page?: number; +} + +export const getHeader = (tags: Tags): string | null => { + if (!tags.Genre?.length) { + return null; + } + + for (const genre of tags.Genre) { + if (genre.tagSlug && VALID_BOOK_HEADERS.includes(genre.tagSlug)) { + return genre.tagSlug; + } + } + + return null; +}; + +const RATE_LIMIT_RETRIES = 2; +const RATE_LIMIT_MAX_WAIT_MS = 10_000; + +type RetryableConfig = InternalAxiosRequestConfig & { + hardcoverRetries?: number; +}; + +export const retryOnRateLimit = (instance: AxiosInstance): void => { + instance.interceptors.response.use(undefined, async (error) => { + const config = error?.config as RetryableConfig | undefined; + + if (error?.response?.status !== 429 || !config) { + throw error; + } + + const attempt = (config.hardcoverRetries ?? 0) + 1; + + if (attempt > RATE_LIMIT_RETRIES) { + throw error; + } + + config.hardcoverRetries = attempt; + + const retryAfterSeconds = Number(error.response.headers?.['retry-after']); + const waitMs = + Number.isFinite(retryAfterSeconds) && retryAfterSeconds > 0 + ? Math.min(retryAfterSeconds * 1000, RATE_LIMIT_MAX_WAIT_MS) + : attempt * 2000; + + await new Promise((resolve) => setTimeout(resolve, waitMs)); + + return instance.request(config); + }); +}; + +// Routes create a client per request, so the limiter has to be shared to keep +// the server inside Hardcover's 60 requests a minute. +let sharedClient: { token: string; axios: AxiosInstance } | undefined; + +class Hardcover extends ExternalAPI { + constructor() { + const token = getSettings().main.hardcoverapikey; + super( + 'https://api.hardcover.app/v1/graphql', + {}, + { + nodeCache: cacheManager.getCache('hardcover').data, + rateLimit: { + maxRPS: 1, + maxRequests: 50, + }, + headers: { + authorization: `Bearer ${token}`, + }, + } + ); + + if (!sharedClient || sharedClient.token !== token) { + retryOnRateLimit(this.axios); + sharedClient = { token, axios: this.axios }; + } + + this.axios = sharedClient.axios; + } + + private getTrendingBooks = async (offset = 0): Promise => { + try { + const now = new Date(); + const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); + const fromDate = thirtyDaysAgo.toISOString().split('T')[0]; + const toDate = new Date().toISOString().split('T')[0]; + + const data = await this.post('/', { + query: ` + query Books_trending($fromDate: date!, $toDate: date!, $offset: Int!) { + books_trending(from: $fromDate, to: $toDate, offset: $offset, limit: 50) { + ids + } + } + `, + variables: { + fromDate, + toDate, + offset, + }, + }); + + if (!data || !data.data || !data.data.books_trending) { + throw new Error( + `Invalid API response structure: ${JSON.stringify(data)}` + ); + } + + const bookIds = data.data.books_trending.ids; + + if (!Array.isArray(bookIds)) { + throw new Error( + `Expected books_trending.ids to be an array, got: ${typeof bookIds}` + ); + } + + return bookIds; + } catch (error) { + throw new Error(`Error fetching trending books: ${error.message}`); + } + }; + + private createMultiSearchResults = ( + hits: HardcoverSearchHit[] + ): (HardcoverBookResult | HardcoverAuthorResult)[] => { + const results = []; + const seenAuthors = new Set(); + + for (const hit of hits) { + for (const contrib of hit.document.contributions || []) { + if ( + contrib.contribution === null && + contrib.author?.id && + !seenAuthors.has(contrib.author.id) + ) { + results.push({ ...contrib.author, media_type: 'author' }); + seenAuthors.add(contrib.author.id); + } + } + + if (!hit.document.compilation) { + results.push({ + ...hit.document, + id: +hit.document.id, + media_type: 'book', + }); + } + } + + return results as (HardcoverBookResult | HardcoverAuthorResult)[]; + }; + + public search = async ({ + query, + page = 1, + }: SearchOptions = {}): Promise => { + try { + const searchData = await this.post('/', { + query: ` + query Search($query: String!, $page: Int!) { + search( + query: $query + page: $page + per_page: 25 + query_type: "book" + fields: "title,isbns,series_names,author_names,alternative_titles" + weights: "5,1,3,5,1" + sort: "ratings_count:desc,_text_match:desc" + ) { + results + } + } + `, + variables: { + query: query, + page: page, + }, + }); + + const hits = searchData.data.search.results.hits || []; + + const results = this.createMultiSearchResults(hits); + + const total_results = searchData.data.search.results.found || 0; + const per_page = 25; + + return { + results, + page: page, + total_results: total_results, + total_pages: Math.ceil(total_results / per_page), + }; + } catch (e) { + throw new Error( + `[HARDCOVER] Failed to fetch search results: ${e.message}` + ); + } + }; + + public getDiscoverBooks = async ({ + page = 1, + genres, + releaseDateGte, + releaseDateLte, + sortBy, + }: DiscoverBookOptions = {}): Promise => { + try { + const hasFilters = !!( + genres?.length || + releaseDateGte || + releaseDateLte || + (sortBy && sortBy !== 'popularity.desc') + ); + + if (!hasFilters) { + const offset = Math.floor((page - 1) * 50); + + const ids = await this.getTrendingBooks(offset); + + return { + data: await this.getBooks(ids), + page: page, + total_results: 10000, + total_pages: 200, + }; + } + + const limit = 20; + const offset = (page - 1) * limit; + + const slugPattern = /^[a-z0-9-]+$/; + const datePattern = /^\d{4}-\d{2}-\d{2}$/; + + const conditions: string[] = [ + '{book_status_id: {_eq: "1"}}', + '{compilation: {_eq: false}}', + ]; + (genres ?? []) + .filter((genre) => slugPattern.test(genre)) + .forEach((genre) => { + conditions.push( + `{cached_tags: {_contains: {Genre: [{tagSlug: "${genre}"}]}}}` + ); + }); + if (releaseDateGte && datePattern.test(releaseDateGte)) { + conditions.push(`{release_date: {_gte: "${releaseDateGte}"}}`); + } + if (releaseDateLte && datePattern.test(releaseDateLte)) { + conditions.push(`{release_date: {_lte: "${releaseDateLte}"}}`); + } else if (sortBy === 'release_date.desc') { + const maxDate = new Date(); + maxDate.setFullYear(maxDate.getFullYear() + 1); + conditions.push( + `{release_date: {_lte: "${maxDate.toISOString().split('T')[0]}"}}` + ); + } + + const orderMap: Record = { + 'popularity.asc': '{users_count: asc}', + 'popularity.desc': '{users_count: desc}', + 'release_date.asc': '{release_date: asc}', + 'release_date.desc': '{release_date: desc}', + 'original_title.asc': '{title: asc}', + 'original_title.desc': '{title: desc}', + }; + + const data = await this.post('/', { + query: ` + query DiscoverBooksFiltered($limit: Int!, $offset: Int!) { + books( + where: {_and: [${conditions.join(', ')}]} + order_by: ${orderMap[sortBy ?? 'popularity.desc']} + limit: $limit + offset: $offset + ) { + ${BOOK_RESULT} + } + } + `, + variables: { + limit, + offset, + }, + }); + + return { + data: { books: data.data.books }, + page, + total_results: 1000, + total_pages: 50, + }; + } catch (e) { + throw new Error( + `[HARDCOVER] Failed to fetch discover books: ${e.message}` + ); + } + }; + + public getAuthor = async ( + authorId: number + ): Promise => { + try { + const data = await this.post('/', { + query: ` + query Authors_by_pk($authorId: Int!) { + authors_by_pk(id: $authorId) { + ${AUTHOR} + contributions( + where: {book: {book_status_id: {_eq: "1"}, compilation: {_eq: false}, default_physical_edition: {language_id: {_eq: 1}}}, _or: [{contribution: {_is_null: true}}, {contribution: {_nin: ["Narrator", "Translator", "Illustrator"]}}]} + order_by: {book: {users_count: desc}} + ) { + id + book { + ${BOOK_RESULT} + } + } + } + } + `, + variables: { + authorId, + }, + }); + + return data.data.authors_by_pk; + } catch (e) { + throw new Error( + `[HARDCOVER] Failed to fetch author details: ${e.message}` + ); + } + }; + + public getBooks = async (bookIds: number[]): Promise => { + try { + const data = await this.post('/', { + query: ` + query Books($bookIds: [Int!]!) { + books(where: { id: { _in: $bookIds } }) { + ${BOOK_RESULT} + } + } + `, + variables: { + bookIds, + }, + }); + + const bookMap = new Map(data.data.books.map((book) => [book.id, book])); + + const orderedBooks = bookIds + .map((id) => bookMap.get(id)) + .filter((book): book is HardcoverBookResult => book !== undefined); + + return { books: orderedBooks }; + } catch (e) { + throw new Error(`[HARDCOVER] Failed to fetch book details: ${e.message}`); + } + }; + + public getBook = async (bookId: number): Promise => { + try { + const data = await this.post('/', { + query: ` + query Books($bookId: Int!) { + books_by_pk(id: $bookId ) { + ${BOOK} + contributions(order_by: {author: {users_count: desc}}, where: {_or: [{contribution: {_is_null: true}}, {contribution: {_nin: ["Narrator", "Translator", "Illustrator"]}}]}) { + author { + ${AUTHOR} + } + } + book_series(order_by: {featured: desc}) { + position + series_id + series { + ${SERIES} + } + } + } + } + `, + variables: { + bookId, + }, + }); + + return data.data.books_by_pk; + } catch (e) { + throw new Error(`[HARDCOVER] Failed to fetch book details: ${e.message}`); + } + }; + + public getSeries = async (seriesId: number): Promise => { + try { + const data = await this.post('/', { + query: ` + query Series($seriesId: Int!) { + series_by_pk(id: $seriesId) { + id + name + description + primary_books_count + books_count + book_series( + order_by: [{position: asc}, {book: {users_count: desc}}] + distinct_on: position + where: {book: {book_status_id: {_eq: "1"}, compilation: {_eq: false}, release_date: {_is_null: false}}, position: {_gt: "0"}} + ) { + series_id + position + book { + ${BOOK_RESULT} + } + } + } + } + `, + variables: { + seriesId, + }, + }); + + return data.data.series_by_pk; + } catch (e) { + throw new Error(`[HARDCOVER] Failed to fetch book details: ${e.message}`); + } + }; +} + +export default Hardcover; diff --git a/server/api/hardcover/interfaces.ts b/server/api/hardcover/interfaces.ts new file mode 100644 index 0000000000..9cfd64e261 --- /dev/null +++ b/server/api/hardcover/interfaces.ts @@ -0,0 +1,334 @@ +export interface HardcoverPaginatedResponse { + page: number; + total_results: number; + total_pages: number; +} + +export interface Image { + color?: string; + color_name?: string; + height: number; + id: number; + url: string; + width: number; +} + +export interface Author { + alternate_names: string[]; + books: string[]; + books_count: number; + id: number; + image?: Image; + name: string; + name_personal: string; + series_names: string[]; + slug: string; +} + +export interface Book { + id: number; + title: string; + description: string; + release_date: string; + tags: Tags; + image: Image; + book_series: { + position: number; + series_id: number; + series: Series; + }[]; +} + +export interface Series { + name: string; + id: number; + slug: string; + description: string; + books_count: number; + primary_books_count: number; +} + +export interface HardcoverBookResult { + id: number; + title: string; + description: string; + release_date: string; + cached_tags?: Tags; + image?: Image; + media_type: 'book'; +} + +export interface HardcoverAuthorResult { + id: number; + name: string; + image?: Image; + slug: string; + media_type: 'author'; +} + +export interface BookTrendingResponse { + data: { + books_trending: { + ids: number[]; + }; + }; +} + +export interface HardcoverSearchMultiResponse extends HardcoverPaginatedResponse { + results: (HardcoverBookResult | HardcoverAuthorResult)[]; +} + +export interface AuthorByPKResponse { + data: { + authors_by_pk: HardcoverAuthorDetails; + }; +} + +export interface BookByPKResponse { + data: { + books_by_pk: HardcoverBookDetails; + }; +} + +export interface SeriesByPKResponse { + data: { + series_by_pk: HardcoverSeries; + }; +} + +export interface RelatedResponse { + data: { + recommendations: { + item_book: HardcoverBookResult; + }[]; + }; +} + +export interface AuthorResponse extends HardcoverPaginatedResponse { + data: { + authors: Author[]; + }; +} + +export interface BookResponse extends HardcoverPaginatedResponse { + data: Books; +} + +export interface Books { + books: HardcoverBookResult[]; +} + +export interface AuthorContribution { + id: number; + book: HardcoverBookResult; +} + +export interface BookContribution { + id: number; + author: Author; + contribution?: string | null; +} + +export interface Identifiers { + goodreads: string[]; + openlibrary: string[]; + isbn_10: string[]; + isbn_13: string[]; + asin: string[]; + kindle_asin: string[]; + inventaire_id: string[]; + lccn: string[]; + ocaid: string[]; + oclc: string[]; +} + +export interface Tags { + Genre: { + tag: string; + tagSlug: string; + category: string; + categorySlug: string; + spoilerRatio: number; + count: number; + }[]; + Mood: { + tag: string; + tagSlug: string; + category: string; + categorySlug: string; + spoilerRatio: number; + count: number; + }[]; + 'Content Warning': { + tag: string; + tagSlug: string; + category: string; + categorySlug: string; + spoilerRatio: number; + count: number; + }[]; + Tag: { + tag: string; + tagSlug: string; + category: string; + categorySlug: string; + spoilerRatio: number; + count: number; + }[]; +} + +export interface HardcoverSearchHit { + document: { + activities_count: number; + alternative_titles: string[]; + author_names: string[]; + compilation: boolean; + content_warnings: string[]; + contribution_types: string[]; + contributions: BookContribution[]; + cover_color?: string; + description?: string; + featured_series: { + details: string; + featured: boolean; + id: number; + position: number; + series: Series; + unreleased: boolean; + }; + genres: string[]; + has_audiobook: boolean; + has_ebook: boolean; + id: string; + image: Image; + isbns: string[]; + lists_count: number; + moods: string[]; + pages?: number; + prompts_count: number; + rating: number; + ratings_count: number; + release_date?: string; + release_year?: number; + reviews_count: number; + series_names: string[]; + slug: string; + subtitle?: string; + tags: string[]; + title: string; + users_count: number; + users_read_count: number; + }; + text_match: number; + text_match_info: { + best_field_score: string; + best_field_weight: number; + fields_matched: number; + score: string; + tokens_matched: number; + }; +} + +export interface HardcoverSearchResponse { + data: { + search: { + results: { + found: number; + hits: HardcoverSearchHit[]; + page: number; + request_params: { + per_page: number; + q: string; + }; + }; + }; + }; +} + +interface BookSeries { + position: number; + book: HardcoverBookResult; +} + +export interface HardcoverSeries { + id: number; + name: string; + description: string; + primary_books_count: number; + books_count: number; + book_series: BookSeries[]; +} + +export interface HardcoverPhysicalEdition { + id: number; + pages?: number; + isbn_10?: string; + isbn_13?: string; + asin?: string; + physical_format?: string; + publisher?: { + name: string; + }; + language?: { + language: string; + }; +} + +export interface HardcoverEbookEdition { + id: number; + isbn_13?: string; + asin?: string; +} + +export interface HardcoverAudioEdition { + id: number; + asin?: string; + audio_seconds?: number; + contributions?: { + author: { + name: string; + }; + }[]; +} + +export interface HardcoverBookDetails { + id: number; + title: string; + description: string; + release_date: string; + image: Image; + subtitle: string; + headline: string; + rating: number; + ratings_count: number; + reviews_count: number; + slug: string; + pages: number; + cached_tags: Tags; + contributions: BookContribution[]; + book_series: { + position: number; + series_id: number; + series: HardcoverSeries; + }[]; + default_physical_edition_id?: number; + default_ebook_edition_id?: number; + default_audio_edition_id?: number; + default_physical_edition?: HardcoverPhysicalEdition; + default_ebook_edition?: HardcoverEbookEdition; + default_audio_edition?: HardcoverAudioEdition; +} + +export interface HardcoverAuthorDetails { + bio: string; + books_count: number; + id: number; + identifiers: Identifiers; + name: string; + image: { + url: string; + }; + users_count: number; + born_date: string; + contributions: AuthorContribution[]; +} diff --git a/server/api/jellyfin.ts b/server/api/jellyfin.ts index 28afc3e0ee..188beb1ff1 100644 --- a/server/api/jellyfin.ts +++ b/server/api/jellyfin.ts @@ -44,6 +44,23 @@ export interface JellyfinLoginResponse { AccessToken: string; } +export interface QuickConnectInitiateResponse { + Secret: string; + Code: string; + DateAdded: string; +} + +export interface QuickConnectStatusResponse { + Authenticated: boolean; + Secret: string; + Code: string; + DeviceId: string; + DeviceName: string; + AppName: string; + AppVersion: string; + DateAdded: string; +} + export interface JellyfinUserListResponse { users: JellyfinUserResponse[]; } @@ -216,7 +233,79 @@ class JellyfinAPI extends ExternalAPI { } ); - throw new ApiError(e.response?.status, ApiErrorCode.Unknown); + if (!e.response) { + throw new ApiError(502, ApiErrorCode.ConnectionError); + } + + throw new ApiError(e.response.status, ApiErrorCode.Unknown); + } + } + + public async initiateQuickConnect(): Promise { + try { + const response = await this.post( + '/QuickConnect/Initiate' + ); + + return response; + } catch (e) { + logger.error( + `Something went wrong while initiating Quick Connect: ${e.message}`, + { label: 'Jellyfin API', error: e.response?.status } + ); + + if (!e.response) { + throw new ApiError(502, ApiErrorCode.ConnectionError); + } + + throw new ApiError(e.response.status, ApiErrorCode.Unknown); + } + } + + public async checkQuickConnect( + secret: string + ): Promise { + try { + const response = await this.get( + '/QuickConnect/Connect', + { params: { secret } } + ); + + return response; + } catch (e) { + logger.error( + `Something went wrong while getting Quick Connect status: ${e.message}`, + { label: 'Jellyfin API', error: e.response?.status } + ); + + if (!e.response) { + throw new ApiError(502, ApiErrorCode.ConnectionError); + } + + throw new ApiError(e.response.status, ApiErrorCode.Unknown); + } + } + + public async authenticateQuickConnect( + secret: string + ): Promise { + try { + const response = await this.post( + '/Users/AuthenticateWithQuickConnect', + { Secret: secret } + ); + return response; + } catch (e) { + logger.error( + `Something went wrong while authenticating with Quick Connect: ${e.message}`, + { label: 'Jellyfin API', error: e.response?.status } + ); + + if (!e.response) { + throw new ApiError(502, ApiErrorCode.ConnectionError); + } + + throw new ApiError(e.response.status, ApiErrorCode.Unknown); } } @@ -231,7 +320,11 @@ class JellyfinAPI extends ExternalAPI { return systemInfoResponse; } catch (e) { - throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken); + if (!e.response) { + throw new ApiError(502, ApiErrorCode.ConnectionError); + } + + throw new ApiError(e.response.status, ApiErrorCode.InvalidAuthToken); } } @@ -248,7 +341,11 @@ class JellyfinAPI extends ExternalAPI { { label: 'Jellyfin API', error: e.response?.status } ); - throw new ApiError(e.response?.status, ApiErrorCode.Unknown); + if (!e.response) { + throw new ApiError(502, ApiErrorCode.ConnectionError); + } + + throw new ApiError(e.response.status, ApiErrorCode.Unknown); } } @@ -263,7 +360,11 @@ class JellyfinAPI extends ExternalAPI { { label: 'Jellyfin API', error: e.response?.status } ); - throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken); + if (!e.response) { + throw new ApiError(502, ApiErrorCode.ConnectionError); + } + + throw new ApiError(e.response.status, ApiErrorCode.InvalidAuthToken); } } @@ -279,7 +380,11 @@ class JellyfinAPI extends ExternalAPI { { label: 'Jellyfin API', error: e.response?.status } ); - throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken); + if (!e.response) { + throw new ApiError(502, ApiErrorCode.ConnectionError); + } + + throw new ApiError(e.response.status, ApiErrorCode.InvalidAuthToken); } } @@ -306,6 +411,10 @@ class JellyfinAPI extends ExternalAPI { } ); + if (!e.response) { + throw new ApiError(502, ApiErrorCode.ConnectionError); + } + return []; } } @@ -352,7 +461,11 @@ class JellyfinAPI extends ExternalAPI { { label: 'Jellyfin API', error: e?.response?.status } ); - throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken); + if (!e.response) { + throw new ApiError(502, ApiErrorCode.ConnectionError); + } + + throw new ApiError(e.response.status, ApiErrorCode.InvalidAuthToken); } } @@ -377,7 +490,11 @@ class JellyfinAPI extends ExternalAPI { { label: 'Jellyfin API', error: e.response?.status } ); - throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken); + if (!e.response) { + throw new ApiError(502, ApiErrorCode.ConnectionError); + } + + throw new ApiError(e.response.status, ApiErrorCode.InvalidAuthToken); } } @@ -404,7 +521,11 @@ class JellyfinAPI extends ExternalAPI { `Something went wrong while getting library content from the Jellyfin server: ${e.message}`, { label: 'Jellyfin API', error: e.response?.status } ); - throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken); + if (!e.response) { + throw new ApiError(502, ApiErrorCode.ConnectionError); + } + + throw new ApiError(e.response.status, ApiErrorCode.InvalidAuthToken); } } @@ -419,7 +540,11 @@ class JellyfinAPI extends ExternalAPI { { label: 'Jellyfin API', error: e.response?.status } ); - throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken); + if (!e.response) { + throw new ApiError(502, ApiErrorCode.ConnectionError); + } + + throw new ApiError(e.response.status, ApiErrorCode.InvalidAuthToken); } } @@ -450,7 +575,11 @@ class JellyfinAPI extends ExternalAPI { { label: 'Jellyfin API', error: e.response?.status } ); - throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken); + if (!e.response) { + throw new ApiError(502, ApiErrorCode.ConnectionError); + } + + throw new ApiError(e.response.status, ApiErrorCode.InvalidAuthToken); } } @@ -467,7 +596,11 @@ class JellyfinAPI extends ExternalAPI { { label: 'Jellyfin API', error: e.response?.status } ); - throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken); + if (!e.response) { + throw new ApiError(502, ApiErrorCode.ConnectionError); + } + + throw new ApiError(e.response.status, ApiErrorCode.InvalidAuthToken); } } } diff --git a/server/api/plexapi.ts b/server/api/plexapi.ts index 617203447f..0837569b03 100644 --- a/server/api/plexapi.ts +++ b/server/api/plexapi.ts @@ -1,7 +1,9 @@ import ExternalAPI from '@server/api/externalapi'; +import { ApiErrorCode } from '@server/constants/error'; import type { Library, PlexSettings } from '@server/lib/settings'; import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; +import { ApiError } from '@server/types/error'; interface PlexStatusResponse { MediaContainer: { @@ -148,7 +150,7 @@ class PlexAPI extends ExternalAPI { .filter((library) => library.agent !== 'com.plexapp.agents.none') .map((library) => { const existing = settings.plex.libraries.find( - (l) => l.id === library.key && l.name === library.title + (l) => l.id === library.key ); return { @@ -167,7 +169,11 @@ class PlexAPI extends ExternalAPI { message: e.message, }); - settings.plex.libraries = []; + if (!e.response) { + throw new ApiError(502, ApiErrorCode.ConnectionError); + } + + throw new ApiError(e.response.status, ApiErrorCode.Unknown); } await settings.save(); @@ -222,8 +228,9 @@ class PlexAPI extends ExternalAPI { mediaType: 'movie' | 'show' ): Promise { const response = await this.get( - `/library/sections/${id}/all?type=${ - mediaType === 'show' ? '4' : '1' + `/library/sections/${id}/all?type=${mediaType === 'show' ? '4' : '1'}${ + // Shows are queried as episodes, whose guids the scanner never reads. + mediaType === 'movie' ? '&includeGuids=1' : '' }&sort=addedAt%3Adesc&addedAt>>=${Math.floor(options.addedAt / 1000)}`, { headers: { diff --git a/server/api/plextv.test.ts b/server/api/plextv.test.ts new file mode 100644 index 0000000000..4bfcf80a6f --- /dev/null +++ b/server/api/plextv.test.ts @@ -0,0 +1,190 @@ +import PlexTvAPI, { WATCHLIST_METADATA_CONCURRENCY } from '@server/api/plextv'; +import cacheManager from '@server/lib/cache'; +import type { + AxiosAdapter, + AxiosResponse, + InternalAxiosRequestConfig, +} from 'axios'; +import assert from 'node:assert/strict'; +import { afterEach, beforeEach, describe, it } from 'node:test'; + +class TestPlexTvAPI extends PlexTvAPI { + public installAdapter(adapter: AxiosAdapter): void { + this.axios.defaults.adapter = adapter; + } +} + +function axiosResponse( + config: InternalAxiosRequestConfig, + data: T, + headers: Record = {} +): AxiosResponse { + return { + data, + status: 200, + statusText: 'OK', + headers, + config, + request: {}, + }; +} + +function watchlistResponse(count: number) { + return { + MediaContainer: { + totalSize: count, + Metadata: Array.from({ length: count }, (_, index) => ({ + ratingKey: `rk-${index + 1}`, + })), + }, + }; +} + +function metadataResponse(ratingKey: string) { + const id = ratingKey.replace('rk-', ''); + + return { + MediaContainer: { + Metadata: [ + { + ratingKey, + type: 'movie' as const, + title: `Movie ${id}`, + Guid: [{ id: `tmdb://${id}` as const }], + }, + ], + }, + }; +} + +function isMetadataRequest(url: string | undefined): boolean { + return !!url?.includes('/library/metadata/'); +} + +function isWatchlistRequest(url: string | undefined): boolean { + return !!url?.includes('/library/sections/watchlist/all'); +} + +describe('PlexTvAPI.getWatchlist metadata fetching', () => { + beforeEach(() => { + cacheManager.getCache('plexwatchlist').flush(); + cacheManager.getCache('plextv').flush(); + }); + + afterEach(() => { + cacheManager.getCache('plexwatchlist').flush(); + cacheManager.getCache('plextv').flush(); + }); + + it('limits concurrent metadata requests', async () => { + const itemCount = 12; + let inFlight = 0; + let maxInFlight = 0; + + const api = new TestPlexTvAPI('test-token'); + api.installAdapter(async (config) => { + const url = config.url; + + if (isWatchlistRequest(url)) { + return axiosResponse(config, watchlistResponse(itemCount), { + etag: 'test-etag', + }); + } + + if (isMetadataRequest(url)) { + inFlight++; + maxInFlight = Math.max(maxInFlight, inFlight); + await new Promise((resolve) => setTimeout(resolve, 20)); + inFlight--; + + const ratingKey = url?.split('/library/metadata/')[1] ?? ''; + return axiosResponse(config, metadataResponse(ratingKey)); + } + + throw new Error(`Unexpected request: ${url}`); + }); + + const result = await api.getWatchlist({ size: itemCount }); + + assert.strictEqual(result.items.length, itemCount); + assert.ok( + maxInFlight <= WATCHLIST_METADATA_CONCURRENCY, + `expected at most ${WATCHLIST_METADATA_CONCURRENCY} concurrent metadata requests, saw ${maxInFlight}` + ); + }); + + it('returns partial results when individual metadata requests fail', async () => { + const itemCount = 8; + + const api = new TestPlexTvAPI('test-token'); + api.installAdapter(async (config) => { + const url = config.url; + + if (isWatchlistRequest(url)) { + return axiosResponse(config, watchlistResponse(itemCount), { + etag: 'test-etag', + }); + } + + if (isMetadataRequest(url)) { + const ratingKey = url?.split('/library/metadata/')[1] ?? ''; + + if (Number(ratingKey.replace('rk-', '')) % 2 === 0) { + const error = new Error('connect ETIMEDOUT') as Error & { + code: string; + }; + error.code = 'ETIMEDOUT'; + throw error; + } + + return axiosResponse(config, metadataResponse(ratingKey)); + } + + throw new Error(`Unexpected request: ${url}`); + }); + + const result = await api.getWatchlist({ size: itemCount }); + + assert.strictEqual(result.items.length, 4); + assert.ok( + result.items.every((item) => Number(item.tmdbId) % 2 === 1), + 'only odd-numbered watchlist items should be returned' + ); + }); + + it('fails the sync when metadata requests return auth errors', async () => { + const itemCount = 3; + + const api = new TestPlexTvAPI('test-token'); + api.installAdapter(async (config) => { + const url = config.url; + + if (isWatchlistRequest(url)) { + return axiosResponse(config, watchlistResponse(itemCount), { + etag: 'test-etag', + }); + } + + if (isMetadataRequest(url)) { + const error = new Error( + 'Request failed with status code 401' + ) as Error & { + response: { status: number }; + }; + error.response = { status: 401 }; + throw error; + } + + throw new Error(`Unexpected request: ${url}`); + }); + + const result = await api.getWatchlist({ size: itemCount }); + + assert.deepStrictEqual(result, { + offset: 0, + size: itemCount, + totalSize: 0, + items: [], + }); + }); +}); diff --git a/server/api/plextv.ts b/server/api/plextv.ts index b8f3c9a47b..20df0a50b5 100644 --- a/server/api/plextv.ts +++ b/server/api/plextv.ts @@ -135,6 +135,38 @@ export interface PlexWatchlistCache { response: WatchlistResponse; } +export const WATCHLIST_METADATA_CONCURRENCY = 5; + +async function mapWithConcurrency( + items: readonly T[], + concurrency: number, + fn: (item: T) => Promise +): Promise { + if (items.length === 0) { + return []; + } + + const results = new Array(items.length); + let nextIndex = 0; + + const worker = async (): Promise => { + while (true) { + const index = nextIndex++; + if (index >= items.length) { + return; + } + + results[index] = await fn(items[index]); + } + }; + + await Promise.all( + Array.from({ length: Math.min(concurrency, items.length) }, () => worker()) + ); + + return results; +} + class PlexTvAPI extends ExternalAPI { private authToken: string; @@ -268,6 +300,70 @@ class PlexTvAPI extends ExternalAPI { return parsedXml; } + private async fetchWatchlistItemMetadata(watchlistItem: { + ratingKey: string; + }): Promise { + let detailedResponse: MetadataResponse; + try { + detailedResponse = await this.getRolling( + `/library/metadata/${watchlistItem.ratingKey}`, + { + baseURL: 'https://discover.provider.plex.tv', + } + ); + } catch (e) { + if (e.response?.status === 404) { + logger.warn( + `Item with ratingKey ${watchlistItem.ratingKey} not found, it may have been removed from the server.`, + { label: 'Plex.TV Metadata API' } + ); + return null; + } + + if (e.code === 'ETIMEDOUT' || e.code === 'ENETUNREACH') { + logger.warn( + `Failed to fetch metadata for ratingKey ${watchlistItem.ratingKey}`, + { + label: 'Plex.TV Metadata API', + errorMessage: e.message, + } + ); + return null; + } + + throw e; + } + + const metadata = + detailedResponse.MediaContainer.Metadata?.[0] ?? + detailedResponse.MediaContainer.Video?.[0]; + + if (!metadata) { + logger.warn( + `Item with ratingKey ${watchlistItem.ratingKey} returned no metadata, skipping.`, + { label: 'Plex.TV Metadata API' } + ); + return null; + } + + const tmdbString = metadata.Guid?.find((guid) => + guid.id.startsWith('tmdb') + ); + const tvdbString = metadata.Guid?.find((guid) => + guid.id.startsWith('tvdb') + ); + + return { + ratingKey: metadata.ratingKey, + // This should always be set? But I guess it also cannot be? + // We will filter out the 0's afterwards + tmdbId: tmdbString ? Number(tmdbString.id.split('//')[1]) : 0, + tvdbId: tvdbString ? Number(tvdbString.id.split('//')[1]) : undefined, + title: metadata.title, + type: metadata.type, + }; + } + public async getWatchlist({ offset = 0, size = 20, @@ -311,61 +407,10 @@ class PlexTvAPI extends ExternalAPI { ); } - const watchlistDetails = await Promise.all( - (cachedWatchlist?.response.MediaContainer.Metadata ?? []).map( - async (watchlistItem) => { - let detailedResponse: MetadataResponse; - try { - detailedResponse = await this.getRolling( - `/library/metadata/${watchlistItem.ratingKey}`, - { - baseURL: 'https://discover.provider.plex.tv', - } - ); - } catch (e) { - if (e.response?.status === 404) { - logger.warn( - `Item with ratingKey ${watchlistItem.ratingKey} not found, it may have been removed from the server.`, - { label: 'Plex.TV Metadata API' } - ); - return null; - } else { - throw e; - } - } - - const metadata = - detailedResponse.MediaContainer.Metadata?.[0] ?? - detailedResponse.MediaContainer.Video?.[0]; - - if (!metadata) { - logger.warn( - `Item with ratingKey ${watchlistItem.ratingKey} returned no metadata, skipping.`, - { label: 'Plex.TV Metadata API' } - ); - return null; - } - - const tmdbString = metadata.Guid?.find((guid) => - guid.id.startsWith('tmdb') - ); - const tvdbString = metadata.Guid?.find((guid) => - guid.id.startsWith('tvdb') - ); - - return { - ratingKey: metadata.ratingKey, - // This should always be set? But I guess it also cannot be? - // We will filter out the 0's afterwards - tmdbId: tmdbString ? Number(tmdbString.id.split('//')[1]) : 0, - tvdbId: tvdbString - ? Number(tvdbString.id.split('//')[1]) - : undefined, - title: metadata.title, - type: metadata.type, - }; - } - ) + const watchlistDetails = await mapWithConcurrency( + cachedWatchlist?.response.MediaContainer.Metadata ?? [], + WATCHLIST_METADATA_CONCURRENCY, + (watchlistItem) => this.fetchWatchlistItemMetadata(watchlistItem) ); const filteredList = watchlistDetails.filter( diff --git a/server/api/servarr/radarr.test.ts b/server/api/servarr/radarr.test.ts new file mode 100644 index 0000000000..f3aaed3848 --- /dev/null +++ b/server/api/servarr/radarr.test.ts @@ -0,0 +1,119 @@ +import assert from 'node:assert/strict'; +import { afterEach, describe, it, mock } from 'node:test'; + +import type { AxiosInstance } from 'axios'; + +import RadarrAPI from '@server/api/servarr/radarr'; + +function buildRadarr(): RadarrAPI { + return new RadarrAPI({ url: 'http://localhost:7878/api/v3', apiKey: 'test' }); +} + +function getAxios(radarr: RadarrAPI): AxiosInstance { + return (radarr as unknown as { axios: AxiosInstance }).axios; +} + +describe('RadarrAPI removeMovie', () => { + afterEach(() => mock.restoreAll()); + + it('removes the movie when it exists in the library', async () => { + const radarr = buildRadarr(); + mock.method(RadarrAPI.prototype, 'getMovieByTmdbId', async () => ({ + id: 7, + title: 'Test Movie', + })); + const del = mock.method(getAxios(radarr), 'delete', async () => ({})); + + await radarr.removeMovie(550); + + assert.strictEqual(del.mock.callCount(), 1); + assert.strictEqual(del.mock.calls[0].arguments[0], '/movie/7'); + }); + + it('does nothing when the movie is not in the library', async () => { + const radarr = buildRadarr(); + mock.method(getAxios(radarr), 'get', async () => ({ + data: [{ id: 0, title: 'Fight Club' }], + })); + const del = mock.method(getAxios(radarr), 'delete', async () => ({})); + + await assert.doesNotReject(() => radarr.removeMovie(550)); + assert.strictEqual(del.mock.callCount(), 0); + }); + + it('rejects when the tmdbId is unknown to the lookup', async () => { + const radarr = buildRadarr(); + mock.method(getAxios(radarr), 'get', async () => ({ data: [] })); + const del = mock.method(getAxios(radarr), 'delete', async () => ({})); + + await assert.rejects(() => radarr.removeMovie(550), /Movie not found/); + assert.strictEqual(del.mock.callCount(), 0); + }); + + it('ignores a 404 when the movie was already removed in Radarr', async () => { + const radarr = buildRadarr(); + mock.method(RadarrAPI.prototype, 'getMovieByTmdbId', async () => ({ + id: 7, + title: 'Test Movie', + })); + mock.method(getAxios(radarr), 'delete', async () => { + throw { response: { status: 404 } }; + }); + + await assert.doesNotReject(() => radarr.removeMovie(550)); + }); + + it('rethrows errors other than 404', async () => { + const radarr = buildRadarr(); + mock.method(RadarrAPI.prototype, 'getMovieByTmdbId', async () => ({ + id: 7, + title: 'Test Movie', + })); + mock.method(getAxios(radarr), 'delete', async () => { + throw { response: { status: 500 } }; + }); + + await assert.rejects(() => radarr.removeMovie(550)); + }); + + it('rethrows a 404 from the lookup instead of treating it as removed', async () => { + const radarr = buildRadarr(); + mock.method(getAxios(radarr), 'get', async () => { + throw { response: { status: 404 } }; + }); + const del = mock.method(getAxios(radarr), 'delete', async () => ({})); + + await assert.rejects( + () => radarr.removeMovie(550), + (e: unknown) => + (e as { response?: { status?: number } }).response?.status === 404 + ); + assert.strictEqual(del.mock.callCount(), 0); + }); +}); + +describe('RadarrAPI getMovieByTmdbId', () => { + afterEach(() => mock.restoreAll()); + + it('rethrows a 401 from the lookup with the status intact', async () => { + const radarr = buildRadarr(); + mock.method(getAxios(radarr), 'get', async () => { + throw { response: { status: 401 } }; + }); + + await assert.rejects( + () => radarr.getMovieByTmdbId(550), + (e: unknown) => + (e as { response?: { status?: number } }).response?.status === 401 + ); + }); + + it('throws "Movie not found" when the lookup returns no results', async () => { + const radarr = buildRadarr(); + mock.method(getAxios(radarr), 'get', async () => ({ data: [] })); + + await assert.rejects(() => radarr.getMovieByTmdbId(550), { + message: 'Movie not found', + }); + }); +}); diff --git a/server/api/servarr/radarr.ts b/server/api/servarr/radarr.ts index 1d35778c0f..e3b231cb03 100644 --- a/server/api/servarr/radarr.ts +++ b/server/api/servarr/radarr.ts @@ -1,4 +1,5 @@ import logger from '@server/logger'; +import type { AxiosResponse } from 'axios'; import ServarrBase from './base'; export interface RadarrMovieOptions { @@ -80,6 +81,23 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> { } }; + public getLibraryMoviesByTmdbId = async ( + tmdbId: number + ): Promise => { + try { + const response = await this.axios.get('/movie', { + params: { tmdbId }, + }); + + return response.data; + } catch (e) { + throw new Error( + `[Radarr] Failed to retrieve movies by TMDB ID: ${e.message}`, + { cause: e } + ); + } + }; + public getMovie = async ({ id }: { id: number }): Promise => { try { const response = await this.axios.get(`/movie/${id}`); @@ -93,26 +111,27 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> { }; public async getMovieByTmdbId(id: number): Promise { + let response: AxiosResponse; try { - const response = await this.axios.get('/movie/lookup', { + response = await this.axios.get('/movie/lookup', { params: { term: `tmdb:${id}`, }, }); - - if (!response.data[0]) { - throw new Error('Movie not found'); - } - - return response.data[0]; } catch (e) { logger.error('Error retrieving movie by TMDB ID', { label: 'Radarr API', errorMessage: e.message, tmdbId: id, }); - throw new Error('Movie not found', { cause: e }); + throw e; } + + if (!response.data[0]) { + throw new Error('Movie not found'); + } + + return response.data[0]; } public addMovie = async ( @@ -267,9 +286,17 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> { ); } } - public removeMovie = async (movieId: number): Promise => { + public removeMovie = async (tmdbId: number): Promise => { + const { id, title } = await this.getMovieByTmdbId(tmdbId); + + if (!id) { + logger.info(`[Radarr] Movie not in library, nothing to remove`, { + tmdbId, + }); + return; + } + try { - const { id, title } = await this.getMovieByTmdbId(movieId); await this.axios.delete(`/movie/${id}`, { params: { deleteFiles: true, @@ -278,9 +305,13 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> { }); logger.info(`[Radarr] Removed movie ${title}`); } catch (e) { - throw new Error(`[Radarr] Failed to remove movie: ${e.message}`, { - cause: e, - }); + if (e?.response?.status === 404) { + logger.info(`[Radarr] Movie already removed from Radarr`, { + tmdbId, + }); + return; + } + throw e; } }; diff --git a/server/api/servarr/readarr.ts b/server/api/servarr/readarr.ts new file mode 100644 index 0000000000..8a1fdc6f63 --- /dev/null +++ b/server/api/servarr/readarr.ts @@ -0,0 +1,393 @@ +import logger from '@server/logger'; +import ServarrBase from './base'; + +export interface MetadataProfile { + id: number; + name: string; + minPopularity: number; + skipMissingDate: boolean; + skipMissingIsbn: boolean; + skipPartsAndSets: boolean; + skipSeriesSecondary: boolean; + allowedLanguages: string; + minPages: number; + ignored: string[]; +} + +export interface ReadarrBookOptions { + title: string; + qualityProfileId: number; + metadataProfileId: number; + tags: number[]; + profileId: number; + rootFolderPath: string; + hcId: number; + authorHcId: number; + monitored?: boolean; + searchNow?: boolean; +} + +export interface ReadarrBook { + id: number; + title: string; + authorTitle: string; + seriesTitle: string; + disambiguation: string; + authorId: number; + foreignBookId: string; + foreignEditionId: string; + titleSlug: string; + monitored: boolean; + anyEditionOk: boolean; + author?: object; + edition?: object; + ratings: { + votes: number; + value: number; + popularity: number; + }; + releaseDate: string; + pageCount: number; + genres: string[]; + images: { + url: string; + coverType: string; + extension: string; + }[]; + links: { + url: string; + name: string; + }[]; + added: string; + remoteCover: string; + grabbed: boolean; + statistics: { + bookFileCount: number; + bookCount: number; + totalBookCount: number; + sizeOnDisk: number; + percentOfBooks: number; + }; +} + +interface ReadarrEdition { + bookId: number; + foreignEditionId: string; + titleSlug: string; + asin: string; + title: string; + language: string; + overview: string; + format: string; + isEbook: boolean; + disambiguation: string; + publisher: string; + pageCount: number; + releaseDate: string; + images: { + url: string; + coverType: string; + extension: string; + }[]; + links: { + url: string; + name: string; + }[]; + ratings: { + votes: number; + value: number; + popularity: number; + }; + monitored: boolean; + manualAdd: boolean; + grabbed: boolean; + id: number; + isbn13?: string; +} + +class ReadarrAPI extends ServarrBase<{ bookId: number }> { + constructor({ url, apiKey }: { url: string; apiKey: string }) { + super({ url, apiKey, cacheName: 'readarr', apiName: 'Readarr' }); + } + + public getMetadataProfiles = async (): Promise => { + try { + const data = await this.getRolling( + `/metadataprofile`, + undefined, + 3600 + ); + + return data; + } catch (e) { + throw new Error( + `[Readarr] Failed to retrieve metadata profiles: ${e.message}`, + { cause: e } + ); + } + }; + + public getBooks = async (): Promise => { + try { + const data = await this.get('/book'); + + return data; + } catch (e) { + throw new Error(`[Readarr] Failed to retrieve books: ${e.message}`, { + cause: e, + }); + } + }; + + public getBook = async (id: number): Promise => { + try { + const data = await this.get(`/book/${id}`); + + return data; + } catch (e) { + throw new Error(`[Readarr] Failed to retrieve book: ${e.message}`, { + cause: e, + }); + } + }; + + public async getBookByHcId(id: number): Promise { + try { + const newBookId = id; + const response = await this.axios.get('/book/lookup', { + params: { term: `work:${newBookId}` }, + }); + const data = response.data; + + if (!data[0]) { + throw new Error('Book not found'); + } + + return data[0]; + } catch (e) { + logger.error('Error retrieving book by ID', { + label: 'Readarr API', + errorMessage: e.message, + hcId: id, + }); + throw new Error('Book not found', { cause: e }); + } + } + + public getEditions = async (id: number): Promise => { + try { + const response = await this.get(`/edition`, { + params: { bookId: id }, + }); + + return response; + } catch (e) { + throw new Error(`[Readarr] Failed to retrieve book: ${e.message}`, { + cause: e, + }); + } + }; + + public addBook = async ( + options: ReadarrBookOptions + ): Promise => { + try { + const newAuthorId = options.authorHcId.toString(); + const book = await this.getBookByHcId(options.hcId); + + if (book.grabbed) { + logger.info( + 'Book already exists and is available. Skipping add and returning success', + { + label: 'Readarr', + book: book, + } + ); + return book; + } + + // book exists in Readarr but is neither downloaded nor monitored + if (book.id && !book.monitored) { + const bookData = await this.getBook(book.id); + const editionData = await this.getEditions(book.id); + const response = await this.axios.put(`/book`, { + ...bookData, + monitored: true, + author: { + ...bookData.author, + qualityProfileId: options.qualityProfileId, + metadataProfileId: options.metadataProfileId, + tags: options.tags, + rootFolderPath: options.rootFolderPath, + }, + editions: editionData.map((edition) => ({ + ...edition, + monitored: edition.foreignEditionId === book.foreignEditionId, + })), + addOptions: { + searchForNewBook: options.searchNow, + }, + }); + const data = response.data; + + if (data.monitored) { + logger.info( + 'Found existing book in Readarr and set it to monitored.', + { + label: 'Readarr', + bookId: data.id, + bookTitle: data.title, + } + ); + logger.debug('Readarr update details', { + label: 'Readarr', + book: data, + }); + + if (options.searchNow) { + this.searchBook(data.id); + } + + return data; + } else { + logger.error('Failed to update existing book in Readarr.', { + label: 'Readarr', + options, + }); + throw new Error('Failed to update existing book in Readarr'); + } + } + + if (book.id) { + logger.info( + 'Book is already monitored in Readarr. Skipping add and returning success', + { label: 'Readarr' } + ); + if (options.searchNow) { + this.searchBook(book.id); + } + return book; + } + + const data = await this.post(`/book`, { + authorId: 0, + foreignBookId: book.foreignBookId, + monitored: true, + anyEditionOk: false, + author: { + authorMetadataId: 0, + foreignAuthorId: newAuthorId, + qualityProfileId: options.qualityProfileId, + metadataProfileId: options.metadataProfileId, + monitored: true, + monitorNewItems: 'none', + tags: options.tags, + addOptions: { + searchForMissingBooks: false, + booksToMonitor: [book.foreignBookId], + }, + rootFolderPath: options.rootFolderPath, + }, + editions: [ + { + bookId: 0, + foreignEditionId: book.foreignEditionId, + monitored: true, + manualAdd: false, + grabbed: false, + }, + ], + grabbed: false, + addOptions: { + searchForNewBook: options.searchNow, + }, + }); + + if (data.id) { + logger.info('Readarr accepted request', { label: 'Readarr' }); + logger.debug('Readarr add details', { + label: 'Readarr', + book: data, + }); + + if (options.searchNow) { + this.searchBook(data.id); + } + } else { + logger.error('Failed to add book to Readarr', { + label: 'Readarr', + options, + }); + throw new Error('Failed to add book to Readarr'); + } + return data; + } catch (e) { + logger.error( + 'Failed to add book to Readarr. This might happen if the book already exists, in which case you can safely ignore this error.', + { + label: 'Readarr', + errorMessage: e.message, + options, + response: e?.response?.data, + } + ); + throw new Error('Failed to add book to Readarr', { cause: e }); + } + }; + + public async searchBook(bookId: number): Promise { + logger.info('Executing book search command', { + label: 'Readarr API', + bookId: bookId, + }); + + try { + await this.runCommand('BookSearch', { bookIds: [bookId] }); + } catch (e) { + logger.error( + 'Something went wrong while executing Readarr book search.', + { + label: 'Readarr API', + errorMessage: e.message, + bookId: bookId, + } + ); + } + } + + public removeBook = async (bookId: number): Promise => { + try { + const { id, title } = await this.getBookByHcId(bookId); + await this.axios.delete(`/book/${id}`, { + params: { + deleteFiles: true, + addImportExclusion: false, + }, + }); + logger.info(`[Readarr] Removed book ${title}`); + } catch (e) { + throw new Error(`[Readarr] Failed to remove book: ${e.message}`, { + cause: e, + }); + } + }; + + public clearCache = ({ + tmdbId: hcId, + externalId, + }: { + tmdbId?: number | null; + externalId?: number | null; + }) => { + if (hcId) { + this.removeCache('/book/lookup', { + term: `work:${hcId}`, + }); + } + if (externalId) { + this.removeCache(`/book/${externalId}`); + } + }; +} + +export default ReadarrAPI; diff --git a/server/api/servarr/sonarr.test.ts b/server/api/servarr/sonarr.test.ts new file mode 100644 index 0000000000..015b57e900 --- /dev/null +++ b/server/api/servarr/sonarr.test.ts @@ -0,0 +1,119 @@ +import assert from 'node:assert/strict'; +import { afterEach, describe, it, mock } from 'node:test'; + +import type { AxiosInstance } from 'axios'; + +import SonarrAPI from '@server/api/servarr/sonarr'; + +function buildSonarr(): SonarrAPI { + return new SonarrAPI({ url: 'http://localhost:8989/api/v3', apiKey: 'test' }); +} + +function getAxios(sonarr: SonarrAPI): AxiosInstance { + return (sonarr as unknown as { axios: AxiosInstance }).axios; +} + +describe('SonarrAPI removeSeries', () => { + afterEach(() => mock.restoreAll()); + + it('removes the series when it exists in the library', async () => { + const sonarr = buildSonarr(); + mock.method(SonarrAPI.prototype, 'getSeriesByTvdbId', async () => ({ + id: 9, + title: 'Test Series', + })); + const del = mock.method(getAxios(sonarr), 'delete', async () => ({})); + + await sonarr.removeSeries(1234); + + assert.strictEqual(del.mock.callCount(), 1); + assert.strictEqual(del.mock.calls[0].arguments[0], '/series/9'); + }); + + it('does nothing when the series is not in the library', async () => { + const sonarr = buildSonarr(); + mock.method(getAxios(sonarr), 'get', async () => ({ + data: [{ id: 0, title: 'Breaking Bad' }], + })); + const del = mock.method(getAxios(sonarr), 'delete', async () => ({})); + + await assert.doesNotReject(() => sonarr.removeSeries(1234)); + assert.strictEqual(del.mock.callCount(), 0); + }); + + it('rejects when the tvdbId is unknown to the lookup', async () => { + const sonarr = buildSonarr(); + mock.method(getAxios(sonarr), 'get', async () => ({ data: [] })); + const del = mock.method(getAxios(sonarr), 'delete', async () => ({})); + + await assert.rejects(() => sonarr.removeSeries(1234), /Series not found/); + assert.strictEqual(del.mock.callCount(), 0); + }); + + it('ignores a 404 when the series was already removed in Sonarr', async () => { + const sonarr = buildSonarr(); + mock.method(SonarrAPI.prototype, 'getSeriesByTvdbId', async () => ({ + id: 9, + title: 'Test Series', + })); + mock.method(getAxios(sonarr), 'delete', async () => { + throw { response: { status: 404 } }; + }); + + await assert.doesNotReject(() => sonarr.removeSeries(1234)); + }); + + it('rethrows errors other than 404', async () => { + const sonarr = buildSonarr(); + mock.method(SonarrAPI.prototype, 'getSeriesByTvdbId', async () => ({ + id: 9, + title: 'Test Series', + })); + mock.method(getAxios(sonarr), 'delete', async () => { + throw { response: { status: 500 } }; + }); + + await assert.rejects(() => sonarr.removeSeries(1234)); + }); + + it('rethrows a 404 from the lookup instead of treating it as removed', async () => { + const sonarr = buildSonarr(); + mock.method(getAxios(sonarr), 'get', async () => { + throw { response: { status: 404 } }; + }); + const del = mock.method(getAxios(sonarr), 'delete', async () => ({})); + + await assert.rejects( + () => sonarr.removeSeries(1234), + (e: unknown) => + (e as { response?: { status?: number } }).response?.status === 404 + ); + assert.strictEqual(del.mock.callCount(), 0); + }); +}); + +describe('SonarrAPI getSeriesByTvdbId', () => { + afterEach(() => mock.restoreAll()); + + it('rethrows a 401 from the lookup with the status intact', async () => { + const sonarr = buildSonarr(); + mock.method(getAxios(sonarr), 'get', async () => { + throw { response: { status: 401 } }; + }); + + await assert.rejects( + () => sonarr.getSeriesByTvdbId(1234), + (e: unknown) => + (e as { response?: { status?: number } }).response?.status === 401 + ); + }); + + it('throws "Series not found" when the lookup returns no results', async () => { + const sonarr = buildSonarr(); + mock.method(getAxios(sonarr), 'get', async () => ({ data: [] })); + + await assert.rejects(() => sonarr.getSeriesByTvdbId(1234), { + message: 'Series not found', + }); + }); +}); diff --git a/server/api/servarr/sonarr.ts b/server/api/servarr/sonarr.ts index 33354e1cda..29679d0e51 100644 --- a/server/api/servarr/sonarr.ts +++ b/server/api/servarr/sonarr.ts @@ -1,4 +1,5 @@ import logger from '@server/logger'; +import type { AxiosResponse } from 'axios'; import ServarrBase from './base'; export interface SonarrSeason { @@ -129,6 +130,23 @@ class SonarrAPI extends ServarrBase<{ } } + public async getLibrarySeriesByTvdbId( + tvdbId: number + ): Promise { + try { + const response = await this.axios.get('/series', { + params: { tvdbId }, + }); + + return response.data; + } catch (e) { + throw new Error( + `[Sonarr] Failed to retrieve series by TVDB ID: ${e.message}`, + { cause: e } + ); + } + } + public async getSeriesById(id: number): Promise { try { const response = await this.axios.get(`/series/${id}`); @@ -166,26 +184,27 @@ class SonarrAPI extends ServarrBase<{ } public async getSeriesByTvdbId(id: number): Promise { + let response: AxiosResponse; try { - const response = await this.axios.get('/series/lookup', { + response = await this.axios.get('/series/lookup', { params: { term: `tvdb:${id}`, }, }); - - if (!response.data[0]) { - throw new Error('Series not found'); - } - - return response.data[0]; } catch (e) { logger.error('Error retrieving series by tvdb ID', { label: 'Sonarr API', errorMessage: e.message, tvdbId: id, }); - throw new Error('Series not found', { cause: e }); + throw e; + } + + if (!response.data[0]) { + throw new Error('Series not found'); } + + return response.data[0]; } public async addSeries(options: AddSeriesOptions): Promise { @@ -410,9 +429,17 @@ class SonarrAPI extends ServarrBase<{ return newSeasons; } - public removeSeries = async (serieId: number): Promise => { + public removeSeries = async (tvdbId: number): Promise => { + const { id, title } = await this.getSeriesByTvdbId(tvdbId); + + if (!id) { + logger.info(`[Sonarr] Series not in library, nothing to remove`, { + tvdbId, + }); + return; + } + try { - const { id, title } = await this.getSeriesByTvdbId(serieId); await this.axios.delete(`/series/${id}`, { params: { deleteFiles: true, @@ -421,9 +448,13 @@ class SonarrAPI extends ServarrBase<{ }); logger.info(`[Sonarr] Removed series ${title}`); } catch (e) { - throw new Error(`[Sonarr] Failed to remove series: ${e.message}`, { - cause: e, - }); + if (e?.response?.status === 404) { + logger.info(`[Sonarr] Series already removed from Sonarr`, { + tvdbId, + }); + return; + } + throw e; } }; diff --git a/server/api/tautulli.ts b/server/api/tautulli.ts index 977560a575..12da124d91 100644 --- a/server/api/tautulli.ts +++ b/server/api/tautulli.ts @@ -1,7 +1,8 @@ import type { User } from '@server/entity/User'; import type { TautulliSettings } from '@server/lib/settings'; import logger from '@server/logger'; -import { requestInterceptorFunction } from '@server/utils/customProxyAgent'; +import { proxyRequestInterceptor } from '@server/utils/customProxyAgent'; +import { userAgentRequestInterceptor } from '@server/utils/userAgent'; import type { AxiosInstance } from 'axios'; import axios from 'axios'; import { uniqWith } from 'lodash'; @@ -124,7 +125,8 @@ class TautulliAPI { }${settings.urlBase ?? ''}`, params: { apikey: settings.apiKey }, }); - this.axios.interceptors.request.use(requestInterceptorFunction); + this.axios.interceptors.request.use(proxyRequestInterceptor); + this.axios.interceptors.request.use(userAgentRequestInterceptor); } public async getInfo(): Promise { diff --git a/server/api/themoviedb/index.ts b/server/api/themoviedb/index.ts index 305d6048d7..4693b3e86e 100644 --- a/server/api/themoviedb/index.ts +++ b/server/api/themoviedb/index.ts @@ -1,5 +1,6 @@ import ExternalAPI from '@server/api/externalapi'; import type { TvShowProvider } from '@server/api/provider'; +import type { CacheStore } from '@server/lib/cache'; import cacheManager from '@server/lib/cache'; import { getSettings } from '@server/lib/settings'; import { sortBy } from 'lodash'; @@ -23,6 +24,7 @@ import type { TmdbSearchTvResponse, TmdbSeasonWithEpisodes, TmdbTvDetails, + TmdbTvScanDetails, TmdbUpcomingMoviesResponse, TmdbWatchProviderDetails, TmdbWatchProviderRegion, @@ -39,26 +41,38 @@ interface SingleSearchOptions extends SearchOptions { year?: number; } -export const SortOptionsIterable = [ +const CommonSortOptionsIterable = [ 'popularity.desc', 'popularity.asc', + 'vote_average.desc', + 'vote_average.asc', + 'vote_count.desc', + 'vote_count.asc', +] as const; + +export const MovieSortOptionsIterable = [ + ...CommonSortOptionsIterable, 'release_date.desc', 'release_date.asc', 'revenue.desc', 'revenue.asc', 'primary_release_date.desc', 'primary_release_date.asc', - 'original_title.asc', 'original_title.desc', - 'vote_average.desc', - 'vote_average.asc', - 'vote_count.desc', - 'vote_count.asc', + 'original_title.asc', +] as const; + +export const TvSortOptionsIterable = [ + ...CommonSortOptionsIterable, 'first_air_date.desc', 'first_air_date.asc', + 'original_name.desc', + 'original_name.asc', ] as const; -export type SortOptions = (typeof SortOptionsIterable)[number]; +export type MovieSortOptions = (typeof MovieSortOptionsIterable)[number]; +export type TvSortOptions = (typeof TvSortOptionsIterable)[number]; +export type SortOptions = MovieSortOptions | TvSortOptions; export interface TmdbCertificationResponse { certifications: { @@ -88,7 +102,7 @@ interface DiscoverMovieOptions { studio?: string; keywords?: string; excludeKeywords?: string; - sortBy?: SortOptions; + sortBy?: MovieSortOptions; watchRegion?: string; watchProviders?: string; certification?: string; @@ -114,7 +128,7 @@ interface DiscoverTvOptions { network?: number; keywords?: string; excludeKeywords?: string; - sortBy?: SortOptions; + sortBy?: TvSortOptions; watchRegion?: string; watchProviders?: string; withStatus?: string; // Returning Series: 0 Planned: 1 In Production: 2 Ended: 3 Cancelled: 4 Pilot: 5 @@ -124,7 +138,43 @@ interface DiscoverTvOptions { certificationCountry?: string; } +const TV_DETAILS_TTL = 43200; + +// Scan lookups read an entry once and never again. +const SCAN_TTL = 900; + +const TV_DETAILS_APPEND_TO_RESPONSE = + 'aggregate_credits,credits,external_ids,keywords,videos,content_ratings,watch/providers'; + +const TV_SCAN_APPEND_TO_RESPONSE = 'keywords,external_ids'; + +type ExternalIdLookup = + | { + externalId: string; + type: 'imdb'; + language?: string; + } + | { + externalId: number; + type: 'tvdb'; + language?: string; + }; + +// Nothing anywhere reads aggregate_credits.crew or credits.cast. +const stripUnreadTvCredits = (data: T): T => { + const show = data as { + aggregate_credits?: { crew?: unknown }; + credits?: { cast?: unknown }; + }; + + delete show.aggregate_credits?.crew; + delete show.credits?.cast; + + return data; +}; + class TheMovieDb extends ExternalAPI implements TvShowProvider { + private scanCache = cacheManager.getCache('tmdbscan').data; private locale: string; private discoverRegion?: string; private originalLanguage?: string; @@ -341,6 +391,37 @@ class TheMovieDb extends ExternalAPI implements TvShowProvider { } }; + // The append list is part of the cache key, so full and narrow entries differ. + private getTvShowDetails = async ({ + tvId, + language, + appendToResponse, + includeVideoLanguage, + ttl, + cache, + }: { + tvId: number; + language?: string; + appendToResponse: string; + includeVideoLanguage?: string; + ttl: number; + cache?: CacheStore; + }): Promise => + this.get( + `/tv/${tvId}`, + { + params: { + language, + append_to_response: appendToResponse, + ...(includeVideoLanguage + ? { include_video_language: includeVideoLanguage } + : {}), + }, + }, + ttl, + { cache, transform: stripUnreadTvCredits } + ); + public getTvShow = async ({ tvId, language = this.locale, @@ -349,35 +430,26 @@ class TheMovieDb extends ExternalAPI implements TvShowProvider { language?: string; }): Promise => { try { - const data = await this.get( - `/tv/${tvId}`, - { - params: { - language, - append_to_response: - 'aggregate_credits,credits,external_ids,keywords,videos,content_ratings,watch/providers', - include_video_language: language, - }, - }, - 43200 - ); + const data = await this.getTvShowDetails({ + tvId, + language, + appendToResponse: TV_DETAILS_APPEND_TO_RESPONSE, + includeVideoLanguage: language, + ttl: TV_DETAILS_TTL, + }); if ( (!language || !language.startsWith('en')) && !data.videos?.results?.some((video) => video.type === 'Trailer') ) { try { - const fallback = await this.get( - `/tv/${tvId}`, - { - params: { - language, - append_to_response: 'videos', - include_video_language: 'en', - }, - }, - 43200 - ); + const fallback = await this.getTvShowDetails({ + tvId, + language, + appendToResponse: 'videos', + includeVideoLanguage: 'en', + ttl: TV_DETAILS_TTL, + }); const localizedVideos = data.videos?.results ?? []; const localizedVideoKeys = new Set( @@ -408,6 +480,29 @@ class TheMovieDb extends ExternalAPI implements TvShowProvider { } }; + public getTvShowForScan = async ({ + tvId, + language = this.locale, + }: { + tvId: number; + language?: string; + }): Promise => { + try { + return await this.getTvShowDetails({ + tvId, + language, + appendToResponse: TV_SCAN_APPEND_TO_RESPONSE, + ttl: SCAN_TTL, + cache: this.scanCache, + }); + } catch (e) { + throw new Error( + `[TMDB] Failed to fetch TV show scan details: ${e.message}`, + { cause: e } + ); + } + }; + public getTvSeason = async ({ tvId, seasonNumber, @@ -871,33 +966,36 @@ class TheMovieDb extends ExternalAPI implements TvShowProvider { } }; - public async getByExternalId({ - externalId, - type, - language = this.locale, - }: - | { - externalId: string; - type: 'imdb'; - language?: string; - } - | { - externalId: number; - type: 'tvdb'; - language?: string; - }): Promise { + public async getByExternalId( + lookup: ExternalIdLookup + ): Promise { + return this.findByExternalId(lookup); + } + + public async getByExternalIdForScan( + lookup: ExternalIdLookup + ): Promise { + return this.findByExternalId(lookup, this.scanCache, SCAN_TTL); + } + + // Omitting ttl here silently falls back to DEFAULT_TTL, regardless of tier. + private async findByExternalId( + { externalId, type, language = this.locale }: ExternalIdLookup, + cache?: CacheStore, + ttl?: number + ): Promise { try { - const data = await this.get( + return await this.get( `/find/${externalId}`, { params: { external_source: type === 'imdb' ? 'imdb_id' : 'tvdb_id', language, }, - } + }, + ttl, + { cache } ); - - return data; } catch (e) { throw new Error(`[TMDB] Failed to find by external ID: ${e.message}`, { cause: e, @@ -905,35 +1003,31 @@ class TheMovieDb extends ExternalAPI implements TvShowProvider { } } - public async getMediaByImdbId({ + // A /find hit can be stale, so confirm the matched id still exists. + public async resolveImdbIdForScan({ imdbId, - language = this.locale, }: { imdbId: string; - language?: string; - }): Promise { + }): Promise { try { - const extResponse = await this.getByExternalId({ - externalId: imdbId, - type: 'imdb', - }); + const extResponse = await this.findByExternalId( + { externalId: imdbId, type: 'imdb' }, + this.scanCache, + SCAN_TTL + ); if (extResponse.movie_results[0]) { - const movie = await this.getMovie({ - movieId: extResponse.movie_results[0].id, - language, - }); - - return movie; + return await this.assertMovieExistsForScan( + extResponse.movie_results[0].id + ); } if (extResponse.tv_results[0]) { - const tvshow = await this.getTvShow({ + const tvShow = await this.getTvShowForScan({ tvId: extResponse.tv_results[0].id, - language, }); - return tvshow; + return tvShow.id; } throw new Error(`No movie or show returned from API for ID ${imdbId}`); @@ -945,6 +1039,35 @@ class TheMovieDb extends ExternalAPI implements TvShowProvider { } } + private async assertMovieExistsForScan(movieId: number): Promise { + const data = await this.get<{ id: number }>( + `/movie/${movieId}`, + {}, + SCAN_TTL, + { cache: this.scanCache } + ); + + return data.id; + } + + private async resolveTvdbId( + tvdbId: number, + cache?: CacheStore, + ttl?: number + ): Promise { + const extResponse = await this.findByExternalId( + { externalId: tvdbId, type: 'tvdb' }, + cache, + ttl + ); + + if (!extResponse.tv_results[0]) { + throw new Error(`No show returned from API for ID ${tvdbId}`); + } + + return extResponse.tv_results[0].id; + } + public async getShowByTvdbId({ tvdbId, language = this.locale, @@ -953,21 +1076,30 @@ class TheMovieDb extends ExternalAPI implements TvShowProvider { language?: string; }): Promise { try { - const extResponse = await this.getByExternalId({ - externalId: tvdbId, - type: 'tvdb', + return await this.getTvShow({ + tvId: await this.resolveTvdbId(tvdbId), + language, }); + } catch (e) { + throw new Error( + `[TMDB] Failed to get TV show using the external TVDB ID: ${e.message}`, + { cause: e } + ); + } + } - if (extResponse.tv_results[0]) { - const tvshow = await this.getTvShow({ - tvId: extResponse.tv_results[0].id, - language, - }); - - return tvshow; - } - - throw new Error(`No show returned from API for ID ${tvdbId}`); + public async getShowByTvdbIdForScan({ + tvdbId, + language = this.locale, + }: { + tvdbId: number; + language?: string; + }): Promise { + try { + return await this.getTvShowForScan({ + tvId: await this.resolveTvdbId(tvdbId, this.scanCache, SCAN_TTL), + language, + }); } catch (e) { throw new Error( `[TMDB] Failed to get TV show using the external TVDB ID: ${e.message}`, diff --git a/server/api/themoviedb/interfaces.ts b/server/api/themoviedb/interfaces.ts index a4c49e9ab0..cd68ccc70b 100644 --- a/server/api/themoviedb/interfaces.ts +++ b/server/api/themoviedb/interfaces.ts @@ -304,6 +304,16 @@ export interface TmdbTvDetails { }; } +export interface TmdbTvScanDetails { + id: number; + name: string; + seasons: TmdbTvSeasonResult[]; + external_ids: TmdbExternalIds; + keywords: { + results: TmdbKeyword[]; + }; +} + export interface TmdbVideoResult { results: TmdbVideo[]; } diff --git a/server/constants/discord.ts b/server/constants/discord.ts new file mode 100644 index 0000000000..c6b7fc1ca1 --- /dev/null +++ b/server/constants/discord.ts @@ -0,0 +1,27 @@ +export const DISCORD_SNOWFLAKE_REGEX = /^\d{17,20}$/; + +export enum EmbedColors { + DEFAULT = 0, + AQUA = 1752220, + GREEN = 3066993, + BLUE = 3447003, + PURPLE = 10181046, + GOLD = 15844367, + ORANGE = 15105570, + RED = 15158332, + GREY = 9807270, + DARKER_GREY = 8359053, + NAVY = 3426654, + DARK_AQUA = 1146986, + DARK_GREEN = 2067276, + DARK_BLUE = 2123412, + DARK_PURPLE = 7419530, + DARK_GOLD = 12745742, + DARK_ORANGE = 11027200, + DARK_RED = 10038562, + DARK_GREY = 9936031, + LIGHT_GREY = 12370112, + DARK_NAVY = 2899536, + LUMINOUS_VIVID_PINK = 16580705, + DARK_VIVID_PINK = 12320855, +} diff --git a/server/constants/discover.ts b/server/constants/discover.ts index fda0682243..2d431ef5ef 100644 --- a/server/constants/discover.ts +++ b/server/constants/discover.ts @@ -22,6 +22,7 @@ export enum DiscoverSliderType { TMDB_NETWORK, TMDB_MOVIE_STREAMING_SERVICES, TMDB_TV_STREAMING_SERVICES, + POPULAR_BOOKS, } export const defaultSliders: Partial[] = [ @@ -97,4 +98,10 @@ export const defaultSliders: Partial[] = [ isBuiltIn: true, order: 11, }, + { + type: DiscoverSliderType.POPULAR_BOOKS, + enabled: false, + isBuiltIn: true, + order: 12, + }, ]; diff --git a/server/constants/error.ts b/server/constants/error.ts index daa02f1a1c..54640b206b 100644 --- a/server/constants/error.ts +++ b/server/constants/error.ts @@ -5,6 +5,7 @@ export enum ApiErrorCode { InvalidEmail = 'INVALID_EMAIL', NotAdmin = 'NOT_ADMIN', NoAdminUser = 'NO_ADMIN_USER', + ConnectionError = 'CONNECTION_ERROR', SyncErrorGroupedFolders = 'SYNC_ERROR_GROUPED_FOLDERS', SyncErrorNoLibraries = 'SYNC_ERROR_NO_LIBRARIES', Unauthorized = 'UNAUTHORIZED', diff --git a/server/constants/media.ts b/server/constants/media.ts index 170109fb5c..da3c430d51 100644 --- a/server/constants/media.ts +++ b/server/constants/media.ts @@ -9,8 +9,18 @@ export enum MediaRequestStatus { export enum MediaType { MOVIE = 'movie', TV = 'tv', + BOOK = 'book', } +export const MEDIA_IDENTIFIER_TYPES = { + [MediaType.MOVIE]: 'tmdb', + [MediaType.TV]: 'tmdb', + [MediaType.BOOK]: 'hardcover', +} as const; + +export type IdentifierType = + (typeof MEDIA_IDENTIFIER_TYPES)[keyof typeof MEDIA_IDENTIFIER_TYPES]; + export enum MediaStatus { UNKNOWN = 1, PENDING, diff --git a/server/datasource.ts b/server/datasource.ts index 699c0bfeb7..79e84d9737 100644 --- a/server/datasource.ts +++ b/server/datasource.ts @@ -1,3 +1,22 @@ +import { Blocklist } from '@server/entity/Blocklist'; +import DiscoverSlider from '@server/entity/DiscoverSlider'; +import Issue from '@server/entity/Issue'; +import IssueComment from '@server/entity/IssueComment'; +import Media from '@server/entity/Media'; +import { MediaRequest } from '@server/entity/MediaRequest'; +import OverrideRule from '@server/entity/OverrideRule'; +import Season from '@server/entity/Season'; +import SeasonRequest from '@server/entity/SeasonRequest'; +import { Session } from '@server/entity/Session'; +import { User } from '@server/entity/User'; +import { UserPushSubscription } from '@server/entity/UserPushSubscription'; +import { UserSettings } from '@server/entity/UserSettings'; +import { Watchlist } from '@server/entity/Watchlist'; +import { IssueCommentSubscriber } from '@server/subscriber/IssueCommentSubscriber'; +import { IssueSubscriber } from '@server/subscriber/IssueSubscriber'; +import { MediaRequestSubscriber } from '@server/subscriber/MediaRequestSubscriber'; +import { MediaSubscriber } from '@server/subscriber/MediaSubscriber'; +import { isPgsql } from '@server/utils/dbType'; import fs from 'fs'; import type { TlsOptions } from 'tls'; import type { DataSourceOptions, EntityTarget, Repository } from 'typeorm'; @@ -5,6 +24,30 @@ import { DataSource } from 'typeorm'; const DB_SSL_PREFIX = 'DB_SSL_'; +const entities = [ + Blocklist, + DiscoverSlider, + Issue, + IssueComment, + Media, + MediaRequest, + OverrideRule, + Season, + SeasonRequest, + Session, + User, + UserPushSubscription, + UserSettings, + Watchlist, +]; + +const subscribers = [ + IssueCommentSubscriber, + IssueSubscriber, + MediaRequestSubscriber, + MediaSubscriber, +]; + function boolFromEnv(envVar: string, defaultVal = false) { if (process.env[envVar]) { return process.env[envVar]?.toLowerCase() === 'true'; @@ -53,9 +96,9 @@ const testConfig: DataSourceOptions = { synchronize: true, dropSchema: true, logging: boolFromEnv('DB_LOG_QUERIES'), - entities: ['server/entity/**/*.ts'], + entities, migrations: ['server/migration/sqlite/**/*.ts'], - subscribers: ['server/subscriber/**/*.ts'], + subscribers, }; const devConfig: DataSourceOptions = { @@ -67,9 +110,9 @@ const devConfig: DataSourceOptions = { migrationsRun: false, logging: boolFromEnv('DB_LOG_QUERIES'), enableWAL: true, - entities: ['server/entity/**/*.ts'], + entities, migrations: ['server/migration/sqlite/**/*.ts'], - subscribers: ['server/subscriber/**/*.ts'], + subscribers, }; const prodConfig: DataSourceOptions = { @@ -81,9 +124,9 @@ const prodConfig: DataSourceOptions = { migrationsRun: false, logging: boolFromEnv('DB_LOG_QUERIES'), enableWAL: true, - entities: ['dist/entity/**/*.js'], + entities, migrations: ['dist/migration/sqlite/**/*.js'], - subscribers: ['dist/subscriber/**/*.js'], + subscribers, }; const postgresDevConfig: DataSourceOptions = { @@ -97,12 +140,14 @@ const postgresDevConfig: DataSourceOptions = { database: process.env.DB_NAME ?? 'seerr', ssl: buildSslConfig(), poolSize: intFromEnv('DB_POOL_SIZE'), + // Bounds pool acquisition waits so exhaustion surfaces as errors instead of a silent hang + connectTimeoutMS: intFromEnv('DB_CONNECT_TIMEOUT_MS', 30000), synchronize: false, - migrationsRun: true, + migrationsRun: false, logging: boolFromEnv('DB_LOG_QUERIES'), - entities: ['server/entity/**/*.ts'], + entities, migrations: ['server/migration/postgres/**/*.ts'], - subscribers: ['server/subscriber/**/*.ts'], + subscribers, }; const postgresProdConfig: DataSourceOptions = { @@ -116,16 +161,16 @@ const postgresProdConfig: DataSourceOptions = { database: process.env.DB_NAME ?? 'seerr', ssl: buildSslConfig(), poolSize: intFromEnv('DB_POOL_SIZE'), + // Bounds pool acquisition waits so exhaustion surfaces as errors instead of a silent hang + connectTimeoutMS: intFromEnv('DB_CONNECT_TIMEOUT_MS', 30000), synchronize: false, migrationsRun: false, logging: boolFromEnv('DB_LOG_QUERIES'), - entities: ['dist/entity/**/*.js'], + entities, migrations: ['dist/migration/postgres/**/*.js'], - subscribers: ['dist/subscriber/**/*.js'], + subscribers, }; -export const isPgsql = process.env.DB_TYPE === 'postgres'; - function getDataSource(): DataSourceOptions { if (process.env.NODE_ENV === 'test') { return testConfig; diff --git a/server/entity/Media.ts b/server/entity/Media.ts index a63003df66..cd22faba87 100644 --- a/server/entity/Media.ts +++ b/server/entity/Media.ts @@ -1,6 +1,11 @@ import RadarrAPI from '@server/api/servarr/radarr'; +import ReadarrAPI from '@server/api/servarr/readarr'; import SonarrAPI from '@server/api/servarr/sonarr'; -import { MediaStatus, MediaType } from '@server/constants/media'; +import { + MediaRequestStatus, + MediaStatus, + MediaType, +} from '@server/constants/media'; import { MediaServerType } from '@server/constants/server'; import { getRepository } from '@server/datasource'; import { Blocklist } from '@server/entity/Blocklist'; @@ -8,6 +13,7 @@ import type { User } from '@server/entity/User'; import { Watchlist } from '@server/entity/Watchlist'; import type { DownloadingItem } from '@server/lib/downloadtracker'; import downloadTracker from '@server/lib/downloadtracker'; +import { Permission } from '@server/lib/permissions'; import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; import { DbAwareColumn, resolveDbType } from '@server/utils/DbColumnHelper'; @@ -31,7 +37,8 @@ import Season from './Season'; class Media { public static async getRelatedMedia( user: User | undefined, - items: { tmdbId: number; mediaType: string }[] + items: { tmdbId: number; mediaType: string }[], + { includeActiveRequest = false }: { includeActiveRequest?: boolean } = {} ): Promise { const mediaRepository = getRepository(Media); @@ -49,13 +56,41 @@ class Media { 'watchlist', 'media.id= watchlist.media and watchlist.requestedBy = :userId', { userId: user?.id } - ) //, + ) + .leftJoinAndSelect('media.requests', 'requests') .where(' media.tmdbId in (:...finalIds)', { finalIds }) .getMany(); - return media.filter((m) => + const relatedMedia = media.filter((m) => items.some((i) => i.tmdbId === m.tmdbId && i.mediaType === m.mediaType) ); + + if ( + includeActiveRequest && + getSettings().main.hideRequested && + relatedMedia.length > 0 + ) { + const activeRequestMediaIds = await mediaRepository + .createQueryBuilder('media') + .select('media.id', 'id') + .distinct(true) + .innerJoin('media.requests', 'request') + .where('media.id IN (:...mediaIds)', { + mediaIds: relatedMedia.map((m) => m.id), + }) + .andWhere('request.status IN (:...statuses)', { + statuses: [MediaRequestStatus.PENDING, MediaRequestStatus.APPROVED], + }) + .getRawMany<{ id: number }>(); + + const activeIds = new Set(activeRequestMediaIds.map((row) => row.id)); + + relatedMedia.forEach((m) => { + m.hasActiveRequest = activeIds.has(m.id); + }); + } + + return relatedMedia; } catch (e) { logger.error(e.message); return []; @@ -187,6 +222,7 @@ class Media { public serviceUrl?: string; public serviceUrl4k?: string; + public hasActiveRequest?: boolean; public downloadStatus?: DownloadingItem[] = []; public downloadStatus4k?: DownloadingItem[] = []; @@ -203,17 +239,21 @@ class Media { Object.assign(this, init); } - public resetServiceData(): void { - this.serviceId = null; - this.serviceId4k = null; - this.externalServiceId = null; - this.externalServiceId4k = null; - this.externalServiceSlug = null; - this.externalServiceSlug4k = null; - this.ratingKey = null; - this.ratingKey4k = null; - this.jellyfinMediaId = null; - this.jellyfinMediaId4k = null; + public resetServiceData(is4k?: boolean): void { + if (is4k === undefined || !is4k) { + this.serviceId = null; + this.externalServiceId = null; + this.externalServiceSlug = null; + this.ratingKey = null; + this.jellyfinMediaId = null; + } + if (is4k === undefined || is4k) { + this.serviceId4k = null; + this.externalServiceId4k = null; + this.externalServiceSlug4k = null; + this.ratingKey4k = null; + this.jellyfinMediaId4k = null; + } } @AfterLoad() @@ -332,6 +372,34 @@ class Media { } } } + + if (this.mediaType === MediaType.BOOK) { + if (this.serviceId !== null && this.externalServiceId !== null) { + const settings = getSettings(); + const server = settings.readarr.find( + (readarr) => readarr.id === this.serviceId + ); + + if (server) { + this.serviceUrl = server.externalUrl + ? `${server.externalUrl}/book/${this.externalServiceId}` + : ReadarrAPI.buildUrl(server, `/book/${this.externalServiceId}`); + } + } + + if (this.serviceId4k !== null && this.externalServiceId4k !== null) { + const settings = getSettings(); + const server = settings.readarr.find( + (readarr) => readarr.id === this.serviceId4k + ); + + if (server) { + this.serviceUrl4k = server.externalUrl + ? `${server.externalUrl}/book/${this.externalServiceId4k}` + : ReadarrAPI.buildUrl(server, `/book/${this.externalServiceId4k}`); + } + } + } } @AfterLoad() @@ -387,6 +455,64 @@ class Media { ); } } + + if (this.mediaType === MediaType.BOOK) { + if ( + this.externalServiceId !== undefined && + this.externalServiceId !== null && + this.serviceId !== undefined && + this.serviceId !== null + ) { + this.downloadStatus = downloadTracker.getBookProgress( + this.serviceId, + this.externalServiceId + ); + } + + if ( + this.externalServiceId4k !== undefined && + this.externalServiceId4k !== null && + this.serviceId4k !== undefined && + this.serviceId4k !== null + ) { + this.downloadStatus4k = downloadTracker.getBookProgress( + this.serviceId4k, + this.externalServiceId4k + ); + } + } + } + + public filter(user?: User): Media { + const canViewIssues = + user?.hasPermission( + [ + Permission.MANAGE_ISSUES, + Permission.VIEW_ISSUES, + Permission.CREATE_ISSUES, + ], + { type: 'or' } + ) ?? false; + + return { + ...this, + requests: (this.requests ?? []).map((request) => ({ + ...request, + requestedBy: request.requestedBy?.filter(), + modifiedBy: request.modifiedBy?.filter(), + })), + // the detail pages call issues.filter() without a null check + issues: canViewIssues + ? (this.issues ?? []).map( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + ({ comments, problemSeason, problemEpisode, ...issue }) => ({ + ...issue, + createdBy: issue.createdBy?.filter(), + modifiedBy: issue.modifiedBy?.filter(), + }) + ) + : [], + } as Media; } } diff --git a/server/entity/MediaRequest.test.ts b/server/entity/MediaRequest.test.ts new file mode 100644 index 0000000000..27a70b7de2 --- /dev/null +++ b/server/entity/MediaRequest.test.ts @@ -0,0 +1,205 @@ +import assert from 'node:assert/strict'; +import { beforeEach, describe, it, mock } from 'node:test'; + +import ExternalAPI from '@server/api/externalapi'; +import { MediaType } from '@server/constants/media'; +import { getRepository } from '@server/datasource'; +import Media from '@server/entity/Media'; +import { + DuplicateMediaRequestError, + MediaRequest, + QuotaRestrictedError, +} from '@server/entity/MediaRequest'; +import SeasonRequest from '@server/entity/SeasonRequest'; +import { User } from '@server/entity/User'; +import { Permission } from '@server/lib/permissions'; +import { setupTestDb } from '@server/test/db'; + +// get is a prototype method unlike getMovie, and replaces the cache lookup too +const externalApiGetMock = mock.method( + ExternalAPI.prototype as unknown as { + get: (endpoint: string) => Promise; + }, + 'get', + async (endpoint: string) => { + const tmdbId = Number(endpoint.replace(/^\/(movie|tv)\//, '')); + + if (!tmdbId) { + throw new Error(`Unstubbed external endpoint: ${endpoint}`); + } + + return { + id: tmdbId, + external_ids: {}, + seasons: [1, 2, 3].map((season_number) => ({ season_number })), + // Skips getMovie's localized fallback call + videos: { results: [{ type: 'Trailer', key: 'trailer' }] }, + }; + } +).mock; + +mock.method(MediaRequest, 'sendNotification', async () => undefined); + +setupTestDb(); + +beforeEach(() => { + externalApiGetMock.resetCalls(); +}); + +async function seedRequester(movieQuotaLimit: number): Promise { + const userRepository = getRepository(User); + + const requester = await userRepository.findOneOrFail({ + where: { email: 'demo@seerr.dev' }, + }); + requester.movieQuotaLimit = movieQuotaLimit; + + return userRepository.save(requester); +} + +async function createRequester( + email: string, + permissions = Permission.REQUEST +): Promise { + return getRepository(User).save(new User({ email, permissions, avatar: '' })); +} + +function requestMovies(mediaIds: number[], requester: User) { + return Promise.allSettled( + mediaIds.map((mediaId) => + MediaRequest.request( + { mediaId, mediaType: MediaType.MOVIE, is4k: false }, + requester + ) + ) + ); +} + +function rejections(results: PromiseSettledResult[]) { + return results.filter( + (result): result is PromiseRejectedResult => result.status === 'rejected' + ); +} + +describe('MediaRequest.request', () => { + it('rejects the second of two concurrent requests at the movie quota', async () => { + const requestRepository = getRepository(MediaRequest); + const requester = await seedRequester(1); + + const results = await requestMovies([11111, 22222], requester); + const rejected = rejections(results); + + assert.strictEqual(rejected.length, 1); + assert.ok(rejected[0].reason instanceof QuotaRestrictedError); + assert.strictEqual(await requestRepository.count(), 1); + assert.strictEqual(externalApiGetMock.callCount(), 1); + }); + + it('rejects a concurrent duplicate request for the same movie', async () => { + const requestRepository = getRepository(MediaRequest); + const requester = await seedRequester(5); + + const results = await requestMovies([33333, 33333], requester); + const rejected = rejections(results); + + assert.strictEqual(rejected.length, 1); + assert.ok(rejected[0].reason instanceof DuplicateMediaRequestError); + assert.strictEqual(await requestRepository.count(), 1); + assert.strictEqual(externalApiGetMock.callCount(), 2); + }); + + it('rejects a duplicate request that omits is4k', async () => { + const requestRepository = getRepository(MediaRequest); + const requester = await seedRequester(5); + const body = { mediaId: 66666, mediaType: MediaType.MOVIE }; + + await MediaRequest.request(body, requester); + + await assert.rejects( + () => MediaRequest.request(body, requester), + DuplicateMediaRequestError + ); + assert.strictEqual(await requestRepository.count(), 1); + }); + + it('rejects a concurrent duplicate request from a different user', async () => { + const requestRepository = getRepository(MediaRequest); + const requester = await seedRequester(5); + const otherRequester = await createRequester('second@seerr.dev'); + + const results = await Promise.allSettled( + [requester, otherRequester].map((user) => + MediaRequest.request( + { mediaId: 44444, mediaType: MediaType.MOVIE, is4k: false }, + user + ) + ) + ); + const rejected = rejections(results); + + assert.strictEqual(rejected.length, 1); + assert.ok(rejected[0].reason instanceof DuplicateMediaRequestError); + assert.strictEqual(await requestRepository.count(), 1); + }); + + it('gives an overlapping season to only one of two concurrent users', async () => { + const seasonRequestRepository = getRepository(SeasonRequest); + const requester = await seedRequester(5); + const otherRequester = await createRequester('second@seerr.dev'); + + const results = await Promise.allSettled( + [ + [requester, [1, 2]], + [otherRequester, [2, 3]], + ].map(([user, seasons]) => + MediaRequest.request( + { + mediaId: 55555, + mediaType: MediaType.TV, + seasons: seasons as number[], + is4k: false, + }, + user as User + ) + ) + ); + + assert.strictEqual(rejections(results).length, 0); + assert.strictEqual( + await seasonRequestRepository.count({ where: { seasonNumber: 2 } }), + 1 + ); + assert.strictEqual(await seasonRequestRepository.count(), 3); + }); + + it('creates one media row for concurrent 4k and non-4k requests', async () => { + const mediaRepository = getRepository(Media); + const requestRepository = getRepository(MediaRequest); + const requester = await seedRequester(5); + const otherRequester = await createRequester( + 'second@seerr.dev', + Permission.REQUEST_4K + ); + + const results = await Promise.allSettled( + [ + [requester, false], + [otherRequester, true], + ].map(([user, is4k]) => + MediaRequest.request( + { mediaId: 88888, mediaType: MediaType.MOVIE, is4k: is4k as boolean }, + user as User + ) + ) + ); + + assert.strictEqual(rejections(results).length, 0); + assert.strictEqual(await requestRepository.count(), 2); + assert.strictEqual( + await mediaRepository.count({ + where: { tmdbId: 88888, mediaType: MediaType.MOVIE }, + }), + 1 + ); + }); +}); diff --git a/server/entity/MediaRequest.ts b/server/entity/MediaRequest.ts index 0252799220..969a2c8438 100644 --- a/server/entity/MediaRequest.ts +++ b/server/entity/MediaRequest.ts @@ -1,19 +1,24 @@ +import Hardcover from '@server/api/hardcover'; import TheMovieDb from '@server/api/themoviedb'; -import { ANIME_KEYWORD_ID } from '@server/api/themoviedb/constants'; -import type { TmdbKeyword } from '@server/api/themoviedb/interfaces'; import { MediaRequestStatus, MediaStatus, MediaType, } from '@server/constants/media'; import { getRepository } from '@server/datasource'; -import OverrideRule from '@server/entity/OverrideRule'; import type { MediaRequestBody } from '@server/interfaces/api/requestInterfaces'; import notificationManager, { Notification } from '@server/lib/notifications'; +import overrideRules from '@server/lib/overrideRules'; import { Permission } from '@server/lib/permissions'; import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; import { DbAwareColumn, resolveDbType } from '@server/utils/DbColumnHelper'; +import requestLock, { + mediaKey, + mediaLock, + userKey, +} from '@server/utils/requestLock'; +import { isBookDetails } from '@server/utils/typeHelpers'; import { truncate } from 'lodash'; import { AfterInsert, @@ -48,8 +53,33 @@ export class MediaRequest { requestBody: MediaRequestBody, user: User, options: MediaRequestOptions = {} + ): Promise { + // is4k is optional, and an undefined one binds as null in the duplicate query + const body = { ...requestBody, is4k: !!requestBody.is4k }; + + // Only a caller allowed to set the request user may queue on their lock + const lockUserId = + body.userId && + user.hasPermission([Permission.MANAGE_USERS, Permission.MANAGE_REQUESTS]) + ? body.userId + : user.id; + + // No is4k in the key: one media row holds both statuses, so a 4k and a + // non-4k request for the same title race to create it + return requestLock.dispatch(userKey(lockUserId), () => + mediaLock.dispatch(mediaKey(body.mediaType, body.mediaId), () => + MediaRequest.createRequest(body, user, options) + ) + ); + } + + private static async createRequest( + requestBody: MediaRequestBody, + user: User, + options: MediaRequestOptions ): Promise { const tmdb = new TheMovieDb(); + const hardcover = new Hardcover(); const mediaRepository = getRepository(Media); const requestRepository = getRepository(MediaRequest); const userRepository = getRepository(User); @@ -109,20 +139,65 @@ export class MediaRequest { requestBody.is4k ? '4K ' : '' }series requests.` ); + } else if ( + requestBody.mediaType === MediaType.BOOK && + !requestUser.hasPermission( + requestBody.is4k + ? [Permission.REQUEST_4K, Permission.REQUEST_AUDIO_BOOK] + : [Permission.REQUEST, Permission.REQUEST_BOOK], + { + type: 'or', + } + ) + ) { + throw new RequestPermissionError( + `You do not have permission to make ${ + requestBody.is4k ? 'audio' : '' + }book requests.` + ); } const quotas = await requestUser.getQuota(); - if (requestBody.mediaType === MediaType.MOVIE && quotas.movie.restricted) { - throw new QuotaRestrictedError('Movie Quota exceeded.'); - } else if (requestBody.mediaType === MediaType.TV && quotas.tv.restricted) { - throw new QuotaRestrictedError('Series Quota exceeded.'); + const canBypassQuota = user.hasPermission(Permission.MANAGE_REQUESTS); + const ignoreQuota = + requestBody.ignoreQuota === true && + canBypassQuota && + ((requestBody.mediaType === MediaType.MOVIE + ? quotas.movie.limit + : requestBody.mediaType === MediaType.BOOK + ? quotas.book.limit + : quotas.tv.limit) ?? 0) > 0; + + if (!ignoreQuota) { + if (requestBody.ignoreQuota && !canBypassQuota) { + throw new RequestPermissionError( + 'You do not have permission to bypass user quota limits.' + ); + } else if ( + requestBody.mediaType === MediaType.MOVIE && + quotas.movie.restricted + ) { + throw new QuotaRestrictedError('Movie Quota exceeded.'); + } else if ( + requestBody.mediaType === MediaType.TV && + quotas.tv.restricted + ) { + throw new QuotaRestrictedError('Series Quota exceeded.'); + } else if ( + requestBody.mediaType === MediaType.BOOK && + quotas.book.restricted + ) { + throw new QuotaRestrictedError('Book Quota exceeded.'); + } } const tmdbMedia = requestBody.mediaType === MediaType.MOVIE ? await tmdb.getMovie({ movieId: requestBody.mediaId }) - : await tmdb.getTvShow({ tvId: requestBody.mediaId }); + : requestBody.mediaType === MediaType.BOOK + ? await hardcover.getBook(requestBody.mediaId) + : await tmdb.getTvShow({ tvId: requestBody.mediaId }); let media = await mediaRepository.findOne({ where: { @@ -135,7 +210,9 @@ export class MediaRequest { if (!media) { media = new Media({ tmdbId: tmdbMedia.id, - tvdbId: requestBody.tvdbId ?? tmdbMedia.external_ids.tvdb_id, + ...(!isBookDetails(tmdbMedia) && { + tvdbId: requestBody.tvdbId ?? tmdbMedia.external_ids.tvdb_id, + }), status: !requestBody.is4k ? MediaStatus.PENDING : MediaStatus.UNKNOWN, status4k: requestBody.is4k ? MediaStatus.PENDING : MediaStatus.UNKNOWN, mediaType: requestBody.mediaType, @@ -151,18 +228,26 @@ export class MediaRequest { throw new BlocklistedMediaError('This media is blocklisted.'); } - if (media.status === MediaStatus.UNKNOWN && !requestBody.is4k) { + if ( + (media.status === MediaStatus.UNKNOWN || + media.status === MediaStatus.DELETED) && + !requestBody.is4k + ) { media.status = MediaStatus.PENDING; } - if (media.status4k === MediaStatus.UNKNOWN && requestBody.is4k) { + if ( + (media.status4k === MediaStatus.UNKNOWN || + media.status4k === MediaStatus.DELETED) && + requestBody.is4k + ) { media.status4k = MediaStatus.PENDING; } } const existing = await requestRepository .createQueryBuilder('request') - .leftJoin('request.media', 'media') + .leftJoinAndSelect('request.media', 'media') .leftJoinAndSelect('request.requestedBy', 'user') .where('request.is4k = :is4k', { is4k: requestBody.is4k }) .andWhere('media.tmdbId = :tmdbId', { tmdbId: tmdbMedia.id }) @@ -172,9 +257,10 @@ export class MediaRequest { .getMany(); if (existing && existing.length > 0) { - // If there is an existing movie request that isn't declined, don't allow a new one. + // If there is an existing movie/book request that isn't declined, don't allow a new one. if ( - requestBody.mediaType === MediaType.MOVIE && + (requestBody.mediaType === MediaType.MOVIE || + requestBody.mediaType === MediaType.BOOK) && existing[0].status !== MediaRequestStatus.DECLINED && existing[0].status !== MediaRequestStatus.COMPLETED ) { @@ -192,9 +278,13 @@ export class MediaRequest { // If an existing auto-request for this media exists from the same user, // don't allow a new one. + const statusKey = requestBody.is4k ? 'status4k' : 'status'; if ( existing.find( - (r) => r.requestedBy.id === requestUser.id && r.isAutoRequest + (r) => + r.requestedBy.id === requestUser.id && + r.isAutoRequest && + r.media?.[statusKey] !== MediaStatus.DELETED ) ) { throw new DuplicateMediaRequestError( @@ -203,130 +293,50 @@ export class MediaRequest { } } - // Apply overrides if the user is not an admin or has the "advanced request" permission - const useOverrides = !user.hasPermission([Permission.MANAGE_REQUESTS], { - type: 'or', - }); - let rootFolder = requestBody.rootFolder; let profileId = requestBody.profileId; let tags = requestBody.tags; + const metadataProfileId = requestBody.metadataProfileId; - if (useOverrides) { - const defaultRadarrId = requestBody.is4k - ? settings.radarr.findIndex((r) => r.is4k && r.isDefault) - : settings.radarr.findIndex((r) => !r.is4k && r.isDefault); - const defaultSonarrId = requestBody.is4k - ? settings.sonarr.findIndex((s) => s.is4k && s.isDefault) - : settings.sonarr.findIndex((s) => !s.is4k && s.isDefault); - - const overrideRuleRepository = getRepository(OverrideRule); - const overrideRules = await overrideRuleRepository.find({ - where: - requestBody.mediaType === MediaType.MOVIE - ? { radarrServiceId: defaultRadarrId } - : { sonarrServiceId: defaultSonarrId }, - }); - - const appliedOverrideRules = overrideRules.filter((rule) => { - const hasAnimeKeyword = - 'results' in tmdbMedia.keywords && - tmdbMedia.keywords.results.some( - (keyword: TmdbKeyword) => keyword.id === ANIME_KEYWORD_ID - ); - - // Skip override rules if the media is an anime TV show as anime TV - // is handled by default and override rules do not explicitly include - // the anime keyword - if ( - requestBody.mediaType === MediaType.TV && - hasAnimeKeyword && - (!rule.keywords || - !rule.keywords.split(',').map(Number).includes(ANIME_KEYWORD_ID)) - ) { - return false; - } - - if ( - rule.users && - !rule.users - .split(',') - .some((userId) => Number(userId) === requestUser.id) - ) { - return false; - } - if ( - rule.genre && - !rule.genre - .split(',') - .some((genreId) => - tmdbMedia.genres - .map((genre) => genre.id) - .includes(Number(genreId)) - ) - ) { - return false; - } - if ( - rule.language && - !rule.language - .split('|') - .some((languageId) => languageId === tmdbMedia.original_language) - ) { - return false; - } - if ( - rule.keywords && - !rule.keywords.split(',').some((keywordId) => { - let keywordList: TmdbKeyword[] = []; - - if ('keywords' in tmdbMedia.keywords) { - keywordList = tmdbMedia.keywords.keywords; - } else if ('results' in tmdbMedia.keywords) { - keywordList = tmdbMedia.keywords.results; - } - - return keywordList - .map((keyword: TmdbKeyword) => keyword.id) - .includes(Number(keywordId)); - }) - ) { - return false; - } - return true; + // Override rules match TMDB genre, language and keywords, which a book + // does not have, so none has ever applied to one + if (!isBookDetails(tmdbMedia)) { + const ruleResult = await overrideRules({ + mediaType: requestBody.mediaType, + is4k: requestBody.is4k || false, + tmdbMedia, + requestUser, + tags, }); - - // hacky way to prioritize rules - // TODO: make this better - const prioritizedRule = appliedOverrideRules.sort((a, b) => { - const keys: (keyof OverrideRule)[] = ['genre', 'language', 'keywords']; - - const aSpecificity = keys.filter((key) => a[key] !== null).length; - const bSpecificity = keys.filter((key) => b[key] !== null).length; - - // Take the rule with the most specific condition first - return bSpecificity - aSpecificity; - })[0]; - - if (prioritizedRule) { - if (prioritizedRule.rootFolder) { - rootFolder = prioritizedRule.rootFolder; - } - if (prioritizedRule.profileId) { - profileId = prioritizedRule.profileId; - } - if (prioritizedRule.tags) { - tags = [ - ...new Set([ - ...(tags || []), - ...prioritizedRule.tags.split(',').map((tag) => Number(tag)), - ]), - ]; - } - + const isAdvanced = user.hasPermission( + [Permission.MANAGE_REQUESTS, Permission.REQUEST_ADVANCED], + { type: 'or' } + ); + // Advanced users pick these in the modal, so we don't want to override them if they are set + const overrideRulesResult = isAdvanced + ? { + rootFolder: rootFolder ? null : ruleResult.rootFolder, + profileId: profileId ? null : ruleResult.profileId, + tags: tags ? null : ruleResult.tags, + } + : ruleResult; + if (overrideRulesResult.rootFolder) { + rootFolder = overrideRulesResult.rootFolder; + } + if (overrideRulesResult.profileId) { + profileId = overrideRulesResult.profileId; + } + if (overrideRulesResult.tags) { + tags = overrideRulesResult.tags; + } + if ( + overrideRulesResult.rootFolder || + overrideRulesResult.profileId || + overrideRulesResult.tags + ) { logger.debug('Override rule applied.', { - label: 'Media Request', - overrides: prioritizedRule, + label: 'Override Rules', + overrides: overrideRulesResult, }); } } @@ -373,6 +383,54 @@ export class MediaRequest { rootFolder: rootFolder, tags: tags, isAutoRequest: options.isAutoRequest ?? false, + ignoreQuota, + }); + + await requestRepository.save(request); + return request; + } else if (requestBody.mediaType === MediaType.BOOK) { + await mediaRepository.save(media); + + const request = new MediaRequest({ + type: MediaType.BOOK, + media, + requestedBy: requestUser, + // If the user is an admin or has the "auto approve" permission, automatically approve the request + status: user.hasPermission( + [ + requestBody.is4k + ? Permission.AUTO_APPROVE_4K + : Permission.AUTO_APPROVE, + requestBody.is4k + ? Permission.AUTO_APPROVE_AUDIO_BOOK + : Permission.AUTO_APPROVE_BOOK, + Permission.MANAGE_REQUESTS, + ], + { type: 'or' } + ) + ? MediaRequestStatus.APPROVED + : MediaRequestStatus.PENDING, + modifiedBy: user.hasPermission( + [ + requestBody.is4k + ? Permission.AUTO_APPROVE_4K + : Permission.AUTO_APPROVE, + requestBody.is4k + ? Permission.AUTO_APPROVE_AUDIO_BOOK + : Permission.AUTO_APPROVE_BOOK, + Permission.MANAGE_REQUESTS, + ], + { type: 'or' } + ) + ? user + : undefined, + is4k: requestBody.is4k, + serverId: requestBody.serverId, + profileId: profileId, + metadataProfileId: metadataProfileId, + rootFolder: rootFolder, + tags: tags, + isAutoRequest: options.isAutoRequest ?? false, }); await requestRepository.save(request); @@ -384,7 +442,10 @@ export class MediaRequest { let requestedSeasons = requestBody.seasons === 'all' ? tmdbMediaShow.seasons - .filter((season) => season.season_number !== 0) + .filter( + (season) => + season.season_number !== 0 && season.episode_count > 0 + ) .map((season) => season.season_number) : (requestBody.seasons as number[]); if (!settings.main.enableSpecialEpisodes) { @@ -436,6 +497,7 @@ export class MediaRequest { if (finalSeasons.length === 0) { throw new NoSeasonsAvailableError('No seasons available to request'); } else if ( + !ignoreQuota && quotas.tv.limit && finalSeasons.length > (quotas.tv.remaining ?? 0) ) { @@ -504,6 +566,7 @@ export class MediaRequest { }) ), isAutoRequest: options.isAutoRequest ?? false, + ignoreQuota, }); await requestRepository.save(request); @@ -570,6 +633,9 @@ export class MediaRequest { @Column({ nullable: true }) public profileId: number; + @Column({ nullable: true }) + public metadataProfileId: number; + @Column({ nullable: true }) public rootFolder: string; @@ -610,6 +676,9 @@ export class MediaRequest { @Column({ default: false }) public isAutoRequest: boolean; + @Column({ default: false }) + public ignoreQuota: boolean; + constructor(init?: Partial) { Object.assign(this, init); } @@ -727,9 +796,15 @@ export class MediaRequest { type: Notification ) { const tmdb = new TheMovieDb(); + const hardcover = new Hardcover(); try { - const mediaType = entity.type === MediaType.MOVIE ? 'Movie' : 'Series'; + const mediaType = + entity.type === MediaType.MOVIE + ? 'Movie' + : entity.type === MediaType.TV + ? 'Series' + : 'Book'; let event: string | undefined; let notifyAdmin = true; let notifySystem = true; @@ -740,30 +815,62 @@ export class MediaRequest { notifyAdmin = false; break; case Notification.MEDIA_APPROVED: - event = `${entity.is4k ? '4K ' : ''}${mediaType} Request Approved`; + event = `${ + entity.is4k + ? entity.type === MediaType.BOOK + ? 'Audio' + : '4K ' + : '' + }${mediaType} Request Approved`; notifyAdmin = false; break; case Notification.MEDIA_DECLINED: - event = `${entity.is4k ? '4K ' : ''}${mediaType} Request Declined`; + event = `${ + entity.is4k + ? entity.type === MediaType.BOOK + ? 'Audio' + : '4K ' + : '' + }${mediaType} Request Declined`; notifyAdmin = false; break; case Notification.MEDIA_PENDING: - event = `New ${entity.is4k ? '4K ' : ''}${mediaType} Request`; + event = `New ${ + entity.is4k + ? entity.type === MediaType.BOOK + ? 'Audio' + : '4K ' + : '' + }${mediaType} Request`; break; case Notification.MEDIA_AUTO_REQUESTED: event = `${ - entity.is4k ? '4K ' : '' + entity.is4k + ? entity.type === MediaType.BOOK + ? 'Audio ' + : '4K ' + : '' }${mediaType} Request Automatically Submitted`; notifyAdmin = false; notifySystem = false; break; case Notification.MEDIA_AUTO_APPROVED: event = `${ - entity.is4k ? '4K ' : '' + entity.is4k + ? entity.type === MediaType.BOOK + ? 'Audio' + : '4K ' + : '' }${mediaType} Request Automatically Approved`; break; case Notification.MEDIA_FAILED: - event = `${entity.is4k ? '4K ' : ''}${mediaType} Request Failed`; + event = `${ + entity.is4k + ? entity.type === MediaType.BOOK + ? 'Audio ' + : '4K ' + : '' + }${mediaType} Request Failed`; break; } @@ -813,6 +920,27 @@ export class MediaRequest { }, ], }); + } else if (entity.type === MediaType.BOOK) { + const book = await hardcover.getBook(media.tmdbId); + notificationManager.sendNotification(type, { + media, + request: entity, + notifyAdmin, + notifySystem, + notifyUser: notifyAdmin ? undefined : entity.requestedBy, + event, + subject: `${book.title}${ + book.release_date ? ` (${book.release_date.slice(0, 4)})` : '' + }`, + message: book.description + ? truncate(book.description, { + length: 500, + separator: /\s/, + omission: '…', + }) + : 'No description available.', + image: book.image.url, + }); } } catch (e) { logger.error('Something went wrong sending media notification(s)', { diff --git a/server/entity/OverrideRule.ts b/server/entity/OverrideRule.ts index f77d721d48..5c6566e06c 100644 --- a/server/entity/OverrideRule.ts +++ b/server/entity/OverrideRule.ts @@ -17,6 +17,9 @@ class OverrideRule { @Column({ type: 'int', nullable: true }) public sonarrServiceId?: number; + @Column({ type: 'int', nullable: true }) + public readarrServiceId?: number; + @Column({ nullable: true }) public users?: string; @@ -32,6 +35,9 @@ class OverrideRule { @Column({ type: 'int', nullable: true }) public profileId?: number; + @Column({ type: 'int', nullable: true }) + public metadataProfileId?: number; + @Column({ nullable: true }) public rootFolder?: string; diff --git a/server/entity/User.ts b/server/entity/User.ts index 739fff32ea..325285a1ba 100644 --- a/server/entity/User.ts +++ b/server/entity/User.ts @@ -51,6 +51,8 @@ export class User { 'settings', ]; + static readonly serializationExcludedFields: string[] = ['settings']; + public displayName: string; @PrimaryGeneratedColumn() @@ -101,7 +103,14 @@ export class User { @Column({ type: 'varchar', nullable: true, select: false }) public plexToken?: string | null; - @Column({ type: 'integer', default: 0 }) + @Column({ + type: 'bigint', + default: 0, + transformer: { + to: (value: number) => value, + from: (value: string | number) => Number(value), + }, + }) public permissions = 0; @Column() @@ -134,6 +143,12 @@ export class User { @Column({ nullable: true }) public tvQuotaDays?: number; + @Column({ nullable: true }) + public bookQuotaLimit?: number; + + @Column({ nullable: true }) + public bookQuotaDays?: number; + @OneToOne(() => UserSettings, (settings) => settings.user, { cascade: true, eager: true, @@ -173,6 +188,16 @@ export class User { return filtered; } + // settings is eager-loaded and would otherwise ride along into every nested User response + public toJSON(): Partial { + return Object.assign( + {}, + ...(Object.keys(this) as (keyof User)[]) + .filter((k) => !User.serializationExcludedFields.includes(k)) + .map((k) => ({ [k]: this[k] })) + ); + } + public hasPermission( permissions: Permission | Permission[], options?: PermissionCheckOptions @@ -299,6 +324,7 @@ export class User { ...(movieQuotaDays ? { createdAt: AfterDate(movieDate) } : {}), type: MediaType.MOVIE, status: Not(MediaRequestStatus.DECLINED), + ignoreQuota: false, }, }) : 0; @@ -336,6 +362,9 @@ export class User { const tvQuotaUsed = tvQuotaLimit ? ( await tvQuotaUsedQuery + .andWhere('request.ignoreQuota = :ignoreQuota', { + ignoreQuota: false, + }) .addSelect((subQuery) => { return subQuery .select('COUNT(season.id)', 'seasonCount') @@ -347,6 +376,41 @@ export class User { ).reduce((sum: number, req: MediaRequest) => sum + req.seasonCount, 0) : 0; + const bookQuotaLimit = !canBypass + ? (this.bookQuotaLimit ?? defaultQuotas.book.quotaLimit) + : 0; + const bookQuotaDays = this.bookQuotaDays ?? defaultQuotas.book.quotaDays; + + // Count book requests made during quota period + const bookDate = new Date(); + if (bookQuotaDays) { + bookDate.setDate(bookDate.getDate() - bookQuotaDays); + } + + // Ebook and audiobook requests for the same book are a single user action + // when format syncing is on, so quota counts distinct books rather than rows + const bookQuotaUsedQuery = requestRepository + .createQueryBuilder('request') + .leftJoin('request.requestedBy', 'requestedBy') + .where('request.type = :requestType', { requestType: MediaType.BOOK }) + .andWhere('requestedBy.id = :userId', { userId: this.id }) + .andWhere('request.status != :declinedStatus', { + declinedStatus: MediaRequestStatus.DECLINED, + }); + + if (bookQuotaDays) { + bookQuotaUsedQuery.andWhere('request.createdAt > :date', { + date: bookDate.toJSON(), + }); + } + + const bookQuotaUsed = bookQuotaLimit + ? await bookQuotaUsedQuery + .select('COUNT(DISTINCT request.mediaId)', 'count') + .getRawOne() + .then((result) => Number(result?.count ?? 0)) + : 0; + return { movie: { days: movieQuotaDays, @@ -368,6 +432,15 @@ export class User { : undefined, restricted: !!(tvQuotaLimit && tvQuotaLimit - tvQuotaUsed <= 0), }, + book: { + days: bookQuotaDays, + limit: bookQuotaLimit, + used: bookQuotaUsed, + remaining: bookQuotaLimit + ? Math.max(0, bookQuotaLimit - bookQuotaUsed) + : undefined, + restricted: !!(bookQuotaLimit && bookQuotaLimit - bookQuotaUsed <= 0), + }, }; } } diff --git a/server/entity/UserSettings.ts b/server/entity/UserSettings.ts index 82671fe3b3..d0f2ef1f7e 100644 --- a/server/entity/UserSettings.ts +++ b/server/entity/UserSettings.ts @@ -14,6 +14,19 @@ export const ALL_NOTIFICATIONS = Object.values(Notification) .filter((v) => !isNaN(Number(v))) .reduce((a, v) => a + Number(v), 0); +// convert between DB representation (JSON string) into typescript array +const jsonArrayTransformer = { + from: (v: string | null): string[] => { + try { + return v ? JSON.parse(v) : []; + } catch { + return []; + } + }, + to: (v: string[] | null): string | null => + v?.length ? JSON.stringify(v) : null, +}; + @Entity() export class UserSettings { constructor(init?: Partial) { @@ -42,8 +55,8 @@ export class UserSettings { @Column({ nullable: true }) public pgpKey?: string; - @Column({ nullable: true }) - public discordId?: string; + @Column({ type: 'text', nullable: true, transformer: jsonArrayTransformer }) + public discordIds: string[]; @Column({ nullable: true }) public pushbulletAccessToken?: string; diff --git a/server/i18n/globalMessages.ts b/server/i18n/globalMessages.ts index 38f53f0df2..106232d733 100644 --- a/server/i18n/globalMessages.ts +++ b/server/i18n/globalMessages.ts @@ -19,6 +19,8 @@ const globalMessages = defineMessages('notifications.common', { openIn: 'Open in {applicationTitle}', movie: 'movie', series: 'series', + book: 'book', + audiobook: 'audiobook', issue: 'issue', issueTypeName: '{type} issue', }); diff --git a/server/i18n/index.ts b/server/i18n/index.ts index ad4fc6fcc2..d79c3f9569 100644 --- a/server/i18n/index.ts +++ b/server/i18n/index.ts @@ -1,4 +1,5 @@ import { createIntl, createIntlCache } from '@formatjs/intl'; +import { getSettings } from '@server/lib/settings'; import type { AvailableLocale } from '@server/types/languages'; import { availableLocales } from '@server/types/languages'; import fs from 'fs'; @@ -38,7 +39,10 @@ export function initI18n(): void { } export function getIntl(locale?: AvailableLocale): IntlInstance { - return intls.get(locale ?? 'en') || intls.get('en')!; + // "Default" stores a falsy locale, so fall back to the server language, then English + const serverLocale = getSettings().main.locale as AvailableLocale; + const resolved = locale || serverLocale || 'en'; + return intls.get(resolved) || intls.get('en')!; } type MessageDescriptorMap> = { diff --git a/server/i18n/locale/ca.json b/server/i18n/locale/ca.json index 0967ef424b..4d8b6c0b6d 100644 --- a/server/i18n/locale/ca.json +++ b/server/i18n/locale/ca.json @@ -1 +1,57 @@ -{} +{ + "notifications.agents.email.approvedRequest": "La teva sol·licitud de {mediaType} ha estat aprovada:", + "notifications.agents.email.approvedRequest4k": "La teva sol·licitud de {mediaType} en 4k ha estat aprovada:", + "notifications.agents.email.autoApproved": "S'ha aprovat automàticament les noves sol·licituds per la següent {mediaType}:", + "notifications.agents.email.autoApproved4k": "S'ha aprovat automàticament una nova sol·licitud per al següent {mediaType} en 4K:", + "notifications.agents.email.autoRequested": "S'ha enviat automàticament una nova sol·licitud per al següent {mediaType}:", + "notifications.agents.email.autoRequested4k": "S'ha enviat automàticament una nova sol·licitud per al següent {mediaType} en 4K:", + "notifications.agents.email.availableRequest": "La teva sol·licitud de {mediaType} ja està disponible:", + "notifications.agents.email.availableRequest4k": "La teva sol·licitud de {mediaType} en 4k ja està disponible:", + "notifications.agents.email.declinedRequest": "La teva sol·licitud de {mediaType} ha estat denegada:", + "notifications.agents.email.declinedRequest4k": "La teva sol·licitud de {mediaType} en 4k ha estat denegada:", + "notifications.agents.email.failedRequest": "La teva sol·licitud per {mediaType} no s'ha pogut afegir a {service}:", + "notifications.agents.email.issue": "incidència", + "notifications.common.available": "Disponible", + "notifications.common.declined": "Rebutjat", + "notifications.common.failed": "Fallit", + "notifications.common.issue": "incidència", + "notifications.common.movie": "pel·lícula", + "notifications.common.open": "Oberta", + "notifications.common.processing": "En procés", + "notifications.common.resolved": "Resolta", + "notifications.common.series": "sèrie", + "notifications.agents.email.issueType": "Incidència de {type}", + "notifications.agents.webpush.viewIssue": "Veure incidència", + "notifications.agents.webpush.viewMedia": "Veure contingut", + "notifications.common.issueStatus": "Estat de la incidència", + "notifications.common.pendingApproval": "Pendent d'aprovació", + "notifications.common.reportedBy": "Reportat per", + "notifications.agents.email.failedRequest4k": "No s'ha pogut afegir una sol·licitud per al següent {mediaType} en 4K a {service}:", + "notifications.agents.email.issueComment": "{userName} ha fet un comentari a la inciència de {issueType} per a {mediaType} {subject}:", + "notifications.agents.email.issueCreated": "{userName} ha informat d'un nou {issueType} per a {mediaType} {subject}:", + "notifications.agents.email.issueReopened": "La incidència de {issueType} per a {mediaType} {subject} ha estat reoberta per {userName}.", + "notifications.agents.email.issueResolved": "La incidència de {issueType} per a {mediaType} {subject} ha estat marcada com a resolta per {userName}!", + "notifications.agents.email.pendingRequest": "Hi ha una nova sol·licitud de {mediaType} pendent d'aprovació:", + "notifications.agents.email.pendingRequest4k": "Una nova sol·licitud de {mediaType} en 4K està pendent d'aprovació:", + "notifications.agents.webpush.approved": "La teva sol·licitud de {quality}{mediaType} ha estat aprovada.", + "notifications.agents.webpush.autoApproved": "S'ha aprovat automàticament una nova sol·licitud de {mediaType} {quality} de {userName}.", + "notifications.agents.webpush.autoRequested": "S'ha enviat automàticament una nova sol·licitud de {mediaType} {quality}.", + "notifications.agents.webpush.available": "La teva sol·licitud de {mediaType} {quality} ja està disponible!", + "notifications.agents.webpush.declined": "La teva sol·licitud de {mediaType} {quality} ha estat rebutjada.", + "notifications.agents.webpush.failed": "No s'ha pogut processar la sol·licitud de {mediaType} {quality}.", + "notifications.agents.webpush.issueComment": "{userName} ha fet un comentari en la incidència de {issueType}.", + "notifications.agents.webpush.issueCreated": "{userName} ha informat d'una nova incidència de ({issueType}).", + "notifications.agents.webpush.issueReopened": "{userName} ha reobert la incidència de {issueType}.", + "notifications.agents.webpush.issueResolved": "{userName} ha marcat la incidència de {issueType} com a resolta!", + "notifications.agents.webpush.pending": "Cal aprovació per a una nova sol·licitud de {mediaType} {quality} de {userName}.", + "notifications.common.commentFrom": "Comentari de {userName}", + "notifications.common.issueType": "Tipus d'incidència", + "notifications.common.issueTypeName": "Incidència de {type}", + "notifications.common.openIn": "Obre a {applicationTitle}", + "notifications.common.requestStatus": "Estat de la sol·licitud", + "notifications.common.requestedBy": "Sol·licitat per", + "notifications.common.viewIssue": "Veure incidència a {applicationTitle}", + "notifications.common.viewMedia": "Veure contingut multimèdia a {applicationTitle}", + "notifications.test.message": "Provant, 1, 2, 3. Es rep alt i clar?", + "notifications.test.subject": "Notificació de prova" +} diff --git a/server/i18n/locale/cs.json b/server/i18n/locale/cs.json index 0967ef424b..d78ab2ed16 100644 --- a/server/i18n/locale/cs.json +++ b/server/i18n/locale/cs.json @@ -1 +1,57 @@ -{} +{ + "notifications.agents.email.approvedRequest": "Vaše žádost o následující {mediaType} byla schválena:", + "notifications.agents.email.approvedRequest4k": "Vaše žádost o následující {mediaType} ve 4K byla schválena:", + "notifications.agents.email.autoApproved": "Nová žádost o následující {mediaType} byla automaticky schválena:", + "notifications.agents.email.autoApproved4k": "Nová žádost o následující {mediaType} ve 4K byla automaticky schválena:", + "notifications.agents.email.autoRequested": "Nová žádost o následující {mediaType} byla automaticky odeslána:", + "notifications.agents.email.autoRequested4k": "Nová žádost o následující {mediaType} ve 4K byla automaticky odeslána:", + "notifications.agents.email.availableRequest": "Vaše žádost o následující {mediaType} je nyní dostupná:", + "notifications.agents.email.availableRequest4k": "Vaše žádost o následující {mediaType} ve 4K je nyní dostupná:", + "notifications.agents.email.declinedRequest": "Vaše žádost o následující {mediaType} byla zamítnuta:", + "notifications.agents.email.declinedRequest4k": "Vaše žádost o následující {mediaType} ve 4K byla zamítnuta:", + "notifications.agents.email.failedRequest": "Žádost o následující {mediaType} se nepodařilo přidat do {service}:", + "notifications.agents.email.failedRequest4k": "Žádost o následující {mediaType} ve 4K se nepodařilo přidat do {service}:", + "notifications.agents.email.issue": "problém", + "notifications.agents.email.issueComment": "{userName} okomentoval {issueType} pro {mediaType} {subject}:", + "notifications.agents.email.issueCreated": "Nový {issueType} byl nahlášen uživatelem {userName} pro {mediaType} {subject}:", + "notifications.agents.email.issueReopened": "{issueType} pro {mediaType} {subject} byl znovu otevřen uživatelem {userName}.", + "notifications.agents.email.issueResolved": "{issueType} pro {mediaType} {subject} byl označen jako vyřešený uživatelem {userName}!", + "notifications.agents.email.issueType": "{type} problém", + "notifications.agents.email.pendingRequest": "Nová žádost o následující {mediaType} čeká na schválení:", + "notifications.agents.email.pendingRequest4k": "Nová žádost o následující {mediaType} ve 4K čeká na schválení:", + "notifications.agents.webpush.approved": "Vaše žádost o {quality}{mediaType} byla schválena.", + "notifications.agents.webpush.autoApproved": "Automaticky schválená nová žádost o {quality}{mediaType} od uživatele {userName}.", + "notifications.agents.webpush.autoRequested": "Automaticky odeslaná nová žádost o {quality}{mediaType}.", + "notifications.agents.webpush.available": "Vaše žádost o {quality}{mediaType} je nyní dostupná!", + "notifications.agents.webpush.declined": "Vaše žádost o {quality}{mediaType} byla zamítnuta.", + "notifications.agents.webpush.failed": "Nepodařilo se zpracovat žádost o {quality}{mediaType}.", + "notifications.agents.webpush.issueComment": "{userName} okomentoval {issueType}.", + "notifications.agents.webpush.issueCreated": "Nový {issueType} byl nahlášen uživatelem {userName}.", + "notifications.agents.webpush.issueReopened": "{issueType} byl znovu otevřen uživatelem {userName}.", + "notifications.agents.webpush.issueResolved": "{issueType} byl označen jako vyřešený uživatelem {userName}!", + "notifications.agents.webpush.pending": "Je vyžadováno schválení pro novou žádost o {quality}{mediaType} od uživatele {userName}.", + "notifications.agents.webpush.viewIssue": "Zobrazit problém", + "notifications.agents.webpush.viewMedia": "Zobrazit médium", + "notifications.common.available": "Dostupné", + "notifications.common.commentFrom": "Komentář od uživatele {userName}", + "notifications.common.declined": "Zamítnuto", + "notifications.common.failed": "Selhalo", + "notifications.common.issue": "problém", + "notifications.common.issueStatus": "Stav problému", + "notifications.common.issueType": "Typ problému", + "notifications.common.issueTypeName": "{type} problém", + "notifications.common.movie": "film", + "notifications.common.open": "Otevřít", + "notifications.common.openIn": "Otevřít v {applicationTitle}", + "notifications.common.pendingApproval": "Čekající na schválení", + "notifications.common.processing": "Zpracovávání", + "notifications.common.reportedBy": "Nahlásil", + "notifications.common.requestStatus": "Stav žádosti", + "notifications.common.requestedBy": "Zažádal", + "notifications.common.resolved": "Vyřešeno", + "notifications.common.series": "seriál", + "notifications.common.viewIssue": "Zobrazit problém v {applicationTitle}", + "notifications.common.viewMedia": "Zobrazit médium v {applicationTitle}", + "notifications.test.message": "Kontrola, kontrola, 1, 2, 3. Jak nás slyšíte?", + "notifications.test.subject": "Testovací oznámení" +} diff --git a/server/i18n/locale/da.json b/server/i18n/locale/da.json index cde7f88bea..d7351ec270 100644 --- a/server/i18n/locale/da.json +++ b/server/i18n/locale/da.json @@ -5,8 +5,8 @@ "notifications.agents.email.autoApproved4k": "En ny anmodning om følgende {mediaType} i 4K er automatisk blevet godkendt:", "notifications.agents.email.autoRequested": "En ny anmodning om følgende {mediaType} blev automatisk oprettet:", "notifications.agents.email.autoRequested4k": "En ny anmodning om følgende {mediaType} i 4K blev automatisk oprettet:", - "notifications.agents.email.availableRequest": "Din anmodning om følgende {mediaType} er nu tilgængelig:", - "notifications.agents.email.availableRequest4k": "Din anmodning om følgende {mediaType} i 4K er nu tilgængelig:", + "notifications.agents.email.availableRequest": "Din anmodede {mediaType} er nu tilgængelig:", + "notifications.agents.email.availableRequest4k": "Din anmodede {mediaType} i 4K er nu tilgængelig:", "notifications.agents.email.declinedRequest": "Din anmodning om følgende {mediaType} blev afvist:", "notifications.agents.email.declinedRequest4k": "Din anmodning om følgende {mediaType} i 4K blev afvist:", "notifications.agents.email.failedRequest": "En anmodning om følgende {mediaType} kunne ikke tilføjes til {service}:", diff --git a/server/i18n/locale/en.json b/server/i18n/locale/en.json index 459ed5fada..2a686c0ba3 100644 --- a/server/i18n/locale/en.json +++ b/server/i18n/locale/en.json @@ -32,7 +32,9 @@ "notifications.agents.webpush.pending": "Approval required for a new {quality}{mediaType} request from {userName}.", "notifications.agents.webpush.viewIssue": "View Issue", "notifications.agents.webpush.viewMedia": "View Media", + "notifications.common.audiobook": "audiobook", "notifications.common.available": "Available", + "notifications.common.book": "book", "notifications.common.commentFrom": "Comment from {userName}", "notifications.common.declined": "Declined", "notifications.common.failed": "Failed", diff --git a/server/i18n/locale/es-MX.json b/server/i18n/locale/es-MX.json index 0967ef424b..fbe66fd29d 100644 --- a/server/i18n/locale/es-MX.json +++ b/server/i18n/locale/es-MX.json @@ -1 +1,57 @@ -{} +{ + "notifications.agents.email.approvedRequest": "Tu solicitud de {mediaType} ha sido aprobada:", + "notifications.agents.email.approvedRequest4k": "Tu solicitud de {mediaType} en 4K ha sido aprobada:", + "notifications.agents.email.autoApproved": "Una nueva solicitud de {mediaType} ha sido aprobada automáticamente:", + "notifications.agents.email.autoApproved4k": "Una nueva solicitud de {mediaType} en 4K ha sido aprobada automáticamente:", + "notifications.agents.email.autoRequested": "Una nueva solicitud de {mediaType} ha sido enviada automáticamente:", + "notifications.agents.email.autoRequested4k": "Una nueva solicitud de {mediaType} en 4K ha sido enviada automáticamente:", + "notifications.agents.email.availableRequest": "Tu solicitud de {mediaType} ya está disponible:", + "notifications.agents.email.availableRequest4k": "Tu solicitud de {mediaType} en 4K ya está disponible:", + "notifications.agents.email.declinedRequest": "Tu solicitud de {mediaType} ha sido rechazada:", + "notifications.agents.email.declinedRequest4k": "Tu solicitud de {mediaType} en 4K ha sido rechazada:", + "notifications.agents.email.failedRequest": "No se pudo agregar {mediaType} a {service}:", + "notifications.agents.email.failedRequest4k": "No se pudo agregar {mediaType} en 4K a {service}:", + "notifications.agents.email.issue": "problema", + "notifications.agents.email.issueComment": "{userName} comentó en el problema de {issueType} para {mediaType} {subject}:", + "notifications.agents.email.issueCreated": "{userName} reportó un nuevo {issueType} para {mediaType} {subject}:", + "notifications.agents.email.issueReopened": "El problema de {issueType} para {mediaType} {subject} fue reabierto por {userName}.", + "notifications.agents.email.issueResolved": "¡El problema de {issueType} para {mediaType} {subject} fue marcado como resuelto por {userName}!", + "notifications.agents.email.issueType": "Problema de {type}", + "notifications.agents.email.pendingRequest": "Una nueva solicitud de {mediaType} está pendiente de aprobación:", + "notifications.agents.email.pendingRequest4k": "Una nueva solicitud de {mediaType} en 4K está pendiente de aprobación:", + "notifications.agents.webpush.approved": "Tu solicitud de {mediaType} {quality} ha sido aprobada.", + "notifications.agents.webpush.autoApproved": "Se aprobó automáticamente una nueva solicitud de {mediaType} {quality} de {userName}.", + "notifications.agents.webpush.autoRequested": "Se envió automáticamente una nueva solicitud de {mediaType} {quality}.", + "notifications.agents.webpush.available": "¡Tu solicitud de {mediaType} {quality} ya está disponible!", + "notifications.agents.webpush.declined": "Tu solicitud de {mediaType} {quality} ha sido rechazada.", + "notifications.agents.webpush.failed": "No se pudo procesar la solicitud de {mediaType} {quality}.", + "notifications.agents.webpush.issueComment": "{userName} comentó en el problema de {issueType}.", + "notifications.agents.webpush.issueCreated": "{userName} reportó un nuevo problema de {issueType}.", + "notifications.agents.webpush.issueReopened": "El problema de {issueType} fue reabierto por {userName}.", + "notifications.agents.webpush.issueResolved": "¡El problema de {issueType} fue marcado como resuelto por {userName}!", + "notifications.agents.webpush.pending": "Se requiere aprobación para una nueva solicitud de {mediaType} {quality} de {userName}.", + "notifications.agents.webpush.viewIssue": "Ver problema", + "notifications.agents.webpush.viewMedia": "Ver contenido", + "notifications.common.available": "Disponible", + "notifications.common.commentFrom": "Comentario de {userName}", + "notifications.common.declined": "Rechazada", + "notifications.common.failed": "Fallida", + "notifications.common.issue": "Problema", + "notifications.common.issueStatus": "Estado del problema", + "notifications.common.issueType": "Tipo de problema", + "notifications.common.issueTypeName": "Problema de {type}", + "notifications.common.movie": "Película", + "notifications.common.open": "Abierto", + "notifications.common.openIn": "Abrir en {applicationTitle}", + "notifications.common.pendingApproval": "Pendiente de aprobación", + "notifications.common.processing": "Procesando", + "notifications.common.reportedBy": "Reportado por", + "notifications.common.requestStatus": "Estado de la solicitud", + "notifications.common.requestedBy": "Solicitado por", + "notifications.common.resolved": "Resuelto", + "notifications.common.series": "Serie", + "notifications.common.viewIssue": "Ver problema en {applicationTitle}", + "notifications.common.viewMedia": "Ver contenido en {applicationTitle}", + "notifications.test.message": "Probando, probando, 1, 2, 3. ¿Se escucha bien?", + "notifications.test.subject": "Notificación de prueba" +} diff --git a/server/i18n/locale/es.json b/server/i18n/locale/es.json index 2e7428a0b4..7dd9f4c99e 100644 --- a/server/i18n/locale/es.json +++ b/server/i18n/locale/es.json @@ -19,5 +19,39 @@ "notifications.common.issueStatus": "Estado del problema", "notifications.common.available": "Disponible", "notifications.common.commentFrom": "Comentario de {userName}", - "notifications.common.declined": "Declinado" + "notifications.common.declined": "Declinado", + "notifications.agents.email.approvedRequest": "Tu solicitud de {mediaType} ha sido aprobada:", + "notifications.agents.email.approvedRequest4k": "Tu solicitud de {mediaType} en 4K ha sido aprobada:", + "notifications.agents.email.autoApproved": "Una nueva solicitud de {mediaType} ha sido aprobada automáticamente:", + "notifications.agents.email.autoApproved4k": "Una nueva solicitud de {mediaType} en 4K ha sido aprobada automáticamente:", + "notifications.agents.email.autoRequested": "Se ha enviado automáticamente una nueva solicitud de {mediaType}:", + "notifications.agents.email.autoRequested4k": "Se ha enviado automáticamente una nueva solicitud de {mediaType} en 4K:", + "notifications.agents.email.availableRequest": "Tu solicitud de {mediaType} ya está disponible:", + "notifications.agents.email.availableRequest4k": "Tu solicitud de {mediaType} en 4K ya está disponible:", + "notifications.agents.email.declinedRequest": "Tu solicitud de {mediaType} ha sido rechazada:", + "notifications.agents.email.declinedRequest4k": "Tu solicitud de {mediaType} en 4K ha sido rechazada:", + "notifications.agents.email.failedRequest": "Ha fallado al añadir a {service} una solicitud de {mediaType}:", + "notifications.agents.email.failedRequest4k": "Ha fallado al añadir a {service} una solicitud de {mediaType} en 4K:", + "notifications.agents.email.issue": "incidencia", + "notifications.agents.email.issueComment": "{userName} ha comentado en la incidencia de {issueType} para {mediaType} {subject}:", + "notifications.agents.email.issueCreated": "{userName} ha reportado una nueva incidencia de {issueType} para {mediaType} {subject}:", + "notifications.agents.email.issueReopened": "La incidencia de {issueType} para {mediaType} {subject} ha sido reabierta por {userName}.", + "notifications.agents.email.issueResolved": "¡La incidencia de {issueType} para {mediaType} {subject} ha sido marcada como resuelta por {userName}!", + "notifications.agents.email.issueType": "Incidencia de {type}", + "notifications.agents.email.pendingRequest": "Hay una nueva solicitud de {mediaType} pendiente de aprobación:", + "notifications.agents.email.pendingRequest4k": "Hay una nueva solicitud de {mediaType} en 4K pendiente de aprobación:", + "notifications.agents.webpush.approved": "Tu solicitud de {mediaType} {quality} ha sido aprobada.", + "notifications.agents.webpush.autoApproved": "Se ha aprobado automáticamente una nueva solicitud de {mediaType} {quality} de {userName}.", + "notifications.agents.webpush.autoRequested": "Se ha enviado automáticamente una nueva solicitud de {mediaType} {quality}.", + "notifications.agents.webpush.available": "¡Tu solicitud de {mediaType} {quality} ya está disponible!", + "notifications.agents.webpush.declined": "Tu solicitud de {mediaType} {quality} ha sido rechazada.", + "notifications.agents.webpush.failed": "Error al procesar la solicitud de {mediaType} {quality}.", + "notifications.agents.webpush.issueComment": "{userName} ha comentado en la incidencia de {issueType}.", + "notifications.agents.webpush.issueCreated": "{userName} ha reportado una nueva incidencia de {issueType}.", + "notifications.agents.webpush.issueReopened": "La incidencia de {issueType} ha sido reabierta por {userName}.", + "notifications.agents.webpush.issueResolved": "¡La incidencia de {issueType} ha sido marcada como resuelta por {userName}!", + "notifications.agents.webpush.pending": "Se requiere aprobación para una nueva solicitud de {mediaType} {quality} de {userName}.", + "notifications.agents.webpush.viewIssue": "Ver incidencia", + "notifications.agents.webpush.viewMedia": "Ver contenido", + "notifications.common.issueTypeName": "Incidencia de {type}" } diff --git a/server/i18n/locale/fr.json b/server/i18n/locale/fr.json index 58fb98478d..d6b5d66e21 100644 --- a/server/i18n/locale/fr.json +++ b/server/i18n/locale/fr.json @@ -21,17 +21,17 @@ "notifications.agents.email.pendingRequest4k": "Une nouvelle demande de {mediaType} en 4K est en attente de validation :", "notifications.test.subject": "Notification de test", "notifications.test.message": "Test, un, deux, un, deux. Vous nous recevez ?", - "notifications.agents.webpush.approved": "Votre demande de {mediaType} {quality} a été validée.", - "notifications.agents.webpush.autoApproved": "Une nouvelle demande de {mediaType} {quality} de {userName} a été validée automatiquement.", - "notifications.agents.webpush.autoRequested": "Une nouvelle demande de {mediaType} {quality} a été créée automatiquement.", - "notifications.agents.webpush.available": "Votre demande de {mediaType} {quality} est maintenant disponible !", - "notifications.agents.webpush.declined": "Votre demande de {mediaType} {quality} a été refusée.", - "notifications.agents.webpush.failed": "Impossible de traiter la demande de {mediaType} {quality}.", + "notifications.agents.webpush.approved": "Votre demande de {mediaType}{quality} a été validée.", + "notifications.agents.webpush.autoApproved": "Une nouvelle demande de {mediaType}{quality} par {userName} a été validée automatiquement.", + "notifications.agents.webpush.autoRequested": "Une nouvelle demande de {mediaType}{quality} a été créée automatiquement.", + "notifications.agents.webpush.available": "Votre demande de {mediaType}{quality} est maintenant disponible !", + "notifications.agents.webpush.declined": "Votre demande de {mediaType}{quality} a été refusée.", + "notifications.agents.webpush.failed": "Impossible de traiter la demande de {mediaType}{quality}.", "notifications.agents.webpush.issueComment": "{userName} a commenté le {issueType}.", "notifications.agents.webpush.issueCreated": "Un nouveau {issueType} a été signalé par {userName}.", "notifications.agents.webpush.issueReopened": "Le {issueType} a été rouvert par {userName}.", "notifications.agents.webpush.issueResolved": "Le {issueType} a été marqué comme résolu par {userName} !", - "notifications.agents.webpush.pending": "Validation requise pour une nouvelle demande de {mediaType} {quality} de {userName}.", + "notifications.agents.webpush.pending": "Validation requise pour une nouvelle demande de {mediaType}{quality} par {userName}.", "notifications.agents.webpush.viewIssue": "Voir le problème", "notifications.agents.webpush.viewMedia": "Voir le média", "notifications.common.available": "Disponible", diff --git a/server/i18n/locale/nl.json b/server/i18n/locale/nl.json index 7c40158188..4eb0573661 100644 --- a/server/i18n/locale/nl.json +++ b/server/i18n/locale/nl.json @@ -1,35 +1,35 @@ { "notifications.agents.email.approvedRequest": "Je aanvraag voor de volgende {mediaType} is goedgekeurd:", "notifications.agents.email.approvedRequest4k": "Je aanvraag voor de volgende {mediaType} in 4K is goedgekeurd:", - "notifications.agents.email.autoApproved": "Een nieuw verzoek voor de volgende {mediaType} is automatisch goedgekeurd:", - "notifications.agents.email.autoApproved4k": "Een nieuw verzoek voor de volgende {mediaType} in 4K is automatisch goedgekeurd:", - "notifications.agents.email.autoRequested": "Een nieuw verzoek voor de volgende {mediaType} is automatisch ingediend:", - "notifications.agents.email.autoRequested4k": "Een nieuw verzoek voor de volgende {mediaType} in 4K is automatisch ingediend:", + "notifications.agents.email.autoApproved": "Een nieuwe aanvraag voor de volgende {mediaType} is automatisch goedgekeurd:", + "notifications.agents.email.autoApproved4k": "Een nieuwe aanvraag voor de volgende {mediaType} in 4K is automatisch goedgekeurd:", + "notifications.agents.email.autoRequested": "Een nieuwe aanvraag voor de volgende {mediaType} is automatisch ingediend:", + "notifications.agents.email.autoRequested4k": "Een nieuwe aanvraag voor de volgende {mediaType} in 4K is automatisch ingediend:", "notifications.agents.email.availableRequest": "Je aanvraag voor de volgende {mediaType} is nu beschikbaar:", "notifications.agents.email.availableRequest4k": "Je aanvraag voor de volgende {mediaType} in 4K is nu beschikbaar:", - "notifications.agents.email.declinedRequest": "Je verzoek voor de volgende {mediaType} is afgewezen:", - "notifications.agents.email.declinedRequest4k": "Je verzoek voor de volgende {mediaType} in 4K is afgewezen:", - "notifications.agents.email.failedRequest": "Een verzoek voor de volgende {mediaType} kon niet worden toegevoegd aan {service}:", - "notifications.agents.email.failedRequest4k": "Een verzoek voor de volgende {mediaType} in 4K kon niet worden toegevoegd aan {service}:", + "notifications.agents.email.declinedRequest": "Je aanvraag voor de volgende {mediaType} is afgewezen:", + "notifications.agents.email.declinedRequest4k": "Je aanvraag voor de volgende {mediaType} in 4K is afgewezen:", + "notifications.agents.email.failedRequest": "Een aanvraag voor de volgende {mediaType} kon niet worden toegevoegd aan {service}:", + "notifications.agents.email.failedRequest4k": "Een aanvraag voor de volgende {mediaType} in 4K kon niet worden toegevoegd aan {service}:", "notifications.agents.email.issue": "probleem", "notifications.agents.email.issueComment": "{userName} heeft gereageerd op het {issueType} voor de {mediaType} {subject}:", "notifications.agents.email.issueCreated": "Er is een nieuw {issueType} gemeld door {userName} voor de {mediaType} {subject}:", "notifications.agents.email.issueReopened": "Het {issueType} voor de {mediaType} {subject} is heropend door {userName}.", "notifications.agents.email.issueResolved": "Het {issueType} voor de {mediaType} {subject} is als opgelost gemarkeerd door {userName}!", "notifications.agents.email.issueType": "{type} probleem", - "notifications.agents.email.pendingRequest": "Een nieuw verzoek voor de volgende {mediaType} wacht op goedkeuring:", - "notifications.agents.email.pendingRequest4k": "Een nieuw verzoek voor de volgende {mediaType} in 4K wacht op goedkeuring:", - "notifications.agents.webpush.approved": "Je verzoek voor {quality}{mediaType} is goedgekeurd.", - "notifications.agents.webpush.autoApproved": "Automatisch een nieuw {quality}{mediaType}-verzoek van {userName} goedgekeurd.", - "notifications.agents.webpush.autoRequested": "Automatisch een nieuw {quality}{mediaType}-verzoek ingediend.", - "notifications.agents.webpush.available": "Je {quality}{mediaType}-verzoek is nu beschikbaar!", - "notifications.agents.webpush.declined": "Je {quality}{mediaType}-verzoek is afgewezen.", - "notifications.agents.webpush.failed": "Het verwerken van het {quality}{mediaType}-verzoek is mislukt.", + "notifications.agents.email.pendingRequest": "Een nieuwe aanvraag voor de volgende {mediaType} wacht op goedkeuring:", + "notifications.agents.email.pendingRequest4k": "Een nieuwe aanvraag voor de volgende {mediaType} in 4K wacht op goedkeuring:", + "notifications.agents.webpush.approved": "Je aanvraag voor {quality}{mediaType} is goedgekeurd.", + "notifications.agents.webpush.autoApproved": "Automatisch een nieuwe {quality}{mediaType}-aanvraag van {userName} goedgekeurd.", + "notifications.agents.webpush.autoRequested": "Automatisch een nieuwe {quality}{mediaType}-aanvraag ingediend.", + "notifications.agents.webpush.available": "Je {quality}{mediaType}-aanvraag is nu beschikbaar!", + "notifications.agents.webpush.declined": "Je {quality}{mediaType}-aanvraag is afgewezen.", + "notifications.agents.webpush.failed": "Het verwerken van het {quality}{mediaType}-aanvraag is mislukt.", "notifications.agents.webpush.issueComment": "{userName} heeft gereageerd op het {issueType}.", "notifications.agents.webpush.issueCreated": "Er is een nieuw {issueType} gemeld door {userName}.", "notifications.agents.webpush.issueReopened": "Het {issueType} is heropend door {userName}.", "notifications.agents.webpush.issueResolved": "Het {issueType} is als opgelost gemarkeerd door {userName}!", - "notifications.agents.webpush.pending": "Goedkeuring vereist voor een nieuw {quality}{mediaType}-verzoek van {userName}.", + "notifications.agents.webpush.pending": "Goedkeuring vereist voor een nieuwe {quality}{mediaType}-aanvraag van {userName}.", "notifications.agents.webpush.viewIssue": "Probleem bekijken", "notifications.agents.webpush.viewMedia": "Media bekijken", "notifications.common.available": "Beschikbaar", @@ -37,16 +37,16 @@ "notifications.common.declined": "Afgewezen", "notifications.common.failed": "Mislukt", "notifications.common.issue": "probleem", - "notifications.common.issueStatus": "Status van het probleem", - "notifications.common.issueType": "Type probleem", + "notifications.common.issueStatus": "Probleemstatus", + "notifications.common.issueType": "Probleemtype", "notifications.common.issueTypeName": "{type} probleem", "notifications.common.movie": "film", - "notifications.common.open": "Open", - "notifications.common.openIn": "Open in {applicationTitle}", + "notifications.common.open": "Openen", + "notifications.common.openIn": "Openen in {applicationTitle}", "notifications.common.pendingApproval": "In afwachting van goedkeuring", "notifications.common.processing": "Verwerken", "notifications.common.reportedBy": "Gemeld door", - "notifications.common.requestStatus": "Status van verzoek", + "notifications.common.requestStatus": "Aanvraagstatus", "notifications.common.requestedBy": "Aangevraagd door", "notifications.common.resolved": "Opgelost", "notifications.common.series": "serie", diff --git a/server/i18n/locale/ru.json b/server/i18n/locale/ru.json index 9d332b30d2..f188fb09c8 100644 --- a/server/i18n/locale/ru.json +++ b/server/i18n/locale/ru.json @@ -19,5 +19,39 @@ "notifications.agents.email.issueType": "Проблема {type}", "notifications.agents.email.pendingRequest": "Новый запрос на {mediaType} ожидает одобрения:", "notifications.agents.email.pendingRequest4k": "Новый запрос на {mediaType} в 4K ожидает одобрения:", - "notifications.agents.webpush.approved": "Ваш {quality}{mediaType} был одобрен." + "notifications.agents.webpush.approved": "Ваш {quality}{mediaType} был одобрен.", + "notifications.agents.webpush.autoRequested": "Автоматически отправил новый запрос на {quality}{mediaType}.", + "notifications.agents.webpush.available": "Ваш запрос на {quality}{mediaType} теперь доступен!", + "notifications.agents.webpush.declined": "Ваш запрос на {quality}{mediaType} был отклонён.", + "notifications.agents.webpush.failed": "Не удалось обработать запрос на {quality}{mediaType}.", + "notifications.agents.webpush.issueComment": "{userName} оставил комментарий к {issueType}.", + "notifications.agents.webpush.issueCreated": "Новый {issueType} был отправлен пользователем {userName}.", + "notifications.agents.webpush.issueReopened": "{issueType} был повторно открыт пользователем {userName}.", + "notifications.agents.webpush.issueResolved": "{issueType} был помечен как решён пользователем {userName}!", + "notifications.agents.webpush.pending": "Требуется подтверждение нового запроса на {quality}{mediaType} от {userName}.", + "notifications.agents.webpush.viewIssue": "Просмотреть обращение", + "notifications.agents.webpush.viewMedia": "Просмотреть медиа", + "notifications.common.available": "Доступно", + "notifications.common.commentFrom": "Комментарий от {userName}", + "notifications.common.declined": "Отклонено", + "notifications.common.failed": "Не удалось", + "notifications.common.issue": "обращение", + "notifications.common.issueStatus": "Статус Обращения", + "notifications.common.issueType": "Тип Обращения", + "notifications.common.issueTypeName": "{type} обращения", + "notifications.common.movie": "фильм", + "notifications.common.open": "Открыть", + "notifications.common.openIn": "Открыть в {applicationTitle}", + "notifications.common.pendingApproval": "Ожидает подтверждения", + "notifications.common.processing": "Обработка", + "notifications.common.reportedBy": "Сообщено", + "notifications.common.requestStatus": "Статус Запроса", + "notifications.common.requestedBy": "Запрошено", + "notifications.common.resolved": "Разрешено", + "notifications.common.series": "серии", + "notifications.common.viewIssue": "Просмотреть обращение в {applicationTitle}", + "notifications.common.viewMedia": "Просмотреть медиа в {applicationTitle}", + "notifications.test.message": "Проверка связи, раз, два, три. Как слышно?", + "notifications.test.subject": "Тестовое уведомление", + "notifications.agents.webpush.autoApproved": "Автоматически одобрен новый запрос на {quality}{mediaType} от {userName}." } diff --git a/server/i18n/locale/tr.json b/server/i18n/locale/tr.json index 0967ef424b..3d82035d48 100644 --- a/server/i18n/locale/tr.json +++ b/server/i18n/locale/tr.json @@ -1 +1,57 @@ -{} +{ + "notifications.agents.email.approvedRequest": "Aşağıdaki {mediaType} talebiniz onaylandı:", + "notifications.agents.email.approvedRequest4k": "Aşağıdaki {mediaType} için 4K talebiniz onaylandı:", + "notifications.agents.email.autoApproved": "Aşağıdaki {mediaType} için yeni bir talep otomatik olarak onaylandı:", + "notifications.agents.email.autoApproved4k": "Aşağıdaki {mediaType} için 4K'da yeni bir talep otomatik olarak onaylandı:", + "notifications.agents.email.autoRequested": "Aşağıdaki {mediaType} için yeni bir talep otomatik olarak gönderildi:", + "notifications.agents.email.autoRequested4k": "Aşağıdaki {mediaType} için 4K'da yeni bir talep otomatik olarak gönderildi:", + "notifications.agents.email.availableRequest": "Aşağıdaki {mediaType} için talebiniz artık kullanıma hazır:", + "notifications.agents.email.availableRequest4k": "Aşağıdaki {mediaType} için 4K talebiniz artık kullanıma hazır:", + "notifications.agents.email.declinedRequest": "Aşağıdaki {mediaType} için talebiniz reddedildi:", + "notifications.agents.email.declinedRequest4k": "Aşağıdaki {mediaType} için 4K talebiniz reddedildi:", + "notifications.agents.email.failedRequest": "Aşağıdaki {mediaType} için talep {service} hizmetine eklenemedi:", + "notifications.agents.email.failedRequest4k": "Aşağıdaki {mediaType} için 4K talebi {service} hizmetine eklenemedi:", + "notifications.agents.email.issue": "sorun", + "notifications.agents.email.issueComment": "{userName}, {mediaType} {subject} için bildirilen {issueType} hakkında yorum yaptı:", + "notifications.agents.email.issueCreated": "{userName} tarafından {mediaType} {subject} için yeni bir {issueType} bildirildi:", + "notifications.agents.email.issueReopened": "{mediaType} {subject} için bildirilen {issueType}, {userName} tarafından yeniden açıldı.", + "notifications.agents.email.issueResolved": "{mediaType} {subject} için bildirilen {issueType}, {userName} tarafından çözüldü olarak işaretlendi!", + "notifications.agents.email.issueType": "{type} sorunu", + "notifications.agents.email.pendingRequest": "Aşağıdaki {mediaType} için yeni bir talep onay bekliyor:", + "notifications.agents.email.pendingRequest4k": "Aşağıdaki {mediaType} için 4K'da yeni bir talep onay bekliyor:", + "notifications.agents.webpush.approved": "{quality}{mediaType} talebiniz onaylandı.", + "notifications.agents.webpush.autoApproved": "{userName} kullanıcısından gelen yeni bir {quality}{mediaType} talebi otomatik olarak onaylandı.", + "notifications.agents.webpush.autoRequested": "Yeni bir {quality}{mediaType} talebi otomatik olarak gönderildi.", + "notifications.agents.webpush.available": "{quality}{mediaType} talebiniz artık kullanıma hazır!", + "notifications.agents.webpush.declined": "{quality}{mediaType} talebiniz reddedildi.", + "notifications.agents.webpush.failed": "{quality}{mediaType} talebi işlenemedi.", + "notifications.agents.webpush.issueComment": "{userName}, {issueType} hakkında yorum yaptı.", + "notifications.agents.webpush.issueCreated": "{userName} tarafından yeni bir {issueType} bildirildi.", + "notifications.agents.webpush.issueReopened": "{issueType}, {userName} tarafından yeniden açıldı.", + "notifications.agents.webpush.issueResolved": "{issueType}, {userName} tarafından çözüldü olarak işaretlendi!", + "notifications.agents.webpush.pending": "{userName} kullanıcısından gelen yeni bir {quality}{mediaType} talebi için onay gerekiyor.", + "notifications.agents.webpush.viewIssue": "Sorunu Görüntüle", + "notifications.agents.webpush.viewMedia": "Medyayı Görüntüle", + "notifications.common.available": "Kullanılabilir", + "notifications.common.commentFrom": "{userName} kullanıcısından yorum", + "notifications.common.declined": "Reddedildi", + "notifications.common.failed": "Başarısız", + "notifications.common.issue": "sorun", + "notifications.common.issueStatus": "Sorun Durumu", + "notifications.common.issueType": "Sorun Türü", + "notifications.common.issueTypeName": "{type} sorunu", + "notifications.common.movie": "film", + "notifications.common.open": "Aç", + "notifications.common.openIn": "{applicationTitle} uygulamasında aç", + "notifications.common.pendingApproval": "Onay Bekliyor", + "notifications.common.processing": "İşleniyor", + "notifications.common.reportedBy": "Bildiren", + "notifications.common.requestStatus": "Talep Durumu", + "notifications.common.requestedBy": "Talep Eden", + "notifications.common.resolved": "Çözüldü", + "notifications.common.series": "dizi", + "notifications.common.viewIssue": "{applicationTitle} uygulamasında sorunu görüntüle", + "notifications.common.viewMedia": "{applicationTitle} uygulamasında medyayı görüntüle", + "notifications.test.message": "Ses testi, 1, 2, 3. Bizi duyuyor musunuz?", + "notifications.test.subject": "Test Bildirimi" +} diff --git a/server/i18n/locale/uk.json b/server/i18n/locale/uk.json index 0967ef424b..60f162e08c 100644 --- a/server/i18n/locale/uk.json +++ b/server/i18n/locale/uk.json @@ -1 +1,27 @@ -{} +{ + "notifications.agents.email.approvedRequest": "Ваш запит на такий {mediaType} схвалено:", + "notifications.agents.email.approvedRequest4k": "Ваш запит на такий {mediaType} в 4K схвалено:", + "notifications.agents.email.autoApproved": "Новий запит для наступного {mediaType} було автоматично схвалено:", + "notifications.agents.email.autoApproved4k": "Новий запит для наступного {mediaType} в 4K було автоматично схвалено:", + "notifications.agents.email.autoRequested": "Новий запит для наступного {mediaType} було автоматично надіслано:", + "notifications.agents.email.autoRequested4k": "Новий запит для наступного {mediaType} в 4K було автоматично надіслано:", + "notifications.agents.email.availableRequest": "Ваш запит на {mediaType} тепер доступний:", + "notifications.agents.email.availableRequest4k": "Ваш запит на {mediaType} в 4K тепер доступний:", + "notifications.agents.email.declinedRequest": "Ваш запит на {mediaType} було відхилено:", + "notifications.agents.email.declinedRequest4k": "Ваш запит на {mediaType} в 4K було відхилено:", + "notifications.agents.email.failedRequest": "Не вдалося додати запит на {mediaType} до {service}:", + "notifications.agents.email.failedRequest4k": "Не вдалося додати запит на {mediaType} в 4K до {service}:", + "notifications.agents.email.issue": "питання", + "notifications.agents.email.issueComment": "{userName} прокоментував {issueType} для {mediaType} {subject}:", + "notifications.agents.email.issueCreated": "Користувач {userName} повідомив про новий {issueType} для {mediaType} {subject}:", + "notifications.agents.email.issueReopened": "{issueType} для {mediaType} {subject} було повторно відкрито користувачем {userName}.", + "notifications.agents.email.issueResolved": "Користувач {userName} позначив як вирішену проблему {issueType} для теми {mediaType} {subject}!", + "notifications.agents.email.issueType": "проблема {тип}", + "notifications.agents.email.pendingRequest": "Новий запит для наступного {mediaType} очікує на схвалення:", + "notifications.agents.email.pendingRequest4k": "Новий запит для наступного {mediaType} в 4K очікує на схвалення:", + "notifications.agents.webpush.approved": "Ваш запит на {quality}{mediaType} схвалено.", + "notifications.agents.webpush.autoApproved": "Автоматично схвалено новий запит {quality}{mediaType} від {userName}.", + "notifications.agents.webpush.autoRequested": "Автоматично надіслано новий запит {quality}{mediaType}.", + "notifications.agents.webpush.available": "Ваш запит на {quality}{mediaType} тепер доступний!", + "notifications.agents.webpush.declined": "Ваш запит {quality}{mediaType} було відхилено." +} diff --git a/server/index.ts b/server/index.ts index b27b517ea2..49b30ddf8b 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1,11 +1,12 @@ import csurf from '@dr.pogodin/csurf'; import PlexAPI from '@server/api/plexapi'; -import dataSource, { getRepository, isPgsql } from '@server/datasource'; +import dataSource, { getRepository } from '@server/datasource'; import DiscoverSlider from '@server/entity/DiscoverSlider'; import { Session } from '@server/entity/Session'; import { User } from '@server/entity/User'; import { initI18n } from '@server/i18n'; import { startJobs } from '@server/job/schedule'; +import { applyForkSchema } from '@server/lib/forkSchema'; import notificationManager from '@server/lib/notifications'; import DiscordAgent from '@server/lib/notifications/agents/discord'; import EmailAgent from '@server/lib/notifications/agents/email'; @@ -26,11 +27,15 @@ import avatarproxy from '@server/routes/avatarproxy'; import imageproxy from '@server/routes/imageproxy'; import { appDataPermissions } from '@server/utils/appDataVolume'; import { getAppVersion } from '@server/utils/appVersion'; -import createCustomProxyAgent from '@server/utils/customProxyAgent'; +import createCustomProxyAgent, { + setForceIpv4First, +} from '@server/utils/customProxyAgent'; +import { isPgsql } from '@server/utils/dbType'; +import { initDemoData } from '@server/utils/demoMode'; import { initializeDnsCache } from '@server/utils/dnsCache'; import restartFlag from '@server/utils/restartFlag'; +import '@server/utils/userAgent'; import { getClientIp } from '@supercharge/request-ip'; -import axios from 'axios'; import { TypeormStore } from 'connect-typeorm/out'; import cookieParser from 'cookie-parser'; import type { NextFunction, Request, Response } from 'express'; @@ -39,8 +44,6 @@ import * as OpenApiValidator from 'express-openapi-validator'; import type { Store } from 'express-session'; import session from 'express-session'; import fs from 'fs/promises'; -import http from 'http'; -import https from 'https'; import yaml from 'js-yaml'; import next from 'next'; import path from 'path'; @@ -70,12 +73,12 @@ app : await dataSource.initialize(); // Run migrations in production - if (process.env.NODE_ENV === 'production') { + if (process.env.NODE_ENV === 'production' || isPgsql) { if (isPgsql) { - await dbConnection.runMigrations(); + await applyForkSchema(dbConnection); } else { await dbConnection.query('PRAGMA foreign_keys=OFF'); - await dbConnection.runMigrations(); + await applyForkSchema(dbConnection); await dbConnection.query('PRAGMA foreign_keys=ON'); } } @@ -86,10 +89,7 @@ app initI18n(); - if (settings.network.forceIpv4First) { - axios.defaults.httpAgent = new http.Agent({ family: 4 }); - axios.defaults.httpsAgent = new https.Agent({ family: 4 }); - } + setForceIpv4First(settings.network.forceIpv4First); // Add DNS caching if (settings.network.dnsCache?.enabled) { @@ -124,7 +124,17 @@ app }); const plexapi = new PlexAPI({ plexToken: admin.plexToken }); - await plexapi.syncLibraries(); + + try { + await plexapi.syncLibraries(); + } catch { + // Leave the existing libraries untouched so the migration retries on + // the next startup instead of discarding the user's configuration + logger.warn( + 'Failed to migrate Plex libraries; will retry on next startup', + { label: 'Settings' } + ); + } } } @@ -245,6 +255,13 @@ app }; next(); }); + + // Init demo mode and catch some API routes + if (process.env.UNSAFE_DO_NOT_USE_DEMO === 'true') { + logger.info('Demo mode enabled, seeding database with demo data'); + await initDemoData(server); + } + server.use('/api/v1', routes); // Do not set cookies so CDNs can cache them @@ -257,10 +274,11 @@ app err: { status: number; message: string; errors: string[] }, _req: Request, res: Response, - // We must provide a next function for the function signature here even though its not used - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _next: NextFunction + next: NextFunction ) => { + if (res.headersSent) { + return next(err); + } // format error res.status(err.status || 500).json({ message: err.message, @@ -271,19 +289,27 @@ app const port = Number(process.env.PORT) || 5055; const host = process.env.HOST; + let httpServer; if (host) { - server.listen(port, host, () => { + httpServer = server.listen(port, host, () => { logger.info(`Server ready on ${host} port ${port}`, { label: 'Server', }); }); } else { - server.listen(port, () => { + httpServer = server.listen(port, () => { logger.info(`Server ready on port ${port}`, { label: 'Server', }); }); } + httpServer.on('error', (err) => { + logger.error('Failed to start server', { + label: 'Server', + message: err.message, + }); + process.exit(1); + }); }) .catch((err) => { logger.error(err.stack); diff --git a/server/interfaces/api/blocklistInterfaces.ts b/server/interfaces/api/blocklistInterfaces.ts index 58e3ebe474..ae1512feee 100644 --- a/server/interfaces/api/blocklistInterfaces.ts +++ b/server/interfaces/api/blocklistInterfaces.ts @@ -3,7 +3,7 @@ import type { PaginatedResponse } from '@server/interfaces/api/common'; export interface BlocklistItem { tmdbId: number; - mediaType: 'movie' | 'tv'; + mediaType: 'movie' | 'tv' | 'book'; title?: string; createdAt?: Date; user?: User; diff --git a/server/interfaces/api/discoverInterfaces.ts b/server/interfaces/api/discoverInterfaces.ts index 6738cbb5fd..5f60ac6cae 100644 --- a/server/interfaces/api/discoverInterfaces.ts +++ b/server/interfaces/api/discoverInterfaces.ts @@ -8,7 +8,7 @@ export interface WatchlistItem { id: number; ratingKey: string; tmdbId: number; - mediaType: 'movie' | 'tv'; + mediaType: 'movie' | 'tv' | 'book'; title: string; } diff --git a/server/interfaces/api/issueInterfaces.ts b/server/interfaces/api/issueInterfaces.ts index e5b3643cd0..136807d56f 100644 --- a/server/interfaces/api/issueInterfaces.ts +++ b/server/interfaces/api/issueInterfaces.ts @@ -1,6 +1,16 @@ +import type { IssueType } from '@server/constants/issue'; import type Issue from '@server/entity/Issue'; import type { PaginatedResponse } from './common'; export interface IssueResultsResponse extends PaginatedResponse { results: Issue[]; } + +export type IssueRequestBody = { + message: string; + mediaId: number; + issueType: IssueType; + problemSeason?: number; + problemEpisode?: number; + userId?: number; +}; diff --git a/server/interfaces/api/requestInterfaces.ts b/server/interfaces/api/requestInterfaces.ts index 9b9c629954..a7e2dceca6 100644 --- a/server/interfaces/api/requestInterfaces.ts +++ b/server/interfaces/api/requestInterfaces.ts @@ -5,6 +5,7 @@ import type { NonFunctionProperties, PaginatedResponse } from './common'; export interface RequestResultsResponse extends PaginatedResponse { results: (NonFunctionProperties & { profileName?: string; + metadataProfileName?: string; canRemove?: boolean; })[]; serviceErrors: { @@ -21,9 +22,11 @@ export type MediaRequestBody = { is4k?: boolean; serverId?: number; profileId?: number; + metadataProfileId?: number; profileName?: string; rootFolder?: string; languageProfileId?: number; userId?: number; tags?: number[]; + ignoreQuota?: boolean; }; diff --git a/server/interfaces/api/serviceInterfaces.ts b/server/interfaces/api/serviceInterfaces.ts index 3b430b0b59..53b9b86868 100644 --- a/server/interfaces/api/serviceInterfaces.ts +++ b/server/interfaces/api/serviceInterfaces.ts @@ -1,4 +1,5 @@ import type { QualityProfile, RootFolder, Tag } from '@server/api/servarr/base'; +import type { MetadataProfile } from '@server/api/servarr/readarr'; import type { LanguageProfile } from '@server/api/servarr/sonarr'; export interface ServiceCommonServer { @@ -12,6 +13,7 @@ export interface ServiceCommonServer { activeAnimeProfileId?: number; activeAnimeDirectory?: string; activeAnimeLanguageProfileId?: number; + activeMetadataProfileId?: number; activeTags: number[]; activeAnimeTags?: number[]; } @@ -21,5 +23,6 @@ export interface ServiceCommonServerWithDetails { profiles: QualityProfile[]; rootFolders: Partial[]; languageProfiles?: LanguageProfile[]; + metadataProfiles?: MetadataProfile[]; tags: Tag[]; } diff --git a/server/interfaces/api/settingsInterfaces.ts b/server/interfaces/api/settingsInterfaces.ts index 3dfa619205..f4e1a2c4a0 100644 --- a/server/interfaces/api/settingsInterfaces.ts +++ b/server/interfaces/api/settingsInterfaces.ts @@ -31,16 +31,20 @@ export interface PublicSettingsResponse { applicationUrl: string; hideAvailable: boolean; hideBlocklisted: boolean; + hideRequested: boolean; localLogin: boolean; mediaServerLogin: boolean; movie4kEnabled: boolean; series4kEnabled: boolean; + bookAudioEnabled: boolean; discoverRegion: string; streamingRegion: string; originalLanguage: string; mediaServerType: number; partialRequestsEnabled: boolean; enableSpecialEpisodes: boolean; + syncBookFormatRequests: boolean; + seriesRequestsEnabled: boolean; cacheImages: boolean; vapidPublic: string; enablePushRegistration: boolean; @@ -48,6 +52,7 @@ export interface PublicSettingsResponse { emailEnabled: boolean; newPlexLogin: boolean; youtubeUrl: string; + versionCheck: boolean; plexClientIdentifier: string; } @@ -65,7 +70,10 @@ export interface CacheItem { export interface CacheResponse { apiCaches: CacheItem[]; - imageCache: Record<'tmdb' | 'avatar', { size: number; imageCount: number }>; + imageCache: Record< + 'tmdb' | 'avatar' | 'hardcover', + { size: number; imageCount: number } + >; dnsCache: { stats: DnsStats | undefined; entries: DnsEntries | undefined; @@ -75,7 +83,7 @@ export interface CacheResponse { export interface StatusResponse { version: string; commitTag: string; - updateAvailable: boolean; - commitsBehind: number; + updateAvailable?: boolean; + commitsBehind?: number; restartRequired: boolean; } diff --git a/server/interfaces/api/userInterfaces.ts b/server/interfaces/api/userInterfaces.ts index 2ac75c5e1f..3503c9f02f 100644 --- a/server/interfaces/api/userInterfaces.ts +++ b/server/interfaces/api/userInterfaces.ts @@ -22,6 +22,7 @@ export interface QuotaStatus { export interface QuotaResponse { movie: QuotaStatus; tv: QuotaStatus; + book: QuotaStatus; } export interface UserWatchDataResponse { diff --git a/server/interfaces/api/userSettingsInterfaces.ts b/server/interfaces/api/userSettingsInterfaces.ts index 327764618e..ad4f098d1e 100644 --- a/server/interfaces/api/userSettingsInterfaces.ts +++ b/server/interfaces/api/userSettingsInterfaces.ts @@ -3,7 +3,6 @@ import type { NotificationAgentKey } from '@server/lib/settings'; export interface UserSettingsGeneralResponse { username?: string; email?: string; - discordId?: string; locale?: string; discoverRegion?: string; streamingRegion?: string; @@ -12,10 +11,14 @@ export interface UserSettingsGeneralResponse { movieQuotaDays?: number; tvQuotaLimit?: number; tvQuotaDays?: number; + bookQuotaLimit?: number; + bookQuotaDays?: number; globalMovieQuotaDays?: number; globalMovieQuotaLimit?: number; globalTvQuotaLimit?: number; globalTvQuotaDays?: number; + globalBookQuotaLimit?: number; + globalBookQuotaDays?: number; watchlistSyncMovies?: boolean; watchlistSyncTv?: boolean; } @@ -26,7 +29,7 @@ export interface UserSettingsNotificationsResponse { pgpKey?: string; discordEnabled?: boolean; discordEnabledTypes?: number; - discordId?: string; + discordIds?: string[]; pushbulletAccessToken?: string; pushoverApplicationToken?: string; pushoverUserKey?: string; diff --git a/server/job/blocklistedTagsProcessor.ts b/server/job/blocklistedTagsProcessor.ts index 038b242b52..d4cbf9e881 100644 --- a/server/job/blocklistedTagsProcessor.ts +++ b/server/job/blocklistedTagsProcessor.ts @@ -1,5 +1,7 @@ -import type { SortOptions } from '@server/api/themoviedb'; -import { SortOptionsIterable } from '@server/api/themoviedb'; +import { + MovieSortOptionsIterable, + TvSortOptionsIterable, +} from '@server/api/themoviedb'; import type { TmdbSearchMovieResponse, TmdbSearchTvResponse, @@ -80,68 +82,40 @@ class BlocklistedTagProcessor implements RunnableScanner { // The maximum number of queries we're expected to execute this.total = - 2 * blocklistedTagsArr.length * pageLimit * SortOptionsIterable.length; + blocklistedTagsArr.length * + pageLimit * + (MovieSortOptionsIterable.length + TvSortOptionsIterable.length); - for (const type of [MediaType.MOVIE, MediaType.TV]) { - const getDiscover = - type === MediaType.MOVIE ? tmdb.getDiscoverMovies : tmdb.getDiscoverTv; + for (const tag of blocklistedTagsArr) { + const keywordDetails = await tmdb.getKeywordDetails({ + keywordId: Number(tag), + }); - // Iterate for each tag - for (const tag of blocklistedTagsArr) { - const keywordDetails = await tmdb.getKeywordDetails({ - keywordId: Number(tag), + if (keywordDetails === null) { + logger.warn('Skipping invalid keyword in blocklisted tags', { + label: 'Blocklisted Tags Processor', + keywordId: tag, }); - - if (keywordDetails === null) { - logger.warn('Skipping invalid keyword in blocklisted tags', { - label: 'Blocklisted Tags Processor', - keywordId: tag, - }); - invalidKeywords.add(tag); - continue; - } - - let queryMax = pageLimit * SortOptionsIterable.length; - let fixedSortMode = false; // Set to true when the page limit allows for getting every page of tag - - for (let query = 0; query < queryMax; query++) { - const page: number = fixedSortMode - ? query + 1 - : (query % pageLimit) + 1; - const sortBy: SortOptions | undefined = fixedSortMode - ? undefined - : SortOptionsIterable[query % SortOptionsIterable.length]; - - if (!this.running) { - throw new AbortTransaction(); - } - - try { - const response = await getDiscover({ - page, - sortBy, - keywords: tag, - }); - - await this.processResults(response, tag, type, em); - await new Promise((res) => setTimeout(res, TMDB_API_DELAY_MS)); - - this.progress++; - if (page === 1 && response.total_pages <= queryMax) { - // We will finish the tag with less queries than expected, move progress accordingly - this.progress += queryMax - response.total_pages; - fixedSortMode = true; - queryMax = response.total_pages; - } - } catch (error) { - logger.error('Error processing keyword in blocklisted tags', { - label: 'Blocklisted Tags Processor', - keywordId: tag, - errorMessage: error.message, - }); - } - } + invalidKeywords.add(tag); + continue; } + + await this.processTagForType( + MediaType.MOVIE, + tag, + em, + tmdb.getDiscoverMovies, + MovieSortOptionsIterable, + pageLimit + ); + await this.processTagForType( + MediaType.TV, + tag, + em, + tmdb.getDiscoverTv, + TvSortOptionsIterable, + pageLimit + ); } if (invalidKeywords.size > 0) { @@ -163,6 +137,57 @@ class BlocklistedTagProcessor implements RunnableScanner { } } + private async processTagForType( + type: MediaType, + tag: string, + em: EntityManager, + getDiscover: (options: { + page: number; + sortBy?: S; + keywords: string; + }) => Promise, + sortOptions: readonly S[], + pageLimit: number + ): Promise { + let queryMax = pageLimit * sortOptions.length; + let fixedSortMode = false; + + for (let query = 0; query < queryMax; query++) { + const page: number = fixedSortMode + ? query + 1 + : Math.floor(query / sortOptions.length) + 1; + const sortBy: S | undefined = fixedSortMode + ? undefined + : sortOptions[query % sortOptions.length]; + + if (!this.running) { + throw new AbortTransaction(); + } + + try { + const response = await getDiscover({ page, sortBy, keywords: tag }); + + await this.processResults(response, tag, type, em); + await new Promise((res) => setTimeout(res, TMDB_API_DELAY_MS)); + + this.progress++; + if (page === 1 && response.total_pages <= queryMax) { + this.progress += queryMax - response.total_pages; + fixedSortMode = true; + queryMax = response.total_pages; + // restart the counter so sequential paging resumes at page 2 even if earlier queries failed + query = 0; + } + } catch (error) { + logger.error('Error processing keyword in blocklisted tags', { + label: 'Blocklisted Tags Processor', + keywordId: tag, + errorMessage: error.message, + }); + } + } + } + private async processResults( response: TmdbSearchMovieResponse | TmdbSearchTvResponse, keywordId: string, diff --git a/server/job/schedule.ts b/server/job/schedule.ts index a9afd2f4d6..d28831e766 100644 --- a/server/job/schedule.ts +++ b/server/job/schedule.ts @@ -10,6 +10,10 @@ import { } from '@server/lib/scanners/jellyfin'; import { plexFullScanner, plexRecentScanner } from '@server/lib/scanners/plex'; import { radarrScanner } from '@server/lib/scanners/radarr'; +import { + readarrRecentScanner, + readarrScanner, +} from '@server/lib/scanners/readarr'; import { sonarrScanner } from '@server/lib/scanners/sonarr'; import type { JobId } from '@server/lib/settings'; import { getSettings } from '@server/lib/settings'; @@ -177,7 +181,42 @@ export const startJobs = (): void => { cancelFn: () => sonarrScanner.cancel(), }); - // Checks if media is still available in plex/sonarr/radarr libs + // Run readarr recently added scan every 10 minutes + scheduledJobs.push({ + id: 'readarr-recently-added-scan', + name: 'Readarr Recently Added Scan', + type: 'process', + interval: 'minutes', + cronSchedule: jobs['readarr-recently-added-scan'].schedule, + job: schedule.scheduleJob( + jobs['readarr-recently-added-scan'].schedule, + () => { + logger.info('Starting scheduled job: Readarr Recently Added Scan', { + label: 'Jobs', + }); + readarrRecentScanner.run(); + } + ), + running: () => readarrRecentScanner.status().running, + cancelFn: () => readarrRecentScanner.cancel(), + }); + + // Run full readarr scan every 24 hours + scheduledJobs.push({ + id: 'readarr-scan', + name: 'Readarr Scan', + type: 'process', + interval: 'hours', + cronSchedule: jobs['readarr-scan'].schedule, + job: schedule.scheduleJob(jobs['readarr-scan'].schedule, () => { + logger.info('Starting scheduled job: Readarr Scan', { label: 'Jobs' }); + readarrScanner.run(); + }), + running: () => readarrScanner.status().running, + cancelFn: () => readarrScanner.cancel(), + }); + + // Checks if media is still available in plex/sonarr/radarr/readarr libs scheduledJobs.push({ id: 'availability-sync', name: 'Media Availability Sync', diff --git a/server/lib/availabilitySync.test.ts b/server/lib/availabilitySync.test.ts index cbfa74d809..7e861e38c5 100644 --- a/server/lib/availabilitySync.test.ts +++ b/server/lib/availabilitySync.test.ts @@ -8,15 +8,28 @@ import type { import JellyfinAPI from '@server/api/jellyfin'; import type { PlexMetadata } from '@server/api/plexapi'; import PlexAPI from '@server/api/plexapi'; +import type { RadarrMovie } from '@server/api/servarr/radarr'; +import RadarrAPI from '@server/api/servarr/radarr'; import type { SonarrSeason, SonarrSeries } from '@server/api/servarr/sonarr'; import SonarrAPI from '@server/api/servarr/sonarr'; -import { MediaStatus, MediaType } from '@server/constants/media'; +import TheMovieDb from '@server/api/themoviedb'; +import type { + TmdbTvDetails, + TmdbTvSeasonResult, +} from '@server/api/themoviedb/interfaces'; +import { + MediaRequestStatus, + MediaStatus, + MediaType, +} from '@server/constants/media'; import { MediaServerType } from '@server/constants/server'; import { getRepository } from '@server/datasource'; import Media from '@server/entity/Media'; +import MediaRequest from '@server/entity/MediaRequest'; import Season from '@server/entity/Season'; import { User } from '@server/entity/User'; -import type { SonarrSettings } from '@server/lib/settings'; +import availabilitySync from '@server/lib/availabilitySync'; +import type { RadarrSettings, SonarrSettings } from '@server/lib/settings'; import { getSettings } from '@server/lib/settings'; import { setupTestDb } from '@server/test/db'; @@ -117,7 +130,105 @@ Object.defineProperty(SonarrAPI.prototype, 'getSeriesById', { configurable: true, }); -import availabilitySync from '@server/lib/availabilitySync'; +// --- Mock RadarrAPI --- +let getMovieImpl: (id: number) => Promise = async () => { + throw new Error('404'); +}; + +Object.defineProperty(RadarrAPI.prototype, 'getMovie', { + get() { + return async ({ id }: { id: number }) => getMovieImpl(id); + }, + set() {}, + configurable: true, +}); + +// --- Mock TheMovieDb --- +let getTvShowImpl: (args: { + tvId: number; + language?: string; +}) => Promise = async () => fakeTmdbShow(1); +let getShowByTvdbIdImpl: (args: { + tvdbId: number; + language?: string; +}) => Promise = async () => fakeTmdbShow(1); + +Object.defineProperty(TheMovieDb.prototype, 'getTvShowForScan', { + get() { + return async (args: { tvId: number; language?: string }) => + getTvShowImpl(args); + }, + set() {}, + configurable: true, +}); + +Object.defineProperty(TheMovieDb.prototype, 'getShowByTvdbIdForScan', { + get() { + return async (args: { tvdbId: number; language?: string }) => + getShowByTvdbIdImpl(args); + }, + set() {}, + configurable: true, +}); + +// getTvShowForScan is assigned in the constructor, so the prototype stub misses +// the instance availabilitySync built when it was first imported +Object.defineProperty(availabilitySync.tmdb, 'getTvShowForScan', { + value: async (args: { tvId: number; language?: string }) => + getTvShowImpl(args), + configurable: true, +}); + +// --- Helpers --- + +function fakeTmdbShow( + tmdbId: number, + seasons: TmdbTvSeasonResult[] = [ + { + id: 1, + air_date: '2024-01-01', + episode_count: 10, + name: 'Season 1', + overview: '', + season_number: 1, + }, + ] +): TmdbTvDetails { + return { + id: tmdbId, + content_ratings: { results: [] }, + created_by: [], + episode_run_time: [], + first_air_date: '2024-01-01', + genres: [], + homepage: '', + in_production: false, + languages: ['en'], + last_air_date: '2024-01-01', + name: 'Test Show', + networks: [], + number_of_episodes: 10, + number_of_seasons: seasons.length, + origin_country: ['US'], + original_language: 'en', + original_name: 'Test Show', + overview: '', + popularity: 0, + production_companies: [], + production_countries: [], + spoken_languages: [], + seasons, + status: 'Ended', + type: 'Scripted', + vote_average: 0, + vote_count: 0, + aggregate_credits: { cast: [] }, + credits: { crew: [] }, + external_ids: {}, + keywords: { results: [] }, + videos: { results: [] }, + }; +} setupTestDb(); @@ -150,6 +261,33 @@ function configureSonarr(overrides: Partial[] = [{}]): void { settings.radarr = []; } +function configureRadarr(overrides: Partial[] = [{}]): void { + const settings = getSettings(); + settings.radarr = overrides.map((o, i) => ({ + id: i, + name: `Radarr ${i}`, + hostname: 'localhost', + port: 7878, + apiKey: 'test-key', + baseUrl: '', + useSsl: false, + activeProfileId: 1, + activeProfileName: 'Default', + activeDirectory: '/movies', + minimumAvailability: 'released', + tags: [], + is4k: false, + isDefault: i === 0, + syncEnabled: true, + preventSearch: false, + tagRequests: false, + overrideRule: [], + externalUrl: '', + ...o, + })) as RadarrSettings[]; + settings.sonarr = []; +} + function configureJellyfin(): void { const settings = getSettings(); settings.main.mediaServerType = MediaServerType.JELLYFIN; @@ -282,8 +420,8 @@ function fakeSonarrSeasons( monitored: true, statistics: { episodeFileCount: seasonsWithFiles[i + 1] ?? 0, - totalEpisodeCount: 22, - episodeCount: 22, + totalEpisodeCount: 10, + episodeCount: 10, percentOfEpisodes: seasonsWithFiles[i + 1] ? 100 : 0, sizeOnDisk: seasonsWithFiles[i + 1] ? 7516192768 : 0, previousAiring: undefined, @@ -304,6 +442,33 @@ describe('AvailabilitySync', () => { getSeriesByIdImpl = async () => { throw new Error('404'); }; + getMovieImpl = async () => { + throw new Error('404'); + }; + getTvShowImpl = async ({ tvId }) => + fakeTmdbShow( + tvId, + Array.from({ length: 4 }, (_, i) => ({ + id: i + 1, + air_date: '2024-01-01', + episode_count: 10, + name: `Season ${i + 1}`, + overview: '', + season_number: i + 1, + })) + ); + getShowByTvdbIdImpl = async ({ tvdbId }) => + fakeTmdbShow( + tvdbId, + Array.from({ length: 4 }, (_, i) => ({ + id: i + 1, + air_date: '2024-01-01', + episode_count: 10, + name: `Season ${i + 1}`, + overview: '', + season_number: i + 1, + })) + ); const userRepository = getRepository(User); const existingAdmin = await userRepository.findOne({ where: { id: 1 } }); @@ -363,7 +528,7 @@ describe('AvailabilitySync', () => { getEpisodesImpl = async (_seriesID: string, seasonID: string) => { if (seasonID === 'jellyfin-season-6-id') { - return fakeJellyfinEpisodes(21); + return fakeJellyfinEpisodes(10); } return []; }; @@ -378,13 +543,13 @@ describe('AvailabilitySync', () => { monitored: true, statistics: { episodeFileCount: 21, - totalEpisodeCount: 177, - episodeCount: 177, - percentOfEpisodes: 11.86, + totalEpisodeCount: 10, + episodeCount: 10, + percentOfEpisodes: 100, sizeOnDisk: 0, seasonCount: 8, }, - seasons: fakeSonarrSeasons(8, { 6: 21 }), + seasons: fakeSonarrSeasons(8, { 6: 10 }), } as unknown as SonarrSeries; } throw new Error('404'); @@ -688,6 +853,106 @@ describe('AvailabilitySync', () => { 'Show should remain AVAILABLE when getEpisodes fails' ); }); + + it('should mark show as PARTIALLY_AVAILABLE when some seasons are available and some are unknown', async () => { + configureJellyfin(); + configureSonarr([{ syncEnabled: true }]); + + const mediaRepository = getRepository(Media); + + const media = new Media(); + media.tmdbId = 1412; + media.mediaType = MediaType.TV; + media.status = MediaStatus.AVAILABLE; + media.jellyfinMediaId = 'jellyfin-partial-id'; + media.externalServiceId = 103; + media.seasons = [ + new Season({ + seasonNumber: 1, + status: MediaStatus.AVAILABLE, + status4k: MediaStatus.UNKNOWN, + }), + new Season({ + seasonNumber: 2, + status: MediaStatus.AVAILABLE, + status4k: MediaStatus.UNKNOWN, + }), + new Season({ + seasonNumber: 3, + status: MediaStatus.UNKNOWN, + status4k: MediaStatus.UNKNOWN, + }), + new Season({ + seasonNumber: 4, + status: MediaStatus.UNKNOWN, + status4k: MediaStatus.UNKNOWN, + }), + ]; + + await mediaRepository.save(media); + + getItemDataImpl = async (id: string) => { + if (id === 'jellyfin-partial-id') { + return fakeJellyfinShow('jellyfin-partial-id', '1412'); + } + return undefined; + }; + + getSeasonsImpl = async (seriesID: string) => { + if (seriesID === 'jellyfin-partial-id') { + return [ + fakeJellyfinSeason(1, 'jellyfin-partial-s1-id'), + fakeJellyfinSeason(2, 'jellyfin-partial-s2-id'), + ]; + } + return []; + }; + + getEpisodesImpl = async (_seriesID: string, seasonID: string) => { + if (seasonID === 'jellyfin-partial-s1-id') { + return fakeJellyfinEpisodes(10); + } + if (seasonID === 'jellyfin-partial-s2-id') { + return fakeJellyfinEpisodes(10); + } + return []; + }; + + getSeriesByIdImpl = async (id: number) => { + if (id === 103) { + return { + tvdbId: 99997, + id: 103, + title: 'Partial Show', + titleSlug: 'partial-show', + monitored: true, + statistics: { + episodeFileCount: 20, + totalEpisodeCount: 40, + episodeCount: 40, + percentOfEpisodes: 50, + sizeOnDisk: 0, + seasonCount: 4, + }, + seasons: fakeSonarrSeasons(4, { 1: 10, 2: 10 }), + } as unknown as SonarrSeries; + } + throw new Error('404'); + }; + + await availabilitySync.run(); + + const updated = await mediaRepository.findOneOrFail({ + where: { tmdbId: 1412 }, + relations: ['seasons'], + }); + + assert.strictEqual( + updated.status, + MediaStatus.PARTIALLY_AVAILABLE, + 'Show should be PARTIALLY_AVAILABLE when some seasons are available and some are unknown' + ); + }); }); describe('TV season availability - Plex', () => { @@ -792,6 +1057,86 @@ describe('AvailabilitySync', () => { ); }); + it('should mark a deleted show as DELETED when a second standard Sonarr instance has a colliding externalServiceId', async () => { + configurePlex(); + configureSonarr([{ syncEnabled: true }, { syncEnabled: true }]); + + const mediaRepository = getRepository(Media); + + const media = new Media(); + media.tmdbId = 3000; + media.tvdbId = 73255; + media.mediaType = MediaType.TV; + media.status = MediaStatus.AVAILABLE; + media.ratingKey = 'gone-from-plex-rk'; + media.externalServiceId = 200; + media.serviceId = 0; + media.seasons = [ + new Season({ + seasonNumber: 1, + status: MediaStatus.AVAILABLE, + status4k: MediaStatus.UNKNOWN, + }), + new Season({ + seasonNumber: 2, + status: MediaStatus.AVAILABLE, + status4k: MediaStatus.UNKNOWN, + }), + ]; + + await mediaRepository.save(media); + + // Probed once per standard instance with the same id (200): origin 404s + // (deleted); the other instance has a different series at 200. + let sonarrCall = 0; + getSeriesByIdImpl = async (id: number) => { + if (id !== 200) { + throw new Error('404'); + } + sonarrCall += 1; + if (sonarrCall === 1) { + throw new Error('404'); + } + return { + tvdbId: 999999, + id: 200, + title: 'Unrelated Colliding Series', + titleSlug: 'unrelated-colliding-series', + monitored: true, + statistics: { + episodeFileCount: 12, + totalEpisodeCount: 12, + episodeCount: 12, + percentOfEpisodes: 100, + sizeOnDisk: 0, + seasonCount: 1, + }, + seasons: fakeSonarrSeasons(1, { 1: 12 }), + } as unknown as SonarrSeries; + }; + + await availabilitySync.run(); + + const updated = await mediaRepository.findOneOrFail({ + where: { tmdbId: 3000 }, + relations: ['seasons'], + }); + + for (const season of updated.seasons) { + assert.strictEqual( + season.status, + MediaStatus.DELETED, + `Season ${season.seasonNumber} should be DELETED (collision must not keep it available) but was ${season.status}` + ); + } + + assert.strictEqual( + updated.status, + MediaStatus.DELETED, + 'Show deleted from its origin instance and Plex must not be kept alive by a colliding externalServiceId on another standard instance' + ); + }); + it('should assume season exists when getChildrenMetadata fails for episodes (safe fallback)', async () => { configurePlex(); configureSonarr([{ syncEnabled: true }]); @@ -959,4 +1304,1015 @@ describe('AvailabilitySync', () => { ); }); }); + + describe('movie availability - Radarr', () => { + it('should mark a deleted movie as DELETED when a second standard Radarr instance has a colliding externalServiceId', async () => { + configurePlex(); + configureRadarr([{ syncEnabled: true }, { syncEnabled: true }]); + + const mediaRepository = getRepository(Media); + + const media = new Media(); + media.tmdbId = 5000; + media.mediaType = MediaType.MOVIE; + media.status = MediaStatus.AVAILABLE; + media.ratingKey = 'gone-from-plex-rk'; + media.externalServiceId = 300; + media.serviceId = 0; + + await mediaRepository.save(media); + + // Probed once per standard instance with the same id (300): origin 404s + // (deleted); the other instance has a different movie at 300. + let radarrCall = 0; + getMovieImpl = async (id: number) => { + if (id !== 300) { + throw new Error('404'); + } + radarrCall += 1; + if (radarrCall === 1) { + throw new Error('404'); + } + return { + id: 300, + tmdbId: 999999, + title: 'Unrelated Colliding Movie', + hasFile: true, + } as unknown as RadarrMovie; + }; + + await availabilitySync.run(); + + const updated = await mediaRepository.findOneOrFail({ + where: { tmdbId: 5000 }, + }); + + assert.strictEqual( + updated.status, + MediaStatus.DELETED, + 'Movie deleted from its origin instance and Plex must not be kept alive by a colliding externalServiceId on another standard instance' + ); + }); + }); + + describe('movie availability - Plex 4K detection', () => { + function fakePlexMovie(ratingKey: string, widths: number[]): PlexMetadata { + return { + ratingKey, + guid: `plex://movie/${ratingKey}`, + type: 'movie', + title: 'Test Movie', + Guid: [], + index: 1, + leafCount: 0, + viewedLeafCount: 0, + addedAt: 0, + updatedAt: 0, + Media: widths.map((width, i) => ({ + id: i, + duration: 7200, + bitrate: 20000, + width, + height: width >= 2000 ? 2160 : 1080, + aspectRatio: 1.78, + audioChannels: 6, + audioCodec: 'eac3', + videoCodec: width >= 2000 ? 'hevc' : 'h264', + videoResolution: width >= 2000 ? '4k' : '1080', + container: 'mkv', + videoFrameRate: '24p', + videoProfile: 'main 10', + })), + }; + } + + it('should keep the 4K status when both statuses share a Plex item that has a 4K version (merged versions)', async () => { + configurePlex(); + configureRadarr([ + { syncEnabled: true }, + { is4k: true, syncEnabled: true }, + ]); + + const mediaRepository = getRepository(Media); + + // The Plex scanner writes the same rating key to both versions of a + // merged item, and a prior deletion left no externalServiceId4k to + // query, so retention rides on the Plex check alone. + const media = new Media(); + media.tmdbId = 687163; + media.mediaType = MediaType.MOVIE; + media.status = MediaStatus.AVAILABLE; + media.status4k = MediaStatus.AVAILABLE; + media.ratingKey = 'merged-versions-rk'; + media.ratingKey4k = 'merged-versions-rk'; + await mediaRepository.save(media); + + getMetadataImpl = async (key: string) => { + if (key === 'merged-versions-rk') { + return fakePlexMovie('merged-versions-rk', [1920, 3840]); + } + throw new Error('404'); + }; + + await availabilitySync.run(); + + const updated = await mediaRepository.findOneOrFail({ + where: { tmdbId: 687163 }, + }); + + assert.strictEqual( + updated.status4k, + MediaStatus.AVAILABLE, + 'A 4K version present on the media server must prevent 4K removal even when both statuses share one rating key' + ); + assert.strictEqual( + updated.status, + MediaStatus.AVAILABLE, + 'The non-4K status must be unaffected' + ); + }); + + it('should still remove a stale 4K status when the shared Plex item has no 4K version', async () => { + configurePlex(); + configureRadarr([ + { syncEnabled: true }, + { is4k: true, syncEnabled: true }, + ]); + + const mediaRepository = getRepository(Media); + + const media = new Media(); + media.tmdbId = 687164; + media.mediaType = MediaType.MOVIE; + media.status = MediaStatus.AVAILABLE; + media.status4k = MediaStatus.AVAILABLE; + media.ratingKey = 'stale-4k-rk'; + media.ratingKey4k = 'stale-4k-rk'; + await mediaRepository.save(media); + + getMetadataImpl = async (key: string) => { + if (key === 'stale-4k-rk') { + return fakePlexMovie('stale-4k-rk', [1920]); + } + throw new Error('404'); + }; + + await availabilitySync.run(); + + const updated = await mediaRepository.findOneOrFail({ + where: { tmdbId: 687164 }, + }); + + assert.strictEqual( + updated.status4k, + MediaStatus.DELETED, + 'A phantom 4K status on a 1080p-only item must still be removed' + ); + assert.strictEqual( + updated.status, + MediaStatus.AVAILABLE, + 'The non-4K status must be kept' + ); + }); + + it('should keep a 4K-only movie referenced only by ratingKey4k', async () => { + configurePlex(); + configureRadarr([ + { syncEnabled: true }, + { is4k: true, syncEnabled: true }, + ]); + + const mediaRepository = getRepository(Media); + + const media = new Media(); + media.tmdbId = 687165; + media.mediaType = MediaType.MOVIE; + media.status = MediaStatus.UNKNOWN; + media.status4k = MediaStatus.AVAILABLE; + media.ratingKey4k = 'pure-4k-rk'; + await mediaRepository.save(media); + + getMetadataImpl = async (key: string) => { + if (key === 'pure-4k-rk') { + return fakePlexMovie('pure-4k-rk', [3840]); + } + throw new Error('404'); + }; + + await availabilitySync.run(); + + const updated = await mediaRepository.findOneOrFail({ + where: { tmdbId: 687165 }, + }); + + assert.strictEqual( + updated.status4k, + MediaStatus.AVAILABLE, + 'A 4K-only item with a distinct rating key must be kept' + ); + }); + + it('should not remove a 4K movie when Plex has not yet analyzed the file (empty Media array)', async () => { + configurePlex(); + configureRadarr([{ is4k: true, syncEnabled: true }]); + + const mediaRepository = getRepository(Media); + + const media = new Media(); + media.tmdbId = 687166; + media.mediaType = MediaType.MOVIE; + media.status = MediaStatus.UNKNOWN; + media.status4k = MediaStatus.AVAILABLE; + media.ratingKey4k = 'analyzing-rk'; + await mediaRepository.save(media); + + getMetadataImpl = async (key: string) => { + if (key === 'analyzing-rk') { + return fakePlexMovie('analyzing-rk', []); + } + throw new Error('404'); + }; + + await availabilitySync.run(); + + const updated = await mediaRepository.findOneOrFail({ + where: { tmdbId: 687166 }, + }); + + assert.strictEqual( + updated.status4k, + MediaStatus.AVAILABLE, + 'Metadata without analyzed versions proves nothing about the file and must not trigger removal' + ); + }); + + it('should remove the non-4K status when the shared Plex item only has a 4K version left', async () => { + configurePlex(); + configureRadarr([ + { syncEnabled: true }, + { is4k: true, syncEnabled: true }, + ]); + + const mediaRepository = getRepository(Media); + + // Merged item whose non-4K file was removed from disk: the item still + // exists in Plex, but only with the 4K version. + const media = new Media(); + media.tmdbId = 687170; + media.mediaType = MediaType.MOVIE; + media.status = MediaStatus.AVAILABLE; + media.status4k = MediaStatus.AVAILABLE; + media.ratingKey = 'shared-4k-only-rk'; + media.ratingKey4k = 'shared-4k-only-rk'; + await mediaRepository.save(media); + + getMetadataImpl = async (key: string) => { + if (key === 'shared-4k-only-rk') { + return fakePlexMovie('shared-4k-only-rk', [3840]); + } + throw new Error('404'); + }; + + await availabilitySync.run(); + + const updated = await mediaRepository.findOneOrFail({ + where: { tmdbId: 687170 }, + }); + + assert.strictEqual( + updated.status, + MediaStatus.DELETED, + 'The non-4K status must be removed when the shared item no longer has a non-4K version' + ); + assert.strictEqual( + updated.status4k, + MediaStatus.AVAILABLE, + 'The 4K status must be kept' + ); + }); + }); + + describe('movie deletion metadata handling', () => { + it('should keep service metadata when deleting a 4K movie with an approved 4K request in flight', async () => { + configurePlex(); + configureRadarr([{ is4k: true, syncEnabled: true }]); + + const mediaRepository = getRepository(Media); + + const media = new Media(); + media.tmdbId = 687167; + media.mediaType = MediaType.MOVIE; + media.status = MediaStatus.UNKNOWN; + media.status4k = MediaStatus.AVAILABLE; + media.ratingKey4k = 'req-in-flight-rk'; + media.serviceId4k = 0; + media.externalServiceId4k = 512; + media.externalServiceSlug4k = 'test-movie'; + await mediaRepository.save(media); + + // Insert through a query builder with listeners disabled so + // MediaRequestSubscriber.afterInsert does not try to reach Radarr. + await getRepository(MediaRequest) + .createQueryBuilder() + .insert() + .into(MediaRequest) + .values({ + status: MediaRequestStatus.APPROVED, + media: { id: media.id }, + requestedBy: { id: 1 }, + type: MediaType.MOVIE, + is4k: true, + }) + .callListeners(false) + .execute(); + + await availabilitySync.run(); + + const updated = await mediaRepository.findOneOrFail({ + where: { tmdbId: 687167 }, + }); + + assert.strictEqual(updated.status4k, MediaStatus.DELETED); + assert.strictEqual( + updated.externalServiceId4k, + 512, + 'externalServiceId4k must be kept while an approved 4K request is in flight' + ); + assert.strictEqual(updated.serviceId4k, 0); + assert.strictEqual(updated.externalServiceSlug4k, 'test-movie'); + assert.strictEqual( + updated.ratingKey4k, + 'req-in-flight-rk', + 'ratingKey4k must be kept while an approved 4K request is in flight' + ); + }); + + it('should null service metadata when deleting a 4K movie with no open request', async () => { + configurePlex(); + configureRadarr([{ is4k: true, syncEnabled: true }]); + + const mediaRepository = getRepository(Media); + + const media = new Media(); + media.tmdbId = 687168; + media.mediaType = MediaType.MOVIE; + media.status = MediaStatus.UNKNOWN; + media.status4k = MediaStatus.AVAILABLE; + media.ratingKey4k = 'no-request-rk'; + media.serviceId4k = 0; + media.externalServiceId4k = 640; + media.externalServiceSlug4k = 'another-movie'; + await mediaRepository.save(media); + + await availabilitySync.run(); + + const updated = await mediaRepository.findOneOrFail({ + where: { tmdbId: 687168 }, + }); + + assert.strictEqual(updated.status4k, MediaStatus.DELETED); + assert.strictEqual(updated.externalServiceId4k, null); + assert.strictEqual(updated.serviceId4k, null); + assert.strictEqual(updated.externalServiceSlug4k, null); + assert.strictEqual(updated.ratingKey4k, null); + }); + }); + + describe('movie availability - scan-disabled servers', () => { + it('should count a configured but scan-disabled 4K Radarr server as existence evidence', async () => { + configurePlex(); + configureRadarr([ + { syncEnabled: true }, + { is4k: true, syncEnabled: false, port: 7879 }, + ]); + + const mediaRepository = getRepository(Media); + + const media = new Media(); + media.tmdbId = 687169; + media.mediaType = MediaType.MOVIE; + media.status = MediaStatus.UNKNOWN; + media.status4k = MediaStatus.AVAILABLE; + media.ratingKey4k = 'not-in-plex-rk'; + media.serviceId4k = 1; + media.externalServiceId4k = 700; + await mediaRepository.save(media); + + getMovieImpl = async (id: number) => { + if (id !== 700) { + throw new Error('404'); + } + return { + id: 700, + tmdbId: 687169, + title: 'Test Movie', + hasFile: true, + } as unknown as RadarrMovie; + }; + + await availabilitySync.run(); + + const updated = await mediaRepository.findOneOrFail({ + where: { tmdbId: 687169 }, + }); + + assert.strictEqual( + updated.status4k, + MediaStatus.AVAILABLE, + 'A movie present on a configured 4K Radarr server must be kept even when that server has scanning disabled' + ); + }); + }); + + describe('show availability - merged Plex versions', () => { + function fakeVersionedEpisode( + ratingKey: string, + widths: number[] + ): PlexMetadata { + return { + ratingKey, + guid: `plex://episode/${ratingKey}`, + type: 'movie' as const, + title: ratingKey, + Guid: [], + index: 1, + leafCount: 0, + viewedLeafCount: 0, + addedAt: 0, + updatedAt: 0, + Media: widths.map((width, i) => ({ + id: i, + duration: 2400, + bitrate: width >= 2000 ? 20000 : 4000, + width, + height: width >= 2000 ? 2160 : 1080, + aspectRatio: 1.78, + audioChannels: 6, + audioCodec: 'eac3', + videoCodec: width >= 2000 ? 'hevc' : 'h264', + videoResolution: width >= 2000 ? '4k' : '1080', + container: 'mkv', + videoFrameRate: '24p', + videoProfile: width >= 2000 ? 'main 10' : 'high', + })), + }; + } + + it('should remove a stale 4K season status when the shared item has no 4K episodes for that season', async () => { + configurePlex(); + configureSonarr([ + { syncEnabled: true }, + { is4k: true, syncEnabled: true }, + ]); + + const mediaRepository = getRepository(Media); + + const media = new Media(); + media.tmdbId = 687171; + media.mediaType = MediaType.TV; + media.status = MediaStatus.AVAILABLE; + media.status4k = MediaStatus.AVAILABLE; + media.ratingKey = 'merged-show-rk'; + media.ratingKey4k = 'merged-show-rk'; + media.seasons = [ + new Season({ + seasonNumber: 1, + status: MediaStatus.AVAILABLE, + status4k: MediaStatus.AVAILABLE, + }), + new Season({ + seasonNumber: 2, + status: MediaStatus.AVAILABLE, + status4k: MediaStatus.AVAILABLE, + }), + ]; + await mediaRepository.save(media); + + getMetadataImpl = async (key: string) => { + if (key === 'merged-show-rk') { + return fakePlexShow('merged-show-rk'); + } + throw new Error('404'); + }; + + // Season 1 has a 4K version, season 2 only has a 1080p file left. + getChildrenMetadataImpl = async (key: string) => { + if (key === 'merged-show-rk') { + return [fakePlexSeason(1, 'ms-s1-rk'), fakePlexSeason(2, 'ms-s2-rk')]; + } + if (key === 'ms-s1-rk') { + return [fakeVersionedEpisode('ms-s1-e1', [1920, 3840])]; + } + if (key === 'ms-s2-rk') { + return [fakeVersionedEpisode('ms-s2-e1', [1920])]; + } + return []; + }; + + await availabilitySync.run(); + + const updated = await mediaRepository.findOneOrFail({ + where: { tmdbId: 687171 }, + relations: ['seasons'], + }); + const s1 = updated.seasons.find((s) => s.seasonNumber === 1); + const s2 = updated.seasons.find((s) => s.seasonNumber === 2); + + assert.strictEqual( + s2?.status4k, + MediaStatus.DELETED, + 'A 4K season status without any 4K episode on the shared item must be removed' + ); + assert.strictEqual( + s1?.status4k, + MediaStatus.AVAILABLE, + 'A season with a 4K episode must be kept' + ); + assert.strictEqual( + updated.status4k, + MediaStatus.PARTIALLY_AVAILABLE, + 'The 4K show must roll up to partially available' + ); + assert.strictEqual(s1?.status, MediaStatus.AVAILABLE); + assert.strictEqual(s2?.status, MediaStatus.AVAILABLE); + assert.strictEqual(updated.status, MediaStatus.AVAILABLE); + }); + + it('should remove a stale non-4K season status when the shared item only has 4K episodes for that season', async () => { + configurePlex(); + configureSonarr([ + { syncEnabled: true }, + { is4k: true, syncEnabled: true }, + ]); + + const mediaRepository = getRepository(Media); + + const media = new Media(); + media.tmdbId = 687172; + media.mediaType = MediaType.TV; + media.status = MediaStatus.AVAILABLE; + media.status4k = MediaStatus.AVAILABLE; + media.ratingKey = 'merged-show2-rk'; + media.ratingKey4k = 'merged-show2-rk'; + media.seasons = [ + new Season({ + seasonNumber: 1, + status: MediaStatus.AVAILABLE, + status4k: MediaStatus.AVAILABLE, + }), + new Season({ + seasonNumber: 2, + status: MediaStatus.AVAILABLE, + status4k: MediaStatus.AVAILABLE, + }), + ]; + await mediaRepository.save(media); + + getMetadataImpl = async (key: string) => { + if (key === 'merged-show2-rk') { + return fakePlexShow('merged-show2-rk'); + } + throw new Error('404'); + }; + + // Season 2's non-4K file was removed; only the 4K version remains. + getChildrenMetadataImpl = async (key: string) => { + if (key === 'merged-show2-rk') { + return [ + fakePlexSeason(1, 'ms2-s1-rk'), + fakePlexSeason(2, 'ms2-s2-rk'), + ]; + } + if (key === 'ms2-s1-rk') { + return [fakeVersionedEpisode('ms2-s1-e1', [1920, 3840])]; + } + if (key === 'ms2-s2-rk') { + return [fakeVersionedEpisode('ms2-s2-e1', [3840])]; + } + return []; + }; + + await availabilitySync.run(); + + const updated = await mediaRepository.findOneOrFail({ + where: { tmdbId: 687172 }, + relations: ['seasons'], + }); + const s1 = updated.seasons.find((s) => s.seasonNumber === 1); + const s2 = updated.seasons.find((s) => s.seasonNumber === 2); + + assert.strictEqual( + s2?.status, + MediaStatus.DELETED, + 'A non-4K season status without any non-4K episode on the shared item must be removed' + ); + assert.strictEqual( + s2?.status4k, + MediaStatus.AVAILABLE, + 'The 4K season status must be kept' + ); + assert.strictEqual( + updated.status, + MediaStatus.PARTIALLY_AVAILABLE, + 'The non-4K show must roll up to partially available' + ); + assert.strictEqual(s1?.status, MediaStatus.AVAILABLE); + assert.strictEqual(s1?.status4k, MediaStatus.AVAILABLE); + assert.strictEqual(updated.status4k, MediaStatus.AVAILABLE); + }); + + it('should keep a non-4K season with only 4K episodes when no 4K Sonarr server is configured', async () => { + // Single-server setups track 4K files under the plain status; + // without a 4K Sonarr the resolution check must stay disabled. + configurePlex(); + configureSonarr([{ syncEnabled: true }]); + + const mediaRepository = getRepository(Media); + + const media = new Media(); + media.tmdbId = 687173; + media.mediaType = MediaType.TV; + media.status = MediaStatus.AVAILABLE; + media.ratingKey = 'single-4k-show-rk'; + media.seasons = [ + new Season({ + seasonNumber: 1, + status: MediaStatus.AVAILABLE, + status4k: MediaStatus.UNKNOWN, + }), + ]; + await mediaRepository.save(media); + + getMetadataImpl = async (key: string) => { + if (key === 'single-4k-show-rk') { + return fakePlexShow('single-4k-show-rk'); + } + throw new Error('404'); + }; + + getChildrenMetadataImpl = async (key: string) => { + if (key === 'single-4k-show-rk') { + return [fakePlexSeason(1, 's4k-s1-rk')]; + } + if (key === 's4k-s1-rk') { + return [fakeVersionedEpisode('s4k-s1-e1', [3840])]; + } + return []; + }; + + await availabilitySync.run(); + + const updated = await mediaRepository.findOneOrFail({ + where: { tmdbId: 687173 }, + relations: ['seasons'], + }); + const s1 = updated.seasons.find((s) => s.seasonNumber === 1); + + assert.strictEqual( + s1?.status, + MediaStatus.AVAILABLE, + 'Without a 4K Sonarr server a 4K-only season must keep its non-4K status' + ); + assert.strictEqual(updated.status, MediaStatus.AVAILABLE); + }); + + it('should not remove the 4K show status when episode metadata cannot be fetched for any season', async () => { + configurePlex(); + configureSonarr([ + { syncEnabled: true }, + { is4k: true, syncEnabled: true }, + ]); + + const mediaRepository = getRepository(Media); + + const media = new Media(); + media.tmdbId = 687174; + media.mediaType = MediaType.TV; + media.status = MediaStatus.AVAILABLE; + media.status4k = MediaStatus.AVAILABLE; + media.ratingKey = 'merged-show3-rk'; + media.ratingKey4k = 'merged-show3-rk'; + media.seasons = [ + new Season({ + seasonNumber: 1, + status: MediaStatus.AVAILABLE, + status4k: MediaStatus.AVAILABLE, + }), + ]; + await mediaRepository.save(media); + + getMetadataImpl = async (key: string) => { + if (key === 'merged-show3-rk') { + return fakePlexShow('merged-show3-rk'); + } + throw new Error('404'); + }; + + getChildrenMetadataImpl = async (key: string) => { + if (key === 'merged-show3-rk') { + return [fakePlexSeason(1, 'ms3-s1-rk')]; + } + throw new Error('episode fetch failed'); + }; + + await availabilitySync.run(); + + const updated = await mediaRepository.findOneOrFail({ + where: { tmdbId: 687174 }, + relations: ['seasons'], + }); + const s1 = updated.seasons.find((s) => s.seasonNumber === 1); + + assert.strictEqual( + updated.status4k, + MediaStatus.AVAILABLE, + 'Unverifiable episode metadata proves nothing about the files and must not trigger 4K removal' + ); + assert.strictEqual(s1?.status4k, MediaStatus.AVAILABLE); + assert.strictEqual(updated.status, MediaStatus.AVAILABLE); + assert.strictEqual(s1?.status, MediaStatus.AVAILABLE); + }); + }); + + describe('specials season handling', () => { + const tmdbSeasonsWithSpecials = [ + { + id: 100, + air_date: '2024-01-01', + episode_count: 3, + name: 'Specials', + overview: '', + season_number: 0, + }, + { + id: 101, + air_date: '2024-01-01', + episode_count: 10, + name: 'Season 1', + overview: '', + season_number: 1, + }, + ]; + + it('should not demote an available show when only the specials season is missing (Jellyfin)', async () => { + configureJellyfin(); + configureSonarr([{ syncEnabled: true }]); + + const mediaRepository = getRepository(Media); + + const media = new Media(); + media.tmdbId = 13862; + media.mediaType = MediaType.TV; + media.status = MediaStatus.AVAILABLE; + media.jellyfinMediaId = 'jellyfin-shogun-id'; + media.externalServiceId = 300; + media.seasons = [ + new Season({ + seasonNumber: 0, + status: MediaStatus.UNKNOWN, + status4k: MediaStatus.UNKNOWN, + }), + new Season({ + seasonNumber: 1, + status: MediaStatus.AVAILABLE, + status4k: MediaStatus.UNKNOWN, + }), + ]; + + await mediaRepository.save(media); + + getTvShowImpl = async () => fakeTmdbShow(13862, tmdbSeasonsWithSpecials); + + getItemDataImpl = async (id: string) => { + if (id === 'jellyfin-shogun-id') { + return fakeJellyfinShow('jellyfin-shogun-id', '13862'); + } + return undefined; + }; + + getSeasonsImpl = async (seriesID: string) => { + if (seriesID === 'jellyfin-shogun-id') { + return [fakeJellyfinSeason(1, 'jellyfin-shogun-s1-id')]; + } + return []; + }; + + getEpisodesImpl = async (_seriesID: string, seasonID: string) => { + if (seasonID === 'jellyfin-shogun-s1-id') { + return fakeJellyfinEpisodes(10); + } + return []; + }; + + getSeriesByIdImpl = async (id: number) => { + if (id === 300) { + return { + tvdbId: 70814, + id: 300, + title: 'Shogun', + titleSlug: 'shogun', + monitored: true, + statistics: { + episodeFileCount: 10, + totalEpisodeCount: 10, + episodeCount: 10, + percentOfEpisodes: 100, + sizeOnDisk: 0, + seasonCount: 1, + }, + seasons: fakeSonarrSeasons(1, { 1: 10 }), + } as unknown as SonarrSeries; + } + throw new Error('404'); + }; + + await availabilitySync.run(); + + const updated = await mediaRepository.findOneOrFail({ + where: { tmdbId: 13862 }, + relations: ['seasons'], + }); + + assert.strictEqual( + updated.status, + MediaStatus.AVAILABLE, + 'Show should stay AVAILABLE when only the specials season is missing' + ); + }); + + it('should not demote an available show when only the specials season is missing (Plex)', async () => { + configurePlex(); + configureSonarr([{ syncEnabled: true }]); + + const mediaRepository = getRepository(Media); + + const media = new Media(); + media.tmdbId = 13863; + media.mediaType = MediaType.TV; + media.status = MediaStatus.AVAILABLE; + media.ratingKey = 'plex-shogun-rk'; + media.externalServiceId = 301; + media.seasons = [ + new Season({ + seasonNumber: 0, + status: MediaStatus.UNKNOWN, + status4k: MediaStatus.UNKNOWN, + }), + new Season({ + seasonNumber: 1, + status: MediaStatus.AVAILABLE, + status4k: MediaStatus.UNKNOWN, + }), + ]; + + await mediaRepository.save(media); + + getTvShowImpl = async () => fakeTmdbShow(13863, tmdbSeasonsWithSpecials); + + getMetadataImpl = async (key: string) => { + if (key === 'plex-shogun-rk') { + return fakePlexShow('plex-shogun-rk'); + } + throw new Error('404'); + }; + + getChildrenMetadataImpl = async (key: string) => { + if (key === 'plex-shogun-rk') { + return [fakePlexSeason(1, 'plex-shogun-s1-rk')]; + } + if (key === 'plex-shogun-s1-rk') { + return fakePlexEpisodes(10); + } + return []; + }; + + getSeriesByIdImpl = async (id: number) => { + if (id === 301) { + return { + tvdbId: 70814, + id: 301, + title: 'Shogun', + titleSlug: 'shogun', + monitored: true, + statistics: { + episodeFileCount: 10, + totalEpisodeCount: 10, + episodeCount: 10, + percentOfEpisodes: 100, + sizeOnDisk: 0, + seasonCount: 1, + }, + seasons: fakeSonarrSeasons(1, { 1: 10 }), + } as unknown as SonarrSeries; + } + throw new Error('404'); + }; + + await availabilitySync.run(); + + const updated = await mediaRepository.findOneOrFail({ + where: { tmdbId: 13863 }, + relations: ['seasons'], + }); + + assert.strictEqual( + updated.status, + MediaStatus.AVAILABLE, + 'Show should stay AVAILABLE when only the specials season is missing' + ); + }); + + it('should mark a removed specials season as DELETED without demoting the show (Jellyfin)', async () => { + configureJellyfin(); + configureSonarr([{ syncEnabled: true }]); + + const mediaRepository = getRepository(Media); + + const media = new Media(); + media.tmdbId = 13864; + media.mediaType = MediaType.TV; + media.status = MediaStatus.AVAILABLE; + media.jellyfinMediaId = 'jellyfin-specials-id'; + media.externalServiceId = 302; + media.seasons = [ + new Season({ + seasonNumber: 0, + status: MediaStatus.AVAILABLE, + status4k: MediaStatus.UNKNOWN, + }), + new Season({ + seasonNumber: 1, + status: MediaStatus.AVAILABLE, + status4k: MediaStatus.UNKNOWN, + }), + ]; + + await mediaRepository.save(media); + + getTvShowImpl = async () => fakeTmdbShow(13864, tmdbSeasonsWithSpecials); + + getItemDataImpl = async (id: string) => { + if (id === 'jellyfin-specials-id') { + return fakeJellyfinShow('jellyfin-specials-id', '13864'); + } + return undefined; + }; + + getSeasonsImpl = async (seriesID: string) => { + if (seriesID === 'jellyfin-specials-id') { + return [fakeJellyfinSeason(1, 'jellyfin-specials-s1-id')]; + } + return []; + }; + + getEpisodesImpl = async (_seriesID: string, seasonID: string) => { + if (seasonID === 'jellyfin-specials-s1-id') { + return fakeJellyfinEpisodes(10); + } + return []; + }; + + getSeriesByIdImpl = async (id: number) => { + if (id === 302) { + return { + tvdbId: 70814, + id: 302, + title: 'Shogun', + titleSlug: 'shogun', + monitored: true, + statistics: { + episodeFileCount: 10, + totalEpisodeCount: 10, + episodeCount: 10, + percentOfEpisodes: 100, + sizeOnDisk: 0, + seasonCount: 1, + }, + seasons: fakeSonarrSeasons(1, { 1: 10 }), + } as unknown as SonarrSeries; + } + throw new Error('404'); + }; + + await availabilitySync.run(); + + const updated = await mediaRepository.findOneOrFail({ + where: { tmdbId: 13864 }, + relations: ['seasons'], + }); + + const specials = updated.seasons.find((s) => s.seasonNumber === 0); + assert.strictEqual( + specials?.status, + MediaStatus.DELETED, + 'Removed specials season should be marked DELETED' + ); + + assert.strictEqual( + updated.status, + MediaStatus.AVAILABLE, + 'Show should stay AVAILABLE when only specials were removed' + ); + }); + }); }); diff --git a/server/lib/availabilitySync.ts b/server/lib/availabilitySync.ts index cdbe80b54d..4a700ef355 100644 --- a/server/lib/availabilitySync.ts +++ b/server/lib/availabilitySync.ts @@ -5,6 +5,11 @@ import PlexAPI from '@server/api/plexapi'; import RadarrAPI, { type RadarrMovie } from '@server/api/servarr/radarr'; import type { SonarrSeason, SonarrSeries } from '@server/api/servarr/sonarr'; import SonarrAPI from '@server/api/servarr/sonarr'; +import TheMovieDb from '@server/api/themoviedb'; +import type { + TmdbTvDetails, + TmdbTvScanDetails, +} from '@server/api/themoviedb/interfaces'; import { MediaRequestStatus, MediaStatus } from '@server/constants/media'; import { MediaServerType } from '@server/constants/server'; import { getRepository } from '@server/datasource'; @@ -30,6 +35,10 @@ class AvailabilitySync { private sonarrSeasonsCache: Record; private radarrServers: RadarrSettings[]; private sonarrServers: SonarrSettings[]; + private enable4kMovie: boolean; + private enable4kShow: boolean; + + readonly tmdb = new TheMovieDb(); async run() { const settings = getSettings(); @@ -40,12 +49,14 @@ class AvailabilitySync { this.jellyfinSeasonsCache = {}; this.jellyfinEpisodeExistsCache = {}; this.sonarrSeasonsCache = {}; - this.radarrServers = settings.radarr.filter((server) => server.syncEnabled); - this.sonarrServers = settings.sonarr.filter((server) => server.syncEnabled); + this.radarrServers = settings.radarr; + this.sonarrServers = settings.sonarr; + this.enable4kMovie = this.radarrServers.some((server) => server.is4k); + this.enable4kShow = this.sonarrServers.some((server) => server.is4k); try { logger.info(`Starting availability sync...`, { - label: 'Availability Sync', + label: 'AvailabilitySync', }); const pageSize = 50; @@ -149,7 +160,7 @@ class AvailabilitySync { if (existsInPlex || existsInRadarr) { movieExists = true; - logger.info( + logger.debug( `The non-4K movie [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`, { label: 'AvailabilitySync', @@ -159,7 +170,7 @@ class AvailabilitySync { if (existsInPlex4k || existsInRadarr4k) { movieExists4k = true; - logger.info( + logger.debug( `The 4K movie [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`, { label: 'AvailabilitySync', @@ -182,7 +193,7 @@ class AvailabilitySync { if (existsInJellyfin || existsInRadarr) { movieExists = true; - logger.info( + logger.debug( `The non-4K movie [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`, { label: 'AvailabilitySync', @@ -192,7 +203,7 @@ class AvailabilitySync { if (existsInJellyfin4k || existsInRadarr4k) { movieExists4k = true; - logger.info( + logger.debug( `The 4K movie [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`, { label: 'AvailabilitySync', @@ -246,7 +257,7 @@ class AvailabilitySync { if (mediaServerType === MediaServerType.PLEX) { if (existsInPlex || existsInSonarr) { showExists = true; - logger.info( + logger.debug( `The non-4K show [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`, { label: 'AvailabilitySync', @@ -258,7 +269,7 @@ class AvailabilitySync { if (mediaServerType === MediaServerType.PLEX) { if (existsInPlex4k || existsInSonarr4k) { showExists4k = true; - logger.info( + logger.debug( `The 4K show [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`, { label: 'AvailabilitySync', @@ -274,7 +285,7 @@ class AvailabilitySync { ) { if (existsInJellyfin || existsInSonarr) { showExists = true; - logger.info( + logger.debug( `The non-4K show [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`, { label: 'AvailabilitySync', @@ -289,7 +300,7 @@ class AvailabilitySync { ) { if (existsInJellyfin4k || existsInSonarr4k) { showExists4k = true; - logger.info( + logger.debug( `The 4K show [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`, { label: 'AvailabilitySync', @@ -353,6 +364,52 @@ class AvailabilitySync { ]); } + // We need to fetch from TMDB to get the episode count for each season + let tvShow: TmdbTvScanDetails | TmdbTvDetails | undefined; + try { + if (media.tmdbId) { + tvShow = await this.tmdb.getTvShowForScan({ + tvId: Number(media.tmdbId), + }); + } else if (media.tvdbId) { + tvShow = await this.tmdb.getShowByTvdbIdForScan({ + tvdbId: Number(media.tvdbId), + }); + } + } catch (e) { + logger.debug( + `Failed to fetch TMDB data for show [TMDB ID ${media.tmdbId}]. Skipping season enrichment.`, + { label: 'AvailabilitySync', errorMessage: e.message } + ); + } + + if (tvShow) { + // fill the finalSeasons and finalSeasons4k maps with false for missing seasons + media.seasons.forEach((season) => { + // Specials don't count towards availability (baseScanner skips them too) + // TODO: doesn't respect enableSpecialEpisodes; needs a shared predicate with baseScanner.ts + if (season.seasonNumber === 0) { + return; + } + if ( + !finalSeasons.has(season.seasonNumber) && + tvShow.seasons.find( + (s) => s.season_number === season.seasonNumber + )?.episode_count + ) { + finalSeasons.set(season.seasonNumber, false); + } + if ( + !finalSeasons4k.has(season.seasonNumber) && + tvShow.seasons.find( + (s) => s.season_number === season.seasonNumber + )?.episode_count + ) { + finalSeasons4k.set(season.seasonNumber, false); + } + }); + } + if ( !showExists && (media.status === MediaStatus.AVAILABLE || @@ -405,11 +462,11 @@ class AvailabilitySync { } catch (ex) { logger.error('Failed to complete availability sync.', { errorMessage: ex.message, - label: 'Availability Sync', + label: 'AvailabilitySync', }); } finally { logger.info(`Availability sync complete.`, { - label: 'Availability Sync', + label: 'AvailabilitySync', }); this.running = false; } @@ -453,31 +510,29 @@ class AvailabilitySync { const mediaRepository = getRepository(Media); try { - // If media type is tv, check if a season is processing + // Check if an approved request for this version is still in flight // to see if we need to keep the external metadata let isMediaProcessing = false; - if (media.mediaType === 'tv') { - const requestRepository = getRepository(MediaRequest); - - const request = await requestRepository - .createQueryBuilder('request') - .leftJoinAndSelect('request.media', 'media') - .where('(media.id = :id)', { - id: media.id, - }) - .andWhere( - '(request.is4k = :is4k AND request.status = :requestStatus)', - { - requestStatus: MediaRequestStatus.APPROVED, - is4k: is4k, - } - ) - .getOne(); + const requestRepository = getRepository(MediaRequest); - if (request) { - isMediaProcessing = true; - } + const request = await requestRepository + .createQueryBuilder('request') + .leftJoinAndSelect('request.media', 'media') + .where('(media.id = :id)', { + id: media.id, + }) + .andWhere( + '(request.is4k = :is4k AND request.status = :requestStatus)', + { + requestStatus: MediaRequestStatus.APPROVED, + is4k: is4k, + } + ) + .getOne(); + + if (request) { + isMediaProcessing = true; } // Set the non-4K or 4K media to deleted @@ -508,7 +563,7 @@ class AvailabilitySync { ? media[is4k ? 'jellyfinMediaId4k' : 'jellyfinMediaId'] : null; } - logger.info( + logger.debug( `The ${is4k ? '4K' : 'non-4K'} ${ media.mediaType === 'movie' ? 'movie' : 'show' } [TMDB ID ${media.tmdbId}] was not found in any ${ @@ -531,7 +586,7 @@ class AvailabilitySync { } [TMDB ID ${media.tmdbId}].`, { errorMessage: ex.message, - label: 'Availability Sync', + label: 'AvailabilitySync', } ); } @@ -554,49 +609,45 @@ class AvailabilitySync { ); // Retrieve the season keys to pass into our log const seasonKeys = [...seasonsPendingRemoval.keys()]; - - // let isSeasonRemoved = false; + // Specials can still be marked DELETED below, but shouldn't demote the show + const nonSpecialSeasonKeys = seasonKeys.filter((key) => key !== 0); try { for (const mediaSeason of media.seasons) { - if (seasonsPendingRemoval.has(mediaSeason.seasonNumber)) { + if ( + seasonsPendingRemoval.has(mediaSeason.seasonNumber) && + (mediaSeason[is4k ? 'status4k' : 'status'] === + MediaStatus.AVAILABLE || + mediaSeason[is4k ? 'status4k' : 'status'] === + MediaStatus.PARTIALLY_AVAILABLE) + ) { mediaSeason[is4k ? 'status4k' : 'status'] = MediaStatus.DELETED; } } - if (media.status === MediaStatus.AVAILABLE && !is4k) { - media.status = MediaStatus.PARTIALLY_AVAILABLE; - logger.info( - `Marking the non-4K show [TMDB ID ${media.tmdbId}] as PARTIALLY_AVAILABLE because season removal has occurred.`, - { label: 'Availability Sync' } - ); - } - - if (media.status4k === MediaStatus.AVAILABLE && is4k) { - media.status4k = MediaStatus.PARTIALLY_AVAILABLE; - logger.info( - `Marking the 4K show [TMDB ID ${media.tmdbId}] as PARTIALLY_AVAILABLE because season removal has occurred.`, - { label: 'Availability Sync' } + if ( + nonSpecialSeasonKeys.length > 0 && + media[is4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE + ) { + media[is4k ? 'status4k' : 'status'] = MediaStatus.PARTIALLY_AVAILABLE; + logger.debug( + `Marking the ${ + is4k ? '4K' : 'non-4K' + } show [TMDB ID ${media.tmdbId}] as PARTIALLY_AVAILABLE because season(s) [${nonSpecialSeasonKeys}] was not found in any ${ + media.mediaType === 'tv' ? 'Sonarr' : 'Radarr' + } and ${ + mediaServerType === MediaServerType.PLEX + ? 'plex' + : mediaServerType === MediaServerType.JELLYFIN + ? 'jellyfin' + : 'emby' + } instance.`, + { label: 'AvailabilitySync' } ); } media.lastSeasonChange = new Date(); await mediaRepository.save(media); - - logger.info( - `The ${is4k ? '4K' : 'non-4K'} season(s) [${seasonKeys}] [TMDB ID ${ - media.tmdbId - }] was not found in any ${ - media.mediaType === 'tv' ? 'Sonarr' : 'Radarr' - } and ${ - mediaServerType === MediaServerType.PLEX - ? 'plex' - : mediaServerType === MediaServerType.JELLYFIN - ? 'jellyfin' - : 'emby' - } instance. Status will be changed to deleted.`, - { label: 'AvailabilitySync' } - ); } catch (ex) { logger.debug( `Failure updating the ${ @@ -604,7 +655,7 @@ class AvailabilitySync { } season(s) [${seasonKeys}], TMDB ID ${media.tmdbId}.`, { errorMessage: ex.message, - label: 'Availability Sync', + label: 'AvailabilitySync', } ); } @@ -648,6 +699,10 @@ class AvailabilitySync { }); } + if (radarr && radarr.tmdbId !== media.tmdbId) { + continue; + } + if (radarr && radarr.hasFile) { const resolution = radarr?.movieFile?.mediaInfo?.resolution?.split('x'); @@ -671,7 +726,7 @@ class AvailabilitySync { }] from Radarr.`, { errorMessage: ex.message, - label: 'Availability Sync', + label: 'AvailabilitySync', } ); } @@ -705,18 +760,26 @@ class AvailabilitySync { if (media.externalServiceId && !is4k) { sonarr = await sonarrAPI.getSeriesById(media.externalServiceId); - this.sonarrSeasonsCache[`${server.id}-${media.externalServiceId}`] = - sonarr.seasons; } if (media.externalServiceId4k && is4k) { sonarr = await sonarrAPI.getSeriesById(media.externalServiceId4k); - this.sonarrSeasonsCache[`${server.id}-${media.externalServiceId4k}`] = - sonarr.seasons; } - if (sonarr && sonarr.statistics.episodeFileCount > 0) { - existsInSonarr = true; + if (sonarr && media.tvdbId != null && sonarr.tvdbId !== media.tvdbId) { + continue; + } + + if (sonarr) { + const externalServiceId = is4k + ? media.externalServiceId4k + : media.externalServiceId; + this.sonarrSeasonsCache[`${server.id}-${externalServiceId}`] = + sonarr.seasons; + + if (sonarr.statistics.episodeFileCount > 0) { + existsInSonarr = true; + } } } catch (ex) { if (!ex.message.includes('404')) { @@ -728,7 +791,7 @@ class AvailabilitySync { }] from Sonarr.`, { errorMessage: ex.message, - label: 'Availability Sync', + label: 'AvailabilitySync', } ); } @@ -827,6 +890,16 @@ class AvailabilitySync { this.plexSeasonsCache[ratingKey] = await this.plexClient?.getChildrenMetadata(ratingKey); } + + if ( + plexMedia && + media.mediaType === 'movie' && + this.enable4kMovie && + plexMedia.Media?.length && + !plexMedia.Media.some((mediaItem) => (mediaItem.width ?? 0) < 2000) + ) { + plexMedia = undefined; + } } if (ratingKey4k && is4k) { @@ -838,16 +911,11 @@ class AvailabilitySync { } if (plexMedia) { - if (ratingKey === ratingKey4k) { - plexMedia = undefined; - } - if ( plexMedia && media.mediaType === 'movie' && - !plexMedia.Media?.some( - (mediaItem) => (mediaItem.width ?? 0) >= 2000 - ) + plexMedia.Media?.length && + !plexMedia.Media.some((mediaItem) => (mediaItem.width ?? 0) >= 2000) ) { plexMedia = undefined; } @@ -856,11 +924,15 @@ class AvailabilitySync { const cachedSeasons = this.plexSeasonsCache[ratingKey4k]; if (cachedSeasons?.length) { let has4kInAnySeason = false; + let verifiedAnySeason = false; for (const season of cachedSeasons) { try { const episodes = await this.plexClient?.getChildrenMetadata( season.ratingKey ); + if (episodes?.some((episode) => episode.Media?.length)) { + verifiedAnySeason = true; + } const has4kEpisode = episodes?.some((episode) => episode.Media?.some( (mediaItem) => (mediaItem.width ?? 0) >= 2000 @@ -874,7 +946,7 @@ class AvailabilitySync { // If we can't fetch episodes for a season, continue checking other seasons } } - if (!has4kInAnySeason) { + if (verifiedAnySeason && !has4kInAnySeason) { plexMedia = undefined; } } @@ -895,7 +967,7 @@ class AvailabilitySync { } [TMDB ID ${media.tmdbId}] from Plex.`, { errorMessage: ex.message, - label: 'Availability Sync', + label: 'AvailabilitySync', } ); } @@ -958,7 +1030,7 @@ class AvailabilitySync { ); if (seasonMeta) { - const cacheKey = seasonMeta.ratingKey; + const cacheKey = `${is4k ? '4k' : 'std'}-${seasonMeta.ratingKey}`; if (cacheKey in this.plexEpisodeExistsCache) { seasonExistsInPlex = this.plexEpisodeExistsCache[cacheKey]; @@ -970,8 +1042,20 @@ class AvailabilitySync { seasonMeta.ratingKey ); - seasonExistsInPlex = - episodes?.some((episode) => episode.Media?.length > 0) ?? false; + const episodeVersions = + episodes?.flatMap((episode) => episode.Media ?? []) ?? []; + + if (is4k) { + seasonExistsInPlex = episodeVersions.some( + (mediaItem) => (mediaItem.width ?? 0) >= 2000 + ); + } else if (this.enable4kShow) { + seasonExistsInPlex = episodeVersions.some( + (mediaItem) => (mediaItem.width ?? 0) < 2000 + ); + } else { + seasonExistsInPlex = episodeVersions.length > 0; + } } catch { // If we can't fetch episodes, assume the season exists // to avoid false removal diff --git a/server/lib/cache.ts b/server/lib/cache.ts index 64b5c79ee8..3309abe64c 100644 --- a/server/lib/cache.ts +++ b/server/lib/cache.ts @@ -1,35 +1,217 @@ -import NodeCache from 'node-cache'; +import { LRUCache } from 'lru-cache'; export type AvailableCacheIds = + | 'hardcover' | 'tmdb' + | 'tmdbscan' | 'radarr' | 'sonarr' + | 'readarr' | 'rt' | 'imdb' | 'github' - | 'plexguid' | 'plextv' | 'plexwatchlist' | 'tvdb'; const DEFAULT_TTL = 300; -const DEFAULT_CHECK_PERIOD = 120; + +const OBJECT_VALUE_SIZE = 80; +const PROMISE_VALUE_SIZE = 80; +const ARRAY_VALUE_SIZE = 40; + +// Limits key count, not memory, since TMDB never caps the credits rosters. +const TMDB_MAX_KEYS = 1000; + +// Backstop against unbounded growth as the 15 minute TTL is what bounds this tier. +const TMDB_SCAN_MAX_KEYS = 2000; + +// Only the rolling quality profile, root folder and language profile lookups reach +// these tiers, and keys are prefixed by server url, so this is a few keys per server. +// Readarr's metadata profiles are the same kind of lookup. +const RADARR_MAX_KEYS = 64; +const SONARR_MAX_KEYS = 64; +const READARR_MAX_KEYS = 64; + +// One key per searched title, each holding twenty search hits. +const RT_MAX_KEYS = 500; + +// One key per IMDb id, requested from the same endpoint as the RT ratings. +const IMDB_MAX_KEYS = 500; + +// Releases and commits, varying only by page size and branch. +const GITHUB_MAX_KEYS = 16; + +// Watchlist item metadata, shared between users as the token is not part of the key. +const PLEX_TV_MAX_KEYS = 5000; + +// Keyed by auth token, so one key per Plex linked user. +const PLEX_WATCHLIST_MAX_KEYS = 500; + +// Several keys per show, holding the largest payloads of any tier as the extended +// series lookup carries every episode. +const TVDB_MAX_KEYS = 500; + +// One key per book, series, author, search and discover page. Library scans look +// every book up here, so this must stay above the size of a Readarr library or a +// scan evicts its own lookups and re-fetches them at Hardcover's rate limit. +const HARDCOVER_MAX_KEYS = 1000; + +export interface CacheStats { + hits: number; + misses: number; + keys: number; + ksize: number; + vsize: number; +} + +export interface CacheStore { + get(key: string): T | undefined; + set(key: string, value: T, ttl?: number): boolean; + del(key: string): number; + getTtl(key: string): number | undefined; + getStats(): CacheStats; + flushAll(): void; +} + +const keyLength = (key: string): number => key.toString().length; + +const valueLength = (value: unknown): number => { + if (typeof value === 'string') { + return value.length; + } + if (Array.isArray(value)) { + return ARRAY_VALUE_SIZE * value.length; + } + if (typeof value === 'number') { + return 8; + } + if ( + typeof (value as { then?: unknown } | null | undefined)?.then === 'function' + ) { + return PROMISE_VALUE_SIZE; + } + if (Buffer.isBuffer(value)) { + return value.length; + } + if (value != null && typeof value === 'object') { + return OBJECT_VALUE_SIZE * Object.keys(value).length; + } + if (typeof value === 'boolean') { + return 8; + } + return 0; +}; + +interface LruEntry { + value: unknown; + ksize: number; + vsize: number; +} + +class LruCacheStore implements CacheStore { + private cache: LRUCache; + private stdTtl: number; + private hits = 0; + private misses = 0; + private ksize = 0; + private vsize = 0; + + constructor({ max, stdTtl }: { max: number; stdTtl: number }) { + this.stdTtl = stdTtl; + this.cache = new LRUCache({ + max, + ttl: stdTtl * 1000, + // Without this, expired entries are only dropped when their key is read. + ttlAutopurge: true, + // Fires on every removal path, so one decrement here covers all of them. + dispose: (entry) => { + this.ksize -= entry.ksize; + this.vsize -= entry.vsize; + }, + }); + } + + // Cloned on read because callers mutate what they get back, as getTvSeason + // does when it rewrites still_path in place. + public get(key: string): T | undefined { + const entry = this.cache.get(key); + + if (entry === undefined) { + this.misses++; + return undefined; + } + + this.hits++; + return structuredClone(entry.value) as T; + } + + public set(key: string, value: T, ttl?: number): boolean { + const entry: LruEntry = { + value: structuredClone(value), + ksize: keyLength(key), + vsize: valueLength(value), + }; + + // A ttl of 0 means the entry never expires. + this.cache.set(key, entry, { ttl: (ttl ?? this.stdTtl) * 1000 }); + + this.ksize += entry.ksize; + this.vsize += entry.vsize; + + return true; + } + + public del(key: string): number { + return this.cache.delete(key) ? 1 : 0; + } + + public getTtl(key: string): number | undefined { + if (!this.cache.has(key)) { + return undefined; + } + + const remaining = this.cache.getRemainingTTL(key); + + // Keys with no expiry report 0. + return remaining === Infinity ? 0 : Date.now() + remaining; + } + + public getStats(): CacheStats { + return { + hits: this.hits, + misses: this.misses, + keys: this.cache.size, + ksize: this.ksize, + vsize: this.vsize, + }; + } + + public flushAll(): void { + this.cache.clear(); + this.hits = 0; + this.misses = 0; + this.ksize = 0; + this.vsize = 0; + } +} class Cache { public id: AvailableCacheIds; - public data: NodeCache; + public data: CacheStore; public name: string; constructor( id: AvailableCacheIds, name: string, - options: { stdTtl?: number; checkPeriod?: number } = {} + options: { max: number; stdTtl?: number } ) { this.id = id; this.name = name; - this.data = new NodeCache({ - stdTTL: options.stdTtl ?? DEFAULT_TTL, - checkperiod: options.checkPeriod ?? DEFAULT_CHECK_PERIOD, + + this.data = new LruCacheStore({ + max: options.max, + stdTtl: options.stdTtl ?? DEFAULT_TTL, }); } @@ -46,34 +228,41 @@ class CacheManager { private availableCaches: Record = { tmdb: new Cache('tmdb', 'The Movie Database API', { stdTtl: 21600, - checkPeriod: 60 * 30, + max: TMDB_MAX_KEYS, + }), + tmdbscan: new Cache('tmdbscan', 'The Movie Database API (Library Scans)', { + stdTtl: 900, + max: TMDB_SCAN_MAX_KEYS, }), - radarr: new Cache('radarr', 'Radarr API'), - sonarr: new Cache('sonarr', 'Sonarr API'), + radarr: new Cache('radarr', 'Radarr API', { max: RADARR_MAX_KEYS }), + sonarr: new Cache('sonarr', 'Sonarr API', { max: SONARR_MAX_KEYS }), + readarr: new Cache('readarr', 'Readarr API', { max: READARR_MAX_KEYS }), rt: new Cache('rt', 'Rotten Tomatoes API', { stdTtl: 43200, - checkPeriod: 60 * 30, + max: RT_MAX_KEYS, }), imdb: new Cache('imdb', 'IMDB Radarr Proxy', { stdTtl: 43200, - checkPeriod: 60 * 30, + max: IMDB_MAX_KEYS, }), github: new Cache('github', 'GitHub API', { stdTtl: 21600, - checkPeriod: 60 * 30, - }), - plexguid: new Cache('plexguid', 'Plex GUID', { - stdTtl: 86400 * 7, // 1 week cache - checkPeriod: 60 * 30, + max: GITHUB_MAX_KEYS, }), plextv: new Cache('plextv', 'Plex TV', { stdTtl: 86400 * 7, // 1 week cache - checkPeriod: 60, + max: PLEX_TV_MAX_KEYS, + }), + plexwatchlist: new Cache('plexwatchlist', 'Plex Watchlist', { + max: PLEX_WATCHLIST_MAX_KEYS, }), - plexwatchlist: new Cache('plexwatchlist', 'Plex Watchlist'), tvdb: new Cache('tvdb', 'The TVDB API', { stdTtl: 21600, - checkPeriod: 60 * 30, + max: TVDB_MAX_KEYS, + }), + hardcover: new Cache('hardcover', 'Hardcover API', { + stdTtl: 21600, + max: HARDCOVER_MAX_KEYS, }), }; diff --git a/server/lib/downloadtracker.ts b/server/lib/downloadtracker.ts index f160f6c89d..af38cb8710 100644 --- a/server/lib/downloadtracker.ts +++ b/server/lib/downloadtracker.ts @@ -1,4 +1,5 @@ import RadarrAPI from '@server/api/servarr/radarr'; +import ReadarrAPI from '@server/api/servarr/readarr'; import SonarrAPI from '@server/api/servarr/sonarr'; import { MediaType } from '@server/constants/media'; import { getSettings } from '@server/lib/settings'; @@ -27,6 +28,7 @@ export interface DownloadingItem { class DownloadTracker { private radarrServers: Record = {}; private sonarrServers: Record = {}; + private readarrServers: Record = {}; public getMovieProgress( serverId: number, @@ -54,14 +56,29 @@ class DownloadTracker { ); } + public getBookProgress( + serverId: number, + externalServiceId: number + ): DownloadingItem[] { + if (!this.readarrServers[serverId]) { + return []; + } + + return this.readarrServers[serverId].filter( + (item) => item.externalId === externalServiceId + ); + } + public async resetDownloadTracker() { this.radarrServers = {}; this.sonarrServers = {}; + this.readarrServers = {}; } public updateDownloads() { this.updateRadarrDownloads(); this.updateSonarrDownloads(); + this.updateReadarrDownloads(); } private async updateRadarrDownloads() { @@ -220,6 +237,84 @@ class DownloadTracker { }) ); } + + private async updateReadarrDownloads() { + const settings = getSettings(); + + // Remove duplicate servers + const filteredServers = uniqWith(settings.readarr, (readarrA, readarrB) => { + return ( + readarrA.hostname === readarrB.hostname && + readarrA.port === readarrB.port && + readarrA.baseUrl === readarrB.baseUrl + ); + }); + + // Load downloads from Readarr servers + Promise.all( + filteredServers.map(async (server) => { + if (server.syncEnabled) { + const readarr = new ReadarrAPI({ + apiKey: server.apiKey, + url: ReadarrAPI.buildUrl(server, '/api/v1'), + }); + + try { + await readarr.refreshMonitoredDownloads(); + const queueItems = await readarr.getQueue(); + + this.readarrServers[server.id] = queueItems.map((item) => ({ + externalId: item.bookId, + estimatedCompletionTime: new Date(item.estimatedCompletionTime), + mediaType: MediaType.BOOK, + size: item.size, + sizeLeft: item.sizeleft, + status: item.status, + timeLeft: item.timeleft, + title: item.title, + downloadId: item.downloadId, + })); + + if (queueItems.length > 0) { + logger.debug( + `Found ${queueItems.length} item(s) in progress on Readarr server: ${server.name}`, + { label: 'Download Tracker' } + ); + } + } catch { + logger.error( + `Unable to get queue from Readarr server: ${server.name}`, + { + label: 'Download Tracker', + } + ); + } + + // Duplicate this data to matching servers + const matchingServers = settings.readarr.filter( + (rs) => + rs.hostname === server.hostname && + rs.port === server.port && + rs.baseUrl === server.baseUrl && + rs.id !== server.id + ); + + if (matchingServers.length > 0) { + logger.debug( + `Matching download data to ${matchingServers.length} other Readarr server(s)`, + { label: 'Download Tracker' } + ); + } + + matchingServers.forEach((ms) => { + if (ms.syncEnabled) { + this.readarrServers[ms.id] = this.readarrServers[server.id]; + } + }); + } + }) + ); + } } const downloadTracker = new DownloadTracker(); diff --git a/server/lib/email/index.ts b/server/lib/email/index.ts index 66b3698faf..d34e2c9b75 100644 --- a/server/lib/email/index.ts +++ b/server/lib/email/index.ts @@ -1,10 +1,48 @@ import type { NotificationAgentEmail } from '@server/lib/settings'; import { getSettings } from '@server/lib/settings'; import Email from 'email-templates'; +import net from 'node:net'; import nodemailer from 'nodemailer'; +import type SMTPTransport from 'nodemailer/lib/smtp-transport'; import { URL } from 'url'; import { openpgpEncrypt } from './openpgpEncrypt'; +const getSocket: SMTPTransport.Options['getSocket'] = (options, callback) => { + if (!options.host || typeof options.port !== 'number') { + callback(new Error('SMTP host and port are required'), undefined); + return; + } + + const socket = net.connect({ + host: options.host, + port: options.port, + }); + const cleanup = () => { + socket.setTimeout(0); + socket.removeListener('error', onError); + socket.removeListener('connect', onConnect); + socket.removeListener('timeout', onTimeout); + }; + const onError = (error: Error) => { + cleanup(); + callback(error, undefined); + }; + const onConnect = () => { + cleanup(); + callback(null, { connection: socket }); + }; + const onTimeout = () => { + cleanup(); + socket.destroy(); + callback(new Error('SMTP connection timed out'), undefined); + }; + + socket.once('error', onError); + socket.once('connect', onConnect); + socket.once('timeout', onTimeout); + socket.setTimeout(10000); +}; + class PreparedEmail extends Email { public constructor(settings: NotificationAgentEmail, pgpKey?: string) { const { applicationUrl } = getSettings().main; @@ -28,6 +66,7 @@ class PreparedEmail extends Email { pass: settings.options.authPass, } : undefined, + getSocket: net.isIP(settings.options.smtpHost) ? undefined : getSocket, }); if (pgpKey) { diff --git a/server/lib/forkSchema.ts b/server/lib/forkSchema.ts new file mode 100644 index 0000000000..d2b530d6c3 --- /dev/null +++ b/server/lib/forkSchema.ts @@ -0,0 +1,290 @@ +import logger from '@server/logger'; +import type { DataSource } from 'typeorm'; + +export interface ForkColumn { + table: string; + column: string; + type: string; +} + +export const FORK_COLUMNS: ForkColumn[] = [ + { table: 'media_request', column: 'metadataProfileId', type: 'integer' }, + { table: 'override_rule', column: 'readarrServiceId', type: 'integer' }, + { table: 'override_rule', column: 'metadataProfileId', type: 'integer' }, + { table: 'user', column: 'bookQuotaLimit', type: 'integer' }, + { table: 'user', column: 'bookQuotaDays', type: 'integer' }, +]; + +const BACKUP_PREFIX = '_fork_backup_'; +const LOG_LABEL = 'Fork Schema'; + +export const applyForkSchema = async ( + dbConnection: DataSource +): Promise => { + await abortIfOldForkDatabase(dbConnection); + await snapshotForkColumns(dbConnection); + await runUpstreamMigrations(dbConnection); + await ensureForkColumns(dbConnection); + await ensurePermissionsColumnIsBigint(dbConnection); + await restoreForkColumns(dbConnection); +}; + +// Check for old fork user +const abortIfOldForkDatabase = async ( + dbConnection: DataSource +): Promise => { + try { + const oldForkMarkers: { table: string; column: string }[] = [ + { table: 'media', column: 'hcId' }, + { table: 'media_request', column: 'isAlt' }, + { table: 'blocklist', column: 'externalId' }, + ]; + + for (const marker of oldForkMarkers) { + if (!(await tableExists(dbConnection, marker.table))) { + continue; + } + const columns = await getColumnNames(dbConnection, marker.table); + if (columns.includes(marker.column)) { + logger.error( + `This database was created by an old version of the fork (found "${marker.table}"."${marker.column}"), which this version can no longer upgrade. Please upgrade to v3.2.0 first, or start with a fresh database.`, + { label: LOG_LABEL } + ); + process.exit(1); + } + } + } catch (error) { + logger.error('Failed to check for old fork database', { + label: LOG_LABEL, + error: error.message, + }); + process.exit(1); + } +}; + +const snapshotForkColumns = async (dbConnection: DataSource): Promise => { + let hasPendingMigrations = false; + try { + hasPendingMigrations = await dbConnection.showMigrations(); + } catch (error) { + logger.error('Failed to check for pending migrations', { + label: LOG_LABEL, + error: error.message, + }); + process.exit(1); + } + + if (!hasPendingMigrations) { + return; + } + + try { + for (const table of forkTables()) { + const backupTable = `${BACKUP_PREFIX}${table}`; + + if (await tableExists(dbConnection, backupTable)) { + logger.warn( + `Found existing backup table "${backupTable}" from an interrupted upgrade, keeping it`, + { label: LOG_LABEL } + ); + continue; + } + + // the table does not exist yet, nothing to back up, fresh install + if (!(await tableExists(dbConnection, table))) { + continue; + } + + const existingColumns = await getColumnNames(dbConnection, table); + const columnsToBackUp = forkColumnsFor(table).filter((forkColumn) => + existingColumns.includes(forkColumn.column) + ); + + // no fork columns, existing seerr user migrating to fork + if (columnsToBackUp.length === 0) { + continue; + } + + logger.info( + `Backing up fork columns of "${table}" before running pending migrations`, + { label: LOG_LABEL } + ); + + const columnList = columnsToBackUp + .map((forkColumn) => `"${forkColumn.column}"`) + .join(', '); + await dbConnection.query( + `CREATE TABLE "${backupTable}" AS SELECT "id", ${columnList} FROM "${table}"` + ); + + await dbConnection.query( + `CREATE INDEX "${backupTable}_id_idx" ON "${backupTable}" ("id")` + ); + } + } catch (error) { + logger.error('Failed to back up fork columns', { + label: LOG_LABEL, + error: error.message, + }); + process.exit(1); + } +}; + +const runUpstreamMigrations = async ( + dbConnection: DataSource +): Promise => { + try { + await dbConnection.runMigrations(); + } catch (error) { + logger.error('Failed to run database migrations', { + label: LOG_LABEL, + error: error.message, + }); + process.exit(1); + } +}; + +const ensureForkColumns = async (dbConnection: DataSource): Promise => { + try { + for (const forkColumn of FORK_COLUMNS) { + if (!(await tableExists(dbConnection, forkColumn.table))) { + logger.warn( + `Expected table "${forkColumn.table}" does not exist, skipping fork column "${forkColumn.column}"`, + { label: LOG_LABEL } + ); + continue; + } + + const existingColumns = await getColumnNames( + dbConnection, + forkColumn.table + ); + if (existingColumns.includes(forkColumn.column)) { + continue; + } + + logger.info( + `Adding fork column "${forkColumn.table}"."${forkColumn.column}"`, + { label: LOG_LABEL } + ); + await dbConnection.query( + `ALTER TABLE "${forkColumn.table}" ADD COLUMN "${forkColumn.column}" ${forkColumn.type}` + ); + } + } catch (error) { + logger.error('Failed to add fork columns', { + label: LOG_LABEL, + error: error.message, + }); + process.exit(1); + } +}; + +const ensurePermissionsColumnIsBigint = async ( + dbConnection: DataSource +): Promise => { + if (dbConnection.options.type !== 'postgres') { + return; + } + + try { + const rows: { data_type: string }[] = await dbConnection.query( + `SELECT data_type FROM information_schema.columns WHERE table_name = 'user' AND column_name = 'permissions'` + ); + if (rows.length > 0 && rows[0].data_type === 'integer') { + logger.info('Widening "user"."permissions" to bigint', { + label: LOG_LABEL, + }); + await dbConnection.query( + `ALTER TABLE "user" ALTER COLUMN "permissions" TYPE bigint` + ); + } + } catch (error) { + logger.error('Failed to widen "user"."permissions" to bigint', { + label: LOG_LABEL, + error: error.message, + }); + process.exit(1); + } +}; + +const restoreForkColumns = async (dbConnection: DataSource): Promise => { + try { + for (const table of forkTables()) { + const backupTable = `${BACKUP_PREFIX}${table}`; + if (!(await tableExists(dbConnection, backupTable))) { + continue; + } + + logger.info(`Restoring fork columns of "${table}" from backup`, { + label: LOG_LABEL, + }); + + // Only restore columns that are both in the backup and in the current + // manifest, so a backup written by an older fork version stays valid + const backupColumns = await getColumnNames(dbConnection, backupTable); + const columnsToRestore = forkColumnsFor(table).filter((forkColumn) => + backupColumns.includes(forkColumn.column) + ); + + for (const forkColumn of columnsToRestore) { + await dbConnection.query( + `UPDATE "${table}" SET "${forkColumn.column}" = (SELECT b."${forkColumn.column}" FROM "${backupTable}" b WHERE b."id" = "${table}"."id")` + ); + } + + await dbConnection.query(`DROP TABLE "${backupTable}"`); + } + } catch (error) { + logger.error('Failed to restore fork columns', { + label: LOG_LABEL, + error: error.message, + }); + process.exit(1); + } +}; + +const forkTables = (): string[] => { + return Array.from( + new Set(FORK_COLUMNS.map((forkColumn) => forkColumn.table)) + ); +}; + +const forkColumnsFor = (table: string): ForkColumn[] => { + return FORK_COLUMNS.filter((forkColumn) => forkColumn.table === table); +}; + +const tableExists = async ( + dbConnection: DataSource, + table: string +): Promise => { + if (dbConnection.options.type === 'postgres') { + const rows = await dbConnection.query( + `SELECT 1 FROM information_schema.tables WHERE table_name = $1`, + [table] + ); + return rows.length > 0; + } + const rows = await dbConnection.query( + `SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?`, + [table] + ); + return rows.length > 0; +}; + +const getColumnNames = async ( + dbConnection: DataSource, + table: string +): Promise => { + if (dbConnection.options.type === 'postgres') { + const rows: { column_name: string }[] = await dbConnection.query( + `SELECT column_name FROM information_schema.columns WHERE table_name = $1 ORDER BY ordinal_position`, + [table] + ); + return rows.map((row) => row.column_name); + } + const rows: { name: string }[] = await dbConnection.query( + `PRAGMA table_info("${table}")` + ); + return rows.map((row) => row.name); +}; diff --git a/server/lib/imageproxy.test.ts b/server/lib/imageproxy.test.ts new file mode 100644 index 0000000000..06c6946776 --- /dev/null +++ b/server/lib/imageproxy.test.ts @@ -0,0 +1,107 @@ +import ImageProxy from '@server/lib/imageproxy'; +import axios, { type AxiosResponse } from 'axios'; +import assert from 'node:assert/strict'; +import { after, afterEach, before, beforeEach, describe, it } from 'node:test'; +import sharp from 'sharp'; + +type Transform = (buffer: Buffer) => Promise<{ + buffer: Buffer; + extension: string; +}>; + +const optimize: Transform = async (buffer) => ({ + buffer: await sharp(buffer) + .resize(600, 900, { fit: 'cover' }) + .webp({ quality: 80 }) + .toBuffer(), + extension: 'webp', +}); + +describe('image proxy transform', () => { + let original: Buffer; + let previousAdapter: typeof axios.defaults.adapter; + const cached: [ImageProxy, string][] = []; + + const freshProxy = async (transform: Transform, path: string) => { + const proxy = new ImageProxy('imageproxy-test', 'https://images.test', { + transform, + }); + await proxy.clearCachedImage(path); + cached.push([proxy, path]); + return proxy; + }; + + before(async () => { + original = await sharp({ + create: { + width: 1200, + height: 1800, + channels: 3, + background: '#808080', + }, + }) + .jpeg() + .toBuffer(); + }); + + beforeEach(() => { + previousAdapter = axios.defaults.adapter; + axios.defaults.adapter = (config) => + Promise.resolve({ + data: original, + status: 200, + statusText: 'OK', + headers: { + 'content-type': 'image/jpeg', + 'cache-control': 'max-age=3600', + }, + config, + request: {}, + } as AxiosResponse); + }); + + afterEach(() => { + axios.defaults.adapter = previousAdapter; + }); + + after(async () => { + for (const [proxy, path] of cached) { + await proxy.clearCachedImage(path); + } + }); + + it('serves the optimized image on the first request', async () => { + const proxy = await freshProxy(optimize, '/first.jpg'); + + const image = await proxy.getImage('/first.jpg'); + + assert.equal(image.meta.cacheMiss, true); + assert.equal(image.meta.extension, 'webp'); + const { format, width, height } = await sharp(image.imageBuffer).metadata(); + assert.deepEqual( + { format, width, height }, + { format: 'webp', width: 600, height: 900 } + ); + }); + + it('serves the same optimized image from the cache afterwards', async () => { + const proxy = await freshProxy(optimize, '/repeat.jpg'); + + const first = await proxy.getImage('/repeat.jpg'); + const second = await proxy.getImage('/repeat.jpg'); + + assert.equal(second.meta.cacheMiss, false); + assert.equal(second.meta.extension, 'webp'); + assert.deepEqual(second.imageBuffer, first.imageBuffer); + }); + + it('falls back to the original when optimizing fails', async () => { + const proxy = await freshProxy(async () => { + throw new Error('unsupported image'); + }, '/broken.jpg'); + + const image = await proxy.getImage('/broken.jpg'); + + assert.deepEqual(image.imageBuffer, original); + }); +}); diff --git a/server/lib/imageproxy.ts b/server/lib/imageproxy.ts index c485c10291..73e7c99d98 100644 --- a/server/lib/imageproxy.ts +++ b/server/lib/imageproxy.ts @@ -1,10 +1,11 @@ import logger from '@server/logger'; -import { requestInterceptorFunction } from '@server/utils/customProxyAgent'; +import { proxyRequestInterceptor } from '@server/utils/customProxyAgent'; +import { userAgentRequestInterceptor } from '@server/utils/userAgent'; import axios, { type AxiosInstance } from 'axios'; import rateLimit, { type rateLimitOptions } from 'axios-rate-limit'; import { createHash } from 'crypto'; import { promises } from 'fs'; -import mime from 'mime/lite'; +import mime from 'mime'; import path, { join } from 'path'; type ImageResponse = { @@ -131,6 +132,9 @@ class ImageProxy { private axios: AxiosInstance; private cacheVersion; private key; + private transform?: ( + buffer: Buffer + ) => Promise<{ buffer: Buffer; extension: string }>; constructor( key: string, @@ -139,15 +143,20 @@ class ImageProxy { cacheVersion?: number; rateLimitOptions?: rateLimitOptions; headers?: Record; + transform?: ( + buffer: Buffer + ) => Promise<{ buffer: Buffer; extension: string }>; } = {} ) { this.cacheVersion = options.cacheVersion ?? 1; this.key = key; + this.transform = options.transform; this.axios = axios.create({ baseURL: baseUrl, headers: options.headers, }); - this.axios.interceptors.request.use(requestInterceptorFunction); + this.axios.interceptors.request.use(proxyRequestInterceptor); + this.axios.interceptors.request.use(userAgentRequestInterceptor); if (options.rateLimitOptions) { this.axios = rateLimit(this.axios, options.rateLimitOptions); @@ -173,17 +182,62 @@ class ImageProxy { } } + if (this.transform && newImage.meta.cacheMiss) { + return (await this.optimizeAndCache(newImage, cacheKey)) ?? newImage; + } + return newImage; } // If the image is stale, we will revalidate it in the background. if (imageResponse.meta.isStale) { - this.set(path, cacheKey); + this.revalidate(path, cacheKey); } return imageResponse; } + // Refreshing writes the original back over the cache entry, so anything that + // was optimized has to be optimized again or it reverts on every expiry + private async revalidate(path: string, cacheKey: string): Promise { + const refreshed = await this.set(path, cacheKey); + + if (this.transform && refreshed) { + await this.optimizeAndCache(refreshed, cacheKey); + } + } + + private async optimizeAndCache( + image: ImageResponse, + cacheKey: string + ): Promise { + if (!this.transform) return null; + + try { + const { buffer, extension } = await this.transform(image.imageBuffer); + const directory = join(this.getCacheDirectory(), cacheKey); + const expireAt = Date.now() + image.meta.curRevalidate * 1000; + + await this.writeToCacheDir( + directory, + extension, + image.meta.curRevalidate, + expireAt, + buffer, + image.meta.etag + ); + + return { meta: { ...image.meta, extension }, imageBuffer: buffer }; + } catch (e) { + logger.debug('Failed to optimize image.', { + label: 'Image Cache', + errorMessage: e.message, + }); + + return null; + } + } + public async clearCachedImage(path: string) { // find cacheKey const cacheKey = this.getCacheKey(path); @@ -270,15 +324,18 @@ class ImageProxy { const buffer = Buffer.from(response.data, 'binary'); const contentType = response.headers['content-type'] || ''; - const extension = mime.getExtension(contentType) || ''; + const extension = (mime.getExtension(contentType) || '').replace( + /[^\w-]/g, + '' + ); let maxAge = Number( (response.headers['cache-control'] ?? '0').split('=')[1] ); - if (!maxAge) maxAge = 86400; + if (!maxAge) maxAge = 604800; const expireAt = Date.now() + maxAge * 1000; - const etag = (response.headers.etag ?? '').replace(/"/g, ''); + const etag = (response.headers.etag ?? '').replace(/[^\w-]/g, ''); await this.writeToCacheDir( directory, diff --git a/server/lib/notifications/agents/discord.ts b/server/lib/notifications/agents/discord.ts index d37bc6e58f..81da3c4c40 100644 --- a/server/lib/notifications/agents/discord.ts +++ b/server/lib/notifications/agents/discord.ts @@ -1,3 +1,7 @@ +import { + DISCORD_SNOWFLAKE_REGEX, + EmbedColors, +} from '@server/constants/discord'; import { IssueStatus, IssueTypeName } from '@server/constants/issue'; import { getRepository } from '@server/datasource'; import { User } from '@server/entity/User'; @@ -16,31 +20,8 @@ import { import type { NotificationAgent, NotificationPayload } from './agent'; import { BaseAgent } from './agent'; -enum EmbedColors { - DEFAULT = 0, - AQUA = 1752220, - GREEN = 3066993, - BLUE = 3447003, - PURPLE = 10181046, - GOLD = 15844367, - ORANGE = 15105570, - RED = 15158332, - GREY = 9807270, - DARKER_GREY = 8359053, - NAVY = 3426654, - DARK_AQUA = 1146986, - DARK_GREEN = 2067276, - DARK_BLUE = 2123412, - DARK_PURPLE = 7419530, - DARK_GOLD = 12745742, - DARK_ORANGE = 11027200, - DARK_RED = 10038562, - DARK_GREY = 9936031, - LIGHT_GREY = 12370112, - DARK_NAVY = 2899536, - LUMINOUS_VIVID_PINK = 16580705, - DARK_VIVID_PINK = 12320855, -} +const isValidSnowflake = (id: string): boolean => + DISCORD_SNOWFLAKE_REGEX.test(id); interface DiscordImageEmbed { url?: string; @@ -278,9 +259,12 @@ class DiscordAgent NotificationAgentKey.DISCORD, type ) && - payload.notifyUser.settings.discordId + payload.notifyUser.settings.discordIds?.length ) { - userMentions.push(`<@${payload.notifyUser.settings.discordId}>`); + const validIds = payload.notifyUser.settings.discordIds.filter( + (id) => isValidSnowflake(id) + ); + userMentions.push(...validIds.map((id) => `<@${id}>`)); } } @@ -296,16 +280,30 @@ class DiscordAgent NotificationAgentKey.DISCORD, type ) && - user.settings.discordId && + user.settings.discordIds?.length && shouldSendAdminNotification(type, user, payload) ) - .map((user) => `<@${user.settings?.discordId}>`) + .flatMap((user) => + user + .settings!.discordIds.filter((id) => isValidSnowflake(id)) + .map((id) => `<@${id}>`) + ) ); } } - if (settings.options.webhookRoleId) { + const allowedUserIds = userMentions.map((mention) => + mention.replace(/[<@>]/g, '') + ); + + const allowedRoleIds: string[] = []; + + if ( + settings.options.webhookRoleId && + isValidSnowflake(settings.options.webhookRoleId) + ) { userMentions.push(`<@&${settings.options.webhookRoleId}>`); + allowedRoleIds.push(settings.options.webhookRoleId); } // Discord webhooks go to a channel, not per-user, @@ -315,13 +313,25 @@ class DiscordAgent ? (payload.notifyUser?.settings?.locale as AvailableLocale) : (settings.options.locale as AvailableLocale); - await axios.post(settings.options.webhookUrl, { + const webhookUrl = new URL(settings.options.webhookUrl); + if (settings.options.webhookThreadId) { + webhookUrl.searchParams.set( + 'thread_id', + settings.options.webhookThreadId + ); + } + + await axios.post(webhookUrl.toString(), { username: settings.options.botUsername ? settings.options.botUsername : getSettings().main.applicationTitle, avatar_url: settings.options.botAvatarUrl, embeds: [this.buildEmbed(type, payload, locale)], content: userMentions.join(' '), + allowed_mentions: { + users: allowedUserIds, + roles: allowedRoleIds, + }, } as DiscordWebhookPayload); return true; diff --git a/server/lib/notifications/agents/email.ts b/server/lib/notifications/agents/email.ts index 4a82b3943b..3066cecadc 100644 --- a/server/lib/notifications/agents/email.ts +++ b/server/lib/notifications/agents/email.ts @@ -16,6 +16,9 @@ import { Notification, shouldSendAdminNotification } from '..'; import type { NotificationAgent, NotificationPayload } from './agent'; import { BaseAgent } from './agent'; +const PUBLIC_LOGO_URL = + 'https://raw.githubusercontent.com/seerr-team/seerr/refs/heads/develop/public/logo_full.svg'; + const messages = defineMessages('notifications.agents.email', { issueType: '{type} issue', issue: 'issue', @@ -96,6 +99,12 @@ class EmailAgent const settings = getSettings(); const { applicationUrl, applicationTitle } = settings.main; const { embedPoster } = settings.notifications.agents.email; + const { usePublicLogo } = settings.notifications.agents.email.options; + const logoUrl = usePublicLogo + ? PUBLIC_LOGO_URL + : applicationUrl + ? `${applicationUrl}/logo_full.svg` + : undefined; if (type === Notification.TEST_NOTIFICATION) { return { @@ -107,18 +116,29 @@ class EmailAgent body: payload.message, applicationUrl, applicationTitle, + logoUrl, recipientName, recipientEmail, }, }; } + const is4k = payload.request?.is4k; + const isBook = payload.media?.mediaType === MediaType.BOOK; + // Books reuse the is4k flag to mean "audiobook". Upstream encodes the 4K + // variant by suffixing " in 4K", which does not apply to audiobooks, so we + // fold the audiobook wording into the mediaType noun and never select the + // 4K message variant for books (see use4k below). const mediaType = payload.media ? payload.media.mediaType === MediaType.MOVIE ? intl.formatMessage(globalMessages.movie) - : intl.formatMessage(globalMessages.series) + : payload.media.mediaType === MediaType.BOOK + ? intl.formatMessage( + is4k ? globalMessages.audiobook : globalMessages.book + ) + : intl.formatMessage(globalMessages.series) : undefined; - const is4k = payload.request?.is4k; + const use4k = is4k && !isBook; if (payload.request) { let body = ''; @@ -126,49 +146,51 @@ class EmailAgent switch (type) { case Notification.MEDIA_PENDING: body = intl.formatMessage( - is4k ? messages.pendingRequest4k : messages.pendingRequest, + use4k ? messages.pendingRequest4k : messages.pendingRequest, { mediaType } ); break; case Notification.MEDIA_AUTO_REQUESTED: body = intl.formatMessage( - is4k ? messages.autoRequested4k : messages.autoRequested, + use4k ? messages.autoRequested4k : messages.autoRequested, { mediaType } ); break; case Notification.MEDIA_APPROVED: body = intl.formatMessage( - is4k ? messages.approvedRequest4k : messages.approvedRequest, + use4k ? messages.approvedRequest4k : messages.approvedRequest, { mediaType } ); break; case Notification.MEDIA_AUTO_APPROVED: body = intl.formatMessage( - is4k ? messages.autoApproved4k : messages.autoApproved, + use4k ? messages.autoApproved4k : messages.autoApproved, { mediaType } ); break; case Notification.MEDIA_AVAILABLE: body = intl.formatMessage( - is4k ? messages.availableRequest4k : messages.availableRequest, + use4k ? messages.availableRequest4k : messages.availableRequest, { mediaType } ); break; case Notification.MEDIA_DECLINED: body = intl.formatMessage( - is4k ? messages.declinedRequest4k : messages.declinedRequest, + use4k ? messages.declinedRequest4k : messages.declinedRequest, { mediaType } ); break; case Notification.MEDIA_FAILED: body = intl.formatMessage( - is4k ? messages.failedRequest4k : messages.failedRequest, + use4k ? messages.failedRequest4k : messages.failedRequest, { mediaType, service: payload.media?.mediaType === MediaType.MOVIE ? 'Radarr' - : 'Sonarr', + : payload.media?.mediaType === MediaType.BOOK + ? 'Readarr' + : 'Sonarr', } ); break; @@ -195,6 +217,7 @@ class EmailAgent : undefined, applicationUrl, applicationTitle, + logoUrl, recipientName, recipientEmail, }, @@ -263,6 +286,7 @@ class EmailAgent : undefined, applicationUrl, applicationTitle, + logoUrl, recipientName, recipientEmail, }, diff --git a/server/lib/notifications/agents/gotify.ts b/server/lib/notifications/agents/gotify.ts index 0470e156e0..8384ab27a6 100644 --- a/server/lib/notifications/agents/gotify.ts +++ b/server/lib/notifications/agents/gotify.ts @@ -52,6 +52,7 @@ class GotifyAgent const settings = this.getSettings(); const intl = getIntl(settings.options.locale); const { applicationUrl, applicationTitle } = getSettings().main; + const embedPoster = settings.embedPoster; const priority = settings.options.priority ?? 1; const title = payload.event @@ -102,6 +103,10 @@ class GotifyAgent message += `\n\n**${extra.name}**\n${extra.value} `; } + if (embedPoster && payload.image) { + message += `\n\n![](${payload.image}) `; + } + if (applicationUrl && payload.media) { const actionUrl = `${applicationUrl}/${payload.media.mediaType}/${payload.media.tmdbId}`; const displayUrl = @@ -114,6 +119,13 @@ class GotifyAgent 'client::display': { contentType: 'text/markdown', }, + ...(embedPoster && payload.image + ? { + 'client::notification': { + bigImageUrl: payload.image, + }, + } + : {}), }, title, message, diff --git a/server/lib/notifications/agents/ntfy.ts b/server/lib/notifications/agents/ntfy.ts index fae329a007..c9efcbe2f6 100644 --- a/server/lib/notifications/agents/ntfy.ts +++ b/server/lib/notifications/agents/ntfy.ts @@ -35,6 +35,10 @@ class NtfyAgent const embedPoster = settings.embedPoster; const topic = settings.options.topic; + const tags = settings.options.tags + ?.split(',') + .map((tag) => tag.trim()) + .filter((tag) => tag.length > 0); const priority = settings.options.priority ?? 3; const title = payload.event @@ -102,6 +106,9 @@ class NtfyAgent message, markdown: true, }; + if (tags && tags.length > 0) { + ntfyPayload.tags = tags; + } if (attach) { ntfyPayload.attach = attach; } diff --git a/server/lib/notifications/agents/webhook.ts b/server/lib/notifications/agents/webhook.ts index a7a80595b9..3fdde03ab8 100644 --- a/server/lib/notifications/agents/webhook.ts +++ b/server/lib/notifications/agents/webhook.ts @@ -23,7 +23,7 @@ const KeyMap: Record = { notifyuser_username: 'notifyUser.displayName', notifyuser_email: 'notifyUser.email', notifyuser_avatar: 'notifyUser.avatar', - notifyuser_settings_discordId: 'notifyUser.settings.discordId', + notifyuser_settings_discordIds: 'notifyUser.settings.discordIds', notifyuser_settings_telegramChatId: 'notifyUser.settings.telegramChatId', media_imdbid: 'media.imdbId', media_tmdbid: 'media.tmdbId', @@ -42,7 +42,7 @@ const KeyMap: Record = { requestedBy_username: 'request.requestedBy.displayName', requestedBy_email: 'request.requestedBy.email', requestedBy_avatar: 'request.requestedBy.avatar', - requestedBy_settings_discordId: 'request.requestedBy.settings.discordId', + requestedBy_settings_discordIds: 'request.requestedBy.settings.discordIds', requestedBy_settings_telegramChatId: 'request.requestedBy.settings.telegramChatId', issue_id: 'issue.id', @@ -53,13 +53,13 @@ const KeyMap: Record = { reportedBy_username: 'issue.createdBy.displayName', reportedBy_email: 'issue.createdBy.email', reportedBy_avatar: 'issue.createdBy.avatar', - reportedBy_settings_discordId: 'issue.createdBy.settings.discordId', + reportedBy_settings_discordIds: 'issue.createdBy.settings.discordIds', reportedBy_settings_telegramChatId: 'issue.createdBy.settings.telegramChatId', comment_message: 'comment.message', commentedBy_username: 'comment.user.displayName', commentedBy_email: 'comment.user.email', commentedBy_avatar: 'comment.user.avatar', - commentedBy_settings_discordId: 'comment.user.settings.discordId', + commentedBy_settings_discordIds: 'comment.user.settings.discordIds', commentedBy_settings_telegramChatId: 'comment.user.settings.telegramChatId', }; diff --git a/server/lib/notifications/agents/webpush.ts b/server/lib/notifications/agents/webpush.ts index 8586dd2960..b67561dbd6 100644 --- a/server/lib/notifications/agents/webpush.ts +++ b/server/lib/notifications/agents/webpush.ts @@ -79,10 +79,15 @@ class WebPushAgent const mediaType = payload.media ? payload.media.mediaType === MediaType.MOVIE ? intl.formatMessage(globalMessages.movie) - : intl.formatMessage(globalMessages.series) + : payload.media.mediaType === MediaType.BOOK + ? intl.formatMessage(globalMessages.book) + : intl.formatMessage(globalMessages.series) : undefined; const is4k = payload.request?.is4k; - const quality = is4k ? '4K ' : ''; + const isBook = payload.media?.mediaType === MediaType.BOOK; + // Books reuse the is4k flag to mean "audiobook"; render "audiobook" (no + // trailing space so it composes to one word) instead of "4K ". + const quality = is4k ? (isBook ? 'audio' : '4K ') : ''; const issueType = payload.issue ? payload.issue.issueType !== IssueType.OTHER diff --git a/server/lib/overrideRules.ts b/server/lib/overrideRules.ts new file mode 100644 index 0000000000..3d31876ca9 --- /dev/null +++ b/server/lib/overrideRules.ts @@ -0,0 +1,166 @@ +import { ANIME_KEYWORD_ID } from '@server/api/themoviedb/constants'; +import type { + TmdbKeyword, + TmdbMovieDetails, + TmdbTvDetails, +} from '@server/api/themoviedb/interfaces'; +import { MediaType } from '@server/constants/media'; +import { getRepository } from '@server/datasource'; +import OverrideRule from '@server/entity/OverrideRule'; +import type { User } from '@server/entity/User'; +import { getSettings } from '@server/lib/settings'; + +export type OverrideRulesResult = { + rootFolder: string | null; + profileId: number | null; + tags: number[] | null; +}; + +async function overrideRules({ + mediaType, + is4k, + tmdbMedia, + requestUser, + tags = null, + serviceId, +}: { + mediaType: MediaType; + is4k: boolean; + tmdbMedia: TmdbMovieDetails | TmdbTvDetails; + requestUser: User; + tags?: number[] | null; + serviceId?: number; +}): Promise { + const settings = getSettings(); + + let rootFolder: string | null = null; + let profileId: number | null = null; + + const serviceIdField = + mediaType === MediaType.MOVIE ? 'radarrServiceId' : 'sonarrServiceId'; + if (serviceId === undefined) { + const defaultRadarrId = is4k + ? settings.radarr.find((r) => r.is4k && r.isDefault)?.id + : settings.radarr.find((r) => !r.is4k && r.isDefault)?.id; + const defaultSonarrId = is4k + ? settings.sonarr.find((s) => s.is4k && s.isDefault)?.id + : settings.sonarr.find((s) => !s.is4k && s.isDefault)?.id; + serviceId = + mediaType === MediaType.MOVIE ? defaultRadarrId : defaultSonarrId; + } + + const overrideRuleRepository = getRepository(OverrideRule); + const rules = + serviceId === undefined + ? [] + : await overrideRuleRepository.find({ + where: { [serviceIdField]: serviceId }, + }); + + const appliedOverrideRules = rules.filter((rule) => { + const hasAnimeKeyword = + 'results' in tmdbMedia.keywords && + tmdbMedia.keywords.results.some( + (keyword: TmdbKeyword) => keyword.id === ANIME_KEYWORD_ID + ); + + // Skip override rules if the media is an anime TV show as anime TV + // is handled by default and override rules do not explicitly include + // the anime keyword + if ( + mediaType === MediaType.TV && + hasAnimeKeyword && + (!rule.keywords || + !rule.keywords.split(',').map(Number).includes(ANIME_KEYWORD_ID)) + ) { + return false; + } + + if ( + rule.users && + !rule.users.split(',').some((userId) => Number(userId) === requestUser.id) + ) { + return false; + } + if ( + rule.genre && + !rule.genre + .split(',') + .some((genreId) => + tmdbMedia.genres.map((genre) => genre.id).includes(Number(genreId)) + ) + ) { + return false; + } + if ( + rule.language && + !rule.language + .split('|') + .some((languageId) => languageId === tmdbMedia.original_language) + ) { + return false; + } + if ( + rule.keywords && + !rule.keywords.split(',').some((keywordId) => { + let keywordList: TmdbKeyword[] = []; + + if ('keywords' in tmdbMedia.keywords) { + keywordList = tmdbMedia.keywords.keywords; + } else if ('results' in tmdbMedia.keywords) { + keywordList = tmdbMedia.keywords.results; + } + + return keywordList + .map((keyword: TmdbKeyword) => keyword.id) + .includes(Number(keywordId)); + }) + ) { + return false; + } + return true; + }); + + // hacky way to prioritize rules + // TODO: make this better + const hasSpecificOverrideValue = ( + rule: OverrideRule, + key: keyof OverrideRule + ) => { + const value = rule[key]; + if (value == null) return false; + return typeof value !== 'string' || value.trim() !== ''; + }; + const prioritizedRule = appliedOverrideRules.sort((a, b) => { + const keys: (keyof OverrideRule)[] = ['genre', 'language', 'keywords']; + const aSpecificity = keys.filter((key) => + hasSpecificOverrideValue(a, key) + ).length; + const bSpecificity = keys.filter((key) => + hasSpecificOverrideValue(b, key) + ).length; + // Take the rule with the most specific condition first + return bSpecificity - aSpecificity; + })[0]; + + if (prioritizedRule) { + if (prioritizedRule.rootFolder) { + rootFolder = prioritizedRule.rootFolder; + } + if (prioritizedRule.profileId) { + profileId = prioritizedRule.profileId; + } + if (prioritizedRule.tags) { + tags = [ + ...new Set([ + ...(tags || []), + ...prioritizedRule.tags.split(',').map((tag) => Number(tag)), + ]), + ]; + } + } + + return { rootFolder, profileId, tags }; +} + +export default overrideRules; diff --git a/server/lib/permissions.ts b/server/lib/permissions.ts index edc9f7e183..46dc2e49cf 100644 --- a/server/lib/permissions.ts +++ b/server/lib/permissions.ts @@ -29,6 +29,10 @@ export enum Permission { WATCHLIST_VIEW = 134217728, MANAGE_BLOCKLIST = 268435456, VIEW_BLOCKLIST = 1073741824, + REQUEST_BOOK = 2147483648, + AUTO_APPROVE_BOOK = 4294967296, + REQUEST_AUDIO_BOOK = 8589934592, + AUTO_APPROVE_AUDIO_BOOK = 17179869184, } export interface PermissionCheckOptions { @@ -51,24 +55,35 @@ export const hasPermission = ( ): boolean => { let total = 0; - // If we are not checking any permissions, bail out and return true if (permissions === 0) { return true; } + if (isNaN(value)) { + return false; + } + + const bigValue = BigInt(value); + if (Array.isArray(permissions)) { - if (value & Permission.ADMIN) { + if (bigValue & BigInt(Permission.ADMIN)) { return true; } switch (options.type) { case 'and': - return permissions.every((permission) => !!(value & permission)); + return permissions.every( + (permission) => !!(bigValue & BigInt(permission)) + ); case 'or': - return permissions.some((permission) => !!(value & permission)); + return permissions.some( + (permission) => !!(bigValue & BigInt(permission)) + ); } } else { total = permissions; } - return !!(value & Permission.ADMIN) || !!(value & total); + return ( + !!(bigValue & BigInt(Permission.ADMIN)) || !!(bigValue & BigInt(total)) + ); }; diff --git a/server/lib/scanners/baseScanner.ts b/server/lib/scanners/baseScanner.ts index 08f200dfdf..4d3ce05f0e 100644 --- a/server/lib/scanners/baseScanner.ts +++ b/server/lib/scanners/baseScanner.ts @@ -1,7 +1,12 @@ import TheMovieDb from '@server/api/themoviedb'; -import { MediaStatus, MediaType } from '@server/constants/media'; +import { + MediaRequestStatus, + MediaStatus, + MediaType, +} from '@server/constants/media'; import { getRepository } from '@server/datasource'; import Media from '@server/entity/Media'; +import MediaRequest from '@server/entity/MediaRequest'; import Season from '@server/entity/Season'; import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; @@ -62,6 +67,7 @@ class BaseScanner { protected scannerName: string; protected enable4kMovie = false; protected enable4kShow = false; + protected enableAudioBook = false; protected sessionId: string; protected running = false; readonly asyncLock = new AsyncLock(); @@ -257,6 +263,150 @@ class BaseScanner { }); } + protected async processBook( + hcId: number, + { + is4k = false, + mediaAddedAt, + ratingKey, + serviceId, + externalServiceId, + externalServiceSlug, + processing = false, + title = 'Unknown Title', + hasFile = true, + }: ProcessOptions = {} + ): Promise { + const mediaRepository = getRepository(Media); + + await this.asyncLock.dispatch(hcId, async () => { + const existing = await this.getExisting(hcId, MediaType.BOOK); + + if (existing) { + let changedExisting = false; + + const statusField = is4k ? 'status4k' : 'status'; + + if (existing[statusField] !== MediaStatus.AVAILABLE || !hasFile) { + const previousStatus = existing[statusField]; + const lostFiles = + !hasFile && + (previousStatus === MediaStatus.PROCESSING || + previousStatus === MediaStatus.AVAILABLE || + previousStatus === MediaStatus.PARTIALLY_AVAILABLE); + + existing[statusField] = + !processing && hasFile + ? MediaStatus.AVAILABLE + : !processing && lostFiles + ? MediaStatus.UNKNOWN + : processing + ? previousStatus === MediaStatus.DELETED + ? MediaStatus.DELETED + : MediaStatus.PROCESSING + : previousStatus; + + if (existing[statusField] !== previousStatus) { + if (mediaAddedAt) { + existing.mediaAddedAt = mediaAddedAt; + } + changedExisting = true; + } + } + + if (!changedExisting && !existing.mediaAddedAt && mediaAddedAt) { + existing.mediaAddedAt = mediaAddedAt; + changedExisting = true; + } + + if ( + ratingKey && + existing[is4k ? 'ratingKey4k' : 'ratingKey'] !== ratingKey + ) { + existing[is4k ? 'ratingKey4k' : 'ratingKey'] = ratingKey; + changedExisting = true; + } + + if ( + serviceId !== undefined && + existing[is4k ? 'serviceId4k' : 'serviceId'] !== serviceId + ) { + existing[is4k ? 'serviceId4k' : 'serviceId'] = serviceId; + changedExisting = true; + } + + if ( + externalServiceId !== undefined && + existing[is4k ? 'externalServiceId4k' : 'externalServiceId'] !== + externalServiceId + ) { + existing[is4k ? 'externalServiceId4k' : 'externalServiceId'] = + externalServiceId; + changedExisting = true; + } + + if ( + externalServiceSlug !== undefined && + existing[is4k ? 'externalServiceSlug4k' : 'externalServiceSlug'] !== + externalServiceSlug + ) { + existing[is4k ? 'externalServiceSlug4k' : 'externalServiceSlug'] = + externalServiceSlug; + changedExisting = true; + } + + if (changedExisting) { + await mediaRepository.save(existing); + this.log( + `Media for ${title} exists. Changes were detected and the title will be updated.`, + 'info' + ); + } else { + this.log(`Title already exists and no changes detected for ${title}`); + } + } else { + if (!processing && !hasFile) { + return; + } + + const newMedia = new Media(); + newMedia.tmdbId = hcId; + + newMedia.status = + !is4k && !processing + ? MediaStatus.AVAILABLE + : !is4k && processing + ? MediaStatus.PROCESSING + : MediaStatus.UNKNOWN; + newMedia.status4k = + is4k && this.enableAudioBook && !processing + ? MediaStatus.AVAILABLE + : is4k && this.enableAudioBook && processing + ? MediaStatus.PROCESSING + : MediaStatus.UNKNOWN; + newMedia.mediaType = MediaType.BOOK; + newMedia.serviceId = !is4k ? serviceId : undefined; + newMedia.serviceId4k = is4k ? serviceId : undefined; + newMedia.externalServiceId = !is4k ? externalServiceId : undefined; + newMedia.externalServiceId4k = is4k ? externalServiceId : undefined; + newMedia.externalServiceSlug = !is4k ? externalServiceSlug : undefined; + newMedia.externalServiceSlug4k = is4k ? externalServiceSlug : undefined; + + if (mediaAddedAt) { + newMedia.mediaAddedAt = mediaAddedAt; + } + + if (ratingKey) { + newMedia.ratingKey = !is4k ? ratingKey : undefined; + newMedia.ratingKey4k = + is4k && this.enableAudioBook ? ratingKey : undefined; + } + await mediaRepository.save(newMedia); + this.log(`Saved new media: ${title}`); + } + }); + } + /** * processShow takes a TMDB ID and an array of ProcessableSeasons, which * should include the total episodes a sesaon has + the total available @@ -301,7 +451,14 @@ class BaseScanner { ) ?? [] ).length; - for (const season of seasons) { + for (const rawSeason of seasons) { + // Files can't promote a season the provider reports as empty. + // Zeroing the counts leaves any existing status for availabilitySync to judge. + const season = + rawSeason.totalEpisodes > 0 + ? rawSeason + : { ...rawSeason, episodes: 0, episodes4k: 0 }; + const existingSeason = media?.seasons.find( (es) => es.seasonNumber === season.seasonNumber ); @@ -467,24 +624,38 @@ class BaseScanner { (s) => s.seasonNumber !== 0 ); - // Check the actual season objects instead scanner input - // to determine overall availability status - // UNKNOWN seasons are treated as neutral (no signal) rather than - // blockers, so a stale/orphan placeholder season can't hold the - // show at PARTIALLY_AVAILABLE indefinitely. + // DB-only seasons block the rollup unless UNKNOWN (orphan placeholders + // can never be revisited by a scan and would pin the show forever). + const countsTowardsRollup = ( + s: Season, + statusKey: 'status' | 'status4k' + ): boolean => { + const scannedSeason = seasons.find( + (season) => season.seasonNumber === s.seasonNumber + ); + + if (scannedSeason) { + return scannedSeason.totalEpisodes > 0; + } + + return s[statusKey] !== MediaStatus.UNKNOWN; + }; + + const standardSeasonsForRollup = nonSpecialSeasons.filter((s) => + countsTowardsRollup(s, 'status') + ); const isAllStandardSeasonsAvailable = - nonSpecialSeasons.length > 0 && - nonSpecialSeasons - .filter((s) => s.status !== MediaStatus.UNKNOWN) - .every((s) => s.status === MediaStatus.AVAILABLE) && - nonSpecialSeasons.some((s) => s.status === MediaStatus.AVAILABLE); + standardSeasonsForRollup.length > 0 && + standardSeasonsForRollup.every( + (s) => s.status === MediaStatus.AVAILABLE + ); + const seasons4kForRollup = nonSpecialSeasons.filter((s) => + countsTowardsRollup(s, 'status4k') + ); const isAll4kSeasonsAvailable = - nonSpecialSeasons.length > 0 && - nonSpecialSeasons - .filter((s) => s.status4k !== MediaStatus.UNKNOWN) - .every((s) => s.status4k === MediaStatus.AVAILABLE) && - nonSpecialSeasons.some((s) => s.status4k === MediaStatus.AVAILABLE); + seasons4kForRollup.length > 0 && + seasons4kForRollup.every((s) => s.status4k === MediaStatus.AVAILABLE); media.status = isAllStandardSeasonsAvailable ? MediaStatus.AVAILABLE @@ -529,19 +700,18 @@ class BaseScanner { (s) => s.seasonNumber !== 0 ); + const newSeasonsForRollup = nonSpecialNewSeasons.filter( + (s) => + (seasons.find((season) => season.seasonNumber === s.seasonNumber) + ?.totalEpisodes ?? 0) > 0 + ); const isAllStandardSeasonsAvailable = - nonSpecialNewSeasons.length > 0 && - nonSpecialNewSeasons - .filter((s) => s.status !== MediaStatus.UNKNOWN) - .every((s) => s.status === MediaStatus.AVAILABLE) && - nonSpecialNewSeasons.some((s) => s.status === MediaStatus.AVAILABLE); + newSeasonsForRollup.length > 0 && + newSeasonsForRollup.every((s) => s.status === MediaStatus.AVAILABLE); const isAll4kSeasonsAvailable = - nonSpecialNewSeasons.length > 0 && - nonSpecialNewSeasons - .filter((s) => s.status4k !== MediaStatus.UNKNOWN) - .every((s) => s.status4k === MediaStatus.AVAILABLE) && - nonSpecialNewSeasons.some( + newSeasonsForRollup.length > 0 && + newSeasonsForRollup.every( (s) => s.status4k === MediaStatus.AVAILABLE ); @@ -624,6 +794,43 @@ class BaseScanner { }); } + /** + * Declines APPROVED requests bound to media that has been orphaned before completion. + * DECLINED clears the duplicate-request guard so the user can re-request it. + * Callers must load the requests relation on the media. + */ + protected async declineOrphanedRequests( + media: Media, + is4k: boolean + ): Promise { + if (media.requests === undefined) { + throw new Error( + `declineOrphanedRequests called for media ${media.id} without the 'requests' relation loaded` + ); + } + + const requestRepository = getRepository(MediaRequest); + + const orphanedRequests = (media.requests ?? []).filter( + (request) => + request.is4k === is4k && request.status === MediaRequestStatus.APPROVED + ); + + for (const request of orphanedRequests) { + request.status = MediaRequestStatus.DECLINED; + // Ensure that the media relation is set so the AfterUpdate + // notification hook can resolve it + request.media = media; + await requestRepository.save(request); + this.log( + `Declined orphaned ${ + media.mediaType === MediaType.MOVIE ? 'movie' : 'series' + } request ${request.id} for ${media.tmdbId} not found in any Sonarr/Radarr server.`, + 'info' + ); + } + } + /** * Call startRun from child class whenever a run is starting to * ensure required values are set @@ -653,6 +860,14 @@ class BaseScanner { ); } + this.enableAudioBook = settings.readarr.some((readarr) => readarr.is4k); + if (this.enableAudioBook) { + this.log( + 'At least one Audiobook Readarr server was detected. Audiobook detection is now enabled', + 'info' + ); + } + this.running = true; return sessionId; diff --git a/server/lib/scanners/jellyfin/index.ts b/server/lib/scanners/jellyfin/index.ts index 7d6a3e5ad1..a3e2300193 100644 --- a/server/lib/scanners/jellyfin/index.ts +++ b/server/lib/scanners/jellyfin/index.ts @@ -10,6 +10,7 @@ import { ANIME_KEYWORD_ID } from '@server/api/themoviedb/constants'; import type { TmdbKeyword, TmdbTvDetails, + TmdbTvScanDetails, } from '@server/api/themoviedb/interfaces'; import { MediaServerType } from '@server/constants/server'; import { getRepository } from '@server/datasource'; @@ -73,10 +74,9 @@ class JellyfinScanner } if (imdbId && !tmdbId) { - const tmdbMovie = await this.tmdb.getMediaByImdbId({ + tmdbId = await this.tmdb.resolveImdbIdForScan({ imdbId: imdbId, }); - tmdbId = tmdbMovie.id; } if (!tmdbId) { @@ -184,15 +184,15 @@ class JellyfinScanner }: { tmdbId?: number; tvdbId?: number; - }): Promise { + }): Promise { let tvShow; if (tmdbId) { - tvShow = await this.tmdb.getTvShow({ + tvShow = await this.tmdb.getTvShowForScan({ tvId: Number(tmdbId), }); } else if (tvdbId) { - tvShow = await this.tmdb.getShowByTvdbId({ + tvShow = await this.tmdb.getShowByTvdbIdForScan({ tvdbId: Number(tvdbId), }); } else { @@ -215,7 +215,7 @@ class JellyfinScanner } private async processJellyfinShow(jellyfinitem: JellyfinLibraryItem) { - let tvShow: TmdbTvDetails | null = null; + let tvShow: TmdbTvScanDetails | TmdbTvDetails | null = null; try { const Id = @@ -262,7 +262,7 @@ class JellyfinScanner tvdbSeasonFromAnidb = result?.tvdbSeason; if (result?.tvdbId) { try { - tvShow = await this.tmdb.getShowByTvdbId({ + tvShow = await this.tmdb.getShowByTvdbIdForScan({ tvdbId: result.tvdbId, }); } catch { @@ -285,11 +285,9 @@ class JellyfinScanner const processableSeasons: ProcessableSeason[] = []; const settings = getSettings(); - const filteredSeasons = ( - settings.main.enableSpecialEpisodes - ? seasons - : seasons.filter((sn) => sn.season_number !== 0) - ).filter((sn) => sn.episode_count > 0); + const filteredSeasons = settings.main.enableSpecialEpisodes + ? seasons + : seasons.filter((sn) => sn.season_number !== 0); for (const season of filteredSeasons) { const matchedJellyfinSeason = jellyfinSeasons.find((md) => { diff --git a/server/lib/scanners/jellyfin/jellyfin.test.ts b/server/lib/scanners/jellyfin/jellyfin.test.ts new file mode 100644 index 0000000000..8bc0ff6452 --- /dev/null +++ b/server/lib/scanners/jellyfin/jellyfin.test.ts @@ -0,0 +1,524 @@ +import animeList from '@server/api/animelist'; +import type { + JellyfinLibraryItem, + JellyfinLibraryItemExtended, +} from '@server/api/jellyfin'; +import JellyfinAPI from '@server/api/jellyfin'; +import TheMovieDb from '@server/api/themoviedb'; +import type { + TmdbTvDetails, + TmdbTvSeasonResult, +} from '@server/api/themoviedb/interfaces'; +import { MediaStatus, MediaType } from '@server/constants/media'; +import { MediaServerType } from '@server/constants/server'; +import { getRepository } from '@server/datasource'; +import Media from '@server/entity/Media'; +import Season from '@server/entity/Season'; +import { User } from '@server/entity/User'; +import { jellyfinFullScanner } from '@server/lib/scanners/jellyfin'; +import type { Library } from '@server/lib/settings'; +import { getSettings } from '@server/lib/settings'; +import { setupTestDb } from '@server/test/db'; +import { runWithMockTimers } from '@server/test/runWithMockTimers'; +import assert from 'node:assert/strict'; +import { beforeEach, describe, it } from 'node:test'; + +// --- Mock animeList.sync to avoid filesystem/network I/O in tests --- +Object.defineProperty(animeList, 'sync', { + value: async () => {}, + configurable: true, + writable: true, +}); + +// --- Mock JellyfinAPI --- +let getLibraryContentsImpl: ( + id: string +) => Promise = async () => []; +let getItemDataImpl: ( + id: string +) => Promise = async () => undefined; +let getSeasonsImpl: ( + seriesID: string +) => Promise = async () => []; +let getEpisodesImpl: ( + seriesID: string, + seasonID: string +) => Promise = async () => []; + +Object.defineProperty(JellyfinAPI.prototype, 'getLibraryContents', { + get() { + return async (id: string) => getLibraryContentsImpl(id); + }, + set() {}, + configurable: true, +}); + +Object.defineProperty(JellyfinAPI.prototype, 'getItemData', { + get() { + return async (id: string) => getItemDataImpl(id); + }, + set() {}, + configurable: true, +}); + +Object.defineProperty(JellyfinAPI.prototype, 'getSeasons', { + get() { + return async (seriesID: string) => getSeasonsImpl(seriesID); + }, + set() {}, + configurable: true, +}); + +Object.defineProperty(JellyfinAPI.prototype, 'getEpisodes', { + get() { + return async (seriesID: string, seasonID: string) => + getEpisodesImpl(seriesID, seasonID); + }, + set() {}, + configurable: true, +}); + +Object.defineProperty(JellyfinAPI.prototype, 'setUserId', { + get() { + return () => {}; + }, + set() {}, + configurable: true, +}); + +// --- Mock TheMovieDb --- +let getTvShowImpl: (args: { + tvId: number; + language?: string; +}) => Promise = async () => fakeTmdbShow(1); + +Object.defineProperty(TheMovieDb.prototype, 'getTvShow', { + get() { + return async (args: { tvId: number; language?: string }) => + getTvShowImpl(args); + }, + set() {}, + configurable: true, +}); + +Object.defineProperty(TheMovieDb.prototype, 'getTvShowForScan', { + get() { + return async (args: { tvId: number; language?: string }) => + getTvShowImpl(args); + }, + set() {}, + configurable: true, +}); + +// both are assigned in the constructor, so the prototype stubs miss the +// instance jellyfinFullScanner built when it was first imported +for (const method of ['getTvShow', 'getTvShowForScan'] as const) { + Object.defineProperty(jellyfinFullScanner.tmdb, method, { + value: async (args: { tvId: number; language?: string }) => + getTvShowImpl(args), + configurable: true, + }); +} + +setupTestDb(); + +// --- Helpers --- + +function fakeTmdbShow( + tmdbId: number, + seasons: TmdbTvSeasonResult[] = [ + { + id: 1, + air_date: '2024-01-01', + episode_count: 10, + name: 'Season 1', + overview: '', + season_number: 1, + }, + ] +): TmdbTvDetails { + return { + id: tmdbId, + content_ratings: { results: [] }, + created_by: [], + episode_run_time: [], + first_air_date: '2024-01-01', + genres: [], + homepage: '', + in_production: false, + languages: ['en'], + last_air_date: '2024-01-01', + name: 'Test Show', + networks: [], + number_of_episodes: 10, + number_of_seasons: seasons.length, + origin_country: ['US'], + original_language: 'en', + original_name: 'Test Show', + overview: '', + popularity: 0, + production_companies: [], + production_countries: [], + spoken_languages: [], + seasons, + status: 'Ended', + type: 'Scripted', + vote_average: 0, + vote_count: 0, + aggregate_credits: { cast: [] }, + credits: { crew: [] }, + external_ids: {}, + keywords: { results: [] }, + videos: { results: [] }, + }; +} + +function fakeJellyfinSeriesItem(id: string): JellyfinLibraryItem { + return { + Name: 'Test Show', + Id: id, + Type: 'Series', + HasSubtitles: false, + LocationType: 'FileSystem', + MediaType: 'Video', + }; +} + +function fakeJellyfinShowMetadata( + id: string, + tmdbId: string +): JellyfinLibraryItemExtended { + return { + Name: 'Test Show', + Id: id, + Type: 'Series', + HasSubtitles: false, + LocationType: 'FileSystem', + MediaType: 'Video', + ProviderIds: { Tmdb: tmdbId }, + }; +} + +function fakeJellyfinSeason( + seasonNumber: number, + id: string +): JellyfinLibraryItem { + return { + Name: `Season ${seasonNumber}`, + Id: id, + IndexNumber: seasonNumber, + Type: 'Season', + HasSubtitles: false, + LocationType: 'FileSystem', + MediaType: 'Video', + }; +} + +function fakeJellyfinEpisodes(count: number): JellyfinLibraryItem[] { + return Array.from({ length: count }, (_, i) => ({ + Name: `Episode ${i + 1}`, + Id: `ep-${i}`, + IndexNumber: i + 1, + Type: 'Episode' as const, + HasSubtitles: false, + LocationType: 'FileSystem' as const, + MediaType: 'Video', + })); +} + +function configureJellyfinWithLibrary( + libraries: Library[] = [ + { id: 'test-library-id', name: 'TV Shows', enabled: true, type: 'show' }, + ] +): void { + const settings = getSettings(); + settings.main.mediaServerType = MediaServerType.JELLYFIN; + settings.jellyfin = { + ...settings.jellyfin, + apiKey: 'test-api-key', + libraries, + }; +} + +describe('Jellyfin Scanner', () => { + beforeEach(async () => { + getLibraryContentsImpl = async () => []; + getItemDataImpl = async () => undefined; + getSeasonsImpl = async () => []; + getEpisodesImpl = async () => []; + getTvShowImpl = async () => fakeTmdbShow(1); + + const userRepository = getRepository(User); + const existingAdmin = await userRepository.findOne({ where: { id: 1 } }); + if (!existingAdmin) { + const admin = new User(); + admin.id = 1; + admin.jellyfinUserId = 'admin-user-id'; + admin.jellyfinDeviceId = 'admin-device-id'; + admin.email = 'admin@test.com'; + admin.permissions = 2; + admin.username = 'admin'; + await userRepository.save(admin); + } + }); + + describe('empty TMDB season handling', () => { + it('should mark show as available when all non-empty TMDB seasons are fully scanned and an empty placeholder season exists in the DB', async () => { + configureJellyfinWithLibrary(); + + const mediaRepository = getRepository(Media); + + const media = new Media(); + media.tmdbId = 5000; + media.mediaType = MediaType.TV; + media.status = MediaStatus.PARTIALLY_AVAILABLE; + media.jellyfinMediaId = 'jf-scanner-show-id'; + media.seasons = [ + new Season({ + seasonNumber: 1, + status: MediaStatus.AVAILABLE, + status4k: MediaStatus.UNKNOWN, + }), + new Season({ + seasonNumber: 2, + status: MediaStatus.AVAILABLE, + status4k: MediaStatus.UNKNOWN, + }), + new Season({ + seasonNumber: 3, + status: MediaStatus.UNKNOWN, + status4k: MediaStatus.UNKNOWN, + }), + ]; + + await mediaRepository.save(media); + + getTvShowImpl = async () => + fakeTmdbShow(5000, [ + { + id: 1, + air_date: '2024-01-01', + episode_count: 10, + name: 'Season 1', + overview: '', + season_number: 1, + }, + { + id: 2, + air_date: '2024-01-01', + episode_count: 10, + name: 'Season 2', + overview: '', + season_number: 2, + }, + { + id: 3, + air_date: '2024-01-01', + episode_count: 0, + name: 'Season 3', + overview: '', + season_number: 3, + }, + ]); + + // Jellyfin: S1 and S2 are fully scanned; S3 has no files. + getLibraryContentsImpl = async (id: string) => { + if (id === 'test-library-id') { + return [fakeJellyfinSeriesItem('jf-scanner-show-id')]; + } + return []; + }; + + getItemDataImpl = async (id: string) => { + if (id === 'jf-scanner-show-id') { + return fakeJellyfinShowMetadata('jf-scanner-show-id', '5000'); + } + return undefined; + }; + + getSeasonsImpl = async (seriesID: string) => { + if (seriesID === 'jf-scanner-show-id') { + return [ + fakeJellyfinSeason(1, 'jf-scanner-s1-id'), + fakeJellyfinSeason(2, 'jf-scanner-s2-id'), + ]; + } + return []; + }; + + getEpisodesImpl = async (_seriesID: string, seasonID: string) => { + if (seasonID === 'jf-scanner-s1-id') return fakeJellyfinEpisodes(10); + if (seasonID === 'jf-scanner-s2-id') return fakeJellyfinEpisodes(10); + return []; + }; + + await runWithMockTimers(() => jellyfinFullScanner.run()); + + const updated = await mediaRepository.findOneOrFail({ + where: { tmdbId: 5000 }, + relations: ['seasons'], + }); + + assert.strictEqual( + updated.status, + MediaStatus.AVAILABLE, + 'Show should be AVAILABLE when all non-empty TMDB seasons are fully scanned, ignoring empty placeholder seasons' + ); + }); + + it('should mark show as available when an orphan UNKNOWN season exists in the DB but not in TMDB', async () => { + configureJellyfinWithLibrary(); + + const mediaRepository = getRepository(Media); + + const media = new Media(); + media.tmdbId = 5001; + media.mediaType = MediaType.TV; + media.status = MediaStatus.PARTIALLY_AVAILABLE; + media.jellyfinMediaId = 'jf-orphan-show-id'; + media.seasons = [ + new Season({ + seasonNumber: 1, + status: MediaStatus.AVAILABLE, + status4k: MediaStatus.UNKNOWN, + }), + // Not present in the TMDB season list below + new Season({ + seasonNumber: 2, + status: MediaStatus.UNKNOWN, + status4k: MediaStatus.UNKNOWN, + }), + ]; + + await mediaRepository.save(media); + + getTvShowImpl = async () => + fakeTmdbShow(5001, [ + { + id: 1, + air_date: '2024-01-01', + episode_count: 10, + name: 'Season 1', + overview: '', + season_number: 1, + }, + ]); + + getLibraryContentsImpl = async (id: string) => { + if (id === 'test-library-id') { + return [fakeJellyfinSeriesItem('jf-orphan-show-id')]; + } + return []; + }; + + getItemDataImpl = async (id: string) => { + if (id === 'jf-orphan-show-id') { + return fakeJellyfinShowMetadata('jf-orphan-show-id', '5001'); + } + return undefined; + }; + + getSeasonsImpl = async (seriesID: string) => { + if (seriesID === 'jf-orphan-show-id') { + return [fakeJellyfinSeason(1, 'jf-orphan-s1-id')]; + } + return []; + }; + + getEpisodesImpl = async (_seriesID: string, seasonID: string) => { + if (seasonID === 'jf-orphan-s1-id') return fakeJellyfinEpisodes(10); + return []; + }; + + await runWithMockTimers(() => jellyfinFullScanner.run()); + + const updated = await mediaRepository.findOneOrFail({ + where: { tmdbId: 5001 }, + relations: ['seasons'], + }); + + assert.strictEqual( + updated.status, + MediaStatus.AVAILABLE, + 'Show should be AVAILABLE when the only DB season missing from TMDB is an UNKNOWN orphan placeholder' + ); + }); + + it('should keep show partially available when a season missing from TMDB was previously available', async () => { + configureJellyfinWithLibrary(); + + const mediaRepository = getRepository(Media); + + const media = new Media(); + media.tmdbId = 5002; + media.mediaType = MediaType.TV; + media.status = MediaStatus.PARTIALLY_AVAILABLE; + media.jellyfinMediaId = 'jf-deleted-show-id'; + media.seasons = [ + new Season({ + seasonNumber: 1, + status: MediaStatus.AVAILABLE, + status4k: MediaStatus.UNKNOWN, + }), + new Season({ + seasonNumber: 2, + status: MediaStatus.DELETED, + status4k: MediaStatus.UNKNOWN, + }), + ]; + + await mediaRepository.save(media); + + getTvShowImpl = async () => + fakeTmdbShow(5002, [ + { + id: 1, + air_date: '2024-01-01', + episode_count: 10, + name: 'Season 1', + overview: '', + season_number: 1, + }, + ]); + + getLibraryContentsImpl = async (id: string) => { + if (id === 'test-library-id') { + return [fakeJellyfinSeriesItem('jf-deleted-show-id')]; + } + return []; + }; + + getItemDataImpl = async (id: string) => { + if (id === 'jf-deleted-show-id') { + return fakeJellyfinShowMetadata('jf-deleted-show-id', '5002'); + } + return undefined; + }; + + getSeasonsImpl = async (seriesID: string) => { + if (seriesID === 'jf-deleted-show-id') { + return [fakeJellyfinSeason(1, 'jf-deleted-s1-id')]; + } + return []; + }; + + getEpisodesImpl = async (_seriesID: string, seasonID: string) => { + if (seasonID === 'jf-deleted-s1-id') return fakeJellyfinEpisodes(10); + return []; + }; + + await runWithMockTimers(() => jellyfinFullScanner.run()); + + const updated = await mediaRepository.findOneOrFail({ + where: { tmdbId: 5002 }, + relations: ['seasons'], + }); + + assert.strictEqual( + updated.status, + MediaStatus.PARTIALLY_AVAILABLE, + 'Show should stay PARTIALLY_AVAILABLE when a DELETED season is missing from the metadata provider' + ); + }); + }); +}); diff --git a/server/lib/scanners/plex/index.ts b/server/lib/scanners/plex/index.ts index 0ea05ac1a8..f3eda4a3bc 100644 --- a/server/lib/scanners/plex/index.ts +++ b/server/lib/scanners/plex/index.ts @@ -7,10 +7,10 @@ import { ANIME_KEYWORD_ID } from '@server/api/themoviedb/constants'; import type { TmdbKeyword, TmdbTvDetails, + TmdbTvScanDetails, } from '@server/api/themoviedb/interfaces'; import { getRepository } from '@server/datasource'; import { User } from '@server/entity/User'; -import cacheManager from '@server/lib/cache'; import type { MediaIds, ProcessableSeason, @@ -27,6 +27,9 @@ const tmdbRegex = new RegExp(/tmdb:\/\/([0-9]+)/); const tvdbRegex = new RegExp(/tvdb:\/\/([0-9]+)/); const tmdbShowRegex = new RegExp(/themoviedb:\/\/([0-9]+)/); const plexRegex = new RegExp(/plex:\/\//); +const plexCustomProviderRegex = new RegExp( + /tv\.plex\.agents\.custom(\.[a-zA-Z0-9]+)+:\/\// +); // Hama agent uses ASS naming, see details here: // https://github.com/ZeroQI/Absolute-Series-Scanner/blob/master/README.md#forcing-the-movieseries-id const hamaTvdbRegex = new RegExp(/hama:\/\/tvdb[0-9]?-([0-9]+)/); @@ -261,15 +264,15 @@ class PlexScanner }: { tmdbId?: number; tvdbId?: number; - }): Promise { + }): Promise { let tvShow; if (tmdbId) { - tvShow = await this.tmdb.getTvShow({ + tvShow = await this.tmdb.getTvShowForScan({ tvId: Number(tmdbId), }); } else if (tvdbId) { - tvShow = await this.tmdb.getShowByTvdbId({ + tvShow = await this.tmdb.getShowByTvdbIdForScan({ tvdbId: Number(tvdbId), }); } else { @@ -321,13 +324,11 @@ class PlexScanner const seasons = tvShow.seasons; const processableSeasons: ProcessableSeason[] = []; - const settings = getSettings(); - const filteredSeasons = ( - settings.main.enableSpecialEpisodes - ? seasons - : seasons.filter((sn) => sn.season_number !== 0) - ).filter((sn) => sn.episode_count > 0); + const settings = getSettings(); + const filteredSeasons = settings.main.enableSpecialEpisodes + ? seasons + : seasons.filter((sn) => sn.season_number !== 0); for (const season of filteredSeasons) { const matchedPlexSeason = metadata.Children?.Metadata.find( @@ -382,21 +383,12 @@ class PlexScanner } private async getMediaIds(plexitem: PlexLibraryItem): Promise { - let mediaIds: Partial = {}; + const mediaIds: Partial = {}; // Check if item is using new plex movie/tv agent - if (plexitem.guid.match(plexRegex)) { - const guidCache = cacheManager.getCache('plexguid'); - - const cachedGuids = guidCache.data.get(plexitem.ratingKey); - - if (cachedGuids) { - this.log('GUIDs are cached. Skipping metadata request.', 'debug', { - mediaIds: cachedGuids, - title: plexitem.title, - }); - mediaIds = cachedGuids; - } - + if ( + plexitem.guid.match(plexRegex) || + plexitem.guid.match(plexCustomProviderRegex) + ) { const metadata = plexitem.Guid && plexitem.Guid.length > 0 ? plexitem @@ -424,31 +416,26 @@ class PlexScanner // If we got an IMDb ID, but no TMDB ID, lookup the TMDB ID with the IMDb ID if (mediaIds.imdbId && !mediaIds.tmdbId) { - const tmdbMedia = await this.tmdb.getMediaByImdbId({ + mediaIds.tmdbId = await this.tmdb.resolveImdbIdForScan({ imdbId: mediaIds.imdbId, }); - mediaIds.tmdbId = tmdbMedia.id; } if (mediaIds.tvdbId && !mediaIds.tmdbId) { - const show = await this.tmdb.getShowByTvdbId({ + const show = await this.tmdb.getShowByTvdbIdForScan({ tvdbId: mediaIds.tvdbId, }); mediaIds.tmdbId = show.id; } - // Cache GUIDs - guidCache.data.set(plexitem.ratingKey, mediaIds); - // Check if the agent is IMDb } else if (plexitem.guid.match(imdbRegex)) { const imdbMatch = plexitem.guid.match(imdbRegex); if (imdbMatch) { mediaIds.imdbId = imdbMatch[1]; - const tmdbMedia = await this.tmdb.getMediaByImdbId({ + mediaIds.tmdbId = await this.tmdb.resolveImdbIdForScan({ imdbId: mediaIds.imdbId, }); - mediaIds.tmdbId = tmdbMedia.id; } // Check if the agent is TMDB } else if (plexitem.guid.match(tmdbRegex)) { @@ -460,9 +447,9 @@ class PlexScanner } else if (plexitem.guid.match(tvdbRegex)) { const matchedtvdb = plexitem.guid.match(tvdbRegex); - // If we can find a tvdb Id, use it to get the full tmdb show details + // If we can find a tvdb Id, use it to resolve the tmdb id if (matchedtvdb) { - const show = await this.tmdb.getShowByTvdbId({ + const show = await this.tmdb.getShowByTvdbIdForScan({ tvdbId: Number(matchedtvdb[1]), }); @@ -480,7 +467,7 @@ class PlexScanner const matchedtvdb = plexitem.guid.match(hamaTvdbRegex); if (matchedtvdb) { - const show = await this.tmdb.getShowByTvdbId({ + const show = await this.tmdb.getShowByTvdbIdForScan({ tvdbId: Number(matchedtvdb[1]), }); @@ -502,19 +489,19 @@ class PlexScanner } else if (matchedhama) { const anidbId = Number(matchedhama[1]); const result = animeList.getFromAnidbId(anidbId); - let tvShow: TmdbTvDetails | null = null; + let tvShow: TmdbTvScanDetails | TmdbTvDetails | null = null; // Set isHama to true, so we can know to add special processing to this item mediaIds.isHama = true; // First try to lookup the show by TVDb ID if (result?.tvdbId) { - const extResponse = await this.tmdb.getByExternalId({ + const extResponse = await this.tmdb.getByExternalIdForScan({ externalId: result.tvdbId, type: 'tvdb', }); if (extResponse.tv_results[0]) { - tvShow = await this.tmdb.getTvShow({ + tvShow = await this.tmdb.getTvShowForScan({ tvId: extResponse.tv_results[0].id, }); mediaIds.tvdbId = result.tvdbId; @@ -533,10 +520,9 @@ class PlexScanner mediaIds.tmdbId = result.tmdbId; mediaIds.imdbId = result?.imdbId; } else if (result?.imdbId) { - const tmdbMovie = await this.tmdb.getMediaByImdbId({ + mediaIds.tmdbId = await this.tmdb.resolveImdbIdForScan({ imdbId: result.imdbId, }); - mediaIds.tmdbId = tmdbMovie.id; mediaIds.imdbId = result.imdbId; } } @@ -582,10 +568,10 @@ class PlexScanner if (special.tmdbId) { await this.processPlexMovieByTmdbId(episode, special.tmdbId); } else if (special.imdbId) { - const tmdbMovie = await this.tmdb.getMediaByImdbId({ + const tmdbId = await this.tmdb.resolveImdbIdForScan({ imdbId: special.imdbId, }); - await this.processPlexMovieByTmdbId(episode, tmdbMovie.id); + await this.processPlexMovieByTmdbId(episode, tmdbId); } } } diff --git a/server/lib/scanners/radarr/index.ts b/server/lib/scanners/radarr/index.ts index 98643dfd49..bde08d65d6 100644 --- a/server/lib/scanners/radarr/index.ts +++ b/server/lib/scanners/radarr/index.ts @@ -28,6 +28,8 @@ class RadarrScanner private scanned4kTmdbIds: Set = new Set(); private didScanStandard = false; private didScan4k = false; + private serverReturnedEmpty = false; + private server4kReturnedEmpty = false; constructor() { super('Radarr Scan', { bundleSize: 50 }); @@ -50,6 +52,8 @@ class RadarrScanner this.scanned4kTmdbIds.clear(); this.didScanStandard = false; this.didScan4k = false; + this.serverReturnedEmpty = false; + this.server4kReturnedEmpty = false; try { this.servers = uniqWith(settings.radarr, (radarrA, radarrB) => { @@ -82,6 +86,18 @@ class RadarrScanner this.didScanStandard = true; } + if (this.items.length === 0) { + if (server4k) { + this.server4kReturnedEmpty = true; + } else { + this.serverReturnedEmpty = true; + } + this.log( + `Radarr server ${server.name} returned no movies. Orphan cleanup for this profile type will be skipped.`, + 'warn' + ); + } + await this.loop(this.processRadarrMovie.bind(this), { sessionId }); } else { this.log(`Sync not enabled. Skipping Radarr server: ${server.name}`); @@ -106,6 +122,13 @@ class RadarrScanner this.didScan4k = false; } + if (this.serverReturnedEmpty) { + this.didScanStandard = false; + } + if (this.server4kReturnedEmpty) { + this.didScan4k = false; + } + await this.cleanupOrphanedMovies(); this.log('Radarr scan complete', 'info'); } catch (e) { @@ -141,18 +164,57 @@ class RadarrScanner } } + private async existsInAnyServer( + tmdbId: number, + is4k: boolean + ): Promise { + const servers = this.servers.filter( + (server) => + server.syncEnabled && (this.enable4kMovie && server.is4k) === is4k + ); + + for (const server of servers) { + try { + const api = new RadarrAPI({ + apiKey: server.apiKey, + url: RadarrAPI.buildUrl(server, '/api/v3'), + }); + const movies = await api.getLibraryMoviesByTmdbId(tmdbId); + + if (movies.some((movie) => movie.tmdbId === tmdbId)) { + return true; + } + } catch (e) { + this.log( + `Could not confirm movie ${tmdbId} against Radarr server ${server.name}. Skipping cleanup for it.`, + 'warn', + { errorMessage: e.message } + ); + return true; + } + } + + return false; + } + private async cleanupOrphanedMovies(): Promise { const mediaRepository = getRepository(Media); if (this.didScanStandard) { const processingMovies = await mediaRepository.find({ where: { mediaType: MediaType.MOVIE, status: MediaStatus.PROCESSING }, + relations: { requests: true }, }); for (const media of processingMovies) { if (!this.scannedTmdbIds.has(media.tmdbId)) { + if (await this.existsInAnyServer(media.tmdbId, false)) { + continue; + } + media.status = MediaStatus.UNKNOWN; await mediaRepository.save(media); + await this.declineOrphanedRequests(media, false); this.log( `Movie ${media.tmdbId} not found in any Radarr server. Status reset to UNKNOWN.`, 'info' @@ -172,12 +234,18 @@ class RadarrScanner mediaType: MediaType.MOVIE, status4k: MediaStatus.PROCESSING, }, + relations: { requests: true }, }); for (const media of processing4kMovies) { if (!this.scanned4kTmdbIds.has(media.tmdbId)) { + if (await this.existsInAnyServer(media.tmdbId, true)) { + continue; + } + media.status4k = MediaStatus.UNKNOWN; await mediaRepository.save(media); + await this.declineOrphanedRequests(media, true); this.log( `Movie ${media.tmdbId} not found in any 4K Radarr server. 4K status reset to UNKNOWN.`, 'info' diff --git a/server/lib/scanners/radarr/radarr.test.ts b/server/lib/scanners/radarr/radarr.test.ts index 382724e658..f99195a713 100644 --- a/server/lib/scanners/radarr/radarr.test.ts +++ b/server/lib/scanners/radarr/radarr.test.ts @@ -1,14 +1,21 @@ -import assert from 'node:assert/strict'; -import { beforeEach, describe, it } from 'node:test'; - import type { RadarrMovie } from '@server/api/servarr/radarr'; import RadarrAPI from '@server/api/servarr/radarr'; -import { MediaStatus, MediaType } from '@server/constants/media'; +import { + MediaRequestStatus, + MediaStatus, + MediaType, +} from '@server/constants/media'; import { getRepository } from '@server/datasource'; import Media from '@server/entity/Media'; +import MediaRequest from '@server/entity/MediaRequest'; +import { User } from '@server/entity/User'; +import { radarrScanner } from '@server/lib/scanners/radarr'; import type { RadarrSettings } from '@server/lib/settings'; import { getSettings } from '@server/lib/settings'; import { setupTestDb } from '@server/test/db'; +import { runWithMockTimers } from '@server/test/runWithMockTimers'; +import assert from 'node:assert/strict'; +import { beforeEach, describe, it, mock } from 'node:test'; let getMoviesImpl: () => Promise = async () => []; Object.defineProperty(RadarrAPI.prototype, 'getMovies', { @@ -19,7 +26,18 @@ Object.defineProperty(RadarrAPI.prototype, 'getMovies', { configurable: true, }); -import { radarrScanner } from '@server/lib/scanners/radarr'; +let getLibraryMoviesByTmdbIdImpl: ( + tmdbId: number +) => Promise = async () => []; +Object.defineProperty(RadarrAPI.prototype, 'getLibraryMoviesByTmdbId', { + set() {}, + get() { + return async (tmdbId: number) => getLibraryMoviesByTmdbIdImpl(tmdbId); + }, + configurable: true, +}); + +mock.method(MediaRequest, 'sendNotification', async () => undefined); setupTestDb(); @@ -70,6 +88,7 @@ function fakeRadarrMovie(overrides: Partial = {}): RadarrMovie { describe('Radarr Scanner', () => { beforeEach(() => { getMoviesImpl = async () => []; + getLibraryMoviesByTmdbIdImpl = async () => []; }); describe('unmonitored movie handling', () => { @@ -87,7 +106,7 @@ describe('Radarr Scanner', () => { fakeRadarrMovie({ monitored: false, hasFile: false }), ]; - await radarrScanner.run(); + await runWithMockTimers(() => radarrScanner.run()); const updated = await mediaRepository.findOneOrFail({ where: { tmdbId: 550 }, @@ -103,7 +122,7 @@ describe('Radarr Scanner', () => { fakeRadarrMovie({ tmdbId: 777, monitored: false, hasFile: false }), ]; - await radarrScanner.run(); + await runWithMockTimers(() => radarrScanner.run()); const media = await mediaRepository.findOne({ where: { tmdbId: 777 }, @@ -125,7 +144,7 @@ describe('Radarr Scanner', () => { fakeRadarrMovie({ tmdbId: 551, monitored: true, hasFile: true }), ]; - await radarrScanner.run(); + await runWithMockTimers(() => radarrScanner.run()); const updated = await mediaRepository.findOneOrFail({ where: { tmdbId: 551 }, @@ -147,7 +166,7 @@ describe('Radarr Scanner', () => { fakeRadarrMovie({ tmdbId: 552, monitored: true, hasFile: false }), ]; - await radarrScanner.run(); + await runWithMockTimers(() => radarrScanner.run()); const updated = await mediaRepository.findOneOrFail({ where: { tmdbId: 552 }, @@ -169,7 +188,7 @@ describe('Radarr Scanner', () => { fakeRadarrMovie({ tmdbId: 553, monitored: true, hasFile: false }), ]; - await radarrScanner.run(); + await runWithMockTimers(() => radarrScanner.run()); const updated = await mediaRepository.findOneOrFail({ where: { tmdbId: 553 }, @@ -191,7 +210,7 @@ describe('Radarr Scanner', () => { fakeRadarrMovie({ tmdbId: 554, monitored: false, hasFile: true }), ]; - await radarrScanner.run(); + await runWithMockTimers(() => radarrScanner.run()); const updated = await mediaRepository.findOneOrFail({ where: { tmdbId: 554 }, @@ -217,7 +236,7 @@ describe('Radarr Scanner', () => { getMoviesImpl = async () => []; - await radarrScanner.run(); + await runWithMockTimers(() => radarrScanner.run()); const updated = await mediaRepository.findOneOrFail({ where: { tmdbId: 950 }, @@ -235,10 +254,9 @@ describe('Radarr Scanner', () => { await mediaRepository.save(media); configureRadarr([{ syncEnabled: true }]); - // Radarr returns empty meaning movie was deleted - getMoviesImpl = async () => []; + getMoviesImpl = async () => [fakeRadarrMovie({ tmdbId: 1, id: 99 })]; - await radarrScanner.run(); + await runWithMockTimers(() => radarrScanner.run()); const updated = await mediaRepository.findOneOrFail({ where: { tmdbId: 999 }, @@ -258,7 +276,7 @@ describe('Radarr Scanner', () => { configureRadarr([{ syncEnabled: true }]); getMoviesImpl = async () => []; - await radarrScanner.run(); + await runWithMockTimers(() => radarrScanner.run()); const updated = await mediaRepository.findOneOrFail({ where: { tmdbId: 888 }, @@ -280,7 +298,7 @@ describe('Radarr Scanner', () => { fakeRadarrMovie({ tmdbId: 700, monitored: true, hasFile: false }), ]; - await radarrScanner.run(); + await runWithMockTimers(() => radarrScanner.run()); const updated = await mediaRepository.findOneOrFail({ where: { tmdbId: 700 }, @@ -301,7 +319,7 @@ describe('Radarr Scanner', () => { configureRadarr([{ syncEnabled: true }]); getMoviesImpl = async () => []; - await radarrScanner.run(); + await runWithMockTimers(() => radarrScanner.run()); const updated = await mediaRepository.findOneOrFail({ where: { tmdbId: 800 }, @@ -336,10 +354,10 @@ describe('Radarr Scanner', () => { if (callCount === 1) { return [fakeRadarrMovie({ tmdbId: 902, id: 10 })]; } - return []; + return [fakeRadarrMovie({ tmdbId: 903, id: 11 })]; }; - await radarrScanner.run(); + await runWithMockTimers(() => radarrScanner.run()); const updatedOrphan = await mediaRepository.findOneOrFail({ where: { tmdbId: 901 }, @@ -351,6 +369,76 @@ describe('Radarr Scanner', () => { }); assert.strictEqual(updatedExisting.status, MediaStatus.AVAILABLE); }); + + it('does not reset a movie added to Radarr after the scan started', async () => { + const mediaRepository = getRepository(Media); + + const media = new Media(); + media.tmdbId = 910; + media.mediaType = MediaType.MOVIE; + media.status = MediaStatus.PROCESSING; + await mediaRepository.save(media); + + configureRadarr([{ syncEnabled: true }]); + getMoviesImpl = async () => [fakeRadarrMovie({ tmdbId: 111 })]; + getLibraryMoviesByTmdbIdImpl = async (tmdbId) => [ + fakeRadarrMovie({ tmdbId }), + ]; + + await radarrScanner.run(); + + const updated = await mediaRepository.findOneOrFail({ + where: { tmdbId: 910 }, + }); + assert.strictEqual(updated.status, MediaStatus.PROCESSING); + }); + + it('does not reset a movie when the server cannot be reached', async () => { + const mediaRepository = getRepository(Media); + + const media = new Media(); + media.tmdbId = 911; + media.mediaType = MediaType.MOVIE; + media.status = MediaStatus.PROCESSING; + await mediaRepository.save(media); + + configureRadarr([{ syncEnabled: true }]); + getMoviesImpl = async () => [fakeRadarrMovie({ tmdbId: 111 })]; + getLibraryMoviesByTmdbIdImpl = async () => { + throw new Error('connect ECONNREFUSED'); + }; + + await radarrScanner.run(); + + const updated = await mediaRepository.findOneOrFail({ + where: { tmdbId: 911 }, + }); + assert.strictEqual(updated.status, MediaStatus.PROCESSING); + }); + + it('resets a movie when the server returns no row matching its id', async () => { + const mediaRepository = getRepository(Media); + + const media = new Media(); + media.tmdbId = 912; + media.mediaType = MediaType.MOVIE; + media.status = MediaStatus.PROCESSING; + await mediaRepository.save(media); + + configureRadarr([{ syncEnabled: true }]); + getMoviesImpl = async () => [fakeRadarrMovie({ tmdbId: 111 })]; + getLibraryMoviesByTmdbIdImpl = async () => [ + fakeRadarrMovie({ tmdbId: 111 }), + fakeRadarrMovie({ tmdbId: 222 }), + ]; + + await radarrScanner.run(); + + const updated = await mediaRepository.findOneOrFail({ + where: { tmdbId: 912 }, + }); + assert.strictEqual(updated.status, MediaStatus.UNKNOWN); + }); }); describe('4k orphaned movie cleanup', () => { @@ -365,9 +453,9 @@ describe('Radarr Scanner', () => { await mediaRepository.save(media); configureRadarr([{ syncEnabled: true, is4k: true }]); - getMoviesImpl = async () => []; + getMoviesImpl = async () => [fakeRadarrMovie({ tmdbId: 1, id: 99 })]; - await radarrScanner.run(); + await runWithMockTimers(() => radarrScanner.run()); const updated = await mediaRepository.findOneOrFail({ where: { tmdbId: 960 }, @@ -388,7 +476,7 @@ describe('Radarr Scanner', () => { configureRadarr([{ syncEnabled: true, is4k: true }]); getMoviesImpl = async () => []; - await radarrScanner.run(); + await runWithMockTimers(() => radarrScanner.run()); const updated = await mediaRepository.findOneOrFail({ where: { tmdbId: 961 }, @@ -396,4 +484,242 @@ describe('Radarr Scanner', () => { assert.strictEqual(updated.status4k, MediaStatus.AVAILABLE); }); }); + + describe('orphaned request handling', () => { + it('declines the approved request and resets media to UNKNOWN when the movie is orphaned', async () => { + const mediaRepository = getRepository(Media); + const requestRepository = getRepository(MediaRequest); + const userRepository = getRepository(User); + + const requestedBy = await userRepository.findOneOrFail({ + where: { id: 1 }, + }); + + const media = await mediaRepository.save( + new Media({ + tmdbId: 1003596, + mediaType: MediaType.MOVIE, + status: MediaStatus.PROCESSING, + }) + ); + + const settings = getSettings(); + settings.radarr = []; + settings.sonarr = []; + const request = await requestRepository.save( + new MediaRequest({ + type: MediaType.MOVIE, + status: MediaRequestStatus.APPROVED, + media, + requestedBy, + is4k: false, + }) + ); + + configureRadarr([{ syncEnabled: true }]); + getMoviesImpl = async () => [fakeRadarrMovie({ tmdbId: 1, id: 99 })]; + + await runWithMockTimers(() => radarrScanner.run()); + + const updatedMedia = await mediaRepository.findOneOrFail({ + where: { tmdbId: 1003596 }, + }); + const updatedRequest = await requestRepository.findOneOrFail({ + where: { id: request.id }, + }); + + assert.strictEqual(updatedMedia.status, MediaStatus.UNKNOWN); + assert.strictEqual(updatedRequest.status, MediaRequestStatus.DECLINED); + }); + + it('does not decline the request when the movie still exists in Radarr', async () => { + const mediaRepository = getRepository(Media); + const requestRepository = getRepository(MediaRequest); + const userRepository = getRepository(User); + + const requestedBy = await userRepository.findOneOrFail({ + where: { id: 1 }, + }); + + const media = await mediaRepository.save( + new Media({ + tmdbId: 700, + mediaType: MediaType.MOVIE, + status: MediaStatus.PROCESSING, + }) + ); + + const settings = getSettings(); + settings.radarr = []; + settings.sonarr = []; + const request = await requestRepository.save( + new MediaRequest({ + type: MediaType.MOVIE, + status: MediaRequestStatus.APPROVED, + media, + requestedBy, + is4k: false, + }) + ); + + configureRadarr([{ syncEnabled: true }]); + getMoviesImpl = async () => [ + fakeRadarrMovie({ tmdbId: 700, monitored: true, hasFile: false }), + ]; + + await runWithMockTimers(() => radarrScanner.run()); + + const updatedRequest = await requestRepository.findOneOrFail({ + where: { id: request.id }, + }); + assert.strictEqual(updatedRequest.status, MediaRequestStatus.APPROVED); + }); + + it('skips cleanup and leaves the request approved when Radarr returns an empty list', async () => { + const mediaRepository = getRepository(Media); + const requestRepository = getRepository(MediaRequest); + const userRepository = getRepository(User); + + const requestedBy = await userRepository.findOneOrFail({ + where: { id: 1 }, + }); + + const media = await mediaRepository.save( + new Media({ + tmdbId: 1234, + mediaType: MediaType.MOVIE, + status: MediaStatus.PROCESSING, + }) + ); + + const settings = getSettings(); + settings.radarr = []; + settings.sonarr = []; + const request = await requestRepository.save( + new MediaRequest({ + type: MediaType.MOVIE, + status: MediaRequestStatus.APPROVED, + media, + requestedBy, + is4k: false, + }) + ); + + configureRadarr([{ syncEnabled: true }]); + getMoviesImpl = async () => []; + + await runWithMockTimers(() => radarrScanner.run()); + + const updatedMedia = await mediaRepository.findOneOrFail({ + where: { tmdbId: 1234 }, + }); + const updatedRequest = await requestRepository.findOneOrFail({ + where: { id: request.id }, + }); + + assert.strictEqual(updatedMedia.status, MediaStatus.PROCESSING); + assert.strictEqual(updatedRequest.status, MediaRequestStatus.APPROVED); + }); + + it('declineOrphanedRequests throws when the requests relation is not loaded', async () => { + const media = new Media(); + media.id = 1; + media.tmdbId = 123; + media.mediaType = MediaType.MOVIE; + + await assert.rejects( + () => + ( + radarrScanner as unknown as { + declineOrphanedRequests: ( + m: Media, + is4k: boolean + ) => Promise; + } + ).declineOrphanedRequests(media, false), + /without the 'requests' relation loaded/ + ); + }); + + it('declines only the 4k request when the 4k dimension is orphaned but standard still exists', async () => { + const mediaRepository = getRepository(Media); + const requestRepository = getRepository(MediaRequest); + const userRepository = getRepository(User); + + const requestedBy = await userRepository.findOneOrFail({ + where: { id: 1 }, + }); + + const media = await mediaRepository.save( + new Media({ + tmdbId: 1003598, + mediaType: MediaType.MOVIE, + status: MediaStatus.PROCESSING, + status4k: MediaStatus.PROCESSING, + }) + ); + + const settings = getSettings(); + settings.radarr = []; + settings.sonarr = []; + const standardRequest = await requestRepository.save( + new MediaRequest({ + type: MediaType.MOVIE, + status: MediaRequestStatus.APPROVED, + media, + requestedBy, + is4k: false, + }) + ); + const fourKRequest = await requestRepository.save( + new MediaRequest({ + type: MediaType.MOVIE, + status: MediaRequestStatus.APPROVED, + media, + requestedBy, + is4k: true, + }) + ); + + configureRadarr([ + { syncEnabled: true, id: 0, hostname: 'server-standard' }, + { syncEnabled: true, id: 1, hostname: 'server-4k', is4k: true }, + ]); + + let callCount = 0; + getMoviesImpl = async () => { + callCount++; + if (callCount === 1) { + // standard server still has the movie + return [ + fakeRadarrMovie({ + tmdbId: 1003598, + id: 42, + monitored: true, + hasFile: false, + }), + ]; + } + // 4k server: populated but the movie is absent, so 4k dimension orphaned + return [fakeRadarrMovie({ tmdbId: 2, id: 88 })]; + }; + + await runWithMockTimers(() => radarrScanner.run()); + + const updatedMedia = await mediaRepository.findOneOrFail({ + where: { tmdbId: 1003598 }, + }); + const updatedStandard = await requestRepository.findOneOrFail({ + where: { id: standardRequest.id }, + }); + const updated4k = await requestRepository.findOneOrFail({ + where: { id: fourKRequest.id }, + }); + + assert.strictEqual(updatedMedia.status, MediaStatus.PROCESSING); + assert.strictEqual(updatedMedia.status4k, MediaStatus.UNKNOWN); + assert.strictEqual(updatedStandard.status, MediaRequestStatus.APPROVED); + assert.strictEqual(updated4k.status, MediaRequestStatus.DECLINED); + }); + }); }); diff --git a/server/lib/scanners/readarr/index.ts b/server/lib/scanners/readarr/index.ts new file mode 100644 index 0000000000..55e9f0d293 --- /dev/null +++ b/server/lib/scanners/readarr/index.ts @@ -0,0 +1,352 @@ +import type { ReadarrBook } from '@server/api/servarr/readarr'; +import ReadarrAPI from '@server/api/servarr/readarr'; +import { MediaStatus, MediaType } from '@server/constants/media'; +import { getRepository } from '@server/datasource'; +import Media from '@server/entity/Media'; +import type { + RunnableScanner, + StatusBase, +} from '@server/lib/scanners/baseScanner'; +import BaseScanner from '@server/lib/scanners/baseScanner'; +import type { ReadarrSettings } from '@server/lib/settings'; +import { getSettings } from '@server/lib/settings'; +import { uniqWith } from 'lodash'; +import { In } from 'typeorm'; + +type SyncStatus = StatusBase & { + currentServer: ReadarrSettings; + servers: ReadarrSettings[]; +}; + +class ReadarrScanner + extends BaseScanner + implements RunnableScanner +{ + private servers: ReadarrSettings[]; + private currentServer: ReadarrSettings; + private readarrApi: ReadarrAPI; + private scannedHcIds: Set = new Set(); + private scannedAudioHcIds: Set = new Set(); + private didScanStandard = false; + private didScanAudio = false; + private isRecentOnly = false; + + constructor(isRecentOnly = false) { + super('Readarr Scan', { bundleSize: 50 }); + this.isRecentOnly = isRecentOnly; + } + + public status(): SyncStatus { + return { + running: this.running, + progress: this.progress, + total: this.items.length, + currentServer: this.currentServer, + servers: this.servers, + }; + } + + public async run(): Promise { + if (this.running) { + this.log('Scan already in progress. Skipping.', 'info'); + return; + } + + const settings = getSettings(); + const sessionId = this.startRun(); + this.scannedHcIds.clear(); + this.scannedAudioHcIds.clear(); + this.didScanStandard = false; + this.didScanAudio = false; + + try { + this.servers = uniqWith(settings.readarr, (readarrA, readarrB) => { + return ( + readarrA.hostname === readarrB.hostname && + readarrA.port === readarrB.port && + readarrA.baseUrl === readarrB.baseUrl + ); + }); + + for (const server of this.servers) { + this.currentServer = server; + if (server.syncEnabled) { + if (this.isRecentOnly) { + await this.runRecentScan(server, sessionId); + } else { + await this.runFullScan(server, sessionId); + } + } else { + this.log(`Sync not enabled. Skipping Readarr server: ${server.name}`); + } + } + + // Only run cleanup if all servers of this profile type have sync enabled. + // If any server is skipped, we can't distinguish truly orphaned media from + // media that exists on an unscanned server (e.g. separate instances for + // different genres or languages). + const allStandardScanned = this.servers + .filter((s) => !this.enableAudioBook || !s.is4k) + .every((s) => s.syncEnabled); + const allAudioScanned = this.servers + .filter((s) => this.enableAudioBook && s.is4k) + .every((s) => s.syncEnabled); + + if (!allStandardScanned) { + this.didScanStandard = false; + } + if (!allAudioScanned) { + this.didScanAudio = false; + } + + await this.cleanupOrphanedBooks(); + + this.log( + this.isRecentOnly + ? 'Recently Added Scan Complete' + : 'Full Scan Complete', + 'info' + ); + } catch (e) { + this.log('Scan interrupted', 'error', { errorMessage: e.message }); + } finally { + this.endRun(sessionId); + } + } + + private async runFullScan( + server: ReadarrSettings, + sessionId: string + ): Promise { + this.log(`Beginning to process Readarr server: ${server.name}`, 'info'); + + this.readarrApi = new ReadarrAPI({ + apiKey: server.apiKey, + url: ReadarrAPI.buildUrl(server, '/api/v1'), + }); + + this.items = await this.readarrApi.getBooks(); + + const serverAudio = this.enableAudioBook && server.is4k; + if (serverAudio) { + this.didScanAudio = true; + } else { + this.didScanStandard = true; + } + + await this.loop(this.processReadarrBook.bind(this), { sessionId }); + } + + private async runRecentScan( + server: ReadarrSettings, + sessionId: string + ): Promise { + this.log( + `Beginning to process recently added for Readarr server: ${server.name}`, + 'info' + ); + + this.readarrApi = new ReadarrAPI({ + apiKey: server.apiKey, + url: ReadarrAPI.buildUrl(server, '/api/v1'), + }); + + const allBooks = await this.readarrApi.getBooks(); + + // Compare Readarr's state against what Seerr already has in its database. + // We skip any book that: + // 1. Has no valid Hardcover ID (can't be processed) + // 2. Is not already in Seerr (discovery is the full scan's job) + // 3. Already exists in Seerr with the same status (nothing to update) + const mediaRepository = getRepository(Media); + const existingMedia = await mediaRepository.find({ + where: { mediaType: MediaType.BOOK }, + select: ['tmdbId', 'status', 'status4k'], + }); + const existingMediaMap = new Map(existingMedia.map((m) => [m.tmdbId, m])); + + const serverAudio = this.enableAudioBook && server.is4k; + + // Readarr's full book list is authoritative here, so record every book it + // still knows about; orphan cleanup relies on a complete set. + if (serverAudio) { + this.didScanAudio = true; + } else { + this.didScanStandard = true; + } + + allBooks.forEach((book) => { + const hcId = parseInt(book.foreignBookId, 10); + if (isNaN(hcId)) { + return; + } + if (serverAudio) { + this.scannedAudioHcIds.add(hcId); + } else { + this.scannedHcIds.add(hcId); + } + }); + + this.items = allBooks.filter((book) => { + const hcId = parseInt(book.foreignBookId, 10); + if (isNaN(hcId)) return false; + + const existing = existingMediaMap.get(hcId); + const hasFile = (book.statistics?.bookFileCount ?? 0) > 0; + + if (!existing) { + // Book not in Seerr — skip it. Discovery of new books is the + // full scan's responsibility. + return false; + } + + // Check if the status in Seerr matches what Readarr reports + const currentStatus = serverAudio ? existing.status4k : existing.status; + + if (hasFile && currentStatus === MediaStatus.AVAILABLE) { + return false; + } + + if ( + !hasFile && + (book.monitored || book.grabbed) && + currentStatus === MediaStatus.PROCESSING + ) { + return false; + } + + // Status mismatch — needs updating + return true; + }); + + this.log( + `Filtered ${allBooks.length} total books to ${this.items.length} with state changes`, + 'info' + ); + + if (this.items.length === 0) { + this.log( + `No recently added books found for server: ${server.name}`, + 'info' + ); + } else { + await this.loop(this.processReadarrBook.bind(this), { sessionId }); + } + } + + private async processReadarrBook(readarrBook: ReadarrBook): Promise { + const serverAudio = this.enableAudioBook && this.currentServer.is4k; + const hcId = parseInt(readarrBook.foreignBookId, 10); + + if (isNaN(hcId)) { + const hasFile = (readarrBook.statistics?.bookFileCount ?? 0) > 0; + + if (!readarrBook.monitored && !readarrBook.grabbed && !hasFile) { + return; + } + + this.log('Invalid Hardcover ID for book. Skipping item.', 'warn', { + title: readarrBook.title, + foreignBookId: readarrBook.foreignBookId, + }); + return; + } + + if (serverAudio) { + this.scannedAudioHcIds.add(hcId); + } else { + this.scannedHcIds.add(hcId); + } + + try { + // Use bookFileCount as indicator of having files. In original Readarr, + // percentOfBooks (bookFileCount / bookCount * 100) works because + // bookCount includes books with files regardless of monitoring state. + // Some forks removed that fallback, with bookCount tied to monitoring + // state, so unmonitored books with files report percentOfBooks=0. + // bookFileCount > 0 inherently implies percentOfBooks >= 100 and works + // correctly for both implementations. + const hasFile = (readarrBook.statistics?.bookFileCount ?? 0) > 0; + + await this.processBook(hcId, { + is4k: serverAudio, + serviceId: this.currentServer.id, + externalServiceId: readarrBook.id, + externalServiceSlug: readarrBook.titleSlug, + title: readarrBook.title, + processing: !hasFile && (readarrBook.monitored || readarrBook.grabbed), + hasFile, + }); + } catch (e) { + this.log('Failed to process Readarr media', 'error', { + errorMessage: e.message, + title: readarrBook.title, + }); + } + } + + private async cleanupOrphanedBooks(): Promise { + const mediaRepository = getRepository(Media); + + if (this.didScanStandard) { + const trackedBooks = await mediaRepository.find({ + where: { + mediaType: MediaType.BOOK, + status: In([ + MediaStatus.PROCESSING, + MediaStatus.AVAILABLE, + MediaStatus.PARTIALLY_AVAILABLE, + ]), + }, + }); + + for (const media of trackedBooks) { + if (!this.scannedHcIds.has(media.tmdbId)) { + media.status = MediaStatus.UNKNOWN; + await mediaRepository.save(media); + this.log( + `Book ${media.tmdbId} not found in any Readarr server. Status reset to UNKNOWN.`, + 'info' + ); + } + } + } else { + this.log( + 'Skipping orphaned book cleanup: no standard Readarr servers were scanned.', + 'info' + ); + } + + if (this.didScanAudio) { + const trackedAudioBooks = await mediaRepository.find({ + where: { + mediaType: MediaType.BOOK, + status4k: In([ + MediaStatus.PROCESSING, + MediaStatus.AVAILABLE, + MediaStatus.PARTIALLY_AVAILABLE, + ]), + }, + }); + + for (const media of trackedAudioBooks) { + if (!this.scannedAudioHcIds.has(media.tmdbId)) { + media.status4k = MediaStatus.UNKNOWN; + await mediaRepository.save(media); + this.log( + `Book ${media.tmdbId} not found in any audiobook Readarr server. Audiobook status reset to UNKNOWN.`, + 'info' + ); + } + } + } else if (this.enableAudioBook) { + this.log( + 'Skipping orphaned audiobook cleanup: no audiobook Readarr servers were scanned.', + 'info' + ); + } + } +} + +export const readarrScanner = new ReadarrScanner(); +export const readarrRecentScanner = new ReadarrScanner(true); diff --git a/server/lib/scanners/sonarr/index.ts b/server/lib/scanners/sonarr/index.ts index 6e7610620c..b7db415dd4 100644 --- a/server/lib/scanners/sonarr/index.ts +++ b/server/lib/scanners/sonarr/index.ts @@ -6,6 +6,7 @@ import { ANIME_KEYWORD_ID } from '@server/api/themoviedb/constants'; import type { TmdbKeyword, TmdbTvDetails, + TmdbTvScanDetails, } from '@server/api/themoviedb/interfaces'; import { MediaStatus, MediaType } from '@server/constants/media'; import { getRepository } from '@server/datasource'; @@ -36,6 +37,8 @@ class SonarrScanner private scanned4kTvdbIds: Set = new Set(); private didScanStandard = false; private didScan4k = false; + private serverReturnedEmpty = false; + private server4kReturnedEmpty = false; constructor() { super('Sonarr Scan', { bundleSize: 50 }); @@ -58,6 +61,8 @@ class SonarrScanner this.scanned4kTvdbIds.clear(); this.didScanStandard = false; this.didScan4k = false; + this.serverReturnedEmpty = false; + this.server4kReturnedEmpty = false; try { this.servers = uniqWith(settings.sonarr, (sonarrA, sonarrB) => { @@ -90,6 +95,18 @@ class SonarrScanner this.didScanStandard = true; } + if (this.items.length === 0) { + if (server4k) { + this.server4kReturnedEmpty = true; + } else { + this.serverReturnedEmpty = true; + } + this.log( + `Sonarr server ${server.name} returned no series. Orphan cleanup for this profile type will be skipped.`, + 'warn' + ); + } + await this.loop(this.processSonarrSeries.bind(this), { sessionId }); } else { this.log(`Sync not enabled. Skipping Sonarr server: ${server.name}`); @@ -114,6 +131,13 @@ class SonarrScanner this.didScan4k = false; } + if (this.serverReturnedEmpty) { + this.didScanStandard = false; + } + if (this.server4kReturnedEmpty) { + this.didScan4k = false; + } + await this.cleanupOrphanedShows(); this.log('Sonarr scan complete', 'info'); } catch (e) { @@ -134,18 +158,18 @@ class SonarrScanner try { const mediaRepository = getRepository(Media); const processableSeasons: ProcessableSeason[] = []; - let tvShow: TmdbTvDetails; + let tvShow: TmdbTvScanDetails | TmdbTvDetails; const media = await mediaRepository.findOne({ where: { tvdbId: sonarrSeries.tvdbId }, }); if (!media || !media.tmdbId) { - tvShow = await this.tmdb.getShowByTvdbId({ + tvShow = await this.tmdb.getShowByTvdbIdForScan({ tvdbId: sonarrSeries.tvdbId, }); } else { - tvShow = await this.tmdb.getTvShow({ tvId: media.tmdbId }); + tvShow = await this.tmdb.getTvShowForScan({ tvId: media.tmdbId }); } const tmdbId = tvShow.id; @@ -158,13 +182,31 @@ class SonarrScanner if (!(metadataProvider instanceof TheMovieDb)) { tvShow = await metadataProvider.getTvShow({ tvId: tmdbId }); } + const settings = getSettings(); - const filteredSeasons = sonarrSeries.seasons.filter( - (sn) => - tvShow.seasons.find((s) => s.season_number === sn.seasonNumber) && - (!settings.main.enableSpecialEpisodes ? sn.seasonNumber !== 0 : true) - ); + const filteredSeasons = tvShow.seasons + .filter( + (sn) => settings.main.enableSpecialEpisodes || sn.season_number !== 0 + ) + .map((season) => { + const sonarrSeason = sonarrSeries.seasons.find( + (s) => s.seasonNumber === season.season_number + ); + if (!sonarrSeason) { + return { + seasonNumber: season.season_number, + episodeCount: season.episode_count, + monitored: false, + statistics: { + episodeFileCount: 0, + totalEpisodeCount: season.episode_count, + }, + }; + } else { + return sonarrSeason; + } + }); for (const season of filteredSeasons) { const totalAvailableEpisodes = season.statistics?.episodeFileCount ?? 0; @@ -194,17 +236,54 @@ class SonarrScanner } } + private async existsInAnyServer( + tvdbId: number, + is4k: boolean + ): Promise { + const servers = this.servers.filter( + (server) => + server.syncEnabled && (this.enable4kShow && server.is4k) === is4k + ); + + for (const server of servers) { + try { + const api = new SonarrAPI({ + apiKey: server.apiKey, + url: SonarrAPI.buildUrl(server, '/api/v3'), + }); + const series = await api.getLibrarySeriesByTvdbId(tvdbId); + + if (series.some((show) => show.tvdbId === tvdbId)) { + return true; + } + } catch (e) { + this.log( + `Could not confirm series ${tvdbId} against Sonarr server ${server.name}. Skipping cleanup for it.`, + 'warn', + { errorMessage: e.message } + ); + return true; + } + } + + return false; + } + private async cleanupOrphanedShows(): Promise { const mediaRepository = getRepository(Media); if (this.didScanStandard) { const processingShows = await mediaRepository.find({ where: { mediaType: MediaType.TV, status: MediaStatus.PROCESSING }, - relations: ['seasons'], + relations: { seasons: true, requests: true }, }); for (const media of processingShows) { if (media.tvdbId && !this.scannedTvdbIds.has(media.tvdbId)) { + if (await this.existsInAnyServer(media.tvdbId, false)) { + continue; + } + media.status = MediaStatus.UNKNOWN; for (const season of media.seasons) { if (season.status === MediaStatus.PROCESSING) { @@ -212,6 +291,7 @@ class SonarrScanner } } await mediaRepository.save(media); + await this.declineOrphanedRequests(media, false); this.log( `Show ${media.tmdbId} (tvdb: ${media.tvdbId}) not found in any Sonarr server. Status reset to UNKNOWN.`, 'info' @@ -228,11 +308,15 @@ class SonarrScanner if (this.didScan4k) { const processing4kShows = await mediaRepository.find({ where: { mediaType: MediaType.TV, status4k: MediaStatus.PROCESSING }, - relations: ['seasons'], + relations: { seasons: true, requests: true }, }); for (const media of processing4kShows) { if (media.tvdbId && !this.scanned4kTvdbIds.has(media.tvdbId)) { + if (await this.existsInAnyServer(media.tvdbId, true)) { + continue; + } + media.status4k = MediaStatus.UNKNOWN; for (const season of media.seasons) { if (season.status4k === MediaStatus.PROCESSING) { @@ -240,6 +324,7 @@ class SonarrScanner } } await mediaRepository.save(media); + await this.declineOrphanedRequests(media, true); this.log( `Show ${media.tmdbId} (tvdb: ${media.tvdbId}) not found in any 4K Sonarr server. 4K status reset to UNKNOWN.`, 'info' diff --git a/server/lib/scanners/sonarr/sonarr.test.ts b/server/lib/scanners/sonarr/sonarr.test.ts index 0cf32aab03..46994ec4c3 100644 --- a/server/lib/scanners/sonarr/sonarr.test.ts +++ b/server/lib/scanners/sonarr/sonarr.test.ts @@ -5,15 +5,23 @@ import type { TmdbTvDetails, TmdbTvSeasonResult, } from '@server/api/themoviedb/interfaces'; -import { MediaStatus, MediaType } from '@server/constants/media'; +import { + MediaRequestStatus, + MediaStatus, + MediaType, +} from '@server/constants/media'; import { getRepository } from '@server/datasource'; import Media from '@server/entity/Media'; +import MediaRequest from '@server/entity/MediaRequest'; import Season from '@server/entity/Season'; +import { User } from '@server/entity/User'; +import { sonarrScanner } from '@server/lib/scanners/sonarr'; import type { SonarrSettings } from '@server/lib/settings'; import { getSettings } from '@server/lib/settings'; import { setupTestDb } from '@server/test/db'; +import { runWithMockTimers } from '@server/test/runWithMockTimers'; import assert from 'node:assert/strict'; -import { beforeEach, describe, it } from 'node:test'; +import { beforeEach, describe, it, mock } from 'node:test'; let getSeriesImpl: () => Promise = async () => []; Object.defineProperty(SonarrAPI.prototype, 'getSeries', { @@ -24,6 +32,17 @@ Object.defineProperty(SonarrAPI.prototype, 'getSeries', { configurable: true, }); +let getLibrarySeriesByTvdbIdImpl: ( + tvdbId: number +) => Promise = async () => []; +Object.defineProperty(SonarrAPI.prototype, 'getLibrarySeriesByTvdbId', { + set() {}, + get() { + return async (tvdbId: number) => getLibrarySeriesByTvdbIdImpl(tvdbId); + }, + configurable: true, +}); + function fakeTmdbShow( tmdbId: number, seasons: TmdbTvSeasonResult[] = [ @@ -82,6 +101,10 @@ TheMovieDb.prototype.getShowByTvdbId = async function (args) { return getShowByTvdbIdImpl(args); }; +TheMovieDb.prototype.getShowByTvdbIdForScan = async function (args) { + return getShowByTvdbIdImpl(args); +}; + let getTvShowImpl: (args: { tvId: number; language?: string; @@ -96,7 +119,26 @@ Object.defineProperty(TheMovieDb.prototype, 'getTvShow', { configurable: true, }); -import { sonarrScanner } from '@server/lib/scanners/sonarr'; +Object.defineProperty(TheMovieDb.prototype, 'getTvShowForScan', { + set() {}, + get() { + return async (args: { tvId: number; language?: string }) => + getTvShowImpl(args); + }, + configurable: true, +}); + +// both are assigned in the constructor, so the prototype stubs miss the instance +// sonarrScanner built when it was first imported +for (const method of ['getTvShow', 'getTvShowForScan'] as const) { + Object.defineProperty(sonarrScanner.tmdb, method, { + value: async (args: { tvId: number; language?: string }) => + getTvShowImpl(args), + configurable: true, + }); +} + +mock.method(MediaRequest, 'sendNotification', async () => undefined); setupTestDb(); @@ -157,6 +199,7 @@ function configureSonarr(overrides: Partial[] = [{}]): void { describe('Sonarr Scanner', () => { beforeEach(() => { getSeriesImpl = async () => []; + getLibrarySeriesByTvdbIdImpl = async () => []; getShowByTvdbIdImpl = async () => fakeTmdbShow(1); getTvShowImpl = async () => fakeTmdbShow(1); }); @@ -186,7 +229,7 @@ describe('Sonarr Scanner', () => { getSeriesImpl = async () => []; - await sonarrScanner.run(); + await runWithMockTimers(() => sonarrScanner.run()); const updated = await mediaRepository.findOneOrFail({ where: { tmdbId: 1050 }, @@ -214,9 +257,9 @@ describe('Sonarr Scanner', () => { await mediaRepository.save(media); configureSonarr([{ syncEnabled: true }]); - getSeriesImpl = async () => []; + getSeriesImpl = async () => [fakeSonarrSeries({ tvdbId: 999 })]; - await sonarrScanner.run(); + await runWithMockTimers(() => sonarrScanner.run()); const updated = await mediaRepository.findOneOrFail({ where: { tmdbId: 1000 }, @@ -246,7 +289,7 @@ describe('Sonarr Scanner', () => { configureSonarr([{ syncEnabled: true }]); getSeriesImpl = async () => []; - await sonarrScanner.run(); + await runWithMockTimers(() => sonarrScanner.run()); const updated = await mediaRepository.findOneOrFail({ where: { tmdbId: 1001 }, @@ -296,7 +339,7 @@ describe('Sonarr Scanner', () => { getShowByTvdbIdImpl = async () => fakeTmdbShow(1); getTvShowImpl = async () => fakeTmdbShow(1); - await sonarrScanner.run(); + await runWithMockTimers(() => sonarrScanner.run()); const updated = await mediaRepository.findOneOrFail({ where: { tmdbId: 1 }, @@ -328,9 +371,9 @@ describe('Sonarr Scanner', () => { await mediaRepository.save(media); configureSonarr([{ syncEnabled: true }]); - getSeriesImpl = async () => []; + getSeriesImpl = async () => [fakeSonarrSeries({ tvdbId: 999 })]; - await sonarrScanner.run(); + await runWithMockTimers(() => sonarrScanner.run()); const updated = await mediaRepository.findOneOrFail({ where: { tmdbId: 1003 }, @@ -356,7 +399,7 @@ describe('Sonarr Scanner', () => { configureSonarr([{ syncEnabled: true }]); getSeriesImpl = async () => []; - await sonarrScanner.run(); + await runWithMockTimers(() => sonarrScanner.run()); const updated = await mediaRepository.findOneOrFail({ where: { tmdbId: 1004 }, @@ -400,19 +443,12 @@ describe('Sonarr Scanner', () => { { syncEnabled: true, id: 1, hostname: 'server-b' }, ]); - let callCount = 0; - getSeriesImpl = async () => { - callCount++; - if (callCount === 2) { - return [fakeSonarrSeries({ tvdbId: 511 })]; - } - return []; - }; + getSeriesImpl = async () => [fakeSonarrSeries({ tvdbId: 511 })]; getShowByTvdbIdImpl = async () => fakeTmdbShow(2); getTvShowImpl = async () => fakeTmdbShow(2); - await sonarrScanner.run(); + await runWithMockTimers(() => sonarrScanner.run()); const updatedOrphan = await mediaRepository.findOneOrFail({ where: { tmdbId: 1010 }, @@ -427,6 +463,79 @@ describe('Sonarr Scanner', () => { assert.notStrictEqual(updatedExisting.status, MediaStatus.UNKNOWN); }); + it('does not reset a show added to Sonarr after the scan started', async () => { + const mediaRepository = getRepository(Media); + + const media = new Media(); + media.tmdbId = 1020; + media.tvdbId = 620; + media.mediaType = MediaType.TV; + media.status = MediaStatus.PROCESSING; + await mediaRepository.save(media); + + configureSonarr([{ syncEnabled: true }]); + getSeriesImpl = async () => [fakeSonarrSeries({ tvdbId: 111 })]; + getLibrarySeriesByTvdbIdImpl = async (tvdbId) => [ + fakeSonarrSeries({ tvdbId }), + ]; + + await sonarrScanner.run(); + + const updated = await mediaRepository.findOneOrFail({ + where: { tmdbId: 1020 }, + }); + assert.strictEqual(updated.status, MediaStatus.PROCESSING); + }); + + it('does not reset a show when the server cannot be reached', async () => { + const mediaRepository = getRepository(Media); + + const media = new Media(); + media.tmdbId = 1021; + media.tvdbId = 621; + media.mediaType = MediaType.TV; + media.status = MediaStatus.PROCESSING; + await mediaRepository.save(media); + + configureSonarr([{ syncEnabled: true }]); + getSeriesImpl = async () => [fakeSonarrSeries({ tvdbId: 111 })]; + getLibrarySeriesByTvdbIdImpl = async () => { + throw new Error('connect ECONNREFUSED'); + }; + + await sonarrScanner.run(); + + const updated = await mediaRepository.findOneOrFail({ + where: { tmdbId: 1021 }, + }); + assert.strictEqual(updated.status, MediaStatus.PROCESSING); + }); + + it('resets a show when the server returns no row matching its id', async () => { + const mediaRepository = getRepository(Media); + + const media = new Media(); + media.tmdbId = 1022; + media.tvdbId = 622; + media.mediaType = MediaType.TV; + media.status = MediaStatus.PROCESSING; + await mediaRepository.save(media); + + configureSonarr([{ syncEnabled: true }]); + getSeriesImpl = async () => [fakeSonarrSeries({ tvdbId: 111 })]; + getLibrarySeriesByTvdbIdImpl = async () => [ + fakeSonarrSeries({ tvdbId: 111 }), + fakeSonarrSeries({ tvdbId: 222 }), + ]; + + await sonarrScanner.run(); + + const updated = await mediaRepository.findOneOrFail({ + where: { tmdbId: 1022 }, + }); + assert.strictEqual(updated.status, MediaStatus.UNKNOWN); + }); + it('skips shows without a tvdbId during cleanup', async () => { const mediaRepository = getRepository(Media); @@ -438,9 +547,9 @@ describe('Sonarr Scanner', () => { await mediaRepository.save(media); configureSonarr([{ syncEnabled: true }]); - getSeriesImpl = async () => []; + getSeriesImpl = async () => [fakeSonarrSeries({ tvdbId: 999 })]; - await sonarrScanner.run(); + await runWithMockTimers(() => sonarrScanner.run()); const updated = await mediaRepository.findOneOrFail({ where: { tmdbId: 1020 }, @@ -469,9 +578,9 @@ describe('Sonarr Scanner', () => { await mediaRepository.save(media); configureSonarr([{ syncEnabled: true, is4k: true }]); - getSeriesImpl = async () => []; + getSeriesImpl = async () => [fakeSonarrSeries({ tvdbId: 999 })]; - await sonarrScanner.run(); + await runWithMockTimers(() => sonarrScanner.run()); const updated = await mediaRepository.findOneOrFail({ where: { tmdbId: 1030 }, @@ -505,9 +614,9 @@ describe('Sonarr Scanner', () => { await mediaRepository.save(media); configureSonarr([{ syncEnabled: true, is4k: true }]); - getSeriesImpl = async () => []; + getSeriesImpl = async () => [fakeSonarrSeries({ tvdbId: 999 })]; - await sonarrScanner.run(); + await runWithMockTimers(() => sonarrScanner.run()); const updated = await mediaRepository.findOneOrFail({ where: { tmdbId: 1031 }, @@ -519,4 +628,307 @@ describe('Sonarr Scanner', () => { assert.strictEqual(s2?.status4k, MediaStatus.UNKNOWN); }); }); + + describe('orphaned request handling', () => { + it('declines the approved request and resets the show to UNKNOWN when orphaned', async () => { + const mediaRepository = getRepository(Media); + const requestRepository = getRepository(MediaRequest); + const userRepository = getRepository(User); + + const requestedBy = await userRepository.findOneOrFail({ + where: { id: 1 }, + }); + + const media = await mediaRepository.save( + new Media({ + tmdbId: 2000, + tvdbId: 555, + mediaType: MediaType.TV, + status: MediaStatus.PROCESSING, + seasons: [ + new Season({ + seasonNumber: 1, + status: MediaStatus.PROCESSING, + status4k: MediaStatus.UNKNOWN, + }), + ], + }) + ); + + const settings = getSettings(); + settings.sonarr = []; + settings.radarr = []; + const request = await requestRepository.save( + new MediaRequest({ + type: MediaType.TV, + status: MediaRequestStatus.APPROVED, + media, + requestedBy, + is4k: false, + }) + ); + + configureSonarr([{ syncEnabled: true }]); + getSeriesImpl = async () => [fakeSonarrSeries({ tvdbId: 999 })]; + + await runWithMockTimers(() => sonarrScanner.run()); + + const updatedMedia = await mediaRepository.findOneOrFail({ + where: { tmdbId: 2000 }, + }); + const updatedRequest = await requestRepository.findOneOrFail({ + where: { id: request.id }, + }); + + assert.strictEqual(updatedMedia.status, MediaStatus.UNKNOWN); + assert.strictEqual(updatedRequest.status, MediaRequestStatus.DECLINED); + }); + + it('does not decline the request when the show still exists in Sonarr', async () => { + const mediaRepository = getRepository(Media); + const requestRepository = getRepository(MediaRequest); + const userRepository = getRepository(User); + + const requestedBy = await userRepository.findOneOrFail({ + where: { id: 1 }, + }); + + const media = await mediaRepository.save( + new Media({ + tmdbId: 2001, + tvdbId: 600, + mediaType: MediaType.TV, + status: MediaStatus.PROCESSING, + seasons: [ + new Season({ + seasonNumber: 1, + status: MediaStatus.PROCESSING, + status4k: MediaStatus.UNKNOWN, + }), + ], + }) + ); + + const settings = getSettings(); + settings.sonarr = []; + settings.radarr = []; + const request = await requestRepository.save( + new MediaRequest({ + type: MediaType.TV, + status: MediaRequestStatus.APPROVED, + media, + requestedBy, + is4k: false, + }) + ); + + configureSonarr([{ syncEnabled: true }]); + getSeriesImpl = async () => [ + fakeSonarrSeries({ + tvdbId: 600, + seasons: [ + { + seasonNumber: 1, + monitored: true, + statistics: { + episodeFileCount: 0, + totalEpisodeCount: 10, + episodeCount: 10, + percentOfEpisodes: 0, + sizeOnDisk: 0, + previousAiring: undefined, + }, + }, + ], + }), + ]; + getShowByTvdbIdImpl = async () => fakeTmdbShow(2001); + getTvShowImpl = async () => fakeTmdbShow(2001); + + await runWithMockTimers(() => sonarrScanner.run()); + + const updatedRequest = await requestRepository.findOneOrFail({ + where: { id: request.id }, + }); + assert.strictEqual(updatedRequest.status, MediaRequestStatus.APPROVED); + }); + + it('skips cleanup and leaves the request approved when Sonarr returns an empty list', async () => { + const mediaRepository = getRepository(Media); + const requestRepository = getRepository(MediaRequest); + const userRepository = getRepository(User); + + const requestedBy = await userRepository.findOneOrFail({ + where: { id: 1 }, + }); + + const media = await mediaRepository.save( + new Media({ + tmdbId: 2005, + tvdbId: 605, + mediaType: MediaType.TV, + status: MediaStatus.PROCESSING, + seasons: [ + new Season({ + seasonNumber: 1, + status: MediaStatus.PROCESSING, + status4k: MediaStatus.UNKNOWN, + }), + ], + }) + ); + + const settings = getSettings(); + settings.sonarr = []; + settings.radarr = []; + const request = await requestRepository.save( + new MediaRequest({ + type: MediaType.TV, + status: MediaRequestStatus.APPROVED, + media, + requestedBy, + is4k: false, + }) + ); + + configureSonarr([{ syncEnabled: true }]); + getSeriesImpl = async () => []; + + await runWithMockTimers(() => sonarrScanner.run()); + + const updatedMedia = await mediaRepository.findOneOrFail({ + where: { tmdbId: 2005 }, + }); + const updatedRequest = await requestRepository.findOneOrFail({ + where: { id: request.id }, + }); + + assert.strictEqual(updatedMedia.status, MediaStatus.PROCESSING); + assert.strictEqual(updatedRequest.status, MediaRequestStatus.APPROVED); + }); + + it('declineOrphanedRequests throws when the requests relation is not loaded', async () => { + const media = new Media(); + media.id = 1; + media.tmdbId = 123; + media.mediaType = MediaType.TV; + + await assert.rejects( + () => + ( + sonarrScanner as unknown as { + declineOrphanedRequests: ( + m: Media, + is4k: boolean + ) => Promise; + } + ).declineOrphanedRequests(media, false), + /without the 'requests' relation loaded/ + ); + }); + + it('declines only the 4k request when the 4k dimension is orphaned but standard still exists', async () => { + const mediaRepository = getRepository(Media); + const requestRepository = getRepository(MediaRequest); + const userRepository = getRepository(User); + + const requestedBy = await userRepository.findOneOrFail({ + where: { id: 1 }, + }); + + const media = await mediaRepository.save( + new Media({ + tmdbId: 2002, + tvdbId: 666, + mediaType: MediaType.TV, + status: MediaStatus.PROCESSING, + status4k: MediaStatus.PROCESSING, + seasons: [ + new Season({ + seasonNumber: 1, + status: MediaStatus.PROCESSING, + status4k: MediaStatus.PROCESSING, + }), + ], + }) + ); + + const settings = getSettings(); + settings.sonarr = []; + settings.radarr = []; + const standardRequest = await requestRepository.save( + new MediaRequest({ + type: MediaType.TV, + status: MediaRequestStatus.APPROVED, + media, + requestedBy, + is4k: false, + }) + ); + const fourKRequest = await requestRepository.save( + new MediaRequest({ + type: MediaType.TV, + status: MediaRequestStatus.APPROVED, + media, + requestedBy, + is4k: true, + }) + ); + + configureSonarr([ + { syncEnabled: true, id: 0, hostname: 'server-standard' }, + { syncEnabled: true, id: 1, hostname: 'server-4k', is4k: true }, + ]); + + let callCount = 0; + getSeriesImpl = async () => { + callCount++; + if (callCount === 1) { + // standard server still has the show (processing, no files) + return [ + fakeSonarrSeries({ + tvdbId: 666, + seasons: [ + { + seasonNumber: 1, + monitored: true, + statistics: { + episodeFileCount: 0, + totalEpisodeCount: 10, + episodeCount: 10, + percentOfEpisodes: 0, + sizeOnDisk: 0, + previousAiring: undefined, + }, + }, + ], + }), + ]; + } + // 4k server: populated but the show is absent, so 4k dimension orphaned + return [fakeSonarrSeries({ tvdbId: 997 })]; + }; + + getShowByTvdbIdImpl = async ({ tvdbId }) => + tvdbId === 666 ? fakeTmdbShow(2002) : fakeTmdbShow(997); + getTvShowImpl = async ({ tvId }) => fakeTmdbShow(tvId); + + await runWithMockTimers(() => sonarrScanner.run()); + + const updatedMedia = await mediaRepository.findOneOrFail({ + where: { tmdbId: 2002 }, + }); + const updatedStandard = await requestRepository.findOneOrFail({ + where: { id: standardRequest.id }, + }); + const updated4k = await requestRepository.findOneOrFail({ + where: { id: fourKRequest.id }, + }); + + assert.strictEqual(updatedMedia.status, MediaStatus.PROCESSING); + assert.strictEqual(updatedMedia.status4k, MediaStatus.UNKNOWN); + assert.strictEqual(updatedStandard.status, MediaRequestStatus.APPROVED); + assert.strictEqual(updated4k.status, MediaRequestStatus.DECLINED); + }); + }); }); diff --git a/server/lib/settings/index.ts b/server/lib/settings/index.ts index bb287c8e04..6e62bbdc06 100644 --- a/server/lib/settings/index.ts +++ b/server/lib/settings/index.ts @@ -103,6 +103,11 @@ export interface SonarrSettings extends DVRSettings { monitorNewItems: 'all' | 'none'; } +export interface ReadarrSettings extends DVRSettings { + activeMetadataProfileId: number; + activeMetadataProfileName: string; +} + interface Quota { quotaLimit?: number; quotaDays?: number; @@ -131,6 +136,7 @@ export interface ProxySettings { export interface MainSettings { apiKey: string; + hardcoverapikey: string; applicationTitle: string; applicationUrl: string; cacheImages: boolean; @@ -138,9 +144,11 @@ export interface MainSettings { defaultQuotas: { movie: Quota; tv: Quota; + book: Quota; }; hideAvailable: boolean; hideBlocklisted: boolean; + hideRequested: boolean; localLogin: boolean; mediaServerLogin: boolean; newPlexLogin: boolean; @@ -154,8 +162,11 @@ export interface MainSettings { mediaServerType: number; partialRequestsEnabled: boolean; enableSpecialEpisodes: boolean; + syncBookFormatRequests: boolean; + seriesRequestsEnabled: boolean; locale: string; youtubeUrl: string; + versionCheck: boolean; } export interface ProxySettings { @@ -193,10 +204,12 @@ interface FullPublicSettings extends PublicSettings { applicationUrl: string; hideAvailable: boolean; hideBlocklisted: boolean; + hideRequested: boolean; localLogin: boolean; mediaServerLogin: boolean; movie4kEnabled: boolean; series4kEnabled: boolean; + bookAudioEnabled: boolean; discoverRegion: string; streamingRegion: string; originalLanguage: string; @@ -206,6 +219,8 @@ interface FullPublicSettings extends PublicSettings { jellyfinServerName?: string; partialRequestsEnabled: boolean; enableSpecialEpisodes: boolean; + syncBookFormatRequests: boolean; + seriesRequestsEnabled: boolean; cacheImages: boolean; vapidPublic: string; enablePushRegistration: boolean; @@ -214,6 +229,7 @@ interface FullPublicSettings extends PublicSettings { userEmailRequired: boolean; newPlexLogin: boolean; youtubeUrl: string; + versionCheck: boolean; plexClientIdentifier: string; } @@ -229,6 +245,7 @@ export interface NotificationAgentDiscord extends NotificationAgentConfig { botAvatarUrl?: string; webhookUrl: string; webhookRoleId?: string; + webhookThreadId?: string; enableMentions: boolean; locale: AvailableLocale; useUserLocale: boolean; @@ -255,6 +272,7 @@ export interface NotificationAgentEmail extends NotificationAgentConfig { authPass?: string; allowSelfSigned: boolean; senderName: string; + usePublicLogo: boolean; pgpPrivateKey?: string; pgpPassword?: string; }; @@ -308,6 +326,7 @@ export interface NotificationAgentNtfy extends NotificationAgentConfig { options: { url: string; topic: string; + tags?: string; authMethodUsernamePassword?: boolean; username?: string; password?: string; @@ -359,6 +378,8 @@ export type JobId = | 'plex-refresh-token' | 'radarr-scan' | 'sonarr-scan' + | 'readarr-scan' + | 'readarr-recently-added-scan' | 'download-sync' | 'download-sync-reset' | 'jellyfin-recently-added-scan' @@ -378,6 +399,7 @@ export interface AllSettings { tautulli: TautulliSettings; radarr: RadarrSettings[]; sonarr: SonarrSettings[]; + readarr: ReadarrSettings[]; public: PublicSettings; notifications: NotificationSettings; jobs: Record; @@ -402,6 +424,7 @@ class Settings { vapidPublic: '', main: { apiKey: '', + hardcoverapikey: '', applicationTitle: 'Seerr', applicationUrl: '', cacheImages: false, @@ -409,9 +432,11 @@ class Settings { defaultQuotas: { movie: {}, tv: {}, + book: {}, }, hideAvailable: false, hideBlocklisted: false, + hideRequested: false, localLogin: true, mediaServerLogin: true, newPlexLogin: true, @@ -425,8 +450,11 @@ class Settings { mediaServerType: MediaServerType.NOT_CONFIGURED, partialRequestsEnabled: true, enableSpecialEpisodes: false, + syncBookFormatRequests: false, + seriesRequestsEnabled: true, locale: 'en', youtubeUrl: '', + versionCheck: true, }, plex: { name: '', @@ -454,6 +482,7 @@ class Settings { }, radarr: [], sonarr: [], + readarr: [], public: { initialized: false, }, @@ -472,6 +501,7 @@ class Settings { requireTls: false, allowSelfSigned: false, senderName: 'Seerr', + usePublicLogo: false, }, }, discord: { @@ -557,6 +587,7 @@ class Settings { options: { url: '', topic: '', + tags: '', priority: 3, locale: 'en', }, @@ -582,6 +613,12 @@ class Settings { 'sonarr-scan': { schedule: '0 30 4 * * *', }, + 'readarr-scan': { + schedule: '0 30 4 * * *', + }, + 'readarr-recently-added-scan': { + schedule: '0 */10 * * * *', + }, 'availability-sync': { schedule: '0 0 5 * * *', }, @@ -691,6 +728,14 @@ class Settings { this.data.sonarr = data; } + get readarr(): ReadarrSettings[] { + return this.data.readarr; + } + + set readarr(data: ReadarrSettings[]) { + this.data.readarr = data; + } + get public(): PublicSettings { return this.data.public; } @@ -706,6 +751,7 @@ class Settings { applicationUrl: this.data.main.applicationUrl, hideAvailable: this.data.main.hideAvailable, hideBlocklisted: this.data.main.hideBlocklisted, + hideRequested: this.data.main.hideRequested, localLogin: this.data.main.localLogin, mediaServerLogin: this.data.main.mediaServerLogin, jellyfinExternalHost: this.data.jellyfin.externalHostname, @@ -716,12 +762,17 @@ class Settings { series4kEnabled: this.data.sonarr.some( (sonarr) => sonarr.is4k && sonarr.isDefault ), + bookAudioEnabled: this.data.readarr.some( + (readarr) => readarr.is4k && readarr.isDefault + ), discoverRegion: this.data.main.discoverRegion, streamingRegion: this.data.main.streamingRegion, originalLanguage: this.data.main.originalLanguage, mediaServerType: this.main.mediaServerType, partialRequestsEnabled: this.data.main.partialRequestsEnabled, enableSpecialEpisodes: this.data.main.enableSpecialEpisodes, + syncBookFormatRequests: this.data.main.syncBookFormatRequests, + seriesRequestsEnabled: this.data.main.seriesRequestsEnabled, cacheImages: this.data.main.cacheImages, vapidPublic: this.vapidPublic, enablePushRegistration: this.data.notifications.agents.webpush.enabled, @@ -731,6 +782,7 @@ class Settings { this.data.notifications.agents.email.options.userEmailRequired, newPlexLogin: this.data.main.newPlexLogin, youtubeUrl: this.data.main.youtubeUrl, + versionCheck: this.data.main.versionCheck, plexClientIdentifier: this.data.clientId, }; } diff --git a/server/lib/watchlistsync.test.ts b/server/lib/watchlistsync.test.ts new file mode 100644 index 0000000000..659bf2be89 --- /dev/null +++ b/server/lib/watchlistsync.test.ts @@ -0,0 +1,198 @@ +import type { PlexWatchlistItem } from '@server/api/plextv'; +import PlexTvAPI from '@server/api/plextv'; +import { + MediaRequestStatus, + MediaStatus, + MediaType, +} from '@server/constants/media'; +import { getRepository } from '@server/datasource'; +import Media from '@server/entity/Media'; +import { MediaRequest } from '@server/entity/MediaRequest'; +import { User } from '@server/entity/User'; +import { UserSettings } from '@server/entity/UserSettings'; +import { Permission } from '@server/lib/permissions'; +import watchlistSync from '@server/lib/watchlistsync'; +import { setupTestDb } from '@server/test/db'; +import assert from 'node:assert/strict'; +import { beforeEach, describe, it } from 'node:test'; + +let watchlistItems: PlexWatchlistItem[] = []; + +Object.defineProperty(PlexTvAPI.prototype, 'getWatchlist', { + get() { + return async () => ({ + offset: 0, + size: 20, + totalSize: watchlistItems.length, + items: watchlistItems, + }); + }, + set() {}, + configurable: true, +}); + +let requestCalls: { mediaId: number; mediaType: MediaType }[] = []; + +Object.defineProperty(MediaRequest, 'request', { + value: async (body: { mediaId: number; mediaType: MediaType }) => { + requestCalls.push({ mediaId: body.mediaId, mediaType: body.mediaType }); + return {} as MediaRequest; + }, + writable: true, + configurable: true, +}); + +setupTestDb(); + +async function configureSyncUser(): Promise { + const userRepository = getRepository(User); + const admin = await userRepository.findOneOrFail({ where: { id: 1 } }); + + admin.plexToken = 'test-plex-token'; + admin.permissions = Permission.AUTO_REQUEST; + await userRepository.save(admin); + + const userSettingsRepository = getRepository(UserSettings); + await userSettingsRepository.save( + new UserSettings({ + user: admin, + watchlistSyncMovies: true, + watchlistSyncTv: true, + }) + ); + + return admin; +} + +async function seedMedia( + tmdbId: number, + mediaType: MediaType, + status: MediaStatus +): Promise { + const mediaRepository = getRepository(Media); + await mediaRepository.save( + new Media({ + tmdbId, + mediaType, + status, + status4k: MediaStatus.UNKNOWN, + }) + ); +} + +function movieItem(tmdbId: number, title: string): PlexWatchlistItem { + return { ratingKey: `rk-${tmdbId}`, tmdbId, title, type: 'movie' }; +} + +function showItem(tmdbId: number, title: string): PlexWatchlistItem { + return { + ratingKey: `rk-${tmdbId}`, + tmdbId, + tvdbId: tmdbId * 1000, + title, + type: 'show', + }; +} + +describe('WatchlistSync re-request gating', () => { + beforeEach(() => { + requestCalls = []; + watchlistItems = []; + }); + + it('re-requests DELETED watchlist items and skips non-requestable ones', async () => { + await configureSyncUser(); + + await seedMedia(100, MediaType.MOVIE, MediaStatus.DELETED); + await seedMedia(101, MediaType.MOVIE, MediaStatus.UNKNOWN); + await seedMedia(102, MediaType.MOVIE, MediaStatus.AVAILABLE); + await seedMedia(103, MediaType.MOVIE, MediaStatus.BLOCKLISTED); + + await seedMedia(200, MediaType.TV, MediaStatus.DELETED); + await seedMedia(201, MediaType.TV, MediaStatus.AVAILABLE); + + watchlistItems = [ + movieItem(100, 'Deleted Movie'), + movieItem(101, 'Unknown Movie'), + movieItem(102, 'Available Movie'), + movieItem(103, 'Blocklisted Movie'), + showItem(200, 'Deleted Show'), + showItem(201, 'Available Show'), + ]; + + await watchlistSync.syncWatchlist(); + + const requestedArray = requestCalls.map( + (c) => `${c.mediaType}:${c.mediaId}` + ); + const requested = new Set(requestedArray); + + assert.strictEqual( + requestedArray.length, + requested.size, + 'Each item should be requested exactly once' + ); + + assert.ok( + requested.has(`${MediaType.MOVIE}:100`), + 'DELETED movie on the watchlist should be re-requested' + ); + + assert.ok( + requested.has(`${MediaType.MOVIE}:101`), + 'UNKNOWN movie should be requested' + ); + assert.ok( + !requested.has(`${MediaType.MOVIE}:102`), + 'AVAILABLE movie should NOT be requested' + ); + assert.ok( + !requested.has(`${MediaType.MOVIE}:103`), + 'BLOCKLISTED movie should NOT be requested' + ); + + assert.ok( + requested.has(`${MediaType.TV}:200`), + 'DELETED show should be re-requested' + ); + assert.ok( + !requested.has(`${MediaType.TV}:201`), + 'AVAILABLE show should NOT be requested' + ); + }); + + it('re-requests DELETED watchlist items even when a stale auto-request exists', async () => { + const user = await configureSyncUser(); + + await seedMedia(100, MediaType.MOVIE, MediaStatus.DELETED); + + const media = await getRepository(Media).findOneOrFail({ + where: { tmdbId: 100, mediaType: MediaType.MOVIE }, + }); + + await getRepository(MediaRequest).save( + new MediaRequest({ + type: MediaType.MOVIE, + status: MediaRequestStatus.COMPLETED, + media, + requestedBy: user, + is4k: false, + isAutoRequest: true, + }) + ); + + watchlistItems = [movieItem(100, 'Deleted Movie')]; + + await watchlistSync.syncWatchlist(); + + const calls = requestCalls.filter( + (c) => c.mediaType === MediaType.MOVIE && c.mediaId === 100 + ); + + assert.strictEqual( + calls.length, + 1, + 'DELETED movie should be re-requested even when a stale auto-request exists' + ); + }); +}); diff --git a/server/lib/watchlistsync.ts b/server/lib/watchlistsync.ts index 550a6524ad..cb8a5d35e2 100644 --- a/server/lib/watchlistsync.ts +++ b/server/lib/watchlistsync.ts @@ -91,7 +91,9 @@ class WatchlistSync { const autoRequestedTmdbIds = new Set( existingAutoRequests - .filter((r) => r.media != null) + .filter( + (r) => r.media != null && r.media.status !== MediaStatus.DELETED + ) .map((r) => `${r.media.mediaType}:${r.media.tmdbId}`) ); @@ -106,7 +108,8 @@ class WatchlistSync { m.mediaType === itemMediaType && (m.status === MediaStatus.BLOCKLISTED || (itemMediaType === MediaType.MOVIE && - m.status !== MediaStatus.UNKNOWN) || + m.status !== MediaStatus.UNKNOWN && + m.status !== MediaStatus.DELETED) || (itemMediaType === MediaType.TV && m.status === MediaStatus.AVAILABLE)) ) diff --git a/server/migration/postgres/1779783365432-AddDiscordIdsColumn.ts b/server/migration/postgres/1779783365432-AddDiscordIdsColumn.ts new file mode 100644 index 0000000000..98a04bd8a7 --- /dev/null +++ b/server/migration/postgres/1779783365432-AddDiscordIdsColumn.ts @@ -0,0 +1,30 @@ +import type { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddDiscordIdsColumn1779783365432 implements MigrationInterface { + name = 'AddDiscordIdsColumn1779783365432'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "user_settings" ADD "discordIds" text` + ); + // same for postgres (convert existing single ID into list with one entry) + await queryRunner.query( + `UPDATE "user_settings" SET "discordIds" = '["' || "discordId" || '"]' WHERE "discordId" IS NOT NULL AND "discordId" != ''` + ); + await queryRunner.query( + `ALTER TABLE "user_settings" DROP COLUMN "discordId"` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "user_settings" ADD "discordId" character varying` + ); + await queryRunner.query( + `UPDATE "user_settings" SET "discordId" = ("discordIds"::jsonb ->> 0) WHERE "discordIds" IS NOT NULL AND "discordIds" != ''` + ); + await queryRunner.query( + `ALTER TABLE "user_settings" DROP COLUMN "discordIds"` + ); + } +} diff --git a/server/migration/postgres/1781732098511-AddIgnoreQuotaToMediaRequest.ts b/server/migration/postgres/1781732098511-AddIgnoreQuotaToMediaRequest.ts new file mode 100644 index 0000000000..a575e153c5 --- /dev/null +++ b/server/migration/postgres/1781732098511-AddIgnoreQuotaToMediaRequest.ts @@ -0,0 +1,17 @@ +import type { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddIgnoreQuotaToMediaRequest1781732098511 implements MigrationInterface { + name = 'AddIgnoreQuotaToMediaRequest1781732098511'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "media_request" ADD "ignoreQuota" boolean NOT NULL DEFAULT false` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "media_request" DROP COLUMN "ignoreQuota"` + ); + } +} diff --git a/server/migration/postgres/1789254652580-RemapOverseerrDeletedStatus.ts b/server/migration/postgres/1789254652580-RemapOverseerrDeletedStatus.ts new file mode 100644 index 0000000000..2733c68ed5 --- /dev/null +++ b/server/migration/postgres/1789254652580-RemapOverseerrDeletedStatus.ts @@ -0,0 +1,28 @@ +import type { MigrationInterface, QueryRunner } from 'typeorm'; + +// Hand written as its data only because Overseerr's MediaStatus.DELETED = 6 +// collides with our BLOCKLISTED, so rows from an imported database read as blocklisted. +// see https://github.com/seerr-team/seerr/issues/3508 +export class RemapOverseerrDeletedStatus1789257612345 implements MigrationInterface { + name = 'RemapOverseerrDeletedStatus1789257612345'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `UPDATE "season" SET "status" = 7 WHERE "status" = 6` + ); + await queryRunner.query( + `UPDATE "season" SET "status4k" = 7 WHERE "status4k" = 6` + ); + await queryRunner.query( + `UPDATE "media" SET "status" = 7 WHERE "status" = 6 AND NOT EXISTS (SELECT 1 FROM "blocklist" WHERE "blocklist"."tmdbId" = "media"."tmdbId" AND "blocklist"."mediaType" = "media"."mediaType")` + ); + await queryRunner.query( + `UPDATE "media" SET "status4k" = 7 WHERE "status4k" = 6 AND NOT EXISTS (SELECT 1 FROM "blocklist" WHERE "blocklist"."tmdbId" = "media"."tmdbId" AND "blocklist"."mediaType" = "media"."mediaType")` + ); + } + + public async down(): Promise { + // Intentionally left empty as remapped rows are indistinguishable from rows + // that were already DELETED. + } +} diff --git a/server/migration/sqlite/1779783365432-AddDiscordIdsColumn.ts b/server/migration/sqlite/1779783365432-AddDiscordIdsColumn.ts new file mode 100644 index 0000000000..36fb1ad59a --- /dev/null +++ b/server/migration/sqlite/1779783365432-AddDiscordIdsColumn.ts @@ -0,0 +1,31 @@ +import type { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddDiscordIdsColumn1779783365432 implements MigrationInterface { + name = 'AddDiscordIdsColumn1779783365432'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "temporary_user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "locale" varchar NOT NULL DEFAULT (''), "discoverRegion" varchar, "streamingRegion" varchar, "originalLanguage" varchar, "pgpKey" varchar, "discordIds" text, "pushbulletAccessToken" varchar, "pushoverApplicationToken" varchar, "pushoverUserKey" varchar, "pushoverSound" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "watchlistSyncMovies" boolean, "watchlistSyncTv" boolean, "notificationTypes" text, "userId" integer, "telegramMessageThreadId" varchar, CONSTRAINT "REL_986a2b6d3c05eb4091bb8066f7" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "temporary_user_settings"("id", "locale", "discoverRegion", "streamingRegion", "originalLanguage", "pgpKey", "discordIds", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "pushoverSound", "telegramChatId", "telegramSendSilently", "watchlistSyncMovies", "watchlistSyncTv", "notificationTypes", "userId", "telegramMessageThreadId") SELECT "id", "locale", "discoverRegion", "streamingRegion", "originalLanguage", "pgpKey", CASE WHEN "discordId" IS NOT NULL AND "discordId" != '' THEN '["' || "discordId" || '"]' ELSE NULL END, "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "pushoverSound", "telegramChatId", "telegramSendSilently", "watchlistSyncMovies", "watchlistSyncTv", "notificationTypes", "userId", "telegramMessageThreadId" FROM "user_settings"` + ); + await queryRunner.query(`DROP TABLE "user_settings"`); + await queryRunner.query( + `ALTER TABLE "temporary_user_settings" RENAME TO "user_settings"` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "user_settings" RENAME TO "temporary_user_settings"` + ); + await queryRunner.query( + `CREATE TABLE "user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "locale" varchar NOT NULL DEFAULT (''), "discoverRegion" varchar, "streamingRegion" varchar, "originalLanguage" varchar, "pgpKey" varchar, "discordId" varchar, "pushbulletAccessToken" varchar, "pushoverApplicationToken" varchar, "pushoverUserKey" varchar, "pushoverSound" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "watchlistSyncMovies" boolean, "watchlistSyncTv" boolean, "notificationTypes" text, "userId" integer, "telegramMessageThreadId" varchar, CONSTRAINT "REL_986a2b6d3c05eb4091bb8066f7" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "user_settings"("id", "locale", "discoverRegion", "streamingRegion", "originalLanguage", "pgpKey", "discordId", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "pushoverSound", "telegramChatId", "telegramSendSilently", "watchlistSyncMovies", "watchlistSyncTv", "notificationTypes", "userId", "telegramMessageThreadId") SELECT "id", "locale", "discoverRegion", "streamingRegion", "originalLanguage", "pgpKey", json_extract("discordIds", '$[0]'), "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "pushoverSound", "telegramChatId", "telegramSendSilently", "watchlistSyncMovies", "watchlistSyncTv", "notificationTypes", "userId", "telegramMessageThreadId" FROM "temporary_user_settings"` + ); + await queryRunner.query(`DROP TABLE "temporary_user_settings"`); + } +} diff --git a/server/migration/sqlite/1781732036510-AddIgnoreQuotaToMediaRequest.ts b/server/migration/sqlite/1781732036510-AddIgnoreQuotaToMediaRequest.ts new file mode 100644 index 0000000000..4ab2c1fdc0 --- /dev/null +++ b/server/migration/sqlite/1781732036510-AddIgnoreQuotaToMediaRequest.ts @@ -0,0 +1,119 @@ +import type { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddIgnoreQuotaToMediaRequest1781732036510 implements MigrationInterface { + name = 'AddIgnoreQuotaToMediaRequest1781732036510'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_03f7958328e311761b0de675fb"`); + await queryRunner.query( + `CREATE TABLE "temporary_user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, "userAgent" varchar, "createdAt" datetime DEFAULT (CURRENT_TIMESTAMP), CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "UQ_6427d07d9a171a3a1ab87480005" UNIQUE ("endpoint", "userId"), CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "temporary_user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt") SELECT "id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt" FROM "user_push_subscription"` + ); + await queryRunner.query(`DROP TABLE "user_push_subscription"`); + await queryRunner.query( + `ALTER TABLE "temporary_user_push_subscription" RENAME TO "user_push_subscription"` + ); + await queryRunner.query( + `CREATE INDEX "IDX_03f7958328e311761b0de675fb" ON "user_push_subscription" ("userId") ` + ); + await queryRunner.query(`DROP INDEX "IDX_f4fc4efa14c3ba2b29c4525fa1"`); + await queryRunner.query(`DROP INDEX "IDX_6997bee94720f1ecb7f3113709"`); + await queryRunner.query(`DROP INDEX "IDX_a1aa713f41c99e9d10c48da75a"`); + await queryRunner.query(`DROP INDEX "IDX_4c696e8ed36ae34fe18abe59d2"`); + await queryRunner.query( + `CREATE TABLE "temporary_media_request" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "status" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "updatedAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "type" varchar NOT NULL, "mediaId" integer, "requestedById" integer, "modifiedById" integer, "is4k" boolean NOT NULL DEFAULT (0), "serverId" integer, "profileId" integer, "rootFolder" varchar, "languageProfileId" integer, "tags" text, "isAutoRequest" boolean NOT NULL DEFAULT (0), "ignoreQuota" boolean NOT NULL DEFAULT (0), CONSTRAINT "FK_a1aa713f41c99e9d10c48da75a0" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_6997bee94720f1ecb7f31137095" FOREIGN KEY ("requestedById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_f4fc4efa14c3ba2b29c4525fa15" FOREIGN KEY ("modifiedById") REFERENCES "user" ("id") ON DELETE SET NULL ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "temporary_media_request"("id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById", "is4k", "serverId", "profileId", "rootFolder", "languageProfileId", "tags", "isAutoRequest") SELECT "id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById", "is4k", "serverId", "profileId", "rootFolder", "languageProfileId", "tags", "isAutoRequest" FROM "media_request"` + ); + await queryRunner.query(`DROP TABLE "media_request"`); + await queryRunner.query( + `ALTER TABLE "temporary_media_request" RENAME TO "media_request"` + ); + await queryRunner.query( + `CREATE INDEX "IDX_f4fc4efa14c3ba2b29c4525fa1" ON "media_request" ("modifiedById") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_6997bee94720f1ecb7f3113709" ON "media_request" ("requestedById") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_a1aa713f41c99e9d10c48da75a" ON "media_request" ("mediaId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_4c696e8ed36ae34fe18abe59d2" ON "media_request" ("status") ` + ); + await queryRunner.query(`DROP INDEX "IDX_03f7958328e311761b0de675fb"`); + await queryRunner.query( + `CREATE TABLE "temporary_user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, "userAgent" varchar, "createdAt" datetime DEFAULT (CURRENT_TIMESTAMP), CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "UQ_6427d07d9a171a3a1ab87480005" UNIQUE ("endpoint", "userId"), CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "temporary_user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt") SELECT "id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt" FROM "user_push_subscription"` + ); + await queryRunner.query(`DROP TABLE "user_push_subscription"`); + await queryRunner.query( + `ALTER TABLE "temporary_user_push_subscription" RENAME TO "user_push_subscription"` + ); + await queryRunner.query( + `CREATE INDEX "IDX_03f7958328e311761b0de675fb" ON "user_push_subscription" ("userId") ` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_03f7958328e311761b0de675fb"`); + await queryRunner.query( + `ALTER TABLE "user_push_subscription" RENAME TO "temporary_user_push_subscription"` + ); + await queryRunner.query( + `CREATE TABLE "user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, "userAgent" varchar, "createdAt" datetime DEFAULT (CURRENT_TIMESTAMP), CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "UQ_6427d07d9a171a3a1ab87480005" UNIQUE ("endpoint", "userId"), CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt") SELECT "id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt" FROM "temporary_user_push_subscription"` + ); + await queryRunner.query(`DROP TABLE "temporary_user_push_subscription"`); + await queryRunner.query( + `CREATE INDEX "IDX_03f7958328e311761b0de675fb" ON "user_push_subscription" ("userId") ` + ); + await queryRunner.query(`DROP INDEX "IDX_4c696e8ed36ae34fe18abe59d2"`); + await queryRunner.query(`DROP INDEX "IDX_a1aa713f41c99e9d10c48da75a"`); + await queryRunner.query(`DROP INDEX "IDX_6997bee94720f1ecb7f3113709"`); + await queryRunner.query(`DROP INDEX "IDX_f4fc4efa14c3ba2b29c4525fa1"`); + await queryRunner.query( + `ALTER TABLE "media_request" RENAME TO "temporary_media_request"` + ); + await queryRunner.query( + `CREATE TABLE "media_request" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "status" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "updatedAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "type" varchar NOT NULL, "mediaId" integer, "requestedById" integer, "modifiedById" integer, "is4k" boolean NOT NULL DEFAULT (0), "serverId" integer, "profileId" integer, "rootFolder" varchar, "languageProfileId" integer, "tags" text, "isAutoRequest" boolean NOT NULL DEFAULT (0), CONSTRAINT "FK_a1aa713f41c99e9d10c48da75a0" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_6997bee94720f1ecb7f31137095" FOREIGN KEY ("requestedById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_f4fc4efa14c3ba2b29c4525fa15" FOREIGN KEY ("modifiedById") REFERENCES "user" ("id") ON DELETE SET NULL ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "media_request"("id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById", "is4k", "serverId", "profileId", "rootFolder", "languageProfileId", "tags", "isAutoRequest") SELECT "id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById", "is4k", "serverId", "profileId", "rootFolder", "languageProfileId", "tags", "isAutoRequest" FROM "temporary_media_request"` + ); + await queryRunner.query(`DROP TABLE "temporary_media_request"`); + await queryRunner.query( + `CREATE INDEX "IDX_4c696e8ed36ae34fe18abe59d2" ON "media_request" ("status") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_a1aa713f41c99e9d10c48da75a" ON "media_request" ("mediaId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_6997bee94720f1ecb7f3113709" ON "media_request" ("requestedById") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_f4fc4efa14c3ba2b29c4525fa1" ON "media_request" ("modifiedById") ` + ); + await queryRunner.query(`DROP INDEX "IDX_03f7958328e311761b0de675fb"`); + await queryRunner.query( + `ALTER TABLE "user_push_subscription" RENAME TO "temporary_user_push_subscription"` + ); + await queryRunner.query( + `CREATE TABLE "user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, "userAgent" varchar, "createdAt" datetime DEFAULT (CURRENT_TIMESTAMP), CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "UQ_6427d07d9a171a3a1ab87480005" UNIQUE ("endpoint", "userId"), CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt") SELECT "id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt" FROM "temporary_user_push_subscription"` + ); + await queryRunner.query(`DROP TABLE "temporary_user_push_subscription"`); + await queryRunner.query( + `CREATE INDEX "IDX_03f7958328e311761b0de675fb" ON "user_push_subscription" ("userId") ` + ); + } +} diff --git a/server/migration/sqlite/1786619443939-DropPushSubscriptionAuthUnique.ts b/server/migration/sqlite/1786619443939-DropPushSubscriptionAuthUnique.ts new file mode 100644 index 0000000000..f2d81695dc --- /dev/null +++ b/server/migration/sqlite/1786619443939-DropPushSubscriptionAuthUnique.ts @@ -0,0 +1,41 @@ +import type { MigrationInterface, QueryRunner } from 'typeorm'; + +// SQLite kept the "auth" UNIQUE that Postgres dropped in 1743023615532-UpdateWebPush. +// Hand-written because the generated rebuild re-adds it and never converges. +export class DropPushSubscriptionAuthUnique1786619443939 implements MigrationInterface { + name = 'DropPushSubscriptionAuthUnique1786619443939'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_03f7958328e311761b0de675fb"`); + await queryRunner.query( + `CREATE TABLE "temporary_user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, "userAgent" varchar, "createdAt" datetime DEFAULT (CURRENT_TIMESTAMP), CONSTRAINT "UQ_6427d07d9a171a3a1ab87480005" UNIQUE ("endpoint", "userId"), CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "temporary_user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt") SELECT "id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt" FROM "user_push_subscription"` + ); + await queryRunner.query(`DROP TABLE "user_push_subscription"`); + await queryRunner.query( + `ALTER TABLE "temporary_user_push_subscription" RENAME TO "user_push_subscription"` + ); + await queryRunner.query( + `CREATE INDEX "IDX_03f7958328e311761b0de675fb" ON "user_push_subscription" ("userId") ` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_03f7958328e311761b0de675fb"`); + await queryRunner.query( + `ALTER TABLE "user_push_subscription" RENAME TO "temporary_user_push_subscription"` + ); + await queryRunner.query( + `CREATE TABLE "user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, "userAgent" varchar, "createdAt" datetime DEFAULT (CURRENT_TIMESTAMP), CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "UQ_6427d07d9a171a3a1ab87480005" UNIQUE ("endpoint", "userId"), CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt") SELECT "id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt" FROM "temporary_user_push_subscription"` + ); + await queryRunner.query(`DROP TABLE "temporary_user_push_subscription"`); + await queryRunner.query( + `CREATE INDEX "IDX_03f7958328e311761b0de675fb" ON "user_push_subscription" ("userId") ` + ); + } +} diff --git a/server/migration/sqlite/1789254652493-RemapOverseerrDeletedStatus.ts b/server/migration/sqlite/1789254652493-RemapOverseerrDeletedStatus.ts new file mode 100644 index 0000000000..2733c68ed5 --- /dev/null +++ b/server/migration/sqlite/1789254652493-RemapOverseerrDeletedStatus.ts @@ -0,0 +1,28 @@ +import type { MigrationInterface, QueryRunner } from 'typeorm'; + +// Hand written as its data only because Overseerr's MediaStatus.DELETED = 6 +// collides with our BLOCKLISTED, so rows from an imported database read as blocklisted. +// see https://github.com/seerr-team/seerr/issues/3508 +export class RemapOverseerrDeletedStatus1789257612345 implements MigrationInterface { + name = 'RemapOverseerrDeletedStatus1789257612345'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `UPDATE "season" SET "status" = 7 WHERE "status" = 6` + ); + await queryRunner.query( + `UPDATE "season" SET "status4k" = 7 WHERE "status4k" = 6` + ); + await queryRunner.query( + `UPDATE "media" SET "status" = 7 WHERE "status" = 6 AND NOT EXISTS (SELECT 1 FROM "blocklist" WHERE "blocklist"."tmdbId" = "media"."tmdbId" AND "blocklist"."mediaType" = "media"."mediaType")` + ); + await queryRunner.query( + `UPDATE "media" SET "status4k" = 7 WHERE "status4k" = 6 AND NOT EXISTS (SELECT 1 FROM "blocklist" WHERE "blocklist"."tmdbId" = "media"."tmdbId" AND "blocklist"."mediaType" = "media"."mediaType")` + ); + } + + public async down(): Promise { + // Intentionally left empty as remapped rows are indistinguishable from rows + // that were already DELETED. + } +} diff --git a/server/models/Author.ts b/server/models/Author.ts new file mode 100644 index 0000000000..f1ca1913ec --- /dev/null +++ b/server/models/Author.ts @@ -0,0 +1,25 @@ +import type { HardcoverAuthorDetails } from '@server/api/hardcover/interfaces'; +import type { BookResult } from './Search'; + +export interface AuthorDetails { + bio: string; + books_count: number; + id: number; + name: string; + born_date: string; + posterPath?: string; + books?: BookResult[]; +} + +export const mapAuthorDetails = ( + author: HardcoverAuthorDetails, + books?: BookResult[] +): AuthorDetails => ({ + id: author.id, + name: author.name, + born_date: author.born_date, + books_count: author.books_count, + posterPath: author.image?.url, + bio: author.bio, + books: books, +}); diff --git a/server/models/Book.ts b/server/models/Book.ts new file mode 100644 index 0000000000..09a4292906 --- /dev/null +++ b/server/models/Book.ts @@ -0,0 +1,93 @@ +import { getHeader } from '@server/api/hardcover'; +import type { + Author, + HardcoverBookDetails, + HardcoverSeries, + Tags, +} from '@server/api/hardcover/interfaces'; +import type Media from '@server/entity/Media'; +import type { ExternalIds } from './common'; + +export interface BookDetails { + id: number; + title: string; + description: string; + releaseDate: string; + image?: { + url: string; + }; + mediaInfo?: Media; + externalIds?: ExternalIds; + author: Author[]; + pages: number; + tags: Tags; + headline: string; + subtitle: string; + series: { + position: number; + series_id: number; + series: HardcoverSeries; + }[]; + posterPath: string; + backdropPath: string; + slug: string; + rating?: number; + ratingsCount?: number; + reviewsCount?: number; + publisher?: string; + language?: string; + isbn?: string; + format?: string; + hasPhysicalEdition: boolean; + hasEbookEdition: boolean; + hasAudioEdition: boolean; + audiobookDuration?: number; + narrators?: string[]; +} + +export const mapBookDetails = ( + book: HardcoverBookDetails, + media?: Media +): BookDetails => ({ + id: book.id, + title: book.title, + description: book.description, + releaseDate: book.release_date, + tags: book.cached_tags, + headline: book.headline, + subtitle: book.subtitle, + pages: book.pages, + mediaInfo: media, + author: book.contributions.map((e) => e.author), + series: book.book_series, + posterPath: book.image?.url + ? book.image.url + : `https://assets.hardcover.app/static/covers/cover${ + (book.id % 9) + 1 + }.png`, + backdropPath: (() => { + const header = getHeader(book.cached_tags); + if (header) { + return `https://assets.hardcover.app/static/bookHeaders/${header}.webp`; + } + return ''; + })(), + slug: book.slug, + rating: book.rating, + ratingsCount: book.ratings_count ?? 0, + reviewsCount: book.reviews_count ?? 0, + publisher: book.default_physical_edition?.publisher?.name, + language: book.default_physical_edition?.language?.language, + isbn: + book.default_physical_edition?.isbn_13 ?? + book.default_physical_edition?.isbn_10 ?? + book.default_ebook_edition?.isbn_13, + format: book.default_physical_edition?.physical_format, + hasPhysicalEdition: !!book.default_physical_edition_id, + hasEbookEdition: !!book.default_ebook_edition_id, + hasAudioEdition: !!book.default_audio_edition_id, + audiobookDuration: book.default_audio_edition?.audio_seconds, + narrators: book.default_audio_edition?.contributions + ?.map((c) => c.author?.name) + .filter(Boolean) as string[] | undefined, +}); diff --git a/server/models/Search.ts b/server/models/Search.ts index 2193bbe158..9b244fb841 100644 --- a/server/models/Search.ts +++ b/server/models/Search.ts @@ -1,3 +1,8 @@ +import { getHeader } from '@server/api/hardcover'; +import type { + HardcoverAuthorResult, + HardcoverBookResult, +} from '@server/api/hardcover/interfaces'; import type { TmdbCollectionResult, TmdbMovieDetails, @@ -10,7 +15,7 @@ import type { import { MediaType as MainMediaType } from '@server/constants/media'; import type Media from '@server/entity/Media'; -export type MediaType = 'tv' | 'movie' | 'person' | 'collection'; +export type MediaType = 'tv' | 'movie' | 'person' | 'collection' | 'book'; interface SearchResult { id: number; @@ -36,6 +41,19 @@ export interface MovieResult extends SearchResult { mediaInfo?: Media; } +export interface BookResult { + id: number; + mediaType: 'book'; + title: string; + overview: string; + releaseDate: string; + posterPath?: string; + backdropPath?: string; + position?: number; + extra?: boolean; + mediaInfo?: Media; +} + export interface TvResult extends SearchResult { mediaType: 'tv'; name: string; @@ -66,7 +84,20 @@ export interface PersonResult { knownFor: (MovieResult | TvResult)[]; } -export type Results = MovieResult | TvResult | PersonResult | CollectionResult; +export interface AuthorResult { + id: number; + name: string; + image?: string; + mediaType: 'author'; +} + +export type Results = + | MovieResult + | TvResult + | PersonResult + | CollectionResult + | BookResult + | AuthorResult; export const mapMovieResult = ( movieResult: TmdbMovieResult, @@ -90,6 +121,43 @@ export const mapMovieResult = ( mediaInfo: media, }); +export const mapBookResult = ( + bookResult: HardcoverBookResult, + media?: Media, + position?: number +): BookResult => ({ + id: bookResult.id, + mediaType: 'book', + title: bookResult.title, + overview: bookResult.description, + releaseDate: bookResult.release_date, + posterPath: bookResult.image?.url + ? bookResult.image.url + : `https://assets.hardcover.app/static/covers/cover${ + (bookResult.id % 9) + 1 + }.png`, + backdropPath: (() => { + if (bookResult.cached_tags) { + const header = getHeader(bookResult.cached_tags); + if (header) { + return `https://assets.hardcover.app/static/bookHeaders/${header}.webp`; + } + } + return ''; + })(), + position: position, + mediaInfo: media, +}); + +export const mapAuthorResult = ( + authorResult: HardcoverAuthorResult +): AuthorResult => ({ + id: authorResult.id, + name: authorResult.name, + image: authorResult?.image?.url, + mediaType: 'author', +}); + export const mapTvResult = ( tvResult: TmdbTvResult, media?: Media @@ -150,6 +218,8 @@ export const mapSearchResults = ( | TmdbTvResult | TmdbPersonResult | TmdbCollectionResult + | HardcoverBookResult + | HardcoverAuthorResult )[], media?: Media[] ): Results[] => @@ -171,10 +241,21 @@ export const mapSearchResults = ( req.tmdbId === result.id && req.mediaType === MainMediaType.TV ) ); + case 'book': + return mapBookResult( + result, + media?.find( + (req) => + req.tmdbId === Number(result.id) && + req.mediaType === MainMediaType.BOOK + ) + ); case 'collection': return mapCollectionResult(result); - default: + case 'person': return mapPersonResult(result); + case 'author': + return mapAuthorResult(result); } }); diff --git a/server/models/Series.ts b/server/models/Series.ts new file mode 100644 index 0000000000..16349fa90d --- /dev/null +++ b/server/models/Series.ts @@ -0,0 +1,34 @@ +import type { HardcoverSeries } from '@server/api/hardcover/interfaces'; +import { MediaType } from '@server/constants/media'; +import type Media from '@server/entity/Media'; +import type { BookResult } from './Search'; +import { mapBookResult } from './Search'; + +export interface Series { + id: number; + name: string; + overview?: string; + posterPath?: string; + backdropPath?: string; + books: BookResult[]; +} + +export const mapSeries = (series: HardcoverSeries, media: Media[]): Series => ({ + id: series.id, + name: series.name, + overview: series.description, + // Hardcover numbers dramatized adaptations, split editions and bonus + // chapters off the book they belong to, so a fractional position marks an + // edition or extra rather than one of the series' own books + books: series.book_series.map((book_series) => ({ + ...mapBookResult( + book_series.book, + media?.find( + (req) => + req.tmdbId === book_series.book.id && req.mediaType === MediaType.BOOK + ), + book_series.position + ), + extra: !Number.isInteger(Number(book_series.position)), + })), +}); diff --git a/server/routes/auth.test.ts b/server/routes/auth.test.ts index 0e7981a23d..d787165cfc 100644 --- a/server/routes/auth.test.ts +++ b/server/routes/auth.test.ts @@ -1,12 +1,24 @@ import assert from 'node:assert/strict'; import { before, beforeEach, describe, it, mock } from 'node:test'; +import JellyfinAPI from '@server/api/jellyfin'; +import { ApiErrorCode } from '@server/constants/error'; +import { MediaServerType } from '@server/constants/server'; +import { UserType } from '@server/constants/user'; import { getRepository } from '@server/datasource'; import { User } from '@server/entity/User'; import PreparedEmail from '@server/lib/email'; +import ImageProxy from '@server/lib/imageproxy'; import { getSettings } from '@server/lib/settings'; import { checkUser } from '@server/middleware/auth'; import { setupTestDb } from '@server/test/db'; +import { + allowlistedSettings, + assertNoCredentials, + seedUserSettings, +} from '@server/test/userSettings'; +import { ApiError } from '@server/types/error'; +import axios from 'axios'; import type { Express } from 'express'; import express from 'express'; import session from 'express-session'; @@ -17,6 +29,81 @@ const emailMock = mock.method(PreparedEmail.prototype, 'send', async () => { return undefined; }).mock; +// Jellyfin Quick Connect mocks +const defaultInitiateResponse = { + Secret: 'abc123def456abc123def456', + Code: '123456', + DateAdded: new Date().toISOString(), +}; + +const defaultCheckResponse = { + Authenticated: false, + Secret: 'abc123def456abc123def456', + Code: '123456', + DeviceId: 'device-1', + DeviceName: 'Test', + AppName: 'Seerr', + AppVersion: '1.0', + DateAdded: new Date().toISOString(), +}; + +const defaultAuthenticateResponse = { + User: { + Id: 'jf-qc-user-001', + Name: 'quickconnectuser', + ServerId: 'server-1', + Policy: { IsAdministrator: false }, + }, + AccessToken: 'fake-qc-access-token', +}; + +const initiateQCMock = mock.method( + JellyfinAPI.prototype, + 'initiateQuickConnect', + async () => ({ ...defaultInitiateResponse }) +); + +const checkQCMock = mock.method( + JellyfinAPI.prototype, + 'checkQuickConnect', + async () => ({ ...defaultCheckResponse }) +); + +const authenticateQCMock = mock.method( + JellyfinAPI.prototype, + 'authenticateQuickConnect', + async () => ({ ...defaultAuthenticateResponse }) +); +const fakeAvatarBuffer = Buffer.from('fake-quickconnect-avatar-bytes'); + +const axiosHeadMock = mock.method(axios, 'head', async () => ({ + status: 200, + headers: { 'last-modified': 'Wed, 01 Jan 2025 00:00:00 GMT' }, +})); + +const clearCachedImageMock = mock.method( + ImageProxy.prototype, + 'clearCachedImage', + async () => undefined +); + +const getImageMock = mock.method( + ImageProxy.prototype, + 'getImage', + async () => ({ + imageBuffer: fakeAvatarBuffer, + meta: { + revalidateAfter: 3600, + curRevalidate: 3600, + isStale: false, + etag: 'mock-meta-etag', + extension: 'jpg', + cacheKey: 'mock-cache-key', + cacheMiss: true, + }, + }) +); + let app: Express; function createApp() { @@ -67,6 +154,427 @@ async function authenticatedAgent(email: string, password: string) { return agent; } +/** Configure Jellyfin settings for testing QC */ +function configureJellyfin() { + const settings = getSettings(); + settings.main.mediaServerType = MediaServerType.JELLYFIN; + settings.main.newPlexLogin = true; + settings.jellyfin.ip = 'localhost'; + settings.jellyfin.port = 8096; + settings.jellyfin.useSsl = false; + settings.jellyfin.urlBase = ''; +} + +describe('POST /auth/jellyfin/quickconnect/initiate', () => { + beforeEach(() => { + initiateQCMock.mock.resetCalls(); + initiateQCMock.mock.mockImplementation(async () => ({ + ...defaultInitiateResponse, + })); + configureJellyfin(); + }); + + it('returns code and secret on success', async () => { + const res = await request(app).post('/auth/jellyfin/quickconnect/initiate'); + + assert.strictEqual(res.status, 200); + assert.strictEqual(res.body.code, '123456'); + assert.strictEqual(res.body.secret, 'abc123def456abc123def456'); + assert.strictEqual(initiateQCMock.mock.callCount(), 1); + }); + + it('returns 403 when the media server is Emby', async () => { + getSettings().main.mediaServerType = MediaServerType.EMBY; + + const res = await request(app).post('/auth/jellyfin/quickconnect/initiate'); + + assert.strictEqual(res.status, 403); + assert.strictEqual(initiateQCMock.mock.callCount(), 0); + }); + + it('returns 500 when Jellyfin API fails', async () => { + initiateQCMock.mock.mockImplementation(async () => { + throw new Error('Connection refused'); + }); + + const res = await request(app).post('/auth/jellyfin/quickconnect/initiate'); + + assert.strictEqual(res.status, 500); + assert.match(res.body.message, /initiate quick connect/i); + }); + + it('returns 500 when initiateQuickConnect throws ApiError', async () => { + initiateQCMock.mock.mockImplementation(async () => { + throw new ApiError(500, ApiErrorCode.Unknown); + }); + + const res = await request(app).post('/auth/jellyfin/quickconnect/initiate'); + assert.strictEqual(res.status, 500); + }); +}); + +describe('GET /auth/jellyfin/quickconnect/check', () => { + beforeEach(() => { + checkQCMock.mock.resetCalls(); + checkQCMock.mock.mockImplementation(async () => ({ + ...defaultCheckResponse, + })); + configureJellyfin(); + }); + + it('returns authenticated: false when not yet authorized', async () => { + const res = await request(app) + .get('/auth/jellyfin/quickconnect/check') + .query({ secret: 'abc123def456abc123def456' }); + + assert.strictEqual(res.status, 200); + assert.strictEqual(res.body.authenticated, false); + assert.strictEqual(checkQCMock.mock.callCount(), 1); + }); + + it('returns authenticated: true when authorized', async () => { + checkQCMock.mock.mockImplementation(async () => ({ + ...defaultCheckResponse, + Authenticated: true, + })); + + const res = await request(app) + .get('/auth/jellyfin/quickconnect/check') + .query({ secret: 'abc123def456abc123def456' }); + + assert.strictEqual(res.status, 200); + assert.strictEqual(res.body.authenticated, true); + }); + + it('returns 400 when secret is missing', async () => { + const res = await request(app).get('/auth/jellyfin/quickconnect/check'); + + assert.strictEqual(res.status, 400); + assert.match(res.body.message, /invalid secret/i); + assert.strictEqual(checkQCMock.mock.callCount(), 0); + }); + + it('returns 400 when secret is too short', async () => { + const res = await request(app) + .get('/auth/jellyfin/quickconnect/check') + .query({ secret: 'ab12' }); + + assert.strictEqual(res.status, 400); + assert.match(res.body.message, /invalid secret/i); + assert.strictEqual(checkQCMock.mock.callCount(), 0); + }); + + it('returns 400 when secret is too long', async () => { + const res = await request(app) + .get('/auth/jellyfin/quickconnect/check') + .query({ secret: 'a'.repeat(129) }); + + assert.strictEqual(res.status, 400); + assert.match(res.body.message, /invalid secret/i); + assert.strictEqual(checkQCMock.mock.callCount(), 0); + }); + + it('returns 400 when secret contains non-hex characters', async () => { + const res = await request(app) + .get('/auth/jellyfin/quickconnect/check') + .query({ secret: 'zzzzzzzzzzzz' }); + + assert.strictEqual(res.status, 400); + assert.match(res.body.message, /invalid secret/i); + assert.strictEqual(checkQCMock.mock.callCount(), 0); + }); + + it('returns 403 when the media server is Emby', async () => { + getSettings().main.mediaServerType = MediaServerType.EMBY; + + const res = await request(app) + .get('/auth/jellyfin/quickconnect/check') + .query({ secret: 'abc123def456abc123def456' }); + + assert.strictEqual(res.status, 403); + assert.strictEqual(checkQCMock.mock.callCount(), 0); + }); + + it('returns error when Jellyfin API fails', async () => { + checkQCMock.mock.mockImplementation(async () => { + throw new ApiError(500, ApiErrorCode.Unknown); + }); + + const res = await request(app) + .get('/auth/jellyfin/quickconnect/check') + .query({ secret: 'abc123def456abc123def456' }); + + assert.strictEqual(res.status, 500); + }); +}); + +describe('POST /auth/jellyfin/quickconnect/authenticate', () => { + beforeEach(() => { + authenticateQCMock.mock.resetCalls(); + authenticateQCMock.mock.mockImplementation(async () => ({ + ...defaultAuthenticateResponse, + })); + axiosHeadMock.mock.resetCalls(); + clearCachedImageMock.mock.resetCalls(); + getImageMock.mock.resetCalls(); + configureJellyfin(); + }); + + it('returns 400 when secret is missing', async () => { + const res = await request(app) + .post('/auth/jellyfin/quickconnect/authenticate') + .send({}); + + assert.strictEqual(res.status, 400); + assert.strictEqual(authenticateQCMock.mock.callCount(), 0); + }); + + it('returns 400 when secret is not a string', async () => { + const res = await request(app) + .post('/auth/jellyfin/quickconnect/authenticate') + .send({ secret: 12345678 }); + + assert.strictEqual(res.status, 400); + assert.strictEqual(authenticateQCMock.mock.callCount(), 0); + }); + + it('returns 400 when secret is too short', async () => { + const res = await request(app) + .post('/auth/jellyfin/quickconnect/authenticate') + .send({ secret: 'ab12' }); + + assert.strictEqual(res.status, 400); + }); + + it('returns 400 when secret contains non-hex characters', async () => { + const res = await request(app) + .post('/auth/jellyfin/quickconnect/authenticate') + .send({ secret: 'zzzzzzzzzzzz' }); + + assert.strictEqual(res.status, 400); + assert.strictEqual(authenticateQCMock.mock.callCount(), 0); + }); + + it('returns 403 when media server is not configured', async () => { + const settings = getSettings(); + settings.main.mediaServerType = MediaServerType.NOT_CONFIGURED; + + const res = await request(app) + .post('/auth/jellyfin/quickconnect/authenticate') + .send({ secret: 'abc123def456abc123def456' }); + + assert.strictEqual(res.status, 403); + assert.match(res.body.message, /initial setup/i); + assert.strictEqual(authenticateQCMock.mock.callCount(), 0); + }); + + it('returns 403 when no users exist in the database', async () => { + // Clear all users to simulate initial setup + const userRepo = getRepository(User); + await userRepo.clear(); + + const res = await request(app) + .post('/auth/jellyfin/quickconnect/authenticate') + .send({ secret: 'abc123def456abc123def456' }); + + assert.strictEqual(res.status, 403); + assert.match(res.body.message, /initial setup/i); + }); + + it('signs in an existing Jellyfin user and sets session', async () => { + const userRepo = getRepository(User); + const existingUser = new User({ + email: 'existing-qc@seerr.dev', + jellyfinUsername: 'quickconnectuser', + jellyfinUserId: 'jf-qc-user-001', + jellyfinDeviceId: 'old-device-id', + permissions: 0, + avatar: '/avatarproxy/jf-qc-user-001?v=0', + userType: UserType.JELLYFIN, + }); + await userRepo.save(existingUser); + + const agent = request.agent(app); + + const res = await agent + .post('/auth/jellyfin/quickconnect/authenticate') + .send({ secret: 'abc123def456abc123def456' }); + + assert.strictEqual(res.status, 200); + assert.ok('id' in res.body); + assert.ok(!('password' in res.body)); + + const meRes = await agent.get('/auth/me'); + assert.strictEqual(meRes.status, 200); + assert.strictEqual(meRes.body.jellyfinUsername, 'quickconnectuser'); + + const updatedUser = await userRepo.findOneOrFail({ + where: { jellyfinUserId: 'jf-qc-user-001' }, + select: { + id: true, + jellyfinAuthToken: true, + jellyfinDeviceId: true, + }, + }); + assert.strictEqual(updatedUser.jellyfinAuthToken, 'fake-qc-access-token'); + assert.notStrictEqual(updatedUser.jellyfinDeviceId, 'old-device-id'); + }); + + it('refreshes avatarVersion/avatarETag when the remote avatar has changed', async () => { + const userRepo = getRepository(User); + const existingUser = new User({ + email: 'qc-avatar-change@seerr.dev', + jellyfinUsername: 'quickconnectuser', + jellyfinUserId: 'jf-qc-user-001', + jellyfinDeviceId: 'old-device-id', + permissions: 0, + avatar: '/avatarproxy/jf-qc-user-001?v=old', + avatarVersion: 'old-version', + avatarETag: 'old-etag', + userType: UserType.JELLYFIN, + }); + await userRepo.save(existingUser); + + const agent = request.agent(app); + const res = await agent + .post('/auth/jellyfin/quickconnect/authenticate') + .send({ secret: 'abc123def456abc123def456' }); + + assert.strictEqual(res.status, 200); + + const updatedUser = await userRepo.findOneOrFail({ + where: { jellyfinUserId: 'jf-qc-user-001' }, + }); + assert.notStrictEqual(updatedUser.avatarVersion, 'old-version'); + assert.notStrictEqual(updatedUser.avatarETag, 'old-etag'); + assert.notStrictEqual( + updatedUser.avatar, + '/avatarproxy/jf-qc-user-001?v=old' + ); + }); + + it('creates a new user when newPlexLogin is enabled and user does not exist', async () => { + const settings = getSettings(); + settings.main.newPlexLogin = true; + + authenticateQCMock.mock.mockImplementation(async () => ({ + User: { + Id: 'jf-brand-new-user', + Name: 'brandnewuser', + ServerId: 'server-1', + Policy: { IsAdministrator: false }, + }, + AccessToken: 'new-user-token', + })); + + const agent = request.agent(app); + + const res = await agent + .post('/auth/jellyfin/quickconnect/authenticate') + .send({ secret: 'abc123def456abc123def456' }); + + assert.strictEqual(res.status, 200); + assert.ok('id' in res.body); + + const userRepo = getRepository(User); + const newUser = await userRepo.findOne({ + where: { jellyfinUserId: 'jf-brand-new-user' }, + }); + assert.ok(newUser); + assert.strictEqual(newUser.jellyfinUsername, 'brandnewuser'); + assert.strictEqual(newUser.userType, UserType.JELLYFIN); + + const meRes = await agent.get('/auth/me'); + assert.strictEqual(meRes.status, 200); + }); + + it('returns 403 when the media server is Emby', async () => { + getSettings().main.mediaServerType = MediaServerType.EMBY; + + const res = await request(app) + .post('/auth/jellyfin/quickconnect/authenticate') + .send({ secret: 'abc123def456abc123def456' }); + + assert.strictEqual(res.status, 403); + assert.strictEqual(authenticateQCMock.mock.callCount(), 0); + }); + + it('applies default permissions to newly created users', async () => { + const settings = getSettings(); + settings.main.newPlexLogin = true; + settings.main.defaultPermissions = 32; + + authenticateQCMock.mock.mockImplementation(async () => ({ + User: { + Id: 'jf-perms-test-user', + Name: 'permsuser', + ServerId: 'server-1', + Policy: { IsAdministrator: false }, + }, + AccessToken: 'perms-token', + })); + + const res = await request(app) + .post('/auth/jellyfin/quickconnect/authenticate') + .send({ secret: 'abc123def456abc123def456' }); + + assert.strictEqual(res.status, 200); + + const userRepo = getRepository(User); + const user = await userRepo.findOneOrFail({ + where: { jellyfinUserId: 'jf-perms-test-user' }, + }); + assert.strictEqual(user.permissions, 32); + }); + + it('returns 403 when newPlexLogin is disabled and user does not exist', async () => { + const settings = getSettings(); + settings.main.newPlexLogin = false; + + authenticateQCMock.mock.mockImplementation(async () => ({ + User: { + Id: 'jf-unknown-user', + Name: 'unknownuser', + ServerId: 'server-1', + Policy: { IsAdministrator: false }, + }, + AccessToken: 'unknown-token', + })); + + const res = await request(app) + .post('/auth/jellyfin/quickconnect/authenticate') + .send({ secret: 'abc123def456abc123def456' }); + + assert.strictEqual(res.status, 403); + assert.strictEqual(res.body.message, 'Access denied.'); + }); + + it('returns error when Jellyfin authenticateQuickConnect fails', async () => { + authenticateQCMock.mock.mockImplementation(async () => { + throw new ApiError(401, ApiErrorCode.InvalidCredentials); + }); + + const res = await request(app) + .post('/auth/jellyfin/quickconnect/authenticate') + .send({ secret: 'abc123def456abc123def456' }); + + assert.strictEqual(res.status, 401); + assert.strictEqual(res.body.message, ApiErrorCode.InvalidCredentials); + }); + + it('returns 500 when Jellyfin throws a generic error', async () => { + authenticateQCMock.mock.mockImplementation(async () => { + throw new Error('Network timeout'); + }); + + const res = await request(app) + .post('/auth/jellyfin/quickconnect/authenticate') + .send({ secret: 'abc123def456abc123def456' }); + + assert.strictEqual(res.status, 500); + }); +}); + describe('GET /auth/me', () => { it('returns 403 when not authenticated', async () => { const res = await request(app).get('/auth/me'); @@ -110,6 +618,30 @@ describe('GET /auth/me', () => { settings.notifications.agents.email.options.userEmailRequired = false; }); + + it('returns only the allowlisted settings fields', async () => { + await seedUserSettings('admin@seerr.dev'); + const agent = await authenticatedAgent('admin@seerr.dev', 'test1234'); + + const res = await agent.get('/auth/me'); + + assert.strictEqual(res.status, 200); + assert.deepStrictEqual(Object.keys(res.body.settings).sort(), [ + 'discoverRegion', + 'locale', + 'notificationTypes', + 'originalLanguage', + 'streamingRegion', + 'watchlistSyncMovies', + 'watchlistSyncTv', + ]); + assert.strictEqual(res.body.settings.locale, allowlistedSettings.locale); + assert.strictEqual( + res.body.settings.discoverRegion, + allowlistedSettings.discoverRegion + ); + assertNoCredentials(res.body); + }); }); describe('POST /auth/local', () => { @@ -189,7 +721,7 @@ describe('POST /auth/local', () => { it('allows the non-admin user to log in', async () => { const res = await request(app) .post('/auth/local') - .send({ email: 'friend@seerr.dev', password: 'test1234' }); + .send({ email: 'demo@seerr.dev', password: 'test1234' }); assert.strictEqual(res.status, 200); assert.ok('id' in res.body); diff --git a/server/routes/auth.ts b/server/routes/auth.ts index f625e68f0e..f83dc80159 100644 --- a/server/routes/auth.ts +++ b/server/routes/auth.ts @@ -18,9 +18,18 @@ import axios from 'axios'; import { Router } from 'express'; import net from 'net'; import validator from 'validator'; +import { z } from 'zod'; const authRoutes = Router(); +export const quickConnectSecret = z.object({ + secret: z + .string() + .min(8) + .max(128) + .regex(/^[A-Fa-f0-9]+$/), +}); + authRoutes.get('/me', isAuthenticated(), async (req, res) => { const userRepository = getRepository(User); if (!req.user) { @@ -43,7 +52,18 @@ authRoutes.get('/me', isAuthenticated(), async (req, res) => { logger.warn(`User ${user.username} has no valid email address`); } - return res.status(200).json(user); + return res.status(200).json({ + ...user.toJSON(), + settings: user.settings && { + locale: user.settings.locale, + discoverRegion: user.settings.discoverRegion, + streamingRegion: user.settings.streamingRegion, + originalLanguage: user.settings.originalLanguage, + notificationTypes: user.settings.notificationTypes, + watchlistSyncMovies: user.settings.watchlistSyncMovies, + watchlistSyncTv: user.settings.watchlistSyncTv, + }, + }); }); authRoutes.post('/plex', async (req, res, next) => { @@ -534,6 +554,30 @@ authRoutes.post('/jellyfin', async (req, res, next) => { message: e.errorCode, }); + case ApiErrorCode.ConnectionError: + logger.error( + `Unable to reach the ${ + settings.main.mediaServerType === MediaServerType.JELLYFIN + ? ServerType.JELLYFIN + : ServerType.EMBY + } server.`, + { + label: 'Auth', + error: e.errorCode, + status: e.statusCode, + hostname: getHostname({ + useSsl: body.useSsl, + ip: body.hostname, + port: body.port, + urlBase: body.urlBase, + }), + } + ); + return next({ + status: e.statusCode, + message: e.errorCode, + }); + case ApiErrorCode.InvalidCredentials: logger.warn( 'Failed sign-in attempt from user with incorrect Jellyfin credentials', @@ -593,6 +637,219 @@ authRoutes.post('/jellyfin', async (req, res, next) => { } }); +authRoutes.post('/jellyfin/quickconnect/initiate', async (req, res, next) => { + const settings = getSettings(); + + if (settings.main.mediaServerType !== MediaServerType.JELLYFIN) { + return next({ + status: 403, + message: 'Quick Connect is only supported by Jellyfin.', + }); + } + + try { + const hostname = getHostname(); + const jellyfinServer = new JellyfinAPI( + hostname ?? '', + undefined, + undefined + ); + + const response = await jellyfinServer.initiateQuickConnect(); + + return res.status(200).json({ + code: response.Code, + secret: response.Secret, + }); + } catch (error) { + logger.error('Error initiating Jellyfin quick connect', { + label: 'Auth', + errorMessage: error.message, + }); + return next({ + status: 500, + message: 'Failed to initiate quick connect.', + }); + } +}); + +authRoutes.get('/jellyfin/quickconnect/check', async (req, res, next) => { + const settings = getSettings(); + + if (settings.main.mediaServerType !== MediaServerType.JELLYFIN) { + return next({ + status: 403, + message: 'Quick Connect is only supported by Jellyfin.', + }); + } + + const result = quickConnectSecret.safeParse(req.query); + if (!result.success) { + return next({ + status: 400, + message: 'Invalid secret format', + }); + } + + const { secret } = result.data; + + try { + const hostname = getHostname(); + const jellyfinServer = new JellyfinAPI( + hostname ?? '', + undefined, + undefined + ); + + const response = await jellyfinServer.checkQuickConnect(secret); + + return res.status(200).json({ authenticated: response.Authenticated }); + } catch (e) { + return next({ + status: e.statusCode || 500, + message: 'Failed to check Quick Connect status', + }); + } +}); + +authRoutes.post( + '/jellyfin/quickconnect/authenticate', + async (req, res, next) => { + const settings = getSettings(); + const userRepository = getRepository(User); + const result = quickConnectSecret.safeParse(req.body); + if (!result.success) { + return next({ + status: 400, + message: 'Secret required', + }); + } + + const { secret } = result.data; + + if ( + settings.main.mediaServerType === MediaServerType.NOT_CONFIGURED || + !(await userRepository.count()) + ) { + return next({ + status: 403, + message: 'Quick Connect is not available during initial setup.', + }); + } + + if (settings.main.mediaServerType !== MediaServerType.JELLYFIN) { + return next({ + status: 403, + message: 'Quick Connect is only supported by Jellyfin.', + }); + } + + try { + const hostname = getHostname(); + const jellyfinServer = new JellyfinAPI( + hostname ?? '', + undefined, + undefined + ); + + const account = await jellyfinServer.authenticateQuickConnect(secret); + + let user = await userRepository.findOne({ + where: { jellyfinUserId: account.User.Id }, + }); + + const deviceId = Buffer.from( + `BOT_seerr_${account.User.Name ?? ''}` + ).toString('base64'); + + if (user) { + logger.info('Quick Connect sign-in from existing user', { + label: 'API', + ip: req.ip, + jellyfinUsername: account.User.Name, + userId: user.id, + }); + + user.jellyfinAuthToken = account.AccessToken; + user.jellyfinDeviceId = deviceId; + user.avatar = getUserAvatarUrl(user); + await userRepository.save(user); + } else if (!settings.main.newPlexLogin) { + logger.warn( + 'Failed Quick Connect sign-in attempt by unimported Jellyfin user', + { + label: 'API', + ip: req.ip, + jellyfinUserId: account.User.Id, + jellyfinUsername: account.User.Name, + } + ); + return next({ + status: 403, + message: 'Access denied.', + }); + } else { + logger.info( + 'Quick Connect sign-in from new Jellyfin user; creating new Seerr user', + { + label: 'API', + ip: req.ip, + jellyfinUsername: account.User.Name, + } + ); + + user = new User({ + email: account.User.Name, + jellyfinUsername: account.User.Name, + jellyfinUserId: account.User.Id, + jellyfinDeviceId: deviceId, + permissions: settings.main.defaultPermissions, + userType: UserType.JELLYFIN, + }); + user.avatar = getUserAvatarUrl(user); + await userRepository.save(user); + } + + if (user.jellyfinUserId) { + try { + const { changed } = await checkAvatarChanged(user); + + if (changed) { + user.avatar = getUserAvatarUrl(user); + await userRepository.save(user); + logger.debug('Avatar updated during Quick Connect login', { + userId: user.id, + jellyfinUserId: user.jellyfinUserId, + }); + } + } catch (error) { + logger.error('Error handling avatar during Quick Connect login', { + label: 'Auth', + errorMessage: error.message, + }); + } + } + + // Set session + if (req.session) { + req.session.userId = user.id; + } + + return res.status(200).json(user?.filter() ?? {}); + } catch (e) { + logger.error('Quick Connect authentication failed', { + label: 'Auth', + error: e.message, + ip: req.ip, + }); + return next({ + status: e.statusCode || 500, + message: ApiErrorCode.InvalidCredentials, + }); + } + } +); + authRoutes.post('/local', async (req, res, next) => { const settings = getSettings(); const userRepository = getRepository(User); @@ -671,7 +928,7 @@ authRoutes.post('/logout', async (req, res, next) => { await axios.delete(`${baseUrl}/Devices`, { params: { Id: user.jellyfinDeviceId }, headers: { - 'X-Emby-Authorization': `MediaBrowser Client="Seerr", Device="Seerr", DeviceId="seerr", Version="${ + Authorization: `MediaBrowser Client="Seerr", Device="Seerr", DeviceId="seerr", Version="${ settings.main.mediaServerType === MediaServerType.EMBY ? '1.0.0' : getAppVersion() diff --git a/server/routes/author.ts b/server/routes/author.ts new file mode 100644 index 0000000000..807481c4c7 --- /dev/null +++ b/server/routes/author.ts @@ -0,0 +1,60 @@ +import Hardcover from '@server/api/hardcover'; +import type { HardcoverAuthorDetails } from '@server/api/hardcover/interfaces'; +import { MediaType } from '@server/constants/media'; +import Media from '@server/entity/Media'; +import { getSettings } from '@server/lib/settings'; +import logger from '@server/logger'; +import { mapAuthorDetails } from '@server/models/Author'; +import { mapBookResult } from '@server/models/Search'; +import { Router } from 'express'; + +const authorRoutes = Router(); + +authorRoutes.get('/:id', async (req, res, next) => { + if (!getSettings().main.hardcoverapikey) { + return next({ + status: 503, + message: 'Hardcover API key is not configured.', + }); + } + + const hardcover = new Hardcover(); + + try { + const hardcoverAuthor = (await hardcover.getAuthor( + Number(req.params.id) + )) as HardcoverAuthorDetails; + + const media = await Media.getRelatedMedia( + req.user, + hardcoverAuthor.contributions.map((result) => ({ + tmdbId: result.book.id, + mediaType: MediaType.BOOK, + })) + ); + + const books = hardcoverAuthor.contributions.map((result) => + mapBookResult( + result.book, + media.find( + (req) => + req.tmdbId === result.book.id && req.mediaType === MediaType.BOOK + ) + ) + ); + + return res.status(200).json(mapAuthorDetails(hardcoverAuthor, books)); + } catch (e) { + logger.debug('Something went wrong retrieving author', { + label: 'API', + errorMessage: e.message, + authorId: req.params.id, + }); + return next({ + status: 500, + message: 'Unable to retrieve author.', + }); + } +}); + +export default authorRoutes; diff --git a/server/routes/avatarproxy.ts b/server/routes/avatarproxy.ts index 2027d79dbf..c61522a516 100644 --- a/server/routes/avatarproxy.ts +++ b/server/routes/avatarproxy.ts @@ -4,7 +4,6 @@ import { User } from '@server/entity/User'; import ImageProxy from '@server/lib/imageproxy'; import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; -import { getAppVersion } from '@server/utils/appVersion'; import { getHostname } from '@server/utils/getHostname'; import axios from 'axios'; import { Router } from 'express'; @@ -15,25 +14,9 @@ const router = Router(); let _avatarImageProxy: ImageProxy | null = null; -async function initAvatarImageProxy() { +function initAvatarImageProxy() { if (!_avatarImageProxy) { - const userRepository = getRepository(User); - const admin = await userRepository.findOne({ - where: { id: 1 }, - select: ['id', 'jellyfinUserId', 'jellyfinDeviceId'], - order: { id: 'ASC' }, - }); - const deviceId = admin?.jellyfinDeviceId || 'BOT_seerr'; - const authToken = getSettings().jellyfin.apiKey; - _avatarImageProxy = new ImageProxy('avatar', '', { - headers: { - 'X-Emby-Authorization': `MediaBrowser Client="Seerr", Device="Seerr", DeviceId="${deviceId}", Version="${ - getSettings().main.mediaServerType === MediaServerType.EMBY - ? '1.0.0' - : getAppVersion() - }", Token="${authToken}"`, - }, - }); + _avatarImageProxy = new ImageProxy('avatar', ''); } return _avatarImageProxy; } @@ -88,7 +71,7 @@ export async function checkAvatarChanged( return { changed: false, etag: user.avatarETag ?? undefined }; } - const avatarImageCache = await initAvatarImageProxy(); + const avatarImageCache = initAvatarImageProxy(); await avatarImageCache.clearCachedImage(jellyfinAvatarUrl); const imageData = await avatarImageCache.getImage( jellyfinAvatarUrl, @@ -115,20 +98,18 @@ export async function checkAvatarChanged( } } -router.get('/:jellyfinUserId', async (req, res) => { +router.get('/:jellyfinUserId', async (req, res, next) => { + if (!req.params.jellyfinUserId.match(/^[a-f0-9]{32}$/)) { + const mediaServerType = getSettings().main.mediaServerType; + return next({ + status: 400, + message: `Provided URL is not ${ + mediaServerType === MediaServerType.JELLYFIN ? 'a Jellyfin' : 'an Emby' + } avatar.`, + }); + } try { - if (!req.params.jellyfinUserId.match(/^[a-f0-9]{32}$/)) { - const mediaServerType = getSettings().main.mediaServerType; - throw new Error( - `Provided URL is not ${ - mediaServerType === MediaServerType.JELLYFIN - ? 'a Jellyfin' - : 'an Emby' - } avatar.` - ); - } - - const avatarImageCache = await initAvatarImageProxy(); + const avatarImageCache = initAvatarImageProxy(); const userEtag = req.headers['if-none-match']; @@ -173,9 +154,14 @@ router.get('/:jellyfinUserId', async (req, res) => { res.end(imageData.imageBuffer); } catch (e) { - logger.error('Failed to proxy avatar image', { - errorMessage: e.message, - }); + logger.error('Failed to proxy avatar image', { errorMessage: e.message }); + if (!res.headersSent) { + return next({ + status: 500, + message: 'Failed to proxy avatar image.', + }); + } + next(e); } }); diff --git a/server/routes/blocklist.test.ts b/server/routes/blocklist.test.ts new file mode 100644 index 0000000000..9273783eff --- /dev/null +++ b/server/routes/blocklist.test.ts @@ -0,0 +1,106 @@ +import assert from 'node:assert/strict'; +import { before, describe, it } from 'node:test'; + +import { MediaStatus, MediaType } from '@server/constants/media'; +import { getRepository } from '@server/datasource'; +import { Blocklist } from '@server/entity/Blocklist'; +import Media from '@server/entity/Media'; +import { getSettings } from '@server/lib/settings'; +import { checkUser } from '@server/middleware/auth'; +import { setupTestDb } from '@server/test/db'; +import { + assertNoCredentials, + seedUserSettings, +} from '@server/test/userSettings'; +import type { Express } from 'express'; +import express from 'express'; +import session from 'express-session'; +import request from 'supertest'; +import authRoutes from './auth'; +import blocklistRoutes from './blocklist'; + +let app: Express; + +function createApp() { + const app = express(); + app.use(express.json()); + app.use( + session({ + secret: 'test-secret', + resave: false, + saveUninitialized: false, + }) + ); + app.use(checkUser); + app.use('/auth', authRoutes); + app.use('/blocklist', blocklistRoutes); + app.use( + ( + err: { status?: number; message?: string }, + _req: express.Request, + res: express.Response, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _next: express.NextFunction + ) => { + res + .status(err.status ?? 500) + .json({ status: err.status ?? 500, message: err.message }); + } + ); + return app; +} + +before(async () => { + app = createApp(); +}); + +setupTestDb(); + +async function loginAs(email: string, password: string) { + const settings = getSettings(); + const priorLocalLogin = settings.main.localLogin; + settings.main.localLogin = true; + + try { + const agent = request.agent(app); + const res = await agent.post('/auth/local').send({ email, password }); + assert.strictEqual(res.status, 200); + return agent; + } finally { + settings.main.localLogin = priorLocalLogin; + } +} + +describe('GET /blocklist/:id', () => { + // this route replies with res.send() rather than res.json() + it('omits notification settings from the blocklisting user', async () => { + const blocklistUser = await seedUserSettings('demo@seerr.dev'); + + const media = await getRepository(Media).save( + new Media({ + mediaType: MediaType.MOVIE, + tmdbId: 12345, + status: MediaStatus.BLOCKLISTED, + status4k: MediaStatus.UNKNOWN, + }) + ); + + await getRepository(Blocklist).save( + new Blocklist({ + mediaType: MediaType.MOVIE, + tmdbId: 12345, + title: 'Blocked Movie', + user: blocklistUser, + media, + }) + ); + + const admin = await loginAs('admin@seerr.dev', 'test1234'); + const res = await admin.get('/blocklist/12345?mediaType=movie'); + + assert.strictEqual(res.status, 200); + assert.strictEqual(res.body.user.email, 'demo@seerr.dev'); + assert.ok(!('settings' in res.body.user)); + assertNoCredentials(res.body); + }); +}); diff --git a/server/routes/blocklist.ts b/server/routes/blocklist.ts index 39315e8d22..77ea001f8d 100644 --- a/server/routes/blocklist.ts +++ b/server/routes/blocklist.ts @@ -92,7 +92,11 @@ blocklistRoutes.get( }), async (req, res, next) => { const mediaType = req.query.mediaType; - if (mediaType !== MediaType.MOVIE && mediaType !== MediaType.TV) { + if ( + mediaType !== MediaType.MOVIE && + mediaType !== MediaType.TV && + mediaType !== MediaType.BOOK + ) { return next({ status: 400, message: 'Invalid or missing mediaType query parameter.', @@ -268,7 +272,11 @@ blocklistRoutes.delete( }), async (req, res, next) => { const mediaType = req.query.mediaType; - if (mediaType !== MediaType.MOVIE && mediaType !== MediaType.TV) { + if ( + mediaType !== MediaType.MOVIE && + mediaType !== MediaType.TV && + mediaType !== MediaType.BOOK + ) { return next({ status: 400, message: 'Invalid or missing mediaType query parameter.', diff --git a/server/routes/book.ts b/server/routes/book.ts new file mode 100644 index 0000000000..e961c58838 --- /dev/null +++ b/server/routes/book.ts @@ -0,0 +1,40 @@ +import Hardcover from '@server/api/hardcover'; +import { MediaType } from '@server/constants/media'; +import Media from '@server/entity/Media'; +import { getSettings } from '@server/lib/settings'; +import logger from '@server/logger'; +import { mapBookDetails } from '@server/models/Book'; +import { Router } from 'express'; + +const bookRoutes = Router(); + +bookRoutes.get('/:id', async (req, res, next) => { + if (!getSettings().main.hardcoverapikey) { + return next({ + status: 503, + message: 'Hardcover API key is not configured.', + }); + } + + const hardcover = new Hardcover(); + + try { + const hardcoverBook = await hardcover.getBook(Number(req.params.id)); + + const media = await Media.getMedia(hardcoverBook.id, MediaType.BOOK); + + return res.status(200).json(mapBookDetails(hardcoverBook, media)); + } catch (e) { + logger.debug('Something went wrong retrieving book', { + label: 'API', + errorMessage: e.message, + bookId: req.params.id, + }); + return next({ + status: 500, + message: 'Unable to retrieve book.', + }); + } +}); + +export default bookRoutes; diff --git a/server/routes/collection.ts b/server/routes/collection.ts index f2a6032278..0dc461974d 100644 --- a/server/routes/collection.ts +++ b/server/routes/collection.ts @@ -21,7 +21,8 @@ collectionRoutes.get<{ id: string }>('/:id', async (req, res, next) => { collection.parts.map((part) => ({ tmdbId: part.id, mediaType: MediaType.MOVIE, - })) + })), + { includeActiveRequest: true } ); return res.status(200).json(mapCollection(collection, media)); diff --git a/server/routes/discover.ts b/server/routes/discover.ts index 7f250e6a6e..3260c9284b 100644 --- a/server/routes/discover.ts +++ b/server/routes/discover.ts @@ -1,6 +1,9 @@ +import Hardcover, { BookSortOptionsIterable } from '@server/api/hardcover'; import PlexTvAPI from '@server/api/plextv'; -import type { SortOptions } from '@server/api/themoviedb'; -import TheMovieDb from '@server/api/themoviedb'; +import TheMovieDb, { + MovieSortOptionsIterable, + TvSortOptionsIterable, +} from '@server/api/themoviedb'; import type { TmdbKeyword } from '@server/api/themoviedb/interfaces'; import { MediaType } from '@server/constants/media'; import { getRepository } from '@server/datasource'; @@ -15,6 +18,7 @@ import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; import { mapProductionCompany } from '@server/models/Movie'; import { + mapBookResult, mapCollectionResult, mapMovieResult, mapPersonResult, @@ -62,7 +66,6 @@ const discoverRoutes = Router(); const QueryFilterOptions = z.object({ page: z.coerce.string().optional(), - sortBy: z.coerce.string().optional(), primaryReleaseDateGte: z.coerce.string().optional(), primaryReleaseDateLte: z.coerce.string().optional(), firstAirDateGte: z.coerce.string().optional(), @@ -90,21 +93,88 @@ const QueryFilterOptions = z.object({ }); export type FilterOptions = z.infer; -const ApiQuerySchema = QueryFilterOptions.omit({ +const MovieApiQuerySchema = QueryFilterOptions.omit({ certificationMode: true, +}).extend({ + sortBy: z.enum(MovieSortOptionsIterable).optional().catch(undefined), +}); +const TvApiQuerySchema = QueryFilterOptions.omit({ + certificationMode: true, +}).extend({ + sortBy: z.enum(TvSortOptionsIterable).optional().catch(undefined), +}); +const BookApiQuerySchema = QueryFilterOptions.omit({ + certificationMode: true, +}).extend({ + sortBy: z.enum(BookSortOptionsIterable).optional().catch(undefined), +}); + +discoverRoutes.get('/books', async (req, res, next) => { + if (!getSettings().main.hardcoverapikey) { + return next({ + status: 503, + message: 'Hardcover API key is not configured.', + }); + } + + const hardcover = new Hardcover(); + + try { + const query = BookApiQuerySchema.parse(req.query); + const data = await hardcover.getDiscoverBooks({ + page: Number(query.page), + genres: query.genre ? query.genre.split(',') : undefined, + releaseDateGte: query.primaryReleaseDateGte, + releaseDateLte: query.primaryReleaseDateLte, + sortBy: query.sortBy, + }); + + const media = await Media.getRelatedMedia( + req.user, + data.data.books.map((result) => ({ + tmdbId: result.id, + mediaType: MediaType.BOOK, + })) + ); + + return res.status(200).json({ + page: data.page, + totalPages: data.total_pages, + totalResults: data.total_results, + keywords: undefined, + results: data.data.books.map((result) => + mapBookResult( + result, + media.find( + (req) => + req.tmdbId === result.id && req.mediaType === MediaType.BOOK + ) + ) + ), + }); + } catch (e) { + logger.debug('Something went wrong retrieving popular books', { + label: 'API', + errorMessage: e.message, + }); + return next({ + status: 500, + message: 'Unable to retrieve popular books.', + }); + } }); discoverRoutes.get('/movies', async (req, res, next) => { const tmdb = createTmdbWithRegionLanguage(req.user); try { - const query = ApiQuerySchema.parse(req.query); + const query = MovieApiQuerySchema.parse(req.query); const keywords = query.keywords; const excludeKeywords = query.excludeKeywords; const data = await tmdb.getDiscoverMovies({ page: Number(query.page), - sortBy: query.sortBy as SortOptions, + sortBy: query.sortBy, language: req.locale ?? query.language, originalLanguage: query.language, genre: query.genre, @@ -136,7 +206,8 @@ discoverRoutes.get('/movies', async (req, res, next) => { data.results.map((result) => ({ tmdbId: result.id, mediaType: MediaType.MOVIE, - })) + })), + { includeActiveRequest: true } ); let keywordData: TmdbKeyword[] = []; @@ -208,7 +279,8 @@ discoverRoutes.get<{ language: string }>( data.results.map((result) => ({ tmdbId: result.id, mediaType: MediaType.MOVIE, - })) + })), + { includeActiveRequest: true } ); return res.status(200).json({ @@ -269,7 +341,8 @@ discoverRoutes.get<{ genreId: string }>( data.results.map((result) => ({ tmdbId: result.id, mediaType: MediaType.MOVIE, - })) + })), + { includeActiveRequest: true } ); return res.status(200).json({ @@ -320,7 +393,8 @@ discoverRoutes.get<{ studioId: string }>( data.results.map((result) => ({ tmdbId: result.id, mediaType: MediaType.MOVIE, - })) + })), + { includeActiveRequest: true } ); return res.status(200).json({ @@ -373,7 +447,8 @@ discoverRoutes.get('/movies/upcoming', async (req, res, next) => { data.results.map((result) => ({ tmdbId: result.id, mediaType: MediaType.MOVIE, - })) + })), + { includeActiveRequest: true } ); return res.status(200).json({ @@ -406,12 +481,12 @@ discoverRoutes.get('/tv', async (req, res, next) => { const tmdb = createTmdbWithRegionLanguage(req.user); try { - const query = ApiQuerySchema.parse(req.query); + const query = TvApiQuerySchema.parse(req.query); const keywords = query.keywords; const excludeKeywords = query.excludeKeywords; const data = await tmdb.getDiscoverTv({ page: Number(query.page), - sortBy: query.sortBy as SortOptions, + sortBy: query.sortBy, language: req.locale ?? query.language, genre: query.genre, network: query.network ? Number(query.network) : undefined, @@ -444,7 +519,8 @@ discoverRoutes.get('/tv', async (req, res, next) => { data.results.map((result) => ({ tmdbId: result.id, mediaType: MediaType.TV, - })) + })), + { includeActiveRequest: true } ); let keywordData: TmdbKeyword[] = []; @@ -515,7 +591,8 @@ discoverRoutes.get<{ language: string }>( data.results.map((result) => ({ tmdbId: result.id, mediaType: MediaType.TV, - })) + })), + { includeActiveRequest: true } ); return res.status(200).json({ @@ -576,7 +653,8 @@ discoverRoutes.get<{ genreId: string }>( data.results.map((result) => ({ tmdbId: result.id, mediaType: MediaType.TV, - })) + })), + { includeActiveRequest: true } ); return res.status(200).json({ @@ -627,7 +705,8 @@ discoverRoutes.get<{ networkId: string }>( data.results.map((result) => ({ tmdbId: result.id, mediaType: MediaType.TV, - })) + })), + { includeActiveRequest: true } ); return res.status(200).json({ @@ -680,7 +759,8 @@ discoverRoutes.get('/tv/upcoming', async (req, res, next) => { data.results.map((result) => ({ tmdbId: result.id, mediaType: MediaType.TV, - })) + })), + { includeActiveRequest: true } ); return res.status(200).json({ @@ -753,7 +833,8 @@ discoverRoutes.get('/trending', async (req, res, next) => { data.results.map((result) => ({ tmdbId: result.id, mediaType: isMovie(result) ? MediaType.MOVIE : MediaType.TV, - })) + })), + { includeActiveRequest: true } ); return res.status(200).json({ @@ -800,7 +881,8 @@ discoverRoutes.get<{ keywordId: string }>( data.results.map((result) => ({ tmdbId: result.id, mediaType: MediaType.MOVIE, - })) + })), + { includeActiveRequest: true } ); return res.status(200).json({ diff --git a/server/routes/imageproxy.ts b/server/routes/imageproxy.ts index 30e3581708..2c8abdd86a 100644 --- a/server/routes/imageproxy.ts +++ b/server/routes/imageproxy.ts @@ -1,6 +1,15 @@ import ImageProxy from '@server/lib/imageproxy'; import logger from '@server/logger'; import { Router } from 'express'; +import sharp from 'sharp'; + +const hardcoverTransform = async (buffer: Buffer) => ({ + buffer: await sharp(buffer) + .resize(600, 900, { fit: 'cover' }) + .webp({ quality: 80 }) + .toBuffer(), + extension: 'webp', +}); const router = Router(); @@ -29,16 +38,33 @@ function initTvdbImageProxy() { } return _tvdbImageProxy; } +let _hardcoverImageProxy: ImageProxy; +function initHardcoverImageProxy() { + if (!_hardcoverImageProxy) { + _hardcoverImageProxy = new ImageProxy( + 'hardcover', + 'https://assets.hardcover.app', + { + rateLimitOptions: { + maxRequests: 20, + maxRPS: 50, + }, + transform: hardcoverTransform, + } + ); + } + return _hardcoverImageProxy; +} router.get<{ type: string; path: string[]; -}>('/:type/*path', async (req, res) => { +}>('/:type/*path', async (req, res, next) => { const imagePath = '/' + req.params.path.join('/'); if (imagePath.startsWith('//') || imagePath.includes('://')) { logger.error('Invalid URL for image proxy', { imagePath }); - return res.status(403).send('Invalid URL for image proxy'); + return next({ status: 403, message: 'Invalid URL for image proxy.' }); } try { @@ -47,13 +73,14 @@ router.get<{ imageData = await initTmdbImageProxy().getImage(imagePath); } else if (req.params.type === 'tvdb') { imageData = await initTvdbImageProxy().getImage(imagePath); + } else if (req.params.type === 'hardcover') { + imageData = await initHardcoverImageProxy().getImage(imagePath); } else { logger.error('Unsupported image type', { imagePath, type: req.params.type, }); - res.status(400).send('Unsupported image type'); - return; + return next({ status: 400, message: 'Unsupported image type.' }); } res.writeHead(200, { @@ -70,7 +97,10 @@ router.get<{ imagePath, errorMessage: e.message, }); - res.status(500).send(); + if (!res.headersSent) { + return next({ status: 500, message: 'Failed to proxy image.' }); + } + next(e); } }); diff --git a/server/routes/index.ts b/server/routes/index.ts index f701acf968..6399f5135b 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -29,7 +29,9 @@ import restartFlag from '@server/utils/restartFlag'; import { isPerson } from '@server/utils/typeHelpers'; import { Router } from 'express'; import authRoutes from './auth'; +import authorRoutes from './author'; import blocklistRoutes from './blocklist'; +import bookRoutes from './book'; import collectionRoutes from './collection'; import discoverRoutes, { createTmdbWithRegionLanguage } from './discover'; import issueRoutes from './issue'; @@ -39,6 +41,7 @@ import movieRoutes from './movie'; import personRoutes from './person'; import requestRoutes from './request'; import searchRoutes from './search'; +import seriesRoutes from './series'; import serviceRoutes from './service'; import tvRoutes from './tv'; import user from './user'; @@ -48,40 +51,47 @@ const router = Router(); router.use(checkUser); router.get('/status', async (req, res) => { - const githubApi = new GithubAPI(); - + const settings = getSettings(); const currentVersion = getAppVersion(); const commitTag = getCommitTag(); + const checkUpdate = + req.query.checkUpdateAvailable !== undefined + ? req.query.checkUpdateAvailable + : settings.fullPublicSettings.versionCheck; let updateAvailable = false; let commitsBehind = 0; - if (currentVersion.startsWith('develop-') && commitTag !== 'local') { - const commits = await githubApi.getSeerrCommits(); + if (checkUpdate) { + const githubApi = new GithubAPI(); - if (commits.length) { - const filteredCommits = commits.filter( - (commit) => !commit.commit.message.includes('[skip ci]') - ); - if (filteredCommits[0].sha !== commitTag) { - updateAvailable = true; - } + if (currentVersion.startsWith('develop-') && commitTag !== 'local') { + const commits = await githubApi.getSeerrCommits(); - const commitIndex = filteredCommits.findIndex( - (commit) => commit.sha === commitTag - ); + if (commits.length) { + const filteredCommits = commits.filter( + (commit) => !commit.commit.message.includes('[skip ci]') + ); + if (filteredCommits[0].sha !== commitTag) { + updateAvailable = true; + } - if (updateAvailable) { - commitsBehind = commitIndex; + const commitIndex = filteredCommits.findIndex( + (commit) => commit.sha === commitTag + ); + + if (updateAvailable) { + commitsBehind = commitIndex; + } } - } - } else if (commitTag !== 'local') { - const releases = await githubApi.getSeerrReleases(); + } else if (commitTag !== 'local') { + const releases = await githubApi.getSeerrReleases(); - if (releases.length) { - const latestVersion = releases[0]; + if (releases.length) { + const latestVersion = releases[0]; - if (!latestVersion.name.includes(currentVersion)) { - updateAvailable = true; + if (!latestVersion.name.includes(currentVersion)) { + updateAvailable = true; + } } } } @@ -89,8 +99,7 @@ router.get('/status', async (req, res) => { return res.status(200).json({ version: getAppVersion(), commitTag: getCommitTag(), - updateAvailable, - commitsBehind, + ...(checkUpdate && { updateAvailable, commitsBehind }), restartRequired: restartFlag.isSet(), }); }); @@ -166,17 +175,16 @@ router.use( router.use('/movie', isAuthenticated(), movieRoutes); router.use('/tv', isAuthenticated(), tvRoutes); router.use('/media', isAuthenticated(), mediaRoutes); +router.use('/book', isAuthenticated(), bookRoutes); +router.use('/author', isAuthenticated(), authorRoutes); +router.use('/series', isAuthenticated(), seriesRoutes); router.use('/person', isAuthenticated(), personRoutes); router.use('/collection', isAuthenticated(), collectionRoutes); router.use('/service', isAuthenticated(), serviceRoutes); router.use('/issue', isAuthenticated(), issueRoutes); router.use('/issueComment', isAuthenticated(), issueCommentRoutes); router.use('/auth', authRoutes); -router.use( - '/overrideRule', - isAuthenticated(Permission.ADMIN), - overrideRuleRoutes -); +router.use('/overrideRule', isAuthenticated(), overrideRuleRoutes); router.get('/regions', isAuthenticated(), async (req, res, next) => { const tmdb = new TheMovieDb(); diff --git a/server/routes/issue.test.ts b/server/routes/issue.test.ts new file mode 100644 index 0000000000..cd98770e7a --- /dev/null +++ b/server/routes/issue.test.ts @@ -0,0 +1,250 @@ +import assert from 'node:assert/strict'; +import { before, beforeEach, describe, it, mock } from 'node:test'; + +import { IssueType } from '@server/constants/issue'; +import { MediaStatus, MediaType } from '@server/constants/media'; +import { getRepository } from '@server/datasource'; +import Issue from '@server/entity/Issue'; +import Media from '@server/entity/Media'; +import { User } from '@server/entity/User'; +import { Permission } from '@server/lib/permissions'; +import { getSettings } from '@server/lib/settings'; +import { checkUser } from '@server/middleware/auth'; +import { IssueCommentSubscriber } from '@server/subscriber/IssueCommentSubscriber'; +import { IssueSubscriber } from '@server/subscriber/IssueSubscriber'; +import { setupTestDb } from '@server/test/db'; +import { + assertNoCredentials, + seedUserSettings, +} from '@server/test/userSettings'; +import type { Express } from 'express'; +import express from 'express'; +import session from 'express-session'; +import request from 'supertest'; +import authRoutes from './auth'; +import issueRoutes from './issue'; +import issueCommentRoutes from './issueComment'; + +const sendIssueNotificationMock = mock.method( + IssueSubscriber.prototype as unknown as { + sendIssueNotification: (...args: unknown[]) => Promise; + }, + 'sendIssueNotification', + async () => undefined +).mock; + +mock.method( + IssueCommentSubscriber.prototype as unknown as { + sendIssueCommentNotification: (...args: unknown[]) => Promise; + }, + 'sendIssueCommentNotification', + async () => undefined +); + +let app: Express; + +function createApp() { + const app = express(); + app.use(express.json()); + app.use( + session({ + secret: 'test-secret', + resave: false, + saveUninitialized: false, + }) + ); + app.use(checkUser); + app.use('/auth', authRoutes); + app.use('/issue', issueRoutes); + app.use('/issueComment', issueCommentRoutes); + app.use( + ( + err: { status?: number; message?: string }, + _req: express.Request, + res: express.Response, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _next: express.NextFunction + ) => { + res + .status(err.status ?? 500) + .json({ status: err.status ?? 500, message: err.message }); + } + ); + return app; +} + +before(async () => { + app = createApp(); +}); + +beforeEach(() => { + sendIssueNotificationMock.resetCalls(); +}); + +setupTestDb(); + +async function loginAs(email: string, password: string) { + const settings = getSettings(); + const priorLocalLogin = settings.main.localLogin; + settings.main.localLogin = true; + + try { + const agent = request.agent(app); + const res = await agent.post('/auth/local').send({ email, password }); + assert.strictEqual(res.status, 200); + return agent; + } finally { + settings.main.localLogin = priorLocalLogin; + } +} + +async function seedMedia() { + return getRepository(Media).save( + new Media({ + mediaType: MediaType.MOVIE, + tmdbId: 12345, + status: MediaStatus.AVAILABLE, + status4k: MediaStatus.UNKNOWN, + }) + ); +} + +describe('POST /issue', () => { + it('creates an issue on behalf of the supplied userId', async () => { + const issueRepo = getRepository(Issue); + const userRepo = getRepository(User); + const media = await seedMedia(); + const friend = await userRepo.findOneOrFail({ + where: { email: 'demo@seerr.dev' }, + }); + + const agent = await loginAs('admin@seerr.dev', 'test1234'); + const res = await agent.post('/issue').send({ + issueType: IssueType.VIDEO, + message: 'Playback stutters near the end.', + mediaId: media.id, + problemSeason: 0, + problemEpisode: 0, + userId: friend.id, + }); + + assert.strictEqual(res.status, 201); + assert.strictEqual(res.body.createdBy.email, 'demo@seerr.dev'); + assert.strictEqual(res.body.comments[0].user.email, 'demo@seerr.dev'); + + const persisted = await issueRepo.findOneOrFail({ + where: { id: res.body.id }, + }); + + assert.strictEqual(persisted.createdBy.id, friend.id); + assert.strictEqual(persisted.comments[0].user.id, friend.id); + }); + + it('defaults to the authenticated user when userId is omitted', async () => { + const media = await seedMedia(); + + const agent = await loginAs('admin@seerr.dev', 'test1234'); + const res = await agent.post('/issue').send({ + issueType: IssueType.AUDIO, + message: 'Audio is out of sync.', + mediaId: media.id, + }); + + assert.strictEqual(res.status, 201); + assert.strictEqual(res.body.createdBy.email, 'admin@seerr.dev'); + assert.strictEqual(res.body.comments[0].user.email, 'admin@seerr.dev'); + }); + + it('allows creators to supply their own userId', async () => { + const userRepo = getRepository(User); + const media = await seedMedia(); + const friend = await userRepo.findOneOrFail({ + where: { email: 'demo@seerr.dev' }, + }); + + friend.permissions = Permission.CREATE_ISSUES; + await userRepo.save(friend); + + const agent = await loginAs('demo@seerr.dev', 'test1234'); + const res = await agent.post('/issue').send({ + issueType: IssueType.SUBTITLES, + message: 'Subtitles are missing.', + mediaId: media.id, + userId: friend.id, + }); + + assert.strictEqual(res.status, 201); + assert.strictEqual(res.body.createdBy.email, 'demo@seerr.dev'); + assert.strictEqual(res.body.comments[0].user.email, 'demo@seerr.dev'); + }); + + it('prevents non-managers from supplying another userId', async () => { + const userRepo = getRepository(User); + const media = await seedMedia(); + const friend = await userRepo.findOneOrFail({ + where: { email: 'demo@seerr.dev' }, + }); + const admin = await userRepo.findOneOrFail({ + where: { email: 'admin@seerr.dev' }, + }); + + friend.permissions = Permission.CREATE_ISSUES; + await userRepo.save(friend); + + const agent = await loginAs('demo@seerr.dev', 'test1234'); + const res = await agent.post('/issue').send({ + issueType: IssueType.OTHER, + message: 'Something else is wrong.', + mediaId: media.id, + userId: admin.id, + }); + + assert.strictEqual(res.status, 403); + assert.strictEqual( + res.body.message, + 'You do not have permission to create an issue on behalf of another user.' + ); + }); + + it('returns 404 when the supplied userId does not exist', async () => { + const media = await seedMedia(); + + const agent = await loginAs('admin@seerr.dev', 'test1234'); + const res = await agent.post('/issue').send({ + issueType: IssueType.OTHER, + message: 'Something else is wrong.', + mediaId: media.id, + userId: 999999, + }); + + assert.strictEqual(res.status, 404); + assert.strictEqual(res.body.message, 'Issue user not found'); + }); +}); + +describe('GET /issueComment/:commentId', () => { + it('omits notification settings from the comment author', async () => { + const userRepo = getRepository(User); + const media = await seedMedia(); + const friend = await userRepo.findOneOrFail({ + where: { email: 'demo@seerr.dev' }, + }); + await seedUserSettings('demo@seerr.dev'); + + const agent = await loginAs('admin@seerr.dev', 'test1234'); + const created = await agent.post('/issue').send({ + issueType: IssueType.VIDEO, + message: 'Playback stutters near the end.', + mediaId: media.id, + userId: friend.id, + }); + assert.strictEqual(created.status, 201); + + const res = await agent.get(`/issueComment/${created.body.comments[0].id}`); + + assert.strictEqual(res.status, 200); + assert.strictEqual(res.body.user.email, 'demo@seerr.dev'); + assert.ok(!('settings' in res.body.user)); + assertNoCredentials(res.body); + }); +}); diff --git a/server/routes/issue.ts b/server/routes/issue.ts index 1e8769f8ec..8719c00716 100644 --- a/server/routes/issue.ts +++ b/server/routes/issue.ts @@ -3,7 +3,11 @@ import { getRepository } from '@server/datasource'; import Issue from '@server/entity/Issue'; import IssueComment from '@server/entity/IssueComment'; import Media from '@server/entity/Media'; -import type { IssueResultsResponse } from '@server/interfaces/api/issueInterfaces'; +import { User } from '@server/entity/User'; +import type { + IssueRequestBody, + IssueResultsResponse, +} from '@server/interfaces/api/issueInterfaces'; import { Permission } from '@server/lib/permissions'; import logger from '@server/logger'; import { isAuthenticated } from '@server/middleware/auth'; @@ -95,17 +99,7 @@ issueRoutes.get, IssueResultsResponse>( } ); -issueRoutes.post< - Record, - Issue, - { - message: string; - mediaId: number; - issueType: number; - problemSeason: number; - problemEpisode: number; - } ->( +issueRoutes.post, Issue, IssueRequestBody>( '/', isAuthenticated([Permission.MANAGE_ISSUES, Permission.CREATE_ISSUES], { type: 'or', @@ -118,6 +112,7 @@ issueRoutes.post< const issueRepository = getRepository(Issue); const mediaRepository = getRepository(Media); + const userRepository = getRepository(User); const media = await mediaRepository.findOne({ where: { id: req.body.mediaId }, @@ -127,15 +122,37 @@ issueRoutes.post< return next({ status: 404, message: 'Media does not exist.' }); } + let createdBy = req.user; + + if (req.body.userId != null && req.body.userId !== req.user.id) { + if (!req.user.hasPermission(Permission.MANAGE_ISSUES)) { + return next({ + status: 403, + message: + 'You do not have permission to create an issue on behalf of another user.', + }); + } + + const user = await userRepository.findOne({ + where: { id: req.body.userId }, + }); + + if (!user) { + return next({ status: 404, message: 'Issue user not found' }); + } + + createdBy = user; + } + const issue = new Issue({ - createdBy: req.user, + createdBy, issueType: req.body.issueType, problemSeason: req.body.problemSeason, problemEpisode: req.body.problemEpisode, media, comments: [ new IssueComment({ - user: req.user, + user: createdBy, message: req.body.message, }), ], @@ -143,7 +160,7 @@ issueRoutes.post< const newIssue = await issueRepository.save(issue); - return res.status(200).json(newIssue); + return res.status(201).json(newIssue); } ); diff --git a/server/routes/media.ts b/server/routes/media.ts index de3c1b6dc3..3ddfd15d5c 100644 --- a/server/routes/media.ts +++ b/server/routes/media.ts @@ -1,4 +1,5 @@ import RadarrAPI from '@server/api/servarr/radarr'; +import ReadarrAPI from '@server/api/servarr/readarr'; import SonarrAPI from '@server/api/servarr/sonarr'; import TautulliAPI from '@server/api/tautulli'; import TheMovieDb from '@server/api/themoviedb'; @@ -17,10 +18,40 @@ import logger from '@server/logger'; import { isAuthenticated } from '@server/middleware/auth'; import { Router } from 'express'; import type { FindOneOptions } from 'typeorm'; -import { In, IsNull, Not } from 'typeorm'; +import { EntityNotFoundError, In, IsNull, Not } from 'typeorm'; const mediaRoutes = Router(); +mediaRoutes.get( + '/lookup/:id', + isAuthenticated(Permission.MANAGE_REQUESTS), + async (req, res, next) => { + const mediaType = req.query.mediaType; + if ( + mediaType !== MediaType.MOVIE && + mediaType !== MediaType.TV && + mediaType !== MediaType.BOOK + ) { + return next({ + status: 400, + message: 'Invalid or missing mediaType query parameter.', + }); + } + + const mediaRepository = getRepository(Media); + + try { + const media = await mediaRepository.findOne({ + where: { tmdbId: Number(req.params.id), mediaType }, + }); + + return res.status(200).json({ id: media?.id || null }); + } catch (e) { + next({ status: 500, message: e.message }); + } + } +); + mediaRoutes.get('/', async (req, res, next) => { const mediaRepository = getRepository(Media); @@ -68,10 +99,14 @@ mediaRoutes.get('/', async (req, res, next) => { let whereClause: FindOneOptions['where']; if (statusFilter || req.query.sort === 'mediaAdded') { - whereClause = {}; - if (statusFilter) whereClause.status = statusFilter; - if (req.query.sort === 'mediaAdded') - whereClause.mediaAddedAt = Not(IsNull()); + const mediaAddedClause = + req.query.sort === 'mediaAdded' ? { mediaAddedAt: Not(IsNull()) } : {}; + whereClause = statusFilter + ? [ + { ...mediaAddedClause, status: statusFilter }, + { ...mediaAddedClause, status4k: statusFilter }, + ] + : mediaAddedClause; } try { @@ -187,11 +222,15 @@ mediaRoutes.delete( return res.status(204).send(); } catch (e) { - logger.error('Something went wrong fetching media in delete request', { + if (e instanceof EntityNotFoundError) { + return res.status(204).send(); + } + logger.error('Something went wrong deleting media', { label: 'Media', + mediaId: req.params.id, message: e.message, }); - next({ status: 404, message: 'Media not found' }); + next({ status: 500, message: 'Failed to delete media' }); } } ); @@ -209,12 +248,17 @@ mediaRoutes.delete( const is4k = String(req.query.is4k) === 'true'; const isMovie = media.mediaType === MediaType.MOVIE; + const isBook = media.mediaType === MediaType.BOOK; let serviceSettings; if (isMovie) { serviceSettings = settings.radarr.find( (radarr) => radarr.isDefault && radarr.is4k === is4k ); + } else if (isBook) { + serviceSettings = settings.readarr.find( + (readarr) => readarr.isDefault && readarr.is4k === is4k + ); } else { serviceSettings = settings.sonarr.find( (sonarr) => sonarr.isDefault && sonarr.is4k === is4k @@ -231,6 +275,10 @@ mediaRoutes.delete( serviceSettings = settings.radarr.find( (radarr) => radarr.id === specificServiceId ); + } else if (isBook) { + serviceSettings = settings.readarr.find( + (readarr) => readarr.id === specificServiceId + ); } else { serviceSettings = settings.sonarr.find( (sonarr) => sonarr.id === specificServiceId @@ -238,19 +286,22 @@ mediaRoutes.delete( } } + const serviceName = isMovie ? 'Radarr' : isBook ? 'Readarr' : 'Sonarr'; + const serviceType = is4k ? (isBook ? 'Audiobook' : '4K ') : ''; + if (!serviceSettings) { - logger.warn( - `There is no default ${ - is4k ? '4K ' : '' + isMovie ? 'Radarr' : 'Sonarr' - }/ server configured. Did you set any of your ${ - is4k ? '4K ' : '' + isMovie ? 'Radarr' : 'Sonarr' - } servers as default?`, + const arrName = serviceType + serviceName; + logger.info( + `There is no default ${arrName} server configured. Did you set any of your ${arrName} servers as default?`, { label: 'Media Request', mediaId: media.id, } ); - return; + return next({ + status: 409, + message: `No ${arrName} server configured to delete media files`, + }); } let service; @@ -259,6 +310,11 @@ mediaRoutes.delete( apiKey: serviceSettings?.apiKey, url: RadarrAPI.buildUrl(serviceSettings, '/api/v3'), }); + } else if (isBook) { + service = new ReadarrAPI({ + apiKey: serviceSettings?.apiKey, + url: ReadarrAPI.buildUrl(serviceSettings, '/api/v1'), + }); } else { service = new SonarrAPI({ apiKey: serviceSettings?.apiKey, @@ -268,6 +324,8 @@ mediaRoutes.delete( if (isMovie) { await (service as RadarrAPI).removeMovie(media.tmdbId); + } else if (isBook) { + await (service as ReadarrAPI).removeBook(media.tmdbId); } else { const tmdb = new TheMovieDb(); const series = await tmdb.getTvShow({ tvId: media.tmdbId }); @@ -276,15 +334,27 @@ mediaRoutes.delete( throw new Error('TVDB ID not found'); } await (service as SonarrAPI).removeSeries(tvdbId); + + for (const season of media.seasons) { + season[is4k ? 'status4k' : 'status'] = MediaStatus.DELETED; + } } + media[is4k ? 'status4k' : 'status'] = MediaStatus.DELETED; + media.resetServiceData(is4k); + await mediaRepository.save(media); + return res.status(204).send(); } catch (e) { - logger.error('Something went wrong fetching media in delete request', { + if (e instanceof EntityNotFoundError) { + return next({ status: 404, message: 'Media not found' }); + } + logger.error('Something went wrong deleting media file', { label: 'Media', + mediaId: req.params.id, message: e.message, }); - next({ status: 404, message: 'Media not found' }); + next({ status: 500, message: 'Failed to delete media file' }); } } ); diff --git a/server/routes/mediaInfo.test.ts b/server/routes/mediaInfo.test.ts new file mode 100644 index 0000000000..1c42456e19 --- /dev/null +++ b/server/routes/mediaInfo.test.ts @@ -0,0 +1,273 @@ +import assert from 'node:assert/strict'; +import { before, describe, it } from 'node:test'; + +import TheMovieDb from '@server/api/themoviedb'; +import type { + TmdbMovieDetails, + TmdbTvDetails, +} from '@server/api/themoviedb/interfaces'; +import { IssueType } from '@server/constants/issue'; +import { + MediaRequestStatus, + MediaStatus, + MediaType, +} from '@server/constants/media'; +import { getRepository } from '@server/datasource'; +import Issue from '@server/entity/Issue'; +import IssueComment from '@server/entity/IssueComment'; +import Media from '@server/entity/Media'; +import { MediaRequest } from '@server/entity/MediaRequest'; +import { User } from '@server/entity/User'; +import { Permission } from '@server/lib/permissions'; +import { getSettings } from '@server/lib/settings'; +import { checkUser, isAuthenticated } from '@server/middleware/auth'; +import { setupTestDb } from '@server/test/db'; +import type { Express } from 'express'; +import express from 'express'; +import session from 'express-session'; +import request from 'supertest'; +import authRoutes from './auth'; +import movieRoutes from './movie'; +import tvRoutes from './tv'; + +const OWNER_EMAIL = 'owner@seerr.dev'; +const OWNER_PLEX_ID = 918273; +const COMMENT_BODY = 'Only visible to issue viewers'; + +Object.defineProperty(TheMovieDb.prototype, 'getMovie', { + get() { + return async ({ movieId }: { movieId: number }) => + ({ + id: movieId, + genres: [], + original_language: 'en', + overview: 'overview', + keywords: { keywords: [] }, + external_ids: {}, + release_dates: { results: [] }, + credits: { cast: [], crew: [] }, + production_companies: [], + production_countries: [], + spoken_languages: [], + videos: { results: [] }, + }) as unknown as TmdbMovieDetails; + }, + set() {}, + configurable: true, +}); + +Object.defineProperty(TheMovieDb.prototype, 'getTvShow', { + get() { + return async ({ tvId }: { tvId: number }) => + ({ + id: tvId, + genres: [], + original_language: 'en', + overview: 'overview', + keywords: { results: [] }, + external_ids: {}, + content_ratings: { results: [] }, + credits: { cast: [], crew: [] }, + aggregate_credits: { cast: [], crew: [] }, + seasons: [], + networks: [], + production_companies: [], + production_countries: [], + spoken_languages: [], + origin_country: [], + episode_run_time: [], + created_by: [], + videos: { results: [] }, + }) as unknown as TmdbTvDetails; + }, + set() {}, + configurable: true, +}); + +let app: Express; + +function createApp() { + const app = express(); + app.use(express.json()); + app.use( + session({ + secret: 'test-secret', + resave: false, + saveUninitialized: false, + }) + ); + app.use(checkUser); + app.use('/auth', authRoutes); + app.use('/movie', isAuthenticated(), movieRoutes); + app.use('/tv', isAuthenticated(), tvRoutes); + app.use( + ( + err: { status?: number; message?: string }, + _req: express.Request, + res: express.Response, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _next: express.NextFunction + ) => { + res + .status(err.status ?? 500) + .json({ status: err.status ?? 500, message: err.message }); + } + ); + return app; +} + +before(async () => { + app = createApp(); +}); + +setupTestDb(); + +async function loginAs(email: string, password: string) { + const settings = getSettings(); + const priorLocalLogin = settings.main.localLogin; + settings.main.localLogin = true; + + try { + const agent = request.agent(app); + const res = await agent.post('/auth/local').send({ email, password }); + assert.strictEqual(res.status, 200); + return agent; + } finally { + settings.main.localLogin = priorLocalLogin; + } +} + +async function setViewerPermissions(permissions: number) { + const userRepository = getRepository(User); + const viewer = await userRepository.findOneOrFail({ + where: { email: 'demo@seerr.dev' }, + }); + viewer.permissions = permissions; + await userRepository.save(viewer); + return viewer; +} + +async function seedMedia(mediaType: MediaType, tmdbId: number) { + const userRepository = getRepository(User); + const owner = await userRepository.findOneOrFail({ where: { id: 1 } }); + owner.email = OWNER_EMAIL; + owner.plexId = OWNER_PLEX_ID; + await userRepository.save(owner); + + const media = await getRepository(Media).save( + new Media({ + mediaType, + tmdbId, + status: MediaStatus.AVAILABLE, + status4k: MediaStatus.UNKNOWN, + }) + ); + + await getRepository(MediaRequest).save( + new MediaRequest({ + type: mediaType, + media, + requestedBy: owner, + modifiedBy: owner, + status: MediaRequestStatus.APPROVED, + is4k: false, + }) + ); + + const issue = await getRepository(Issue).save( + new Issue({ + issueType: IssueType.VIDEO, + media, + createdBy: owner, + problemSeason: 2, + problemEpisode: 5, + }) + ); + + await getRepository(IssueComment).save( + new IssueComment({ user: owner, issue, message: COMMENT_BODY }) + ); + + return { media, owner }; +} + +const routes = [ + { path: '/movie', tmdbId: 12345, mediaType: MediaType.MOVIE }, + { path: '/tv', tmdbId: 67890, mediaType: MediaType.TV }, +]; + +for (const { path, tmdbId, mediaType } of routes) { + describe(`GET ${path}/:id mediaInfo`, () => { + it('omits the requesting user’s email and plexId', async () => { + await seedMedia(mediaType, tmdbId); + await setViewerPermissions(Permission.REQUEST); + + const agent = await loginAs('demo@seerr.dev', 'test1234'); + const res = await agent.get(`${path}/${tmdbId}`); + + assert.strictEqual(res.status, 200); + assert.strictEqual(res.body.mediaInfo.requests.length, 1); + + const serialized = JSON.stringify(res.body); + assert.ok(!serialized.includes(OWNER_EMAIL), 'email was serialized'); + assert.ok( + !serialized.includes(String(OWNER_PLEX_ID)), + 'plexId was serialized' + ); + }); + + it('keeps the request fields the UI renders', async () => { + await seedMedia(mediaType, tmdbId); + await setViewerPermissions(Permission.REQUEST); + + const agent = await loginAs('demo@seerr.dev', 'test1234'); + const res = await agent.get(`${path}/${tmdbId}`); + + const { requestedBy } = res.body.mediaInfo.requests[0]; + assert.strictEqual(requestedBy.id, 1); + assert.strictEqual(typeof requestedBy.displayName, 'string'); + assert.ok('avatar' in requestedBy); + }); + + it('hides issues from a viewer without an issue permission', async () => { + await seedMedia(mediaType, tmdbId); + await setViewerPermissions(Permission.REQUEST); + + const agent = await loginAs('demo@seerr.dev', 'test1234'); + const res = await agent.get(`${path}/${tmdbId}`); + + assert.deepStrictEqual(res.body.mediaInfo.issues, []); + assert.ok(!JSON.stringify(res.body).includes(COMMENT_BODY)); + }); + + for (const permission of [ + Permission.MANAGE_ISSUES, + Permission.VIEW_ISSUES, + Permission.CREATE_ISSUES, + ]) { + it(`returns issues without contents to a viewer with ${Permission[permission]}`, async () => { + await seedMedia(mediaType, tmdbId); + await setViewerPermissions(Permission.REQUEST | permission); + + const agent = await loginAs('demo@seerr.dev', 'test1234'); + const res = await agent.get(`${path}/${tmdbId}`); + + assert.strictEqual(res.body.mediaInfo.issues.length, 1); + + const [issue] = res.body.mediaInfo.issues; + assert.strictEqual(issue.issueType, IssueType.VIDEO); + assert.ok(!('comments' in issue), 'comments were serialized'); + assert.ok(!('problemSeason' in issue), 'problemSeason was serialized'); + assert.ok( + !('problemEpisode' in issue), + 'problemEpisode was serialized' + ); + + const serialized = JSON.stringify(res.body); + assert.ok(!serialized.includes(COMMENT_BODY)); + assert.ok(!serialized.includes(OWNER_EMAIL)); + assert.ok(!serialized.includes(String(OWNER_PLEX_ID))); + }); + } + }); +} diff --git a/server/routes/movie.ts b/server/routes/movie.ts index bd86e447a9..affd2d54e1 100644 --- a/server/routes/movie.ts +++ b/server/routes/movie.ts @@ -34,7 +34,11 @@ movieRoutes.get('/:id', async (req, res, next) => { }, }); - const data = mapMovieDetails(tmdbMovie, media, onUserWatchlist); + const data = mapMovieDetails( + tmdbMovie, + media?.filter(req.user), + onUserWatchlist + ); // TMDB issue where it doesnt fallback to English when no overview is available in requested locale. if (!data.overview) { @@ -71,7 +75,8 @@ movieRoutes.get('/:id/recommendations', async (req, res, next) => { results.results.map((result) => ({ tmdbId: result.id, mediaType: MediaType.MOVIE, - })) + })), + { includeActiveRequest: true } ); return res.status(200).json({ @@ -116,7 +121,8 @@ movieRoutes.get('/:id/similar', async (req, res, next) => { results.results.map((result) => ({ tmdbId: result.id, mediaType: MediaType.MOVIE, - })) + })), + { includeActiveRequest: true } ); return res.status(200).json({ diff --git a/server/routes/overrideRule.ts b/server/routes/overrideRule.ts index 912a68aae6..49a550facb 100644 --- a/server/routes/overrideRule.ts +++ b/server/routes/overrideRule.ts @@ -1,7 +1,19 @@ +import TheMovieDb from '@server/api/themoviedb'; +import type { + TmdbMovieDetails, + TmdbTvDetails, +} from '@server/api/themoviedb/interfaces'; +import { MediaType } from '@server/constants/media'; import { getRepository } from '@server/datasource'; +import { MediaRequest } from '@server/entity/MediaRequest'; import OverrideRule from '@server/entity/OverrideRule'; +import { User } from '@server/entity/User'; import type { OverrideRuleResultsResponse } from '@server/interfaces/api/overrideRuleInterfaces'; +import overrideRules, { + type OverrideRulesResult, +} from '@server/lib/overrideRules'; import { Permission } from '@server/lib/permissions'; +import logger from '@server/logger'; import { isAuthenticated } from '@server/middleware/auth'; import { Router } from 'express'; @@ -32,10 +44,12 @@ overrideRuleRoutes.post< language?: string; keywords?: string; profileId?: number; + metadataProfileId?: number; rootFolder?: string; tags?: string; radarrServiceId?: number; sonarrServiceId?: number; + readarrServiceId?: number; } >('/', isAuthenticated(Permission.ADMIN), async (req, res, next) => { const overrideRuleRepository = getRepository(OverrideRule); @@ -47,10 +61,12 @@ overrideRuleRoutes.post< language: req.body.language, keywords: req.body.keywords, profileId: req.body.profileId, + metadataProfileId: req.body.metadataProfileId, rootFolder: req.body.rootFolder, tags: req.body.tags, radarrServiceId: req.body.radarrServiceId, sonarrServiceId: req.body.sonarrServiceId, + readarrServiceId: req.body.readarrServiceId, }); const newRule = await overrideRuleRepository.save(rule); @@ -61,6 +77,127 @@ overrideRuleRoutes.post< } }); +overrideRuleRoutes.post< + Record, + OverrideRulesResult, + { + mediaType: MediaType; + is4k: boolean; + tmdbId: number; + requestUser: number; + requestId: number | null; + tags?: number[] | null; + serviceId?: number; + } +>( + '/advancedRequest', + isAuthenticated([Permission.REQUEST_ADVANCED, Permission.MANAGE_REQUESTS], { + type: 'or', + }), + async (req, res, next) => { + try { + const tmdb = new TheMovieDb(); + let tmdbMedia: TmdbMovieDetails | TmdbTvDetails; + try { + tmdbMedia = + req.body.mediaType === MediaType.MOVIE + ? await tmdb.getMovie({ movieId: req.body.tmdbId }) + : await tmdb.getTvShow({ tvId: req.body.tmdbId }); + } catch { + return next({ status: 404, message: 'Media not found' }); + } + + const userRepository = getRepository(User); + const requestRepository = getRepository(MediaRequest); + + let ruleUser: User | null = req.user || null; + + if (req.body.requestId) { + const request = await requestRepository.findOne({ + where: { + id: Number(req.body.requestId), + }, + }); + + if (!request) { + return next({ status: 404, message: 'Request not found.' }); + } + + if ( + (request.requestedBy.id !== req.user?.id || + (req.body.mediaType !== MediaType.TV && + !req.user?.hasPermission(Permission.REQUEST_ADVANCED))) && + !req.user?.hasPermission(Permission.MANAGE_REQUESTS) + ) { + return next({ + status: 403, + message: 'You do not have permission to modify this request.', + }); + } + + ruleUser = request.requestedBy; + + if ( + req.body.requestUser && + req.body.requestUser !== request.requestedBy.id && + !req.user?.hasPermission([ + Permission.MANAGE_USERS, + Permission.MANAGE_REQUESTS, + ]) + ) { + return next({ + status: 403, + message: 'You do not have permission to modify the request user.', + }); + } else if (req.body.requestUser) { + ruleUser = await userRepository.findOneOrFail({ + where: { id: req.body.requestUser }, + }); + } + } else if (req.body.requestUser !== req.user?.id) { + if ( + req.body.requestUser && + !req.user?.hasPermission([ + Permission.MANAGE_USERS, + Permission.MANAGE_REQUESTS, + ]) + ) { + return next({ + status: 403, + message: 'You do not have permission to modify the request user.', + }); + } else if (req.body.requestUser) { + ruleUser = await userRepository.findOneOrFail({ + where: { id: req.body.requestUser }, + }); + } + } + + if (!ruleUser) { + return next({ status: 404, message: 'User not found.' }); + } + + const overrideRulesResult: OverrideRulesResult = await overrideRules({ + mediaType: req.body.mediaType, + is4k: req.body.is4k, + tmdbMedia, + requestUser: ruleUser, + tags: req.body.tags, + serviceId: req.body.serviceId, + }); + + res.status(200).json(overrideRulesResult); + } catch (e) { + logger.error('Something went wrong evaluating override rules', { + label: 'API', + errorMessage: e.message, + tmdbId: req.body.tmdbId, + }); + next({ status: 500, message: 'Unable to evaluate override rules.' }); + } + } +); + overrideRuleRoutes.put< { ruleId: string }, OverrideRule, @@ -70,10 +207,12 @@ overrideRuleRoutes.put< language?: string; keywords?: string; profileId?: number; + metadataProfileId?: number; rootFolder?: string; tags?: string; radarrServiceId?: number; sonarrServiceId?: number; + readarrServiceId?: number; } >('/:ruleId', isAuthenticated(Permission.ADMIN), async (req, res, next) => { const overrideRuleRepository = getRepository(OverrideRule); @@ -94,10 +233,12 @@ overrideRuleRoutes.put< rule.language = req.body.language; rule.keywords = req.body.keywords; rule.profileId = req.body.profileId; + rule.metadataProfileId = req.body.metadataProfileId; rule.rootFolder = req.body.rootFolder; rule.tags = req.body.tags; rule.radarrServiceId = req.body.radarrServiceId; rule.sonarrServiceId = req.body.sonarrServiceId; + rule.readarrServiceId = req.body.readarrServiceId; const newRule = await overrideRuleRepository.save(rule); diff --git a/server/routes/request.test.ts b/server/routes/request.test.ts index 2b45127dd3..6e0f27dc24 100644 --- a/server/routes/request.test.ts +++ b/server/routes/request.test.ts @@ -1,6 +1,11 @@ import assert from 'node:assert/strict'; import { before, beforeEach, describe, it, mock } from 'node:test'; +import TheMovieDb from '@server/api/themoviedb'; +import type { + TmdbMovieDetails, + TmdbTvDetails, +} from '@server/api/themoviedb/interfaces'; import { MediaRequestStatus, MediaStatus, @@ -9,10 +14,20 @@ import { import { getRepository } from '@server/datasource'; import Media from '@server/entity/Media'; import { MediaRequest } from '@server/entity/MediaRequest'; +import OverrideRule from '@server/entity/OverrideRule'; +import Season from '@server/entity/Season'; +import SeasonRequest from '@server/entity/SeasonRequest'; import { User } from '@server/entity/User'; +import { Permission } from '@server/lib/permissions'; +import type { RadarrSettings, SonarrSettings } from '@server/lib/settings'; import { getSettings } from '@server/lib/settings'; import { checkUser } from '@server/middleware/auth'; +import { MediaRequestSubscriber } from '@server/subscriber/MediaRequestSubscriber'; import { setupTestDb } from '@server/test/db'; +import { + assertNoCredentials, + seedUserSettings, +} from '@server/test/userSettings'; import type { Express } from 'express'; import express from 'express'; import session from 'express-session'; @@ -26,6 +41,102 @@ const sendNotificationMock = mock.method( async () => undefined ).mock; +const getMovieImpl: (args: { + movieId: number; + language?: string; +}) => Promise = async ({ movieId }) => fakeTmdbMovie(movieId); + +Object.defineProperty(TheMovieDb.prototype, 'getMovie', { + get() { + return async (args: { movieId: number; language?: string }) => + getMovieImpl(args); + }, + set() {}, + configurable: true, +}); + +const getTvShowImpl: (args: { + tvId: number; + language?: string; +}) => Promise = async ({ tvId }) => fakeTmdbShow(tvId); + +Object.defineProperty(TheMovieDb.prototype, 'getTvShow', { + get() { + return async (args: { tvId: number; language?: string }) => + getTvShowImpl(args); + }, + set() {}, + configurable: true, +}); + +function fakeTmdbMovie(tmdbId: number): TmdbMovieDetails { + return { + id: tmdbId, + genres: [], + original_language: 'en', + keywords: { keywords: [] }, + external_ids: {}, + } as unknown as TmdbMovieDetails; +} + +function fakeTmdbShow(tmdbId: number): TmdbTvDetails { + return { + id: tmdbId, + genres: [], + original_language: 'en', + keywords: { results: [] }, + external_ids: {}, + } as unknown as TmdbTvDetails; +} + +function configureRadarr(overrides: Partial[]): void { + const settings = getSettings(); + settings.radarr = overrides.map((o, i) => ({ + id: i, + name: `Radarr ${i}`, + hostname: 'localhost', + port: 7878, + apiKey: 'test-key', + baseUrl: '', + useSsl: false, + activeProfileId: 1, + activeDirectory: '/movies', + is4k: false, + minimumAvailability: 'released', + tags: [], + isDefault: i === 0, + syncEnabled: true, + preventSearch: false, + externalUrl: '', + ...o, + })) as RadarrSettings[]; +} + +function configureSonarr(overrides: Partial[]): void { + const settings = getSettings(); + settings.sonarr = overrides.map((o, i) => ({ + id: i, + name: `Sonarr ${i}`, + hostname: 'localhost', + port: 8989, + apiKey: 'test-key', + baseUrl: '', + useSsl: false, + activeProfileId: 1, + activeDirectory: '/tv', + activeLanguageProfileId: 1, + animeTags: [], + is4k: false, + enableSeasonFolders: true, + tags: [], + isDefault: i === 0, + syncEnabled: true, + preventSearch: false, + externalUrl: '', + ...o, + })) as SonarrSettings[]; +} + let app: Express; function createApp() { @@ -88,7 +199,7 @@ async function seedRequest(status = MediaRequestStatus.PENDING) { const requestRepo = getRepository(MediaRequest); const requestedBy = await userRepo.findOneOrFail({ - where: { email: 'friend@seerr.dev' }, + where: { email: 'demo@seerr.dev' }, }); const media = await mediaRepo.save( @@ -121,7 +232,7 @@ describe('DELETE /request/:requestId', () => { it('allows the owner to delete their own pending request', async () => { const mediaRequest = await seedRequest(); - const agent = await loginAs('friend@seerr.dev', 'test1234'); + const agent = await loginAs('demo@seerr.dev', 'test1234'); const res = await agent.delete(`/request/${mediaRequest.id}`); assert.strictEqual(res.status, 204); @@ -165,7 +276,7 @@ describe('DELETE /request/:requestId', () => { }) ); - const agent = await loginAs('friend@seerr.dev', 'test1234'); + const agent = await loginAs('demo@seerr.dev', 'test1234'); const res = await agent.delete(`/request/${mediaRequest.id}`); assert.strictEqual(res.status, 401); @@ -174,7 +285,7 @@ describe('DELETE /request/:requestId', () => { it('prevents the owner from deleting an approved request', async () => { const mediaRequest = await seedRequest(MediaRequestStatus.APPROVED); - const agent = await loginAs('friend@seerr.dev', 'test1234'); + const agent = await loginAs('demo@seerr.dev', 'test1234'); const res = await agent.delete(`/request/${mediaRequest.id}`); assert.strictEqual(res.status, 401); @@ -186,6 +297,20 @@ describe('DELETE /request/:requestId', () => { assert.strictEqual(res.status, 404); }); + + it('deletes a request once when two deletes race', async () => { + const repo = getRepository(MediaRequest); + const mediaRequest = await seedRequest(); + const admin = await loginAs('admin@seerr.dev', 'test1234'); + + const results = await Promise.all([ + admin.delete(`/request/${mediaRequest.id}`), + admin.delete(`/request/${mediaRequest.id}`), + ]); + + assert.deepStrictEqual(results.map((r) => r.status).sort(), [204, 404]); + assert.strictEqual(await repo.count({ where: { id: mediaRequest.id } }), 0); + }); }); describe('PUT /request/:requestId (movie)', () => { @@ -211,57 +336,1295 @@ describe('PUT /request/:requestId (movie)', () => { assert.strictEqual(saved.profileId, 7); assert.strictEqual(saved.rootFolder, '/updated/movies'); }); + + it('refuses to modify a request that is no longer pending', async () => { + const requestRepo = getRepository(MediaRequest); + const mediaRequest = await seedRequest(MediaRequestStatus.APPROVED); + + const agent = await loginAs('admin@seerr.dev', 'test1234'); + const res = await agent.put(`/request/${mediaRequest.id}`).send({ + mediaType: MediaType.MOVIE, + serverId: 3, + rootFolder: '/updated/movies', + }); + + assert.strictEqual(res.status, 409); + assert.match(res.body.message, /pending/i); + + const saved = await requestRepo.findOneOrFail({ + where: { id: mediaRequest.id }, + }); + assert.strictEqual(saved.serverId, null); + assert.strictEqual(saved.rootFolder, null); + }); }); -describe('POST /request/:requestId/:status', () => { - const cases = [ - { action: 'approve', expected: MediaRequestStatus.APPROVED }, - { action: 'decline', expected: MediaRequestStatus.DECLINED }, - ] as const; +async function seedUser( + email: string, + quotas: { + movieQuotaLimit?: number; + tvQuotaLimit?: number; + tvQuotaDays?: number; + } = {} +) { + const userRepo = getRepository(User); + const user = await userRepo.findOneOrFail({ where: { email } }); + Object.assign(user, quotas); - for (const { action, expected } of cases) { - it(`transitions to ${action}d and records the acting user`, async () => { - const repo = getRepository(MediaRequest); - const pending = await seedRequest(); - const admin = await loginAs('admin@seerr.dev', 'test1234'); + return userRepo.save(user); +} - const res = await admin.post(`/request/${pending.id}/${action}`); +async function seedTvMedia(tmdbId: number) { + const mediaRepo = getRepository(Media); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.body.status, expected); - assert.strictEqual(res.body.modifiedBy.email, 'admin@seerr.dev'); + return ( + (await mediaRepo.findOne({ + where: { tmdbId, mediaType: MediaType.TV }, + })) ?? + (await mediaRepo.save( + new Media({ + mediaType: MediaType.TV, + tmdbId, + status: MediaStatus.PENDING, + status4k: MediaStatus.UNKNOWN, + }) + )) + ); +} - const persisted = await repo.findOneOrFail({ - where: { id: pending.id }, - relations: { modifiedBy: true }, - }); +async function seedMediaSeasons( + tmdbId: number, + seasons: { seasonNumber: number; status: MediaStatus }[] +) { + const media = await seedTvMedia(tmdbId); + media.seasons = seasons.map( + ({ seasonNumber, status }) => + new Season({ seasonNumber, status, status4k: MediaStatus.UNKNOWN }) + ); - assert.strictEqual(persisted.status, expected); - assert.strictEqual(persisted.modifiedBy?.email, 'admin@seerr.dev'); - assert.ok(persisted.updatedAt > pending.updatedAt); + return getRepository(Media).save(media); +} + +async function seedTvRequest( + requestedBy: User, + seasons: number[], + { tmdbId = 67890, ignoreQuota = false, createdAt = new Date() } = {} +) { + return getRepository(MediaRequest).save( + new MediaRequest({ + type: MediaType.TV, + status: MediaRequestStatus.PENDING, + media: await seedTvMedia(tmdbId), + requestedBy, + is4k: false, + ignoreQuota, + createdAt, + seasons: seasons.map( + (seasonNumber) => + new SeasonRequest({ + seasonNumber, + status: MediaRequestStatus.PENDING, + }) + ), + }) + ); +} + +describe('PUT /request/:requestId (tv)', () => { + it('does not add a season held by another request', async () => { + const requestRepo = getRepository(MediaRequest); + + const owner = await seedUser('admin@seerr.dev'); + const otherUser = await seedUser('demo@seerr.dev'); + + const mediaRequest = await seedTvRequest(owner, [1, 2]); + const otherRequest = await seedTvRequest(otherUser, [3]); + + const agent = await loginAs('admin@seerr.dev', 'test1234'); + const res = await agent.put(`/request/${mediaRequest.id}`).send({ + mediaType: MediaType.TV, + seasons: [1, 2, 3], }); - } -}); -describe('POST /request/:requestId/retry', () => { - it('re-approves a failed request and records the acting user', async () => { - const repo = getRepository(MediaRequest); - const failed = await seedRequest(MediaRequestStatus.FAILED); + assert.strictEqual(res.status, 200); + + const saved = await requestRepo.findOneOrFail({ + where: { id: mediaRequest.id }, + }); + assert.deepStrictEqual( + saved.seasons.map((s) => s.seasonNumber).sort((a, b) => a - b), + [1, 2] + ); + + const otherSaved = await requestRepo.findOneOrFail({ + where: { id: otherRequest.id }, + }); + assert.deepStrictEqual( + otherSaved.seasons.map((s) => s.seasonNumber), + [3] + ); + }); + + it('gives a season to only one of two concurrent edits', async () => { + const requestRepo = getRepository(MediaRequest); + + const owner = await seedUser('admin@seerr.dev'); + const otherUser = await seedUser('demo@seerr.dev'); + + const mediaRequest = await seedTvRequest(owner, [1]); + const otherRequest = await seedTvRequest(otherUser, [3]); + const admin = await loginAs('admin@seerr.dev', 'test1234'); + const friend = await loginAs('demo@seerr.dev', 'test1234'); - const res = await admin.post(`/request/${failed.id}/retry`); + const [adminRes, friendRes] = await Promise.all([ + admin + .put(`/request/${mediaRequest.id}`) + .send({ mediaType: MediaType.TV, seasons: [1, 2] }), + friend + .put(`/request/${otherRequest.id}`) + .send({ mediaType: MediaType.TV, seasons: [3, 2] }), + ]); + + assert.strictEqual(adminRes.status, 200); + assert.strictEqual(friendRes.status, 200); + + const saved = await requestRepo.findOneOrFail({ + where: { id: mediaRequest.id }, + }); + const otherSaved = await requestRepo.findOneOrFail({ + where: { id: otherRequest.id }, + }); + + const holders = [saved, otherSaved].filter((r) => + r.seasons.some((s) => s.seasonNumber === 2) + ); + assert.strictEqual(holders.length, 1); + + assert.deepStrictEqual( + [...saved.seasons, ...otherSaved.seasons] + .map((s) => s.seasonNumber) + .sort((a, b) => a - b), + [1, 2, 3] + ); + }); +}); + +describe('PUT /request/:requestId (season availability)', () => { + it('does not add a season the media already has', async () => { + const requestRepo = getRepository(MediaRequest); + const owner = await seedUser('demo@seerr.dev'); + const mediaRequest = await seedTvRequest(owner, [1]); + await seedMediaSeasons(67890, [ + { seasonNumber: 2, status: MediaStatus.AVAILABLE }, + ]); + + const agent = await loginAs('demo@seerr.dev', 'test1234'); + const res = await agent.put(`/request/${mediaRequest.id}`).send({ + mediaType: MediaType.TV, + seasons: [1, 2, 3], + }); assert.strictEqual(res.status, 200); - assert.strictEqual(res.body.status, MediaRequestStatus.APPROVED); - assert.strictEqual(res.body.modifiedBy.email, 'admin@seerr.dev'); - const persisted = await repo.findOneOrFail({ - where: { id: failed.id }, - relations: { modifiedBy: true }, + const saved = await requestRepo.findOneOrFail({ + where: { id: mediaRequest.id }, }); + assert.deepStrictEqual( + saved.seasons.map((s) => s.seasonNumber).sort((a, b) => a - b), + [1, 3] + ); + }); - assert.strictEqual(persisted.status, MediaRequestStatus.APPROVED); - assert.strictEqual(persisted.modifiedBy?.email, 'admin@seerr.dev'); - assert.ok(persisted.updatedAt > failed.updatedAt); + it('returns 202 when every requested season is already covered', async () => { + const owner = await seedUser('demo@seerr.dev'); + const mediaRequest = await seedTvRequest(owner, [1]); + await seedMediaSeasons(67890, [ + { seasonNumber: 2, status: MediaStatus.AVAILABLE }, + ]); + + const agent = await loginAs('demo@seerr.dev', 'test1234'); + const res = await agent.put(`/request/${mediaRequest.id}`).send({ + mediaType: MediaType.TV, + seasons: [2], + }); + + assert.strictEqual(res.status, 202); + }); + + it('keeps the seasons it already holds once they are available', async () => { + const requestRepo = getRepository(MediaRequest); + const owner = await seedUser('demo@seerr.dev'); + const mediaRequest = await seedTvRequest(owner, [1, 2]); + await seedMediaSeasons(67890, [ + { seasonNumber: 1, status: MediaStatus.AVAILABLE }, + { seasonNumber: 2, status: MediaStatus.PROCESSING }, + ]); + + const agent = await loginAs('demo@seerr.dev', 'test1234'); + const res = await agent.put(`/request/${mediaRequest.id}`).send({ + mediaType: MediaType.TV, + seasons: [1, 2], + serverId: 3, + }); + + assert.strictEqual(res.status, 200); + + const saved = await requestRepo.findOneOrFail({ + where: { id: mediaRequest.id }, + }); + assert.deepStrictEqual( + saved.seasons.map((s) => s.seasonNumber).sort((a, b) => a - b), + [1, 2] + ); + assert.strictEqual(saved.serverId, 3); + }); + + it('does not charge quota for a season the media already has', async () => { + const requestRepo = getRepository(MediaRequest); + const owner = await seedUser('demo@seerr.dev', { tvQuotaLimit: 1 }); + const mediaRequest = await seedTvRequest(owner, [1]); + await seedMediaSeasons(67890, [ + { seasonNumber: 2, status: MediaStatus.AVAILABLE }, + ]); + + const agent = await loginAs('demo@seerr.dev', 'test1234'); + const res = await agent.put(`/request/${mediaRequest.id}`).send({ + mediaType: MediaType.TV, + seasons: [1, 2], + }); + + assert.strictEqual(res.status, 200); + + const saved = await requestRepo.findOneOrFail({ + where: { id: mediaRequest.id }, + }); + assert.deepStrictEqual( + saved.seasons.map((s) => s.seasonNumber), + [1] + ); + }); +}); + +describe('PUT /request/:requestId (quota)', () => { + it('rejects adding seasons beyond the season limit', async () => { + const requestRepo = getRepository(MediaRequest); + const owner = await seedUser('demo@seerr.dev', { tvQuotaLimit: 2 }); + const mediaRequest = await seedTvRequest(owner, [1, 2]); + + const agent = await loginAs('demo@seerr.dev', 'test1234'); + const res = await agent.put(`/request/${mediaRequest.id}`).send({ + mediaType: MediaType.TV, + seasons: [1, 2, 3], + }); + + assert.strictEqual(res.status, 403); + + const saved = await requestRepo.findOneOrFail({ + where: { id: mediaRequest.id }, + }); + assert.deepStrictEqual( + saved.seasons.map((s) => s.seasonNumber).sort((a, b) => a - b), + [1, 2] + ); + }); + + it('rejects adding seasons to a request older than the quota window', async () => { + const requestRepo = getRepository(MediaRequest); + const owner = await seedUser('demo@seerr.dev', { + tvQuotaLimit: 2, + tvQuotaDays: 7, + }); + const mediaRequest = await seedTvRequest(owner, [1, 2], { + createdAt: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000), + }); + + const agent = await loginAs('demo@seerr.dev', 'test1234'); + const res = await agent.put(`/request/${mediaRequest.id}`).send({ + mediaType: MediaType.TV, + seasons: [1, 2, 3], + }); + + assert.strictEqual(res.status, 403); + + const saved = await requestRepo.findOneOrFail({ + where: { id: mediaRequest.id }, + }); + assert.deepStrictEqual( + saved.seasons.map((s) => s.seasonNumber).sort((a, b) => a - b), + [1, 2] + ); + }); + + it('allows swapping seasons at the season limit', async () => { + const requestRepo = getRepository(MediaRequest); + const owner = await seedUser('demo@seerr.dev', { tvQuotaLimit: 2 }); + const mediaRequest = await seedTvRequest(owner, [1, 2]); + + const agent = await loginAs('demo@seerr.dev', 'test1234'); + const res = await agent.put(`/request/${mediaRequest.id}`).send({ + mediaType: MediaType.TV, + seasons: [3, 4], + }); + + assert.strictEqual(res.status, 200); + + const saved = await requestRepo.findOneOrFail({ + where: { id: mediaRequest.id }, + }); + assert.deepStrictEqual( + saved.seasons.map((s) => s.seasonNumber).sort((a, b) => a - b), + [3, 4] + ); + }); + + it('rejects reassignment to a user without room for the existing seasons', async () => { + const requestRepo = getRepository(MediaRequest); + const owner = await seedUser('admin@seerr.dev'); + const target = await seedUser('demo@seerr.dev', { tvQuotaLimit: 1 }); + const mediaRequest = await seedTvRequest(owner, [1, 2]); + + const agent = await loginAs('admin@seerr.dev', 'test1234'); + const res = await agent.put(`/request/${mediaRequest.id}`).send({ + mediaType: MediaType.TV, + seasons: [1, 2], + userId: target.id, + }); + + assert.strictEqual(res.status, 403); + + const saved = await requestRepo.findOneOrFail({ + where: { id: mediaRequest.id }, + }); + assert.strictEqual(saved.requestedBy.id, owner.id); + }); + + it('rejects reassignment of a movie request to a user at their limit', async () => { + const requestRepo = getRepository(MediaRequest); + const mediaRepo = getRepository(Media); + + const owner = await seedUser('admin@seerr.dev'); + const target = await seedUser('demo@seerr.dev', { movieQuotaLimit: 1 }); + + // Uses up the target's single movie request + await seedRequest(); + + const media = await mediaRepo.save( + new Media({ + mediaType: MediaType.MOVIE, + tmdbId: 55555, + status: MediaStatus.PENDING, + status4k: MediaStatus.UNKNOWN, + }) + ); + const mediaRequest = await requestRepo.save( + new MediaRequest({ + type: MediaType.MOVIE, + status: MediaRequestStatus.PENDING, + media, + requestedBy: owner, + is4k: false, + }) + ); + + const agent = await loginAs('admin@seerr.dev', 'test1234'); + const res = await agent.put(`/request/${mediaRequest.id}`).send({ + mediaType: MediaType.MOVIE, + userId: target.id, + }); + + assert.strictEqual(res.status, 403); + + const saved = await requestRepo.findOneOrFail({ + where: { id: mediaRequest.id }, + }); + assert.strictEqual(saved.requestedBy.id, owner.id); + }); + + it('allows reassignment to a user who bypasses quotas', async () => { + const requestRepo = getRepository(MediaRequest); + const owner = await seedUser('demo@seerr.dev', { tvQuotaLimit: 1 }); + const target = await seedUser('admin@seerr.dev'); + const mediaRequest = await seedTvRequest(owner, [1, 2]); + + const agent = await loginAs('admin@seerr.dev', 'test1234'); + const res = await agent.put(`/request/${mediaRequest.id}`).send({ + mediaType: MediaType.TV, + seasons: [1, 2], + userId: target.id, + }); + + assert.strictEqual(res.status, 200); + + const saved = await requestRepo.findOneOrFail({ + where: { id: mediaRequest.id }, + }); + assert.strictEqual(saved.requestedBy.id, target.id); + }); + + it('allows an edit that exceeds the limit when the request ignores quota', async () => { + const requestRepo = getRepository(MediaRequest); + const owner = await seedUser('demo@seerr.dev', { tvQuotaLimit: 2 }); + + await seedTvRequest(owner, [1, 2], { tmdbId: 77777 }); + const mediaRequest = await seedTvRequest(owner, [1, 2], { + ignoreQuota: true, + }); + + const agent = await loginAs('demo@seerr.dev', 'test1234'); + const res = await agent.put(`/request/${mediaRequest.id}`).send({ + mediaType: MediaType.TV, + seasons: [1, 2, 3], + }); + + assert.strictEqual(res.status, 200); + + const saved = await requestRepo.findOneOrFail({ + where: { id: mediaRequest.id }, + }); + assert.deepStrictEqual( + saved.seasons.map((s) => s.seasonNumber).sort((a, b) => a - b), + [1, 2, 3] + ); + }); + + it('charges only the net increase when an edit both adds and removes', async () => { + const requestRepo = getRepository(MediaRequest); + const owner = await seedUser('demo@seerr.dev', { tvQuotaLimit: 4 }); + const mediaRequest = await seedTvRequest(owner, [1, 2, 3]); + + const agent = await loginAs('demo@seerr.dev', 'test1234'); + const res = await agent.put(`/request/${mediaRequest.id}`).send({ + mediaType: MediaType.TV, + seasons: [1, 2, 4, 5], + }); + + assert.strictEqual(res.status, 200); + + const saved = await requestRepo.findOneOrFail({ + where: { id: mediaRequest.id }, + }); + assert.deepStrictEqual( + saved.seasons.map((s) => s.seasonNumber).sort((a, b) => a - b), + [1, 2, 4, 5] + ); + }); +}); + +describe('GET /request/:requestId', () => { + it('omits notification settings from requestedBy and modifiedBy', async () => { + const pending = await seedRequest(); + await seedUserSettings('demo@seerr.dev'); + await seedUserSettings('admin@seerr.dev'); + + const admin = await loginAs('admin@seerr.dev', 'test1234'); + const approved = await admin.post(`/request/${pending.id}/approve`); + assert.strictEqual(approved.status, 200); + + const res = await admin.get(`/request/${pending.id}`); + + assert.strictEqual(res.status, 200); + assert.strictEqual(res.body.requestedBy.email, 'demo@seerr.dev'); + assert.strictEqual(res.body.modifiedBy.email, 'admin@seerr.dev'); + assert.ok(!('settings' in res.body.requestedBy)); + assert.ok(!('settings' in res.body.modifiedBy)); + assertNoCredentials(res.body); + }); +}); + +describe('POST /request/:requestId/:status', () => { + const cases = [ + { action: 'approve', expected: MediaRequestStatus.APPROVED }, + { action: 'decline', expected: MediaRequestStatus.DECLINED }, + ] as const; + + for (const { action, expected } of cases) { + it(`transitions to ${action}d and records the acting user`, async () => { + const repo = getRepository(MediaRequest); + const pending = await seedRequest(); + const admin = await loginAs('admin@seerr.dev', 'test1234'); + + const res = await admin.post(`/request/${pending.id}/${action}`); + + assert.strictEqual(res.status, 200); + assert.strictEqual(res.body.status, expected); + assert.strictEqual(res.body.modifiedBy.email, 'admin@seerr.dev'); + + const persisted = await repo.findOneOrFail({ + where: { id: pending.id }, + relations: { modifiedBy: true }, + }); + + assert.strictEqual(persisted.status, expected); + assert.strictEqual(persisted.modifiedBy?.email, 'admin@seerr.dev'); + assert.ok(persisted.updatedAt > pending.updatedAt); + }); + } + + it('rejects a status the route does not define', async () => { + const repo = getRepository(MediaRequest); + const pending = await seedRequest(); + const admin = await loginAs('admin@seerr.dev', 'test1234'); + + const res = await admin.post(`/request/${pending.id}/frobnicate`); + + assert.strictEqual(res.status, 400); + assert.match(res.body.message, /approve/i); + + const persisted = await repo.findOneOrFail({ + where: { id: pending.id }, + relations: { modifiedBy: true }, + }); + assert.strictEqual(persisted.status, MediaRequestStatus.PENDING); + assert.ok(!persisted.modifiedBy); + }); + + it('refuses to act on a request that is no longer pending', async () => { + const repo = getRepository(MediaRequest); + const approved = await seedRequest(MediaRequestStatus.APPROVED); + const admin = await loginAs('admin@seerr.dev', 'test1234'); + + const res = await admin.post(`/request/${approved.id}/decline`); + + assert.strictEqual(res.status, 409); + assert.match(res.body.message, /pending/i); + + const persisted = await repo.findOneOrFail({ where: { id: approved.id } }); + assert.strictEqual(persisted.status, MediaRequestStatus.APPROVED); + }); + + it('applies only one of a concurrent approve and decline', async () => { + const repo = getRepository(MediaRequest); + const pending = await seedRequest(); + const admin = await loginAs('admin@seerr.dev', 'test1234'); + + const [approve, decline] = await Promise.all([ + admin.post(`/request/${pending.id}/approve`), + admin.post(`/request/${pending.id}/decline`), + ]); + + assert.deepStrictEqual([approve.status, decline.status].sort(), [200, 409]); + + const persisted = await repo.findOneOrFail({ where: { id: pending.id } }); + assert.strictEqual( + persisted.status, + approve.status === 200 + ? MediaRequestStatus.APPROVED + : MediaRequestStatus.DECLINED + ); + }); + + it('rejects the removed pending verb even on a non-pending request', async () => { + const repo = getRepository(MediaRequest); + const declined = await seedRequest(MediaRequestStatus.DECLINED); + const admin = await loginAs('admin@seerr.dev', 'test1234'); + + const res = await admin.post(`/request/${declined.id}/pending`); + + assert.strictEqual(res.status, 400); + assert.match(res.body.message, /approve/i); + + const persisted = await repo.findOneOrFail({ where: { id: declined.id } }); + assert.strictEqual(persisted.status, MediaRequestStatus.DECLINED); + }); +}); + +describe('POST /request/:requestId/retry', () => { + it('re-approves a failed request and records the acting user', async () => { + const repo = getRepository(MediaRequest); + const failed = await seedRequest(MediaRequestStatus.FAILED); + const admin = await loginAs('admin@seerr.dev', 'test1234'); + + const res = await admin.post(`/request/${failed.id}/retry`); + + assert.strictEqual(res.status, 200); + assert.strictEqual(res.body.status, MediaRequestStatus.APPROVED); + assert.strictEqual(res.body.modifiedBy.email, 'admin@seerr.dev'); + + const persisted = await repo.findOneOrFail({ + where: { id: failed.id }, + relations: { modifiedBy: true }, + }); + + assert.strictEqual(persisted.status, MediaRequestStatus.APPROVED); + assert.strictEqual(persisted.modifiedBy?.email, 'admin@seerr.dev'); + assert.ok(persisted.updatedAt > failed.updatedAt); + }); + + it('refuses to retry a request that has not failed', async () => { + const repo = getRepository(MediaRequest); + const pending = await seedRequest(); + const admin = await loginAs('admin@seerr.dev', 'test1234'); + + const res = await admin.post(`/request/${pending.id}/retry`); + + assert.strictEqual(res.status, 409); + assert.match(res.body.message, /failed/i); + + const persisted = await repo.findOneOrFail({ where: { id: pending.id } }); + assert.strictEqual(persisted.status, MediaRequestStatus.PENDING); + }); + + it('sends a concurrently retried request to *arr once', async (t) => { + const repo = getRepository(MediaRequest); + const failed = await seedRequest(MediaRequestStatus.FAILED); + const admin = await loginAs('admin@seerr.dev', 'test1234'); + + const sendToRadarr = t.mock.method( + MediaRequestSubscriber.prototype, + 'sendToRadarr', + async () => undefined + ); + + const results = await Promise.all([ + admin.post(`/request/${failed.id}/retry`), + admin.post(`/request/${failed.id}/retry`), + ]); + + assert.deepStrictEqual(results.map((r) => r.status).sort(), [200, 409]); + assert.strictEqual(sendToRadarr.mock.callCount(), 1); + + const persisted = await repo.findOneOrFail({ where: { id: failed.id } }); + assert.strictEqual(persisted.status, MediaRequestStatus.APPROVED); + }); +}); + +describe('DELETE /request/:requestId, deleted media status restoration', () => { + async function seedDeletedMediaScenario() { + const userRepo = getRepository(User); + const mediaRepo = getRepository(Media); + const requestRepo = getRepository(MediaRequest); + + const admin = await userRepo.findOneOrFail({ + where: { email: 'admin@seerr.dev' }, + }); + + const media = await mediaRepo.save( + new Media({ + mediaType: MediaType.MOVIE, + tmdbId: 99001, + status: MediaStatus.DELETED, + status4k: MediaStatus.UNKNOWN, + }) + ); + + const staleRequest = await requestRepo.save( + new MediaRequest({ + type: MediaType.MOVIE, + status: MediaRequestStatus.COMPLETED, + media, + requestedBy: admin, + is4k: false, + isAutoRequest: true, + }) + ); + + media.status = MediaStatus.PENDING; + await mediaRepo.save(media); + + const newRequest = await requestRepo.save( + new MediaRequest({ + type: MediaType.MOVIE, + status: MediaRequestStatus.APPROVED, + media, + requestedBy: admin, + is4k: false, + }) + ); + + return { media, staleRequest, newRequest, admin }; + } + + it('restores media status to DELETED when the re-request is deleted and a stale completed request remains', async () => { + const mediaRepo = getRepository(Media); + const { media, newRequest } = await seedDeletedMediaScenario(); + + const agent = await loginAs('admin@seerr.dev', 'test1234'); + const res = await agent.delete(`/request/${newRequest.id}`); + + assert.strictEqual(res.status, 204); + + const updated = await mediaRepo.findOneOrFail({ where: { id: media.id } }); + assert.strictEqual(updated.status, MediaStatus.DELETED); + }); + + it('restores media status4k to DELETED when the re-request is deleted and a stale completed request remains', async () => { + const userRepo = getRepository(User); + const mediaRepo = getRepository(Media); + const requestRepo = getRepository(MediaRequest); + + const admin = await userRepo.findOneOrFail({ + where: { email: 'admin@seerr.dev' }, + }); + + const media = await mediaRepo.save( + new Media({ + mediaType: MediaType.MOVIE, + tmdbId: 99003, + status: MediaStatus.UNKNOWN, + status4k: MediaStatus.DELETED, + }) + ); + + await requestRepo.save( + new MediaRequest({ + type: MediaType.MOVIE, + status: MediaRequestStatus.COMPLETED, + media, + requestedBy: admin, + is4k: true, + isAutoRequest: true, + }) + ); + + media.status4k = MediaStatus.PENDING; + await mediaRepo.save(media); + + const newRequest = await requestRepo.save( + new MediaRequest({ + type: MediaType.MOVIE, + status: MediaRequestStatus.APPROVED, + media, + requestedBy: admin, + is4k: true, + }) + ); + + const agent = await loginAs('admin@seerr.dev', 'test1234'); + const res = await agent.delete(`/request/${newRequest.id}`); + + assert.strictEqual(res.status, 204); + + const updated = await mediaRepo.findOneOrFail({ where: { id: media.id } }); + assert.strictEqual(updated.status4k, MediaStatus.DELETED); + }); + + it('resets media status to UNKNOWN when the stale completed request is also deleted', async () => { + const mediaRepo = getRepository(Media); + const requestRepo = getRepository(MediaRequest); + const { media, newRequest, staleRequest } = + await seedDeletedMediaScenario(); + + const agent = await loginAs('admin@seerr.dev', 'test1234'); + + await agent.delete(`/request/${newRequest.id}`); + + const res = await agent.delete(`/request/${staleRequest.id}`); + assert.strictEqual(res.status, 204); + + const updated = await mediaRepo.findOneOrFail({ where: { id: media.id } }); + assert.strictEqual(updated.status, MediaStatus.UNKNOWN); + + const remaining = await requestRepo.find({ + where: { media: { id: media.id } }, + }); + assert.strictEqual(remaining.length, 0); + }); + + it('resets media status4k to UNKNOWN when the stale completed 4K request is also deleted', async () => { + const userRepo = getRepository(User); + const mediaRepo = getRepository(Media); + const requestRepo = getRepository(MediaRequest); + + const admin = await userRepo.findOneOrFail({ + where: { email: 'admin@seerr.dev' }, + }); + + const media = await mediaRepo.save( + new Media({ + mediaType: MediaType.MOVIE, + tmdbId: 99004, + status: MediaStatus.UNKNOWN, + status4k: MediaStatus.DELETED, + }) + ); + + const staleRequest = await requestRepo.save( + new MediaRequest({ + type: MediaType.MOVIE, + status: MediaRequestStatus.COMPLETED, + media, + requestedBy: admin, + is4k: true, + isAutoRequest: true, + }) + ); + + media.status4k = MediaStatus.PENDING; + await mediaRepo.save(media); + + const newRequest = await requestRepo.save( + new MediaRequest({ + type: MediaType.MOVIE, + status: MediaRequestStatus.APPROVED, + media, + requestedBy: admin, + is4k: true, + }) + ); + + const agent = await loginAs('admin@seerr.dev', 'test1234'); + + await agent.delete(`/request/${newRequest.id}`); + + const res = await agent.delete(`/request/${staleRequest.id}`); + assert.strictEqual(res.status, 204); + + const updated = await mediaRepo.findOneOrFail({ where: { id: media.id } }); + assert.strictEqual(updated.status4k, MediaStatus.UNKNOWN); + }); + + it('does not reset media status when other active requests still exist', async () => { + const userRepo = getRepository(User); + const mediaRepo = getRepository(Media); + const requestRepo = getRepository(MediaRequest); + + const admin = await userRepo.findOneOrFail({ + where: { email: 'admin@seerr.dev' }, + }); + + const media = await mediaRepo.save( + new Media({ + mediaType: MediaType.MOVIE, + tmdbId: 99002, + status: MediaStatus.PENDING, + status4k: MediaStatus.UNKNOWN, + }) + ); + + const req1 = await requestRepo.save( + new MediaRequest({ + type: MediaType.MOVIE, + status: MediaRequestStatus.PENDING, + media, + requestedBy: admin, + is4k: false, + }) + ); + + await requestRepo.save( + new MediaRequest({ + type: MediaType.MOVIE, + status: MediaRequestStatus.PENDING, + media, + requestedBy: admin, + is4k: false, + }) + ); + + const agent = await loginAs('admin@seerr.dev', 'test1234'); + const res = await agent.delete(`/request/${req1.id}`); + + assert.strictEqual(res.status, 204); + + const updated = await mediaRepo.findOneOrFail({ where: { id: media.id } }); + assert.strictEqual(updated.status, MediaStatus.PENDING); + }); + + it('does not reset media status when status is PARTIALLY_AVAILABLE and only completed requests remain', async () => { + const userRepo = getRepository(User); + const mediaRepo = getRepository(Media); + const requestRepo = getRepository(MediaRequest); + + const admin = await userRepo.findOneOrFail({ + where: { email: 'admin@seerr.dev' }, + }); + + const media = await mediaRepo.save( + new Media({ + mediaType: MediaType.MOVIE, + tmdbId: 99005, + status: MediaStatus.PARTIALLY_AVAILABLE, + status4k: MediaStatus.UNKNOWN, + }) + ); + + const completedRequest = await requestRepo.save( + new MediaRequest({ + type: MediaType.MOVIE, + status: MediaRequestStatus.COMPLETED, + media, + requestedBy: admin, + is4k: false, + }) + ); + + const agent = await loginAs('admin@seerr.dev', 'test1234'); + const res = await agent.delete(`/request/${completedRequest.id}`); + + assert.strictEqual(res.status, 204); + + const updated = await mediaRepo.findOneOrFail({ where: { id: media.id } }); + assert.strictEqual(updated.status, MediaStatus.PARTIALLY_AVAILABLE); + }); +}); + +describe('POST /request (movie), override rules', () => { + it('applies an override rule when the default Radarr server id differs from its array index', async () => { + configureRadarr([{ id: 5, isDefault: true, is4k: false }]); + getSettings().sonarr = []; + + const userRepo = getRepository(User); + const friend = await userRepo.findOneOrFail({ + where: { email: 'demo@seerr.dev' }, + }); + + const overrideRuleRepo = getRepository(OverrideRule); + await overrideRuleRepo.save( + new OverrideRule({ + radarrServiceId: 5, + users: String(friend.id), + rootFolder: '/overridden/movies', + }) + ); + + const agent = await loginAs('demo@seerr.dev', 'test1234'); + const res = await agent.post('/request').send({ + mediaType: MediaType.MOVIE, + mediaId: 88001, + }); + + assert.strictEqual(res.status, 201); + assert.strictEqual(res.body.rootFolder, '/overridden/movies'); + }); + + it('applies an override rule when the default Radarr server id matches its array index (sanity check)', async () => { + configureRadarr([{ id: 0, isDefault: true, is4k: false }]); + getSettings().sonarr = []; + + const userRepo = getRepository(User); + const friend = await userRepo.findOneOrFail({ + where: { email: 'demo@seerr.dev' }, + }); + + const overrideRuleRepo = getRepository(OverrideRule); + await overrideRuleRepo.save( + new OverrideRule({ + radarrServiceId: 0, + users: String(friend.id), + rootFolder: '/overridden/movies', + }) + ); + + const agent = await loginAs('demo@seerr.dev', 'test1234'); + const res = await agent.post('/request').send({ + mediaType: MediaType.MOVIE, + mediaId: 88002, + }); + + assert.strictEqual(res.status, 201); + assert.strictEqual(res.body.rootFolder, '/overridden/movies'); + }); + + it('does not apply an unrelated override rule when there is no default Radarr server configured', async () => { + getSettings().radarr = []; + getSettings().sonarr = []; + + const userRepo = getRepository(User); + const friend = await userRepo.findOneOrFail({ + where: { email: 'demo@seerr.dev' }, + }); + + const overrideRuleRepo = getRepository(OverrideRule); + await overrideRuleRepo.save( + new OverrideRule({ + radarrServiceId: 999, + users: String(friend.id), + rootFolder: '/overridden/movies', + }) + ); + + const agent = await loginAs('demo@seerr.dev', 'test1234'); + const res = await agent.post('/request').send({ + mediaType: MediaType.MOVIE, + mediaId: 88005, + }); + + assert.strictEqual(res.status, 201); + assert.strictEqual(res.body.rootFolder, null); + }); +}); + +describe('POST /request (tv), override rules', () => { + it('applies an override rule when the default Sonarr server id differs from its array index', async () => { + configureSonarr([{ id: 5, isDefault: true, is4k: false }]); + getSettings().radarr = []; + + const userRepo = getRepository(User); + const friend = await userRepo.findOneOrFail({ + where: { email: 'demo@seerr.dev' }, + }); + + const overrideRuleRepo = getRepository(OverrideRule); + await overrideRuleRepo.save( + new OverrideRule({ + sonarrServiceId: 5, + users: String(friend.id), + rootFolder: '/overridden/tv', + }) + ); + + const agent = await loginAs('demo@seerr.dev', 'test1234'); + const res = await agent.post('/request').send({ + mediaType: MediaType.TV, + mediaId: 88003, + seasons: [1], + }); + + assert.strictEqual(res.status, 201); + assert.strictEqual(res.body.rootFolder, '/overridden/tv'); + }); + + it('applies an override rule when the default Sonarr server id matches its array index (sanity check)', async () => { + configureSonarr([{ id: 0, isDefault: true, is4k: false }]); + getSettings().radarr = []; + + const userRepo = getRepository(User); + const friend = await userRepo.findOneOrFail({ + where: { email: 'demo@seerr.dev' }, + }); + + const overrideRuleRepo = getRepository(OverrideRule); + await overrideRuleRepo.save( + new OverrideRule({ + sonarrServiceId: 0, + users: String(friend.id), + rootFolder: '/overridden/tv', + }) + ); + + const agent = await loginAs('demo@seerr.dev', 'test1234'); + const res = await agent.post('/request').send({ + mediaType: MediaType.TV, + mediaId: 88004, + seasons: [1], + }); + + assert.strictEqual(res.status, 201); + assert.strictEqual(res.body.rootFolder, '/overridden/tv'); + }); + + it('does not apply an unrelated override rule when there is no default Sonarr server configured', async () => { + getSettings().radarr = []; + getSettings().sonarr = []; + + const userRepo = getRepository(User); + const friend = await userRepo.findOneOrFail({ + where: { email: 'demo@seerr.dev' }, + }); + + const overrideRuleRepo = getRepository(OverrideRule); + await overrideRuleRepo.save( + new OverrideRule({ + sonarrServiceId: 999, + users: String(friend.id), + rootFolder: '/overridden/tv', + }) + ); + + const agent = await loginAs('demo@seerr.dev', 'test1234'); + const res = await agent.post('/request').send({ + mediaType: MediaType.TV, + mediaId: 88006, + seasons: [1], + }); + + assert.strictEqual(res.status, 201); + assert.strictEqual(res.body.rootFolder, null); + }); +}); + +describe('POST /request, override rules and requester choices', () => { + async function requestWithRule(permissions: number) { + configureRadarr([{ id: 1, isDefault: true, is4k: false }]); + getSettings().sonarr = []; + + const userRepo = getRepository(User); + const requester = await userRepo.findOneOrFail({ + where: { email: 'demo@seerr.dev' }, + }); + requester.permissions = permissions; + await userRepo.save(requester); + + await getRepository(OverrideRule).save( + new OverrideRule({ + radarrServiceId: 1, + rootFolder: '/rule', + profileId: 7, + tags: '2', + }) + ); + + const agent = await loginAs('demo@seerr.dev', 'test1234'); + return agent.post('/request').send({ + mediaType: MediaType.MOVIE, + mediaId: 88010, + serverId: 1, + rootFolder: '/chosen', + tags: [], + }); + } + + it('keeps what an advanced requester sets and fills the rest from the rule', async () => { + const res = await requestWithRule( + Permission.REQUEST | Permission.REQUEST_ADVANCED + ); + + assert.strictEqual(res.status, 201); + assert.strictEqual(res.body.rootFolder, '/chosen'); + assert.strictEqual(res.body.profileId, 7); + assert.deepStrictEqual(res.body.tags, []); + }); + + it('keeps what a request manager sets and fills the rest from the rule', async (t) => { + // Manage Requests auto-approves; keep it from calling Radarr + t.mock.method( + MediaRequestSubscriber.prototype, + 'sendToRadarr', + async () => undefined + ); + const res = await requestWithRule( + Permission.REQUEST | Permission.MANAGE_REQUESTS + ); + + assert.strictEqual(res.status, 201); + assert.strictEqual(res.body.rootFolder, '/chosen'); + assert.strictEqual(res.body.profileId, 7); + assert.deepStrictEqual(res.body.tags, []); + }); + + it('applies the rule over what a regular requester sets', async () => { + const res = await requestWithRule(Permission.REQUEST); + + assert.strictEqual(res.status, 201); + assert.strictEqual(res.body.rootFolder, '/rule'); + }); +}); + +describe('DELETE /request/:requestId, orphaned season status reset', () => { + async function seedTvShow( + tmdbId: number, + seasons: Partial[] + ): Promise { + const mediaRepo = getRepository(Media); + + return mediaRepo.save( + new Media({ + mediaType: MediaType.TV, + tmdbId, + status: MediaStatus.PROCESSING, + status4k: MediaStatus.UNKNOWN, + seasons: seasons.map((season) => new Season(season)), + }) + ); + } + + async function seedTvRequest( + media: Media, + seasonNumbers: number[] + ): Promise { + const userRepo = getRepository(User); + const requestRepo = getRepository(MediaRequest); + + const admin = await userRepo.findOneOrFail({ + where: { email: 'admin@seerr.dev' }, + }); + + return requestRepo.save( + new MediaRequest({ + type: MediaType.TV, + status: MediaRequestStatus.APPROVED, + media, + requestedBy: admin, + is4k: false, + seasons: seasonNumbers.map( + (seasonNumber) => + new SeasonRequest({ + seasonNumber, + status: MediaRequestStatus.APPROVED, + }) + ), + }) + ); + } + + it('resets a request-covered PROCESSING season to UNKNOWN so it can be re-requested', async () => { + const mediaRepo = getRepository(Media); + const media = await seedTvShow(99101, [ + { seasonNumber: 1, status: MediaStatus.PROCESSING }, + ]); + const tvRequest = await seedTvRequest(media, [1]); + + const admin = await loginAs('admin@seerr.dev', 'test1234'); + const res = await admin.delete(`/request/${tvRequest.id}`); + assert.strictEqual(res.status, 204); + + const updated = await mediaRepo.findOneOrFail({ + where: { id: media.id }, + }); + assert.strictEqual(updated.seasons[0].status, MediaStatus.UNKNOWN); + + const friend = await loginAs('demo@seerr.dev', 'test1234'); + const reRequest = await friend.post('/request').send({ + mediaType: MediaType.TV, + mediaId: 99101, + seasons: [1], + }); + assert.strictEqual(reRequest.status, 201); + }); + + it('does not touch PROCESSING seasons the deleted request did not cover', async () => { + const mediaRepo = getRepository(Media); + const media = await seedTvShow(99102, [ + { seasonNumber: 1, status: MediaStatus.PROCESSING }, + { seasonNumber: 2, status: MediaStatus.PROCESSING }, + ]); + const tvRequest = await seedTvRequest(media, [1]); + + const admin = await loginAs('admin@seerr.dev', 'test1234'); + const res = await admin.delete(`/request/${tvRequest.id}`); + assert.strictEqual(res.status, 204); + + const updated = await mediaRepo.findOneOrFail({ where: { id: media.id } }); + const seasonOne = updated.seasons.find((s) => s.seasonNumber === 1); + const seasonTwo = updated.seasons.find((s) => s.seasonNumber === 2); + assert.strictEqual(seasonOne?.status, MediaStatus.UNKNOWN); + assert.strictEqual(seasonTwo?.status, MediaStatus.PROCESSING); + }); + + it('keeps a season PROCESSING while another active request still covers it', async () => { + const mediaRepo = getRepository(Media); + const media = await seedTvShow(99103, [ + { seasonNumber: 1, status: MediaStatus.PROCESSING }, + ]); + const firstRequest = await seedTvRequest(media, [1]); + await seedTvRequest(media, [1]); + + const admin = await loginAs('admin@seerr.dev', 'test1234'); + const res = await admin.delete(`/request/${firstRequest.id}`); + assert.strictEqual(res.status, 204); + + const updated = await mediaRepo.findOneOrFail({ where: { id: media.id } }); + assert.strictEqual(updated.seasons[0].status, MediaStatus.PROCESSING); + }); + + it('leaves season status4k untouched when deleting a non-4K request', async () => { + const mediaRepo = getRepository(Media); + const media = await seedTvShow(99104, [ + { + seasonNumber: 1, + status: MediaStatus.PROCESSING, + status4k: MediaStatus.PROCESSING, + }, + ]); + const tvRequest = await seedTvRequest(media, [1]); + + const admin = await loginAs('admin@seerr.dev', 'test1234'); + const res = await admin.delete(`/request/${tvRequest.id}`); + assert.strictEqual(res.status, 204); + + const updated = await mediaRepo.findOneOrFail({ where: { id: media.id } }); + assert.strictEqual(updated.seasons[0].status, MediaStatus.UNKNOWN); + assert.strictEqual(updated.seasons[0].status4k, MediaStatus.PROCESSING); }); }); diff --git a/server/routes/request.ts b/server/routes/request.ts index fafa90692e..50315cce4c 100644 --- a/server/routes/request.ts +++ b/server/routes/request.ts @@ -1,4 +1,5 @@ import RadarrAPI from '@server/api/servarr/radarr'; +import ReadarrAPI from '@server/api/servarr/readarr'; import SonarrAPI from '@server/api/servarr/sonarr'; import { MediaRequestStatus, @@ -25,6 +26,12 @@ import { Permission } from '@server/lib/permissions'; import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; import { isAuthenticated } from '@server/middleware/auth'; +import requestLock, { + mediaKey, + mediaLock, + requestKey, + userKey, +} from '@server/utils/requestLock'; import { Router } from 'express'; const requestRoutes = Router(); @@ -173,6 +180,11 @@ requestRoutes.get, RequestResultsResponse>( type: MediaType.TV, }); break; + case 'book': + query = query.andWhere('request.type = :type', { + type: MediaType.BOOK, + }); + break; } const [requests, requestCount] = await query @@ -213,6 +225,24 @@ requestRoutes.get, RequestResultsResponse>( }) ); + // get all quality profiles for every configured readarr server + const readarrServers = await Promise.all( + settings.readarr.map(async (readarrSetting) => { + const readarr = new ReadarrAPI({ + apiKey: readarrSetting.apiKey, + url: ReadarrAPI.buildUrl(readarrSetting, '/api/v1'), + }); + + return { + id: readarrSetting.id, + profiles: await readarr.getProfiles().catch(() => undefined), + metadataProfiles: await readarr + .getMetadataProfiles() + .catch(() => undefined), + }; + }) + ); + // add profile names to the media requests, with undefined if not found let mappedRequests = requests.map((r) => { switch (r.type) { @@ -234,6 +264,20 @@ requestRoutes.get, RequestResultsResponse>( ?.profiles?.find((profile) => profile.id === r.profileId)?.name, }; } + case MediaType.BOOK: { + return { + ...r, + profileName: readarrServers + .find((serverr) => serverr.id === r.serverId) + ?.profiles?.find((profile) => profile.id === r.profileId)?.name, + metadataProfileName: readarrServers + .find((serverr) => serverr.id === r.serverId) + ?.metadataProfiles?.find( + (metadataProfile) => + metadataProfile.id === r.metadataProfileId + )?.name, + }; + } } }); @@ -263,6 +307,23 @@ requestRoutes.get, RequestResultsResponse>( ), }; } + case MediaType.BOOK: { + return { + ...r, + // check if the readarr server for this request is configured + canRemove: readarrServers.some( + (server) => + server.id === + (r.is4k ? r.media.serviceId4k : r.media.serviceId) + ), + }; + } + default: { + return { + ...r, + canRemove: false, + }; + } } }); } @@ -462,136 +523,235 @@ requestRoutes.put<{ requestId: string }>( async (req, res, next) => { const requestRepository = getRepository(MediaRequest); const userRepository = getRepository(User); + const requestId = Number(req.params.requestId); try { - const request = await requestRepository.findOne({ - where: { - id: Number(req.params.requestId), - }, - }); - - if (!request) { - return next({ status: 404, message: 'Request not found.' }); - } - - if ( - (request.requestedBy.id !== req.user?.id || - (req.body.mediaType !== 'tv' && - !req.user?.hasPermission(Permission.REQUEST_ADVANCED))) && - !req.user?.hasPermission(Permission.MANAGE_REQUESTS) - ) { - return next({ - status: 403, - message: 'You do not have permission to modify this request.', - }); - } - - let requestUser = request.requestedBy; - - if ( - req.body.userId && - req.body.userId !== request.requestedBy.id && - !req.user?.hasPermission([ - Permission.MANAGE_USERS, - Permission.MANAGE_REQUESTS, - ]) - ) { - return next({ - status: 403, - message: 'You do not have permission to modify the request user.', - }); - } else if (req.body.userId) { - requestUser = await userRepository.findOneOrFail({ - where: { id: req.body.userId }, + // Ordering is request then owner here, user then media on create, so no cycle + return await requestLock.dispatch(requestKey(requestId), async () => { + const request = await requestRepository.findOne({ + where: { + id: requestId, + }, }); - } - - if (req.body.mediaType === MediaType.MOVIE) { - request.serverId = req.body.serverId; - request.profileId = req.body.profileId; - request.rootFolder = req.body.rootFolder; - request.tags = req.body.tags; - request.requestedBy = requestUser as User; - await requestRepository.save(request); - } else if (req.body.mediaType === MediaType.TV) { - const mediaRepository = getRepository(Media); - request.serverId = req.body.serverId; - request.profileId = req.body.profileId; - request.rootFolder = req.body.rootFolder; - request.languageProfileId = req.body.languageProfileId; - request.tags = req.body.tags; - request.requestedBy = requestUser as User; - - const requestedSeasons = req.body.seasons as number[] | undefined; - - if (!requestedSeasons || requestedSeasons.length === 0) { - throw new Error( - 'Missing seasons. If you want to cancel a series request, use the DELETE method.' - ); + if (!request) { + return next({ status: 404, message: 'Request not found.' }); } - // Get existing media so we can work with all the requests - const media = await mediaRepository.findOneOrFail({ - where: { tmdbId: request.media.tmdbId, mediaType: MediaType.TV }, - relations: { requests: true }, - }); - - // Get all requested seasons that are not part of this request we are editing - const existingSeasons = media.requests - .filter( - (r) => - r.is4k === request.is4k && - r.id !== request.id && - r.status !== MediaRequestStatus.DECLINED && - r.status !== MediaRequestStatus.COMPLETED - ) - .reduce((seasons, r) => { - const combinedSeasons = r.seasons.map( - (season) => season.seasonNumber - ); - - return [...seasons, ...combinedSeasons]; - }, [] as number[]); - - const filteredSeasons = requestedSeasons.filter( - (rs) => !existingSeasons.includes(rs) - ); - - if (filteredSeasons.length === 0) { + if ( + (request.requestedBy.id !== req.user?.id || + (req.body.mediaType !== 'tv' && + !req.user?.hasPermission(Permission.REQUEST_ADVANCED))) && + !req.user?.hasPermission(Permission.MANAGE_REQUESTS) + ) { return next({ - status: 202, - message: 'No seasons available to request', + status: 403, + message: 'You do not have permission to modify this request.', }); } - const newSeasons = requestedSeasons.filter( - (sn) => !request.seasons.map((s) => s.seasonNumber).includes(sn) - ); - - request.seasons = request.seasons.filter((rs) => - filteredSeasons.includes(rs.seasonNumber) - ); + if (request.status !== MediaRequestStatus.PENDING) { + return next({ + status: 409, + message: 'Only pending requests can be modified.', + }); + } - if (newSeasons.length > 0) { - logger.debug('Adding new seasons to request', { - label: 'Media Request', - newSeasons, + const previousOwnerId = request.requestedBy.id; + let requestUser = request.requestedBy; + + if ( + req.body.userId && + req.body.userId !== request.requestedBy.id && + !req.user?.hasPermission([ + Permission.MANAGE_USERS, + Permission.MANAGE_REQUESTS, + ]) + ) { + return next({ + status: 403, + message: 'You do not have permission to modify the request user.', + }); + } else if (req.body.userId) { + requestUser = await userRepository.findOneOrFail({ + where: { id: req.body.userId }, }); - request.seasons.push( - ...newSeasons.map( - (ns) => - new SeasonRequest({ - seasonNumber: ns, - status: MediaRequestStatus.PENDING, - }) - ) - ); } - await requestRepository.save(request); - } + // Reassignment moves every season on the request onto the new owner's + // quota, so it is charged in full rather than as a delta + const ownerChanging = requestUser.id !== previousOwnerId; + + return requestLock.dispatch(userKey(requestUser.id), async () => { + if (req.body.mediaType === MediaType.MOVIE) { + if (ownerChanging && !request.ignoreQuota) { + const quotas = await requestUser.getQuota(); + + if (quotas.movie.restricted) { + return next({ + status: 403, + message: 'Movie Quota exceeded.', + }); + } + } - return res.status(200).json(request); + request.serverId = req.body.serverId; + request.profileId = req.body.profileId; + request.rootFolder = req.body.rootFolder; + request.tags = req.body.tags; + request.requestedBy = requestUser as User; + + await requestRepository.save(request); + } else if (req.body.mediaType === MediaType.TV) { + const mediaRepository = getRepository(Media); + request.serverId = req.body.serverId; + request.profileId = req.body.profileId; + request.rootFolder = req.body.rootFolder; + request.languageProfileId = req.body.languageProfileId; + request.tags = req.body.tags; + request.requestedBy = requestUser as User; + + const requestedSeasons = req.body.seasons as number[] | undefined; + + if (!requestedSeasons || requestedSeasons.length === 0) { + throw new Error( + 'Missing seasons. If you want to cancel a series request, use the DELETE method.' + ); + } + + // Same key as create, so an edit cannot claim a season that a new + // request is taking at the same moment + return mediaLock.dispatch( + mediaKey(MediaType.TV, request.media.tmdbId), + async () => { + // Get existing media so we can work with all the requests + const media = await mediaRepository.findOneOrFail({ + where: { + tmdbId: request.media.tmdbId, + mediaType: MediaType.TV, + }, + relations: { requests: true }, + }); + + // Get all requested seasons that are not part of this request we are editing + const existingSeasons = media.requests + .filter( + (r) => + r.is4k === request.is4k && + r.id !== request.id && + r.status !== MediaRequestStatus.DECLINED && + r.status !== MediaRequestStatus.COMPLETED + ) + .reduce((seasons, r) => { + const combinedSeasons = r.seasons.map( + (season) => season.seasonNumber + ); + + return [...seasons, ...combinedSeasons]; + }, [] as number[]); + + const currentSeasons = request.seasons.map( + (s) => s.seasonNumber + ); + + // Seasons the media already covers cannot be requested again, while + // the ones this request holds stay on it + const coveredSeasons = (media.seasons ?? []) + .filter( + (season) => + season[request.is4k ? 'status4k' : 'status'] !== + MediaStatus.UNKNOWN && + season[request.is4k ? 'status4k' : 'status'] !== + MediaStatus.DELETED + ) + .map((season) => season.seasonNumber) + .filter((sn) => !currentSeasons.includes(sn)); + + const filteredSeasons = requestedSeasons.filter( + (rs) => !existingSeasons.includes(rs) + ); + + const keptSeasons = filteredSeasons.filter((sn) => + currentSeasons.includes(sn) + ); + + const newSeasons = filteredSeasons.filter( + (sn) => + !currentSeasons.includes(sn) && !coveredSeasons.includes(sn) + ); + + const resultingSeasonCount = + keptSeasons.length + newSeasons.length; + + if (resultingSeasonCount === 0) { + return next({ + status: 202, + message: 'No seasons available to request', + }); + } + + if (!request.ignoreQuota) { + const quotas = await requestUser.getQuota(); + + // Only the seasons getQuota already counted for this owner are + // paid for, so the edit is charged for everything it left out + const quotaWindowStart = new Date(); + if (quotas.tv.days) { + quotaWindowStart.setDate( + quotaWindowStart.getDate() - quotas.tv.days + ); + } + + const countedAlready = + !ownerChanging && + (!quotas.tv.days || request.createdAt > quotaWindowStart); + + const priorSeasonCount = countedAlready + ? request.seasons.length + : 0; + const requiredSeasons = + resultingSeasonCount - priorSeasonCount; + + if ( + quotas.tv.limit && + requiredSeasons > (quotas.tv.remaining ?? 0) + ) { + return next({ + status: 403, + message: 'Series Quota exceeded.', + }); + } + } + + request.seasons = request.seasons.filter((rs) => + keptSeasons.includes(rs.seasonNumber) + ); + + if (newSeasons.length > 0) { + logger.debug('Adding new seasons to request', { + label: 'Media Request', + newSeasons, + }); + request.seasons.push( + ...newSeasons.map( + (ns) => + new SeasonRequest({ + seasonNumber: ns, + status: MediaRequestStatus.PENDING, + }) + ) + ); + } + + await requestRepository.save(request); + + return res.status(200).json(request); + } + ); + } + + return res.status(200).json(request); + }); + }); } catch (e) { next({ status: 500, message: e.message }); } @@ -600,27 +760,30 @@ requestRoutes.put<{ requestId: string }>( requestRoutes.delete('/:requestId', async (req, res, next) => { const requestRepository = getRepository(MediaRequest); + const requestId = Number(req.params.requestId); try { - const request = await requestRepository.findOneOrFail({ - where: { id: Number(req.params.requestId) }, - relations: { requestedBy: true, modifiedBy: true }, - }); - - if ( - !req.user?.hasPermission(Permission.MANAGE_REQUESTS) && - (request.requestedBy.id !== req.user?.id || - request.status !== MediaRequestStatus.PENDING) - ) { - return next({ - status: 401, - message: 'You do not have permission to delete this request.', + return await requestLock.dispatch(requestKey(requestId), async () => { + const request = await requestRepository.findOneOrFail({ + where: { id: requestId }, + relations: { requestedBy: true, modifiedBy: true }, }); - } - await requestRepository.remove(request); + if ( + !req.user?.hasPermission(Permission.MANAGE_REQUESTS) && + (request.requestedBy.id !== req.user?.id || + request.status !== MediaRequestStatus.PENDING) + ) { + return next({ + status: 401, + message: 'You do not have permission to delete this request.', + }); + } + + await requestRepository.remove(request); - return res.status(204).send(); + return res.status(204).send(); + }); } catch (e) { logger.error('Something went wrong deleting a request.', { label: 'API', @@ -637,19 +800,29 @@ requestRoutes.post<{ isAuthenticated(Permission.MANAGE_REQUESTS), async (req, res, next) => { const requestRepository = getRepository(MediaRequest); + const requestId = Number(req.params.requestId); try { - const request = await requestRepository.findOneOrFail({ - where: { id: Number(req.params.requestId) }, - relations: { requestedBy: true, modifiedBy: true }, - }); + return await requestLock.dispatch(requestKey(requestId), async () => { + const request = await requestRepository.findOneOrFail({ + where: { id: requestId }, + relations: { requestedBy: true, modifiedBy: true }, + }); - // this also triggers updating the parent media's status & sending to *arr - request.status = MediaRequestStatus.APPROVED; - request.modifiedBy = req.user; - await requestRepository.save(request); + if (request.status !== MediaRequestStatus.FAILED) { + return next({ + status: 409, + message: 'Only failed requests can be retried.', + }); + } - return res.status(200).json(request); + // this also triggers updating the parent media's status & sending to *arr + request.status = MediaRequestStatus.APPROVED; + request.modifiedBy = req.user; + await requestRepository.save(request); + + return res.status(200).json(request); + }); } catch (e) { logger.error('Error processing request retry', { label: 'Media Request', @@ -662,38 +835,50 @@ requestRoutes.post<{ requestRoutes.post<{ requestId: string; - status: 'pending' | 'approve' | 'decline'; + status: 'approve' | 'decline'; }>( '/:requestId/:status', isAuthenticated(Permission.MANAGE_REQUESTS), async (req, res, next) => { const requestRepository = getRepository(MediaRequest); + const requestId = Number(req.params.requestId); try { - const request = await requestRepository.findOneOrFail({ - where: { id: Number(req.params.requestId) }, - relations: { requestedBy: true, modifiedBy: true }, - }); + return await requestLock.dispatch(requestKey(requestId), async () => { + const request = await requestRepository.findOneOrFail({ + where: { id: requestId }, + relations: { requestedBy: true, modifiedBy: true }, + }); - let newStatus: MediaRequestStatus; + let newStatus: MediaRequestStatus; + + switch (req.params.status) { + case 'approve': + newStatus = MediaRequestStatus.APPROVED; + break; + case 'decline': + newStatus = MediaRequestStatus.DECLINED; + break; + default: + return next({ + status: 400, + message: 'Status must be approve or decline.', + }); + } - switch (req.params.status) { - case 'pending': - newStatus = MediaRequestStatus.PENDING; - break; - case 'approve': - newStatus = MediaRequestStatus.APPROVED; - break; - case 'decline': - newStatus = MediaRequestStatus.DECLINED; - break; - } + if (request.status !== MediaRequestStatus.PENDING) { + return next({ + status: 409, + message: 'Only pending requests can be approved or declined.', + }); + } - request.status = newStatus; - request.modifiedBy = req.user; - await requestRepository.save(request); + request.status = newStatus; + request.modifiedBy = req.user; + await requestRepository.save(request); - return res.status(200).json(request); + return res.status(200).json(request); + }); } catch (e) { logger.error('Error processing request update', { label: 'Media Request', diff --git a/server/routes/search.ts b/server/routes/search.ts index cccb38f286..03f7d8cf4b 100644 --- a/server/routes/search.ts +++ b/server/routes/search.ts @@ -1,7 +1,10 @@ +import Hardcover from '@server/api/hardcover'; +import type { HardcoverSearchMultiResponse } from '@server/api/hardcover/interfaces'; import TheMovieDb from '@server/api/themoviedb'; import type { TmdbSearchMultiResponse } from '@server/api/themoviedb/interfaces'; import Media from '@server/entity/Media'; import { findSearchProvider } from '@server/lib/search'; +import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; import { mapSearchResults } from '@server/models/Search'; import { Router } from 'express'; @@ -11,7 +14,7 @@ const searchRoutes = Router(); searchRoutes.get('/', async (req, res, next) => { const queryString = req.query.query as string; const searchProvider = findSearchProvider(queryString.toLowerCase()); - let results: TmdbSearchMultiResponse; + let results: TmdbSearchMultiResponse | HardcoverSearchMultiResponse; try { if (searchProvider) { @@ -23,6 +26,20 @@ searchRoutes.get('/', async (req, res, next) => { language: (req.query.language as string) ?? req.locale, query: queryString, }); + } else if (req.query.type === 'hardcover') { + if (!getSettings().main.hardcoverapikey) { + return next({ + status: 503, + message: 'Hardcover API key is not configured.', + }); + } + + const hardcover = new Hardcover(); + + results = await hardcover.search({ + query: queryString, + page: Number(req.query.page), + }); } else { const tmdb = new TheMovieDb(); diff --git a/server/routes/series.ts b/server/routes/series.ts new file mode 100644 index 0000000000..5da6f569c3 --- /dev/null +++ b/server/routes/series.ts @@ -0,0 +1,46 @@ +import Hardcover from '@server/api/hardcover'; +import { MediaType } from '@server/constants/media'; +import Media from '@server/entity/Media'; +import { getSettings } from '@server/lib/settings'; +import logger from '@server/logger'; +import { mapSeries } from '@server/models/Series'; +import { Router } from 'express'; + +const seriesRoutes = Router(); + +seriesRoutes.get<{ id: string }>('/:id', async (req, res, next) => { + if (!getSettings().main.hardcoverapikey) { + return next({ + status: 503, + message: 'Hardcover API key is not configured.', + }); + } + + const hardcover = new Hardcover(); + + try { + const series = await hardcover.getSeries(Number(req.params.id)); + + const media = await Media.getRelatedMedia( + req.user, + series.book_series.map((book) => ({ + tmdbId: book.book.id, + mediaType: MediaType.BOOK, + })) + ); + + return res.status(200).json(mapSeries(series, media)); + } catch (e) { + logger.debug('Something went wrong retrieving series', { + label: 'API', + errorMessage: e.message, + seriesId: req.params.id, + }); + return next({ + status: 500, + message: 'Unable to retrieve series.', + }); + } +}); + +export default seriesRoutes; diff --git a/server/routes/service.ts b/server/routes/service.ts index 8f6c92b0de..b24058b8f7 100644 --- a/server/routes/service.ts +++ b/server/routes/service.ts @@ -1,4 +1,5 @@ import RadarrAPI from '@server/api/servarr/radarr'; +import ReadarrAPI from '@server/api/servarr/readarr'; import SonarrAPI from '@server/api/servarr/sonarr'; import TheMovieDb from '@server/api/themoviedb'; import type { @@ -213,4 +214,76 @@ serviceRoutes.get<{ tmdbId: string }>( } ); +serviceRoutes.get('/readarr', async (req, res) => { + const settings = getSettings(); + + const filteredReadarrServers: ServiceCommonServer[] = settings.readarr.map( + (readarr) => ({ + id: readarr.id, + name: readarr.name, + is4k: readarr.is4k, + isDefault: readarr.isDefault, + activeDirectory: readarr.activeDirectory, + activeProfileId: readarr.activeProfileId, + activeMetadataProfileId: readarr.activeMetadataProfileId, + activeTags: readarr.tags ?? [], + }) + ); + + return res.status(200).json(filteredReadarrServers); +}); + +serviceRoutes.get<{ readarrId: string }>( + '/readarr/:readarrId', + async (req, res, next) => { + const settings = getSettings(); + + const readarrSettings = settings.readarr.find( + (readarr) => readarr.id === Number(req.params.readarrId) + ); + + if (!readarrSettings) { + return next({ + status: 404, + message: 'Readarr server with provided ID does not exist.', + }); + } + + const readarr = new ReadarrAPI({ + apiKey: readarrSettings.apiKey, + url: ReadarrAPI.buildUrl(readarrSettings, '/api/v1'), + }); + + const profiles = await readarr.getProfiles(); + const rootFolders = await readarr.getRootFolders(); + const tags = await readarr.getTags(); + const metadataProfiles = await readarr.getMetadataProfiles(); + + return res.status(200).json({ + server: { + id: readarrSettings.id, + name: readarrSettings.name, + is4k: readarrSettings.is4k, + isDefault: readarrSettings.isDefault, + activeDirectory: readarrSettings.activeDirectory, + activeProfileId: readarrSettings.activeProfileId, + activeMetadataProfileId: readarrSettings.activeMetadataProfileId, + activeTags: readarrSettings.tags, + }, + profiles: profiles.map((profile) => ({ + id: profile.id, + name: profile.name, + })), + rootFolders: rootFolders.map((folder) => ({ + id: folder.id, + freeSpace: folder.freeSpace, + path: folder.path, + totalSpace: folder.totalSpace, + })), + metadataProfiles, + tags, + } as ServiceCommonServerWithDetails); + } +); + export default serviceRoutes; diff --git a/server/routes/settings/index.ts b/server/routes/settings/index.ts index 12b5746595..3f2b77dd1e 100644 --- a/server/routes/settings/index.ts +++ b/server/routes/settings/index.ts @@ -1,3 +1,4 @@ +import type { JellyfinLibrary } from '@server/api/jellyfin'; import JellyfinAPI from '@server/api/jellyfin'; import PlexAPI from '@server/api/plexapi'; import PlexTvAPI from '@server/api/plextv'; @@ -39,9 +40,11 @@ import { rescheduleJob } from 'node-schedule'; import path from 'path'; import semver from 'semver'; import { URL } from 'url'; +import { z } from 'zod'; import metadataRoutes from './metadata'; import notificationRoutes from './notifications'; import radarrRoutes from './radarr'; +import readarrRoutes from './readarr'; import sonarrRoutes from './sonarr'; const settingsRoutes = Router(); @@ -49,9 +52,14 @@ const settingsRoutes = Router(); settingsRoutes.use('/notifications', notificationRoutes); settingsRoutes.use('/radarr', radarrRoutes); settingsRoutes.use('/sonarr', sonarrRoutes); +settingsRoutes.use('/readarr', readarrRoutes); settingsRoutes.use('/discover', discoverSettingRoutes); settingsRoutes.use('/metadatas', metadataRoutes); +const libraryUpdateSchema = z.object({ + enabled: z.boolean(), +}); + const filteredMainSettings = ( user: User, main: MainSettings @@ -76,6 +84,20 @@ settingsRoutes.get('/main', (req, res, next) => { settingsRoutes.post('/main', async (req, res) => { const settings = getSettings(); + if (req.body.hardcoverapikey) { + req.body.hardcoverapikey = req.body.hardcoverapikey + .trim() + .replace(/^Bearer\s+/i, ''); + } + + if ( + req.body.hardcoverapikey && + req.body.hardcoverapikey !== settings.main.hardcoverapikey + ) { + const hardcoverCache = cacheManager.getCache('hardcover'); + hardcoverCache.flush(); + } + settings.main = merge(settings.main, req.body); await settings.save(); @@ -229,28 +251,54 @@ settingsRoutes.get('/plex/devices/servers', async (req, res, next) => { } }); -settingsRoutes.get('/plex/library', async (req, res) => { +settingsRoutes.get('/plex/library', (_req, res) => { const settings = getSettings(); - if (req.query.sync) { - const userRepository = getRepository(User); - const admin = await userRepository.findOneOrFail({ - select: { id: true, plexToken: true }, - where: { id: 1 }, - }); - const plexapi = new PlexAPI({ plexToken: admin.plexToken }); + return res.status(200).json(settings.plex.libraries); +}); - await plexapi.syncLibraries(); +settingsRoutes.put('/plex/library/:libraryId', async (req, res, next) => { + const settings = getSettings(); + + const bodyResult = libraryUpdateSchema.safeParse(req.body); + + if (!bodyResult.success) { + return next({ status: 400, message: 'Invalid request body.' }); } - const enabledLibraries = req.query.enable - ? (req.query.enable as string).split(',') - : []; - settings.plex.libraries = settings.plex.libraries.map((library) => ({ - ...library, - enabled: enabledLibraries.includes(library.id), - })); + const library = settings.plex.libraries.find( + (l) => l.id === req.params.libraryId + ); + + if (!library) { + return next({ status: 404, message: 'Library does not exist.' }); + } + + library.enabled = bodyResult.data.enabled; await settings.save(); + + return res.status(200).json(library); +}); + +settingsRoutes.post('/plex/library/sync', async (_req, res, next) => { + const settings = getSettings(); + + const userRepository = getRepository(User); + const admin = await userRepository.findOneOrFail({ + select: { id: true, plexToken: true }, + where: { id: 1 }, + }); + const plexapi = new PlexAPI({ plexToken: admin.plexToken }); + + try { + await plexapi.syncLibraries(); + } catch (e) { + return next({ + status: e.statusCode ?? 500, + message: e.errorCode ?? ApiErrorCode.Unknown, + }); + } + return res.status(200).json(settings.plex.libraries); }); @@ -330,25 +378,56 @@ settingsRoutes.post('/jellyfin', async (req, res, next) => { return res.status(200).json(settings.jellyfin); }); -settingsRoutes.get('/jellyfin/library', async (req, res, next) => { +settingsRoutes.get('/jellyfin/library', (_req, res) => { const settings = getSettings(); - if (req.query.sync) { - const userRepository = getRepository(User); - const admin = await userRepository.findOneOrFail({ - select: ['id', 'jellyfinDeviceId', 'jellyfinUserId'], - where: { id: 1 }, - order: { id: 'ASC' }, - }); - const jellyfinClient = new JellyfinAPI( - getHostname(), - settings.jellyfin.apiKey, - admin.jellyfinDeviceId ?? '' - ); + return res.status(200).json(settings.jellyfin.libraries); +}); + +settingsRoutes.put('/jellyfin/library/:libraryId', async (req, res, next) => { + const settings = getSettings(); + + const bodyResult = libraryUpdateSchema.safeParse(req.body); + + if (!bodyResult.success) { + return next({ status: 400, message: 'Invalid request body.' }); + } + + const library = settings.jellyfin.libraries.find( + (l) => l.id === req.params.libraryId + ); + + if (!library) { + return next({ status: 404, message: 'Library does not exist.' }); + } + + library.enabled = bodyResult.data.enabled; + await settings.save(); + + return res.status(200).json(library); +}); + +settingsRoutes.post('/jellyfin/library/sync', async (_req, res, next) => { + const settings = getSettings(); + + const userRepository = getRepository(User); + const admin = await userRepository.findOneOrFail({ + select: ['id', 'jellyfinDeviceId', 'jellyfinUserId'], + where: { id: 1 }, + order: { id: 'ASC' }, + }); + const jellyfinClient = new JellyfinAPI( + getHostname(), + settings.jellyfin.apiKey, + admin.jellyfinDeviceId ?? '' + ); + + jellyfinClient.setUserId(admin.jellyfinUserId ?? ''); - jellyfinClient.setUserId(admin.jellyfinUserId ?? ''); + let libraries: JellyfinLibrary[]; - const libraries = await jellyfinClient.getLibraries(); + try { + libraries = await jellyfinClient.getLibraries(); if (libraries.length === 0) { // Check if no libraries are found due to the fallback to user views @@ -365,31 +444,30 @@ settingsRoutes.get('/jellyfin/library', async (req, res, next) => { return next({ status: 404, message: ApiErrorCode.SyncErrorNoLibraries }); } - - const newLibraries: Library[] = libraries.map((library) => { - const existing = settings.jellyfin.libraries.find( - (l) => l.id === library.key && l.name === library.title - ); - - return { - id: library.key, - name: library.title, - enabled: existing?.enabled ?? false, - type: library.type, - }; + } catch (e) { + return next({ + status: e.statusCode ?? 500, + message: e.errorCode ?? ApiErrorCode.Unknown, }); - - settings.jellyfin.libraries = newLibraries; } - const enabledLibraries = req.query.enable - ? (req.query.enable as string).split(',') - : []; - settings.jellyfin.libraries = settings.jellyfin.libraries.map((library) => ({ - ...library, - enabled: enabledLibraries.includes(library.id), - })); + const newLibraries: Library[] = libraries.map((library) => { + const existing = settings.jellyfin.libraries.find( + (l) => l.id === library.key + ); + + return { + id: library.key, + name: library.title, + enabled: existing?.enabled ?? false, + type: library.type, + lastScan: existing?.lastScan, + }; + }); + + settings.jellyfin.libraries = newLibraries; await settings.save(); + return res.status(200).json(settings.jellyfin.libraries); }); @@ -761,6 +839,7 @@ settingsRoutes.get('/cache', async (_req, res) => { const tmdbImageCache = await ImageProxy.getImageStats('tmdb'); const avatarImageCache = await ImageProxy.getImageStats('avatar'); + const hardcoverImageCache = await ImageProxy.getImageStats('hardcover'); const stats: DnsStats | undefined = dnsCache?.getStats(); const entries: DnsEntries | undefined = dnsCache?.getCacheEntries(); @@ -770,6 +849,7 @@ settingsRoutes.get('/cache', async (_req, res) => { imageCache: { tmdb: tmdbImageCache, avatar: avatarImageCache, + hardcover: hardcoverImageCache, }, dnsCache: { stats, diff --git a/server/routes/settings/readarr.ts b/server/routes/settings/readarr.ts new file mode 100644 index 0000000000..94cad09a6d --- /dev/null +++ b/server/routes/settings/readarr.ts @@ -0,0 +1,185 @@ +import ReadarrAPI from '@server/api/servarr/readarr'; +import type { ReadarrSettings } from '@server/lib/settings'; +import { getSettings } from '@server/lib/settings'; +import logger from '@server/logger'; +import { Router } from 'express'; + +const readarrRoutes = Router(); + +readarrRoutes.get('/', (_req, res) => { + const settings = getSettings(); + + res.status(200).json(settings.readarr); +}); + +readarrRoutes.post('/', async (req, res) => { + const settings = getSettings(); + + const newReadarr = req.body as ReadarrSettings; + const lastItem = settings.readarr[settings.readarr.length - 1]; + newReadarr.id = lastItem ? lastItem.id + 1 : 0; + + // If we are setting this as the default, clear any previous defaults for the same type first + // ex: if is4k is true, it will only remove defaults for other servers that have is4k set to true + // and are the default + if (req.body.isDefault) { + settings.readarr + .filter((readarrInstance) => readarrInstance.is4k === req.body.is4k) + .forEach((readarrInstance) => { + readarrInstance.isDefault = false; + }); + } + + settings.readarr = [...settings.readarr, newReadarr]; + await settings.save(); + + return res.status(201).json(newReadarr); +}); + +readarrRoutes.post< + undefined, + Record, + ReadarrSettings & { tagLabel?: string } +>('/test', async (req, res, next) => { + try { + const readarr = new ReadarrAPI({ + apiKey: req.body.apiKey, + url: ReadarrAPI.buildUrl(req.body, '/api/v1'), + }); + + const urlBase = await readarr + .getSystemStatus() + .then((value) => value.urlBase) + .catch(() => req.body.baseUrl); + const profiles = await readarr.getProfiles(); + const folders = await readarr.getRootFolders(); + const metadataProfiles = await readarr.getMetadataProfiles(); + const tags = await readarr.getTags(); + + return res.status(200).json({ + profiles, + rootFolders: folders.map((folder) => ({ + id: folder.id, + path: folder.path, + })), + metadataProfiles, + tags, + urlBase, + }); + } catch (e) { + logger.error('Failed to test Readarr', { + label: 'Readarr', + message: e.message, + }); + + next({ status: 500, message: 'Failed to connect to Readarr' }); + } +}); + +readarrRoutes.put<{ id: string }, ReadarrSettings, ReadarrSettings>( + '/:id', + async (req, res, next) => { + const settings = getSettings(); + + const readarrIndex = settings.readarr.findIndex( + (r) => r.id === Number(req.params.id) + ); + + if (readarrIndex === -1) { + return next({ status: '404', message: 'Settings instance not found' }); + } + + // If we are setting this as the default, clear any previous defaults for the same type first + // ex: if is4k is true, it will only remove defaults for other servers that have is4k set to true + // and are the default + if (req.body.isDefault) { + settings.readarr + .filter((readarrInstance) => readarrInstance.is4k === req.body.is4k) + .forEach((readarrInstance) => { + readarrInstance.isDefault = false; + }); + } + + settings.readarr[readarrIndex] = { + ...req.body, + id: Number(req.params.id), + } as ReadarrSettings; + await settings.save(); + + return res.status(200).json(settings.readarr[readarrIndex]); + } +); + +readarrRoutes.get<{ id: string }>('/:id/profiles', async (req, res, next) => { + const settings = getSettings(); + + const readarrSettings = settings.readarr.find( + (r) => r.id === Number(req.params.id) + ); + + if (!readarrSettings) { + return next({ status: '404', message: 'Settings instance not found' }); + } + + const readarr = new ReadarrAPI({ + apiKey: readarrSettings.apiKey, + url: ReadarrAPI.buildUrl(readarrSettings, '/api/v1'), + }); + + const profiles = await readarr.getProfiles(); + + return res.status(200).json( + profiles.map((profile) => ({ + id: profile.id, + name: profile.name, + })) + ); +}); + +readarrRoutes.get<{ id: string }>( + '/:id/metadataprofiles', + async (req, res, next) => { + const settings = getSettings(); + + const readarrSettings = settings.readarr.find( + (r) => r.id === Number(req.params.id) + ); + + if (!readarrSettings) { + return next({ status: '404', message: 'Settings instance not found' }); + } + + const readarr = new ReadarrAPI({ + apiKey: readarrSettings.apiKey, + url: ReadarrAPI.buildUrl(readarrSettings, '/api/v1'), + }); + + const metadataProfiles = await readarr.getMetadataProfiles(); + + return res.status(200).json( + metadataProfiles.map((profile) => ({ + id: profile.id, + name: profile.name, + })) + ); + } +); + +readarrRoutes.delete<{ id: string }>('/:id', (req, res, next) => { + const settings = getSettings(); + + const readarrIndex = settings.readarr.findIndex( + (r) => r.id === Number(req.params.id) + ); + + if (readarrIndex === -1) { + return next({ status: '404', message: 'Settings instance not found' }); + } + + const removed = settings.readarr.splice(readarrIndex, 1); + settings.save(); + + return res.status(200).json(removed[0]); +}); + +export default readarrRoutes; diff --git a/server/routes/tv.ts b/server/routes/tv.ts index 743f407e48..865564ba66 100644 --- a/server/routes/tv.ts +++ b/server/routes/tv.ts @@ -42,7 +42,7 @@ tvRoutes.get('/:id', async (req, res, next) => { }, }); - const data = mapTvDetails(tv, media, onUserWatchlist); + const data = mapTvDetails(tv, media?.filter(req.user), onUserWatchlist); // TMDB issue where it doesnt fallback to English when no overview is available in requested locale. if (!data.overview) { @@ -114,7 +114,8 @@ tvRoutes.get('/:id/recommendations', async (req, res, next) => { results.results.map((result) => ({ tmdbId: result.id, mediaType: MediaType.TV, - })) + })), + { includeActiveRequest: true } ); return res.status(200).json({ @@ -158,7 +159,8 @@ tvRoutes.get('/:id/similar', async (req, res, next) => { results.results.map((result) => ({ tmdbId: result.id, mediaType: MediaType.TV, - })) + })), + { includeActiveRequest: true } ); return res.status(200).json({ diff --git a/server/routes/user/index.test.ts b/server/routes/user/index.test.ts new file mode 100644 index 0000000000..2513ea7c78 --- /dev/null +++ b/server/routes/user/index.test.ts @@ -0,0 +1,147 @@ +import assert from 'node:assert/strict'; +import { before, describe, it } from 'node:test'; + +import { MediaStatus, MediaType } from '@server/constants/media'; +import { getRepository } from '@server/datasource'; +import Media from '@server/entity/Media'; +import { User } from '@server/entity/User'; +import { Watchlist } from '@server/entity/Watchlist'; +import { getSettings } from '@server/lib/settings'; +import { checkUser, isAuthenticated } from '@server/middleware/auth'; +import authRoutes from '@server/routes/auth'; +import { setupTestDb } from '@server/test/db'; +import { + assertNoCredentials, + seedUserSettings, +} from '@server/test/userSettings'; +import type { Express } from 'express'; +import express from 'express'; +import session from 'express-session'; +import request from 'supertest'; +import userRoutes from '.'; + +let app: Express; + +function createApp() { + const app = express(); + app.use(express.json()); + app.use( + session({ + secret: 'test-secret', + resave: false, + saveUninitialized: false, + }) + ); + app.use(checkUser); + app.use('/auth', authRoutes); + app.use('/user', isAuthenticated(), userRoutes); + app.use( + ( + err: { status?: number; message?: string }, + _req: express.Request, + res: express.Response, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _next: express.NextFunction + ) => { + res + .status(err.status ?? 500) + .json({ status: err.status ?? 500, message: err.message }); + } + ); + return app; +} + +before(async () => { + app = createApp(); +}); + +setupTestDb(); + +async function loginAs(email: string, password: string) { + const settings = getSettings(); + const priorLocalLogin = settings.main.localLogin; + settings.main.localLogin = true; + + try { + const agent = request.agent(app); + const res = await agent.post('/auth/local').send({ email, password }); + assert.strictEqual(res.status, 200); + return agent; + } finally { + settings.main.localLogin = priorLocalLogin; + } +} + +describe('GET /user/:id/watchlist', () => { + it('omits notification settings from every requestedBy in the page', async () => { + const owner = await seedUserSettings('demo@seerr.dev'); + + const media = await getRepository(Media).save( + new Media({ + mediaType: MediaType.MOVIE, + tmdbId: 12345, + status: MediaStatus.UNKNOWN, + status4k: MediaStatus.UNKNOWN, + }) + ); + + await getRepository(Watchlist).save( + new Watchlist({ + mediaType: MediaType.MOVIE, + tmdbId: 12345, + title: 'Watchlisted Movie', + ratingKey: 'rk-12345', + requestedBy: owner, + media, + }) + ); + + const admin = await loginAs('admin@seerr.dev', 'test1234'); + const res = await admin.get(`/user/${owner.id}/watchlist`); + + assert.strictEqual(res.status, 200); + assert.strictEqual(res.body.results.length, 1); + assert.ok(!('settings' in res.body.results[0].requestedBy)); + assertNoCredentials(res.body); + }); +}); + +describe('GET /user/:id', () => { + it('still returns full settings to the user themselves', async () => { + const owner = await seedUserSettings('demo@seerr.dev'); + + const agent = await loginAs('demo@seerr.dev', 'test1234'); + const res = await agent.get(`/user/${owner.id}`); + + assert.strictEqual(res.status, 200); + assert.strictEqual(res.body.settings.pgpKey, 'test-pgp-key'); + assert.strictEqual( + res.body.settings.pushoverUserKey, + 'test-pushover-user-key' + ); + }); + + it('still returns full settings to a manage-users admin', async () => { + const owner = await seedUserSettings('demo@seerr.dev'); + + const admin = await loginAs('admin@seerr.dev', 'test1234'); + const res = await admin.get(`/user/${owner.id}`); + + assert.strictEqual(res.status, 200); + assert.strictEqual(res.body.settings.pgpKey, 'test-pgp-key'); + }); + + it('strips settings for an unrelated user', async () => { + const admin = await getRepository(User).findOneOrFail({ + where: { email: 'admin@seerr.dev' }, + }); + await seedUserSettings('admin@seerr.dev'); + + const agent = await loginAs('demo@seerr.dev', 'test1234'); + const res = await agent.get(`/user/${admin.id}`); + + assert.strictEqual(res.status, 200); + assert.ok(!('settings' in res.body)); + assertNoCredentials(res.body); + }); +}); diff --git a/server/routes/user/index.ts b/server/routes/user/index.ts index 663945027a..7d6234d9bb 100644 --- a/server/routes/user/index.ts +++ b/server/routes/user/index.ts @@ -244,13 +244,13 @@ router.post< const transactionalRepo = transactionalEntityManager.getRepository(UserPushSubscription); - // Check for existing subscription by auth or endpoint within transaction + // Check for existing subscription by endpoint within transaction const existingSubscription = await transactionalRepo.findOne({ relations: { user: true }, - where: [ - { auth: req.body.auth, user: { id: req.user?.id } }, - { endpoint: req.body.endpoint, user: { id: req.user?.id } }, - ], + where: { + endpoint: req.body.endpoint, + user: { id: req.user?.id }, + }, }); if (existingSubscription) { @@ -279,17 +279,15 @@ router.post< return; } - // Clean up old subscriptions from the same device (userAgent) for this user - // iOS can silently refresh endpoints, leaving stale subscriptions in the database - // Only clean up if we're creating a new subscription (not updating an existing one) - if (req.body.userAgent) { + // Clean up only true endpoint rotations. Same push-service subscription + // (matched by `auth`) but with a stale endpoint. Matching on `auth` + // avoids deleting sibling devices that only share a user agent. + if (req.body.auth) { const staleSubscriptions = await transactionalRepo.find({ relations: { user: true }, where: { - userAgent: req.body.userAgent, user: { id: req.user?.id }, - // Only remove subscriptions with different endpoints (stale ones) - // Keep subscriptions that might be from different browsers/tabs + auth: req.body.auth, endpoint: Not(req.body.endpoint), }, }); @@ -672,6 +670,7 @@ router.post( const plexUsersResponse = await mainPlexTv.getUsers(); const createdUsers: User[] = []; + let refreshedUsers = 0; for (const rawUser of plexUsersResponse.MediaContainer.User) { const account = rawUser.$; @@ -696,6 +695,7 @@ router.post( user.plexId = parseInt(account.id); } await userRepository.save(user); + refreshedUsers += 1; } else if (!body || body.plexIds.includes(account.id)) { if (await mainPlexTv.checkUserAccess(parseInt(account.id))) { const newUser = new User({ @@ -714,7 +714,10 @@ router.post( } } - return res.status(201).json(User.filterMany(createdUsers)); + return res.status(201).json({ + createdUsers: User.filterMany(createdUsers), + refreshedUsers, + }); } catch (e) { next({ status: 500, message: e.message }); } diff --git a/server/routes/user/usersettings.test.ts b/server/routes/user/usersettings.test.ts new file mode 100644 index 0000000000..90d0f7fbbc --- /dev/null +++ b/server/routes/user/usersettings.test.ts @@ -0,0 +1,134 @@ +import assert from 'node:assert/strict'; +import { before, beforeEach, describe, it, mock } from 'node:test'; + +import JellyfinAPI from '@server/api/jellyfin'; +import { MediaServerType } from '@server/constants/server'; +import { UserType } from '@server/constants/user'; +import { getRepository } from '@server/datasource'; +import { User } from '@server/entity/User'; +import { getSettings } from '@server/lib/settings'; +import { checkUser, isAuthenticated } from '@server/middleware/auth'; +import authRoutes from '@server/routes/auth'; +import { setupTestDb } from '@server/test/db'; +import type { Express } from 'express'; +import express from 'express'; +import session from 'express-session'; +import request from 'supertest'; +import userRoutes from '.'; + +const defaultAuthenticateResponse = { + User: { + Id: 'jf-link-user-001', + Name: 'linkeduser', + ServerId: 'server-1', + Policy: { IsAdministrator: false }, + }, + AccessToken: 'fake-qc-access-token', +}; + +const authenticateQCMock = mock.method( + JellyfinAPI.prototype, + 'authenticateQuickConnect', + async () => ({ ...defaultAuthenticateResponse }) +); + +let app: Express; + +function createApp() { + const app = express(); + app.use(express.json()); + app.use( + session({ + secret: 'test-secret', + resave: false, + saveUninitialized: false, + }) + ); + app.use(checkUser); + app.use('/auth', authRoutes); + app.use('/user', isAuthenticated(), userRoutes); + app.use( + ( + err: { status?: number; message?: string }, + _req: express.Request, + res: express.Response, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _next: express.NextFunction + ) => { + res + .status(err.status ?? 500) + .json({ status: err.status ?? 500, message: err.message }); + } + ); + return app; +} + +before(async () => { + app = createApp(); +}); + +setupTestDb(); + +function configureJellyfin() { + const settings = getSettings(); + settings.main.mediaServerType = MediaServerType.JELLYFIN; + settings.jellyfin.ip = 'localhost'; + settings.jellyfin.port = 8096; + settings.jellyfin.useSsl = false; + settings.jellyfin.urlBase = ''; +} + +async function loginAs(email: string, password: string) { + const settings = getSettings(); + settings.main.localLogin = true; + + const agent = request.agent(app); + const res = await agent.post('/auth/local').send({ email, password }); + + assert.strictEqual(res.status, 200); + return { agent, userId: res.body.id as number }; +} + +describe('POST /user/:id/settings/linked-accounts/jellyfin/quickconnect', () => { + beforeEach(() => { + authenticateQCMock.mock.resetCalls(); + authenticateQCMock.mock.mockImplementation(async () => ({ + ...defaultAuthenticateResponse, + })); + configureJellyfin(); + }); + + it('links the account when the media server is Jellyfin', async () => { + const { agent, userId } = await loginAs('demo@seerr.dev', 'test1234'); + + const res = await agent + .post(`/user/${userId}/settings/linked-accounts/jellyfin/quickconnect`) + .send({ secret: 'abc123def456abc123def456' }); + + assert.strictEqual(res.status, 204); + assert.strictEqual(authenticateQCMock.mock.callCount(), 1); + + const user = await getRepository(User).findOneOrFail({ + where: { id: userId }, + }); + assert.strictEqual(user.jellyfinUserId, 'jf-link-user-001'); + assert.strictEqual(user.userType, UserType.JELLYFIN); + }); + + it('returns 403 when the media server is Emby', async () => { + const { agent, userId } = await loginAs('demo@seerr.dev', 'test1234'); + getSettings().main.mediaServerType = MediaServerType.EMBY; + + const res = await agent + .post(`/user/${userId}/settings/linked-accounts/jellyfin/quickconnect`) + .send({ secret: 'abc123def456abc123def456' }); + + assert.strictEqual(res.status, 403); + assert.strictEqual(authenticateQCMock.mock.callCount(), 0); + + const user = await getRepository(User).findOneOrFail({ + where: { id: userId }, + }); + assert.strictEqual(user.jellyfinUserId, null); + }); +}); diff --git a/server/routes/user/usersettings.ts b/server/routes/user/usersettings.ts index 784f7b5fd8..4f54386920 100644 --- a/server/routes/user/usersettings.ts +++ b/server/routes/user/usersettings.ts @@ -14,6 +14,7 @@ import { Permission } from '@server/lib/permissions'; import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; import { isAuthenticated } from '@server/middleware/auth'; +import { quickConnectSecret } from '@server/routes/auth'; import { ApiError } from '@server/types/error'; import { getHostname } from '@server/utils/getHostname'; import { @@ -48,7 +49,6 @@ userSettingsRoutes.get<{ id: string }, UserSettingsGeneralResponse>( return res.status(200).json({ username: user.username, email: user.email, - discordId: user.settings?.discordId, locale: user.settings?.locale, discoverRegion: user.settings?.discoverRegion, streamingRegion: user.settings?.streamingRegion, @@ -57,6 +57,8 @@ userSettingsRoutes.get<{ id: string }, UserSettingsGeneralResponse>( movieQuotaDays: user.movieQuotaDays, tvQuotaLimit: user.tvQuotaLimit, tvQuotaDays: user.tvQuotaDays, + bookQuotaDays: user.bookQuotaDays, + bookQuotaLimit: user.bookQuotaLimit, globalMovieQuotaDays: defaultQuotas.movie.quotaDays, globalMovieQuotaLimit: defaultQuotas.movie.quotaLimit, globalTvQuotaDays: defaultQuotas.tv.quotaDays, @@ -117,12 +119,13 @@ userSettingsRoutes.post< user.movieQuotaLimit = req.body.movieQuotaLimit; user.tvQuotaDays = req.body.tvQuotaDays; user.tvQuotaLimit = req.body.tvQuotaLimit; + user.bookQuotaDays = req.body.bookQuotaDays; + user.bookQuotaLimit = req.body.bookQuotaLimit; } if (!user.settings) { user.settings = new UserSettings({ user: req.user, - discordId: req.body.discordId, locale: req.body.locale, discoverRegion: req.body.discoverRegion, streamingRegion: req.body.streamingRegion, @@ -131,7 +134,6 @@ userSettingsRoutes.post< watchlistSyncTv: req.body.watchlistSyncTv, }); } else { - user.settings.discordId = req.body.discordId; user.settings.locale = req.body.locale; user.settings.discoverRegion = req.body.discoverRegion; user.settings.streamingRegion = req.body.streamingRegion; @@ -144,7 +146,6 @@ userSettingsRoutes.post< return res.status(200).json({ username: savedUser.username, - discordId: savedUser.settings?.discordId, locale: savedUser.settings?.locale, discoverRegion: savedUser.settings?.discoverRegion, streamingRegion: savedUser.settings?.streamingRegion, @@ -518,6 +519,81 @@ userSettingsRoutes.delete<{ id: string }>( } ); +userSettingsRoutes.post<{ secret: string }>( + '/linked-accounts/jellyfin/quickconnect', + isOwnProfile(), + async (req, res) => { + const settings = getSettings(); + const userRepository = getRepository(User); + + if (!req.user) { + return res.status(401).json({ code: ApiErrorCode.Unauthorized }); + } + + const result = quickConnectSecret.safeParse(req.body); + if (!result.success) { + return res.status(400).json({ message: 'Invalid secret format' }); + } + + const { secret } = result.data; + + if ( + settings.main.mediaServerType !== MediaServerType.JELLYFIN && + settings.main.mediaServerType !== MediaServerType.EMBY + ) { + return res + .status(500) + .json({ message: 'Jellyfin/Emby login is disabled' }); + } + + if (settings.main.mediaServerType !== MediaServerType.JELLYFIN) { + return res + .status(403) + .json({ message: 'Quick Connect is only supported by Jellyfin.' }); + } + + const hostname = getHostname(); + const jellyfinServer = new JellyfinAPI(hostname); + + try { + const account = await jellyfinServer.authenticateQuickConnect(secret); + + if ( + await userRepository.exist({ + where: { jellyfinUserId: account.User.Id }, + }) + ) { + return res.status(422).json({ + message: 'The specified account is already linked to a Seerr user', + }); + } + + const user = req.user; + const deviceId = Buffer.from( + user.id === 1 ? 'BOT_seerr' : `BOT_seerr_${user.username ?? ''}` + ).toString('base64'); + + user.userType = UserType.JELLYFIN; + user.jellyfinUserId = account.User.Id; + user.jellyfinUsername = account.User.Name; + user.jellyfinAuthToken = account.AccessToken; + user.jellyfinDeviceId = deviceId; + await userRepository.save(user); + + return res.status(204).send(); + } catch (e) { + logger.error('Failed to link account with Quick Connect.', { + label: 'API', + ip: req.ip, + error: e, + }); + + const status = e instanceof ApiError ? e.statusCode : 500; + return res.status(status).send(); + } + } +); + userSettingsRoutes.get<{ id: string }, UserSettingsNotificationsResponse>( '/notifications', isOwnProfileOrAdmin(), @@ -543,7 +619,7 @@ userSettingsRoutes.get<{ id: string }, UserSettingsNotificationsResponse>( settings?.discord.enabled && settings.discord.options.enableMentions ? settings.discord.types : 0, - discordId: user.settings?.discordId, + discordIds: user.settings?.discordIds ?? [], pushbulletAccessToken: user.settings?.pushbulletAccessToken, pushoverApplicationToken: user.settings?.pushoverApplicationToken, pushoverUserKey: user.settings?.pushoverUserKey, @@ -585,11 +661,14 @@ userSettingsRoutes.post<{ id: string }, UserSettingsNotificationsResponse>( }); } + const discordIds = + req.body.discordIds?.filter((id: string) => id !== '') ?? []; + if (!user.settings) { user.settings = new UserSettings({ user: req.user, pgpKey: req.body.pgpKey, - discordId: req.body.discordId, + discordIds, pushbulletAccessToken: req.body.pushbulletAccessToken, pushoverApplicationToken: req.body.pushoverApplicationToken, pushoverUserKey: req.body.pushoverUserKey, @@ -600,7 +679,7 @@ userSettingsRoutes.post<{ id: string }, UserSettingsNotificationsResponse>( }); } else { user.settings.pgpKey = req.body.pgpKey; - user.settings.discordId = req.body.discordId; + user.settings.discordIds = discordIds; user.settings.pushbulletAccessToken = req.body.pushbulletAccessToken; user.settings.pushoverApplicationToken = req.body.pushoverApplicationToken; @@ -621,7 +700,7 @@ userSettingsRoutes.post<{ id: string }, UserSettingsNotificationsResponse>( return res.status(200).json({ pgpKey: user.settings.pgpKey, - discordId: user.settings.discordId, + discordIds: user.settings.discordIds ?? [], pushbulletAccessToken: user.settings.pushbulletAccessToken, pushoverApplicationToken: user.settings.pushoverApplicationToken, pushoverUserKey: user.settings.pushoverUserKey, diff --git a/server/scripts/prepareTestDb.ts b/server/scripts/prepareTestDb.ts index ccde8bb74b..bd917c447d 100644 --- a/server/scripts/prepareTestDb.ts +++ b/server/scripts/prepareTestDb.ts @@ -12,6 +12,7 @@ const prepareDb = async () => { await seedTestDb({ preserveDb: process.env.PRESERVE_DB === 'true', withMigrations: process.env.WITH_MIGRATIONS === 'true', + allowOutsideTest: true, }); }; diff --git a/server/subscriber/IssueCommentSubscriber.ts b/server/subscriber/IssueCommentSubscriber.ts index 3a7a37ebd9..cfa9681aa8 100644 --- a/server/subscriber/IssueCommentSubscriber.ts +++ b/server/subscriber/IssueCommentSubscriber.ts @@ -1,7 +1,7 @@ +import Hardcover from '@server/api/hardcover'; import TheMovieDb from '@server/api/themoviedb'; import { IssueType, IssueTypeName } from '@server/constants/issue'; import { MediaType } from '@server/constants/media'; -import { getRepository } from '@server/datasource'; import IssueComment from '@server/entity/IssueComment'; import Media from '@server/entity/Media'; import { User } from '@server/entity/User'; @@ -9,7 +9,11 @@ import notificationManager, { Notification } from '@server/lib/notifications'; import { Permission } from '@server/lib/permissions'; import logger from '@server/logger'; import { sortBy } from 'lodash'; -import type { EntitySubscriberInterface, InsertEvent } from 'typeorm'; +import type { + EntityManager, + EntitySubscriberInterface, + InsertEvent, +} from 'typeorm'; import { EventSubscriber } from 'typeorm'; @EventSubscriber() @@ -18,24 +22,28 @@ export class IssueCommentSubscriber implements EntitySubscriberInterface): void { + public async afterInsert(event: InsertEvent): Promise { if (!event.entity) { return; } - this.sendIssueCommentNotification(event.entity); + await this.sendIssueCommentNotification(event.manager, event.entity); } } diff --git a/server/subscriber/IssueSubscriber.ts b/server/subscriber/IssueSubscriber.ts index e600cd14f5..d301984a2f 100644 --- a/server/subscriber/IssueSubscriber.ts +++ b/server/subscriber/IssueSubscriber.ts @@ -1,3 +1,4 @@ +import Hardcover from '@server/api/hardcover'; import TheMovieDb from '@server/api/themoviedb'; import { IssueStatus, IssueType, IssueTypeName } from '@server/constants/issue'; import { MediaType } from '@server/constants/media'; @@ -23,6 +24,7 @@ export class IssueSubscriber implements EntitySubscriberInterface { let title: string; let image: string; const tmdb = new TheMovieDb(); + const hardcover = new Hardcover(); try { if (entity.media.mediaType === MediaType.MOVIE) { @@ -32,6 +34,17 @@ export class IssueSubscriber implements EntitySubscriberInterface { movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : '' }`; image = `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`; + } else if (entity.media.mediaType === MediaType.BOOK) { + const book = await hardcover.getBook(entity.media.tmdbId); + + title = `${book.title}${ + book.release_date ? ` (${book.release_date.slice(0, 4)})` : '' + }`; + image = book.image?.url + ? book.image.url + : `https://assets.hardcover.app/static/covers/cover${ + (entity.media.tmdbId % 9) + 1 + }.png`; } else { const tvshow = await tmdb.getTvShow({ tvId: entity.media.tmdbId }); diff --git a/server/subscriber/MediaRequestSubscriber.ts b/server/subscriber/MediaRequestSubscriber.ts index 0f10c33804..6e7a40c998 100644 --- a/server/subscriber/MediaRequestSubscriber.ts +++ b/server/subscriber/MediaRequestSubscriber.ts @@ -1,5 +1,8 @@ +import Hardcover from '@server/api/hardcover'; import type { RadarrMovieOptions } from '@server/api/servarr/radarr'; import RadarrAPI from '@server/api/servarr/radarr'; +import type { ReadarrBookOptions } from '@server/api/servarr/readarr'; +import ReadarrAPI from '@server/api/servarr/readarr'; import type { AddSeriesOptions, SonarrSeries, @@ -20,6 +23,7 @@ import SeasonRequest from '@server/entity/SeasonRequest'; import notificationManager, { Notification } from '@server/lib/notifications'; import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; +import { withNestedTransaction } from '@server/utils/nestedTransaction'; import { isEqual, truncate } from 'lodash'; import type { EntityManager, @@ -180,13 +184,16 @@ export class MediaRequestSubscriber implements EntitySubscriberInterface { + public async sendToRadarr( + entity: MediaRequest, + manager: EntityManager + ): Promise { if ( entity.status === MediaRequestStatus.APPROVED && entity.type === MediaType.MOVIE ) { try { - const mediaRepository = getRepository(Media); + const mediaRepository = manager.getRepository(Media); const settings = getSettings(); if (settings.radarr.length === 0 && !settings.radarr[0]) { logger.info( @@ -280,13 +287,6 @@ export class MediaRequestSubscriber implements EntitySubscriberInterface { + // Needs its own repository as this runs detached from the request transaction + const mediaRepository = getRepository(Media); // We grab media again here to make sure we have the latest version of it const media = await mediaRepository.findOne({ where: { id: entity.media.id }, @@ -444,8 +453,8 @@ export class MediaRequestSubscriber implements EntitySubscriberInterface { + public async sendToSonarr( + entity: MediaRequest, + manager: EntityManager + ): Promise { if ( entity.status === MediaRequestStatus.APPROVED && entity.type === MediaType.TV ) { try { - const mediaRepository = getRepository(Media); + const mediaRepository = manager.getRepository(Media); const settings = getSettings(); if (settings.sonarr.length === 0 && !settings.sonarr[0]) { logger.warn( @@ -549,7 +561,7 @@ export class MediaRequestSubscriber implements EntitySubscriberInterface { season.status = MediaRequestStatus.COMPLETED; @@ -567,43 +579,44 @@ export class MediaRequestSubscriber implements EntitySubscriberInterface keyword.id === ANIME_KEYWORD_ID + ); - // Change series type to anime if the anime keyword is present on tmdb - if ( - series.keywords.results.some( - (keyword) => keyword.id === ANIME_KEYWORD_ID - ) - ) { + // seriesType only controls how Sonarr parses/numbers episodes + // it is sent in the addSeries payload and must not gate anime routing + let seriesType: SonarrSeries['seriesType'] = + sonarrSettings.seriesType ?? 'standard'; + + if (isAnime) { seriesType = sonarrSettings.animeSeriesType ?? 'anime'; } let rootFolder = - seriesType === 'anime' && sonarrSettings.activeAnimeDirectory + isAnime && sonarrSettings.activeAnimeDirectory ? sonarrSettings.activeAnimeDirectory : sonarrSettings.activeDirectory; let qualityProfile = - seriesType === 'anime' && sonarrSettings.activeAnimeProfileId + isAnime && sonarrSettings.activeAnimeProfileId ? sonarrSettings.activeAnimeProfileId : sonarrSettings.activeProfileId; let languageProfile = - seriesType === 'anime' && sonarrSettings.activeAnimeLanguageProfileId + isAnime && sonarrSettings.activeAnimeLanguageProfileId ? sonarrSettings.activeAnimeLanguageProfileId : sonarrSettings.activeLanguageProfileId; - let tags = - seriesType === 'anime' - ? sonarrSettings.animeTags - ? [...sonarrSettings.animeTags] - : [] - : sonarrSettings.tags - ? [...sonarrSettings.tags] - : []; + let tags = isAnime + ? sonarrSettings.animeTags + ? [...sonarrSettings.animeTags] + : [] + : sonarrSettings.tags + ? [...sonarrSettings.tags] + : []; if ( entity.rootFolder && @@ -719,6 +732,8 @@ export class MediaRequestSubscriber implements EntitySubscriberInterface { + // Needs its own repository as this runs detached from the request transaction + const mediaRepository = getRepository(Media); // We grab media again here to make sure we have the latest version of it const media = await mediaRepository.findOne({ where: { id: entity.media.id }, @@ -787,8 +802,8 @@ export class MediaRequestSubscriber implements EntitySubscriberInterface { - const mediaRepository = getRepository(Media); + public async sendToReadarr( + entity: MediaRequest, + manager: EntityManager + ): Promise { + if ( + entity.status === MediaRequestStatus.APPROVED && + entity.type === MediaType.BOOK + ) { + try { + const mediaRepository = manager.getRepository(Media); + const settings = getSettings(); + if (settings.readarr.length === 0 && !settings.readarr[0]) { + logger.info( + 'No Readarr server configured, skipping request processing', + { + label: 'Media Request', + requestId: entity.id, + mediaId: entity.media.id, + } + ); + return; + } + + let readarrSettings = settings.readarr.find( + (readarr) => readarr.isDefault && readarr.is4k === entity.is4k + ); + + if ( + entity.serverId !== null && + entity.serverId >= 0 && + readarrSettings?.id !== entity.serverId + ) { + readarrSettings = settings.readarr.find( + (readarr) => readarr.id === entity.serverId + ); + logger.info( + `Request has an override server: ${readarrSettings?.name}`, + { + label: 'Media Request', + requestId: entity.id, + mediaId: entity.media.id, + } + ); + } + + if (!readarrSettings) { + logger.warn( + `There is no default ${ + entity.is4k ? 'audiobook ' : '' + }Readarr server configured. Did you set any of your ${ + entity.is4k ? 'audiobook ' : '' + }Readarr servers as default?`, + { + label: 'Media Request', + requestId: entity.id, + mediaId: entity.media.id, + } + ); + return; + } + + let rootFolder = readarrSettings.activeDirectory; + let qualityProfile = readarrSettings.activeProfileId; + let metadataProfile = readarrSettings.activeMetadataProfileId; + let tags = readarrSettings.tags ? [...readarrSettings.tags] : []; + + if ( + entity.rootFolder && + entity.rootFolder !== '' && + entity.rootFolder !== readarrSettings.activeDirectory + ) { + rootFolder = entity.rootFolder; + logger.info(`Request has an override root folder: ${rootFolder}`, { + label: 'Media Request', + requestId: entity.id, + mediaId: entity.media.id, + }); + } + + if ( + entity.profileId && + entity.profileId !== readarrSettings.activeProfileId + ) { + qualityProfile = entity.profileId; + logger.info( + `Request has an override quality profile ID: ${qualityProfile}`, + { + label: 'Media Request', + requestId: entity.id, + mediaId: entity.media.id, + } + ); + } + + if ( + entity.metadataProfileId && + entity.metadataProfileId !== readarrSettings.activeMetadataProfileId + ) { + metadataProfile = entity.metadataProfileId; + logger.info( + `Request has an override metadata profile ID: ${metadataProfile}`, + { + label: 'Media Request', + requestId: entity.id, + mediaId: entity.media.id, + } + ); + } + + if (entity.tags && !isEqual(entity.tags, readarrSettings.tags)) { + tags = entity.tags; + logger.info(`Request has override tags`, { + label: 'Media Request', + requestId: entity.id, + mediaId: entity.media.id, + tagIds: tags, + }); + } + + const hardcover = new Hardcover(); + const readarr = new ReadarrAPI({ + apiKey: readarrSettings.apiKey, + url: ReadarrAPI.buildUrl(readarrSettings, '/api/v1'), + }); + const book = await hardcover.getBook(entity.media.tmdbId); + + const media = await mediaRepository.findOne({ + where: { id: entity.media.id }, + }); + + if (!media) { + logger.error('Media data not found', { + label: 'Media Request', + requestId: entity.id, + mediaId: entity.media.id, + }); + return; + } + + if (readarrSettings.tagRequests) { + const readarrTags = await readarr.getTags(); + // old tags had space around the hyphen + let userTag = readarrTags.find((v) => + v.label.startsWith(entity.requestedBy.id + ' - ') + ); + // new tags do not have spaces around the hyphen, since spaces are not allowed anymore + if (!userTag) { + userTag = readarrTags.find((v) => + v.label.startsWith(entity.requestedBy.id + '-') + ); + } + if (!userTag) { + logger.info(`Requester has no active tag. Creating new`, { + label: 'Media Request', + requestId: entity.id, + mediaId: entity.media.id, + userId: entity.requestedBy.id, + newTag: + entity.requestedBy.id + '-' + entity.requestedBy.displayName, + }); + userTag = await readarr.createTag({ + label: + entity.requestedBy.id + '-' + entity.requestedBy.displayName, + }); + } + if (userTag.id) { + if (!tags?.find((v) => v === userTag?.id)) { + tags?.push(userTag.id); + } + } else { + logger.warn(`Requester has no tag and failed to add one`, { + label: 'Media Request', + requestId: entity.id, + mediaId: entity.media.id, + userId: entity.requestedBy.id, + radarrServer: + readarrSettings.hostname + ':' + readarrSettings.port, + }); + } + } + + if ( + media[entity.is4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE + ) { + logger.warn('Media already exists, marking request as APPROVED', { + label: 'Media Request', + requestId: entity.id, + mediaId: entity.media.id, + }); + + const requestRepository = manager.getRepository(MediaRequest); + entity.status = MediaRequestStatus.APPROVED; + await requestRepository.save(entity); + return; + } + + const readarrBookOptions: ReadarrBookOptions = { + profileId: qualityProfile, + qualityProfileId: qualityProfile, + metadataProfileId: metadataProfile, + rootFolderPath: rootFolder, + title: book.title, + hcId: book.id, + authorHcId: book.contributions[0].author.id, + monitored: true, + tags, + searchNow: !readarrSettings.preventSearch, + }; + + // Run entity asynchronously so we don't wait for it on the UI side + readarr + .addBook(readarrBookOptions) + .then(async (readarrBook) => { + const mediaRepository = getRepository(Media); + // We grab media again here to make sure we have the latest version of it + const media = await mediaRepository.findOne({ + where: { id: entity.media.id }, + }); + + if (!media) { + throw new Error('Media data not found'); + } + + media[entity.is4k ? 'externalServiceId4k' : 'externalServiceId'] = + readarrBook.id; + media[ + entity.is4k ? 'externalServiceSlug4k' : 'externalServiceSlug' + ] = readarrBook.titleSlug; + media[entity.is4k ? 'serviceId4k' : 'serviceId'] = + readarrSettings?.id; + await mediaRepository.save(media); + }) + .catch(async () => { + const requestRepository = getRepository(MediaRequest); + + entity.status = MediaRequestStatus.FAILED; + requestRepository.save(entity); + + logger.warn( + 'Something went wrong sending book request to Readarr, marking status as FAILED', + { + label: 'Media Request', + requestId: entity.id, + mediaId: entity.media.id, + readarrBookOptions, + } + ); + + MediaRequest.sendNotification( + entity, + media, + Notification.MEDIA_FAILED + ); + }) + .finally(() => { + readarr.clearCache({ + tmdbId: book.id, + externalId: entity.is4k + ? media.externalServiceId4k + : media.externalServiceId, + }); + }); + logger.info('Sent request to Readarr', { + label: 'Media Request', + requestId: entity.id, + mediaId: entity.media.id, + }); + } catch (e) { + logger.error('Something went wrong sending request to Readarr', { + label: 'Media Request', + errorMessage: e.message, + requestId: entity.id, + mediaId: entity.media.id, + }); + throw new Error(e.message); + } + } + } + + private async notifyAvailableBook( + entity: MediaRequest, + event?: UpdateEvent + ) { + // Get fresh media state using event manager + let latestMedia: Media | null = null; + if (event?.manager) { + latestMedia = await event.manager.findOne(Media, { + where: { id: entity.media.id }, + }); + } + if (!latestMedia) { + const mediaRepository = getRepository(Media); + latestMedia = await mediaRepository.findOne({ + where: { id: entity.media.id }, + }); + } + + // Check availability using fresh media state + if ( + !latestMedia || + latestMedia[entity.is4k ? 'status4k' : 'status'] !== MediaStatus.AVAILABLE + ) { + return; + } + + const hardcover = new Hardcover(); + + try { + const book = await hardcover.getBook(entity.media.tmdbId); + + notificationManager.sendNotification(Notification.MEDIA_AVAILABLE, { + event: `${entity.is4k ? 'Audiobook ' : 'Book'} Request Now Available`, + notifyAdmin: false, + notifySystem: true, + notifyUser: entity.requestedBy, + subject: `${book.title}${ + book.release_date ? ` (${book.release_date.slice(0, 4)})` : '' + }`, + message: book.description + ? truncate(book.description, { + length: 500, + separator: /\s/, + omission: '…', + }) + : 'No description available.', + media: latestMedia, + image: book.image?.url, + request: entity, + }); + } catch (e) { + logger.error('Something went wrong sending book notification(s)', { + label: 'Notifications', + errorMessage: e.message, + mediaId: entity.id, + }); + } + } + + public async updateParentStatus( + manager: EntityManager, + entity: MediaRequest + ): Promise { + const mediaRepository = manager.getRepository(Media); const media = await mediaRepository.findOne({ where: { id: entity.media.id }, }); @@ -832,8 +1188,8 @@ export class MediaRequestSubscriber implements EntitySubscriberInterface { const fullMedia = await manager.findOneOrFail(Media, { where: { id: entity.media.id }, - relations: { requests: true }, + relations: { requests: { seasons: true }, seasons: true }, }); + const hasActive = fullMedia.requests.some( + (request) => + !request.is4k && + request.status !== MediaRequestStatus.COMPLETED && + request.status !== MediaRequestStatus.DECLINED + ); + const hasActive4k = fullMedia.requests.some( + (request) => + request.is4k && + request.status !== MediaRequestStatus.COMPLETED && + request.status !== MediaRequestStatus.DECLINED + ); + const needsStatusUpdate = - !fullMedia.requests.some((request) => !request.is4k) && - fullMedia.status !== MediaStatus.AVAILABLE; + !hasActive && + fullMedia.status !== MediaStatus.AVAILABLE && + fullMedia.status !== MediaStatus.PARTIALLY_AVAILABLE; const needs4kStatusUpdate = - !fullMedia.requests.some((request) => request.is4k) && - fullMedia.status4k !== MediaStatus.AVAILABLE; + !hasActive4k && + fullMedia.status4k !== MediaStatus.AVAILABLE && + fullMedia.status4k !== MediaStatus.PARTIALLY_AVAILABLE; if (needsStatusUpdate || needs4kStatusUpdate) { // Re-fetch WITHOUT requests to avoid cascade issues on save @@ -967,14 +1339,59 @@ export class MediaRequestSubscriber implements EntitySubscriberInterface !r.is4k && r.status === MediaRequestStatus.COMPLETED + ); + cleanMedia.status = hadCompleted + ? MediaStatus.DELETED + : MediaStatus.UNKNOWN; } + if (needs4kStatusUpdate) { - cleanMedia.status4k = MediaStatus.UNKNOWN; + const hadCompleted4k = fullMedia.requests.some( + (r) => r.is4k && r.status === MediaRequestStatus.COMPLETED + ); + cleanMedia.status4k = hadCompleted4k + ? MediaStatus.DELETED + : MediaStatus.UNKNOWN; } await manager.save(cleanMedia); } + + // Reset stale seasons or re-requests fail ("No seasons available to request") + if (fullMedia.mediaType === MediaType.TV) { + const statusKey = entity.is4k ? 'status4k' : 'status'; + const removedSeasonNumbers = new Set( + entity.seasons.map((s) => s.seasonNumber) + ); + const activeSeasonNumbers = new Set( + fullMedia.requests + .filter( + (request) => + request.is4k === entity.is4k && + request.status !== MediaRequestStatus.COMPLETED && + request.status !== MediaRequestStatus.DECLINED + ) + .flatMap((request) => request.seasons.map((s) => s.seasonNumber)) + ); + + const changedSeasons: Season[] = []; + for (const season of fullMedia.seasons) { + if ( + (season[statusKey] === MediaStatus.PENDING || + season[statusKey] === MediaStatus.PROCESSING) && + removedSeasonNumbers.has(season.seasonNumber) && + !activeSeasonNumbers.has(season.seasonNumber) + ) { + season[statusKey] = MediaStatus.UNKNOWN; + changedSeasons.push(season); + } + } + if (changedSeasons.length) { + await manager.save(changedSeasons); + } + } } public async afterUpdate(event: UpdateEvent): Promise { @@ -983,8 +1400,9 @@ export class MediaRequestSubscriber implements EntitySubscriberInterface { + await this.updateParentStatus(manager, event.entity as MediaRequest); + }); if (event.entity.status === MediaRequestStatus.COMPLETED) { if (event.entity.media.mediaType === MediaType.MOVIE) { @@ -1003,6 +1423,9 @@ export class MediaRequestSubscriber implements EntitySubscriberInterface { + await this.updateParentStatus(manager, event.entity as MediaRequest); + }); } catch (e) { logger.error( 'Error while updating parent status in afterInsert subscriber', diff --git a/server/subscriber/MediaSubscriber.ts b/server/subscriber/MediaSubscriber.ts index 333fa3961a..77ad19c6bc 100644 --- a/server/subscriber/MediaSubscriber.ts +++ b/server/subscriber/MediaSubscriber.ts @@ -3,18 +3,27 @@ import { MediaStatus, MediaType, } from '@server/constants/media'; -import { getRepository } from '@server/datasource'; import Media from '@server/entity/Media'; import { MediaRequest } from '@server/entity/MediaRequest'; import Season from '@server/entity/Season'; import SeasonRequest from '@server/entity/SeasonRequest'; -import type { EntitySubscriberInterface, UpdateEvent } from 'typeorm'; +import logger from '@server/logger'; +import { withNestedTransaction } from '@server/utils/nestedTransaction'; +import type { + EntityManager, + EntitySubscriberInterface, + UpdateEvent, +} from 'typeorm'; import { EventSubscriber, In } from 'typeorm'; @EventSubscriber() export class MediaSubscriber implements EntitySubscriberInterface { - private async updateChildRequestStatus(event: Media, is4k: boolean) { - const requestRepository = getRepository(MediaRequest); + private async updateChildRequestStatus( + manager: EntityManager, + event: Media, + is4k: boolean + ) { + const requestRepository = manager.getRepository(MediaRequest); const requests = await requestRepository.find({ where: { media: { id: event.id } }, @@ -32,12 +41,13 @@ export class MediaSubscriber implements EntitySubscriberInterface { } private async updateRelatedMediaRequest( + manager: EntityManager, event: Media, databaseEvent: Media, is4k: boolean ) { - const requestRepository = getRepository(MediaRequest); - const seasonRequestRepository = getRepository(SeasonRequest); + const requestRepository = manager.getRepository(MediaRequest); + const seasonRequestRepository = manager.getRepository(SeasonRequest); const relatedRequests = await requestRepository.find({ relations: { @@ -63,49 +73,53 @@ export class MediaSubscriber implements EntitySubscriberInterface { MediaStatus.AVAILABLE || event[request.is4k ? 'status4k' : 'status'] === MediaStatus.DELETED) && - event.mediaType === MediaType.MOVIE + (event.mediaType === MediaType.MOVIE || + event.mediaType === MediaType.BOOK) ) { shouldComplete = true; } else if (event.mediaType === 'tv') { - const allSeasonResults = await Promise.all( - request.seasons.map(async (requestSeason) => { - const matchingSeason = event.seasons.find( - (mediaSeason) => - mediaSeason.seasonNumber === requestSeason.seasonNumber - ); - const matchingOldSeason = databaseEvent.seasons.find( - (oldSeason) => - oldSeason.seasonNumber === requestSeason.seasonNumber - ); - - if (!matchingSeason) { - return false; - } - - const currentSeasonStatus = - matchingSeason[request.is4k ? 'status4k' : 'status']; - const previousSeasonStatus = - matchingOldSeason?.[request.is4k ? 'status4k' : 'status']; - - const hasStatusChanged = - currentSeasonStatus !== previousSeasonStatus; - - const shouldUpdate = - (hasStatusChanged || - requestSeason.status === MediaRequestStatus.COMPLETED) && - (currentSeasonStatus === MediaStatus.AVAILABLE || - currentSeasonStatus === MediaStatus.DELETED); - - if (shouldUpdate) { - requestSeason.status = MediaRequestStatus.COMPLETED; - await seasonRequestRepository.save(requestSeason); - - return true; - } - - return false; - }) - ); + const allSeasonResults: boolean[] = []; + + // Sequential on purpose as these saves share the transaction's connection + for (const requestSeason of request.seasons) { + const matchingSeason = event.seasons.find( + (mediaSeason) => + mediaSeason.seasonNumber === requestSeason.seasonNumber + ); + const matchingOldSeason = databaseEvent.seasons.find( + (oldSeason) => + oldSeason.seasonNumber === requestSeason.seasonNumber + ); + + if (!matchingSeason) { + allSeasonResults.push(false); + continue; + } + + const currentSeasonStatus = + matchingSeason[request.is4k ? 'status4k' : 'status']; + const previousSeasonStatus = + matchingOldSeason?.[request.is4k ? 'status4k' : 'status']; + + const hasStatusChanged = + currentSeasonStatus !== previousSeasonStatus; + + const shouldUpdate = + (hasStatusChanged || + requestSeason.status === MediaRequestStatus.COMPLETED) && + (currentSeasonStatus === MediaStatus.AVAILABLE || + currentSeasonStatus === MediaStatus.DELETED); + + if (shouldUpdate) { + requestSeason.status = MediaRequestStatus.COMPLETED; + await seasonRequestRepository.save(requestSeason); + + allSeasonResults.push(true); + continue; + } + + allSeasonResults.push(false); + } const allSeasonsReady = allSeasonResults.every((result) => result); shouldComplete = allSeasonsReady; @@ -122,22 +136,58 @@ export class MediaSubscriber implements EntitySubscriberInterface { } public async beforeUpdate(event: UpdateEvent): Promise { - if (!event.entity) { + if (!event.entity || !event.databaseEntity) { return; } - if ( - event.entity.status === MediaStatus.AVAILABLE && - event.databaseEntity.status === MediaStatus.PENDING - ) { - this.updateChildRequestStatus(event.entity as Media, false); + try { + if ( + event.entity.status === MediaStatus.AVAILABLE && + event.databaseEntity?.status === MediaStatus.PENDING + ) { + await withNestedTransaction(event.manager, async (manager) => { + await this.updateChildRequestStatus( + manager, + event.entity as Media, + false + ); + }); + } + } catch (e) { + logger.error( + 'Error while updating child request status in beforeUpdate subscriber', + { + label: 'Media', + mediaId: event.entity.id, + is4k: false, + errorMessage: e instanceof Error ? e.message : String(e), + } + ); } - if ( - event.entity.status4k === MediaStatus.AVAILABLE && - event.databaseEntity.status4k === MediaStatus.PENDING - ) { - this.updateChildRequestStatus(event.entity as Media, true); + try { + if ( + event.entity.status4k === MediaStatus.AVAILABLE && + event.databaseEntity?.status4k === MediaStatus.PENDING + ) { + await withNestedTransaction(event.manager, async (manager) => { + await this.updateChildRequestStatus( + manager, + event.entity as Media, + true + ); + }); + } + } catch (e) { + logger.error( + 'Error while updating child request status in beforeUpdate subscriber', + { + label: 'Media', + mediaId: event.entity.id, + is4k: true, + errorMessage: e instanceof Error ? e.message : String(e), + } + ); } // Manually load related seasons into databaseEntity @@ -153,7 +203,7 @@ export class MediaSubscriber implements EntitySubscriberInterface { } public async afterUpdate(event: UpdateEvent): Promise { - if (!event.entity) { + if (!event.entity || !event.databaseEntity) { return; } @@ -174,28 +224,59 @@ export class MediaSubscriber implements EntitySubscriberInterface { }); }; - if ( - (event.entity.status !== event.databaseEntity?.status || - (event.entity.mediaType === MediaType.TV && - seasonStatusCheck(false))) && - validStatuses.includes(event.entity.status) - ) { - this.updateRelatedMediaRequest( - event.entity as Media, - event.databaseEntity as Media, - false + try { + if ( + (event.entity.status !== event.databaseEntity?.status || + (event.entity.mediaType === MediaType.TV && + seasonStatusCheck(false))) && + validStatuses.includes(event.entity.status) + ) { + await withNestedTransaction(event.manager, async (manager) => { + await this.updateRelatedMediaRequest( + manager, + event.entity as Media, + event.databaseEntity as Media, + false + ); + }); + } + } catch (e) { + logger.error( + 'Error while updating related requests in afterUpdate subscriber', + { + label: 'Media', + mediaId: event.entity.id, + is4k: false, + errorMessage: e instanceof Error ? e.message : String(e), + } ); } - if ( - (event.entity.status4k !== event.databaseEntity?.status4k || - (event.entity.mediaType === MediaType.TV && seasonStatusCheck(true))) && - validStatuses.includes(event.entity.status4k) - ) { - this.updateRelatedMediaRequest( - event.entity as Media, - event.databaseEntity as Media, - true + try { + if ( + (event.entity.status4k !== event.databaseEntity?.status4k || + (event.entity.mediaType === MediaType.TV && + seasonStatusCheck(true))) && + validStatuses.includes(event.entity.status4k) + ) { + await withNestedTransaction(event.manager, async (manager) => { + await this.updateRelatedMediaRequest( + manager, + event.entity as Media, + event.databaseEntity as Media, + true + ); + }); + } + } catch (e) { + logger.error( + 'Error while updating related requests in afterUpdate subscriber', + { + label: 'Media', + mediaId: event.entity.id, + is4k: true, + errorMessage: e instanceof Error ? e.message : String(e), + } ); } } diff --git a/server/templates/email/generatedpassword/html.pug b/server/templates/email/generatedpassword/html.pug index 1563c59b30..63bc513752 100644 --- a/server/templates/email/generatedpassword/html.pug +++ b/server/templates/email/generatedpassword/html.pug @@ -19,9 +19,12 @@ div(style='display: block; background-color: #111827; padding: 2.5rem 0;') table(style='margin: 0 auto; font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", Arial, sans-serif; color: #fff; font-size: 16px; width: 26rem;') tr td(style="text-align: center;") - if applicationUrl - a(href=applicationUrl style='margin: 0 1rem;') - img(src=applicationUrl +'/logo_full.png' style='width: 26rem; image-rendering: crisp-edges; image-rendering: -webkit-optimize-contrast;') + if logoUrl + if applicationUrl + a(href=applicationUrl style='margin: 0 1rem;') + img(src=logoUrl style='width: 26rem; image-rendering: crisp-edges; image-rendering: -webkit-optimize-contrast;') + else + img(src=logoUrl style='margin: 0 1rem; width: 26rem; image-rendering: crisp-edges; image-rendering: -webkit-optimize-contrast;') else div(style='margin: 0 1rem 2.5rem; font-size: 3em; font-weight: 700;') | #{applicationTitle} diff --git a/server/templates/email/media-issue/html.pug b/server/templates/email/media-issue/html.pug index 1e34e835ac..0f20e7517b 100644 --- a/server/templates/email/media-issue/html.pug +++ b/server/templates/email/media-issue/html.pug @@ -19,9 +19,12 @@ div(style='display: block; background-color: #111827; padding: 2.5rem 0;') table(style='margin: 0 auto; font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", Arial, sans-serif; color: #fff; font-size: 16px; width: 26rem;') tr td(style="text-align: center;") - if applicationUrl - a(href=applicationUrl style='margin: 0 1rem;') - img(src=applicationUrl +'/logo_full.png' style='width: 26rem; image-rendering: crisp-edges; image-rendering: -webkit-optimize-contrast;') + if logoUrl + if applicationUrl + a(href=applicationUrl style='margin: 0 1rem;') + img(src=logoUrl style='width: 26rem; image-rendering: crisp-edges; image-rendering: -webkit-optimize-contrast;') + else + img(src=logoUrl style='margin: 0 1rem; width: 26rem; image-rendering: crisp-edges; image-rendering: -webkit-optimize-contrast;') else div(style='margin: 0 1rem 2.5rem; font-size: 3em; font-weight: 700;') | #{applicationTitle} diff --git a/server/templates/email/media-request/html.pug b/server/templates/email/media-request/html.pug index bd7c9aa61e..0e80287e78 100644 --- a/server/templates/email/media-request/html.pug +++ b/server/templates/email/media-request/html.pug @@ -19,9 +19,12 @@ div(style='display: block; background-color: #111827; padding: 2.5rem 0;') table(style='margin: 0 auto; font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", Arial, sans-serif; color: #fff; font-size: 16px; width: 26rem;') tr td(style="text-align: center;") - if applicationUrl - a(href=applicationUrl style='margin: 0 1rem;') - img(src=applicationUrl +'/logo_full.png' style='width: 26rem; image-rendering: crisp-edges; image-rendering: -webkit-optimize-contrast;') + if logoUrl + if applicationUrl + a(href=applicationUrl style='margin: 0 1rem;') + img(src=logoUrl style='width: 26rem; image-rendering: crisp-edges; image-rendering: -webkit-optimize-contrast;') + else + img(src=logoUrl style='margin: 0 1rem; width: 26rem; image-rendering: crisp-edges; image-rendering: -webkit-optimize-contrast;') else div(style='margin: 0 1rem 2.5rem; font-size: 3em; font-weight: 700;') | #{applicationTitle} diff --git a/server/templates/email/resetpassword/html.pug b/server/templates/email/resetpassword/html.pug index 0b0b1fa044..e41cd6c5c2 100644 --- a/server/templates/email/resetpassword/html.pug +++ b/server/templates/email/resetpassword/html.pug @@ -19,9 +19,12 @@ div(style='display: block; background-color: #111827; padding: 2.5rem 0;') table(style='margin: 0 auto; font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", Arial, sans-serif; color: #fff; font-size: 16px; width: 26rem;') tr td(style="text-align: center;") - if applicationUrl - a(href=applicationUrl style='margin: 0 1rem;') - img(src=applicationUrl +'/logo_full.png' style='width: 26rem; image-rendering: crisp-edges; image-rendering: -webkit-optimize-contrast;') + if logoUrl + if applicationUrl + a(href=applicationUrl style='margin: 0 1rem;') + img(src=logoUrl style='width: 26rem; image-rendering: crisp-edges; image-rendering: -webkit-optimize-contrast;') + else + img(src=logoUrl style='margin: 0 1rem; width: 26rem; image-rendering: crisp-edges; image-rendering: -webkit-optimize-contrast;') else div(style='margin: 0 1rem 2.5rem; font-size: 3em; font-weight: 700;') | #{applicationTitle} @@ -46,4 +49,4 @@ div(style='display: block; background-color: #111827; padding: 2.5rem 0;') tr td(style='text-align: center;') div(style='margin: 1rem 1rem 0; font-size: 1.25em;') - | If you did not initiate this request, you may safely disregard this message. \ No newline at end of file + | If you did not initiate this request, you may safely disregard this message. diff --git a/server/templates/email/test-email/html.pug b/server/templates/email/test-email/html.pug index eb9fbe9bf0..44e57f1be4 100644 --- a/server/templates/email/test-email/html.pug +++ b/server/templates/email/test-email/html.pug @@ -19,9 +19,12 @@ div(style='display: block; background-color: #111827; padding: 2.5rem 0;') table(style='margin: 0 auto; font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", Arial, sans-serif; color: #fff; font-size: 16px; width: 26rem;') tr td(style="text-align: center;") - if applicationUrl - a(href=applicationUrl style='margin: 0 1rem;') - img(src=applicationUrl +'/logo_full.png' style='width: 26rem; image-rendering: crisp-edges; image-rendering: -webkit-optimize-contrast;') + if logoUrl + if applicationUrl + a(href=applicationUrl style='margin: 0 1rem;') + img(src=logoUrl style='width: 26rem; image-rendering: crisp-edges; image-rendering: -webkit-optimize-contrast;') + else + img(src=logoUrl style='margin: 0 1rem; width: 26rem; image-rendering: crisp-edges; image-rendering: -webkit-optimize-contrast;') else div(style='margin: 0 1rem 2.5rem; font-size: 3em; font-weight: 700;') | #{applicationTitle} diff --git a/server/test/index.mts b/server/test/index.mts index 0b1c59c9a8..2f6bf81c46 100644 --- a/server/test/index.mts +++ b/server/test/index.mts @@ -59,6 +59,9 @@ if (positionals.length > 0) { for await (const entry of glob(join(BASE_DIR, 'server/**/*.test.ts'))) { files.push(resolve(entry)); } + for await (const entry of glob(join(BASE_DIR, 'src/**/*.test.ts'))) { + files.push(resolve(entry)); + } files.sort(); } diff --git a/server/test/runWithMockTimers.ts b/server/test/runWithMockTimers.ts new file mode 100644 index 0000000000..597f44dd21 --- /dev/null +++ b/server/test/runWithMockTimers.ts @@ -0,0 +1,34 @@ +import { mock } from 'node:test'; + +export async function runWithMockTimers( + runFn: () => Promise, + tickMs = 4000 +): Promise { + mock.timers.enable({ apis: ['setTimeout'] }); + try { + let settled = false; + const runPromise = runFn(); + runPromise + .catch(() => undefined) + .finally(() => { + settled = true; + }); + const maxTicks = 100000; + for (let i = 0; i < maxTicks && !settled; i++) { + await new Promise((resolve) => setImmediate(resolve)); + mock.timers.tick(tickMs); + } + // The final tick may have settled runPromise, but `settled` only updates + // in a microtask; yield once more before the guard check to avoid false + // negatives when the resolving tick lands on the last iteration. + await new Promise((resolve) => setImmediate(resolve)); + if (!settled) { + throw new Error( + `runWithMockTimers: promise did not settle after ${maxTicks} ticks` + ); + } + return await runPromise; + } finally { + mock.timers.reset(); + } +} diff --git a/server/test/setup.ts b/server/test/setup.ts index b9db8a9354..bb616b0071 100644 --- a/server/test/setup.ts +++ b/server/test/setup.ts @@ -1,10 +1,83 @@ import logger from '@server/logger'; +import http from 'node:http'; +import https from 'node:https'; import { after, before } from 'node:test'; +// supertest serves the app over a real loopback socket, so only external hosts +// can be refused +const LOOPBACK = new Set(['localhost', '127.0.0.1', '::1']); + +const blocked = new Set(); + +function stripPort(host: string): string { + const bracketed = host.match(/^\[(.+)\]/); + if (bracketed) { + return bracketed[1]; + } + + const parts = host.split(':'); + return parts.length === 2 ? parts[0] : host; +} + +function hostnameOf(args: unknown[]): string { + const [first, second] = args; + const fromUrl = + typeof first === 'string' || first instanceof URL + ? new URL(String(first)).hostname + : undefined; + + const options = (fromUrl !== undefined ? second : first) as + | { hostname?: string; host?: string } + | undefined; + const fromOptions = + typeof options === 'object' && options !== null + ? (options.hostname ?? + (options.host ? stripPort(options.host) : undefined)) + : undefined; + + return stripPort(fromOptions ?? fromUrl ?? 'localhost'); +} + +function blockOutboundRequests( + mod: typeof http | typeof https, + scheme: string +) { + // get() calls the module's internal request(), not the exported one + for (const name of ['request', 'get'] as const) { + const original = mod[name] as (...args: unknown[]) => unknown; + + mod[name] = ((...args: unknown[]) => { + const hostname = hostnameOf(args); + + if (!LOOPBACK.has(hostname)) { + const target = `${scheme}//${hostname}`; + blocked.add(target); + throw new Error( + `Blocked outbound request to ${target}. Stub the API client this test uses, or set ALLOW_NETWORK=true.` + ); + } + + return original(...args); + }) as typeof mod.request; + } +} + +if (process.env.ALLOW_NETWORK != 'true') { + blockOutboundRequests(http, 'http:'); + blockOutboundRequests(https, 'https:'); +} + before(() => { if (process.env.VERBOSE != 'true') logger.silent = true; }); after(() => { if (process.env.VERBOSE != 'true') logger.silent = false; + + // callers that swallow the error would otherwise leave the suite green + if (blocked.size) { + throw new Error( + `Test reached the network: ${[...blocked].join(', ')}. Stub the API client this test uses.` + ); + } }); diff --git a/server/test/userSettings.ts b/server/test/userSettings.ts new file mode 100644 index 0000000000..8ab0bb1294 --- /dev/null +++ b/server/test/userSettings.ts @@ -0,0 +1,52 @@ +import { getRepository } from '@server/datasource'; +import { User } from '@server/entity/User'; +import { UserSettings } from '@server/entity/UserSettings'; +import assert from 'node:assert/strict'; + +const credentials = { + pgpKey: 'test-pgp-key', + discordIds: ['112233445566'], + pushbulletAccessToken: 'test-pushbullet-token', + pushoverApplicationToken: 'test-pushover-app-token', + pushoverUserKey: 'test-pushover-user-key', + telegramChatId: '-1001234567', +}; + +export const allowlistedSettings = { + locale: 'de', + discoverRegion: 'DE', + streamingRegion: 'DE', + originalLanguage: 'de', + watchlistSyncMovies: true, + watchlistSyncTv: false, +}; + +export async function seedUserSettings(email: string): Promise { + const userRepository = getRepository(User); + const user = await userRepository.findOneOrFail({ where: { email } }); + + user.settings = new UserSettings({ + ...credentials, + ...allowlistedSettings, + notificationTypes: {}, + }); + + return userRepository.save(user); +} + +export function assertNoCredentials(body: unknown): void { + const serialized = JSON.stringify(body); + + const secrets = [ + credentials.pgpKey, + ...credentials.discordIds, + credentials.pushbulletAccessToken, + credentials.pushoverApplicationToken, + credentials.pushoverUserKey, + credentials.telegramChatId, + ]; + + for (const secret of secrets) { + assert.ok(!serialized.includes(secret), `${secret} was serialized`); + } +} diff --git a/server/tsconfig.build.json b/server/tsconfig.build.json new file mode 100644 index 0000000000..d472b2c2aa --- /dev/null +++ b/server/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["**/*.test.ts"] +} diff --git a/server/tsconfig.json b/server/tsconfig.json index 6907bd7429..aff5f8c37f 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -3,6 +3,7 @@ "compilerOptions": { "target": "ES2020", "module": "commonjs", + "moduleResolution": "node", "outDir": "../dist", "noEmit": false, "incremental": true, diff --git a/server/utils/DbColumnHelper.ts b/server/utils/DbColumnHelper.ts index 1c030b92d2..6039270476 100644 --- a/server/utils/DbColumnHelper.ts +++ b/server/utils/DbColumnHelper.ts @@ -1,4 +1,4 @@ -import { isPgsql } from '@server/datasource'; +import { isPgsql } from '@server/utils/dbType'; import type { ColumnOptions, ColumnType } from 'typeorm'; import { Column } from 'typeorm'; const pgTypeMapping: { [key: string]: ColumnType } = { diff --git a/server/utils/asyncLock.ts b/server/utils/asyncLock.ts index 51794a989c..ba7279c2cc 100644 --- a/server/utils/asyncLock.ts +++ b/server/utils/asyncLock.ts @@ -37,14 +37,14 @@ class AsyncLock { setImmediate(() => this.ee.emit(key)); }; - public dispatch = async ( + public dispatch = async ( key: string | number, - callback: () => Promise - ) => { + callback: () => Promise + ): Promise => { const skey = String(key); await this.acquire(skey); try { - await callback(); + return await callback(); } finally { this.release(skey); } diff --git a/server/utils/customProxyAgent.test.ts b/server/utils/customProxyAgent.test.ts new file mode 100644 index 0000000000..c785d08403 --- /dev/null +++ b/server/utils/customProxyAgent.test.ts @@ -0,0 +1,72 @@ +import ExternalAPI from '@server/api/externalapi'; +import type { ProxySettings } from '@server/lib/settings'; +import createCustomProxyAgent from '@server/utils/customProxyAgent'; +import axios, { + type AxiosResponse, + type InternalAxiosRequestConfig, +} from 'axios'; +import { HttpsProxyAgent } from 'https-proxy-agent'; +import assert from 'node:assert/strict'; +import { afterEach, beforeEach, describe, it, mock } from 'node:test'; + +class TestExternalAPI extends ExternalAPI { + public constructor() { + super('https://api.themoviedb.org/3', {}, {}); + } + + public async resolvedHttpsAgent(path = '/movie/123'): Promise { + let captured: InternalAxiosRequestConfig | undefined; + this.axios.defaults.adapter = (config) => { + captured = config; + return Promise.resolve({ + data: {}, + status: 200, + statusText: 'OK', + headers: {}, + config, + request: {}, + } as AxiosResponse); + }; + await this.axios.get(path); + return captured?.httpsAgent; + } +} + +// constructed before any test configures the proxy, like BaseScanner.tmdb +const preProxyClient = new TestExternalAPI(); + +const proxySettings: ProxySettings = { + enabled: true, + hostname: 'proxy.test', + port: 3128, + useSsl: false, + user: '', + password: '', + bypassFilter: '*.bypass.test', + bypassLocalAddresses: true, +}; + +describe('proxy routing (construction-order independence)', () => { + beforeEach(() => { + mock.method(axios, 'head', async () => ({ status: 200 })); + }); + + afterEach(() => { + mock.restoreAll(); + }); + + it('routes a client constructed BEFORE the proxy was configured', async () => { + await createCustomProxyAgent(proxySettings); + const agent = await preProxyClient.resolvedHttpsAgent(); + assert.ok( + agent instanceof HttpsProxyAgent, + 'client created before proxy setup must still route through the proxy' + ); + }); + + it('routes a client constructed AFTER the proxy was configured', async () => { + await createCustomProxyAgent(proxySettings); + const agent = await new TestExternalAPI().resolvedHttpsAgent(); + assert.ok(agent instanceof HttpsProxyAgent); + }); +}); diff --git a/server/utils/customProxyAgent.ts b/server/utils/customProxyAgent.ts index 11f56980df..020fae3815 100644 --- a/server/utils/customProxyAgent.ts +++ b/server/utils/customProxyAgent.ts @@ -1,14 +1,66 @@ import type { ProxySettings } from '@server/lib/settings'; import logger from '@server/logger'; -import axios, { type InternalAxiosRequestConfig } from 'axios'; +import type { InternalAxiosRequestConfig } from 'axios'; +import axios from 'axios'; +import http from 'http'; import { HttpProxyAgent } from 'http-proxy-agent'; +import https from 'https'; import { HttpsProxyAgent } from 'https-proxy-agent'; import type { Dispatcher } from 'undici'; import { Agent, ProxyAgent, setGlobalDispatcher } from 'undici'; -export let requestInterceptorFunction: ( +interface ProxyState { + httpAgent: HttpProxyAgent; + httpsAgent: HttpsProxyAgent; + skipUrl: (url: string | URL) => boolean; +} + +let proxyState: ProxyState | null = null; +let ipv4Agents: { httpAgent: http.Agent; httpsAgent: https.Agent } | null = + null; + +export function setForceIpv4First(enabled: boolean) { + ipv4Agents = enabled + ? { + httpAgent: new http.Agent({ family: 4 }), + httpsAgent: new https.Agent({ family: 4 }), + } + : null; +} + +export function proxyRequestInterceptor( config: InternalAxiosRequestConfig -) => InternalAxiosRequestConfig; +): InternalAxiosRequestConfig { + let url: URL | undefined; + try { + if (config.baseURL) { + url = new URL(config.url ?? '', config.baseURL); + } else if (config.url) { + url = new URL(config.url); + } + } catch { + url = undefined; + } + + if (proxyState) { + if (url && proxyState.skipUrl(url)) { + config.httpAgent = ipv4Agents?.httpAgent ?? false; + config.httpsAgent = ipv4Agents?.httpsAgent ?? false; + } else { + config.httpAgent = proxyState.httpAgent; + config.httpsAgent = proxyState.httpsAgent; + } + config.proxy = false; + } else if (ipv4Agents) { + config.httpAgent = ipv4Agents.httpAgent; + config.httpsAgent = ipv4Agents.httpsAgent; + } + + return config; +} + +// default instance only, axios.create() clients register this themselves +axios.interceptors.request.use(proxyRequestInterceptor); export default async function createCustomProxyAgent( proxySettings: ProxySettings, @@ -87,25 +139,18 @@ export default async function createCustomProxyAgent( scheduling: 'lifo' as const, family: forceIpv4First ? 4 : undefined, }; - axios.defaults.httpAgent = new HttpProxyAgent(proxyUrl, agentOptions); - axios.defaults.httpsAgent = new HttpsProxyAgent(proxyUrl, agentOptions); - - requestInterceptorFunction = (config) => { - const url = config.baseURL - ? new URL(config.baseURL + (config.url || '')) - : config.url; - if (url && skipUrl(url)) { - config.httpAgent = false; - config.httpsAgent = false; - } - return config; + + proxyState = { + httpAgent: new HttpProxyAgent(proxyUrl, agentOptions), + httpsAgent: new HttpsProxyAgent(proxyUrl, agentOptions), + skipUrl, }; - axios.interceptors.request.use(requestInterceptorFunction); } catch (e) { logger.error('Failed to connect to the proxy: ' + e.message, { label: 'Proxy', }); setGlobalDispatcher(defaultAgent); + proxyState = null; return; } @@ -117,7 +162,6 @@ export default async function createCustomProxyAgent( 'Failed to connect to the proxy: ' + e.message + ': ' + e.cause, { label: 'Proxy' } ); - setGlobalDispatcher(defaultAgent); } } diff --git a/server/utils/dbType.ts b/server/utils/dbType.ts new file mode 100644 index 0000000000..d4cd2bf52c --- /dev/null +++ b/server/utils/dbType.ts @@ -0,0 +1,2 @@ +// Must stay import-free, entity decorators read this during datasource evaluation +export const isPgsql = process.env.DB_TYPE === 'postgres'; diff --git a/server/utils/demoMode.ts b/server/utils/demoMode.ts new file mode 100644 index 0000000000..a5a1808a87 --- /dev/null +++ b/server/utils/demoMode.ts @@ -0,0 +1,86 @@ +import dataSource from '@server/datasource'; +import DiscoverSlider from '@server/entity/DiscoverSlider'; +import { getSettings } from '@server/lib/settings'; +import logger from '@server/logger'; +import { seedTestUsers } from '@server/utils/seedTestDb'; +import type { Express } from 'express'; +import fs from 'fs'; +import path from 'path'; + +async function resetDemoData(): Promise { + try { + // Drop all tables except users and sessions + const entities = dataSource.entityMetadatas; + for (const entity of entities) { + if (['session', 'user'].includes(entity.tableName.toLowerCase())) { + continue; + } + await dataSource.getRepository(entity.name).deleteAll(); + } + // Add the default users + await seedTestUsers(); + // Remove admin user from the database + await dataSource.getRepository('user').delete({ email: 'admin@seerr.dev' }); + // Re-seed discover sliders + await DiscoverSlider.bootstrapSliders(); + } catch (error) { + logger.error('Failed to reset demo data', { error: error.message }); + } +} + +export async function initDemoData(server: Express): Promise { + // Check that no other data than the demo user exists in the database before seeding demo data + const userCount = await dataSource.getRepository('user').count(); + if ( + userCount > 1 || + (userCount === 1 && + !(await dataSource + .getRepository('user') + .findOneBy({ email: 'demo@seerr.dev' }))) + ) { + logger.error('Database is not empty, cannot seed demo data. Aborting.'); + process.exit(1); + } + + // Retrieve settings from Cypress tests + try { + fs.copyFileSync( + path.join(__dirname, '../../cypress/config/settings.cypress.json'), + path.join(__dirname, '../../config/settings.json') + ); + } catch (error) { + logger.error('Failed to copy settings from Cypress tests', { error }); + process.exit(1); + } + const settings = getSettings(); + await settings.load(); + settings.main.mediaServerLogin = false; + await settings.save(); + // Seed the database with demo data + await resetDemoData(); + // Resets the database every hour + setInterval(resetDemoData, 60 * 60 * 1000); + + // Disable password sign-in for non-demo users + server.post('/api/v1/auth/local', async (req, res, next) => { + if (req.body?.email !== 'demo@seerr.dev') { + return res.status(403).json({ error: 'Password sign-in is disabled.' }); + } + next(); + }); + // Disable password change + server.post('/api/v1/user/:id/settings/password', async (_, res) => { + return res.status(204).send(); + }); + // Disable test user modification + server.post('/api/v1/user/:id/settings/main', async (_, res) => { + return res.status(200).json({}); + }); + // Disable unlinking media servers + server.post( + '/api/v1/user/:id/settings/linked-accounts/plex', + async (_, res) => { + return res.status(204).send(); + } + ); +} diff --git a/server/utils/nestedTransaction.ts b/server/utils/nestedTransaction.ts new file mode 100644 index 0000000000..d6da6bb1b1 --- /dev/null +++ b/server/utils/nestedTransaction.ts @@ -0,0 +1,12 @@ +import type { EntityManager } from 'typeorm'; + +// sqlite savepoints race on shared transactionDepth as it shares one query runner process-wide +// so nesting collides on savepoint counter, see https://github.com/typeorm/typeorm/issues/10209 +// and later reverted by https://github.com/typeorm/typeorm/issues/11260 +export const withNestedTransaction = ( + manager: EntityManager, + run: (manager: EntityManager) => Promise +): Promise => + manager.connection.options.type === 'sqlite' + ? run(manager) + : manager.transaction(run); diff --git a/server/utils/requestLock.ts b/server/utils/requestLock.ts new file mode 100644 index 0000000000..86654673d4 --- /dev/null +++ b/server/utils/requestLock.ts @@ -0,0 +1,16 @@ +import type { MediaType } from '@server/constants/media'; +import AsyncLock from '@server/utils/asyncLock'; + +// Keyed on user id or request id. Never dispatch from a subscriber or +// transaction as a waiter would block while holding the save's connection. +const requestLock = new AsyncLock(); + +// keyed on media. always taken inside requestLock, never around it. +export const mediaLock = new AsyncLock(); + +export const userKey = (userId: number) => `user:${userId}`; +export const requestKey = (requestId: number) => `request:${requestId}`; +export const mediaKey = (mediaType: MediaType, mediaId: number) => + `${mediaType}:${mediaId}`; + +export default requestLock; diff --git a/server/utils/seedTestDb.test.ts b/server/utils/seedTestDb.test.ts new file mode 100644 index 0000000000..2176bb9868 --- /dev/null +++ b/server/utils/seedTestDb.test.ts @@ -0,0 +1,29 @@ +import assert from 'node:assert/strict'; +import { afterEach, describe, it } from 'node:test'; + +import { resetTestDb, seedTestDb } from '@server/utils/seedTestDb'; + +// NODE_ENV is typed readonly, so it is swapped through the whole env object +function setNodeEnv(value: string | undefined) { + Object.assign(process.env, { NODE_ENV: value }); +} + +describe('test database guard', () => { + const originalNodeEnv = process.env.NODE_ENV; + + afterEach(() => { + setNodeEnv(originalNodeEnv); + }); + + it('refuses to seed when NODE_ENV is not test', async () => { + setNodeEnv('production'); + + await assert.rejects(() => seedTestDb(), /Refusing to seed/); + }); + + it('refuses to reset when NODE_ENV is not test', async () => { + setNodeEnv('production'); + + await assert.rejects(() => resetTestDb(), /Refusing to reset/); + }); +}); diff --git a/server/utils/seedTestDb.ts b/server/utils/seedTestDb.ts index 266169d45c..dd1b76b004 100644 --- a/server/utils/seedTestDb.ts +++ b/server/utils/seedTestDb.ts @@ -1,6 +1,7 @@ import { UserType } from '@server/constants/user'; import dataSource, { getRepository } from '@server/datasource'; import { User } from '@server/entity/User'; +import { applyForkSchema } from '@server/lib/forkSchema'; import gravatarUrl from 'gravatar-url'; export interface SeedDbOptions { @@ -8,6 +9,8 @@ export interface SeedDbOptions { preserveDb?: boolean; /** If true, runs migrations instead of synchronizing schema */ withMigrations?: boolean; + /** If true, permits seeding while NODE_ENV is not test */ + allowOutsideTest?: boolean; } // Precomputed bcrypt hash of 'test1234'. We precompute this to avoid @@ -15,11 +18,21 @@ export interface SeedDbOptions { const TEST_USER_PASSWORD_HASH = '$2b$12$Z5V2P5HZgmx4/AnWFMZN1.aD5AM1NucNi.mhNTSQ9oVtmdzu7Le/a'; +function assertTestDatabase(operation: string, allowOutsideTest = false): void { + if (allowOutsideTest || process.env.NODE_ENV === 'test') { + return; + } + + throw new Error( + `Refusing to ${operation} while NODE_ENV is not test: this drops every table and seeds accounts with a known password.` + ); +} + /** * Seeds test users into the database. * Assumes the database schema is already set up. */ -async function seedTestUsers(): Promise { +export async function seedTestUsers(): Promise { const userRepository = getRepository(User); const admin = await userRepository.findOne({ @@ -46,17 +59,17 @@ async function seedTestUsers(): Promise { // Create the other user const otherUser = (await userRepository.findOne({ - where: { email: 'friend@seerr.dev' }, + where: { email: 'demo@seerr.dev' }, })) ?? new User(); otherUser.plexId = admin?.plexId ?? 1; otherUser.plexToken = '1234'; - otherUser.plexUsername = 'friend'; - otherUser.username = 'friend'; - otherUser.email = 'friend@seerr.dev'; + otherUser.plexUsername = 'demo'; + otherUser.username = 'demo'; + otherUser.email = 'demo@seerr.dev'; otherUser.userType = UserType.PLEX; otherUser.password = TEST_USER_PASSWORD_HASH; otherUser.permissions = 32; - otherUser.avatar = gravatarUrl('friend@seerr.dev', { + otherUser.avatar = gravatarUrl('demo@seerr.dev', { default: 'mm', size: 200, }); @@ -68,6 +81,8 @@ async function seedTestUsers(): Promise { * Used by both Cypress tests and Vitest unit tests. */ export async function seedTestDb(options: SeedDbOptions = {}): Promise { + assertTestDatabase('seed the test database', options.allowOutsideTest); + const dbConnection = dataSource.isInitialized ? dataSource : await dataSource.initialize(); @@ -77,7 +92,7 @@ export async function seedTestDb(options: SeedDbOptions = {}): Promise { } if (options.withMigrations) { - await dbConnection.runMigrations(); + await applyForkSchema(dbConnection); } else { await dbConnection.synchronize(); } @@ -91,6 +106,8 @@ export async function seedTestDb(options: SeedDbOptions = {}): Promise { * Assumes DB has been initialized. */ export async function resetTestDb(): Promise { + assertTestDatabase('reset the test database'); + await dataSource.synchronize(true); await seedTestUsers(); } diff --git a/server/utils/typeHelpers.ts b/server/utils/typeHelpers.ts index 548378ff7b..b6c9ac45a1 100644 --- a/server/utils/typeHelpers.ts +++ b/server/utils/typeHelpers.ts @@ -1,3 +1,4 @@ +import type { HardcoverBookDetails } from '@server/api/hardcover/interfaces'; import type { TmdbCollectionResult, TmdbMovieDetails, @@ -39,13 +40,31 @@ export const isCollection = ( }; export const isMovieDetails = ( - movie: TmdbMovieDetails | TmdbTvDetails | TmdbPersonDetails + movie: + | TmdbMovieDetails + | TmdbTvDetails + | TmdbPersonDetails + | HardcoverBookDetails ): movie is TmdbMovieDetails => { return (movie as TmdbMovieDetails).title !== undefined; }; export const isTvDetails = ( - tv: TmdbMovieDetails | TmdbTvDetails | TmdbPersonDetails + tv: + | TmdbMovieDetails + | TmdbTvDetails + | TmdbPersonDetails + | HardcoverBookDetails ): tv is TmdbTvDetails => { return (tv as TmdbTvDetails).number_of_seasons !== undefined; }; + +export const isBookDetails = ( + book: + | TmdbMovieDetails + | TmdbTvDetails + | TmdbPersonDetails + | HardcoverBookDetails +): book is HardcoverBookDetails => { + return (book as HardcoverBookDetails).pages !== undefined; +}; diff --git a/server/utils/userAgent.test.ts b/server/utils/userAgent.test.ts new file mode 100644 index 0000000000..471da21b03 --- /dev/null +++ b/server/utils/userAgent.test.ts @@ -0,0 +1,73 @@ +import ExternalAPI from '@server/api/externalapi'; +import { getUserAgent } from '@server/utils/userAgent'; +import axios, { + type AxiosInstance, + type AxiosRequestConfig, + type AxiosResponse, + type InternalAxiosRequestConfig, +} from 'axios'; +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; + +const captureRequest = (instance: AxiosInstance) => { + const captured: { config?: InternalAxiosRequestConfig } = {}; + + instance.defaults.adapter = (config) => { + captured.config = config; + return Promise.resolve({ + data: {}, + status: 200, + statusText: 'OK', + headers: {}, + config, + request: {}, + } as AxiosResponse); + }; + + return captured; +}; + +class TestExternalAPI extends ExternalAPI { + public constructor() { + super('https://api.themoviedb.org/3', {}, {}); + } + + public async sentUserAgent( + config?: AxiosRequestConfig + ): Promise { + const captured = captureRequest(this.axios); + await this.axios.get('/movie/123', config); + return captured.config?.headers.get('User-Agent') as string | null; + } +} + +describe('outbound user agent', () => { + it('identifies the application by name and version', () => { + assert.match(getUserAgent(), /^Seerr\/.+$/); + }); + + it('is sent by clients built on ExternalAPI', async () => { + assert.equal(await new TestExternalAPI().sentUserAgent(), getUserAgent()); + }); + + it('is sent on default axios calls', async () => { + const previousAdapter = axios.defaults.adapter; + const captured = captureRequest(axios); + + try { + await axios.get('https://example.test/'); + } finally { + axios.defaults.adapter = previousAdapter; + } + + assert.equal(captured.config?.headers.get('User-Agent'), getUserAgent()); + }); + + it('does not override a user agent set on the request', async () => { + const sent = await new TestExternalAPI().sentUserAgent({ + headers: { 'user-agent': 'Custom/1.0' }, + }); + + assert.equal(sent, 'Custom/1.0'); + }); +}); diff --git a/server/utils/userAgent.ts b/server/utils/userAgent.ts new file mode 100644 index 0000000000..49c8a5f7ad --- /dev/null +++ b/server/utils/userAgent.ts @@ -0,0 +1,22 @@ +import { getAppVersion } from '@server/utils/appVersion'; +import type { InternalAxiosRequestConfig } from 'axios'; +import axios from 'axios'; + +const USER_AGENT = `Seerr/${getAppVersion()}`; + +export const getUserAgent = (): string => { + return USER_AGENT; +}; + +export function userAgentRequestInterceptor( + config: InternalAxiosRequestConfig +): InternalAxiosRequestConfig { + if (!config.headers.has('User-Agent')) { + config.headers.set('User-Agent', getUserAgent()); + } + + return config; +} + +// default instance only, axios.create() clients register this themselves +axios.interceptors.request.use(userAgentRequestInterceptor); diff --git a/src/assets/hardcover.svg b/src/assets/hardcover.svg new file mode 100644 index 0000000000..8036276de3 --- /dev/null +++ b/src/assets/hardcover.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/services/chaptarr.svg b/src/assets/services/chaptarr.svg new file mode 100644 index 0000000000..32c1bcd3aa --- /dev/null +++ b/src/assets/services/chaptarr.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/services/readarr.svg b/src/assets/services/readarr.svg new file mode 100644 index 0000000000..c3365f0068 --- /dev/null +++ b/src/assets/services/readarr.svg @@ -0,0 +1,21 @@ + + +background + + + + + + +Layer 1 + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/services/simkl.svg b/src/assets/services/simkl.svg new file mode 100644 index 0000000000..3b3bf131cf --- /dev/null +++ b/src/assets/services/simkl.svg @@ -0,0 +1 @@ + diff --git a/src/components/AirDateBadge/index.tsx b/src/components/AirDateBadge/index.tsx index 1143c3e3e0..3914984abd 100644 --- a/src/components/AirDateBadge/index.tsx +++ b/src/components/AirDateBadge/index.tsx @@ -12,25 +12,21 @@ type AirDateBadgeProps = { }; const AirDateBadge = ({ airDate }: AirDateBadgeProps) => { - const WEEK = 1000 * 60 * 60 * 24 * 8; + const DAY_MS = 1000 * 60 * 60 * 24; + const RELATIVE_WINDOW_MS = DAY_MS * 8; const intl = useIntl(); + // TMDB air_date has no time/tz, it's just the origin country's local date. + // We pin it to UTC anyway for consistency. + // https://www.themoviedb.org/talk/6365be67d7107e008d777337 const dAirDate = new Date(airDate); - const nowDate = new Date(); - const alreadyAired = dAirDate.getTime() < nowDate.getTime(); - const compareWeek = new Date( - alreadyAired ? Date.now() - WEEK : Date.now() + WEEK - ); - let showRelative = false; - if ( - (alreadyAired && dAirDate.getTime() > compareWeek.getTime()) || - (!alreadyAired && dAirDate.getTime() < compareWeek.getTime()) - ) { - showRelative = true; - } - - const diffInDays = Math.round( - (dAirDate.getTime() - nowDate.getTime()) / (1000 * 60 * 60 * 24) - ); + dAirDate.setUTCHours(0, 0, 0, 0); + const todayUtc = new Date(); + todayUtc.setUTCHours(0, 0, 0, 0); + const diffMs = dAirDate.getTime() - todayUtc.getTime(); + // The air date itself counts as "Airing" and "Aired" starts the next UTC day. + const alreadyAired = diffMs < 0; + const showRelative = Math.abs(diffMs) <= RELATIVE_WINDOW_MS; + const diffInDays = diffMs / DAY_MS; return (

diff --git a/src/components/AuthorCard/index.tsx b/src/components/AuthorCard/index.tsx new file mode 100644 index 0000000000..1b59ebd5e4 --- /dev/null +++ b/src/components/AuthorCard/index.tsx @@ -0,0 +1,95 @@ +import CachedImage from '@app/components/Common/CachedImage'; +import { UserCircleIcon } from '@heroicons/react/24/solid'; +import Link from 'next/link'; +import { useState } from 'react'; + +interface AuthorCardProps { + authorId: number; + name: string; + subName?: string; + image?: string; + canExpand?: boolean; +} + +const AuthorCard = ({ + authorId, + name, + subName, + image, + canExpand = false, +}: AuthorCardProps) => { + const [isHovered, setHovered] = useState(false); + + return ( + { + setHovered(true); + }} + onMouseLeave={() => setHovered(false)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + setHovered(true); + } + }} + role="link" + tabIndex={0} + > +
+
+
+
+ {image ? ( +
+ +
+ ) : ( + + )} +
+
{name}
+ {subName && ( +
+ {subName} +
+ )} +
+
+
+
+ + ); +}; + +export default AuthorCard; diff --git a/src/components/AuthorDetails/index.tsx b/src/components/AuthorDetails/index.tsx new file mode 100644 index 0000000000..3fc7667ee2 --- /dev/null +++ b/src/components/AuthorDetails/index.tsx @@ -0,0 +1,137 @@ +import Ellipsis from '@app/assets/ellipsis.svg'; +import CachedImage from '@app/components/Common/CachedImage'; +import HardcoverSetup from '@app/components/Common/HardcoverSetup'; +import ImageFader from '@app/components/Common/ImageFader'; +import LoadingSpinner from '@app/components/Common/LoadingSpinner'; +import PageTitle from '@app/components/Common/PageTitle'; +import TitleCard from '@app/components/TitleCard'; +import Error from '@app/pages/_error'; +import defineMessages from '@app/utils/defineMessages'; +import type { AuthorDetails as AuthorDetailsType } from '@server/models/Author'; +import { useRouter } from 'next/router'; +import { useState } from 'react'; +import { useIntl } from 'react-intl'; +import TruncateMarkup from 'react-truncate-markup'; +import useSWR from 'swr'; + +const messages = defineMessages('components.AuthorDetails', { + books: 'Books', +}); + +const AuthorDetails = () => { + const intl = useIntl(); + const router = useRouter(); + const { data, error } = useSWR( + `/api/v1/author/${router.query.authorId}` + ); + + const [showBio, setShowBio] = useState(false); + + if (!data && !error) { + return ; + } + + if ((error as any)?.response?.status === 503) { + return ; + } + + if (!data) { + return ; + } + + const books = ( + <> +
+
+ {intl.formatMessage(messages.books)} +
+
+
    + {data.books?.map((book, index) => { + return ( +
  • + +
  • + ); + })} +
+ + ); + + return ( + <> + + {data.books && ( +
+ media.backdropPath) + .map((media) => media.backdropPath) + ), + ] as string[] + ).slice(0, 6)} + /> +
+ )} +
+ {data.posterPath && ( +
+ +
+ )} +
+

{data.name}

+
+ {data.bio && ( +
+ {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */} +
setShowBio((show) => !show)} + role="button" + tabIndex={-1} + > + + } + > +

{data.bio}

+
+
+
+ )} +
+
+ {books} + + ); +}; + +export default AuthorDetails; diff --git a/src/components/Blocklist/index.tsx b/src/components/Blocklist/index.tsx index a5c7ad6523..814664c997 100644 --- a/src/components/Blocklist/index.tsx +++ b/src/components/Blocklist/index.tsx @@ -24,6 +24,7 @@ import type { BlocklistItem, BlocklistResultsResponse, } from '@server/interfaces/api/blocklistInterfaces'; +import type { BookDetails } from '@server/models/Book'; import type { MovieDetails } from '@server/models/Movie'; import type { TvDetails } from '@server/models/Tv'; import axios from 'axios'; @@ -55,8 +56,16 @@ enum Filter { BLOCKLISTEDTAGS = 'blocklistedTags', } -const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => { - return (movie as MovieDetails).title !== undefined; +const isMovie = ( + media: MovieDetails | TvDetails | BookDetails +): media is MovieDetails => { + return (media as MovieDetails).title !== undefined; +}; + +const isBook = ( + media: MovieDetails | TvDetails | BookDetails +): media is BookDetails => { + return (media as BookDetails).author !== undefined; }; const Blocklist = () => { @@ -282,8 +291,10 @@ const BlocklistedItem = ({ item, revalidateList }: BlocklistedItemProps) => { const url = item.mediaType === 'movie' ? `/api/v1/movie/${item.tmdbId}` - : `/api/v1/tv/${item.tmdbId}`; - const { data: title, error } = useSWR( + : item.mediaType === 'tv' + ? `/api/v1/tv/${item.tmdbId}` + : `/api/v1/book/${item.tmdbId}`; + const { data: title, error } = useSWR( inView ? url : null ); @@ -329,8 +340,12 @@ const BlocklistedItem = ({ item, revalidateList }: BlocklistedItemProps) => { {title && title.backdropPath && (
{ href={ item.mediaType === 'movie' ? `/movie/${item.tmdbId}` - : `/tv/${item.tmdbId}` + : item.mediaType === 'tv' + ? `/tv/${item.tmdbId}` + : `/book/${item.tmdbId}` } className="relative h-auto w-12 flex-shrink-0 scale-100 transform-gpu overflow-hidden rounded-md transition duration-300 hover:scale-105" > { {title && (isMovie(title) ? title.releaseDate - : title.firstAirDate + : isBook(title) + ? title.releaseDate + : title.firstAirDate )?.slice(0, 4)}
- {title && (isMovie(title) ? title.title : title.name)} + {title && + (isMovie(title) + ? title.title + : isBook(title) + ? title.title + : title.name)}
@@ -451,12 +479,18 @@ const BlocklistedItem = ({ item, revalidateList }: BlocklistedItemProps) => { {intl.formatMessage(globalMessages.movie)}
- ) : ( + ) : item.mediaType === 'tv' ? (
{intl.formatMessage(globalMessages.tvshow)}
+ ) : ( +
+
+ {intl.formatMessage(globalMessages.book)} +
+
)} @@ -467,7 +501,12 @@ const BlocklistedItem = ({ item, revalidateList }: BlocklistedItemProps) => { onClick={() => removeFromBlocklist( item.tmdbId, - title && (isMovie(title) ? title.title : title.name) + title && + (isMovie(title) + ? title.title + : isBook(title) + ? title.title + : title.name) ) } confirmText={intl.formatMessage( diff --git a/src/components/BlocklistModal/index.tsx b/src/components/BlocklistModal/index.tsx index 0c386c8f2a..13582c80ea 100644 --- a/src/components/BlocklistModal/index.tsx +++ b/src/components/BlocklistModal/index.tsx @@ -2,7 +2,7 @@ import Modal from '@app/components/Common/Modal'; import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; import { Transition } from '@headlessui/react'; - +import type { BookDetails } from '@server/models/Book'; import type { Collection } from '@server/models/Collection'; import type { MovieDetails } from '@server/models/Movie'; import type { TvDetails } from '@server/models/Tv'; @@ -12,7 +12,7 @@ import { useIntl } from 'react-intl'; interface BlocklistModalProps { tmdbId: number; - type: 'movie' | 'tv' | 'collection'; + type: 'movie' | 'tv' | 'collection' | 'book'; show: boolean; onComplete?: () => void; onCancel?: () => void; @@ -24,7 +24,7 @@ const messages = defineMessages('component.BlocklistModal', { }); const isCollection = ( - data: MovieDetails | TvDetails | Collection | null + data: MovieDetails | TvDetails | Collection | BookDetails | null ): data is Collection => { return ( data !== null && @@ -34,12 +34,19 @@ const isCollection = ( }; const isMovie = ( - movie: MovieDetails | TvDetails | Collection | null + movie: MovieDetails | TvDetails | Collection | BookDetails | null ): movie is MovieDetails => { if (!movie) return false; return (movie as MovieDetails).title !== undefined; }; +const isBook = ( + item: MovieDetails | TvDetails | Collection | BookDetails | null +): item is BookDetails => { + if (!item) return false; + return (item as BookDetails).id !== undefined && 'author' in item; +}; + const BlocklistModal = ({ tmdbId, type, @@ -50,7 +57,7 @@ const BlocklistModal = ({ }: BlocklistModalProps) => { const intl = useIntl(); const [data, setData] = useState< - TvDetails | MovieDetails | Collection | null + TvDetails | MovieDetails | Collection | BookDetails | null >(null); const [error, setError] = useState(null); @@ -84,16 +91,20 @@ const BlocklistModal = ({ title={`${intl.formatMessage(globalMessages.blocklist)} ${ type === 'collection' ? intl.formatMessage(globalMessages.collection) - : isMovie(data) - ? intl.formatMessage(globalMessages.movie) - : intl.formatMessage(globalMessages.tvshow) + : type === 'book' + ? intl.formatMessage(globalMessages.book) + : isMovie(data) + ? intl.formatMessage(globalMessages.movie) + : intl.formatMessage(globalMessages.tvshow) }`} subTitle={`${ isCollection(data) ? data.name : isMovie(data) ? data.title - : data?.name + : isBook(data) + ? data.title + : data?.name }`} onCancel={onCancel} onOk={onComplete} @@ -104,7 +115,12 @@ const BlocklistModal = ({ } okButtonType="danger" okDisabled={isUpdating} - backdrop={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${data?.backdropPath}`} + backdrop={ + type === 'book' + ? data?.backdropPath + : `https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${data?.backdropPath}` + } + cache={type === 'book' ? 'hardcover' : 'tmdb'} /> ); diff --git a/src/components/BookDetails/index.tsx b/src/components/BookDetails/index.tsx new file mode 100644 index 0000000000..e29d831993 --- /dev/null +++ b/src/components/BookDetails/index.tsx @@ -0,0 +1,616 @@ +import HardcoverLogo from '@app/assets/hardcover.svg'; +import AuthorCard from '@app/components/AuthorCard'; +import BlocklistModal from '@app/components/BlocklistModal'; +import Button from '@app/components/Common/Button'; +import CachedImage from '@app/components/Common/CachedImage'; +import HardcoverSetup from '@app/components/Common/HardcoverSetup'; +import LoadingSpinner from '@app/components/Common/LoadingSpinner'; +import PageTitle from '@app/components/Common/PageTitle'; +import Tag from '@app/components/Common/Tag'; +import Tooltip from '@app/components/Common/Tooltip'; +import IssueModal from '@app/components/IssueModal'; +import ManageSlideOver from '@app/components/ManageSlideOver'; +import RequestButton from '@app/components/RequestButton'; +import Slider from '@app/components/Slider'; +import StatusBadge from '@app/components/StatusBadge'; +import useDeepLinks from '@app/hooks/useDeepLinks'; +import useSettings from '@app/hooks/useSettings'; +import useToasts from '@app/hooks/useToasts'; +import { Permission, useUser } from '@app/hooks/useUser'; +import globalMessages from '@app/i18n/globalMessages'; +import ErrorPage from '@app/pages/_error'; +import defineMessages from '@app/utils/defineMessages'; +import { refreshIntervalHelper } from '@app/utils/refreshIntervalHelper'; +import { + CogIcon, + ExclamationTriangleIcon, + EyeSlashIcon, +} from '@heroicons/react/24/outline'; +import { IssueStatus } from '@server/constants/issue'; +import { MediaStatus, MediaType } from '@server/constants/media'; +import type { BookDetails as BookDetailsType } from '@server/models/Book'; +import axios from 'axios'; +import Link from 'next/link'; +import { useRouter } from 'next/router'; +import { useCallback, useEffect, useState } from 'react'; +import { useIntl } from 'react-intl'; +import useSWR from 'swr'; + +const messages = defineMessages('components.BookDetails', { + overview: 'Overview', + author: 'Author', + overviewunavailable: 'Overview unavailable.', + openreadarr: 'Open Book in Readarr', + openreadarrAudio: 'Open Audiobook in Readarr', + downloadstatus: 'Download Status', + markavailable: 'Mark as Available', + markAudioavailable: 'Mark Audiobook as Available', + reportissue: 'Report an Issue', + managebook: 'Manage Book', + releasedate: 'Release Date', + publisher: 'Publisher', + language: 'Language', + isbn: 'ISBN', + format: 'Format', + audiobookduration: 'Audiobook Duration', + narrators: 'Narrator{narratorCount, plural, one {} other {s}}', +}); + +interface BookDetailsProps { + book?: BookDetailsType; +} + +const BookDetails = ({ book }: BookDetailsProps) => { + const settings = useSettings(); + const { user, hasPermission } = useUser(); + const router = useRouter(); + const intl = useIntl(); + const [showManager, setShowManager] = useState( + router.query.manage == '1' ? true : false + ); + const [showIssueModal, setShowIssueModal] = useState(false); + const [isBlocklistUpdating, setIsBlocklistUpdating] = + useState(false); + const [showBlocklistModal, setShowBlocklistModal] = useState(false); + const { addToast } = useToasts(); + + const { + data, + error, + mutate: revalidate, + } = useSWR(`/api/v1/book/${router.query.bookId}`, { + fallbackData: book, + refreshInterval: refreshIntervalHelper( + { + downloadStatus: book?.mediaInfo?.downloadStatus, + downloadStatus4k: book?.mediaInfo?.downloadStatus4k, + }, + 15000 + ), + }); + + useEffect(() => { + setShowManager(router.query.manage == '1' ? true : false); + }, [router.query.manage]); + + const closeBlocklistModal = useCallback( + () => setShowBlocklistModal(false), + [] + ); + + const { mediaUrl: plexUrl, mediaUrl4k: plexUrl4k } = useDeepLinks({ + mediaUrl: data?.mediaInfo?.mediaUrl, + mediaUrl4k: data?.mediaInfo?.mediaUrl4k, + iOSPlexUrl: data?.mediaInfo?.iOSPlexUrl, + iOSPlexUrl4k: data?.mediaInfo?.iOSPlexUrl4k, + }); + + const [showError, setShowError] = useState(false); + useEffect(() => { + if (!error || data) { + setShowError(false); + return; + } + const timer = setTimeout(() => setShowError(true), 20000); + return () => clearTimeout(timer); + }, [error, data]); + + if (!data && (!error || !showError)) { + return ; + } + + if ((error as any)?.response?.status === 503) { + return ; + } + + if (!data) { + return ; + } + + const onClickHideItemBtn = async (): Promise => { + setIsBlocklistUpdating(true); + + try { + await axios.post('/api/v1/blocklist', { + tmdbId: book?.id, + mediaType: 'book', + title: book?.title, + user: user?.id, + }); + + addToast( + + {intl.formatMessage(globalMessages.blocklistSuccess, { + title: book?.title, + strong: (msg: React.ReactNode) => {msg}, + })} + , + { appearance: 'success', autoDismiss: true } + ); + + revalidate(); + } catch (e) { + if (e?.response?.status === 412) { + addToast( + + {intl.formatMessage(globalMessages.blocklistDuplicateError, { + title: book?.title, + strong: (msg: React.ReactNode) => {msg}, + })} + , + { appearance: 'info', autoDismiss: true } + ); + } else { + addToast(intl.formatMessage(globalMessages.blocklistError), { + appearance: 'error', + autoDismiss: true, + }); + } + } + + setIsBlocklistUpdating(false); + closeBlocklistModal(); + }; + + const showHideButton = hasPermission([Permission.MANAGE_BLOCKLIST], { + type: 'or', + }); + + const showAudiobookStatus = + settings.currentSettings.bookAudioEnabled && + hasPermission( + [ + Permission.MANAGE_REQUESTS, + Permission.REQUEST_4K, + Permission.REQUEST_AUDIO_BOOK, + ], + { type: 'or' } + ); + + const isAvailableStatus = (mediaStatus?: MediaStatus) => + mediaStatus === MediaStatus.AVAILABLE || + mediaStatus === MediaStatus.PARTIALLY_AVAILABLE; + + const ebookDisplayStatus = + data?.mediaInfo?.status === MediaStatus.AVAILABLE && + showAudiobookStatus && + !isAvailableStatus(data?.mediaInfo?.status4k) + ? MediaStatus.PARTIALLY_AVAILABLE + : data?.mediaInfo?.status; + const audiobookDisplayStatus = + data?.mediaInfo?.status4k === MediaStatus.AVAILABLE && + !isAvailableStatus(data?.mediaInfo?.status) + ? MediaStatus.PARTIALLY_AVAILABLE + : data?.mediaInfo?.status4k; + + return ( +
+
+ +
+
+ + setShowIssueModal(false)} + show={showIssueModal} + mediaType={MediaType.BOOK} + tmdbId={data.id} + /> + { + setShowManager(false); + router.push({ + pathname: router.pathname, + query: { bookId: router.query.bookId }, + }); + }} + revalidate={() => revalidate()} + show={showManager} + /> + +
+
+ +
+
+
+ 0} + tmdbId={data.mediaInfo?.tmdbId} + mediaType={MediaType.BOOK} + plexUrl={plexUrl} + serviceUrl={data.mediaInfo?.serviceUrl} + /> + {settings.currentSettings.bookAudioEnabled && + hasPermission( + [ + Permission.MANAGE_REQUESTS, + Permission.REQUEST_4K, + Permission.REQUEST_AUDIO_BOOK, + ], + { + type: 'or', + } + ) && ( + 0 + } + tmdbId={data.mediaInfo?.tmdbId} + mediaType={MediaType.BOOK} + plexUrl={plexUrl4k} + serviceUrl={data.mediaInfo?.serviceUrl4k} + /> + )} +
+

+ {data.title}{' '} + {data.releaseDate && ( + + ({data.releaseDate.slice(0, 4)}) + + )} +

+ + {data.pages && {`${data.pages} pages`}} + +
+
+ {showHideButton && + data?.mediaInfo?.status !== MediaStatus.PROCESSING && + data?.mediaInfo?.status !== MediaStatus.AVAILABLE && + data?.mediaInfo?.status !== MediaStatus.PARTIALLY_AVAILABLE && + data?.mediaInfo?.status !== MediaStatus.PENDING && + data?.mediaInfo?.status !== MediaStatus.BLOCKLISTED && ( + + + + )} + revalidate()} + /> + {(data.mediaInfo?.status === MediaStatus.AVAILABLE || + (settings.currentSettings.bookAudioEnabled && + hasPermission( + [Permission.REQUEST_4K, Permission.REQUEST_AUDIO_BOOK], + { + type: 'or', + } + ) && + data.mediaInfo?.status4k === MediaStatus.AVAILABLE)) && + hasPermission( + [Permission.CREATE_ISSUES, Permission.MANAGE_ISSUES], + { + type: 'or', + } + ) && ( + + + + )} + {hasPermission(Permission.MANAGE_REQUESTS) && + data.mediaInfo && + (data.mediaInfo.jellyfinMediaId || + data.mediaInfo.jellyfinMediaId4k || + data.mediaInfo.status !== MediaStatus.UNKNOWN || + data.mediaInfo.status4k !== MediaStatus.UNKNOWN) && ( + + + + )} +
+
+
+
+ {data.headline &&
{data.headline}
} +

{intl.formatMessage(messages.overview)}

+

+ {data.description + ? data.description + : intl.formatMessage(messages.overviewunavailable)} +

+
+ {data.tags.Genre?.length > 0 && ( +
+

Genres

+
+ {data.tags.Genre.map((genre) => ( + + {genre.tag} + + ))} +
+
+ )} + {data.tags.Mood?.length > 0 && ( +
+

Moods

+
+ {data.tags.Mood.map((mood) => ( + + {mood.tag} + + ))} +
+
+ )} + {data.tags['Content Warning']?.length > 0 && ( +
+

Content Warning

+
+ {data.tags['Content Warning'].map((warn) => ( + + {warn.tag} + + ))} +
+
+ )} +
+
+
+ {data.series.length > 0 && + data.series.map((series) => ( +
+ +
+
+ +
+
+
+
+
{series.series.name}
+ {series.position && ( +
+ Book {series.position} of{' '} + {series.series.primary_books_count} +
+ )} +
+ +
+
+ +
+ ))} +
+ {data.rating != null && ( +
+ {data.slug ? ( + + + {data.rating.toFixed(1)} + + ) : ( + + + {data.rating.toFixed(1)} + + )} +
+ )} + {data.releaseDate && ( +
+ {intl.formatMessage(messages.releasedate)} + + {intl.formatDate(data.releaseDate, { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + })} + +
+ )} + {data.publisher && ( +
+ {intl.formatMessage(messages.publisher)} + {data.publisher} +
+ )} + {data.language && ( +
+ {intl.formatMessage(messages.language)} + {data.language} +
+ )} + {data.isbn && ( +
+ {intl.formatMessage(messages.isbn)} + {data.isbn} +
+ )} + {data.format && ( +
+ {intl.formatMessage(messages.format)} + {data.format} +
+ )} + {data.audiobookDuration && data.audiobookDuration > 0 && ( +
+ {intl.formatMessage(messages.audiobookduration)} + + {Math.floor(data.audiobookDuration / 3600)}h{' '} + {Math.floor((data.audiobookDuration % 3600) / 60)}m + +
+ )} + {data.narrators && data.narrators.length > 0 && ( +
+ + {intl.formatMessage(messages.narrators, { + narratorCount: data.narrators.length, + })} + + + {data.narrators.join(', ')} + +
+ )} +
+
+
+ {data.author.length > 0 && ( + <> +
+
+ {intl.formatMessage(messages.author)} +
+
+ ( + + ))} + /> + + )} +
+
+ ); +}; + +export default BookDetails; diff --git a/src/components/CollectionDetails/index.tsx b/src/components/CollectionDetails/index.tsx index c6e1374ab5..b780b99f2d 100644 --- a/src/components/CollectionDetails/index.tsx +++ b/src/components/CollectionDetails/index.tsx @@ -27,7 +27,7 @@ import axios from 'axios'; import { uniq } from 'lodash'; import Link from 'next/link'; import { useRouter } from 'next/router'; -import { useMemo, useState } from 'react'; +import { Fragment, useMemo, useState } from 'react'; import { useIntl } from 'react-intl'; import useSWR from 'swr'; @@ -179,6 +179,7 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => { if (isCollectionBlocklisted) { collectionStatus = MediaStatus.BLOCKLISTED; } else if ( + data.parts.length > 0 && data.parts.every( (part) => part.mediaInfo && part.mediaInfo.status === MediaStatus.AVAILABLE @@ -195,6 +196,7 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => { } if ( + data.parts.length > 0 && data.parts.every( (part) => part.mediaInfo && part.mediaInfo.status4k === MediaStatus.AVAILABLE @@ -264,12 +266,12 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => { )) .reduce((prev, curr) => ( - <> + {intl.formatMessage(globalMessages.delimitedlist, { a: prev, b: curr, })} - + )) ); } @@ -378,11 +380,11 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => { collectionAttributes .map((t, k) => {t}) .reduce((prev, curr) => ( - <> + {prev} | {curr} - + ))}
diff --git a/src/components/Common/Accordion/index.tsx b/src/components/Common/Accordion/index.tsx index 0b434209a3..8b1f7a29bb 100644 --- a/src/components/Common/Accordion/index.tsx +++ b/src/components/Common/Accordion/index.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, type JSX } from 'react'; import AnimateHeight from 'react-animate-height'; export interface AccordionProps { diff --git a/src/components/Common/Button/index.tsx b/src/components/Common/Button/index.tsx index eb422f2248..757ec187ff 100644 --- a/src/components/Common/Button/index.tsx +++ b/src/components/Common/Button/index.tsx @@ -1,4 +1,4 @@ -import type { ForwardedRef } from 'react'; +import type { ForwardedRef, JSX } from 'react'; import React from 'react'; import { twMerge } from 'tailwind-merge'; diff --git a/src/components/Common/ButtonWithDropdown/index.tsx b/src/components/Common/ButtonWithDropdown/index.tsx index 21a88d2061..4a8e8ef62b 100644 --- a/src/components/Common/ButtonWithDropdown/index.tsx +++ b/src/components/Common/ButtonWithDropdown/index.tsx @@ -1,6 +1,6 @@ import Dropdown from '@app/components/Common/Dropdown'; import { withProperties } from '@app/utils/typeHelpers'; -import { Menu } from '@headlessui/react'; +import { Menu, MenuButton } from '@headlessui/react'; import { ChevronDownIcon } from '@heroicons/react/24/solid'; import type { AnchorHTMLAttributes, ButtonHTMLAttributes } from 'react'; @@ -54,13 +54,13 @@ const ButtonWithDropdown = ({ {children && ( - {dropdownIcon ? dropdownIcon : } - + {children} )} diff --git a/src/components/Common/CachedImage/index.tsx b/src/components/Common/CachedImage/index.tsx index 8af2c722db..c3357c722c 100644 --- a/src/components/Common/CachedImage/index.tsx +++ b/src/components/Common/CachedImage/index.tsx @@ -6,7 +6,7 @@ const imageLoader: ImageLoader = ({ src }) => src; export type CachedImageProps = ImageProps & { src: string; - type: 'tmdb' | 'avatar' | 'tvdb'; + type: 'tmdb' | 'avatar' | 'hardcover' | 'tvdb'; }; /** @@ -35,6 +35,14 @@ const CachedImage = ({ src, type, ...props }: CachedImageProps) => { } else if (type === 'avatar') { // jellyfin avatar (if any) imageUrl = src; + } else if (type === 'hardcover') { + // force cache for hardcover + imageUrl = !src.startsWith('/') + ? src.replace( + /^https:\/\/assets\.hardcover\.app\//, + '/imageproxy/hardcover/' + ) + : src; } else { return null; } diff --git a/src/components/Common/Dropdown/index.tsx b/src/components/Common/Dropdown/index.tsx index 9a8501b061..d0e24fae31 100644 --- a/src/components/Common/Dropdown/index.tsx +++ b/src/components/Common/Dropdown/index.tsx @@ -1,5 +1,11 @@ import { withProperties } from '@app/utils/typeHelpers'; -import { Menu, Transition } from '@headlessui/react'; +import { + Menu, + MenuButton, + MenuItem, + MenuItems, + Transition, +} from '@headlessui/react'; import { ChevronDownIcon } from '@heroicons/react/24/solid'; import { Fragment, @@ -19,7 +25,7 @@ const DropdownItem = ({ ...props }: DropdownItemProps) => { return ( - + {children} - + ); }; @@ -55,7 +61,7 @@ const DropdownItems = ({ leaveFrom="opacity-100 scale-100" leaveTo="opacity-0 scale-95" > -
{children}
-
+ ); }; @@ -89,7 +95,7 @@ const Dropdown = ({ return ( - {text} {children && (dropdownIcon ? dropdownIcon : )} - + {children && ( {children} )} diff --git a/src/components/Common/HardcoverSetup/index.tsx b/src/components/Common/HardcoverSetup/index.tsx new file mode 100644 index 0000000000..f3a92e52e2 --- /dev/null +++ b/src/components/Common/HardcoverSetup/index.tsx @@ -0,0 +1,70 @@ +import HardcoverLogo from '@app/assets/hardcover.svg'; +import Button from '@app/components/Common/Button'; +import { Permission, useUser } from '@app/hooks/useUser'; +import defineMessages from '@app/utils/defineMessages'; +import { CogIcon } from '@heroicons/react/24/outline'; +import Link from 'next/link'; +import { useIntl } from 'react-intl'; + +const messages = defineMessages('components.Common.HardcoverSetup', { + title: 'Book Support Requires Hardcover', + description: + 'To browse, search, and request books, a Hardcover API key needs to be configured. You can get one from your Hardcover account.', + opensettings: 'Open Settings', + getapikey: 'Get an API Key', + contactadmin: 'Book support is not currently configured on this server.', + supportnote: 'Consider supporting Hardcover if you enjoy the book features.', +}); + +const HardcoverSetup = () => { + const intl = useIntl(); + const { hasPermission } = useUser(); + + return ( +
+
+
+ +

+ {intl.formatMessage(messages.title)} +

+

+ {intl.formatMessage(messages.description)} +

+
+ {hasPermission(Permission.ADMIN) && ( + + + + )} + +
+ {!hasPermission(Permission.ADMIN) && ( +

+ {intl.formatMessage(messages.contactadmin)} +

+ )} + {hasPermission(Permission.ADMIN) && ( +

+ {intl.formatMessage(messages.supportnote)} +

+ )} +
+
+ ); +}; + +export default HardcoverSetup; diff --git a/src/components/Common/ImageFader/index.tsx b/src/components/Common/ImageFader/index.tsx index 930471e949..a875ed6b68 100644 --- a/src/components/Common/ImageFader/index.tsx +++ b/src/components/Common/ImageFader/index.tsx @@ -7,6 +7,7 @@ interface ImageFaderProps extends HTMLAttributes { rotationSpeed?: number; isDarker?: boolean; forceOptimize?: boolean; + cache?: 'tmdb' | 'avatar' | 'hardcover' | 'tvdb'; } const DEFAULT_ROTATION_SPEED = 6000; @@ -14,6 +15,7 @@ const DEFAULT_ROTATION_SPEED = 6000; const ImageFader: ForwardRefRenderFunction = ( { backgroundImages, + cache = 'tmdb', rotationSpeed = DEFAULT_ROTATION_SPEED, isDarker, forceOptimize, @@ -61,7 +63,7 @@ const ImageFader: ForwardRefRenderFunction = ( {...props} > ); break; + case 'book': + titleCard = ( + 0 + } + canExpand + /> + ); + break; + case 'author': + titleCard = ( + + ); + break; case 'collection': titleCard = ( ( @@ -71,6 +72,7 @@ const Modal = React.forwardRef( cancelButtonProps, secondaryButtonProps, tertiaryButtonProps, + cache = 'tmdb', }, parentRef ) => { @@ -88,8 +90,7 @@ const Modal = React.forwardRef( useLockBodyScroll(true, disableScrollLock); return ReactDOM.createPortal( - ( {backdrop && (
(
)} -
, + , document.body ); } diff --git a/src/components/Common/QuickConnectModal/index.tsx b/src/components/Common/QuickConnectModal/index.tsx new file mode 100644 index 0000000000..b81a3383d1 --- /dev/null +++ b/src/components/Common/QuickConnectModal/index.tsx @@ -0,0 +1,152 @@ +import Alert from '@app/components/Common/Alert'; +import LoadingSpinner from '@app/components/Common/LoadingSpinner'; +import Modal from '@app/components/Common/Modal'; +import { useQuickConnect } from '@app/hooks/useQuickConnect'; +import defineMessages from '@app/utils/defineMessages'; +import { Transition } from '@headlessui/react'; +import { useIntl } from 'react-intl'; + +const messages = defineMessages('components.Common.QuickConnectModal', { + waitingForAuth: 'Waiting for authorization...', + expired: 'Code Expired', + expiredMessage: 'This Quick Connect code has expired. Please try again.', + error: 'Error', + tryAgain: 'Try Again', +}); + +interface QuickConnectModalProps { + show: boolean; + title: string; + subTitle: string; + cancelText: string; + instructionsMessage: string; + dialogClass?: string; + showInlineError?: boolean; + onCancel: () => void; + onSuccess: () => void; + onError?: (error: string) => void; + authenticate: (secret: string) => Promise; +} + +const QuickConnectModal = ({ + show, + title, + subTitle, + cancelText, + instructionsMessage, + dialogClass, + showInlineError, + onCancel, + onSuccess, + onError, + authenticate, +}: QuickConnectModalProps) => { + const intl = useIntl(); + + const { + code, + isLoading, + hasError, + isExpired, + errorMessage, + initiateQuickConnect, + cleanup, + } = useQuickConnect({ + show, + onSuccess, + onError, + authenticate, + }); + + const handleCancel = () => { + cleanup(); + onCancel(); + }; + + return ( + + + {showInlineError && errorMessage && ( +
+ {errorMessage} +
+ )} + + {isLoading && ( +
+ +
+ )} + + {!isLoading && !hasError && !isExpired && ( +
+

{instructionsMessage}

+ +
+
+ + {code} + +
+
+ +
+
+ +
+ {intl.formatMessage(messages.waitingForAuth)} +
+
+ )} + + {hasError && ( +
+
+

+ {intl.formatMessage(messages.error)} +

+

{errorMessage}

+
+
+ )} + + {isExpired && ( +
+
+

+ {intl.formatMessage(messages.expired)} +

+

+ {intl.formatMessage(messages.expiredMessage)} +

+
+
+ )} +
+
+ ); +}; + +export default QuickConnectModal; diff --git a/src/components/Common/SlideOver/index.tsx b/src/components/Common/SlideOver/index.tsx index 7b06ae7305..e2d5aa911c 100644 --- a/src/components/Common/SlideOver/index.tsx +++ b/src/components/Common/SlideOver/index.tsx @@ -1,6 +1,6 @@ /* eslint-disable jsx-a11y/click-events-have-key-events */ import { useLockBodyScroll } from '@app/hooks/useLockBodyScroll'; -import { Transition } from '@headlessui/react'; +import { Transition, TransitionChild } from '@headlessui/react'; import { XMarkIcon } from '@heroicons/react/24/outline'; import { Fragment, useEffect, useRef, useState } from 'react'; import ReactDOM from 'react-dom'; @@ -40,7 +40,7 @@ const SlideOver = ({ enter="transition-opacity ease-in-out duration-300" enterFrom="opacity-0" enterTo="opacity-100" - leave="transition-opacity ease-in-out duration-300" + leave="transition-opacity ease-in-out duration-500 sm:duration-700" leaveFrom="opacity-100" leaveTo="opacity-0" > @@ -56,8 +56,8 @@ const SlideOver = ({ >
-
- +
diff --git a/src/components/Common/StatusBadgeMini/index.tsx b/src/components/Common/StatusBadgeMini/index.tsx index ea93a2a9b5..e90be72eb8 100644 --- a/src/components/Common/StatusBadgeMini/index.tsx +++ b/src/components/Common/StatusBadgeMini/index.tsx @@ -2,16 +2,22 @@ import Spinner from '@app/assets/spinner.svg'; import { CheckCircleIcon } from '@heroicons/react/20/solid'; import { BellIcon, + BookOpenIcon, ClockIcon, EyeSlashIcon, MinusSmallIcon, + SpeakerWaveIcon, TrashIcon, + XMarkIcon, } from '@heroicons/react/24/solid'; import { MediaStatus } from '@server/constants/media'; interface StatusBadgeMiniProps { status: MediaStatus; is4k?: boolean; + audiobook?: boolean; + book?: boolean; + missing?: boolean; inProgress?: boolean; // Should the badge shrink on mobile to a smaller size? (TitleCard) shrink?: boolean; @@ -20,12 +26,23 @@ interface StatusBadgeMiniProps { const StatusBadgeMini = ({ status, is4k = false, + audiobook = false, + book = false, + missing = false, inProgress = false, shrink = false, }: StatusBadgeMiniProps) => { + const hasFormatIcon = audiobook || book; + const badgeStyle = [ `rounded-full shadow-md ${ - shrink ? 'w-4 sm:w-5 border p-0' : 'w-5 ring-1 p-0.5' + hasFormatIcon + ? `flex items-center gap-0.5 ${ + shrink ? 'h-4 border p-0 pr-1 sm:h-5' : 'ring-1 p-0.5 pr-1.5' + }` + : shrink + ? 'w-4 sm:w-5 border p-0' + : 'w-5 ring-1 p-0.5' }`, ]; @@ -66,17 +83,40 @@ const StatusBadgeMini = ({ break; } + if (missing) { + badgeStyle.splice(1); + badgeStyle.push('bg-red-500/80 border-red-400 ring-red-400 text-red-100'); + indicatorIcon = ; + } + if (inProgress) { indicatorIcon = ; } + const formatIcon = audiobook ? ( + + ) : book ? ( + + ) : null; + return (
-
{indicatorIcon}
+
+ {formatIcon ? ( + <> + + {indicatorIcon} + + {formatIcon} + + ) : ( + indicatorIcon + )} +
{is4k && 4K}
); diff --git a/src/components/Common/Tag/index.tsx b/src/components/Common/Tag/index.tsx index 90c3c5ca7e..e55749e471 100644 --- a/src/components/Common/Tag/index.tsx +++ b/src/components/Common/Tag/index.tsx @@ -1,5 +1,5 @@ import { TagIcon } from '@heroicons/react/24/outline'; -import React from 'react'; +import React, { type JSX } from 'react'; type TagProps = { children: React.ReactNode; diff --git a/src/components/Common/Tooltip/index.tsx b/src/components/Common/Tooltip/index.tsx index e8574699ef..5ba75815fe 100644 --- a/src/components/Common/Tooltip/index.tsx +++ b/src/components/Common/Tooltip/index.tsx @@ -5,7 +5,7 @@ import { usePopperTooltip } from 'react-popper-tooltip'; type TooltipProps = { content: React.ReactNode; - children: React.ReactElement; + children: React.ReactElement; tooltipConfig?: Partial; className?: string; }; diff --git a/src/components/Discover/DiscoverBooks/index.tsx b/src/components/Discover/DiscoverBooks/index.tsx new file mode 100644 index 0000000000..7b85d8b824 --- /dev/null +++ b/src/components/Discover/DiscoverBooks/index.tsx @@ -0,0 +1,142 @@ +import Button from '@app/components/Common/Button'; +import HardcoverSetup from '@app/components/Common/HardcoverSetup'; +import Header from '@app/components/Common/Header'; +import ListView from '@app/components/Common/ListView'; +import PageTitle from '@app/components/Common/PageTitle'; +import type { FilterOptions } from '@app/components/Discover/constants'; +import { + countActiveFilters, + prepareFilterValues, +} from '@app/components/Discover/constants'; +import FilterSlideover from '@app/components/Discover/FilterSlideover'; +import useDiscover from '@app/hooks/useDiscover'; +import { useUpdateQueryParams } from '@app/hooks/useUpdateQueryParams'; +import Error from '@app/pages/_error'; +import defineMessages from '@app/utils/defineMessages'; +import { BarsArrowDownIcon, FunnelIcon } from '@heroicons/react/24/solid'; +import type { BookResult } from '@server/models/Search'; +import { useRouter } from 'next/router'; +import { useState } from 'react'; +import { useIntl } from 'react-intl'; + +const messages = defineMessages('components.Discover.DiscoverBooks', { + discoverbooks: 'Books', + activefilters: + '{count, plural, one {# Active Filter} other {# Active Filters}}', + sortPopularityAsc: 'Popularity Ascending', + sortPopularityDesc: 'Popularity Descending', + sortReleaseDateAsc: 'Release Date Ascending', + sortReleaseDateDesc: 'Release Date Descending', + sortTitleAsc: 'Title (A-Z) Ascending', + sortTitleDesc: 'Title (Z-A) Descending', +}); + +const SortOptions = { + PopularityAsc: 'popularity.asc', + PopularityDesc: 'popularity.desc', + ReleaseDateAsc: 'release_date.asc', + ReleaseDateDesc: 'release_date.desc', + TitleAsc: 'original_title.asc', + TitleDesc: 'original_title.desc', +} as const; + +const DiscoverBooks = () => { + const intl = useIntl(); + const router = useRouter(); + const updateQueryParams = useUpdateQueryParams({}); + + const preparedFilters = prepareFilterValues(router.query); + + const { + isLoadingInitialData, + isEmpty, + isLoadingMore, + isReachingEnd, + titles, + fetchMore, + error, + } = useDiscover( + '/api/v1/discover/books', + preparedFilters + ); + const [showFilters, setShowFilters] = useState(false); + + if ((error as any)?.response?.status === 503) { + return ; + } + + if (error) { + return ; + } + + const title = intl.formatMessage(messages.discoverbooks); + + return ( + <> + +
+
{title}
+
+
+ + + + +
+ setShowFilters(false)} + show={showFilters} + /> +
+ +
+
+
+ 0) + } + isReachingEnd={isReachingEnd} + onScrollBottom={fetchMore} + /> + + ); +}; + +export default DiscoverBooks; diff --git a/src/components/Discover/DiscoverMovies/index.tsx b/src/components/Discover/DiscoverMovies/index.tsx index cc96b7c69f..b57364d31a 100644 --- a/src/components/Discover/DiscoverMovies/index.tsx +++ b/src/components/Discover/DiscoverMovies/index.tsx @@ -13,7 +13,7 @@ import { useUpdateQueryParams } from '@app/hooks/useUpdateQueryParams'; import ErrorPage from '@app/pages/_error'; import defineMessages from '@app/utils/defineMessages'; import { BarsArrowDownIcon, FunnelIcon } from '@heroicons/react/24/solid'; -import type { SortOptions as TMDBSortOptions } from '@server/api/themoviedb'; +import type { MovieSortOptions as TMDBSortOptions } from '@server/api/themoviedb'; import type { MovieResult } from '@server/models/Search'; import { useRouter } from 'next/router'; import { useState } from 'react'; diff --git a/src/components/Discover/DiscoverSliderEdit/index.tsx b/src/components/Discover/DiscoverSliderEdit/index.tsx index c24a0591ad..1db2e382a1 100644 --- a/src/components/Discover/DiscoverSliderEdit/index.tsx +++ b/src/components/Discover/DiscoverSliderEdit/index.tsx @@ -169,6 +169,8 @@ const DiscoverSliderEdit = ({ return intl.formatMessage(sliderTitles.tmdbmoviestreamingservices); case DiscoverSliderType.TMDB_TV_STREAMING_SERVICES: return intl.formatMessage(sliderTitles.tmdbtvstreamingservices); + case DiscoverSliderType.POPULAR_BOOKS: + return intl.formatMessage(sliderTitles.popularbooks); default: return 'Unknown Slider'; } diff --git a/src/components/Discover/DiscoverTv/index.tsx b/src/components/Discover/DiscoverTv/index.tsx index c5611c3128..36f72bf865 100644 --- a/src/components/Discover/DiscoverTv/index.tsx +++ b/src/components/Discover/DiscoverTv/index.tsx @@ -13,7 +13,7 @@ import { useUpdateQueryParams } from '@app/hooks/useUpdateQueryParams'; import ErrorPage from '@app/pages/_error'; import defineMessages from '@app/utils/defineMessages'; import { BarsArrowDownIcon, FunnelIcon } from '@heroicons/react/24/solid'; -import type { SortOptions as TMDBSortOptions } from '@server/api/themoviedb'; +import type { TvSortOptions as TMDBSortOptions } from '@server/api/themoviedb'; import type { TvResult } from '@server/models/Search'; import { useRouter } from 'next/router'; import { useState } from 'react'; @@ -40,8 +40,8 @@ const SortOptions: Record = { FirstAirDateDesc: 'first_air_date.desc', TmdbRatingAsc: 'vote_average.asc', TmdbRatingDesc: 'vote_average.desc', - TitleAsc: 'original_title.asc', - TitleDesc: 'original_title.desc', + TitleAsc: 'original_name.asc', + TitleDesc: 'original_name.desc', } as const; const DiscoverTv = () => { diff --git a/src/components/Discover/FilterSlideover/index.tsx b/src/components/Discover/FilterSlideover/index.tsx index a20cb50142..930041e0d4 100644 --- a/src/components/Discover/FilterSlideover/index.tsx +++ b/src/components/Discover/FilterSlideover/index.tsx @@ -5,6 +5,7 @@ import type { FilterOptions } from '@app/components/Discover/constants'; import { countActiveFilters } from '@app/components/Discover/constants'; import LanguageSelector from '@app/components/LanguageSelector'; import { + BookGenreSelector, CompanySelector, GenreSelector, KeywordSelector, @@ -50,7 +51,7 @@ const messages = defineMessages('components.Discover.FilterSlideover', { type FilterSlideoverProps = { show: boolean; onClose: () => void; - type: 'movie' | 'tv'; + type: 'movie' | 'tv' | 'book'; currentFilters: FilterOptions; }; @@ -65,10 +66,101 @@ const FilterSlideover = ({ const updateQueryParams = useUpdateQueryParams({}); const batchUpdateQueryParams = useBatchUpdateQueryParams({}); - const dateGte = - type === 'movie' ? 'primaryReleaseDateGte' : 'firstAirDateGte'; - const dateLte = - type === 'movie' ? 'primaryReleaseDateLte' : 'firstAirDateLte'; + const dateGte = type === 'tv' ? 'firstAirDateGte' : 'primaryReleaseDateGte'; + const dateLte = type === 'tv' ? 'firstAirDateLte' : 'primaryReleaseDateLte'; + + if (type === 'book') { + return ( + onClose()} + > +
+
+
+ {intl.formatMessage(messages.releaseDate)} +
+
+
+
{intl.formatMessage(messages.from)}
+ { + updateQueryParams( + dateGte, + value?.startDate ? (value.startDate as string) : undefined + ); + }} + inputName="fromdate" + useRange={false} + asSingle + containerClassName="datepicker-wrapper" + inputClassName="pr-1 sm:pr-4 text-base leading-5" + /> +
+
+
{intl.formatMessage(messages.to)}
+ { + updateQueryParams( + dateLte, + value?.startDate ? (value.startDate as string) : undefined + ); + }} + inputName="todate" + useRange={false} + asSingle + containerClassName="datepicker-wrapper" + inputClassName="pr-1 sm:pr-4 text-base leading-5" + /> +
+
+
+ + {intl.formatMessage(messages.genres)} + + { + updateQueryParams('genre', value?.map((v) => v.value).join(',')); + }} + /> +
+ +
+
+
+ ); + } return ( { const { hasPermission } = useUser(); const { data: requests, error: requestError } = useSWR( - '/api/v1/request?filter=all&take=10&sort=modified&skip=0', + '/api/v1/request?filter=all&take=10&sort=added&skip=0', { revalidateOnMount: true, } diff --git a/src/components/Discover/constants.ts b/src/components/Discover/constants.ts index 4ce5e34f66..351096f99a 100644 --- a/src/components/Discover/constants.ts +++ b/src/components/Discover/constants.ts @@ -70,6 +70,7 @@ export const sliderTitles = defineMessages('components.Discover', { recentrequests: 'Recent Requests', popularmovies: 'Popular Movies', populartv: 'Popular Series', + popularbooks: 'Popular Books', upcomingtv: 'Upcoming Series', recentlyAdded: 'Recently Added', upcoming: 'Upcoming Movies', diff --git a/src/components/Discover/index.tsx b/src/components/Discover/index.tsx index 5638d6fb34..d726ecc2e3 100644 --- a/src/components/Discover/index.tsx +++ b/src/components/Discover/index.tsx @@ -147,6 +147,7 @@ const Discover = () => { )} { { /> ); break; + case DiscoverSliderType.POPULAR_BOOKS: + sliderComponent = ( + + ); + break; } if (isEditing) { diff --git a/src/components/ExternalLinkBlock/index.tsx b/src/components/ExternalLinkBlock/index.tsx index 3a26b8506a..2e1704d3d8 100644 --- a/src/components/ExternalLinkBlock/index.tsx +++ b/src/components/ExternalLinkBlock/index.tsx @@ -4,6 +4,7 @@ import JellyfinLogo from '@app/assets/services/jellyfin.svg'; import LetterboxdLogo from '@app/assets/services/letterboxd.svg'; import PlexLogo from '@app/assets/services/plex.svg'; import RTLogo from '@app/assets/services/rt.svg'; +import SimklLogo from '@app/assets/services/simkl.svg'; import TmdbLogo from '@app/assets/services/tmdb.svg'; import TraktLogo from '@app/assets/services/trakt.svg'; import TvdbLogo from '@app/assets/services/tvdb.svg'; @@ -35,7 +36,7 @@ const ExternalLinkBlock = ({ const { locale } = useLocale(); return ( -
+
{mediaUrl && ( )} + {imdbId && mediaType !== 'person' && ( + + + + )} {tmdbId && mediaType === MediaType.MOVIE && ( ( <>
- + Open options +
-
{isActiveUser && ( - + {({ active }) => ( )} - + )} - + {({ active }) => ( )} - +
-
+
)} diff --git a/src/components/IssueDetails/IssueDescription/index.tsx b/src/components/IssueDetails/IssueDescription/index.tsx index 274d202936..e3b2bbf53d 100644 --- a/src/components/IssueDetails/IssueDescription/index.tsx +++ b/src/components/IssueDetails/IssueDescription/index.tsx @@ -2,7 +2,13 @@ import Button from '@app/components/Common/Button'; import { Permission, useUser } from '@app/hooks/useUser'; import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; -import { Menu, Transition } from '@headlessui/react'; +import { + Menu, + MenuButton, + MenuItem, + MenuItems, + Transition, +} from '@headlessui/react'; import { EllipsisVerticalIcon } from '@heroicons/react/24/solid'; import { Field, Form, Formik } from 'formik'; import { useState } from 'react'; @@ -45,13 +51,13 @@ const IssueDescription = ({ {({ open }) => ( <>
- + Open options +
-
{belongsToUser && ( - + {({ active }) => ( )} - + )} {(hasPermission(Permission.MANAGE_ISSUES) || !commentCount) && ( - + {({ active }) => ( )} - + )}
-
+
)} diff --git a/src/components/IssueDetails/index.tsx b/src/components/IssueDetails/index.tsx index e272da96ba..749f44d896 100644 --- a/src/components/IssueDetails/index.tsx +++ b/src/components/IssueDetails/index.tsx @@ -26,6 +26,7 @@ import { IssueStatus } from '@server/constants/issue'; import { MediaType } from '@server/constants/media'; import { MediaServerType } from '@server/constants/server'; import type Issue from '@server/entity/Issue'; +import type { BookDetails } from '@server/models/Book'; import type { MovieDetails } from '@server/models/Movie'; import type { TvDetails } from '@server/models/Tv'; import axios from 'axios'; @@ -73,8 +74,16 @@ const messages = defineMessages('components.IssueDetails', { commentplaceholder: 'Add a comment…', }); -const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => { - return (movie as MovieDetails).title !== undefined; +const isMovie = ( + media: MovieDetails | TvDetails | BookDetails +): media is MovieDetails => { + return (media as MovieDetails).title !== undefined; +}; + +const isBook = ( + media: MovieDetails | TvDetails | BookDetails +): media is BookDetails => { + return (media as BookDetails).author !== undefined; }; const IssueDetails = () => { @@ -86,7 +95,7 @@ const IssueDetails = () => { const { data: issueData, mutate: revalidateIssue } = useSWR( `/api/v1/issue/${router.query.issueId}` ); - const { data, error } = useSWR( + const { data, error } = useSWR( issueData?.media.tmdbId ? `/api/v1/${issueData.media.mediaType}/${issueData.media.tmdbId}` : null @@ -175,8 +184,9 @@ const IssueDetails = () => { } }; - const title = isMovie(data) ? data.title : data.name; - const releaseYear = isMovie(data) ? data.releaseDate : data.firstAirDate; + const title = isBook(data) || isMovie(data) ? data.title : data.name; + const releaseYear = + isBook(data) || isMovie(data) ? data.releaseDate : data.firstAirDate; return (
{ onOk={() => deleteIssue()} okText={intl.formatMessage(messages.deleteissue)} okButtonType="danger" + cache={ + issueData.media.mediaType === MediaType.BOOK ? 'hardcover' : 'tmdb' + } > {intl.formatMessage(messages.deleteissueconfirm)} @@ -209,9 +222,13 @@ const IssueDetails = () => { {data.backdropPath && (
{
{

@@ -414,7 +437,9 @@ const IssueDetails = () => { arr: issueData.media.mediaType === MediaType.MOVIE ? 'Radarr' - : 'Sonarr', + : issueData.media.mediaType === MediaType.BOOK + ? 'Readarr' + : 'Sonarr', })} @@ -462,7 +487,9 @@ const IssueDetails = () => { arr: issueData.media.mediaType === MediaType.MOVIE ? 'Radarr' - : 'Sonarr', + : issueData.media.mediaType === MediaType.BOOK + ? 'Readarr' + : 'Sonarr', })} @@ -679,7 +706,9 @@ const IssueDetails = () => { arr: issueData.media.mediaType === MediaType.MOVIE ? 'Radarr' - : 'Sonarr', + : issueData.media.mediaType === MediaType.BOOK + ? 'Readarr' + : 'Sonarr', })} @@ -727,7 +756,9 @@ const IssueDetails = () => { arr: issueData.media.mediaType === MediaType.MOVIE ? 'Radarr' - : 'Sonarr', + : issueData.media.mediaType === MediaType.BOOK + ? 'Readarr' + : 'Sonarr', })} diff --git a/src/components/IssueList/IssueItem/index.tsx b/src/components/IssueList/IssueItem/index.tsx index 23b7d5a680..35d37f721d 100644 --- a/src/components/IssueList/IssueItem/index.tsx +++ b/src/components/IssueList/IssueItem/index.tsx @@ -10,6 +10,7 @@ import { EyeIcon } from '@heroicons/react/24/solid'; import { IssueStatus } from '@server/constants/issue'; import { MediaType } from '@server/constants/media'; import type Issue from '@server/entity/Issue'; +import type { BookDetails } from '@server/models/Book'; import type { MovieDetails } from '@server/models/Movie'; import type { TvDetails } from '@server/models/Tv'; import Link from 'next/link'; @@ -30,8 +31,16 @@ const messages = defineMessages('components.IssueList.IssueItem', { descriptionpreview: 'Issue Description', }); -const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => { - return (movie as MovieDetails).title !== undefined; +const isMovie = ( + media: MovieDetails | TvDetails | BookDetails +): media is MovieDetails => { + return (media as MovieDetails).title !== undefined; +}; + +const isBook = ( + media: MovieDetails | TvDetails | BookDetails +): media is BookDetails => { + return (media as BookDetails).author !== undefined; }; interface IssueItemProps { @@ -47,8 +56,10 @@ const IssueItem = ({ issue }: IssueItemProps) => { const url = issue.media.mediaType === 'movie' ? `/api/v1/movie/${issue.media.tmdbId}` - : `/api/v1/tv/${issue.media.tmdbId}`; - const { data: title, error } = useSWR( + : issue.media.mediaType === 'book' + ? `/api/v1/book/${issue.media.tmdbId}` + : `/api/v1/tv/${issue.media.tmdbId}`; + const { data: title, error } = useSWR( inView ? url : null ); @@ -121,8 +132,12 @@ const IssueItem = ({ issue }: IssueItemProps) => { {title.backdropPath && (
{ href={ issue.media.mediaType === MediaType.MOVIE ? `/movie/${issue.media.tmdbId}` - : `/tv/${issue.media.tmdbId}` + : issue.media.mediaType === MediaType.BOOK + ? `/book/${issue.media.tmdbId}` + : `/tv/${issue.media.tmdbId}` } className="relative h-auto w-12 flex-shrink-0 scale-100 transform-gpu overflow-hidden rounded-md transition duration-300 hover:scale-105" > {
- {(isMovie(title) ? title.releaseDate : title.firstAirDate)?.slice( - 0, - 4 - )} + {(isBook(title) + ? title.releaseDate + : isMovie(title) + ? title.releaseDate + : title.firstAirDate + )?.slice(0, 4)}
- {isMovie(title) ? title.title : title.name} + {isBook(title) + ? title.title + : isMovie(title) + ? title.title + : title.name} {description && (
diff --git a/src/components/IssueModal/CreateIssueModal/index.tsx b/src/components/IssueModal/CreateIssueModal/index.tsx index e874889d1d..14cc543912 100644 --- a/src/components/IssueModal/CreateIssueModal/index.tsx +++ b/src/components/IssueModal/CreateIssueModal/index.tsx @@ -6,10 +6,11 @@ import useToasts from '@app/hooks/useToasts'; import { Permission, useUser } from '@app/hooks/useUser'; import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; -import { RadioGroup } from '@headlessui/react'; +import { Label, Radio, RadioGroup } from '@headlessui/react'; import { ArrowRightCircleIcon } from '@heroicons/react/24/solid'; import { MediaStatus } from '@server/constants/media'; import type Issue from '@server/entity/Issue'; +import type { BookDetails } from '@server/models/Book'; import type { MovieDetails } from '@server/models/Movie'; import type { TvDetails } from '@server/models/Tv'; import axios from 'axios'; @@ -39,16 +40,26 @@ const messages = defineMessages('components.IssueModal.CreateIssueModal', { submitissue: 'Submit Issue', }); -const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => { +const isMovie = ( + movie: MovieDetails | TvDetails | BookDetails | null +): movie is MovieDetails => { + if (!movie) return false; return (movie as MovieDetails).title !== undefined; }; +const isBook = ( + item: MovieDetails | TvDetails | BookDetails | null +): item is BookDetails => { + if (!item) return false; + return (item as BookDetails).id !== undefined && 'author' in item; +}; + const classNames = (...classes: string[]) => { return classes.filter(Boolean).join(' '); }; interface CreateIssueModalProps { - mediaType: 'movie' | 'tv'; + mediaType: 'movie' | 'tv' | 'book'; tmdbId?: number; onCancel?: () => void; } @@ -62,7 +73,7 @@ const CreateIssueModal = ({ const settings = useSettings(); const { hasPermission } = useUser(); const { addToast } = useToasts(); - const { data, error } = useSWR( + const { data, error } = useSWR( tmdbId ? `/api/v1/${mediaType}/${tmdbId}` : null ); @@ -115,7 +126,11 @@ const CreateIssueModal = ({ <>
{intl.formatMessage(messages.toastSuccessCreate, { - title: isMovie(data) ? data.title : data.name, + title: isMovie(data) + ? data.title + : isBook(data) + ? data.title + : data.name, strong: (msg: React.ReactNode) => {msg}, })}
@@ -152,14 +167,29 @@ const CreateIssueModal = ({ backgroundClickable onCancel={onCancel} title={intl.formatMessage(messages.reportissue)} - subTitle={data && isMovie(data) ? data?.title : data?.name} + subTitle={ + data + ? isMovie(data) + ? data.title + : isBook(data) + ? data.title + : data.name + : undefined + } cancelText={intl.formatMessage(globalMessages.close)} onOk={() => handleSubmit()} okText={intl.formatMessage(messages.submitissue)} loading={!data && !error} - backdrop={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${data?.backdropPath}`} + backdrop={ + data && isBook(data) + ? data.backdropPath + : data?.backdropPath + ? `https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${data.backdropPath}` + : undefined + } + cache={mediaType === 'book' ? 'hardcover' : 'tmdb'} > - {mediaType === 'tv' && data && !isMovie(data) && ( + {mediaType === 'tv' && data && !isMovie(data) && !isBook(data) && ( <>

+ {process.env.unsafeDoNotUseDemo === 'true' && ( + + {intl.formatMessage(messages.demoModeInfo)} + + )} +
{ @@ -51,6 +59,8 @@ const messages = defineMessages('components.ManageSlideOver', { manageModalRequests: 'Requests', manageModalMedia: 'Media', manageModalMedia4k: '4K Media', + manageModalMediaAudiobook: 'Audiobook Media', + manageModalMediaEbook: 'Ebook Media', manageModalAdvanced: 'Advanced', manageModalNoRequests: 'No requests.', manageModalClearMedia: 'Clear Data', @@ -62,9 +72,15 @@ const messages = defineMessages('components.ManageSlideOver', { removearr: 'Remove from {arr}', openarr4k: 'Open in 4K {arr}', removearr4k: 'Remove from 4K {arr}', + openarraudiobook: 'Open in Audiobook {arr}', + removearraudiobook: 'Remove Audiobook from {arr}', + removearrebook: 'Remove Ebook from {arr}', + clearmediadataerror: 'Something went wrong while clearing the media data.', + removemediaerror: 'Something went wrong while removing the media.', downloadstatus: 'Downloads', markavailable: 'Mark as Available', mark4kavailable: 'Mark as Available in 4K', + markaudiobookavailable: 'Mark as Available (Audiobook)', markallseasonsavailable: 'Mark All Seasons as Available', markallseasons4kavailable: 'Mark All Seasons as Available in 4K', opentautulli: 'Open in Tautulli', @@ -75,12 +91,21 @@ const messages = defineMessages('components.ManageSlideOver', { playedby: 'Played By', movie: 'movie', tvshow: 'series', + book: 'book', }); -const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => { +const isMovie = ( + movie: MovieDetails | TvDetails | BookDetails +): movie is MovieDetails => { return (movie as MovieDetails).title !== undefined; }; +const isBook = ( + book: MovieDetails | TvDetails | BookDetails +): book is BookDetails => { + return (book as BookDetails).author !== undefined; +}; + interface ManageSlideOverProps { // mediaType: 'movie' | 'tv'; show?: boolean; @@ -98,15 +123,24 @@ interface ManageSlideOverTvProps extends ManageSlideOverProps { data: TvDetails; } +interface ManageSlideOverBookProps extends ManageSlideOverProps { + mediaType: 'book'; + data: BookDetails; +} + const ManageSlideOver = ({ show, mediaType, onClose, data, revalidate, -}: ManageSlideOverMovieProps | ManageSlideOverTvProps) => { +}: + | ManageSlideOverMovieProps + | ManageSlideOverTvProps + | ManageSlideOverBookProps) => { const { user: currentUser, hasPermission } = useUser(); const intl = useIntl(); + const { addToast } = useToasts(); const settings = useSettings(); const { data: watchData } = useSWR( settings.currentSettings.mediaServerType === MediaServerType.PLEX && @@ -121,21 +155,41 @@ const ManageSlideOver = ({ const { data: sonarrData } = useSWR( hasPermission(Permission.ADMIN) ? '/api/v1/settings/sonarr' : null ); + const { data: readarrData } = useSWR( + hasPermission(Permission.ADMIN) ? '/api/v1/settings/readarr' : null + ); const deleteMedia = async () => { if (data.mediaInfo) { - await axios.delete(`/api/v1/media/${data.mediaInfo.id}`); - revalidate(); - onClose(); + try { + await axios.delete(`/api/v1/media/${data.mediaInfo.id}`); + revalidate(); + onClose(); + } catch { + addToast(intl.formatMessage(messages.clearmediadataerror), { + appearance: 'error', + autoDismiss: true, + }); + } } }; const deleteMediaFile = async (is4k = false) => { if (data.mediaInfo) { - await axios.delete( - `/api/v1/media/${data.mediaInfo.id}/file?is4k=${is4k}` - ); - await axios.delete(`/api/v1/media/${data.mediaInfo.id}`); + try { + await axios.delete( + `/api/v1/media/${data.mediaInfo.id}/file?is4k=${is4k}` + ); + } catch (e) { + if (!axios.isAxiosError(e) || e.response?.status !== 404) { + addToast(intl.formatMessage(messages.removemediaerror), { + appearance: 'error', + autoDismiss: true, + }); + revalidate(); + return; + } + } revalidate(); onClose(); } @@ -150,6 +204,13 @@ const ManageSlideOver = ({ radarr.isDefault && radarr.id === data.mediaInfo?.serviceId ) !== undefined ); + } else if (data.mediaInfo.mediaType === MediaType.BOOK) { + return ( + readarrData?.find( + (readarr) => + readarr.isDefault && readarr.id === data.mediaInfo?.serviceId + ) !== undefined + ); } else { return ( sonarrData?.find( @@ -173,6 +234,15 @@ const ManageSlideOver = ({ radarr.id === data.mediaInfo?.serviceId4k ) !== undefined ); + } else if (data.mediaInfo.mediaType === MediaType.BOOK) { + return ( + readarrData?.find( + (readarr) => + readarr.isDefault && + readarr.is4k && + readarr.id === data.mediaInfo?.serviceId4k + ) !== undefined + ); } else { return ( sonarrData?.find( @@ -227,11 +297,15 @@ const ManageSlideOver = ({ show={show} title={intl.formatMessage(messages.manageModalTitle, { mediaType: intl.formatMessage( - mediaType === 'movie' ? globalMessages.movie : globalMessages.tvshow + mediaType === 'movie' + ? globalMessages.movie + : mediaType === 'tv' + ? globalMessages.tvshow + : messages.book ), })} onClose={() => onClose()} - subText={isMovie(data) ? data.title : data.name} + subText={isBook(data) || isMovie(data) ? data.title : data.name} >
{((data?.mediaInfo?.downloadStatus ?? []).length > 0 || @@ -335,7 +409,11 @@ const ManageSlideOver = ({ watchData?.data) && (

- {intl.formatMessage(messages.manageModalMedia)} + {intl.formatMessage( + mediaType === 'book' + ? messages.manageModalMediaEbook + : messages.manageModalMedia + )}

{(watchData?.data || data.mediaInfo?.tautulliUrl) && ( @@ -446,7 +524,12 @@ const ManageSlideOver = ({ {intl.formatMessage(messages.openarr, { - arr: mediaType === 'movie' ? 'Radarr' : 'Sonarr', + arr: + mediaType === 'movie' + ? 'Radarr' + : mediaType === 'tv' + ? 'Sonarr' + : 'Readarr', })} @@ -466,9 +549,19 @@ const ManageSlideOver = ({ > - {intl.formatMessage(messages.removearr, { - arr: mediaType === 'movie' ? 'Radarr' : 'Sonarr', - })} + {intl.formatMessage( + mediaType === 'book' + ? messages.removearrebook + : messages.removearr, + { + arr: + mediaType === 'movie' + ? 'Radarr' + : mediaType === 'tv' + ? 'Sonarr' + : 'Readarr', + } + )}
@@ -478,9 +571,16 @@ const ManageSlideOver = ({ mediaType: intl.formatMessage( mediaType === 'movie' ? messages.movie - : messages.tvshow + : mediaType === 'tv' + ? messages.tvshow + : messages.book ), - arr: mediaType === 'movie' ? 'Radarr' : 'Sonarr', + arr: + mediaType === 'movie' + ? 'Radarr' + : mediaType === 'tv' + ? 'Sonarr' + : 'Readarr', } )}
@@ -495,7 +595,11 @@ const ManageSlideOver = ({ watchData?.data4k) && (

- {intl.formatMessage(messages.manageModalMedia4k)} + {intl.formatMessage( + mediaType === 'book' + ? messages.manageModalMediaAudiobook + : messages.manageModalMedia4k + )}

{(watchData?.data4k || data.mediaInfo?.tautulliUrl4k) && ( @@ -608,9 +712,19 @@ const ManageSlideOver = ({ @@ -625,9 +739,19 @@ const ManageSlideOver = ({ > - {intl.formatMessage(messages.removearr4k, { - arr: mediaType === 'movie' ? 'Radarr' : 'Sonarr', - })} + {intl.formatMessage( + mediaType === 'book' + ? messages.removearraudiobook + : messages.removearr4k, + { + arr: + mediaType === 'movie' + ? 'Radarr' + : mediaType === 'tv' + ? 'Sonarr' + : 'Readarr', + } + )}
@@ -637,9 +761,16 @@ const ManageSlideOver = ({ mediaType: intl.formatMessage( mediaType === 'movie' ? messages.movie - : messages.tvshow + : mediaType === 'tv' + ? messages.tvshow + : messages.book ), - arr: mediaType === 'movie' ? 'Radarr' : 'Sonarr', + arr: + mediaType === 'movie' + ? 'Radarr' + : mediaType === 'tv' + ? 'Sonarr' + : 'Readarr', } )}
@@ -669,13 +800,20 @@ const ManageSlideOver = ({ {intl.formatMessage( mediaType === 'movie' ? messages.markavailable - : messages.markallseasonsavailable + : mediaType === 'book' + ? messages.markavailable + : messages.markallseasonsavailable )} )} {data?.mediaInfo.status4k !== MediaStatus.AVAILABLE && - settings.currentSettings.series4kEnabled && ( + ((mediaType === 'tv' && + settings.currentSettings.series4kEnabled) || + (mediaType === 'movie' && + settings.currentSettings.movie4kEnabled) || + (mediaType === 'book' && + settings.currentSettings.bookAudioEnabled)) && ( @@ -705,7 +845,11 @@ const ManageSlideOver = ({
{intl.formatMessage(messages.manageModalClearMediaWarning, { mediaType: intl.formatMessage( - mediaType === 'movie' ? messages.movie : messages.tvshow + mediaType === 'movie' + ? messages.movie + : mediaType === 'tv' + ? messages.tvshow + : messages.book ), mediaServerName: settings.currentSettings.mediaServerType === diff --git a/src/components/MediaSlider/ShowMoreCard/index.tsx b/src/components/MediaSlider/ShowMoreCard/index.tsx index 8b85f6ba15..bf1742d368 100644 --- a/src/components/MediaSlider/ShowMoreCard/index.tsx +++ b/src/components/MediaSlider/ShowMoreCard/index.tsx @@ -13,7 +13,10 @@ const messages = defineMessages('components.MediaSlider.ShowMoreCard', { interface ShowMoreCardProps { url: string; - posters: (string | undefined)[]; + posters: { + url: string | undefined; + cache: 'tmdb' | 'avatar' | 'hardcover' | 'tvdb'; + }[]; } const ShowMoreCard = ({ url, posters }: ShowMoreCardProps) => { @@ -57,11 +60,15 @@ const ShowMoreCard = ({ url, posters }: ShowMoreCardProps) => {
- {posters[0] && ( + {posters[0]?.url && (
{ />
)} - {posters[1] && ( + {posters[1]?.url && (
{ />
)} - {posters[2] && ( + {posters[2]?.url && (
{ />
)} - {posters[3] && ( + {posters[3]?.url && (
{ const settings = useSettings(); const { hasPermission } = useUser(); - const { data, error, setSize, size } = useSWRInfinite( + const { data, setSize, size } = useSWRInfinite( (pageIndex: number, previousPageData: MixedResult | null) => { if (previousPageData && pageIndex + 1 > previousPageData.totalPages) { return null; @@ -62,26 +63,43 @@ const MediaSlider = ({ let titles = (data ?? []).reduce( (a, v) => [...a, ...v.results], - [] as (MovieResult | TvResult | PersonResult)[] + [] as (MovieResult | TvResult | PersonResult | BookResult)[] ); if (settings.currentSettings.hideAvailable) { titles = titles.filter( (i) => - (i.mediaType === 'movie' || i.mediaType === 'tv') && - i.mediaInfo?.status !== MediaStatus.AVAILABLE && - i.mediaInfo?.status !== MediaStatus.PARTIALLY_AVAILABLE + !( + i.mediaType === 'movie' || + i.mediaType === 'tv' || + i.mediaType === 'book' + ) || + (i.mediaInfo?.status !== MediaStatus.AVAILABLE && + i.mediaInfo?.status !== MediaStatus.PARTIALLY_AVAILABLE) ); } if (settings.currentSettings.hideBlocklisted) { titles = titles.filter( (i) => - (i.mediaType === 'movie' || i.mediaType === 'tv') && - i.mediaInfo?.status !== MediaStatus.BLOCKLISTED + !( + i.mediaType === 'movie' || + i.mediaType === 'tv' || + i.mediaType === 'book' + ) || i.mediaInfo?.status !== MediaStatus.BLOCKLISTED ); } + if (settings.currentSettings.hideRequested) { + titles = titles.filter((i) => { + if (i.mediaType !== 'movie' && i.mediaType !== 'tv') { + return true; + } + + return !i.mediaInfo?.hasActiveRequest; + }); + } + useEffect(() => { if ( titles.length < 24 && @@ -151,6 +169,23 @@ const MediaSlider = ({ inProgress={(title.mediaInfo?.downloadStatus ?? []).length > 0} /> ); + case 'book': + return ( + 0} + /> + ); case 'person': return ( - title.mediaType !== 'person' ? title.posterPath : undefined - )} + posters={titles.slice(20, 24).map((title) => + title.mediaType !== 'person' + ? { + url: title.posterPath ?? '', + cache: title.mediaType === 'book' ? 'hardcover' : 'tmdb', + } + : { url: undefined, cache: 'tmdb' } + )} /> ); } @@ -191,7 +229,7 @@ const MediaSlider = ({
diff --git a/src/components/MovieDetails/index.tsx b/src/components/MovieDetails/index.tsx index 4df319d387..80aded586e 100644 --- a/src/components/MovieDetails/index.tsx +++ b/src/components/MovieDetails/index.tsx @@ -59,7 +59,7 @@ import 'country-flag-icons/3x2/flags.css'; import { uniqBy } from 'lodash'; import Link from 'next/link'; import { useRouter } from 'next/router'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { Fragment, useCallback, useEffect, useMemo, useState } from 'react'; import { useIntl } from 'react-intl'; import useSWR from 'swr'; @@ -118,9 +118,7 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => { const router = useRouter(); const intl = useIntl(); const { locale } = useLocale(); - const [showManager, setShowManager] = useState( - router.query.manage == '1' ? true : false - ); + const [showManager, setShowManager] = useState(false); const minStudios = 3; const [showMoreStudios, setShowMoreStudios] = useState(false); const [showIssueModal, setShowIssueModal] = useState(false); @@ -158,8 +156,14 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => { ); useEffect(() => { - setShowManager(router.query.manage == '1' ? true : false); - }, [router.query.manage]); + if (router.query.manage === '1') { + setShowManager(true); + router.replace({ + pathname: router.pathname, + query: { movieId: router.query.movieId }, + }); + } + }, [router, router.query.manage]); const closeBlocklistModal = useCallback( () => setShowBlocklistModal(false), @@ -279,12 +283,12 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => { )) .reduce((prev, curr) => ( - <> + {intl.formatMessage(globalMessages.delimitedlist, { a: prev, b: curr, })} - + )) ); } @@ -469,7 +473,7 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => { mediaType="movie" onClose={() => { setShowManager(false); - router.push({ + router.replace({ pathname: router.pathname, query: { movieId: router.query.movieId }, }); @@ -553,11 +557,11 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => { movieAttributes .map((t, k) => {t}) .reduce((prev, curr) => ( - <> + {prev} | {curr} - + ))}
@@ -1067,7 +1071,7 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => { {streamingProviders.map((p) => { return ( - + {movies} per {quotaDays} {days}', tvRequests: '{quotaLimit} {seasons} per {quotaDays} {days}', + bookRequests: + '{quotaLimit} {books} per {quotaDays} {days}', movies: '{count, plural, one {movie} other {movies}}', seasons: '{count, plural, one {season} other {seasons}}', + books: '{count, plural, one {book} other {books}}', days: '{count, plural, one {day} other {days}}', unlimited: 'Unlimited', }); interface QuotaSelectorProps { - mediaType: 'movie' | 'tv'; + mediaType: 'movie' | 'tv' | 'book'; defaultDays?: number; defaultLimit?: number; dayOverride?: number; @@ -53,7 +56,11 @@ const QuotaSelector = ({ return (
{intl.formatMessage( - mediaType === 'movie' ? messages.movieRequests : messages.tvRequests, + mediaType === 'movie' + ? messages.movieRequests + : mediaType === 'book' + ? messages.bookRequests + : messages.tvRequests, { quotaLimit: (
diff --git a/src/components/RequestModal/AdvancedRequester/index.tsx b/src/components/RequestModal/AdvancedRequester/index.tsx index c9362ae8f0..3531e67112 100644 --- a/src/components/RequestModal/AdvancedRequester/index.tsx +++ b/src/components/RequestModal/AdvancedRequester/index.tsx @@ -1,21 +1,32 @@ /* eslint-disable react-hooks/exhaustive-deps */ import CachedImage from '@app/components/Common/CachedImage'; import { SmallLoadingSpinner } from '@app/components/Common/LoadingSpinner'; +import SlideCheckbox from '@app/components/Common/SlideCheckbox'; +import useToasts from '@app/hooks/useToasts'; import type { User } from '@app/hooks/useUser'; import { Permission, useUser } from '@app/hooks/useUser'; import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; import { formatBytes } from '@app/utils/numberHelpers'; -import { Listbox, Transition } from '@headlessui/react'; +import { + Label, + Listbox, + ListboxButton, + ListboxOption, + ListboxOptions, + Transition, +} from '@headlessui/react'; import { CheckIcon, ChevronDownIcon } from '@heroicons/react/24/solid'; import type { ServiceCommonServer, ServiceCommonServerWithDetails, } from '@server/interfaces/api/serviceInterfaces'; import type { UserResultsResponse } from '@server/interfaces/api/userInterfaces'; +import type { OverrideRulesResult } from '@server/lib/overrideRules'; import { hasPermission } from '@server/lib/permissions'; +import axios from 'axios'; import { isEqual } from 'lodash'; -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { useIntl } from 'react-intl'; import Select from 'react-select'; import useSWR from 'swr'; @@ -29,6 +40,7 @@ const messages = defineMessages('components.RequestModal.AdvancedRequester', { advancedoptions: 'Advanced', destinationserver: 'Destination Server', qualityprofile: 'Quality Profile', + metadataprofile: 'Metadata Profile', rootfolder: 'Root Folder', animenote: '* This series is an anime.', default: '{name} (Default)', @@ -38,38 +50,57 @@ const messages = defineMessages('components.RequestModal.AdvancedRequester', { tags: 'Tags', selecttags: 'Select tags', notagoptions: 'No tags.', + ignoreQuotaTitle: 'Bypass User Quota', + ignoreQuotaDescription: + "This request will not count against the user's quota limits. Use with caution.", }); +// Readarr recreates this profile on every start; it rejects every book +const NONE_METADATA_PROFILE = 'None'; + export type RequestOverrides = { server?: number; profile?: number; + metadataProfile?: number; folder?: string; tags?: number[]; language?: number; user?: User; + ignoreQuota?: boolean; }; interface AdvancedRequesterProps { - type: 'movie' | 'tv'; + type: 'movie' | 'tv' | 'book'; + tmdbId?: number; is4k: boolean; isAnime?: boolean; defaultOverrides?: RequestOverrides; requestUser?: User; + requestId?: number; + quota?: { movie: { limit?: number }; tv: { limit?: number } }; + hideTitle?: boolean; onChange: (overrides: RequestOverrides) => void; } const AdvancedRequester = ({ type, + tmdbId, is4k = false, isAnime = false, defaultOverrides, requestUser, + requestId, + quota, + hideTitle = false, onChange, }: AdvancedRequesterProps) => { const intl = useIntl(); + const { addToast } = useToasts(); const { user: currentUser, hasPermission: currentHasPermission } = useUser(); const { data, error } = useSWR( - `/api/v1/service/${type === 'movie' ? 'radarr' : 'sonarr'}`, + `/api/v1/service/${ + type === 'movie' ? 'radarr' : type === 'book' ? 'readarr' : 'sonarr' + }`, { refreshInterval: 0, refreshWhenHidden: false, @@ -85,6 +116,8 @@ const AdvancedRequester = ({ const [selectedProfile, setSelectedProfile] = useState( defaultOverrides?.profile ?? -1 ); + const [selectedMetadataProfile, setSelectedMetadataProfile] = + useState(defaultOverrides?.metadataProfile ?? -1); const [selectedFolder, setSelectedFolder] = useState( defaultOverrides?.folder ?? '' ); @@ -97,11 +130,18 @@ const AdvancedRequester = ({ defaultOverrides?.tags ?? [] ); + const [ignoreQuota, setIgnoreQuota] = useState( + defaultOverrides?.ignoreQuota ?? false + ); + const isIgnoreQuotaVisible = + currentHasPermission([Permission.MANAGE_REQUESTS]) && + ((type === 'movie' ? quota?.movie.limit : quota?.tv.limit) ?? 0) > 0; + const { data: serverData, isValidating } = useSWR( selectedServer !== null ? `/api/v1/service/${ - type === 'movie' ? 'radarr' : 'sonarr' + type === 'movie' ? 'radarr' : type === 'book' ? 'readarr' : 'sonarr' }/${selectedServer}` : null, { @@ -110,10 +150,15 @@ const AdvancedRequester = ({ revalidateOnFocus: false, } ); + const metadataProfiles = (serverData?.metadataProfiles ?? []).filter( + (metadataProfile) => metadataProfile.name !== NONE_METADATA_PROFILE + ); const [selectedUser, setSelectedUser] = useState( requestUser ?? null ); + const selectedUserId = selectedUser?.id; + const previousSelectedUserIdRef = useRef(selectedUserId); const { data: userData } = useSWR( currentHasPermission([Permission.MANAGE_REQUESTS, Permission.MANAGE_USERS]) @@ -129,13 +174,17 @@ const AdvancedRequester = ({ Permission.REQUEST_4K, type === 'movie' ? Permission.REQUEST_4K_MOVIE - : Permission.REQUEST_4K_TV, + : type === 'tv' + ? Permission.REQUEST_4K_TV + : Permission.REQUEST_AUDIO_BOOK, ] : [ Permission.REQUEST, type === 'movie' ? Permission.REQUEST_MOVIE - : Permission.REQUEST_TV, + : type === 'tv' + ? Permission.REQUEST_TV + : Permission.REQUEST_BOOK, ], user.permissions, { type: 'or' } @@ -146,9 +195,14 @@ const AdvancedRequester = ({ useEffect(() => { if (filteredUserData && !requestUser) { - setSelectedUser( - filteredUserData.find((u) => u.id === currentUser?.id) ?? null - ); + const nextSelectedUser = + filteredUserData.find((u) => u.id === currentUser?.id) ?? null; + + if (nextSelectedUser?.id !== selectedUserId) { + setIgnoreQuota(false); + } + + setSelectedUser(nextSelectedUser); } }, [filteredUserData]); @@ -193,6 +247,10 @@ const AdvancedRequester = ({ ? serverData.server.activeAnimeLanguageProfileId : serverData.server.activeLanguageProfileId) ); + const defaultMetadataProfile = metadataProfiles.find( + (metadataProfile) => + metadataProfile.id === serverData.server.activeMetadataProfileId + ); const defaultTags = isAnime ? serverData.server.activeAnimeTags : serverData.server.activeTags; @@ -226,6 +284,14 @@ const AdvancedRequester = ({ setSelectedLanguage(defaultLanguage.id); } + if ( + defaultMetadataProfile && + defaultMetadataProfile.id !== selectedMetadataProfile && + (!applyOverrides || defaultOverrides.metadataProfile === null) + ) { + setSelectedMetadataProfile(defaultMetadataProfile.id); + } + if ( defaultTags && !isEqual(defaultTags, selectedTags) && @@ -253,35 +319,123 @@ const AdvancedRequester = ({ setSelectedLanguage(defaultOverrides.language); } + if (defaultOverrides && defaultOverrides.metadataProfile != null) { + setSelectedMetadataProfile(defaultOverrides.metadataProfile); + } + if (defaultOverrides && defaultOverrides.tags != null) { setSelectedTags(defaultOverrides.tags); } + + if (defaultOverrides && defaultOverrides.ignoreQuota != null) { + setIgnoreQuota(defaultOverrides.ignoreQuota); + } }, [ defaultOverrides?.server, defaultOverrides?.folder, defaultOverrides?.profile, defaultOverrides?.language, + defaultOverrides?.metadataProfile, defaultOverrides?.tags, + defaultOverrides?.ignoreQuota, ]); + useEffect(() => { + const selectedUserChanged = + previousSelectedUserIdRef.current !== selectedUserId; + previousSelectedUserIdRef.current = selectedUserId; + + if (!isIgnoreQuotaVisible || selectedUserChanged) { + setIgnoreQuota(false); + } + }, [isIgnoreQuotaVisible, selectedUserId]); + useEffect(() => { if (selectedServer !== null || selectedUser) { onChange({ folder: selectedFolder !== '' ? selectedFolder : undefined, profile: selectedProfile !== -1 ? selectedProfile : undefined, + metadataProfile: + selectedMetadataProfile !== -1 ? selectedMetadataProfile : undefined, server: selectedServer ?? undefined, user: selectedUser ?? undefined, language: selectedLanguage !== -1 ? selectedLanguage : undefined, tags: selectedTags, + ignoreQuota: isIgnoreQuotaVisible && ignoreQuota ? true : undefined, }); } }, [ selectedFolder, selectedServer, selectedProfile, + selectedMetadataProfile, selectedUser, selectedLanguage, selectedTags, + ignoreQuota, + isIgnoreQuotaVisible, + ]); + + useEffect(() => { + let cancelled = false; + (async () => { + if (tmdbId && serverData?.server.id === selectedServer) { + try { + const { data: override } = await axios.post( + '/api/v1/overrideRule/advancedRequest', + { + mediaType: type, + is4k, + requestUser: + selectedUser?.id ?? requestUser?.id ?? currentUser?.id, + tmdbId, + tags: selectedTags.length > 0 ? selectedTags : undefined, + serviceId: selectedServer ?? undefined, + requestId: requestId ?? undefined, + } + ); + if (cancelled) { + return; + } + if (!defaultOverrides?.folder && override.rootFolder) { + setSelectedFolder(override.rootFolder); + } + if (!defaultOverrides?.profile && override.profileId) { + setSelectedProfile(override.profileId); + } + if ( + !defaultOverrides?.tags && + override.tags && + !isEqual(override.tags, selectedTags) + ) { + setSelectedTags(override.tags); + } + } catch { + if (cancelled) { + return; + } + addToast(intl.formatMessage(globalMessages.error), { + appearance: 'error', + autoDismiss: true, + }); + } + } + })(); + return () => { + cancelled = true; + }; + }, [ + tmdbId, + type, + is4k, + serverData?.server.id, + selectedServer, + selectedUserId, + requestUser?.id, + currentUser?.id, + defaultOverrides?.folder, + defaultOverrides?.profile, + defaultOverrides?.tags, ]); if (!data && !error) { @@ -300,6 +454,7 @@ const AdvancedRequester = ({ (serverData.profiles.length < 2 && serverData.rootFolders.length < 2 && (serverData.languageProfiles ?? []).length < 2 && + metadataProfiles.length < 2 && !serverData.tags?.length)))) && (!selectedUser || (filteredUserData ?? []).length < 2) ) { @@ -308,9 +463,11 @@ const AdvancedRequester = ({ return ( <> -
- {intl.formatMessage(messages.advancedoptions)} -
+ {!hideTitle && ( +
+ {intl.formatMessage(messages.advancedoptions)} +
+ )}
{!!data && selectedServer !== null && (
@@ -395,6 +552,48 @@ const AdvancedRequester = ({
)} + {type === 'book' && + (isValidating || !serverData || metadataProfiles.length > 1) && ( +
+ + +
+ )} {(isValidating || !serverData || serverData.rootFolders.length > 1) && ( @@ -547,6 +746,22 @@ const AdvancedRequester = ({ />
)} + {isIgnoreQuotaVisible && ( +
+ +
+

+ {intl.formatMessage(messages.ignoreQuotaDescription)} +

+ setIgnoreQuota(!ignoreQuota)} + /> +
+
+ )} {currentHasPermission([ Permission.MANAGE_REQUESTS, Permission.MANAGE_USERS, @@ -556,17 +771,18 @@ const AdvancedRequester = ({ setSelectedUser(value)} + onChange={(value) => { + setIgnoreQuota(false); + setSelectedUser(value); + }} className="space-y-1" > {({ open }) => ( <> - - {intl.formatMessage(messages.requestas)} - +
- + - + - {filteredUserData?.map((user) => ( - + {({ selected, active }) => (
)} - + ))} - +
diff --git a/src/components/RequestModal/BookRequestModal.tsx b/src/components/RequestModal/BookRequestModal.tsx new file mode 100644 index 0000000000..bd9772296c --- /dev/null +++ b/src/components/RequestModal/BookRequestModal.tsx @@ -0,0 +1,589 @@ +import Alert from '@app/components/Common/Alert'; +import Badge from '@app/components/Common/Badge'; +import Modal from '@app/components/Common/Modal'; +import SlideCheckbox from '@app/components/Common/SlideCheckbox'; +import type { RequestOverrides } from '@app/components/RequestModal/AdvancedRequester'; +import AdvancedRequester from '@app/components/RequestModal/AdvancedRequester'; +import QuotaDisplay from '@app/components/RequestModal/QuotaDisplay'; +import useSettings from '@app/hooks/useSettings'; +import useToasts from '@app/hooks/useToasts'; +import { useUser } from '@app/hooks/useUser'; +import globalMessages from '@app/i18n/globalMessages'; +import defineMessages from '@app/utils/defineMessages'; +import { requestBookFormats } from '@app/utils/requestBookFormats'; +import { + MediaRequestStatus, + MediaStatus, + MediaType, +} from '@server/constants/media'; +import type { MediaRequest } from '@server/entity/MediaRequest'; +import type { NonFunctionProperties } from '@server/interfaces/api/common'; +import type { QuotaResponse } from '@server/interfaces/api/userInterfaces'; +import { Permission } from '@server/lib/permissions'; +import type { BookDetails } from '@server/models/Book'; +import axios from 'axios'; +import { useEffect, useState } from 'react'; +import { useIntl } from 'react-intl'; +import useSWR, { mutate } from 'swr'; + +const messages = defineMessages('components.RequestModal', { + requestadmin: 'This request will be approved automatically.', + requestSuccess: '{title} requested successfully!', + requestCancel: 'Request for {title} canceled.', + requestbooktitle: 'Request Book', + requestebook: 'Request Ebook', + requestbothformats: 'Request Both Formats', + selectformat: 'Select Format(s)', + format: 'Format', + ebook: 'Ebook', + audiobook: 'Audiobook', + alreadyrequested: 'Already Requested', + edit: 'Edit Request', + approve: 'Approve Request', + cancel: 'Cancel Request', + pendingrequest: 'Pending Book Request', + pendingaudiobookrequest: 'Pending Audiobook Request', + requestfrom: "{username}'s request is pending approval.", + errorediting: 'Something went wrong while editing the request.', + requestedited: 'Request for {title} edited successfully!', + requestApproved: 'Request for {title} approved!', + requesterror: 'Something went wrong while submitting the request.', + pendingapproval: 'Your request is pending approval.', +}); + +const FORMATS = [false, true]; + +interface RequestModalProps extends React.HTMLAttributes { + hcId?: number; + isAudio?: boolean; + editRequest?: NonFunctionProperties; + onCancel?: () => void; + onComplete?: (newStatus: MediaStatus, newStatus4k?: MediaStatus) => void; + onUpdating?: (isUpdating: boolean) => void; +} + +const BookRequestModal = ({ + onCancel, + onComplete, + hcId, + onUpdating, + editRequest, + isAudio = false, +}: RequestModalProps) => { + const [isUpdating, setIsUpdating] = useState(false); + const [requestOverrides, setRequestOverrides] = + useState(null); + const [selectedFormats, setSelectedFormats] = useState( + null + ); + const [formatOverrides, setFormatOverrides] = useState< + Record + >({}); + const { addToast } = useToasts(); + const { data, error } = useSWR(`/api/v1/book/${hcId}`, { + revalidateOnMount: true, + }); + + const intl = useIntl(); + const settings = useSettings(); + const { user, hasPermission } = useUser(); + + useEffect(() => { + if (onUpdating) { + onUpdating(isUpdating); + } + }, [isUpdating, onUpdating]); + + const canRequest = (is4k: boolean) => + is4k + ? settings.currentSettings.bookAudioEnabled && + hasPermission([Permission.REQUEST_4K, Permission.REQUEST_AUDIO_BOOK], { + type: 'or', + }) + : hasPermission([Permission.REQUEST, Permission.REQUEST_BOOK], { + type: 'or', + }); + + const autoApproves = (is4k: boolean) => + hasPermission( + [ + Permission.MANAGE_REQUESTS, + is4k ? Permission.AUTO_APPROVE_4K : Permission.AUTO_APPROVE, + is4k + ? Permission.AUTO_APPROVE_AUDIO_BOOK + : Permission.AUTO_APPROVE_BOOK, + ], + { type: 'or' } + ); + + const formatStatus = (is4k: boolean) => + data?.mediaInfo?.[is4k ? 'status4k' : 'status'] ?? MediaStatus.UNKNOWN; + + const formatRequest = (is4k: boolean) => + data?.mediaInfo?.requests?.find( + (request) => + request.is4k === is4k && + request.status !== MediaRequestStatus.DECLINED && + request.status !== MediaRequestStatus.FAILED && + request.status !== MediaRequestStatus.COMPLETED + ); + + const isRequestable = (is4k: boolean) => + canRequest(is4k) && + (formatStatus(is4k) === MediaStatus.UNKNOWN || + (formatStatus(is4k) === MediaStatus.DELETED && !formatRequest(is4k))); + + const visibleFormats = FORMATS.filter(canRequest); + const requestableFormats = visibleFormats.filter(isRequestable); + const defaultFormats = settings.currentSettings.syncBookFormatRequests + ? requestableFormats + : requestableFormats.includes(isAudio) + ? [isAudio] + : requestableFormats.slice(0, 1); + const formats = (selectedFormats ?? defaultFormats) + .slice() + .sort((a, b) => Number(a) - Number(b)); + const isAllSelected = + requestableFormats.length > 0 && + requestableFormats.every((is4k) => formats.includes(is4k)); + const overridesFor = (is4k: boolean) => formatOverrides[String(is4k)]; + const overrideUser = + editRequest || formats.length === 0 + ? requestOverrides?.user + : formats.map((is4k) => overridesFor(is4k)?.user).find((u) => u); + + const { data: quota } = useSWR( + user && (!overrideUser?.id || hasPermission(Permission.MANAGE_USERS)) + ? `/api/v1/user/${overrideUser?.id ?? user.id}/quota` + : null + ); + + const toggleFormat = (is4k: boolean) => { + if (!isRequestable(is4k)) { + return; + } + setSelectedFormats( + formats.includes(is4k) + ? formats.filter((format) => format !== is4k) + : [...formats, is4k] + ); + }; + + const toggleAllFormats = () => + setSelectedFormats(isAllSelected ? [] : requestableFormats); + + const sendRequest = async () => { + if (!data || formats.length === 0) { + return; + } + setIsUpdating(true); + + try { + const outcomes = await requestBookFormats(data.id, formats, overridesFor); + const requested = outcomes.filter((outcome) => outcome.request); + + if (!requested.length) { + throw new Error('No book format request succeeded'); + } + + // The request response carries the media row as it was created, which is + // always pending; the approved-to-processing flip happens after it returns + const statusAfter = (is4k: boolean) => + requested.some((outcome) => outcome.is4k === is4k) + ? autoApproves(is4k) + ? MediaStatus.PROCESSING + : MediaStatus.PENDING + : formatStatus(is4k); + + if (onComplete) { + onComplete(statusAfter(false), statusAfter(true)); + } + addToast( + + {intl.formatMessage(messages.requestSuccess, { + title: data.title, + strong: (msg: React.ReactNode) => {msg}, + })} + , + { appearance: 'success', autoDismiss: true } + ); + } catch { + addToast(intl.formatMessage(messages.requesterror), { + appearance: 'error', + autoDismiss: true, + }); + } finally { + setIsUpdating(false); + } + }; + + const cancelRequest = async () => { + setIsUpdating(true); + + try { + const response = await axios.delete( + `/api/v1/request/${editRequest?.id}` + ); + mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0'); + mutate('/api/v1/request/count'); + + if (response.status === 204) { + if (onComplete) { + onComplete(MediaStatus.UNKNOWN); + } + addToast( + + {intl.formatMessage(messages.requestCancel, { + title: data?.title, + strong: (msg: React.ReactNode) => {msg}, + })} + , + { appearance: 'success', autoDismiss: true } + ); + } + } catch { + setIsUpdating(false); + } + }; + + const updateRequest = async (alsoApproveRequest = false) => { + setIsUpdating(true); + + try { + await axios.put(`/api/v1/request/${editRequest?.id}`, { + mediaType: 'book', + serverId: requestOverrides?.server, + profileId: requestOverrides?.profile, + metadataProfileId: requestOverrides?.metadataProfile, + rootFolder: requestOverrides?.folder, + userId: requestOverrides?.user?.id, + tags: requestOverrides?.tags, + }); + + if (alsoApproveRequest) { + await axios.post(`/api/v1/request/${editRequest?.id}/approve`); + } + mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0'); + mutate('/api/v1/request/count'); + + addToast( + + {intl.formatMessage( + alsoApproveRequest + ? messages.requestApproved + : messages.requestedited, + { + title: data?.title, + strong: (msg: React.ReactNode) => {msg}, + } + )} + , + { + appearance: 'success', + autoDismiss: true, + } + ); + + if (onComplete) { + onComplete(MediaStatus.PENDING); + } + } catch { + addToast({intl.formatMessage(messages.errorediting)}, { + appearance: 'error', + autoDismiss: true, + }); + } finally { + setIsUpdating(false); + } + }; + + if (editRequest) { + const isOwner = editRequest.requestedBy.id === user?.id; + + return ( + + hasPermission(Permission.MANAGE_REQUESTS) + ? updateRequest(true) + : hasPermission(Permission.REQUEST_ADVANCED) + ? updateRequest() + : cancelRequest() + } + okDisabled={isUpdating} + okText={ + hasPermission(Permission.MANAGE_REQUESTS) + ? intl.formatMessage(messages.approve) + : hasPermission(Permission.REQUEST_ADVANCED) + ? intl.formatMessage(messages.edit) + : intl.formatMessage(messages.cancel) + } + okButtonType={ + hasPermission(Permission.MANAGE_REQUESTS) + ? 'success' + : hasPermission(Permission.REQUEST_ADVANCED) + ? 'primary' + : 'danger' + } + onSecondary={ + isOwner && + hasPermission( + [Permission.REQUEST_ADVANCED, Permission.MANAGE_REQUESTS], + { type: 'or' } + ) + ? () => cancelRequest() + : undefined + } + secondaryDisabled={isUpdating} + secondaryText={ + isOwner && + hasPermission( + [Permission.REQUEST_ADVANCED, Permission.MANAGE_REQUESTS], + { type: 'or' } + ) + ? intl.formatMessage(messages.cancel) + : undefined + } + secondaryButtonType="danger" + cancelText={intl.formatMessage(globalMessages.close)} + backdrop={`${data?.backdropPath}`} + > + {isOwner + ? intl.formatMessage(messages.pendingapproval) + : intl.formatMessage(messages.requestfrom, { + username: editRequest.requestedBy.displayName, + })} + {(hasPermission(Permission.REQUEST_ADVANCED) || + hasPermission(Permission.MANAGE_REQUESTS)) && ( + { + setRequestOverrides(overrides); + }} + /> + )} + + ); + } + + const hasAutoApprove = + formats.length > 0 && formats.every((is4k) => autoApproves(is4k)); + + const formatBadge = (is4k: boolean) => { + const status = formatStatus(is4k); + const request = formatRequest(is4k); + + if (status === MediaStatus.AVAILABLE) { + return ( + + {intl.formatMessage(globalMessages.available)} + + ); + } + if (status === MediaStatus.PARTIALLY_AVAILABLE) { + return ( + + {intl.formatMessage(globalMessages.partiallyavailable)} + + ); + } + if (status === MediaStatus.BLOCKLISTED) { + return ( + + {intl.formatMessage(globalMessages.blocklisted)} + + ); + } + if ( + status === MediaStatus.PROCESSING || + request?.status === MediaRequestStatus.APPROVED + ) { + return ( + + {intl.formatMessage(globalMessages.requested)} + + ); + } + if ( + status === MediaStatus.PENDING || + request?.status === MediaRequestStatus.PENDING + ) { + return ( + + {intl.formatMessage(globalMessages.pending)} + + ); + } + if (status === MediaStatus.DELETED) { + return ( + + {intl.formatMessage(globalMessages.deleted)} + + ); + } + return {intl.formatMessage(globalMessages.notrequested)}; + }; + + return ( + 1 + ? intl.formatMessage(messages.requestbothformats) + : intl.formatMessage( + formats[0] + ? globalMessages.requestAudio + : messages.requestebook + ) + } + okButtonType={'primary'} + backdrop={`${data?.backdropPath}`} + > + {hasAutoApprove && !quota?.book.restricted && ( +
+ +
+ )} + {(quota?.book.limit ?? 0) > 0 && ( + + )} +
+
+
+
+ + + + + + + + + + {visibleFormats.map((is4k) => ( + + + + + + ))} + +
+
+ +
+
+ {intl.formatMessage(messages.format)} + + {intl.formatMessage(globalMessages.status)} +
+
+ toggleFormat(is4k)} + /> +
+
+ {intl.formatMessage( + is4k ? messages.audiobook : messages.ebook + )} + + {formatBadge(is4k)} +
+
+
+
+
+ {(hasPermission(Permission.REQUEST_ADVANCED) || + hasPermission(Permission.MANAGE_REQUESTS)) && + formats.length > 0 && ( + <> + {formats.length > 1 && ( +
+ {intl.formatMessage(globalMessages.advanced)} +
+ )} + {formats.map((is4k) => ( +
+ {formats.length > 1 && ( +

+ {intl.formatMessage( + is4k ? messages.audiobook : messages.ebook + )} +

+ )} + 1} + onChange={(overrides) => { + setFormatOverrides((current) => ({ + ...current, + [String(is4k)]: overrides, + })); + }} + /> +
+ ))} + + )} +
+ ); +}; + +export default BookRequestModal; diff --git a/src/components/RequestModal/CollectionRequestModal.tsx b/src/components/RequestModal/CollectionRequestModal.tsx index f704b09c02..a0374e4a6e 100644 --- a/src/components/RequestModal/CollectionRequestModal.tsx +++ b/src/components/RequestModal/CollectionRequestModal.tsx @@ -517,8 +517,10 @@ const CollectionRequestModal = ({
- {(hasPermission(Permission.REQUEST_ADVANCED) || - hasPermission(Permission.MANAGE_REQUESTS)) && ( + {hasPermission( + [Permission.REQUEST_ADVANCED, Permission.MANAGE_REQUESTS], + { type: 'or' } + ) && ( )} - {(hasPermission(Permission.REQUEST_ADVANCED) || - hasPermission(Permission.MANAGE_REQUESTS)) && ( + {hasPermission( + [Permission.REQUEST_ADVANCED, Permission.MANAGE_REQUESTS], + { type: 'or' } + ) && ( { setRequestOverrides(overrides); }} diff --git a/src/components/RequestModal/QuotaDisplay/index.tsx b/src/components/RequestModal/QuotaDisplay/index.tsx index 68013b76bc..4329e5b56b 100644 --- a/src/components/RequestModal/QuotaDisplay/index.tsx +++ b/src/components/RequestModal/QuotaDisplay/index.tsx @@ -11,6 +11,7 @@ const messages = defineMessages('components.RequestModal.QuotaDisplay', { '{remaining, plural, =0 {No} other {#}} {type} {remaining, plural, one {request} other {requests}} remaining', movielimit: '{limit, plural, one {movie} other {movies}}', seasonlimit: '{limit, plural, one {season} other {seasons}}', + booklimit: '{limit, plural, one {book} other {books}}', allowedRequests: 'You are allowed to request {limit} {type}{days, plural, =0 {} one { every day} other { every {days} days}}.', allowedRequestsUser: @@ -21,6 +22,7 @@ const messages = defineMessages('components.RequestModal.QuotaDisplay', { "You can view a summary of this user's request limits on their profile page.", movie: 'movie', season: 'season', + book: 'book', notenoughseasonrequests: 'Not enough season requests remaining', requiredquota: 'You need to have at least {seasons} {seasons, plural, one {season request} other {season requests}} remaining in order to submit a request for this series.', @@ -30,7 +32,7 @@ const messages = defineMessages('components.RequestModal.QuotaDisplay', { interface QuotaDisplayProps { quota?: QuotaStatus; - mediaType: 'movie' | 'tv'; + mediaType: 'movie' | 'tv' | 'book'; userOverride?: number | null; remaining?: number; overLimit?: number; @@ -78,7 +80,11 @@ const QuotaDisplay = ({ : intl.formatMessage(messages.requestsremaining, { remaining: remaining ?? quota?.remaining ?? 0, type: intl.formatMessage( - mediaType === 'movie' ? messages.movie : messages.season + mediaType === 'movie' + ? messages.movie + : mediaType === 'book' + ? messages.book + : messages.season ), strong: (msg: React.ReactNode) => {msg}, })} @@ -118,7 +124,9 @@ const QuotaDisplay = ({ type: intl.formatMessage( mediaType === 'movie' ? messages.movielimit - : messages.seasonlimit, + : mediaType === 'book' + ? messages.booklimit + : messages.seasonlimit, { limit: quota?.limit } ), strong: (msg: React.ReactNode) => {msg}, diff --git a/src/components/RequestModal/SeriesRequestModal.tsx b/src/components/RequestModal/SeriesRequestModal.tsx new file mode 100644 index 0000000000..4dc1d2442f --- /dev/null +++ b/src/components/RequestModal/SeriesRequestModal.tsx @@ -0,0 +1,683 @@ +import Alert from '@app/components/Common/Alert'; +import Badge from '@app/components/Common/Badge'; +import CachedImage from '@app/components/Common/CachedImage'; +import Modal from '@app/components/Common/Modal'; +import SlideCheckbox from '@app/components/Common/SlideCheckbox'; +import type { RequestOverrides } from '@app/components/RequestModal/AdvancedRequester'; +import AdvancedRequester from '@app/components/RequestModal/AdvancedRequester'; +import QuotaDisplay from '@app/components/RequestModal/QuotaDisplay'; +import useSettings from '@app/hooks/useSettings'; +import useToasts from '@app/hooks/useToasts'; +import { useUser } from '@app/hooks/useUser'; +import globalMessages from '@app/i18n/globalMessages'; +import defineMessages from '@app/utils/defineMessages'; +import { requestBookFormats } from '@app/utils/requestBookFormats'; +import { + MediaRequestStatus, + MediaStatus, + MediaType, +} from '@server/constants/media'; +import type { QuotaResponse } from '@server/interfaces/api/userInterfaces'; +import { Permission } from '@server/lib/permissions'; +import type { Series } from '@server/models/Series'; +import { useEffect, useState } from 'react'; +import { useIntl } from 'react-intl'; +import useSWR, { mutate } from 'swr'; + +const messages = defineMessages('components.RequestModal', { + requestadmin: 'This request will be approved automatically.', + requestSuccess: '{title} requested successfully!', + requestseriestitle: 'Request Series', + requesterror: 'Something went wrong while submitting the request.', + selectbooks: 'Select Book(s)', + ebook: 'Ebook', + audiobook: 'Audiobook', + fullseries: 'Full Series', + editionsextras: 'Editions & Extras', + requestbooks: 'Request {count} {count, plural, one {Book} other {Books}}', +}); + +const FORMATS = [false, true]; + +interface RequestModalProps extends React.HTMLAttributes { + seriesId?: number; + onCancel?: () => void; + onComplete?: (newStatus: MediaStatus) => void; + onUpdating?: (isUpdating: boolean) => void; +} + +const SeriesRequestModal = ({ + onCancel, + onComplete, + seriesId, + onUpdating, +}: RequestModalProps) => { + const [isUpdating, setIsUpdating] = useState(false); + const [formatOverrides, setFormatOverrides] = useState< + Record + >({}); + const [excludedState, setExcludedBooks] = useState(null); + const [bookFormats, setBookFormats] = useState>({}); + const [headerFormats, setHeaderFormats] = useState(null); + const { addToast } = useToasts(); + const { data, error } = useSWR(`/api/v1/series/${seriesId}`, { + revalidateOnMount: true, + }); + const intl = useIntl(); + const settings = useSettings(); + const { user, hasPermission } = useUser(); + + useEffect(() => { + if (onUpdating) { + onUpdating(isUpdating); + } + }, [isUpdating, onUpdating]); + + const canRequestFormat = (is4k: boolean) => + is4k + ? settings.currentSettings.bookAudioEnabled && + hasPermission([Permission.REQUEST_4K, Permission.REQUEST_AUDIO_BOOK], { + type: 'or', + }) + : hasPermission([Permission.REQUEST, Permission.REQUEST_BOOK], { + type: 'or', + }); + + const visibleFormats = FORMATS.filter(canRequestFormat); + const books = data?.books ?? []; + const ownBooks = books.filter((book) => !book.extra); + const extraBooks = books.filter((book) => book.extra); + const extraIds = extraBooks.map((book) => book.id); + + // Editions and extras start out of the request; the series' own books start in + const excludedBooks = excludedState ?? extraIds; + + const bookStatus = (bookId: number, is4k: boolean) => { + const book = books.find((b) => b.id === bookId); + return ( + book?.mediaInfo?.[is4k ? 'status4k' : 'status'] ?? MediaStatus.UNKNOWN + ); + }; + + const bookRequest = (bookId: number, is4k: boolean) => { + const book = books.find((b) => b.id === bookId); + return (book?.mediaInfo?.requests ?? []).find( + (request) => + request.is4k === is4k && + request.status !== MediaRequestStatus.DECLINED && + request.status !== MediaRequestStatus.FAILED && + request.status !== MediaRequestStatus.COMPLETED + ); + }; + + // A format of a book can be requested when nothing is tracking it yet + const isRequestable = (bookId: number, is4k: boolean) => { + const book = books.find((b) => b.id === bookId); + if ( + !canRequestFormat(is4k) || + book?.mediaInfo?.status === MediaStatus.BLOCKLISTED || + bookRequest(bookId, is4k) + ) { + return false; + } + const status = bookStatus(bookId, is4k); + return status === MediaStatus.UNKNOWN || status === MediaStatus.DELETED; + }; + + // Both formats are pre-selected only when the global default says so + const defaultFormats = settings.currentSettings.syncBookFormatRequests + ? visibleFormats + : visibleFormats.slice(0, 1); + + // The header choice, which a book follows until that book is changed + const activeFormats = (headerFormats ?? defaultFormats).filter((is4k) => + visibleFormats.includes(is4k) + ); + + const requestableFormatsFor = (bookId: number) => + visibleFormats.filter((is4k) => isRequestable(bookId, is4k)); + + const formatsForBook = (bookId: number) => + (bookFormats[bookId] ?? activeFormats).filter((is4k) => + isRequestable(bookId, is4k) + ); + + const isSelected = (bookId: number, is4k: boolean) => + formatsForBook(bookId).includes(is4k); + + const bookSelectable = (bookId: number) => + requestableFormatsFor(bookId).length > 0; + + const selectableBooks = books + .filter((book) => bookSelectable(book.id)) + .map((book) => book.id); + + // Full Series covers the series' own books; editions are opted into singly + const primarySelectable = selectableBooks.filter( + (bookId) => !extraIds.includes(bookId) + ); + + const bookExcluded = (bookId: number) => excludedBooks.includes(bookId); + + // Switched on and holding a format is what puts a book in the request, so + // clearing the formats above reads through to every book's own switch + const bookRequested = (bookId: number) => + !bookExcluded(bookId) && formatsForBook(bookId).length > 0; + + // A book with nothing left to request shows on, and shows what it has + const bookToggleOn = (bookId: number) => + bookRequested(bookId) || !bookSelectable(bookId); + + // Nothing chosen above for this book to take leaves its switch nothing to do + const bookActionable = (bookId: number) => + bookSelectable(bookId) && formatsForBook(bookId).length > 0; + + const anyBookIncluded = selectableBooks.some( + (bookId) => !bookExcluded(bookId) + ); + + const selectedBookIds = selectableBooks.filter(bookRequested); + + const selectedFormats = visibleFormats.filter((is4k) => + selectedBookIds.some((bookId) => isSelected(bookId, is4k)) + ); + + const quotaUser = + selectedFormats + .map((is4k) => formatOverrides[String(is4k)]?.user) + .find((selectedUser) => selectedUser) ?? undefined; + + const { data: quota } = useSWR( + user && (!quotaUser?.id || hasPermission(Permission.MANAGE_USERS)) + ? `/api/v1/user/${quotaUser?.id ?? user.id}/quota` + : null + ); + + // A book costs one unit however many of its formats are taken + const currentlyRemaining = + (quota?.book.remaining ?? 0) - selectedBookIds.length; + + const wouldExceedQuota = (bookId: number) => + !!quota?.book.limit && + currentlyRemaining <= 0 && + !selectedBookIds.includes(bookId); + + // Changing a book's formats detaches that book from the header choice + const toggle = (bookId: number, is4k: boolean) => { + if (!isRequestable(bookId, is4k) || bookExcluded(bookId)) { + return; + } + const current = formatsForBook(bookId); + const adding = !current.includes(is4k); + if (adding && wouldExceedQuota(bookId)) { + return; + } + setBookFormats({ + ...bookFormats, + [bookId]: adding + ? [...current, is4k] + : current.filter((format) => format !== is4k), + }); + }; + + const formatColumn = (is4k: boolean) => + selectableBooks.filter((bookId) => isRequestable(bookId, is4k)); + + const isWholeColumn = (is4k: boolean) => activeFormats.includes(is4k); + + // Every book that was not changed individually follows this + const toggleColumn = (is4k: boolean) => { + if (!formatColumn(is4k).length || !anyBookIncluded) { + return; + } + setHeaderFormats( + isWholeColumn(is4k) + ? activeFormats.filter((format) => format !== is4k) + : [...activeFormats, is4k] + ); + }; + + // Switching a book off leaves its formats alone; it is simply not requested + const toggleBook = (bookId: number) => { + if (!bookActionable(bookId)) { + return; + } + if (bookRequested(bookId)) { + setExcludedBooks([...new Set([...excludedBooks, bookId])]); + return; + } + if (wouldExceedQuota(bookId)) { + return; + } + setExcludedBooks(excludedBooks.filter((id) => id !== bookId)); + if (bookFormats[bookId] && !formatsForBook(bookId).length) { + // An emptied book rejoins the formats chosen above + const restored = { ...bookFormats }; + delete restored[bookId]; + setBookFormats(restored); + } + }; + + const allBooksIncluded = + primarySelectable.length > 0 && primarySelectable.every(bookRequested); + + // Only decides which books are in; the chosen formats are left untouched + const toggleAllBooks = () => { + if (allBooksIncluded) { + setExcludedBooks([...new Set([...excludedBooks, ...primarySelectable])]); + return; + } + if ( + quota?.book.limit && + primarySelectable.length > (quota.book.remaining ?? 0) + ) { + return; + } + setExcludedBooks( + excludedBooks.filter((bookId) => !primarySelectable.includes(bookId)) + ); + if (!activeFormats.length) { + setHeaderFormats(visibleFormats); + } + const restored = { ...bookFormats }; + Object.keys(restored).forEach((key) => { + if (!restored[Number(key)].length) { + delete restored[Number(key)]; + } + }); + setBookFormats(restored); + }; + + // Every book available is available; some is partly so + const seriesFormatStatus = (is4k: boolean) => { + const key = is4k ? 'status4k' : 'status'; + const available = ownBooks.filter( + (book) => book.mediaInfo?.[key] === MediaStatus.AVAILABLE + ).length; + + if (!ownBooks.length || !available) { + return MediaStatus.UNKNOWN; + } + return available === ownBooks.length + ? MediaStatus.AVAILABLE + : MediaStatus.PARTIALLY_AVAILABLE; + }; + + const renderStatusBadge = ( + status: MediaStatus, + requestStatus?: MediaRequestStatus + ) => { + if (status === MediaStatus.AVAILABLE) { + return ( + + {intl.formatMessage(globalMessages.available)} + + ); + } + if (status === MediaStatus.PARTIALLY_AVAILABLE) { + return ( + + {intl.formatMessage(globalMessages.partiallyavailable)} + + ); + } + if (status === MediaStatus.BLOCKLISTED) { + return ( + + {intl.formatMessage(globalMessages.blocklisted)} + + ); + } + if ( + status === MediaStatus.PROCESSING || + requestStatus === MediaRequestStatus.APPROVED + ) { + return ( + + {intl.formatMessage(globalMessages.requested)} + + ); + } + if (status === MediaStatus.PENDING || requestStatus !== undefined) { + return ( + + {intl.formatMessage(globalMessages.pending)} + + ); + } + return {intl.formatMessage(globalMessages.notrequested)}; + }; + + const statusBadge = (bookId: number, is4k: boolean) => + renderStatusBadge( + bookStatus(bookId, is4k), + bookRequest(bookId, is4k)?.status + ); + + const allBooksToggle = () => ( +
+ +
+ ); + + const columnToggle = (is4k: boolean) => ( +
+ toggleColumn(is4k)} + /> +
+ ); + + const bookToggle = (bookId: number) => ( +
+ toggleBook(bookId)} + /> +
+ ); + + const formatToggle = (bookId: number, is4k: boolean) => { + const selectable = isRequestable(bookId, is4k); + const locked = !bookToggleOn(bookId); + + return ( +
+
+ toggle(bookId, is4k)} + /> +
+ + {intl.formatMessage(is4k ? messages.audiobook : messages.ebook)} + + {statusBadge(bookId, is4k)} +
+ ); + }; + + const sendRequest = async () => { + if (!selectedBookIds.length) { + return; + } + setIsUpdating(true); + + try { + const outcomes = ( + await Promise.all( + selectedBookIds.map((bookId) => + requestBookFormats( + bookId, + formatsForBook(bookId), + (is4k) => formatOverrides[String(is4k)] + ) + ) + ) + ).flat(); + + if (!outcomes.some((outcome) => outcome.request)) { + throw new Error('No book format request succeeded'); + } + + mutate('/api/v1/request/count'); + + if (onComplete) { + onComplete( + selectedBookIds.length === books.length + ? MediaStatus.UNKNOWN + : MediaStatus.PARTIALLY_AVAILABLE + ); + } + + addToast( + + {intl.formatMessage(messages.requestSuccess, { + title: data?.name, + strong: (msg: React.ReactNode) => {msg}, + })} + , + { appearance: 'success', autoDismiss: true } + ); + } catch { + addToast(intl.formatMessage(messages.requesterror), { + appearance: 'error', + autoDismiss: true, + }); + } finally { + setIsUpdating(false); + } + }; + + const hasAutoApprove = + selectedFormats.length > 0 && + selectedFormats.every((is4k) => + hasPermission( + [ + Permission.MANAGE_REQUESTS, + is4k ? Permission.AUTO_APPROVE_4K : Permission.AUTO_APPROVE, + is4k + ? Permission.AUTO_APPROVE_AUDIO_BOOK + : Permission.AUTO_APPROVE_BOOK, + ], + { type: 'or' } + ) + ); + + const blocklistVisibility = hasPermission( + [Permission.MANAGE_BLOCKLIST, Permission.VIEW_BLOCKLIST], + { type: 'or' } + ); + + const booksTable = (title: string, list: Series['books']) => ( +
+
+
+
+ + + + + + + + {list + .filter((book) => { + if (!blocklistVisibility) + return book.mediaInfo?.status !== MediaStatus.BLOCKLISTED; + return book; + }) + .map((book) => ( + + + + ))} + +
+ {title} +
+
+
+
+ +
+
+
+ {book.releaseDate?.slice(0, 4)} + {book.position && ` - #${book.position}`} +
+
+ {book.title} +
+
+
+
+ {bookToggle(book.id)} +
+
+ {visibleFormats.map((is4k) => ( +
+ {formatToggle(book.id, is4k)} +
+ ))} +
+
+
+
+
+
+
+
+ ); + + return ( + + {hasAutoApprove && !quota?.book.restricted && ( +
+ +
+ )} + {(quota?.book.limit ?? 0) > 0 && ( + + )} +
+
+
+
+ + + + + + + + + + {visibleFormats.map((is4k) => ( + + + + + + ))} + +
+ {allBooksToggle()} + + {intl.formatMessage(messages.fullseries)} + + {intl.formatMessage(globalMessages.status)} +
+ {columnToggle(is4k)} + + {intl.formatMessage( + is4k ? messages.audiobook : messages.ebook + )} + + {renderStatusBadge(seriesFormatStatus(is4k))} +
+
+
+
+
+ {booksTable(intl.formatMessage(globalMessages.books), ownBooks)} + {extraBooks.length > 0 && + booksTable(intl.formatMessage(messages.editionsextras), extraBooks)} + {(hasPermission(Permission.REQUEST_ADVANCED) || + hasPermission(Permission.MANAGE_REQUESTS)) && + selectedFormats.length > 0 && ( + <> + {selectedFormats.length > 1 && ( +
+ {intl.formatMessage(globalMessages.advanced)} +
+ )} + {selectedFormats.map((is4k) => ( +
+ {selectedFormats.length > 1 && ( +

+ {intl.formatMessage( + is4k ? messages.audiobook : messages.ebook + )} +

+ )} + 1} + onChange={(overrides) => { + setFormatOverrides((current) => ({ + ...current, + [String(is4k)]: overrides, + })); + }} + /> +
+ ))} + + )} +
+ ); +}; + +export default SeriesRequestModal; diff --git a/src/components/RequestModal/TvRequestModal.tsx b/src/components/RequestModal/TvRequestModal.tsx index 941b8dc71e..9db81c7706 100644 --- a/src/components/RequestModal/TvRequestModal.tsx +++ b/src/components/RequestModal/TvRequestModal.tsx @@ -199,6 +199,7 @@ const TvRequestModal = ({ tvdbId: tvdbId ?? data?.externalIds.tvdbId, mediaType: 'tv', is4k, + ignoreQuota: requestOverrides?.ignoreQuota, seasons: settings.currentSettings.partialRequestsEnabled ? selectedSeasons.sort((a, b) => a - b) : getAllSeasons().filter( @@ -417,7 +418,7 @@ const TvRequestModal = ({ : hasPermission(Permission.MANAGE_REQUESTS) ? intl.formatMessage(messages.approve) : intl.formatMessage(messages.edit) - : getAllRequestedSeasons().length >= getAllSeasons().length + : unrequestedSeasons.length === 0 ? intl.formatMessage(messages.alreadyrequested) : !settings.currentSettings.partialRequestsEnabled ? intl.formatMessage( @@ -437,9 +438,10 @@ const TvRequestModal = ({ ? false : !settings.currentSettings.partialRequestsEnabled && quota?.tv.limit && - unrequestedSeasons.length > quota.tv.limit + unrequestedSeasons.length > quota.tv.limit && + !requestOverrides?.ignoreQuota ? true - : getAllRequestedSeasons().length >= getAllSeasons().length || + : unrequestedSeasons.length === 0 || (settings.currentSettings.partialRequestsEnabled && selectedSeasons.length === 0) } @@ -484,12 +486,12 @@ const TvRequestModal = ({ ) && getAllRequestedSeasons().length < getAllSeasons().length && !editRequest && ( -

+

-

+
)} {(quota?.tv.limit ?? 0) > 0 && (
- {(hasPermission(Permission.REQUEST_ADVANCED) || - hasPermission(Permission.MANAGE_REQUESTS)) && ( + {hasPermission( + [Permission.REQUEST_ADVANCED, Permission.MANAGE_REQUESTS], + { type: 'or' } + ) && ( keyword.id === ANIME_KEYWORD_ID )} + quota={quota} onChange={(overrides) => setRequestOverrides(overrides)} requestUser={editRequest?.requestedBy} + requestId={editRequest?.id} defaultOverrides={ editRequest ? { diff --git a/src/components/RequestModal/index.tsx b/src/components/RequestModal/index.tsx index 198741793d..9361316e18 100644 --- a/src/components/RequestModal/index.tsx +++ b/src/components/RequestModal/index.tsx @@ -1,5 +1,7 @@ +import BookRequestModal from '@app/components/RequestModal/BookRequestModal'; import CollectionRequestModal from '@app/components/RequestModal/CollectionRequestModal'; import MovieRequestModal from '@app/components/RequestModal/MovieRequestModal'; +import SeriesRequestModal from '@app/components/RequestModal/SeriesRequestModal'; import TvRequestModal from '@app/components/RequestModal/TvRequestModal'; import { Transition } from '@headlessui/react'; import type { MediaStatus } from '@server/constants/media'; @@ -8,11 +10,11 @@ import type { NonFunctionProperties } from '@server/interfaces/api/common'; interface RequestModalProps { show: boolean; - type: 'movie' | 'tv' | 'collection'; + type: 'movie' | 'tv' | 'book' | 'collection' | 'series'; tmdbId: number; is4k?: boolean; editRequest?: NonFunctionProperties; - onComplete?: (newStatus: MediaStatus) => void; + onComplete?: (newStatus: MediaStatus, newStatus4k?: MediaStatus) => void; onCancel?: () => void; onUpdating?: (isUpdating: boolean) => void; } @@ -56,6 +58,22 @@ const RequestModal = ({ is4k={is4k} editRequest={editRequest} /> + ) : type === 'book' ? ( + + ) : type === 'series' ? ( + ) : ( { titles, fetchMore, error, - } = useDiscover( + } = useDiscover< + MovieResult | TvResult | PersonResult | BookResult | AuthorResult + >( `/api/v1/search`, { query: router.query.query, + type: router.query.type, }, - { hideAvailable: false, hideBlocklisted: false } + { hideAvailable: false, hideBlocklisted: false, hideRequested: false } ); + if ((error as any)?.response?.status === 503) { + return ; + } + if (error) { return ; } diff --git a/src/components/Selector/index.tsx b/src/components/Selector/index.tsx index 2c459466f8..b07d8bec21 100644 --- a/src/components/Selector/index.tsx +++ b/src/components/Selector/index.tsx @@ -23,6 +23,7 @@ import orderBy from 'lodash/orderBy'; import { useEffect, useMemo, useState } from 'react'; import { useIntl } from 'react-intl'; import type { MultiValue, SingleValue } from 'react-select'; +import Select from 'react-select'; import AsyncSelect from 'react-select/async'; import useSWR from 'swr'; @@ -294,13 +295,14 @@ export const KeywordSelector = ({ onChange, }: BaseSelectorMultiProps | BaseSelectorSingleProps) => { const intl = useIntl(); - const [defaultDataValue, setDefaultDataValue] = useState< - { label: string; value: number }[] | null + const [selectedValue, setSelectedValue] = useState< + MultiValue | SingleValue | null >(null); useEffect(() => { const loadDefaultKeywords = async (): Promise => { if (!defaultValue) { + setSelectedValue(null); return; } @@ -317,16 +319,16 @@ export const KeywordSelector = ({ (keyword): keyword is Keyword => keyword !== null ); - setDefaultDataValue( - validKeywords.map((keyword) => ({ - label: keyword.name, - value: keyword.id, - })) - ); + const nextValue = validKeywords.map((keyword) => ({ + label: keyword.name, + value: keyword.id, + })); + + setSelectedValue(isMulti ? nextValue : (nextValue[0] ?? null)); }; loadDefaultKeywords(); - }, [defaultValue]); + }, [defaultValue, isMulti]); const loadKeywordOptions = async (inputValue: string) => { const results = await axios.get( @@ -346,7 +348,6 @@ export const KeywordSelector = ({ return ( { + setSelectedValue(value); // eslint-disable-next-line @typescript-eslint/no-explicit-any onChange(value as any); }} @@ -633,3 +635,53 @@ export const UserSelector = ({ }; export { default as USCertificationSelector } from './USCertificationSelector'; + +type BookGenreVal = { + label: string; + value: string; +}; + +export const bookGenreOptions: BookGenreVal[] = [ + { label: 'Biography', value: 'biography' }, + { label: 'Classics', value: 'classics' }, + { label: 'Fantasy', value: 'fantasy' }, + { label: 'Historical Fiction', value: 'historical-fiction' }, + { label: 'Horror', value: 'horror' }, + { label: 'Mystery', value: 'mystery' }, + { label: 'Nonfiction', value: 'nonfiction' }, + { label: 'Romance', value: 'romance' }, + { label: 'Science Fiction', value: 'science-fiction' }, + { label: 'Thriller', value: 'thriller' }, + { label: 'Young Adult', value: 'young-adult' }, +]; + +type BookGenreSelectorProps = { + defaultValue?: string; + onChange: (value: MultiValue | null) => void; +}; + +export const BookGenreSelector = ({ + defaultValue, + onChange, +}: BookGenreSelectorProps) => { + const intl = useIntl(); + + const defaultDataValue = defaultValue + ? bookGenreOptions.filter((option) => + defaultValue.split(',').includes(option.value) + ) + : null; + + return ( + + key={`book-genre-select-${defaultValue}`} + className="react-select-container" + classNamePrefix="react-select" + defaultValue={defaultDataValue} + options={bookGenreOptions} + isMulti + placeholder={intl.formatMessage(messages.searchGenres)} + onChange={(value) => onChange(value)} + /> + ); +}; diff --git a/src/components/SeriesDetails/index.tsx b/src/components/SeriesDetails/index.tsx new file mode 100644 index 0000000000..e18bb8d9ea --- /dev/null +++ b/src/components/SeriesDetails/index.tsx @@ -0,0 +1,329 @@ +import Button from '@app/components/Common/Button'; +import CachedImage from '@app/components/Common/CachedImage'; +import HardcoverSetup from '@app/components/Common/HardcoverSetup'; +import ImageFader from '@app/components/Common/ImageFader'; +import LoadingSpinner from '@app/components/Common/LoadingSpinner'; +import PageTitle from '@app/components/Common/PageTitle'; +import RequestModal from '@app/components/RequestModal'; +import StatusBadge from '@app/components/StatusBadge'; +import TitleCard from '@app/components/TitleCard'; +import useSettings from '@app/hooks/useSettings'; +import { Permission, useUser } from '@app/hooks/useUser'; +import globalMessages from '@app/i18n/globalMessages'; +import Error from '@app/pages/_error'; +import defineMessages from '@app/utils/defineMessages'; +import { refreshIntervalHelper } from '@app/utils/refreshIntervalHelper'; +import { ArrowDownTrayIcon } from '@heroicons/react/24/outline'; +import { MediaStatus, MediaType } from '@server/constants/media'; +import type { Series } from '@server/models/Series'; +import { useRouter } from 'next/router'; +import { Fragment, useMemo, useState } from 'react'; +import { useIntl } from 'react-intl'; +import useSWR from 'swr'; + +const messages = defineMessages('components.SeriesDetails', { + overview: 'Overview', + editionsextras: 'Editions & Extras', + numberofbooks: '{count} Books', + requestseries: 'Request Series', + requestseriesaudio: 'Request Series in Audiobook', +}); + +interface SeriesDetailsProps { + series?: Series; +} + +const SeriesDetails = ({ series }: SeriesDetailsProps) => { + const intl = useIntl(); + const router = useRouter(); + const settings = useSettings(); + const { hasPermission } = useUser(); + const [requestModal, setRequestModal] = useState(false); + + const returnSeriesDownloadItems = (data: Series | undefined) => { + const [downloadStatus, downloadStatus4k] = [ + data?.books.flatMap((item) => + item.mediaInfo?.downloadStatus ? item.mediaInfo?.downloadStatus : [] + ), + data?.books.flatMap((item) => + item.mediaInfo?.downloadStatus4k ? item.mediaInfo?.downloadStatus4k : [] + ), + ]; + + return { downloadStatus, downloadStatus4k }; + }; + + const { + data, + error, + mutate: revalidate, + } = useSWR(`/api/v1/series/${router.query.seriesId}`, { + fallbackData: series, + revalidateOnMount: true, + refreshInterval: refreshIntervalHelper( + returnSeriesDownloadItems(series), + 15000 + ), + }); + + const [downloadStatus, downloadStatusAudio] = useMemo(() => { + const downloadItems = returnSeriesDownloadItems(data); + return [downloadItems.downloadStatus, downloadItems.downloadStatus4k]; + }, [data]); + + const [titles, titlesAudio] = useMemo(() => { + return [ + data?.books + .filter((book) => (book.mediaInfo?.downloadStatus ?? []).length > 0) + .map((title) => title.title), + data?.books + .filter((book) => (book.mediaInfo?.downloadStatus4k ?? []).length > 0) + .map((title) => title.title), + ]; + }, [data?.books]); + + if (!data && !error) { + return ; + } + + if ((error as any)?.response?.status === 503) { + return ; + } + + if (!data) { + return ; + } + + // Editions and extras are listed on their own and left out of the series status + const primaryBooks = data.books.filter((book) => !book.extra); + const extraBooks = data.books.filter((book) => book.extra); + + let collectionStatus = MediaStatus.UNKNOWN; + let collectionStatusAudio = MediaStatus.UNKNOWN; + + if ( + primaryBooks.every( + (book) => + book.mediaInfo && book.mediaInfo.status === MediaStatus.AVAILABLE + ) + ) { + collectionStatus = MediaStatus.AVAILABLE; + } else if ( + primaryBooks.some( + (book) => + book.mediaInfo && book.mediaInfo.status === MediaStatus.AVAILABLE + ) + ) { + collectionStatus = MediaStatus.PARTIALLY_AVAILABLE; + } + + if ( + primaryBooks.every( + (book) => + book.mediaInfo && book.mediaInfo.status4k === MediaStatus.AVAILABLE + ) + ) { + collectionStatusAudio = MediaStatus.AVAILABLE; + } else if ( + primaryBooks.some( + (book) => + book.mediaInfo && book.mediaInfo.status4k === MediaStatus.AVAILABLE + ) + ) { + collectionStatusAudio = MediaStatus.PARTIALLY_AVAILABLE; + } + + const hasRequestable = + hasPermission([Permission.REQUEST, Permission.REQUEST_BOOK], { + type: 'or', + }) && + data.books.filter( + (book) => !book.mediaInfo || book.mediaInfo.status === MediaStatus.UNKNOWN + ).length > 0; + + const hasRequestableAudio = + settings.currentSettings.bookAudioEnabled && + hasPermission([Permission.REQUEST_4K, Permission.REQUEST_AUDIO_BOOK], { + type: 'or', + }) && + data.books.filter( + (book) => + !book.mediaInfo || book.mediaInfo.status4k === MediaStatus.UNKNOWN + ).length > 0; + + const collectionAttributes: React.ReactNode[] = []; + + collectionAttributes.push( + intl.formatMessage(messages.numberofbooks, { + count: primaryBooks.length, + }) + ); + + const bookCards = (list: Series['books']) => ( +
    + {list.map((book, index) => ( +
  • + +
  • + ))} +
+ ); + + return ( +
+ {data.books && ( +
+ media.backdropPath) + .map((media) => media.backdropPath) + ), + ] as string[] + ).slice(0, 6)} + /> +
+ )} + + { + revalidate(); + setRequestModal(false); + }} + onCancel={() => setRequestModal(false)} + /> +
+
+ +
+
+
+ (book.mediaInfo?.downloadStatus ?? []).length > 0 + )} + /> + {settings.currentSettings.bookAudioEnabled && + hasPermission( + [Permission.REQUEST_4K, Permission.REQUEST_AUDIO_BOOK], + { + type: 'or', + } + ) && ( + + (book.mediaInfo?.downloadStatus4k ?? []).length > 0 + )} + /> + )} +
+

{data.name}

+ + {collectionAttributes.length > 0 && + collectionAttributes + .map((t, k) => {t}) + .reduce((prev, curr) => ( + + {intl.formatMessage(globalMessages.delimitedlist, { + a: prev, + b: curr, + })} + + ))} + +
+
+ {settings.currentSettings.seriesRequestsEnabled && + (hasRequestable || hasRequestableAudio) && ( + + )} +
+
+ {data.overview && ( +
+
+

{intl.formatMessage(messages.overview)}

+

{data.overview}

+
+
+ )} +
+
+ {intl.formatMessage(globalMessages.books)} +
+
+ {bookCards(primaryBooks)} + {extraBooks.length > 0 && ( + <> +
+
+ {intl.formatMessage(messages.editionsextras)} +
+
+ {bookCards(extraBooks)} + + )} +
+
+ ); +}; + +export default SeriesDetails; diff --git a/src/components/Settings/Notifications/NotificationsDiscord.tsx b/src/components/Settings/Notifications/NotificationsDiscord.tsx index 937d2dde10..91faa0401e 100644 --- a/src/components/Settings/Notifications/NotificationsDiscord.tsx +++ b/src/components/Settings/Notifications/NotificationsDiscord.tsx @@ -25,6 +25,9 @@ const messages = defineMessages('components.Settings.Notifications', { webhookRoleId: 'Notification Role ID', webhookRoleIdTip: 'The role ID to mention in the webhook message. Leave empty to disable mentions', + webhookThreadId: 'Thread ID', + webhookThreadIdTip: + 'The ID of the thread channel to post notifications in. Leave empty to post in the webhook channel', useUserLocale: 'Use Notification Recipient Locale', discordsettingssaved: 'Discord notification settings saved successfully!', discordsettingsfailed: 'Discord notification settings failed to save.', @@ -33,6 +36,7 @@ const messages = defineMessages('components.Settings.Notifications', { toastDiscordTestFailed: 'Discord test notification failed to send.', validationUrl: 'You must provide a valid URL', validationWebhookRoleId: 'You must provide a valid Discord Role ID', + validationWebhookThreadId: 'You must provide a valid Discord Thread ID', validationTypes: 'You must select at least one notification type', enableMentions: 'Enable Mentions', }); @@ -68,6 +72,13 @@ const NotificationsDiscord = () => { /^\d{17,19}$/, intl.formatMessage(messages.validationWebhookRoleId) ), + webhookThreadId: Yup.string() + .nullable() + .transform((value) => (value === '' ? null : value)) + .matches(/^\d{17,19}$/, { + message: intl.formatMessage(messages.validationWebhookThreadId), + excludeEmptyString: true, + }), }); if (!data && !error) { @@ -84,6 +95,7 @@ const NotificationsDiscord = () => { botAvatarUrl: data?.options.botAvatarUrl, webhookUrl: data.options.webhookUrl, webhookRoleId: data?.options.webhookRoleId, + webhookThreadId: data?.options.webhookThreadId, enableMentions: data?.options.enableMentions, locale: data?.options.locale || 'en', useUserLocale: data?.options.useUserLocale ?? false, @@ -100,6 +112,7 @@ const NotificationsDiscord = () => { botAvatarUrl: values.botAvatarUrl, webhookUrl: values.webhookUrl, webhookRoleId: values.webhookRoleId, + webhookThreadId: values.webhookThreadId, enableMentions: values.enableMentions, locale: values.locale, useUserLocale: values.useUserLocale, @@ -152,6 +165,7 @@ const NotificationsDiscord = () => { botAvatarUrl: values.botAvatarUrl, webhookUrl: values.webhookUrl, webhookRoleId: values.webhookRoleId, + webhookThreadId: values.webhookThreadId, enableMentions: values.enableMentions, locale: values.locale, useUserLocale: values.useUserLocale, @@ -292,6 +306,28 @@ const NotificationsDiscord = () => { )}
+
+ +
+
+ +
+ {errors.webhookThreadId && + touched.webhookThreadId && + typeof errors.webhookThreadId === 'string' && ( +
{errors.webhookThreadId}
+ )} +
+
+
+ +
+
+
+
+ +
+ +
+
+
+ +
+
+ +
+
+
{errors.rootFolder && @@ -334,85 +390,91 @@ const OverrideRuleModal = ({ )}
-
- -
-
- { - setFieldValue( - 'genre', - genres?.map((v) => v.value).join(',') - ); - }} - /> + {values.readarrServiceId === null && ( +
+ +
+
+ { + setFieldValue( + 'genre', + genres?.map((v) => v.value).join(',') + ); + }} + /> +
+ {errors.genre && + touched.genre && + typeof errors.genre === 'string' && ( +
{errors.genre}
+ )}
- {errors.genre && - touched.genre && - typeof errors.genre === 'string' && ( -
{errors.genre}
- )}
-
-
- -
-
- { - setFieldValue('language', value); - }} - isDisabled={!isValidated || isTesting} - /> + )} + {values.readarrServiceId === null && ( +
+ +
+
+ { + setFieldValue('language', value); + }} + isDisabled={!isValidated || isTesting} + /> +
+ {errors.language && + touched.language && + typeof errors.language === 'string' && ( +
{errors.language}
+ )}
- {errors.language && - touched.language && - typeof errors.language === 'string' && ( -
{errors.language}
- )}
-
-
- -
-
- { - setFieldValue( - 'keywords', - value?.map((v) => v.value).join(',') - ); - }} - /> + )} + {values.readarrServiceId === null && ( +
+ +
+
+ { + setFieldValue( + 'keywords', + value?.map((v) => v.value).join(',') + ); + }} + /> +
+ {errors.keywords && + touched.keywords && + typeof errors.keywords === 'string' && ( +
{errors.keywords}
+ )}
- {errors.keywords && - touched.keywords && - typeof errors.keywords === 'string' && ( -
{errors.keywords}
- )}
-
+ )}

{intl.formatMessage(messages.settings)}

@@ -485,6 +547,48 @@ const OverrideRuleModal = ({ )}
+ {values.readarrServiceId !== null && ( +
+ +
+
+ + + {'metadataProfiles' in testResponse && + testResponse.metadataProfiles && + testResponse.metadataProfiles.length > 0 && + testResponse.metadataProfiles.map((profile) => ( + + ))} + +
+ {errors.metadataProfileId && + touched.metadataProfileId && + typeof errors.metadataProfileId === 'string' && ( +
+ {errors.metadataProfileId} +
+ )} +
+
+ )}
@@ -740,7 +731,9 @@ const SettingsJobs = () => {

{intl.formatMessage(messages.imagecacheDescription, { code: (msg: React.ReactNode) => ( - {msg} + + {msg} + ), appDataPath: appData ? appData.appDataPath : '/app/config', })} @@ -780,6 +773,17 @@ const SettingsJobs = () => { {formatBytes(cacheData?.imageCache.avatar.size ?? 0)} + + Hardcover + + {intl.formatNumber( + cacheData?.imageCache.hardcover.imageCount ?? 0 + )} + + + {formatBytes(cacheData?.imageCache.hardcover.size ?? 0)} + +

diff --git a/src/components/Settings/SettingsMain/index.tsx b/src/components/Settings/SettingsMain/index.tsx index 00703ddd37..28f5f3acc4 100644 --- a/src/components/Settings/SettingsMain/index.tsx +++ b/src/components/Settings/SettingsMain/index.tsx @@ -9,6 +9,7 @@ import CopyButton from '@app/components/Settings/CopyButton'; import SettingsBadge from '@app/components/Settings/SettingsBadge'; import { availableLanguages } from '@app/context/LanguageContext'; import useLocale from '@app/hooks/useLocale'; +import useSettings from '@app/hooks/useSettings'; import useToasts from '@app/hooks/useToasts'; import { Permission, useUser } from '@app/hooks/useUser'; import globalMessages from '@app/i18n/globalMessages'; @@ -31,6 +32,7 @@ const messages = defineMessages('components.Settings.SettingsMain', { generalsettingsDescription: 'Configure global and default settings for Seerr.', apikey: 'API Key', + hardcoverapikey: 'Hardcover API Key', apikeyCopied: 'Copied API key to clipboard.', applicationTitle: 'Application Title', applicationurl: 'Application URL', @@ -62,6 +64,9 @@ const messages = defineMessages('components.Settings.SettingsMain', { hideAvailable: 'Hide Available Media', hideAvailableTip: 'Hide available media from the discover pages but not search results', + hideRequested: 'Hide Requested Media', + hideRequestedTip: + 'Hide media that has been requested from the discover pages but not search results', cacheImages: 'Enable Image Caching', cacheImagesTip: 'Cache externally sourced images (requires a significant amount of disk space)', @@ -70,10 +75,20 @@ const messages = defineMessages('components.Settings.SettingsMain', { validationApplicationUrlTrailingSlash: 'URL must not end in a trailing slash', partialRequestsEnabled: 'Allow Partial Series Requests', enableSpecialEpisodes: 'Allow Special Episodes Requests', + syncBookFormatRequests: 'Request Both Book Formats by Default', + syncBookFormatRequestsTip: + 'Pre-select the ebook and audiobook together when requesting a book', + syncBookFormatRequestsDisabledTip: + 'A default Audiobook Readarr server must be configured to enable this setting', + seriesRequestsEnabled: 'Allow Book Series Requests', + seriesRequestsEnabledTip: + 'Allow every book in a series to be requested at once', locale: 'Display Language', youtubeUrl: 'YouTube URL', youtubeUrlTip: 'Base URL for YouTube videos if a self-hosted YouTube instance is used.', + versionCheck: 'Version Check', + versionCheckTip: 'Automatically check for new versions on GitHub.', validationUrl: 'You must provide a valid URL', validationUrlTrailingSlash: 'URL must not end in a trailing slash', }); @@ -83,6 +98,7 @@ const SettingsMain = () => { const { user: currentUser, hasPermission: userHasPermission } = useUser(); const intl = useIntl(); const { setLocale } = useLocale(); + const settings = useSettings(); const { data, error, @@ -167,10 +183,12 @@ const SettingsMain = () => {
{ blocklistedTagsLimit: data?.blocklistedTagsLimit || 50, partialRequestsEnabled: data?.partialRequestsEnabled, enableSpecialEpisodes: data?.enableSpecialEpisodes, + syncBookFormatRequests: data?.syncBookFormatRequests, + seriesRequestsEnabled: data?.seriesRequestsEnabled, cacheImages: data?.cacheImages, youtubeUrl: data?.youtubeUrl, + versionCheck: data?.versionCheck, }} enableReinitialize validationSchema={MainSettingsSchema} onSubmit={async (values) => { try { await axios.post('/api/v1/settings/main', { + hardcoverapikey: values.hardcoverapikey, applicationTitle: values.applicationTitle, applicationUrl: values.applicationUrl, hideAvailable: values.hideAvailable, hideBlocklisted: values.hideBlocklisted, + hideRequested: values.hideRequested, locale: values.locale, discoverRegion: values.discoverRegion, streamingRegion: values.streamingRegion, @@ -203,11 +226,14 @@ const SettingsMain = () => { blocklistedTagsLimit: values.blocklistedTagsLimit, partialRequestsEnabled: values.partialRequestsEnabled, enableSpecialEpisodes: values.enableSpecialEpisodes, + syncBookFormatRequests: values.syncBookFormatRequests, + seriesRequestsEnabled: values.seriesRequestsEnabled, cacheImages: values.cacheImages, youtubeUrl: values.youtubeUrl, + versionCheck: values?.versionCheck, }); mutate('/api/v1/settings/public'); - mutate('/api/v1/status'); + mutate('/api/v1/status?checkUpdateAvailable=false'); if (setLocale) { setLocale( @@ -242,39 +268,61 @@ const SettingsMain = () => { return (
{userHasPermission(Permission.ADMIN) && ( -
- -
-
- - - + <> +
+ +
+
+ + + +
-
+
+ +
+
+ + +
+
+
+ )}
+
+ +
+ { + setFieldValue('hideRequested', !values.hideRequested); + }} + /> +
+
+
+ +
+ { + setFieldValue( + 'syncBookFormatRequests', + !values.syncBookFormatRequests + ); + }} + /> +
+
+
+ +
+ { + setFieldValue( + 'seriesRequestsEnabled', + !values.seriesRequestsEnabled + ); + }} + /> +
+
+
+ +
+ { + setFieldValue('versionCheck', !values.versionCheck); + }} + /> +
+
diff --git a/src/components/Settings/SettingsNetwork/index.tsx b/src/components/Settings/SettingsNetwork/index.tsx index dd4fd5fe50..997f20f4d7 100644 --- a/src/components/Settings/SettingsNetwork/index.tsx +++ b/src/components/Settings/SettingsNetwork/index.tsx @@ -175,7 +175,7 @@ const SettingsNetwork = () => { apiRequestTimeout: Number(values.apiRequestTimeout) * 1000, }); mutate('/api/v1/settings/public'); - mutate('/api/v1/status'); + mutate('/api/v1/status?checkUpdateAvailable=false'); addToast(intl.formatMessage(messages.toastSettingsSuccess), { autoDismiss: true, diff --git a/src/components/Settings/SettingsPlex.tsx b/src/components/Settings/SettingsPlex.tsx index 7dca6f67fd..2945344b73 100644 --- a/src/components/Settings/SettingsPlex.tsx +++ b/src/components/Settings/SettingsPlex.tsx @@ -41,6 +41,9 @@ const messages = defineMessages('components.Settings', { toastPlexRefresh: 'Retrieving server list from Plex…', toastPlexRefreshSuccess: 'Plex server list retrieved successfully!', toastPlexRefreshFailure: 'Failed to retrieve Plex server list.', + toastPlexSyncFailure: 'Failed to sync Plex libraries.', + invalidurlerror: 'Unable to connect to {mediaServerName} server.', + toggleLibraryFailure: 'Failed to update library.', toastPlexConnecting: 'Attempting to connect to Plex…', toastPlexConnectingSuccess: 'Plex connection established successfully!', toastPlexConnectingFailure: 'Failed to connect to Plex.', @@ -66,7 +69,7 @@ const messages = defineMessages('components.Settings', { validationPortRequired: 'You must provide a valid port number', webAppUrl: 'Web App URL', webAppUrlTip: - 'Optionally direct users to the web app on your server instead of the "hosted" web app', + 'Optionally direct users to the web app on your server instead of https://app.plex.tv/desktop', tautulliSettings: 'Tautulli Settings', tautulliSettingsDescription: 'Optionally configure the settings for your Tautulli server. Seerr fetches watch history data for your Plex media from Tautulli.', @@ -108,10 +111,10 @@ interface PresetServerDisplay { message?: string; } interface SettingsPlexProps { - onComplete?: () => void; + isSetupSettings?: boolean; } -const SettingsPlex = ({ onComplete }: SettingsPlexProps) => { +const SettingsPlex = ({ isSetupSettings }: SettingsPlexProps) => { const [isSyncing, setIsSyncing] = useState(false); const [isRefreshingPresets, setIsRefreshingPresets] = useState(false); const [availableServers, setAvailableServers] = useState( @@ -122,12 +125,17 @@ const SettingsPlex = ({ onComplete }: SettingsPlexProps) => { error, mutate: revalidate, } = useSWR('/api/v1/settings/plex'); - const { data: dataTautulli, mutate: revalidateTautulli } = - useSWR('/api/v1/settings/tautulli'); + const { + data: dataTautulli, + error: errorTautulli, + mutate: revalidateTautulli, + } = useSWR( + isSetupSettings ? null : '/api/v1/settings/tautulli' + ); const { data: dataSync, mutate: revalidateSync } = useSWR( '/api/v1/settings/plex/sync', { - refreshInterval: 1000, + refreshInterval: (latestData) => (latestData?.running ? 1000 : 10000), } ); const intl = useIntl(); @@ -240,19 +248,24 @@ const SettingsPlex = ({ onComplete }: SettingsPlexProps) => { const syncLibraries = async () => { setIsSyncing(true); - const params: { sync: boolean; enable?: string } = { - sync: true, - }; - - if (activeLibraries.length > 0) { - params.enable = activeLibraries.join(','); + try { + await axios.post('/api/v1/settings/plex/library/sync'); + } catch (e) { + addToast( + e?.response?.data?.message === 'CONNECTION_ERROR' + ? intl.formatMessage(messages.invalidurlerror, { + mediaServerName: 'Plex', + }) + : intl.formatMessage(messages.toastPlexSyncFailure), + { + autoDismiss: true, + appearance: 'error', + } + ); + } finally { + setIsSyncing(false); + revalidate(); } - - await axios.get('/api/v1/settings/plex/library', { - params, - }); - setIsSyncing(false); - revalidate(); }; const refreshPresetServers = async () => { @@ -311,34 +324,26 @@ const SettingsPlex = ({ onComplete }: SettingsPlexProps) => { const toggleLibrary = async (libraryId: string) => { setIsSyncing(true); - if (activeLibraries.includes(libraryId)) { - const params: { enable?: string } = {}; - - if (activeLibraries.length > 1) { - params.enable = activeLibraries - .filter((id) => id !== libraryId) - .join(','); - } - - await axios.get('/api/v1/settings/plex/library', { - params, + try { + await axios.put(`/api/v1/settings/plex/library/${libraryId}`, { + enabled: !activeLibraries.includes(libraryId), }); - } else { - await axios.get('/api/v1/settings/plex/library', { - params: { - enable: [...activeLibraries, libraryId].join(','), - }, + } catch { + addToast(intl.formatMessage(messages.toggleLibraryFailure), { + autoDismiss: true, + appearance: 'error', }); + } finally { + setIsSyncing(false); + revalidate(); } - - if (onComplete) { - onComplete(); - } - setIsSyncing(false); - revalidate(); }; - if ((!data || !dataTautulli) && !error) { + if ( + (!data || (!isSetupSettings && !dataTautulli)) && + !error && + !errorTautulli + ) { return ; } return ( @@ -354,7 +359,7 @@ const SettingsPlex = ({ onComplete }: SettingsPlexProps) => {

{intl.formatMessage(messages.plexsettingsDescription)}

- {!!onComplete && ( + {isSetupSettings && (
{ inputMode="url" id="webAppUrl" name="webAppUrl" - placeholder="https://app.plex.tv/desktop" + placeholder="https://your-server-fqdn.com/web/index.html" />
{errors.webAppUrl && @@ -738,7 +743,7 @@ const SettingsPlex = ({ onComplete }: SettingsPlexProps) => {
- {!onComplete && ( + {!isSetupSettings && ( <>

diff --git a/src/components/Settings/SettingsServices.tsx b/src/components/Settings/SettingsServices.tsx index 66179b10b5..b4f0739aca 100644 --- a/src/components/Settings/SettingsServices.tsx +++ b/src/components/Settings/SettingsServices.tsx @@ -1,4 +1,5 @@ import RadarrLogo from '@app/assets/services/radarr.svg'; +import ReadarrLogo from '@app/assets/services/readarr.svg'; import SonarrLogo from '@app/assets/services/sonarr.svg'; import Alert from '@app/components/Common/Alert'; import Badge from '@app/components/Common/Badge'; @@ -6,6 +7,7 @@ import Button from '@app/components/Common/Button'; import LoadingSpinner from '@app/components/Common/LoadingSpinner'; import Modal from '@app/components/Common/Modal'; import PageTitle from '@app/components/Common/PageTitle'; +import ReadarrModal from '@app/components/Settings//ReadarrModal'; import OverrideRuleModal from '@app/components/Settings/OverrideRule/OverrideRuleModal'; import OverrideRuleTiles from '@app/components/Settings/OverrideRule/OverrideRuleTiles'; import RadarrModal from '@app/components/Settings/RadarrModal'; @@ -16,7 +18,11 @@ import { Transition } from '@headlessui/react'; import { PencilIcon, PlusIcon, TrashIcon } from '@heroicons/react/24/solid'; import type OverrideRule from '@server/entity/OverrideRule'; import type { OverrideRuleResultsResponse } from '@server/interfaces/api/overrideRuleInterfaces'; -import type { RadarrSettings, SonarrSettings } from '@server/lib/settings'; +import type { + RadarrSettings, + ReadarrSettings, + SonarrSettings, +} from '@server/lib/settings'; import axios from 'axios'; import { Fragment, useState } from 'react'; import { useIntl } from 'react-intl'; @@ -26,25 +32,38 @@ const messages = defineMessages('components.Settings', { services: 'Services', radarrsettings: 'Radarr Settings', sonarrsettings: 'Sonarr Settings', + readarrsettings: 'Readarr Settings', serviceSettingsDescription: 'Configure your {serverType} server(s) below. You can connect multiple {serverType} servers, but only two of them can be marked as defaults (one non-4K and one 4K). Administrators are able to override the server used to process new requests prior to approval.', + readarrServiceSettingsDescription: + 'Configure your Readarr server(s) below. You can connect multiple Readarr servers, but only one of them can be marked as a default. Administrators are able to override the server used to process new requests prior to approval.', deleteserverconfirm: 'Are you sure you want to delete this server?', ssl: 'SSL', default: 'Default', default4k: 'Default 4K', + defaultAudio: 'Default Audiobook', is4k: '4K', + isAudio: 'Audiobook', address: 'Address', activeProfile: 'Active Profile', + activeMetadataProfile: 'Active Metadata Profile', addradarr: 'Add Radarr Server', addsonarr: 'Add Sonarr Server', + addreadarr: 'Add Readarr Server', noDefaultServer: 'At least one {serverType} server must be marked as default in order for {mediaType} requests to be processed.', noDefaultNon4kServer: 'If you only have a single {serverType} server for both non-4K and 4K content (or if you only download 4K content), your {serverType} server should NOT be designated as a 4K server.', noDefault4kServer: 'A 4K {serverType} server must be marked as default in order to enable users to submit 4K {mediaType} requests.', + noDefaultNonAudioServer: + 'If you only have a single {serverType} server for both non-Audiobook and Audiobook content (or if you only download Audiobook content), your {serverType} server should NOT be designated as a Audiobook server.', + noDefaultAudioServer: + 'A Audiobook {serverType} server must be marked as default in order to enable users to submit Audio {mediaType} requests.', + mediaTypeMovie: 'movie', mediaTypeSeries: 'series', + mediaTypeBooks: 'book', deleteServer: 'Delete {serverType} Server', overrideRules: 'Override Rules', overrideRulesDescription: @@ -61,7 +80,9 @@ interface ServerInstanceProps { isSSL?: boolean; externalUrl?: string; profileName: string; + metadataProfileName?: string; isSonarr?: boolean; + isReadarr?: boolean; onEdit: () => void; onDelete: () => void; } @@ -84,6 +105,13 @@ export interface DVRTestResponse { export type RadarrTestResponse = DVRTestResponse; +export type ReadarrTestResponse = DVRTestResponse & { + metadataProfiles: { + id: number; + name: string; + }[]; +}; + export type SonarrTestResponse = DVRTestResponse & { languageProfiles: | { @@ -98,10 +126,12 @@ const ServerInstance = ({ hostname, port, profileName, + metadataProfileName, is4k = false, isDefault = false, isSSL = false, isSonarr = false, + isReadarr = false, externalUrl, onEdit, onDelete, @@ -131,11 +161,17 @@ const ServerInstance = ({ {intl.formatMessage(messages.default)} )} {isDefault && is4k && ( - {intl.formatMessage(messages.default4k)} + + {isReadarr + ? intl.formatMessage(messages.defaultAudio) + : intl.formatMessage(messages.default4k)} + )} {!isDefault && is4k && ( - {intl.formatMessage(messages.is4k)} + {isReadarr + ? intl.formatMessage(messages.isAudio) + : intl.formatMessage(messages.is4k)} )} {isSSL && ( @@ -163,6 +199,14 @@ const ServerInstance = ({ {profileName}

+ {isReadarr && metadataProfileName && ( +

+ + {intl.formatMessage(messages.activeMetadataProfile)} + + {metadataProfileName} +

+ )}

{isSonarr ? ( + ) : isReadarr ? ( + ) : ( )} @@ -215,6 +261,11 @@ const SettingsServices = () => { error: sonarrError, mutate: revalidateSonarr, } = useSWR('/api/v1/settings/sonarr'); + const { + data: readarrData, + error: readarrError, + mutate: revalidateReadarr, + } = useSWR('/api/v1/settings/readarr'); const { data: rules, mutate: revalidate } = useSWR('/api/v1/overrideRule'); const [editRadarrModal, setEditRadarrModal] = useState<{ @@ -231,9 +282,16 @@ const SettingsServices = () => { open: false, sonarr: null, }); + const [editReadarrModal, setEditReadarrModal] = useState<{ + open: boolean; + readarr: ReadarrSettings | null; + }>({ + open: false, + readarr: null, + }); const [deleteServerModal, setDeleteServerModal] = useState<{ open: boolean; - type: 'radarr' | 'sonarr'; + type: 'radarr' | 'sonarr' | 'readarr'; serverId: number | null; }>({ open: false, @@ -255,6 +313,7 @@ const SettingsServices = () => { setDeleteServerModal({ open: false, serverId: null, type: 'radarr' }); revalidateRadarr(); revalidateSonarr(); + revalidateReadarr(); mutate('/api/v1/settings/public'); }; @@ -304,6 +363,20 @@ const SettingsServices = () => { }} /> )} + {editReadarrModal.open && ( + { + if (!overrideRuleModal.open) + setEditReadarrModal({ open: false, readarr: null }); + }} + onSave={() => { + revalidateReadarr(); + mutate('/api/v1/settings/public'); + setEditReadarrModal({ open: false, readarr: null }); + }} + /> + )} { } title={intl.formatMessage(messages.deleteServer, { serverType: - deleteServerModal.type === 'radarr' ? 'Radarr' : 'Sonarr', + deleteServerModal.type === 'radarr' + ? 'Radarr' + : deleteServerModal.type === 'readarr' + ? 'Readarr' + : 'Sonarr', })} > {intl.formatMessage(messages.deleteserverconfirm)} @@ -499,23 +576,111 @@ const SettingsServices = () => { )}
+
+

+ {intl.formatMessage(messages.readarrsettings)} +

+

+ {intl.formatMessage(messages.readarrServiceSettingsDescription)} +

+
+
+ {!readarrData && !readarrError && } + {readarrData && !readarrError && ( + <> + {readarrData.length > 0 && + (!readarrData.some((readarr) => readarr.isDefault) ? ( + + ) : !readarrData.some( + (readarr) => readarr.isDefault && !readarr.is4k + ) ? ( + ( + + {msg} + + ), + })} + /> + ) : ( + readarrData.some((readarr) => readarr.is4k) && + !readarrData.some( + (readarr) => readarr.isDefault && readarr.is4k + ) && ( + + ) + ))} +
    + {readarrData.map((readarr) => ( + + setEditReadarrModal({ open: true, readarr: readarr }) + } + onDelete={() => + setDeleteServerModal({ + open: true, + serverId: readarr.id, + type: 'readarr', + }) + } + /> + ))} +
  • +
    + +
    +
  • +
+ + )} +

{intl.formatMessage(messages.overrideRules)}

- {intl.formatMessage(messages.overrideRulesDescription, { - serverType: 'Sonarr', - })} + {intl.formatMessage(messages.overrideRulesDescription)}

    - {rules && radarrData && sonarrData && ( + {rules && radarrData && sonarrData && readarrData && ( @@ -524,7 +689,11 @@ const SettingsServices = () => {
- {overrideRuleModal.open && radarrData && sonarrData && ( + {overrideRuleModal.open && radarrData && sonarrData && readarrData && ( { @@ -551,6 +720,7 @@ const SettingsServices = () => { }} radarrServices={radarrData} sonarrServices={sonarrData} + readarrServices={readarrData} /> )} diff --git a/src/components/Settings/SettingsUsers/index.tsx b/src/components/Settings/SettingsUsers/index.tsx index b69ea2bec4..8538dcd6cf 100644 --- a/src/components/Settings/SettingsUsers/index.tsx +++ b/src/components/Settings/SettingsUsers/index.tsx @@ -38,6 +38,7 @@ const messages = defineMessages('components.Settings.SettingsUsers', { 'Allow {mediaServerName} users to sign in without first being imported', movieRequestLimitLabel: 'Global Movie Request Limit', tvRequestLimitLabel: 'Global Series Request Limit', + bookRequestLimitLabel: 'Global Books Request Limit', defaultPermissions: 'Default Permissions', defaultPermissionsTip: 'Initial permissions assigned to new users', disabledMediaServerLoginWarning: @@ -114,6 +115,8 @@ const SettingsUsers = () => { movieQuotaDays: data?.defaultQuotas.movie.quotaDays ?? 7, tvQuotaLimit: data?.defaultQuotas.tv.quotaLimit ?? 0, tvQuotaDays: data?.defaultQuotas.tv.quotaDays ?? 7, + bookQuotaLimit: data?.defaultQuotas.book.quotaLimit ?? 0, + bookQuotaDays: data?.defaultQuotas.book.quotaDays ?? 7, defaultPermissions: data?.defaultPermissions ?? 0, }} validationSchema={schema} @@ -133,6 +136,10 @@ const SettingsUsers = () => { quotaLimit: values.tvQuotaLimit, quotaDays: values.tvQuotaDays, }, + book: { + quotaLimit: values.bookQuotaLimit, + quotaDays: values.bookQuotaDays, + }, }, defaultPermissions: values.defaultPermissions, }); @@ -276,6 +283,21 @@ const SettingsUsers = () => { />
+
+ +
+ +
+
{ activeLanguageProfileId: sonarr?.activeLanguageProfileId, rootFolder: sonarr?.activeDirectory, seriesType: sonarr?.seriesType, - animeSeriesType: sonarr?.animeSeriesType, + animeSeriesType: sonarr?.animeSeriesType ?? 'anime', activeAnimeProfileId: sonarr?.activeAnimeProfileId, activeAnimeLanguageProfileId: sonarr?.activeAnimeLanguageProfileId, activeAnimeRootFolder: sonarr?.activeAnimeDirectory, @@ -791,8 +791,8 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => { name="animeSeriesType" disabled={!isValidated || isTesting} > - +
diff --git a/src/components/Setup/JellyfinSetup.tsx b/src/components/Setup/JellyfinSetup.tsx index ecde73d504..c6b259f7e1 100644 --- a/src/components/Setup/JellyfinSetup.tsx +++ b/src/components/Setup/JellyfinSetup.tsx @@ -133,6 +133,7 @@ function JellyfinSetup({ let errorMessage = messages.loginerror; switch (e?.response?.data?.message) { case ApiErrorCode.InvalidUrl: + case ApiErrorCode.ConnectionError: errorMessage = messages.invalidurlerror; break; case ApiErrorCode.InvalidCredentials: diff --git a/src/components/Setup/index.tsx b/src/components/Setup/index.tsx index 947104d352..dbf2f65fd8 100644 --- a/src/components/Setup/index.tsx +++ b/src/components/Setup/index.tsx @@ -2,6 +2,7 @@ import EmbyLogo from '@app/assets/services/emby.svg'; import JellyfinLogo from '@app/assets/services/jellyfin.svg'; import PlexLogo from '@app/assets/services/plex.svg'; import AppDataWarning from '@app/components/AppDataWarning'; +import Alert from '@app/components/Common/Alert'; import Button from '@app/components/Common/Button'; import ImageFader from '@app/components/Common/ImageFader'; import PageTitle from '@app/components/Common/PageTitle'; @@ -12,14 +13,13 @@ import SettingsServices from '@app/components/Settings/SettingsServices'; import SetupSteps from '@app/components/Setup/SetupSteps'; import useLocale from '@app/hooks/useLocale'; import useSettings from '@app/hooks/useSettings'; -import useToasts from '@app/hooks/useToasts'; import defineMessages from '@app/utils/defineMessages'; import { MediaServerType } from '@server/constants/server'; import type { Library } from '@server/lib/settings'; import axios from 'axios'; import Image from 'next/image'; import { useRouter } from 'next/router'; -import { useCallback, useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { useIntl } from 'react-intl'; import useSWR, { mutate } from 'swr'; import SetupLogin from './SetupLogin'; @@ -39,22 +39,19 @@ const messages = defineMessages('components.Setup', { configuremediaserver: 'Configure Media Server', configureservices: 'Configure Services', librarieserror: - 'Validation failed. Please toggle the libraries again to continue.', + 'Unable to load libraries from your media server. Check that it is reachable and still configured correctly.', }); const Setup = () => { const intl = useIntl(); const [isUpdating, setIsUpdating] = useState(false); const [currentStep, setCurrentStep] = useState(1); - const [mediaServerSettingsComplete, setMediaServerSettingsComplete] = - useState(false); const [mediaServerType, setMediaServerType] = useState( MediaServerType.NOT_CONFIGURED ); const router = useRouter(); const { locale } = useLocale(); const settings = useSettings(); - const toasts = useToasts(); const finishSetup = async () => { setIsUpdating(true); @@ -71,34 +68,21 @@ const Setup = () => { } }; - const validateLibraries = useCallback(async () => { - try { - const endpointMap: Record = { - [MediaServerType.JELLYFIN]: '/api/v1/settings/jellyfin', - [MediaServerType.EMBY]: '/api/v1/settings/jellyfin', - [MediaServerType.PLEX]: '/api/v1/settings/plex', - [MediaServerType.NOT_CONFIGURED]: '', - }; - - const endpoint = endpointMap[mediaServerType]; - if (!endpoint) return; - - const response = await axios.get(endpoint); - - const hasEnabledLibraries = response.data?.libraries?.some( - (library: Library) => library.enabled - ); + const mediaServerSettingsEndpoint: Record = { + [MediaServerType.JELLYFIN]: '/api/v1/settings/jellyfin', + [MediaServerType.EMBY]: '/api/v1/settings/jellyfin', + [MediaServerType.PLEX]: '/api/v1/settings/plex', + [MediaServerType.NOT_CONFIGURED]: null, + }; - setMediaServerSettingsComplete(hasEnabledLibraries); - } catch { - toasts.addToast(intl.formatMessage(messages.librarieserror), { - autoDismiss: true, - appearance: 'error', - }); + const { data: mediaServerSettings, error: mediaServerSettingsError } = + useSWR<{ libraries: Library[] }>( + currentStep === 3 ? mediaServerSettingsEndpoint[mediaServerType] : null + ); - setMediaServerSettingsComplete(false); - } - }, [intl, mediaServerType, toasts]); + const mediaServerSettingsComplete = !!mediaServerSettings?.libraries?.some( + (library) => library.enabled + ); const { data: backdrops } = useSWR('/api/v1/backdrops', { refreshInterval: 0, @@ -124,24 +108,11 @@ const Setup = () => { settings.currentSettings.mediaServerType, settings.currentSettings.initialized, router, - toasts, intl, currentStep, mediaServerType, - validateLibraries, ]); - useEffect(() => { - if (currentStep === 3) { - validateLibraries(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentStep]); - - const handleComplete = () => { - validateLibraries(); - }; - if (settings.currentSettings.initialized) return <>; return ( @@ -266,10 +237,16 @@ const Setup = () => { )} {currentStep === 3 && (
+ {!!mediaServerSettingsError && ( + + )} {mediaServerType === MediaServerType.PLEX ? ( - + ) : ( - + )}
diff --git a/src/components/Slider/index.tsx b/src/components/Slider/index.tsx index 476eeb6272..3be9c383a6 100644 --- a/src/components/Slider/index.tsx +++ b/src/components/Slider/index.tsx @@ -3,7 +3,7 @@ import globalMessages from '@app/i18n/globalMessages'; import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/24/outline'; import { useSpring } from '@react-spring/web'; import { debounce } from 'lodash'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState, type JSX } from 'react'; import { useIntl } from 'react-intl'; interface SliderProps { diff --git a/src/components/StatusBadge/index.tsx b/src/components/StatusBadge/index.tsx index faea1fb5c2..bd4bcf6b83 100644 --- a/src/components/StatusBadge/index.tsx +++ b/src/components/StatusBadge/index.tsx @@ -14,6 +14,8 @@ import { useIntl } from 'react-intl'; const messages = defineMessages('components.StatusBadge', { status: '{status}', status4k: '4K {status}', + statusAudiobook: 'Audiobook {status}', + statusEbook: 'Ebook {status}', playonplex: 'Play on {mediaServerName}', openinarr: 'Open in {arr}', managemedia: 'Manage {mediaType}', @@ -29,9 +31,12 @@ interface StatusBadgeProps { plexUrl?: string; serviceUrl?: string; tmdbId?: number; - mediaType?: 'movie' | 'tv'; + mediaType?: 'movie' | 'tv' | 'book'; title?: string | string[]; statusLabelOverride?: string; + // Books drop the format prefix when partially available, which only reads + // correctly where a sibling format is shown beside it + alwaysLabelFormat?: boolean; } const StatusBadge = ({ @@ -45,6 +50,7 @@ const StatusBadge = ({ mediaType, title, statusLabelOverride, + alwaysLabelFormat = false, }: StatusBadgeProps) => { const intl = useIntl(); const { hasPermission } = useUser(); @@ -97,13 +103,22 @@ const StatusBadge = ({ mediaLink = `/${mediaType}/${tmdbId}?manage=1`; mediaLinkDescription = intl.formatMessage(messages.managemedia, { mediaType: intl.formatMessage( - mediaType === 'movie' ? globalMessages.movie : globalMessages.tvshow + mediaType === 'movie' + ? globalMessages.movie + : mediaType === 'book' + ? globalMessages.book + : globalMessages.tvshow ), }); } else if (hasPermission(Permission.ADMIN) && serviceUrl) { mediaLink = serviceUrl; mediaLinkDescription = intl.formatMessage(messages.openinarr, { - arr: mediaType === 'movie' ? 'Radarr' : 'Sonarr', + arr: + mediaType === 'movie' + ? 'Radarr' + : mediaType === 'book' + ? 'Readarr' + : 'Sonarr', }); } } @@ -181,7 +196,13 @@ const StatusBadge = ({ > {intl.formatMessage( - is4k ? messages.status4k : messages.status, + is4k + ? mediaType === 'book' + ? messages.statusAudiobook + : messages.status4k + : mediaType === 'book' + ? messages.statusEbook + : messages.status, { status: inProgress ? intl.formatMessage(globalMessages.processing) @@ -246,7 +267,15 @@ const StatusBadge = ({ > {intl.formatMessage( - is4k ? messages.status4k : messages.status, + mediaType === 'book' + ? alwaysLabelFormat + ? is4k + ? messages.statusAudiobook + : messages.statusEbook + : messages.status + : is4k + ? messages.status4k + : messages.status, { status: inProgress ? intl.formatMessage(globalMessages.processing) @@ -311,7 +340,13 @@ const StatusBadge = ({ > {intl.formatMessage( - is4k ? messages.status4k : messages.status, + is4k + ? mediaType === 'book' + ? messages.statusAudiobook + : messages.status4k + : mediaType === 'book' + ? messages.statusEbook + : messages.status, { status: inProgress ? intl.formatMessage(globalMessages.processing) @@ -354,9 +389,18 @@ const StatusBadge = ({ return ( - {intl.formatMessage(is4k ? messages.status4k : messages.status, { - status: intl.formatMessage(globalMessages.pending), - })} + {intl.formatMessage( + is4k + ? mediaType === 'book' + ? messages.statusAudiobook + : messages.status4k + : mediaType === 'book' + ? messages.statusEbook + : messages.status, + { + status: intl.formatMessage(globalMessages.pending), + } + )} ); @@ -365,11 +409,20 @@ const StatusBadge = ({ return ( - {intl.formatMessage(is4k ? messages.status4k : messages.status, { - status: - statusLabelOverride ?? - intl.formatMessage(globalMessages.blocklisted), - })} + {intl.formatMessage( + is4k + ? mediaType === 'book' + ? messages.statusAudiobook + : messages.status4k + : mediaType === 'book' + ? messages.statusEbook + : messages.status, + { + status: + statusLabelOverride ?? + intl.formatMessage(globalMessages.blocklisted), + } + )} ); @@ -400,7 +453,13 @@ const StatusBadge = ({ > {intl.formatMessage( - is4k ? messages.status4k : messages.status, + is4k + ? mediaType === 'book' + ? messages.statusAudiobook + : messages.status4k + : mediaType === 'book' + ? messages.statusEbook + : messages.status, { status: inProgress ? intl.formatMessage(globalMessages.processing) diff --git a/src/components/StatusChecker/index.tsx b/src/components/StatusChecker/index.tsx index 9ec17c9873..5190b5603e 100644 --- a/src/components/StatusChecker/index.tsx +++ b/src/components/StatusChecker/index.tsx @@ -23,9 +23,12 @@ const StatusChecker = () => { const intl = useIntl(); const settings = useSettings(); const { hasPermission } = useUser(); - const { data, error } = useSWR('/api/v1/status', { - refreshInterval: 60 * 1000, - }); + const { data, error } = useSWR( + '/api/v1/status?checkUpdateAvailable=false', + { + refreshInterval: 60 * 1000, + } + ); const [alertDismissed, setAlertDismissed] = useState(false); useEffect(() => { diff --git a/src/components/TitleCard/ErrorCard.tsx b/src/components/TitleCard/ErrorCard.tsx index 93356c81be..06e449584b 100644 --- a/src/components/TitleCard/ErrorCard.tsx +++ b/src/components/TitleCard/ErrorCard.tsx @@ -1,4 +1,5 @@ import Button from '@app/components/Common/Button'; +import useToasts from '@app/hooks/useToasts'; import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; import { CheckIcon, TrashIcon } from '@heroicons/react/24/solid'; @@ -10,7 +11,7 @@ interface ErrorCardProps { id: number; tmdbId: number; tvdbId?: number; - type: 'movie' | 'tv'; + type: 'movie' | 'tv' | 'book'; canExpand?: boolean; } @@ -18,14 +19,34 @@ const messages = defineMessages('components.TitleCard', { mediaerror: '{mediaType} Not Found', tmdbid: 'TMDB ID', tvdbid: 'TheTVDB ID', + hcid: 'Hardcover ID', cleardata: 'Clear Data', }); const ErrorCard = ({ id, tmdbId, tvdbId, type, canExpand }: ErrorCardProps) => { const intl = useIntl(); + const { addToast } = useToasts(); const deleteMedia = async () => { - await axios.delete(`/api/v1/media/${id}`); + try { + await axios.delete(`/api/v1/watchlist/${tmdbId}?mediaType=${type}`); + } catch (e) { + if (!axios.isAxiosError(e) || e.response?.status !== 404) { + addToast(intl.formatMessage(globalMessages.error), { + appearance: 'error', + autoDismiss: true, + }); + return; + } + } + await axios.delete(`/api/v1/media/${id}`).catch((e) => { + if (axios.isAxiosError(e) && e.response?.status === 404) return; + addToast(intl.formatMessage(globalMessages.error), { + appearance: 'error', + autoDismiss: true, + }); + }); + mutate('/api/v1/discover/watchlist'); mutate('/api/v1/media?filter=allavailable&take=20&sort=mediaAdded'); mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0'); }; @@ -45,13 +66,19 @@ const ErrorCard = ({ id, tmdbId, tvdbId, type, canExpand }: ErrorCardProps) => {
{type === 'movie' ? intl.formatMessage(globalMessages.movie) - : intl.formatMessage(globalMessages.tvshow)} + : type === 'book' + ? intl.formatMessage(globalMessages.book) + : intl.formatMessage(globalMessages.tvshow)}
@@ -78,7 +105,9 @@ const ErrorCard = ({ id, tmdbId, tvdbId, type, canExpand }: ErrorCardProps) => { mediaType: intl.formatMessage( type === 'movie' ? globalMessages.movie - : globalMessages.tvshow + : type === 'book' + ? globalMessages.book + : globalMessages.tvshow ), })} @@ -94,7 +123,9 @@ const ErrorCard = ({ id, tmdbId, tvdbId, type, canExpand }: ErrorCardProps) => { >
- {intl.formatMessage(messages.tmdbid)} + {intl.formatMessage( + type === 'book' ? messages.hcid : messages.tmdbid + )} {tmdbId}
diff --git a/src/components/TitleCard/TmdbTitleCard.tsx b/src/components/TitleCard/TmdbTitleCard.tsx index 825e52ccf0..bff7b2ccc9 100644 --- a/src/components/TitleCard/TmdbTitleCard.tsx +++ b/src/components/TitleCard/TmdbTitleCard.tsx @@ -1,7 +1,9 @@ import TitleCard from '@app/components/TitleCard'; import { Permission, useUser } from '@app/hooks/useUser'; +import type { BookDetails } from '@server/models/Book'; import type { MovieDetails } from '@server/models/Movie'; import type { TvDetails } from '@server/models/Tv'; +import { useEffect, useState } from 'react'; import { useInView } from 'react-intersection-observer'; import useSWR from 'swr'; @@ -9,16 +11,24 @@ export interface TmdbTitleCardProps { id: number; tmdbId: number; tvdbId?: number; - type: 'movie' | 'tv'; + type: 'movie' | 'tv' | 'book'; canExpand?: boolean; isAddedToWatchlist?: boolean; mutateParent?: () => void; } -const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => { +const isMovie = ( + movie: MovieDetails | TvDetails | BookDetails +): movie is MovieDetails => { return (movie as MovieDetails).title !== undefined; }; +const isBook = ( + book: MovieDetails | TvDetails | BookDetails +): book is BookDetails => { + return (book as BookDetails).author !== undefined; +}; + const TmdbTitleCard = ({ id, tmdbId, @@ -34,12 +44,26 @@ const TmdbTitleCard = ({ triggerOnce: true, }); const url = - type === 'movie' ? `/api/v1/movie/${tmdbId}` : `/api/v1/tv/${tmdbId}`; - const { data: title, error } = useSWR( + type === 'movie' + ? `/api/v1/movie/${tmdbId}` + : type === 'book' + ? `/api/v1/book/${tmdbId}` + : `/api/v1/tv/${tmdbId}`; + const { data: title, error } = useSWR( inView ? `${url}` : null ); - if (!title && !error) { + const [showError, setShowError] = useState(false); + useEffect(() => { + if (!error || title) { + setShowError(false); + return; + } + const timer = setTimeout(() => setShowError(true), 20000); + return () => clearTimeout(timer); + }, [error, title]); + + if (!title && (!error || !showError)) { return (
@@ -58,7 +82,22 @@ const TmdbTitleCard = ({ ) : null; } - return isMovie(title) ? ( + return isBook(title) ? ( + + ) : isMovie(title) ? ( void; } @@ -62,17 +67,22 @@ const TitleCard = ({ year, title, status, + status4k, + mediaRequests, mediaType, isAddedToWatchlist = false, inProgress = false, + position, canExpand = false, mutateParent, }: TitleCardProps) => { const isTouch = useIsTouch(); const intl = useIntl(); + const settings = useSettings(); const { user, hasPermission } = useUser(); const [isUpdating, setIsUpdating] = useState(false); const [currentStatus, setCurrentStatus] = useState(status); + const [currentStatus4k, setCurrentStatus4k] = useState(status4k); const [showDetail, setShowDetail] = useState(false); const [showRequestModal, setShowRequestModal] = useState(false); const { addToast } = useToasts(); @@ -90,10 +100,20 @@ const TitleCard = ({ setCurrentStatus(status); }, [status]); - const requestComplete = useCallback((newStatus: MediaStatus) => { - setCurrentStatus(newStatus); - setShowRequestModal(false); - }, []); + useEffect(() => { + setCurrentStatus4k(status4k); + }, [status4k]); + + const requestComplete = useCallback( + (newStatus: MediaStatus, newStatus4k?: MediaStatus) => { + setCurrentStatus(newStatus); + if (newStatus4k !== undefined) { + setCurrentStatus4k(newStatus4k); + } + setShowRequestModal(false); + }, + [] + ); const requestUpdating = useCallback( (status: boolean) => setIsUpdating(status), @@ -300,20 +320,88 @@ const TitleCard = ({ const closeModal = useCallback(() => setShowRequestModal(false), []); - const showRequestButton = hasPermission( - [ - Permission.REQUEST, - mediaType === 'movie' || mediaType === 'collection' - ? Permission.REQUEST_MOVIE - : Permission.REQUEST_TV, - ], - { type: 'or' } - ); + const canRequestFormat = (is4k: boolean) => + is4k + ? settings.currentSettings.bookAudioEnabled && + hasPermission([Permission.REQUEST_4K, Permission.REQUEST_AUDIO_BOOK], { + type: 'or', + }) + : hasPermission([Permission.REQUEST, Permission.REQUEST_BOOK], { + type: 'or', + }); + + const isRequestableStatus = (mediaStatus?: MediaStatus) => + !mediaStatus || + mediaStatus === MediaStatus.UNKNOWN || + mediaStatus === MediaStatus.DELETED; + + const showRequestButton = + mediaType === 'book' + ? canRequestFormat(false) || canRequestFormat(true) + : hasPermission( + [ + Permission.REQUEST, + mediaType === 'movie' || mediaType === 'collection' + ? Permission.REQUEST_MOVIE + : Permission.REQUEST_TV, + ], + { type: 'or' } + ); + + const canRequestNow = + mediaType === 'book' + ? (canRequestFormat(false) && isRequestableStatus(currentStatus)) || + (canRequestFormat(true) && isRequestableStatus(currentStatus4k)) + : isRequestableStatus(currentStatus); const showHideButton = hasPermission([Permission.MANAGE_BLOCKLIST], { type: 'or', }); + const showAudiobookStatus = + mediaType === 'book' && + settings.currentSettings.bookAudioEnabled && + hasPermission( + [ + Permission.MANAGE_REQUESTS, + Permission.REQUEST_4K, + Permission.REQUEST_AUDIO_BOOK, + ], + { type: 'or' } + ); + + const hasActiveRequest = (is4k: boolean) => + !!mediaRequests?.some( + (request) => + request.is4k === is4k && + request.status !== MediaRequestStatus.DECLINED && + request.status !== MediaRequestStatus.FAILED + ); + + const isAvailableStatus = (mediaStatus?: MediaStatus) => + mediaStatus === MediaStatus.AVAILABLE || + mediaStatus === MediaStatus.PARTIALLY_AVAILABLE; + + const ebookMissing = + mediaType === 'book' && + currentStatus === MediaStatus.PROCESSING && + !hasActiveRequest(false); + const audiobookMissing = + currentStatus4k === MediaStatus.PROCESSING && !hasActiveRequest(true); + + const ebookDisplayStatus = + mediaType === 'book' && + showAudiobookStatus && + currentStatus === MediaStatus.AVAILABLE && + !isAvailableStatus(currentStatus4k) + ? MediaStatus.PARTIALLY_AVAILABLE + : currentStatus; + const audiobookDisplayStatus = + currentStatus4k === MediaStatus.AVAILABLE && + !isAvailableStatus(currentStatus) + ? MediaStatus.PARTIALLY_AVAILABLE + : currentStatus4k; + return (
+ {mediaType === 'book' && position && ( + + + {`#${position}`} + + + )}
@@ -398,7 +514,9 @@ const TitleCard = ({ ? intl.formatMessage(globalMessages.movie) : mediaType === 'collection' ? intl.formatMessage(globalMessages.collection) - : intl.formatMessage(globalMessages.tvshow)} + : mediaType === 'book' + ? intl.formatMessage(globalMessages.book) + : intl.formatMessage(globalMessages.tvshow)}
{showDetail && currentStatus !== MediaStatus.BLOCKLISTED && ( @@ -456,15 +574,35 @@ const TitleCard = ({ )} - {currentStatus && currentStatus !== MediaStatus.UNKNOWN && ( + {((currentStatus && currentStatus !== MediaStatus.UNKNOWN) || + (showAudiobookStatus && + currentStatus4k && + currentStatus4k !== MediaStatus.UNKNOWN)) && (
-
- -
+ {ebookDisplayStatus && + ebookDisplayStatus !== MediaStatus.UNKNOWN && ( +
+ +
+ )} + {showAudiobookStatus && + audiobookDisplayStatus && + audiobookDisplayStatus !== MediaStatus.UNKNOWN && ( +
+ +
+ )}
)}
@@ -500,7 +638,9 @@ const TitleCard = ({ ? `/movie/${id}` : mediaType === 'collection' ? `/collection/${id}` - : `/tv/${id}` + : mediaType === 'book' + ? `/book/${id}` + : `/tv/${id}` } className="absolute inset-0 h-full w-full cursor-pointer overflow-hidden text-left" style={{ @@ -511,12 +651,7 @@ const TitleCard = ({
{year &&
{year}
} @@ -538,12 +673,7 @@ const TitleCard = ({ className="whitespace-normal text-xs" style={{ WebkitLineClamp: - !showRequestButton || - (currentStatus && - currentStatus !== MediaStatus.UNKNOWN && - currentStatus !== MediaStatus.DELETED) - ? 5 - : 3, + !showRequestButton || !canRequestNow ? 5 : 3, display: '-webkit-box', overflow: 'hidden', WebkitBoxOrient: 'vertical', @@ -558,9 +688,8 @@ const TitleCard = ({
{showRequestButton && - (!currentStatus || - currentStatus === MediaStatus.UNKNOWN || - currentStatus === MediaStatus.DELETED) && ( + (showDetail || (!position && !image)) && + canRequestNow && (
@@ -848,7 +866,7 @@ const TvDetails = ({ tv }: TvDetailsProps) => { {({ open }) => ( <> - { open ? 'rotate-180' : '' } h-6 w-6 text-gray-500`} /> - + { // Not sure why this transition is adding a margin without this here style={{ margin: '0px' }} > - + - + )} @@ -1271,12 +1290,12 @@ const TvDetails = ({ tv }: TvDetailsProps) => { )) .reduce((prev, curr) => ( - <> + {intl.formatMessage(globalMessages.delimitedlist, { a: prev, b: curr, })} - + ))}
@@ -1287,7 +1306,7 @@ const TvDetails = ({ tv }: TvDetailsProps) => { {streamingProviders.map((p) => { return ( - + void; } +interface PlexImportResponse { + createdUsers: unknown[]; + refreshedUsers: number; +} + const messages = defineMessages('components.UserList', { importfromplex: 'Import Plex Users', importfromplexerror: 'Something went wrong while importing Plex users.', + importfromplexnochanges: + 'Plex import completed. No users were created or refreshed.', + importfromplexsynced: + 'Plex users synced successfully. Existing users were refreshed.', + syncnotice: + 'You can still click Sync to refresh existing Plex users, such as updated email or username details.', importedfromplex: '{userCount} Plex {userCount, plural, one {user} other {users}} imported successfully!', importedPlexUsersNoPassword: @@ -50,35 +61,57 @@ const PlexImportModal = ({ onCancel, onComplete }: PlexImportProps) => { setImporting(true); try { - const { data: createdUsers } = await axios.post( + const { data: importResponse } = await axios.post( '/api/v1/user/import-from-plex', { plexIds: selectedUsers } ); - if (!Array.isArray(createdUsers) || createdUsers.length === 0) { - throw new Error('No users were imported from Plex.'); - } + const { createdUsers, refreshedUsers } = importResponse; - addToast( - intl.formatMessage(messages.importedfromplex, { - userCount: createdUsers.length, - strong: (msg: React.ReactNode) => {msg}, - }), - { - autoDismiss: true, - appearance: 'success', + if (isSyncOnly) { + if (refreshedUsers > 0) { + addToast(intl.formatMessage(messages.importfromplexsynced), { + autoDismiss: true, + appearance: 'success', + }); + } else { + addToast(intl.formatMessage(messages.importfromplexnochanges), { + autoDismiss: true, + appearance: 'info', + }); } - ); + } else if (createdUsers.length > 0) { + addToast( + intl.formatMessage(messages.importedfromplex, { + userCount: createdUsers.length, + strong: (msg: React.ReactNode) => {msg}, + }), + { + autoDismiss: true, + appearance: 'success', + } + ); - addToast( - intl.formatMessage(messages.importedPlexUsersNoPassword, { - applicationTitle: settings.currentSettings.applicationTitle, - }), - { - autoDismiss: false, - appearance: 'warning', - } - ); + addToast( + intl.formatMessage(messages.importedPlexUsersNoPassword, { + applicationTitle: settings.currentSettings.applicationTitle, + }), + { + autoDismiss: false, + appearance: 'warning', + } + ); + } else if (refreshedUsers > 0) { + addToast(intl.formatMessage(messages.importfromplexsynced), { + autoDismiss: true, + appearance: 'success', + }); + } else { + addToast(intl.formatMessage(messages.importfromplexnochanges), { + autoDismiss: true, + appearance: 'info', + }); + } if (onComplete) { onComplete(); @@ -114,6 +147,8 @@ const PlexImportModal = ({ onCancel, onComplete }: PlexImportProps) => { } }; + const isSyncOnly = !!data && data.length === 0; + return ( { onOk={() => { importUsers(); }} - okDisabled={isImporting || !selectedUsers.length} + okDisabled={ + isImporting || !data || (data.length > 0 && !selectedUsers.length) + } okText={intl.formatMessage( - isImporting ? globalMessages.importing : globalMessages.import + isImporting + ? isSyncOnly + ? globalMessages.syncing + : globalMessages.importing + : isSyncOnly + ? globalMessages.sync + : globalMessages.import )} onCancel={onCancel} > @@ -245,10 +288,9 @@ const PlexImportModal = ({ onCancel, onComplete }: PlexImportProps) => {
) : ( - + + {intl.formatMessage(messages.syncnotice)} + )} ); diff --git a/src/components/UserList/index.tsx b/src/components/UserList/index.tsx index c36d7d692a..0a20d4276d 100644 --- a/src/components/UserList/index.tsx +++ b/src/components/UserList/index.tsx @@ -10,6 +10,7 @@ import SensitiveInput from '@app/components/Common/SensitiveInput'; import Table from '@app/components/Common/Table'; import BulkEditModal from '@app/components/UserList/BulkEditModal'; import PlexImportModal from '@app/components/UserList/PlexImportModal'; +import useDebouncedState from '@app/hooks/useDebouncedState'; import useSettings from '@app/hooks/useSettings'; import useToasts from '@app/hooks/useToasts'; import { useUpdateQueryParams } from '@app/hooks/useUpdateQueryParams'; @@ -26,6 +27,7 @@ import { ChevronRightIcon, ChevronUpIcon, InboxArrowDownIcon, + MagnifyingGlassIcon, PencilIcon, UserPlusIcon, } from '@heroicons/react/24/solid'; @@ -36,7 +38,7 @@ import axios from 'axios'; import { Field, Form, Formik } from 'formik'; import Link from 'next/link'; import { useRouter } from 'next/router'; -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { useIntl } from 'react-intl'; import useSWR from 'swr'; import validator from 'validator'; @@ -91,6 +93,7 @@ const messages = defineMessages('components.UserList', { toggleSortDirectionAria: 'Toggle sort direction', ascending: 'ascending', descending: 'descending', + searchUsers: 'Search…', localLoginDisabled: 'The Enable Local Sign-In setting is currently disabled.', }); @@ -112,10 +115,17 @@ const UserList = () => { const { user: currentUser, hasPermission: currentHasPermission } = useUser(); const [currentSort, setCurrentSort] = useState('created'); const [currentPageSize, setCurrentPageSize] = useState(10); + const [searchInput, searchQuery, setSearchInput] = useDebouncedState( + '', + 300 + ); const page = router.query.page ? Number(router.query.page) : 1; const pageIndex = page - 1; const updateQueryParams = useUpdateQueryParams({ page: page.toString() }); + const previousSearchQueryRef = useRef(undefined); + const searchInputRef = useRef(null); + const isSearchFocusedRef = useRef(false); const defaultSortDirection = (sortKey: Sort): SortDirection => sortKey === 'requests' || sortKey === 'updated' ? 'desc' : 'asc'; @@ -131,7 +141,10 @@ const UserList = () => { } = useSWR( `/api/v1/user?take=${currentPageSize}&skip=${ pageIndex * currentPageSize - }&sort=${currentSort}&sortDirection=${sortDirection}` + }&sort=${currentSort}&sortDirection=${sortDirection}${ + searchQuery ? `&q=${encodeURIComponent(searchQuery)}` : '' + }`, + { keepPreviousData: true } ); const handleSortChange = (sortKey: Sort) => { @@ -249,6 +262,23 @@ const UserList = () => { ); }; + useEffect(() => { + if ( + previousSearchQueryRef.current !== undefined && + previousSearchQueryRef.current !== searchQuery && + page > 1 + ) { + updateQueryParams('page', '1'); + } + previousSearchQueryRef.current = searchQuery; + }, [searchQuery, page, updateQueryParams]); + + useEffect(() => { + if (isSearchFocusedRef.current) { + searchInputRef.current?.focus(); + } + }, [data]); + const isUserPermsEditable = (userId: number) => userId !== 1 && userId !== currentUser?.id; const isAllUsersSelected = () => { @@ -648,49 +678,74 @@ const UserList = () => {
- -
- - +
+
+ + + + setSearchInput(e.target.value)} + onFocus={() => { + isSearchFocusedRef.current = true; + }} + onBlur={() => { + isSearchFocusedRef.current = false; + }} + aria-label={intl.formatMessage(messages.searchUsers)} + /> +
+
+ + +
@@ -750,12 +805,12 @@ const UserList = () => { > {intl.formatMessage(messages.created)} - + {(data.results ?? []).length > 1 && (
- + +
+ + +
))} diff --git a/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx b/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx index 525dabb07d..2f15f278ed 100644 --- a/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx +++ b/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx @@ -58,15 +58,13 @@ const messages = defineMessages( streamingRegionTip: 'Show streaming sites by regional availability', movierequestlimit: 'Movie Request Limit', seriesrequestlimit: 'Series Request Limit', + bookrequestlimit: 'Book Request Limit', + booksrequestlimit: 'Books Request Limit', enableOverride: 'Override Global Limit', applanguage: 'Display Language', languageDefault: 'Default ({language})', - discordId: 'Discord User ID', - discordIdTip: - 'The multi-digit ID number associated with your Discord user account', validationemailrequired: 'Email required', validationemailformat: 'Valid email required', - validationDiscordId: 'You must provide a valid Discord user ID', plexwatchlistsyncmovies: 'Auto-Request Movies', plexwatchlistsyncmoviestip: 'Automatically request movies on your Plex Watchlist', @@ -82,6 +80,7 @@ const UserGeneralSettings = () => { const { locale, setLocale } = useLocale(); const [movieQuotaEnabled, setMovieQuotaEnabled] = useState(false); const [tvQuotaEnabled, setTvQuotaEnabled] = useState(false); + const [bookQuotaEnabled, setBookQuotaEnabled] = useState(false); const router = useRouter(); const { user, @@ -119,9 +118,6 @@ const UserGeneralSettings = () => { (value) => !value || validator.isEmail(value, { require_tld: false }) ), - discordId: Yup.string() - .nullable() - .matches(/^\d{17,19}$/, intl.formatMessage(messages.validationDiscordId)), }); useEffect(() => { @@ -131,6 +127,9 @@ const UserGeneralSettings = () => { setTvQuotaEnabled( data?.tvQuotaLimit != undefined && data?.tvQuotaDays != undefined ); + setBookQuotaEnabled( + data?.bookQuotaLimit != undefined && data?.bookQuotaDays != undefined + ); }, [data]); if (!data && !error) { @@ -158,7 +157,6 @@ const UserGeneralSettings = () => { initialValues={{ displayName: data?.username !== user?.email ? data?.username : '', email: data?.email?.includes('@') ? data.email : '', - discordId: data?.discordId ?? '', locale: data?.locale, discoverRegion: data?.discoverRegion, streamingRegion: data?.streamingRegion, @@ -167,6 +165,8 @@ const UserGeneralSettings = () => { movieQuotaDays: data?.movieQuotaDays, tvQuotaLimit: data?.tvQuotaLimit, tvQuotaDays: data?.tvQuotaDays, + bookQuotaLimit: data?.bookQuotaLimit, + bookQuotaDays: data?.bookQuotaDays, watchlistSyncMovies: data?.watchlistSyncMovies, watchlistSyncTv: data?.watchlistSyncTv, }} @@ -178,7 +178,6 @@ const UserGeneralSettings = () => { username: values.displayName, email: values.email || user?.jellyfinUsername || user?.plexUsername, - discordId: values.discordId, locale: values.locale, discoverRegion: values.discoverRegion, streamingRegion: values.streamingRegion, @@ -189,6 +188,8 @@ const UserGeneralSettings = () => { movieQuotaDays: movieQuotaEnabled ? values.movieQuotaDays : null, tvQuotaLimit: tvQuotaEnabled ? values.tvQuotaLimit : null, tvQuotaDays: tvQuotaEnabled ? values.tvQuotaDays : null, + bookQuotaLimit: bookQuotaEnabled ? values.bookQuotaLimit : null, + bookQuotaDays: bookQuotaEnabled ? values.bookQuotaDays : null, watchlistSyncMovies: values.watchlistSyncMovies, watchlistSyncTv: values.watchlistSyncTv, }); @@ -341,36 +342,6 @@ const UserGeneralSettings = () => { )}
-
- - {msg} - - ), - })} - - )} - -
-
- -
- {errors.discordId && - touched.discordId && - typeof errors.discordId === 'string' && ( -
{errors.discordId}
- )} -
-
+
+ +
+
+
+ setBookQuotaEnabled((s) => !s)} + /> + + {intl.formatMessage(messages.enableOverride)} + +
+ +
+
+
)} {hasPermission( diff --git a/src/components/UserProfile/UserSettings/UserLinkedAccountsSettings/LinkJellyfinModal.tsx b/src/components/UserProfile/UserSettings/UserLinkedAccountsSettings/LinkJellyfinModal.tsx index b06bbcfdc5..1d530d9084 100644 --- a/src/components/UserProfile/UserSettings/UserLinkedAccountsSettings/LinkJellyfinModal.tsx +++ b/src/components/UserProfile/UserSettings/UserLinkedAccountsSettings/LinkJellyfinModal.tsx @@ -1,9 +1,11 @@ import Alert from '@app/components/Common/Alert'; +import Button from '@app/components/Common/Button'; import Modal from '@app/components/Common/Modal'; import useSettings from '@app/hooks/useSettings'; import { useUser } from '@app/hooks/useUser'; import defineMessages from '@app/utils/defineMessages'; import { Transition } from '@headlessui/react'; +import { QrCodeIcon } from '@heroicons/react/24/outline'; import { MediaServerType } from '@server/constants/server'; import axios from 'axios'; import { Field, Form, Formik } from 'formik'; @@ -27,6 +29,7 @@ const messages = defineMessages( 'Unable to connect to {mediaServerName} using your credentials', errorExists: 'This account is already linked to a {applicationName} user', errorUnknown: 'An unknown error occurred', + quickConnect: 'Use Quick Connect', } ); @@ -34,13 +37,15 @@ interface LinkJellyfinModalProps { show: boolean; onClose: () => void; onSave: () => void; + onSwitchToQuickConnect: () => void; } -const LinkJellyfinModal: React.FC = ({ +const LinkJellyfinModal = ({ show, onClose, onSave, -}) => { + onSwitchToQuickConnect, +}: LinkJellyfinModalProps) => { const intl = useIntl(); const settings = useSettings(); const { user } = useUser(); @@ -63,11 +68,12 @@ const LinkJellyfinModal: React.FC = ({ return ( = ({
{errors.password}
)}
+ {settings.currentSettings.mediaServerType === + MediaServerType.JELLYFIN && ( +
+ +
+ )} ); diff --git a/src/components/UserProfile/UserSettings/UserLinkedAccountsSettings/LinkJellyfinQuickConnectModal.tsx b/src/components/UserProfile/UserSettings/UserLinkedAccountsSettings/LinkJellyfinQuickConnectModal.tsx new file mode 100644 index 0000000000..c253b21d65 --- /dev/null +++ b/src/components/UserProfile/UserSettings/UserLinkedAccountsSettings/LinkJellyfinQuickConnectModal.tsx @@ -0,0 +1,78 @@ +import QuickConnectModal from '@app/components/Common/QuickConnectModal'; +import useSettings from '@app/hooks/useSettings'; +import { useUser } from '@app/hooks/useUser'; +import defineMessages from '@app/utils/defineMessages'; +import { MediaServerType } from '@server/constants/server'; +import axios from 'axios'; +import { useCallback } from 'react'; +import { useIntl } from 'react-intl'; + +const messages = defineMessages( + 'components.UserProfile.UserSettings.LinkJellyfinQuickConnectModal', + { + title: 'Link {mediaServerName} Account', + subtitle: 'Quick Connect', + instructions: 'Enter this code in your {mediaServerName} app', + usePassword: 'Use Password Instead', + } +); + +interface LinkJellyfinQuickConnectModalProps { + show: boolean; + onClose: () => void; + onSave: () => void; + onSwitchToPassword: () => void; +} + +const LinkJellyfinQuickConnectModal = ({ + show, + onClose, + onSave, + onSwitchToPassword, +}: LinkJellyfinQuickConnectModalProps) => { + const intl = useIntl(); + const settings = useSettings(); + const { user } = useUser(); + + const mediaServerName = + settings.currentSettings.mediaServerType === MediaServerType.JELLYFIN + ? 'Jellyfin' + : 'Emby'; + + const authenticate = useCallback( + async (secret: string) => { + await axios.post( + `/api/v1/user/${user?.id}/settings/linked-accounts/jellyfin/quickconnect`, + { secret } + ); + }, + [user] + ); + + const handleCancel = () => { + onClose(); + onSwitchToPassword(); + }; + + return ( + { + onSave(); + onClose(); + }} + authenticate={authenticate} + /> + ); +}; + +export default LinkJellyfinQuickConnectModal; diff --git a/src/components/UserProfile/UserSettings/UserLinkedAccountsSettings/index.tsx b/src/components/UserProfile/UserSettings/UserLinkedAccountsSettings/index.tsx index 9940828299..d4b5365d50 100644 --- a/src/components/UserProfile/UserSettings/UserLinkedAccountsSettings/index.tsx +++ b/src/components/UserProfile/UserSettings/UserLinkedAccountsSettings/index.tsx @@ -5,6 +5,7 @@ import Alert from '@app/components/Common/Alert'; import ConfirmButton from '@app/components/Common/ConfirmButton'; import Dropdown from '@app/components/Common/Dropdown'; import PageTitle from '@app/components/Common/PageTitle'; +import LinkJellyfinQuickConnectModal from '@app/components/UserProfile/UserSettings/UserLinkedAccountsSettings/LinkJellyfinQuickConnectModal'; import useSettings from '@app/hooks/useSettings'; import { Permission, UserType, useUser } from '@app/hooks/useUser'; import globalMessages from '@app/i18n/globalMessages'; @@ -63,6 +64,8 @@ const UserLinkedAccountsSettings = () => { user ? `/api/v1/user/${user?.id}/settings/password` : null ); const [showJellyfinModal, setShowJellyfinModal] = useState(false); + const [showJellyfinQuickConnectModal, setShowJellyfinQuickConnectModal] = + useState(false); const [error, setError] = useState(null); const applicationName = settings.currentSettings.applicationTitle; @@ -267,6 +270,23 @@ const UserLinkedAccountsSettings = () => { setShowJellyfinModal(false); revalidateUser(); }} + onSwitchToQuickConnect={() => { + setShowJellyfinModal(false); + setShowJellyfinQuickConnectModal(true); + }} + /> + + setShowJellyfinQuickConnectModal(false)} + onSave={() => { + setShowJellyfinQuickConnectModal(false); + revalidateUser(); + }} + onSwitchToPassword={() => { + setShowJellyfinQuickConnectModal(false); + setShowJellyfinModal(true); + }} /> ); diff --git a/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsDiscord.tsx b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsDiscord.tsx index d58c054e4b..5da939f8d3 100644 --- a/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsDiscord.tsx +++ b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsDiscord.tsx @@ -1,3 +1,4 @@ +import Alert from '@app/components/Common/Alert'; import Button from '@app/components/Common/Button'; import LoadingSpinner from '@app/components/Common/LoadingSpinner'; import NotificationTypeSelector from '@app/components/NotificationTypeSelector'; @@ -5,7 +6,12 @@ import useToasts from '@app/hooks/useToasts'; import { useUser } from '@app/hooks/useUser'; import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; -import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline'; +import { + ArrowDownOnSquareIcon, + PlusIcon, + TrashIcon, +} from '@heroicons/react/24/outline'; +import { DISCORD_SNOWFLAKE_REGEX } from '@server/constants/discord'; import type { UserSettingsNotificationsResponse } from '@server/interfaces/api/userSettingsInterfaces'; import axios from 'axios'; import { Field, Form, Formik } from 'formik'; @@ -17,12 +23,17 @@ import * as Yup from 'yup'; const messages = defineMessages( 'components.UserProfile.UserSettings.UserNotificationSettings', { + discordNotificationsNotEnabled: + 'The server owner has not enabled Discord notifications. This information will only be used if the server owner configures an external service.', discordsettingssaved: 'Discord notification settings saved successfully!', discordsettingsfailed: 'Discord notification settings failed to save.', - discordId: 'User ID', + discordId: 'User IDs', discordIdTip: - 'The multi-digit ID number associated with your user account', - validationDiscordId: 'You must provide a valid user ID', + 'The multi-digit ID number associated with your user account. For multiple household accounts you can add more than one Discord user ID.', + discordIdPlaceholder: 'Discord User ID', + discordIdAdd: 'Add User ID', + discordIdRemove: 'Remove', + validationDiscordId: 'Each ID must be a valid Discord user ID', } ); @@ -41,16 +52,20 @@ const UserNotificationsDiscord = () => { ); const UserNotificationsDiscordSchema = Yup.object().shape({ - discordId: Yup.string() + discordIds: Yup.array() + .of( + Yup.string().matches(DISCORD_SNOWFLAKE_REGEX, { + message: intl.formatMessage(messages.validationDiscordId), + excludeEmptyString: true, + }) + ) .when('types', { is: (types: number) => !!types, then: (schema) => schema - .nullable() - .required(intl.formatMessage(messages.validationDiscordId)), - otherwise: (schema) => schema.nullable(), - }) - .matches(/^\d{17,19}$/, intl.formatMessage(messages.validationDiscordId)), + .compact((value) => value === '') + .min(1, intl.formatMessage(messages.validationDiscordId)), + }), }); if (!data && !error) { @@ -60,7 +75,7 @@ const UserNotificationsDiscord = () => { return ( { try { await axios.post(`/api/v1/user/${user?.id}/settings/notifications`, { pgpKey: data?.pgpKey, - discordId: values.discordId, + discordIds: values.discordIds, pushbulletAccessToken: data?.pushbulletAccessToken, pushoverApplicationToken: data?.pushoverApplicationToken, pushoverUserKey: data?.pushoverUserKey, @@ -106,8 +121,16 @@ const UserNotificationsDiscord = () => { }) => { return (
+ {!(data?.discordEnabledTypes ?? 0) && ( + + )}
-
+
+
+ {quota.book.limit + ? intl.formatMessage(messages.pastdays, { + type: intl.formatMessage(messages.bookrequests), + days: quota?.book.days, + }) + : intl.formatMessage(messages.bookrequests)} +
+
+ {quota.book.limit ? ( + <> + +
+ {intl.formatMessage(messages.requestsperdays, { + limit: ( + + {intl.formatMessage(messages.limit, { + remaining: quota.book.remaining, + limit: quota.book.limit, + })} + + ), + })} +
+ + ) : ( + + {intl.formatMessage(messages.unlimited)} + + )} +
+
)} diff --git a/src/context/SettingsContext.tsx b/src/context/SettingsContext.tsx index f2ed116756..a1103780fa 100644 --- a/src/context/SettingsContext.tsx +++ b/src/context/SettingsContext.tsx @@ -14,16 +14,20 @@ const defaultSettings = { applicationUrl: '', hideAvailable: false, hideBlocklisted: false, + hideRequested: false, localLogin: true, mediaServerLogin: true, movie4kEnabled: false, series4kEnabled: false, + bookAudioEnabled: false, discoverRegion: '', streamingRegion: '', originalLanguage: '', mediaServerType: MediaServerType.NOT_CONFIGURED, partialRequestsEnabled: true, enableSpecialEpisodes: false, + syncBookFormatRequests: false, + seriesRequestsEnabled: true, cacheImages: false, vapidPublic: '', enablePushRegistration: false, @@ -31,6 +35,7 @@ const defaultSettings = { emailEnabled: false, newPlexLogin: true, youtubeUrl: '', + versionCheck: true, plexClientIdentifier: '', }; diff --git a/src/hooks/useClickOutside.ts b/src/hooks/useClickOutside.ts index 0c762593ab..0df196a605 100644 --- a/src/hooks/useClickOutside.ts +++ b/src/hooks/useClickOutside.ts @@ -10,7 +10,7 @@ import { useEffect } from 'react'; * @param callback Callback triggered when clicking outside of ref element */ const useClickOutside = ( - ref: React.RefObject, + ref: React.RefObject, callback: (e: MouseEvent) => void ): void => { useEffect(() => { diff --git a/src/hooks/useDiscover.ts b/src/hooks/useDiscover.ts index 940cd6f5d4..53a169ac18 100644 --- a/src/hooks/useDiscover.ts +++ b/src/hooks/useDiscover.ts @@ -19,6 +19,7 @@ interface BaseMedia { mediaType: string; mediaInfo?: { status: MediaStatus; + hasActiveRequest?: boolean; }; } @@ -58,7 +59,7 @@ const useDiscover = < >( endpoint: string, options?: O, - { hideAvailable = true, hideBlocklisted = true } = {} + { hideAvailable = true, hideBlocklisted = true, hideRequested = true } = {} ): DiscoverResult => { const settings = useSettings(); const { hasPermission } = useUser(); @@ -124,9 +125,13 @@ const useDiscover = < if (settings.currentSettings.hideAvailable && hideAvailable) { titles = titles.filter( (i) => - (i.mediaType === 'movie' || i.mediaType === 'tv') && - i.mediaInfo?.status !== MediaStatus.AVAILABLE && - i.mediaInfo?.status !== MediaStatus.PARTIALLY_AVAILABLE + !( + i.mediaType === 'movie' || + i.mediaType === 'tv' || + i.mediaType === 'book' + ) || + (i.mediaInfo?.status !== MediaStatus.AVAILABLE && + i.mediaInfo?.status !== MediaStatus.PARTIALLY_AVAILABLE) ); } @@ -137,11 +142,24 @@ const useDiscover = < ) { titles = titles.filter( (i) => - (i.mediaType === 'movie' || i.mediaType === 'tv') && - i.mediaInfo?.status !== MediaStatus.BLOCKLISTED + !( + i.mediaType === 'movie' || + i.mediaType === 'tv' || + i.mediaType === 'book' + ) || i.mediaInfo?.status !== MediaStatus.BLOCKLISTED ); } + if (settings.currentSettings.hideRequested && hideRequested) { + titles = titles.filter((i) => { + if (i.mediaType !== 'movie' && i.mediaType !== 'tv') { + return true; + } + + return !i.mediaInfo?.hasActiveRequest; + }); + } + const isEmpty = !isLoadingInitialData && titles?.length === 0; const isReachingEnd = isEmpty || diff --git a/src/hooks/useQuickConnect.ts b/src/hooks/useQuickConnect.ts new file mode 100644 index 0000000000..c9494ee7f9 --- /dev/null +++ b/src/hooks/useQuickConnect.ts @@ -0,0 +1,187 @@ +import defineMessages from '@app/utils/defineMessages'; +import axios from 'axios'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { useIntl } from 'react-intl'; + +const messages = defineMessages('hooks.useQuickConnect', { + errorMessage: 'Failed to initiate Quick Connect. Please try again.', +}); + +interface UseQuickConnectOptions { + show: boolean; + onSuccess: () => void; + onError?: (error: string) => void; + authenticate: (secret: string) => Promise; +} + +export const useQuickConnect = ({ + show, + onSuccess, + onError, + authenticate, +}: UseQuickConnectOptions) => { + const intl = useIntl(); + const [code, setCode] = useState(''); + const [isLoading, setIsLoading] = useState(true); + const [hasError, setHasError] = useState(false); + const [isExpired, setIsExpired] = useState(false); + const [errorMessage, setErrorMessage] = useState(null); + + const pollingInterval = useRef(undefined); + const isMounted = useRef(true); + const hasInitiated = useRef(false); + const errorCount = useRef(0); + + useEffect(() => { + isMounted.current = true; + + return () => { + isMounted.current = false; + if (pollingInterval.current) { + clearInterval(pollingInterval.current); + } + }; + }, []); + + useEffect(() => { + if (!show) { + hasInitiated.current = false; + if (pollingInterval.current) { + clearInterval(pollingInterval.current); + pollingInterval.current = undefined; + } + } + }, [show]); + + const authenticateWithQuickConnect = useCallback( + async (secret: string) => { + try { + await authenticate(secret); + if (!isMounted.current) return; + onSuccess(); + } catch (error) { + if (!isMounted.current) return; + + const errMsg = + error?.response?.data?.message || + intl.formatMessage(messages.errorMessage); + setErrorMessage(errMsg); + setHasError(true); + onError?.(errMsg); + + if (pollingInterval.current) { + clearInterval(pollingInterval.current); + } + } + }, + [authenticate, intl, onError, onSuccess] + ); + + const startPolling = useCallback( + (secret: string) => { + pollingInterval.current = setInterval(async () => { + try { + const response = await axios.get( + '/api/v1/auth/jellyfin/quickconnect/check', + { + params: { secret }, + } + ); + + errorCount.current = 0; + + if (!isMounted.current) { + if (pollingInterval.current) { + clearInterval(pollingInterval.current); + } + return; + } + + if (response.data.authenticated) { + if (pollingInterval.current) { + clearInterval(pollingInterval.current); + } + await authenticateWithQuickConnect(secret); + } + } catch (error) { + if (!isMounted.current) return; + + if (error?.response?.status === 404) { + if (pollingInterval.current) { + clearInterval(pollingInterval.current); + } + setIsExpired(true); + } else { + errorCount.current++; + if (errorCount.current >= 5) { + if (pollingInterval.current) { + clearInterval(pollingInterval.current); + } + setHasError(true); + const errorMessage = intl.formatMessage(messages.errorMessage); + setErrorMessage(errorMessage); + onError?.(errorMessage); + } + } + } + }, 2000); + }, + [authenticateWithQuickConnect, intl, onError] + ); + + const initiateQuickConnect = useCallback(async () => { + if (pollingInterval.current) { + clearInterval(pollingInterval.current); + } + + setIsLoading(true); + setHasError(false); + setIsExpired(false); + setErrorMessage(null); + setCode(''); + + try { + const response = await axios.post( + '/api/v1/auth/jellyfin/quickconnect/initiate' + ); + + if (!isMounted.current) return; + + setCode(response.data.code); + setIsLoading(false); + startPolling(response.data.secret); + } catch { + if (!isMounted.current) return; + + setHasError(true); + setIsLoading(false); + const errMessage = intl.formatMessage(messages.errorMessage); + setErrorMessage(errMessage); + onError?.(errMessage); + } + }, [startPolling, onError, intl]); + + useEffect(() => { + if (show && !hasInitiated.current) { + hasInitiated.current = true; + initiateQuickConnect(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [show]); + + const cleanup = useCallback(() => { + if (pollingInterval.current) { + clearInterval(pollingInterval.current); + } + }, []); + + return { + code, + isLoading, + hasError, + isExpired, + errorMessage, + initiateQuickConnect, + cleanup, + }; +}; diff --git a/src/hooks/useRequestOverride.ts b/src/hooks/useRequestOverride.ts index 03336363ff..61fc29888d 100644 --- a/src/hooks/useRequestOverride.ts +++ b/src/hooks/useRequestOverride.ts @@ -10,17 +10,23 @@ interface OverrideStatus { profile?: string; rootFolder?: string; languageProfile?: string; + metadataProfile?: string; } const useRequestOverride = (request: MediaRequest): OverrideStatus => { + const serviceType = + request.type === 'movie' + ? 'radarr' + : request.type === 'book' + ? 'readarr' + : 'sonarr'; + const { data: allServers } = useSWR( - `/api/v1/service/${request.type === 'movie' ? 'radarr' : 'sonarr'}` + `/api/v1/service/${serviceType}` ); const { data } = useSWR( - `/api/v1/service/${request.type === 'movie' ? 'radarr' : 'sonarr'}/${ - request.serverId - }` + `/api/v1/service/${serviceType}/${request.serverId}` ); if (!data || !allServers) { @@ -56,6 +62,13 @@ const useRequestOverride = (request: MediaRequest): OverrideStatus => { (profile) => profile.id === request.languageProfileId )?.name : undefined, + metadataProfile: + request.type === 'book' && + defaultServer?.activeMetadataProfileId !== request.metadataProfileId + ? data.metadataProfiles?.find( + (profile) => profile.id === request.metadataProfileId + )?.name + : undefined, }; }; diff --git a/src/hooks/useSearchInput.ts b/src/hooks/useSearchInput.ts index a60b28c075..bd21cf0e56 100644 --- a/src/hooks/useSearchInput.ts +++ b/src/hooks/useSearchInput.ts @@ -1,4 +1,5 @@ /* eslint-disable react-hooks/exhaustive-deps */ +import { isBookRoute } from '@app/utils/mediaTypeRoutes'; import type { Nullable } from '@app/utils/typeHelpers'; import { useRouter } from 'next/router'; import type { Dispatch, SetStateAction } from 'react'; @@ -11,8 +12,10 @@ type Url = string | UrlObject; interface SearchObject { searchValue: string; searchOpen: boolean; + searchType: string; setIsOpen: Dispatch>; setSearchValue: Dispatch>; + setSearchType: Dispatch>; clear: () => void; } @@ -23,6 +26,37 @@ const useSearchInput = (): SearchObject => { const [searchValue, debouncedValue, setSearchValue] = useDebouncedState( (router.query.query as string) ?? '' ); + const [searchType, setSearchType] = useState( + isBookRoute(router.pathname) ? 'hardcover' : 'tmdb' + ); + + /** + * This effect syncs the searchType state with the current route + * + * When navigating to pages other than /search, the search type + * automatically updates to match the page content (hardcover for books, + * tmdb for movies/tv) + */ + useEffect(() => { + if (!router.pathname.startsWith('/search')) { + setSearchType(isBookRoute(router.pathname) ? 'hardcover' : 'tmdb'); + } + }, [router.pathname]); + + /** + * This effect syncs the searchType state with URL query parameter + * + * When on the /search page, keep searchType in sync with the URL type param + * to handle direct navigation + */ + useEffect(() => { + if (router.pathname.startsWith('/search') && router.query.type) { + const queryType = router.query.type as string; + if (queryType === 'hardcover' || queryType === 'tmdb') { + setSearchType(queryType); + } + } + }, [router.query.type, router.pathname]); /** * This effect handles routing when the debounced search input @@ -39,6 +73,7 @@ const useSearchInput = (): SearchObject => { query: { ...router.query, query: debouncedValue, + type: searchType, }, }); } else { @@ -46,12 +81,15 @@ const useSearchInput = (): SearchObject => { router .push({ pathname: '/search', - query: { query: debouncedValue }, + query: { + query: debouncedValue, + type: searchType, + }, }) .then(() => window.scrollTo(0, 0)); } } - }, [debouncedValue]); + }, [debouncedValue, searchType]); /** * This effect is handling behavior when the search input is closed. @@ -114,8 +152,10 @@ const useSearchInput = (): SearchObject => { return { searchValue, searchOpen, + searchType, setIsOpen, setSearchValue, + setSearchType, clear, }; }; diff --git a/src/hooks/useUser.ts b/src/hooks/useUser.ts index d948f2de85..ce990d1454 100644 --- a/src/hooks/useUser.ts +++ b/src/hooks/useUser.ts @@ -29,7 +29,6 @@ export interface User { type NotificationAgentTypes = Record; export interface UserSettings { - discordId?: string; discoverRegion?: string; streamingRegion?: string; originalLanguage?: string; diff --git a/src/hooks/useVerticalScroll.ts b/src/hooks/useVerticalScroll.ts index 5e6514a1cb..4a7c480149 100644 --- a/src/hooks/useVerticalScroll.ts +++ b/src/hooks/useVerticalScroll.ts @@ -19,7 +19,7 @@ const useVerticalScroll = ( type SetTimeoutReturnType = ReturnType; const scrollingTimer: MutableRefObject = - useRef(); + useRef(undefined); const runCallback = () => { if (shouldFetch) { diff --git a/src/i18n/globalMessages.ts b/src/i18n/globalMessages.ts index a150afa992..9aae96f29a 100644 --- a/src/i18n/globalMessages.ts +++ b/src/i18n/globalMessages.ts @@ -11,6 +11,7 @@ const globalMessages = defineMessages('i18n', { requesting: 'Requesting…', request: 'Request', request4k: 'Request in 4K', + requestAudio: 'Request Audiobook', failed: 'Failed', pending: 'Pending', declined: 'Declined', @@ -19,6 +20,8 @@ const globalMessages = defineMessages('i18n', { movie: 'Movie', movies: 'Movies', collection: 'Collection', + book: 'Book', + books: 'Books', tvshow: 'Series', tvshows: 'Series', cancel: 'Cancel', @@ -36,6 +39,8 @@ const globalMessages = defineMessages('i18n', { saving: 'Saving…', import: 'Import', importing: 'Importing…', + sync: 'Sync', + syncing: 'Syncing…', close: 'Close', edit: 'Edit', areyousure: 'Are you sure?', diff --git a/src/i18n/locale/ar.json b/src/i18n/locale/ar.json index 3980f39df5..3cba731c92 100644 --- a/src/i18n/locale/ar.json +++ b/src/i18n/locale/ar.json @@ -766,7 +766,6 @@ "components.UserProfile.UserSettings.UserGeneralSettings.admin": "اداري", "components.UserProfile.UserSettings.UserGeneralSettings.applanguage": "لغة العرض", "components.UserProfile.UserSettings.UserGeneralSettings.regionTip": "تصفية المحتوى بحسب توفره بالمنطقة", - "components.UserProfile.UserSettings.UserGeneralSettings.validationDiscordId": "يجب ذكر رقم مستخدم ديسكورد صحيح", "components.UserProfile.UserSettings.UserNotificationSettings.discordsettingssaved": "تم حفظ إعدادات تنيه الديسكورد بنجاح!", "components.UserProfile.UserSettings.UserNotificationSettings.notificationsettings": "إعدادات التنبيه", "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletsettingsfailed": "فشل حفظ إعدادات تنبيه Pushbullet.", @@ -838,8 +837,6 @@ "components.UserProfile.ProfileHeader.joindate": "منضم {joindate}", "components.UserProfile.ProfileHeader.profile": "رؤية ملف التعريف", "components.UserProfile.ProfileHeader.userid": "رقم المستخدم: {userid}", - "components.UserProfile.UserSettings.UserGeneralSettings.discordId": "رقم ديسكورد المستخدم", - "components.UserProfile.UserSettings.UserGeneralSettings.discordIdTip": "رقم متعدد الخانات مرتبط بحساب عضوية الديسكورد", "components.UserProfile.UserSettings.UserGeneralSettings.displayName": "إسم العرض", "components.UserProfile.UserSettings.UserGeneralSettings.enableOverride": "تجاوز الحد العام", "components.UserProfile.UserSettings.UserGeneralSettings.general": "عام", @@ -1636,5 +1633,40 @@ "i18n.error": "حدث خطأ ما. يرجى المحاولة مرة أخرى.", "i18n.partiallyblocklisted": "محظور جزئياً", "i18n.removeFromBlocklistSuccess": "تمت إزالة {title} من قائمة الحظر بنجاح.", - "i18n.removefromBlocklist": "إزالة من قائمة الحظر" + "i18n.removefromBlocklist": "إزالة من قائمة الحظر", + "components.Layout.UserWarnings.profileIncomplete": "", + "components.Login.emailAlreadyTaken": "", + "components.Login.jellyfinLocalLoginHint": "", + "components.Login.saveFailed": "", + "components.Login.urlBaseHelp": "", + "components.Settings.Notifications.useUserLocale": "", + "components.Settings.RadarrModal.enableSearchHelp": "", + "components.Settings.RadarrModal.externalUrlHelp": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdAdd": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdPlaceholder": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdRemove": "", + "components.Settings.Notifications.usePublicLogo": "", + "components.Settings.Notifications.usePublicLogoTip": "", + "components.Settings.RadarrModal.apiKeyHelp": "", + "components.Settings.RadarrModal.baseUrlHelp": "", + "components.Settings.RadarrModal.server4kHelp": "", + "components.Settings.RadarrModal.syncEnabledHelp": "", + "components.Settings.SettingsMain.blocklistLanguage": "", + "components.Settings.SettingsMain.blocklistLanguageTip": "", + "components.Settings.SettingsMain.blocklistRegion": "", + "components.Settings.SettingsMain.blocklistRegionTip": "", + "components.Settings.SettingsUsers.disabledMediaServerLoginWarning": "", + "components.Settings.SonarrModal.apiKeyHelp": "", + "components.Settings.SonarrModal.baseUrlHelp": "", + "components.Settings.SonarrModal.enableSearchHelp": "", + "components.Settings.SonarrModal.externalUrlHelp": "", + "components.Settings.SonarrModal.monitorNewItemsHelp": "", + "components.Settings.SonarrModal.server4kHelp": "", + "components.Settings.SonarrModal.syncEnabledHelp": "", + "components.UserList.importedPlexUsersNoPassword": "", + "components.UserList.importedUsersNoPassword": "", + "components.UserProfile.UserSettings.UserPasswordChange.localPasswordDescription": "", + "i18n.notificationLocale": "", + "components.RequestCard.failedmodify": "", + "components.RequestList.RequestItem.failedmodify": "" } diff --git a/src/i18n/locale/bg.json b/src/i18n/locale/bg.json index bd48ba42cc..71ee7c38ce 100644 --- a/src/i18n/locale/bg.json +++ b/src/i18n/locale/bg.json @@ -745,7 +745,6 @@ "components.Settings.notificationsettings": "Настройки за известията", "components.Settings.SettingsLogs.logsDescription": "Можете също да видите тези логове директно чрез stdout или във файла {appDataPath}/logs/seerr.log.", "components.TvDetails.episodeCount": "{episodeCount, plural, one {# Епизод} other {# Епизоди}}", - "components.UserProfile.UserSettings.UserGeneralSettings.discordId": "ID на потребител в Discord", "components.TvDetails.firstAirDate": "Дата на първо излъчване", "pages.errormessagewithcode": "{statusCode} - {error}", "components.UserList.validationEmail": "Изисква се имейл адрес", @@ -807,7 +806,6 @@ "components.Settings.SonarrModal.tagRequests": "Етикети към заявките", "components.UserProfile.UserSettings.UserPasswordChange.validationConfirmPasswordSame": "Паролите трябва да съвпадат", "components.UserProfile.UserSettings.UserNotificationSettings.pushoverApplicationToken": "API токен на приложението", - "components.UserProfile.UserSettings.UserGeneralSettings.validationDiscordId": "Трябва да предоставите валиден ID на потребител в Discord", "i18n.importing": "Импортиране…", "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseries": "Автоматична заявка на сериали", "components.UserList.create": "Създаване", @@ -1023,7 +1021,6 @@ "components.Settings.SettingsJobsCache.plex-full-scan": "Пълно сканиране на библиотеката в Plex", "components.Settings.SonarrModal.selecttags": "Изберете етикети", "i18n.next": "Напред", - "components.UserProfile.UserSettings.UserGeneralSettings.discordIdTip": "Многоцифреният идентификационен номер, свързан с вашия Discord акаунт", "components.UserProfile.UserSettings.UserNotificationSettings.email": "Имейл", "components.UserProfile.UserSettings.UserPasswordChange.toastSettingsFailure": "Нещо се обърка при запазването на паролата.", "components.Settings.SonarrModal.validationProfileRequired": "Трябва да изберете профил на качеството", @@ -1666,5 +1663,10 @@ "i18n.notificationLocale": "Език на известията", "i18n.partiallyblocklisted": "Частично блокирано", "i18n.removeFromBlocklistSuccess": "{title} беше успешно премахнато от списъка с блокирани.", - "i18n.removefromBlocklist": "Премахване от списъка с блокирани" + "i18n.removefromBlocklist": "Премахване от списъка с блокирани", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdAdd": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdPlaceholder": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdRemove": "", + "components.Settings.Notifications.usePublicLogo": "", + "components.Settings.Notifications.usePublicLogoTip": "" } diff --git a/src/i18n/locale/ca.json b/src/i18n/locale/ca.json index d15e5b8005..339906bd06 100644 --- a/src/i18n/locale/ca.json +++ b/src/i18n/locale/ca.json @@ -29,7 +29,7 @@ "components.RequestModal.QuotaDisplay.quotaLink": "Podeu veure un resum dels límits de sol·licituds a la vostra pàgina de perfil.", "components.RequestModal.QuotaDisplay.notenoughseasonrequests": "No podeu sol·licitar més temporades", "components.RequestModal.QuotaDisplay.movielimit": "{limit, plural, one {pel·lícula} other {pel·lícules}}", - "components.RequestModal.QuotaDisplay.movie": "Pel·lícula", + "components.RequestModal.QuotaDisplay.movie": "pel·lícula", "components.RequestModal.QuotaDisplay.allowedRequestsUser": "Aquest usuari pot sol·licitar {limit} {type}{days, plural, =0 {} one { cada dia} other { cada{days} dies}}.", "components.RequestModal.QuotaDisplay.allowedRequests": "Podeu sol·licitar {limit} {type}{days, plural, =0 {} one { cada dia} other { cada {days} dies}}.", "components.RequestModal.AdvancedRequester.rootfolder": "Carpeta arrel", @@ -186,7 +186,7 @@ "components.Discover.TvGenreSlider.tvgenres": "Gèneres de Sèries", "components.Discover.TvGenreList.seriesgenres": "Gèneres de Sèries", "components.Discover.StudioSlider.studios": "Estudis", - "components.Discover.NetworkSlider.networks": "Emissors", + "components.Discover.NetworkSlider.networks": "Plataformes", "components.Discover.MovieGenreSlider.moviegenres": "Gèneres de Pel·lícules", "components.Discover.MovieGenreList.moviegenres": "Gèneres de Pel·lícules", "components.Discover.DiscoverTvLanguage.languageSeries": "Sèries en {language}", @@ -348,7 +348,7 @@ "components.UserProfile.UserSettings.UserPasswordChange.currentpassword": "Contrasenya actual", "components.UserProfile.UserSettings.UserPasswordChange.confirmpassword": "Confirmeu la contrasenya", "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramChatId": "Heu de proporcionar un identificador de xat vàlid", - "components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "Heu de proporcionar un identificador d'usuari vàlid", + "components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "Cada ID ha de ser un ID d'usuari de Discord vàlid", "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsSuccess": "La configuració s'ha desat correctament!", "components.UserProfile.UserSettings.UserGeneralSettings.role": "Rol", "components.UserProfile.UserSettings.UserNotificationSettings.telegramChatIdTipLong": "Inicieu un xat, afegiu @get_id_bot i executeu l'ordre / my_id", @@ -357,8 +357,8 @@ "components.UserProfile.UserSettings.UserNotificationSettings.sendSilently": "Envia missatges silenciosament", "components.UserProfile.UserSettings.UserNotificationSettings.notificationsettings": "Configuració de les notificacions", "components.UserProfile.UserSettings.UserNotificationSettings.notifications": "Notificacions", - "components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "El número d'ID de diversos dígits associat al vostre compte d'usuari", - "components.UserProfile.UserSettings.UserNotificationSettings.discordId": "ID de Discord", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "El número d'ID de diversos dígits associat al vostre compte d'usuari. Si disposeu de multiples comptes podeu afegir mes d'un ID de Discord.", + "components.UserProfile.UserSettings.UserNotificationSettings.discordId": "IDs d'usuari", "components.UserProfile.UserSettings.UserGeneralSettings.user": "Usuari", "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailure": "S'ha produït un error en desar la configuració.", "components.UserProfile.UserSettings.UserGeneralSettings.seriesrequestlimit": "Límit de sol·licituds de sèries", @@ -381,7 +381,7 @@ "components.UserProfile.ProfileHeader.profile": "Mostra el perfil", "components.UserProfile.ProfileHeader.joindate": "Unit el {joindate}", "components.UserList.validationpasswordminchars": "La contrasenya és massa curta; ha de tenir un mínim de 8 caràcters", - "components.UserList.validationEmail": "Heu de proporcionar una adreça de correu electrònic vàlida", + "components.UserList.validationEmail": "Adreça de correu electrònic requerida", "components.UserList.userssaved": "Els permisos d'usuari s'han desat correctament!", "components.UserList.users": "Usuaris", "components.UserList.userlist": "Llista d'usuaris", @@ -392,7 +392,7 @@ "components.TvDetails.originaltitle": "Títol original", "components.TvDetails.originallanguage": "Idioma original", "components.TvDetails.nextAirDate": "Pròxima data d'emissió", - "components.TvDetails.network": "{networkCount, plural, one {Emissor} other {Emissors}}", + "components.TvDetails.network": "{networkCount, plural, one {Plataforma} other {Plataformes}}", "components.TvDetails.firstAirDate": "Primera data d'emissió", "components.TvDetails.episodeRuntimeMinutes": "{runtime} minuts", "components.TvDetails.episodeRuntime": "Duració de l'episodi", @@ -434,10 +434,10 @@ "components.Settings.port": "Port", "components.Settings.plexsettingsDescription": "Configureu la paràmetres del vostre servidor Plex. Seerr explora les vostres biblioteques Plex per determinar la disponibilitat de continguts.", "components.Settings.plexsettings": "Configuració de Plex", - "components.Settings.plexlibrariesDescription": "Les biblioteques en les que Seerr explora títols. Configureu i deseu la configuració de la connexió Plex i feu clic al botó següent si no apareix cap.", + "components.Settings.plexlibrariesDescription": "Les biblioteques en les que Seerr explora títols. Configureu i deseu la configuració de la connexió de Plex i feu clic al botó següent si no apareix cap.", "components.Settings.plexlibraries": "Biblioteques Plex", "components.Settings.plex": "Plex", - "components.Settings.manualscanDescription": "Normalment, només s’executarà una vegada cada 24 hores. Seerr comprovarà de forma més agressiva el contingut afegit recentment del seu servidor Plex. Si és la primera vegada que configureu Plex, es recomana fer una exploració manual completa de la biblioteca!", + "components.Settings.manualscanDescription": "Normalment, només s’executarà una vegada cada 24 hores. Seerr comprovarà de forma més agressiva el contingut afegit recentment del vostre servidor Plex. Si és la primera vegada que configureu Plex, es recomana fer una exploració manual completa de la biblioteca!", "components.Settings.SettingsJobsCache.plex-recently-added-scan": "Exploració d'elements de Plex afegits recentment", "components.Settings.notrunning": "No s'està executant", "components.Settings.notificationsettings": "Configuració de les notificacions", @@ -562,7 +562,7 @@ "components.Settings.SettingsJobsCache.process": "Procés", "components.Settings.SettingsJobsCache.jobtype": "Tipus", "components.Settings.SettingsJobsCache.jobsandcache": "Tasques programades i memòria cau", - "components.Settings.SettingsJobsCache.jobsDescription": "Seerr realitza certes tasques de manteniment com a feines programades regularment, però també es poden activar manualment a continuació. L’execució manual d’un treball no alterarà la seva programació.", + "components.Settings.SettingsJobsCache.jobsDescription": "Seerr realitza certes tasques de manteniment com a feines programades regularment, però també es poden activar manualment a continuació. L’execució manual d’un tasca no alterarà la seva programació.", "components.Settings.SettingsJobsCache.jobs": "Tasques programades", "components.Settings.SettingsJobsCache.jobname": "Nom de la tasca programada", "components.Settings.SettingsJobsCache.download-sync-reset": "Restableix la sincronització de descàrregues", @@ -759,7 +759,7 @@ "components.Settings.Notifications.webhookUrlTip": "Creeu una integració de webhook al vostre servidor", "components.Settings.Notifications.encryptionOpportunisticTls": "Utilitzeu sempre STARTTLS", "components.Settings.Notifications.encryptionNone": "Cap", - "components.Settings.SettingsUsers.localLoginTip": "Permetre als usuaris iniciar la sessió mitjançant la seva adreça de correu electrònic i contrasenya, en lloc de l'autenticació de Plex", + "components.Settings.SettingsUsers.localLoginTip": "Permetre als usuaris iniciar la sessió mitjançant la seva adreça de correu electrònic i contrasenya", "components.Settings.SettingsUsers.defaultPermissionsTip": "Permisos inicials assignats a usuaris nous", "components.RequestList.RequestItem.requesteddate": "Sol·licitat", "components.RequestCard.failedretry": "S'ha produït un error en tornar a demanar la sol·licitud.", @@ -893,7 +893,7 @@ "components.PermissionEdit.viewissuesDescription": "Doneu permís per veure incidències de continguts informats per altres usuaris.", "components.PermissionEdit.createissuesDescription": "Doneu permís per informar incidències en continguts.", "components.PermissionEdit.manageissues": "Gestiona les incidències", - "components.Settings.SettingsAbout.runningDevelop": "Esteu executant la branca develop d'Seerr, que només es recomana per a aquells que contribueixen al desenvolupament o ajuden amb proves de nous desenvolupaments.", + "components.Settings.SettingsAbout.runningDevelop": "Esteu executant la branca develop de Seerr, que només es recomana per a aquells que contribueixen al desenvolupament o ajuden amb proves de nous desenvolupaments.", "components.TvDetails.streamingproviders": "Actualment s'està retransmetent", "components.UserProfile.UserSettings.UserNotificationSettings.pushoverUserKey": "Clau d'usuari o grup", "components.UserProfile.UserSettings.UserNotificationSettings.pushoversettingsfailed": "La configuració de notificacions Pushover no s'ha pogut desar.", @@ -930,7 +930,6 @@ "components.Settings.Notifications.NotificationsGotify.gotifysettingsfailed": "La configuració de notificacions de Gotify no s'ha pogut desar.", "components.Settings.Notifications.NotificationsGotify.gotifysettingssaved": "La configuració de notificacions de Gotify s'ha desat correctament!", "components.Settings.Notifications.NotificationsGotify.validationTokenRequired": "Heu de proporcionar un testimoni d'aplicació", - "components.UserProfile.UserSettings.UserGeneralSettings.discordIdTip": "El número d'ID de diversos dígits associat al vostre compte d'usuari de Discord", "components.ManageSlideOver.manageModalAdvanced": "Avançat", "components.ManageSlideOver.manageModalMedia": "Contingut", "components.ManageSlideOver.manageModalMedia4k": "Contingut 4K", @@ -966,8 +965,6 @@ "components.Settings.Notifications.NotificationsGotify.toastGotifyTestFailed": "No s'ha pogut enviar la notificació de prova de Gotify.", "components.Settings.Notifications.NotificationsGotify.toastGotifyTestSending": "S'està enviant una notificació de prova de Gotify…", "components.Settings.SettingsAbout.appDataPath": "Directori de dades", - "components.UserProfile.UserSettings.UserGeneralSettings.discordId": "Identificador d'usuari de Discord", - "components.UserProfile.UserSettings.UserGeneralSettings.validationDiscordId": "Heu de proporcionar un identificador d'usuari de Discord vàlid", "i18n.importing": "S'està important…", "components.RequestBlock.languageprofile": "Perfil d'idioma", "components.AirDateBadge.airedrelative": "Emès {relativeTime}", @@ -1025,7 +1022,7 @@ "components.RequestBlock.decline": "Rebutja la sol·licitud", "components.RequestBlock.requestdate": "Data de sol·licitud", "components.RequestBlock.requestedby": "Sol·licitat per", - "components.Settings.restartrequiredTooltip": "Seerr s'ha de reiniciar perquè els canvis a aquesta configuració tinguin efecte", + "components.Settings.restartrequiredTooltip": "Seerr s'ha de reiniciar perquè els canvis de configuració tinguin efecte", "components.StatusBadge.managemedia": "Gestiona {mediaType}", "components.TvDetails.Season.somethingwentwrong": "S'ha produït un error en recuperar les dades de la temporada.", "components.TvDetails.rtaudiencescore": "Puntuació d'audiència de Rotten Tomatoes", @@ -1116,10 +1113,10 @@ "components.Discover.DiscoverTv.sortPopularityDesc": "Popularitat descendent", "components.Discover.DiscoverTv.sortTmdbRatingAsc": "Puntuació TMDB ascendent", "components.Discover.DiscoverTv.sortTmdbRatingDesc": "Puntuació TMDB descendent", - "components.Discover.networks": "Emissors", + "components.Discover.networks": "Plataformes", "components.Discover.resetwarning": "Restablir tots els controls lliscants al valor predeterminat. Això també suprimirà els controls lliscants personalitzats!", "components.Discover.tmdbmoviekeyword": "Paraula clau de pel·lícula TMDB", - "components.Discover.tmdbnetwork": "Emissors TMDB", + "components.Discover.tmdbnetwork": "Plataforma TMDB", "components.Discover.FilterSlideover.tmdbuserscore": "Puntuació d'usuaris TMDB", "components.Discover.tvgenres": "Gèneres de sèries", "components.Discover.DiscoverTvKeyword.keywordSeries": "Sèries {keywordTitle}", @@ -1188,7 +1185,7 @@ "components.Discover.tmdbtvstreamingservices": "Serveis de transmissió de TV TMDB", "components.Layout.UserWarnings.emailRequired": "És requereix un n correu electrònic.", "components.Layout.UserWarnings.passwordRequired": "Es requereix una contrasenya.", - "components.Login.description": "Com que és la primera vegada que inicieu sessió a {applicationName}, es necessita afegir un correu electrònic vàlid.", + "components.Login.description": "Afegeix un correu electrònic vàlid per a completar el teu perfil. S'utiitzara per a enviar-te notificacions i iniciar sessió localment.", "components.Discover.FilterSlideover.tmdbuservotecount": "Recompte de vots d'usuaris de TMDB", "components.Discover.FilterSlideover.voteCount": "Nombre de vots entre {minValue} i {maxValue}", "components.Layout.UserWarnings.emailInvalid": "El correu electrònic no és vàlid.", @@ -1282,7 +1279,7 @@ "components.Settings.Notifications.NotificationsPushover.sound": "So de notificació", "components.Settings.Notifications.NotificationsSlack.embedPoster": "Incrustar pòster", "components.Settings.Notifications.NotificationsWebhook.supportVariables": "Admet variables d'URL", - "components.Settings.Notifications.NotificationsWebhook.supportVariablesTip": "Les variables permeses estan documentades en la secció de variables de la plantilla del webhook.", + "components.Settings.Notifications.NotificationsWebhook.supportVariablesTip": "Les variables permeses estan documentades en la secció de variables de la plantilla del webhook", "components.Login.back": "Tornar enrere", "components.Selector.canceled": "Cancel·lat", "components.Selector.ended": "Finalitzat", @@ -1299,7 +1296,7 @@ "components.Blocklist.filterBlocklistedTags": "Etiquetes de les llistes de bloqueig", "components.Blocklist.filterManual": "Manual", "components.Blocklist.mediaName": "Nom", - "components.Blocklist.mediaTmdbId": "ID de tmdb", + "components.Blocklist.mediaTmdbId": "Id de tmdb", "components.Blocklist.mediaType": "Tipus", "components.Blocklist.showAllBlocklisted": "Mostra tots els elements multimèdia bloquejats", "components.CollectionDetails.removefromblocklistpartialcount": "{removeLabel} ({count, plural, one {# pel·lícula} other {# pel·lícules}})", @@ -1328,5 +1325,371 @@ "components.Settings.Notifications.NotificationsWebhook.customHeadersKey": "Nom de la capçalera", "components.Settings.Notifications.NotificationsWebhook.customHeadersRemove": "Treure", "components.Settings.Notifications.NotificationsWebhook.customHeadersTip": "Afegeix capçaleres HTTP personalitzades per incloure a les sol·licituds de webhook", - "components.Settings.Notifications.NotificationsWebhook.customHeadersValue": "Valor de capçalera" + "components.Settings.Notifications.NotificationsWebhook.customHeadersValue": "Valor de capçalera", + "components.Settings.Notifications.embedPoster": "Incrustar pòster", + "components.Layout.UserWarnings.profileIncomplete": "El perfil està incomplert", + "components.Login.emailAlreadyTaken": "Aquest correu eletrònic ja està en us.", + "components.Login.jellyfinLocalLoginHint": "Si no heu definit una adreça electrònica al vostre perfil, feu servir el vostre nom d'usuari {mediaServerName}.", + "components.Login.saveFailed": "S'ha produït un error al guardar.", + "components.Settings.Notifications.NotificationsWebPush.embedPoster": "Incrustar pòster", + "components.Settings.Notifications.messageThreadId": "ID del fil/tema", + "components.Settings.Notifications.messageThreadIdTip": "Si el vostre xat en grup té temes activats, podeu especificar aquí l'ID d'un fil/tema", + "components.Settings.OverrideRuleTile.tags": "Etiquetes", + "components.Settings.SettingsNetwork.docs": "documentació", + "i18n.specials": "Especials", + "components.Settings.OverrideRuleModal.languages": "Idiomes", + "components.Settings.SettingsJobsCache.dnscacheage": "Edat", + "components.Settings.SettingsMain.youtubeUrl": "", + "components.Settings.SettingsNetwork.trustProxyTip": "", + "components.Settings.apiKey": "", + "components.UserList.toggleSortDirectionAria": "", + "components.Settings.Notifications.NotificationsWebhook.webhookUrlTip": "L'URL de notificació de prova està definit com a {testUrl} en comptes de l'URL actual del webhook.", + "components.Settings.OverrideRuleModal.selecttags": "Selecciona etiquetes", + "components.Settings.RadarrModal.syncEnabledHelp": "Escaneja Radarr per detectar contingut multimèdia existent i sol·licita l'estat per a que els usuaris no puguin sol·licitar contingut ja disponible.", + "components.UserProfile.UserSettings.UserGeneralSettings.email": "Correu electrònic", + "components.UserProfile.UserSettings.UserGeneralSettings.mediaServerUser": "", + "components.UserProfile.UserSettings.UserGeneralSettings.saving": "S'està guardant…", + "components.UserProfile.UserSettings.UserGeneralSettings.streamingRegion": "", + "components.UserProfile.UserSettings.UserGeneralSettings.streamingRegionTip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmailEmpty": "", + "components.UserProfile.UserSettings.UserGeneralSettings.validationemailformat": "", + "components.UserProfile.UserSettings.UserGeneralSettings.validationemailrequired": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.browser": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.created": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablewebpush": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablingwebpusherror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablewebpush": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablingwebpusherror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.engine": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.managedevices": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.nodevicestoshow": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.operatingsystem": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleted": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleteerror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.type": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.unknown": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeendisabled": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeenenabled": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.deviceDefault": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdAdd": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdPlaceholder": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdRemove": "", + "components.Settings.SettingsJobsCache.dnsCacheGlobalStatsDescription": "Aquestes estadístiques s'agreguen a totes les entrades de la memòria cau DNS.", + "components.Settings.SettingsJobsCache.dnscacheactiveaddress": "Adreça activa", + "components.Settings.SettingsJobsCache.dnscacheflushed": "S'ha buidat la memòria cau DNS de {hostname}.", + "components.Settings.SettingsJobsCache.dnscachehits": "Consultes", + "components.Settings.SettingsJobsCache.dnscachemisses": "Fallades", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorDays": "Cada {jobScheduleDays, plural, one {dia} other {{jobScheduleDays} dies}}", + "components.Settings.SettingsJobsCache.hitRate": "Taxa de consultes", + "components.Settings.SettingsJobsCache.hits": "Consultes", + "components.Settings.SettingsJobsCache.ipv4Fallbacks": "Alternatives d'IPv4", + "components.Settings.SettingsMain.discoverRegion": "", + "components.Settings.SettingsMain.discoverRegionTip": "", + "components.Settings.SettingsMain.enableSpecialEpisodes": "", + "components.Settings.SettingsMain.hideAvailableTip": "", + "components.Settings.SettingsMain.hideBlocklisted": "", + "components.Settings.SettingsMain.hideBlocklistedTip": "", + "components.Settings.SettingsMain.validationUrl": "", + "components.Settings.SettingsMain.validationUrlTrailingSlash": "", + "components.Settings.SettingsMain.youtubeUrlTip": "", + "components.Settings.SettingsUsers.mediaServerLoginTip": "", + "components.Settings.SonarrModal.monitorNewItemsHelp": "", + "components.Settings.SonarrModal.syncEnabledHelp": "", + "components.Settings.SonarrModal.tagRequests": "", + "components.Settings.SonarrModal.tagRequestsInfo": "", + "components.Settings.importBlocklistedTagsTip": "", + "components.Settings.invalidKeyword": "", + "components.Settings.invalidurlerror": "", + "components.Settings.jellyfinForgotPasswordUrl": "", + "components.Settings.jellyfinSettings": "", + "components.Settings.jellyfinSettingsDescription": "", + "components.Settings.jellyfinSettingsFailure": "", + "components.Settings.jellyfinSettingsSuccess": "", + "components.Settings.manualscanDescriptionJellyfin": "", + "components.Settings.manualscanJellyfin": "", + "components.Settings.menuJellyfinSettings": "{mediaServerName}", + "components.Settings.menuNetwork": "", + "components.Settings.valueRequired": "", + "components.TitleCard.watchlistDeleted": "", + "components.TitleCard.watchlistError": "", + "components.UserList.importfromJellyfin": "", + "components.UserList.mediaServerUser": "", + "components.UserList.noJellyfinuserstoimport": "", + "components.UserList.sortBy": "", + "components.UserList.sortByJoined": "", + "components.UserList.sortByRequests": "", + "components.UserList.sortByRole": "", + "components.UserList.sortByType": "", + "components.UserList.sortByUser": "", + "components.UserList.toggleSortDirection": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.deleteFailed": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.errorUnknown": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.linkedAccounts": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.linkedAccountsHint": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.noLinkedAccounts": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.noPermissionDescription": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorExists": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorUnauthorized": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramMessageThreadId": "", + "components.UserProfile.UserSettings.menuLinkedAccounts": "", + "i18n.blocklist": "Llista de bloqueig", + "components.Settings.SettingsAbout.supportseerr": "Doneu suport a Seer", + "components.Settings.jellyfinSyncFailedNoLibrariesFound": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmail": "", + "components.Settings.Notifications.usePublicLogo": "Utilitza el logotip públic de Seerr en lloc del logotip de la instància", + "components.Settings.Notifications.usePublicLogoTip": "Si la vostra instància de Seerr no és accessible públicament, activeu aquesta opció perquè els clients de correu electrònic fora de la vostra xarxa puguin mostrar les imatges. Les imatges s'extreurà del repositori públic de GitHub.", + "components.Settings.Notifications.useUserLocale": "Utilitza la configuració regional del destinatari de notificacions", + "components.Settings.Notifications.validationMessageThreadId": "L'ID del fil/tema ha de ser un nombre enter positiu", + "components.Settings.Notifications.validationWebhookRoleId": "Heu de proporcionar un ID de rol de Discord vàlid", + "components.Settings.Notifications.webhookRoleId": "ID de rol de notificació", + "components.Settings.Notifications.webhookRoleIdTip": "L'ID del rol que s'ha d'esmentar al missatge del webhook. Deixeu-ho en blanc per desactivar les mencions", + "components.Settings.OverrideRuleModal.conditions": "Condicions", + "components.Settings.OverrideRuleModal.conditionsDescription": "Especifica les condicions abans d'aplicar els canvis de paràmetres. Cada camp ha d'estar validat perquè s'apliquin les regles (operació AND). Un camp es considera verificat si alguna de les seves propietats coincideix (operació OR).", + "components.Settings.OverrideRuleModal.create": "Crea una regla", + "components.Settings.OverrideRuleModal.createrule": "Nova regla de substitució", + "components.Settings.OverrideRuleModal.editrule": "Edita la regla de substitució", + "components.Settings.OverrideRuleModal.genres": "Géneres", + "components.Settings.OverrideRuleModal.keywords": "Paraules clau", + "components.Settings.OverrideRuleModal.notagoptions": "Sense etiquetes.", + "components.Settings.OverrideRuleModal.rootfolder": "Carpeta arrel", + "components.Settings.OverrideRuleModal.ruleCreated": "Regla de substitució creada correctament!", + "components.Settings.OverrideRuleModal.selectQualityProfile": "Selecciona el perfil de qualitat", + "components.Settings.OverrideRuleModal.selectRootFolder": "Selecciona la carpeta arrel", + "components.Settings.OverrideRuleModal.selectService": "Selecciona un servei", + "components.Settings.OverrideRuleModal.service": "Servei", + "components.Settings.OverrideRuleModal.serviceDescription": "Aplica aquesta regla al servei seleccionat.", + "components.Settings.OverrideRuleModal.settings": "Configuració", + "components.Settings.OverrideRuleModal.settingsDescription": "Especifiqueu quins paràmetres es canviaran quan es compleixin les condicions anteriors.", + "components.Settings.OverrideRuleModal.users": "Usuaris", + "components.Settings.OverrideRuleTile.conditions": "Condicions", + "components.Settings.OverrideRuleTile.genre": "Génere", + "components.Settings.OverrideRuleTile.keywords": "Paraules clau", + "components.Settings.OverrideRuleTile.language": "Idioma", + "components.Settings.OverrideRuleTile.qualityprofile": "Perfil de qualitat", + "components.Settings.OverrideRuleTile.rootfolder": "Carpeta arrel", + "components.Settings.OverrideRuleTile.settings": "Configuració", + "components.Settings.OverrideRuleTile.users": "Usuaris", + "components.Settings.RadarrModal.apiKeyHelp": "Troba-ho a Radarr: Configuració > General > Seguretat > Clau API", + "components.Settings.RadarrModal.baseUrlHelp": "Si definiu una URL base a Radarr (Configuració > General > Host), introduïu-la aquí (per exemple, /radarr). Si no, deixeu-la en blanc.", + "components.Settings.RadarrModal.enableSearchHelp": "Activa automàticament una cerca a Radarr quan s'aprova una sol·licitud.", + "components.Settings.RadarrModal.externalUrlHelp": "Per a enllaços clicables a les pàgines multimèdia quan el nom de l'amfitrió no és accessible des de fora de la xarxa.", + "components.Settings.RadarrModal.server4kHelp": "Només si teniu una instància de 4K separada. Deixeu-ho sense marcar per a un sol servidor.", + "components.Settings.RadarrModal.tagRequests": "Sol·licituds d'etiquetes", + "components.Settings.RadarrModal.tagRequestsInfo": "Afegeix automàticament una etiqueta addicional amb l'ID d'usuari i el nom de visualització del sol·licitant", + "components.Settings.SettingsAbout.aboutseerr": "Quant a Seer", + "components.Settings.SettingsAbout.contribute": "Contribuïu al projecte", + "components.Settings.SettingsJobsCache.dnsCache": "Memòria cau DNS", + "components.Settings.SettingsJobsCache.dnsCacheDescription": "Seerr emmagatzema a la memòria cau les cerques DNS per optimitzar el rendiment i evitar fer crides a l'API innecessàries.", + "components.Settings.SettingsJobsCache.dnsCacheGlobalStats": "Estadístiques globals de la memòria cau del DNS", + "components.Settings.SettingsJobsCache.dnscachename": "Nom d'amfitrió", + "components.Settings.SettingsJobsCache.flushdnscache": "Buidar la memòria cau DNS", + "components.Settings.SettingsJobsCache.plex-refresh-token": "Actualiza el testimoni de Plex", + "components.Settings.SettingsJobsCache.process-blocklisted-tags": "Processa les etiquetes bloquejades", + "components.Settings.SettingsMain.apikeyCopied": "Clau API copiada al porta-retalls.", + "components.Settings.SettingsMain.blocklistLanguage": "Afegeix idioma a la llista de bloqueig", + "components.Settings.SettingsMain.blocklistLanguageTip": "Idioma utilitzat per a l'escaneig de contingut de la llista de bloqueig (independentment de la configuració de descobriment)", + "components.Settings.SettingsMain.blocklistRegion": "Regió de la llista de bloqueig", + "components.Settings.SettingsMain.blocklistRegionTip": "Regió utilitzada per a l'escaneig de contingut de la llista de bloqueig (independentment de la configuració de descobriment)", + "components.Settings.SettingsMain.blocklistedTags": "Bloca contingut amb etiquetes", + "components.Settings.SettingsMain.blocklistedTagsLimit": "Limita el contingut blocat per etiqueta", + "components.Settings.SettingsMain.blocklistedTagsLimitTip": "", + "components.Settings.SettingsMain.blocklistedTagsTip": "", + "components.Settings.SettingsMain.streamingRegion": "", + "components.Settings.SettingsMain.streamingRegionTip": "", + "components.Settings.SettingsNetwork.apiRequestTimeout": "", + "components.Settings.SettingsNetwork.apiRequestTimeoutTip": "", + "components.Settings.SettingsNetwork.csrfProtection": "", + "components.Settings.SettingsNetwork.csrfProtectionHoverTip": "", + "components.Settings.SettingsNetwork.csrfProtectionTip": "", + "components.Settings.SettingsNetwork.dnsCache": "", + "components.Settings.SettingsNetwork.dnsCacheForceMaxTtl": "", + "components.Settings.SettingsNetwork.dnsCacheForceMinTtl": "", + "components.Settings.SettingsNetwork.dnsCacheHoverTip": "", + "components.Settings.SettingsNetwork.dnsCacheTip": "", + "components.Settings.SettingsNetwork.networkDisclaimer": "", + "components.Settings.SettingsNetwork.networksettings": "", + "components.Settings.SettingsNetwork.networksettingsDescription": "", + "components.Settings.SettingsNetwork.proxyBypassFilter": "", + "components.Settings.SettingsNetwork.proxyBypassFilterTip": "", + "components.Settings.SettingsNetwork.proxyBypassLocalAddresses": "", + "components.Settings.SettingsNetwork.proxyEnabled": "", + "components.Settings.SettingsNetwork.proxyEnabledTip": "", + "components.Settings.SettingsNetwork.proxyHostname": "", + "components.Settings.SettingsNetwork.proxyPassword": "", + "components.Settings.SettingsNetwork.proxyPort": "", + "components.Settings.SettingsNetwork.proxySsl": "", + "components.Settings.SettingsNetwork.proxyUser": "", + "components.Settings.SettingsNetwork.toastSettingsFailure": "", + "components.Settings.SettingsNetwork.toastSettingsSuccess": "", + "components.Settings.SettingsNetwork.validationApiRequestTimeout": "", + "components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "", + "components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "", + "components.Settings.SettingsNetwork.validationProxyPort": "", + "components.Settings.SettingsUsers.atLeastOneAuth": "", + "components.Settings.SettingsUsers.disabledMediaServerLoginWarning": "", + "components.Settings.SettingsUsers.loginMethods": "", + "components.Settings.SettingsUsers.loginMethodsTip": "", + "components.Settings.SettingsUsers.mediaServerLogin": "", + "components.Settings.SonarrModal.animeSeriesType": "", + "components.Settings.SonarrModal.apiKeyHelp": "", + "components.Settings.SonarrModal.baseUrlHelp": "", + "components.Settings.SonarrModal.enableSearchHelp": "", + "components.Settings.SonarrModal.externalUrlHelp": "", + "components.Settings.SonarrModal.monitorNewItems": "", + "components.Settings.SonarrModal.server4kHelp": "", + "components.Settings.addrule": "", + "components.Settings.allChosenProvidersAreOperational": "", + "components.Settings.animeMetadataProvider": "", + "components.Settings.blocklistedTagImportInstructions": "", + "components.Settings.blocklistedTagImportTitle": "", + "components.Settings.blocklistedTagsText": "", + "components.Settings.chooseProvider": "", + "components.Settings.clearBlocklistedTagsConfirm": "", + "components.Settings.clickTest": "", + "components.Settings.connectionTestFailed": "", + "components.Settings.copyBlocklistedTags": "", + "components.Settings.copyBlocklistedTagsEmpty": "", + "components.Settings.failedToSaveMetadataSettings": "", + "components.Settings.general": "", + "components.Settings.jellyfinSyncFailedAutomaticGroupedFolders": "", + "components.Settings.jellyfinSyncFailedGenericError": "", + "components.Settings.jellyfinlibraries": "", + "components.Settings.jellyfinlibrariesDescription": "", + "components.Settings.jellyfinsettings": "", + "components.Settings.metadataProviderSelection": "", + "components.Settings.metadataProviderSettings": "", + "components.Settings.metadataSettings": "", + "components.Settings.metadataSettingsSaved": "", + "components.Settings.noSpecialCharacters": "", + "components.Settings.nooptions": "", + "components.Settings.notTested": "", + "components.Settings.operational": "", + "components.Settings.overrideRules": "", + "components.Settings.overrideRulesDescription": "", + "components.Settings.providerStatus": "", + "components.Settings.save": "", + "components.Settings.saving": "S'està desant…", + "components.Settings.scanbackground": "", + "components.Settings.searchKeywords": "", + "components.Settings.seriesMetadataProvider": "", + "components.Settings.settings": "", + "components.Settings.starttyping": "", + "components.Settings.syncJellyfin": "", + "components.Settings.syncing": "S'està sincronitzant", + "components.Settings.timeout": "Temps d'espera", + "components.Settings.tip": "Consell", + "components.Settings.tmdbProviderDoesnotWork": "", + "components.Settings.tvdbProviderDoesnotWork": "", + "components.Settings.yes": "", + "components.Setup.back": "", + "components.Setup.configemby": "", + "components.Setup.configjellyfin": "", + "components.Setup.configplex": "", + "components.Setup.configuremediaserver": "", + "components.Setup.librarieserror": "", + "components.Setup.servertype": "", + "components.Setup.signin": "", + "components.Setup.signinWithEmby": "", + "components.Setup.signinWithJellyfin": "", + "components.Setup.signinWithPlex": "", + "components.Setup.subtitle": "", + "components.StatusBadge.seasonnumber": "T{seasonNumber}", + "components.TitleCard.addToWatchList": "", + "components.TitleCard.watchlistSuccess": "", + "components.TvDetails.play": "", + "components.TvDetails.play4k": "", + "components.TvDetails.removefromwatchlist": "", + "components.TvDetails.watchlistDeleted": "", + "components.TvDetails.watchlistSuccess": "", + "components.UserList.ascending": "", + "components.UserList.descending": "", + "components.UserList.importedPlexUsersNoPassword": "", + "components.UserList.importedUsersNoPassword": "", + "components.UserList.importedfromJellyfin": "", + "components.UserList.validationUsername": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.description": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorExists": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorUnauthorized": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorUnknown": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.password": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.passwordRequired": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.save": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.saving": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.title": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.username": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.usernameRequired": "", + "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegion": "", + "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegionTip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.save": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.deletesubscription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.device": "", + "components.UserProfile.UserSettings.UserNotificationSettings.sound": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadId": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadIdTip": "", + "components.UserProfile.UserSettings.UserPasswordChange.localPasswordDescription": "", + "components.UserProfile.localWatchlist": "", + "i18n.blocklistDuplicateError": "", + "i18n.blocklistError": "", + "i18n.blocklisted": "", + "i18n.deleted": "", + "i18n.error": "", + "i18n.notificationLocale": "", + "i18n.partiallyblocklisted": "", + "i18n.removeFromBlocklistSuccess": "", + "i18n.removefromBlocklist": "", + "components.Settings.Notifications.userEmailRequired": "Requereix el correu electrònic de l'usuari", + "components.Settings.OverrideRuleModal.qualityprofile": "Perfil de qualitat", + "components.Settings.SettingsNetwork.forceIpv4First": "", + "components.Settings.SettingsNetwork.forceIpv4FirstTip": "", + "components.Settings.no": "", + "i18n.collection": "Col·lecció", + "i18n.completed": "", + "components.Settings.OverrideRuleModal.ruleUpdated": "La regla de substitució s'ha actualitzat correctament!", + "components.Settings.SettingsJobsCache.dnsNoCacheEntries": "Encara no s'ha emmagatzemat cap cerca DNS a la memòria cau.", + "components.Settings.SettingsJobsCache.misses": "", + "components.Settings.SettingsNetwork.trustProxy": "", + "components.Settings.copyBlocklistedTagsTip": "", + "components.TitleCard.watchlistCancel": "", + "components.TvDetails.watchlistError": "", + "components.Settings.OverrideRuleModal.tags": "Etiquetes", + "components.Settings.SettingsNetwork.network": "Plataforma", + "components.Settings.SettingsJobsCache.failures": "Errors", + "components.Settings.SettingsJobsCache.size": "Grandària", + "components.Settings.SettingsJobsCache.usersavatars": "Avatars dels usuaris", + "components.UserList.newJellyfinsigninenabled": "", + "components.Settings.SonarrModal.seriesType": "", + "components.Settings.jellyfinsettingsDescription": "", + "components.UserList.importfromJellyfinerror": "", + "components.UserList.username": "Nom d'usuari", + "i18n.blocklistSuccess": "", + "components.TvDetails.addtowatchlist": "", + "components.Settings.menuMetadataProviders": "", + "i18n.addToBlocklist": "Afegeix a la llista de bloqueig", + "components.Settings.failed": "", + "components.Common.QuickConnectModal.error": "Error", + "components.Common.QuickConnectModal.expired": "Codi caducat", + "components.Common.QuickConnectModal.expiredMessage": "Aquest codi de connexió ràpida ha caducat. Torna-ho a provar.", + "components.Common.QuickConnectModal.tryAgain": "Torneu-ho a provar", + "components.Common.QuickConnectModal.waitingForAuth": "Esperant autorització...", + "components.Login.JellyfinQuickConnectModal.cancel": "Cancel·la", + "components.Login.JellyfinQuickConnectModal.instructions": "Introdueix aquest codi a l'aplicació de {mediaServerName}", + "components.Login.JellyfinQuickConnectModal.subtitle": "Inicia la sessió amb connexió ràpida", + "components.Login.JellyfinQuickConnectModal.title": "Connexió ràpida", + "components.Login.quickconnect": "Connexió ràpida", + "components.Login.quickconnecterror": "La connexió ràpida ha fallat. Torneu-ho a provar.", + "components.ManageSlideOver.clearmediadataerror": "S'ha produït un error en esborrar les dades multimèdia.", + "components.ManageSlideOver.removemediaerror": "S'ha produït un error en esborrar el suport multimèdia.", + "components.RequestList.RequestItem.removemediaerror": "S'ha produït un error en esborrar el suport multimèdia.", + "components.RequestModal.AdvancedRequester.ignoreQuotaDescription": "Aquesta sol·licitud no comptarà per als límits de quota de l'usuari. Utilitzeu-la amb precaució.", + "components.RequestModal.AdvancedRequester.ignoreQuotaTitle": "Ometre de la quota d'usuari", + "components.Settings.Notifications.NotificationsGotify.embedPoster": "Pòster incrustat", + "components.Settings.Notifications.validationWebhookThreadId": "Heu de proporcionar un ID de fil de Discord vàlid", + "hooks.useQuickConnect.errorMessage": "No s'ha pogut iniciar la connexió ràpida. Torneu-ho a provar.", + "components.Settings.Notifications.webhookThreadId": "ID del fil", + "components.Settings.Notifications.webhookThreadIdTip": "L'ID del canal de fil on publicar notificacions. Deixeu-ho en blanc per publicar al canal webhook", + "components.Settings.SettingsAbout.versionCheckDisabled": "Comprovació de versió desactivada", + "components.Settings.Notifications.NotificationsNtfy.tags": "Etiquetes" } diff --git a/src/i18n/locale/cs.json b/src/i18n/locale/cs.json index a0469fdcc0..a719b70b98 100644 --- a/src/i18n/locale/cs.json +++ b/src/i18n/locale/cs.json @@ -507,7 +507,7 @@ "components.UserProfile.UserSettings.UserGeneralSettings.originallanguage": "Jazyk pro objevování", "components.UserProfile.UserSettings.UserGeneralSettings.plexuser": "Uživatel Plexu", "components.UserProfile.UserSettings.UserGeneralSettings.region": "Region pro objevování", - "components.UserProfile.UserSettings.UserNotificationSettings.discordId": "ID uživatele", + "components.UserProfile.UserSettings.UserNotificationSettings.discordId": "ID uživatelů", "components.UserProfile.UserSettings.UserNotificationSettings.notificationsettings": "Nastavení oznámení", "components.UserProfile.UserSettings.UserNotificationSettings.telegramChatId": "ID chatu", "components.UserProfile.UserSettings.UserPasswordChange.confirmpassword": "Potvrďte heslo", @@ -911,7 +911,7 @@ "components.UserList.validationpasswordminchars": "Heslo je příliš krátké; mělo by mít minimálně 8 znaků", "components.UserProfile.pastdays": "{type} (posledních {days} dnů)", "components.Settings.SettingsJobsCache.cacheflushed": "Mezipaměť {cachename} byla vyprázdněna.", - "components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "Musíte zadat platné ID uživatele", + "components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "Každé ID musí být platné ID uživatele Discordu", "components.Settings.SonarrModal.validationLanguageProfileRequired": "Musíte vybrat jazykový profil", "components.Settings.SonarrModal.validationNameRequired": "Musíte zadat název serveru", "components.UserProfile.UserSettings.UserGeneralSettings.displayName": "Zobrazované jméno", @@ -922,13 +922,10 @@ "components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverUserKey": "Musíte zadat platný klíč uživatele nebo skupiny", "components.UserList.usercreatedsuccess": "Uživatel úspěšně vytvořen!", "components.UserList.userdeleted": "Uživatel úspěšně odstraněn!", - "components.UserProfile.UserSettings.UserGeneralSettings.discordIdTip": "vícemístné identifikační číslo spojené s vaším uživatelským účtem Discord", - "components.UserProfile.UserSettings.UserGeneralSettings.discordId": "Uživatelské ID Discordu", "components.UserProfile.UserSettings.UserGeneralSettings.enableOverride": "Přepsat globální omezení", - "components.UserProfile.UserSettings.UserGeneralSettings.validationDiscordId": "Musíte zadat platné ID uživatele Discordu", "components.UserProfile.UserSettings.UserNotificationSettings.pgpPublicKey": "Veřejný klíč PGP", "components.UserProfile.UserSettings.UserNotificationSettings.pgpPublicKeyTip": "Šifrování e-mailových zpráv pomocí OpenPGP", - "components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "Vícemístné identifikační číslo spojené s vaším uživatelským účtem", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "Vícemístné identifikační číslo spojené s vaším uživatelským účtem. Pro více účtů v jedné domácnosti můžete přidat více než jedno ID uživatele Discordu.", "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessToken": "Přístupový token", "components.UserProfile.UserSettings.UserNotificationSettings.pushoverUserKey": "Klíč uživatele nebo skupiny", "components.UserProfile.UserSettings.UserNotificationSettings.telegramChatIdTipLong": "Zahajte chat, přidejte @get_id_bot a zadejte příkaz /my_id", @@ -1666,5 +1663,10 @@ "components.UserList.importedPlexUsersNoPassword": "Importovaní uživatelé nemají nastavené heslo pro {applicationTitle}. Pokud zakážete přihlášení přes Plex, budou si muset heslo nastavit ve svém profilu nebo pomocí odkazu pro obnovení hesla.", "components.UserList.importedUsersNoPassword": "Importovaní uživatelé nemají nastavené heslo pro {applicationTitle}. Pokud zakážete přihlášení přes {mediaServerName}, budou si muset heslo nastavit ve svém profilu nebo pomocí odkazu pro obnovení hesla.", "components.UserProfile.UserSettings.UserPasswordChange.localPasswordDescription": "Toto heslo slouží k přihlášení přes místní přihlašovací formulář {applicationTitle}. Jedná se o heslo odlišné od hesla k mediálnímu serveru.", - "i18n.notificationLocale": "Jazyk oznámení" + "i18n.notificationLocale": "Jazyk oznámení", + "components.Settings.Notifications.usePublicLogo": "Použít veřejné logo Seerr namísto loga instance", + "components.Settings.Notifications.usePublicLogoTip": "Pokud vaše instance Seerr není veřejně přístupná, povolte tuto volbu, aby e-mailoví klienti mimo vaši síť mohly obrázek zobrazit. Obrázek bude načten z veřejného repozitáře GitHub.", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdAdd": "Přidat ID uživatele", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdPlaceholder": "ID uživatele Discord", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdRemove": "Odebrat" } diff --git a/src/i18n/locale/da.json b/src/i18n/locale/da.json index 43bbd51af5..40cc5b86d1 100644 --- a/src/i18n/locale/da.json +++ b/src/i18n/locale/da.json @@ -634,7 +634,7 @@ "components.Settings.SettingsLogs.time": "Tidsstempel", "components.Settings.SettingsUsers.defaultPermissions": "Standardtilladelser", "components.Settings.SettingsUsers.defaultPermissionsTip": "Indledende tilladelser som tildeles nye brugere", - "components.Settings.SettingsUsers.localLogin": "Aktivér lokal login", + "components.Settings.SettingsUsers.localLogin": "Aktivér lokalt login", "components.Settings.SettingsUsers.localLoginTip": "Tillad brugere at logge ind med deres email-adresse og adgangskode", "components.Settings.SettingsUsers.movieRequestLimitLabel": "Global grænse for filmanmodninger", "components.Settings.SettingsUsers.newPlexLoginTip": "Tillad {mediaServerName}-brugere til at logge ind uden først at blive importeret", @@ -673,7 +673,7 @@ "components.Settings.sonarrsettings": "Sonarr-indstillinger", "components.Settings.ssl": "SSL", "components.Settings.startscan": "Start scanning", - "components.Settings.webAppUrlTip": "Du kan eventuelt dirigere brugere til webappen på din server i stedet for den \"hostede\" webapp", + "components.Settings.webAppUrlTip": "Du kan eventuelt omdirigere brugere til webappen på din server i stedet for https://app.plex.tv/desktop", "components.Settings.webAppUrl": "Webapp-URL", "components.Settings.webhook": "Webhook", "components.StatusBadge.status4k": "4K {status}", @@ -684,7 +684,7 @@ "components.TvDetails.originallanguage": "Originalsprog", "components.TvDetails.originaltitle": "Originaltitel", "components.UserList.importedfromplex": "{userCount} Plex {userCount, plural, one {user} other {users}} importeret.", - "components.UserList.localLoginDisabled": "Indstillingen Aktivér lokal login er i øjeblikket deaktiveret.", + "components.UserList.localLoginDisabled": "Indstillingen Aktivér lokalt login er i øjeblikket deaktiveret.", "components.UserList.validationpasswordminchars": "Adgangskoden er for kort; skal være mindst 8 tegn lang", "components.UserProfile.ProfileHeader.settings": "Redigér indstillinger", "components.UserProfile.UserSettings.UserGeneralSettings.accounttype": "Kontotype", @@ -726,8 +726,8 @@ "components.UserProfile.UserSettings.UserGeneralSettings.seriesrequestlimit": "Grænse for serieanmodninger", "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsSuccess": "Indstillinger gemt!", "components.UserProfile.UserSettings.UserGeneralSettings.user": "Bruger", - "components.UserProfile.UserSettings.UserNotificationSettings.discordId": "Bruger-ID", - "components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "Det flercifrede ID-nummer, der er knyttet til din brugerkonto", + "components.UserProfile.UserSettings.UserNotificationSettings.discordId": "Bruger-IDer", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "Det flercifrede ID-nummer, der er knyttet til din brugerkonto. Hvis der er flere konti i husstanden, kan du tilføje mere end ét Discord-bruger-ID.", "components.UserProfile.UserSettings.UserNotificationSettings.discordsettingsfailed": "Indstillinger for Discord-notifikationer kunne ikke gemmes.", "components.UserProfile.UserSettings.UserNotificationSettings.pgpPublicKeyTip": "Kryptér e-mails med OpenPGP", "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessToken": "Adgangstoken", @@ -820,7 +820,7 @@ "components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverApplicationToken": "Du skal angive et gyldigt applikationstoken", "components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverUserKey": "Du skal angive en gyldig bruger- eller gruppenøgle", "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramChatId": "Du skal angive et gyldigt chat-ID", - "components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "Du skal angive et gyldigt bruger-ID", + "components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "Hvert ID skal være et gyldigt Discord-bruger-ID", "components.UserProfile.UserSettings.UserNotificationSettings.validationPgpPublicKey": "Du skal angive en gyldig offentlig PGP-nøgle", "components.UserProfile.UserSettings.UserPasswordChange.confirmpassword": "Bekræft adgangskode", "components.UserProfile.UserSettings.UserPasswordChange.newpassword": "Ny adgangskode", @@ -948,12 +948,9 @@ "components.Settings.validationApiKey": "Du skal angive en API-nøgle", "components.UserProfile.recentlywatched": "Senest set", "components.Settings.toastTautulliSettingsSuccess": "Tautulli-indstillinger gemt!", - "components.UserProfile.UserSettings.UserGeneralSettings.discordId": "Discord-bruger-ID", "components.Settings.validationUrl": "Du skal angive en gyldig URL", "components.Settings.validationUrlBaseLeadingSlash": "URL-basen skal have en indledende skråstreg", - "components.UserProfile.UserSettings.UserGeneralSettings.discordIdTip": "Det flercifrede ID-nummer der er knyttet til din Discord-brugerkonto", "components.Settings.validationUrlBaseTrailingSlash": "URL-basen må ikke slutte med en efterfølgende skråstreg", - "components.UserProfile.UserSettings.UserGeneralSettings.validationDiscordId": "Du skal angive et gyldigt Discord-bruger-ID", "i18n.import": "Importér", "components.ManageSlideOver.manageModalMedia4k": "4K-medier", "components.Settings.externalUrl": "Ekstern URL", @@ -1450,7 +1447,7 @@ "components.Setup.configjellyfin": "Konfigurér Jellyfin", "components.Setup.configplex": "Konfigurér Plex", "components.Setup.configuremediaserver": "Konfigurér medieserver", - "components.Setup.librarieserror": "Validering mislykkedes. Slå venligst bibliotekerne til/fra igen for at fortsætte.", + "components.Setup.librarieserror": "Kunne ikke indlæse biblioteker fra din medieserver. Kontrollér, at den er tilgængelig, og at den stadig er konfigureret korrekt.", "components.Setup.servertype": "Vælg servertype", "components.Setup.signin": "Log ind", "components.Setup.signinWithEmby": "Indtast dine Emby-oplysninger", @@ -1666,5 +1663,51 @@ "components.UserList.importedUsersNoPassword": "Importerede brugere har ikke en adgangskode til {applicationTitle}. Hvis du deaktiverer {mediaServerName}-login, skal de oprette en adgangskode i deres profil eller via et nulstillingslink.", "components.UserProfile.UserSettings.UserPasswordChange.localPasswordDescription": "Denne adgangskode bruges til login via {applicationTitle}s lokale loginformular. Den er adskilt fra din medieserver-adgangskode.", "components.Settings.Notifications.useUserLocale": "Brug modtagerens sprogindstilling til notifikationer", - "i18n.notificationLocale": "Sprog til notifikationer" + "i18n.notificationLocale": "Sprog til notifikationer", + "components.Settings.Notifications.usePublicLogo": "Brug Seerrs offentlige logo i stedet for instansens logo", + "components.Settings.Notifications.usePublicLogoTip": "Hvis din Seerr-instans ikke er offentligt tilgængelig, skal du aktivere denne indstilling, så email-klienter uden for dit netværk kan vise billedet. Billedet hentes fra det offentlige GitHub-repository.", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdAdd": "Tilføj bruger-ID", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdPlaceholder": "Discord-bruger-ID", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdRemove": "Fjern", + "components.Settings.Notifications.validationWebhookThreadId": "Du skal angive et gyldigt Discord-tråd-ID", + "components.Settings.Notifications.webhookThreadId": "Tråd-ID", + "components.Settings.Notifications.webhookThreadIdTip": "ID’et på den trådkanal, som notifikationer skal sendes til. Lad feltet være tomt for at sende til webhook-kanalen", + "components.UserProfile.UserSettings.UserNotificationSettings.discordNotificationsNotEnabled": "Serverejeren har ikke aktiveret Discord-notifikationer. Disse oplysninger bruges kun, hvis serverejeren konfigurerer en ekstern tjeneste.", + "components.RequestModal.AdvancedRequester.ignoreQuotaDescription": "Denne anmodning tæller ikke med i brugerens kvotebegrænsninger. Brug med omtanke.", + "components.RequestModal.AdvancedRequester.ignoreQuotaTitle": "Omgå brugerkvote", + "components.Common.QuickConnectModal.error": "Fejl", + "components.Common.QuickConnectModal.expired": "Kode udløbet", + "components.Common.QuickConnectModal.tryAgain": "Prøv igen", + "components.Login.JellyfinQuickConnectModal.cancel": "Annullér", + "components.Login.JellyfinQuickConnectModal.instructions": "Indtast denne kode i din {mediaServerName}-app", + "components.Common.QuickConnectModal.expiredMessage": "Denne Lyn-forbind-kode er udløbet. Prøv igen.", + "components.Common.QuickConnectModal.waitingForAuth": "Venter på godkendelse...", + "components.Login.JellyfinQuickConnectModal.subtitle": "Log ind med Lyn-forbind", + "components.Login.JellyfinQuickConnectModal.title": "Lyn-forbind", + "components.Login.quickconnect": "Lyn-forbind", + "components.Login.quickconnecterror": "Lyn-forbind mislykkedes. Prøv igen.", + "components.UserList.importfromplexnochanges": "Importen fra Plex er fuldført. Ingen brugere blev oprettet eller opdateret.", + "components.UserList.importfromplexsynced": "Plex-brugere blev synkroniseret. Eksisterende brugere blev opdateret.", + "components.UserList.syncnotice": "Du kan stadig klikke på Synkronisér for at opdatere eksisterende Plex-brugere, f.eks. hvis deres email-adresse eller brugernavn er ændret.", + "components.UserProfile.UserSettings.LinkJellyfinModal.quickConnect": "Brug Lyn-forbind", + "components.UserProfile.UserSettings.LinkJellyfinQuickConnectModal.instructions": "Indtast denne kode i din {mediaServerName}-app", + "components.UserProfile.UserSettings.LinkJellyfinQuickConnectModal.subtitle": "Lyn-forbind", + "components.UserProfile.UserSettings.LinkJellyfinQuickConnectModal.title": "Forbind {mediaServerName}-konto", + "components.UserProfile.UserSettings.LinkJellyfinQuickConnectModal.usePassword": "Brug adgangskode i stedet", + "hooks.useQuickConnect.errorMessage": "Kunne ikke starte Lyn-forbind. Prøv igen.", + "i18n.sync": "Synkronisér", + "i18n.syncing": "Synkroniserer…", + "components.Settings.SettingsAbout.versionCheckDisabled": "Tjek af version deaktiveret", + "components.Settings.SettingsMain.versionCheck": "Tjek af version", + "components.Settings.SettingsMain.versionCheckTip": "Søg automatisk efter nye versioner på GitHub.", + "components.ManageSlideOver.clearmediadataerror": "Der opstod en fejl under rydning af mediedata.", + "components.ManageSlideOver.removemediaerror": "Der opstod en fejl under fjernelse af medie.", + "components.RequestList.RequestItem.removemediaerror": "Der opstod en fejl under fjernelse af mediet.", + "components.Settings.Notifications.NotificationsGotify.embedPoster": "Indlejr plakat", + "components.Settings.jellyfinSyncFailedConnectionError": "Kunne ikke oprette forbindelse til {mediaServerName}-serveren. Kontrollér, at den kører og kan nås fra Seerr.", + "components.Settings.toastPlexSyncFailure": "Synkronisering af Plex-biblioteker mislykkedes.", + "components.Settings.toggleLibraryFailure": "Kunne ikke opdatere biblioteket.", + "components.UserList.searchUsers": "Søg…", + "components.Settings.Notifications.NotificationsNtfy.tags": "Tags", + "components.Settings.Notifications.NotificationsNtfy.tagsPlaceholder": "eyes,warning" } diff --git a/src/i18n/locale/de.json b/src/i18n/locale/de.json index df27316512..b464f227ff 100644 --- a/src/i18n/locale/de.json +++ b/src/i18n/locale/de.json @@ -1014,13 +1014,10 @@ "components.UserProfile.UserSettings.UserGeneralSettings.languageDefault": "Standard ({language})", "components.UserProfile.UserSettings.UserNotificationSettings.webpush": "Web Push", "components.UserProfile.UserSettings.UserGeneralSettings.applanguage": "Anzeigesprache", - "components.UserProfile.UserSettings.UserGeneralSettings.discordId": "Discord Benutzer ID", - "components.UserProfile.UserSettings.UserGeneralSettings.discordIdTip": "Die mehrstellige ID-Nummer deines Discord-Accounts", "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmovies": "Filme automatisch aus Plex-Merkliste anfragen", "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmoviestip": "Automatisch Filme aus deiner Plex Merkliste anfragen", "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseries": "Serien automatisch aus Plex-Merkliste anfragen", "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseriestip": "Automatisch Serien aus deiner Plex Merkliste anfragen", - "components.UserProfile.UserSettings.UserGeneralSettings.validationDiscordId": "Du musst eine gültige Discord Benutzer ID angeben", "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessToken": "Zugangs-Token", "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessTokenTip": "Erstelle ein Token aus deinen Kontoeinstellungen", "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletsettingsfailed": "Die Einstellungen für Pushbullet-Benachrichtigungen konnten nicht gespeichert werden.", @@ -1666,5 +1663,10 @@ "components.UserProfile.UserSettings.UserPasswordChange.localPasswordDescription": "Dieses Passwort wird für die lokale Anmeldung bei {applicationTitle} verwendet. Es ist unabhängig von deinem Medienserver-Passwort.", "i18n.error": "Etwas ist schiefgelaufen. Bitte versuche es erneut.", "i18n.notificationLocale": "Benachrichtigungssprache", - "i18n.partiallyblocklisted": "Teilweise blockiert" + "i18n.partiallyblocklisted": "Teilweise blockiert", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdAdd": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdPlaceholder": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdRemove": "", + "components.Settings.Notifications.usePublicLogo": "", + "components.Settings.Notifications.usePublicLogoTip": "" } diff --git a/src/i18n/locale/el.json b/src/i18n/locale/el.json index 6fe9481cce..3169a23891 100644 --- a/src/i18n/locale/el.json +++ b/src/i18n/locale/el.json @@ -830,7 +830,6 @@ "components.TvDetails.productioncountries": "Παραγωγή {countryCount, plural, one {Country} other {Countries}}", "components.TvDetails.rtcriticsscore": "Τοματόμετρο Rotten Tomatoes", "components.TvDetails.seasonnumber": "Σεζόν {seasonNumber}", - "components.UserProfile.UserSettings.UserGeneralSettings.validationDiscordId": "Πρέπει να εισάγετε ένα έγκυρο αναγνωριστικό χρήστη Discord", "components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverUserKey": "Πρέπει να δώσετε ένα έγκυρο κλειδί χρήστη ή ομάδας", "components.UserProfile.emptywatchlist": "Περιεχόμενο που εισάγεται στη λίστα παρακολούθησης του Plex θα εμφανίζεται εδώ.", "components.UserProfile.recentlywatched": "Είδατε πρόσφατα", @@ -1076,7 +1075,6 @@ "components.TitleCard.mediaerror": "{mediaType} Δε βρέθηκε", "components.TitleCard.tvdbid": "TheTVDB ID", "components.TvDetails.Season.noepisodes": "Η λίστα επεισοδίων δεν είναι διαθέσιμη.", - "components.UserProfile.UserSettings.UserGeneralSettings.discordIdTip": "Ο multi-digit ID αριθμός που σχετίζεται με τον λογαριασμό σας στο Discord", "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletsettingsfailed": "Οι ρυθμίσεις ειδοποιήσεων Pushbullet απέτυχαν να αποθηκευτούν.", "components.UserProfile.UserSettings.UserNotificationSettings.pushoverUserKey": "Κλειδί χρήστη ή ομάδας", "components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverApplicationToken": "Πρέπει να δώσετε ένα έγκυρο token εφαρμογής", @@ -1190,7 +1188,6 @@ "components.StatusChecker.appUpdatedDescription": "Παρακαλώ πατήστε το κουμπί παρακάτω ώστε να γίνει επαναφόρτωση της εφαρμογής.", "components.TvDetails.status4k": "4Κ {status}", "components.UserList.newplexsigninenabled": "Η ρύθμιση Ενεργοποίηση νέας εισόδου Plex είναι ενεργοποιημένη. Οι χρήστες Plex με πρόσβαση στη βιβλιοθήκη δε χρειάζεται να γίνουν εισαγωγή ώστε να συνδεθούν.", - "components.UserProfile.UserSettings.UserGeneralSettings.discordId": "Αναγνωριστικό χρήστη Discord", "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseriestip": "Ζητήστε αυτόματα σειρές που βρίσκονται στη λίστα παρακολούθησης του Plex", "components.UserProfile.plexwatchlist": "Λίστα παρακολούθησης Plex", "components.MovieDetails.imdbuserscore": "Βαθμολογία χρηστών IMDB", @@ -1199,5 +1196,477 @@ "components.Settings.Notifications.NotificationsPushover.sound": "Ήχος ειδοποίησης", "components.UserProfile.UserSettings.UserNotificationSettings.deviceDefault": "Προεπιλογή συσκευής", "components.UserProfile.UserSettings.UserNotificationSettings.sound": "Ήχος ειδοποίησης", - "components.Settings.Notifications.NotificationsPushover.deviceDefault": "Προεπιλογή συσκευής" + "components.Settings.Notifications.NotificationsPushover.deviceDefault": "Προεπιλογή συσκευής", + "components.Settings.no": "", + "components.Discover.timeWindowWeek": "", + "components.Settings.overrideRulesDescription": "", + "components.Settings.SettingsNetwork.dnsCacheHoverTip": "", + "components.Blocklist.blocklistNotFoundError": "", + "components.Login.validationUrlBaseLeadingSlash": "", + "components.Login.validationUrlBaseTrailingSlash": "", + "components.Settings.Notifications.userEmailRequired": "", + "components.Setup.servertype": "", + "components.Login.hostname": "", + "components.Blocklist.filterManual": "", + "components.Blocklist.mediaTmdbId": "", + "components.Blocklist.mediaType": "", + "components.Blocklist.showAllBlocklisted": "", + "components.Layout.Sidebar.blocklist": "", + "components.Discover.RecentRequestsSlider.unableToConnect": "", + "components.DiscoverTvUpcoming.upcomingtv": "", + "components.Setup.signinWithPlex": "", + "components.Login.enablessl": "", + "components.Discover.FilterSlideover.excludeKeywords": "", + "components.IssueList.IssueItem.descriptionpreview": "", + "components.MetadataSelector.tmdbLabel": "", + "components.Login.adminerror": "", + "components.Login.credentialerror": "", + "components.Layout.UserWarnings.emailInvalid": "", + "components.Layout.UserWarnings.emailRequired": "", + "components.Layout.UserWarnings.passwordRequired": "", + "components.Login.emailtooltip": "", + "components.MovieDetails.watchlistSuccess": "", + "components.Login.tipEmailHasTrailingWhitespace": "", + "components.Login.tipUsernameHasTrailingWhitespace": "", + "components.Login.title": "", + "components.Login.urlBase": "", + "components.Login.username": "", + "components.Login.validationUrlTrailingSlash": "", + "components.Blocklist.blocklistSettingsDescription": "", + "components.Blocklist.blocklistedby": "", + "components.Blocklist.blocklistsettings": "", + "components.Login.description": "", + "components.Login.initialsignin": "", + "components.Settings.SettingsNetwork.apiRequestTimeout": "", + "components.CollectionDetails.removefromblocklistpartialcount": "", + "components.Discover.FilterSlideover.certification": "", + "components.Discover.FilterSlideover.status": "", + "components.Login.servertype": "", + "component.BlocklistBlock.blocklistdate": "", + "component.BlocklistBlock.blocklistedby": "", + "component.BlocklistModal.blocklisting": "", + "components.Layout.UserWarnings.profileIncomplete": "", + "components.Login.emailAlreadyTaken": "", + "components.Login.invalidurlerror": "", + "components.Login.jellyfinLocalLoginHint": "", + "components.Login.loginwithapp": "", + "components.Login.noadminerror": "", + "components.Login.orsigninwith": "", + "components.Login.port": "", + "components.Login.save": "", + "components.Login.saveFailed": "", + "components.Login.saving": "", + "components.Login.signinwithjellyfin": "", + "components.Login.validationhostformat": "", + "components.Login.validationusernamerequired": "", + "components.ManageSlideOver.manageModalRemoveMediaWarning": "", + "components.ManageSlideOver.removearr4k": "", + "components.MetadataSelector.selectMetdataProvider": "", + "components.MetadataSelector.tvdbLabel": "", + "components.MovieDetails.addtowatchlist": "", + "components.MovieDetails.downloadstatus": "", + "components.MovieDetails.watchlistDeleted": "", + "components.MovieDetails.watchlistError": "", + "components.PermissionEdit.blocklistedItemsDescription": "", + "components.PermissionEdit.manageblocklist": "", + "components.PermissionEdit.manageblocklistDescription": "", + "components.PermissionEdit.viewblocklistedItems": "", + "components.PermissionEdit.viewblocklistedItemsDescription": "", + "components.RequestList.RequestItem.failedmodify": "", + "components.RequestList.RequestItem.profileName": "", + "components.RequestList.RequestItem.removearr": "", + "components.Selector.CertificationSelector.errorLoading": "", + "components.Selector.CertificationSelector.maxRating": "", + "components.Selector.CertificationSelector.minRating": "", + "components.Selector.CertificationSelector.noOptions": "", + "components.Selector.CertificationSelector.selectCertification": "", + "components.Selector.CertificationSelector.selectCountry": "", + "components.Selector.inProduction": "", + "components.Selector.pilot": "", + "components.Selector.searchStatus": "", + "components.Selector.searchUsers": "", + "components.Settings.Notifications.NotificationsGotify.priority": "", + "components.Settings.Notifications.NotificationsGotify.validationPriorityRequired": "", + "components.Settings.Notifications.NotificationsNtfy.agentenabled": "", + "components.Settings.Notifications.NotificationsNtfy.embedPoster": "", + "components.Settings.Notifications.NotificationsNtfy.ntfysettingsfailed": "", + "components.Settings.Notifications.NotificationsNtfy.ntfysettingssaved": "", + "components.Settings.Notifications.NotificationsNtfy.password": "", + "components.Settings.Notifications.NotificationsNtfy.priority": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestFailed": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestSending": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestSuccess": "", + "components.Settings.Notifications.NotificationsNtfy.token": "", + "components.Settings.Notifications.NotificationsNtfy.tokenAuth": "", + "components.Settings.Notifications.NotificationsNtfy.topic": "", + "components.Settings.Notifications.NotificationsNtfy.username": "", + "components.Settings.Notifications.NotificationsNtfy.validationNtfyTopic": "", + "components.Settings.Notifications.NotificationsNtfy.validationNtfyUrl": "", + "components.Settings.Notifications.NotificationsNtfy.validationPriorityRequired": "", + "components.Settings.Notifications.NotificationsPushover.embedPoster": "", + "components.Settings.Notifications.NotificationsSlack.embedPoster": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersKey": "", + "components.Settings.Notifications.messageThreadId": "", + "components.Settings.Notifications.messageThreadIdTip": "", + "components.Settings.SettingsMain.validationUrl": "", + "components.Settings.SonarrModal.enableSearchHelp": "", + "components.Settings.SonarrModal.syncEnabledHelp": "", + "components.Settings.starttyping": "", + "components.UserList.sortByRequests": "", + "i18n.removefromBlocklist": "", + "i18n.specials": "", + "components.UserProfile.localWatchlist": "", + "components.Blocklist.mediaName": "", + "components.PermissionEdit.blocklistedItems": "", + "components.Settings.connectionTestFailed": "", + "i18n.completed": "", + "components.Login.back": "", + "components.TvDetails.addtowatchlist": "", + "components.UserProfile.UserSettings.UserGeneralSettings.email": "", + "components.UserProfile.UserSettings.UserGeneralSettings.streamingRegion": "", + "components.UserProfile.UserSettings.UserGeneralSettings.streamingRegionTip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmail": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmailEmpty": "", + "components.UserProfile.UserSettings.UserGeneralSettings.validationemailformat": "", + "components.UserProfile.UserSettings.UserGeneralSettings.validationemailrequired": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.deleteFailed": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.errorUnknown": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.linkedAccounts": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.linkedAccountsHint": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.noLinkedAccounts": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.noPermissionDescription": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorUnauthorized": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.browser": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.created": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.deletesubscription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.managedevices": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.nodevicestoshow": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.type": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.unknown": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeendisabled": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdAdd": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdPlaceholder": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdRemove": "", + "components.Login.urlBaseHelp": "", + "components.Login.validationEmailFormat": "", + "components.Login.validationPortRequired": "", + "components.Login.validationemailformat": "", + "components.Login.validationhostrequired": "", + "components.Login.validationservertyperequired": "", + "components.MovieDetails.openradarr": "", + "components.MovieDetails.openradarr4k": "", + "components.MovieDetails.play": "", + "components.RequestList.sortDirection": "", + "components.RequestList.unableToConnect": "", + "components.Selector.ended": "", + "components.Settings.OverrideRuleModal.create": "", + "components.Settings.Notifications.NotificationsWebPush.embedPoster": "", + "components.Settings.Notifications.NotificationsWebhook.customHeaders": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersAdd": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersAuthConflict": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersIncomplete": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersRemove": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersTip": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersValue": "", + "components.Settings.Notifications.NotificationsWebhook.supportVariables": "", + "components.Settings.Notifications.NotificationsWebhook.supportVariablesTip": "", + "components.Settings.Notifications.useUserLocale": "", + "components.Settings.Notifications.validationMessageThreadId": "", + "components.Settings.Notifications.validationWebhookRoleId": "", + "components.Settings.Notifications.webhookRoleId": "", + "components.Settings.Notifications.webhookRoleIdTip": "", + "components.Settings.OverrideRuleModal.conditions": "", + "components.Settings.SettingsJobsCache.dnscacheage": "", + "components.Settings.SettingsJobsCache.dnscachehits": "", + "components.Settings.SettingsMain.apikeyCopied": "", + "components.Settings.SettingsMain.blocklistLanguage": "", + "components.Settings.SettingsMain.blocklistLanguageTip": "", + "components.Settings.SettingsMain.blocklistRegion": "", + "components.Settings.SettingsMain.blocklistRegionTip": "", + "components.Settings.SettingsMain.blocklistedTags": "", + "components.Settings.SettingsMain.blocklistedTagsLimit": "", + "components.Settings.SettingsMain.blocklistedTagsLimitTip": "", + "components.Settings.SettingsNetwork.csrfProtection": "", + "components.Settings.SettingsNetwork.network": "", + "components.Settings.SettingsNetwork.networkDisclaimer": "", + "components.Settings.SettingsNetwork.networksettings": "", + "components.Settings.SettingsNetwork.networksettingsDescription": "", + "components.Settings.SettingsNetwork.proxyBypassFilter": "", + "components.Settings.SettingsNetwork.proxyBypassFilterTip": "", + "components.Settings.SettingsNetwork.proxyBypassLocalAddresses": "", + "components.Settings.SettingsNetwork.proxyEnabledTip": "", + "components.Settings.SettingsNetwork.proxyPassword": "", + "components.Settings.SettingsNetwork.proxyPort": "", + "components.Settings.SettingsNetwork.proxySsl": "", + "components.Settings.SettingsNetwork.proxyUser": "", + "components.Settings.SettingsNetwork.toastSettingsFailure": "", + "components.Settings.SettingsNetwork.toastSettingsSuccess": "", + "components.Settings.SettingsNetwork.trustProxy": "", + "components.Settings.SettingsNetwork.trustProxyTip": "", + "components.Settings.SettingsNetwork.validationApiRequestTimeout": "", + "components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "", + "components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "", + "components.Settings.SettingsUsers.disabledMediaServerLoginWarning": "", + "components.Settings.SettingsUsers.loginMethodsTip": "", + "components.Settings.SettingsUsers.mediaServerLoginTip": "", + "components.Settings.SonarrModal.apiKeyHelp": "", + "components.Settings.SonarrModal.baseUrlHelp": "", + "components.Settings.SonarrModal.externalUrlHelp": "", + "components.Settings.SonarrModal.monitorNewItemsHelp": "", + "components.Settings.SonarrModal.server4kHelp": "", + "components.Settings.noSpecialCharacters": "", + "components.Settings.notTested": "", + "components.Settings.timeout": "", + "components.UserList.importedPlexUsersNoPassword": "", + "components.UserList.importedUsersNoPassword": "", + "components.UserList.importedfromJellyfin": "", + "components.UserProfile.UserSettings.UserGeneralSettings.mediaServerUser": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorExists": "", + "i18n.partiallyblocklisted": "", + "components.Settings.Notifications.usePublicLogo": "", + "components.Settings.Notifications.usePublicLogoTip": "", + "components.Settings.OverrideRuleModal.conditionsDescription": "", + "components.Settings.OverrideRuleModal.createrule": "", + "components.Settings.OverrideRuleModal.editrule": "", + "components.Settings.OverrideRuleModal.genres": "", + "components.Settings.OverrideRuleModal.keywords": "", + "components.Settings.OverrideRuleModal.languages": "", + "components.Settings.OverrideRuleModal.notagoptions": "", + "components.Settings.OverrideRuleModal.qualityprofile": "", + "components.Settings.OverrideRuleModal.selectQualityProfile": "", + "components.Settings.OverrideRuleModal.selectRootFolder": "", + "components.Settings.OverrideRuleModal.selectService": "", + "components.Settings.OverrideRuleModal.selecttags": "", + "components.Settings.OverrideRuleModal.settingsDescription": "", + "components.Settings.OverrideRuleModal.tags": "", + "components.Settings.OverrideRuleModal.users": "", + "components.Settings.OverrideRuleTile.conditions": "", + "components.Settings.OverrideRuleTile.genre": "", + "components.Settings.OverrideRuleTile.keywords": "", + "components.Settings.OverrideRuleTile.language": "", + "components.Settings.OverrideRuleTile.qualityprofile": "", + "components.Settings.OverrideRuleTile.rootfolder": "", + "components.Settings.OverrideRuleTile.settings": "", + "components.Settings.OverrideRuleTile.tags": "", + "components.Settings.OverrideRuleTile.users": "", + "components.Settings.RadarrModal.apiKeyHelp": "", + "components.Settings.RadarrModal.baseUrlHelp": "", + "components.Settings.RadarrModal.enableSearchHelp": "", + "components.Settings.RadarrModal.externalUrlHelp": "", + "components.Settings.RadarrModal.server4kHelp": "", + "components.Settings.RadarrModal.syncEnabledHelp": "", + "components.Settings.SettingsAbout.contribute": "", + "components.Settings.SettingsAbout.supportseerr": "", + "components.Settings.SettingsJobsCache.dnsCache": "", + "components.Settings.SettingsJobsCache.dnsCacheGlobalStats": "", + "components.Settings.SettingsJobsCache.dnsCacheGlobalStatsDescription": "", + "components.Settings.SettingsJobsCache.dnsNoCacheEntries": "", + "components.Settings.SettingsJobsCache.dnscacheactiveaddress": "", + "components.Settings.SettingsJobsCache.dnscacheflushed": "", + "components.Settings.SettingsJobsCache.dnscachemisses": "", + "components.Settings.SettingsJobsCache.dnscachename": "", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorDays": "", + "components.Settings.SettingsJobsCache.flushdnscache": "", + "components.Settings.SettingsJobsCache.hitRate": "", + "components.Settings.SettingsJobsCache.hits": "", + "components.Settings.SettingsJobsCache.ipv4Fallbacks": "", + "components.Settings.SettingsJobsCache.misses": "", + "components.Settings.SettingsJobsCache.plex-refresh-token": "", + "components.Settings.SettingsJobsCache.process-blocklisted-tags": "", + "components.Settings.SettingsJobsCache.size": "", + "components.Settings.SettingsJobsCache.usersavatars": "", + "components.Settings.SettingsMain.blocklistedTagsTip": "", + "components.Settings.SettingsMain.discoverRegion": "", + "components.Settings.SettingsMain.discoverRegionTip": "", + "components.Settings.SettingsMain.enableSpecialEpisodes": "", + "components.Settings.SettingsMain.hideAvailableTip": "", + "components.Settings.SettingsMain.hideBlocklisted": "", + "components.Settings.SettingsMain.hideBlocklistedTip": "", + "components.Settings.SettingsMain.streamingRegion": "", + "components.Settings.SettingsMain.streamingRegionTip": "", + "components.Settings.SettingsMain.youtubeUrl": "", + "components.Settings.SettingsMain.youtubeUrlTip": "", + "components.Settings.SettingsNetwork.apiRequestTimeoutTip": "", + "components.Settings.SettingsNetwork.csrfProtectionHoverTip": "", + "components.Settings.SettingsNetwork.csrfProtectionTip": "", + "components.Settings.SettingsNetwork.dnsCache": "", + "components.Settings.SettingsNetwork.dnsCacheForceMaxTtl": "", + "components.Settings.SettingsNetwork.dnsCacheForceMinTtl": "", + "components.Settings.SettingsNetwork.dnsCacheTip": "", + "components.Settings.SettingsNetwork.docs": "", + "components.Settings.SettingsNetwork.forceIpv4First": "", + "components.Settings.SettingsNetwork.forceIpv4FirstTip": "", + "components.Settings.allChosenProvidersAreOperational": "", + "components.Settings.animeMetadataProvider": "", + "components.Settings.blocklistedTagImportInstructions": "", + "components.Settings.blocklistedTagImportTitle": "", + "components.Settings.blocklistedTagsText": "", + "components.Settings.chooseProvider": "", + "components.Settings.clearBlocklistedTagsConfirm": "", + "components.Settings.copyBlocklistedTags": "", + "components.Settings.copyBlocklistedTagsEmpty": "", + "components.Settings.copyBlocklistedTagsTip": "", + "components.Settings.failed": "", + "components.Settings.failedToSaveMetadataSettings": "", + "components.Settings.general": "", + "components.Settings.importBlocklistedTagsTip": "", + "components.Settings.invalidKeyword": "", + "components.Settings.invalidurlerror": "", + "components.Settings.jellyfinForgotPasswordUrl": "", + "components.Settings.jellyfinSettings": "", + "components.Settings.jellyfinSettingsDescription": "", + "components.Settings.jellyfinSettingsFailure": "", + "components.Settings.jellyfinSettingsSuccess": "", + "components.Settings.jellyfinSyncFailedAutomaticGroupedFolders": "", + "components.Settings.jellyfinSyncFailedGenericError": "", + "components.Settings.jellyfinSyncFailedNoLibrariesFound": "", + "components.Settings.jellyfinlibraries": "", + "components.Settings.jellyfinlibrariesDescription": "", + "components.Settings.jellyfinsettings": "", + "components.Settings.jellyfinsettingsDescription": "", + "components.Settings.manualscanDescriptionJellyfin": "", + "components.Settings.menuJellyfinSettings": "", + "components.Settings.menuNetwork": "", + "components.Settings.metadataProviderSelection": "", + "components.Settings.metadataSettings": "", + "components.Settings.metadataSettingsSaved": "", + "components.Settings.save": "", + "components.Settings.saving": "", + "components.Settings.scanbackground": "", + "components.Settings.searchKeywords": "", + "components.Settings.seriesMetadataProvider": "", + "components.Settings.settings": "", + "components.Settings.syncJellyfin": "", + "components.Settings.syncing": "", + "components.Settings.tip": "", + "components.Settings.tmdbProviderDoesnotWork": "", + "components.Settings.tvdbProviderDoesnotWork": "", + "components.Settings.valueRequired": "", + "components.Settings.yes": "", + "components.Setup.back": "", + "components.Setup.configemby": "", + "components.Setup.configjellyfin": "", + "components.Setup.configplex": "", + "components.Setup.configuremediaserver": "", + "components.Setup.librarieserror": "", + "components.Setup.signin": "", + "components.Setup.subtitle": "", + "components.StatusBadge.seasonnumber": "", + "components.TitleCard.addToWatchList": "", + "components.TitleCard.watchlistCancel": "", + "components.TitleCard.watchlistDeleted": "", + "components.TitleCard.watchlistError": "", + "components.TitleCard.watchlistSuccess": "", + "components.TvDetails.play": "", + "components.TvDetails.play4k": "", + "components.TvDetails.removefromwatchlist": "", + "components.TvDetails.watchlistDeleted": "", + "components.TvDetails.watchlistError": "", + "components.TvDetails.watchlistSuccess": "", + "components.UserList.descending": "", + "components.UserList.mediaServerUser": "", + "components.UserList.newJellyfinsigninenabled": "", + "components.UserList.noJellyfinuserstoimport": "", + "components.UserList.sortBy": "", + "components.UserList.sortByJoined": "", + "components.UserList.sortByRole": "", + "components.UserList.sortByType": "", + "components.UserList.toggleSortDirectionAria": "", + "components.UserList.username": "", + "components.UserList.validationUsername": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.description": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorExists": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorUnauthorized": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorUnknown": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.password": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.passwordRequired": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.save": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.saving": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.title": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.username": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.usernameRequired": "", + "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegion": "", + "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegionTip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.save": "", + "components.UserProfile.UserSettings.UserGeneralSettings.saving": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.device": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablewebpush": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablingwebpusherror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablewebpush": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablingwebpusherror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.engine": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadId": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadIdTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramMessageThreadId": "", + "components.UserProfile.UserSettings.UserPasswordChange.localPasswordDescription": "", + "components.UserProfile.UserSettings.menuLinkedAccounts": "", + "i18n.addToBlocklist": "", + "i18n.blocklist": "", + "i18n.blocklistDuplicateError": "", + "i18n.blocklistError": "", + "i18n.blocklistSuccess": "", + "i18n.blocklisted": "", + "i18n.deleted": "", + "i18n.error": "", + "i18n.notificationLocale": "", + "i18n.removeFromBlocklistSuccess": "", + "components.RequestCard.failedmodify": "", + "components.Settings.Notifications.NotificationsNtfy.url": "", + "components.Settings.Notifications.embedPoster": "", + "components.Settings.OverrideRuleModal.serviceDescription": "", + "components.Settings.SettingsJobsCache.failures": "", + "components.MovieDetails.play4k": "", + "components.Settings.clickTest": "", + "components.Settings.overrideRules": "", + "components.UserList.sortByUser": "", + "components.UserList.toggleSortDirection": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleted": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleteerror": "", + "components.Selector.CertificationSelector.starttyping": "", + "components.Settings.SettingsMain.validationUrlTrailingSlash": "", + "components.Settings.addrule": "", + "components.Blocklist.filterBlocklistedTags": "", + "components.Selector.canceled": "", + "components.Settings.Notifications.NotificationsWebhook.webhookUrlTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.operatingsystem": "", + "components.Settings.Notifications.NotificationsNtfy.usernamePasswordAuth": "", + "components.Settings.manualscanJellyfin": "", + "components.Blocklist.blocklistdate": "", + "components.Selector.returningSeries": "", + "components.Settings.apiKey": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeenenabled": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingsfailed": "", + "components.Settings.Notifications.NotificationsNtfy.validationTypes": "", + "components.Settings.SettingsNetwork.proxyHostname": "", + "components.Settings.providerStatus": "", + "components.Settings.SonarrModal.monitorNewItems": "", + "components.UserList.importfromJellyfin": "", + "components.Login.validationEmailRequired": "", + "components.Settings.nooptions": "", + "components.Setup.signinWithEmby": "", + "components.Settings.OverrideRuleModal.settings": "", + "components.ManageSlideOver.removearr": "", + "components.Selector.planned": "", + "components.Settings.SettingsNetwork.validationProxyPort": "", + "components.Settings.OverrideRuleModal.ruleUpdated": "", + "components.Settings.OverrideRuleModal.service": "", + "components.Settings.operational": "", + "components.UserList.ascending": "", + "components.UserList.importfromJellyfinerror": "", + "components.Setup.signinWithJellyfin": "", + "components.Discover.timeWindowDay": "", + "components.Login.initialsigningin": "", + "components.MovieDetails.removefromwatchlist": "", + "components.Settings.OverrideRuleModal.rootfolder": "", + "components.Settings.OverrideRuleModal.ruleCreated": "", + "components.Settings.SettingsJobsCache.dnsCacheDescription": "", + "components.Settings.SettingsNetwork.proxyEnabled": "", + "components.Settings.SettingsUsers.atLeastOneAuth": "", + "components.Settings.SettingsUsers.loginMethods": "", + "components.Settings.menuMetadataProviders": "", + "components.Settings.SettingsUsers.mediaServerLogin": "", + "components.Settings.metadataProviderSettings": "", + "components.Settings.SettingsAbout.aboutseerr": "" } diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index 31ef033898..f3facacf5d 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -5,6 +5,7 @@ "components.AirDateBadge.airedrelative": "Aired {relativeTime}", "components.AirDateBadge.airsrelative": "Airing {relativeTime}", "components.AppDataWarning.dockerVolumeMissingDescription": "The {appDataPath} volume mount was not configured properly. All data will be cleared when the container is stopped or restarted.", + "components.AuthorDetails.books": "Books", "components.Blocklist.blocklistNotFoundError": "{title} is not blocklisted.", "components.Blocklist.blocklistSettingsDescription": "Manage blocklisted media.", "components.Blocklist.blocklistdate": "date", @@ -16,11 +17,39 @@ "components.Blocklist.mediaTmdbId": "tmdb Id", "components.Blocklist.mediaType": "Type", "components.Blocklist.showAllBlocklisted": "Show All Blocklisted Media", + "components.BookDetails.audiobookduration": "Audiobook Duration", + "components.BookDetails.author": "Author", + "components.BookDetails.downloadstatus": "Download Status", + "components.BookDetails.format": "Format", + "components.BookDetails.isbn": "ISBN", + "components.BookDetails.language": "Language", + "components.BookDetails.managebook": "Manage Book", + "components.BookDetails.markAudioavailable": "Mark Audiobook as Available", + "components.BookDetails.markavailable": "Mark as Available", + "components.BookDetails.narrators": "Narrator{narratorCount, plural, one {} other {s}}", + "components.BookDetails.openreadarr": "Open Book in Readarr", + "components.BookDetails.openreadarrAudio": "Open Audiobook in Readarr", + "components.BookDetails.overview": "Overview", + "components.BookDetails.overviewunavailable": "Overview unavailable.", + "components.BookDetails.publisher": "Publisher", + "components.BookDetails.releasedate": "Release Date", + "components.BookDetails.reportissue": "Report an Issue", "components.CollectionDetails.numberofmovies": "{count} Movies", "components.CollectionDetails.overview": "Overview", "components.CollectionDetails.removefromblocklistpartialcount": "{removeLabel} ({count, plural, one {# movie} other {# movies}})", "components.CollectionDetails.requestcollection": "Request Collection", "components.CollectionDetails.requestcollection4k": "Request Collection in 4K", + "components.Common.HardcoverSetup.contactadmin": "Book support is not currently configured on this server.", + "components.Common.HardcoverSetup.description": "To browse, search, and request books, a Hardcover API key needs to be configured. You can get one from your Hardcover account.", + "components.Common.HardcoverSetup.getapikey": "Get an API Key", + "components.Common.HardcoverSetup.opensettings": "Open Settings", + "components.Common.HardcoverSetup.supportnote": "Consider supporting Hardcover if you enjoy the book features.", + "components.Common.HardcoverSetup.title": "Book Support Requires Hardcover", + "components.Common.QuickConnectModal.error": "Error", + "components.Common.QuickConnectModal.expired": "Code Expired", + "components.Common.QuickConnectModal.expiredMessage": "This Quick Connect code has expired. Please try again.", + "components.Common.QuickConnectModal.tryAgain": "Try Again", + "components.Common.QuickConnectModal.waitingForAuth": "Waiting for authorization...", "components.Discover.CreateSlider.addSlider": "Add Slider", "components.Discover.CreateSlider.addcustomslider": "Create Custom Slider", "components.Discover.CreateSlider.addfail": "Failed to create new slider.", @@ -42,6 +71,14 @@ "components.Discover.CreateSlider.starttyping": "Starting typing to search.", "components.Discover.CreateSlider.validationDatarequired": "You must provide a data value.", "components.Discover.CreateSlider.validationTitlerequired": "You must provide a title.", + "components.Discover.DiscoverBooks.activefilters": "{count, plural, one {# Active Filter} other {# Active Filters}}", + "components.Discover.DiscoverBooks.discoverbooks": "Books", + "components.Discover.DiscoverBooks.sortPopularityAsc": "Popularity Ascending", + "components.Discover.DiscoverBooks.sortPopularityDesc": "Popularity Descending", + "components.Discover.DiscoverBooks.sortReleaseDateAsc": "Release Date Ascending", + "components.Discover.DiscoverBooks.sortReleaseDateDesc": "Release Date Descending", + "components.Discover.DiscoverBooks.sortTitleAsc": "Title (A-Z) Ascending", + "components.Discover.DiscoverBooks.sortTitleDesc": "Title (Z-A) Descending", "components.Discover.DiscoverMovieGenre.genreMovies": "{genre} Movies", "components.Discover.DiscoverMovieKeyword.keywordMovies": "{keywordTitle} Movies", "components.Discover.DiscoverMovieLanguage.languageMovies": "{language} Movies", @@ -114,6 +151,7 @@ "components.Discover.moviegenres": "Movie Genres", "components.Discover.networks": "Networks", "components.Discover.plexwatchlist": "Your Watchlist", + "components.Discover.popularbooks": "Popular Books", "components.Discover.popularmovies": "Popular Movies", "components.Discover.populartv": "Popular Series", "components.Discover.recentlyAdded": "Recently Added", @@ -221,8 +259,13 @@ "components.LanguageSelector.languageServerDefault": "Default ({language})", "components.LanguageSelector.originalLanguageDefault": "All Languages", "components.Layout.LanguagePicker.displaylanguage": "Display Language", - "components.Layout.SearchInput.searchPlaceholder": "Search Movies & TV", + "components.Layout.SearchInput.books": "Books", + "components.Layout.SearchInput.mediaTypeLabel": "Media Type", + "components.Layout.SearchInput.moviesAndTv": "Movies & Series", + "components.Layout.SearchInput.searchPlaceholder": "Search Movies & Series", + "components.Layout.SearchInput.searchPlaceholderBooks": "Search Books", "components.Layout.Sidebar.blocklist": "Blocklist", + "components.Layout.Sidebar.browsebooks": "Books", "components.Layout.Sidebar.browsemovies": "Movies", "components.Layout.Sidebar.browsetv": "Series", "components.Layout.Sidebar.dashboard": "Discover", @@ -230,6 +273,7 @@ "components.Layout.Sidebar.requests": "Requests", "components.Layout.Sidebar.settings": "Settings", "components.Layout.Sidebar.users": "Users", + "components.Layout.UserDropdown.MiniQuotaDisplay.bookrequests": "Book Requests", "components.Layout.UserDropdown.MiniQuotaDisplay.movierequests": "Movie Requests", "components.Layout.UserDropdown.MiniQuotaDisplay.seriesrequests": "Series Requests", "components.Layout.UserDropdown.myprofile": "Profile", @@ -244,9 +288,14 @@ "components.Layout.VersionStatus.outofdate": "Out of Date", "components.Layout.VersionStatus.streamdevelop": "Seerr Develop", "components.Layout.VersionStatus.streamstable": "Seerr Stable", + "components.Login.JellyfinQuickConnectModal.cancel": "Cancel", + "components.Login.JellyfinQuickConnectModal.instructions": "Enter this code in your {mediaServerName} app", + "components.Login.JellyfinQuickConnectModal.subtitle": "Sign in with Quick Connect", + "components.Login.JellyfinQuickConnectModal.title": "Quick Connect", "components.Login.adminerror": "You must use an admin account to sign in.", "components.Login.back": "Go back", "components.Login.credentialerror": "The username or password is incorrect.", + "components.Login.demoModeInfo": "Demo mode is enabled. Use the demo credentials to sign in.", "components.Login.description": "Add a valid email address to complete your profile. This will be used for notifications and local sign-in.", "components.Login.email": "Email Address", "components.Login.emailAlreadyTaken": "This email is already in use.", @@ -264,6 +313,8 @@ "components.Login.orsigninwith": "Or sign in with", "components.Login.password": "Password", "components.Login.port": "Port", + "components.Login.quickconnect": "Quick Connect", + "components.Login.quickconnecterror": "Quick Connect failed. Please try again.", "components.Login.save": "Add", "components.Login.saveFailed": "Something went wrong while saving.", "components.Login.saving": "Adding…", @@ -294,6 +345,8 @@ "components.Login.validationservertyperequired": "Please select a server type", "components.Login.validationusernamerequired": "Username required", "components.ManageSlideOver.alltime": "All Time", + "components.ManageSlideOver.book": "book", + "components.ManageSlideOver.clearmediadataerror": "Something went wrong while clearing the media data.", "components.ManageSlideOver.downloadstatus": "Downloads", "components.ManageSlideOver.manageModalAdvanced": "Advanced", "components.ManageSlideOver.manageModalClearMedia": "Clear Data", @@ -301,6 +354,8 @@ "components.ManageSlideOver.manageModalIssues": "Open Issues", "components.ManageSlideOver.manageModalMedia": "Media", "components.ManageSlideOver.manageModalMedia4k": "4K Media", + "components.ManageSlideOver.manageModalMediaAudiobook": "Audiobook Media", + "components.ManageSlideOver.manageModalMediaEbook": "Ebook Media", "components.ManageSlideOver.manageModalNoRequests": "No requests.", "components.ManageSlideOver.manageModalRemoveMediaWarning": "* This will irreversibly remove this {mediaType} from {arr}, including all files.", "components.ManageSlideOver.manageModalRequests": "Requests", @@ -308,16 +363,21 @@ "components.ManageSlideOver.mark4kavailable": "Mark as Available in 4K", "components.ManageSlideOver.markallseasons4kavailable": "Mark All Seasons as Available in 4K", "components.ManageSlideOver.markallseasonsavailable": "Mark All Seasons as Available", + "components.ManageSlideOver.markaudiobookavailable": "Mark as Available (Audiobook)", "components.ManageSlideOver.markavailable": "Mark as Available", "components.ManageSlideOver.movie": "movie", "components.ManageSlideOver.openarr": "Open in {arr}", "components.ManageSlideOver.openarr4k": "Open in 4K {arr}", + "components.ManageSlideOver.openarraudiobook": "Open in Audiobook {arr}", "components.ManageSlideOver.opentautulli": "Open in Tautulli", "components.ManageSlideOver.pastdays": "Past {days, number} Days", "components.ManageSlideOver.playedby": "Played By", "components.ManageSlideOver.plays": "{playCount, number} {playCount, plural, one {play} other {plays}}", "components.ManageSlideOver.removearr": "Remove from {arr}", "components.ManageSlideOver.removearr4k": "Remove from 4K {arr}", + "components.ManageSlideOver.removearraudiobook": "Remove Audiobook from {arr}", + "components.ManageSlideOver.removearrebook": "Remove Ebook from {arr}", + "components.ManageSlideOver.removemediaerror": "Something went wrong while removing the media.", "components.ManageSlideOver.tvshow": "series", "components.MediaSlider.ShowMoreCard.seemore": "See More", "components.MetadataSelector.selectMetdataProvider": "Select a metadata provider", @@ -405,13 +465,17 @@ "components.PermissionEdit.advancedrequest": "Advanced Requests", "components.PermissionEdit.advancedrequestDescription": "Grant permission to modify advanced media request options.", "components.PermissionEdit.autoapprove": "Auto-Approve", - "components.PermissionEdit.autoapprove4k": "Auto-Approve 4K", - "components.PermissionEdit.autoapprove4kDescription": "Grant automatic approval for all 4K media requests.", + "components.PermissionEdit.autoapprove4k": "Auto-Approve 4K & Audiobooks", + "components.PermissionEdit.autoapprove4kDescription": "Grant automatic approval for all 4K movie, 4K series, and audiobook requests.", "components.PermissionEdit.autoapprove4kMovies": "Auto-Approve 4K Movies", "components.PermissionEdit.autoapprove4kMoviesDescription": "Grant automatic approval for 4K movie requests.", "components.PermissionEdit.autoapprove4kSeries": "Auto-Approve 4K Series", "components.PermissionEdit.autoapprove4kSeriesDescription": "Grant automatic approval for 4K series requests.", - "components.PermissionEdit.autoapproveDescription": "Grant automatic approval for all non-4K media requests.", + "components.PermissionEdit.autoapproveAudiobooks": "Auto-Approve Audiobooks", + "components.PermissionEdit.autoapproveAudiobooksDescription": "Grant automatic approval for audiobook requests.", + "components.PermissionEdit.autoapproveBooks": "Auto-Approve Ebooks", + "components.PermissionEdit.autoapproveBooksDescription": "Grant automatic approval for ebook requests.", + "components.PermissionEdit.autoapproveDescription": "Grant automatic approval for all non-4K movie, series, and ebook requests.", "components.PermissionEdit.autoapproveMovies": "Auto-Approve Movies", "components.PermissionEdit.autoapproveMoviesDescription": "Grant automatic approval for non-4K movie requests.", "components.PermissionEdit.autoapproveSeries": "Auto-Approve Series", @@ -433,13 +497,17 @@ "components.PermissionEdit.managerequests": "Manage Requests", "components.PermissionEdit.managerequestsDescription": "Grant permission to manage media requests. All requests made by a user with this permission will be automatically approved.", "components.PermissionEdit.request": "Request", - "components.PermissionEdit.request4k": "Request 4K", - "components.PermissionEdit.request4kDescription": "Grant permission to submit requests for 4K media.", + "components.PermissionEdit.request4k": "Request 4K & Audiobooks", + "components.PermissionEdit.request4kDescription": "Grant permission to submit requests for 4K movies, 4K series, and audiobooks.", "components.PermissionEdit.request4kMovies": "Request 4K Movies", "components.PermissionEdit.request4kMoviesDescription": "Grant permission to submit requests for 4K movies.", "components.PermissionEdit.request4kTv": "Request 4K Series", "components.PermissionEdit.request4kTvDescription": "Grant permission to submit requests for 4K series.", - "components.PermissionEdit.requestDescription": "Grant permission to submit requests for non-4K media.", + "components.PermissionEdit.requestAudiobooks": "Request Audiobooks", + "components.PermissionEdit.requestAudiobooksDescription": "Grant permission to submit requests for audiobooks.", + "components.PermissionEdit.requestBooks": "Request Ebooks", + "components.PermissionEdit.requestBooksDescription": "Grant permission to submit requests for ebooks.", + "components.PermissionEdit.requestDescription": "Grant permission to submit requests for non-4K movies, series, and ebooks.", "components.PermissionEdit.requestMovies": "Request Movies", "components.PermissionEdit.requestMoviesDescription": "Grant permission to submit requests for non-4K movies.", "components.PermissionEdit.requestTv": "Request Series", @@ -462,6 +530,8 @@ "components.PersonDetails.birthdate": "Born {birthdate}", "components.PersonDetails.crewmember": "Crew", "components.PersonDetails.lifespan": "{birthdate} – {deathdate}", + "components.QuotaSelector.bookRequests": "{quotaLimit} {books} per {quotaDays} {days}", + "components.QuotaSelector.books": "{count, plural, one {book} other {books}}", "components.QuotaSelector.days": "{count, plural, one {day} other {days}}", "components.QuotaSelector.movieRequests": "{quotaLimit} {movies} per {quotaDays} {days}", "components.QuotaSelector.movies": "{count, plural, one {movie} other {movies}}", @@ -476,6 +546,7 @@ "components.RequestBlock.edit": "Edit Request", "components.RequestBlock.languageprofile": "Language Profile", "components.RequestBlock.lastmodifiedby": "Last Modified By", + "components.RequestBlock.metadataprofile": "Metadata Profile", "components.RequestBlock.profilechanged": "Quality Profile", "components.RequestBlock.requestdate": "Request Date", "components.RequestBlock.requestedby": "Requested By", @@ -486,15 +557,19 @@ "components.RequestButton.approve4krequests": "Approve {requestCount, plural, one {4K Request} other {{requestCount} 4K Requests}}", "components.RequestButton.approverequest": "Approve Request", "components.RequestButton.approverequest4k": "Approve 4K Request", + "components.RequestButton.approverequestaudiobook": "Approve Audiobook Request", "components.RequestButton.approverequests": "Approve {requestCount, plural, one {Request} other {{requestCount} Requests}}", "components.RequestButton.decline4krequests": "Decline {requestCount, plural, one {4K Request} other {{requestCount} 4K Requests}}", "components.RequestButton.declinerequest": "Decline Request", "components.RequestButton.declinerequest4k": "Decline 4K Request", + "components.RequestButton.declinerequestaudiobook": "Decline Audiobook Request", "components.RequestButton.declinerequests": "Decline {requestCount, plural, one {Request} other {{requestCount} Requests}}", "components.RequestButton.requestmore": "Request More", "components.RequestButton.requestmore4k": "Request More in 4K", + "components.RequestButton.requestmoreaudiobook": "Request More Audiobook", "components.RequestButton.viewrequest": "View Request", "components.RequestButton.viewrequest4k": "View 4K Request", + "components.RequestButton.viewrequestaudiobook": "View Audiobook Request", "components.RequestCard.approverequest": "Approve Request", "components.RequestCard.cancelrequest": "Cancel Request", "components.RequestCard.declinerequest": "Decline Request", @@ -502,6 +577,7 @@ "components.RequestCard.editrequest": "Edit Request", "components.RequestCard.failedmodify": "Something went wrong while modifying the request.", "components.RequestCard.failedretry": "Something went wrong while retrying the request.", + "components.RequestCard.hcid": "Hardcover ID", "components.RequestCard.mediaerror": "{mediaType} Not Found", "components.RequestCard.seasons": "{seasonCount, plural, one {Season} other {Seasons}}", "components.RequestCard.tmdbid": "TMDB ID", @@ -512,11 +588,14 @@ "components.RequestList.RequestItem.editrequest": "Edit Request", "components.RequestList.RequestItem.failedmodify": "Something went wrong while modifying the request.", "components.RequestList.RequestItem.failedretry": "Something went wrong while retrying the request.", + "components.RequestList.RequestItem.hcid": "Hardcover ID", "components.RequestList.RequestItem.mediaerror": "{mediaType} Not Found", + "components.RequestList.RequestItem.metadataProfileName": "Metadata Profile", "components.RequestList.RequestItem.modified": "Modified", "components.RequestList.RequestItem.modifieduserdate": "{date} by {user}", "components.RequestList.RequestItem.profileName": "Profile", "components.RequestList.RequestItem.removearr": "Remove from {arr}", + "components.RequestList.RequestItem.removemediaerror": "Something went wrong while removing the media.", "components.RequestList.RequestItem.requested": "Requested", "components.RequestList.RequestItem.requesteddate": "Requested", "components.RequestList.RequestItem.seasons": "{seasonCount, plural, one {Season} other {Seasons}}", @@ -534,7 +613,10 @@ "components.RequestModal.AdvancedRequester.default": "{name} (Default)", "components.RequestModal.AdvancedRequester.destinationserver": "Destination Server", "components.RequestModal.AdvancedRequester.folder": "{path} ({space})", + "components.RequestModal.AdvancedRequester.ignoreQuotaDescription": "This request will not count against the user's quota limits. Use with caution.", + "components.RequestModal.AdvancedRequester.ignoreQuotaTitle": "Bypass User Quota", "components.RequestModal.AdvancedRequester.languageprofile": "Language Profile", + "components.RequestModal.AdvancedRequester.metadataprofile": "Metadata Profile", "components.RequestModal.AdvancedRequester.notagoptions": "No tags.", "components.RequestModal.AdvancedRequester.qualityprofile": "Quality Profile", "components.RequestModal.AdvancedRequester.requestas": "Request As", @@ -543,6 +625,8 @@ "components.RequestModal.AdvancedRequester.tags": "Tags", "components.RequestModal.QuotaDisplay.allowedRequests": "You are allowed to request {limit} {type}{days, plural, =0 {} one { every day} other { every {days} days}}.", "components.RequestModal.QuotaDisplay.allowedRequestsUser": "This user is allowed to request {limit} {type}{days, plural, =0 {} one { every day} other { every {days} days}}.", + "components.RequestModal.QuotaDisplay.book": "book", + "components.RequestModal.QuotaDisplay.booklimit": "{limit, plural, one {book} other {books}}", "components.RequestModal.QuotaDisplay.movie": "movie", "components.RequestModal.QuotaDisplay.movielimit": "{limit, plural, one {movie} other {movies}}", "components.RequestModal.QuotaDisplay.notenoughseasonrequests": "Not enough season requests remaining", @@ -557,21 +641,31 @@ "components.RequestModal.SearchByNameModal.notvdbiddescription": "We were unable to automatically match this series. Please select the correct match below.", "components.RequestModal.alreadyrequested": "Already Requested", "components.RequestModal.approve": "Approve Request", + "components.RequestModal.audiobook": "Audiobook", "components.RequestModal.autoapproval": "Automatic Approval", "components.RequestModal.cancel": "Cancel Request", + "components.RequestModal.ebook": "Ebook", "components.RequestModal.edit": "Edit Request", + "components.RequestModal.editionsextras": "Editions & Extras", "components.RequestModal.errorediting": "Something went wrong while editing the request.", + "components.RequestModal.format": "Format", + "components.RequestModal.fullseries": "Full Series", "components.RequestModal.numberofepisodes": "# of Episodes", "components.RequestModal.pending4krequest": "Pending 4K Request", "components.RequestModal.pendingapproval": "Your request is pending approval.", + "components.RequestModal.pendingaudiobookrequest": "Pending Audiobook Request", "components.RequestModal.pendingrequest": "Pending Request", "components.RequestModal.requestApproved": "Request for {title} approved!", "components.RequestModal.requestCancel": "Request for {title} canceled.", "components.RequestModal.requestSuccess": "{title} requested successfully!", "components.RequestModal.requestadmin": "This request will be approved automatically.", + "components.RequestModal.requestbooks": "Request {count} {count, plural, one {Book} other {Books}}", + "components.RequestModal.requestbooktitle": "Request Book", + "components.RequestModal.requestbothformats": "Request Both Formats", "components.RequestModal.requestcancelled": "Request for {title} canceled.", "components.RequestModal.requestcollection4ktitle": "Request Collection in 4K", "components.RequestModal.requestcollectiontitle": "Request Collection", + "components.RequestModal.requestebook": "Request Ebook", "components.RequestModal.requestedited": "Request for {title} edited successfully!", "components.RequestModal.requesterror": "Something went wrong while submitting the request.", "components.RequestModal.requestfrom": "{username}'s request is pending approval.", @@ -585,6 +679,8 @@ "components.RequestModal.requestseriestitle": "Request Series", "components.RequestModal.season": "Season", "components.RequestModal.seasonnumber": "Season {number}", + "components.RequestModal.selectbooks": "Select Book(s)", + "components.RequestModal.selectformat": "Select Format(s)", "components.RequestModal.selectmovies": "Select Movie(s)", "components.RequestModal.selectseason": "Select Season(s)", "components.ResetPassword.confirmpassword": "Confirm Password", @@ -624,7 +720,13 @@ "components.Selector.showless": "Show Less", "components.Selector.showmore": "Show More", "components.Selector.starttyping": "Starting typing to search.", + "components.SeriesDetails.editionsextras": "Editions & Extras", + "components.SeriesDetails.numberofbooks": "{count} Books", + "components.SeriesDetails.overview": "Overview", + "components.SeriesDetails.requestseries": "Request Series", + "components.SeriesDetails.requestseriesaudio": "Request Series in Audiobook", "components.Settings.Notifications.NotificationsGotify.agentenabled": "Enable Agent", + "components.Settings.Notifications.NotificationsGotify.embedPoster": "Embed Poster", "components.Settings.Notifications.NotificationsGotify.gotifysettingsfailed": "Gotify notification settings failed to save.", "components.Settings.Notifications.NotificationsGotify.gotifysettingssaved": "Gotify notification settings saved successfully!", "components.Settings.Notifications.NotificationsGotify.priority": "Priority", @@ -644,6 +746,8 @@ "components.Settings.Notifications.NotificationsNtfy.ntfysettingssaved": "Ntfy notification settings saved successfully!", "components.Settings.Notifications.NotificationsNtfy.password": "Password", "components.Settings.Notifications.NotificationsNtfy.priority": "Priority", + "components.Settings.Notifications.NotificationsNtfy.tags": "Tags", + "components.Settings.Notifications.NotificationsNtfy.tagsPlaceholder": "eyes,warning", "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestFailed": "Ntfy test notification failed to send.", "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestSending": "Sending ntfy test notification…", "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestSuccess": "Ntfy test notification sent!", @@ -775,6 +879,8 @@ "components.Settings.Notifications.toastTelegramTestFailed": "Telegram test notification failed to send.", "components.Settings.Notifications.toastTelegramTestSending": "Sending Telegram test notification…", "components.Settings.Notifications.toastTelegramTestSuccess": "Telegram test notification sent!", + "components.Settings.Notifications.usePublicLogo": "Use public Seerr logo instead of instance logo", + "components.Settings.Notifications.usePublicLogoTip": "If your Seerr instance is not publicly accessible, enable this option so email clients outside your network can display the image. The image will be pulled from the public GitHub repository.", "components.Settings.Notifications.useUserLocale": "Use Notification Recipient Locale", "components.Settings.Notifications.userEmailRequired": "Require user email", "components.Settings.Notifications.validationBotAPIRequired": "You must provide a bot authorization token", @@ -788,8 +894,11 @@ "components.Settings.Notifications.validationTypes": "You must select at least one notification type", "components.Settings.Notifications.validationUrl": "You must provide a valid URL", "components.Settings.Notifications.validationWebhookRoleId": "You must provide a valid Discord Role ID", + "components.Settings.Notifications.validationWebhookThreadId": "You must provide a valid Discord Thread ID", "components.Settings.Notifications.webhookRoleId": "Notification Role ID", "components.Settings.Notifications.webhookRoleIdTip": "The role ID to mention in the webhook message. Leave empty to disable mentions", + "components.Settings.Notifications.webhookThreadId": "Thread ID", + "components.Settings.Notifications.webhookThreadIdTip": "The ID of the thread channel to post notifications in. Leave empty to post in the webhook channel", "components.Settings.Notifications.webhookUrl": "Webhook URL", "components.Settings.Notifications.webhookUrlTip": "Create a webhook integration in your server", "components.Settings.OverrideRuleModal.conditions": "Conditions", @@ -800,11 +909,13 @@ "components.Settings.OverrideRuleModal.genres": "Genres", "components.Settings.OverrideRuleModal.keywords": "Keywords", "components.Settings.OverrideRuleModal.languages": "Languages", + "components.Settings.OverrideRuleModal.metadataprofile": "Metadata Profile", "components.Settings.OverrideRuleModal.notagoptions": "No tags.", "components.Settings.OverrideRuleModal.qualityprofile": "Quality Profile", "components.Settings.OverrideRuleModal.rootfolder": "Root Folder", "components.Settings.OverrideRuleModal.ruleCreated": "Override rule created successfully!", "components.Settings.OverrideRuleModal.ruleUpdated": "Override rule updated successfully!", + "components.Settings.OverrideRuleModal.selectMetadataProfile": "Select metadata profile", "components.Settings.OverrideRuleModal.selectQualityProfile": "Select quality profile", "components.Settings.OverrideRuleModal.selectRootFolder": "Select root folder", "components.Settings.OverrideRuleModal.selectService": "Select service", @@ -819,6 +930,7 @@ "components.Settings.OverrideRuleTile.genre": "Genre", "components.Settings.OverrideRuleTile.keywords": "Keywords", "components.Settings.OverrideRuleTile.language": "Language", + "components.Settings.OverrideRuleTile.metadataprofile": "Metadata Profile", "components.Settings.OverrideRuleTile.qualityprofile": "Quality Profile", "components.Settings.OverrideRuleTile.rootfolder": "Root Folder", "components.Settings.OverrideRuleTile.settings": "Settings", @@ -880,6 +992,55 @@ "components.Settings.RadarrModal.validationPortRequired": "You must provide a valid port number", "components.Settings.RadarrModal.validationProfileRequired": "You must select a quality profile", "components.Settings.RadarrModal.validationRootFolderRequired": "You must select a root folder", + "components.Settings.ReadarrModal.add": "Add Server", + "components.Settings.ReadarrModal.apiKey": "API Key", + "components.Settings.ReadarrModal.baseUrl": "URL Base", + "components.Settings.ReadarrModal.createAudioreadarr": "Add New Audiobook Readarr Server", + "components.Settings.ReadarrModal.createreadarr": "Add New Readarr Server", + "components.Settings.ReadarrModal.defaultAudioserver": "Default Audiobook Server", + "components.Settings.ReadarrModal.defaultserver": "Default Server", + "components.Settings.ReadarrModal.editAudioreadarr": "Edit Audiobook Readarr Server", + "components.Settings.ReadarrModal.editreadarr": "Edit Readarr Server", + "components.Settings.ReadarrModal.enableSearch": "Enable Automatic Search", + "components.Settings.ReadarrModal.externalUrl": "External URL", + "components.Settings.ReadarrModal.hostname": "Hostname or IP Address", + "components.Settings.ReadarrModal.loadingTags": "Loading tags…", + "components.Settings.ReadarrModal.loadingmetadataprofiles": "Loading metadata profiles…", + "components.Settings.ReadarrModal.loadingprofiles": "Loading quality profiles…", + "components.Settings.ReadarrModal.loadingrootfolders": "Loading root folders…", + "components.Settings.ReadarrModal.metadataprofile": "Metadata Profile", + "components.Settings.ReadarrModal.notagoptions": "No tags.", + "components.Settings.ReadarrModal.port": "Port", + "components.Settings.ReadarrModal.qualityprofile": "Quality Profile", + "components.Settings.ReadarrModal.rootfolder": "Root Folder", + "components.Settings.ReadarrModal.selectMetadataProfile": "Select metadata profile", + "components.Settings.ReadarrModal.selectQualityProfile": "Select quality profile", + "components.Settings.ReadarrModal.selectRootFolder": "Select root folder", + "components.Settings.ReadarrModal.selecttags": "Select tags", + "components.Settings.ReadarrModal.serverAudio": "Audiobook Server", + "components.Settings.ReadarrModal.servername": "Server Name", + "components.Settings.ReadarrModal.ssl": "Use SSL", + "components.Settings.ReadarrModal.syncEnabled": "Enable Scan", + "components.Settings.ReadarrModal.tagRequests": "Tag Requests", + "components.Settings.ReadarrModal.tagRequestsInfo": "Automatically add an additional tag with the requester's user ID & display name", + "components.Settings.ReadarrModal.tags": "Tags", + "components.Settings.ReadarrModal.testFirstMetadataProfiles": "Test connection to load metadata profiles", + "components.Settings.ReadarrModal.testFirstQualityProfiles": "Test connection to load quality profiles", + "components.Settings.ReadarrModal.testFirstRootFolders": "Test connection to load root folders", + "components.Settings.ReadarrModal.testFirstTags": "Test connection to load tags", + "components.Settings.ReadarrModal.toastReadarrTestFailure": "Failed to connect to Readarr.", + "components.Settings.ReadarrModal.toastReadarrTestSuccess": "Readarr connection established successfully!", + "components.Settings.ReadarrModal.validationApiKeyRequired": "You must provide an API key", + "components.Settings.ReadarrModal.validationApplicationUrl": "You must provide a valid URL", + "components.Settings.ReadarrModal.validationApplicationUrlTrailingSlash": "URL must not end in a trailing slash", + "components.Settings.ReadarrModal.validationBaseUrlLeadingSlash": "URL base must have a leading slash", + "components.Settings.ReadarrModal.validationBaseUrlTrailingSlash": "URL base must not end in a trailing slash", + "components.Settings.ReadarrModal.validationHostnameRequired": "You must provide a valid hostname or IP address", + "components.Settings.ReadarrModal.validationMetadataProfileRequired": "You must select a metadata profile", + "components.Settings.ReadarrModal.validationNameRequired": "You must provide a server name", + "components.Settings.ReadarrModal.validationPortRequired": "You must provide a valid port number", + "components.Settings.ReadarrModal.validationProfileRequired": "You must select a quality profile", + "components.Settings.ReadarrModal.validationRootFolderRequired": "You must select a root folder", "components.Settings.SettingsAbout.Releases.currentversion": "Current", "components.Settings.SettingsAbout.Releases.latestversion": "Latest", "components.Settings.SettingsAbout.Releases.releasedataMissing": "Release data is currently unavailable.", @@ -902,6 +1063,7 @@ "components.Settings.SettingsAbout.totalrequests": "Total Requests", "components.Settings.SettingsAbout.uptodate": "Up to Date", "components.Settings.SettingsAbout.version": "Version", + "components.Settings.SettingsAbout.versionCheckDisabled": "Version Check Disabled", "components.Settings.SettingsJobsCache.availability-sync": "Media Availability Sync", "components.Settings.SettingsJobsCache.cache": "Cache", "components.Settings.SettingsJobsCache.cacheDescription": "Seerr caches requests to external API endpoints to optimize performance and avoid making unnecessary API calls.", @@ -965,6 +1127,8 @@ "components.Settings.SettingsJobsCache.process": "Process", "components.Settings.SettingsJobsCache.process-blocklisted-tags": "Process Blocklisted Tags", "components.Settings.SettingsJobsCache.radarr-scan": "Radarr Scan", + "components.Settings.SettingsJobsCache.readarr-recently-added-scan": "Readarr Recently Added Scan", + "components.Settings.SettingsJobsCache.readarr-scan": "Readarr Scan", "components.Settings.SettingsJobsCache.runnow": "Run Now", "components.Settings.SettingsJobsCache.size": "Size", "components.Settings.SettingsJobsCache.sonarr-scan": "Sonarr Scan", @@ -1008,16 +1172,24 @@ "components.Settings.SettingsMain.general": "General", "components.Settings.SettingsMain.generalsettings": "General Settings", "components.Settings.SettingsMain.generalsettingsDescription": "Configure global and default settings for Seerr.", + "components.Settings.SettingsMain.hardcoverapikey": "Hardcover API Key", "components.Settings.SettingsMain.hideAvailable": "Hide Available Media", "components.Settings.SettingsMain.hideAvailableTip": "Hide available media from the discover pages but not search results", "components.Settings.SettingsMain.hideBlocklisted": "Hide Blocklisted Items", "components.Settings.SettingsMain.hideBlocklistedTip": "Hide blocklisted items from discover pages for all users with the \"Manage Blocklist\" permission", + "components.Settings.SettingsMain.hideRequested": "Hide Requested Media", + "components.Settings.SettingsMain.hideRequestedTip": "Hide media that has been requested from the discover pages but not search results", "components.Settings.SettingsMain.locale": "Display Language", "components.Settings.SettingsMain.originallanguage": "Discover Language", "components.Settings.SettingsMain.originallanguageTip": "Filter content by original language", "components.Settings.SettingsMain.partialRequestsEnabled": "Allow Partial Series Requests", + "components.Settings.SettingsMain.seriesRequestsEnabled": "Allow Book Series Requests", + "components.Settings.SettingsMain.seriesRequestsEnabledTip": "Allow every book in a series to be requested at once", "components.Settings.SettingsMain.streamingRegion": "Streaming Region", "components.Settings.SettingsMain.streamingRegionTip": "Show streaming sites by regional availability", + "components.Settings.SettingsMain.syncBookFormatRequests": "Request Both Book Formats by Default", + "components.Settings.SettingsMain.syncBookFormatRequestsDisabledTip": "A default Audiobook Readarr server must be configured to enable this setting", + "components.Settings.SettingsMain.syncBookFormatRequestsTip": "Pre-select the ebook and audiobook together when requesting a book", "components.Settings.SettingsMain.toastApiKeyFailure": "Something went wrong while generating a new API key.", "components.Settings.SettingsMain.toastApiKeySuccess": "New API key generated successfully!", "components.Settings.SettingsMain.toastSettingsFailure": "Something went wrong while saving settings.", @@ -1027,6 +1199,8 @@ "components.Settings.SettingsMain.validationApplicationUrlTrailingSlash": "URL must not end in a trailing slash", "components.Settings.SettingsMain.validationUrl": "You must provide a valid URL", "components.Settings.SettingsMain.validationUrlTrailingSlash": "URL must not end in a trailing slash", + "components.Settings.SettingsMain.versionCheck": "Version Check", + "components.Settings.SettingsMain.versionCheckTip": "Automatically check for new versions on GitHub.", "components.Settings.SettingsMain.youtubeUrl": "YouTube URL", "components.Settings.SettingsMain.youtubeUrlTip": "Base URL for YouTube videos if a self-hosted YouTube instance is used.", "components.Settings.SettingsNetwork.apiRequestTimeout": "API Request Timeout", @@ -1065,6 +1239,7 @@ "components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "You must provide a valid minimum TTL", "components.Settings.SettingsNetwork.validationProxyPort": "You must provide a valid port", "components.Settings.SettingsUsers.atLeastOneAuth": "At least one authentication method must be selected.", + "components.Settings.SettingsUsers.bookRequestLimitLabel": "Global Books Request Limit", "components.Settings.SettingsUsers.defaultPermissions": "Default Permissions", "components.Settings.SettingsUsers.defaultPermissionsTip": "Initial permissions assigned to new users", "components.Settings.SettingsUsers.disabledMediaServerLoginWarning": "Some users may not have a {applicationTitle} password set. Disabling {mediaServerName} sign-in could lock them out. Affected users will need to set a password from their profile or via a password reset link.", @@ -1147,8 +1322,10 @@ "components.Settings.SonarrModal.validationPortRequired": "You must provide a valid port number", "components.Settings.SonarrModal.validationProfileRequired": "You must select a quality profile", "components.Settings.SonarrModal.validationRootFolderRequired": "You must select a root folder", + "components.Settings.activeMetadataProfile": "Active Metadata Profile", "components.Settings.activeProfile": "Active Profile", "components.Settings.addradarr": "Add Radarr Server", + "components.Settings.addreadarr": "Add Readarr Server", "components.Settings.address": "Address", "components.Settings.addrule": "New Override Rule", "components.Settings.addsonarr": "Add Sonarr Server", @@ -1170,6 +1347,7 @@ "components.Settings.currentlibrary": "Current Library: {name}", "components.Settings.default": "Default", "components.Settings.default4k": "Default 4K", + "components.Settings.defaultAudio": "Default Audiobook", "components.Settings.deleteServer": "Delete {serverType} Server", "components.Settings.deleteserverconfirm": "Are you sure you want to delete this server?", "components.Settings.email": "Email", @@ -1184,12 +1362,14 @@ "components.Settings.invalidKeyword": "{keywordId} is not a TMDB keyword.", "components.Settings.invalidurlerror": "Unable to connect to {mediaServerName} server.", "components.Settings.is4k": "4K", + "components.Settings.isAudio": "Audiobook", "components.Settings.jellyfinForgotPasswordUrl": "Forgot Password URL", "components.Settings.jellyfinSettings": "{mediaServerName} Settings", "components.Settings.jellyfinSettingsDescription": "Optionally configure the internal and external endpoints for your {mediaServerName} server. In most cases, the external URL is different to the internal URL. A custom password reset URL can also be set for {mediaServerName} login, in case you would like to redirect to a different password reset page. You can also change the Jellyfin API key, which was automatically generated previously.", "components.Settings.jellyfinSettingsFailure": "Something went wrong while saving {mediaServerName} settings.", "components.Settings.jellyfinSettingsSuccess": "{mediaServerName} settings saved successfully!", "components.Settings.jellyfinSyncFailedAutomaticGroupedFolders": "Custom authentication with Automatic Library Grouping not supported", + "components.Settings.jellyfinSyncFailedConnectionError": "Unable to reach the {mediaServerName} server. Check that it is running and reachable from Seerr.", "components.Settings.jellyfinSyncFailedGenericError": "Something went wrong while syncing libraries", "components.Settings.jellyfinSyncFailedNoLibrariesFound": "No libraries were found", "components.Settings.jellyfinlibraries": "{mediaServerName} Libraries", @@ -1201,6 +1381,7 @@ "components.Settings.manualscanDescription": "Normally, this will only be run once every 24 hours. Seerr will check your Plex server's recently added more aggressively. If this is your first time configuring Plex, a one-time full manual library scan is recommended!", "components.Settings.manualscanDescriptionJellyfin": "Normally, this will only be run once every 24 hours. Seerr will check your {mediaServerName} server's recently added more aggressively. If this is your first time configuring Seerr, a one-time full manual library scan is recommended!", "components.Settings.manualscanJellyfin": "Manual Library Scan", + "components.Settings.mediaTypeBooks": "book", "components.Settings.mediaTypeMovie": "movie", "components.Settings.mediaTypeSeries": "series", "components.Settings.menuAbout": "About", @@ -1220,7 +1401,9 @@ "components.Settings.metadataSettingsSaved": "Metadata provider settings saved", "components.Settings.no": "No", "components.Settings.noDefault4kServer": "A 4K {serverType} server must be marked as default in order to enable users to submit 4K {mediaType} requests.", + "components.Settings.noDefaultAudioServer": "A Audiobook {serverType} server must be marked as default in order to enable users to submit Audio {mediaType} requests.", "components.Settings.noDefaultNon4kServer": "If you only have a single {serverType} server for both non-4K and 4K content (or if you only download 4K content), your {serverType} server should NOT be designated as a 4K server.", + "components.Settings.noDefaultNonAudioServer": "If you only have a single {serverType} server for both non-Audiobook and Audiobook content (or if you only download Audiobook content), your {serverType} server should NOT be designated as a Audiobook server.", "components.Settings.noDefaultServer": "At least one {serverType} server must be marked as default in order for {mediaType} requests to be processed.", "components.Settings.noSpecialCharacters": "Configuration must be a comma delimited list of TMDB keyword ids, and must not start or end with a comma.", "components.Settings.nooptions": "No results.", @@ -1240,6 +1423,8 @@ "components.Settings.port": "Port", "components.Settings.providerStatus": "Metadata Provider Status", "components.Settings.radarrsettings": "Radarr Settings", + "components.Settings.readarrServiceSettingsDescription": "Configure your Readarr server(s) below. You can connect multiple Readarr servers, but only one of them can be marked as a default. Administrators are able to override the server used to process new requests prior to approval.", + "components.Settings.readarrsettings": "Readarr Settings", "components.Settings.restartrequiredTooltip": "Seerr must be restarted for changes to this setting to take effect", "components.Settings.save": "Save Changes", "components.Settings.saving": "Saving…", @@ -1277,8 +1462,10 @@ "components.Settings.toastPlexRefresh": "Retrieving server list from Plex…", "components.Settings.toastPlexRefreshFailure": "Failed to retrieve Plex server list.", "components.Settings.toastPlexRefreshSuccess": "Plex server list retrieved successfully!", + "components.Settings.toastPlexSyncFailure": "Failed to sync Plex libraries.", "components.Settings.toastTautulliSettingsFailure": "Something went wrong while saving Tautulli settings.", "components.Settings.toastTautulliSettingsSuccess": "Tautulli settings saved successfully!", + "components.Settings.toggleLibraryFailure": "Failed to update library.", "components.Settings.tvdbProviderDoesnotWork": "TVDB provider does not work, please select another metadata provider", "components.Settings.urlBase": "URL Base", "components.Settings.validationApiKey": "You must provide an API key", @@ -1290,7 +1477,7 @@ "components.Settings.validationUrlTrailingSlash": "URL must not end in a trailing slash", "components.Settings.valueRequired": "You must provide a value.", "components.Settings.webAppUrl": "Web App URL", - "components.Settings.webAppUrlTip": "Optionally direct users to the web app on your server instead of the \"hosted\" web app", + "components.Settings.webAppUrlTip": "Optionally direct users to the web app on your server instead of https://app.plex.tv/desktop", "components.Settings.webhook": "Webhook", "components.Settings.webpush": "Web Push", "components.Settings.yes": "Yes", @@ -1303,7 +1490,7 @@ "components.Setup.continue": "Continue", "components.Setup.finish": "Finish Setup", "components.Setup.finishing": "Finishing…", - "components.Setup.librarieserror": "Validation failed. Please toggle the libraries again to continue.", + "components.Setup.librarieserror": "Unable to load libraries from your media server. Check that it is reachable and still configured correctly.", "components.Setup.servertype": "Choose Server Type", "components.Setup.setup": "Setup", "components.Setup.signin": "Sign In", @@ -1320,6 +1507,8 @@ "components.StatusBadge.seasonnumber": "S{seasonNumber}", "components.StatusBadge.status": "{status}", "components.StatusBadge.status4k": "4K {status}", + "components.StatusBadge.statusAudiobook": "Audiobook {status}", + "components.StatusBadge.statusEbook": "Ebook {status}", "components.StatusChecker.appUpdated": "{applicationTitle} Updated", "components.StatusChecker.appUpdatedDescription": "Please click the button below to reload the application.", "components.StatusChecker.reloadApp": "Reload {applicationTitle}", @@ -1327,6 +1516,7 @@ "components.StatusChecker.restartRequiredDescription": "Please restart the server to apply the updated settings.", "components.TitleCard.addToWatchList": "Add to watchlist", "components.TitleCard.cleardata": "Clear Data", + "components.TitleCard.hcid": "Hardcover ID", "components.TitleCard.mediaerror": "{mediaType} Not Found", "components.TitleCard.tmdbid": "TMDB ID", "components.TitleCard.tvdbid": "TheTVDB ID", @@ -1397,6 +1587,8 @@ "components.UserList.importfrommediaserver": "Import {mediaServerName} Users", "components.UserList.importfromplex": "Import Plex Users", "components.UserList.importfromplexerror": "Something went wrong while importing Plex users.", + "components.UserList.importfromplexnochanges": "Plex import completed. No users were created or refreshed.", + "components.UserList.importfromplexsynced": "Plex users synced successfully. Existing users were refreshed.", "components.UserList.localLoginDisabled": "The Enable Local Sign-In setting is currently disabled.", "components.UserList.localuser": "Local User", "components.UserList.mediaServerUser": "{mediaServerName} User", @@ -1409,12 +1601,14 @@ "components.UserList.passwordinfodescription": "Configure an application URL and enable email notifications to allow automatic password generation.", "components.UserList.plexuser": "Plex User", "components.UserList.role": "Role", + "components.UserList.searchUsers": "Search…", "components.UserList.sortBy": "Sort by {field}", "components.UserList.sortByJoined": "Sort by join date", "components.UserList.sortByRequests": "Sort by number of requests", "components.UserList.sortByRole": "Sort by user role", "components.UserList.sortByType": "Sort by account type", "components.UserList.sortByUser": "Sort by username", + "components.UserList.syncnotice": "You can still click Sync to refresh existing Plex users, such as updated email or username details.", "components.UserList.toggleSortDirection": "Click again to sort {direction}", "components.UserList.toggleSortDirectionAria": "Toggle sort direction", "components.UserList.totalrequests": "Requests", @@ -1442,16 +1636,21 @@ "components.UserProfile.UserSettings.LinkJellyfinModal.errorUnknown": "An unknown error occurred", "components.UserProfile.UserSettings.LinkJellyfinModal.password": "Password", "components.UserProfile.UserSettings.LinkJellyfinModal.passwordRequired": "You must provide a password", + "components.UserProfile.UserSettings.LinkJellyfinModal.quickConnect": "Use Quick Connect", "components.UserProfile.UserSettings.LinkJellyfinModal.save": "Link", "components.UserProfile.UserSettings.LinkJellyfinModal.saving": "Adding…", "components.UserProfile.UserSettings.LinkJellyfinModal.title": "Link {mediaServerName} Account", "components.UserProfile.UserSettings.LinkJellyfinModal.username": "Username", "components.UserProfile.UserSettings.LinkJellyfinModal.usernameRequired": "You must provide a username", + "components.UserProfile.UserSettings.LinkJellyfinQuickConnectModal.instructions": "Enter this code in your {mediaServerName} app", + "components.UserProfile.UserSettings.LinkJellyfinQuickConnectModal.subtitle": "Quick Connect", + "components.UserProfile.UserSettings.LinkJellyfinQuickConnectModal.title": "Link {mediaServerName} Account", + "components.UserProfile.UserSettings.LinkJellyfinQuickConnectModal.usePassword": "Use Password Instead", "components.UserProfile.UserSettings.UserGeneralSettings.accounttype": "Account Type", "components.UserProfile.UserSettings.UserGeneralSettings.admin": "Admin", "components.UserProfile.UserSettings.UserGeneralSettings.applanguage": "Display Language", - "components.UserProfile.UserSettings.UserGeneralSettings.discordId": "Discord User ID", - "components.UserProfile.UserSettings.UserGeneralSettings.discordIdTip": "The multi-digit ID number associated with your Discord user account", + "components.UserProfile.UserSettings.UserGeneralSettings.bookrequestlimit": "Book Request Limit", + "components.UserProfile.UserSettings.UserGeneralSettings.booksrequestlimit": "Books Request Limit", "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegion": "Discover Region", "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegionTip": "Filter content by regional availability", "components.UserProfile.UserSettings.UserGeneralSettings.displayName": "Display Name", @@ -1484,7 +1683,6 @@ "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmailEmpty": "Another user already has this username. You must set an email", "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsSuccess": "Settings saved successfully!", "components.UserProfile.UserSettings.UserGeneralSettings.user": "User", - "components.UserProfile.UserSettings.UserGeneralSettings.validationDiscordId": "You must provide a valid Discord user ID", "components.UserProfile.UserSettings.UserGeneralSettings.validationemailformat": "Valid email required", "components.UserProfile.UserSettings.UserGeneralSettings.validationemailrequired": "Email required", "components.UserProfile.UserSettings.UserLinkedAccountsSettings.deleteFailed": "Unable to delete linked account.", @@ -1517,8 +1715,12 @@ "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingsfailed": "Web push notification settings failed to save.", "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingssaved": "Web push notification settings saved successfully!", "components.UserProfile.UserSettings.UserNotificationSettings.deviceDefault": "Device Default", - "components.UserProfile.UserSettings.UserNotificationSettings.discordId": "User ID", - "components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "The multi-digit ID number associated with your user account", + "components.UserProfile.UserSettings.UserNotificationSettings.discordId": "User IDs", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdAdd": "Add User ID", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdPlaceholder": "Discord User ID", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdRemove": "Remove", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "The multi-digit ID number associated with your user account. For multiple household accounts you can add more than one Discord user ID.", + "components.UserProfile.UserSettings.UserNotificationSettings.discordNotificationsNotEnabled": "The server owner has not enabled Discord notifications. This information will only be used if the server owner configures an external service.", "components.UserProfile.UserSettings.UserNotificationSettings.discordsettingsfailed": "Discord notification settings failed to save.", "components.UserProfile.UserSettings.UserNotificationSettings.discordsettingssaved": "Discord notification settings saved successfully!", "components.UserProfile.UserSettings.UserNotificationSettings.email": "Email", @@ -1547,7 +1749,7 @@ "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadIdTip": "If your group-chat has topics enabled, you can specify a thread/topic's ID here", "components.UserProfile.UserSettings.UserNotificationSettings.telegramsettingsfailed": "Telegram notification settings failed to save.", "components.UserProfile.UserSettings.UserNotificationSettings.telegramsettingssaved": "Telegram notification settings saved successfully!", - "components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "You must provide a valid user ID", + "components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "Each ID must be a valid Discord user ID", "components.UserProfile.UserSettings.UserNotificationSettings.validationPgpPublicKey": "You must provide a valid PGP public key", "components.UserProfile.UserSettings.UserNotificationSettings.validationPushbulletAccessToken": "You must provide an access token", "components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverApplicationToken": "You must provide a valid application token", @@ -1581,6 +1783,7 @@ "components.UserProfile.UserSettings.menuNotifications": "Notifications", "components.UserProfile.UserSettings.menuPermissions": "Permissions", "components.UserProfile.UserSettings.unauthorizedDescription": "You do not have permission to modify this user's settings.", + "components.UserProfile.bookrequests": "Book Requests", "components.UserProfile.emptywatchlist": "Media added to your Plex Watchlist will appear here.", "components.UserProfile.limit": "{remaining} of {limit}", "components.UserProfile.localWatchlist": "{username}'s Watchlist", @@ -1593,6 +1796,7 @@ "components.UserProfile.seriesrequest": "Series Requests", "components.UserProfile.totalrequests": "Total Requests", "components.UserProfile.unlimited": "Unlimited", + "hooks.useQuickConnect.errorMessage": "Failed to initiate Quick Connect. Please try again.", "i18n.addToBlocklist": "Add to Blocklist", "i18n.advanced": "Advanced", "i18n.all": "All", @@ -1606,6 +1810,8 @@ "i18n.blocklistError": "Something went wrong. Please try again.", "i18n.blocklistSuccess": "{title} was successfully blocklisted.", "i18n.blocklisted": "Blocklisted", + "i18n.book": "Book", + "i18n.books": "Books", "i18n.cancel": "Cancel", "i18n.canceling": "Canceling…", "i18n.close": "Close", @@ -1640,6 +1846,7 @@ "i18n.removefromBlocklist": "Remove from Blocklist", "i18n.request": "Request", "i18n.request4k": "Request in 4K", + "i18n.requestAudio": "Request Audiobook", "i18n.requested": "Requested", "i18n.requesting": "Requesting…", "i18n.resolved": "Resolved", @@ -1653,6 +1860,8 @@ "i18n.showingresults": "Showing {from} to {to} of {total} results", "i18n.specials": "Specials", "i18n.status": "Status", + "i18n.sync": "Sync", + "i18n.syncing": "Syncing…", "i18n.test": "Test", "i18n.testing": "Testing…", "i18n.tvshow": "Series", diff --git a/src/i18n/locale/es.json b/src/i18n/locale/es.json index 1e41142c48..630257cdf4 100644 --- a/src/i18n/locale/es.json +++ b/src/i18n/locale/es.json @@ -92,7 +92,7 @@ "components.Layout.Sidebar.settings": "Ajustes", "components.Layout.Sidebar.requests": "Solicitudes", "components.Layout.Sidebar.dashboard": "Descubrir", - "components.Layout.SearchInput.searchPlaceholder": "Buscar Películas y Series", + "components.Layout.SearchInput.searchPlaceholder": "Buscar películas y series", "components.Discover.upcomingmovies": "Próximas Películas", "components.Discover.upcoming": "Próximas Películas", "components.Discover.trending": "Tendencias", @@ -409,7 +409,7 @@ "components.Settings.SettingsJobsCache.cachekeys": "Claves Totales", "components.Settings.SettingsJobsCache.cachehits": "Consultas", "components.Settings.SettingsJobsCache.cacheflushed": "{cachename} caché limpiada.", - "components.Settings.SettingsJobsCache.cacheDescription": "Overseer cachea peticiones a APIs externas para optimizar el rendimiento y evitar llamadas innecesarias a esas APIs.", + "components.Settings.SettingsJobsCache.cacheDescription": "Seer cachea peticiones a APIs externas para optimizar el rendimiento y evitar llamadas innecesarias a esas APIs.", "components.Settings.SettingsJobsCache.cache": "Caché", "components.Settings.RadarrModal.validationBaseUrlTrailingSlash": "La base de la URL no debe terminar en una barra al final", "components.Settings.RadarrModal.validationBaseUrlLeadingSlash": "La base de la URL debe tener una barra al principio", @@ -617,8 +617,8 @@ "components.UserProfile.movierequests": "Solicitudes de Películas", "components.UserProfile.limit": "{remaining} de {limit}", "components.UserProfile.UserSettings.UserGeneralSettings.seriesrequestlimit": "Límite de Solicitudes de Series", - "components.UserProfile.UserSettings.UserGeneralSettings.movierequestlimit": "Límite de Solicitudes de Películas", - "components.UserProfile.UserSettings.UserGeneralSettings.enableOverride": "Límite global de Sobreescritura", + "components.UserProfile.UserSettings.UserGeneralSettings.movierequestlimit": "Límite de solicitudes de películas", + "components.UserProfile.UserSettings.UserGeneralSettings.enableOverride": "Sobrescribir límite global", "components.TvDetails.originaltitle": "Título Original", "components.Settings.SettingsUsers.tvRequestLimitLabel": "Límite Global de Solicitudes de Series", "components.Settings.SettingsUsers.movieRequestLimitLabel": "Límite Global de Solicitudes de Películas", @@ -632,8 +632,8 @@ "components.RequestModal.QuotaDisplay.notenoughseasonrequests": "No te quedan suficientes solicitudes de temporadas restantes", "components.RequestModal.QuotaDisplay.movielimit": "{limit, plural, one {película} other {películas}}", "components.RequestModal.QuotaDisplay.movie": "película", - "components.RequestModal.QuotaDisplay.allowedRequestsUser": "Este usuario tiene permitido {limit} {type} cada {days} días.", - "components.RequestModal.QuotaDisplay.allowedRequests": "Se te permite pedir {limit} {type} cada {days} días.", + "components.RequestModal.QuotaDisplay.allowedRequestsUser": "A este usuario se le permite solicitar {limit} {type}{days, plural, =0 {} one { cada día} other { cada {days} días}}.", + "components.RequestModal.QuotaDisplay.allowedRequests": "Se te permite solicitar {limit} {type}{days, plural, =0 {} one { cada día} other { cada {days} días}}.", "components.QuotaSelector.unlimited": "Ilimitadas", "components.MovieDetails.originaltitle": "Título Original", "components.LanguageSelector.originalLanguageDefault": "Todos los Idiomas", @@ -700,7 +700,7 @@ "components.RequestModal.edit": "Editar Solicitud", "components.RequestList.RequestItem.editrequest": "Editar Solicitud", "components.UserProfile.UserSettings.UserNotificationSettings.webpush": "Web Push", - "components.UserProfile.UserSettings.UserGeneralSettings.applanguage": "Mostrar Idioma", + "components.UserProfile.UserSettings.UserGeneralSettings.applanguage": "Idioma de la interfaz", "components.Settings.webpush": "Web Push", "components.Settings.noDefault4kServer": "Un servidor 4K de {serverType} debe ser marcado por defecto para poder habilitar las solicitudes 4K de {mediaType} de los usuarios.", "components.Settings.is4k": "4K", @@ -755,7 +755,7 @@ "components.Settings.Notifications.NotificationsWebPush.httpsRequirement": "Para recibir notificaciones web push, Seerr debe servirse mediante HTTPS.", "components.Settings.Notifications.NotificationsWebhook.validationTypes": "Debes seleccionar, al menos, un tipo de notificación", "components.Settings.Notifications.validationTypes": "Debes seleccionar, al menos, un tipo de notificación", - "components.Settings.SettingsUsers.localLoginTip": "Permite a los usuarios registrarse consumo email y password, en lugar de la OAuth de Plex", + "components.Settings.SettingsUsers.localLoginTip": "Permitir a los usuarios el inicio de sesión email y password", "components.Settings.webAppUrl": "Url de la Web App", "components.Settings.Notifications.encryption": "Método de Encriptación", "components.Settings.Notifications.encryptionDefault": "Usa STARTTLS si está disponible", @@ -1042,8 +1042,7 @@ "components.TvDetails.Season.somethingwentwrong": "Algo ha ido mal al obtener los datos de la temporada.", "components.TvDetails.episodeCount": "{episodeCount, plural, one {# Episodio} other {# Episodios}}", "components.TvDetails.rtaudiencescore": "Puntuación audiencia de Rotten Tomatoes", - "components.UserProfile.UserSettings.UserGeneralSettings.discordIdTip": "El multi-digit ID number asociado a su cuenta de usuario de Discord", - "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmovies": "Solicitar Películas automáticamente", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmovies": "Solicitar películas automáticamente", "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmoviestip": "Solicite películas automáticamente en su Plex Watchlist", "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseriestip": "Solicite series automáticamente series en su Plex Watchlist", "components.UserProfile.emptywatchlist": "Los medios añadidos a su Plex Watchlist aparecerán aquí.", @@ -1076,7 +1075,6 @@ "components.StatusChecker.appUpdatedDescription": "Haga clic en el botón de abajo para recargar la aplicación.", "components.StatusChecker.restartRequiredDescription": "Reinicie el servidor para aplicar la configuración actualizada.", "components.TitleCard.cleardata": "Limpiar Datos", - "components.UserProfile.UserSettings.UserGeneralSettings.discordId": "ID de usuario de Discord", "components.Settings.Notifications.NotificationsGotify.toastGotifyTestFailed": "No se ha podido enviar la notificación de prueba de Gotify.", "components.Settings.Notifications.NotificationsGotify.toastGotifyTestSending": "Enviando notificación de prueba de Gotify…", "components.StatusChecker.restartRequired": "Es necesario reiniciar el servidor", @@ -1119,7 +1117,7 @@ "components.Settings.RadarrModal.inCinemas": "En Cines", "components.UserProfile.recentlywatched": "Vistos recientemente", "components.Settings.Notifications.NotificationsGotify.agentenabled": "Habilitar agente", - "components.Settings.Notifications.NotificationsGotify.gotifysettingsfailed": "No se han guardado los ajustes de notificación de Gotify.", + "components.Settings.Notifications.NotificationsGotify.gotifysettingsfailed": "No se han podido guardar los ajustes de notificación de Gotify.", "components.Settings.Notifications.NotificationsGotify.toastGotifyTestSuccess": "¡Notificación de prueba de Gotify enviada!", "components.Settings.Notifications.NotificationsGotify.token": "Token de aplicación", "components.TvDetails.manageseries": "Gestionar Series", @@ -1139,7 +1137,6 @@ "components.TitleCard.tvdbid": "Identificador de TheTVDB", "components.TvDetails.Season.noepisodes": "Lista de episodios no disponible.", "components.TvDetails.productioncountries": "Producción {countryCount, plural, one {País} other {Países}}", - "components.UserProfile.UserSettings.UserGeneralSettings.validationDiscordId": "Debe proporcionar un ID de Discord válido", "i18n.restartRequired": "Reinicio necesario", "components.Settings.deleteServer": "Eliminar servidor {serverType}", "components.Settings.experimentalTooltip": "Activar esta opción puede provocar un comportamiento inesperado de la aplicación", @@ -1196,7 +1193,7 @@ "components.Settings.jellyfinsettings": "Ajustes de {mediaServerName}", "components.Settings.jellyfinSettingsFailure": "Algo salió mal al guardar la configuración de {mediaServerName}.", "components.Settings.jellyfinSettingsSuccess": "¡La configuración de {mediaServerName} se guardó correctamente!", - "components.Settings.jellyfinlibraries": "Bibliotecas {mediaServerName}", + "components.Settings.jellyfinlibraries": "Bibliotecas de {mediaServerName}", "components.Settings.jellyfinlibrariesDescription": "La biblioteca {mediaServerName} busca títulos. Haga clic en el botón a continuación si no aparece ninguna biblioteca.", "components.Settings.manualscanDescriptionJellyfin": "Normalmente, esto sólo se ejecutará una vez cada 24 horas. Seerr comprobará de forma más agresiva los añadidos recientemente de su servidor {mediaServerName}. ¡Si es la primera vez que configura Seerr, se recomienda un escaneo manual completo de la biblioteca!", "components.Settings.save": "Guardar Cambios", @@ -1237,7 +1234,7 @@ "components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverApplicationToken": "Debes proporcionar un token de aplicación válido", "components.Settings.syncJellyfin": "Sincronizar Bibliotecas", "components.Layout.UserWarnings.emailInvalid": "La dirección de correo es inválida.", - "components.Login.description": "Como es la primera vez que inicias sesión en {applicationName}, es necesario añadir una dirección de email válida.", + "components.Login.description": "Añade una dirección de correo electrónico válida para completar tu perfil. Se utilizará para enviarte notificaciones e iniciar sesión localmente.", "components.Login.save": "Añadir", "components.NotificationTypeSelector.issueresolvedDescription": "Enviar notificación cuando se resuelvan incidencias.", "components.PermissionEdit.manageissues": "Administrar incidencias", @@ -1430,7 +1427,7 @@ "components.Login.tipUsernameHasTrailingWhitespace": "El nombre de usuario termina con un espacio en blanco", "i18n.removeFromBlocklistSuccess": "{title} Se eliminó correctamente de la lista de bloqueo.", "components.RequestList.unableToConnect": "No se pudo conectar con {services}. Es posible que parte de la información no esté disponible.", - "components.PermissionEdit.blocklistedItems": "Lista negra de medios", + "components.PermissionEdit.blocklistedItems": "Lista de bloqueo de multimedia.", "components.PermissionEdit.manageblocklist": "Gestionar la lista de bloqueo", "components.PermissionEdit.viewblocklistedItems": "Ver contenido bloqueado.", "components.Settings.Notifications.NotificationsNtfy.priority": "Prioridad", @@ -1442,5 +1439,261 @@ "components.Settings.SettingsJobsCache.dnsNoCacheEntries": "Aún no se ha agregado ninguna entrada DNS al caché.", "components.RequestCard.failedmodify": "Algo salió mal al modificar la solicitud.", "components.RequestList.RequestItem.failedmodify": "Algo salió mal al modificar la solicitud.", - "i18n.notificationLocale": "Idioma de las notificaciones" + "i18n.notificationLocale": "Idioma de las notificaciones", + "components.Settings.starttyping": "", + "components.Layout.UserWarnings.profileIncomplete": "El perfil está incompleto", + "components.Login.emailAlreadyTaken": "Esta dirección de correo electrónico ya está en uso.", + "components.Login.jellyfinLocalLoginHint": "Si no has configurado una dirección de correo electrónico en tu perfil, usa tu nombre de usuario de {mediaServerName} en su lugar.", + "components.Login.saveFailed": "Algo salió mal al guardar.", + "components.MovieDetails.watchlistSuccess": "{title} añadido a la lista de seguimiento correctamente!", + "components.Settings.SettingsMain.blocklistedTagsLimit": "Limitar el contenido bloqueado por etiqueta", + "components.Settings.SettingsNetwork.networksettings": "", + "components.Settings.SonarrModal.apiKeyHelp": "", + "components.Settings.SonarrModal.baseUrlHelp": "", + "components.Settings.addrule": "", + "components.Settings.noSpecialCharacters": "", + "components.TvDetails.removefromwatchlist": "", + "components.UserList.username": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.title": "", + "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegion": "Región en sección \"Descubre\"", + "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegionTip": "Filtrar contenido por disponibilidad regional", + "components.Settings.SettingsMain.blocklistedTags": "Bloquear contenido con etiquetas", + "components.UserProfile.UserSettings.UserGeneralSettings.streamingRegion": "", + "components.UserProfile.UserSettings.UserGeneralSettings.streamingRegionTip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmailEmpty": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablewebpush": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablingwebpusherror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablewebpush": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablingwebpusherror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.managedevices": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.nodevicestoshow": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.operatingsystem": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleted": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleteerror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.type": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.unknown": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeendisabled": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeenenabled": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdAdd": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdPlaceholder": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdRemove": "", + "components.Login.urlBaseHelp": "Si has configurado una URL base en Jellyfin (Panel de control > Redes), introdúcela aquí (ej. /jellyfin). Si no, déjalo en blanco.", + "components.Setup.signinWithEmby": "", + "components.Settings.Notifications.useUserLocale": "Usar la configuración regional del destinatario de notificaciones", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorDays": "", + "components.Settings.SettingsJobsCache.failures": "", + "components.Settings.SettingsNetwork.proxyHostname": "", + "components.Setup.back": "", + "components.Setup.configemby": "", + "components.TvDetails.addtowatchlist": "", + "components.UserProfile.UserSettings.menuLinkedAccounts": "", + "components.Settings.Notifications.usePublicLogo": "Usar el logotipo público de Seerr en lugar del logotipo de la instancia", + "components.Settings.Notifications.usePublicLogoTip": "Si tu instancia de Seerr no es accesible públicamente, activa esta opción para que los clientes de correo fuera de tu red puedan mostrar la imagen. La imagen se obtendrá del repositorio público de GitHub.", + "components.Settings.RadarrModal.apiKeyHelp": "Encuéntralo en Radarr: Ajustes > General > Seguridad > Clave API", + "components.Settings.RadarrModal.baseUrlHelp": "Si has configurado una URL base en Radarr (Ajustes > General > Host), introdúcela aquí (ej. /radarr). Si no, déjalo en blanco.", + "components.Settings.RadarrModal.enableSearchHelp": "Activar automáticamente una búsqueda en Radarr cuando se apruebe una solicitud.", + "components.Settings.RadarrModal.externalUrlHelp": "Para enlaces clicables en las páginas de contenido cuando el nombre de host no es accesible desde fuera de tu red.", + "components.Settings.RadarrModal.server4kHelp": "Solo si tienes una instancia 4K separada. Déjalo sin marcar para un único servidor.", + "components.Settings.RadarrModal.syncEnabledHelp": "Analizar Radarr en busca de contenido existente y estado de solicitudes para que los usuarios no puedan solicitar contenido que ya está disponible.", + "components.Settings.SettingsJobsCache.dnscachehits": "Aciertos", + "components.Settings.SettingsJobsCache.dnscachemisses": "Fallos", + "components.Settings.SettingsJobsCache.dnscachename": "", + "components.Settings.SettingsJobsCache.flushdnscache": "Limpiar el cache de DNS", + "components.Settings.SettingsJobsCache.hitRate": "Tasa de aciertos", + "components.Settings.SettingsJobsCache.hits": "Aciertos", + "components.Settings.SettingsJobsCache.ipv4Fallbacks": "", + "components.Settings.SettingsJobsCache.misses": "", + "components.Settings.SettingsJobsCache.plex-refresh-token": "", + "components.Settings.SettingsJobsCache.size": "Tamaño", + "components.Settings.SettingsJobsCache.usersavatars": "Avatares de usuarios", + "components.Settings.SettingsMain.apikeyCopied": "Llave de API copiada al portapapeles.", + "components.Settings.SettingsMain.blocklistLanguage": "Idioma de la lista de bloqueo", + "components.Settings.SettingsMain.blocklistLanguageTip": "Idioma utilizado por el escaneado de contenido de la lista de bloqueo (independientemente de la configuración de descubrimiento)", + "components.Settings.SettingsMain.blocklistRegion": "Región de la lista de bloqueo", + "components.Settings.SettingsMain.blocklistRegionTip": "", + "components.Settings.SettingsMain.blocklistedTagsLimitTip": "", + "components.Settings.SettingsMain.blocklistedTagsTip": "", + "components.Settings.SettingsMain.discoverRegionTip": "Filtrar contenido por disponibilidad regional", + "components.Settings.SettingsMain.enableSpecialEpisodes": "Permitir la petición de episodios especiales", + "components.Settings.SettingsMain.hideAvailableTip": "Ocultar multimedia disponible de las páginas de descubrimiento pero no los resultados de busqueda", + "components.Settings.SettingsMain.hideBlocklisted": "Ocultar elementos bloqueados", + "components.Settings.SettingsMain.hideBlocklistedTip": "", + "components.Settings.SettingsMain.streamingRegion": "", + "components.Settings.SettingsMain.streamingRegionTip": "Mostrar sitios de streaming por disponibilidad regional", + "components.Settings.SettingsMain.validationUrl": "Debe proveer una URL válida", + "components.Settings.SettingsMain.validationUrlTrailingSlash": "La URL debe no terminar en una barra final", + "components.Settings.SettingsMain.youtubeUrl": "URL de YouTube", + "components.Settings.SettingsMain.youtubeUrlTip": "", + "components.Settings.SettingsNetwork.apiRequestTimeout": "", + "components.Settings.SettingsNetwork.apiRequestTimeoutTip": "", + "components.Settings.SettingsNetwork.csrfProtection": "Habilitar la protección de CSRF", + "components.Settings.SettingsNetwork.csrfProtectionHoverTip": "¡NO habilite este ajuste excepto si entiende lo que esta haciendo!", + "components.Settings.SettingsNetwork.csrfProtectionTip": "", + "components.Settings.SettingsNetwork.dnsCache": "Cache DNS", + "components.Settings.SettingsNetwork.dnsCacheForceMaxTtl": "", + "components.Settings.SettingsNetwork.dnsCacheTip": "", + "components.Settings.SettingsNetwork.docs": "", + "components.Settings.SettingsNetwork.forceIpv4First": "", + "components.Settings.SettingsNetwork.forceIpv4FirstTip": "", + "components.Settings.SettingsNetwork.networkDisclaimer": "", + "components.Settings.SettingsNetwork.networksettingsDescription": "", + "components.Settings.SettingsNetwork.proxyBypassFilter": "", + "components.Settings.SettingsNetwork.proxyBypassFilterTip": "Utilize \",\" como separador y \"*.\" como comodín para subdominios", + "components.Settings.SettingsNetwork.proxyBypassLocalAddresses": "", + "components.Settings.SettingsNetwork.proxyEnabled": "", + "components.Settings.SettingsNetwork.proxyEnabledTip": "", + "components.Settings.SettingsNetwork.proxyPassword": "Contraseña del proxy", + "components.Settings.SettingsNetwork.proxyPort": "Puerto del proxy", + "components.Settings.SettingsNetwork.proxySsl": "Utilizar SSL para el proxy", + "components.Settings.SettingsNetwork.toastSettingsFailure": "Algo ha fallado al guardar los ajustes.", + "components.Settings.SettingsNetwork.toastSettingsSuccess": "¡Ajustes guardados con éxito!", + "components.Settings.SettingsNetwork.trustProxy": "Habilitar soporte para proxy", + "components.Settings.SettingsNetwork.trustProxyTip": "", + "components.Settings.SettingsNetwork.validationApiRequestTimeout": "Debe proveer un valor valido para la expiración", + "components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "", + "components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "", + "components.Settings.SettingsNetwork.validationProxyPort": "", + "components.Settings.SettingsUsers.atLeastOneAuth": "", + "components.Settings.SettingsUsers.disabledMediaServerLoginWarning": "", + "components.Settings.SettingsUsers.loginMethods": "", + "components.Settings.SettingsUsers.loginMethodsTip": "", + "components.Settings.SettingsUsers.mediaServerLogin": "", + "components.Settings.SettingsUsers.mediaServerLoginTip": "", + "components.Settings.SonarrModal.enableSearchHelp": "", + "components.Settings.SonarrModal.externalUrlHelp": "", + "components.Settings.SonarrModal.monitorNewItemsHelp": "", + "components.Settings.SonarrModal.server4kHelp": "", + "components.Settings.SonarrModal.syncEnabledHelp": "", + "components.Settings.allChosenProvidersAreOperational": "", + "components.Settings.animeMetadataProvider": "", + "components.Settings.chooseProvider": "", + "components.Settings.clearBlocklistedTagsConfirm": "", + "components.Settings.clickTest": "", + "components.Settings.connectionTestFailed": "", + "components.Settings.copyBlocklistedTags": "", + "components.Settings.copyBlocklistedTagsEmpty": "", + "components.Settings.copyBlocklistedTagsTip": "", + "components.Settings.failed": "", + "components.Settings.failedToSaveMetadataSettings": "", + "components.Settings.general": "", + "components.Settings.importBlocklistedTagsTip": "", + "components.Settings.invalidKeyword": "", + "components.Settings.invalidurlerror": "", + "components.Settings.jellyfinForgotPasswordUrl": "", + "components.Settings.jellyfinSyncFailedAutomaticGroupedFolders": "", + "components.Settings.jellyfinSyncFailedGenericError": "Algo falló mientras se sincronizaban las bibliotecas", + "components.Settings.jellyfinSyncFailedNoLibrariesFound": "No se encontró ninguna biblioteca", + "components.Settings.menuMetadataProviders": "", + "components.Settings.menuNetwork": "", + "components.Settings.metadataProviderSelection": "", + "components.Settings.metadataProviderSettings": "", + "components.Settings.metadataSettings": "", + "components.Settings.metadataSettingsSaved": "", + "components.Settings.no": "", + "components.Settings.operational": "", + "components.Settings.overrideRules": "", + "components.Settings.overrideRulesDescription": "", + "components.Settings.providerStatus": "", + "components.Settings.scanbackground": "", + "components.Settings.searchKeywords": "", + "components.Settings.seriesMetadataProvider": "", + "components.Settings.tip": "", + "components.Settings.tmdbProviderDoesnotWork": "", + "components.Settings.valueRequired": "", + "components.Settings.yes": "", + "components.Setup.configplex": "", + "components.Setup.librarieserror": "", + "components.Setup.servertype": "", + "components.Setup.subtitle": "", + "components.StatusBadge.seasonnumber": "", + "components.TvDetails.watchlistDeleted": "", + "components.TvDetails.watchlistError": "", + "components.TvDetails.watchlistSuccess": "", + "components.UserList.ascending": "", + "components.UserList.descending": "", + "components.UserList.importedPlexUsersNoPassword": "", + "components.UserList.importedUsersNoPassword": "", + "components.UserList.sortBy": "", + "components.UserList.sortByJoined": "", + "components.UserList.sortByRequests": "", + "components.UserList.sortByType": "", + "components.UserList.sortByUser": "", + "components.UserList.toggleSortDirection": "", + "components.UserList.validationUsername": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.description": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorExists": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorUnauthorized": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorUnknown": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.password": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.passwordRequired": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.saving": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.username": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.usernameRequired": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadId": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadIdTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramMessageThreadId": "", + "components.UserProfile.UserSettings.UserPasswordChange.localPasswordDescription": "", + "i18n.addToBlocklist": "", + "i18n.blocklistDuplicateError": "", + "i18n.blocklistSuccess": "", + "i18n.blocklisted": "", + "i18n.deleted": "", + "i18n.error": "", + "i18n.partiallyblocklisted": "", + "components.Settings.Notifications.NotificationsNtfy.token": "Token", + "components.Settings.SettingsJobsCache.process-blocklisted-tags": "", + "components.Settings.SettingsMain.discoverRegion": "Región de descubrir", + "components.Settings.SettingsNetwork.dnsCacheForceMinTtl": "", + "components.Settings.SettingsNetwork.dnsCacheHoverTip": "", + "components.Settings.blocklistedTagImportInstructions": "", + "components.Settings.blocklistedTagImportTitle": "", + "components.Settings.blocklistedTagsText": "", + "components.Settings.tvdbProviderDoesnotWork": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmail": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.save": "", + "components.UserList.sortByRole": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.engine": "", + "i18n.completed": "", + "components.Settings.SettingsNetwork.network": "", + "components.MetadataSelector.tmdbLabel": "The Movie Database (TMDB)", + "components.MetadataSelector.tvdbLabel": "TheTVDB", + "components.Settings.nooptions": "", + "components.Settings.SonarrModal.monitorNewItems": "", + "components.Setup.configjellyfin": "", + "i18n.blocklistError": "", + "components.Settings.notTested": "", + "components.UserList.toggleSortDirectionAria": "", + "components.Settings.settings": "", + "components.Settings.apiKey": "", + "components.Settings.SettingsNetwork.proxyUser": "Nombre de usuario del proxy", + "i18n.blocklist": "", + "components.Settings.Notifications.validationWebhookThreadId": "Debes proporcionar un ID de hilo de Discord válido", + "components.Settings.Notifications.webhookThreadId": "ID de hilo", + "components.Settings.Notifications.webhookThreadIdTip": "El ID del canal de hilo donde publicar las notificaciones. Déjalo vacío para publicar en el canal del webhook", + "components.Settings.Notifications.NotificationsGotify.embedPoster": "Incrustar poster", + "components.Common.QuickConnectModal.error": "Error", + "components.Common.QuickConnectModal.expired": "Código expirado", + "components.Common.QuickConnectModal.expiredMessage": "Este código de conexión rápida ha expirado, por favor intentelo de nuevo.", + "components.Common.QuickConnectModal.tryAgain": "Volver a intentarlo", + "components.Common.QuickConnectModal.waitingForAuth": "Esperando a la autorización...", + "components.Login.JellyfinQuickConnectModal.cancel": "Cancelar", + "components.Login.JellyfinQuickConnectModal.instructions": "Introduce este código en tu aplicación de {mediaServerName}", + "components.Login.JellyfinQuickConnectModal.subtitle": "Iniciar sesión con conexión rápida", + "components.Login.JellyfinQuickConnectModal.title": "Conexión rápida", + "components.Login.quickconnect": "Conexión rápida", + "components.Login.quickconnecterror": "La conexión rápida ha fallado, por favor intentelo de nuevo.", + "components.ManageSlideOver.clearmediadataerror": "Algo ha fallado al intentar borrar los datos de la multimedia.", + "components.ManageSlideOver.removemediaerror": "Algo ha fallado al intentar borrar la multimedia.", + "components.RequestList.RequestItem.removemediaerror": "Algo ha fallado al intentar borrar la multimedia.", + "components.RequestModal.AdvancedRequester.ignoreQuotaDescription": "Esta petición no contará contra los límites de cuota del usuario, usar con precaución.", + "components.Settings.SettingsAbout.versionCheckDisabled": "Comprobación de versión desactivada", + "components.Settings.SettingsMain.versionCheck": "Comprobación de versión", + "components.Settings.SettingsMain.versionCheckTip": "Comprobar automáticamente GitHub para buscar nuevas versiones.", + "components.RequestModal.AdvancedRequester.ignoreQuotaTitle": "Omitir de la cuota de usuario", + "components.UserProfile.UserSettings.LinkJellyfinQuickConnectModal.subtitle": "Conexión rápida", + "components.UserProfile.UserSettings.LinkJellyfinQuickConnectModal.title": "Vincular cuenta de {mediaServerName}", + "components.UserProfile.UserSettings.LinkJellyfinQuickConnectModal.usePassword": "Usar contraseña en su lugar", + "components.Settings.jellyfinSyncFailedConnectionError": "No se pudo conectar con el servidor de {mediaServerName} . Compruebe que se esta ejecutando y es accesible desde Seerr." } diff --git a/src/i18n/locale/es_MX.json b/src/i18n/locale/es_MX.json index b5b3e20a90..955495a7fa 100644 --- a/src/i18n/locale/es_MX.json +++ b/src/i18n/locale/es_MX.json @@ -1042,7 +1042,6 @@ "components.TvDetails.Season.somethingwentwrong": "Algo ha ido mal al obtener los datos de la temporada.", "components.TvDetails.episodeCount": "{episodeCount, plural, one {# Episodio} other {# Episodios}}", "components.TvDetails.rtaudiencescore": "Puntuación audiencia de Rotten Tomatoes", - "components.UserProfile.UserSettings.UserGeneralSettings.discordIdTip": "El multi-digit ID number asociado a su cuenta de usuario de Discord", "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmovies": "Solicitar Películas automáticamente", "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmoviestip": "Solicite películas automáticamente en su Plex Watchlist", "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseriestip": "Solicite series automáticamente series en su Plex Watchlist", @@ -1076,7 +1075,6 @@ "components.StatusChecker.appUpdatedDescription": "Haga clic en el botón de abajo para recargar la aplicación.", "components.StatusChecker.restartRequiredDescription": "Reinicie el servidor para aplicar la configuración actualizada.", "components.TitleCard.cleardata": "Limpiar Datos", - "components.UserProfile.UserSettings.UserGeneralSettings.discordId": "ID de usuario de Discord", "components.Settings.Notifications.NotificationsGotify.toastGotifyTestFailed": "No se ha podido enviar la notificación de prueba de Gotify.", "components.Settings.Notifications.NotificationsGotify.toastGotifyTestSending": "Enviando notificación de prueba de Gotify…", "components.StatusChecker.restartRequired": "Es necesario reiniciar el servidor", @@ -1139,7 +1137,6 @@ "components.TitleCard.tvdbid": "Identificador de TheTVDB", "components.TvDetails.Season.noepisodes": "Lista de episodios no disponible.", "components.TvDetails.productioncountries": "Producción {countryCount, plural, one {País} other {Países}}", - "components.UserProfile.UserSettings.UserGeneralSettings.validationDiscordId": "Debe proporcionar un ID de Discord válido", "i18n.restartRequired": "Reinicio necesario", "components.Settings.deleteServer": "Eliminar servidor {serverType}", "components.Settings.experimentalTooltip": "Activar esta opción puede provocar un comportamiento inesperado de la aplicación", @@ -1184,5 +1181,492 @@ "components.Login.urlBaseHelp": "Si configuras una URL de base en Jellyfin (Panel > Red), ingrésala aquí (p. ej. /jellyfin). Déjala en blanco de lo contrario.", "components.RequestCard.failedmodify": "Algo salió mal al modificar la solicitud.", "components.RequestList.RequestItem.failedmodify": "Algo salió mal al modificar la solicitud.", - "i18n.notificationLocale": "Idioma de las notificationes" + "i18n.notificationLocale": "Idioma de las notificationes", + "components.Layout.UserWarnings.profileIncomplete": "", + "components.Login.emailAlreadyTaken": "", + "components.Login.jellyfinLocalLoginHint": "", + "components.Login.saveFailed": "", + "components.Login.signinwithjellyfin": "", + "components.Login.validationusernamerequired": "", + "components.ManageSlideOver.removearr4k": "", + "components.MetadataSelector.selectMetdataProvider": "", + "components.MetadataSelector.tmdbLabel": "", + "components.MovieDetails.addtowatchlist": "", + "components.MovieDetails.openradarr": "", + "components.MovieDetails.openradarr4k": "", + "components.MovieDetails.play4k": "", + "components.MovieDetails.watchlistDeleted": "", + "components.MovieDetails.watchlistSuccess": "", + "components.NotificationTypeSelector.userissuecommentDescription": "", + "components.NotificationTypeSelector.userissuecreatedDescription": "", + "components.NotificationTypeSelector.userissuereopenedDescription": "", + "components.NotificationTypeSelector.userissueresolvedDescription": "", + "components.PermissionEdit.createissuesDescription": "", + "components.PermissionEdit.manageissues": "", + "components.PermissionEdit.manageissuesDescription": "", + "components.RequestList.RequestItem.profileName": "", + "components.Selector.CertificationSelector.selectCountry": "", + "components.Selector.CertificationSelector.starttyping": "", + "components.Selector.inProduction": "", + "components.Settings.Notifications.NotificationsGotify.priority": "", + "components.Settings.Notifications.NotificationsGotify.validationPriorityRequired": "", + "components.Settings.Notifications.NotificationsNtfy.agentenabled": "", + "components.Settings.Notifications.NotificationsNtfy.usernamePasswordAuth": "", + "components.Settings.Notifications.NotificationsNtfy.validationNtfyTopic": "", + "components.Settings.Notifications.NotificationsNtfy.validationNtfyUrl": "", + "components.Settings.Notifications.NotificationsNtfy.validationPriorityRequired": "", + "components.Settings.Notifications.NotificationsNtfy.validationTypes": "", + "components.Settings.Notifications.NotificationsPushover.embedPoster": "", + "components.Settings.Notifications.embedPoster": "", + "components.Settings.OverrideRuleModal.notagoptions": "", + "components.Settings.OverrideRuleModal.qualityprofile": "", + "components.Settings.OverrideRuleModal.rootfolder": "", + "components.Settings.OverrideRuleModal.ruleCreated": "", + "components.Settings.OverrideRuleModal.ruleUpdated": "", + "components.Settings.OverrideRuleModal.selectQualityProfile": "", + "components.Settings.OverrideRuleModal.selectRootFolder": "", + "components.Settings.OverrideRuleModal.selectService": "", + "components.Settings.OverrideRuleModal.settings": "", + "components.Settings.OverrideRuleModal.settingsDescription": "", + "components.Settings.OverrideRuleModal.tags": "", + "components.Settings.OverrideRuleModal.users": "", + "components.Settings.OverrideRuleTile.conditions": "", + "components.Settings.OverrideRuleTile.genre": "", + "components.Settings.OverrideRuleTile.keywords": "", + "components.Settings.OverrideRuleTile.language": "", + "components.Settings.OverrideRuleTile.qualityprofile": "", + "components.Settings.OverrideRuleTile.rootfolder": "", + "components.Settings.OverrideRuleTile.settings": "", + "components.Settings.OverrideRuleTile.users": "", + "components.Settings.SettingsJobsCache.ipv4Fallbacks": "", + "components.Settings.SettingsNetwork.proxyBypassFilter": "", + "components.Settings.SettingsNetwork.proxyBypassFilterTip": "", + "components.Settings.SettingsUsers.loginMethods": "", + "components.Settings.jellyfinForgotPasswordUrl": "", + "components.Settings.jellyfinSettings": "", + "components.TvDetails.play4k": "", + "components.UserList.descending": "", + "components.UserList.importedfromJellyfin": "", + "components.UserList.sortByRole": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.title": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.username": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.usernameRequired": "", + "components.UserProfile.UserSettings.UserGeneralSettings.streamingRegionTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.created": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablewebpush": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.engine": "", + "components.UserProfile.UserSettings.UserGeneralSettings.save": "", + "components.UserProfile.UserSettings.UserGeneralSettings.saving": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.noLinkedAccounts": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.noPermissionDescription": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorExists": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorUnauthorized": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.browser": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.deletesubscription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.device": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablewebpush": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablingwebpusherror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.deviceDefault": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdAdd": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdPlaceholder": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdRemove": "", + "components.ManageSlideOver.manageModalRemoveMediaWarning": "", + "components.MovieDetails.downloadstatus": "", + "components.MovieDetails.imdbuserscore": "", + "components.NotificationTypeSelector.issueresolvedDescription": "", + "components.Selector.pilot": "", + "components.Selector.planned": "", + "components.Selector.returningSeries": "", + "components.Selector.searchStatus": "", + "components.Selector.searchUsers": "", + "components.Settings.Notifications.NotificationsNtfy.embedPoster": "", + "components.Settings.Notifications.NotificationsNtfy.ntfysettingsfailed": "", + "components.Settings.Notifications.NotificationsNtfy.ntfysettingssaved": "", + "components.Settings.Notifications.NotificationsNtfy.password": "", + "components.Settings.Notifications.NotificationsNtfy.priority": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestFailed": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestSending": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestSuccess": "", + "components.Settings.Notifications.NotificationsNtfy.token": "", + "components.Settings.Notifications.NotificationsNtfy.tokenAuth": "", + "components.Settings.Notifications.NotificationsNtfy.topic": "", + "components.Settings.Notifications.NotificationsNtfy.url": "", + "components.Settings.Notifications.NotificationsNtfy.username": "", + "components.PermissionEdit.createissues": "", + "components.Settings.Notifications.messageThreadIdTip": "", + "components.Settings.OverrideRuleModal.create": "", + "components.Settings.OverrideRuleModal.createrule": "", + "components.Settings.OverrideRuleModal.editrule": "", + "components.Settings.SettingsNetwork.validationApiRequestTimeout": "", + "components.TitleCard.watchlistSuccess": "", + "components.Settings.SettingsNetwork.docs": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletsettingsfailed": "", + "components.Layout.UserWarnings.emailInvalid": "", + "components.Layout.UserWarnings.emailRequired": "", + "components.Login.emailtooltip": "", + "components.Login.enablessl": "", + "components.Login.save": "", + "components.Login.description": "", + "components.Login.saving": "", + "components.Login.servertype": "", + "components.DiscoverTvUpcoming.upcomingtv": "", + "components.Discover.FilterSlideover.certification": "", + "components.IssueList.IssueItem.descriptionpreview": "", + "component.BlocklistModal.blocklisting": "", + "components.Blocklist.blocklistNotFoundError": "", + "components.Blocklist.filterBlocklistedTags": "", + "components.Blocklist.filterManual": "", + "components.Blocklist.mediaName": "", + "components.Blocklist.mediaTmdbId": "", + "components.Blocklist.mediaType": "", + "components.Blocklist.showAllBlocklisted": "", + "components.PermissionEdit.manageblocklist": "", + "components.PermissionEdit.manageblocklistDescription": "", + "components.PermissionEdit.viewblocklistedItems": "", + "components.PermissionEdit.viewblocklistedItemsDescription": "", + "components.Login.hostname": "", + "components.Login.initialsignin": "", + "components.Login.initialsigningin": "", + "components.Login.invalidurlerror": "", + "components.Login.port": "", + "components.Login.loginwithapp": "", + "components.Login.noadminerror": "", + "components.Login.orsigninwith": "", + "components.MetadataSelector.tvdbLabel": "", + "components.RequestList.unableToConnect": "", + "components.Discover.FilterSlideover.excludeKeywords": "", + "component.BlocklistBlock.blocklistedby": "", + "components.Blocklist.blocklistSettingsDescription": "", + "components.Blocklist.blocklistdate": "", + "components.Blocklist.blocklistedby": "", + "components.Blocklist.blocklistsettings": "", + "components.Layout.Sidebar.blocklist": "", + "components.PermissionEdit.blocklistedItems": "", + "components.PermissionEdit.blocklistedItemsDescription": "", + "components.Login.urlBase": "", + "components.Login.validationhostformat": "", + "components.Login.validationhostrequired": "", + "components.PermissionEdit.viewissues": "", + "components.PermissionEdit.viewissuesDescription": "", + "components.Login.validationservertyperequired": "", + "components.Login.tipEmailHasTrailingWhitespace": "", + "components.Login.tipUsernameHasTrailingWhitespace": "", + "components.Discover.RecentRequestsSlider.unableToConnect": "", + "components.Settings.Notifications.NotificationsWebPush.embedPoster": "", + "components.Settings.Notifications.NotificationsWebhook.customHeaders": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersAdd": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersAuthConflict": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersIncomplete": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersKey": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersRemove": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersTip": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersValue": "", + "components.Settings.Notifications.NotificationsWebhook.supportVariables": "", + "components.Settings.Notifications.NotificationsWebhook.supportVariablesTip": "", + "components.Settings.OverrideRuleModal.keywords": "", + "components.Settings.SettingsNetwork.networksettings": "", + "components.Settings.addrule": "", + "components.Setup.configplex": "", + "components.Setup.configuremediaserver": "", + "components.TvDetails.watchlistError": "", + "i18n.blocklistError": "", + "components.Settings.SettingsNetwork.forceIpv4FirstTip": "", + "components.Settings.Notifications.usePublicLogo": "", + "components.Settings.Notifications.usePublicLogoTip": "", + "components.Settings.Notifications.useUserLocale": "", + "components.Settings.Notifications.validationMessageThreadId": "", + "components.Settings.Notifications.validationWebhookRoleId": "", + "components.Settings.Notifications.webhookRoleIdTip": "", + "components.Settings.OverrideRuleModal.conditionsDescription": "", + "components.Settings.OverrideRuleModal.languages": "", + "components.Settings.OverrideRuleModal.selecttags": "", + "components.Settings.OverrideRuleModal.service": "", + "components.Settings.OverrideRuleModal.serviceDescription": "", + "components.Settings.OverrideRuleTile.tags": "", + "components.Settings.RadarrModal.tagRequestsInfo": "", + "components.Settings.SettingsAbout.aboutseerr": "", + "components.Settings.SettingsAbout.supportseerr": "", + "components.Settings.SettingsJobsCache.dnsCacheGlobalStats": "", + "components.Settings.SettingsJobsCache.dnsCacheGlobalStatsDescription": "", + "components.Settings.SettingsJobsCache.dnscacheactiveaddress": "", + "components.Settings.SettingsJobsCache.dnscacheage": "", + "components.Settings.SettingsJobsCache.dnscachename": "", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorDays": "", + "components.Settings.SettingsJobsCache.failures": "", + "components.Settings.SettingsJobsCache.flushdnscache": "", + "components.Settings.SettingsJobsCache.hitRate": "", + "components.Settings.SettingsJobsCache.hits": "", + "components.Settings.SettingsJobsCache.jellyfin-full-scan": "", + "components.Settings.SettingsJobsCache.jellyfin-recently-added-scan": "", + "components.Settings.SettingsJobsCache.misses": "", + "components.Settings.SettingsJobsCache.plex-refresh-token": "", + "components.Settings.SettingsJobsCache.process-blocklisted-tags": "", + "components.Settings.SettingsJobsCache.size": "", + "components.Settings.SettingsJobsCache.usersavatars": "", + "components.Settings.SettingsMain.apikeyCopied": "", + "components.Settings.SettingsMain.blocklistLanguage": "", + "components.Settings.SettingsMain.blocklistLanguageTip": "", + "components.Settings.SettingsMain.blocklistRegion": "", + "components.Settings.SettingsMain.blocklistRegionTip": "", + "components.Settings.SettingsMain.blocklistedTags": "", + "components.Settings.SettingsMain.blocklistedTagsLimit": "", + "components.Settings.SettingsMain.blocklistedTagsLimitTip": "", + "components.Settings.SettingsMain.blocklistedTagsTip": "", + "components.Settings.SettingsMain.discoverRegionTip": "", + "components.Settings.SettingsMain.enableSpecialEpisodes": "", + "components.Settings.SettingsMain.hideAvailableTip": "", + "components.Settings.SettingsMain.hideBlocklisted": "", + "components.Settings.SettingsMain.hideBlocklistedTip": "", + "components.Settings.SettingsMain.streamingRegion": "", + "components.Settings.SettingsMain.streamingRegionTip": "", + "components.Settings.SettingsMain.validationUrl": "", + "components.Settings.SettingsMain.validationUrlTrailingSlash": "", + "components.Settings.SettingsMain.youtubeUrl": "", + "components.Settings.SettingsMain.youtubeUrlTip": "", + "components.Settings.SettingsNetwork.apiRequestTimeout": "", + "components.Settings.SettingsNetwork.apiRequestTimeoutTip": "", + "components.Settings.SettingsNetwork.csrfProtection": "", + "components.Settings.SettingsNetwork.csrfProtectionHoverTip": "", + "components.Settings.SettingsNetwork.csrfProtectionTip": "", + "components.Settings.SettingsNetwork.dnsCache": "", + "components.Settings.SettingsNetwork.dnsCacheForceMaxTtl": "", + "components.Settings.SettingsNetwork.dnsCacheForceMinTtl": "", + "components.Settings.SettingsNetwork.dnsCacheHoverTip": "", + "components.Settings.SettingsNetwork.dnsCacheTip": "", + "components.Settings.SettingsNetwork.forceIpv4First": "", + "components.Settings.SettingsNetwork.network": "", + "components.Settings.SettingsNetwork.networkDisclaimer": "", + "components.Settings.SettingsNetwork.networksettingsDescription": "", + "components.Settings.SettingsNetwork.proxyEnabledTip": "", + "components.Settings.SettingsNetwork.proxyHostname": "", + "components.Settings.SettingsNetwork.proxyPassword": "", + "components.Settings.SettingsNetwork.proxyPort": "", + "components.Settings.SettingsNetwork.proxySsl": "", + "components.Settings.SettingsNetwork.proxyUser": "", + "components.Settings.SettingsNetwork.toastSettingsFailure": "", + "components.Settings.SettingsNetwork.toastSettingsSuccess": "", + "components.Settings.SettingsNetwork.trustProxy": "", + "components.Settings.SettingsNetwork.trustProxyTip": "", + "components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "", + "components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "", + "components.Settings.SettingsNetwork.validationProxyPort": "", + "components.Settings.SettingsUsers.atLeastOneAuth": "", + "components.Settings.SettingsUsers.disabledMediaServerLoginWarning": "", + "components.Settings.SettingsUsers.loginMethodsTip": "", + "components.Settings.SettingsUsers.mediaServerLogin": "", + "components.Settings.SettingsUsers.mediaServerLoginTip": "", + "components.Settings.SonarrModal.animeSeriesType": "", + "components.Settings.SonarrModal.seriesType": "", + "components.Settings.SonarrModal.tagRequestsInfo": "", + "components.Settings.allChosenProvidersAreOperational": "", + "components.Settings.animeMetadataProvider": "", + "components.Settings.apiKey": "", + "components.Settings.blocklistedTagImportInstructions": "", + "components.Settings.blocklistedTagImportTitle": "", + "components.Settings.blocklistedTagsText": "", + "components.Settings.chooseProvider": "", + "components.Settings.clearBlocklistedTagsConfirm": "", + "components.Settings.copyBlocklistedTags": "", + "components.Settings.copyBlocklistedTagsEmpty": "", + "components.Settings.copyBlocklistedTagsTip": "", + "components.Settings.failed": "", + "components.Settings.failedToSaveMetadataSettings": "", + "components.Settings.general": "", + "components.Settings.importBlocklistedTagsTip": "", + "components.Settings.invalidKeyword": "", + "components.Settings.invalidurlerror": "", + "components.Settings.jellyfinSettingsDescription": "", + "components.Settings.jellyfinSettingsFailure": "", + "components.Settings.jellyfinSyncFailedNoLibrariesFound": "", + "components.Settings.jellyfinlibraries": "", + "components.Settings.jellyfinlibrariesDescription": "", + "components.Settings.jellyfinsettings": "", + "components.Settings.jellyfinsettingsDescription": "", + "components.Settings.manualscanDescriptionJellyfin": "", + "components.Settings.notTested": "", + "components.Settings.operational": "", + "components.Settings.overrideRules": "", + "components.Settings.overrideRulesDescription": "", + "components.Settings.save": "", + "components.Settings.saving": "", + "components.Settings.scanbackground": "", + "components.Settings.searchKeywords": "", + "components.Settings.seriesMetadataProvider": "", + "components.Settings.starttyping": "", + "components.Settings.syncJellyfin": "", + "components.Settings.syncing": "", + "components.Settings.timeout": "", + "components.Settings.tip": "", + "components.Settings.tmdbProviderDoesnotWork": "", + "components.Settings.tvdbProviderDoesnotWork": "", + "components.Settings.yes": "", + "components.Setup.back": "", + "components.Setup.configemby": "", + "components.Setup.configjellyfin": "", + "components.Setup.librarieserror": "", + "components.Setup.servertype": "", + "components.Setup.signinWithEmby": "", + "components.Setup.signinWithJellyfin": "", + "components.Setup.signinWithPlex": "", + "components.Setup.subtitle": "", + "components.StatusBadge.seasonnumber": "", + "components.TitleCard.addToWatchList": "", + "components.TitleCard.watchlistDeleted": "", + "components.TitleCard.watchlistError": "", + "components.TvDetails.addtowatchlist": "", + "components.TvDetails.play": "", + "components.TvDetails.removefromwatchlist": "", + "components.TvDetails.watchlistSuccess": "", + "components.UserList.ascending": "", + "components.UserList.importedPlexUsersNoPassword": "", + "components.UserList.importedUsersNoPassword": "", + "components.UserList.importfromJellyfinerror": "", + "components.UserList.importfrommediaserver": "", + "components.UserList.mediaServerUser": "", + "components.UserList.newJellyfinsigninenabled": "", + "components.UserList.noJellyfinuserstoimport": "", + "components.UserList.sortBy": "", + "components.UserList.sortByJoined": "", + "components.UserList.sortByRequests": "", + "components.UserList.sortByType": "", + "components.UserList.sortByUser": "", + "components.UserList.toggleSortDirection": "", + "components.UserList.toggleSortDirectionAria": "", + "components.UserList.validationUsername": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.description": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorUnauthorized": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.passwordRequired": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.save": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.saving": "", + "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegionTip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmail": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmailEmpty": "", + "components.UserProfile.UserSettings.UserGeneralSettings.validationemailformat": "", + "components.UserProfile.UserSettings.UserGeneralSettings.validationemailrequired": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.deleteFailed": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.errorUnknown": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.linkedAccounts": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.linkedAccountsHint": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablingwebpusherror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.managedevices": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.nodevicestoshow": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleted": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleteerror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.type": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.unknown": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeendisabled": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeenenabled": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessToken": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessTokenTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverApplicationTokenTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverUserKeyTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoversettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.sound": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadIdTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPushbulletAccessToken": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverApplicationToken": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverUserKey": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramMessageThreadId": "", + "components.UserProfile.UserSettings.UserPasswordChange.localPasswordDescription": "", + "components.UserProfile.UserSettings.menuLinkedAccounts": "", + "components.UserProfile.localWatchlist": "", + "i18n.blocklist": "", + "i18n.blocklistDuplicateError": "", + "i18n.blocklistSuccess": "", + "i18n.blocklisted": "", + "i18n.completed": "", + "i18n.deleted": "", + "i18n.error": "", + "i18n.open": "", + "i18n.partiallyblocklisted": "", + "i18n.removeFromBlocklistSuccess": "", + "i18n.removefromBlocklist": "", + "i18n.resolved": "", + "i18n.specials": "", + "components.RequestList.RequestItem.removearr": "", + "components.Settings.SettingsJobsCache.dnsCache": "", + "components.Settings.SettingsJobsCache.dnsCacheDescription": "", + "components.Settings.SettingsMain.discoverRegion": "", + "components.Settings.clickTest": "", + "components.Settings.connectionTestFailed": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.password": "", + "components.UserProfile.UserSettings.UserGeneralSettings.streamingRegion": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.operatingsystem": "", + "i18n.addToBlocklist": "", + "components.Settings.SettingsJobsCache.dnsNoCacheEntries": "", + "components.Settings.SettingsJobsCache.dnscacheflushed": "", + "components.UserList.username": "", + "components.Settings.SonarrModal.monitorNewItems": "", + "components.Settings.valueRequired": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverApplicationToken": "", + "components.Login.back": "", + "components.Settings.no": "", + "components.Settings.SettingsJobsCache.dnscachehits": "", + "components.Settings.SettingsJobsCache.dnscachemisses": "", + "components.NotificationTypeSelector.issuereopenedDescription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoversettingsfailed": "", + "component.BlocklistBlock.blocklistdate": "", + "components.Settings.OverrideRuleModal.genres": "", + "components.MovieDetails.play": "", + "components.MovieDetails.watchlistError": "", + "components.Selector.CertificationSelector.selectCertification": "", + "components.Settings.Notifications.NotificationsWebhook.webhookUrlTip": "", + "components.Settings.OverrideRuleModal.conditions": "", + "components.TitleCard.watchlistCancel": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorExists": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingsfailed": "", + "components.Discover.FilterSlideover.status": "", + "components.Settings.Notifications.NotificationsPushover.deviceDefault": "", + "components.Settings.nooptions": "", + "components.TvDetails.watchlistDeleted": "", + "components.UserList.importfromJellyfin": "", + "components.Selector.canceled": "", + "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegion": "", + "components.UserProfile.UserSettings.UserGeneralSettings.email": "", + "components.UserProfile.UserSettings.UserGeneralSettings.mediaServerUser": "", + "components.Settings.providerStatus": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverUserKey": "", + "components.Settings.menuMetadataProviders": "", + "components.RequestList.sortDirection": "", + "components.Discover.timeWindowDay": "", + "components.Discover.timeWindowWeek": "", + "components.Login.adminerror": "", + "components.Login.credentialerror": "", + "components.NotificationTypeSelector.issueresolved": "", + "components.MovieDetails.removefromwatchlist": "", + "components.CollectionDetails.removefromblocklistpartialcount": "", + "components.ManageSlideOver.removearr": "", + "components.Selector.CertificationSelector.errorLoading": "", + "components.Selector.CertificationSelector.maxRating": "", + "components.Selector.CertificationSelector.minRating": "", + "components.Selector.CertificationSelector.noOptions": "", + "components.Layout.UserWarnings.passwordRequired": "", + "components.Settings.Notifications.webhookRoleId": "", + "components.Settings.SettingsNetwork.proxyEnabled": "", + "components.Settings.jellyfinSettingsSuccess": "", + "components.Settings.jellyfinSyncFailedAutomaticGroupedFolders": "", + "components.Settings.jellyfinSyncFailedGenericError": "", + "components.Login.title": "", + "components.Settings.SettingsAbout.contribute": "", + "components.Settings.SettingsNetwork.proxyBypassLocalAddresses": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadId": "", + "components.Settings.Notifications.NotificationsSlack.embedPoster": "", + "components.Settings.Notifications.messageThreadId": "", + "components.Settings.Notifications.userEmailRequired": "", + "components.Settings.manualscanJellyfin": "", + "components.Settings.settings": "", + "components.Setup.signin": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorUnknown": "", + "components.Selector.ended": "", + "components.Settings.Notifications.NotificationsPushover.sound": "", + "components.Settings.menuJellyfinSettings": "", + "components.Settings.menuNetwork": "", + "components.Settings.metadataProviderSelection": "", + "components.Settings.metadataProviderSettings": "", + "components.Settings.metadataSettings": "", + "components.Settings.metadataSettingsSaved": "", + "components.Settings.noSpecialCharacters": "" } diff --git a/src/i18n/locale/et.json b/src/i18n/locale/et.json index e94755fff4..559f1225a4 100644 --- a/src/i18n/locale/et.json +++ b/src/i18n/locale/et.json @@ -1407,8 +1407,6 @@ "components.UserProfile.UserSettings.UserGeneralSettings.accounttype": "Konto tüüp", "components.UserProfile.UserSettings.UserGeneralSettings.admin": "Administraator", "components.UserProfile.UserSettings.UserGeneralSettings.applanguage": "Kuvakeel", - "components.UserProfile.UserSettings.UserGeneralSettings.discordId": "Discordi kasutaja ID", - "components.UserProfile.UserSettings.UserGeneralSettings.discordIdTip": "Sinu Discordi kasutajakontoga seotud mitmekohaline ID-number", "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegion": "Avastamise piirkond", "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegionTip": "Filtreeri sisu piirkondliku kättesaadavuse järgi", "components.UserProfile.UserSettings.UserGeneralSettings.displayName": "Kuvatav nimi", @@ -1441,7 +1439,6 @@ "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmailEmpty": "See kasutajanimi on juba teise kasutaja poolt võetud. Pead lisama e-posti aadressi", "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsSuccess": "Seaded on edukalt salvestatud!", "components.UserProfile.UserSettings.UserGeneralSettings.user": "Kasutaja", - "components.UserProfile.UserSettings.UserGeneralSettings.validationDiscordId": "Pead sisestama kehtiva Discordi kasutaja ID", "components.UserProfile.UserSettings.UserGeneralSettings.validationemailformat": "Vajalik on kehtiv e-posti aadress", "components.UserProfile.UserSettings.UserGeneralSettings.validationemailrequired": "E-posti aadress on nõutav", "components.UserProfile.UserSettings.UserLinkedAccountsSettings.deleteFailed": "Lingitud konto kustutamine ebaõnnestus.", @@ -1666,5 +1663,10 @@ "components.UserList.importedPlexUsersNoPassword": "Imporditud kasutajatel puudub {applicationTitle} parool. Kui Plexi sisselogimine keelata, peavad nad parooli määrama profiilis või kasutama parooli lähtestamise linki.", "components.UserList.importedUsersNoPassword": "Imporditud kasutajatel puudub {applicationTitle} parool. Kui {mediaServerName} sisselogimine keelata, peavad nad parooli määrama profiilis või kasutama parooli lähtestamise linki.", "components.UserProfile.UserSettings.UserPasswordChange.localPasswordDescription": "See parool on mõeldud sisselogimiseks {applicationTitle} lokaalse sisselogimise kaudu. See ei ole sama mis sinu meediaserveri parool.", - "i18n.notificationLocale": "Teavituste keel" + "i18n.notificationLocale": "Teavituste keel", + "components.Settings.Notifications.usePublicLogo": "Kasuta avalikku Seerri logo instantsi logo asemel", + "components.Settings.Notifications.usePublicLogoTip": "Kui sinu Seerri instants ei ole avalikult ligipääsetav, luba see valik, et sinu võrgust väljas olevad e-posti kliendid saaksid pilti kuvada. Pilt laaditakse avalikust GitHubi repositooriumist.", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdAdd": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdPlaceholder": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdRemove": "" } diff --git a/src/i18n/locale/eu.json b/src/i18n/locale/eu.json index a66276ab50..d52e2fa74e 100644 --- a/src/i18n/locale/eu.json +++ b/src/i18n/locale/eu.json @@ -418,7 +418,7 @@ "components.Settings.noDefaultServer": "Gutxienez {serverType} zerbitzari bat lehenetsi gisa markatu behar da {mediaType} eskaerak prozesatu ahal izateko.", "components.Settings.tautulliSettingsDescription": "Aukeran, konfiguratu zure Tautulli zerbitzariaren ezarpenak. Seerr-ek Tautulliren Plex multimedia erreprodukzioen historialaren datuak lortu ditu.", "components.Settings.plexsettingsDescription": "Konfiguratu zure Plex zerbitzariaren ezarpenak. Seerr-ek zure Plexeko liburutegiak eskaneatzen ditu edukiaren erabilgarritasuna zehazteko.", - "components.Settings.webAppUrlTip": "Aukeran erabiltzaileak zure zerbitzariko web aplikaziora zuzendu, web-aplikazio \"ostatuaren\" ordez", + "components.Settings.webAppUrlTip": "Aukeran erabiltzaileak zure zerbitzariko web aplikaziora zuzendu,https://app.plex.tv/desktop ordez", "components.UserList.deleteconfirm": "Ziur al zaude erabiltzaile hau ezabatu nahi duzula? Bere eskaera guztiak betirako ezabatuko dira.", "components.Login.emailtooltip": "Helbidea ez da zertan zure {mediaServerName} instantziari lotu behar.", "components.ManageSlideOver.manageModalRemoveMediaWarning": "* Honek {mediaType} hau betirako ezabatuko du {arr}(e)tik, fitxategi guztiak barne.", @@ -454,7 +454,7 @@ "components.PermissionEdit.viewrequestsDescription": "Baimendu beste erabiltzaile batzuek bidalitako bitarteko-eskaerak ikustea.", "components.RequestButton.approve4krequests": "Onartu {requestCount, plural, one {4K Request} other {{requestCount} 4K Requests}}", "components.RequestButton.decline4krequests": "Ukatu {requestCount, plural, one {4K Request} other {{requestCount} 4K Requests}}", - "components.RequestModal.QuotaDisplay.allowedRequests": " {limit} {type} eskaera egin ditzakezu {days} egunero.", + "components.RequestModal.QuotaDisplay.allowedRequests": " {limit} {type} eskaera egin ditzakezu {type}{days, plural, =0 {} one { egunero } other {{days} egunero}}.", "components.RequestModal.SearchByNameModal.nomatches": "Ezin izan da telesail honetarako bat-etortze bat aurkitu.", "components.ResetPassword.validationpasswordminchars": "Pasahitza laburregia da; gutxienez 8 karaktere izan beharko lituzke", "components.Settings.Notifications.botUsernameTip": "Baimendu erabiltzaileei zure bot-ekin txat bat hasi eta jakinarazpenak konfiguratzen", @@ -468,7 +468,7 @@ "components.UserProfile.UserSettings.unauthorizedDescription": "Ez duzu baimenik erabiltzaile honen ezarpenak aldatzeko.", "components.UserProfile.UserSettings.UserPasswordChange.validationNewPasswordLength": "Pasahitza laburregia da; gutxienez 8 karaktere izan beharko lituzke", "components.Settings.SettingsUsers.localLoginTip": "Baimendu erabiltzaileei haien helbide elektronikoa eta pasahitza erabiliz saioa hasten", - "components.RequestModal.QuotaDisplay.allowedRequestsUser": "Erabiltzaile honek {limit} {type} eskaera egin ditzake {days} egunero.", + "components.RequestModal.QuotaDisplay.allowedRequestsUser": "Erabiltzaile honek {limit} {type} eskaera egin ditzake {type}{days, plural, =0 {} one { egunero } other {{days} egunero}}.", "components.RequestModal.QuotaDisplay.quotaLinkUser": "Erabiltzailearen eskaera-mugen laburpena ikus dezakezu profil orrian.", "components.RequestModal.requestmovies4k": "Eskatu {count} {count, plural, one {film} other {film}} 4K-n", "components.RequestModal.QuotaDisplay.requestsremaining": "{remaining, plural, =0 {0} other {#}} {type} {remaining, plural, one {eskaera} other {eskaera}} geratzen dira", @@ -519,8 +519,6 @@ "components.UserList.importedfromplex": "{userCount} Plex {userCount, plural, one {erabiltzaile} other {erabiltzaile}} ondo inportatu dira!", "components.TvDetails.seasons": "{seasonCount, plural, one {Denboraldi #} other {# denboraldi}}", "components.UserList.localLoginDisabled": "Gaitu tokiko saioa ezarpena desgaituta dago.", - "components.UserProfile.UserSettings.UserGeneralSettings.discordIdTip": "Zure Discord kontuarekin bat datorren multi-digitu ID zenbakia", - "components.UserProfile.UserSettings.UserGeneralSettings.validationDiscordId": "Baliozko Discord erabiltzaile ID bat eman behar duzu", "components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "Zure kontuarekin bat datorren multi-digitu ID zenbakia", "components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverUserKey": "Baliozko erabiltzaile edo talde-gakoa eman behar duzu", "components.UserProfile.UserSettings.UserNotificationSettings.validationPgpPublicKey": "Baliozko PGP gako publiko bat eman behar duzu", @@ -624,7 +622,7 @@ "components.UserList.userfail": "Zerbait gaizki joan da erabiltzailearen baimenak gordetzerakoan.", "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmoviestip": "Automatikoki eskatu zure Plex jarraipen zerrendan dauden filmak", "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessTokenTip": "Sortu token bat zure kontuaren ezarpenetatik", - "components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "Baliozko erabiltzaile ID bat eman behar duzu", + "components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "ID bakoitza baliozko Discord erabiltzaile ID bat izan behar da", "components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverApplicationToken": "Baliozko aplikazio token bat eman behar duzu", "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramChatId": "Baliozko txat ID bat eman behar duzu", "components.UserProfile.UserSettings.UserPasswordChange.toastSettingsFailure": "Zerbait gaizki joan da pasahitza gordetzean.", @@ -1201,7 +1199,6 @@ "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegionTip": "Iragazi edukia eskualdeko eskuragarritasunarengatik", "components.UserProfile.UserSettings.UserGeneralSettings.originallanguageTip": "Iragazi edukia iturrizko hizkuntzarengatik", "components.UserProfile.ProfileHeader.userid": "Erabiltzailearen ID: {userid}", - "components.UserProfile.UserSettings.UserGeneralSettings.discordId": "Discord erabiltzailearen ID", "components.UserProfile.UserSettings.UserGeneralSettings.enableOverride": "Baliogabetu muga globala", "components.UserProfile.UserSettings.UserGeneralSettings.movierequestlimit": "Film eskaera muga", "components.UserProfile.ProfileHeader.joindate": "{joindate} batu zen", @@ -1240,7 +1237,7 @@ "components.UserProfile.UserSettings.UserNotificationSettings.sendSilentlyDescription": "Bidali jakinarazpenak soinurik gabe", "components.UserProfile.UserSettings.UserNotificationSettings.pushoverUserKey": "Erabiltzaile edo talde gakoa", "components.UserProfile.UserSettings.UserNotificationSettings.pgpPublicKey": "PGP gako publikoa", - "components.UserProfile.UserSettings.UserNotificationSettings.discordId": "Erabiltzailearen IDa", + "components.UserProfile.UserSettings.UserNotificationSettings.discordId": "Erabiltzailearen IDak", "components.UserProfile.UserSettings.UserNotificationSettings.notificationsettings": "Jakinarazpen ezarpenak", "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessToken": "Sarbide tokena", "components.UserProfile.UserSettings.UserNotificationSettings.sendSilently": "Bidali isilean", @@ -1628,5 +1625,75 @@ "components.Layout.UserWarnings.profileIncomplete": "Profila osatu gabe dago", "components.Login.emailAlreadyTaken": "E-posta hau dagoeneko erabiltzen ari da.", "components.Login.jellyfinLocalLoginHint": "Zure profilean helbide elektronikorik ezarri ez baduzu, erabili zure {mediaServerName} erabiltzaile-izena horren ordez.", - "components.Login.saveFailed": "Zerbait gaizki joan da gordetzean." + "components.Login.saveFailed": "Zerbait gaizki joan da gordetzean.", + "components.RequestCard.failedmodify": "Zerbait gaizki joan da eskaera aldatzean.", + "components.RequestList.RequestItem.failedmodify": "Zerbait gaizki joan da eskaera aldatzean.", + "components.Login.urlBaseHelp": "Jellyfinen (Arbela > Sareak) oinarrizko URL bat ezartzen baduzu, idatzi hemen (adibidez, /jellyfin). Bestela, utzi hutsik.", + "components.Settings.Notifications.usePublicLogo": "Erabili Seerr-en logo publikoa instantziaren logoaren ordez", + "components.Settings.SettingsMain.blocklistLanguage": "Blokeo-zerrendaren hizkuntza", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdAdd": "Gehitu erabiltzaile IDa", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdPlaceholder": "Discord erabiltzaile IDa", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdRemove": "Kendu", + "components.UserList.toggleSortDirectionAria": "Aldatu ordenatze norabidea", + "components.Settings.RadarrModal.syncEnabledHelp": "", + "components.Settings.SettingsMain.blocklistLanguageTip": "", + "components.Settings.SettingsMain.blocklistRegion": "Blokeo-zerrendaren eskualdea", + "components.Settings.SettingsMain.blocklistRegionTip": "", + "components.Settings.SettingsUsers.disabledMediaServerLoginWarning": "", + "components.Settings.SonarrModal.apiKeyHelp": "Aurkitu Sonarr > Orokorra > Segurtasuna > API gakoa atalean", + "components.Settings.SonarrModal.baseUrlHelp": "", + "components.Settings.SonarrModal.enableSearchHelp": "", + "components.Settings.SonarrModal.externalUrlHelp": "", + "components.Settings.SonarrModal.monitorNewItemsHelp": "", + "components.Settings.SonarrModal.server4kHelp": "", + "components.Settings.SonarrModal.syncEnabledHelp": "", + "components.UserList.importedPlexUsersNoPassword": "", + "components.UserList.importedUsersNoPassword": "", + "components.UserList.sortBy": "", + "components.UserList.sortByJoined": "", + "components.UserList.sortByRequests": "", + "components.UserList.sortByRole": "", + "components.UserList.sortByType": "Ordenatu kontu motaren arabera", + "components.UserList.sortByUser": "Ordenatu erabiltzaile-izenaren arabera", + "components.UserList.toggleSortDirection": "", + "components.UserProfile.UserSettings.UserPasswordChange.localPasswordDescription": "", + "i18n.notificationLocale": "Jakinarazpenen hizkuntza", + "i18n.partiallyblocklisted": "", + "components.UserList.descending": "", + "i18n.error": "Zerbait gaizki joan da. Saiatu berriro.", + "components.UserList.ascending": "", + "components.Settings.Notifications.usePublicLogoTip": "Zure Seerr instantzia publikoki eskuragarri ez badago, gaitu aukera hau zure saretik kanpoko e-posta bezeroek irudia bistaratu ahal izan dezaten. Irudia GitHub biltegi publikotik aterako da.", + "components.Settings.RadarrModal.apiKeyHelp": "Radarr-en aurkitu: Ezarpenak > Orokorra > Segurtasuna > API gakoa", + "components.Settings.RadarrModal.externalUrlHelp": "Saretik kanpo ostalari-izena eskuraezina denean, multimedia orrialdeetako klik egin daitezkeen estekentzako.", + "components.Settings.RadarrModal.server4kHelp": "4K instantzia bereizi bat baduzu bakarrik. Utzi markatu gabe zerbitzari bakar baterako.", + "components.Settings.RadarrModal.baseUrlHelp": "Radarr-en oinarrizko URL bat ezartzen baduzu (Ezarpenak > Orokorra > Ostalaria), idatzi hemen (adib. /radarr). Bestela, utzi hutsik.", + "components.Settings.Notifications.useUserLocale": "Erabili jakinarazpen edukiontziaren kokapena", + "components.Settings.RadarrModal.enableSearchHelp": "Automatikoki gaitu bilaketa bat Radarr-en eskaera bat onartzen denean.", + "components.Common.QuickConnectModal.error": "Errorea", + "components.Common.QuickConnectModal.expired": "Kodea iraungita", + "components.Common.QuickConnectModal.expiredMessage": "Quick Connect kode hau iraungita dago. Mesedez, saiatu berriro.", + "components.Common.QuickConnectModal.tryAgain": "Saiatu berriro", + "components.Common.QuickConnectModal.waitingForAuth": "Baimenaren zain...", + "components.Login.JellyfinQuickConnectModal.cancel": "Utzi", + "components.Login.JellyfinQuickConnectModal.instructions": "Sartu kode hau zure {mediaServerName} aplikazioan", + "components.Login.JellyfinQuickConnectModal.subtitle": "Hasi saioa Quick Connect-ekin", + "components.Login.JellyfinQuickConnectModal.title": "Quick Connect", + "components.Login.quickconnect": "Quick Connect", + "components.Login.quickconnecterror": "Quick Connect-ek huts egin du. Mesedez, saiatu berriro.", + "components.RequestModal.AdvancedRequester.ignoreQuotaDescription": "Eskaera honek ez du erabiltzailearen kuota-muga handituko. Kontuz ibili.", + "components.RequestModal.AdvancedRequester.ignoreQuotaTitle": "Erabiltzailearen kuota saihestu", + "components.ManageSlideOver.clearmediadataerror": "Zerbait gaizki joan da multimedia datuak garbitzean.", + "components.ManageSlideOver.removemediaerror": "Zerbait gaizki joan da multimedia kentzean.", + "components.RequestList.RequestItem.removemediaerror": "Zerbait gaizki joan da multimedia kentzean.", + "components.Settings.Notifications.NotificationsGotify.embedPoster": "Kartel txertatua", + "components.Settings.Notifications.validationWebhookThreadId": "Baliozko Discord Thread ID bat eman behar duzu", + "components.Settings.Notifications.webhookThreadId": "Thread ID", + "components.UserList.importfromplexnochanges": "Plex inportazioa bukatuta. Ez da erabiltzailerik sortu edo eguneratu.", + "components.UserList.importfromplexsynced": "Plex erabiltzaileak ondo sinkronizatu dira. Dauden erabiltzaileak freskatu dira.", + "components.UserProfile.UserSettings.LinkJellyfinModal.quickConnect": "Erabili Quick Connect", + "components.UserProfile.UserSettings.LinkJellyfinQuickConnectModal.subtitle": "Quick Connect", + "components.UserProfile.UserSettings.LinkJellyfinQuickConnectModal.usePassword": "Erabili pasahitza horren ordez", + "hooks.useQuickConnect.errorMessage": "Quick Connect hasieratzeak huts egin du. Mesedez, saiatu berriro.", + "i18n.sync": "Sinkronizazioa", + "i18n.syncing": "Sinkronizatzen…" } diff --git a/src/i18n/locale/fi.json b/src/i18n/locale/fi.json index b7e80d43c5..7516c7f7e1 100644 --- a/src/i18n/locale/fi.json +++ b/src/i18n/locale/fi.json @@ -718,7 +718,7 @@ "components.StatusBadge.playonplex": "Toista palvelussa {mediaServerName}", "components.StatusBadge.seasonepisodenumber": "K{seasonNumber}J{episodeNumber}", "components.StatusBadge.seasonnumber": "K{seasonNumber}", - "components.StatusBadge.status": "Tila", + "components.StatusBadge.status": "{status}", "components.TvDetails.Season.noepisodes": "Jaksolistausta ei saatavilla.", "components.TvDetails.Season.somethingwentwrong": "Jotain meni vikaan haettaessa kausien tietoja.", "components.TvDetails.TvCast.fullseriescast": "Kaikki sarjan näyttelijät", @@ -759,7 +759,6 @@ "components.UserProfile.UserSettings.UserGeneralSettings.accounttype": "Tilin tyyppi", "components.UserProfile.UserSettings.UserGeneralSettings.admin": "Ylläpitäjä", "components.UserProfile.UserSettings.UserGeneralSettings.applanguage": "Näyttökieli", - "components.UserProfile.UserSettings.UserGeneralSettings.discordId": "Discord käyttäjä-ID", "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegion": "Tutustu-osion alue", "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegionTip": "Suodata sisältöä alueellisen saatavuuden perusteella", "components.UserProfile.UserSettings.UserGeneralSettings.displayName": "Näyttönimi", @@ -788,7 +787,6 @@ "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmailEmpty": "Toinen käyttäjä on jo valinnut tämän käyttäjänimen. Sinun täytyy asettaa sähköpostiosoite", "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsSuccess": "Asetukset tallennettu!", "components.UserProfile.UserSettings.UserGeneralSettings.user": "Käyttäjä", - "components.UserProfile.UserSettings.UserGeneralSettings.validationDiscordId": "Anna paikkansapitävä Discord käyttäjä-ID", "components.UserProfile.UserSettings.UserGeneralSettings.validationemailformat": "Anna paikkansapitävä sähköpostiosoite", "components.UserProfile.UserSettings.UserGeneralSettings.validationemailrequired": "Sähköposti vaaditaan", "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.managedevices": "Hallinnoi laitteita", @@ -946,5 +944,729 @@ "i18n.addToBlocklist": "Lisää estolistalle", "i18n.blocklist": "Estolista", "i18n.blocklistError": "Jotain meni vikaan. Yritä uudelleen.", - "i18n.removeFromBlocklistSuccess": "{title} poistettu estolistalta." + "i18n.removeFromBlocklistSuccess": "{title} poistettu estolistalta.", + "components.Settings.RadarrModal.servername": "", + "components.Layout.UserWarnings.profileIncomplete": "", + "components.Layout.VersionStatus.commitsbehind": "", + "components.Login.emailAlreadyTaken": "", + "components.Login.jellyfinLocalLoginHint": "", + "components.Login.saveFailed": "", + "components.RequestList.RequestItem.failedmodify": "", + "components.RequestModal.AdvancedRequester.folder": "", + "components.RequestModal.QuotaDisplay.requestsremaining": "", + "components.RequestModal.QuotaDisplay.requiredquota": "", + "components.RequestModal.requestedited": "", + "components.RequestModal.requestmovies": "", + "components.RequestModal.requestmovies4k": "", + "components.RequestModal.requestseasons": "", + "components.RequestModal.requestseasons4k": "", + "components.ResetPassword.requestresetlinksuccessmessage": "", + "components.ResetPassword.resetpassword": "", + "components.ResetPassword.resetpasswordsuccessmessage": "", + "components.ResetPassword.validationemailrequired": "", + "components.ResetPassword.validationpasswordmatch": "", + "components.ResetPassword.validationpasswordminchars": "", + "components.ResetPassword.validationpasswordrequired": "", + "components.Selector.CertificationSelector.noOptions": "", + "components.Selector.CertificationSelector.selectCertification": "", + "components.Selector.pilot": "", + "components.Settings.Notifications.NotificationsGotify.agentenabled": "", + "components.Settings.Notifications.NotificationsGotify.gotifysettingsfailed": "", + "components.Settings.Notifications.NotificationsGotify.gotifysettingssaved": "", + "components.Settings.Notifications.NotificationsGotify.toastGotifyTestFailed": "", + "components.Settings.Notifications.NotificationsGotify.toastGotifyTestSending": "", + "components.Settings.Notifications.NotificationsGotify.toastGotifyTestSuccess": "", + "components.Settings.Notifications.NotificationsGotify.token": "", + "components.Settings.Notifications.NotificationsGotify.url": "", + "components.Settings.Notifications.NotificationsNtfy.embedPoster": "", + "components.Settings.Notifications.NotificationsNtfy.ntfysettingssaved": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestFailed": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestSending": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestSuccess": "", + "components.Settings.Notifications.NotificationsNtfy.token": "", + "components.Settings.Notifications.NotificationsNtfy.tokenAuth": "", + "components.Settings.Notifications.NotificationsNtfy.url": "", + "components.Settings.Notifications.NotificationsNtfy.validationPriorityRequired": "", + "components.Settings.Notifications.NotificationsPushbullet.accessToken": "", + "components.Settings.Notifications.NotificationsPushbullet.accessTokenTip": "", + "components.Settings.Notifications.NotificationsPushbullet.agentEnabled": "", + "components.Settings.Notifications.NotificationsPushbullet.channelTag": "", + "components.Settings.Notifications.NotificationsPushbullet.pushbulletSettingsFailed": "", + "components.Settings.Notifications.NotificationsPushbullet.pushbulletSettingsSaved": "", + "components.Settings.Notifications.NotificationsPushbullet.validationAccessTokenRequired": "", + "components.Settings.Notifications.NotificationsPushbullet.validationTypes": "", + "components.Settings.Notifications.NotificationsPushover.deviceDefault": "", + "components.Settings.Notifications.NotificationsPushover.embedPoster": "", + "components.Settings.Notifications.NotificationsPushover.pushoversettingsfailed": "", + "components.Settings.Notifications.NotificationsPushover.pushoversettingssaved": "", + "components.Settings.Notifications.NotificationsPushover.toastPushoverTestFailed": "", + "components.Settings.Notifications.NotificationsPushover.toastPushoverTestSending": "", + "components.Settings.Notifications.NotificationsPushover.toastPushoverTestSuccess": "", + "components.Settings.Notifications.NotificationsPushover.userToken": "", + "components.Settings.Notifications.NotificationsPushover.userTokenTip": "", + "components.Settings.Notifications.NotificationsPushover.validationAccessTokenRequired": "", + "components.Settings.Notifications.NotificationsPushover.validationTypes": "", + "components.Settings.Notifications.NotificationsPushover.validationUserTokenRequired": "", + "components.Settings.Notifications.NotificationsSlack.agentenabled": "", + "components.Settings.Notifications.NotificationsSlack.embedPoster": "", + "components.Settings.Notifications.NotificationsSlack.validationWebhookUrl": "", + "components.Settings.Notifications.NotificationsSlack.webhookUrl": "", + "components.Settings.Notifications.NotificationsSlack.webhookUrlTip": "", + "components.Settings.Notifications.NotificationsWebhook.authheader": "", + "components.Settings.Notifications.NotificationsWebhook.customHeaders": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersAdd": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersAuthConflict": "", + "components.Settings.Notifications.NotificationsWebPush.agentenabled": "", + "components.Settings.Notifications.NotificationsWebPush.embedPoster": "", + "components.Settings.Notifications.NotificationsWebPush.toastWebPushTestFailed": "", + "components.Settings.Notifications.NotificationsWebPush.toastWebPushTestSending": "", + "components.Settings.Notifications.NotificationsWebPush.toastWebPushTestSuccess": "", + "components.Settings.Notifications.NotificationsWebPush.webpushsettingssaved": "", + "components.Settings.Notifications.allowselfsigned": "", + "components.Settings.Notifications.authPass": "", + "components.Settings.Notifications.authUser": "", + "components.Settings.Notifications.botAPI": "", + "components.Settings.Notifications.botApiTip": "", + "components.Settings.Notifications.botAvatarUrl": "", + "components.Settings.Notifications.botUsername": "", + "components.Settings.Notifications.botUsernameTip": "", + "components.Settings.Notifications.chatIdTip": "", + "components.Settings.Notifications.discordsettingsfailed": "", + "components.Settings.Notifications.emailsender": "", + "components.Settings.Notifications.emailsettingsfailed": "", + "components.Settings.Notifications.emailsettingssaved": "", + "components.Settings.Notifications.embedPoster": "", + "components.Settings.Notifications.enableMentions": "", + "components.Settings.Notifications.encryption": "", + "components.Settings.Notifications.encryptionDefault": "", + "components.Settings.Notifications.encryptionImplicitTls": "", + "components.Settings.Notifications.encryptionNone": "", + "components.Settings.Notifications.encryptionOpportunisticTls": "", + "components.Settings.Notifications.encryptionTip": "", + "components.Settings.Notifications.messageThreadId": "", + "components.Settings.Notifications.messageThreadIdTip": "", + "components.Settings.Notifications.pgpPassword": "", + "components.Settings.Notifications.pgpPasswordTip": "", + "components.Settings.Notifications.pgpPrivateKeyTip": "", + "components.Settings.Notifications.sendSilently": "", + "components.Settings.Notifications.sendSilentlyTip": "", + "components.Settings.Notifications.smtpPort": "", + "components.Settings.Notifications.toastDiscordTestFailed": "", + "components.Settings.Notifications.toastDiscordTestSending": "", + "components.Settings.Notifications.toastEmailTestSending": "", + "components.Settings.Notifications.toastTelegramTestFailed": "", + "components.Settings.Notifications.toastTelegramTestSending": "", + "components.Settings.Notifications.toastTelegramTestSuccess": "", + "components.Settings.OverrideRuleModal.create": "", + "components.Settings.RadarrModal.externalUrlHelp": "", + "components.Settings.SettingsJobsCache.sonarr-scan": "", + "components.Settings.SettingsMain.blocklistedTagsTip": "", + "components.Settings.SettingsNetwork.csrfProtection": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.deletesubscription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.device": "", + "components.UserProfile.UserSettings.UserPasswordChange.toastSettingsFailure": "", + "components.UserProfile.UserSettings.UserPasswordChange.toastSettingsFailureVerifyCurrent": "", + "components.UserProfile.UserSettings.UserPasswordChange.toastSettingsSuccess": "", + "components.UserProfile.UserSettings.UserPasswordChange.validationConfirmPassword": "", + "components.UserProfile.UserSettings.UserPermissions.toastSettingsSuccess": "", + "components.UserProfile.UserSettings.menuLinkedAccounts": "", + "components.UserProfile.UserSettings.menuNotifications": "", + "components.UserProfile.UserSettings.menuPermissions": "", + "i18n.error": "", + "i18n.notrequested": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmovies": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseries": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseriestip": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.errorUnknown": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.linkedAccounts": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.linkedAccountsHint": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.noLinkedAccounts": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.noPermissionDescription": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorExists": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.browser": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablewebpush": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablingwebpusherror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablingwebpusherror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.engine": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.nodevicestoshow": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.operatingsystem": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleted": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.unknown": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeendisabled": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeenenabled": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordId": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdAdd": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdPlaceholder": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdRemove": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "", + "components.Login.urlBaseHelp": "", + "components.RequestButton.decline4krequests": "", + "components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestSending": "", + "components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestSuccess": "", + "components.Settings.Notifications.NotificationsPushover.agentenabled": "", + "components.Settings.Notifications.toastDiscordTestSuccess": "", + "components.Settings.Notifications.toastEmailTestFailed": "", + "components.Settings.SonarrModal.validationBaseUrlTrailingSlash": "", + "components.Settings.SonarrModal.validationHostnameRequired": "", + "components.Settings.SonarrModal.validationNameRequired": "", + "components.Settings.SonarrModal.validationPortRequired": "", + "components.Settings.SonarrModal.validationProfileRequired": "", + "components.TitleCard.watchlistSuccess": "", + "components.UserList.admin": "", + "components.UserList.creating": "", + "components.UserList.toggleSortDirectionAria": "", + "components.Settings.SonarrModal.seriesType": "", + "components.Settings.SettingsJobsCache.cachekeys": "", + "components.Settings.notifications": "", + "components.Settings.SettingsJobsCache.dnscacheage": "", + "components.RequestButton.declinerequests": "", + "components.Settings.Notifications.NotificationsNtfy.agentenabled": "", + "components.Settings.Notifications.NotificationsWebPush.webpushsettingsfailed": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersIncomplete": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersKey": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersRemove": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersTip": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersValue": "", + "components.Settings.Notifications.NotificationsWebhook.customJson": "", + "components.Settings.Notifications.NotificationsWebhook.resetPayload": "", + "components.Settings.Notifications.NotificationsWebhook.resetPayloadSuccess": "", + "components.Settings.Notifications.NotificationsWebhook.supportVariables": "", + "components.Settings.Notifications.NotificationsWebhook.supportVariablesTip": "", + "components.Settings.Notifications.NotificationsWebhook.validationWebhookUrl": "", + "components.Settings.Notifications.NotificationsWebhook.webhookUrl": "", + "components.Settings.Notifications.NotificationsWebhook.webhookUrlTip": "", + "components.Settings.Notifications.NotificationsWebhook.webhooksettingsfailed": "", + "components.Settings.Notifications.NotificationsWebhook.webhooksettingssaved": "", + "components.Settings.OverrideRuleModal.conditions": "", + "components.Settings.OverrideRuleModal.selectService": "", + "components.Settings.RadarrModal.server4k": "", + "components.Settings.RadarrModal.tags": "", + "components.Settings.SettingsNetwork.proxyBypassLocalAddresses": "", + "components.Settings.clickTest": "", + "components.UserList.owner": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorUnauthorized": "", + "components.RequestCard.failedmodify": "", + "components.Selector.CertificationSelector.errorLoading": "", + "components.Settings.Notifications.NotificationsWebPush.httpsRequirement": "", + "components.Settings.Notifications.senderName": "", + "components.Settings.toastPlexRefreshSuccess": "", + "components.Settings.urlBase": "", + "components.Settings.Notifications.usePublicLogo": "", + "components.Settings.Notifications.usePublicLogoTip": "", + "components.Settings.Notifications.useUserLocale": "", + "components.Settings.Notifications.validationChatIdRequired": "", + "components.Settings.Notifications.validationEmail": "", + "components.Settings.Notifications.validationMessageThreadId": "", + "components.Settings.Notifications.validationSmtpHostRequired": "", + "components.Settings.Notifications.validationSmtpPortRequired": "", + "components.Settings.Notifications.validationTypes": "", + "components.Settings.Notifications.validationWebhookRoleId": "", + "components.Settings.Notifications.webhookRoleId": "", + "components.Settings.Notifications.webhookRoleIdTip": "", + "components.Settings.Notifications.webhookUrlTip": "", + "components.Settings.OverrideRuleModal.conditionsDescription": "", + "components.Settings.OverrideRuleModal.createrule": "", + "components.Settings.OverrideRuleModal.editrule": "", + "components.Settings.OverrideRuleModal.notagoptions": "", + "components.Settings.OverrideRuleModal.ruleCreated": "", + "components.Settings.OverrideRuleModal.ruleUpdated": "", + "components.Settings.OverrideRuleModal.selecttags": "", + "components.Settings.OverrideRuleModal.service": "", + "components.Settings.OverrideRuleModal.serviceDescription": "", + "components.Settings.OverrideRuleModal.settings": "", + "components.Settings.OverrideRuleModal.settingsDescription": "", + "components.Settings.OverrideRuleModal.tags": "", + "components.Settings.OverrideRuleTile.conditions": "", + "components.Settings.RadarrModal.apiKeyHelp": "", + "components.Settings.RadarrModal.baseUrlHelp": "", + "components.Settings.RadarrModal.create4kradarr": "", + "components.Settings.RadarrModal.createradarr": "", + "components.Settings.RadarrModal.default4kserver": "", + "components.Settings.RadarrModal.defaultserver": "", + "components.Settings.RadarrModal.edit4kradarr": "", + "components.Settings.RadarrModal.editradarr": "", + "components.Settings.RadarrModal.enableSearch": "", + "components.Settings.RadarrModal.enableSearchHelp": "", + "components.Settings.RadarrModal.server4kHelp": "", + "components.Settings.RadarrModal.syncEnabled": "", + "components.Settings.RadarrModal.syncEnabledHelp": "", + "components.Settings.RadarrModal.tagRequests": "", + "components.Settings.RadarrModal.tagRequestsInfo": "", + "components.Settings.RadarrModal.testFirstQualityProfiles": "", + "components.Settings.RadarrModal.testFirstRootFolders": "", + "components.Settings.RadarrModal.testFirstTags": "", + "components.Settings.RadarrModal.toastRadarrTestFailure": "", + "components.Settings.RadarrModal.toastRadarrTestSuccess": "", + "components.Settings.RadarrModal.validationApiKeyRequired": "", + "components.Settings.RadarrModal.validationHostnameRequired": "", + "components.Settings.RadarrModal.validationMinimumAvailabilityRequired": "", + "components.Settings.RadarrModal.validationNameRequired": "", + "components.Settings.RadarrModal.validationPortRequired": "", + "components.Settings.RadarrModal.validationProfileRequired": "", + "components.Settings.RadarrModal.validationRootFolderRequired": "", + "components.Settings.SettingsAbout.runningDevelop": "", + "components.Settings.SettingsJobsCache.cacheDescription": "", + "components.Settings.SettingsJobsCache.cacheksize": "", + "components.Settings.SettingsJobsCache.cachevsize": "", + "components.Settings.SettingsJobsCache.dnsCacheDescription": "", + "components.Settings.SettingsJobsCache.dnsCacheGlobalStats": "", + "components.Settings.SettingsJobsCache.dnsCacheGlobalStatsDescription": "", + "components.Settings.SettingsJobsCache.dnsNoCacheEntries": "", + "components.Settings.SettingsJobsCache.dnscacheactiveaddress": "", + "components.Settings.SettingsJobsCache.dnscacheflushed": "", + "components.Settings.SettingsJobsCache.dnscachename": "", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorDays": "", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorHours": "", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorMinutes": "", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorSeconds": "", + "components.Settings.SettingsJobsCache.imagecacheDescription": "", + "components.Settings.SettingsJobsCache.imagecachesize": "", + "components.Settings.SettingsJobsCache.ipv4Fallbacks": "", + "components.Settings.SettingsJobsCache.jobScheduleEditFailed": "", + "components.Settings.SettingsJobsCache.jobcancelled": "", + "components.Settings.SettingsJobsCache.jobname": "", + "components.Settings.SettingsJobsCache.jobsDescription": "", + "components.Settings.SettingsJobsCache.plex-full-scan": "", + "components.Settings.SettingsJobsCache.plex-recently-added-scan": "", + "components.Settings.SettingsJobsCache.plex-refresh-token": "", + "components.Settings.SettingsJobsCache.plex-watchlist-sync": "", + "components.Settings.SettingsJobsCache.process": "", + "components.Settings.SettingsJobsCache.radarr-scan": "", + "components.Settings.SettingsJobsCache.size": "", + "components.Settings.SettingsJobsCache.unknownJob": "", + "components.Settings.SettingsJobsCache.usersavatars": "", + "components.Settings.SettingsLogs.copiedLogMessage": "", + "components.Settings.SettingsLogs.copyToClipboard": "", + "components.Settings.SettingsLogs.extraData": "", + "components.Settings.SettingsLogs.label": "", + "components.Settings.SettingsLogs.logDetails": "", + "components.Settings.SettingsLogs.logsDescription": "", + "components.Settings.SettingsMain.blocklistLanguage": "", + "components.Settings.SettingsMain.blocklistLanguageTip": "", + "components.Settings.SettingsMain.blocklistRegion": "", + "components.Settings.SettingsMain.blocklistRegionTip": "", + "components.Settings.SettingsMain.blocklistedTagsLimit": "", + "components.Settings.SettingsMain.blocklistedTagsLimitTip": "", + "components.Settings.SettingsMain.cacheImagesTip": "", + "components.Settings.SettingsMain.toastSettingsFailure": "", + "components.Settings.SettingsMain.validationApplicationUrl": "", + "components.Settings.SettingsMain.validationApplicationUrlTrailingSlash": "", + "components.Settings.SettingsMain.validationUrl": "", + "components.Settings.SettingsMain.validationUrlTrailingSlash": "", + "components.Settings.SettingsMain.youtubeUrl": "", + "components.Settings.SettingsMain.youtubeUrlTip": "", + "components.Settings.SettingsNetwork.apiRequestTimeout": "", + "components.Settings.SettingsNetwork.apiRequestTimeoutTip": "", + "components.Settings.SettingsNetwork.csrfProtectionHoverTip": "", + "components.Settings.SettingsNetwork.csrfProtectionTip": "", + "components.Settings.SettingsNetwork.dnsCacheForceMaxTtl": "", + "components.Settings.SettingsNetwork.dnsCacheForceMinTtl": "", + "components.Settings.SettingsNetwork.dnsCacheHoverTip": "", + "components.Settings.SettingsNetwork.dnsCacheTip": "", + "components.Settings.SettingsNetwork.docs": "", + "components.Settings.SettingsNetwork.forceIpv4First": "", + "components.Settings.SettingsNetwork.forceIpv4FirstTip": "", + "components.Settings.SettingsNetwork.networkDisclaimer": "", + "components.Settings.SettingsNetwork.networksettingsDescription": "", + "components.Settings.SettingsNetwork.proxyBypassFilterTip": "", + "components.Settings.SettingsNetwork.proxyEnabledTip": "", + "components.Settings.SettingsNetwork.proxyHostname": "", + "components.Settings.SettingsNetwork.proxyPassword": "", + "components.Settings.SettingsNetwork.proxyPort": "", + "components.Settings.SettingsNetwork.proxySsl": "", + "components.Settings.SettingsNetwork.proxyUser": "", + "components.Settings.SettingsNetwork.toastSettingsFailure": "", + "components.Settings.SettingsNetwork.validationApiRequestTimeout": "", + "components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "", + "components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "", + "components.Settings.SettingsNetwork.validationProxyPort": "", + "components.Settings.SettingsUsers.atLeastOneAuth": "", + "components.Settings.SettingsUsers.defaultPermissionsTip": "", + "components.Settings.SettingsUsers.disabledMediaServerLoginWarning": "", + "components.Settings.SettingsUsers.loginMethodsTip": "", + "components.Settings.SettingsUsers.mediaServerLogin": "", + "components.Settings.SettingsUsers.mediaServerLoginTip": "", + "components.Settings.SettingsUsers.newPlexLogin": "", + "components.Settings.SettingsUsers.newPlexLoginTip": "", + "components.Settings.SettingsUsers.userSettingsDescription": "", + "components.Settings.SonarrModal.animelanguageprofile": "", + "components.Settings.SonarrModal.animequalityprofile": "", + "components.Settings.SonarrModal.animerootfolder": "", + "components.Settings.SonarrModal.apiKeyHelp": "", + "components.Settings.SonarrModal.baseUrlHelp": "", + "components.Settings.SonarrModal.create4ksonarr": "", + "components.Settings.SonarrModal.createsonarr": "", + "components.Settings.SonarrModal.default4kserver": "", + "components.Settings.SonarrModal.edit4ksonarr": "", + "components.Settings.SonarrModal.enableSearch": "", + "components.Settings.SonarrModal.enableSearchHelp": "", + "components.Settings.SonarrModal.externalUrl": "", + "components.Settings.SonarrModal.externalUrlHelp": "", + "components.Settings.SonarrModal.hostname": "", + "components.Settings.SonarrModal.languageprofile": "", + "components.Settings.SonarrModal.loadingTags": "", + "components.Settings.SonarrModal.loadinglanguageprofiles": "", + "components.Settings.SonarrModal.loadingrootfolders": "", + "components.Settings.SonarrModal.monitorNewItemsHelp": "", + "components.Settings.SonarrModal.notagoptions": "", + "components.Settings.SonarrModal.qualityprofile": "", + "components.Settings.SonarrModal.seasonfolders": "", + "components.Settings.SonarrModal.server4k": "", + "components.Settings.SonarrModal.server4kHelp": "", + "components.Settings.SonarrModal.servername": "", + "components.Settings.SonarrModal.ssl": "", + "components.Settings.SonarrModal.syncEnabled": "", + "components.Settings.SonarrModal.validationRootFolderRequired": "", + "components.Settings.addradarr": "", + "components.Settings.addsonarr": "", + "components.Settings.advancedTooltip": "", + "components.Settings.blocklistedTagImportInstructions": "", + "components.Settings.blocklistedTagImportTitle": "", + "components.Settings.clearBlocklistedTagsConfirm": "", + "components.Settings.copyBlocklistedTags": "", + "components.Settings.copyBlocklistedTagsTip": "", + "components.Settings.deleteServer": "", + "components.Settings.deleteserverconfirm": "", + "components.Settings.email": "", + "components.Settings.enablessl": "", + "components.Settings.experimentalTooltip": "", + "components.Settings.externalUrl": "", + "components.Settings.failed": "", + "components.Settings.failedToSaveMetadataSettings": "", + "components.Settings.hostname": "", + "components.Settings.importBlocklistedTagsTip": "", + "components.Settings.jellyfinSettings": "", + "components.Settings.jellyfinSettingsDescription": "", + "components.Settings.jellyfinSettingsSuccess": "", + "components.Settings.jellyfinSyncFailedAutomaticGroupedFolders": "", + "components.Settings.jellyfinSyncFailedGenericError": "", + "components.Settings.jellyfinSyncFailedNoLibrariesFound": "", + "components.Settings.jellyfinlibraries": "", + "components.Settings.jellyfinlibrariesDescription": "", + "components.Settings.jellyfinsettings": "", + "components.Settings.librariesRemaining": "", + "components.Settings.manualscan": "", + "components.Settings.manualscanDescription": "", + "components.Settings.manualscanDescriptionJellyfin": "", + "components.Settings.menuJellyfinSettings": "", + "components.Settings.menuPlexSettings": "", + "components.Settings.no": "", + "components.Settings.noDefault4kServer": "", + "components.Settings.noDefaultNon4kServer": "", + "components.Settings.noSpecialCharacters": "", + "components.Settings.notificationAgentSettingsDescription": "", + "components.Settings.notificationsettings": "", + "components.Settings.overrideRules": "", + "components.Settings.overrideRulesDescription": "", + "components.Settings.plex": "", + "components.Settings.plexlibraries": "", + "components.Settings.plexsettings": "", + "components.Settings.port": "", + "components.Settings.radarrsettings": "", + "components.Settings.save": "", + "components.Settings.saving": "", + "components.Settings.scan": "", + "components.Settings.scanbackground": "", + "components.Settings.scanning": "", + "components.Settings.searchKeywords": "", + "components.Settings.serverRemote": "", + "components.Settings.serverSecure": "", + "components.Settings.serverpreset": "", + "components.Settings.serverpresetLoad": "", + "components.Settings.serverpresetManualMessage": "", + "components.Settings.serverpresetRefreshing": "", + "components.Settings.serviceSettingsDescription": "", + "components.Settings.services": "", + "components.Settings.settingUpPlexDescription": "", + "components.Settings.settings": "", + "components.Settings.sonarrsettings": "", + "components.Settings.ssl": "", + "components.Settings.startscan": "", + "components.Settings.starttyping": "", + "components.Settings.syncJellyfin": "", + "components.Settings.tautulliApiKey": "", + "components.Settings.toastPlexRefresh": "", + "components.Settings.toastPlexRefreshFailure": "", + "components.Settings.toastTautulliSettingsFailure": "", + "components.Settings.toastTautulliSettingsSuccess": "", + "components.Settings.validationApiKey": "", + "components.Settings.validationHostnameRequired": "", + "components.Settings.validationPortRequired": "", + "components.Settings.webAppUrl": "", + "components.Settings.webAppUrlTip": "", + "components.Settings.webhook": "", + "components.Settings.webpush": "", + "components.Setup.back": "", + "components.Setup.configemby": "", + "components.Setup.configjellyfin": "", + "components.Setup.configplex": "", + "components.Setup.configuremediaserver": "", + "components.Setup.configureservices": "", + "components.Setup.setup": "", + "components.Setup.signinMessage": "", + "components.Setup.signinWithEmby": "", + "components.Setup.welcome": "", + "components.StatusBadge.status4k": "", + "components.StatusChecker.appUpdated": "", + "components.StatusChecker.appUpdatedDescription": "", + "components.StatusChecker.restartRequired": "", + "components.StatusChecker.restartRequiredDescription": "", + "components.TitleCard.addToWatchList": "", + "components.TitleCard.cleardata": "", + "components.TitleCard.mediaerror": "", + "components.TitleCard.tvdbid": "", + "components.TitleCard.watchlistCancel": "", + "components.TitleCard.watchlistDeleted": "", + "components.TitleCard.watchlistError": "", + "components.TvDetails.anime": "", + "components.TvDetails.play4k": "", + "components.TvDetails.status4k": "", + "components.UserList.accounttype": "", + "components.UserList.ascending": "", + "components.UserList.autogeneratepassword": "", + "components.UserList.autogeneratepasswordTip": "", + "components.UserList.bulkedit": "", + "components.UserList.create": "", + "components.UserList.created": "", + "components.UserList.createlocaluser": "", + "components.UserList.deleteconfirm": "", + "components.UserList.deleteuser": "", + "components.UserList.descending": "", + "components.UserList.edituser": "", + "components.UserList.importedPlexUsersNoPassword": "", + "components.UserList.importedUsersNoPassword": "", + "components.UserList.importedfromJellyfin": "", + "components.UserList.importfromJellyfin": "", + "components.UserList.importfromJellyfinerror": "", + "components.UserList.importfrommediaserver": "", + "components.UserList.importfromplex": "", + "components.UserList.importfromplexerror": "", + "components.UserList.mediaServerUser": "", + "components.UserList.newJellyfinsigninenabled": "", + "components.UserList.newplexsigninenabled": "", + "components.UserList.noJellyfinuserstoimport": "", + "components.UserList.nouserstoimport": "", + "components.UserList.password": "", + "components.UserList.passwordinfodescription": "", + "components.UserList.plexuser": "", + "components.UserList.role": "", + "components.UserList.sortBy": "", + "components.UserList.sortByJoined": "", + "components.UserList.sortByRequests": "", + "components.UserList.sortByRole": "", + "components.UserList.sortByType": "", + "components.UserList.sortByUser": "", + "components.UserList.toggleSortDirection": "", + "components.UserList.user": "", + "components.UserList.usercreatedfailed": "", + "components.UserList.usercreatedfailedexisting": "", + "components.UserList.usercreatedsuccess": "", + "components.UserList.userdeleteerror": "", + "components.UserList.userfail": "", + "components.UserList.username": "", + "components.UserList.users": "", + "components.UserList.userssaved": "", + "components.UserList.validationEmail": "", + "components.UserList.validationUsername": "", + "components.UserList.validationpasswordminchars": "", + "components.UserProfile.ProfileHeader.joindate": "", + "components.UserProfile.ProfileHeader.profile": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.description": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorUnauthorized": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.save": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.saving": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.title": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.username": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.usernameRequired": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.deleteFailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.emailsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pgpPublicKeyTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessToken": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessTokenTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverApplicationToken": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverApplicationTokenTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverUserKey": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverUserKeyTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoversettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoversettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.sendSilentlyDescription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.sound": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramChatIdTipLong": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadId": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadIdTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPgpPublicKey": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPushbulletAccessToken": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverApplicationToken": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverUserKey": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramChatId": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramMessageThreadId": "", + "components.UserProfile.UserSettings.UserPasswordChange.confirmpassword": "", + "components.UserProfile.UserSettings.UserPasswordChange.currentpassword": "", + "components.UserProfile.UserSettings.UserPasswordChange.localPasswordDescription": "", + "components.UserProfile.UserSettings.UserPasswordChange.newpassword": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "", + "components.UserProfile.UserSettings.UserPasswordChange.noPasswordSet": "", + "components.UserProfile.UserSettings.UserPasswordChange.nopermissionDescription": "", + "components.UserProfile.UserSettings.UserPasswordChange.password": "", + "components.UserProfile.UserSettings.UserPasswordChange.validationConfirmPasswordSame": "", + "components.UserProfile.UserSettings.UserPasswordChange.validationCurrentPassword": "", + "components.UserProfile.UserSettings.UserPasswordChange.validationNewPassword": "", + "components.UserProfile.UserSettings.UserPasswordChange.validationNewPasswordLength": "", + "components.UserProfile.UserSettings.UserPermissions.permissions": "", + "components.UserProfile.UserSettings.UserPermissions.toastSettingsFailure": "", + "components.UserProfile.UserSettings.UserPermissions.unauthorizedDescription": "", + "components.UserProfile.UserSettings.menuChangePass": "", + "components.UserProfile.UserSettings.unauthorizedDescription": "", + "components.UserProfile.emptywatchlist": "", + "components.UserProfile.limit": "", + "components.UserProfile.localWatchlist": "", + "components.UserProfile.movierequests": "", + "components.UserProfile.seriesrequest": "", + "i18n.notificationLocale": "", + "i18n.retrying": "", + "components.Settings.SettingsNetwork.proxyBypassFilter": "", + "components.Settings.SonarrModal.add": "", + "components.Settings.SonarrModal.animeSeriesType": "", + "components.Settings.SonarrModal.validationLanguageProfileRequired": "", + "components.Settings.currentlibrary": "", + "components.Settings.jellyfinsettingsDescription": "", + "components.Settings.plexlibrariesDescription": "", + "components.Settings.plexsettingsDescription": "", + "components.Settings.restartrequiredTooltip": "", + "components.Settings.tautulliSettings": "", + "components.Settings.toastPlexConnecting": "", + "components.Settings.toastPlexConnectingFailure": "", + "components.Settings.validationUrl": "", + "components.Settings.validationUrlTrailingSlash": "", + "components.Settings.valueRequired": "", + "components.Setup.finishing": "", + "components.Setup.librarieserror": "", + "components.Setup.servertype": "", + "components.Setup.signin": "", + "components.Setup.signinWithPlex": "", + "components.Setup.subtitle": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.created": "", + "components.UserProfile.pastdays": "", + "components.Settings.SettingsMain.toastApiKeySuccess": "", + "components.Settings.SettingsMain.toastApiKeyFailure": "", + "components.Settings.invalidKeyword": "", + "components.Settings.invalidurlerror": "", + "components.Settings.jellyfinForgotPasswordUrl": "", + "components.Settings.noDefaultServer": "", + "components.StatusChecker.reloadApp": "", + "components.Settings.Notifications.validationPgpPassword": "", + "components.Settings.timeout": "", + "components.Settings.SonarrModal.monitorNewItems": "", + "components.Settings.SonarrModal.selectQualityProfile": "", + "components.Settings.SonarrModal.selectRootFolder": "", + "components.Settings.SonarrModal.selecttags": "", + "components.Settings.copyBlocklistedTagsEmpty": "", + "components.Settings.toastPlexConnectingSuccess": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablewebpush": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.type": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleteerror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.email": "", + "components.UserProfile.UserSettings.UserNotificationSettings.deviceDefault": "", + "components.Settings.SonarrModal.animeTags": "", + "components.Settings.SonarrModal.loadingprofiles": "", + "components.Settings.is4k": "", + "components.Settings.Notifications.telegramsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.emailsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.notifications": "", + "components.UserProfile.UserSettings.UserNotificationSettings.notificationsettings": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pgpPublicKey": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletsettingsfailed": "", + "components.ResetPassword.emailresetlink": "", + "components.Settings.Notifications.NotificationsGotify.validationTokenRequired": "", + "components.Settings.Notifications.NotificationsGotify.validationTypes": "", + "components.Settings.Notifications.NotificationsGotify.validationUrlRequired": "", + "components.Settings.Notifications.NotificationsGotify.validationPriorityRequired": "", + "components.Settings.Notifications.NotificationsNtfy.validationNtfyUrl": "", + "components.Settings.Notifications.NotificationsNtfy.usernamePasswordAuth": "", + "components.Settings.Notifications.NotificationsNtfy.validationNtfyTopic": "", + "components.Settings.Notifications.NotificationsNtfy.validationTypes": "", + "components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestFailed": "", + "components.Settings.Notifications.NotificationsPushover.sound": "", + "components.Settings.Notifications.NotificationsSlack.slacksettingsfailed": "", + "components.Settings.Notifications.NotificationsSlack.slacksettingssaved": "", + "components.Settings.Notifications.NotificationsSlack.toastSlackTestFailed": "", + "components.Settings.Notifications.NotificationsSlack.toastSlackTestSending": "", + "components.Settings.Notifications.NotificationsSlack.toastSlackTestSuccess": "", + "components.Settings.Notifications.NotificationsSlack.validationTypes": "", + "components.Settings.Notifications.NotificationsWebhook.agentenabled": "", + "components.Settings.Notifications.pgpPrivateKey": "", + "components.Settings.Notifications.telegramsettingsfailed": "", + "components.Settings.Notifications.toastEmailTestSuccess": "", + "components.Settings.Notifications.validationPgpPrivateKey": "", + "components.Settings.SettingsUsers.localLoginTip": "", + "components.Settings.SonarrModal.defaultserver": "", + "components.Settings.SettingsUsers.localLogin": "", + "components.Settings.SonarrModal.editsonarr": "", + "components.Settings.tip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmoviestip": "", + "components.UserList.email": "", + "components.Settings.SonarrModal.port": "", + "components.Settings.SonarrModal.rootfolder": "", + "components.Settings.Notifications.NotificationsNtfy.priority": "", + "components.Settings.Notifications.NotificationsNtfy.topic": "", + "components.Settings.Notifications.discordsettingssaved": "", + "components.UserList.importedfromplex": "", + "components.UserList.localuser": "", + "components.UserList.localLoginDisabled": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.password": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorUnknown": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.passwordRequired": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramChatId": "", + "components.UserProfile.unlimited": "", + "components.Settings.Notifications.userEmailRequired": "", + "components.Settings.Notifications.validationBotAPIRequired": "", + "components.Settings.SonarrModal.selectLanguageProfile": "", + "components.Setup.continue": "", + "components.UserList.userdeleted": "", + "components.UserList.userlist": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorExists": "", + "components.Settings.Notifications.agentenabled": "", + "components.Settings.SettingsUsers.defaultPermissions": "", + "components.Settings.jellyfinSettingsFailure": "", + "components.Settings.Notifications.chatId": "", + "components.Setup.finish": "", + "components.RequestModal.QuotaDisplay.requiredquotaUser": "", + "components.Settings.Notifications.NotificationsPushover.accessToken": "", + "components.Settings.Notifications.NotificationsPushover.accessTokenTip": "", + "components.Settings.Notifications.NotificationsWebhook.templatevariablehelp": "", + "components.Settings.Notifications.NotificationsWebhook.toastWebhookTestFailed": "", + "components.Settings.Notifications.NotificationsWebhook.toastWebhookTestSending": "", + "components.Settings.Notifications.NotificationsWebhook.toastWebhookTestSuccess": "", + "components.Settings.Notifications.NotificationsWebhook.validationJsonPayloadRequired": "", + "components.Settings.Notifications.NotificationsWebhook.validationTypes": "", + "components.Settings.SettingsMain.validationApplicationTitle": "", + "components.Settings.address": "", + "components.Settings.addrule": "", + "i18n.delimitedlist": "", + "components.Settings.Notifications.validationUrl": "", + "components.Settings.SettingsNetwork.proxyEnabled": "", + "components.Settings.serverLocal": "", + "components.Settings.syncing": "", + "components.Settings.yes": "", + "components.UserProfile.ProfileHeader.userid": "", + "components.Settings.Notifications.NotificationsNtfy.ntfysettingsfailed": "", + "components.Settings.Notifications.smtpHost": "", + "components.Settings.Notifications.webhookUrl": "", + "components.UserProfile.UserSettings.UserNotificationSettings.sendSilently": "", + "components.Settings.SettingsJobsCache.jobstarted": "", + "components.Settings.SettingsJobsCache.jobtype": "", + "components.Settings.SettingsJobsCache.misses": "", + "components.Settings.SettingsJobsCache.nextexecution": "", + "components.UserProfile.UserSettings.UserNotificationSettings.webpush": "", + "components.Settings.tautulliSettingsDescription": "", + "components.TitleCard.tmdbid": "", + "components.UserProfile.UserSettings.UserPasswordChange.noPasswordSetOwnAccount": "" } diff --git a/src/i18n/locale/fr.json b/src/i18n/locale/fr.json index 22e67110ed..acd5878a35 100644 --- a/src/i18n/locale/fr.json +++ b/src/i18n/locale/fr.json @@ -118,7 +118,7 @@ "components.Discover.tvgenres": "Séries par genres", "components.Discover.upcoming": "Films à venir", "components.Discover.upcomingmovies": "Films à venir", - "components.Layout.SearchInput.searchPlaceholder": "Rechercher un film ou une série", + "components.Layout.SearchInput.searchPlaceholder": "Rechercher films et séries", "components.Layout.Sidebar.dashboard": "Découvrir", "components.Layout.Sidebar.requests": "Demandes", "components.Layout.Sidebar.settings": "Paramètres", @@ -172,7 +172,7 @@ "components.Settings.RadarrModal.port": "Port", "components.Settings.RadarrModal.qualityprofile": "Profil de qualité", "components.Settings.RadarrModal.rootfolder": "Dossier racine", - "components.Settings.RadarrModal.selectMinimumAvailability": "Sélectionner une disponibilté minimale", + "components.Settings.RadarrModal.selectMinimumAvailability": "Sélectionner une disponibilité minimale", "components.Settings.RadarrModal.selectQualityProfile": "Sélectionner un profil de qualité", "components.Settings.RadarrModal.selectRootFolder": "Sélectionner un dossier racine", "components.Settings.RadarrModal.server4k": "Serveur 4K", @@ -323,14 +323,14 @@ "components.PersonDetails.crewmember": "Équipe", "components.MovieDetails.viewfullcrew": "Voir l'équipe complète", "components.MovieDetails.MovieCrew.fullcrew": "Équipe complète", - "components.TvDetails.firstAirDate": "Date de première diffusion", + "components.TvDetails.firstAirDate": "Première diffusion", "components.Settings.Notifications.allowselfsigned": "Autoriser les certificats autosignés", - "components.TvDetails.watchtrailer": "Regarder la bande-annonce", - "components.MovieDetails.watchtrailer": "Regarder la bande-annonce", + "components.TvDetails.watchtrailer": "Bande-annonce", + "components.MovieDetails.watchtrailer": "Bande-annonce", "i18n.requested": "Demandé", "i18n.retry": "Réessayer", "i18n.failed": "Échec", - "components.Settings.Notifications.NotificationsSlack.webhookUrl": "URL de webhook", + "components.Settings.Notifications.NotificationsSlack.webhookUrl": "URL du webhook", "components.Settings.Notifications.NotificationsSlack.slacksettingssaved": "Paramètres de notification Slack enregistrés !", "components.Settings.Notifications.NotificationsSlack.slacksettingsfailed": "Les paramètres de notification Slack n'ont pas pu être enregistrés.", "components.Settings.Notifications.NotificationsSlack.agentenabled": "Activer l'agent", @@ -365,15 +365,15 @@ "components.StatusBadge.status4k": "{status} en 4K", "components.Settings.Notifications.NotificationsWebhook.webhooksettingssaved": "Paramètres de notification webhook enregistrés !", "components.Settings.Notifications.NotificationsWebhook.webhooksettingsfailed": "Échec de l'enregistrement des paramètres de notification du webhook.", - "components.Settings.Notifications.NotificationsWebhook.webhookUrl": "URL de webhook", + "components.Settings.Notifications.NotificationsWebhook.webhookUrl": "URL du webhook", "components.Settings.Notifications.NotificationsWebhook.templatevariablehelp": "Aide sur les variables de modèle", "components.Settings.Notifications.NotificationsWebhook.authheader": "En-tête d'autorisation", "components.Settings.Notifications.NotificationsWebhook.agentenabled": "Activer l'agent", "components.RequestModal.pending4krequest": "Demande 4K en attente", "components.RequestButton.viewrequest4k": "Voir la demande 4K", "components.RequestButton.viewrequest": "Voir la demande", - "components.RequestButton.requestmore4k": "Demander d'autres ajouts en 4K", - "components.RequestButton.requestmore": "Demander d'autres ajouts", + "components.RequestButton.requestmore4k": "Demander des ajouts en 4K", + "components.RequestButton.requestmore": "Demander des ajouts", "components.RequestButton.declinerequests": "Refuser {requestCount, plural, one {la demande} other {{requestCount} demandes}}", "components.RequestButton.declinerequest4k": "Refuser la demande 4K", "components.RequestButton.declinerequest": "Refuser la demande", @@ -385,7 +385,7 @@ "components.Settings.Notifications.NotificationsWebhook.customJson": "Données utiles JSON", "components.Settings.Notifications.NotificationsWebhook.resetPayloadSuccess": "Les données JSON ont été réinitialisées !", "components.Settings.Notifications.NotificationsWebhook.resetPayload": "Réinitialiser les données par défaut", - "components.Settings.Notifications.NotificationsWebhook.validationJsonPayloadRequired": "Vous devez fournir un JSON payload valide", + "components.Settings.Notifications.NotificationsWebhook.validationJsonPayloadRequired": "Vous devez fournir un payload JSON valide", "components.UserList.validationpasswordminchars": "Le mot de passe est trop court ; il doit contenir au moins 8 caractères", "components.UserList.usercreatedsuccess": "L'utilisateur a bien été créé !", "components.UserList.usercreatedfailed": "Une erreur s'est produite lors de la création de l'utilisateur.", @@ -412,7 +412,7 @@ "components.RequestModal.AdvancedRequester.rootfolder": "Dossier racine", "components.RequestModal.AdvancedRequester.qualityprofile": "Profil de qualité", "components.RequestModal.AdvancedRequester.destinationserver": "Serveur de destination", - "components.RequestModal.AdvancedRequester.default": "{name} (Par défaut)", + "components.RequestModal.AdvancedRequester.default": "{name} (par défaut)", "components.RequestModal.AdvancedRequester.animenote": "* Cette série est un anime.", "components.RequestModal.AdvancedRequester.advancedoptions": "Options avancées", "components.RequestBlock.requestoverrides": "Modifications de la demande", @@ -449,7 +449,7 @@ "components.PermissionEdit.autoapproveMovies": "Valider automatiquement les films", "components.PermissionEdit.autoapproveDescription": "Valide automatiquement toutes les demandes de médias non-4K.", "components.PermissionEdit.autoapprove": "Valider automatiquement", - "components.PermissionEdit.advancedrequestDescription": "Permet de modifier les options de demandes de média avancées.", + "components.PermissionEdit.advancedrequestDescription": "Permet de modifier les options de demandes de médias avancées.", "components.PermissionEdit.advancedrequest": "Demandes avancées", "components.PermissionEdit.adminDescription": "Accès administrateur complet. Contourne toutes les autres permissions (sélectionnées ou non).", "components.PermissionEdit.admin": "Administrateur", @@ -543,7 +543,7 @@ "components.PermissionEdit.autoapprove4kMovies": "Valider automatiquement les films 4K", "components.PermissionEdit.autoapprove4kDescription": "Valide automatiquement toutes les demandes de médias en 4K.", "components.PermissionEdit.autoapprove4k": "Valider automatiquement en 4K", - "components.UserProfile.UserSettings.UserNotificationSettings.discordId": "Identifiant", + "components.UserProfile.UserSettings.UserNotificationSettings.discordId": "Identifiants", "components.UserProfile.UserSettings.UserNotificationSettings.notificationsettings": "Paramètres de notification", "components.UserProfile.UserSettings.UserPasswordChange.newpassword": "Nouveau mot de passe", "components.UserProfile.UserSettings.UserPasswordChange.currentpassword": "Mot de passe actuel", @@ -569,8 +569,8 @@ "components.UserProfile.UserSettings.UserPasswordChange.validationConfirmPassword": "Vous devez confirmer le nouveau mot de passe", "components.UserProfile.UserSettings.UserPasswordChange.toastSettingsSuccess": "Le mot de passe a été enregistré !", "components.UserProfile.UserSettings.UserPasswordChange.toastSettingsFailure": "Une erreur s'est produite lors de l'enregistrement du mot de passe.", - "components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "Vous devez fournir un identifiant valide", - "components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "L' IDassocié à votre compte utilisateur", + "components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "Chaque ID doit être un ID utilisateur Discord valide", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "Le numéro d'identification à plusieurs chiffres associé à votre compte utilisateur. Pour les foyers disposant de plusieurs comptes, vous pouvez ajouter plus d'un ID utilisateur Discord.", "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsSuccess": "Les paramètres ont été enregistrés !", "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailure": "Une erreur s'est produite lors de l'enregistrement des paramètres.", "components.UserProfile.UserSettings.UserGeneralSettings.plexuser": "Utilisateur Plex", @@ -592,7 +592,7 @@ "components.Settings.webhook": "Webhook", "components.Settings.email": "E-mail", "components.RegionSelector.regionServerDefault": "Par défaut ({region})", - "components.UserProfile.UserSettings.UserPasswordChange.nopermissionDescription": "Vous n'avez l'autorisation de modifier le mot de passe de cet utilisateur.", + "components.UserProfile.UserSettings.UserPasswordChange.nopermissionDescription": "Vous n'avez pas l'autorisation de modifier le mot de passe de cet utilisateur.", "components.UserProfile.UserSettings.UserGeneralSettings.user": "Utilisateur", "components.UserProfile.UserSettings.UserGeneralSettings.role": "Rôle", "components.UserProfile.UserSettings.UserGeneralSettings.owner": "Propriétaire", @@ -632,7 +632,7 @@ "components.Settings.SettingsUsers.userSettings": "Paramètres utilisateur", "components.Settings.SettingsUsers.toastSettingsSuccess": "Les paramètres utilisateur ont été enregistrés !", "components.NotificationTypeSelector.mediaAutoApproved": "Demande validée automatiquement", - "components.NotificationTypeSelector.mediaAutoApprovedDescription": "Envoyer des notifications lorsque des utilisateurs soumettent des demandes de média qui sont validées automatiquement.", + "components.NotificationTypeSelector.mediaAutoApprovedDescription": "Envoyer des notifications lorsque des utilisateurs soumettent des demandes de médias qui sont validées automatiquement.", "components.UserProfile.UserSettings.unauthorizedDescription": "Vous n'avez pas l'autorisation de modifier les paramètres de cet utilisateur.", "components.UserProfile.UserSettings.UserPermissions.unauthorizedDescription": "Vous ne pouvez pas modifier vos propres permissions.", "components.Settings.Notifications.pgpPrivateKeyTip": "Signer des e-mails chiffrés en utilisant OpenPGP", @@ -690,8 +690,8 @@ "components.RequestModal.QuotaDisplay.notenoughseasonrequests": "Pas assez de demandes de saisons restantes", "components.RequestModal.QuotaDisplay.movielimit": "{limit, plural, one {film} other {films}}", "components.RequestModal.QuotaDisplay.movie": "film", - "components.RequestModal.QuotaDisplay.allowedRequestsUser": "Cet utilisateur est autorisé à demander {limit} {type} tous les {days} jour(s).", - "components.RequestModal.QuotaDisplay.allowedRequests": "Vous êtes autorisé à demander {limit} {type} tous les {days} jour(s).", + "components.RequestModal.QuotaDisplay.allowedRequestsUser": "Cet utilisateur peut demander {limit} {type}{days, plural, =0 {} one { par jour} other { tous les {days} jours}}.", + "components.RequestModal.QuotaDisplay.allowedRequests": "Vous pouvez demander {limit} {type}{days, plural, =0 {} one { par jour} other { tous les {days} jours}}.", "components.QuotaSelector.unlimited": "Illimité", "components.UserProfile.unlimited": "Illimité", "components.TvDetails.originaltitle": "Titre original", @@ -710,9 +710,9 @@ "i18n.view": "Voir", "i18n.testing": "Test en cours…", "i18n.status": "Statut", - "i18n.saving": "Enregistrement en cours…", + "i18n.saving": "Enregistrement…", "i18n.resultsperpage": "Afficher {pageSize} résultats par page", - "i18n.requesting": "Demande en cours…", + "i18n.requesting": "Demande…", "i18n.previous": "Précédent", "i18n.notrequested": "Non demandé", "i18n.noresults": "Aucun résultat.", @@ -755,7 +755,7 @@ "components.UserProfile.UserSettings.UserNotificationSettings.validationPgpPublicKey": "Vous devez fournir une clé publique PGP valide", "components.UserProfile.UserSettings.UserNotificationSettings.telegramsettingssaved": "Les paramètres de notification Telegram ont été enregistrés !", "components.UserProfile.UserSettings.UserNotificationSettings.telegramsettingsfailed": "Impossible d'enregistrer les paramètres de notification de Telegram.", - "components.UserProfile.UserSettings.UserNotificationSettings.pgpPublicKey": "Clé Publique PGP", + "components.UserProfile.UserSettings.UserNotificationSettings.pgpPublicKey": "Clé publique PGP", "components.UserProfile.UserSettings.UserNotificationSettings.email": "E-mail", "components.UserProfile.UserSettings.UserNotificationSettings.discordsettingsfailed": "Les paramètres de notification Discord n'ont pas pu être enregistrés.", "components.Settings.serviceSettingsDescription": "Configurez votre serveur {serverType} ci-dessous. Vous pouvez connecter plusieurs serveurs {serverType}, mais seuls deux d'entre eux peuvent être définis par défaut (un non-4K et un 4K). Les administrateurs peuvent modifier le serveur utilisé pour traiter les nouvelles demandes avant la validation.", @@ -774,7 +774,7 @@ "components.RequestCard.deleterequest": "Supprimer la demande", "components.NotificationTypeSelector.notificationTypes": "Types de notifications", "components.Layout.VersionStatus.streamstable": "Seerr stable", - "components.Layout.VersionStatus.streamdevelop": "Développement de Seerr", + "components.Layout.VersionStatus.streamdevelop": "Seerr développement", "components.Layout.VersionStatus.commitsbehind": "{commitsBehind} {commitsBehind, plural, one {commit} other {commits}} en retard", "components.Layout.VersionStatus.outofdate": "Obsolète", "components.RequestModal.QuotaDisplay.requiredquota": "Vous devez avoir au moins {seasons} {seasons, plural, one {demande de saison restante} other {demandes de saison restantes}} pour demander cette série.", @@ -817,7 +817,7 @@ "components.Settings.Notifications.encryption": "Méthode de chiffrement", "components.Settings.Notifications.encryptionDefault": "Utiliser STARTTLS si disponible", "components.Settings.Notifications.encryptionOpportunisticTls": "Toujours utiliser STARTTLS", - "components.DownloadBlock.estimatedtime": "Estimation {time}", + "components.DownloadBlock.estimatedtime": "Fin estimée : {time}", "components.Settings.Notifications.NotificationsPushover.userTokenTip": "Votre identifiant d'utilisateur ou de groupe de 30 caractères", "components.Settings.Notifications.NotificationsPushover.accessTokenTip": "Enregistrer une application à utiliser avec Seerr", "components.RequestList.RequestItem.requesteddate": "Demandé", @@ -837,15 +837,15 @@ "components.QuotaSelector.movieRequests": "{quotaLimit} {movies} tous les {quotaDays} {days}", "components.QuotaSelector.days": "{count, plural, one {jour} other {jours}}", "components.UserProfile.UserSettings.UserGeneralSettings.languageDefault": "Par défaut ({language})", - "components.Settings.webAppUrlTip": "Dirigez éventuellement les utilisateurs vers l'application web de votre serveur au lieu de l'application web « hébergée »", - "components.Settings.webAppUrl": "URL application web", + "components.Settings.webAppUrlTip": "Rediriger éventuellement les utilisateurs vers l'application web de votre serveur plutôt que vers https://app.plex.tv/desktop", + "components.Settings.webAppUrl": "URL de l'application web", "components.Settings.noDefault4kServer": "Un serveur {serverType} 4K doit être défini par défaut pour permettre aux utilisateurs de faire des demandes de {mediaType} en 4K.", "components.Settings.is4k": "4K", "components.Settings.SettingsUsers.newPlexLoginTip": "Autoriser les utilisateurs {mediaServerName} à se connecter sans avoir été importés", "components.Settings.SettingsUsers.newPlexLogin": "Autoriser l'inscription via {mediaServerName}", "components.Settings.SettingsUsers.localLoginTip": "Permettre aux utilisateurs de se connecter en utilisant leur adresse e-mail et leur mot de passe", "components.Settings.SettingsUsers.defaultPermissionsTip": "Autorisations par défaut attribuées aux nouveaux utilisateurs", - "components.Settings.Notifications.webhookUrlTip": "Créez une intégration de webhook dans votre serveur", + "components.Settings.Notifications.webhookUrlTip": "Créez une intégration webhook dans votre serveur", "components.Settings.Notifications.validationTypes": "Vous devez sélectionner au moins un type de notification", "components.Settings.Notifications.toastTelegramTestSuccess": "Notification de test Telegram envoyée !", "components.Settings.Notifications.toastTelegramTestSending": "Envoi de la notification de test à Telegram…", @@ -864,9 +864,9 @@ "components.QuotaSelector.tvRequests": "{quotaLimit} {seasons} tous les {quotaDays} {days}", "components.NotificationTypeSelector.usermediarequestedDescription": "M'avertir lorsque d'autres utilisateurs soumettent une demande de média nécessitant une validation.", "components.NotificationTypeSelector.usermediafailedDescription": "M'avertir lorsqu'une demande de média n'a pas pu être ajoutée à Radarr ou Sonarr.", - "components.NotificationTypeSelector.usermediadeclinedDescription": "M'avertir lorsque mes demandes de média sont refusées.", - "components.NotificationTypeSelector.usermediaavailableDescription": "M'avertir lorsque mes demandes de média deviennent disponibles.", - "components.NotificationTypeSelector.usermediaapprovedDescription": "M'avertir lorsque mes demandes de média sont validées.", + "components.NotificationTypeSelector.usermediadeclinedDescription": "M'avertir lorsque mes demandes de médias sont refusées.", + "components.NotificationTypeSelector.usermediaavailableDescription": "M'avertir lorsque mes demandes de médias deviennent disponibles.", + "components.NotificationTypeSelector.usermediaapprovedDescription": "M'avertir lorsque mes demandes de médias sont validées.", "components.NotificationTypeSelector.usermediaAutoApprovedDescription": "M'avertir lorsque des utilisateurs soumettent une demande de média qui est validée automatiquement.", "components.MovieDetails.showmore": "Montrer plus", "components.MovieDetails.showless": "Montrer moins", @@ -946,7 +946,7 @@ "components.IssueModal.CreateIssueModal.toastSuccessCreate": "Le problème concernant {title} a bien été signalé !", "components.IssueModal.CreateIssueModal.whatswrong": "Qu'est-ce qui ne va pas ?", "components.Layout.Sidebar.issues": "Problèmes", - "components.ManageSlideOver.downloadstatus": "Téléchargement(s)", + "components.ManageSlideOver.downloadstatus": "Téléchargements", "components.ManageSlideOver.manageModalNoRequests": "Aucune demande.", "components.ManageSlideOver.manageModalRequests": "Demandes", "components.ManageSlideOver.manageModalTitle": "Gérer {mediaType}", @@ -1030,7 +1030,7 @@ "components.Settings.Notifications.NotificationsGotify.validationTokenRequired": "Vous devez fournir un jeton d'application", "components.Settings.Notifications.NotificationsGotify.validationUrlTrailingSlash": "L'URL ne doit pas se terminer par un slash (/)", "components.ManageSlideOver.manageModalAdvanced": "Avancé", - "components.ManageSlideOver.manageModalMedia4k": "Média(s) 4K", + "components.ManageSlideOver.manageModalMedia4k": "Médias 4K", "components.ManageSlideOver.markallseasons4kavailable": "Marquer toutes les saisons comme disponibles en 4K", "components.ManageSlideOver.playedby": "Visionné par", "components.Settings.validationUrlTrailingSlash": "L'URL ne doit pas se terminer par un slash (/)", @@ -1048,15 +1048,12 @@ "components.ManageSlideOver.pastdays": "{days, number} derniers jours", "components.ManageSlideOver.plays": "{playCount, number} {playCount, plural, one {lecture} other {lectures}}", "components.Settings.Notifications.NotificationsPushbullet.channelTag": "Étiquette de canal", - "components.ManageSlideOver.alltime": "Tout le temps", - "components.ManageSlideOver.manageModalMedia": "Média", + "components.ManageSlideOver.alltime": "Depuis le début", + "components.ManageSlideOver.manageModalMedia": "Médias", "components.ManageSlideOver.markallseasonsavailable": "Marquer toutes les saisons comme disponibles", "components.Settings.validationUrlBaseTrailingSlash": "L'URL de base ne doit pas se terminer par un slash (/)", "components.Settings.tautulliSettingsDescription": "Configuration optionnelle de votre serveur Tautulli. Seerr récupérera l'historique de visionnage de votre serveur Plex depuis Tautulli.", "components.UserList.newplexsigninenabled": "L'option Autoriser l'inscription via Plex est actuellement activée. Les utilisateurs Plex ayant accès aux bibliothèques n'ont pas besoin d'être importés pour se connecter.", - "components.UserProfile.UserSettings.UserGeneralSettings.discordId": "ID utilisateur Discord", - "components.UserProfile.UserSettings.UserGeneralSettings.discordIdTip": "Le numéro d'identification à plusieurs chiffres associé à votre compte Discord", - "components.UserProfile.UserSettings.UserGeneralSettings.validationDiscordId": "Indiquez un ID utilisateur Discord valide", "components.Settings.SettingsAbout.appDataPath": "Répertoire de données", "components.MovieDetails.digitalrelease": "Sortie numérique", "components.MovieDetails.physicalrelease": "Sortie physique", @@ -1208,7 +1205,7 @@ "components.Settings.jellyfinSettingsFailure": "Une erreur s'est produite lors de l'enregistrement des paramètres de {mediaServerName}.", "components.Settings.jellyfinSettingsSuccess": "Paramètres de {mediaServerName} enregistrés !", "components.Settings.jellyfinlibraries": "Bibliothèques {mediaServerName}", - "components.Settings.jellyfinlibrariesDescription": "Les bibliothèques de {mediaServerName} sont en cours d'analyse. Cliquez sur le bouton ci-dessous si aucune bibliothèque n'est répertoriée.", + "components.Settings.jellyfinlibrariesDescription": "Les bibliothèques analysées par {mediaServerName} pour trouver des titres. Cliquez sur le bouton ci-dessous si aucune bibliothèque ne s'affiche.", "components.Settings.jellyfinsettings": "Paramètres {mediaServerName}", "components.Settings.manualscanJellyfin": "Analyse manuelle des bibliothèques", "components.Settings.menuJellyfinSettings": "{mediaServerName}", @@ -1221,7 +1218,7 @@ "components.StatusBadge.openinarr": "Ouvrir dans {arr}", "components.StatusBadge.playonplex": "Lire sur {mediaServerName}", "components.TitleCard.addToWatchList": "Ajouter à la liste de suivi", - "components.TitleCard.watchlistCancel": "List de suivi pour {title} annulée.", + "components.TitleCard.watchlistCancel": "Liste de suivi pour {title} annulée.", "components.TitleCard.watchlistError": "Une erreur s'est produite. Veuillez réessayer.", "components.TitleCard.watchlistSuccess": "{title} a été ajouté à votre liste de suivi !", "components.TvDetails.Season.somethingwentwrong": "Une erreur s'est produite lors de la récupération des données de la saison.", @@ -1236,7 +1233,7 @@ "components.UserList.importfromJellyfin": "Importer les utilisateurs {mediaServerName}", "components.UserList.importfromJellyfinerror": "Une erreur s'est produite lors de l'importation des utilisateurs de {mediaServerName}.", "components.UserList.importfrommediaserver": "Importer les utilisateurs {mediaServerName}", - "components.UserList.noJellyfinuserstoimport": "Il n'y a aucun utilisateur à importer pour {mediaServerName}.", + "components.UserList.noJellyfinuserstoimport": "Aucun nouvel utilisateur {mediaServerName} à importer.", "components.UserProfile.UserSettings.UserGeneralSettings.saving": "Enregistrement en cours…", "components.UserProfile.plexwatchlist": "Liste de suivi Plex", "components.Settings.syncJellyfin": "Synchroniser les bibliothèques", @@ -1258,7 +1255,7 @@ "components.UserProfile.UserSettings.UserGeneralSettings.mediaServerUser": "Utilisateur {mediaServerName}", "components.Setup.signinWithJellyfin": "Entrez vos identifiants Jellyfin", "components.UserList.mediaServerUser": "Utilisateur {mediaServerName}", - "components.TvDetails.episodeCount": "{episodeCount, plural, one {# Épisode} other {# Épisodes}}", + "components.TvDetails.episodeCount": "{episodeCount, plural, one {# épisode} other {# épisodes}}", "components.UserList.newJellyfinsigninenabled": "L'option Autoriser l'inscription via {mediaServerName} est actuellement activée. Les utilisateurs {mediaServerName} ayant accès aux bibliothèques n'ont pas besoin d'être importés pour se connecter.", "components.UserProfile.UserSettings.UserGeneralSettings.email": "E-mail", "components.UserProfile.UserSettings.UserGeneralSettings.save": "Enregistrer les modifications", @@ -1346,7 +1343,7 @@ "components.DiscoverTvUpcoming.upcomingtv": "Séries à venir", "components.Login.noadminerror": "Aucun compte administrateur trouvé sur ce serveur.", "components.Login.orsigninwith": "Ou se connecter avec", - "components.Settings.OverrideRuleModal.keywords": "Mots clés", + "components.Settings.OverrideRuleModal.keywords": "Mots-clés", "components.Settings.OverrideRuleModal.qualityprofile": "Profil de qualité", "components.Settings.OverrideRuleModal.rootfolder": "Dossier racine", "components.Settings.OverrideRuleModal.selectService": "Choisir le service", @@ -1355,7 +1352,7 @@ "components.Settings.OverrideRuleModal.tags": "Tags", "components.Settings.OverrideRuleModal.users": "Utilisateurs", "components.Settings.OverrideRuleTile.genre": "Genre", - "components.Settings.OverrideRuleTile.keywords": "Mots clés", + "components.Settings.OverrideRuleTile.keywords": "Mots-clés", "components.Settings.OverrideRuleTile.qualityprofile": "Profil de qualité", "components.Settings.OverrideRuleTile.rootfolder": "Dossier racine", "components.Settings.OverrideRuleTile.settings": "Paramètres", @@ -1368,9 +1365,9 @@ "components.Settings.SettingsUsers.loginMethods": "Méthodes de connexion", "components.Settings.menuNetwork": "Réseau", "components.Settings.SettingsUsers.mediaServerLoginTip": "Autoriser les utilisateurs à se connecter en utilisant leur compte {mediaServerName}", - "components.UserProfile.UserSettings.LinkJellyfinModal.description": "Saisissez vos informations de connexion à {mediaServerName} pour lier votre compte à {applicationName}.", + "components.UserProfile.UserSettings.LinkJellyfinModal.description": "Saisissez vos identifiants {mediaServerName} pour associer votre compte à {applicationName}.", "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadId": "ID du fil de discussion", - "components.Setup.librarieserror": "La validation a échoué. Veuillez réactiver les bibliothèques pour continuer.", + "components.Setup.librarieserror": "Impossible de charger les bibliothèques depuis votre serveur multimédia. Vérifiez qu'il est accessible et que sa configuration est toujours correcte.", "components.Settings.Notifications.messageThreadId": "ID du fil de discussion", "components.Settings.overrideRulesDescription": "Les règles de contournement vous permettent de spécifier les propriétés qui seront remplacées si une demande correspond à la règle.", "components.Settings.Notifications.messageThreadIdTip": "Si votre discussion de groupe prend en charge les fils de discussion, vous pouvez spécifier son identifiant ici", @@ -1379,7 +1376,7 @@ "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadIdTip": "Si votre discussion de groupe prend en charge les fils de discussion, vous pouvez spécifier son identifiant ici", "components.Settings.OverrideRuleModal.conditionsDescription": "Spécifie les conditions avant d'appliquer les modifications des paramètres. Chaque champ doit être validé pour que les règles s'appliquent (opération ET). Un champ est considéré comme vérifié si l'une de ses propriétés correspond (opération OU).", "components.Settings.SettingsNetwork.toastSettingsFailure": "Une erreur s'est produite lors de l'enregistrement des paramètres.", - "components.UserProfile.UserSettings.LinkJellyfinModal.errorUnauthorized": "Impossible de se connecter à {mediaServerName} en utilisant vos informations de connexion", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorUnauthorized": "Impossible de se connecter à {mediaServerName} avec vos identifiants", "components.UserProfile.UserSettings.LinkJellyfinModal.passwordRequired": "Vous devez fournir un mot de passe", "components.Settings.addrule": "Nouvelle règle de contournement", "components.Settings.Notifications.validationMessageThreadId": "L'identifiant du fil de discussion doit être un nombre entier positif", @@ -1420,7 +1417,7 @@ "components.UserProfile.UserSettings.LinkJellyfinModal.password": "Mot de passe", "components.UserProfile.UserSettings.LinkJellyfinModal.save": "Lien", "components.UserProfile.UserSettings.LinkJellyfinModal.saving": "Ajout en cours…", - "components.UserProfile.UserSettings.LinkJellyfinModal.title": "Lier le compte {mediaServerName}", + "components.UserProfile.UserSettings.LinkJellyfinModal.title": "Associer le compte {mediaServerName}", "components.UserProfile.UserSettings.LinkJellyfinModal.username": "Nom d'utilisateur", "components.UserProfile.UserSettings.LinkJellyfinModal.usernameRequired": "Le nom d'utilisateur est requis", "components.UserProfile.UserSettings.UserLinkedAccountsSettings.deleteFailed": "Impossible de supprimer le compte associé.", @@ -1528,7 +1525,7 @@ "components.MetadataSelector.tvdbLabel": "TheTVDB", "components.Settings.Notifications.NotificationsGotify.priority": "Priorité", "components.Settings.Notifications.NotificationsNtfy.ntfysettingssaved": "Les paramètres de notifications Ntfy ont été enregistrés !", - "components.Settings.Notifications.NotificationsWebhook.webhookUrlTip": "L'URL de notification de test est définie à {testUrl} au lieu de la véritable URL de webhook.", + "components.Settings.Notifications.NotificationsWebhook.webhookUrlTip": "L'URL de test des notifications est définie sur {testUrl} au lieu de la véritable URL du webhook.", "components.Settings.SettingsJobsCache.misses": "Échecs", "components.Settings.SettingsMain.hideAvailableTip": "Masquer les médias disponibles dans les pages de découverte, mais pas dans les résultats de recherche", "components.Settings.SettingsMain.validationUrlTrailingSlash": "L'URL ne doit pas se terminer par un slash (/)", @@ -1553,10 +1550,10 @@ "components.Settings.animeMetadataProvider": "Fournisseur de métadonnées pour les anime", "components.Settings.SettingsJobsCache.editJobScheduleSelectorDays": "Tous les {jobScheduleDays, plural, one {jours} other {{jobScheduleDays} jours}}", "components.Settings.SettingsAbout.aboutseerr": "À propos de Seerr", - "components.Settings.SettingsAbout.contribute": "Faites une contribution", + "components.Settings.SettingsAbout.contribute": "Faire un don", "components.Settings.SettingsAbout.supportseerr": "Soutenez Seerr", "components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "Vous devez fournir un TTL maximum valide", - "components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "Vous devez fournir un TTL maximum valide", + "components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "Vous devez fournir un TTL minimum valide", "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": "Abonnement actif", "components.Discover.RecentRequestsSlider.unableToConnect": "Impossible de se connecter à {services}. Certaines informations pourraient ne pas être disponibles.", "components.RequestList.unableToConnect": "Impossible de se connecter à {services}. Certaines informations pourraient ne pas être disponibles.", @@ -1599,7 +1596,7 @@ "components.Settings.SettingsMain.blocklistedTagsLimit": "Limiter le contenu bloqué par tag", "components.Settings.SettingsMain.blocklistedTagsLimitTip": "La tâche « Traiter les tags bloqués » bloquera autant de pages dans chaque tri. Des nombres plus élevés créeront une liste de blocage plus précise, mais utiliseront plus d'espace.", "components.Settings.SettingsMain.blocklistedTagsTip": "Ajouter automatiquement le contenu avec des tags à la liste de blocage en utilisant la tâche « Traiter les tags bloqués »", - "components.Settings.blocklistedTagImportInstructions": "Coller la configuration du tag liste de blocage ci-dessous.", + "components.Settings.blocklistedTagImportInstructions": "Collez la configuration des tags de liste de blocage ci-dessous.", "components.Settings.blocklistedTagImportTitle": "Importer la configuration des tags bloqués", "components.Settings.blocklistedTagsText": "Tags bloqués", "components.Settings.clearBlocklistedTagsConfirm": "Êtes-vous sûr de vouloir effacer les tags bloqués ?", @@ -1666,5 +1663,50 @@ "components.UserList.importedUsersNoPassword": "Les utilisateurs importés n'ont pas de mot de passe {applicationTitle}. Si vous désactivez la connexion via {mediaServerName}, ils devront en configurer un depuis leur profil Seerr ou avec un lien de réinitialisation.", "components.UserProfile.UserSettings.UserPasswordChange.localPasswordDescription": "Ce mot de passe permet de vous connecter localement à {applicationTitle}. Il est différent de celui de votre serveur multimédia.", "i18n.notificationLocale": "Langue des notifications", - "components.Settings.Notifications.useUserLocale": "Utiliser la langue du destinataire" + "components.Settings.Notifications.useUserLocale": "Utiliser la langue du destinataire", + "components.Settings.Notifications.usePublicLogo": "Utiliser le logo public de Seerr au lieu de celui de l'instance", + "components.Settings.Notifications.usePublicLogoTip": "Si votre instance Seerr n'est pas accessible publiquement, activez cette option pour que les clients de messagerie extérieurs à votre réseau puissent afficher le logo. Celui-ci sera récupéré depuis le dépôt GitHub public.", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdAdd": "Ajouter l'ID utilisateur", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdPlaceholder": "ID utilisateur Discord", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdRemove": "Retirer", + "components.Settings.Notifications.validationWebhookThreadId": "Vous devez fournir un ID de thread Discord valide", + "components.Settings.Notifications.webhookThreadId": "ID de thread", + "components.Settings.Notifications.webhookThreadIdTip": "ID du salon de discussion où publier les notifications. Laissez vide pour publier dans le salon du webhook", + "components.UserProfile.UserSettings.UserNotificationSettings.discordNotificationsNotEnabled": "Le propriétaire du serveur n'a pas activé les notifications Discord. Ces informations ne seront utilisées que s'il configure un service externe.", + "components.RequestModal.AdvancedRequester.ignoreQuotaDescription": "Cette demande ne sera pas décomptée de la limite de l'utilisateur. À utiliser avec prudence.", + "components.RequestModal.AdvancedRequester.ignoreQuotaTitle": "Ignorer la limite utilisateur", + "components.Common.QuickConnectModal.error": "Erreur", + "components.Common.QuickConnectModal.expired": "Code expiré", + "components.Common.QuickConnectModal.expiredMessage": "Ce code Quick Connect a expiré. Veuillez réessayer.", + "components.Common.QuickConnectModal.tryAgain": "Réessayer", + "components.Common.QuickConnectModal.waitingForAuth": "En attente d'autorisation...", + "components.Login.JellyfinQuickConnectModal.cancel": "Annuler", + "components.Login.JellyfinQuickConnectModal.instructions": "Saisissez ce code dans votre application {mediaServerName}", + "components.Login.JellyfinQuickConnectModal.subtitle": "Se connecter avec Quick Connect", + "components.Login.JellyfinQuickConnectModal.title": "Quick Connect", + "components.Login.quickconnect": "Quick Connect", + "components.Login.quickconnecterror": "Échec de Quick Connect. Veuillez réessayer.", + "components.UserList.importfromplexnochanges": "Import Plex terminé. Aucun utilisateur n'a été créé ni mis à jour.", + "components.UserList.importfromplexsynced": "Synchronisation des utilisateurs Plex réussie. Les utilisateurs existants ont été mis à jour.", + "components.UserList.syncnotice": "Vous pouvez toujours cliquer sur Synchroniser pour actualiser les utilisateurs Plex existants, par exemple après la modification de leur e-mail ou de leur nom d'utilisateur.", + "components.UserProfile.UserSettings.LinkJellyfinModal.quickConnect": "Utiliser Quick Connect", + "components.UserProfile.UserSettings.LinkJellyfinQuickConnectModal.instructions": "Saisissez ce code dans votre application {mediaServerName}", + "components.UserProfile.UserSettings.LinkJellyfinQuickConnectModal.subtitle": "Quick Connect", + "components.UserProfile.UserSettings.LinkJellyfinQuickConnectModal.title": "Associer le compte {mediaServerName}", + "components.UserProfile.UserSettings.LinkJellyfinQuickConnectModal.usePassword": "Utiliser plutôt le mot de passe", + "hooks.useQuickConnect.errorMessage": "Impossible d'initialiser Quick Connect. Veuillez réessayer.", + "i18n.sync": "Synchroniser", + "i18n.syncing": "Synchronisation…", + "components.Settings.SettingsMain.versionCheck": "Vérification de la version", + "components.Settings.SettingsMain.versionCheckTip": "Vérifier automatiquement les nouvelles versions sur GitHub.", + "components.Settings.SettingsAbout.versionCheckDisabled": "Vérification de la version désactivée", + "components.ManageSlideOver.clearmediadataerror": "Une erreur s'est produite lors de l'effacement des données du média.", + "components.ManageSlideOver.removemediaerror": "Une erreur s'est produite lors de la suppression du média.", + "components.RequestList.RequestItem.removemediaerror": "Une erreur s'est produite lors de la suppression du média.", + "components.Settings.Notifications.NotificationsGotify.embedPoster": "Intégrer l'affiche", + "components.Settings.jellyfinSyncFailedConnectionError": "Impossible de joindre le serveur {mediaServerName}. Vérifiez qu'il est en cours d'exécution et accessible depuis Seerr.", + "components.Settings.toastPlexSyncFailure": "Échec de la synchronisation des bibliothèques Plex.", + "components.Settings.toggleLibraryFailure": "Échec de la mise à jour de la bibliothèque.", + "components.UserList.searchUsers": "Rechercher…", + "components.Settings.Notifications.NotificationsNtfy.tags": "Tags" } diff --git a/src/i18n/locale/he.json b/src/i18n/locale/he.json index 1235cdd207..3e1e47cb94 100644 --- a/src/i18n/locale/he.json +++ b/src/i18n/locale/he.json @@ -732,5 +732,941 @@ "components.Settings.Notifications.validationTypes": "יש לבחור לפחות סוג התראות אחד", "components.Settings.Notifications.validationUrl": "יש לספק כתובת URL תקינה", "components.Settings.Notifications.webhookUrl": "URL של ה-Webhook", - "components.Settings.Notifications.chatId": "מזהה צ'אט" + "components.Settings.Notifications.chatId": "מזהה צ'אט", + "component.BlocklistBlock.blocklistdate": "", + "components.Blocklist.blocklistsettings": "", + "components.Blocklist.filterBlocklistedTags": "", + "components.Blocklist.filterManual": "", + "components.Blocklist.mediaName": "", + "components.Blocklist.mediaTmdbId": "", + "components.Blocklist.mediaType": "", + "components.Blocklist.showAllBlocklisted": "", + "components.Layout.Sidebar.blocklist": "", + "components.Discover.timeWindowDay": "", + "components.Discover.timeWindowWeek": "", + "components.Discover.FilterSlideover.certification": "", + "components.Discover.FilterSlideover.excludeKeywords": "", + "components.IssueList.IssueItem.descriptionpreview": "", + "component.BlocklistBlock.blocklistedby": "", + "component.BlocklistModal.blocklisting": "", + "components.Blocklist.blocklistNotFoundError": "", + "components.Blocklist.blocklistSettingsDescription": "", + "components.Blocklist.blocklistdate": "", + "components.Blocklist.blocklistedby": "", + "components.CollectionDetails.removefromblocklistpartialcount": "", + "components.Layout.UserWarnings.profileIncomplete": "", + "components.Login.emailAlreadyTaken": "", + "components.Login.jellyfinLocalLoginHint": "", + "components.Login.saveFailed": "", + "components.PermissionEdit.blocklistedItems": "", + "components.PermissionEdit.blocklistedItemsDescription": "", + "components.PermissionEdit.manageblocklist": "", + "components.PermissionEdit.manageblocklistDescription": "", + "components.PermissionEdit.viewblocklistedItems": "", + "components.PermissionEdit.viewblocklistedItemsDescription": "", + "components.Selector.CertificationSelector.minRating": "", + "components.Settings.Notifications.NotificationsGotify.priority": "", + "components.Settings.Notifications.NotificationsGotify.validationPriorityRequired": "", + "components.Settings.Notifications.NotificationsNtfy.agentenabled": "", + "components.Settings.Notifications.NotificationsNtfy.password": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestSending": "", + "components.Settings.Notifications.NotificationsWebhook.supportVariables": "", + "components.Settings.RadarrModal.servername": "", + "components.Settings.RadarrModal.ssl": "", + "components.Settings.SettingsJobsCache.cacheDescription": "", + "components.Settings.SettingsJobsCache.cacheflushed": "", + "components.Settings.SettingsJobsCache.cachehits": "", + "components.Settings.SettingsJobsCache.cachekeys": "", + "components.Settings.SettingsJobsCache.cacheksize": "", + "components.Settings.SettingsJobsCache.hitRate": "", + "components.Settings.SettingsJobsCache.hits": "", + "components.Settings.SettingsMain.blocklistLanguageTip": "", + "components.Settings.SettingsMain.blocklistRegion": "", + "components.Settings.SettingsMain.blocklistRegionTip": "", + "components.Settings.SettingsMain.generalsettings": "", + "components.Settings.SettingsMain.streamingRegion": "", + "components.Settings.SettingsMain.validationApplicationUrlTrailingSlash": "", + "components.Settings.SettingsMain.validationUrlTrailingSlash": "", + "components.Settings.SettingsNetwork.apiRequestTimeout": "", + "components.Settings.SettingsNetwork.apiRequestTimeoutTip": "", + "components.Settings.SettingsNetwork.csrfProtectionHoverTip": "", + "components.Settings.SettingsNetwork.csrfProtectionTip": "", + "components.Settings.SettingsNetwork.dnsCache": "", + "components.Settings.SettingsNetwork.docs": "", + "components.Settings.SettingsNetwork.forceIpv4First": "", + "components.Settings.SettingsNetwork.networkDisclaimer": "", + "components.Settings.SettingsNetwork.networksettings": "", + "components.Settings.SettingsNetwork.proxyEnabledTip": "", + "components.Settings.SettingsNetwork.proxyPassword": "", + "components.Settings.SettingsNetwork.proxyPort": "", + "components.Settings.SettingsNetwork.proxySsl": "", + "components.Settings.SettingsNetwork.proxyUser": "", + "components.Settings.SonarrModal.enableSearchHelp": "", + "components.Settings.SonarrModal.externalUrl": "", + "components.Settings.SonarrModal.externalUrlHelp": "", + "components.Settings.SonarrModal.validationApplicationUrlTrailingSlash": "", + "components.Settings.metadataSettings": "", + "components.Settings.webhook": "", + "components.UserList.importfromplexerror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.sendSilently": "", + "components.UserProfile.UserSettings.UserPasswordChange.newpassword": "", + "components.UserProfile.UserSettings.UserGeneralSettings.email": "", + "components.UserProfile.UserSettings.UserGeneralSettings.general": "", + "components.UserProfile.UserSettings.UserGeneralSettings.generalsettings": "", + "components.UserProfile.UserSettings.UserGeneralSettings.languageDefault": "", + "components.UserProfile.UserSettings.UserGeneralSettings.localuser": "", + "components.UserProfile.UserSettings.UserGeneralSettings.mediaServerUser": "", + "components.UserProfile.UserSettings.UserGeneralSettings.movierequestlimit": "", + "components.UserProfile.UserSettings.UserGeneralSettings.originallanguage": "", + "components.UserProfile.UserSettings.UserGeneralSettings.regionTip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.save": "", + "components.UserProfile.UserSettings.UserGeneralSettings.saving": "", + "components.UserProfile.UserSettings.UserGeneralSettings.seriesrequestlimit": "", + "components.UserProfile.UserSettings.UserGeneralSettings.streamingRegion": "", + "components.UserProfile.UserSettings.UserGeneralSettings.streamingRegionTip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailure": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmailEmpty": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsSuccess": "", + "components.UserProfile.UserSettings.UserGeneralSettings.user": "", + "components.UserProfile.UserSettings.UserGeneralSettings.validationemailformat": "", + "components.UserProfile.UserSettings.UserGeneralSettings.validationemailrequired": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.deleteFailed": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.errorUnknown": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.linkedAccounts": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.browser": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.created": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.device": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablewebpush": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablingwebpusherror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablewebpush": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablingwebpusherror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.engine": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.nodevicestoshow": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.operatingsystem": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleted": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleteerror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.type": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.unknown": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeendisabled": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeenenabled": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdAdd": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdPlaceholder": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdRemove": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "", + "components.Login.urlBaseHelp": "", + "components.MetadataSelector.tmdbLabel": "", + "components.Settings.Notifications.NotificationsNtfy.embedPoster": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestSuccess": "", + "components.Settings.Notifications.NotificationsNtfy.token": "", + "components.Settings.Notifications.NotificationsNtfy.validationPriorityRequired": "", + "components.Settings.Notifications.NotificationsPushover.embedPoster": "", + "components.Settings.Notifications.embedPoster": "", + "components.Settings.Notifications.messageThreadIdTip": "", + "components.Settings.Notifications.pgpPrivateKeyTip": "", + "components.Settings.Notifications.senderName": "", + "components.Settings.Notifications.telegramsettingsfailed": "", + "components.Settings.Notifications.telegramsettingssaved": "", + "components.Settings.Notifications.toastDiscordTestFailed": "", + "components.Settings.SettingsJobsCache.dnscacheactiveaddress": "", + "components.Settings.SettingsLogs.logDetails": "", + "components.Settings.SonarrModal.notagoptions": "", + "components.Settings.failedToSaveMetadataSettings": "", + "components.TvDetails.network": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmail": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadId": "", + "components.Settings.Notifications.NotificationsWebPush.embedPoster": "", + "components.Settings.Notifications.NotificationsWebhook.customHeaders": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersAdd": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersAuthConflict": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersIncomplete": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersKey": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersRemove": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersTip": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersValue": "", + "components.Settings.Notifications.NotificationsWebhook.supportVariablesTip": "", + "components.Settings.Notifications.NotificationsWebhook.webhookUrlTip": "", + "components.Settings.Notifications.useUserLocale": "", + "components.Settings.RadarrModal.add": "", + "components.Settings.RadarrModal.validationBaseUrlLeadingSlash": "", + "components.Settings.SettingsLogs.filterError": "", + "components.Settings.SonarrModal.create4ksonarr": "", + "components.Settings.activeProfile": "", + "components.Settings.seriesMetadataProvider": "", + "components.Settings.serverLocal": "", + "components.UserList.create": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexuser": "", + "i18n.view": "", + "components.Settings.SettingsNetwork.forceIpv4FirstTip": "", + "components.UserList.descending": "", + "components.Settings.OverrideRuleModal.keywords": "", + "components.Settings.SettingsMain.streamingRegionTip": "", + "components.Settings.SettingsNetwork.toastSettingsSuccess": "", + "components.Settings.SonarrModal.tags": "", + "components.Setup.signin": "", + "components.TvDetails.rtcriticsscore": "", + "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegionTip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.owner": "", + "components.TvDetails.anime": "", + "components.RequestCard.failedmodify": "", + "components.RequestList.RequestItem.failedmodify": "", + "components.Settings.Notifications.NotificationsNtfy.ntfysettingsfailed": "", + "components.Settings.Notifications.NotificationsNtfy.ntfysettingssaved": "", + "components.Settings.Notifications.messageThreadId": "", + "components.Settings.Notifications.smtpPort": "", + "components.Settings.RadarrModal.createradarr": "", + "components.Settings.SonarrModal.monitorNewItems": "", + "components.Settings.jellyfinsettings": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.username": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordsettingssaved": "", + "components.Settings.Notifications.usePublicLogo": "", + "components.Settings.Notifications.usePublicLogoTip": "", + "components.Settings.Notifications.validationWebhookRoleId": "", + "components.Settings.Notifications.webhookRoleId": "", + "components.Settings.Notifications.webhookRoleIdTip": "", + "components.Settings.Notifications.webhookUrlTip": "", + "components.Settings.OverrideRuleModal.conditions": "", + "components.Settings.OverrideRuleModal.conditionsDescription": "", + "components.Settings.OverrideRuleModal.create": "", + "components.Settings.OverrideRuleModal.editrule": "", + "components.Settings.OverrideRuleModal.genres": "", + "components.Settings.OverrideRuleModal.ruleUpdated": "", + "components.Settings.OverrideRuleModal.selectQualityProfile": "", + "components.Settings.OverrideRuleModal.selectRootFolder": "", + "components.Settings.OverrideRuleModal.selectService": "", + "components.Settings.OverrideRuleModal.selecttags": "", + "components.Settings.OverrideRuleModal.service": "", + "components.Settings.OverrideRuleModal.serviceDescription": "", + "components.Settings.OverrideRuleModal.settingsDescription": "", + "components.Settings.OverrideRuleModal.tags": "", + "components.Settings.OverrideRuleModal.users": "", + "components.Settings.OverrideRuleTile.conditions": "", + "components.Settings.OverrideRuleTile.genre": "", + "components.Settings.OverrideRuleTile.keywords": "", + "components.Settings.OverrideRuleTile.language": "", + "components.Settings.OverrideRuleTile.qualityprofile": "", + "components.Settings.OverrideRuleTile.rootfolder": "", + "components.Settings.OverrideRuleTile.settings": "", + "components.Settings.OverrideRuleTile.tags": "", + "components.Settings.OverrideRuleTile.users": "", + "components.Settings.RadarrModal.announced": "", + "components.Settings.RadarrModal.apiKey": "", + "components.Settings.RadarrModal.apiKeyHelp": "", + "components.Settings.RadarrModal.baseUrl": "", + "components.Settings.RadarrModal.baseUrlHelp": "", + "components.Settings.RadarrModal.create4kradarr": "", + "components.Settings.RadarrModal.enableSearch": "", + "components.Settings.RadarrModal.enableSearchHelp": "", + "components.Settings.RadarrModal.externalUrl": "", + "components.Settings.RadarrModal.externalUrlHelp": "", + "components.Settings.RadarrModal.loadingTags": "", + "components.Settings.RadarrModal.loadingprofiles": "", + "components.Settings.RadarrModal.loadingrootfolders": "", + "components.Settings.RadarrModal.minimumAvailability": "", + "components.Settings.RadarrModal.notagoptions": "", + "components.Settings.RadarrModal.qualityprofile": "", + "components.Settings.RadarrModal.released": "", + "components.Settings.RadarrModal.rootfolder": "", + "components.Settings.RadarrModal.selectMinimumAvailability": "", + "components.Settings.RadarrModal.selectQualityProfile": "", + "components.Settings.RadarrModal.selectRootFolder": "", + "components.Settings.RadarrModal.selecttags": "", + "components.Settings.RadarrModal.server4k": "", + "components.Settings.RadarrModal.server4kHelp": "", + "components.Settings.RadarrModal.syncEnabled": "", + "components.Settings.RadarrModal.syncEnabledHelp": "", + "components.Settings.RadarrModal.tagRequests": "", + "components.Settings.RadarrModal.tagRequestsInfo": "", + "components.Settings.RadarrModal.tags": "", + "components.Settings.RadarrModal.testFirstQualityProfiles": "", + "components.Settings.RadarrModal.testFirstRootFolders": "", + "components.Settings.RadarrModal.toastRadarrTestFailure": "", + "components.Settings.RadarrModal.toastRadarrTestSuccess": "", + "components.Settings.RadarrModal.validationApiKeyRequired": "", + "components.Settings.RadarrModal.validationApplicationUrl": "", + "components.Settings.RadarrModal.validationApplicationUrlTrailingSlash": "", + "components.Settings.RadarrModal.validationBaseUrlTrailingSlash": "", + "components.Settings.RadarrModal.validationMinimumAvailabilityRequired": "", + "components.Settings.RadarrModal.validationNameRequired": "", + "components.Settings.RadarrModal.validationPortRequired": "", + "components.Settings.RadarrModal.validationProfileRequired": "", + "components.Settings.RadarrModal.validationRootFolderRequired": "", + "components.Settings.SettingsAbout.Releases.currentversion": "", + "components.Settings.SettingsAbout.Releases.latestversion": "", + "components.Settings.SettingsAbout.Releases.releasedataMissing": "", + "components.Settings.SettingsAbout.Releases.releases": "", + "components.Settings.SettingsAbout.Releases.versionChangelog": "", + "components.Settings.SettingsAbout.Releases.viewchangelog": "", + "components.Settings.SettingsAbout.Releases.viewongithub": "", + "components.Settings.SettingsAbout.about": "", + "components.Settings.SettingsAbout.aboutseerr": "", + "components.Settings.SettingsAbout.appDataPath": "", + "components.Settings.SettingsAbout.documentation": "", + "components.Settings.SettingsAbout.githubdiscussions": "", + "components.Settings.SettingsAbout.outofdate": "", + "components.Settings.SettingsAbout.runningDevelop": "", + "components.Settings.SettingsAbout.supportseerr": "", + "components.Settings.SettingsAbout.timezone": "", + "components.Settings.SettingsAbout.totalmedia": "", + "components.Settings.SettingsAbout.totalrequests": "", + "components.Settings.SettingsAbout.uptodate": "", + "components.Settings.SettingsAbout.version": "", + "components.Settings.SettingsJobsCache.availability-sync": "", + "components.Settings.SettingsJobsCache.cache": "", + "components.Settings.SettingsJobsCache.cachemisses": "", + "components.Settings.SettingsJobsCache.cachename": "", + "components.Settings.SettingsJobsCache.cachevsize": "", + "components.Settings.SettingsJobsCache.canceljob": "", + "components.Settings.SettingsJobsCache.command": "", + "components.Settings.SettingsJobsCache.dnsCache": "", + "components.Settings.SettingsJobsCache.dnsCacheGlobalStatsDescription": "", + "components.Settings.SettingsJobsCache.dnsNoCacheEntries": "", + "components.Settings.SettingsJobsCache.dnscacheflushed": "", + "components.Settings.SettingsJobsCache.dnscachehits": "", + "components.Settings.SettingsJobsCache.dnscachemisses": "", + "components.Settings.SettingsJobsCache.dnscachename": "", + "components.Settings.SettingsJobsCache.download-sync": "", + "components.Settings.SettingsJobsCache.editJobSchedule": "", + "components.Settings.SettingsJobsCache.editJobScheduleCurrent": "", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorDays": "", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorHours": "", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorMinutes": "", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorSeconds": "", + "components.Settings.SettingsJobsCache.failures": "", + "components.Settings.SettingsJobsCache.flushcache": "", + "components.Settings.SettingsJobsCache.flushdnscache": "", + "components.Settings.SettingsJobsCache.image-cache-cleanup": "", + "components.Settings.SettingsJobsCache.imagecache": "", + "components.Settings.SettingsJobsCache.imagecacheDescription": "", + "components.Settings.SettingsJobsCache.imagecachecount": "", + "components.Settings.SettingsJobsCache.imagecachesize": "", + "components.Settings.SettingsJobsCache.ipv4Fallbacks": "", + "components.Settings.SettingsJobsCache.jellyfin-full-scan": "", + "components.Settings.SettingsJobsCache.jellyfin-recently-added-scan": "", + "components.Settings.SettingsJobsCache.jobcancelled": "", + "components.Settings.SettingsJobsCache.jobname": "", + "components.Settings.SettingsJobsCache.jobs": "", + "components.Settings.SettingsJobsCache.jobsDescription": "", + "components.Settings.SettingsJobsCache.jobsandcache": "", + "components.Settings.SettingsJobsCache.jobstarted": "", + "components.Settings.SettingsJobsCache.jobtype": "", + "components.Settings.SettingsJobsCache.misses": "", + "components.Settings.SettingsJobsCache.nextexecution": "", + "components.Settings.SettingsJobsCache.plex-full-scan": "", + "components.Settings.SettingsJobsCache.plex-recently-added-scan": "", + "components.Settings.SettingsJobsCache.plex-refresh-token": "", + "components.Settings.SettingsJobsCache.plex-watchlist-sync": "", + "components.Settings.SettingsJobsCache.process": "", + "components.Settings.SettingsJobsCache.process-blocklisted-tags": "", + "components.Settings.SettingsJobsCache.unknownJob": "", + "components.Settings.SettingsLogs.copiedLogMessage": "", + "components.Settings.SettingsLogs.copyToClipboard": "", + "components.Settings.SettingsLogs.extraData": "", + "components.Settings.SettingsLogs.filterDebug": "", + "components.Settings.SettingsLogs.filterInfo": "", + "components.Settings.SettingsLogs.label": "", + "components.Settings.SettingsLogs.level": "", + "components.Settings.SettingsLogs.logs": "", + "components.Settings.SettingsLogs.logsDescription": "", + "components.Settings.SettingsLogs.message": "", + "components.Settings.SettingsLogs.showall": "", + "components.Settings.SettingsLogs.time": "", + "components.Settings.SettingsMain.apikeyCopied": "", + "components.Settings.SettingsMain.applicationurl": "", + "components.Settings.SettingsMain.blocklistLanguage": "", + "components.Settings.SettingsMain.blocklistedTags": "", + "components.Settings.SettingsMain.blocklistedTagsLimitTip": "", + "components.Settings.SettingsMain.blocklistedTagsTip": "", + "components.Settings.SettingsMain.cacheImages": "", + "components.Settings.SettingsMain.cacheImagesTip": "", + "components.Settings.SettingsMain.discoverRegion": "", + "components.Settings.SettingsMain.discoverRegionTip": "", + "components.Settings.SettingsMain.enableSpecialEpisodes": "", + "components.Settings.SettingsMain.general": "", + "components.Settings.SettingsMain.generalsettingsDescription": "", + "components.Settings.SettingsMain.hideAvailable": "", + "components.Settings.SettingsMain.hideAvailableTip": "", + "components.Settings.SettingsMain.hideBlocklisted": "", + "components.Settings.SettingsMain.hideBlocklistedTip": "", + "components.Settings.SettingsMain.locale": "", + "components.Settings.SettingsMain.originallanguage": "", + "components.Settings.SettingsMain.originallanguageTip": "", + "components.Settings.SettingsMain.partialRequestsEnabled": "", + "components.Settings.SettingsMain.toastApiKeyFailure": "", + "components.Settings.SettingsMain.toastApiKeySuccess": "", + "components.Settings.SettingsMain.toastSettingsSuccess": "", + "components.Settings.SettingsMain.validationApplicationTitle": "", + "components.Settings.SettingsMain.validationApplicationUrl": "", + "components.Settings.SettingsMain.validationUrl": "", + "components.Settings.SettingsNetwork.dnsCacheForceMaxTtl": "", + "components.Settings.SettingsNetwork.dnsCacheForceMinTtl": "", + "components.Settings.SettingsNetwork.dnsCacheHoverTip": "", + "components.Settings.SettingsNetwork.dnsCacheTip": "", + "components.Settings.SettingsNetwork.networksettingsDescription": "", + "components.Settings.SettingsNetwork.proxyBypassFilter": "", + "components.Settings.SettingsNetwork.proxyBypassFilterTip": "", + "components.Settings.SettingsNetwork.proxyBypassLocalAddresses": "", + "components.Settings.SettingsNetwork.proxyEnabled": "", + "components.Settings.SettingsNetwork.toastSettingsFailure": "", + "components.Settings.SettingsNetwork.trustProxy": "", + "components.Settings.SettingsNetwork.trustProxyTip": "", + "components.Settings.SettingsNetwork.validationApiRequestTimeout": "", + "components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "", + "components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "", + "components.Settings.SettingsNetwork.validationProxyPort": "", + "components.Settings.SettingsUsers.atLeastOneAuth": "", + "components.Settings.SettingsUsers.defaultPermissions": "", + "components.Settings.SettingsUsers.defaultPermissionsTip": "", + "components.Settings.SettingsUsers.disabledMediaServerLoginWarning": "", + "components.Settings.SettingsUsers.localLogin": "", + "components.Settings.SettingsUsers.localLoginTip": "", + "components.Settings.SettingsUsers.loginMethods": "", + "components.Settings.SettingsUsers.loginMethodsTip": "", + "components.Settings.SettingsUsers.mediaServerLogin": "", + "components.Settings.SettingsUsers.mediaServerLoginTip": "", + "components.Settings.SettingsUsers.movieRequestLimitLabel": "", + "components.Settings.SettingsUsers.toastSettingsFailure": "", + "components.Settings.SettingsUsers.toastSettingsSuccess": "", + "components.Settings.SettingsUsers.tvRequestLimitLabel": "", + "components.Settings.SettingsUsers.userSettings": "", + "components.Settings.SettingsUsers.userSettingsDescription": "", + "components.Settings.SonarrModal.animeSeriesType": "", + "components.Settings.SonarrModal.animeTags": "", + "components.Settings.SonarrModal.animelanguageprofile": "", + "components.Settings.SonarrModal.animequalityprofile": "", + "components.Settings.SonarrModal.animerootfolder": "", + "components.Settings.SonarrModal.apiKey": "", + "components.Settings.SonarrModal.apiKeyHelp": "", + "components.Settings.SonarrModal.baseUrl": "", + "components.Settings.SonarrModal.baseUrlHelp": "", + "components.Settings.SonarrModal.createsonarr": "", + "components.Settings.SonarrModal.default4kserver": "", + "components.Settings.SonarrModal.editsonarr": "", + "components.Settings.SonarrModal.enableSearch": "", + "components.Settings.SonarrModal.languageprofile": "", + "components.Settings.SonarrModal.loadingTags": "", + "components.Settings.SonarrModal.loadinglanguageprofiles": "", + "components.Settings.SonarrModal.loadingprofiles": "", + "components.Settings.SonarrModal.loadingrootfolders": "", + "components.Settings.SonarrModal.monitorNewItemsHelp": "", + "components.Settings.SonarrModal.qualityprofile": "", + "components.Settings.SonarrModal.rootfolder": "", + "components.Settings.SonarrModal.selectLanguageProfile": "", + "components.Settings.SonarrModal.selectQualityProfile": "", + "components.Settings.SonarrModal.selectRootFolder": "", + "components.Settings.SonarrModal.selecttags": "", + "components.Settings.SonarrModal.server4kHelp": "", + "components.Settings.SonarrModal.syncEnabledHelp": "", + "components.Settings.SonarrModal.tagRequests": "", + "components.Settings.SonarrModal.tagRequestsInfo": "", + "components.Settings.SonarrModal.testFirstLanguageProfiles": "", + "components.Settings.SonarrModal.testFirstQualityProfiles": "", + "components.Settings.SonarrModal.testFirstRootFolders": "", + "components.Settings.SonarrModal.testFirstTags": "", + "components.Settings.SonarrModal.toastSonarrTestFailure": "", + "components.Settings.SonarrModal.toastSonarrTestSuccess": "", + "components.Settings.SonarrModal.validationApiKeyRequired": "", + "components.Settings.SonarrModal.validationApplicationUrl": "", + "components.Settings.SonarrModal.validationBaseUrlLeadingSlash": "", + "components.Settings.SonarrModal.validationBaseUrlTrailingSlash": "", + "components.Settings.SonarrModal.validationLanguageProfileRequired": "", + "components.Settings.SonarrModal.validationNameRequired": "", + "components.Settings.SonarrModal.validationPortRequired": "", + "components.Settings.SonarrModal.validationProfileRequired": "", + "components.Settings.SonarrModal.validationRootFolderRequired": "", + "components.Settings.addradarr": "", + "components.Settings.address": "", + "components.Settings.addrule": "", + "components.Settings.addsonarr": "", + "components.Settings.advancedTooltip": "", + "components.Settings.allChosenProvidersAreOperational": "", + "components.Settings.animeMetadataProvider": "", + "components.Settings.apiKey": "", + "components.Settings.blocklistedTagImportInstructions": "", + "components.Settings.blocklistedTagImportTitle": "", + "components.Settings.blocklistedTagsText": "", + "components.Settings.chooseProvider": "", + "components.Settings.clearBlocklistedTagsConfirm": "", + "components.Settings.clickTest": "", + "components.Settings.connectionTestFailed": "", + "components.Settings.copyBlocklistedTags": "", + "components.Settings.copyBlocklistedTagsEmpty": "", + "components.Settings.copyBlocklistedTagsTip": "", + "components.Settings.currentlibrary": "", + "components.Settings.default": "", + "components.Settings.default4k": "", + "components.Settings.deleteServer": "", + "components.Settings.deleteserverconfirm": "", + "components.Settings.externalUrl": "", + "components.Settings.failed": "", + "components.Settings.general": "", + "components.Settings.importBlocklistedTagsTip": "", + "components.Settings.invalidKeyword": "", + "components.Settings.invalidurlerror": "", + "components.Settings.is4k": "", + "components.Settings.jellyfinForgotPasswordUrl": "", + "components.Settings.jellyfinSettings": "", + "components.Settings.jellyfinSettingsDescription": "", + "components.Settings.jellyfinSettingsFailure": "", + "components.Settings.jellyfinSettingsSuccess": "", + "components.Settings.jellyfinSyncFailedAutomaticGroupedFolders": "", + "components.Settings.jellyfinSyncFailedGenericError": "", + "components.Settings.jellyfinSyncFailedNoLibrariesFound": "", + "components.Settings.jellyfinlibraries": "", + "components.Settings.jellyfinlibrariesDescription": "", + "components.Settings.jellyfinsettingsDescription": "", + "components.Settings.librariesRemaining": "", + "components.Settings.manualscan": "", + "components.Settings.manualscanDescription": "", + "components.Settings.manualscanDescriptionJellyfin": "", + "components.Settings.manualscanJellyfin": "", + "components.Settings.mediaTypeMovie": "", + "components.Settings.mediaTypeSeries": "", + "components.Settings.menuAbout": "", + "components.Settings.menuGeneralSettings": "", + "components.Settings.menuJellyfinSettings": "", + "components.Settings.menuJobs": "", + "components.Settings.menuLogs": "", + "components.Settings.menuMetadataProviders": "", + "components.Settings.menuNetwork": "", + "components.Settings.menuNotifications": "", + "components.Settings.menuPlexSettings": "", + "components.Settings.menuServices": "", + "components.Settings.menuUsers": "", + "components.Settings.metadataProviderSelection": "", + "components.Settings.metadataProviderSettings": "", + "components.Settings.metadataSettingsSaved": "", + "components.Settings.no": "", + "components.Settings.noDefault4kServer": "", + "components.Settings.noSpecialCharacters": "", + "components.Settings.nooptions": "", + "components.Settings.notTested": "", + "components.Settings.notificationAgentSettingsDescription": "", + "components.Settings.notifications": "", + "components.Settings.notificationsettings": "", + "components.Settings.notrunning": "", + "components.Settings.operational": "", + "components.Settings.overrideRules": "", + "components.Settings.overrideRulesDescription": "", + "components.Settings.plexsettingsDescription": "", + "components.Settings.port": "", + "components.Settings.radarrsettings": "", + "components.Settings.restartrequiredTooltip": "", + "components.Settings.save": "", + "components.Settings.saving": "", + "components.Settings.scan": "", + "components.Settings.scanbackground": "", + "components.Settings.scanning": "", + "components.Settings.searchKeywords": "", + "components.Settings.serverRemote": "", + "components.Settings.serverSecure": "", + "components.Settings.serverpreset": "", + "components.Settings.serverpresetLoad": "", + "components.Settings.serverpresetManualMessage": "", + "components.Settings.serverpresetRefreshing": "", + "components.Settings.serviceSettingsDescription": "", + "components.Settings.services": "", + "components.Settings.settingUpPlexDescription": "", + "components.Settings.sonarrsettings": "", + "components.Settings.ssl": "", + "components.Settings.startscan": "", + "components.Settings.starttyping": "", + "components.Settings.syncJellyfin": "", + "components.Settings.syncing": "", + "components.Settings.tautulliApiKey": "", + "components.Settings.tautulliSettings": "", + "components.Settings.tautulliSettingsDescription": "", + "components.Settings.timeout": "", + "components.Settings.tip": "", + "components.Settings.tmdbProviderDoesnotWork": "", + "components.Settings.toastPlexConnecting": "", + "components.Settings.toastPlexConnectingFailure": "", + "components.Settings.toastPlexConnectingSuccess": "", + "components.Settings.toastPlexRefresh": "", + "components.Settings.toastPlexRefreshFailure": "", + "components.Settings.toastPlexRefreshSuccess": "", + "components.Settings.toastTautulliSettingsFailure": "", + "components.Settings.toastTautulliSettingsSuccess": "", + "components.Settings.tvdbProviderDoesnotWork": "", + "components.Settings.urlBase": "", + "components.Settings.validationApiKey": "", + "components.Settings.validationPortRequired": "", + "components.Settings.validationUrl": "", + "components.Settings.validationUrlBaseLeadingSlash": "", + "components.Settings.validationUrlBaseTrailingSlash": "", + "components.Settings.validationUrlTrailingSlash": "", + "components.Settings.valueRequired": "", + "components.Settings.webAppUrl": "", + "components.Settings.webpush": "", + "components.Settings.yes": "", + "components.Setup.back": "", + "components.Setup.configemby": "", + "components.Setup.configjellyfin": "", + "components.Setup.configplex": "", + "components.Setup.configuremediaserver": "", + "components.Setup.configureservices": "", + "components.Setup.continue": "", + "components.Setup.finish": "", + "components.Setup.finishing": "", + "components.Setup.librarieserror": "", + "components.Setup.servertype": "", + "components.Setup.setup": "", + "components.Setup.signinMessage": "", + "components.Setup.signinWithEmby": "", + "components.Setup.signinWithJellyfin": "", + "components.Setup.signinWithPlex": "", + "components.Setup.subtitle": "", + "components.Setup.welcome": "", + "components.StatusBadge.managemedia": "", + "components.StatusBadge.openinarr": "", + "components.StatusBadge.playonplex": "", + "components.StatusBadge.seasonepisodenumber": "", + "components.StatusBadge.seasonnumber": "", + "components.StatusBadge.status": "", + "components.StatusBadge.status4k": "", + "components.StatusChecker.appUpdated": "", + "components.StatusChecker.appUpdatedDescription": "", + "components.StatusChecker.restartRequiredDescription": "", + "components.TitleCard.addToWatchList": "", + "components.TitleCard.cleardata": "", + "components.TitleCard.mediaerror": "", + "components.TitleCard.tmdbid": "", + "components.TitleCard.tvdbid": "", + "components.TitleCard.watchlistCancel": "", + "components.TitleCard.watchlistDeleted": "", + "components.TitleCard.watchlistError": "", + "components.TitleCard.watchlistSuccess": "", + "components.TvDetails.Season.noepisodes": "", + "components.TvDetails.Season.somethingwentwrong": "", + "components.TvDetails.TvCrew.fullseriescrew": "", + "components.TvDetails.addtowatchlist": "", + "components.TvDetails.cast": "", + "components.TvDetails.episodeCount": "", + "components.TvDetails.episodeRuntime": "", + "components.TvDetails.episodeRuntimeMinutes": "", + "components.TvDetails.firstAirDate": "", + "components.TvDetails.manageseries": "", + "components.TvDetails.nextAirDate": "", + "components.TvDetails.originallanguage": "", + "components.TvDetails.originaltitle": "", + "components.TvDetails.overview": "", + "components.TvDetails.overviewunavailable": "", + "components.TvDetails.play": "", + "components.TvDetails.play4k": "", + "components.TvDetails.productioncountries": "", + "components.TvDetails.recommendations": "", + "components.TvDetails.removefromwatchlist": "", + "components.TvDetails.reportissue": "", + "components.TvDetails.rtaudiencescore": "", + "components.TvDetails.seasonnumber": "", + "components.TvDetails.seasons": "", + "components.TvDetails.seasonstitle": "", + "components.TvDetails.streamingproviders": "", + "components.TvDetails.tmdbuserscore": "", + "components.TvDetails.viewfullcrew": "", + "components.TvDetails.watchlistDeleted": "", + "components.TvDetails.watchlistError": "", + "components.TvDetails.watchlistSuccess": "", + "components.TvDetails.watchtrailer": "", + "components.UserList.ascending": "", + "components.UserList.autogeneratepassword": "", + "components.UserList.autogeneratepasswordTip": "", + "components.UserList.created": "", + "components.UserList.createlocaluser": "", + "components.UserList.creating": "", + "components.UserList.deleteconfirm": "", + "components.UserList.edituser": "", + "components.UserList.email": "", + "components.UserList.importedPlexUsersNoPassword": "", + "components.UserList.importedUsersNoPassword": "", + "components.UserList.importedfromJellyfin": "", + "components.UserList.importedfromplex": "", + "components.UserList.importfromJellyfin": "", + "components.UserList.importfromJellyfinerror": "", + "components.UserList.importfrommediaserver": "", + "components.UserList.importfromplex": "", + "components.UserList.localLoginDisabled": "", + "components.UserList.localuser": "", + "components.UserList.mediaServerUser": "", + "components.UserList.newJellyfinsigninenabled": "", + "components.UserList.newplexsigninenabled": "", + "components.UserList.noJellyfinuserstoimport": "", + "components.UserList.nouserstoimport": "", + "components.UserList.owner": "", + "components.UserList.password": "", + "components.UserList.passwordinfodescription": "", + "components.UserList.plexuser": "", + "components.UserList.role": "", + "components.UserList.sortBy": "", + "components.UserList.sortByJoined": "", + "components.UserList.sortByRequests": "", + "components.UserList.sortByRole": "", + "components.UserList.sortByType": "", + "components.UserList.sortByUser": "", + "components.UserList.toggleSortDirection": "", + "components.UserList.toggleSortDirectionAria": "", + "components.UserList.totalrequests": "", + "components.UserList.user": "", + "components.UserList.usercreatedfailed": "", + "components.UserList.usercreatedfailedexisting": "", + "components.UserList.usercreatedsuccess": "", + "components.UserList.userdeleted": "", + "components.UserList.userdeleteerror": "", + "components.UserList.userfail": "", + "components.UserList.userlist": "", + "components.UserList.username": "", + "components.UserList.users": "", + "components.UserList.userssaved": "", + "components.UserList.validationEmail": "", + "components.UserList.validationUsername": "", + "components.UserProfile.ProfileHeader.joindate": "", + "components.UserProfile.ProfileHeader.profile": "", + "components.UserProfile.ProfileHeader.settings": "", + "components.UserProfile.ProfileHeader.userid": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.description": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorExists": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorUnauthorized": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorUnknown": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.password": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.passwordRequired": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.save": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.saving": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.title": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.usernameRequired": "", + "components.UserProfile.UserSettings.UserGeneralSettings.accounttype": "", + "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegion": "", + "components.UserProfile.UserSettings.UserGeneralSettings.displayName": "", + "components.UserProfile.UserSettings.UserGeneralSettings.enableOverride": "", + "components.UserProfile.UserSettings.UserGeneralSettings.originallanguageTip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmovies": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmoviestip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseries": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseriestip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.region": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.linkedAccountsHint": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.noLinkedAccounts": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.noPermissionDescription": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorExists": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordId": "", + "components.UserProfile.UserSettings.UserNotificationSettings.deviceDefault": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.email": "", + "components.UserProfile.UserSettings.UserNotificationSettings.emailsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.emailsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.notifications": "", + "components.UserProfile.UserSettings.UserNotificationSettings.notificationsettings": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pgpPublicKey": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pgpPublicKeyTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessToken": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessTokenTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverApplicationToken": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverApplicationTokenTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverUserKey": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverUserKeyTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoversettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoversettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.sendSilentlyDescription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.sound": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramChatId": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramChatIdTipLong": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadIdTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPgpPublicKey": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPushbulletAccessToken": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverApplicationToken": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverUserKey": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramMessageThreadId": "", + "components.UserProfile.UserSettings.UserPasswordChange.confirmpassword": "", + "components.UserProfile.UserSettings.UserPasswordChange.localPasswordDescription": "", + "components.UserProfile.UserSettings.UserPasswordChange.noPasswordSet": "", + "components.UserProfile.UserSettings.UserPasswordChange.noPasswordSetOwnAccount": "", + "components.UserProfile.UserSettings.UserPasswordChange.nopermissionDescription": "", + "components.UserProfile.UserSettings.UserPasswordChange.password": "", + "components.UserProfile.UserSettings.UserPasswordChange.toastSettingsFailure": "", + "components.UserProfile.UserSettings.UserPasswordChange.toastSettingsFailureVerifyCurrent": "", + "components.UserProfile.UserSettings.UserPasswordChange.toastSettingsSuccess": "", + "components.UserProfile.UserSettings.UserPasswordChange.validationConfirmPassword": "", + "components.UserProfile.UserSettings.UserPasswordChange.validationCurrentPassword": "", + "components.UserProfile.UserSettings.UserPasswordChange.validationNewPassword": "", + "components.UserProfile.UserSettings.UserPasswordChange.validationNewPasswordLength": "", + "components.UserProfile.UserSettings.UserPermissions.permissions": "", + "components.UserProfile.UserSettings.UserPermissions.toastSettingsFailure": "", + "components.UserProfile.UserSettings.UserPermissions.toastSettingsSuccess": "", + "components.UserProfile.UserSettings.UserPermissions.unauthorizedDescription": "", + "components.UserProfile.UserSettings.menuChangePass": "", + "components.UserProfile.UserSettings.menuGeneralSettings": "", + "components.UserProfile.UserSettings.menuPermissions": "", + "components.UserProfile.UserSettings.unauthorizedDescription": "", + "components.UserProfile.emptywatchlist": "", + "components.UserProfile.limit": "", + "components.UserProfile.localWatchlist": "", + "components.UserProfile.movierequests": "", + "components.UserProfile.pastdays": "", + "components.UserProfile.plexwatchlist": "", + "components.UserProfile.recentlywatched": "", + "components.UserProfile.recentrequests": "", + "components.UserProfile.requestsperdays": "", + "components.UserProfile.unlimited": "", + "i18n.addToBlocklist": "", + "i18n.advanced": "", + "i18n.all": "", + "i18n.approve": "", + "i18n.available": "", + "i18n.back": "", + "i18n.blocklist": "", + "i18n.blocklistDuplicateError": "", + "i18n.blocklistError": "", + "i18n.blocklistSuccess": "", + "i18n.blocklisted": "", + "i18n.canceling": "", + "i18n.completed": "", + "i18n.decline": "", + "i18n.declined": "", + "i18n.deleted": "", + "i18n.deleting": "", + "i18n.delimitedlist": "", + "i18n.edit": "", + "i18n.error": "", + "i18n.experimental": "", + "i18n.failed": "", + "i18n.import": "", + "i18n.importing": "", + "i18n.loading": "", + "i18n.movie": "", + "i18n.movies": "", + "i18n.next": "", + "i18n.notificationLocale": "", + "i18n.partiallyblocklisted": "", + "i18n.removeFromBlocklistSuccess": "", + "i18n.removefromBlocklist": "", + "components.RequestList.unableToConnect": "", + "components.Selector.CertificationSelector.errorLoading": "", + "components.Selector.CertificationSelector.noOptions": "", + "components.Selector.CertificationSelector.selectCertification": "", + "components.Selector.CertificationSelector.selectCountry": "", + "components.Selector.CertificationSelector.starttyping": "", + "components.Settings.Notifications.NotificationsNtfy.tokenAuth": "", + "components.Settings.Notifications.NotificationsNtfy.topic": "", + "components.Settings.Notifications.NotificationsNtfy.url": "", + "components.Settings.Notifications.NotificationsNtfy.username": "", + "components.Settings.Notifications.NotificationsNtfy.usernamePasswordAuth": "", + "components.Settings.Notifications.NotificationsNtfy.validationNtfyTopic": "", + "components.Settings.Notifications.NotificationsNtfy.validationNtfyUrl": "", + "components.Settings.Notifications.NotificationsNtfy.validationTypes": "", + "components.Settings.Notifications.NotificationsSlack.webhookUrlTip": "", + "components.Settings.Notifications.allowselfsigned": "", + "components.Settings.Notifications.validationMessageThreadId": "", + "components.Settings.OverrideRuleModal.notagoptions": "", + "components.Settings.OverrideRuleModal.qualityprofile": "", + "components.Settings.OverrideRuleModal.rootfolder": "", + "components.Settings.OverrideRuleModal.ruleCreated": "", + "components.Settings.RadarrModal.edit4kradarr": "", + "components.Settings.RadarrModal.editradarr": "", + "components.Settings.SettingsAbout.gettingsupport": "", + "components.Settings.SettingsJobsCache.dnsCacheDescription": "", + "components.Settings.SettingsJobsCache.dnsCacheGlobalStats": "", + "components.Settings.SettingsMain.blocklistedTagsLimit": "", + "components.UserList.validationpasswordminchars": "", + "components.UserProfile.UserSettings.UserGeneralSettings.role": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramChatId": "", + "components.UserProfile.UserSettings.UserNotificationSettings.webpush": "", + "components.UserProfile.UserSettings.menuLinkedAccounts": "", + "components.UserProfile.UserSettings.menuNotifications": "", + "components.Settings.SonarrModal.defaultserver": "", + "components.Settings.SettingsMain.apikey": "", + "components.Settings.SettingsMain.youtubeUrlTip": "", + "components.Settings.SettingsNetwork.csrfProtection": "", + "components.Settings.SettingsNetwork.network": "", + "components.Settings.SonarrModal.edit4ksonarr": "", + "components.Settings.cancelscan": "", + "components.Settings.noDefaultNon4kServer": "", + "components.Settings.noDefaultServer": "", + "components.Settings.plex": "", + "components.Settings.plexlibraries": "", + "components.Settings.plexlibrariesDescription": "", + "components.Settings.webAppUrlTip": "", + "components.TvDetails.TvCast.fullseriescast": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestFailed": "", + "components.Settings.SonarrModal.syncEnabled": "", + "components.UserProfile.UserSettings.UserGeneralSettings.applanguage": "", + "components.Settings.SettingsAbout.contribute": "", + "components.Settings.providerStatus": "", + "i18n.approved": "", + "components.Settings.settings": "", + "i18n.areyousure": "", + "components.Settings.Notifications.smtpHost": "", + "components.Settings.OverrideRuleModal.createrule": "", + "components.Settings.SonarrModal.add": "", + "components.Settings.SettingsJobsCache.editJobSchedulePrompt": "", + "components.Settings.SettingsJobsCache.jobScheduleEditFailed": "", + "components.Settings.SettingsJobsCache.jobScheduleEditSaved": "", + "components.Settings.SettingsJobsCache.radarr-scan": "", + "components.Settings.SettingsJobsCache.runnow": "", + "components.Settings.SettingsJobsCache.size": "", + "components.Settings.SettingsJobsCache.sonarr-scan": "", + "components.Settings.SettingsJobsCache.usersavatars": "", + "components.Settings.SettingsLogs.filterWarn": "", + "components.Settings.SettingsLogs.pauseLogs": "", + "components.Settings.SettingsLogs.resumeLogs": "", + "components.Settings.SettingsLogs.viewdetails": "", + "components.Settings.SettingsMain.applicationTitle": "", + "components.Settings.RadarrModal.port": "", + "components.Settings.Notifications.NotificationsNtfy.priority": "", + "components.TvDetails.showtype": "", + "components.UserProfile.totalrequests": "", + "components.TvDetails.status4k": "", + "components.UserList.admin": "", + "components.UserProfile.seriesrequest": "", + "components.UserProfile.UserSettings.UserGeneralSettings.admin": "", + "components.Settings.SonarrModal.seasonfolders": "", + "components.Discover.RecentRequestsSlider.unableToConnect": "", + "components.Settings.RadarrModal.defaultserver": "", + "components.Settings.RadarrModal.inCinemas": "", + "components.Settings.SettingsJobsCache.download-sync-reset": "", + "components.Settings.SettingsMain.youtubeUrl": "", + "components.Settings.SettingsUsers.users": "", + "components.Settings.email": "", + "components.Settings.enablessl": "", + "components.Settings.experimentalTooltip": "", + "components.Settings.plexsettings": "", + "components.UserList.accounttype": "", + "i18n.cancel": "", + "components.TvDetails.similar": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorUnauthorized": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.deletesubscription": "", + "components.UserProfile.UserSettings.UserPasswordChange.validationConfirmPasswordSame": "", + "i18n.delete": "", + "components.Settings.OverrideRuleModal.settings": "", + "components.Settings.Notifications.NotificationsSlack.embedPoster": "", + "components.StatusChecker.reloadApp": "", + "components.StatusChecker.restartRequired": "", + "components.UserList.bulkedit": "", + "components.UserList.deleteuser": "", + "components.UserProfile.UserSettings.UserPasswordChange.currentpassword": "", + "i18n.close": "", + "i18n.collection": "", + "components.MetadataSelector.selectMetdataProvider": "", + "components.Selector.CertificationSelector.maxRating": "", + "components.Settings.OverrideRuleModal.languages": "", + "components.Settings.RadarrModal.default4kserver": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.managedevices": "", + "components.MetadataSelector.tvdbLabel": "", + "components.Settings.RadarrModal.testFirstTags": "", + "components.Settings.SettingsJobsCache.dnscacheage": "", + "components.Settings.SettingsMain.toastSettingsFailure": "", + "components.Settings.SettingsUsers.newPlexLogin": "", + "components.Settings.SettingsUsers.newPlexLoginTip": "", + "components.Settings.SonarrModal.port": "", + "components.Settings.SonarrModal.servername": "", + "components.Settings.SonarrModal.ssl": "", + "components.Settings.SonarrModal.seriesType": "", + "components.Settings.SonarrModal.server4k": "", + "components.Login.tipEmailHasTrailingWhitespace": "", + "components.Login.tipUsernameHasTrailingWhitespace": "" } diff --git a/src/i18n/locale/hi.json b/src/i18n/locale/hi.json index 8eeee03d2d..a0d01e4feb 100644 --- a/src/i18n/locale/hi.json +++ b/src/i18n/locale/hi.json @@ -139,5 +139,1534 @@ "components.Discover.resetwarning": "सभी स्लाइडर्स को डिफ़ॉल्ट रूप से रीसेट करें। यह किसी भी कस्टम स्लाइडर को भी हटा देगा!", "components.Discover.tmdbtvgenre": "TMDB श्रृंखला शैली", "components.Discover.PlexWatchlistSlider.emptywatchlist": "आपकी Plex वॉचलिस्ट में जोड़ा गया मीडिया यहां दिखाई देगा.", - "components.Discover.discover": "खोज" + "components.Discover.discover": "खोज", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletsettingssaved": "", + "components.Settings.RadarrModal.selectMinimumAvailability": "", + "components.Settings.RadarrModal.selectQualityProfile": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoversettingsfailed": "", + "components.Settings.SettingsNetwork.networksettings": "", + "i18n.loading": "", + "i18n.partiallyblocklisted": "", + "components.Discover.upcoming": "", + "components.IssueList.IssueItem.issuetype": "", + "components.Settings.email": "", + "component.BlocklistBlock.blocklistdate": "", + "component.BlocklistBlock.blocklistedby": "", + "component.BlocklistModal.blocklisting": "", + "components.Blocklist.blocklistNotFoundError": "", + "components.Blocklist.blocklistSettingsDescription": "", + "components.Blocklist.blocklistdate": "", + "components.Blocklist.blocklistedby": "", + "components.Blocklist.blocklistsettings": "", + "components.Blocklist.filterBlocklistedTags": "", + "components.Blocklist.filterManual": "", + "components.Blocklist.mediaName": "", + "components.Blocklist.mediaTmdbId": "", + "components.Blocklist.mediaType": "", + "components.Blocklist.showAllBlocklisted": "", + "components.Layout.Sidebar.blocklist": "", + "components.Layout.Sidebar.browsemovies": "", + "components.Layout.Sidebar.browsetv": "", + "components.Layout.Sidebar.requests": "", + "components.Layout.Sidebar.users": "", + "components.Layout.UserDropdown.MiniQuotaDisplay.movierequests": "", + "components.Layout.UserDropdown.MiniQuotaDisplay.seriesrequests": "", + "components.Login.adminerror": "", + "components.Login.back": "", + "components.Discover.FilterSlideover.excludeKeywords": "", + "components.Discover.upcomingmovies": "", + "components.DownloadBlock.estimatedtime": "", + "components.DownloadBlock.formattedTitle": "", + "components.IssueDetails.IssueComment.areyousuredelete": "", + "components.IssueDetails.IssueComment.delete": "", + "components.IssueDetails.IssueComment.edit": "", + "components.IssueDetails.IssueComment.postedby": "", + "components.IssueDetails.IssueComment.postedbyedited": "", + "components.IssueDetails.IssueComment.validationComment": "", + "components.IssueDetails.IssueDescription.deleteissue": "", + "components.IssueDetails.IssueDescription.description": "", + "components.IssueDetails.IssueDescription.edit": "", + "components.IssueDetails.allepisodes": "", + "components.IssueDetails.allseasons": "", + "components.IssueDetails.closeissue": "", + "components.IssueDetails.closeissueandcomment": "", + "components.IssueDetails.issuetype": "", + "components.IssueDetails.lastupdated": "", + "components.IssueDetails.nocomments": "", + "components.IssueDetails.openedby": "", + "components.IssueDetails.openin4karr": "", + "components.IssueDetails.openinarr": "", + "components.IssueDetails.play4konplex": "", + "components.IssueDetails.playonplex": "", + "components.IssueDetails.problemepisode": "", + "components.IssueDetails.problemseason": "", + "components.IssueDetails.reopenissue": "", + "components.IssueDetails.reopenissueandcomment": "", + "components.IssueDetails.season": "", + "components.IssueDetails.toasteditdescriptionfailed": "", + "components.IssueDetails.toastissuedeleted": "", + "components.IssueDetails.toastissuedeletefailed": "", + "components.IssueDetails.toaststatusupdated": "", + "components.IssueDetails.toaststatusupdatefailed": "", + "components.IssueDetails.unknownissuetype": "", + "components.IssueList.IssueItem.descriptionpreview": "", + "components.IssueList.IssueItem.episodes": "", + "components.IssueList.IssueItem.issuestatus": "", + "components.IssueList.IssueItem.opened": "", + "components.IssueList.IssueItem.openeduserdate": "", + "components.IssueList.IssueItem.problemepisode": "", + "components.IssueList.IssueItem.seasons": "", + "components.IssueList.IssueItem.unknownissuetype": "", + "components.IssueList.IssueItem.viewissue": "", + "components.IssueList.issues": "", + "components.IssueList.showallissues": "", + "components.IssueList.sortAdded": "", + "components.IssueList.sortModified": "", + "components.IssueModal.CreateIssueModal.allepisodes": "", + "components.IssueModal.CreateIssueModal.allseasons": "", + "components.IssueModal.CreateIssueModal.episode": "", + "components.IssueModal.CreateIssueModal.extras": "", + "components.IssueModal.CreateIssueModal.problemepisode": "", + "components.IssueModal.CreateIssueModal.providedetail": "", + "components.IssueModal.CreateIssueModal.reportissue": "", + "components.IssueModal.CreateIssueModal.submitissue": "", + "components.IssueModal.CreateIssueModal.toastFailedCreate": "", + "components.IssueModal.CreateIssueModal.toastSuccessCreate": "", + "components.IssueModal.CreateIssueModal.toastviewissue": "", + "components.IssueModal.CreateIssueModal.validationMessageRequired": "", + "components.Layout.LanguagePicker.displaylanguage": "", + "components.Layout.UserWarnings.passwordRequired": "", + "components.QuotaSelector.seasons": "", + "components.QuotaSelector.unlimited": "", + "components.Settings.Notifications.validationUrl": "", + "components.Settings.urlBase": "", + "components.RequestModal.approve": "", + "components.Discover.resetsuccess": "", + "components.IssueModal.CreateIssueModal.whatswrong": "", + "components.IssueModal.issueSubtitles": "", + "components.IssueModal.issueVideo": "", + "components.LanguageSelector.languageServerDefault": "", + "components.LanguageSelector.originalLanguageDefault": "", + "components.Layout.SearchInput.searchPlaceholder": "", + "components.IssueModal.CreateIssueModal.problemseason": "", + "components.IssueDetails.leavecomment": "", + "components.MovieDetails.markavailable": "", + "components.Discover.FilterSlideover.voteCount": "", + "components.Layout.UserDropdown.settings": "", + "components.Layout.UserDropdown.signout": "", + "components.Layout.UserWarnings.emailInvalid": "", + "components.Discover.FilterSlideover.certification": "", + "components.Layout.UserDropdown.myprofile": "", + "components.IssueDetails.commentplaceholder": "", + "components.IssueDetails.comments": "", + "components.IssueDetails.deleteissue": "", + "components.IssueDetails.deleteissueconfirm": "", + "components.IssueDetails.episode": "", + "components.IssueDetails.issuepagetitle": "", + "components.IssueDetails.toasteditdescriptionsuccess": "", + "components.IssueModal.CreateIssueModal.season": "", + "components.Login.signinheader": "", + "components.Login.signinwithjellyfin": "", + "components.Login.signinwithoverseerr": "", + "components.Login.signinwithplex": "", + "components.Layout.Sidebar.issues": "", + "components.Discover.FilterSlideover.tmdbuservotecount": "", + "components.IssueModal.issueAudio": "", + "components.Layout.Sidebar.dashboard": "", + "components.Layout.Sidebar.settings": "", + "components.Layout.UserDropdown.requests": "", + "components.Layout.UserWarnings.emailRequired": "", + "components.Discover.FilterSlideover.status": "", + "components.Discover.RecentRequestsSlider.unableToConnect": "", + "components.Layout.UserWarnings.profileIncomplete": "", + "components.Layout.VersionStatus.commitsbehind": "", + "components.Layout.VersionStatus.outofdate": "", + "components.Layout.VersionStatus.streamdevelop": "", + "components.Layout.VersionStatus.streamstable": "", + "components.Login.credentialerror": "", + "components.Login.description": "", + "components.Login.email": "", + "components.Login.emailAlreadyTaken": "", + "components.Login.emailtooltip": "", + "components.Login.enablessl": "", + "components.Login.forgotpassword": "", + "components.Login.hostname": "", + "components.Login.initialsignin": "", + "components.Login.invalidurlerror": "", + "components.Login.jellyfinLocalLoginHint": "", + "components.Login.loginerror": "", + "components.Login.loginwithapp": "", + "components.Login.noadminerror": "", + "components.Login.orsigninwith": "", + "components.Login.password": "", + "components.Login.port": "", + "components.Login.save": "", + "components.Login.saveFailed": "", + "components.Login.servertype": "", + "components.Login.signin": "", + "components.Login.signingin": "", + "components.Login.username": "", + "components.Login.validationEmailFormat": "", + "components.Login.validationUrlBaseTrailingSlash": "", + "components.Login.validationUrlTrailingSlash": "", + "components.ManageSlideOver.manageModalTitle": "", + "components.ManageSlideOver.mark4kavailable": "", + "components.ManageSlideOver.markallseasons4kavailable": "", + "components.ManageSlideOver.markallseasonsavailable": "", + "components.ManageSlideOver.markavailable": "", + "components.ManageSlideOver.movie": "", + "components.ManageSlideOver.openarr": "", + "components.ManageSlideOver.openarr4k": "", + "components.ManageSlideOver.opentautulli": "", + "components.ManageSlideOver.pastdays": "", + "components.ManageSlideOver.playedby": "", + "components.ManageSlideOver.removearr": "", + "components.ManageSlideOver.removearr4k": "", + "components.ManageSlideOver.tvshow": "", + "components.MediaSlider.ShowMoreCard.seemore": "", + "components.MetadataSelector.selectMetdataProvider": "", + "components.MetadataSelector.tmdbLabel": "", + "components.MetadataSelector.tvdbLabel": "", + "components.MovieDetails.MovieCast.fullcast": "", + "components.MovieDetails.MovieCrew.fullcrew": "", + "components.MovieDetails.addtowatchlist": "", + "components.MovieDetails.budget": "", + "components.MovieDetails.cast": "", + "components.MovieDetails.digitalrelease": "", + "components.MovieDetails.downloadstatus": "", + "components.MovieDetails.imdbuserscore": "", + "components.MovieDetails.overviewunavailable": "", + "components.MovieDetails.play4k": "", + "components.MovieDetails.removefromwatchlist": "", + "components.MovieDetails.reportissue": "", + "components.MovieDetails.revenue": "", + "components.MovieDetails.rtaudiencescore": "", + "components.MovieDetails.rtcriticsscore": "", + "components.MovieDetails.showless": "", + "components.MovieDetails.similar": "", + "components.MovieDetails.streamingproviders": "", + "components.MovieDetails.watchlistDeleted": "", + "components.MovieDetails.watchlistError": "", + "components.MovieDetails.watchlistSuccess": "", + "components.MovieDetails.watchtrailer": "", + "components.NotificationTypeSelector.adminissuecommentDescription": "", + "components.NotificationTypeSelector.adminissuereopenedDescription": "", + "components.NotificationTypeSelector.adminissueresolvedDescription": "", + "components.NotificationTypeSelector.issuecomment": "", + "components.NotificationTypeSelector.issuereopenedDescription": "", + "components.NotificationTypeSelector.usermediaAutoApprovedDescription": "", + "components.NotificationTypeSelector.usermediaapprovedDescription": "", + "components.NotificationTypeSelector.usermediaavailableDescription": "", + "components.NotificationTypeSelector.usermediadeclinedDescription": "", + "components.NotificationTypeSelector.usermediafailedDescription": "", + "components.NotificationTypeSelector.usermediarequestedDescription": "", + "components.PermissionEdit.admin": "", + "components.PermissionEdit.adminDescription": "", + "components.PermissionEdit.advancedrequest": "", + "components.PermissionEdit.advancedrequestDescription": "", + "components.PermissionEdit.autoapprove": "", + "components.PermissionEdit.autoapprove4k": "", + "components.PermissionEdit.autoapprove4kDescription": "", + "components.PermissionEdit.autoapprove4kMovies": "", + "components.PermissionEdit.autoapproveMoviesDescription": "", + "components.PermissionEdit.autoapproveSeries": "", + "components.PermissionEdit.autoapproveSeriesDescription": "", + "components.PermissionEdit.autorequest": "", + "components.PermissionEdit.autorequestDescription": "", + "components.PermissionEdit.autorequestMovies": "", + "components.PermissionEdit.autorequestMoviesDescription": "", + "components.PermissionEdit.autorequestSeries": "", + "components.PermissionEdit.autorequestSeriesDescription": "", + "components.PermissionEdit.blocklistedItems": "", + "components.PermissionEdit.blocklistedItemsDescription": "", + "components.PermissionEdit.createissues": "", + "components.PermissionEdit.createissuesDescription": "", + "components.PermissionEdit.manageblocklist": "", + "components.PermissionEdit.manageblocklistDescription": "", + "components.PermissionEdit.manageissues": "", + "components.PermissionEdit.manageissuesDescription": "", + "components.PermissionEdit.managerequests": "", + "components.PermissionEdit.managerequestsDescription": "", + "components.PermissionEdit.request": "", + "components.PermissionEdit.request4k": "", + "components.PermissionEdit.request4kDescription": "", + "components.PermissionEdit.request4kMovies": "", + "components.PermissionEdit.request4kMoviesDescription": "", + "components.PermissionEdit.request4kTv": "", + "components.PermissionEdit.request4kTvDescription": "", + "components.PermissionEdit.requestDescription": "", + "components.PermissionEdit.requestMovies": "", + "components.PermissionEdit.requestMoviesDescription": "", + "components.PermissionEdit.requestTv": "", + "components.PermissionEdit.requestTvDescription": "", + "components.PermissionEdit.usersDescription": "", + "components.PermissionEdit.viewblocklistedItems": "", + "components.PermissionEdit.viewblocklistedItemsDescription": "", + "components.PermissionEdit.viewissues": "", + "components.PermissionEdit.viewissuesDescription": "", + "components.PermissionEdit.viewrecent": "", + "components.PermissionEdit.viewrecentDescription": "", + "components.PermissionEdit.viewrequests": "", + "components.PermissionEdit.viewrequestsDescription": "", + "components.PermissionEdit.viewwatchlists": "", + "components.PermissionEdit.viewwatchlistsDescription": "", + "components.PersonDetails.alsoknownas": "", + "components.PersonDetails.appearsin": "", + "components.PersonDetails.ascharacter": "", + "components.PersonDetails.birthdate": "", + "components.PersonDetails.crewmember": "", + "components.PersonDetails.lifespan": "", + "components.QuotaSelector.days": "", + "components.QuotaSelector.movieRequests": "", + "components.QuotaSelector.movies": "", + "components.QuotaSelector.tvRequests": "", + "components.RegionSelector.regionDefault": "", + "components.RegionSelector.regionServerDefault": "", + "components.RequestBlock.approve": "", + "components.RequestBlock.decline": "", + "components.RequestBlock.delete": "", + "components.RequestBlock.edit": "", + "components.RequestBlock.languageprofile": "", + "components.RequestBlock.lastmodifiedby": "", + "components.RequestBlock.profilechanged": "", + "components.RequestBlock.requestdate": "", + "components.RequestBlock.requestedby": "", + "components.RequestBlock.requestoverrides": "", + "components.RequestBlock.rootfolder": "", + "components.RequestBlock.seasons": "", + "components.RequestBlock.server": "", + "components.RequestButton.approve4krequests": "", + "components.RequestButton.approverequest": "", + "components.RequestButton.approverequest4k": "", + "components.RequestButton.approverequests": "", + "components.RequestButton.decline4krequests": "", + "components.RequestButton.declinerequest": "", + "components.RequestButton.declinerequest4k": "", + "components.RequestButton.declinerequests": "", + "components.RequestButton.requestmore4k": "", + "components.RequestButton.viewrequest": "", + "components.RequestButton.viewrequest4k": "", + "components.RequestCard.approverequest": "", + "components.RequestCard.cancelrequest": "", + "components.RequestCard.declinerequest": "", + "components.RequestCard.deleterequest": "", + "components.RequestCard.editrequest": "", + "components.RequestCard.failedretry": "", + "components.RequestCard.mediaerror": "", + "components.RequestCard.seasons": "", + "components.RequestCard.tmdbid": "", + "components.RequestCard.tvdbid": "", + "components.RequestCard.unknowntitle": "", + "components.RequestList.RequestItem.cancelRequest": "", + "components.RequestList.RequestItem.deleterequest": "", + "components.RequestList.RequestItem.editrequest": "", + "components.RequestList.RequestItem.failedmodify": "", + "components.RequestList.RequestItem.failedretry": "", + "components.RequestList.RequestItem.modified": "", + "components.RequestList.RequestItem.modifieduserdate": "", + "components.RequestList.RequestItem.profileName": "", + "components.RequestList.RequestItem.removearr": "", + "components.RequestList.RequestItem.requested": "", + "components.RequestList.RequestItem.seasons": "", + "components.RequestList.RequestItem.tmdbid": "", + "components.RequestList.RequestItem.tvdbid": "", + "components.RequestList.RequestItem.unknowntitle": "", + "components.RequestList.requests": "", + "components.RequestList.showallrequests": "", + "components.RequestList.sortAdded": "", + "components.RequestList.sortDirection": "", + "components.RequestList.sortModified": "", + "components.RequestList.unableToConnect": "", + "components.RequestModal.AdvancedRequester.advancedoptions": "", + "components.RequestModal.AdvancedRequester.animenote": "", + "components.RequestModal.AdvancedRequester.default": "", + "components.RequestModal.AdvancedRequester.destinationserver": "", + "components.RequestModal.AdvancedRequester.qualityprofile": "", + "components.RequestModal.AdvancedRequester.requestas": "", + "components.RequestModal.AdvancedRequester.rootfolder": "", + "components.RequestModal.AdvancedRequester.selecttags": "", + "components.RequestModal.AdvancedRequester.tags": "", + "components.RequestModal.QuotaDisplay.allowedRequests": "", + "components.RequestModal.QuotaDisplay.allowedRequestsUser": "", + "components.RequestModal.QuotaDisplay.movie": "", + "components.RequestModal.QuotaDisplay.movielimit": "", + "components.RequestModal.QuotaDisplay.notenoughseasonrequests": "", + "components.RequestModal.QuotaDisplay.quotaLink": "", + "components.RequestModal.QuotaDisplay.quotaLinkUser": "", + "components.RequestModal.QuotaDisplay.requestsremaining": "", + "components.RequestModal.QuotaDisplay.requiredquota": "", + "components.RequestModal.QuotaDisplay.requiredquotaUser": "", + "components.RequestModal.alreadyrequested": "", + "components.RequestModal.autoapproval": "", + "components.RequestModal.cancel": "", + "components.RequestModal.edit": "", + "components.RequestModal.errorediting": "", + "components.RequestModal.numberofepisodes": "", + "components.RequestModal.pending4krequest": "", + "components.RequestModal.requestApproved": "", + "components.RequestModal.requestSuccess": "", + "components.RequestModal.requestadmin": "", + "components.RequestModal.requestcancelled": "", + "components.RequestModal.requestcollection4ktitle": "", + "components.RequestModal.requestcollectiontitle": "", + "components.RequestModal.requestedited": "", + "components.RequestModal.requesterror": "", + "components.RequestModal.requestfrom": "", + "components.RequestModal.requestmovie4ktitle": "", + "components.RequestModal.requestmovies": "", + "components.RequestModal.requestmovies4k": "", + "components.RequestModal.requestmovietitle": "", + "components.RequestModal.requestseasons": "", + "components.RequestModal.requestseasons4k": "", + "components.RequestModal.requestseries4ktitle": "", + "components.RequestModal.requestseriestitle": "", + "components.RequestModal.season": "", + "components.RequestModal.seasonnumber": "", + "components.RequestModal.selectmovies": "", + "components.RequestModal.selectseason": "", + "components.ResetPassword.confirmpassword": "", + "components.ResetPassword.email": "", + "components.ResetPassword.emailresetlink": "", + "components.ResetPassword.validationemailrequired": "", + "components.ResetPassword.validationpasswordmatch": "", + "components.ResetPassword.validationpasswordminchars": "", + "components.ResetPassword.validationpasswordrequired": "", + "components.Selector.CertificationSelector.maxRating": "", + "components.Selector.CertificationSelector.minRating": "", + "components.Selector.CertificationSelector.noOptions": "", + "components.Selector.CertificationSelector.selectCertification": "", + "components.Selector.CertificationSelector.selectCountry": "", + "components.Selector.CertificationSelector.starttyping": "", + "components.Selector.canceled": "", + "components.Selector.ended": "", + "components.Selector.inProduction": "", + "components.Selector.planned": "", + "components.Selector.returningSeries": "", + "components.Selector.searchGenres": "", + "components.Selector.searchKeywords": "", + "components.Selector.searchStatus": "", + "components.Selector.searchStudios": "", + "components.Selector.searchUsers": "", + "components.Selector.showless": "", + "components.Selector.showmore": "", + "components.Selector.starttyping": "", + "components.Settings.Notifications.NotificationsGotify.agentenabled": "", + "components.Settings.Notifications.NotificationsGotify.gotifysettingsfailed": "", + "components.Settings.Notifications.NotificationsGotify.gotifysettingssaved": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestSuccess": "", + "components.Settings.Notifications.NotificationsNtfy.token": "", + "components.Settings.Notifications.NotificationsNtfy.tokenAuth": "", + "components.Settings.Notifications.NotificationsNtfy.url": "", + "components.Settings.Notifications.NotificationsWebPush.toastWebPushTestSending": "", + "components.Settings.Notifications.NotificationsWebPush.toastWebPushTestSuccess": "", + "components.Settings.Notifications.allowselfsigned": "", + "components.Settings.Notifications.authPass": "", + "components.Settings.Notifications.authUser": "", + "components.Settings.Notifications.botApiTip": "", + "components.Settings.Notifications.botAvatarUrl": "", + "components.Settings.Notifications.botUsername": "", + "components.Settings.Notifications.botUsernameTip": "", + "components.Settings.Notifications.chatIdTip": "", + "components.Settings.Notifications.discordsettingsfailed": "", + "components.Settings.Notifications.encryptionOpportunisticTls": "", + "components.Settings.Notifications.encryptionTip": "", + "components.Settings.Notifications.messageThreadId": "", + "components.Settings.Notifications.messageThreadIdTip": "", + "components.Settings.Notifications.pgpPasswordTip": "", + "components.Settings.Notifications.pgpPrivateKey": "", + "components.Settings.Notifications.pgpPrivateKeyTip": "", + "components.Settings.Notifications.sendSilently": "", + "components.Settings.Notifications.sendSilentlyTip": "", + "components.Settings.Notifications.senderName": "", + "components.Settings.Notifications.smtpHost": "", + "components.Settings.Notifications.smtpPort": "", + "components.Settings.Notifications.telegramsettingsfailed": "", + "components.Settings.Notifications.telegramsettingssaved": "", + "components.Settings.Notifications.toastDiscordTestFailed": "", + "components.Settings.Notifications.toastDiscordTestSending": "", + "components.Settings.Notifications.toastDiscordTestSuccess": "", + "components.Settings.Notifications.toastEmailTestFailed": "", + "components.Settings.Notifications.toastEmailTestSending": "", + "components.Settings.Notifications.toastTelegramTestFailed": "", + "components.Settings.Notifications.toastTelegramTestSending": "", + "components.Settings.Notifications.validationBotAPIRequired": "", + "components.Settings.OverrideRuleModal.serviceDescription": "", + "components.Settings.OverrideRuleModal.settings": "", + "components.Settings.OverrideRuleModal.settingsDescription": "", + "components.Settings.OverrideRuleModal.users": "", + "components.Settings.OverrideRuleTile.conditions": "", + "components.Settings.OverrideRuleTile.keywords": "", + "components.Settings.OverrideRuleTile.language": "", + "components.Settings.OverrideRuleTile.qualityprofile": "", + "components.Settings.OverrideRuleTile.rootfolder": "", + "components.Settings.OverrideRuleTile.settings": "", + "components.Settings.OverrideRuleTile.users": "", + "components.Settings.RadarrModal.loadingTags": "", + "components.Settings.RadarrModal.loadingprofiles": "", + "components.Settings.RadarrModal.loadingrootfolders": "", + "components.Settings.SettingsAbout.about": "", + "components.Settings.SettingsAbout.aboutseerr": "", + "components.Settings.SettingsJobsCache.jellyfin-recently-added-scan": "", + "components.Settings.SettingsJobsCache.jobScheduleEditSaved": "", + "components.Settings.SettingsJobsCache.jobcancelled": "", + "components.Settings.SettingsJobsCache.jobname": "", + "components.Settings.SettingsJobsCache.jobs": "", + "components.Settings.SettingsJobsCache.jobsDescription": "", + "components.Settings.SettingsJobsCache.jobsandcache": "", + "components.Settings.SettingsJobsCache.jobstarted": "", + "components.Settings.SettingsJobsCache.jobtype": "", + "components.Settings.SettingsJobsCache.misses": "", + "components.Settings.SettingsJobsCache.nextexecution": "", + "components.Settings.SettingsLogs.message": "", + "components.Settings.SettingsLogs.pauseLogs": "", + "components.Settings.SettingsMain.validationUrl": "", + "components.Settings.SettingsMain.validationUrlTrailingSlash": "", + "components.Settings.SonarrModal.add": "", + "components.Settings.manualscanJellyfin": "", + "components.Settings.noSpecialCharacters": "", + "components.TvDetails.episodeRuntime": "", + "components.TvDetails.episodeRuntimeMinutes": "", + "components.TvDetails.firstAirDate": "", + "components.UserList.autogeneratepassword": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.device": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramChatIdTipLong": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadId": "", + "components.UserProfile.UserSettings.UserPasswordChange.validationNewPasswordLength": "", + "components.UserProfile.UserSettings.UserPermissions.permissions": "", + "components.UserProfile.UserSettings.UserPermissions.toastSettingsFailure": "", + "components.UserProfile.UserSettings.UserGeneralSettings.mediaServerUser": "", + "components.UserProfile.UserSettings.UserGeneralSettings.originallanguage": "", + "components.UserProfile.UserSettings.UserGeneralSettings.originallanguageTip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.owner": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexuser": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmovies": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmoviestip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseries": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseriestip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.region": "", + "components.UserProfile.UserSettings.UserGeneralSettings.streamingRegion": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailure": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmail": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmailEmpty": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsSuccess": "", + "components.UserProfile.UserSettings.UserGeneralSettings.user": "", + "components.UserProfile.UserSettings.UserGeneralSettings.validationemailformat": "", + "components.UserProfile.UserSettings.UserGeneralSettings.validationemailrequired": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.deleteFailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.created": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.deletesubscription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablewebpush": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablingwebpusherror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablewebpush": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablingwebpusherror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.engine": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.managedevices": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.operatingsystem": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.type": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.unknown": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeendisabled": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeenenabled": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordId": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdAdd": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdPlaceholder": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdRemove": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "", + "components.Login.initialsigningin": "", + "components.Login.urlBaseHelp": "", + "components.Login.validationEmailRequired": "", + "components.Login.validationPortRequired": "", + "components.Login.validationUrlBaseLeadingSlash": "", + "components.Login.validationemailformat": "", + "components.Login.validationemailrequired": "", + "components.Login.validationhostformat": "", + "components.Login.validationhostrequired": "", + "components.Login.validationpasswordrequired": "", + "components.Login.validationservertyperequired": "", + "components.Login.validationusernamerequired": "", + "components.ManageSlideOver.alltime": "", + "components.ManageSlideOver.downloadstatus": "", + "components.ManageSlideOver.manageModalAdvanced": "", + "components.ManageSlideOver.manageModalClearMedia": "", + "components.ManageSlideOver.manageModalClearMediaWarning": "", + "components.ManageSlideOver.manageModalIssues": "", + "components.ManageSlideOver.manageModalMedia": "", + "components.ManageSlideOver.manageModalMedia4k": "", + "components.ManageSlideOver.manageModalNoRequests": "", + "components.ManageSlideOver.manageModalRemoveMediaWarning": "", + "components.ManageSlideOver.manageModalRequests": "", + "components.MovieDetails.managemovie": "", + "components.MovieDetails.mark4kavailable": "", + "components.MovieDetails.openradarr": "", + "components.MovieDetails.openradarr4k": "", + "components.MovieDetails.originallanguage": "", + "components.MovieDetails.originaltitle": "", + "components.MovieDetails.overview": "", + "components.MovieDetails.physicalrelease": "", + "components.MovieDetails.play": "", + "components.MovieDetails.productioncountries": "", + "components.MovieDetails.recommendations": "", + "components.MovieDetails.releasedate": "", + "components.MovieDetails.studio": "", + "components.MovieDetails.theatricalrelease": "", + "components.MovieDetails.tmdbuserscore": "", + "components.MovieDetails.viewfullcrew": "", + "components.NotificationTypeSelector.issuecommentDescription": "", + "components.NotificationTypeSelector.issuecreated": "", + "components.NotificationTypeSelector.issuecreatedDescription": "", + "components.NotificationTypeSelector.issuereopened": "", + "components.NotificationTypeSelector.issueresolved": "", + "components.NotificationTypeSelector.issueresolvedDescription": "", + "components.NotificationTypeSelector.mediaAutoApproved": "", + "components.NotificationTypeSelector.mediaAutoApprovedDescription": "", + "components.NotificationTypeSelector.mediaapproved": "", + "components.NotificationTypeSelector.mediaapprovedDescription": "", + "components.NotificationTypeSelector.mediaautorequested": "", + "components.NotificationTypeSelector.mediaautorequestedDescription": "", + "components.NotificationTypeSelector.mediaavailable": "", + "components.NotificationTypeSelector.mediaavailableDescription": "", + "components.NotificationTypeSelector.mediadeclined": "", + "components.NotificationTypeSelector.mediadeclinedDescription": "", + "components.NotificationTypeSelector.mediafailed": "", + "components.NotificationTypeSelector.mediafailedDescription": "", + "components.NotificationTypeSelector.mediarequested": "", + "components.NotificationTypeSelector.mediarequestedDescription": "", + "components.NotificationTypeSelector.notificationTypes": "", + "components.NotificationTypeSelector.userissuecommentDescription": "", + "components.NotificationTypeSelector.userissuereopenedDescription": "", + "components.NotificationTypeSelector.userissueresolvedDescription": "", + "components.PermissionEdit.autoapprove4kMoviesDescription": "", + "components.PermissionEdit.autoapprove4kSeries": "", + "components.PermissionEdit.autoapprove4kSeriesDescription": "", + "components.PermissionEdit.autoapproveDescription": "", + "components.PermissionEdit.autoapproveMovies": "", + "components.RequestButton.requestmore": "", + "components.RequestModal.QuotaDisplay.seasonlimit": "", + "components.RequestModal.SearchByNameModal.nomatches": "", + "components.RequestModal.SearchByNameModal.notvdbiddescription": "", + "components.ResetPassword.gobacklogin": "", + "components.ResetPassword.password": "", + "components.ResetPassword.passwordreset": "", + "components.ResetPassword.requestresetlinksuccessmessage": "", + "components.ResetPassword.resetpassword": "", + "components.ResetPassword.resetpasswordsuccessmessage": "", + "components.Settings.Notifications.NotificationsGotify.priority": "", + "components.Settings.Notifications.NotificationsGotify.toastGotifyTestFailed": "", + "components.Settings.Notifications.NotificationsGotify.toastGotifyTestSending": "", + "components.Settings.Notifications.NotificationsGotify.toastGotifyTestSuccess": "", + "components.Settings.Notifications.NotificationsGotify.token": "", + "components.Settings.Notifications.NotificationsGotify.validationTypes": "", + "components.Settings.Notifications.NotificationsGotify.validationUrlRequired": "", + "components.Settings.Notifications.NotificationsGotify.validationUrlTrailingSlash": "", + "components.Settings.Notifications.NotificationsNtfy.agentenabled": "", + "components.Settings.Notifications.NotificationsNtfy.embedPoster": "", + "components.Settings.Notifications.NotificationsNtfy.ntfysettingsfailed": "", + "components.Settings.Notifications.NotificationsNtfy.ntfysettingssaved": "", + "components.Settings.Notifications.NotificationsNtfy.priority": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestFailed": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestSending": "", + "components.Settings.Notifications.NotificationsNtfy.username": "", + "components.Settings.Notifications.NotificationsNtfy.usernamePasswordAuth": "", + "components.Settings.Notifications.NotificationsNtfy.validationNtfyTopic": "", + "components.Settings.Notifications.NotificationsNtfy.validationNtfyUrl": "", + "components.Settings.Notifications.NotificationsNtfy.validationPriorityRequired": "", + "components.Settings.Notifications.NotificationsNtfy.validationTypes": "", + "components.Settings.Notifications.NotificationsPushbullet.accessToken": "", + "components.Settings.Notifications.NotificationsPushbullet.accessTokenTip": "", + "components.Settings.Notifications.NotificationsPushbullet.pushbulletSettingsFailed": "", + "components.Settings.Notifications.NotificationsPushbullet.pushbulletSettingsSaved": "", + "components.Settings.Notifications.NotificationsPushover.accessToken": "", + "components.Settings.Notifications.NotificationsPushover.agentenabled": "", + "components.Settings.Notifications.NotificationsPushover.deviceDefault": "", + "components.Settings.Notifications.NotificationsPushover.embedPoster": "", + "components.Settings.Notifications.NotificationsPushover.pushoversettingssaved": "", + "components.Settings.Notifications.NotificationsPushover.sound": "", + "components.Settings.Notifications.NotificationsPushover.toastPushoverTestFailed": "", + "components.Settings.Notifications.NotificationsPushover.toastPushoverTestSending": "", + "components.Settings.Notifications.NotificationsPushover.toastPushoverTestSuccess": "", + "components.Settings.Notifications.NotificationsPushover.userToken": "", + "components.Settings.Notifications.NotificationsPushover.userTokenTip": "", + "components.Settings.Notifications.NotificationsSlack.validationWebhookUrl": "", + "components.Settings.Notifications.discordsettingssaved": "", + "components.Settings.Notifications.emailsettingsfailed": "", + "components.Settings.Notifications.emailsettingssaved": "", + "components.Settings.Notifications.embedPoster": "", + "components.Settings.Notifications.enableMentions": "", + "components.Settings.Notifications.encryption": "", + "components.Settings.Notifications.encryptionDefault": "", + "components.Settings.RadarrModal.testFirstQualityProfiles": "", + "components.Settings.SettingsAbout.supportseerr": "", + "components.Settings.SettingsJobsCache.editJobSchedule": "", + "components.Settings.SettingsLogs.extraData": "", + "components.UserList.create": "", + "components.Discover.tmdbtvstreamingservices": "", + "components.Discover.upcomingtv": "", + "components.Discover.updatefailed": "", + "components.Discover.updatesuccess": "", + "components.DiscoverTvUpcoming.upcomingtv": "", + "components.Settings.Notifications.NotificationsWebPush.agentenabled": "", + "components.Settings.Notifications.NotificationsWebPush.embedPoster": "", + "components.Settings.Notifications.NotificationsWebPush.httpsRequirement": "", + "components.Settings.Notifications.NotificationsWebPush.toastWebPushTestFailed": "", + "components.Settings.Notifications.NotificationsWebPush.webpushsettingsfailed": "", + "components.Settings.Notifications.NotificationsWebPush.webpushsettingssaved": "", + "components.Settings.Notifications.NotificationsWebhook.customJson": "", + "components.Settings.Notifications.NotificationsWebhook.resetPayload": "", + "components.Settings.Notifications.NotificationsWebhook.resetPayloadSuccess": "", + "components.Settings.Notifications.NotificationsWebhook.supportVariables": "", + "components.Settings.Notifications.NotificationsWebhook.supportVariablesTip": "", + "components.Settings.Notifications.NotificationsWebhook.templatevariablehelp": "", + "components.Settings.Notifications.NotificationsWebhook.toastWebhookTestFailed": "", + "components.Settings.Notifications.NotificationsWebhook.validationJsonPayloadRequired": "", + "components.Settings.Notifications.NotificationsWebhook.validationTypes": "", + "components.Settings.Notifications.NotificationsWebhook.validationWebhookUrl": "", + "components.Settings.Notifications.NotificationsWebhook.webhookUrlTip": "", + "components.Settings.Notifications.NotificationsWebhook.webhooksettingsfailed": "", + "components.Settings.Notifications.NotificationsWebhook.webhooksettingssaved": "", + "components.Settings.RadarrModal.announced": "", + "components.Settings.RadarrModal.apiKey": "", + "components.Settings.RadarrModal.create4kradarr": "", + "components.Settings.RadarrModal.createradarr": "", + "components.Settings.RadarrModal.default4kserver": "", + "components.Settings.RadarrModal.editradarr": "", + "components.Settings.SettingsAbout.outofdate": "", + "components.Settings.SettingsAbout.totalmedia": "", + "components.Settings.SettingsJobsCache.cachehits": "", + "components.Settings.SettingsJobsCache.cachekeys": "", + "components.Settings.SettingsJobsCache.cacheksize": "", + "components.Settings.SettingsJobsCache.cachename": "", + "components.Settings.SettingsJobsCache.cachevsize": "", + "components.Settings.SettingsJobsCache.canceljob": "", + "components.Settings.SettingsJobsCache.command": "", + "components.Settings.SettingsJobsCache.dnscacheage": "", + "components.Settings.SettingsJobsCache.dnscacheflushed": "", + "components.Settings.SettingsJobsCache.dnscachehits": "", + "components.Settings.SettingsJobsCache.download-sync-reset": "", + "components.Settings.SettingsJobsCache.editJobScheduleCurrent": "", + "components.Settings.SettingsJobsCache.editJobSchedulePrompt": "", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorDays": "", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorHours": "", + "components.Settings.SettingsJobsCache.failures": "", + "components.UserProfile.UserSettings.UserGeneralSettings.regionTip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.role": "", + "components.UserProfile.UserSettings.UserGeneralSettings.save": "", + "components.UserProfile.UserSettings.UserGeneralSettings.saving": "", + "components.UserProfile.UserSettings.UserGeneralSettings.seriesrequestlimit": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.errorUnknown": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.linkedAccounts": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.linkedAccountsHint": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.noPermissionDescription": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorExists": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorUnauthorized": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.browser": "", + "i18n.delete": "", + "i18n.request": "", + "i18n.requested": "", + "i18n.resolved": "", + "i18n.restartRequired": "", + "i18n.resultsperpage": "", + "i18n.retrying": "", + "i18n.save": "", + "i18n.settings": "", + "i18n.showingresults": "", + "i18n.error": "", + "components.RequestCard.failedmodify": "", + "components.RequestModal.AdvancedRequester.folder": "", + "components.RequestModal.AdvancedRequester.notagoptions": "", + "components.RequestModal.requestCancel": "", + "components.Search.search": "", + "components.Search.searchresults": "", + "components.Selector.CertificationSelector.errorLoading": "", + "components.Settings.Notifications.NotificationsGotify.url": "", + "components.Settings.Notifications.NotificationsGotify.validationPriorityRequired": "", + "components.Settings.Notifications.NotificationsGotify.validationTokenRequired": "", + "components.Settings.Notifications.NotificationsPushover.validationAccessTokenRequired": "", + "components.Settings.Notifications.NotificationsPushover.validationTypes": "", + "components.Settings.Notifications.botAPI": "", + "components.Settings.OverrideRuleModal.ruleCreated": "", + "components.Settings.RadarrModal.ssl": "", + "components.Settings.SettingsJobsCache.image-cache-cleanup": "", + "components.Settings.SettingsLogs.logsDescription": "", + "components.Settings.toastPlexConnectingFailure": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleteerror": "", + "components.Settings.Notifications.usePublicLogo": "", + "components.Settings.Notifications.usePublicLogoTip": "", + "components.Settings.Notifications.useUserLocale": "", + "components.Settings.Notifications.validationEmail": "", + "components.Settings.Notifications.validationSmtpPortRequired": "", + "components.Settings.Notifications.validationTypes": "", + "components.Settings.Notifications.validationWebhookRoleId": "", + "components.Settings.Notifications.webhookRoleId": "", + "components.Settings.Notifications.webhookRoleIdTip": "", + "components.Settings.Notifications.webhookUrlTip": "", + "components.Settings.OverrideRuleModal.conditions": "", + "components.Settings.OverrideRuleModal.conditionsDescription": "", + "components.Settings.OverrideRuleModal.create": "", + "components.Settings.OverrideRuleModal.createrule": "", + "components.Settings.OverrideRuleModal.editrule": "", + "components.Settings.OverrideRuleModal.genres": "", + "components.Settings.OverrideRuleModal.languages": "", + "components.Settings.OverrideRuleModal.notagoptions": "", + "components.Settings.OverrideRuleModal.qualityprofile": "", + "components.Settings.OverrideRuleModal.rootfolder": "", + "components.Settings.OverrideRuleModal.ruleUpdated": "", + "components.Settings.OverrideRuleModal.selectQualityProfile": "", + "components.Settings.OverrideRuleModal.selectRootFolder": "", + "components.Settings.OverrideRuleModal.selectService": "", + "components.Settings.OverrideRuleModal.selecttags": "", + "components.Settings.OverrideRuleModal.service": "", + "components.Settings.RadarrModal.add": "", + "components.Settings.RadarrModal.apiKeyHelp": "", + "components.Settings.RadarrModal.baseUrl": "", + "components.Settings.RadarrModal.baseUrlHelp": "", + "components.Settings.RadarrModal.defaultserver": "", + "components.Settings.RadarrModal.edit4kradarr": "", + "components.Settings.RadarrModal.enableSearch": "", + "components.Settings.RadarrModal.enableSearchHelp": "", + "components.Settings.RadarrModal.externalUrl": "", + "components.Settings.RadarrModal.externalUrlHelp": "", + "components.Settings.RadarrModal.hostname": "", + "components.Settings.RadarrModal.inCinemas": "", + "components.Settings.RadarrModal.minimumAvailability": "", + "components.Settings.RadarrModal.notagoptions": "", + "components.Settings.RadarrModal.port": "", + "components.Settings.RadarrModal.qualityprofile": "", + "components.Settings.RadarrModal.released": "", + "components.Settings.RadarrModal.rootfolder": "", + "components.Settings.RadarrModal.selectRootFolder": "", + "components.Settings.RadarrModal.selecttags": "", + "components.Settings.RadarrModal.server4k": "", + "components.Settings.RadarrModal.server4kHelp": "", + "components.Settings.RadarrModal.servername": "", + "components.Settings.RadarrModal.syncEnabled": "", + "components.Settings.RadarrModal.syncEnabledHelp": "", + "components.Settings.RadarrModal.tagRequests": "", + "components.Settings.RadarrModal.tagRequestsInfo": "", + "components.Settings.RadarrModal.testFirstRootFolders": "", + "components.Settings.RadarrModal.testFirstTags": "", + "components.Settings.RadarrModal.toastRadarrTestFailure": "", + "components.Settings.RadarrModal.toastRadarrTestSuccess": "", + "components.Settings.RadarrModal.validationApiKeyRequired": "", + "components.Settings.RadarrModal.validationApplicationUrl": "", + "components.Settings.RadarrModal.validationApplicationUrlTrailingSlash": "", + "components.Settings.RadarrModal.validationBaseUrlLeadingSlash": "", + "components.Settings.RadarrModal.validationBaseUrlTrailingSlash": "", + "components.Settings.RadarrModal.validationHostnameRequired": "", + "components.Settings.RadarrModal.validationMinimumAvailabilityRequired": "", + "components.Settings.RadarrModal.validationNameRequired": "", + "components.Settings.RadarrModal.validationPortRequired": "", + "components.Settings.RadarrModal.validationProfileRequired": "", + "components.Settings.RadarrModal.validationRootFolderRequired": "", + "components.Settings.SettingsAbout.Releases.currentversion": "", + "components.Settings.SettingsAbout.Releases.latestversion": "", + "components.Settings.SettingsAbout.Releases.releasedataMissing": "", + "components.Settings.SettingsAbout.Releases.versionChangelog": "", + "components.Settings.SettingsAbout.Releases.viewchangelog": "", + "components.Settings.SettingsAbout.Releases.viewongithub": "", + "components.Settings.SettingsAbout.appDataPath": "", + "components.Settings.SettingsAbout.contribute": "", + "components.Settings.SettingsAbout.gettingsupport": "", + "components.Settings.SettingsAbout.githubdiscussions": "", + "components.Settings.SettingsAbout.runningDevelop": "", + "components.Settings.SettingsAbout.uptodate": "", + "components.Settings.SettingsAbout.version": "", + "components.Settings.SettingsJobsCache.availability-sync": "", + "components.Settings.SettingsJobsCache.cache": "", + "components.Settings.SettingsJobsCache.cacheDescription": "", + "components.Settings.SettingsJobsCache.cacheflushed": "", + "components.Settings.SettingsJobsCache.dnsCache": "", + "components.Settings.SettingsJobsCache.dnsCacheDescription": "", + "components.Settings.SettingsJobsCache.dnsCacheGlobalStats": "", + "components.Settings.SettingsJobsCache.dnsNoCacheEntries": "", + "components.Settings.SettingsJobsCache.dnscachemisses": "", + "components.Settings.SettingsJobsCache.dnscachename": "", + "components.Settings.SettingsJobsCache.download-sync": "", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorMinutes": "", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorSeconds": "", + "components.Settings.SettingsJobsCache.flushcache": "", + "components.Settings.SettingsJobsCache.flushdnscache": "", + "components.Settings.SettingsJobsCache.imagecache": "", + "components.Settings.SettingsJobsCache.imagecacheDescription": "", + "components.Settings.SettingsJobsCache.imagecachecount": "", + "components.Settings.SettingsJobsCache.imagecachesize": "", + "components.Settings.SettingsJobsCache.ipv4Fallbacks": "", + "components.Settings.SettingsJobsCache.jellyfin-full-scan": "", + "components.Settings.SettingsJobsCache.plex-full-scan": "", + "components.Settings.SettingsJobsCache.plex-recently-added-scan": "", + "components.Settings.SettingsJobsCache.plex-refresh-token": "", + "components.Settings.SettingsJobsCache.plex-watchlist-sync": "", + "components.Settings.SettingsJobsCache.process": "", + "components.Settings.SettingsJobsCache.radarr-scan": "", + "components.Settings.SettingsJobsCache.runnow": "", + "components.Settings.SettingsJobsCache.size": "", + "components.Settings.SettingsJobsCache.unknownJob": "", + "components.Settings.SettingsJobsCache.usersavatars": "", + "components.Settings.SettingsLogs.copiedLogMessage": "", + "components.Settings.SettingsLogs.copyToClipboard": "", + "components.Settings.SettingsLogs.filterDebug": "", + "components.Settings.SettingsLogs.filterError": "", + "components.Settings.SettingsLogs.filterInfo": "", + "components.Settings.SettingsLogs.filterWarn": "", + "components.Settings.SettingsLogs.label": "", + "components.Settings.SettingsLogs.level": "", + "components.Settings.SettingsLogs.logDetails": "", + "components.Settings.SettingsLogs.logs": "", + "components.Settings.SettingsLogs.resumeLogs": "", + "components.Settings.SettingsLogs.showall": "", + "components.Settings.SettingsLogs.time": "", + "components.Settings.SettingsLogs.viewdetails": "", + "components.Settings.SettingsMain.apikey": "", + "components.Settings.SettingsMain.apikeyCopied": "", + "components.Settings.SettingsMain.applicationurl": "", + "components.Settings.SettingsMain.blocklistLanguage": "", + "components.Settings.SettingsMain.blocklistLanguageTip": "", + "components.Settings.SettingsMain.blocklistRegion": "", + "components.Settings.SettingsMain.blocklistRegionTip": "", + "components.Settings.SettingsMain.blocklistedTags": "", + "components.Settings.SettingsMain.blocklistedTagsLimit": "", + "components.Settings.SettingsMain.blocklistedTagsLimitTip": "", + "components.Settings.SettingsMain.blocklistedTagsTip": "", + "components.Settings.SettingsMain.cacheImages": "", + "components.Settings.SettingsMain.cacheImagesTip": "", + "components.Settings.SettingsMain.discoverRegion": "", + "components.Settings.SettingsMain.discoverRegionTip": "", + "components.Settings.SettingsMain.enableSpecialEpisodes": "", + "components.Settings.SettingsMain.general": "", + "components.Settings.SettingsMain.generalsettings": "", + "components.Settings.SettingsMain.generalsettingsDescription": "", + "components.Settings.SettingsMain.hideAvailable": "", + "components.Settings.SettingsMain.hideAvailableTip": "", + "components.Settings.SettingsMain.hideBlocklisted": "", + "components.Settings.SettingsMain.hideBlocklistedTip": "", + "components.Settings.SettingsMain.locale": "", + "components.Settings.SettingsMain.partialRequestsEnabled": "", + "components.Settings.SettingsMain.streamingRegion": "", + "components.Settings.SettingsMain.streamingRegionTip": "", + "components.Settings.SettingsMain.toastApiKeyFailure": "", + "components.Settings.SettingsMain.toastApiKeySuccess": "", + "components.Settings.SettingsMain.toastSettingsFailure": "", + "components.Settings.SettingsMain.toastSettingsSuccess": "", + "components.Settings.SettingsMain.validationApplicationTitle": "", + "components.Settings.SettingsMain.validationApplicationUrl": "", + "components.Settings.SettingsMain.validationApplicationUrlTrailingSlash": "", + "components.Settings.SettingsMain.youtubeUrl": "", + "components.Settings.SettingsMain.youtubeUrlTip": "", + "components.Settings.SettingsNetwork.apiRequestTimeout": "", + "components.Settings.SettingsNetwork.apiRequestTimeoutTip": "", + "components.Settings.SettingsNetwork.csrfProtection": "", + "components.Settings.SettingsNetwork.csrfProtectionHoverTip": "", + "components.Settings.SettingsNetwork.csrfProtectionTip": "", + "components.Settings.SettingsNetwork.dnsCache": "", + "components.Settings.SettingsNetwork.dnsCacheForceMaxTtl": "", + "components.Settings.SettingsNetwork.dnsCacheHoverTip": "", + "components.Settings.SettingsNetwork.dnsCacheTip": "", + "components.Settings.SettingsNetwork.docs": "", + "components.Settings.SettingsNetwork.forceIpv4First": "", + "components.Settings.SettingsNetwork.forceIpv4FirstTip": "", + "components.Settings.SettingsNetwork.network": "", + "components.Settings.SettingsNetwork.networkDisclaimer": "", + "components.Settings.SettingsNetwork.networksettingsDescription": "", + "components.Settings.SettingsNetwork.proxyBypassFilter": "", + "components.Settings.SettingsNetwork.proxyEnabled": "", + "components.Settings.SettingsNetwork.proxyEnabledTip": "", + "components.Settings.SettingsNetwork.proxyHostname": "", + "components.Settings.SettingsNetwork.proxyPassword": "", + "components.Settings.SettingsNetwork.proxyPort": "", + "components.Settings.SettingsNetwork.proxyUser": "", + "components.Settings.SettingsNetwork.toastSettingsFailure": "", + "components.Settings.SettingsNetwork.toastSettingsSuccess": "", + "components.Settings.SettingsNetwork.trustProxy": "", + "components.Settings.SettingsNetwork.trustProxyTip": "", + "components.Settings.SettingsNetwork.validationApiRequestTimeout": "", + "components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "", + "components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "", + "components.Settings.SettingsNetwork.validationProxyPort": "", + "components.Settings.SettingsUsers.atLeastOneAuth": "", + "components.Settings.SettingsUsers.defaultPermissions": "", + "components.Settings.SettingsUsers.defaultPermissionsTip": "", + "components.Settings.SettingsUsers.disabledMediaServerLoginWarning": "", + "components.Settings.SettingsUsers.localLogin": "", + "components.Settings.SettingsUsers.localLoginTip": "", + "components.Settings.SettingsUsers.loginMethods": "", + "components.Settings.SettingsUsers.mediaServerLogin": "", + "components.Settings.SettingsUsers.mediaServerLoginTip": "", + "components.Settings.SettingsUsers.movieRequestLimitLabel": "", + "components.Settings.SettingsUsers.newPlexLogin": "", + "components.Settings.SettingsUsers.newPlexLoginTip": "", + "components.Settings.SettingsUsers.toastSettingsFailure": "", + "components.Settings.SettingsUsers.toastSettingsSuccess": "", + "components.Settings.SettingsUsers.tvRequestLimitLabel": "", + "components.Settings.SettingsUsers.userSettings": "", + "components.Settings.SettingsUsers.userSettingsDescription": "", + "components.Settings.SettingsUsers.users": "", + "components.Settings.SonarrModal.animeSeriesType": "", + "components.Settings.SonarrModal.animeTags": "", + "components.Settings.SonarrModal.animelanguageprofile": "", + "components.Settings.SonarrModal.animerootfolder": "", + "components.Settings.SonarrModal.apiKey": "", + "components.Settings.SonarrModal.apiKeyHelp": "", + "components.Settings.SonarrModal.baseUrl": "", + "components.Settings.SonarrModal.baseUrlHelp": "", + "components.Settings.SonarrModal.create4ksonarr": "", + "components.Settings.SonarrModal.createsonarr": "", + "components.Settings.SonarrModal.default4kserver": "", + "components.Settings.SonarrModal.edit4ksonarr": "", + "components.Settings.SonarrModal.editsonarr": "", + "components.Settings.SonarrModal.enableSearchHelp": "", + "components.Settings.SonarrModal.externalUrlHelp": "", + "components.Settings.SonarrModal.loadinglanguageprofiles": "", + "components.Settings.SonarrModal.loadingprofiles": "", + "components.Settings.SonarrModal.loadingrootfolders": "", + "components.Settings.SonarrModal.monitorNewItemsHelp": "", + "components.Settings.SonarrModal.port": "", + "components.Settings.SonarrModal.selectRootFolder": "", + "components.Settings.SonarrModal.selecttags": "", + "components.Settings.SonarrModal.seriesType": "", + "components.Settings.SonarrModal.server4k": "", + "components.Settings.SonarrModal.server4kHelp": "", + "components.Settings.SonarrModal.servername": "", + "components.Settings.SonarrModal.ssl": "", + "components.Settings.SonarrModal.syncEnabled": "", + "components.Settings.SonarrModal.syncEnabledHelp": "", + "components.Settings.SonarrModal.tagRequests": "", + "components.Settings.SonarrModal.tagRequestsInfo": "", + "components.Settings.SonarrModal.tags": "", + "components.Settings.SonarrModal.testFirstLanguageProfiles": "", + "components.Settings.SonarrModal.testFirstQualityProfiles": "", + "components.Settings.SonarrModal.toastSonarrTestFailure": "", + "components.Settings.SonarrModal.toastSonarrTestSuccess": "", + "components.Settings.SonarrModal.validationApiKeyRequired": "", + "components.Settings.SonarrModal.validationApplicationUrl": "", + "components.Settings.SonarrModal.validationApplicationUrlTrailingSlash": "", + "components.Settings.SonarrModal.validationBaseUrlLeadingSlash": "", + "components.Settings.SonarrModal.validationLanguageProfileRequired": "", + "components.Settings.SonarrModal.validationNameRequired": "", + "components.Settings.SonarrModal.validationPortRequired": "", + "components.Settings.SonarrModal.validationProfileRequired": "", + "components.Settings.SonarrModal.validationRootFolderRequired": "", + "components.Settings.activeProfile": "", + "components.Settings.addradarr": "", + "components.Settings.address": "", + "components.Settings.addrule": "", + "components.Settings.addsonarr": "", + "components.Settings.advancedTooltip": "", + "components.Settings.allChosenProvidersAreOperational": "", + "components.Settings.animeMetadataProvider": "", + "components.Settings.apiKey": "", + "components.Settings.blocklistedTagImportInstructions": "", + "components.Settings.blocklistedTagsText": "", + "components.Settings.cancelscan": "", + "components.Settings.clearBlocklistedTagsConfirm": "", + "components.Settings.clickTest": "", + "components.Settings.copyBlocklistedTags": "", + "components.Settings.copyBlocklistedTagsEmpty": "", + "components.Settings.copyBlocklistedTagsTip": "", + "components.Settings.default": "", + "components.Settings.default4k": "", + "components.Settings.deleteServer": "", + "components.Settings.deleteserverconfirm": "", + "components.Settings.enablessl": "", + "components.Settings.experimentalTooltip": "", + "components.Settings.externalUrl": "", + "components.Settings.failed": "", + "components.Settings.failedToSaveMetadataSettings": "", + "components.Settings.hostname": "", + "components.Settings.importBlocklistedTagsTip": "", + "components.Settings.invalidKeyword": "", + "components.Settings.invalidurlerror": "", + "components.Settings.jellyfinForgotPasswordUrl": "", + "components.Settings.jellyfinSettings": "", + "components.Settings.jellyfinSettingsDescription": "", + "components.Settings.jellyfinSettingsFailure": "", + "components.Settings.jellyfinSyncFailedAutomaticGroupedFolders": "", + "components.Settings.jellyfinSyncFailedGenericError": "", + "components.Settings.jellyfinSyncFailedNoLibrariesFound": "", + "components.Settings.jellyfinlibraries": "", + "components.Settings.jellyfinlibrariesDescription": "", + "components.Settings.jellyfinsettings": "", + "components.Settings.jellyfinsettingsDescription": "", + "components.Settings.librariesRemaining": "", + "components.Settings.manualscan": "", + "components.Settings.manualscanDescription": "", + "components.Settings.manualscanDescriptionJellyfin": "", + "components.Settings.mediaTypeMovie": "", + "components.Settings.mediaTypeSeries": "", + "components.Settings.menuAbout": "", + "components.Settings.menuGeneralSettings": "", + "components.Settings.menuJellyfinSettings": "", + "components.Settings.menuJobs": "", + "components.Settings.menuLogs": "", + "components.Settings.menuMetadataProviders": "", + "components.Settings.menuNetwork": "", + "components.Settings.menuNotifications": "", + "components.Settings.menuPlexSettings": "", + "components.Settings.menuServices": "", + "components.Settings.menuUsers": "", + "components.Settings.metadataProviderSelection": "", + "components.Settings.metadataProviderSettings": "", + "components.Settings.metadataSettingsSaved": "", + "components.Settings.no": "", + "components.Settings.noDefault4kServer": "", + "components.Settings.noDefaultNon4kServer": "", + "components.Settings.noDefaultServer": "", + "components.Settings.nooptions": "", + "components.Settings.notTested": "", + "components.Settings.notifications": "", + "components.Settings.notificationsettings": "", + "components.Settings.notrunning": "", + "components.Settings.operational": "", + "components.Settings.overrideRules": "", + "components.Settings.overrideRulesDescription": "", + "components.Settings.plex": "", + "components.Settings.plexlibraries": "", + "components.Settings.plexlibrariesDescription": "", + "components.Settings.plexsettings": "", + "components.Settings.plexsettingsDescription": "", + "components.Settings.providerStatus": "", + "components.Settings.radarrsettings": "", + "components.Settings.restartrequiredTooltip": "", + "components.Settings.save": "", + "components.Settings.saving": "", + "components.Settings.scan": "", + "components.Settings.scanbackground": "", + "components.Settings.scanning": "", + "components.Settings.searchKeywords": "", + "components.Settings.seriesMetadataProvider": "", + "components.Settings.serverLocal": "", + "components.Settings.serverRemote": "", + "components.Settings.serverSecure": "", + "components.Settings.serverpreset": "", + "components.Settings.serverpresetLoad": "", + "components.Settings.serverpresetManualMessage": "", + "components.Settings.serviceSettingsDescription": "", + "components.Settings.services": "", + "components.Settings.settingUpPlexDescription": "", + "components.Settings.settings": "", + "components.Settings.sonarrsettings": "", + "components.Settings.ssl": "", + "components.Settings.startscan": "", + "components.Settings.starttyping": "", + "components.Settings.syncJellyfin": "", + "components.Settings.syncing": "", + "components.Settings.tautulliApiKey": "", + "components.Settings.tautulliSettings": "", + "components.Settings.tautulliSettingsDescription": "", + "components.Settings.timeout": "", + "components.Settings.tmdbProviderDoesnotWork": "", + "components.Settings.toastPlexConnecting": "", + "components.Settings.toastPlexConnectingSuccess": "", + "components.Settings.toastPlexRefreshSuccess": "", + "components.Settings.toastTautulliSettingsFailure": "", + "components.Settings.toastTautulliSettingsSuccess": "", + "components.Settings.tvdbProviderDoesnotWork": "", + "components.Settings.validationApiKey": "", + "components.Settings.validationHostnameRequired": "", + "components.Settings.validationPortRequired": "", + "components.Settings.validationUrl": "", + "components.Settings.validationUrlBaseLeadingSlash": "", + "components.Settings.validationUrlBaseTrailingSlash": "", + "components.Settings.validationUrlTrailingSlash": "", + "components.Settings.valueRequired": "", + "components.Settings.webAppUrl": "", + "components.Settings.webAppUrlTip": "", + "components.Settings.yes": "", + "components.Setup.back": "", + "components.Setup.configemby": "", + "components.Setup.configjellyfin": "", + "components.Setup.configplex": "", + "components.Setup.configuremediaserver": "", + "components.Setup.configureservices": "", + "components.Setup.continue": "", + "components.Setup.finish": "", + "components.Setup.finishing": "", + "components.Setup.librarieserror": "", + "components.Setup.servertype": "", + "components.Setup.signin": "", + "components.Setup.signinMessage": "", + "components.Setup.signinWithEmby": "", + "components.Setup.signinWithJellyfin": "", + "components.Setup.signinWithPlex": "", + "components.Setup.subtitle": "", + "components.Setup.welcome": "", + "components.StatusBadge.managemedia": "", + "components.StatusBadge.openinarr": "", + "components.StatusBadge.playonplex": "", + "components.StatusBadge.status": "", + "components.StatusBadge.status4k": "", + "components.StatusChecker.appUpdated": "", + "components.StatusChecker.appUpdatedDescription": "", + "components.StatusChecker.reloadApp": "", + "components.StatusChecker.restartRequired": "", + "components.StatusChecker.restartRequiredDescription": "", + "components.TitleCard.addToWatchList": "", + "components.TitleCard.cleardata": "", + "components.TitleCard.mediaerror": "", + "components.TitleCard.tvdbid": "", + "components.TitleCard.watchlistCancel": "", + "components.TitleCard.watchlistDeleted": "", + "components.TitleCard.watchlistError": "", + "components.TitleCard.watchlistSuccess": "", + "components.TvDetails.Season.noepisodes": "", + "components.TvDetails.Season.somethingwentwrong": "", + "components.TvDetails.TvCrew.fullseriescrew": "", + "components.TvDetails.addtowatchlist": "", + "components.TvDetails.anime": "", + "components.TvDetails.cast": "", + "components.TvDetails.episodeCount": "", + "components.TvDetails.manageseries": "", + "components.TvDetails.network": "", + "components.TvDetails.nextAirDate": "", + "components.TvDetails.originallanguage": "", + "components.TvDetails.originaltitle": "", + "components.TvDetails.overview": "", + "components.TvDetails.overviewunavailable": "", + "components.TvDetails.play": "", + "components.TvDetails.play4k": "", + "components.TvDetails.productioncountries": "", + "components.TvDetails.removefromwatchlist": "", + "components.TvDetails.reportissue": "", + "components.TvDetails.rtaudiencescore": "", + "components.TvDetails.rtcriticsscore": "", + "components.TvDetails.seasonnumber": "", + "components.TvDetails.seasons": "", + "components.TvDetails.similar": "", + "components.TvDetails.status4k": "", + "components.TvDetails.streamingproviders": "", + "components.TvDetails.tmdbuserscore": "", + "components.TvDetails.viewfullcrew": "", + "components.TvDetails.watchlistDeleted": "", + "components.TvDetails.watchlistError": "", + "components.TvDetails.watchlistSuccess": "", + "components.TvDetails.watchtrailer": "", + "components.UserList.accounttype": "", + "components.UserList.admin": "", + "components.UserList.autogeneratepasswordTip": "", + "components.UserList.bulkedit": "", + "components.UserList.created": "", + "components.UserList.creating": "", + "components.UserList.deleteconfirm": "", + "components.UserList.deleteuser": "", + "components.UserList.descending": "", + "components.UserList.importedPlexUsersNoPassword": "", + "components.UserList.importedUsersNoPassword": "", + "components.UserList.importedfromJellyfin": "", + "components.UserList.importedfromplex": "", + "components.UserList.importfromJellyfin": "", + "components.UserList.importfromJellyfinerror": "", + "components.UserList.importfrommediaserver": "", + "components.UserList.localLoginDisabled": "", + "components.UserList.mediaServerUser": "", + "components.UserList.newJellyfinsigninenabled": "", + "components.UserList.newplexsigninenabled": "", + "components.UserList.noJellyfinuserstoimport": "", + "components.UserList.sortBy": "", + "components.UserList.sortByJoined": "", + "components.UserList.sortByRequests": "", + "components.UserList.sortByRole": "", + "components.UserList.sortByType": "", + "components.UserList.sortByUser": "", + "components.UserList.toggleSortDirection": "", + "components.UserList.toggleSortDirectionAria": "", + "components.UserList.validationUsername": "", + "components.UserProfile.ProfileHeader.userid": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.description": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorExists": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorUnauthorized": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorUnknown": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.password": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.passwordRequired": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.save": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.saving": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.title": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.username": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.usernameRequired": "", + "components.UserProfile.UserSettings.UserGeneralSettings.accounttype": "", + "components.UserProfile.UserSettings.UserGeneralSettings.admin": "", + "components.UserProfile.UserSettings.UserGeneralSettings.applanguage": "", + "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegion": "", + "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegionTip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.enableOverride": "", + "components.UserProfile.UserSettings.UserGeneralSettings.general": "", + "components.UserProfile.UserSettings.UserGeneralSettings.generalsettings": "", + "components.UserProfile.UserSettings.UserGeneralSettings.languageDefault": "", + "components.UserProfile.UserSettings.UserGeneralSettings.localuser": "", + "components.UserProfile.UserSettings.UserGeneralSettings.movierequestlimit": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.nodevicestoshow": "", + "components.UserProfile.UserSettings.UserNotificationSettings.deviceDefault": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.email": "", + "components.UserProfile.UserSettings.UserNotificationSettings.emailsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.emailsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.notifications": "", + "components.UserProfile.UserSettings.UserNotificationSettings.notificationsettings": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pgpPublicKeyTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessToken": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessTokenTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoversettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.sendSilently": "", + "components.UserProfile.UserSettings.UserNotificationSettings.sendSilentlyDescription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.sound": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramChatId": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadIdTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPgpPublicKey": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPushbulletAccessToken": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverApplicationToken": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramChatId": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramMessageThreadId": "", + "components.UserProfile.UserSettings.UserNotificationSettings.webpush": "", + "components.UserProfile.UserSettings.UserPasswordChange.confirmpassword": "", + "components.UserProfile.UserSettings.UserPasswordChange.currentpassword": "", + "components.UserProfile.UserSettings.UserPasswordChange.localPasswordDescription": "", + "components.UserProfile.UserSettings.UserPasswordChange.noPasswordSet": "", + "components.UserProfile.UserSettings.UserPasswordChange.noPasswordSetOwnAccount": "", + "components.UserProfile.UserSettings.UserPasswordChange.nopermissionDescription": "", + "components.UserProfile.UserSettings.UserPasswordChange.password": "", + "components.UserProfile.UserSettings.UserPasswordChange.toastSettingsFailure": "", + "components.UserProfile.UserSettings.UserPasswordChange.toastSettingsFailureVerifyCurrent": "", + "components.UserProfile.UserSettings.UserPasswordChange.validationConfirmPassword": "", + "components.UserProfile.UserSettings.UserPasswordChange.validationNewPassword": "", + "components.UserProfile.UserSettings.UserPermissions.toastSettingsSuccess": "", + "components.UserProfile.UserSettings.UserPermissions.unauthorizedDescription": "", + "components.UserProfile.UserSettings.menuChangePass": "", + "components.UserProfile.UserSettings.menuGeneralSettings": "", + "components.UserProfile.UserSettings.menuLinkedAccounts": "", + "components.UserProfile.UserSettings.menuNotifications": "", + "components.UserProfile.UserSettings.unauthorizedDescription": "", + "components.UserProfile.limit": "", + "components.UserProfile.localWatchlist": "", + "components.UserProfile.movierequests": "", + "components.UserProfile.pastdays": "", + "components.UserProfile.plexwatchlist": "", + "components.UserProfile.recentlywatched": "", + "components.UserProfile.recentrequests": "", + "components.UserProfile.requestsperdays": "", + "components.UserProfile.seriesrequest": "", + "components.UserProfile.totalrequests": "", + "components.UserProfile.unlimited": "", + "i18n.addToBlocklist": "", + "i18n.all": "", + "i18n.areyousure": "", + "i18n.back": "", + "i18n.blocklistDuplicateError": "", + "i18n.blocklistError": "", + "i18n.blocklistSuccess": "", + "i18n.blocklisted": "", + "i18n.cancel": "", + "i18n.canceling": "", + "i18n.close": "", + "i18n.collection": "", + "i18n.completed": "", + "i18n.decline": "", + "i18n.declined": "", + "i18n.deleted": "", + "i18n.deleting": "", + "i18n.delimitedlist": "", + "i18n.edit": "", + "i18n.experimental": "", + "i18n.failed": "", + "i18n.import": "", + "i18n.importing": "", + "i18n.movie": "", + "i18n.movies": "", + "i18n.next": "", + "i18n.noresults": "", + "i18n.notificationLocale": "", + "i18n.previous": "", + "i18n.processing": "", + "i18n.removeFromBlocklistSuccess": "", + "i18n.removefromBlocklist": "", + "i18n.requesting": "", + "i18n.retry": "", + "i18n.specials": "", + "i18n.test": "", + "i18n.tvshow": "", + "i18n.tvshows": "", + "i18n.unavailable": "", + "i18n.usersettings": "", + "i18n.view": "", + "pages.errormessagewithcode": "", + "pages.internalservererror": "", + "pages.oops": "", + "pages.returnHome": "", + "components.RequestList.RequestItem.requesteddate": "", + "components.RequestModal.AdvancedRequester.languageprofile": "", + "components.RequestModal.pendingapproval": "", + "components.Settings.Notifications.pgpPassword": "", + "components.Settings.Notifications.toastEmailTestSuccess": "", + "components.Settings.Notifications.toastTelegramTestSuccess": "", + "components.Settings.SettingsJobsCache.dnsCacheGlobalStatsDescription": "", + "components.Settings.SettingsJobsCache.dnscacheactiveaddress": "", + "components.Settings.SonarrModal.testFirstRootFolders": "", + "components.Settings.SonarrModal.testFirstTags": "", + "components.Settings.jellyfinSettingsSuccess": "", + "components.Settings.notificationAgentSettingsDescription": "", + "i18n.status": "", + "components.UserProfile.UserSettings.menuPermissions": "", + "components.Settings.OverrideRuleModal.keywords": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverApplicationTokenTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverUserKey": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverUserKeyTip": "", + "pages.pagenotfound": "", + "components.Settings.SonarrModal.enableSearch": "", + "components.Settings.SonarrModal.externalUrl": "", + "components.Settings.SonarrModal.seasonfolders": "", + "components.TitleCard.tmdbid": "", + "components.Settings.SonarrModal.validationBaseUrlTrailingSlash": "", + "components.Settings.SonarrModal.validationHostnameRequired": "", + "components.StatusBadge.seasonepisodenumber": "", + "components.StatusBadge.seasonnumber": "", + "components.UserList.createlocaluser": "", + "components.UserProfile.UserSettings.UserPasswordChange.validationConfirmPasswordSame": "", + "components.UserProfile.emptywatchlist": "", + "i18n.blocklist": "", + "i18n.testing": "", + "components.MovieDetails.showmore": "", + "components.Settings.Notifications.NotificationsNtfy.topic": "", + "components.Settings.Notifications.NotificationsPushbullet.agentEnabled": "", + "components.Settings.Notifications.NotificationsPushbullet.channelTag": "", + "components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestFailed": "", + "components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestSending": "", + "components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestSuccess": "", + "components.Settings.Notifications.NotificationsPushbullet.validationAccessTokenRequired": "", + "components.Settings.Notifications.NotificationsPushbullet.validationTypes": "", + "components.Settings.Notifications.NotificationsPushover.accessTokenTip": "", + "components.Settings.Notifications.NotificationsPushover.pushoversettingsfailed": "", + "components.Settings.Notifications.NotificationsPushover.validationUserTokenRequired": "", + "components.Settings.Notifications.NotificationsSlack.agentenabled": "", + "components.Settings.Notifications.NotificationsSlack.embedPoster": "", + "components.Settings.Notifications.NotificationsSlack.slacksettingsfailed": "", + "components.Settings.SettingsJobsCache.cachemisses": "", + "components.Settings.Notifications.NotificationsSlack.slacksettingssaved": "", + "components.Settings.Notifications.NotificationsSlack.toastSlackTestFailed": "", + "components.Settings.Notifications.NotificationsSlack.toastSlackTestSending": "", + "components.Settings.Notifications.NotificationsSlack.toastSlackTestSuccess": "", + "components.Settings.Notifications.NotificationsSlack.validationTypes": "", + "components.Settings.Notifications.NotificationsSlack.webhookUrl": "", + "components.Settings.Notifications.NotificationsSlack.webhookUrlTip": "", + "components.Settings.Notifications.NotificationsWebhook.agentenabled": "", + "components.Settings.Notifications.NotificationsWebhook.authheader": "", + "components.Settings.Notifications.NotificationsWebhook.customHeaders": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersAdd": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersAuthConflict": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersIncomplete": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersKey": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersRemove": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersTip": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersValue": "", + "components.Settings.OverrideRuleTile.genre": "", + "components.Settings.SettingsJobsCache.hitRate": "", + "components.Settings.SettingsJobsCache.process-blocklisted-tags": "", + "components.Settings.SettingsNetwork.dnsCacheForceMinTtl": "", + "components.Settings.RadarrModal.tags": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverUserKey": "", + "components.NotificationTypeSelector.userissuecreatedDescription": "", + "components.Settings.Notifications.NotificationsNtfy.password": "", + "components.Settings.Notifications.NotificationsWebhook.webhookUrl": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pgpPublicKey": "", + "components.UserProfile.UserSettings.UserGeneralSettings.email": "", + "i18n.notrequested": "", + "i18n.open": "", + "i18n.partiallyavailable": "", + "i18n.request4k": "", + "i18n.saving": "", + "components.Settings.SettingsJobsCache.jobScheduleEditFailed": "", + "components.Settings.SettingsMain.applicationTitle": "", + "components.Settings.SettingsMain.originallanguage": "", + "components.Settings.SettingsMain.originallanguageTip": "", + "components.Settings.SettingsNetwork.proxyBypassFilterTip": "", + "components.Settings.SettingsNetwork.proxyBypassLocalAddresses": "", + "components.Settings.SonarrModal.animequalityprofile": "", + "components.Settings.SonarrModal.loadingTags": "", + "components.Settings.SonarrModal.notagoptions": "", + "components.Settings.serverpresetRefreshing": "", + "components.Settings.toastPlexRefresh": "", + "components.Settings.toastPlexRefreshFailure": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordsettingssaved": "", + "components.UserProfile.UserSettings.UserPasswordChange.newpassword": "", + "components.UserProfile.UserSettings.UserPasswordChange.toastSettingsSuccess": "", + "components.UserProfile.UserSettings.UserPasswordChange.validationCurrentPassword": "", + "i18n.advanced": "", + "i18n.approve": "", + "i18n.approved": "", + "components.MovieDetails.runtime": "", + "components.PermissionEdit.users": "", + "components.RequestModal.QuotaDisplay.season": "", + "components.RequestModal.pendingrequest": "", + "components.Settings.Notifications.emailsender": "", + "components.Settings.SettingsJobsCache.hits": "", + "components.Settings.SonarrModal.hostname": "", + "components.Settings.general": "", + "components.Settings.OverrideRuleTile.tags": "", + "components.Settings.SonarrModal.monitorNewItems": "", + "components.Settings.SonarrModal.qualityprofile": "", + "components.Settings.SonarrModal.rootfolder": "", + "components.Settings.SonarrModal.selectLanguageProfile": "", + "components.Settings.SonarrModal.selectQualityProfile": "", + "components.Settings.blocklistedTagImportTitle": "", + "components.Settings.SettingsJobsCache.sonarr-scan": "", + "components.Setup.setup": "", + "i18n.available": "", + "components.Settings.Notifications.encryptionNone": "", + "components.Discover.tmdbmoviestreamingservices": "", + "components.ManageSlideOver.plays": "", + "components.Selector.nooptions": "", + "components.Selector.pilot": "", + "components.Settings.webpush": "", + "components.UserList.ascending": "", + "i18n.pending": "", + "components.Settings.SettingsUsers.loginMethodsTip": "", + "components.Settings.Notifications.chatId": "", + "components.Settings.Notifications.webhookUrl": "", + "components.Settings.OverrideRuleModal.tags": "", + "components.Settings.SettingsAbout.totalrequests": "", + "components.Settings.chooseProvider": "", + "components.Settings.connectionTestFailed": "", + "components.Settings.currentlibrary": "", + "components.Settings.metadataSettings": "", + "components.Settings.webhook": "", + "components.UserProfile.UserSettings.UserGeneralSettings.displayName": "", + "components.UserProfile.UserSettings.UserGeneralSettings.streamingRegionTip": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.noLinkedAccounts": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleted": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverApplicationToken": "", + "components.Settings.Notifications.NotificationsWebhook.toastWebhookTestSending": "", + "components.Settings.Notifications.NotificationsWebhook.toastWebhookTestSuccess": "", + "components.Settings.Notifications.agentenabled": "", + "components.Settings.Notifications.encryptionImplicitTls": "", + "components.Settings.Notifications.userEmailRequired": "", + "components.Settings.Notifications.validationChatIdRequired": "", + "components.Settings.Notifications.validationPgpPassword": "", + "components.Settings.Notifications.validationPgpPrivateKey": "", + "components.Settings.Notifications.validationSmtpHostRequired": "", + "components.Settings.SettingsAbout.timezone": "", + "components.Settings.SettingsNetwork.proxySsl": "", + "components.Settings.SonarrModal.languageprofile": "", + "components.TvDetails.recommendations": "", + "components.TvDetails.seasonstitle": "", + "components.TvDetails.showtype": "", + "components.Settings.SettingsAbout.documentation": "", + "components.Discover.timeWindowWeek": "", + "components.Discover.timeWindowDay": "", + "components.Settings.Notifications.validationMessageThreadId": "", + "components.CollectionDetails.removefromblocklistpartialcount": "", + "components.IssueModal.issueOther": "", + "components.Settings.SonarrModal.defaultserver": "", + "components.Settings.port": "", + "components.Settings.tip": "", + "components.Login.tipEmailHasTrailingWhitespace": "", + "components.Login.tipUsernameHasTrailingWhitespace": "", + "components.Login.title": "", + "components.Login.urlBase": "", + "components.RequestList.RequestItem.mediaerror": "", + "components.Login.saving": "", + "components.Settings.SettingsAbout.Releases.releases": "", + "components.Settings.is4k": "", + "components.TvDetails.TvCast.fullseriescast": "", + "components.UserList.username": "" } diff --git a/src/i18n/locale/hr.json b/src/i18n/locale/hr.json index c6c3f8ace1..83de4a3c34 100644 --- a/src/i18n/locale/hr.json +++ b/src/i18n/locale/hr.json @@ -435,8 +435,6 @@ "components.UserProfile.UserSettings.UserGeneralSettings.accounttype": "Vrsta računa", "components.UserProfile.UserSettings.UserGeneralSettings.admin": "Administrator", "components.UserProfile.UserSettings.UserGeneralSettings.applanguage": "Jezik prikaza", - "components.UserProfile.UserSettings.UserGeneralSettings.discordId": "Korisnički ID na Discordu", - "components.UserProfile.UserSettings.UserGeneralSettings.discordIdTip": "Višeznamenkasti ID broj povezan s tvojim korisničkim računom na Discordu", "components.UserProfile.UserSettings.UserGeneralSettings.displayName": "Prikazano ime", "components.UserProfile.UserSettings.UserGeneralSettings.enableOverride": "Mijenjanje globalnih ograničenja", "components.UserProfile.UserSettings.UserGeneralSettings.general": "Opće", @@ -955,7 +953,6 @@ "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailure": "Dogodila se greška prilikom spremanja postavki.", "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsSuccess": "Postavke su uspješno spremljene!", "components.UserProfile.UserSettings.UserGeneralSettings.user": "Korisnik", - "components.UserProfile.UserSettings.UserGeneralSettings.validationDiscordId": "Moraš navesti valjani Discord korisnički ID", "components.UserProfile.UserSettings.UserNotificationSettings.discordId": "ID korisnika", "components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "Višeznamenkasti ID broj povezan s tvojim korisničkim računom", "components.UserProfile.UserSettings.UserNotificationSettings.discordsettingsfailed": "Neuspjelo spremanje Discord postavki obavijesti.", @@ -1195,5 +1192,481 @@ "components.Discover.createnewslider": "Stvori novi klizač", "components.Settings.SettingsMain.partialRequestsEnabled": "Dopusti djelomične zahtjeve za seriju", "components.Settings.SonarrModal.tagRequests": "Označi zahtjeve", - "components.Settings.RadarrModal.tagRequests": "Označi zahtjeve" + "components.Settings.RadarrModal.tagRequests": "Označi zahtjeve", + "components.Setup.signin": "", + "components.MovieDetails.openradarr4k": "", + "components.MovieDetails.removefromwatchlist": "", + "components.CollectionDetails.removefromblocklistpartialcount": "", + "components.Discover.FilterSlideover.status": "", + "components.DiscoverTvUpcoming.upcomingtv": "", + "components.Login.invalidurlerror": "", + "components.Login.noadminerror": "", + "components.Login.orsigninwith": "", + "components.Login.save": "", + "components.Login.saving": "", + "components.Login.servertype": "", + "components.Discover.timeWindowDay": "", + "components.Discover.timeWindowWeek": "", + "components.Login.title": "", + "component.BlocklistBlock.blocklistdate": "", + "component.BlocklistBlock.blocklistedby": "", + "component.BlocklistModal.blocklisting": "", + "components.Blocklist.blocklistNotFoundError": "", + "components.Blocklist.blocklistSettingsDescription": "", + "components.Blocklist.blocklistdate": "", + "components.Blocklist.blocklistedby": "", + "components.Blocklist.blocklistsettings": "", + "components.Blocklist.filterBlocklistedTags": "", + "components.Blocklist.filterManual": "", + "components.Blocklist.mediaName": "", + "components.Blocklist.showAllBlocklisted": "", + "components.Layout.Sidebar.blocklist": "", + "components.Discover.FilterSlideover.excludeKeywords": "", + "components.Login.enablessl": "", + "components.Discover.RecentRequestsSlider.unableToConnect": "", + "components.Blocklist.mediaTmdbId": "", + "components.Blocklist.mediaType": "", + "components.Settings.OverrideRuleModal.selectRootFolder": "", + "components.Discover.FilterSlideover.certification": "", + "components.Layout.UserWarnings.profileIncomplete": "", + "components.Login.emailAlreadyTaken": "", + "components.Login.jellyfinLocalLoginHint": "", + "components.Login.saveFailed": "", + "components.Login.signinwithjellyfin": "", + "components.Login.urlBase": "", + "components.ManageSlideOver.manageModalRemoveMediaWarning": "", + "components.ManageSlideOver.removearr": "", + "components.ManageSlideOver.removearr4k": "", + "components.MetadataSelector.selectMetdataProvider": "", + "components.MetadataSelector.tmdbLabel": "", + "components.MetadataSelector.tvdbLabel": "", + "components.MovieDetails.addtowatchlist": "", + "components.MovieDetails.downloadstatus": "", + "components.MovieDetails.openradarr": "", + "components.MovieDetails.play": "", + "components.MovieDetails.watchlistDeleted": "", + "components.MovieDetails.watchlistError": "", + "components.MovieDetails.watchlistSuccess": "", + "components.PermissionEdit.blocklistedItems": "", + "components.PermissionEdit.blocklistedItemsDescription": "", + "components.PermissionEdit.manageblocklist": "", + "components.PermissionEdit.manageblocklistDescription": "", + "components.PermissionEdit.viewblocklistedItems": "", + "components.PermissionEdit.viewblocklistedItemsDescription": "", + "components.RequestList.RequestItem.failedmodify": "", + "components.RequestList.RequestItem.profileName": "", + "components.RequestList.RequestItem.removearr": "", + "components.RequestList.sortDirection": "", + "components.Selector.CertificationSelector.errorLoading": "", + "components.Selector.CertificationSelector.maxRating": "", + "components.Selector.CertificationSelector.minRating": "", + "components.Selector.CertificationSelector.noOptions": "", + "components.Selector.CertificationSelector.selectCertification": "", + "components.Selector.CertificationSelector.selectCountry": "", + "components.Selector.CertificationSelector.starttyping": "", + "components.Selector.canceled": "", + "components.Selector.ended": "", + "components.Selector.inProduction": "", + "components.Selector.pilot": "", + "components.Selector.planned": "", + "components.Selector.returningSeries": "", + "components.Selector.searchUsers": "", + "components.Settings.Notifications.NotificationsGotify.priority": "", + "components.Settings.Notifications.NotificationsNtfy.ntfysettingsfailed": "", + "components.Settings.Notifications.NotificationsNtfy.ntfysettingssaved": "", + "components.Settings.Notifications.NotificationsNtfy.password": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestFailed": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestSending": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestSuccess": "", + "components.Settings.Notifications.NotificationsNtfy.token": "", + "components.Settings.Notifications.NotificationsNtfy.topic": "", + "components.Settings.Notifications.NotificationsNtfy.url": "", + "components.Settings.Notifications.NotificationsNtfy.username": "", + "components.Settings.Notifications.NotificationsNtfy.usernamePasswordAuth": "", + "components.Settings.Notifications.NotificationsNtfy.validationNtfyTopic": "", + "components.Settings.Notifications.NotificationsNtfy.validationNtfyUrl": "", + "components.Settings.Notifications.NotificationsNtfy.validationPriorityRequired": "", + "components.Settings.Notifications.NotificationsNtfy.validationTypes": "", + "components.Settings.SettingsMain.blocklistedTagsLimitTip": "", + "components.Settings.SettingsNetwork.proxyPort": "", + "components.Settings.SettingsNetwork.proxySsl": "", + "components.Settings.SettingsUsers.disabledMediaServerLoginWarning": "", + "components.Settings.menuNetwork": "", + "components.Settings.metadataProviderSelection": "", + "components.Settings.metadataProviderSettings": "", + "components.Settings.metadataSettings": "", + "components.Settings.scanbackground": "", + "components.Settings.searchKeywords": "", + "components.Settings.seriesMetadataProvider": "", + "components.Settings.valueRequired": "", + "components.Setup.librarieserror": "", + "components.Setup.servertype": "", + "components.Setup.signinWithEmby": "", + "components.Setup.signinWithJellyfin": "", + "components.Setup.signinWithPlex": "", + "components.Setup.subtitle": "", + "components.TitleCard.addToWatchList": "", + "components.TvDetails.watchlistError": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.description": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmail": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmailEmpty": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleted": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleteerror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.type": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.unknown": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingsfailed": "", + "i18n.error": "", + "i18n.partiallyblocklisted": "", + "components.UserProfile.UserSettings.UserGeneralSettings.mediaServerUser": "", + "components.UserProfile.UserSettings.UserGeneralSettings.streamingRegion": "", + "components.UserProfile.UserSettings.UserGeneralSettings.validationemailformat": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.linkedAccounts": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.linkedAccountsHint": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.noLinkedAccounts": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.noPermissionDescription": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorUnauthorized": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.deletesubscription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.device": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablewebpush": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablingwebpusherror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablewebpush": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.managedevices": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.nodevicestoshow": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.operatingsystem": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeenenabled": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdAdd": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdPlaceholder": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdRemove": "", + "components.Login.port": "", + "components.Settings.OverrideRuleTile.language": "", + "components.Layout.UserWarnings.emailRequired": "", + "components.Layout.UserWarnings.passwordRequired": "", + "components.Layout.VersionStatus.commitsbehind": "", + "components.Login.adminerror": "", + "components.Login.credentialerror": "", + "components.Login.emailtooltip": "", + "components.Login.description": "", + "components.Login.hostname": "", + "components.Login.initialsignin": "", + "components.Login.initialsigningin": "", + "components.Login.loginwithapp": "", + "components.Login.urlBaseHelp": "", + "components.Login.username": "", + "components.Login.validationEmailFormat": "", + "components.Login.validationEmailRequired": "", + "components.Login.validationPortRequired": "", + "components.Login.validationUrlBaseLeadingSlash": "", + "components.Login.validationUrlBaseTrailingSlash": "", + "components.Login.validationUrlTrailingSlash": "", + "components.Login.validationemailformat": "", + "components.Login.validationhostformat": "", + "components.Login.validationhostrequired": "", + "components.Login.validationservertyperequired": "", + "components.Login.validationusernamerequired": "", + "components.MovieDetails.play4k": "", + "components.RequestList.unableToConnect": "", + "components.Settings.Notifications.NotificationsGotify.validationPriorityRequired": "", + "components.Settings.Notifications.NotificationsNtfy.priority": "", + "components.Settings.Notifications.NotificationsPushover.embedPoster": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersIncomplete": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersRemove": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersValue": "", + "components.Settings.Notifications.messageThreadIdTip": "", + "components.Settings.SettingsJobsCache.flushdnscache": "", + "components.Settings.SettingsNetwork.proxyUser": "", + "components.Settings.SonarrModal.baseUrlHelp": "", + "components.Settings.jellyfinSettingsFailure": "", + "components.Settings.notTested": "", + "components.UserList.ascending": "", + "components.UserList.importfromJellyfin": "", + "components.UserList.noJellyfinuserstoimport": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.created": "", + "i18n.blocklistError": "", + "components.Settings.Notifications.NotificationsSlack.embedPoster": "", + "components.Settings.Notifications.NotificationsWebPush.embedPoster": "", + "components.Settings.Notifications.NotificationsWebhook.customHeaders": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersAdd": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersAuthConflict": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersKey": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersTip": "", + "components.Settings.Notifications.NotificationsWebhook.supportVariables": "", + "components.Settings.Notifications.NotificationsWebhook.supportVariablesTip": "", + "components.Settings.Notifications.NotificationsWebhook.webhookUrlTip": "", + "components.Settings.SettingsMain.blocklistLanguageTip": "", + "components.Settings.SettingsNetwork.validationApiRequestTimeout": "", + "components.Settings.copyBlocklistedTagsEmpty": "", + "components.Settings.copyBlocklistedTagsTip": "", + "components.Settings.failed": "", + "components.Settings.failedToSaveMetadataSettings": "", + "components.Settings.general": "", + "components.Settings.importBlocklistedTagsTip": "", + "components.Settings.invalidKeyword": "", + "components.Settings.invalidurlerror": "", + "components.Settings.jellyfinForgotPasswordUrl": "", + "components.Settings.jellyfinSettingsSuccess": "", + "components.Settings.tip": "", + "components.Settings.tmdbProviderDoesnotWork": "", + "components.Settings.yes": "", + "components.UserList.importedPlexUsersNoPassword": "", + "components.UserList.importedfromJellyfin": "", + "components.UserList.importfromJellyfinerror": "", + "components.UserProfile.UserSettings.UserGeneralSettings.streamingRegionTip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.validationemailrequired": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.deleteFailed": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.errorUnknown": "", + "i18n.specials": "", + "components.Settings.SettingsMain.blocklistedTagsLimit": "", + "components.RequestCard.failedmodify": "", + "components.Settings.Notifications.NotificationsNtfy.agentenabled": "", + "components.Settings.Notifications.NotificationsNtfy.embedPoster": "", + "components.Settings.Notifications.usePublicLogo": "", + "components.Settings.Notifications.usePublicLogoTip": "", + "components.Settings.Notifications.useUserLocale": "", + "components.Settings.Notifications.userEmailRequired": "", + "components.Settings.Notifications.validationMessageThreadId": "", + "components.Settings.Notifications.validationWebhookRoleId": "", + "components.Settings.Notifications.webhookRoleId": "", + "components.Settings.Notifications.webhookRoleIdTip": "", + "components.Settings.OverrideRuleModal.conditions": "", + "components.Settings.OverrideRuleModal.conditionsDescription": "", + "components.Settings.OverrideRuleModal.create": "", + "components.Settings.OverrideRuleModal.createrule": "", + "components.Settings.OverrideRuleModal.editrule": "", + "components.Settings.OverrideRuleModal.genres": "", + "components.Settings.OverrideRuleModal.keywords": "", + "components.Settings.OverrideRuleModal.languages": "", + "components.Settings.OverrideRuleModal.notagoptions": "", + "components.Settings.OverrideRuleModal.qualityprofile": "", + "components.Settings.OverrideRuleModal.rootfolder": "", + "components.Settings.OverrideRuleModal.ruleCreated": "", + "components.Settings.OverrideRuleModal.ruleUpdated": "", + "components.Settings.OverrideRuleModal.selectQualityProfile": "", + "components.Settings.OverrideRuleModal.selectService": "", + "components.Settings.OverrideRuleModal.selecttags": "", + "components.Settings.OverrideRuleModal.service": "", + "components.Settings.OverrideRuleModal.serviceDescription": "", + "components.Settings.OverrideRuleModal.settings": "", + "components.Settings.OverrideRuleModal.settingsDescription": "", + "components.Settings.OverrideRuleModal.tags": "", + "components.Settings.OverrideRuleModal.users": "", + "components.Settings.OverrideRuleTile.conditions": "", + "components.Settings.OverrideRuleTile.genre": "", + "components.Settings.OverrideRuleTile.keywords": "", + "components.Settings.OverrideRuleTile.qualityprofile": "", + "components.Settings.OverrideRuleTile.rootfolder": "", + "components.Settings.OverrideRuleTile.settings": "", + "components.Settings.OverrideRuleTile.tags": "", + "components.Settings.OverrideRuleTile.users": "", + "components.Settings.RadarrModal.apiKeyHelp": "", + "components.Settings.RadarrModal.baseUrlHelp": "", + "components.Settings.RadarrModal.enableSearchHelp": "", + "components.Settings.RadarrModal.externalUrlHelp": "", + "components.Settings.RadarrModal.server4kHelp": "", + "components.Settings.RadarrModal.syncEnabledHelp": "", + "components.Settings.SettingsAbout.contribute": "", + "components.Settings.SettingsAbout.supportseerr": "", + "components.Settings.SettingsJobsCache.dnsCache": "", + "components.Settings.SettingsJobsCache.dnsCacheDescription": "", + "components.Settings.SettingsJobsCache.dnsCacheGlobalStats": "", + "components.Settings.SettingsJobsCache.dnsCacheGlobalStatsDescription": "", + "components.Settings.SettingsJobsCache.dnsNoCacheEntries": "", + "components.Settings.SettingsJobsCache.dnscacheactiveaddress": "", + "components.Settings.SettingsJobsCache.dnscacheage": "", + "components.Settings.SettingsJobsCache.dnscacheflushed": "", + "components.Settings.SettingsJobsCache.dnscachehits": "", + "components.Settings.SettingsJobsCache.dnscachemisses": "", + "components.Settings.SettingsJobsCache.dnscachename": "", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorDays": "", + "components.Settings.SettingsJobsCache.failures": "", + "components.Settings.SettingsJobsCache.hitRate": "", + "components.Settings.SettingsJobsCache.hits": "", + "components.Settings.SettingsJobsCache.ipv4Fallbacks": "", + "components.Settings.SettingsJobsCache.jellyfin-full-scan": "", + "components.Settings.SettingsJobsCache.jellyfin-recently-added-scan": "", + "components.Settings.SettingsJobsCache.misses": "", + "components.Settings.SettingsJobsCache.plex-refresh-token": "", + "components.Settings.SettingsJobsCache.process-blocklisted-tags": "", + "components.Settings.SettingsJobsCache.size": "", + "components.Settings.SettingsJobsCache.usersavatars": "", + "components.Settings.SettingsMain.apikeyCopied": "", + "components.Settings.SettingsMain.blocklistLanguage": "", + "components.Settings.SettingsMain.blocklistRegion": "", + "components.Settings.SettingsMain.blocklistRegionTip": "", + "components.Settings.SettingsMain.blocklistedTags": "", + "components.Settings.SettingsMain.blocklistedTagsTip": "", + "components.Settings.SettingsMain.discoverRegion": "", + "components.Settings.SettingsMain.discoverRegionTip": "", + "components.Settings.SettingsMain.enableSpecialEpisodes": "", + "components.Settings.SettingsMain.hideAvailableTip": "", + "components.Settings.SettingsMain.hideBlocklisted": "", + "components.Settings.SettingsMain.hideBlocklistedTip": "", + "components.Settings.SettingsMain.streamingRegion": "", + "components.Settings.SettingsMain.streamingRegionTip": "", + "components.Settings.SettingsMain.validationUrl": "", + "components.Settings.SettingsMain.validationUrlTrailingSlash": "", + "components.Settings.SettingsMain.youtubeUrl": "", + "components.Settings.SettingsMain.youtubeUrlTip": "", + "components.Settings.SettingsNetwork.apiRequestTimeoutTip": "", + "components.Settings.SettingsNetwork.csrfProtectionHoverTip": "", + "components.Settings.SettingsNetwork.dnsCache": "", + "components.Settings.SettingsNetwork.dnsCacheForceMinTtl": "", + "components.Settings.SettingsNetwork.dnsCacheHoverTip": "", + "components.Settings.SettingsNetwork.forceIpv4First": "", + "components.Settings.SettingsNetwork.networksettings": "", + "components.Settings.SettingsNetwork.networksettingsDescription": "", + "components.Settings.SettingsNetwork.proxyBypassFilterTip": "", + "components.Settings.SettingsNetwork.proxyBypassLocalAddresses": "", + "components.Settings.SettingsNetwork.proxyEnabled": "", + "components.Settings.SettingsNetwork.proxyEnabledTip": "", + "components.Settings.SettingsNetwork.proxyHostname": "", + "components.Settings.SettingsNetwork.proxyPassword": "", + "components.Settings.SettingsNetwork.toastSettingsFailure": "", + "components.Settings.SettingsNetwork.trustProxyTip": "", + "components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "", + "components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "", + "components.Settings.SettingsNetwork.validationProxyPort": "", + "components.Settings.SettingsUsers.atLeastOneAuth": "", + "components.Settings.SettingsUsers.loginMethodsTip": "", + "components.Settings.SettingsUsers.mediaServerLogin": "", + "components.Settings.SettingsUsers.mediaServerLoginTip": "", + "components.Settings.SonarrModal.apiKeyHelp": "", + "components.Settings.SonarrModal.enableSearchHelp": "", + "components.Settings.SonarrModal.externalUrlHelp": "", + "components.Settings.SonarrModal.monitorNewItemsHelp": "", + "components.Settings.SonarrModal.server4kHelp": "", + "components.Settings.SonarrModal.syncEnabledHelp": "", + "components.Settings.addrule": "", + "components.Settings.blocklistedTagsText": "", + "components.Settings.chooseProvider": "", + "components.Settings.clearBlocklistedTagsConfirm": "", + "components.Settings.clickTest": "", + "components.Settings.connectionTestFailed": "", + "components.Settings.copyBlocklistedTags": "", + "components.Settings.jellyfinSettingsDescription": "", + "components.Settings.jellyfinSyncFailedAutomaticGroupedFolders": "", + "components.Settings.jellyfinlibraries": "", + "components.Settings.jellyfinlibrariesDescription": "", + "components.Settings.jellyfinsettings": "", + "components.Settings.jellyfinsettingsDescription": "", + "components.Settings.manualscanDescriptionJellyfin": "", + "components.Settings.menuJellyfinSettings": "", + "components.Settings.no": "", + "components.Settings.noSpecialCharacters": "", + "components.Settings.nooptions": "", + "components.Settings.operational": "", + "components.Settings.overrideRules": "", + "components.Settings.overrideRulesDescription": "", + "components.Settings.save": "", + "components.Settings.saving": "", + "components.Settings.starttyping": "", + "components.Settings.syncJellyfin": "", + "components.Settings.syncing": "", + "components.Settings.timeout": "", + "components.Settings.tvdbProviderDoesnotWork": "", + "components.Setup.back": "", + "components.Setup.configemby": "", + "components.Setup.configjellyfin": "", + "components.Setup.configplex": "", + "components.Setup.configuremediaserver": "", + "components.StatusBadge.seasonnumber": "", + "components.TitleCard.watchlistCancel": "", + "components.TitleCard.watchlistDeleted": "", + "components.TitleCard.watchlistError": "", + "components.TitleCard.watchlistSuccess": "", + "components.TvDetails.addtowatchlist": "", + "components.TvDetails.play": "", + "components.TvDetails.play4k": "", + "components.TvDetails.removefromwatchlist": "", + "components.TvDetails.watchlistDeleted": "", + "components.TvDetails.watchlistSuccess": "", + "components.UserList.descending": "", + "components.UserList.importedUsersNoPassword": "", + "components.UserList.importfrommediaserver": "", + "components.UserList.mediaServerUser": "", + "components.UserList.newJellyfinsigninenabled": "", + "components.UserList.sortByJoined": "", + "components.UserList.sortByUser": "", + "components.UserList.toggleSortDirection": "", + "components.UserList.username": "", + "components.UserList.validationUsername": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorExists": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorUnauthorized": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorUnknown": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.password": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.passwordRequired": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.save": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.saving": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.title": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.usernameRequired": "", + "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegion": "", + "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegionTip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.email": "", + "components.UserProfile.UserSettings.UserGeneralSettings.save": "", + "components.UserProfile.UserSettings.UserGeneralSettings.saving": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablingwebpusherror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.engine": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadId": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadIdTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramMessageThreadId": "", + "components.UserProfile.UserSettings.UserPasswordChange.localPasswordDescription": "", + "components.UserProfile.UserSettings.menuLinkedAccounts": "", + "i18n.addToBlocklist": "", + "i18n.blocklist": "", + "i18n.blocklistDuplicateError": "", + "i18n.blocklistSuccess": "", + "i18n.blocklisted": "", + "i18n.completed": "", + "i18n.deleted": "", + "i18n.notificationLocale": "", + "i18n.removeFromBlocklistSuccess": "", + "i18n.removefromBlocklist": "", + "components.UserList.sortBy": "", + "components.Settings.allChosenProvidersAreOperational": "", + "components.UserProfile.localWatchlist": "", + "components.Selector.searchStatus": "", + "components.Settings.Notifications.NotificationsNtfy.tokenAuth": "", + "components.Settings.SettingsNetwork.apiRequestTimeout": "", + "components.Settings.jellyfinSyncFailedNoLibrariesFound": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.username": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorExists": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeendisabled": "", + "components.UserList.sortByRequests": "", + "components.UserList.sortByRole": "", + "components.UserList.sortByType": "", + "components.Settings.providerStatus": "", + "components.Login.tipUsernameHasTrailingWhitespace": "", + "components.Settings.SettingsNetwork.forceIpv4FirstTip": "", + "components.Settings.SettingsNetwork.networkDisclaimer": "", + "components.Settings.SettingsNetwork.toastSettingsSuccess": "", + "components.Settings.SettingsNetwork.trustProxy": "", + "components.Settings.SettingsUsers.loginMethods": "", + "components.Settings.SonarrModal.monitorNewItems": "", + "components.Settings.animeMetadataProvider": "", + "components.Settings.apiKey": "", + "components.Settings.blocklistedTagImportInstructions": "", + "components.Settings.blocklistedTagImportTitle": "", + "components.Settings.jellyfinSettings": "", + "components.UserList.toggleSortDirectionAria": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.browser": "", + "components.Settings.SettingsNetwork.csrfProtectionTip": "", + "components.Settings.SettingsNetwork.csrfProtection": "", + "components.Settings.SettingsNetwork.dnsCacheForceMaxTtl": "", + "components.Settings.SettingsNetwork.dnsCacheTip": "", + "components.Settings.SettingsNetwork.docs": "", + "components.Settings.SettingsNetwork.network": "", + "components.Settings.menuMetadataProviders": "", + "components.Settings.SettingsNetwork.proxyBypassFilter": "", + "components.Settings.Notifications.messageThreadId": "", + "components.Settings.Notifications.embedPoster": "", + "components.Settings.settings": "", + "components.Settings.jellyfinSyncFailedGenericError": "", + "components.IssueList.IssueItem.descriptionpreview": "", + "components.Settings.manualscanJellyfin": "", + "components.Layout.UserWarnings.emailInvalid": "", + "components.Settings.metadataSettingsSaved": "", + "components.Settings.SettingsAbout.aboutseerr": "", + "components.Login.tipEmailHasTrailingWhitespace": "", + "components.Login.back": "" } diff --git a/src/i18n/locale/hu.json b/src/i18n/locale/hu.json index eea9b76c71..d7e7791a62 100644 --- a/src/i18n/locale/hu.json +++ b/src/i18n/locale/hu.json @@ -220,7 +220,7 @@ "components.UserProfile.UserSettings.UserNotificationSettings.webpush": "Web Push", "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramChatId": "Meg kell adnia egy érvényes chat azonosítót", "components.UserProfile.UserSettings.UserNotificationSettings.validationPgpPublicKey": "Érvényes PGP- kulcsot kell megadnia", - "components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "Érvényes felhasználói azonosítót kell megadnia", + "components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "Minden azonosító érvényes Discord felhasználói azonosító kell legyen", "components.UserProfile.UserSettings.UserNotificationSettings.telegramsettingssaved": "Telegram értesítés beállítások mentése sikeres!", "components.UserProfile.UserSettings.UserNotificationSettings.telegramsettingsfailed": "A Telegram-értesítés beállításait nem sikerült elmenteni.", "components.UserProfile.UserSettings.UserNotificationSettings.telegramChatIdTipLong": "Csevegés indítása, @get_id_bot hozzáadása, és a /my_id parancs kiadása", @@ -236,8 +236,8 @@ "components.UserProfile.UserSettings.UserNotificationSettings.email": "E-mail", "components.UserProfile.UserSettings.UserNotificationSettings.discordsettingssaved": "Discord értesítés beállítások sikeresen mentve!", "components.UserProfile.UserSettings.UserNotificationSettings.discordsettingsfailed": "A Discord értesítés beállításait nem sikerült elmenteni.", - "components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "A felhasználói fiók azonosítószáma", - "components.UserProfile.UserSettings.UserNotificationSettings.discordId": "Felhasználói azonosító", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "A felhasználói fiókjához tartozó többjegyű azonosítószám . Több háztartási fiók esetén több Discord‑felhasználói azonosítót is hozzáadhat.", + "components.UserProfile.UserSettings.UserNotificationSettings.discordId": "Felhasználói azonosítók", "components.UserProfile.UserSettings.UserGeneralSettings.user": "Felhasználó", "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsSuccess": "A beállítások sikeresen mentve!", "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailure": "Valami elromlott a beállítások mentése közben.", @@ -279,7 +279,7 @@ "components.Setup.setup": "Beállítás", "components.Settings.webpush": "Web Push", "components.Settings.webhook": "Webhook", - "components.Settings.webAppUrlTip": "Opcionálisan átirányíthatja a felhasználókat a szerveren lévő webes alkalmazáshoz a \"hosztolt\" webes alkalmazás helyett", + "components.Settings.webAppUrlTip": "Opcionálisan átirányíthatja a felhasználókat a szerveren lévő webes alkalmazáshoz a https://app.plex.tv/desktop helyett", "components.Settings.webAppUrl": "Web App URL", "components.Settings.validationPortRequired": "Érvényes portszámot kell megadnia", "components.Settings.validationHostnameRequired": "Érvényes hosztnevet vagy IP-címet kell megadnia", @@ -316,7 +316,7 @@ "components.Settings.notifications": "Értesítések", "components.Settings.notificationAgentSettingsDescription": "Értesítési ügynökök konfigurálása és engedélyezése.", "components.Settings.noDefaultServer": "Legalább egy {serverType} szervert meg kell jelölni alapértelmezettként a {mediaType} kérések feldolgozásához.", - "components.Settings.noDefaultNon4kServer": "Ha csak egyetlen {serverType} szervert használmind a nem 4K, mind a 4K tartalomhoz (vagy ha csak 4K tartalmat tölt le), akkor a {serverType} szervert NEM kell 4K szerverként kijelölni.", + "components.Settings.noDefaultNon4kServer": "Ha csak egyetlen {serverType} szervert használ mind a nem 4K, mind a 4K tartalomhoz (vagy ha csak 4K tartalmat tölt le), akkor a {serverType} szervert NEM kell 4K szerverként kijelölni.", "components.Settings.noDefault4kServer": "A 4K {serverType} szervert alapértelmezettként kell megjelölni, hogy a felhasználók 4K {mediaType} kéréseket nyújthassanak be.", "components.Settings.menuUsers": "Felhasználók", "components.Settings.menuServices": "Szolgáltatások", @@ -891,12 +891,9 @@ "i18n.open": "Megnyitva", "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessToken": "Hozzáférési Token", "components.Settings.Notifications.NotificationsGotify.gotifysettingssaved": "Gotify értesítési beállítások sikeresen mentve!", - "components.UserProfile.UserSettings.UserGeneralSettings.discordIdTip": "A többjegyű azonosítószám, amely az Ön Discord felhasználói fiókjához kapcsolódik", "components.Settings.RadarrModal.released": "Megjelent", "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessTokenTip": "Hozzon létre egy tokent a Fiókbeállításokban", - "components.UserProfile.UserSettings.UserGeneralSettings.validationDiscordId": "Egy érvényes Discord felhasználói azonosítót kell megadnia", "components.RequestModal.selectmovies": "Film(ek) Kiválasztása", - "components.UserProfile.UserSettings.UserGeneralSettings.discordId": "Discord Felhasználó ID", "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletsettingssaved": "A Pushbullet értesítési beállításai sikeresen mentve lettek!", "components.ManageSlideOver.playedby": "Lejátszotta", "components.NotificationTypeSelector.adminissuecommentDescription": "Értesítést kap, ha más felhasználók hozzászólnak a problémákhoz.", @@ -1451,7 +1448,7 @@ "components.Setup.configjellyfin": "Jellyfin beállítása", "components.Setup.configplex": "Plex beállítása", "components.Setup.configuremediaserver": "Médiszerver beállítása", - "components.Setup.librarieserror": "Az érvényesítés sikertelen. Kérjük, kapcsolja be újra a könyvtárakat a folytatáshoz.", + "components.Setup.librarieserror": "Sikertelen a könyvtárak betöltése a médiaszerveréről. Ellenőrizze, hogy elérhető-e és továbbra is helyesen beállított-e.", "components.Setup.servertype": "Válassza ki a szerver típusát", "components.Setup.signin": "Bejelentkezés", "components.Setup.signinWithEmby": "Adja meg az Emby szerverének adatait", @@ -1666,5 +1663,48 @@ "components.UserList.importedPlexUsersNoPassword": "Az importált felhasználóknak nincs beállítva {applicationTitle}-jelszavuk. Ha letiltod a Plex‑es bejelentkezést, jelszót kell majd beállítaniuk a profiljukban, vagy egy jelszó‑visszaállító linken keresztül.", "components.UserList.importedUsersNoPassword": "Az importált felhasználóknak nincs beállítva {applicationTitle}-jelszavuk. Ha letiltod a {mediaServerName}-es bejelentkezést, jelszót kell majd beállítaniuk a profiljukban, vagy egy jelszó‑visszaállító linken keresztül.", "components.UserProfile.UserSettings.UserPasswordChange.localPasswordDescription": "Ez a jelszó a {applicationTitle} helyi bejelentkezésénél kerül felhasználásra. Különbözik a médiaszerver jelszavától.", - "i18n.notificationLocale": "Értesítés nyelve" + "i18n.notificationLocale": "Értesítés nyelve", + "components.Settings.Notifications.usePublicLogo": "Az egyedi logó helyett a Seerr nyilvános logóját használja", + "components.Settings.Notifications.usePublicLogoTip": "Ha a Seerr-példánya nyilvánosan nem elérhető, kapcsolja be ezt az opciót, hogy a hálózatán kívüli e-mail kliensek is láthassák a logót. A logó a nyilvános GitHub-tárhelyről kerül letöltésre.", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdAdd": "Felhasználói azonosító hozzáadása", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdPlaceholder": "Discord felhasználói azonosító", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdRemove": "Eltávolítás", + "components.Settings.Notifications.validationWebhookThreadId": "Meg kell adnia egy érvényes Discord-szál azonosítót", + "components.Settings.Notifications.webhookThreadId": "Szál azonosító", + "components.Settings.Notifications.webhookThreadIdTip": "A szálcsatorna azonosítója, amelybe az értesítéseket küldjük. Ha üresen hagyja, akkor a webhook-csatornába kerülnek", + "components.UserProfile.UserSettings.UserNotificationSettings.discordNotificationsNotEnabled": "A szerver tulajdonosa nem engedélyezte a Discord-értesítéseket. Ez az információ csak akkor kerül felhasználásra, ha a szerver tulajdonosa beállít egy külső szolgáltatást.", + "components.RequestModal.AdvancedRequester.ignoreQuotaDescription": "Ez a kérés nem számít bele a felhasználó kvótájába. Kérjük, óvatosan használja.", + "components.RequestModal.AdvancedRequester.ignoreQuotaTitle": "Felhasználói kvóta megkerülése", + "i18n.syncing": "Szinkronizálás…", + "components.Common.QuickConnectModal.error": "Hiba", + "components.Common.QuickConnectModal.expired": "A kód lejárt", + "components.Common.QuickConnectModal.expiredMessage": "Ez a Quick Connect-kód lejárt. Kérjük, próbálja meg újra.", + "components.Common.QuickConnectModal.tryAgain": "Próbálja meg újra", + "components.Common.QuickConnectModal.waitingForAuth": "Engedélyezésre vár...", + "components.Login.JellyfinQuickConnectModal.cancel": "Mégsem", + "components.Login.JellyfinQuickConnectModal.instructions": "Írja be ezt a kódot a {mediaServerName} alkalmazásba", + "components.Login.JellyfinQuickConnectModal.subtitle": "Bejelentkezés Quick Connect segítségével", + "components.Login.JellyfinQuickConnectModal.title": "Quick Connect", + "components.Login.quickconnect": "Quick Connect", + "components.Login.quickconnecterror": "A Quick Connect művelet sikertelen volt. Kérjük, próbálja meg újra.", + "components.UserList.importfromplexnochanges": "A Plex-import befejeződött. Nem jöttek létre és nem frissültek felhasználók.", + "components.UserList.importfromplexsynced": "A Plex-felhasználók szinkronizálása sikeresen megtörtént. A meglévő felhasználók adatai frissültek.", + "components.UserList.syncnotice": "A „Szinkronizálás” gombra kattintva továbbra is frissítheti a meglévő Plex-felhasználók adatait, például a frissített e-mail-címet vagy felhasználónevet.", + "components.UserProfile.UserSettings.LinkJellyfinModal.quickConnect": "Quick Connect használata", + "components.UserProfile.UserSettings.LinkJellyfinQuickConnectModal.instructions": "Írja be ezt a kódot a {mediaServerName} alkalmazásba", + "components.UserProfile.UserSettings.LinkJellyfinQuickConnectModal.subtitle": "Quick Connect", + "components.UserProfile.UserSettings.LinkJellyfinQuickConnectModal.title": "{mediaServerName} fiók összekapcsolása", + "components.UserProfile.UserSettings.LinkJellyfinQuickConnectModal.usePassword": "Inkább jelszót használjon", + "hooks.useQuickConnect.errorMessage": "A Quick Connect elindítása nem sikerült. Kérjük, próbálja meg újra.", + "i18n.sync": "Szinkronizálás", + "components.ManageSlideOver.clearmediadataerror": "Hiba történt a médiaadatok törlése közben.", + "components.ManageSlideOver.removemediaerror": "A média eltávolítása közben hiba történt.", + "components.RequestList.RequestItem.removemediaerror": "A média eltávolítása közben hiba történt.", + "components.Settings.SettingsAbout.versionCheckDisabled": "Verzióellenőrzés kikapcsolva", + "components.Settings.SettingsMain.versionCheck": "Verzióellenőrzés", + "components.Settings.SettingsMain.versionCheckTip": "Új verziók elérhetőségének automatikus ellenőrizése a GitHubon.", + "components.Settings.Notifications.NotificationsGotify.embedPoster": "Poszter beágyazása", + "components.Settings.jellyfinSyncFailedConnectionError": "A {mediaServerName} szerver nem elérhető. Ellenőrizze, hogy fut-e és elérhető-e a Seerr-ből.", + "components.Settings.toastPlexSyncFailure": "Sikertelen a Plex könyvtárak szinkronizációja.", + "components.Settings.toggleLibraryFailure": "Nem sikerült a könyvtár frissítése." } diff --git a/src/i18n/locale/it.json b/src/i18n/locale/it.json index 6f61425e17..caa7d5b223 100644 --- a/src/i18n/locale/it.json +++ b/src/i18n/locale/it.json @@ -962,9 +962,6 @@ "components.ManageSlideOver.playedby": "Riprodotto da", "components.Settings.validationUrlBaseTrailingSlash": "L'URL di base non deve terminare con una barra obliqua", "components.Settings.Notifications.NotificationsPushbullet.channelTag": "Tag del canale", - "components.UserProfile.UserSettings.UserGeneralSettings.validationDiscordId": "È necessario fornire un ID Utente Discord valido", - "components.UserProfile.UserSettings.UserGeneralSettings.discordId": "ID Utente Discord", - "components.UserProfile.UserSettings.UserGeneralSettings.discordIdTip": "Il numero ID a più cifre associato al tuo account utente Discord", "components.Settings.SettingsAbout.appDataPath": "Cartella applicazione", "components.RequestBlock.languageprofile": "Lingua Profilo", "components.MovieDetails.digitalrelease": "Uscita versione digitale", @@ -1636,5 +1633,40 @@ "components.UserList.toggleSortDirection": "Clicca di nuovo per ordinare in modo {direction}", "components.UserList.toggleSortDirectionAria": "Inverti direzione di ordinamento", "i18n.error": "Qualcosa è andato storto. Prova di nuovo.", - "i18n.partiallyblocklisted": "Parzialmente Bloccato" + "i18n.partiallyblocklisted": "Parzialmente Bloccato", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdAdd": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdPlaceholder": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdRemove": "", + "components.Layout.UserWarnings.profileIncomplete": "", + "components.Login.emailAlreadyTaken": "", + "components.Login.jellyfinLocalLoginHint": "", + "components.Login.saveFailed": "", + "components.RequestList.RequestItem.failedmodify": "", + "components.Settings.SonarrModal.apiKeyHelp": "", + "components.UserProfile.UserSettings.UserPasswordChange.localPasswordDescription": "", + "components.Login.urlBaseHelp": "", + "components.UserList.importedPlexUsersNoPassword": "", + "components.RequestCard.failedmodify": "", + "components.Settings.Notifications.usePublicLogo": "", + "components.Settings.Notifications.usePublicLogoTip": "", + "components.Settings.Notifications.useUserLocale": "", + "components.Settings.RadarrModal.apiKeyHelp": "", + "components.Settings.RadarrModal.baseUrlHelp": "", + "components.Settings.RadarrModal.enableSearchHelp": "", + "components.Settings.RadarrModal.externalUrlHelp": "", + "components.Settings.RadarrModal.server4kHelp": "", + "components.Settings.RadarrModal.syncEnabledHelp": "", + "components.Settings.SettingsMain.blocklistLanguage": "", + "components.Settings.SettingsMain.blocklistLanguageTip": "", + "components.Settings.SettingsMain.blocklistRegion": "", + "components.Settings.SettingsMain.blocklistRegionTip": "", + "components.Settings.SettingsUsers.disabledMediaServerLoginWarning": "", + "components.Settings.SonarrModal.baseUrlHelp": "", + "components.Settings.SonarrModal.enableSearchHelp": "", + "components.Settings.SonarrModal.externalUrlHelp": "", + "components.Settings.SonarrModal.monitorNewItemsHelp": "", + "components.Settings.SonarrModal.server4kHelp": "", + "components.Settings.SonarrModal.syncEnabledHelp": "", + "components.UserList.importedUsersNoPassword": "", + "i18n.notificationLocale": "" } diff --git a/src/i18n/locale/ja.json b/src/i18n/locale/ja.json index 4c3700bcc5..3296a2ba7f 100644 --- a/src/i18n/locale/ja.json +++ b/src/i18n/locale/ja.json @@ -6,7 +6,7 @@ "components.Discover.trending": "トレンド", "components.Discover.upcoming": "今後リリースされる映画", "components.Discover.upcomingmovies": "今後リリースされる映画", - "components.Layout.SearchInput.searchPlaceholder": "作品名で検索", + "components.Layout.SearchInput.searchPlaceholder": "映画とシリーズで検索", "components.Layout.Sidebar.dashboard": "ホーム", "components.Layout.Sidebar.requests": "リクエスト", "components.Layout.Sidebar.settings": "設定", @@ -1388,8 +1388,6 @@ "components.UserProfile.UserSettings.LinkJellyfinModal.usernameRequired": "ユーザー名を指定してください", "components.UserProfile.UserSettings.UserGeneralSettings.accounttype": "アカウントの種類", "components.UserProfile.UserSettings.UserGeneralSettings.admin": "管理", - "components.UserProfile.UserSettings.UserGeneralSettings.discordId": "DiscordのユーザーID", - "components.UserProfile.UserSettings.UserGeneralSettings.discordIdTip": "自身のDiscordユーザーアカウントに関連付けられた複数桁のID番号", "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegion": "地域を見つける", "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegionTip": "利用可能な地域に応じてコンテンツをフィルタリングする", "components.UserProfile.UserSettings.UserGeneralSettings.displayName": "表示名", @@ -1418,7 +1416,6 @@ "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmailEmpty": "別のユーザーがすでにこのユーザー名を持っています。メールアドレスを設定してください", "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsSuccess": "設定が保存されました!", "components.UserProfile.UserSettings.UserGeneralSettings.user": "ユーザー", - "components.UserProfile.UserSettings.UserGeneralSettings.validationDiscordId": "有効なDiscordユーザーIDを指定してください", "components.UserProfile.UserSettings.UserGeneralSettings.validationemailformat": "有効なメールが必要です", "components.UserProfile.UserSettings.UserGeneralSettings.validationemailrequired": "メールアドレスが必要です", "components.UserProfile.UserSettings.UserLinkedAccountsSettings.deleteFailed": "リンクされたアカウントを削除できません。", @@ -1451,7 +1448,7 @@ "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingssaved": "Webプッシュ通知設定が保存されました!", "components.UserProfile.UserSettings.UserNotificationSettings.deviceDefault": "デバイスのデフォルト", "components.UserProfile.UserSettings.UserNotificationSettings.discordId": "ユーザーID", - "components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "自分のユーザーアカウントに関連付けられた複数桁のID番号", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "アカウントに関連付けられた複数桁のID番号。家族など複数のアカウントがある場合は、複数のDiscordユーザーIDを追加できます。", "components.UserProfile.UserSettings.UserNotificationSettings.email": "メール", "components.UserProfile.UserSettings.UserNotificationSettings.notifications": "通知", "components.UserProfile.UserSettings.UserNotificationSettings.notificationsettings": "通知設定", @@ -1473,7 +1470,7 @@ "components.UserProfile.UserSettings.UserNotificationSettings.telegramChatIdTipLong": "チャットを開始し、@get_id_botを追加して/my_idコマンドを実行してください", "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadId": "スレッド/トピック ID", "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadIdTip": "グループチャットでトピックが有効になっている場合ここでスレッド/トピックのIDを指定できます", - "components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "有効なユーザーIDを指定してください", + "components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "有効なDiscordユーザーIDを指定してください", "components.UserProfile.UserSettings.UserNotificationSettings.validationPgpPublicKey": "有効なPGP公開キーを指定してください", "components.UserProfile.UserSettings.UserNotificationSettings.validationPushbulletAccessToken": "アクセストークンを指定してください", "components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverApplicationToken": "有効なアプリケーショントークンを指定してください", @@ -1666,5 +1663,14 @@ "components.UserList.importedPlexUsersNoPassword": "インポートされたユーザーには{applicationTitle}パスワードが設定されていません。Plexサインインを無効にすると、ユーザーはプロフィールから、またはパスワードリセットリンクを使用してパスワードを設定する必要があります。", "components.UserList.importedUsersNoPassword": "インポートされたユーザーには、{applicationTitle} のパスワードが設定されていません。{mediaServerName} のサインインを無効にすると、ユーザーはプロフィールから、またはパスワードリセットリンクを使用してパスワードを設定する必要があります。", "components.UserProfile.UserSettings.UserPasswordChange.localPasswordDescription": "このパスワードは{applicationTitle}のローカルログインフォームでサインインする際に使用します。メディアサーバーのパスワードとは別のものです。", - "i18n.notificationLocale": "通知言語" + "i18n.notificationLocale": "通知言語", + "components.Settings.Notifications.usePublicLogo": "インスタンスロゴの代わりに公開されているSeerrロゴを使用する", + "components.Settings.Notifications.usePublicLogoTip": "Seerrインスタンスが一般公開されていない場合は、このオプションを有効にすることでネットワーク外のメールクライアントから画像を表示できるようになります。画像は公開されているGitHubリポジトリから取得されます。", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdRemove": "取り除く", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdAdd": "ユーザーIDを追加", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdPlaceholder": "DiscordユーザーID", + "components.Settings.Notifications.validationWebhookThreadId": "有効なDiscordのスレッドIDを入力してください", + "components.Settings.Notifications.webhookThreadId": "スレッドID", + "components.Settings.Notifications.webhookThreadIdTip": "通知を投稿するスレッドチャンネルのID。空欄の場合Webhookチャンネルに投稿されます", + "components.UserProfile.UserSettings.UserNotificationSettings.discordNotificationsNotEnabled": "サーバー所有者はDiscord通知を有効にしていません。この情報は、サーバー所有者が外部サービスを設定した場合にのみ使用されます。" } diff --git a/src/i18n/locale/kab.json b/src/i18n/locale/kab.json index 8c15dc2c5b..67d3380efe 100644 --- a/src/i18n/locale/kab.json +++ b/src/i18n/locale/kab.json @@ -72,5 +72,1601 @@ "components.UserProfile.UserSettings.UserPasswordChange.confirmpassword": "Sentem awal n uɛeddi", "components.UserProfile.UserSettings.UserPasswordChange.currentpassword": "Awal n uɛeddi amiran", "components.UserProfile.UserSettings.UserPasswordChange.newpassword": "Awal n uɛeddi amaynut", - "i18n.usersettings": "Iɣewwaren n useqdac" + "i18n.usersettings": "Iɣewwaren n useqdac", + "components.Layout.SearchInput.searchPlaceholder": "", + "components.UserProfile.UserSettings.UserGeneralSettings.mediaServerUser": "", + "components.UserProfile.UserSettings.UserGeneralSettings.seriesrequestlimit": "", + "components.UserProfile.UserSettings.UserGeneralSettings.streamingRegion": "", + "components.UserProfile.UserSettings.UserGeneralSettings.streamingRegionTip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailure": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmail": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmailEmpty": "", + "components.UserProfile.UserSettings.UserGeneralSettings.validationemailformat": "", + "components.UserProfile.UserSettings.UserGeneralSettings.validationemailrequired": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.deleteFailed": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.errorUnknown": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.linkedAccounts": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.noPermissionDescription": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorExists": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorUnauthorized": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.created": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.deletesubscription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablewebpush": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablingwebpusherror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablewebpush": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablingwebpusherror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleted": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleteerror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.unknown": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.deviceDefault": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdAdd": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdPlaceholder": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdRemove": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "", + "components.Layout.UserWarnings.profileIncomplete": "", + "components.Login.emailAlreadyTaken": "", + "components.Login.jellyfinLocalLoginHint": "", + "components.Login.saveFailed": "", + "components.Login.validationPortRequired": "", + "components.Login.validationUrlBaseLeadingSlash": "", + "components.MovieDetails.originallanguage": "", + "components.MovieDetails.originaltitle": "", + "components.MovieDetails.overview": "", + "components.MovieDetails.overviewunavailable": "", + "components.MovieDetails.physicalrelease": "", + "components.MovieDetails.play": "", + "components.MovieDetails.play4k": "", + "components.MovieDetails.productioncountries": "", + "components.MovieDetails.recommendations": "", + "components.MovieDetails.similar": "", + "components.MovieDetails.streamingproviders": "", + "components.MovieDetails.studio": "", + "components.MovieDetails.tmdbuserscore": "", + "components.MovieDetails.viewfullcrew": "", + "components.MovieDetails.watchlistDeleted": "", + "components.MovieDetails.watchlistError": "", + "components.MovieDetails.watchlistSuccess": "", + "components.MovieDetails.watchtrailer": "", + "components.NotificationTypeSelector.adminissuecommentDescription": "", + "components.NotificationTypeSelector.adminissuereopenedDescription": "", + "components.NotificationTypeSelector.adminissueresolvedDescription": "", + "components.NotificationTypeSelector.issuecomment": "", + "components.NotificationTypeSelector.issuecommentDescription": "", + "components.NotificationTypeSelector.issuecreated": "", + "components.NotificationTypeSelector.issueresolved": "", + "components.NotificationTypeSelector.issueresolvedDescription": "", + "components.NotificationTypeSelector.mediaAutoApproved": "", + "components.NotificationTypeSelector.mediaapprovedDescription": "", + "components.NotificationTypeSelector.mediaautorequestedDescription": "", + "components.NotificationTypeSelector.mediaavailableDescription": "", + "components.NotificationTypeSelector.mediadeclined": "", + "components.NotificationTypeSelector.mediafailedDescription": "", + "components.NotificationTypeSelector.mediarequested": "", + "components.NotificationTypeSelector.mediarequestedDescription": "", + "components.NotificationTypeSelector.notificationTypes": "", + "components.NotificationTypeSelector.userissuecommentDescription": "", + "components.NotificationTypeSelector.userissuereopenedDescription": "", + "components.NotificationTypeSelector.userissueresolvedDescription": "", + "components.NotificationTypeSelector.usermediaAutoApprovedDescription": "", + "components.NotificationTypeSelector.usermediaapprovedDescription": "", + "components.NotificationTypeSelector.usermediaavailableDescription": "", + "components.NotificationTypeSelector.usermediadeclinedDescription": "", + "components.NotificationTypeSelector.usermediafailedDescription": "", + "components.NotificationTypeSelector.usermediarequestedDescription": "", + "components.PermissionEdit.autoapprove4kSeriesDescription": "", + "components.PermissionEdit.autoapproveDescription": "", + "components.PermissionEdit.autoapproveMoviesDescription": "", + "components.PermissionEdit.autoapproveSeriesDescription": "", + "components.PermissionEdit.autorequestDescription": "", + "components.PermissionEdit.blocklistedItems": "", + "components.PermissionEdit.createissuesDescription": "", + "components.PermissionEdit.manageblocklist": "", + "components.PermissionEdit.manageblocklistDescription": "", + "components.PermissionEdit.manageissues": "", + "components.PermissionEdit.manageissuesDescription": "", + "components.PermissionEdit.managerequests": "", + "components.PermissionEdit.managerequestsDescription": "", + "components.PermissionEdit.request": "", + "components.PermissionEdit.request4kDescription": "", + "components.PermissionEdit.request4kMovies": "", + "components.PermissionEdit.request4kMoviesDescription": "", + "components.PermissionEdit.request4kTvDescription": "", + "components.PermissionEdit.requestDescription": "", + "components.PermissionEdit.requestMovies": "", + "components.PermissionEdit.requestMoviesDescription": "", + "components.PermissionEdit.requestTv": "", + "components.PermissionEdit.requestTvDescription": "", + "components.PermissionEdit.viewblocklistedItems": "", + "components.RequestBlock.profilechanged": "", + "components.RequestBlock.requestedby": "", + "components.RequestBlock.requestoverrides": "", + "components.RequestBlock.rootfolder": "", + "components.RequestBlock.seasons": "", + "components.RequestBlock.server": "", + "components.RequestButton.approve4krequests": "", + "components.RequestButton.approverequest": "", + "components.RequestButton.decline4krequests": "", + "components.RequestButton.declinerequest4k": "", + "components.RequestButton.declinerequests": "", + "components.RequestCard.tmdbid": "", + "components.RequestList.RequestItem.failedmodify": "", + "components.RequestList.RequestItem.failedretry": "", + "components.RequestList.RequestItem.modifieduserdate": "", + "components.RequestList.RequestItem.removearr": "", + "components.RequestList.sortModified": "", + "components.RequestList.unableToConnect": "", + "components.RequestModal.QuotaDisplay.allowedRequests": "", + "components.RequestModal.QuotaDisplay.allowedRequestsUser": "", + "components.RequestModal.QuotaDisplay.movielimit": "", + "components.RequestModal.QuotaDisplay.notenoughseasonrequests": "", + "components.RequestModal.QuotaDisplay.quotaLink": "", + "components.RequestModal.QuotaDisplay.quotaLinkUser": "", + "components.RequestModal.QuotaDisplay.requestsremaining": "", + "components.RequestModal.QuotaDisplay.requiredquota": "", + "components.RequestModal.QuotaDisplay.requiredquotaUser": "", + "components.RequestModal.cancel": "", + "components.RequestModal.edit": "", + "components.RequestModal.errorediting": "", + "components.RequestModal.numberofepisodes": "", + "components.RequestModal.pendingrequest": "", + "components.RequestModal.requestSuccess": "", + "components.RequestModal.requestadmin": "", + "components.RequestModal.requestcancelled": "", + "components.RequestModal.requestcollection4ktitle": "", + "components.RequestModal.requestcollectiontitle": "", + "components.RequestModal.requestmovie4ktitle": "", + "components.RequestModal.requestmovies": "", + "components.ResetPassword.validationpasswordminchars": "", + "components.ResetPassword.validationpasswordrequired": "", + "components.Search.searchresults": "", + "components.Selector.CertificationSelector.minRating": "", + "components.Selector.CertificationSelector.selectCertification": "", + "components.Selector.CertificationSelector.selectCountry": "", + "components.Selector.CertificationSelector.starttyping": "", + "components.Selector.canceled": "", + "components.Selector.ended": "", + "components.Selector.inProduction": "", + "components.Selector.nooptions": "", + "components.Selector.pilot": "", + "components.Selector.planned": "", + "components.Selector.searchKeywords": "", + "components.Selector.searchStatus": "", + "components.Selector.searchStudios": "", + "components.Selector.showless": "", + "components.Selector.showmore": "", + "components.Selector.starttyping": "", + "components.Settings.Notifications.NotificationsGotify.agentenabled": "", + "components.Settings.Notifications.NotificationsGotify.gotifysettingsfailed": "", + "components.Settings.Notifications.NotificationsGotify.gotifysettingssaved": "", + "components.Settings.Notifications.NotificationsGotify.priority": "", + "components.Settings.Notifications.NotificationsGotify.validationTypes": "", + "components.Settings.Notifications.NotificationsGotify.validationUrlRequired": "", + "components.Settings.Notifications.NotificationsGotify.validationUrlTrailingSlash": "", + "components.Settings.Notifications.NotificationsNtfy.agentenabled": "", + "components.Settings.Notifications.NotificationsNtfy.priority": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestFailed": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestSending": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestSuccess": "", + "components.Settings.Notifications.NotificationsNtfy.tokenAuth": "", + "components.Settings.Notifications.NotificationsNtfy.topic": "", + "components.Settings.Notifications.NotificationsNtfy.url": "", + "components.Settings.Notifications.NotificationsNtfy.usernamePasswordAuth": "", + "components.Settings.Notifications.NotificationsNtfy.validationNtfyTopic": "", + "components.Settings.Notifications.NotificationsNtfy.validationNtfyUrl": "", + "components.Settings.Notifications.NotificationsNtfy.validationPriorityRequired": "", + "components.Settings.Notifications.NotificationsNtfy.validationTypes": "", + "components.Settings.Notifications.NotificationsPushbullet.accessToken": "", + "components.Settings.Notifications.NotificationsPushbullet.accessTokenTip": "", + "components.Settings.Notifications.NotificationsPushbullet.agentEnabled": "", + "components.Settings.Notifications.NotificationsPushbullet.channelTag": "", + "components.Settings.Notifications.NotificationsPushbullet.pushbulletSettingsFailed": "", + "components.Settings.Notifications.NotificationsPushbullet.pushbulletSettingsSaved": "", + "components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestFailed": "", + "components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestSending": "", + "components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestSuccess": "", + "components.Settings.Notifications.NotificationsPushbullet.validationAccessTokenRequired": "", + "components.Settings.Notifications.NotificationsPushover.deviceDefault": "", + "components.Settings.Notifications.NotificationsPushover.embedPoster": "", + "components.Settings.Notifications.NotificationsPushover.pushoversettingsfailed": "", + "components.Settings.Notifications.NotificationsPushover.pushoversettingssaved": "", + "components.Settings.Notifications.NotificationsPushover.sound": "", + "components.Settings.Notifications.NotificationsPushover.toastPushoverTestFailed": "", + "components.Settings.Notifications.NotificationsPushover.toastPushoverTestSending": "", + "components.Settings.Notifications.NotificationsPushover.toastPushoverTestSuccess": "", + "components.Settings.Notifications.NotificationsPushover.userToken": "", + "components.Settings.Notifications.NotificationsPushover.validationAccessTokenRequired": "", + "components.Settings.Notifications.NotificationsPushover.validationTypes": "", + "components.Settings.Notifications.NotificationsPushover.validationUserTokenRequired": "", + "components.Settings.Notifications.NotificationsSlack.agentenabled": "", + "components.Settings.Notifications.NotificationsSlack.embedPoster": "", + "components.Settings.Notifications.NotificationsSlack.slacksettingsfailed": "", + "components.Settings.Notifications.NotificationsSlack.slacksettingssaved": "", + "components.Settings.Notifications.NotificationsSlack.toastSlackTestFailed": "", + "components.Settings.Notifications.NotificationsSlack.toastSlackTestSending": "", + "components.Settings.Notifications.NotificationsSlack.toastSlackTestSuccess": "", + "components.Settings.Notifications.NotificationsSlack.validationTypes": "", + "components.Settings.Notifications.NotificationsSlack.validationWebhookUrl": "", + "components.Settings.Notifications.NotificationsSlack.webhookUrl": "", + "components.Settings.Notifications.NotificationsSlack.webhookUrlTip": "", + "components.Settings.Notifications.NotificationsWebhook.agentenabled": "", + "components.Settings.Notifications.NotificationsWebhook.authheader": "", + "components.Settings.Notifications.NotificationsWebhook.customHeaders": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersAdd": "", + "components.Settings.Notifications.NotificationsWebPush.agentenabled": "", + "components.Settings.Notifications.NotificationsWebPush.embedPoster": "", + "components.Settings.Notifications.agentenabled": "", + "components.Settings.Notifications.authUser": "", + "components.Settings.Notifications.botAPI": "", + "components.Settings.Notifications.botApiTip": "", + "components.Settings.Notifications.botAvatarUrl": "", + "components.Settings.Notifications.botUsername": "", + "components.Settings.Notifications.botUsernameTip": "", + "components.Settings.Notifications.chatId": "", + "components.Settings.Notifications.chatIdTip": "", + "components.Settings.Notifications.discordsettingsfailed": "", + "components.Settings.Notifications.discordsettingssaved": "", + "components.Settings.Notifications.emailsender": "", + "components.Settings.Notifications.emailsettingsfailed": "", + "components.Settings.Notifications.emailsettingssaved": "", + "components.Settings.Notifications.embedPoster": "", + "components.Settings.Notifications.enableMentions": "", + "components.Settings.Notifications.encryption": "", + "components.Settings.Notifications.encryptionDefault": "", + "components.Settings.Notifications.encryptionImplicitTls": "", + "components.Settings.Notifications.encryptionNone": "", + "components.Settings.Notifications.encryptionTip": "", + "components.Settings.Notifications.messageThreadIdTip": "", + "components.Settings.Notifications.pgpPassword": "", + "components.Settings.Notifications.pgpPasswordTip": "", + "components.Settings.Notifications.pgpPrivateKey": "", + "components.Settings.Notifications.pgpPrivateKeyTip": "", + "components.Settings.Notifications.sendSilently": "", + "components.Settings.Notifications.sendSilentlyTip": "", + "components.Settings.Notifications.senderName": "", + "components.Settings.Notifications.smtpHost": "", + "components.Settings.Notifications.smtpPort": "", + "components.Settings.Notifications.telegramsettingssaved": "", + "components.Settings.Notifications.toastDiscordTestFailed": "", + "components.Settings.Notifications.toastDiscordTestSending": "", + "components.Settings.Notifications.toastDiscordTestSuccess": "", + "components.Settings.Notifications.toastEmailTestFailed": "", + "components.Settings.Notifications.toastEmailTestSending": "", + "components.Settings.Notifications.toastEmailTestSuccess": "", + "components.Settings.Notifications.toastTelegramTestFailed": "", + "components.Settings.Notifications.toastTelegramTestSending": "", + "components.Settings.Notifications.toastTelegramTestSuccess": "", + "components.Settings.Notifications.userEmailRequired": "", + "components.Settings.Notifications.validationBotAPIRequired": "", + "components.Settings.Notifications.validationChatIdRequired": "", + "components.Settings.Notifications.webhookRoleId": "", + "components.Settings.OverrideRuleModal.genres": "", + "components.Settings.OverrideRuleModal.keywords": "", + "components.Settings.OverrideRuleModal.languages": "", + "components.Settings.OverrideRuleModal.qualityprofile": "", + "components.Settings.OverrideRuleModal.rootfolder": "", + "components.Settings.OverrideRuleModal.ruleCreated": "", + "components.Settings.OverrideRuleModal.service": "", + "components.Settings.OverrideRuleModal.serviceDescription": "", + "components.Settings.OverrideRuleModal.settingsDescription": "", + "components.Settings.OverrideRuleTile.rootfolder": "", + "components.Settings.OverrideRuleTile.tags": "", + "components.Settings.RadarrModal.add": "", + "components.Settings.RadarrModal.announced": "", + "components.Settings.RadarrModal.externalUrlHelp": "", + "components.Settings.RadarrModal.hostname": "", + "components.Settings.RadarrModal.validationApplicationUrl": "", + "components.Settings.RadarrModal.validationApplicationUrlTrailingSlash": "", + "components.Settings.RadarrModal.validationBaseUrlLeadingSlash": "", + "components.Settings.RadarrModal.validationBaseUrlTrailingSlash": "", + "components.Settings.RadarrModal.validationHostnameRequired": "", + "components.Settings.RadarrModal.validationMinimumAvailabilityRequired": "", + "components.Settings.RadarrModal.validationNameRequired": "", + "components.Settings.RadarrModal.validationPortRequired": "", + "components.Settings.RadarrModal.validationProfileRequired": "", + "components.Settings.RadarrModal.validationRootFolderRequired": "", + "components.Settings.SettingsAbout.Releases.currentversion": "", + "components.Settings.SettingsAbout.Releases.releases": "", + "components.Settings.SettingsAbout.Releases.viewchangelog": "", + "components.Settings.SettingsJobsCache.download-sync-reset": "", + "components.Settings.SettingsJobsCache.editJobSchedule": "", + "components.Settings.SettingsJobsCache.editJobScheduleCurrent": "", + "components.Settings.SettingsJobsCache.editJobSchedulePrompt": "", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorDays": "", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorHours": "", + "components.Settings.SettingsJobsCache.plex-refresh-token": "", + "components.Settings.SettingsUsers.defaultPermissions": "", + "components.Settings.SettingsUsers.defaultPermissionsTip": "", + "components.Settings.SonarrModal.add": "", + "components.Settings.SonarrModal.animeSeriesType": "", + "components.Settings.SonarrModal.monitorNewItemsHelp": "", + "components.Settings.SonarrModal.notagoptions": "", + "components.Settings.SonarrModal.port": "", + "components.Settings.SonarrModal.qualityprofile": "", + "components.Settings.SonarrModal.rootfolder": "", + "components.Settings.SonarrModal.seasonfolders": "", + "components.Settings.SonarrModal.selectLanguageProfile": "", + "components.Settings.address": "", + "components.Settings.menuGeneralSettings": "", + "components.Settings.menuJellyfinSettings": "", + "components.Settings.menuJobs": "", + "components.Settings.tip": "", + "components.Settings.tmdbProviderDoesnotWork": "", + "components.Settings.toastPlexConnecting": "", + "components.Settings.toastPlexConnectingFailure": "", + "components.Settings.toastPlexConnectingSuccess": "", + "components.Settings.toastPlexRefresh": "", + "components.StatusChecker.restartRequired": "", + "components.TitleCard.cleardata": "", + "components.UserList.importfromplex": "", + "components.UserList.importfromplexerror": "", + "components.UserList.localLoginDisabled": "", + "components.UserList.localuser": "", + "components.UserList.newJellyfinsigninenabled": "", + "components.UserList.newplexsigninenabled": "", + "components.UserList.nouserstoimport": "", + "components.UserList.owner": "", + "components.UserList.sortByJoined": "", + "components.UserList.usercreatedsuccess": "", + "components.UserList.userdeleted": "", + "components.UserList.userdeleteerror": "", + "components.UserList.userfail": "", + "components.UserList.userlist": "", + "components.UserList.username": "", + "components.UserList.userssaved": "", + "components.UserList.validationEmail": "", + "components.UserList.validationUsername": "", + "components.UserList.validationpasswordminchars": "", + "components.UserProfile.ProfileHeader.joindate": "", + "components.UserProfile.ProfileHeader.profile": "", + "components.UserProfile.ProfileHeader.settings": "", + "components.UserProfile.ProfileHeader.userid": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorExists": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.saving": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.title": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.usernameRequired": "", + "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegion": "", + "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegionTip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.displayName": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmovies": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmoviestip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseries": "", + "components.UserProfile.UserSettings.UserGeneralSettings.region": "", + "components.UserProfile.UserSettings.UserGeneralSettings.regionTip": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.linkedAccountsHint": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.noLinkedAccounts": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.emailsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverApplicationToken": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverUserKey": "", + "i18n.areyousure": "", + "i18n.available": "", + "i18n.back": "", + "i18n.blocklist": "", + "i18n.blocklistDuplicateError": "", + "pages.internalservererror": "", + "components.RequestList.RequestItem.modified": "", + "components.Blocklist.filterManual": "", + "components.Settings.Notifications.authPass": "", + "components.ManageSlideOver.manageModalMedia4k": "", + "components.Settings.notificationsettings": "", + "components.Setup.finishing": "", + "components.TvDetails.nextAirDate": "", + "components.Discover.DiscoverTv.sortTmdbRatingAsc": "", + "components.IssueModal.CreateIssueModal.problemseason": "", + "components.RequestBlock.lastmodifiedby": "", + "component.BlocklistBlock.blocklistdate": "", + "components.AppDataWarning.dockerVolumeMissingDescription": "", + "components.Blocklist.blocklistNotFoundError": "", + "components.Blocklist.blocklistSettingsDescription": "", + "components.CollectionDetails.requestcollection": "", + "components.Blocklist.blocklistedby": "", + "components.Blocklist.blocklistsettings": "", + "components.Blocklist.filterBlocklistedTags": "", + "components.CollectionDetails.requestcollection4k": "", + "components.Blocklist.mediaTmdbId": "", + "components.Blocklist.showAllBlocklisted": "", + "components.CollectionDetails.numberofmovies": "", + "components.CollectionDetails.overview": "", + "components.Discover.CreateSlider.starttyping": "", + "components.Discover.CreateSlider.validationDatarequired": "", + "components.Discover.DiscoverMovies.activefilters": "", + "components.Discover.DiscoverMovies.discovermovies": "", + "components.Discover.DiscoverWatchlist.discoverwatchlist": "", + "components.Discover.DiscoverWatchlist.watchlist": "", + "components.Discover.FilterSlideover.activefilters": "", + "components.Discover.FilterSlideover.certification": "", + "components.Discover.FilterSlideover.clearfilters": "", + "components.Discover.FilterSlideover.genres": "", + "components.Discover.FilterSlideover.keywords": "", + "components.Discover.PlexWatchlistSlider.plexwatchlist": "", + "components.Discover.RecentRequestsSlider.unableToConnect": "", + "components.Layout.Sidebar.browsetv": "", + "components.ManageSlideOver.removearr": "", + "components.Selector.returningSeries": "", + "components.Selector.searchGenres": "", + "components.Settings.RadarrModal.testFirstQualityProfiles": "", + "components.Login.urlBaseHelp": "", + "components.Login.validationUrlBaseTrailingSlash": "", + "components.Login.validationUrlTrailingSlash": "", + "components.Login.validationemailformat": "", + "components.Login.validationemailrequired": "", + "components.Login.validationhostformat": "", + "components.Login.validationhostrequired": "", + "components.Login.validationpasswordrequired": "", + "components.Login.validationservertyperequired": "", + "components.ManageSlideOver.manageModalTitle": "", + "components.ManageSlideOver.mark4kavailable": "", + "components.ManageSlideOver.markallseasons4kavailable": "", + "components.ManageSlideOver.removearr4k": "", + "components.MovieDetails.cast": "", + "components.MovieDetails.downloadstatus": "", + "components.MovieDetails.imdbuserscore": "", + "components.MovieDetails.managemovie": "", + "components.MovieDetails.mark4kavailable": "", + "components.MovieDetails.markavailable": "", + "components.NotificationTypeSelector.userissuecreatedDescription": "", + "components.RequestButton.approverequests": "", + "components.RequestButton.declinerequest": "", + "components.RequestModal.requestmovies4k": "", + "components.RequestModal.requestmovietitle": "", + "components.RequestModal.requestseasons4k": "", + "components.RequestModal.requestseries4ktitle": "", + "components.RequestModal.requestseriestitle": "", + "components.RequestModal.season": "", + "components.RequestModal.seasonnumber": "", + "components.RequestModal.selectseason": "", + "components.ResetPassword.confirmpassword": "", + "components.ResetPassword.email": "", + "components.ResetPassword.emailresetlink": "", + "components.ResetPassword.gobacklogin": "", + "components.ResetPassword.passwordreset": "", + "components.ResetPassword.requestresetlinksuccessmessage": "", + "components.ResetPassword.resetpassword": "", + "components.ResetPassword.resetpasswordsuccessmessage": "", + "components.ResetPassword.validationemailrequired": "", + "components.ResetPassword.validationpasswordmatch": "", + "components.Settings.Notifications.NotificationsGotify.toastGotifyTestFailed": "", + "components.Settings.Notifications.NotificationsGotify.toastGotifyTestSending": "", + "components.Settings.Notifications.NotificationsGotify.toastGotifyTestSuccess": "", + "components.Settings.Notifications.NotificationsGotify.url": "", + "components.Settings.Notifications.NotificationsGotify.validationPriorityRequired": "", + "components.ManageSlideOver.manageModalNoRequests": "", + "components.ManageSlideOver.manageModalRemoveMediaWarning": "", + "components.ManageSlideOver.manageModalRequests": "", + "components.ManageSlideOver.markallseasonsavailable": "", + "components.ManageSlideOver.markavailable": "", + "components.ManageSlideOver.openarr": "", + "components.ManageSlideOver.openarr4k": "", + "components.ManageSlideOver.opentautulli": "", + "components.ManageSlideOver.pastdays": "", + "components.ManageSlideOver.playedby": "", + "components.ManageSlideOver.plays": "", + "components.PermissionEdit.advancedrequestDescription": "", + "components.PermissionEdit.autoapprove": "", + "components.PermissionEdit.autoapprove4k": "", + "components.PermissionEdit.autoapprove4kDescription": "", + "components.PermissionEdit.autoapprove4kMovies": "", + "components.PermissionEdit.autoapprove4kMoviesDescription": "", + "components.Selector.CertificationSelector.noOptions": "", + "components.Discover.CreateSlider.providetmdbnetwork": "", + "components.Discover.CreateSlider.providetmdbsearch": "", + "components.Discover.CreateSlider.providetmdbstudio": "", + "components.Discover.CreateSlider.searchGenres": "", + "components.Discover.CreateSlider.searchKeywords": "", + "components.Discover.CreateSlider.slidernameplaceholder": "", + "components.Discover.DiscoverMovies.sortPopularityAsc": "", + "components.Discover.DiscoverMovies.sortPopularityDesc": "", + "components.Discover.DiscoverMovies.sortReleaseDateDesc": "", + "components.Discover.DiscoverMovies.sortTitleAsc": "", + "components.Discover.DiscoverMovies.sortTitleDesc": "", + "components.Discover.DiscoverMovies.sortTmdbRatingAsc": "", + "components.Discover.DiscoverMovies.sortTmdbRatingDesc": "", + "components.Discover.DiscoverNetwork.networkSeries": "", + "components.Discover.DiscoverSliderEdit.deletefail": "", + "components.Discover.DiscoverSliderEdit.deletesuccess": "", + "components.Discover.DiscoverTv.sortTmdbRatingDesc": "", + "components.Discover.DiscoverTvGenre.genreSeries": "", + "components.Discover.DiscoverTvKeyword.keywordSeries": "", + "components.Discover.DiscoverTvLanguage.languageSeries": "", + "components.Discover.FilterSlideover.excludeKeywords": "", + "components.Discover.FilterSlideover.voteCount": "", + "components.Discover.createnewslider": "", + "components.IssueDetails.IssueComment.areyousuredelete": "", + "components.IssueDetails.IssueComment.delete": "", + "components.IssueDetails.IssueComment.edit": "", + "components.IssueDetails.IssueComment.postedby": "", + "components.IssueDetails.IssueComment.postedbyedited": "", + "components.IssueDetails.IssueComment.validationComment": "", + "components.IssueDetails.IssueDescription.deleteissue": "", + "components.IssueDetails.allseasons": "", + "components.IssueDetails.closeissue": "", + "components.IssueDetails.closeissueandcomment": "", + "components.IssueDetails.commentplaceholder": "", + "components.IssueDetails.deleteissue": "", + "components.IssueDetails.deleteissueconfirm": "", + "components.IssueDetails.openin4karr": "", + "components.IssueDetails.play4konplex": "", + "components.IssueDetails.playonplex": "", + "components.IssueDetails.problemepisode": "", + "components.IssueDetails.problemseason": "", + "components.IssueList.sortModified": "", + "components.IssueModal.CreateIssueModal.allepisodes": "", + "components.IssueModal.CreateIssueModal.allseasons": "", + "components.IssueModal.CreateIssueModal.episode": "", + "components.IssueModal.CreateIssueModal.extras": "", + "components.IssueModal.CreateIssueModal.providedetail": "", + "components.IssueModal.CreateIssueModal.reportissue": "", + "components.IssueModal.CreateIssueModal.toastSuccessCreate": "", + "components.IssueModal.CreateIssueModal.whatswrong": "", + "components.IssueModal.issueOther": "", + "components.IssueModal.issueSubtitles": "", + "components.LanguageSelector.languageServerDefault": "", + "components.LanguageSelector.originalLanguageDefault": "", + "components.Layout.LanguagePicker.displaylanguage": "", + "components.Layout.Sidebar.blocklist": "", + "components.Layout.Sidebar.dashboard": "", + "components.Layout.Sidebar.issues": "", + "components.Layout.Sidebar.requests": "", + "components.Layout.UserDropdown.MiniQuotaDisplay.movierequests": "", + "components.Layout.UserDropdown.MiniQuotaDisplay.seriesrequests": "", + "components.Layout.UserDropdown.requests": "", + "components.Layout.UserDropdown.settings": "", + "components.MovieDetails.releasedate": "", + "components.MovieDetails.removefromwatchlist": "", + "components.MovieDetails.reportissue": "", + "components.PersonDetails.lifespan": "", + "components.QuotaSelector.seasons": "", + "components.QuotaSelector.tvRequests": "", + "components.RegionSelector.regionServerDefault": "", + "components.RequestBlock.approve": "", + "components.RequestBlock.decline": "", + "components.RequestBlock.delete": "", + "components.RequestBlock.edit": "", + "components.RequestBlock.languageprofile": "", + "components.RequestButton.approverequest4k": "", + "components.PermissionEdit.autorequestSeriesDescription": "", + "components.QuotaSelector.days": "", + "components.QuotaSelector.movieRequests": "", + "components.QuotaSelector.movies": "", + "components.RequestBlock.requestdate": "", + "components.AirDateBadge.airsrelative": "", + "components.Discover.studios": "", + "components.IssueDetails.issuepagetitle": "", + "components.IssueDetails.lastupdated": "", + "components.IssueDetails.nocomments": "", + "components.IssueList.IssueItem.opened": "", + "components.QuotaSelector.unlimited": "", + "components.RequestModal.AdvancedRequester.requestas": "", + "components.PermissionEdit.viewrecent": "", + "components.PermissionEdit.viewrecentDescription": "", + "components.PermissionEdit.viewrequests": "", + "components.RegionSelector.regionDefault": "", + "components.RequestButton.requestmore": "", + "components.RequestButton.requestmore4k": "", + "components.RequestButton.viewrequest": "", + "components.RequestButton.viewrequest4k": "", + "components.Settings.Notifications.NotificationsNtfy.embedPoster": "", + "components.Settings.Notifications.NotificationsNtfy.ntfysettingsfailed": "", + "components.Settings.Notifications.NotificationsNtfy.ntfysettingssaved": "", + "components.Settings.Notifications.NotificationsNtfy.password": "", + "components.MediaSlider.ShowMoreCard.seemore": "", + "components.MetadataSelector.selectMetdataProvider": "", + "components.MetadataSelector.tmdbLabel": "", + "components.MovieDetails.MovieCast.fullcast": "", + "components.MovieDetails.MovieCrew.fullcrew": "", + "components.MovieDetails.addtowatchlist": "", + "components.MovieDetails.revenue": "", + "components.MovieDetails.rtaudiencescore": "", + "components.MovieDetails.rtcriticsscore": "", + "components.MovieDetails.runtime": "", + "components.MovieDetails.showless": "", + "components.MovieDetails.showmore": "", + "components.NotificationTypeSelector.mediaAutoApprovedDescription": "", + "components.NotificationTypeSelector.mediaapproved": "", + "components.PermissionEdit.autorequestMovies": "", + "components.PermissionEdit.autorequestMoviesDescription": "", + "components.PermissionEdit.blocklistedItemsDescription": "", + "components.PermissionEdit.createissues": "", + "components.RequestList.sortDirection": "", + "components.RequestModal.AdvancedRequester.advancedoptions": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersKey": "", + "components.Discover.CreateSlider.editsuccess": "", + "components.Discover.CreateSlider.needresults": "", + "components.Discover.CreateSlider.providetmdbgenreid": "", + "components.Discover.CreateSlider.providetmdbkeywordid": "", + "components.Discover.DiscoverStudio.studioMovies": "", + "components.Discover.DiscoverTv.activefilters": "", + "components.Discover.DiscoverTv.discovertv": "", + "components.Discover.DiscoverTv.sortFirstAirDateAsc": "", + "components.Discover.DiscoverTv.sortFirstAirDateDesc": "", + "components.Discover.DiscoverTv.sortPopularityAsc": "", + "components.Discover.DiscoverTv.sortPopularityDesc": "", + "components.Discover.DiscoverTv.sortTitleAsc": "", + "components.Discover.DiscoverTv.sortTitleDesc": "", + "components.Discover.FilterSlideover.originalLanguage": "", + "components.Discover.FilterSlideover.ratingText": "", + "components.Discover.FilterSlideover.releaseDate": "", + "components.Discover.FilterSlideover.runtime": "", + "components.Discover.FilterSlideover.runtimeText": "", + "components.Discover.FilterSlideover.status": "", + "components.Discover.FilterSlideover.streamingservices": "", + "components.Discover.FilterSlideover.studio": "", + "components.Discover.FilterSlideover.tmdbuserscore": "", + "components.Discover.FilterSlideover.tmdbuservotecount": "", + "components.Discover.FilterSlideover.to": "", + "components.Discover.MovieGenreList.moviegenres": "", + "components.Discover.MovieGenreSlider.moviegenres": "", + "components.Discover.NetworkSlider.networks": "", + "components.Discover.PlexWatchlistSlider.emptywatchlist": "", + "components.Discover.StudioSlider.studios": "", + "components.Discover.TvGenreList.seriesgenres": "", + "components.Discover.TvGenreSlider.tvgenres": "", + "components.Discover.timeWindowWeek": "", + "components.Discover.tmdbmoviegenre": "", + "components.Discover.tmdbmoviekeyword": "", + "components.Discover.tmdbmoviestreamingservices": "", + "components.DownloadBlock.estimatedtime": "", + "components.DownloadBlock.formattedTitle": "", + "components.IssueDetails.toaststatusupdatefailed": "", + "components.IssueDetails.unknownissuetype": "", + "components.IssueList.IssueItem.descriptionpreview": "", + "components.IssueList.IssueItem.episodes": "", + "components.IssueList.IssueItem.openeduserdate": "", + "components.IssueList.IssueItem.problemepisode": "", + "components.IssueList.IssueItem.seasons": "", + "components.IssueList.IssueItem.unknownissuetype": "", + "components.IssueList.IssueItem.viewissue": "", + "components.IssueList.issues": "", + "components.IssueList.showallissues": "", + "components.IssueModal.CreateIssueModal.season": "", + "components.IssueModal.CreateIssueModal.submitissue": "", + "components.IssueModal.CreateIssueModal.toastFailedCreate": "", + "components.IssueModal.CreateIssueModal.validationMessageRequired": "", + "components.Layout.UserWarnings.emailRequired": "", + "components.Layout.UserWarnings.passwordRequired": "", + "components.Layout.VersionStatus.commitsbehind": "", + "components.Layout.VersionStatus.streamdevelop": "", + "components.Layout.VersionStatus.streamstable": "", + "components.Login.adminerror": "", + "components.Login.back": "", + "components.Login.emailtooltip": "", + "components.Login.enablessl": "", + "components.Login.forgotpassword": "", + "components.Login.servertype": "", + "components.Login.tipUsernameHasTrailingWhitespace": "", + "components.Login.validationEmailFormat": "", + "components.Login.validationEmailRequired": "", + "components.Login.description": "", + "components.Login.validationusernamerequired": "", + "components.ManageSlideOver.alltime": "", + "components.ManageSlideOver.downloadstatus": "", + "components.ManageSlideOver.manageModalMedia": "", + "components.ManageSlideOver.tvshow": "", + "components.MovieDetails.openradarr": "", + "components.MovieDetails.openradarr4k": "", + "components.NotificationTypeSelector.mediadeclinedDescription": "", + "components.PermissionEdit.viewrequestsDescription": "", + "components.PermissionEdit.viewwatchlists": "", + "components.PermissionEdit.viewwatchlistsDescription": "", + "components.PersonDetails.alsoknownas": "", + "components.PersonDetails.appearsin": "", + "components.PersonDetails.ascharacter": "", + "components.PersonDetails.birthdate": "", + "components.PersonDetails.crewmember": "", + "components.TvDetails.TvCast.fullseriescast": "", + "components.UserList.password": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.description": "", + "components.UserProfile.localWatchlist": "", + "components.MovieDetails.digitalrelease": "", + "components.Settings.Notifications.messageThreadId": "", + "components.StatusBadge.status4k": "", + "components.RequestModal.AdvancedRequester.qualityprofile": "", + "components.Discover.RecentlyAddedSlider.recentlyAdded": "", + "components.Discover.customizediscover": "", + "components.Discover.emptywatchlist": "", + "components.Discover.moviegenres": "", + "components.Discover.networks": "", + "components.Discover.popularmovies": "", + "components.Discover.populartv": "", + "components.Discover.recentlyAdded": "", + "components.Discover.recentrequests": "", + "components.Discover.resetfailed": "", + "components.Discover.resetsuccess": "", + "components.Discover.resettodefault": "", + "components.Discover.resetwarning": "", + "components.Discover.tmdbnetwork": "", + "components.Discover.tmdbsearch": "", + "components.Discover.tmdbstudio": "", + "components.Discover.tmdbtvgenre": "", + "components.Discover.tmdbtvkeyword": "", + "components.Discover.tmdbtvstreamingservices": "", + "components.Discover.trending": "", + "components.Discover.tvgenres": "", + "components.Discover.upcoming": "", + "components.Discover.upcomingmovies": "", + "components.Discover.upcomingtv": "", + "components.Discover.updatefailed": "", + "components.Discover.updatesuccess": "", + "components.DiscoverTvUpcoming.upcomingtv": "", + "components.Settings.Notifications.NotificationsWebPush.httpsRequirement": "", + "components.Settings.Notifications.NotificationsWebPush.toastWebPushTestFailed": "", + "components.Settings.Notifications.NotificationsWebPush.toastWebPushTestSending": "", + "components.Settings.Notifications.NotificationsWebPush.toastWebPushTestSuccess": "", + "components.Settings.Notifications.NotificationsWebPush.webpushsettingsfailed": "", + "components.Settings.Notifications.NotificationsWebPush.webpushsettingssaved": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersAuthConflict": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersIncomplete": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersTip": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersValue": "", + "components.Settings.Notifications.NotificationsWebhook.customJson": "", + "components.Settings.Notifications.NotificationsWebhook.resetPayload": "", + "components.Settings.Notifications.NotificationsWebhook.resetPayloadSuccess": "", + "components.Settings.Notifications.NotificationsWebhook.supportVariables": "", + "components.Settings.Notifications.NotificationsWebhook.supportVariablesTip": "", + "components.Settings.Notifications.NotificationsWebhook.templatevariablehelp": "", + "components.Settings.Notifications.NotificationsWebhook.toastWebhookTestFailed": "", + "components.Settings.Notifications.NotificationsWebhook.toastWebhookTestSending": "", + "components.Settings.Notifications.NotificationsWebhook.toastWebhookTestSuccess": "", + "components.Settings.Notifications.NotificationsWebhook.validationJsonPayloadRequired": "", + "components.Settings.Notifications.NotificationsWebhook.validationTypes": "", + "components.Settings.Notifications.NotificationsWebhook.validationWebhookUrl": "", + "components.Settings.Notifications.NotificationsWebhook.webhookUrl": "", + "components.Settings.Notifications.NotificationsWebhook.webhookUrlTip": "", + "components.Settings.Notifications.NotificationsWebhook.webhooksettingsfailed": "", + "components.Settings.Notifications.NotificationsWebhook.webhooksettingssaved": "", + "components.Settings.Notifications.useUserLocale": "", + "components.Settings.Notifications.validationEmail": "", + "components.Settings.Notifications.validationMessageThreadId": "", + "components.Settings.Notifications.validationPgpPrivateKey": "", + "components.Settings.Notifications.validationSmtpHostRequired": "", + "components.Settings.Notifications.validationSmtpPortRequired": "", + "components.Settings.Notifications.validationUrl": "", + "components.Settings.OverrideRuleModal.conditions": "", + "components.Settings.RadarrModal.notagoptions": "", + "components.Settings.RadarrModal.port": "", + "components.Settings.RadarrModal.qualityprofile": "", + "components.Settings.RadarrModal.released": "", + "components.Settings.RadarrModal.validationApiKeyRequired": "", + "components.Settings.SettingsAbout.Releases.latestversion": "", + "components.Settings.SettingsAbout.Releases.releasedataMissing": "", + "components.Settings.SettingsAbout.Releases.viewongithub": "", + "components.Settings.SettingsAbout.about": "", + "components.Settings.SettingsAbout.appDataPath": "", + "components.Settings.SettingsAbout.contribute": "", + "components.Settings.SettingsAbout.githubdiscussions": "", + "components.Settings.SettingsJobsCache.cachehits": "", + "components.Settings.SettingsJobsCache.canceljob": "", + "components.Settings.SettingsJobsCache.command": "", + "components.Settings.SettingsJobsCache.dnsCache": "", + "components.Settings.SettingsJobsCache.dnsCacheDescription": "", + "components.Settings.SettingsJobsCache.failures": "", + "components.Settings.SettingsJobsCache.flushcache": "", + "components.Settings.SettingsJobsCache.flushdnscache": "", + "components.Settings.SettingsJobsCache.hitRate": "", + "components.Settings.SettingsJobsCache.hits": "", + "components.Settings.SettingsJobsCache.image-cache-cleanup": "", + "components.Settings.SettingsJobsCache.process": "", + "components.Settings.SettingsLogs.viewdetails": "", + "components.Settings.SettingsNetwork.forceIpv4First": "", + "components.Settings.SettingsNetwork.forceIpv4FirstTip": "", + "components.Settings.SettingsNetwork.network": "", + "components.Settings.SettingsNetwork.networkDisclaimer": "", + "components.Settings.SettingsNetwork.trustProxy": "", + "components.Settings.SettingsNetwork.trustProxyTip": "", + "components.Settings.SettingsNetwork.validationApiRequestTimeout": "", + "components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "", + "components.Settings.SettingsUsers.localLogin": "", + "components.Settings.SonarrModal.loadinglanguageprofiles": "", + "components.Settings.SonarrModal.loadingprofiles": "", + "components.Settings.invalidurlerror": "", + "components.Settings.mediaTypeSeries": "", + "components.Settings.menuAbout": "", + "components.Settings.menuLogs": "", + "components.Settings.settings": "", + "components.Settings.sonarrsettings": "", + "components.Settings.ssl": "", + "components.Settings.startscan": "", + "components.Settings.starttyping": "", + "components.Settings.tvdbProviderDoesnotWork": "", + "components.Settings.urlBase": "", + "components.Settings.validationApiKey": "", + "components.TitleCard.mediaerror": "", + "components.TitleCard.tmdbid": "", + "components.TitleCard.tvdbid": "", + "components.TitleCard.watchlistCancel": "", + "components.TitleCard.watchlistDeleted": "", + "components.TvDetails.rtcriticsscore": "", + "components.TvDetails.seasonnumber": "", + "components.UserList.sortByType": "", + "components.UserList.sortByUser": "", + "components.UserList.toggleSortDirection": "", + "components.UserList.toggleSortDirectionAria": "", + "components.UserList.totalrequests": "", + "components.UserProfile.UserSettings.UserGeneralSettings.movierequestlimit": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletsettingsfailed": "", + "i18n.deleting": "", + "i18n.delimitedlist": "", + "i18n.edit": "", + "i18n.error": "", + "i18n.experimental": "", + "i18n.failed": "", + "i18n.importing": "", + "i18n.loading": "", + "i18n.movie": "", + "i18n.movies": "", + "i18n.next": "", + "i18n.notificationLocale": "", + "i18n.open": "", + "i18n.test": "", + "components.Discover.CreateSlider.searchStudios": "", + "components.Discover.DiscoverMovies.sortReleaseDateAsc": "", + "components.Discover.plexwatchlist": "", + "components.IssueDetails.IssueDescription.edit": "", + "components.IssueDetails.allepisodes": "", + "components.IssueDetails.episode": "", + "components.IssueDetails.openedby": "", + "components.IssueDetails.openinarr": "", + "components.IssueDetails.season": "", + "components.IssueDetails.toaststatusupdated": "", + "components.IssueList.sortAdded": "", + "components.IssueModal.CreateIssueModal.problemepisode": "", + "components.IssueModal.CreateIssueModal.toastviewissue": "", + "components.IssueModal.issueAudio": "", + "components.Layout.VersionStatus.outofdate": "", + "components.Login.credentialerror": "", + "components.Login.email": "", + "components.Login.loginerror": "", + "components.Login.loginwithapp": "", + "components.PermissionEdit.request4kTv": "", + "components.RequestCard.failedmodify": "", + "components.RequestCard.tvdbid": "", + "components.RequestCard.unknowntitle": "", + "components.RequestList.RequestItem.cancelRequest": "", + "components.RequestList.RequestItem.deleterequest": "", + "components.RequestList.RequestItem.editrequest": "", + "components.RequestModal.requestseasons": "", + "components.Settings.Notifications.usePublicLogo": "", + "components.Settings.Notifications.usePublicLogoTip": "", + "components.Settings.Notifications.validationPgpPassword": "", + "components.Settings.Notifications.validationTypes": "", + "components.Settings.Notifications.validationWebhookRoleId": "", + "components.Settings.Notifications.webhookRoleIdTip": "", + "components.Settings.Notifications.webhookUrlTip": "", + "components.Settings.OverrideRuleModal.conditionsDescription": "", + "components.Settings.OverrideRuleModal.create": "", + "components.Settings.OverrideRuleModal.createrule": "", + "components.Settings.Notifications.NotificationsGotify.token": "", + "components.Settings.OverrideRuleModal.editrule": "", + "components.Settings.OverrideRuleModal.notagoptions": "", + "components.Settings.OverrideRuleModal.ruleUpdated": "", + "components.Settings.OverrideRuleModal.selectQualityProfile": "", + "components.Settings.OverrideRuleModal.selectRootFolder": "", + "components.Settings.OverrideRuleModal.selecttags": "", + "components.Settings.OverrideRuleTile.genre": "", + "components.Settings.OverrideRuleTile.keywords": "", + "components.Settings.RadarrModal.apiKey": "", + "components.Settings.RadarrModal.apiKeyHelp": "", + "components.Settings.RadarrModal.baseUrl": "", + "components.Settings.RadarrModal.baseUrlHelp": "", + "components.Settings.RadarrModal.create4kradarr": "", + "components.Settings.RadarrModal.createradarr": "", + "components.Settings.RadarrModal.default4kserver": "", + "components.Settings.RadarrModal.defaultserver": "", + "components.Settings.RadarrModal.edit4kradarr": "", + "components.Settings.RadarrModal.editradarr": "", + "components.Settings.RadarrModal.enableSearch": "", + "components.Settings.RadarrModal.enableSearchHelp": "", + "components.Settings.RadarrModal.externalUrl": "", + "components.Settings.RadarrModal.inCinemas": "", + "components.Settings.RadarrModal.loadingTags": "", + "components.Settings.RadarrModal.loadingprofiles": "", + "components.Settings.RadarrModal.loadingrootfolders": "", + "components.Settings.RadarrModal.minimumAvailability": "", + "components.Settings.RadarrModal.rootfolder": "", + "components.Settings.RadarrModal.selectMinimumAvailability": "", + "components.Settings.RadarrModal.selectQualityProfile": "", + "components.Settings.RadarrModal.selectRootFolder": "", + "components.Settings.RadarrModal.selecttags": "", + "components.Settings.RadarrModal.server4k": "", + "components.Settings.RadarrModal.server4kHelp": "", + "components.Settings.RadarrModal.servername": "", + "components.Settings.RadarrModal.ssl": "", + "components.Settings.RadarrModal.syncEnabled": "", + "components.Settings.RadarrModal.syncEnabledHelp": "", + "components.Settings.RadarrModal.tagRequests": "", + "components.Settings.RadarrModal.tagRequestsInfo": "", + "components.Settings.RadarrModal.tags": "", + "components.Settings.RadarrModal.testFirstRootFolders": "", + "components.Settings.RadarrModal.testFirstTags": "", + "components.Settings.RadarrModal.toastRadarrTestFailure": "", + "components.Settings.RadarrModal.toastRadarrTestSuccess": "", + "components.Settings.SettingsAbout.Releases.versionChangelog": "", + "components.Settings.SettingsAbout.documentation": "", + "components.Settings.SettingsAbout.gettingsupport": "", + "components.Settings.SettingsAbout.outofdate": "", + "components.Settings.SettingsAbout.runningDevelop": "", + "components.Settings.SettingsAbout.supportseerr": "", + "components.Settings.SettingsAbout.timezone": "", + "components.Settings.SettingsAbout.totalmedia": "", + "components.Settings.SettingsAbout.totalrequests": "", + "components.Settings.SettingsAbout.uptodate": "", + "components.Settings.SettingsAbout.version": "", + "components.Settings.SettingsJobsCache.availability-sync": "", + "components.Settings.SettingsJobsCache.cache": "", + "components.Settings.SettingsJobsCache.cacheDescription": "", + "components.Settings.SettingsJobsCache.cacheflushed": "", + "components.Settings.SettingsJobsCache.cachekeys": "", + "components.Settings.SettingsJobsCache.cacheksize": "", + "components.Settings.SettingsJobsCache.cachemisses": "", + "components.Settings.SettingsJobsCache.cachename": "", + "components.Settings.SettingsJobsCache.cachevsize": "", + "components.Settings.SettingsJobsCache.dnsCacheGlobalStats": "", + "components.Settings.SettingsJobsCache.dnsCacheGlobalStatsDescription": "", + "components.Settings.SettingsJobsCache.dnsNoCacheEntries": "", + "components.Settings.SettingsJobsCache.dnscacheactiveaddress": "", + "components.Settings.SettingsJobsCache.dnscacheage": "", + "components.Settings.SettingsJobsCache.dnscacheflushed": "", + "components.Settings.SettingsJobsCache.dnscachehits": "", + "components.Settings.SettingsJobsCache.dnscachemisses": "", + "components.Settings.SettingsJobsCache.dnscachename": "", + "components.Settings.SettingsJobsCache.download-sync": "", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorMinutes": "", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorSeconds": "", + "components.Settings.SettingsJobsCache.imagecache": "", + "components.Settings.SettingsJobsCache.imagecacheDescription": "", + "components.Settings.SettingsJobsCache.imagecachecount": "", + "components.Settings.SettingsJobsCache.imagecachesize": "", + "components.Settings.SettingsJobsCache.ipv4Fallbacks": "", + "components.Settings.SettingsJobsCache.jellyfin-full-scan": "", + "components.Settings.SettingsJobsCache.jellyfin-recently-added-scan": "", + "components.Settings.SettingsJobsCache.jobScheduleEditFailed": "", + "components.Settings.SettingsJobsCache.jobScheduleEditSaved": "", + "components.Settings.SettingsJobsCache.jobcancelled": "", + "components.Settings.SettingsJobsCache.jobname": "", + "components.Settings.SettingsJobsCache.jobs": "", + "components.Settings.SettingsJobsCache.jobsDescription": "", + "components.Settings.SettingsJobsCache.jobsandcache": "", + "components.Settings.SettingsJobsCache.jobstarted": "", + "components.Settings.SettingsJobsCache.jobtype": "", + "components.Settings.SettingsJobsCache.misses": "", + "components.Settings.SettingsJobsCache.nextexecution": "", + "components.Settings.SettingsJobsCache.plex-full-scan": "", + "components.Settings.SettingsJobsCache.plex-recently-added-scan": "", + "components.Settings.SettingsJobsCache.plex-watchlist-sync": "", + "components.Settings.SettingsJobsCache.process-blocklisted-tags": "", + "components.Settings.SettingsJobsCache.radarr-scan": "", + "components.Settings.SettingsJobsCache.runnow": "", + "components.Settings.SettingsJobsCache.size": "", + "components.Settings.SettingsJobsCache.sonarr-scan": "", + "components.Settings.SettingsJobsCache.usersavatars": "", + "components.Settings.SettingsLogs.copiedLogMessage": "", + "components.Settings.SettingsLogs.extraData": "", + "components.Settings.SettingsLogs.filterDebug": "", + "components.Settings.SettingsLogs.filterError": "", + "components.Settings.SettingsLogs.filterInfo": "", + "components.Settings.SettingsLogs.filterWarn": "", + "components.Settings.SettingsLogs.label": "", + "components.Settings.SettingsLogs.level": "", + "components.Settings.SettingsLogs.logDetails": "", + "components.Settings.SettingsLogs.logs": "", + "components.Settings.SettingsLogs.logsDescription": "", + "components.Settings.SettingsLogs.message": "", + "components.Settings.SettingsLogs.pauseLogs": "", + "components.Settings.SettingsLogs.resumeLogs": "", + "components.Settings.SettingsLogs.showall": "", + "components.Settings.SettingsLogs.time": "", + "components.Settings.SettingsMain.apikey": "", + "components.Settings.SettingsMain.apikeyCopied": "", + "components.Settings.SettingsMain.applicationTitle": "", + "components.Settings.SettingsMain.applicationurl": "", + "components.Settings.SettingsMain.blocklistLanguage": "", + "components.Settings.SettingsMain.blocklistLanguageTip": "", + "components.Settings.SettingsMain.blocklistRegion": "", + "components.Settings.SettingsMain.blocklistRegionTip": "", + "components.Settings.SettingsMain.blocklistedTags": "", + "components.Settings.SettingsMain.blocklistedTagsLimit": "", + "components.Settings.SettingsMain.blocklistedTagsLimitTip": "", + "components.Settings.SettingsMain.blocklistedTagsTip": "", + "components.Settings.SettingsMain.generalsettingsDescription": "", + "components.Settings.SettingsMain.hideAvailable": "", + "components.Settings.SettingsMain.hideAvailableTip": "", + "components.Settings.SettingsMain.hideBlocklistedTip": "", + "components.Settings.SettingsMain.locale": "", + "components.Settings.SettingsMain.originallanguage": "", + "components.Settings.SettingsMain.originallanguageTip": "", + "components.Settings.SettingsMain.partialRequestsEnabled": "", + "components.Settings.SettingsMain.streamingRegion": "", + "components.Settings.SettingsMain.toastApiKeySuccess": "", + "components.Settings.SettingsMain.toastSettingsFailure": "", + "components.Settings.SettingsMain.toastSettingsSuccess": "", + "components.Settings.SettingsMain.validationApplicationTitle": "", + "components.Settings.SettingsMain.validationApplicationUrl": "", + "components.Settings.SettingsMain.validationApplicationUrlTrailingSlash": "", + "components.Settings.SettingsMain.validationUrl": "", + "components.Settings.SettingsMain.validationUrlTrailingSlash": "", + "components.Settings.SettingsMain.youtubeUrl": "", + "components.Settings.SettingsMain.youtubeUrlTip": "", + "components.Settings.SettingsNetwork.apiRequestTimeout": "", + "components.Settings.SettingsNetwork.apiRequestTimeoutTip": "", + "components.Settings.SettingsNetwork.csrfProtection": "", + "components.Settings.SettingsNetwork.csrfProtectionHoverTip": "", + "components.Settings.SettingsNetwork.csrfProtectionTip": "", + "components.Settings.SettingsNetwork.dnsCache": "", + "components.Settings.SettingsNetwork.dnsCacheForceMaxTtl": "", + "components.Settings.SettingsNetwork.dnsCacheForceMinTtl": "", + "components.Settings.SettingsNetwork.dnsCacheHoverTip": "", + "components.Settings.SettingsNetwork.dnsCacheTip": "", + "components.Settings.SettingsNetwork.docs": "", + "components.Settings.SettingsNetwork.networksettings": "", + "components.Settings.SettingsNetwork.networksettingsDescription": "", + "components.Settings.SettingsNetwork.proxyBypassFilter": "", + "components.Settings.SettingsNetwork.proxyBypassFilterTip": "", + "components.Settings.SettingsNetwork.proxyBypassLocalAddresses": "", + "components.Settings.SettingsNetwork.proxyEnabled": "", + "components.Settings.SettingsNetwork.proxyEnabledTip": "", + "components.Settings.SettingsNetwork.proxyHostname": "", + "components.Settings.SettingsNetwork.proxyPassword": "", + "components.Settings.SettingsNetwork.proxyPort": "", + "components.Settings.SettingsNetwork.proxySsl": "", + "components.Settings.SettingsNetwork.proxyUser": "", + "components.Settings.SettingsNetwork.toastSettingsFailure": "", + "components.Settings.SettingsNetwork.toastSettingsSuccess": "", + "components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "", + "components.Settings.SettingsNetwork.validationProxyPort": "", + "components.Settings.SettingsUsers.atLeastOneAuth": "", + "components.Settings.SettingsUsers.disabledMediaServerLoginWarning": "", + "components.Settings.SettingsUsers.localLoginTip": "", + "components.Settings.SettingsUsers.loginMethods": "", + "components.Settings.SettingsUsers.loginMethodsTip": "", + "components.Settings.SettingsUsers.userSettingsDescription": "", + "components.Settings.SonarrModal.animeTags": "", + "components.Settings.SonarrModal.animelanguageprofile": "", + "components.Settings.SonarrModal.animequalityprofile": "", + "components.Settings.SonarrModal.animerootfolder": "", + "components.Settings.SonarrModal.apiKey": "", + "components.Settings.SonarrModal.apiKeyHelp": "", + "components.Settings.SonarrModal.baseUrl": "", + "components.Settings.SonarrModal.baseUrlHelp": "", + "components.Settings.SonarrModal.create4ksonarr": "", + "components.Settings.SonarrModal.createsonarr": "", + "components.Settings.SonarrModal.default4kserver": "", + "components.Settings.SonarrModal.defaultserver": "", + "components.Settings.SonarrModal.edit4ksonarr": "", + "components.Settings.SonarrModal.editsonarr": "", + "components.Settings.SonarrModal.enableSearch": "", + "components.Settings.SonarrModal.enableSearchHelp": "", + "components.Settings.SonarrModal.externalUrl": "", + "components.Settings.SonarrModal.externalUrlHelp": "", + "components.Settings.SonarrModal.languageprofile": "", + "components.Settings.SonarrModal.loadingTags": "", + "components.Settings.SonarrModal.loadingrootfolders": "", + "components.Settings.SonarrModal.monitorNewItems": "", + "components.Settings.SonarrModal.selectQualityProfile": "", + "components.Settings.SonarrModal.selectRootFolder": "", + "components.Settings.SonarrModal.seriesType": "", + "components.Settings.SonarrModal.server4k": "", + "components.Settings.SonarrModal.server4kHelp": "", + "components.Settings.SonarrModal.servername": "", + "components.Settings.SonarrModal.ssl": "", + "components.Settings.SonarrModal.syncEnabled": "", + "components.Settings.SonarrModal.syncEnabledHelp": "", + "components.Settings.SonarrModal.tagRequests": "", + "components.Settings.SonarrModal.tagRequestsInfo": "", + "components.Settings.SonarrModal.tags": "", + "components.Settings.SonarrModal.testFirstLanguageProfiles": "", + "components.Settings.SonarrModal.testFirstQualityProfiles": "", + "components.Settings.SonarrModal.testFirstRootFolders": "", + "components.Settings.SonarrModal.testFirstTags": "", + "components.Settings.SonarrModal.toastSonarrTestFailure": "", + "components.Settings.SonarrModal.toastSonarrTestSuccess": "", + "components.Settings.SonarrModal.validationApiKeyRequired": "", + "components.Settings.SonarrModal.validationApplicationUrl": "", + "components.Settings.SonarrModal.validationApplicationUrlTrailingSlash": "", + "components.Settings.SonarrModal.validationBaseUrlLeadingSlash": "", + "components.Settings.SonarrModal.validationBaseUrlTrailingSlash": "", + "components.Settings.SonarrModal.validationHostnameRequired": "", + "components.Settings.SonarrModal.validationLanguageProfileRequired": "", + "components.Settings.SonarrModal.validationNameRequired": "", + "components.Settings.SettingsAbout.aboutseerr": "", + "components.Settings.SonarrModal.validationPortRequired": "", + "components.Settings.SonarrModal.validationProfileRequired": "", + "components.Settings.SonarrModal.validationRootFolderRequired": "", + "components.Settings.addradarr": "", + "components.Settings.addrule": "", + "components.Settings.addsonarr": "", + "components.Settings.blocklistedTagImportInstructions": "", + "components.Settings.blocklistedTagImportTitle": "", + "components.Settings.blocklistedTagsText": "", + "components.Settings.cancelscan": "", + "components.Settings.chooseProvider": "", + "components.Settings.clearBlocklistedTagsConfirm": "", + "components.Settings.clickTest": "", + "components.Settings.connectionTestFailed": "", + "components.Settings.copyBlocklistedTags": "", + "components.Settings.copyBlocklistedTagsEmpty": "", + "components.Settings.copyBlocklistedTagsTip": "", + "components.Settings.currentlibrary": "", + "components.Settings.default": "", + "components.Settings.default4k": "", + "components.Settings.deleteServer": "", + "components.Settings.deleteserverconfirm": "", + "components.Settings.email": "", + "components.Settings.enablessl": "", + "components.Settings.experimentalTooltip": "", + "components.Settings.externalUrl": "", + "components.Settings.failed": "", + "components.Settings.failedToSaveMetadataSettings": "", + "components.Settings.general": "", + "components.Settings.hostname": "", + "components.Settings.importBlocklistedTagsTip": "", + "components.Settings.invalidKeyword": "", + "components.Settings.is4k": "", + "components.Settings.jellyfinForgotPasswordUrl": "", + "components.Settings.jellyfinSettings": "", + "components.Settings.jellyfinSettingsDescription": "", + "components.Settings.jellyfinSettingsFailure": "", + "components.Settings.jellyfinSettingsSuccess": "", + "components.Settings.jellyfinSyncFailedAutomaticGroupedFolders": "", + "components.Settings.jellyfinSyncFailedGenericError": "", + "components.Settings.jellyfinSyncFailedNoLibrariesFound": "", + "components.Settings.jellyfinlibraries": "", + "components.Settings.jellyfinlibrariesDescription": "", + "components.Settings.jellyfinsettings": "", + "components.Settings.jellyfinsettingsDescription": "", + "components.Settings.librariesRemaining": "", + "components.Settings.manualscan": "", + "components.Settings.manualscanDescription": "", + "components.Settings.manualscanDescriptionJellyfin": "", + "components.Settings.manualscanJellyfin": "", + "components.Settings.mediaTypeMovie": "", + "components.Settings.menuNetwork": "", + "components.Settings.menuNotifications": "", + "components.Settings.menuPlexSettings": "", + "components.Settings.menuServices": "", + "components.Settings.metadataProviderSelection": "", + "components.Settings.metadataProviderSettings": "", + "components.Settings.notificationAgentSettingsDescription": "", + "components.Settings.notifications": "", + "components.Settings.notrunning": "", + "components.Settings.operational": "", + "components.Settings.overrideRules": "", + "components.Settings.overrideRulesDescription": "", + "components.Settings.plexlibraries": "", + "components.Settings.plexlibrariesDescription": "", + "components.Settings.plexsettings": "", + "components.Settings.plexsettingsDescription": "", + "components.Settings.providerStatus": "", + "components.Settings.scanbackground": "", + "components.Settings.searchKeywords": "", + "components.Settings.seriesMetadataProvider": "", + "components.Settings.serverLocal": "", + "components.Settings.serverRemote": "", + "components.Settings.SettingsJobsCache.unknownJob": "", + "components.Settings.serverSecure": "", + "components.Settings.serverpreset": "", + "components.Settings.serverpresetLoad": "", + "components.Settings.serverpresetManualMessage": "", + "components.Settings.serviceSettingsDescription": "", + "components.Settings.services": "", + "components.Settings.settingUpPlexDescription": "", + "components.Settings.toastPlexRefreshFailure": "", + "components.Settings.toastPlexRefreshSuccess": "", + "components.Settings.toastTautulliSettingsFailure": "", + "components.Settings.toastTautulliSettingsSuccess": "", + "components.Settings.validationHostnameRequired": "", + "components.Settings.validationPortRequired": "", + "components.Settings.validationUrl": "", + "components.Settings.validationUrlBaseLeadingSlash": "", + "components.Setup.continue": "", + "components.Setup.librarieserror": "", + "components.Setup.servertype": "", + "components.Setup.setup": "", + "components.Setup.signin": "", + "components.Setup.signinMessage": "", + "components.Setup.signinWithEmby": "", + "components.Setup.signinWithJellyfin": "", + "components.Setup.signinWithPlex": "", + "components.Setup.subtitle": "", + "components.Setup.welcome": "", + "components.StatusBadge.managemedia": "", + "components.StatusBadge.openinarr": "", + "components.StatusBadge.playonplex": "", + "components.StatusBadge.seasonepisodenumber": "", + "components.StatusBadge.seasonnumber": "", + "components.StatusBadge.status": "", + "components.StatusChecker.appUpdatedDescription": "", + "components.StatusChecker.reloadApp": "", + "components.StatusChecker.restartRequiredDescription": "", + "components.Settings.SettingsMain.streamingRegionTip": "", + "components.Settings.SettingsMain.toastApiKeyFailure": "", + "components.TitleCard.addToWatchList": "", + "components.TitleCard.watchlistError": "", + "components.TitleCard.watchlistSuccess": "", + "components.TvDetails.Season.noepisodes": "", + "components.TvDetails.Season.somethingwentwrong": "", + "components.TvDetails.TvCrew.fullseriescrew": "", + "components.TvDetails.addtowatchlist": "", + "components.TvDetails.anime": "", + "components.TvDetails.cast": "", + "components.TvDetails.episodeCount": "", + "components.TvDetails.episodeRuntime": "", + "components.TvDetails.episodeRuntimeMinutes": "", + "components.TvDetails.firstAirDate": "", + "components.TvDetails.manageseries": "", + "components.TvDetails.network": "", + "components.TvDetails.originallanguage": "", + "components.TvDetails.productioncountries": "", + "components.TvDetails.removefromwatchlist": "", + "components.TvDetails.reportissue": "", + "components.TvDetails.rtaudiencescore": "", + "components.TvDetails.seasons": "", + "components.TvDetails.seasonstitle": "", + "components.TvDetails.showtype": "", + "components.TvDetails.similar": "", + "components.TvDetails.status4k": "", + "components.TvDetails.streamingproviders": "", + "components.TvDetails.tmdbuserscore": "", + "components.TvDetails.watchlistDeleted": "", + "components.TvDetails.watchlistError": "", + "components.TvDetails.watchlistSuccess": "", + "components.TvDetails.watchtrailer": "", + "components.UserList.accounttype": "", + "components.UserList.admin": "", + "components.UserList.ascending": "", + "components.UserList.createlocaluser": "", + "components.UserList.creating": "", + "components.UserList.deleteconfirm": "", + "components.UserList.deleteuser": "", + "components.UserList.descending": "", + "components.UserList.edituser": "", + "components.UserList.email": "", + "components.UserList.importedPlexUsersNoPassword": "", + "components.UserList.importedUsersNoPassword": "", + "components.UserList.importedfromJellyfin": "", + "components.UserList.importedfromplex": "", + "components.UserList.importfromJellyfin": "", + "components.UserList.importfromJellyfinerror": "", + "components.UserList.importfrommediaserver": "", + "components.UserList.passwordinfodescription": "", + "components.UserList.plexuser": "", + "components.UserList.role": "", + "components.UserList.sortBy": "", + "components.UserList.sortByRequests": "", + "components.UserList.sortByRole": "", + "components.UserList.user": "", + "components.UserList.usercreatedfailed": "", + "components.UserList.usercreatedfailedexisting": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorUnauthorized": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorUnknown": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.passwordRequired": "", + "components.Settings.SonarrModal.hostname": "", + "components.UserProfile.UserSettings.UserGeneralSettings.enableOverride": "", + "components.UserProfile.UserSettings.UserGeneralSettings.localuser": "", + "components.Settings.advancedTooltip": "", + "components.Settings.allChosenProvidersAreOperational": "", + "components.Settings.apiKey": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.nodevicestoshow": "", + "components.UserProfile.UserSettings.UserNotificationSettings.emailsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pgpPublicKey": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pgpPublicKeyTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessTokenTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverApplicationTokenTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverUserKey": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverUserKeyTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoversettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoversettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.sendSilently": "", + "components.UserProfile.UserSettings.UserNotificationSettings.sendSilentlyDescription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.sound": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPushbulletAccessToken": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramChatId": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramMessageThreadId": "", + "components.UserProfile.UserSettings.UserNotificationSettings.webpush": "", + "components.UserProfile.UserSettings.UserPasswordChange.localPasswordDescription": "", + "components.UserProfile.UserSettings.UserPasswordChange.noPasswordSet": "", + "components.UserProfile.UserSettings.UserPasswordChange.noPasswordSetOwnAccount": "", + "components.UserProfile.UserSettings.UserPasswordChange.nopermissionDescription": "", + "components.UserProfile.UserSettings.UserPasswordChange.toastSettingsFailure": "", + "components.UserProfile.UserSettings.UserPasswordChange.toastSettingsFailureVerifyCurrent": "", + "components.UserProfile.UserSettings.UserPasswordChange.toastSettingsSuccess": "", + "components.UserProfile.UserSettings.UserPasswordChange.validationConfirmPassword": "", + "components.UserProfile.UserSettings.UserPermissions.toastSettingsFailure": "", + "components.UserProfile.UserSettings.menuLinkedAccounts": "", + "components.UserProfile.UserSettings.unauthorizedDescription": "", + "components.UserProfile.emptywatchlist": "", + "components.UserProfile.movierequests": "", + "components.UserProfile.pastdays": "", + "components.UserProfile.plexwatchlist": "", + "components.UserProfile.recentlywatched": "", + "components.UserProfile.recentrequests": "", + "components.UserProfile.requestsperdays": "", + "components.UserProfile.seriesrequest": "", + "components.UserProfile.totalrequests": "", + "components.UserProfile.unlimited": "", + "i18n.addToBlocklist": "", + "i18n.advanced": "", + "i18n.all": "", + "i18n.approve": "", + "i18n.approved": "", + "i18n.blocklistError": "", + "i18n.blocklistSuccess": "", + "i18n.blocklisted": "", + "i18n.cancel": "", + "i18n.canceling": "", + "i18n.close": "", + "i18n.collection": "", + "i18n.completed": "", + "i18n.decline": "", + "i18n.declined": "", + "i18n.delete": "", + "i18n.partiallyavailable": "", + "i18n.partiallyblocklisted": "", + "i18n.pending": "", + "i18n.previous": "", + "i18n.processing": "", + "i18n.removeFromBlocklistSuccess": "", + "i18n.removefromBlocklist": "", + "i18n.request": "", + "i18n.request4k": "", + "i18n.requested": "", + "i18n.requesting": "", + "i18n.resolved": "", + "i18n.restartRequired": "", + "i18n.status": "", + "i18n.testing": "", + "i18n.tvshow": "", + "i18n.tvshows": "", + "i18n.unavailable": "", + "pages.errormessagewithcode": "", + "pages.oops": "", + "pages.pagenotfound": "", + "pages.returnHome": "", + "pages.serviceunavailable": "", + "pages.somethingwentwrong": "", + "components.Settings.validationUrlBaseTrailingSlash": "", + "components.Setup.configureservices": "", + "components.Setup.finish": "", + "components.UserList.mediaServerUser": "", + "components.UserList.noJellyfinuserstoimport": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsSuccess": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeendisabled": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeenenabled": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramChatId": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramChatIdTipLong": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadId": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadIdTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramsettingssaved": "", + "components.UserProfile.UserSettings.UserPermissions.toastSettingsSuccess": "", + "components.UserProfile.UserSettings.UserPermissions.unauthorizedDescription": "", + "components.UserProfile.UserSettings.menuPermissions": "", + "i18n.noresults": "", + "i18n.notrequested": "", + "components.Settings.metadataSettings": "", + "components.Settings.webhook": "", + "components.MetadataSelector.tvdbLabel": "", + "components.MovieDetails.theatricalrelease": "", + "components.Settings.activeProfile": "", + "components.Setup.configjellyfin": "", + "i18n.deleted": "", + "component.BlocklistBlock.blocklistedby": "", + "components.Discover.CreateSlider.validationTitlerequired": "", + "components.Login.urlBase": "", + "components.ManageSlideOver.movie": "", + "components.PermissionEdit.autoapproveSeries": "", + "components.RequestModal.selectmovies": "", + "components.Selector.CertificationSelector.errorLoading": "", + "components.Selector.CertificationSelector.maxRating": "", + "components.Settings.Notifications.NotificationsGotify.validationTokenRequired": "", + "components.Settings.Notifications.NotificationsPushover.userTokenTip": "", + "components.Settings.Notifications.allowselfsigned": "", + "components.Settings.Notifications.telegramsettingsfailed": "", + "components.Settings.OverrideRuleModal.tags": "", + "components.Settings.OverrideRuleTile.conditions": "", + "components.Settings.OverrideRuleTile.qualityprofile": "", + "components.Settings.serverpresetRefreshing": "", + "components.RequestModal.requestedited": "", + "components.Settings.Notifications.webhookUrl": "", + "components.Settings.SettingsLogs.copyToClipboard": "", + "components.Settings.animeMetadataProvider": "", + "components.Settings.menuMetadataProviders": "", + "component.BlocklistModal.blocklisting": "", + "components.AirDateBadge.airedrelative": "", + "components.Discover.DiscoverMovieGenre.genreMovies": "", + "components.Discover.DiscoverMovieKeyword.keywordMovies": "", + "components.Discover.DiscoverMovieLanguage.languageMovies": "", + "components.Discover.FilterSlideover.firstAirDate": "", + "components.Discover.FilterSlideover.from": "", + "components.PermissionEdit.autoapprove4kSeries": "", + "components.PermissionEdit.autoapproveMovies": "", + "components.PermissionEdit.autorequestSeries": "", + "components.PermissionEdit.request4k": "", + "components.PermissionEdit.usersDescription": "", + "components.PermissionEdit.viewblocklistedItemsDescription": "", + "components.PermissionEdit.viewissues": "", + "components.PermissionEdit.viewissuesDescription": "", + "components.RequestCard.approverequest": "", + "components.RequestCard.cancelrequest": "", + "components.RequestCard.declinerequest": "", + "components.RequestCard.deleterequest": "", + "components.RequestCard.editrequest": "", + "components.RequestCard.failedretry": "", + "components.RequestCard.mediaerror": "", + "components.RequestCard.seasons": "", + "components.RequestList.RequestItem.mediaerror": "", + "components.RequestList.RequestItem.requested": "", + "components.RequestList.RequestItem.requesteddate": "", + "components.RequestModal.AdvancedRequester.animenote": "", + "components.RequestModal.AdvancedRequester.default": "", + "components.RequestModal.AdvancedRequester.destinationserver": "", + "components.RequestModal.AdvancedRequester.folder": "", + "components.RequestModal.AdvancedRequester.languageprofile": "", + "components.RequestModal.AdvancedRequester.notagoptions": "", + "components.RequestModal.AdvancedRequester.rootfolder": "", + "components.RequestModal.AdvancedRequester.selecttags": "", + "components.RequestModal.AdvancedRequester.tags": "", + "components.RequestModal.QuotaDisplay.movie": "", + "components.RequestModal.QuotaDisplay.seasonlimit": "", + "components.RequestModal.SearchByNameModal.nomatches": "", + "components.RequestModal.SearchByNameModal.notvdbiddescription": "", + "components.RequestModal.alreadyrequested": "", + "components.RequestModal.approve": "", + "i18n.view": "", + "components.RequestModal.autoapproval": "", + "components.RequestModal.pending4krequest": "", + "components.RequestModal.pendingapproval": "", + "components.RequestModal.requestApproved": "", + "components.RequestModal.requestCancel": "", + "components.RequestModal.requesterror": "", + "components.RequestModal.requestfrom": "", + "components.Settings.Notifications.encryptionOpportunisticTls": "", + "components.Settings.OverrideRuleModal.selectService": "", + "components.MovieDetails.budget": "", + "components.Settings.SonarrModal.selecttags": "", + "components.Settings.port": "", + "i18n.resultsperpage": "", + "i18n.retry": "", + "i18n.retrying": "", + "i18n.save": "", + "i18n.saving": "", + "i18n.settings": "", + "i18n.showingresults": "", + "i18n.specials": "", + "components.UserProfile.UserSettings.UserGeneralSettings.originallanguageTip": "", + "components.CollectionDetails.removefromblocklistpartialcount": "", + "components.Discover.CreateSlider.addSlider": "", + "components.Discover.CreateSlider.addcustomslider": "", + "components.Discover.CreateSlider.addfail": "", + "components.Discover.CreateSlider.addsuccess": "", + "components.Discover.CreateSlider.editSlider": "", + "components.Discover.CreateSlider.editfail": "", + "components.Discover.CreateSlider.nooptions": "", + "components.Discover.DiscoverSliderEdit.enable": "", + "components.Discover.FilterSlideover.filters": "", + "components.Discover.stopediting": "", + "components.Discover.timeWindowDay": "", + "components.IssueDetails.reopenissue": "", + "components.IssueDetails.reopenissueandcomment": "", + "components.IssueDetails.toasteditdescriptionfailed": "", + "components.IssueDetails.toasteditdescriptionsuccess": "", + "components.IssueDetails.toastissuedeleted": "", + "components.IssueDetails.toastissuedeletefailed": "", + "components.Layout.UserDropdown.signout": "", + "components.Layout.UserWarnings.emailInvalid": "", + "components.Login.hostname": "", + "components.Login.initialsignin": "", + "components.Login.initialsigningin": "", + "components.Login.invalidurlerror": "", + "components.Login.noadminerror": "", + "components.Login.orsigninwith": "", + "components.Login.saving": "", + "components.Login.signin": "", + "components.Login.signingin": "", + "components.Login.signinheader": "", + "components.Login.signinwithjellyfin": "", + "components.Login.signinwithoverseerr": "", + "components.Login.signinwithplex": "", + "components.Login.tipEmailHasTrailingWhitespace": "", + "components.Login.title": "", + "components.ManageSlideOver.manageModalAdvanced": "", + "components.ManageSlideOver.manageModalClearMedia": "", + "components.ManageSlideOver.manageModalClearMediaWarning": "", + "components.ManageSlideOver.manageModalIssues": "", + "components.NotificationTypeSelector.issuecreatedDescription": "", + "components.NotificationTypeSelector.issuereopened": "", + "components.NotificationTypeSelector.issuereopenedDescription": "", + "components.NotificationTypeSelector.mediaautorequested": "", + "components.NotificationTypeSelector.mediaavailable": "", + "components.NotificationTypeSelector.mediafailed": "", + "components.PermissionEdit.adminDescription": "", + "components.PermissionEdit.advancedrequest": "", + "components.PermissionEdit.autorequest": "", + "components.RequestList.RequestItem.seasons": "", + "components.RequestList.RequestItem.tmdbid": "", + "components.RequestList.RequestItem.tvdbid": "", + "components.RequestList.RequestItem.unknowntitle": "", + "components.RequestList.requests": "", + "components.RequestList.showallrequests": "", + "components.RequestList.sortAdded": "", + "components.RequestModal.QuotaDisplay.season": "", + "components.Settings.Notifications.NotificationsPushbullet.validationTypes": "", + "components.Settings.Notifications.NotificationsPushover.accessToken": "", + "components.Settings.Notifications.NotificationsPushover.accessTokenTip": "", + "components.Settings.Notifications.NotificationsPushover.agentenabled": "", + "components.Settings.SettingsMain.cacheImages": "", + "components.Settings.SettingsMain.cacheImagesTip": "", + "components.Settings.SettingsMain.discoverRegion": "", + "components.Settings.SettingsMain.discoverRegionTip": "", + "components.Settings.SettingsMain.enableSpecialEpisodes": "", + "components.Settings.SettingsMain.general": "", + "components.Settings.SettingsMain.generalsettings": "", + "components.Settings.SettingsMain.hideBlocklisted": "", + "components.Settings.SettingsUsers.mediaServerLogin": "", + "components.Settings.SettingsUsers.mediaServerLoginTip": "", + "components.Settings.SettingsUsers.movieRequestLimitLabel": "", + "components.Settings.SettingsUsers.newPlexLogin": "", + "components.Settings.SettingsUsers.newPlexLoginTip": "", + "components.Settings.SettingsUsers.toastSettingsFailure": "", + "components.Settings.SettingsUsers.toastSettingsSuccess": "", + "components.Settings.SettingsUsers.tvRequestLimitLabel": "", + "components.Settings.metadataSettingsSaved": "", + "components.Settings.no": "", + "components.Settings.noDefault4kServer": "", + "components.Settings.noDefaultNon4kServer": "", + "components.Settings.noDefaultServer": "", + "components.Settings.noSpecialCharacters": "", + "components.Settings.nooptions": "", + "components.Settings.notTested": "", + "components.Settings.plex": "", + "components.Settings.radarrsettings": "", + "components.Settings.restartrequiredTooltip": "", + "components.Settings.save": "", + "components.Settings.saving": "", + "components.Settings.scan": "", + "components.Settings.scanning": "", + "components.Settings.syncJellyfin": "", + "components.Settings.syncing": "", + "components.Settings.tautulliApiKey": "", + "components.Settings.tautulliSettings": "", + "components.Settings.tautulliSettingsDescription": "", + "components.Settings.timeout": "", + "components.Settings.validationUrlTrailingSlash": "", + "components.Settings.valueRequired": "", + "components.Settings.webAppUrl": "", + "components.Settings.webAppUrlTip": "", + "components.Settings.webpush": "", + "components.Settings.yes": "", + "components.Setup.back": "", + "components.Setup.configemby": "", + "components.Setup.configplex": "", + "components.Setup.configuremediaserver": "", + "components.StatusChecker.appUpdated": "", + "components.TvDetails.originaltitle": "", + "components.TvDetails.overview": "", + "components.TvDetails.overviewunavailable": "", + "components.TvDetails.play": "", + "components.TvDetails.play4k": "", + "components.TvDetails.recommendations": "", + "components.TvDetails.viewfullcrew": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverApplicationToken": "", + "components.UserProfile.limit": "", + "i18n.import": "", + "components.UserList.autogeneratepassword": "", + "components.UserList.autogeneratepasswordTip": "", + "components.UserList.bulkedit": "", + "components.UserList.create": "", + "components.UserList.created": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseriestip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPgpPublicKey": "", + "components.UserProfile.UserSettings.UserPasswordChange.validationConfirmPasswordSame": "", + "components.UserProfile.UserSettings.UserPasswordChange.validationCurrentPassword": "", + "components.UserProfile.UserSettings.UserPasswordChange.validationNewPassword": "", + "components.UserProfile.UserSettings.UserPasswordChange.validationNewPasswordLength": "", + "components.UserProfile.UserSettings.UserPermissions.permissions": "" } diff --git a/src/i18n/locale/ko.json b/src/i18n/locale/ko.json index 5c4adcf51d..eeabbef5e5 100644 --- a/src/i18n/locale/ko.json +++ b/src/i18n/locale/ko.json @@ -303,7 +303,6 @@ "components.UserProfile.ProfileHeader.settings": "설정 수정", "components.UserProfile.UserSettings.UserGeneralSettings.admin": "관리자", "components.UserProfile.UserSettings.UserGeneralSettings.applanguage": "표시 언어", - "components.UserProfile.UserSettings.UserGeneralSettings.discordId": "디스코드 사용자 ID", "i18n.partiallyavailable": "일부 사용 가능", "pages.pagenotfound": "페이지를 찾을 수 없음", "components.Settings.RadarrModal.tagRequestsInfo": "요청자의 사용자 ID 및 표시 이름으로 추가 태그를 자동으로 추가합니다", @@ -1134,7 +1133,6 @@ "components.UserList.users": "사용자", "components.UserList.password": "비밀번호", "components.UserList.validationpasswordminchars": "비밀번호가 너무 짧습니다. 최소 8자리 이상이어야 합니다", - "components.UserProfile.UserSettings.UserGeneralSettings.discordIdTip": "Discord 사용자 계정과 연결된 다중 자릿수 ID 번호", "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletsettingssaved": "Pushbullet 알림 설정이 성공적으로 저장되었습니다!", "components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverApplicationToken": "유효한 애플리케이션 토큰을 입력해야 합니다", "components.UserProfile.UserSettings.UserGeneralSettings.plexuser": "Plex 사용자", @@ -1148,7 +1146,6 @@ "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsSuccess": "설정이 성공적으로 저장되었습니다!", "components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "사용자 계정과 연결된 다중 자릿수 ID 번호", "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseries": "영화 자동 요청", - "components.UserProfile.UserSettings.UserGeneralSettings.validationDiscordId": "유효한 Discord 사용자 ID를 입력해야 합니다", "components.UserProfile.UserSettings.UserNotificationSettings.discordsettingsfailed": "Discord 알림 설정을 저장하지 못했습니다.", "components.UserProfile.UserSettings.UserNotificationSettings.pushoverApplicationToken": "애플리케이션 API 토큰", "components.UserProfile.UserSettings.UserNotificationSettings.discordsettingssaved": "Discord 알림 설정이 성공적으로 저장되었습니다!", @@ -1190,5 +1187,486 @@ "components.QuotaSelector.seasons": "{count, plural, one {시즌} other {시즌}}", "components.RequestModal.QuotaDisplay.requestsremaining": "{remaining, plural, =0 {없음} other {#개의}} {type} {remaining, plural, one {요청} other {요청}} 남음", "components.Settings.SettingsJobsCache.editJobScheduleSelectorHours": "매 {jobScheduleHours, plural, one {시간} other {{jobScheduleHours} 시간}}", - "components.MovieDetails.imdbuserscore": "IMDB 사용자 점수" + "components.MovieDetails.imdbuserscore": "IMDB 사용자 점수", + "component.BlocklistBlock.blocklistdate": "", + "components.Settings.Notifications.NotificationsSlack.embedPoster": "", + "components.Discover.FilterSlideover.excludeKeywords": "", + "components.Layout.UserWarnings.emailInvalid": "", + "components.Layout.UserWarnings.emailRequired": "", + "components.Layout.UserWarnings.passwordRequired": "", + "components.Login.credentialerror": "", + "components.Settings.jellyfinlibraries": "", + "components.Login.port": "", + "components.Discover.FilterSlideover.status": "", + "components.IssueList.IssueItem.descriptionpreview": "", + "components.Login.tipEmailHasTrailingWhitespace": "", + "components.DiscoverTvUpcoming.upcomingtv": "", + "components.Settings.SettingsJobsCache.size": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersTip": "", + "components.CollectionDetails.removefromblocklistpartialcount": "", + "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegionTip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.mediaServerUser": "", + "components.UserProfile.UserSettings.UserGeneralSettings.save": "", + "components.UserProfile.UserSettings.UserGeneralSettings.saving": "", + "components.UserProfile.UserSettings.UserGeneralSettings.streamingRegion": "", + "components.UserProfile.UserSettings.UserGeneralSettings.streamingRegionTip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmailEmpty": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.linkedAccountsHint": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.noLinkedAccounts": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.noPermissionDescription": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorUnauthorized": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablewebpush": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.nodevicestoshow": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleted": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleteerror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.type": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.unknown": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeendisabled": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.deviceDefault": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdAdd": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdPlaceholder": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdRemove": "", + "components.Layout.UserWarnings.profileIncomplete": "", + "components.Login.adminerror": "", + "components.Login.description": "", + "components.Login.emailAlreadyTaken": "", + "components.Login.emailtooltip": "", + "components.Login.hostname": "", + "components.Login.initialsignin": "", + "components.Login.initialsigningin": "", + "components.Login.invalidurlerror": "", + "components.Login.jellyfinLocalLoginHint": "", + "components.Login.loginwithapp": "", + "components.Login.noadminerror": "", + "components.Login.orsigninwith": "", + "components.Login.save": "", + "components.Login.saveFailed": "", + "components.Login.signinwithjellyfin": "", + "components.Login.tipUsernameHasTrailingWhitespace": "", + "components.Login.title": "", + "components.Login.urlBase": "", + "components.Login.username": "", + "components.Login.validationEmailFormat": "", + "components.Login.validationPortRequired": "", + "components.MetadataSelector.selectMetdataProvider": "", + "components.MetadataSelector.tvdbLabel": "", + "components.MovieDetails.addtowatchlist": "", + "components.MovieDetails.openradarr": "", + "components.MovieDetails.play4k": "", + "components.MovieDetails.removefromwatchlist": "", + "components.MovieDetails.watchlistDeleted": "", + "components.MovieDetails.watchlistError": "", + "components.MovieDetails.watchlistSuccess": "", + "components.PermissionEdit.blocklistedItems": "", + "components.PermissionEdit.blocklistedItemsDescription": "", + "components.PermissionEdit.manageblocklist": "", + "components.PermissionEdit.manageblocklistDescription": "", + "components.RequestList.RequestItem.removearr": "", + "components.RequestList.sortDirection": "", + "components.Selector.CertificationSelector.minRating": "", + "components.Selector.CertificationSelector.selectCertification": "", + "components.Selector.CertificationSelector.selectCountry": "", + "components.Selector.CertificationSelector.starttyping": "", + "components.Selector.ended": "", + "components.Selector.inProduction": "", + "components.Selector.pilot": "", + "components.Selector.returningSeries": "", + "components.Selector.searchStatus": "", + "components.Selector.searchUsers": "", + "components.Settings.Notifications.NotificationsGotify.priority": "", + "components.Settings.Notifications.NotificationsGotify.validationPriorityRequired": "", + "components.Settings.Notifications.NotificationsNtfy.agentenabled": "", + "components.Settings.Notifications.NotificationsNtfy.embedPoster": "", + "components.Settings.Notifications.NotificationsNtfy.ntfysettingsfailed": "", + "components.Settings.Notifications.NotificationsNtfy.ntfysettingssaved": "", + "components.Settings.Notifications.NotificationsNtfy.priority": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestSending": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestSuccess": "", + "components.Settings.Notifications.NotificationsNtfy.token": "", + "components.Settings.Notifications.NotificationsNtfy.tokenAuth": "", + "components.Settings.Notifications.NotificationsNtfy.topic": "", + "components.Settings.Notifications.NotificationsNtfy.url": "", + "components.Settings.Notifications.NotificationsNtfy.username": "", + "components.Settings.Notifications.NotificationsNtfy.usernamePasswordAuth": "", + "components.Settings.Notifications.NotificationsNtfy.validationNtfyTopic": "", + "components.Settings.Notifications.NotificationsNtfy.validationNtfyUrl": "", + "components.Settings.Notifications.NotificationsNtfy.validationPriorityRequired": "", + "components.Settings.Notifications.NotificationsNtfy.validationTypes": "", + "components.Settings.Notifications.NotificationsPushover.deviceDefault": "", + "components.Settings.Notifications.NotificationsWebPush.embedPoster": "", + "components.Settings.Notifications.embedPoster": "", + "components.Settings.Notifications.messageThreadId": "", + "components.Settings.Notifications.messageThreadIdTip": "", + "components.Settings.RadarrModal.baseUrlHelp": "", + "components.Settings.SettingsJobsCache.dnsCache": "", + "components.Settings.SettingsJobsCache.process-blocklisted-tags": "", + "components.Settings.SettingsMain.hideBlocklistedTip": "", + "components.Settings.SettingsMain.validationUrl": "", + "components.Settings.SettingsMain.validationUrlTrailingSlash": "", + "components.Settings.SettingsMain.youtubeUrl": "", + "components.Settings.SettingsMain.youtubeUrlTip": "", + "components.Settings.SettingsNetwork.csrfProtectionHoverTip": "", + "components.Settings.SettingsNetwork.csrfProtectionTip": "", + "components.Settings.SettingsNetwork.networkDisclaimer": "", + "components.Settings.SettingsNetwork.networksettings": "", + "components.Settings.SettingsNetwork.networksettingsDescription": "", + "components.Settings.SettingsNetwork.proxyBypassFilter": "", + "components.Settings.SettingsNetwork.proxyEnabled": "", + "components.Settings.SettingsNetwork.proxyHostname": "", + "components.Settings.SonarrModal.seriesType": "", + "components.Settings.SonarrModal.syncEnabledHelp": "", + "components.Settings.chooseProvider": "", + "components.Setup.configemby": "", + "components.Setup.configuremediaserver": "", + "components.Setup.servertype": "", + "components.Setup.signin": "", + "components.Setup.signinWithEmby": "", + "components.Setup.signinWithJellyfin": "", + "components.Setup.signinWithPlex": "", + "components.Setup.subtitle": "", + "components.Blocklist.mediaTmdbId": "", + "components.Blocklist.mediaType": "", + "components.Blocklist.showAllBlocklisted": "", + "components.Layout.Sidebar.blocklist": "", + "components.Login.urlBaseHelp": "", + "components.Login.validationEmailRequired": "", + "components.Login.validationUrlBaseLeadingSlash": "", + "components.Login.validationUrlBaseTrailingSlash": "", + "components.Login.validationUrlTrailingSlash": "", + "components.Login.validationemailformat": "", + "components.Login.validationhostrequired": "", + "components.Login.validationservertyperequired": "", + "components.Login.validationusernamerequired": "", + "components.ManageSlideOver.removearr": "", + "components.Settings.Notifications.validationWebhookRoleId": "", + "components.Settings.Notifications.webhookRoleId": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.errorUnknown": "", + "components.MovieDetails.downloadstatus": "", + "components.Selector.CertificationSelector.maxRating": "", + "component.BlocklistBlock.blocklistedby": "", + "component.BlocklistModal.blocklisting": "", + "components.Blocklist.blocklistNotFoundError": "", + "components.Blocklist.blocklistSettingsDescription": "", + "components.Blocklist.blocklistdate": "", + "components.Blocklist.blocklistedby": "", + "components.Blocklist.blocklistsettings": "", + "components.Blocklist.filterBlocklistedTags": "", + "components.Blocklist.filterManual": "", + "components.Blocklist.mediaName": "", + "components.ManageSlideOver.manageModalRemoveMediaWarning": "", + "components.MetadataSelector.tmdbLabel": "", + "components.ManageSlideOver.removearr4k": "", + "components.MovieDetails.openradarr4k": "", + "components.PermissionEdit.viewblocklistedItems": "", + "components.PermissionEdit.viewblocklistedItemsDescription": "", + "components.RequestList.RequestItem.profileName": "", + "components.Discover.timeWindowDay": "", + "components.Selector.CertificationSelector.errorLoading": "", + "components.Selector.canceled": "", + "components.Selector.planned": "", + "components.Settings.Notifications.NotificationsPushover.embedPoster": "", + "components.Settings.Notifications.NotificationsWebhook.customHeaders": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersAdd": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersAuthConflict": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersIncomplete": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersKey": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersRemove": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersValue": "", + "components.Settings.Notifications.NotificationsWebhook.supportVariables": "", + "components.Settings.Notifications.NotificationsWebhook.supportVariablesTip": "", + "components.Settings.Notifications.NotificationsWebhook.webhookUrlTip": "", + "components.Settings.Notifications.useUserLocale": "", + "components.Settings.SettingsJobsCache.dnsCacheGlobalStats": "", + "components.Settings.SettingsJobsCache.dnsCacheGlobalStatsDescription": "", + "components.Settings.SettingsJobsCache.dnscacheage": "", + "components.Settings.SettingsJobsCache.ipv4Fallbacks": "", + "components.Settings.SettingsJobsCache.jellyfin-full-scan": "", + "components.Settings.SettingsJobsCache.jellyfin-recently-added-scan": "", + "components.Settings.SettingsMain.enableSpecialEpisodes": "", + "components.Settings.operational": "", + "components.Settings.overrideRules": "", + "components.Settings.yes": "", + "components.UserList.importfromJellyfinerror": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.password": "", + "components.Discover.timeWindowWeek": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingssaved": "", + "components.Login.saving": "", + "components.Login.servertype": "", + "components.Settings.blocklistedTagImportInstructions": "", + "components.Settings.menuJellyfinSettings": "", + "components.Settings.valueRequired": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": "", + "i18n.specials": "", + "components.Settings.Notifications.usePublicLogo": "", + "components.Settings.Notifications.usePublicLogoTip": "", + "components.Settings.Notifications.validationMessageThreadId": "", + "components.Settings.Notifications.webhookRoleIdTip": "", + "components.Settings.OverrideRuleModal.conditionsDescription": "", + "components.Settings.OverrideRuleModal.createrule": "", + "components.Settings.OverrideRuleModal.editrule": "", + "components.Settings.OverrideRuleModal.genres": "", + "components.Settings.OverrideRuleModal.keywords": "", + "components.Settings.OverrideRuleModal.languages": "", + "components.Settings.OverrideRuleModal.notagoptions": "", + "components.Settings.OverrideRuleModal.qualityprofile": "", + "components.Settings.OverrideRuleModal.rootfolder": "", + "components.Settings.OverrideRuleModal.ruleCreated": "", + "components.Settings.OverrideRuleModal.ruleUpdated": "", + "components.Settings.OverrideRuleModal.selectQualityProfile": "", + "components.Settings.OverrideRuleModal.selectRootFolder": "", + "components.Settings.OverrideRuleModal.selectService": "", + "components.Settings.OverrideRuleModal.selecttags": "", + "components.Settings.OverrideRuleModal.service": "", + "components.Settings.OverrideRuleModal.serviceDescription": "", + "components.Settings.OverrideRuleModal.settings": "", + "components.Settings.OverrideRuleModal.settingsDescription": "", + "components.Settings.OverrideRuleModal.tags": "", + "components.Settings.OverrideRuleModal.users": "", + "components.Settings.OverrideRuleTile.conditions": "", + "components.Settings.OverrideRuleTile.keywords": "", + "components.Settings.OverrideRuleTile.language": "", + "components.Settings.OverrideRuleTile.qualityprofile": "", + "components.Settings.OverrideRuleTile.rootfolder": "", + "components.Settings.OverrideRuleTile.settings": "", + "components.Settings.OverrideRuleTile.tags": "", + "components.Settings.OverrideRuleTile.users": "", + "components.Settings.RadarrModal.apiKeyHelp": "", + "components.Settings.RadarrModal.enableSearchHelp": "", + "components.Settings.RadarrModal.externalUrlHelp": "", + "components.Settings.RadarrModal.server4kHelp": "", + "components.Settings.RadarrModal.syncEnabledHelp": "", + "components.Settings.SettingsAbout.aboutseerr": "", + "components.Settings.SettingsAbout.contribute": "", + "components.Settings.SettingsAbout.supportseerr": "", + "components.Settings.SettingsJobsCache.dnsCacheDescription": "", + "components.Settings.SettingsJobsCache.dnscacheactiveaddress": "", + "components.Settings.SettingsJobsCache.dnscacheflushed": "", + "components.Settings.SettingsJobsCache.dnscachehits": "", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorDays": "", + "components.Settings.SettingsJobsCache.failures": "", + "components.Settings.SettingsJobsCache.flushdnscache": "", + "components.Settings.SettingsJobsCache.plex-refresh-token": "", + "components.Settings.SettingsMain.apikeyCopied": "", + "components.Settings.SettingsMain.blocklistLanguage": "", + "components.Settings.SettingsMain.blocklistLanguageTip": "", + "components.Settings.SettingsMain.blocklistRegion": "", + "components.Settings.SettingsMain.blocklistRegionTip": "", + "components.Settings.SettingsMain.blocklistedTags": "", + "components.Settings.SettingsMain.discoverRegion": "", + "components.Settings.SettingsMain.discoverRegionTip": "", + "components.Settings.SettingsMain.hideBlocklisted": "", + "components.Settings.SettingsMain.streamingRegionTip": "", + "components.Settings.SettingsNetwork.dnsCache": "", + "components.Settings.SettingsNetwork.dnsCacheForceMaxTtl": "", + "components.Settings.SettingsNetwork.dnsCacheForceMinTtl": "", + "components.Settings.SettingsNetwork.dnsCacheHoverTip": "", + "components.Settings.SettingsNetwork.dnsCacheTip": "", + "components.Settings.SettingsNetwork.forceIpv4First": "", + "components.Settings.SettingsNetwork.forceIpv4FirstTip": "", + "components.Settings.SettingsNetwork.network": "", + "components.Settings.SettingsNetwork.proxyEnabledTip": "", + "components.Settings.SettingsNetwork.proxyPassword": "", + "components.Settings.SettingsNetwork.proxyPort": "", + "components.Settings.SettingsNetwork.proxySsl": "", + "components.Settings.SettingsNetwork.proxyUser": "", + "components.Settings.SettingsNetwork.toastSettingsFailure": "", + "components.Settings.SettingsNetwork.toastSettingsSuccess": "", + "components.Settings.SettingsNetwork.trustProxy": "", + "components.Settings.SettingsNetwork.trustProxyTip": "", + "components.Settings.SettingsNetwork.validationApiRequestTimeout": "", + "components.Settings.SettingsUsers.atLeastOneAuth": "", + "components.Settings.SettingsUsers.disabledMediaServerLoginWarning": "", + "components.Settings.SettingsUsers.loginMethods": "", + "components.Settings.SettingsUsers.loginMethodsTip": "", + "components.Settings.SettingsUsers.mediaServerLogin": "", + "components.Settings.SettingsUsers.mediaServerLoginTip": "", + "components.Settings.SonarrModal.animeSeriesType": "", + "components.Settings.SonarrModal.apiKeyHelp": "", + "components.Settings.SonarrModal.baseUrlHelp": "", + "components.Settings.SonarrModal.enableSearchHelp": "", + "components.Settings.SonarrModal.externalUrlHelp": "", + "components.Settings.SonarrModal.monitorNewItems": "", + "components.Settings.SonarrModal.monitorNewItemsHelp": "", + "components.Settings.SonarrModal.server4kHelp": "", + "components.Settings.addrule": "", + "components.Settings.clearBlocklistedTagsConfirm": "", + "components.Settings.clickTest": "", + "components.Settings.connectionTestFailed": "", + "components.Settings.copyBlocklistedTags": "", + "components.Settings.copyBlocklistedTagsEmpty": "", + "components.Settings.copyBlocklistedTagsTip": "", + "components.Settings.failedToSaveMetadataSettings": "", + "components.Settings.general": "", + "components.Settings.importBlocklistedTagsTip": "", + "components.Settings.invalidKeyword": "", + "components.Settings.jellyfinForgotPasswordUrl": "", + "components.Settings.jellyfinSettings": "", + "components.Settings.jellyfinSettingsDescription": "", + "components.Settings.jellyfinSettingsSuccess": "", + "components.Settings.jellyfinSyncFailedAutomaticGroupedFolders": "", + "components.Settings.jellyfinSyncFailedGenericError": "", + "components.Settings.jellyfinSyncFailedNoLibrariesFound": "", + "components.Settings.jellyfinsettingsDescription": "", + "components.RequestCard.failedmodify": "", + "components.RequestList.RequestItem.failedmodify": "", + "components.RequestList.unableToConnect": "", + "components.Settings.manualscanDescriptionJellyfin": "", + "components.Settings.menuNetwork": "", + "components.Settings.metadataProviderSelection": "", + "components.Settings.metadataSettings": "", + "components.Settings.metadataSettingsSaved": "", + "components.Settings.no": "", + "components.Settings.noSpecialCharacters": "", + "components.Settings.nooptions": "", + "components.Settings.notTested": "", + "components.Settings.overrideRulesDescription": "", + "components.Settings.providerStatus": "", + "components.Settings.save": "", + "components.Settings.saving": "", + "components.Settings.scanbackground": "", + "components.Settings.searchKeywords": "", + "components.Settings.seriesMetadataProvider": "", + "components.Settings.settings": "", + "components.Settings.starttyping": "", + "components.Settings.syncJellyfin": "", + "components.Settings.syncing": "", + "components.Settings.timeout": "", + "components.Settings.tmdbProviderDoesnotWork": "", + "components.Settings.tvdbProviderDoesnotWork": "", + "components.Selector.CertificationSelector.noOptions": "", + "components.Setup.back": "", + "components.StatusBadge.seasonnumber": "", + "components.TitleCard.watchlistCancel": "", + "components.TitleCard.watchlistDeleted": "", + "components.TitleCard.watchlistError": "", + "components.TitleCard.watchlistSuccess": "", + "components.TvDetails.addtowatchlist": "", + "components.TvDetails.play": "", + "components.TvDetails.play4k": "", + "components.Settings.Notifications.NotificationsNtfy.password": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestFailed": "", + "components.TvDetails.watchlistDeleted": "", + "components.TvDetails.watchlistError": "", + "components.TvDetails.watchlistSuccess": "", + "components.UserList.ascending": "", + "components.UserList.descending": "", + "components.UserList.importedPlexUsersNoPassword": "", + "components.UserList.importedUsersNoPassword": "", + "components.UserList.importedfromJellyfin": "", + "components.UserList.importfromJellyfin": "", + "components.UserList.importfrommediaserver": "", + "components.UserList.sortBy": "", + "components.Settings.Notifications.NotificationsPushover.sound": "", + "components.UserList.sortByJoined": "", + "components.UserList.sortByRequests": "", + "components.UserList.sortByRole": "", + "components.UserList.sortByType": "", + "components.UserList.sortByUser": "", + "components.UserList.toggleSortDirection": "", + "components.UserList.toggleSortDirectionAria": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.description": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorExists": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorUnauthorized": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorUnknown": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.passwordRequired": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.save": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.title": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.username": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.usernameRequired": "", + "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegion": "", + "components.UserProfile.UserSettings.UserGeneralSettings.email": "", + "components.UserProfile.UserSettings.UserGeneralSettings.validationemailformat": "", + "components.UserProfile.UserSettings.UserGeneralSettings.validationemailrequired": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.deleteFailed": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorExists": "", + "components.Settings.Notifications.userEmailRequired": "", + "components.Settings.OverrideRuleModal.create": "", + "components.UserProfile.UserSettings.UserNotificationSettings.sound": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadId": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadIdTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramMessageThreadId": "", + "components.UserProfile.UserSettings.UserPasswordChange.localPasswordDescription": "", + "components.UserProfile.UserSettings.menuLinkedAccounts": "", + "components.UserProfile.localWatchlist": "", + "i18n.addToBlocklist": "", + "i18n.blocklist": "", + "i18n.blocklistDuplicateError": "", + "i18n.blocklistError": "", + "i18n.blocklistSuccess": "", + "i18n.blocklisted": "", + "i18n.completed": "", + "i18n.deleted": "", + "i18n.error": "", + "i18n.notificationLocale": "", + "components.Settings.SettingsJobsCache.dnscachemisses": "", + "components.Settings.SettingsJobsCache.dnscachename": "", + "components.Settings.SettingsJobsCache.hits": "", + "components.Settings.SettingsMain.streamingRegion": "", + "components.Settings.SettingsNetwork.csrfProtection": "", + "components.Settings.SettingsNetwork.proxyBypassFilterTip": "", + "components.Settings.SettingsNetwork.proxyBypassLocalAddresses": "", + "components.Settings.allChosenProvidersAreOperational": "", + "components.Settings.animeMetadataProvider": "", + "components.Settings.apiKey": "", + "components.Settings.blocklistedTagImportTitle": "", + "components.Settings.blocklistedTagsText": "", + "components.Settings.jellyfinlibrariesDescription": "", + "components.Settings.jellyfinsettings": "", + "components.Settings.tip": "", + "components.TitleCard.addToWatchList": "", + "components.TvDetails.removefromwatchlist": "", + "components.UserList.mediaServerUser": "", + "components.UserList.validationUsername": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablingwebpusherror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.engine": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.managedevices": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.operatingsystem": "", + "i18n.removeFromBlocklistSuccess": "", + "i18n.removefromBlocklist": "", + "components.Login.enablessl": "", + "components.Settings.OverrideRuleModal.conditions": "", + "components.Settings.SettingsMain.hideAvailableTip": "", + "components.Settings.SettingsNetwork.docs": "", + "components.Setup.librarieserror": "", + "components.Settings.SettingsJobsCache.dnsNoCacheEntries": "", + "components.Settings.SettingsJobsCache.usersavatars": "", + "components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "", + "components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "", + "components.Settings.SettingsNetwork.validationProxyPort": "", + "components.Settings.manualscanJellyfin": "", + "components.Setup.configjellyfin": "", + "components.Setup.configplex": "", + "components.Login.back": "", + "components.UserList.username": "", + "components.Settings.OverrideRuleTile.genre": "", + "components.Settings.SettingsMain.blocklistedTagsLimit": "", + "components.Settings.SettingsMain.blocklistedTagsLimitTip": "", + "components.Settings.SettingsMain.blocklistedTagsTip": "", + "components.Settings.metadataProviderSettings": "", + "components.UserList.newJellyfinsigninenabled": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.created": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.deletesubscription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablewebpush": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablingwebpusherror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeenenabled": "", + "i18n.partiallyblocklisted": "", + "components.UserList.noJellyfinuserstoimport": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmail": "", + "components.Settings.invalidurlerror": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.linkedAccounts": "", + "components.Discover.FilterSlideover.certification": "", + "components.Settings.SettingsNetwork.apiRequestTimeout": "", + "components.Settings.SettingsNetwork.apiRequestTimeoutTip": "", + "components.Settings.failed": "", + "components.Settings.jellyfinSettingsFailure": "", + "components.Settings.SettingsJobsCache.misses": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.saving": "", + "components.Settings.SettingsJobsCache.hitRate": "", + "components.Discover.RecentRequestsSlider.unableToConnect": "", + "components.Settings.menuMetadataProviders": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.device": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.browser": "", + "components.Login.validationhostformat": "", + "components.MovieDetails.play": "" } diff --git a/src/i18n/locale/lb.json b/src/i18n/locale/lb.json index 0967ef424b..f6cd96e52b 100644 --- a/src/i18n/locale/lb.json +++ b/src/i18n/locale/lb.json @@ -1 +1,1672 @@ -{} +{ + "components.UserProfile.UserSettings.UserGeneralSettings.email": "", + "components.UserProfile.UserSettings.UserGeneralSettings.general": "", + "components.UserProfile.UserSettings.UserGeneralSettings.generalsettings": "", + "components.UserProfile.UserSettings.UserGeneralSettings.languageDefault": "", + "components.UserProfile.UserSettings.UserGeneralSettings.mediaServerUser": "", + "components.UserProfile.UserSettings.UserGeneralSettings.movierequestlimit": "", + "components.UserProfile.UserSettings.UserGeneralSettings.originallanguage": "", + "components.UserProfile.UserSettings.UserGeneralSettings.originallanguageTip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexuser": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmovies": "", + "components.UserProfile.UserSettings.UserGeneralSettings.region": "", + "components.UserProfile.UserSettings.UserGeneralSettings.role": "", + "components.UserProfile.UserSettings.UserGeneralSettings.save": "", + "components.UserProfile.UserSettings.UserGeneralSettings.saving": "", + "components.UserProfile.UserSettings.UserGeneralSettings.seriesrequestlimit": "", + "components.UserProfile.UserSettings.UserGeneralSettings.streamingRegion": "", + "components.UserProfile.UserSettings.UserGeneralSettings.streamingRegionTip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailure": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmail": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmailEmpty": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsSuccess": "", + "components.UserProfile.UserSettings.UserGeneralSettings.validationemailformat": "", + "components.UserProfile.UserSettings.UserGeneralSettings.validationemailrequired": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.deleteFailed": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.errorUnknown": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorUnauthorized": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.browser": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.created": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.deletesubscription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.device": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablewebpush": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleteerror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.type": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeendisabled": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeenenabled": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.deviceDefault": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordId": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdAdd": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdPlaceholder": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdRemove": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "", + "components.Discover.upcoming": "", + "components.IssueModal.CreateIssueModal.reportissue": "", + "components.Settings.SettingsJobsCache.cachekeys": "", + "components.Settings.SettingsJobsCache.hits": "", + "components.Settings.SettingsMain.originallanguageTip": "", + "components.Settings.SonarrModal.tags": "", + "components.Discover.tmdbmoviestreamingservices": "", + "components.Layout.Sidebar.users": "", + "components.RequestBlock.delete": "", + "components.Settings.default": "", + "components.IssueDetails.playonplex": "", + "components.IssueDetails.problemepisode": "", + "components.Discover.DiscoverMovies.sortTitleAsc": "", + "components.Discover.DiscoverMovies.sortTitleDesc": "", + "components.Discover.DiscoverMovies.sortTmdbRatingAsc": "", + "components.Discover.DiscoverMovies.sortTmdbRatingDesc": "", + "components.Discover.DiscoverNetwork.networkSeries": "", + "components.Discover.DiscoverSliderEdit.deletefail": "", + "components.Discover.DiscoverSliderEdit.deletesuccess": "", + "components.Discover.DiscoverSliderEdit.enable": "", + "components.Discover.DiscoverSliderEdit.remove": "", + "components.Discover.DiscoverStudio.studioMovies": "", + "components.Discover.DiscoverTv.activefilters": "", + "components.Discover.FilterSlideover.to": "", + "components.Discover.FilterSlideover.voteCount": "", + "components.Discover.MovieGenreList.moviegenres": "", + "components.Discover.MovieGenreSlider.moviegenres": "", + "components.Discover.NetworkSlider.networks": "", + "components.Settings.Notifications.encryptionTip": "", + "components.Discover.popularmovies": "", + "components.Discover.tvgenres": "", + "components.Discover.updatefailed": "", + "components.Discover.updatesuccess": "", + "components.DownloadBlock.estimatedtime": "", + "components.DownloadBlock.formattedTitle": "", + "components.IssueDetails.IssueComment.areyousuredelete": "", + "components.IssueDetails.IssueComment.delete": "", + "components.IssueDetails.IssueComment.postedby": "", + "components.IssueDetails.IssueComment.postedbyedited": "", + "components.IssueDetails.IssueComment.validationComment": "", + "components.IssueDetails.IssueDescription.deleteissue": "", + "components.IssueDetails.IssueDescription.description": "", + "components.IssueDetails.IssueDescription.edit": "", + "components.IssueDetails.allepisodes": "", + "components.IssueDetails.allseasons": "", + "components.IssueDetails.closeissue": "", + "components.IssueDetails.closeissueandcomment": "", + "components.IssueDetails.commentplaceholder": "", + "components.IssueDetails.comments": "", + "components.IssueDetails.deleteissue": "", + "components.IssueDetails.deleteissueconfirm": "", + "components.IssueDetails.episode": "", + "components.IssueDetails.issuepagetitle": "", + "components.IssueDetails.issuetype": "", + "components.IssueDetails.lastupdated": "", + "components.IssueDetails.leavecomment": "", + "components.IssueDetails.toaststatusupdatefailed": "", + "components.IssueDetails.unknownissuetype": "", + "components.IssueList.IssueItem.descriptionpreview": "", + "components.IssueList.IssueItem.episodes": "", + "components.IssueList.IssueItem.issuestatus": "", + "components.IssueList.IssueItem.issuetype": "", + "components.IssueList.IssueItem.opened": "", + "components.IssueList.IssueItem.openeduserdate": "", + "components.IssueList.IssueItem.problemepisode": "", + "components.IssueList.IssueItem.seasons": "", + "components.IssueList.IssueItem.unknownissuetype": "", + "components.IssueList.IssueItem.viewissue": "", + "components.IssueList.showallissues": "", + "components.IssueList.sortAdded": "", + "components.IssueList.sortModified": "", + "components.IssueModal.CreateIssueModal.allepisodes": "", + "components.IssueModal.CreateIssueModal.allseasons": "", + "components.IssueModal.CreateIssueModal.episode": "", + "components.IssueModal.CreateIssueModal.extras": "", + "components.IssueModal.CreateIssueModal.problemepisode": "", + "components.IssueModal.CreateIssueModal.problemseason": "", + "components.IssueModal.CreateIssueModal.providedetail": "", + "components.IssueModal.CreateIssueModal.season": "", + "components.IssueModal.CreateIssueModal.submitissue": "", + "components.IssueModal.CreateIssueModal.toastFailedCreate": "", + "components.IssueModal.CreateIssueModal.toastSuccessCreate": "", + "components.IssueModal.CreateIssueModal.toastviewissue": "", + "components.IssueModal.CreateIssueModal.validationMessageRequired": "", + "components.IssueModal.CreateIssueModal.whatswrong": "", + "components.Layout.UserWarnings.emailInvalid": "", + "components.Layout.UserWarnings.emailRequired": "", + "components.Layout.UserWarnings.passwordRequired": "", + "components.Layout.VersionStatus.commitsbehind": "", + "components.MovieDetails.imdbuserscore": "", + "components.MovieDetails.markavailable": "", + "components.Settings.RadarrModal.defaultserver": "", + "components.Layout.UserWarnings.profileIncomplete": "", + "components.Login.emailAlreadyTaken": "", + "components.Login.jellyfinLocalLoginHint": "", + "components.Login.saveFailed": "", + "components.Login.validationemailrequired": "", + "components.Login.validationhostrequired": "", + "components.Login.validationpasswordrequired": "", + "components.Login.validationservertyperequired": "", + "components.ManageSlideOver.alltime": "", + "components.ManageSlideOver.manageModalClearMedia": "", + "components.MovieDetails.MovieCast.fullcast": "", + "components.MovieDetails.MovieCrew.fullcrew": "", + "components.MovieDetails.addtowatchlist": "", + "components.MovieDetails.budget": "", + "components.MovieDetails.cast": "", + "components.MovieDetails.digitalrelease": "", + "components.MovieDetails.downloadstatus": "", + "components.MovieDetails.managemovie": "", + "components.MovieDetails.mark4kavailable": "", + "components.MovieDetails.originaltitle": "", + "components.MovieDetails.overview": "", + "components.MovieDetails.overviewunavailable": "", + "components.MovieDetails.play": "", + "components.MovieDetails.play4k": "", + "components.MovieDetails.productioncountries": "", + "components.MovieDetails.recommendations": "", + "components.MovieDetails.releasedate": "", + "components.MovieDetails.removefromwatchlist": "", + "components.MovieDetails.reportissue": "", + "components.MovieDetails.revenue": "", + "components.MovieDetails.rtaudiencescore": "", + "components.MovieDetails.rtcriticsscore": "", + "components.MovieDetails.runtime": "", + "components.MovieDetails.showless": "", + "components.MovieDetails.showmore": "", + "components.MovieDetails.similar": "", + "components.MovieDetails.studio": "", + "components.MovieDetails.theatricalrelease": "", + "components.MovieDetails.tmdbuserscore": "", + "components.MovieDetails.viewfullcrew": "", + "components.MovieDetails.watchlistDeleted": "", + "components.MovieDetails.watchlistError": "", + "components.MovieDetails.watchlistSuccess": "", + "components.MovieDetails.watchtrailer": "", + "components.NotificationTypeSelector.adminissuecommentDescription": "", + "components.NotificationTypeSelector.adminissuereopenedDescription": "", + "components.NotificationTypeSelector.adminissueresolvedDescription": "", + "components.NotificationTypeSelector.issuecreated": "", + "components.NotificationTypeSelector.issuecreatedDescription": "", + "components.NotificationTypeSelector.issuereopened": "", + "components.NotificationTypeSelector.issuereopenedDescription": "", + "components.NotificationTypeSelector.issueresolved": "", + "components.NotificationTypeSelector.issueresolvedDescription": "", + "components.NotificationTypeSelector.mediaAutoApproved": "", + "components.NotificationTypeSelector.mediaAutoApprovedDescription": "", + "components.NotificationTypeSelector.mediaapproved": "", + "components.NotificationTypeSelector.mediaapprovedDescription": "", + "components.NotificationTypeSelector.mediaautorequested": "", + "components.NotificationTypeSelector.mediaautorequestedDescription": "", + "components.NotificationTypeSelector.mediaavailable": "", + "components.NotificationTypeSelector.mediaavailableDescription": "", + "components.NotificationTypeSelector.mediadeclined": "", + "components.NotificationTypeSelector.mediadeclinedDescription": "", + "components.NotificationTypeSelector.mediafailed": "", + "components.NotificationTypeSelector.mediafailedDescription": "", + "components.NotificationTypeSelector.mediarequested": "", + "components.NotificationTypeSelector.notificationTypes": "", + "components.NotificationTypeSelector.userissuecommentDescription": "", + "components.NotificationTypeSelector.userissuecreatedDescription": "", + "components.NotificationTypeSelector.userissuereopenedDescription": "", + "components.NotificationTypeSelector.userissueresolvedDescription": "", + "components.NotificationTypeSelector.usermediaAutoApprovedDescription": "", + "components.NotificationTypeSelector.usermediaapprovedDescription": "", + "components.NotificationTypeSelector.usermediaavailableDescription": "", + "components.NotificationTypeSelector.usermediadeclinedDescription": "", + "components.NotificationTypeSelector.usermediafailedDescription": "", + "components.NotificationTypeSelector.usermediarequestedDescription": "", + "components.PermissionEdit.admin": "", + "components.PermissionEdit.adminDescription": "", + "components.PermissionEdit.advancedrequest": "", + "components.PermissionEdit.advancedrequestDescription": "", + "components.PermissionEdit.autoapprove": "", + "components.PermissionEdit.autoapprove4kDescription": "", + "components.PermissionEdit.autoapprove4kMovies": "", + "components.PermissionEdit.autoapproveMovies": "", + "components.PermissionEdit.autoapproveMoviesDescription": "", + "components.PermissionEdit.autorequestMovies": "", + "components.PermissionEdit.autorequestMoviesDescription": "", + "components.PermissionEdit.autorequestSeries": "", + "components.PermissionEdit.autorequestSeriesDescription": "", + "components.PermissionEdit.blocklistedItems": "", + "components.PermissionEdit.blocklistedItemsDescription": "", + "components.PermissionEdit.createissues": "", + "components.PermissionEdit.createissuesDescription": "", + "components.PermissionEdit.manageblocklist": "", + "components.PermissionEdit.manageblocklistDescription": "", + "components.PermissionEdit.manageissues": "", + "components.PermissionEdit.manageissuesDescription": "", + "components.PermissionEdit.managerequests": "", + "components.PermissionEdit.managerequestsDescription": "", + "components.PermissionEdit.request4k": "", + "components.PermissionEdit.request4kDescription": "", + "components.PermissionEdit.request4kMovies": "", + "components.PermissionEdit.request4kMoviesDescription": "", + "components.PermissionEdit.request4kTv": "", + "components.PermissionEdit.requestMovies": "", + "components.PermissionEdit.requestMoviesDescription": "", + "components.PermissionEdit.requestTv": "", + "components.PermissionEdit.requestTvDescription": "", + "components.PermissionEdit.viewissues": "", + "components.PermissionEdit.viewissuesDescription": "", + "components.PermissionEdit.viewrecent": "", + "components.PermissionEdit.viewrecentDescription": "", + "components.PermissionEdit.viewrequests": "", + "components.PermissionEdit.viewrequestsDescription": "", + "components.PermissionEdit.viewwatchlists": "", + "components.PermissionEdit.viewwatchlistsDescription": "", + "components.PersonDetails.alsoknownas": "", + "components.PersonDetails.crewmember": "", + "components.QuotaSelector.seasons": "", + "components.QuotaSelector.tvRequests": "", + "components.RegionSelector.regionDefault": "", + "components.RegionSelector.regionServerDefault": "", + "components.RequestBlock.approve": "", + "components.RequestBlock.decline": "", + "components.RequestBlock.edit": "", + "components.RequestBlock.languageprofile": "", + "components.RequestButton.declinerequest4k": "", + "components.RequestButton.declinerequests": "", + "components.RequestButton.requestmore": "", + "components.RequestButton.viewrequest": "", + "components.RequestButton.viewrequest4k": "", + "components.RequestModal.AdvancedRequester.destinationserver": "", + "components.RequestModal.AdvancedRequester.folder": "", + "components.RequestModal.AdvancedRequester.notagoptions": "", + "components.RequestModal.AdvancedRequester.qualityprofile": "", + "components.RequestModal.AdvancedRequester.rootfolder": "", + "components.RequestModal.AdvancedRequester.selecttags": "", + "components.RequestModal.QuotaDisplay.movie": "", + "components.RequestModal.QuotaDisplay.movielimit": "", + "components.RequestModal.QuotaDisplay.notenoughseasonrequests": "", + "components.RequestModal.QuotaDisplay.quotaLink": "", + "components.RequestModal.QuotaDisplay.quotaLinkUser": "", + "components.RequestModal.QuotaDisplay.requestsremaining": "", + "components.RequestModal.QuotaDisplay.requiredquota": "", + "components.RequestModal.QuotaDisplay.requiredquotaUser": "", + "components.RequestModal.QuotaDisplay.seasonlimit": "", + "components.RequestModal.SearchByNameModal.nomatches": "", + "components.RequestModal.SearchByNameModal.notvdbiddescription": "", + "components.RequestModal.autoapproval": "", + "components.RequestModal.cancel": "", + "components.RequestModal.edit": "", + "components.RequestModal.errorediting": "", + "components.RequestModal.numberofepisodes": "", + "components.RequestModal.pending4krequest": "", + "components.RequestModal.pendingapproval": "", + "components.RequestModal.requestadmin": "", + "components.RequestModal.requestcancelled": "", + "components.RequestModal.requestcollection4ktitle": "", + "components.RequestModal.requestcollectiontitle": "", + "components.RequestModal.requestedited": "", + "components.RequestModal.requesterror": "", + "components.RequestModal.requestfrom": "", + "components.RequestModal.requestmovie4ktitle": "", + "components.RequestModal.requestmovies": "", + "components.RequestModal.requestmovies4k": "", + "components.RequestModal.requestmovietitle": "", + "components.RequestModal.requestseasons": "", + "components.RequestModal.requestseasons4k": "", + "components.RequestModal.requestseries4ktitle": "", + "components.RequestModal.requestseriestitle": "", + "components.RequestModal.seasonnumber": "", + "components.RequestModal.selectmovies": "", + "components.RequestModal.selectseason": "", + "components.ResetPassword.confirmpassword": "", + "components.ResetPassword.email": "", + "components.ResetPassword.emailresetlink": "", + "components.ResetPassword.gobacklogin": "", + "components.ResetPassword.password": "", + "components.ResetPassword.requestresetlinksuccessmessage": "", + "components.ResetPassword.resetpasswordsuccessmessage": "", + "components.ResetPassword.validationemailrequired": "", + "components.ResetPassword.validationpasswordmatch": "", + "components.ResetPassword.validationpasswordminchars": "", + "components.Search.search": "", + "components.Search.searchresults": "", + "components.Selector.CertificationSelector.errorLoading": "", + "components.Selector.CertificationSelector.maxRating": "", + "components.Selector.CertificationSelector.selectCountry": "", + "components.Selector.ended": "", + "components.Selector.inProduction": "", + "components.Selector.nooptions": "", + "components.Selector.pilot": "", + "components.Selector.planned": "", + "components.Settings.Notifications.NotificationsGotify.validationUrlRequired": "", + "components.Settings.Notifications.NotificationsGotify.validationUrlTrailingSlash": "", + "components.Settings.Notifications.NotificationsNtfy.agentenabled": "", + "components.Settings.Notifications.NotificationsNtfy.embedPoster": "", + "components.Settings.Notifications.NotificationsNtfy.ntfysettingsfailed": "", + "components.Settings.Notifications.NotificationsNtfy.ntfysettingssaved": "", + "components.Settings.Notifications.NotificationsNtfy.password": "", + "components.Settings.Notifications.NotificationsNtfy.priority": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestSending": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestSuccess": "", + "components.Settings.Notifications.NotificationsNtfy.token": "", + "components.Settings.Notifications.NotificationsNtfy.tokenAuth": "", + "components.Settings.Notifications.NotificationsNtfy.topic": "", + "components.Settings.Notifications.NotificationsNtfy.url": "", + "components.Settings.Notifications.NotificationsNtfy.username": "", + "components.Settings.Notifications.NotificationsNtfy.usernamePasswordAuth": "", + "components.Settings.Notifications.NotificationsNtfy.validationNtfyTopic": "", + "components.Settings.Notifications.NotificationsNtfy.validationNtfyUrl": "", + "components.Settings.Notifications.NotificationsNtfy.validationPriorityRequired": "", + "components.Settings.Notifications.NotificationsNtfy.validationTypes": "", + "components.Settings.Notifications.NotificationsPushbullet.accessToken": "", + "components.Settings.Notifications.NotificationsPushover.agentenabled": "", + "components.Settings.Notifications.NotificationsPushover.deviceDefault": "", + "components.Settings.Notifications.NotificationsPushover.embedPoster": "", + "components.Settings.Notifications.NotificationsPushover.pushoversettingsfailed": "", + "components.Settings.Notifications.NotificationsPushover.pushoversettingssaved": "", + "components.Settings.Notifications.NotificationsPushover.toastPushoverTestFailed": "", + "components.Settings.Notifications.NotificationsPushover.toastPushoverTestSending": "", + "components.Settings.Notifications.NotificationsPushover.toastPushoverTestSuccess": "", + "components.Settings.Notifications.NotificationsPushover.userTokenTip": "", + "components.Settings.Notifications.NotificationsPushover.validationAccessTokenRequired": "", + "components.Settings.Notifications.NotificationsPushover.validationTypes": "", + "components.Settings.Notifications.NotificationsSlack.embedPoster": "", + "components.Settings.Notifications.NotificationsSlack.slacksettingsfailed": "", + "components.Settings.Notifications.NotificationsSlack.slacksettingssaved": "", + "components.Settings.Notifications.NotificationsSlack.toastSlackTestFailed": "", + "components.Settings.Notifications.NotificationsSlack.toastSlackTestSending": "", + "components.Settings.Notifications.NotificationsSlack.toastSlackTestSuccess": "", + "components.Settings.Notifications.NotificationsSlack.validationTypes": "", + "components.Settings.Notifications.NotificationsSlack.validationWebhookUrl": "", + "components.Settings.Notifications.NotificationsSlack.webhookUrl": "", + "components.Settings.Notifications.NotificationsSlack.webhookUrlTip": "", + "components.Settings.Notifications.allowselfsigned": "", + "components.Settings.Notifications.authPass": "", + "components.Settings.Notifications.authUser": "", + "components.Settings.Notifications.botAPI": "", + "components.Settings.Notifications.emailsender": "", + "components.Settings.Notifications.emailsettingsfailed": "", + "components.Settings.Notifications.emailsettingssaved": "", + "components.Settings.Notifications.embedPoster": "", + "components.Settings.Notifications.enableMentions": "", + "components.Settings.Notifications.encryptionImplicitTls": "", + "components.Settings.Notifications.encryptionNone": "", + "components.Settings.Notifications.encryptionOpportunisticTls": "", + "components.Settings.Notifications.messageThreadId": "", + "components.Settings.Notifications.messageThreadIdTip": "", + "components.Settings.Notifications.pgpPassword": "", + "components.Settings.Notifications.pgpPasswordTip": "", + "components.Settings.Notifications.pgpPrivateKey": "", + "components.Settings.Notifications.pgpPrivateKeyTip": "", + "components.Settings.Notifications.sendSilently": "", + "components.Settings.Notifications.sendSilentlyTip": "", + "components.Settings.Notifications.senderName": "", + "components.Settings.Notifications.smtpHost": "", + "components.Settings.Notifications.smtpPort": "", + "components.Settings.Notifications.telegramsettingsfailed": "", + "components.Settings.Notifications.telegramsettingssaved": "", + "components.Settings.Notifications.toastDiscordTestSending": "", + "components.Settings.Notifications.toastDiscordTestSuccess": "", + "components.Settings.Notifications.toastEmailTestFailed": "", + "components.Settings.Notifications.toastEmailTestSending": "", + "components.Settings.Notifications.toastEmailTestSuccess": "", + "components.Settings.Notifications.toastTelegramTestFailed": "", + "components.Settings.Notifications.toastTelegramTestSuccess": "", + "components.Settings.OverrideRuleTile.conditions": "", + "components.Settings.RadarrModal.validationApiKeyRequired": "", + "components.Settings.SettingsLogs.logs": "", + "components.Settings.SettingsNetwork.dnsCacheForceMaxTtl": "", + "components.Settings.SettingsAbout.supportseerr": "", + "components.Settings.notificationAgentSettingsDescription": "", + "components.Settings.SettingsJobsCache.cacheDescription": "", + "components.Settings.SettingsJobsCache.cacheflushed": "", + "components.Settings.serverpresetManualMessage": "", + "components.Settings.serverpresetRefreshing": "", + "components.Setup.finishing": "", + "components.UserList.edituser": "", + "components.UserList.email": "", + "components.UserProfile.UserSettings.UserGeneralSettings.enableOverride": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.noLinkedAccounts": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadId": "", + "i18n.blocklistError": "", + "components.Settings.SettingsJobsCache.dnsCacheGlobalStats": "", + "components.Settings.SettingsJobsCache.flushdnscache": "", + "components.Settings.SettingsJobsCache.jellyfin-full-scan": "", + "components.Settings.SettingsJobsCache.jellyfin-recently-added-scan": "", + "components.Settings.SettingsJobsCache.size": "", + "components.Settings.SettingsJobsCache.sonarr-scan": "", + "components.Settings.SettingsLogs.viewdetails": "", + "components.Settings.SettingsMain.apikey": "", + "components.Settings.SettingsMain.apikeyCopied": "", + "components.Settings.SettingsMain.applicationTitle": "", + "components.Settings.SettingsMain.applicationurl": "", + "components.Settings.SettingsMain.blocklistedTagsLimitTip": "", + "components.Settings.SettingsMain.cacheImages": "", + "components.Settings.SettingsMain.discoverRegion": "", + "components.Settings.SettingsMain.discoverRegionTip": "", + "components.Settings.SettingsMain.hideAvailable": "", + "components.Settings.SettingsMain.locale": "", + "components.Settings.SettingsMain.originallanguage": "", + "components.Settings.SettingsMain.partialRequestsEnabled": "", + "components.Settings.SettingsMain.streamingRegion": "", + "components.Settings.SettingsMain.streamingRegionTip": "", + "components.Settings.SettingsMain.toastApiKeyFailure": "", + "components.Settings.services": "", + "components.Settings.ssl": "", + "component.BlocklistBlock.blocklistedby": "", + "component.BlocklistModal.blocklisting": "", + "components.AirDateBadge.airedrelative": "", + "components.AirDateBadge.airsrelative": "", + "components.AppDataWarning.dockerVolumeMissingDescription": "", + "components.Blocklist.blocklistNotFoundError": "", + "components.Blocklist.blocklistSettingsDescription": "", + "components.Blocklist.blocklistdate": "", + "components.Blocklist.blocklistedby": "", + "components.Blocklist.blocklistsettings": "", + "components.Blocklist.filterManual": "", + "components.Blocklist.mediaName": "", + "components.Blocklist.mediaTmdbId": "", + "components.Blocklist.mediaType": "", + "components.Blocklist.showAllBlocklisted": "", + "components.CollectionDetails.numberofmovies": "", + "components.Login.signinwithjellyfin": "", + "components.Login.signinwithoverseerr": "", + "components.Login.signinwithplex": "", + "components.Login.title": "", + "components.Discover.DiscoverMovieLanguage.languageMovies": "", + "components.Discover.DiscoverMovies.activefilters": "", + "components.Discover.DiscoverMovies.discovermovies": "", + "components.Discover.DiscoverMovies.sortPopularityAsc": "", + "components.Discover.DiscoverMovies.sortPopularityDesc": "", + "components.Discover.DiscoverMovies.sortReleaseDateAsc": "", + "components.Discover.DiscoverMovies.sortReleaseDateDesc": "", + "components.Discover.DiscoverWatchlist.discoverwatchlist": "", + "components.Discover.DiscoverWatchlist.watchlist": "", + "components.Discover.FilterSlideover.activefilters": "", + "components.Discover.FilterSlideover.certification": "", + "components.Discover.FilterSlideover.clearfilters": "", + "components.Discover.FilterSlideover.excludeKeywords": "", + "components.Discover.FilterSlideover.filters": "", + "components.Discover.FilterSlideover.firstAirDate": "", + "components.Discover.FilterSlideover.runtimeText": "", + "components.Discover.FilterSlideover.status": "", + "components.Discover.FilterSlideover.streamingservices": "", + "components.Discover.FilterSlideover.studio": "", + "components.Discover.FilterSlideover.tmdbuserscore": "", + "components.Discover.PlexWatchlistSlider.emptywatchlist": "", + "components.Discover.PlexWatchlistSlider.plexwatchlist": "", + "components.Discover.RecentlyAddedSlider.recentlyAdded": "", + "components.Discover.RecentRequestsSlider.unableToConnect": "", + "components.Discover.StudioSlider.studios": "", + "components.Discover.TvGenreList.seriesgenres": "", + "components.Discover.TvGenreSlider.tvgenres": "", + "components.Discover.moviegenres": "", + "components.Discover.networks": "", + "components.Discover.plexwatchlist": "", + "components.Discover.timeWindowDay": "", + "components.Discover.timeWindowWeek": "", + "components.Discover.tmdbmoviekeyword": "", + "components.IssueDetails.IssueComment.edit": "", + "components.Layout.Sidebar.settings": "", + "components.Layout.UserDropdown.MiniQuotaDisplay.movierequests": "", + "components.Layout.UserDropdown.MiniQuotaDisplay.seriesrequests": "", + "components.Layout.UserDropdown.myprofile": "", + "components.Layout.UserDropdown.requests": "", + "components.Layout.UserDropdown.settings": "", + "components.Layout.UserDropdown.signout": "", + "components.Login.email": "", + "components.Login.emailtooltip": "", + "components.Login.enablessl": "", + "components.Login.forgotpassword": "", + "components.Login.validationhostformat": "", + "components.PermissionEdit.autoapproveSeriesDescription": "", + "components.PermissionEdit.autorequestDescription": "", + "components.RequestList.RequestItem.tvdbid": "", + "components.RequestModal.season": "", + "components.Selector.CertificationSelector.noOptions": "", + "components.Selector.CertificationSelector.selectCertification": "", + "components.Settings.Notifications.encryption": "", + "components.Settings.Notifications.encryptionDefault": "", + "components.Setup.signin": "", + "components.UserProfile.UserSettings.UserGeneralSettings.localuser": "", + "components.Login.tipEmailHasTrailingWhitespace": "", + "components.Login.tipUsernameHasTrailingWhitespace": "", + "components.Login.username": "", + "components.Login.validationEmailFormat": "", + "components.Discover.CreateSlider.searchStudios": "", + "components.Discover.CreateSlider.slidernameplaceholder": "", + "components.Discover.CreateSlider.starttyping": "", + "components.Discover.CreateSlider.validationDatarequired": "", + "components.Discover.DiscoverMovieKeyword.keywordMovies": "", + "components.Discover.studios": "", + "components.PermissionEdit.request4kTvDescription": "", + "components.PermissionEdit.requestDescription": "", + "components.CollectionDetails.overview": "", + "components.CollectionDetails.requestcollection": "", + "components.CollectionDetails.requestcollection4k": "", + "components.Discover.CreateSlider.validationTitlerequired": "", + "components.Discover.DiscoverMovieGenre.genreMovies": "", + "components.Discover.discover": "", + "components.IssueDetails.nocomments": "", + "components.IssueDetails.openedby": "", + "components.IssueDetails.openin4karr": "", + "components.IssueDetails.openinarr": "", + "components.IssueDetails.play4konplex": "", + "components.IssueDetails.reopenissue": "", + "components.IssueDetails.reopenissueandcomment": "", + "components.IssueDetails.season": "", + "components.IssueDetails.toasteditdescriptionfailed": "", + "components.IssueDetails.toasteditdescriptionsuccess": "", + "components.IssueDetails.toastissuedeleted": "", + "components.IssueDetails.toastissuedeletefailed": "", + "components.IssueDetails.toaststatusupdated": "", + "components.Discover.DiscoverTv.discovertv": "", + "components.Discover.DiscoverTv.sortFirstAirDateAsc": "", + "components.Discover.DiscoverTv.sortFirstAirDateDesc": "", + "components.Discover.DiscoverTv.sortPopularityAsc": "", + "components.Discover.FilterSlideover.tmdbuservotecount": "", + "components.ResetPassword.passwordreset": "", + "components.ResetPassword.resetpassword": "", + "components.Settings.Notifications.NotificationsPushover.accessTokenTip": "", + "components.Settings.Notifications.NotificationsWebhook.resetPayload": "", + "components.Settings.Notifications.NotificationsWebhook.supportVariables": "", + "components.Settings.OverrideRuleTile.users": "", + "components.Settings.SettingsJobsCache.dnsCacheGlobalStatsDescription": "", + "components.Settings.enablessl": "", + "components.Blocklist.filterBlocklistedTags": "", + "components.Discover.FilterSlideover.genres": "", + "components.Discover.FilterSlideover.ratingText": "", + "components.Discover.FilterSlideover.runtime": "", + "components.Discover.tmdbmoviegenre": "", + "components.Discover.tmdbnetwork": "", + "components.ManageSlideOver.downloadstatus": "", + "components.ManageSlideOver.manageModalAdvanced": "", + "components.Settings.Notifications.NotificationsWebPush.agentenabled": "", + "components.Settings.Notifications.chatId": "", + "components.Discover.CreateSlider.editsuccess": "", + "components.Discover.CreateSlider.needresults": "", + "components.Discover.CreateSlider.nooptions": "", + "components.Discover.CreateSlider.providetmdbgenreid": "", + "components.Discover.CreateSlider.providetmdbkeywordid": "", + "components.Discover.CreateSlider.providetmdbnetwork": "", + "components.Discover.CreateSlider.providetmdbsearch": "", + "components.Discover.CreateSlider.providetmdbstudio": "", + "components.Discover.CreateSlider.searchGenres": "", + "components.Discover.CreateSlider.searchKeywords": "", + "components.Discover.FilterSlideover.originalLanguage": "", + "components.Discover.FilterSlideover.releaseDate": "", + "components.Discover.populartv": "", + "components.Layout.Sidebar.browsemovies": "", + "components.Layout.Sidebar.dashboard": "", + "components.Layout.Sidebar.issues": "", + "components.Layout.VersionStatus.outofdate": "", + "components.Layout.VersionStatus.streamdevelop": "", + "components.Layout.VersionStatus.streamstable": "", + "components.Login.adminerror": "", + "components.Login.back": "", + "components.Login.hostname": "", + "components.Login.initialsignin": "", + "components.Login.initialsigningin": "", + "components.Login.invalidurlerror": "", + "components.QuotaSelector.unlimited": "", + "components.Setup.finish": "", + "components.Login.urlBaseHelp": "", + "components.Login.validationEmailRequired": "", + "components.Login.validationPortRequired": "", + "components.Login.validationUrlBaseLeadingSlash": "", + "components.Login.validationUrlBaseTrailingSlash": "", + "components.Login.validationUrlTrailingSlash": "", + "components.Login.validationemailformat": "", + "components.Login.validationusernamerequired": "", + "components.NotificationTypeSelector.issuecommentDescription": "", + "components.PermissionEdit.autoapprove4k": "", + "components.PersonDetails.appearsin": "", + "components.PersonDetails.ascharacter": "", + "components.RequestBlock.lastmodifiedby": "", + "components.RequestBlock.profilechanged": "", + "components.RequestBlock.requestdate": "", + "components.RequestBlock.requestedby": "", + "components.RequestBlock.rootfolder": "", + "components.RequestBlock.seasons": "", + "components.RequestBlock.server": "", + "components.RequestButton.approve4krequests": "", + "components.RequestButton.approverequest": "", + "components.RequestButton.approverequest4k": "", + "components.RequestButton.approverequests": "", + "components.RequestButton.declinerequest": "", + "components.Selector.canceled": "", + "components.Settings.Notifications.NotificationsPushover.userToken": "", + "components.UserList.password": "", + "components.UserList.userssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablingwebpusherror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.managedevices": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.nodevicestoshow": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.operatingsystem": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleted": "", + "components.UserProfile.UserSettings.menuChangePass": "", + "components.UserProfile.UserSettings.menuLinkedAccounts": "", + "components.UserProfile.UserSettings.menuNotifications": "", + "components.UserProfile.UserSettings.menuPermissions": "", + "i18n.delete": "", + "i18n.loading": "", + "components.Settings.SettingsJobsCache.jobs": "", + "components.Discover.createnewslider": "", + "components.Discover.customizediscover": "", + "components.Discover.emptywatchlist": "", + "components.Discover.recentlyAdded": "", + "components.Discover.recentrequests": "", + "components.Discover.resetfailed": "", + "components.Discover.resetsuccess": "", + "components.Discover.resettodefault": "", + "components.Discover.resetwarning": "", + "components.Discover.stopediting": "", + "components.Discover.tmdbsearch": "", + "components.Discover.tmdbstudio": "", + "components.Discover.tmdbtvgenre": "", + "components.Discover.tmdbtvkeyword": "", + "components.Discover.tmdbtvstreamingservices": "", + "components.Discover.trending": "", + "components.Discover.upcomingmovies": "", + "components.Discover.upcomingtv": "", + "components.DiscoverTvUpcoming.upcomingtv": "", + "components.Settings.Notifications.NotificationsWebPush.embedPoster": "", + "components.Settings.Notifications.NotificationsWebPush.httpsRequirement": "", + "components.Settings.Notifications.NotificationsWebPush.toastWebPushTestFailed": "", + "components.Settings.Notifications.NotificationsWebPush.toastWebPushTestSending": "", + "components.Settings.Notifications.NotificationsWebPush.toastWebPushTestSuccess": "", + "components.Settings.Notifications.NotificationsWebPush.webpushsettingsfailed": "", + "components.Settings.Notifications.NotificationsWebPush.webpushsettingssaved": "", + "components.Settings.Notifications.NotificationsWebhook.agentenabled": "", + "components.Settings.Notifications.NotificationsWebhook.authheader": "", + "components.Settings.Notifications.NotificationsWebhook.customHeaders": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersAdd": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersValue": "", + "components.Settings.Notifications.NotificationsWebhook.customJson": "", + "components.Settings.Notifications.NotificationsWebhook.resetPayloadSuccess": "", + "components.Settings.Notifications.NotificationsWebhook.supportVariablesTip": "", + "components.Settings.Notifications.NotificationsWebhook.templatevariablehelp": "", + "components.Settings.Notifications.NotificationsWebhook.toastWebhookTestFailed": "", + "components.Settings.Notifications.NotificationsWebhook.toastWebhookTestSending": "", + "components.Settings.Notifications.NotificationsWebhook.toastWebhookTestSuccess": "", + "components.Settings.Notifications.NotificationsWebhook.validationJsonPayloadRequired": "", + "components.Settings.Notifications.NotificationsWebhook.validationWebhookUrl": "", + "components.Settings.Notifications.NotificationsWebhook.webhookUrl": "", + "components.Settings.Notifications.NotificationsWebhook.webhooksettingsfailed": "", + "components.Settings.Notifications.NotificationsWebhook.webhooksettingssaved": "", + "components.Settings.Notifications.useUserLocale": "", + "components.Settings.Notifications.validationEmail": "", + "components.Settings.Notifications.validationUrl": "", + "components.Settings.Notifications.webhookUrl": "", + "components.Settings.SettingsAbout.documentation": "", + "components.Settings.SettingsAbout.gettingsupport": "", + "components.Settings.SettingsJobsCache.failures": "", + "components.Settings.SettingsJobsCache.flushcache": "", + "components.Settings.SettingsNetwork.dnsCacheTip": "", + "components.Settings.SettingsNetwork.docs": "", + "components.Settings.SettingsNetwork.forceIpv4First": "", + "components.Settings.SettingsUsers.defaultPermissions": "", + "components.Settings.SettingsUsers.localLogin": "", + "components.Settings.SettingsUsers.userSettings": "", + "components.Settings.SonarrModal.rootfolder": "", + "components.Settings.SonarrModal.seasonfolders": "", + "components.Settings.SonarrModal.selectLanguageProfile": "", + "components.Settings.SonarrModal.selecttags": "", + "components.Settings.SonarrModal.validationHostnameRequired": "", + "components.Settings.plexlibrariesDescription": "", + "components.Settings.plexsettings": "", + "components.Settings.plexsettingsDescription": "", + "components.Settings.port": "", + "components.Settings.serviceSettingsDescription": "", + "components.Settings.settingUpPlexDescription": "", + "components.Settings.valueRequired": "", + "components.UserList.importedfromplex": "", + "components.UserList.importfromJellyfin": "", + "components.UserList.importfromJellyfinerror": "", + "i18n.all": "", + "i18n.canceling": "", + "i18n.close": "", + "i18n.collection": "", + "i18n.completed": "", + "i18n.decline": "", + "i18n.declined": "", + "i18n.failed": "", + "i18n.import": "", + "i18n.previous": "", + "i18n.request4k": "", + "i18n.resultsperpage": "", + "i18n.settings": "", + "components.Settings.Notifications.usePublicLogo": "", + "components.Settings.Notifications.usePublicLogoTip": "", + "components.Settings.Notifications.userEmailRequired": "", + "components.Settings.Notifications.validationBotAPIRequired": "", + "components.Settings.Notifications.validationChatIdRequired": "", + "components.Settings.Notifications.validationWebhookRoleId": "", + "components.Settings.Notifications.webhookRoleId": "", + "components.Settings.Notifications.webhookRoleIdTip": "", + "components.Settings.Notifications.webhookUrlTip": "", + "components.Settings.OverrideRuleModal.conditions": "", + "components.Settings.OverrideRuleModal.genres": "", + "components.Settings.OverrideRuleModal.keywords": "", + "components.Settings.OverrideRuleModal.ruleCreated": "", + "components.Settings.OverrideRuleModal.ruleUpdated": "", + "components.Settings.OverrideRuleModal.selectQualityProfile": "", + "components.Settings.OverrideRuleModal.selectRootFolder": "", + "components.Settings.OverrideRuleModal.selecttags": "", + "components.Settings.OverrideRuleModal.settingsDescription": "", + "components.Settings.OverrideRuleModal.users": "", + "components.Settings.OverrideRuleTile.genre": "", + "components.Settings.OverrideRuleTile.keywords": "", + "components.Settings.OverrideRuleTile.language": "", + "components.Settings.OverrideRuleTile.qualityprofile": "", + "components.Settings.OverrideRuleTile.rootfolder": "", + "components.Settings.OverrideRuleTile.settings": "", + "components.Settings.RadarrModal.add": "", + "components.Settings.RadarrModal.announced": "", + "components.Settings.RadarrModal.apiKey": "", + "components.Settings.RadarrModal.apiKeyHelp": "", + "components.Settings.RadarrModal.baseUrl": "", + "components.Settings.RadarrModal.baseUrlHelp": "", + "components.Settings.RadarrModal.create4kradarr": "", + "components.Settings.RadarrModal.createradarr": "", + "components.Settings.RadarrModal.default4kserver": "", + "components.Settings.RadarrModal.edit4kradarr": "", + "components.Settings.RadarrModal.enableSearch": "", + "components.Settings.RadarrModal.enableSearchHelp": "", + "components.Settings.RadarrModal.externalUrl": "", + "components.Settings.RadarrModal.externalUrlHelp": "", + "components.Settings.RadarrModal.hostname": "", + "components.Settings.RadarrModal.inCinemas": "", + "components.Settings.RadarrModal.loadingTags": "", + "components.Settings.RadarrModal.loadingprofiles": "", + "components.Settings.RadarrModal.notagoptions": "", + "components.Settings.RadarrModal.port": "", + "components.Settings.RadarrModal.qualityprofile": "", + "components.Settings.RadarrModal.released": "", + "components.Settings.RadarrModal.rootfolder": "", + "components.Settings.RadarrModal.selectMinimumAvailability": "", + "components.Settings.RadarrModal.server4kHelp": "", + "components.Settings.RadarrModal.ssl": "", + "components.Settings.RadarrModal.syncEnabledHelp": "", + "components.Settings.RadarrModal.tagRequestsInfo": "", + "components.Settings.RadarrModal.tags": "", + "components.Settings.RadarrModal.testFirstQualityProfiles": "", + "components.Settings.RadarrModal.testFirstRootFolders": "", + "components.Settings.RadarrModal.testFirstTags": "", + "components.Settings.RadarrModal.toastRadarrTestFailure": "", + "components.Settings.RadarrModal.toastRadarrTestSuccess": "", + "components.Settings.RadarrModal.validationNameRequired": "", + "components.Settings.RadarrModal.validationPortRequired": "", + "components.Settings.RadarrModal.validationProfileRequired": "", + "components.Settings.RadarrModal.validationRootFolderRequired": "", + "components.Settings.SettingsAbout.Releases.currentversion": "", + "components.Settings.SettingsAbout.Releases.latestversion": "", + "components.Settings.SettingsAbout.Releases.releasedataMissing": "", + "components.Settings.SettingsAbout.Releases.releases": "", + "components.Settings.SettingsAbout.Releases.versionChangelog": "", + "components.Settings.SettingsAbout.Releases.viewchangelog": "", + "components.Settings.SettingsAbout.about": "", + "components.Settings.SettingsAbout.aboutseerr": "", + "components.Settings.SettingsAbout.appDataPath": "", + "components.Settings.SettingsAbout.githubdiscussions": "", + "components.Settings.SettingsAbout.outofdate": "", + "components.Settings.SettingsAbout.runningDevelop": "", + "components.Settings.SettingsAbout.timezone": "", + "components.Settings.SettingsAbout.totalmedia": "", + "components.Settings.SettingsAbout.totalrequests": "", + "components.Settings.SettingsAbout.uptodate": "", + "components.Settings.SettingsAbout.version": "", + "components.Settings.SettingsJobsCache.availability-sync": "", + "components.Settings.SettingsJobsCache.cache": "", + "components.Settings.SettingsJobsCache.cacheksize": "", + "components.Settings.SettingsJobsCache.cachemisses": "", + "components.Settings.SettingsJobsCache.cachename": "", + "components.Settings.SettingsJobsCache.cachevsize": "", + "components.Settings.SettingsJobsCache.canceljob": "", + "components.Settings.SettingsJobsCache.command": "", + "components.Settings.SettingsJobsCache.dnsCache": "", + "components.Settings.SettingsJobsCache.dnsCacheDescription": "", + "components.Settings.SettingsJobsCache.dnsNoCacheEntries": "", + "components.Settings.SettingsJobsCache.dnscacheactiveaddress": "", + "components.Settings.SettingsJobsCache.dnscacheage": "", + "components.Settings.SettingsJobsCache.dnscacheflushed": "", + "components.Settings.SettingsJobsCache.dnscachemisses": "", + "components.Settings.SettingsJobsCache.dnscachename": "", + "components.Settings.SettingsJobsCache.download-sync": "", + "components.Settings.SettingsJobsCache.download-sync-reset": "", + "components.Settings.SettingsJobsCache.editJobSchedule": "", + "components.Settings.SettingsJobsCache.editJobScheduleCurrent": "", + "components.Settings.SettingsJobsCache.editJobSchedulePrompt": "", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorDays": "", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorHours": "", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorMinutes": "", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorSeconds": "", + "components.Settings.SettingsJobsCache.hitRate": "", + "components.Settings.SettingsJobsCache.image-cache-cleanup": "", + "components.Settings.SettingsJobsCache.imagecache": "", + "components.Settings.SettingsJobsCache.imagecacheDescription": "", + "components.Settings.SettingsJobsCache.imagecachecount": "", + "components.Settings.SettingsJobsCache.ipv4Fallbacks": "", + "components.Settings.SettingsJobsCache.jobScheduleEditFailed": "", + "components.Settings.SettingsJobsCache.jobScheduleEditSaved": "", + "components.Settings.SettingsJobsCache.jobcancelled": "", + "components.Settings.SettingsJobsCache.jobname": "", + "components.Settings.SettingsJobsCache.jobsDescription": "", + "components.Settings.SettingsJobsCache.jobsandcache": "", + "components.Settings.SettingsJobsCache.jobstarted": "", + "components.Settings.SettingsJobsCache.jobtype": "", + "components.Settings.SettingsJobsCache.misses": "", + "components.Settings.SettingsJobsCache.plex-full-scan": "", + "components.Settings.SettingsJobsCache.plex-recently-added-scan": "", + "components.Settings.SettingsJobsCache.plex-refresh-token": "", + "components.Settings.SettingsJobsCache.plex-watchlist-sync": "", + "components.Settings.SettingsJobsCache.process": "", + "components.Settings.SettingsJobsCache.process-blocklisted-tags": "", + "components.Settings.SettingsJobsCache.radarr-scan": "", + "components.Settings.SettingsLogs.filterDebug": "", + "components.Settings.SettingsLogs.filterError": "", + "components.Settings.SettingsLogs.filterInfo": "", + "components.Settings.SettingsLogs.filterWarn": "", + "components.Settings.SettingsLogs.label": "", + "components.Settings.SettingsLogs.level": "", + "components.Settings.SettingsLogs.logDetails": "", + "components.Settings.SettingsLogs.logsDescription": "", + "components.Settings.SettingsLogs.message": "", + "components.Settings.SettingsLogs.resumeLogs": "", + "components.Settings.SettingsLogs.showall": "", + "components.Settings.SettingsLogs.time": "", + "components.Settings.SettingsMain.blocklistLanguage": "", + "components.Settings.SettingsMain.blocklistLanguageTip": "", + "components.Settings.SettingsMain.blocklistRegion": "", + "components.Settings.SettingsMain.blocklistRegionTip": "", + "components.Settings.SettingsMain.blocklistedTags": "", + "components.Settings.SettingsMain.blocklistedTagsLimit": "", + "components.Settings.SettingsMain.blocklistedTagsTip": "", + "components.Settings.SettingsMain.cacheImagesTip": "", + "components.Settings.SettingsMain.enableSpecialEpisodes": "", + "components.Settings.SettingsMain.general": "", + "components.Settings.SettingsMain.generalsettings": "", + "components.Settings.SettingsMain.generalsettingsDescription": "", + "components.Settings.SettingsMain.hideAvailableTip": "", + "components.Settings.SettingsMain.hideBlocklisted": "", + "components.Settings.SettingsMain.hideBlocklistedTip": "", + "components.Settings.SettingsMain.toastApiKeySuccess": "", + "components.Settings.SettingsMain.toastSettingsFailure": "", + "components.Settings.SettingsMain.toastSettingsSuccess": "", + "components.Settings.SettingsMain.validationApplicationTitle": "", + "components.Settings.SettingsMain.validationApplicationUrl": "", + "components.Settings.SettingsMain.validationApplicationUrlTrailingSlash": "", + "components.Settings.SettingsMain.validationUrl": "", + "components.Settings.SettingsMain.validationUrlTrailingSlash": "", + "components.Settings.SettingsMain.youtubeUrl": "", + "components.Settings.SettingsNetwork.apiRequestTimeoutTip": "", + "components.Settings.SettingsNetwork.csrfProtection": "", + "components.Settings.SettingsNetwork.csrfProtectionHoverTip": "", + "components.Settings.SettingsNetwork.csrfProtectionTip": "", + "components.Settings.SettingsNetwork.dnsCache": "", + "components.Settings.SettingsNetwork.dnsCacheForceMinTtl": "", + "components.Settings.SettingsNetwork.dnsCacheHoverTip": "", + "components.Settings.SettingsNetwork.forceIpv4FirstTip": "", + "components.Settings.SettingsNetwork.network": "", + "components.Settings.SettingsNetwork.networkDisclaimer": "", + "components.Settings.SettingsNetwork.networksettings": "", + "components.Settings.SettingsNetwork.networksettingsDescription": "", + "components.Settings.SettingsNetwork.proxyBypassFilter": "", + "components.Settings.SettingsNetwork.proxyBypassFilterTip": "", + "components.Settings.SettingsNetwork.proxyBypassLocalAddresses": "", + "components.Settings.SettingsNetwork.proxyEnabled": "", + "components.Settings.SettingsNetwork.proxyEnabledTip": "", + "components.Settings.SettingsNetwork.proxyHostname": "", + "components.Settings.SettingsNetwork.proxyPassword": "", + "components.Settings.SettingsNetwork.proxyPort": "", + "components.Settings.SettingsNetwork.proxySsl": "", + "components.Settings.SettingsNetwork.proxyUser": "", + "components.Settings.SettingsNetwork.toastSettingsFailure": "", + "components.Settings.SettingsNetwork.toastSettingsSuccess": "", + "components.Settings.SettingsNetwork.trustProxy": "", + "components.Settings.SettingsNetwork.trustProxyTip": "", + "components.Settings.SettingsNetwork.validationApiRequestTimeout": "", + "components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "", + "components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "", + "components.Settings.SettingsNetwork.validationProxyPort": "", + "components.Settings.SettingsUsers.atLeastOneAuth": "", + "components.Settings.SettingsUsers.defaultPermissionsTip": "", + "components.Settings.SettingsUsers.disabledMediaServerLoginWarning": "", + "components.Settings.SettingsUsers.localLoginTip": "", + "components.Settings.SettingsUsers.loginMethods": "", + "components.Settings.SettingsUsers.loginMethodsTip": "", + "components.Settings.SettingsUsers.mediaServerLogin": "", + "components.Settings.SettingsUsers.mediaServerLoginTip": "", + "components.Settings.SettingsUsers.movieRequestLimitLabel": "", + "components.Settings.SettingsUsers.newPlexLogin": "", + "components.Settings.SettingsUsers.newPlexLoginTip": "", + "components.Settings.SettingsUsers.toastSettingsFailure": "", + "components.Settings.SettingsUsers.toastSettingsSuccess": "", + "components.Settings.SettingsUsers.tvRequestLimitLabel": "", + "components.Settings.SettingsUsers.userSettingsDescription": "", + "components.Settings.SettingsUsers.users": "", + "components.Settings.SonarrModal.add": "", + "components.Settings.SonarrModal.animeSeriesType": "", + "components.Settings.SonarrModal.apiKeyHelp": "", + "components.Settings.SonarrModal.baseUrlHelp": "", + "components.Settings.SonarrModal.edit4ksonarr": "", + "components.Settings.SonarrModal.editsonarr": "", + "components.Settings.SonarrModal.enableSearchHelp": "", + "components.Settings.SonarrModal.externalUrlHelp": "", + "components.Settings.SonarrModal.monitorNewItems": "", + "components.Settings.SonarrModal.monitorNewItemsHelp": "", + "components.Settings.SonarrModal.notagoptions": "", + "components.Settings.SonarrModal.port": "", + "components.Settings.SonarrModal.qualityprofile": "", + "components.Settings.SonarrModal.selectQualityProfile": "", + "components.Settings.SonarrModal.selectRootFolder": "", + "components.Settings.SonarrModal.seriesType": "", + "components.Settings.SonarrModal.server4k": "", + "components.Settings.SonarrModal.server4kHelp": "", + "components.Settings.SonarrModal.servername": "", + "components.Settings.SonarrModal.ssl": "", + "components.Settings.SonarrModal.syncEnabled": "", + "components.Settings.SonarrModal.syncEnabledHelp": "", + "components.Settings.SonarrModal.tagRequests": "", + "components.Settings.SonarrModal.tagRequestsInfo": "", + "components.Settings.SonarrModal.testFirstLanguageProfiles": "", + "components.Settings.SonarrModal.testFirstQualityProfiles": "", + "components.Settings.SonarrModal.testFirstRootFolders": "", + "components.Settings.SonarrModal.testFirstTags": "", + "components.Settings.SonarrModal.toastSonarrTestFailure": "", + "components.Settings.SonarrModal.toastSonarrTestSuccess": "", + "components.Settings.SonarrModal.validationApiKeyRequired": "", + "components.Settings.SonarrModal.validationApplicationUrl": "", + "components.Settings.SonarrModal.validationApplicationUrlTrailingSlash": "", + "components.Settings.SonarrModal.validationBaseUrlLeadingSlash": "", + "components.Settings.SonarrModal.validationBaseUrlTrailingSlash": "", + "components.Settings.SonarrModal.validationLanguageProfileRequired": "", + "components.Settings.SonarrModal.validationNameRequired": "", + "components.Settings.SonarrModal.validationPortRequired": "", + "components.Settings.SonarrModal.validationProfileRequired": "", + "components.Settings.SonarrModal.validationRootFolderRequired": "", + "components.Settings.activeProfile": "", + "components.Settings.addradarr": "", + "components.Settings.address": "", + "components.Settings.addrule": "", + "components.Settings.animeMetadataProvider": "", + "components.Settings.apiKey": "", + "components.Settings.blocklistedTagImportInstructions": "", + "components.Settings.blocklistedTagImportTitle": "", + "components.Settings.blocklistedTagsText": "", + "components.Settings.cancelscan": "", + "components.Settings.chooseProvider": "", + "components.Settings.clearBlocklistedTagsConfirm": "", + "components.Settings.clickTest": "", + "components.Settings.copyBlocklistedTags": "", + "components.Settings.copyBlocklistedTagsEmpty": "", + "components.Settings.copyBlocklistedTagsTip": "", + "components.Settings.currentlibrary": "", + "components.Settings.default4k": "", + "components.Settings.deleteServer": "", + "components.Settings.deleteserverconfirm": "", + "components.Settings.experimentalTooltip": "", + "components.Settings.externalUrl": "", + "components.Settings.failed": "", + "components.Settings.general": "", + "components.Settings.hostname": "", + "components.Settings.importBlocklistedTagsTip": "", + "components.Settings.invalidKeyword": "", + "components.Settings.invalidurlerror": "", + "components.Settings.is4k": "", + "components.Settings.jellyfinForgotPasswordUrl": "", + "components.Settings.jellyfinSettings": "", + "components.Settings.jellyfinSettingsDescription": "", + "components.Settings.jellyfinSettingsFailure": "", + "components.Settings.jellyfinSettingsSuccess": "", + "components.Settings.jellyfinSyncFailedAutomaticGroupedFolders": "", + "components.Settings.jellyfinSyncFailedGenericError": "", + "components.Settings.jellyfinSyncFailedNoLibrariesFound": "", + "components.Settings.jellyfinlibraries": "", + "components.Settings.jellyfinlibrariesDescription": "", + "components.Settings.jellyfinsettingsDescription": "", + "components.Settings.librariesRemaining": "", + "components.Settings.manualscan": "", + "components.Settings.manualscanDescription": "", + "components.Settings.manualscanDescriptionJellyfin": "", + "components.Settings.manualscanJellyfin": "", + "components.Settings.mediaTypeMovie": "", + "components.Settings.mediaTypeSeries": "", + "components.Settings.menuAbout": "", + "components.Settings.menuGeneralSettings": "", + "components.Settings.metadataSettingsSaved": "", + "components.Settings.noDefaultNon4kServer": "", + "components.Settings.noDefaultServer": "", + "components.Settings.noSpecialCharacters": "", + "components.Settings.nooptions": "", + "components.Settings.notTested": "", + "components.Settings.notificationsettings": "", + "components.Settings.notrunning": "", + "components.Settings.operational": "", + "components.Settings.overrideRules": "", + "components.Settings.overrideRulesDescription": "", + "components.Settings.plexlibraries": "", + "components.Settings.providerStatus": "", + "components.Settings.radarrsettings": "", + "components.Settings.restartrequiredTooltip": "", + "components.Settings.save": "", + "components.Settings.saving": "", + "components.Settings.scan": "", + "components.Settings.scanbackground": "", + "components.Settings.scanning": "", + "components.Settings.seriesMetadataProvider": "", + "components.Settings.serverLocal": "", + "components.Settings.serverRemote": "", + "components.Settings.serverSecure": "", + "components.Settings.serverpreset": "", + "components.Settings.serverpresetLoad": "", + "components.Settings.settings": "", + "components.Settings.sonarrsettings": "", + "components.Settings.startscan": "", + "components.Settings.starttyping": "", + "components.Settings.syncJellyfin": "", + "components.Settings.syncing": "", + "components.Settings.tautulliApiKey": "", + "components.Settings.tautulliSettings": "", + "components.Settings.tautulliSettingsDescription": "", + "components.Settings.timeout": "", + "components.Settings.tip": "", + "components.Settings.tmdbProviderDoesnotWork": "", + "components.Settings.toastPlexConnecting": "", + "components.Settings.toastPlexConnectingFailure": "", + "components.Settings.toastPlexConnectingSuccess": "", + "components.Settings.toastPlexRefresh": "", + "components.Settings.toastPlexRefreshFailure": "", + "components.Settings.toastPlexRefreshSuccess": "", + "components.Settings.toastTautulliSettingsFailure": "", + "components.Settings.toastTautulliSettingsSuccess": "", + "components.Settings.tvdbProviderDoesnotWork": "", + "components.Settings.validationUrlTrailingSlash": "", + "components.Settings.webAppUrl": "", + "components.Settings.yes": "", + "components.Setup.configjellyfin": "", + "components.Setup.configplex": "", + "components.Setup.configuremediaserver": "", + "components.Setup.configureservices": "", + "components.Setup.continue": "", + "components.Setup.librarieserror": "", + "components.Setup.servertype": "", + "components.Setup.setup": "", + "components.Setup.signinMessage": "", + "components.Setup.signinWithEmby": "", + "components.Setup.signinWithPlex": "", + "components.Setup.subtitle": "", + "components.Setup.welcome": "", + "components.StatusBadge.managemedia": "", + "components.StatusBadge.openinarr": "", + "components.StatusBadge.playonplex": "", + "components.StatusBadge.seasonepisodenumber": "", + "components.StatusBadge.seasonnumber": "", + "components.StatusBadge.status": "", + "components.StatusBadge.status4k": "", + "components.StatusChecker.appUpdated": "", + "components.StatusChecker.appUpdatedDescription": "", + "components.StatusChecker.reloadApp": "", + "components.StatusChecker.restartRequired": "", + "components.StatusChecker.restartRequiredDescription": "", + "components.TitleCard.addToWatchList": "", + "components.TitleCard.cleardata": "", + "components.TitleCard.mediaerror": "", + "components.TitleCard.tmdbid": "", + "components.TitleCard.tvdbid": "", + "components.TitleCard.watchlistCancel": "", + "components.TitleCard.watchlistDeleted": "", + "components.TitleCard.watchlistError": "", + "components.TitleCard.watchlistSuccess": "", + "components.TvDetails.Season.noepisodes": "", + "components.TvDetails.Season.somethingwentwrong": "", + "components.TvDetails.firstAirDate": "", + "components.TvDetails.manageseries": "", + "components.TvDetails.network": "", + "components.TvDetails.nextAirDate": "", + "components.TvDetails.originallanguage": "", + "components.TvDetails.originaltitle": "", + "components.TvDetails.overview": "", + "components.TvDetails.overviewunavailable": "", + "components.TvDetails.play": "", + "components.TvDetails.play4k": "", + "components.TvDetails.productioncountries": "", + "components.TvDetails.recommendations": "", + "components.TvDetails.removefromwatchlist": "", + "components.TvDetails.reportissue": "", + "components.TvDetails.rtaudiencescore": "", + "components.TvDetails.seasonnumber": "", + "components.TvDetails.seasons": "", + "components.TvDetails.seasonstitle": "", + "components.TvDetails.showtype": "", + "components.TvDetails.similar": "", + "components.TvDetails.tmdbuserscore": "", + "components.TvDetails.viewfullcrew": "", + "components.TvDetails.watchlistDeleted": "", + "components.TvDetails.watchlistError": "", + "components.TvDetails.watchlistSuccess": "", + "components.TvDetails.watchtrailer": "", + "components.UserList.admin": "", + "components.UserList.ascending": "", + "components.UserList.autogeneratepassword": "", + "components.UserList.autogeneratepasswordTip": "", + "components.UserList.bulkedit": "", + "components.UserList.create": "", + "components.UserList.created": "", + "components.UserList.createlocaluser": "", + "components.UserList.creating": "", + "components.UserList.deleteconfirm": "", + "components.UserList.deleteuser": "", + "components.UserList.descending": "", + "components.UserList.importedPlexUsersNoPassword": "", + "components.UserList.importedUsersNoPassword": "", + "components.UserList.importedfromJellyfin": "", + "components.UserList.importfrommediaserver": "", + "components.UserList.importfromplex": "", + "components.UserList.importfromplexerror": "", + "components.UserList.localLoginDisabled": "", + "components.UserList.localuser": "", + "components.UserList.newJellyfinsigninenabled": "", + "components.UserList.newplexsigninenabled": "", + "components.UserList.nouserstoimport": "", + "components.UserList.owner": "", + "components.UserList.sortBy": "", + "components.UserList.sortByRequests": "", + "components.UserList.sortByRole": "", + "components.UserList.sortByType": "", + "components.UserList.sortByUser": "", + "components.UserList.toggleSortDirection": "", + "components.UserList.toggleSortDirectionAria": "", + "components.UserList.usercreatedsuccess": "", + "components.UserList.userdeleted": "", + "components.UserList.userdeleteerror": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.description": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorExists": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorUnauthorized": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorUnknown": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.password": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.passwordRequired": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.save": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.saving": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.title": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.username": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.usernameRequired": "", + "components.UserProfile.UserSettings.UserGeneralSettings.accounttype": "", + "components.UserProfile.UserSettings.UserGeneralSettings.admin": "", + "components.UserProfile.UserSettings.UserGeneralSettings.applanguage": "", + "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegion": "", + "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegionTip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.displayName": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.linkedAccounts": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.linkedAccountsHint": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.noPermissionDescription": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorExists": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.email": "", + "components.UserProfile.UserSettings.UserNotificationSettings.emailsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.emailsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.notifications": "", + "components.RequestCard.failedmodify": "", + "components.RequestCard.tvdbid": "", + "components.RequestList.RequestItem.failedmodify": "", + "components.RequestList.RequestItem.failedretry": "", + "components.RequestList.RequestItem.mediaerror": "", + "components.RequestList.RequestItem.modified": "", + "components.RequestList.RequestItem.modifieduserdate": "", + "components.RequestList.RequestItem.profileName": "", + "components.UserProfile.UserSettings.UserNotificationSettings.notificationsettings": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pgpPublicKey": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessToken": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessTokenTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverApplicationToken": "", + "components.RequestList.RequestItem.removearr": "", + "components.RequestList.RequestItem.requested": "", + "components.RequestList.RequestItem.requesteddate": "", + "components.RequestList.RequestItem.seasons": "", + "components.RequestList.RequestItem.tmdbid": "", + "components.RequestList.unableToConnect": "", + "components.RequestModal.AdvancedRequester.advancedoptions": "", + "components.RequestModal.AdvancedRequester.animenote": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverApplicationTokenTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverUserKey": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverUserKeyTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoversettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoversettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.sound": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramChatId": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramChatIdTipLong": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadIdTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramsettingssaved": "", + "components.RequestModal.AdvancedRequester.default": "", + "components.RequestModal.AdvancedRequester.requestas": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPgpPublicKey": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPushbulletAccessToken": "", + "components.UserProfile.UserSettings.UserNotificationSettings.webpush": "", + "components.UserProfile.UserSettings.UserPasswordChange.confirmpassword": "", + "components.UserProfile.UserSettings.UserPasswordChange.currentpassword": "", + "components.UserProfile.UserSettings.UserPasswordChange.localPasswordDescription": "", + "components.UserProfile.UserSettings.UserPasswordChange.newpassword": "", + "components.UserProfile.UserSettings.UserPasswordChange.nopermissionDescription": "", + "components.UserProfile.UserSettings.UserPasswordChange.password": "", + "components.RequestModal.approve": "", + "components.UserProfile.UserSettings.UserPasswordChange.toastSettingsFailure": "", + "components.UserProfile.UserSettings.UserPasswordChange.toastSettingsFailureVerifyCurrent": "", + "components.UserProfile.UserSettings.UserPasswordChange.toastSettingsSuccess": "", + "components.UserProfile.UserSettings.UserPasswordChange.validationConfirmPassword": "", + "components.UserProfile.UserSettings.UserPasswordChange.validationConfirmPasswordSame": "", + "components.UserProfile.UserSettings.UserPasswordChange.validationNewPassword": "", + "components.UserProfile.UserSettings.UserPasswordChange.validationNewPasswordLength": "", + "components.UserProfile.UserSettings.UserPermissions.toastSettingsSuccess": "", + "components.UserProfile.UserSettings.UserPermissions.unauthorizedDescription": "", + "components.UserProfile.UserSettings.menuGeneralSettings": "", + "components.UserProfile.UserSettings.unauthorizedDescription": "", + "components.UserProfile.emptywatchlist": "", + "components.UserProfile.localWatchlist": "", + "components.UserProfile.movierequests": "", + "components.UserProfile.pastdays": "", + "i18n.addToBlocklist": "", + "i18n.advanced": "", + "i18n.approve": "", + "i18n.approved": "", + "i18n.areyousure": "", + "i18n.available": "", + "i18n.back": "", + "i18n.blocklist": "", + "i18n.blocklistDuplicateError": "", + "i18n.blocklistSuccess": "", + "i18n.blocklisted": "", + "i18n.cancel": "", + "i18n.notificationLocale": "", + "i18n.partiallyblocklisted": "", + "i18n.pending": "", + "i18n.processing": "", + "i18n.removeFromBlocklistSuccess": "", + "i18n.removefromBlocklist": "", + "i18n.request": "", + "i18n.retry": "", + "i18n.retrying": "", + "i18n.save": "", + "i18n.saving": "", + "i18n.tvshow": "", + "pages.internalservererror": "", + "pages.pagenotfound": "", + "pages.returnHome": "", + "pages.serviceunavailable": "", + "components.Selector.returningSeries": "", + "components.Selector.searchGenres": "", + "components.Selector.searchStatus": "", + "components.Selector.searchStudios": "", + "components.Selector.searchUsers": "", + "components.Selector.showless": "", + "components.Settings.Notifications.NotificationsGotify.toastGotifyTestFailed": "", + "components.Settings.Notifications.NotificationsGotify.toastGotifyTestSending": "", + "components.Settings.Notifications.NotificationsGotify.toastGotifyTestSuccess": "", + "components.Settings.Notifications.NotificationsPushbullet.accessTokenTip": "", + "components.Settings.Notifications.NotificationsPushbullet.agentEnabled": "", + "components.Settings.Notifications.NotificationsPushbullet.channelTag": "", + "components.Settings.Notifications.NotificationsPushbullet.pushbulletSettingsFailed": "", + "components.Settings.Notifications.NotificationsPushover.accessToken": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersAuthConflict": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersIncomplete": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersKey": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersRemove": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersTip": "", + "components.Settings.Notifications.botApiTip": "", + "components.Settings.Notifications.botAvatarUrl": "", + "components.Settings.Notifications.botUsername": "", + "components.Settings.Notifications.botUsernameTip": "", + "components.Settings.Notifications.chatIdTip": "", + "components.Settings.Notifications.discordsettingsfailed": "", + "components.Settings.Notifications.discordsettingssaved": "", + "components.Settings.RadarrModal.editradarr": "", + "components.Settings.SettingsJobsCache.cachehits": "", + "components.Settings.SettingsJobsCache.runnow": "", + "components.Settings.SettingsLogs.copiedLogMessage": "", + "components.Settings.SettingsLogs.copyToClipboard": "", + "components.Settings.SettingsLogs.extraData": "", + "components.Settings.SettingsNetwork.apiRequestTimeout": "", + "components.Settings.connectionTestFailed": "", + "components.Settings.email": "", + "components.Settings.jellyfinsettings": "", + "components.Settings.no": "", + "components.Settings.searchKeywords": "", + "components.Setup.back": "", + "components.Setup.configemby": "", + "components.TvDetails.TvCast.fullseriescast": "", + "components.TvDetails.TvCrew.fullseriescrew": "", + "components.TvDetails.addtowatchlist": "", + "components.TvDetails.anime": "", + "components.TvDetails.cast": "", + "components.TvDetails.episodeCount": "", + "components.TvDetails.episodeRuntime": "", + "components.TvDetails.episodeRuntimeMinutes": "", + "components.TvDetails.rtcriticsscore": "", + "components.TvDetails.status4k": "", + "components.TvDetails.streamingproviders": "", + "components.UserList.passwordinfodescription": "", + "components.UserList.plexuser": "", + "components.UserProfile.UserSettings.UserGeneralSettings.owner": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablewebpush": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablingwebpusherror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pgpPublicKeyTip": "", + "components.UserProfile.UserSettings.UserPermissions.permissions": "", + "components.UserProfile.UserSettings.UserPermissions.toastSettingsFailure": "", + "components.Layout.Sidebar.browsetv": "", + "components.Settings.Notifications.NotificationsPushbullet.pushbulletSettingsSaved": "", + "components.IssueModal.issueAudio": "", + "components.Layout.Sidebar.blocklist": "", + "components.Layout.Sidebar.requests": "", + "components.MovieDetails.streamingproviders": "", + "components.PersonDetails.birthdate": "", + "components.RequestCard.unknowntitle": "", + "components.Settings.RadarrModal.validationApplicationUrl": "", + "i18n.deleted": "", + "i18n.deleting": "", + "components.ManageSlideOver.openarr": "", + "components.Settings.SettingsJobsCache.nextexecution": "", + "components.MovieDetails.physicalrelease": "", + "components.RequestBlock.requestoverrides": "", + "components.RequestButton.requestmore4k": "", + "components.RequestCard.editrequest": "", + "components.Selector.CertificationSelector.starttyping": "", + "components.Settings.addsonarr": "", + "components.Settings.failedToSaveMetadataSettings": "", + "components.NotificationTypeSelector.mediarequestedDescription": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestFailed": "", + "components.Settings.Notifications.NotificationsPushover.validationUserTokenRequired": "", + "components.Settings.Notifications.NotificationsWebhook.validationTypes": "", + "components.Settings.Notifications.toastTelegramTestSending": "", + "components.Settings.OverrideRuleModal.selectService": "", + "components.Settings.OverrideRuleModal.service": "", + "components.Settings.SettingsAbout.Releases.viewongithub": "", + "components.Settings.SettingsJobsCache.imagecachesize": "", + "components.Settings.SettingsMain.youtubeUrlTip": "", + "components.IssueDetails.problemseason": "", + "components.IssueList.issues": "", + "components.Settings.Notifications.toastDiscordTestFailed": "", + "components.Settings.OverrideRuleTile.tags": "", + "components.Settings.SettingsAbout.contribute": "", + "components.Settings.plex": "", + "components.Discover.FilterSlideover.from": "", + "components.RequestModal.QuotaDisplay.season": "", + "components.Selector.searchKeywords": "", + "components.Settings.Notifications.validationMessageThreadId": "", + "components.Settings.Notifications.validationPgpPassword": "", + "components.Settings.Notifications.validationPgpPrivateKey": "", + "components.Settings.Notifications.validationSmtpHostRequired": "", + "components.Settings.Notifications.validationSmtpPortRequired": "", + "components.Settings.Notifications.validationTypes": "", + "components.Settings.OverrideRuleModal.conditionsDescription": "", + "components.Settings.OverrideRuleModal.createrule": "", + "components.Settings.OverrideRuleModal.editrule": "", + "components.Settings.OverrideRuleModal.languages": "", + "components.Settings.OverrideRuleModal.notagoptions": "", + "components.Settings.OverrideRuleModal.qualityprofile": "", + "components.Settings.OverrideRuleModal.rootfolder": "", + "components.Settings.OverrideRuleModal.serviceDescription": "", + "components.Settings.OverrideRuleModal.settings": "", + "components.Settings.OverrideRuleModal.tags": "", + "components.Settings.RadarrModal.loadingrootfolders": "", + "components.Settings.RadarrModal.minimumAvailability": "", + "components.Settings.RadarrModal.selectQualityProfile": "", + "components.Settings.RadarrModal.selectRootFolder": "", + "components.Settings.RadarrModal.selecttags": "", + "components.Settings.RadarrModal.server4k": "", + "components.Settings.RadarrModal.servername": "", + "components.Settings.RadarrModal.syncEnabled": "", + "components.Settings.RadarrModal.tagRequests": "", + "components.UserList.role": "", + "components.UserProfile.UserSettings.UserGeneralSettings.regionTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.sendSilently": "", + "components.UserProfile.UserSettings.UserNotificationSettings.sendSilentlyDescription": "", + "components.UserProfile.recentlywatched": "", + "components.UserProfile.recentrequests": "", + "components.UserProfile.requestsperdays": "", + "components.UserProfile.seriesrequest": "", + "components.UserProfile.totalrequests": "", + "components.UserProfile.unlimited": "", + "components.UserProfile.UserSettings.UserGeneralSettings.user": "", + "components.UserProfile.plexwatchlist": "", + "component.BlocklistBlock.blocklistdate": "", + "components.Login.loginerror": "", + "components.Login.loginwithapp": "", + "components.Login.noadminerror": "", + "components.Login.orsigninwith": "", + "components.Login.password": "", + "components.Login.port": "", + "components.Login.save": "", + "components.Login.saving": "", + "components.Login.servertype": "", + "components.Login.signin": "", + "components.Login.signingin": "", + "components.Login.signinheader": "", + "components.ManageSlideOver.manageModalMedia4k": "", + "components.ManageSlideOver.openarr4k": "", + "components.ManageSlideOver.opentautulli": "", + "components.ManageSlideOver.pastdays": "", + "components.ManageSlideOver.playedby": "", + "components.ManageSlideOver.plays": "", + "components.ManageSlideOver.removearr": "", + "components.ManageSlideOver.removearr4k": "", + "components.MovieDetails.openradarr": "", + "components.MovieDetails.openradarr4k": "", + "components.MovieDetails.originallanguage": "", + "components.PermissionEdit.autoapproveSeries": "", + "components.PermissionEdit.autorequest": "", + "components.PermissionEdit.users": "", + "components.PersonDetails.lifespan": "", + "components.RequestButton.decline4krequests": "", + "components.RequestList.RequestItem.unknowntitle": "", + "components.RequestList.requests": "", + "components.RequestList.showallrequests": "", + "components.RequestList.sortAdded": "", + "components.RequestList.sortDirection": "", + "components.RequestList.sortModified": "", + "components.RequestModal.AdvancedRequester.languageprofile": "", + "components.RequestModal.QuotaDisplay.allowedRequests": "", + "components.RequestModal.QuotaDisplay.allowedRequestsUser": "", + "components.RequestModal.pendingrequest": "", + "components.RequestModal.requestApproved": "", + "components.RequestModal.requestCancel": "", + "components.RequestModal.requestSuccess": "", + "components.ResetPassword.validationpasswordrequired": "", + "components.Selector.showmore": "", + "components.Selector.starttyping": "", + "components.Settings.Notifications.NotificationsGotify.agentenabled": "", + "components.Settings.Notifications.NotificationsGotify.gotifysettingsfailed": "", + "components.Settings.Notifications.NotificationsGotify.gotifysettingssaved": "", + "components.Settings.Notifications.NotificationsGotify.priority": "", + "components.Settings.Notifications.NotificationsGotify.token": "", + "components.Settings.Notifications.NotificationsGotify.url": "", + "components.Settings.Notifications.NotificationsGotify.validationPriorityRequired": "", + "components.Settings.Notifications.NotificationsGotify.validationTokenRequired": "", + "components.Settings.Notifications.NotificationsGotify.validationTypes": "", + "components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestFailed": "", + "components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestSending": "", + "components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestSuccess": "", + "components.Settings.Notifications.NotificationsPushbullet.validationAccessTokenRequired": "", + "components.Settings.Notifications.NotificationsPushbullet.validationTypes": "", + "components.Settings.Notifications.NotificationsSlack.agentenabled": "", + "components.CollectionDetails.removefromblocklistpartialcount": "", + "components.Discover.CreateSlider.addSlider": "", + "components.Discover.CreateSlider.addcustomslider": "", + "components.Discover.CreateSlider.addfail": "", + "components.Discover.CreateSlider.addsuccess": "", + "components.Discover.CreateSlider.editSlider": "", + "components.Discover.CreateSlider.editfail": "", + "components.Discover.DiscoverTv.sortPopularityDesc": "", + "components.Discover.DiscoverTv.sortTitleAsc": "", + "components.Discover.DiscoverTv.sortTitleDesc": "", + "components.Discover.DiscoverTv.sortTmdbRatingAsc": "", + "components.Discover.DiscoverTv.sortTmdbRatingDesc": "", + "components.Discover.DiscoverTvGenre.genreSeries": "", + "components.Discover.DiscoverTvKeyword.keywordSeries": "", + "components.Discover.DiscoverTvLanguage.languageSeries": "", + "components.Discover.FilterSlideover.keywords": "", + "components.IssueModal.issueOther": "", + "components.IssueModal.issueSubtitles": "", + "components.IssueModal.issueVideo": "", + "components.Layout.SearchInput.searchPlaceholder": "", + "components.LanguageSelector.languageServerDefault": "", + "components.LanguageSelector.originalLanguageDefault": "", + "components.Layout.LanguagePicker.displaylanguage": "", + "components.Login.credentialerror": "", + "components.Login.description": "", + "components.Settings.SonarrModal.createsonarr": "", + "components.Settings.SonarrModal.default4kserver": "", + "components.Settings.SonarrModal.defaultserver": "", + "components.Settings.advancedTooltip": "", + "components.Settings.allChosenProvidersAreOperational": "", + "components.Settings.notifications": "", + "components.ManageSlideOver.manageModalClearMediaWarning": "", + "components.ManageSlideOver.manageModalIssues": "", + "components.ManageSlideOver.manageModalMedia": "", + "components.ManageSlideOver.manageModalNoRequests": "", + "components.ManageSlideOver.manageModalRemoveMediaWarning": "", + "components.ManageSlideOver.manageModalRequests": "", + "components.ManageSlideOver.manageModalTitle": "", + "components.ManageSlideOver.mark4kavailable": "", + "components.ManageSlideOver.markallseasons4kavailable": "", + "components.UserProfile.limit": "", + "components.ManageSlideOver.markallseasonsavailable": "", + "components.ManageSlideOver.markavailable": "", + "components.ManageSlideOver.movie": "", + "components.ManageSlideOver.tvshow": "", + "components.MediaSlider.ShowMoreCard.seemore": "", + "components.MetadataSelector.selectMetdataProvider": "", + "components.MetadataSelector.tmdbLabel": "", + "components.MetadataSelector.tvdbLabel": "", + "components.NotificationTypeSelector.issuecomment": "", + "components.PermissionEdit.autoapprove4kMoviesDescription": "", + "components.PermissionEdit.autoapprove4kSeries": "", + "components.PermissionEdit.autoapprove4kSeriesDescription": "", + "components.PermissionEdit.autoapproveDescription": "", + "components.PermissionEdit.request": "", + "components.PermissionEdit.usersDescription": "", + "components.PermissionEdit.viewblocklistedItems": "", + "components.PermissionEdit.viewblocklistedItemsDescription": "", + "components.QuotaSelector.days": "", + "components.QuotaSelector.movieRequests": "", + "components.QuotaSelector.movies": "", + "components.RequestCard.approverequest": "", + "components.RequestCard.cancelrequest": "", + "components.RequestCard.declinerequest": "", + "components.RequestCard.deleterequest": "", + "components.RequestCard.failedretry": "", + "components.RequestCard.mediaerror": "", + "components.RequestCard.seasons": "", + "components.RequestCard.tmdbid": "", + "components.RequestList.RequestItem.cancelRequest": "", + "components.RequestList.RequestItem.deleterequest": "", + "components.RequestList.RequestItem.editrequest": "", + "components.RequestModal.AdvancedRequester.tags": "", + "components.RequestModal.alreadyrequested": "", + "components.Selector.CertificationSelector.minRating": "", + "components.Settings.Notifications.NotificationsPushover.sound": "", + "components.Settings.Notifications.NotificationsWebhook.webhookUrlTip": "", + "components.Settings.Notifications.agentenabled": "", + "components.Settings.OverrideRuleModal.create": "", + "components.Settings.RadarrModal.validationApplicationUrlTrailingSlash": "", + "components.Settings.RadarrModal.validationBaseUrlLeadingSlash": "", + "components.Settings.RadarrModal.validationBaseUrlTrailingSlash": "", + "components.Settings.RadarrModal.validationHostnameRequired": "", + "components.Settings.RadarrModal.validationMinimumAvailabilityRequired": "", + "components.Settings.SettingsJobsCache.dnscachehits": "", + "components.Settings.SettingsJobsCache.unknownJob": "", + "components.Settings.SettingsJobsCache.usersavatars": "", + "components.Settings.SettingsLogs.pauseLogs": "", + "components.Settings.SonarrModal.animeTags": "", + "components.Settings.SonarrModal.animelanguageprofile": "", + "components.Settings.SonarrModal.animequalityprofile": "", + "components.Settings.SonarrModal.animerootfolder": "", + "components.Settings.SonarrModal.apiKey": "", + "components.Settings.SonarrModal.baseUrl": "", + "components.Settings.SonarrModal.create4ksonarr": "", + "components.Settings.SonarrModal.enableSearch": "", + "components.Settings.SonarrModal.externalUrl": "", + "components.Settings.SonarrModal.hostname": "", + "components.Settings.SonarrModal.languageprofile": "", + "components.Settings.SonarrModal.loadingTags": "", + "components.Settings.SonarrModal.loadinglanguageprofiles": "", + "components.Settings.SonarrModal.loadingprofiles": "", + "components.Settings.SonarrModal.loadingrootfolders": "", + "components.Settings.menuJellyfinSettings": "", + "components.Settings.menuJobs": "", + "components.Settings.menuLogs": "", + "components.Settings.menuMetadataProviders": "", + "components.Settings.menuNetwork": "", + "components.Settings.menuNotifications": "", + "components.Settings.menuPlexSettings": "", + "components.Settings.menuServices": "", + "components.Settings.menuUsers": "", + "components.Settings.metadataProviderSelection": "", + "components.Settings.metadataProviderSettings": "", + "components.Settings.metadataSettings": "", + "components.Settings.noDefault4kServer": "", + "components.Settings.urlBase": "", + "components.Settings.validationApiKey": "", + "components.Settings.validationHostnameRequired": "", + "components.Settings.validationPortRequired": "", + "components.Settings.validationUrl": "", + "components.Settings.validationUrlBaseLeadingSlash": "", + "components.Settings.validationUrlBaseTrailingSlash": "", + "components.Settings.webAppUrlTip": "", + "components.Settings.webhook": "", + "components.Settings.webpush": "", + "components.Setup.signinWithJellyfin": "", + "components.UserList.accounttype": "", + "components.UserList.mediaServerUser": "", + "components.UserList.noJellyfinuserstoimport": "", + "components.UserList.sortByJoined": "", + "components.UserList.totalrequests": "", + "components.UserList.user": "", + "components.UserList.usercreatedfailed": "", + "components.UserList.usercreatedfailedexisting": "", + "components.UserList.userfail": "", + "components.UserList.userlist": "", + "components.UserList.username": "", + "components.UserList.users": "", + "components.UserList.validationEmail": "", + "components.UserList.validationUsername": "", + "components.UserList.validationpasswordminchars": "", + "components.UserProfile.ProfileHeader.joindate": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmoviestip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.engine": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverApplicationToken": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverUserKey": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramChatId": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramMessageThreadId": "", + "components.UserProfile.UserSettings.UserPasswordChange.noPasswordSet": "", + "components.UserProfile.UserSettings.UserPasswordChange.noPasswordSetOwnAccount": "", + "components.UserProfile.UserSettings.UserPasswordChange.validationCurrentPassword": "", + "i18n.delimitedlist": "", + "i18n.edit": "", + "i18n.error": "", + "i18n.experimental": "", + "i18n.importing": "", + "i18n.movie": "", + "i18n.movies": "", + "i18n.next": "", + "i18n.noresults": "", + "i18n.notrequested": "", + "i18n.open": "", + "i18n.partiallyavailable": "", + "i18n.requested": "", + "i18n.requesting": "", + "i18n.resolved": "", + "i18n.restartRequired": "", + "i18n.showingresults": "", + "i18n.specials": "", + "i18n.status": "", + "i18n.test": "", + "i18n.testing": "", + "i18n.tvshows": "", + "i18n.unavailable": "", + "i18n.usersettings": "", + "i18n.view": "", + "pages.errormessagewithcode": "", + "pages.oops": "", + "pages.somethingwentwrong": "", + "components.Login.urlBase": "", + "components.UserProfile.ProfileHeader.profile": "", + "components.UserProfile.ProfileHeader.settings": "", + "components.UserProfile.ProfileHeader.userid": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseries": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseriestip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.unknown": "" +} diff --git a/src/i18n/locale/lt.json b/src/i18n/locale/lt.json index 9e837d34c8..1b80225fa2 100644 --- a/src/i18n/locale/lt.json +++ b/src/i18n/locale/lt.json @@ -718,5 +718,955 @@ "components.PermissionEdit.autoapproveSeriesDescription": "Suteikite automatinį patvirtinimą ne 4K serialų rezervacijoms.", "components.Discover.emptywatchlist": "Čia bus rodoma medija, pridėta prie Plex grojaraščio.", "components.Discover.PlexWatchlistSlider.emptywatchlist": "Čia bus rodoma į Plex grojaraštyje pridėta medija.", - "components.Discover.FilterSlideover.voteCount": "Balsų skaičius nuo {minValue} iki {maxValue}" + "components.Discover.FilterSlideover.voteCount": "Balsų skaičius nuo {minValue} iki {maxValue}", + "components.TvDetails.addtowatchlist": "", + "components.Selector.canceled": "", + "components.Layout.UserWarnings.emailInvalid": "", + "components.Layout.UserWarnings.emailRequired": "", + "components.Layout.UserWarnings.passwordRequired": "", + "components.Login.credentialerror": "", + "components.Login.description": "", + "components.Login.adminerror": "", + "components.IssueList.IssueItem.descriptionpreview": "", + "components.Discover.timeWindowDay": "", + "components.UserProfile.UserSettings.UserGeneralSettings.languageDefault": "", + "components.UserProfile.UserSettings.UserGeneralSettings.localuser": "", + "components.UserProfile.UserSettings.UserGeneralSettings.mediaServerUser": "", + "components.UserProfile.UserSettings.UserGeneralSettings.movierequestlimit": "", + "components.UserProfile.UserSettings.UserGeneralSettings.originallanguage": "", + "components.UserProfile.UserSettings.UserGeneralSettings.originallanguageTip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.save": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailure": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmailEmpty": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsSuccess": "", + "components.UserProfile.UserSettings.UserGeneralSettings.validationemailformat": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.created": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablewebpush": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleteerror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.unknown": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdAdd": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdPlaceholder": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdRemove": "", + "components.TvDetails.manageseries": "", + "components.Login.port": "", + "components.Settings.Notifications.NotificationsGotify.priority": "", + "components.DiscoverTvUpcoming.upcomingtv": "", + "components.Discover.FilterSlideover.excludeKeywords": "", + "components.CollectionDetails.removefromblocklistpartialcount": "", + "components.Layout.UserWarnings.profileIncomplete": "", + "components.Login.back": "", + "components.Login.emailAlreadyTaken": "", + "components.Login.emailtooltip": "", + "components.Login.enablessl": "", + "components.Login.hostname": "", + "components.Login.initialsignin": "", + "components.Login.initialsigningin": "", + "components.Login.invalidurlerror": "", + "components.Login.jellyfinLocalLoginHint": "", + "components.Login.loginwithapp": "", + "components.Login.noadminerror": "", + "components.Login.orsigninwith": "", + "components.Login.save": "", + "components.Login.saveFailed": "", + "components.Login.servertype": "", + "components.Login.tipUsernameHasTrailingWhitespace": "", + "components.Login.validationPortRequired": "", + "components.Login.validationusernamerequired": "", + "components.ManageSlideOver.removearr4k": "", + "components.MetadataSelector.selectMetdataProvider": "", + "components.MetadataSelector.tmdbLabel": "", + "components.MetadataSelector.tvdbLabel": "", + "components.MovieDetails.addtowatchlist": "", + "components.MovieDetails.imdbuserscore": "", + "components.MovieDetails.openradarr": "", + "components.MovieDetails.openradarr4k": "", + "components.MovieDetails.play": "", + "components.MovieDetails.play4k": "", + "components.MovieDetails.watchlistDeleted": "", + "components.NotificationTypeSelector.mediaautorequested": "", + "components.NotificationTypeSelector.mediaautorequestedDescription": "", + "components.PermissionEdit.autorequestDescription": "", + "components.PermissionEdit.blocklistedItemsDescription": "", + "components.PermissionEdit.request4kDescription": "", + "components.PermissionEdit.requestDescription": "", + "components.PermissionEdit.requestMoviesDescription": "", + "components.PermissionEdit.requestTvDescription": "", + "components.PermissionEdit.usersDescription": "", + "components.PermissionEdit.viewblocklistedItems": "", + "components.PermissionEdit.viewblocklistedItemsDescription": "", + "components.QuotaSelector.tvRequests": "", + "components.RequestList.RequestItem.removearr": "", + "components.RequestList.RequestItem.unknowntitle": "", + "components.RequestList.sortDirection": "", + "components.RequestModal.pending4krequest": "", + "components.RequestModal.pendingrequest": "", + "components.RequestModal.requestmovie4ktitle": "", + "components.Selector.CertificationSelector.maxRating": "", + "components.Selector.CertificationSelector.selectCertification": "", + "components.Selector.CertificationSelector.selectCountry": "", + "components.Selector.CertificationSelector.starttyping": "", + "components.Selector.ended": "", + "components.Selector.inProduction": "", + "components.Selector.nooptions": "", + "components.Selector.planned": "", + "components.Selector.searchGenres": "", + "components.Selector.searchKeywords": "", + "components.Selector.searchStatus": "", + "components.Selector.searchStudios": "", + "components.Selector.searchUsers": "", + "components.Selector.showless": "", + "components.Selector.starttyping": "", + "components.Settings.Notifications.NotificationsGotify.gotifysettingsfailed": "", + "components.Settings.Notifications.NotificationsGotify.gotifysettingssaved": "", + "components.Settings.Notifications.NotificationsGotify.toastGotifyTestFailed": "", + "components.Settings.Notifications.NotificationsGotify.toastGotifyTestSending": "", + "components.Settings.Notifications.NotificationsGotify.toastGotifyTestSuccess": "", + "components.Settings.Notifications.NotificationsGotify.validationPriorityRequired": "", + "components.Settings.Notifications.NotificationsGotify.validationTokenRequired": "", + "components.Settings.Notifications.NotificationsGotify.validationTypes": "", + "components.Settings.Notifications.NotificationsGotify.validationUrlRequired": "", + "components.Settings.Notifications.NotificationsGotify.validationUrlTrailingSlash": "", + "components.Settings.Notifications.NotificationsNtfy.agentenabled": "", + "components.Settings.Notifications.NotificationsNtfy.embedPoster": "", + "components.Settings.Notifications.NotificationsNtfy.ntfysettingsfailed": "", + "components.Settings.Notifications.NotificationsNtfy.ntfysettingssaved": "", + "components.Settings.Notifications.NotificationsNtfy.password": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestSuccess": "", + "components.Settings.Notifications.NotificationsNtfy.token": "", + "components.Settings.Notifications.NotificationsNtfy.tokenAuth": "", + "components.Settings.Notifications.NotificationsNtfy.topic": "", + "components.Settings.Notifications.NotificationsNtfy.url": "", + "components.Settings.Notifications.NotificationsNtfy.username": "", + "components.Settings.Notifications.NotificationsNtfy.usernamePasswordAuth": "", + "components.Settings.Notifications.NotificationsNtfy.validationNtfyTopic": "", + "components.Settings.Notifications.NotificationsNtfy.validationNtfyUrl": "", + "components.Settings.Notifications.NotificationsNtfy.validationPriorityRequired": "", + "components.Settings.Notifications.NotificationsNtfy.validationTypes": "", + "components.Settings.Notifications.NotificationsPushbullet.accessToken": "", + "components.Settings.Notifications.NotificationsPushbullet.accessTokenTip": "", + "components.Settings.Notifications.NotificationsPushbullet.agentEnabled": "", + "components.Settings.Notifications.NotificationsPushbullet.channelTag": "", + "components.Settings.Notifications.NotificationsPushbullet.pushbulletSettingsFailed": "", + "components.Settings.Notifications.NotificationsPushbullet.pushbulletSettingsSaved": "", + "components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestSending": "", + "components.Settings.Notifications.NotificationsPushbullet.validationAccessTokenRequired": "", + "components.Settings.Notifications.NotificationsPushover.agentenabled": "", + "components.Settings.Notifications.NotificationsPushover.deviceDefault": "", + "components.Settings.Notifications.NotificationsPushover.embedPoster": "", + "components.Settings.Notifications.NotificationsPushover.pushoversettingsfailed": "", + "components.Settings.Notifications.NotificationsPushover.pushoversettingssaved": "", + "components.Settings.Notifications.NotificationsPushover.sound": "", + "components.Settings.Notifications.NotificationsPushover.toastPushoverTestFailed": "", + "components.Settings.Notifications.NotificationsPushover.toastPushoverTestSending": "", + "components.Settings.Notifications.NotificationsPushover.toastPushoverTestSuccess": "", + "components.Settings.Notifications.NotificationsPushover.userToken": "", + "components.Settings.Notifications.NotificationsPushover.userTokenTip": "", + "components.Settings.Notifications.NotificationsPushover.validationAccessTokenRequired": "", + "components.Settings.Notifications.NotificationsPushover.validationTypes": "", + "components.Settings.Notifications.NotificationsPushover.validationUserTokenRequired": "", + "components.Settings.Notifications.NotificationsSlack.agentenabled": "", + "components.Settings.Notifications.NotificationsSlack.embedPoster": "", + "components.Settings.Notifications.NotificationsSlack.slacksettingsfailed": "", + "components.Settings.Notifications.NotificationsSlack.toastSlackTestFailed": "", + "components.Settings.Notifications.NotificationsSlack.toastSlackTestSending": "", + "components.Settings.Notifications.NotificationsSlack.toastSlackTestSuccess": "", + "components.Settings.Notifications.NotificationsSlack.validationTypes": "", + "components.Settings.Notifications.NotificationsSlack.validationWebhookUrl": "", + "components.Settings.Notifications.NotificationsSlack.webhookUrl": "", + "components.Settings.Notifications.NotificationsSlack.webhookUrlTip": "", + "components.Settings.Notifications.NotificationsWebPush.agentenabled": "", + "components.Settings.Notifications.NotificationsWebPush.embedPoster": "", + "components.Settings.Notifications.NotificationsWebPush.httpsRequirement": "", + "components.Settings.Notifications.NotificationsWebPush.toastWebPushTestFailed": "", + "components.Settings.Notifications.NotificationsWebPush.toastWebPushTestSending": "", + "components.Settings.Notifications.NotificationsWebPush.toastWebPushTestSuccess": "", + "components.Settings.Notifications.NotificationsWebPush.webpushsettingssaved": "", + "components.Settings.Notifications.allowselfsigned": "", + "components.Settings.Notifications.authPass": "", + "components.Settings.Notifications.botApiTip": "", + "components.Settings.Notifications.botAvatarUrl": "", + "components.Settings.Notifications.botUsernameTip": "", + "components.Settings.Notifications.discordsettingsfailed": "", + "components.Settings.Notifications.discordsettingssaved": "", + "components.Settings.Notifications.emailsender": "", + "components.Settings.Notifications.emailsettingsfailed": "", + "components.Settings.Notifications.emailsettingssaved": "", + "components.Settings.Notifications.enableMentions": "", + "components.Settings.Notifications.encryption": "", + "components.Settings.Notifications.encryptionImplicitTls": "", + "components.Settings.Notifications.encryptionOpportunisticTls": "", + "components.Settings.Notifications.encryptionTip": "", + "components.Settings.Notifications.messageThreadId": "", + "components.Settings.Notifications.pgpPasswordTip": "", + "components.Settings.Notifications.pgpPrivateKey": "", + "components.Settings.Notifications.pgpPrivateKeyTip": "", + "components.Settings.Notifications.sendSilently": "", + "components.Settings.Notifications.sendSilentlyTip": "", + "components.Settings.Notifications.smtpHost": "", + "components.Settings.Notifications.smtpPort": "", + "components.Settings.Notifications.telegramsettingsfailed": "", + "components.Settings.Notifications.telegramsettingssaved": "", + "components.Settings.Notifications.toastDiscordTestFailed": "", + "components.Settings.Notifications.toastDiscordTestSending": "", + "components.Settings.Notifications.toastDiscordTestSuccess": "", + "components.Settings.Notifications.toastEmailTestFailed": "", + "components.Settings.Notifications.toastEmailTestSending": "", + "components.Settings.Notifications.toastEmailTestSuccess": "", + "components.Settings.Notifications.toastTelegramTestFailed": "", + "components.Settings.Notifications.toastTelegramTestSending": "", + "components.Settings.Notifications.toastTelegramTestSuccess": "", + "components.Settings.Notifications.userEmailRequired": "", + "components.Settings.Notifications.validationPgpPassword": "", + "components.Settings.Notifications.validationPgpPrivateKey": "", + "components.Settings.Notifications.validationUrl": "", + "components.Settings.OverrideRuleTile.genre": "", + "components.Settings.OverrideRuleTile.keywords": "", + "components.Settings.OverrideRuleTile.qualityprofile": "", + "components.Settings.OverrideRuleTile.tags": "", + "components.Settings.OverrideRuleTile.users": "", + "components.Settings.RadarrModal.apiKeyHelp": "", + "components.Settings.RadarrModal.baseUrlHelp": "", + "components.Settings.RadarrModal.server4k": "", + "components.Settings.RadarrModal.server4kHelp": "", + "components.Settings.RadarrModal.servername": "", + "components.Settings.RadarrModal.ssl": "", + "components.Settings.RadarrModal.syncEnabled": "", + "components.Settings.RadarrModal.tagRequestsInfo": "", + "components.Settings.RadarrModal.testFirstQualityProfiles": "", + "components.Settings.RadarrModal.testFirstRootFolders": "", + "components.Settings.RadarrModal.testFirstTags": "", + "components.Settings.RadarrModal.toastRadarrTestFailure": "", + "components.Settings.RadarrModal.validationApiKeyRequired": "", + "components.Settings.RadarrModal.validationApplicationUrl": "", + "components.Settings.SettingsAbout.githubdiscussions": "", + "components.Settings.SettingsAbout.runningDevelop": "", + "components.Settings.SettingsJobsCache.availability-sync": "", + "components.Settings.SettingsJobsCache.editJobScheduleCurrent": "", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorDays": "", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorHours": "", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorMinutes": "", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorSeconds": "", + "components.Settings.SettingsJobsCache.failures": "", + "components.Settings.SettingsJobsCache.flushcache": "", + "components.Settings.SettingsJobsCache.flushdnscache": "", + "components.Settings.SettingsJobsCache.hitRate": "", + "components.Settings.SettingsJobsCache.hits": "", + "components.Settings.SettingsJobsCache.image-cache-cleanup": "", + "components.Settings.SettingsJobsCache.imagecacheDescription": "", + "components.Settings.SettingsJobsCache.imagecachecount": "", + "components.Settings.SettingsJobsCache.imagecachesize": "", + "components.Settings.SettingsJobsCache.ipv4Fallbacks": "", + "components.Settings.SettingsJobsCache.jellyfin-full-scan": "", + "components.Settings.SettingsJobsCache.jobScheduleEditFailed": "", + "components.Settings.SettingsJobsCache.jobcancelled": "", + "components.Settings.SettingsJobsCache.jobname": "", + "components.Settings.SettingsJobsCache.jobsDescription": "", + "components.Settings.SettingsJobsCache.jobsandcache": "", + "components.Settings.SettingsJobsCache.jobstarted": "", + "components.Settings.SettingsJobsCache.nextexecution": "", + "components.Settings.SettingsJobsCache.plex-full-scan": "", + "components.Settings.SettingsJobsCache.plex-refresh-token": "", + "components.Settings.SettingsJobsCache.plex-watchlist-sync": "", + "components.Settings.SettingsJobsCache.process-blocklisted-tags": "", + "components.Settings.SettingsJobsCache.size": "", + "components.Settings.SettingsJobsCache.unknownJob": "", + "components.Settings.SettingsLogs.logDetails": "", + "components.Settings.SettingsLogs.logsDescription": "", + "components.Settings.SettingsLogs.pauseLogs": "", + "components.Settings.SettingsLogs.showall": "", + "components.Settings.SettingsLogs.viewdetails": "", + "components.Settings.SettingsMain.apikey": "", + "components.Settings.SettingsMain.apikeyCopied": "", + "components.Settings.SettingsMain.applicationTitle": "", + "components.Settings.SettingsMain.applicationurl": "", + "components.Settings.SettingsMain.blocklistLanguage": "", + "components.Settings.SettingsMain.blocklistedTags": "", + "components.Settings.SettingsMain.blocklistedTagsLimitTip": "", + "components.Settings.SettingsMain.blocklistedTagsTip": "", + "components.Settings.SettingsMain.cacheImages": "", + "components.Settings.SettingsMain.cacheImagesTip": "", + "components.Settings.SettingsMain.discoverRegion": "", + "components.Settings.SettingsMain.generalsettings": "", + "components.Settings.SettingsMain.generalsettingsDescription": "", + "components.Settings.SettingsMain.hideAvailableTip": "", + "components.Settings.SettingsNetwork.toastSettingsFailure": "", + "components.Settings.SettingsNetwork.toastSettingsSuccess": "", + "components.Settings.SettingsNetwork.trustProxyTip": "", + "components.Settings.SettingsNetwork.validationApiRequestTimeout": "", + "components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "", + "components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "", + "components.Settings.SettingsNetwork.validationProxyPort": "", + "components.Settings.SettingsUsers.atLeastOneAuth": "", + "components.Settings.SonarrModal.apiKeyHelp": "", + "components.Settings.SonarrModal.baseUrlHelp": "", + "components.Settings.SonarrModal.enableSearchHelp": "", + "components.Settings.SonarrModal.externalUrlHelp": "", + "components.Settings.SonarrModal.server4kHelp": "", + "components.Settings.SonarrModal.syncEnabledHelp": "", + "components.Settings.SonarrModal.tagRequestsInfo": "", + "components.Settings.addsonarr": "", + "components.Settings.advancedTooltip": "", + "components.Settings.allChosenProvidersAreOperational": "", + "components.Settings.animeMetadataProvider": "", + "components.Settings.apiKey": "", + "components.Settings.blocklistedTagImportTitle": "", + "components.Settings.blocklistedTagsText": "", + "components.Settings.plexlibraries": "", + "components.Settings.serverLocal": "", + "components.Settings.toastPlexConnectingSuccess": "", + "components.Setup.signinWithEmby": "", + "components.UserList.usercreatedfailed": "", + "components.UserProfile.UserSettings.UserGeneralSettings.generalsettings": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmail": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.noPermissionDescription": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorExists": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorUnauthorized": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.deletesubscription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.device": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablewebpush": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.engine": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordId": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeendisabled": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeenenabled": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.deviceDefault": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.notifications": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadIdTip": "", + "components.UserProfile.UserSettings.UserPermissions.unauthorizedDescription": "", + "components.UserProfile.UserSettings.menuPermissions": "", + "components.UserProfile.UserSettings.unauthorizedDescription": "", + "components.UserProfile.emptywatchlist": "", + "components.UserProfile.limit": "", + "components.UserProfile.localWatchlist": "", + "components.UserProfile.movierequests": "", + "components.UserProfile.pastdays": "", + "components.UserProfile.recentlywatched": "", + "components.UserProfile.requestsperdays": "", + "components.UserProfile.seriesrequest": "", + "components.UserProfile.totalrequests": "", + "components.UserProfile.unlimited": "", + "i18n.addToBlocklist": "", + "i18n.specials": "", + "component.BlocklistBlock.blocklistdate": "", + "component.BlocklistBlock.blocklistedby": "", + "components.Login.urlBaseHelp": "", + "components.Login.username": "", + "components.Login.validationEmailFormat": "", + "components.Login.validationUrlBaseLeadingSlash": "", + "components.Login.validationUrlBaseTrailingSlash": "", + "components.Login.validationUrlTrailingSlash": "", + "components.Login.validationemailformat": "", + "components.Login.validationhostformat": "", + "components.Login.validationhostrequired": "", + "components.Login.validationservertyperequired": "", + "components.MovieDetails.watchlistError": "", + "components.MovieDetails.watchlistSuccess": "", + "components.PermissionEdit.autorequestMovies": "", + "components.PermissionEdit.autorequestMoviesDescription": "", + "components.PermissionEdit.autorequestSeries": "", + "components.PermissionEdit.autorequestSeriesDescription": "", + "components.PermissionEdit.blocklistedItems": "", + "components.PermissionEdit.manageblocklist": "", + "components.PermissionEdit.manageblocklistDescription": "", + "components.PermissionEdit.manageissuesDescription": "", + "components.PermissionEdit.managerequestsDescription": "", + "components.PermissionEdit.viewissuesDescription": "", + "components.PermissionEdit.viewrequestsDescription": "", + "components.PermissionEdit.viewwatchlistsDescription": "", + "components.RequestCard.unknowntitle": "", + "components.RequestList.RequestItem.failedmodify": "", + "components.RequestList.RequestItem.profileName": "", + "components.RequestList.unableToConnect": "", + "components.Selector.showmore": "", + "components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestSuccess": "", + "components.Settings.Notifications.NotificationsWebhook.supportVariables": "", + "components.Settings.Notifications.chatId": "", + "components.Settings.Notifications.messageThreadIdTip": "", + "components.Settings.Notifications.pgpPassword": "", + "components.Settings.OverrideRuleModal.editrule": "", + "components.Settings.SettingsMain.validationUrl": "", + "components.Settings.SettingsMain.validationUrlTrailingSlash": "", + "components.Settings.SettingsNetwork.dnsCacheForceMinTtl": "", + "components.Settings.metadataProviderSettings": "", + "components.UserProfile.UserSettings.menuLinkedAccounts": "", + "components.UserProfile.recentrequests": "", + "components.TvDetails.status4k": "", + "components.UserList.validationUsername": "", + "components.Discover.resetsuccess": "", + "components.Discover.resetwarning": "", + "components.Discover.updatefailed": "", + "components.Settings.Notifications.NotificationsWebhook.authheader": "", + "components.Settings.Notifications.NotificationsWebhook.customHeaders": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersAdd": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersAuthConflict": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersIncomplete": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersValue": "", + "components.Settings.Notifications.NotificationsWebhook.resetPayload": "", + "components.Settings.Notifications.NotificationsWebhook.resetPayloadSuccess": "", + "components.Settings.Notifications.NotificationsWebhook.templatevariablehelp": "", + "components.Settings.Notifications.NotificationsWebhook.toastWebhookTestFailed": "", + "components.Settings.Notifications.NotificationsWebhook.toastWebhookTestSending": "", + "components.Settings.Notifications.NotificationsWebhook.toastWebhookTestSuccess": "", + "components.Settings.Notifications.NotificationsWebhook.validationTypes": "", + "components.Settings.Notifications.NotificationsWebhook.validationWebhookUrl": "", + "components.Settings.Notifications.NotificationsWebhook.webhookUrl": "", + "components.Settings.Notifications.NotificationsWebhook.webhookUrlTip": "", + "components.Settings.Notifications.NotificationsWebhook.webhooksettingssaved": "", + "components.Settings.Notifications.webhookRoleId": "", + "components.Settings.OverrideRuleModal.conditions": "", + "components.Settings.SettingsMain.blocklistLanguageTip": "", + "components.Settings.SettingsMain.blocklistRegion": "", + "components.Settings.SonarrModal.testFirstLanguageProfiles": "", + "components.Settings.SonarrModal.testFirstQualityProfiles": "", + "components.Settings.SonarrModal.testFirstRootFolders": "", + "components.Settings.invalidKeyword": "", + "components.Settings.nooptions": "", + "components.Settings.settingUpPlexDescription": "", + "components.Settings.settings": "", + "components.Setup.configureservices": "", + "components.Setup.finish": "", + "components.Setup.librarieserror": "", + "components.Setup.servertype": "", + "components.Setup.signin": "", + "components.Setup.signinMessage": "", + "components.Setup.signinWithJellyfin": "", + "components.Settings.OverrideRuleModal.notagoptions": "", + "components.Settings.serverpresetLoad": "", + "components.TvDetails.Season.somethingwentwrong": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.title": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.username": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.usernameRequired": "", + "components.UserProfile.UserSettings.UserGeneralSettings.accounttype": "", + "components.UserProfile.UserSettings.UserGeneralSettings.displayName": "", + "components.Discover.timeWindowWeek": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexuser": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmovies": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseriestip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.regionTip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.saving": "", + "components.UserProfile.UserSettings.UserGeneralSettings.seriesrequestlimit": "", + "components.UserProfile.UserSettings.UserGeneralSettings.streamingRegion": "", + "components.UserProfile.UserSettings.UserGeneralSettings.streamingRegionTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablingwebpusherror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.nodevicestoshow": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleted": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPgpPublicKey": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "", + "components.UserProfile.UserSettings.UserPasswordChange.newpassword": "", + "components.UserProfile.UserSettings.UserPermissions.toastSettingsFailure": "", + "components.UserProfile.UserSettings.UserPermissions.toastSettingsSuccess": "", + "i18n.blocklist": "", + "i18n.blocklistDuplicateError": "", + "components.PermissionEdit.viewwatchlists": "", + "components.Selector.pilot": "", + "components.Settings.Notifications.NotificationsGotify.token": "", + "components.Settings.Notifications.encryptionDefault": "", + "components.Settings.RadarrModal.validationNameRequired": "", + "components.Settings.SettingsJobsCache.dnsCache": "", + "components.Settings.SettingsJobsCache.imagecache": "", + "components.Settings.SettingsJobsCache.plex-recently-added-scan": "", + "components.Settings.SettingsMain.blocklistedTagsLimit": "", + "components.Settings.SettingsMain.hideAvailable": "", + "components.Settings.SettingsNetwork.proxySsl": "", + "components.Settings.SettingsUsers.loginMethods": "", + "components.Settings.SettingsUsers.mediaServerLogin": "", + "components.Settings.blocklistedTagImportInstructions": "", + "components.Settings.jellyfinSettings": "", + "components.Settings.serverpresetRefreshing": "", + "components.Settings.OverrideRuleModal.rootfolder": "", + "components.TvDetails.Season.noepisodes": "", + "components.UserProfile.UserSettings.UserGeneralSettings.validationemailrequired": "", + "components.Settings.SettingsMain.toastApiKeySuccess": "", + "components.UserProfile.UserSettings.UserPasswordChange.validationNewPassword": "", + "components.Settings.validationUrlBaseLeadingSlash": "", + "components.Discover.FilterSlideover.certification": "", + "components.Login.signinwithjellyfin": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseries": "", + "component.BlocklistModal.blocklisting": "", + "components.Blocklist.blocklistNotFoundError": "", + "components.Blocklist.blocklistSettingsDescription": "", + "components.Blocklist.blocklistdate": "", + "components.Blocklist.blocklistedby": "", + "components.Blocklist.blocklistsettings": "", + "components.Blocklist.filterBlocklistedTags": "", + "components.Blocklist.filterManual": "", + "components.Blocklist.mediaName": "", + "components.Blocklist.mediaTmdbId": "", + "components.Blocklist.mediaType": "", + "components.Blocklist.showAllBlocklisted": "", + "components.Layout.Sidebar.blocklist": "", + "components.ManageSlideOver.manageModalRemoveMediaWarning": "", + "components.Settings.Notifications.NotificationsSlack.slacksettingssaved": "", + "components.Settings.addradarr": "", + "components.StatusChecker.reloadApp": "", + "components.StatusChecker.restartRequired": "", + "components.TvDetails.reportissue": "", + "components.TvDetails.seasonnumber": "", + "components.UserProfile.UserSettings.UserGeneralSettings.applanguage": "", + "i18n.delimitedlist": "", + "i18n.removeFromBlocklistSuccess": "", + "components.Settings.Notifications.NotificationsWebhook.webhooksettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.operatingsystem": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverApplicationToken": "", + "components.Settings.save": "", + "components.Settings.Notifications.NotificationsWebhook.validationJsonPayloadRequired": "", + "components.Settings.Notifications.usePublicLogo": "", + "components.Settings.Notifications.usePublicLogoTip": "", + "components.Settings.Notifications.useUserLocale": "", + "components.Settings.Notifications.validationSmtpHostRequired": "", + "components.Settings.Notifications.validationSmtpPortRequired": "", + "components.Settings.Notifications.validationTypes": "", + "components.Settings.Notifications.webhookRoleIdTip": "", + "components.Settings.Notifications.webhookUrlTip": "", + "components.Settings.OverrideRuleModal.conditionsDescription": "", + "components.Settings.OverrideRuleModal.create": "", + "components.Settings.OverrideRuleModal.createrule": "", + "components.Settings.OverrideRuleModal.genres": "", + "components.Settings.OverrideRuleModal.keywords": "", + "components.Settings.OverrideRuleModal.languages": "", + "components.Settings.OverrideRuleModal.qualityprofile": "", + "components.Settings.OverrideRuleModal.ruleCreated": "", + "components.Settings.OverrideRuleModal.ruleUpdated": "", + "components.Settings.OverrideRuleModal.selectService": "", + "components.Settings.OverrideRuleModal.selecttags": "", + "components.Settings.OverrideRuleModal.service": "", + "components.Settings.OverrideRuleModal.serviceDescription": "", + "components.Settings.OverrideRuleModal.settings": "", + "components.Settings.OverrideRuleModal.settingsDescription": "", + "components.Settings.OverrideRuleModal.users": "", + "components.Settings.OverrideRuleTile.conditions": "", + "components.Settings.RadarrModal.enableSearchHelp": "", + "components.Settings.RadarrModal.externalUrlHelp": "", + "components.Settings.RadarrModal.syncEnabledHelp": "", + "components.Settings.RadarrModal.validationApplicationUrlTrailingSlash": "", + "components.Settings.RadarrModal.validationBaseUrlLeadingSlash": "", + "components.Settings.RadarrModal.validationBaseUrlTrailingSlash": "", + "components.Settings.RadarrModal.validationHostnameRequired": "", + "components.Settings.RadarrModal.validationMinimumAvailabilityRequired": "", + "components.Settings.RadarrModal.validationPortRequired": "", + "components.Settings.RadarrModal.validationProfileRequired": "", + "components.Settings.RadarrModal.validationRootFolderRequired": "", + "components.Settings.SettingsAbout.Releases.versionChangelog": "", + "components.Settings.SettingsAbout.Releases.viewongithub": "", + "components.Settings.SettingsAbout.aboutseerr": "", + "components.Settings.SettingsAbout.contribute": "", + "components.Settings.SettingsAbout.gettingsupport": "", + "components.Settings.SettingsAbout.outofdate": "", + "components.Settings.SettingsJobsCache.cacheDescription": "", + "components.Settings.SettingsJobsCache.canceljob": "", + "components.Settings.SettingsJobsCache.dnsCacheDescription": "", + "components.Settings.SettingsJobsCache.dnsCacheGlobalStats": "", + "components.Settings.SettingsJobsCache.dnsCacheGlobalStatsDescription": "", + "components.Settings.SettingsJobsCache.dnsNoCacheEntries": "", + "components.Settings.SettingsJobsCache.dnscacheactiveaddress": "", + "components.Settings.SettingsJobsCache.dnscacheage": "", + "components.Settings.SettingsJobsCache.dnscacheflushed": "", + "components.Settings.SettingsJobsCache.dnscachehits": "", + "components.Settings.SettingsJobsCache.dnscachemisses": "", + "components.Settings.SettingsJobsCache.dnscachename": "", + "components.Settings.SettingsJobsCache.jellyfin-recently-added-scan": "", + "components.Settings.SettingsJobsCache.jobScheduleEditSaved": "", + "components.Settings.SettingsJobsCache.sonarr-scan": "", + "components.Settings.SettingsJobsCache.usersavatars": "", + "components.Settings.SettingsLogs.copiedLogMessage": "", + "components.Settings.SettingsLogs.copyToClipboard": "", + "components.Settings.SettingsLogs.extraData": "", + "components.Settings.SettingsLogs.filterDebug": "", + "components.Settings.SettingsMain.blocklistRegionTip": "", + "components.Settings.SettingsMain.hideBlocklisted": "", + "components.Settings.SettingsMain.hideBlocklistedTip": "", + "components.Settings.SettingsMain.locale": "", + "components.Settings.SettingsMain.originallanguage": "", + "components.Settings.SettingsMain.originallanguageTip": "", + "components.Settings.SettingsMain.partialRequestsEnabled": "", + "components.Settings.SettingsMain.streamingRegion": "", + "components.Settings.SettingsMain.streamingRegionTip": "", + "components.Settings.SettingsMain.toastApiKeyFailure": "", + "components.Settings.SettingsMain.validationApplicationUrl": "", + "components.Settings.SettingsMain.validationApplicationUrlTrailingSlash": "", + "components.Settings.SettingsMain.youtubeUrl": "", + "components.Settings.SettingsMain.youtubeUrlTip": "", + "components.Settings.SettingsNetwork.apiRequestTimeout": "", + "components.Settings.SettingsNetwork.apiRequestTimeoutTip": "", + "components.Settings.SettingsNetwork.csrfProtection": "", + "components.Settings.SettingsNetwork.csrfProtectionHoverTip": "", + "components.Settings.SettingsNetwork.csrfProtectionTip": "", + "components.Settings.SettingsNetwork.dnsCache": "", + "components.Settings.SettingsNetwork.dnsCacheForceMaxTtl": "", + "components.Settings.SettingsNetwork.dnsCacheHoverTip": "", + "components.Settings.SettingsNetwork.dnsCacheTip": "", + "components.Settings.SettingsNetwork.network": "", + "components.Settings.SettingsNetwork.networkDisclaimer": "", + "components.Settings.SettingsNetwork.networksettings": "", + "components.Settings.SettingsNetwork.networksettingsDescription": "", + "components.Settings.SettingsNetwork.proxyBypassFilter": "", + "components.Settings.SettingsNetwork.proxyBypassLocalAddresses": "", + "components.Settings.SettingsNetwork.proxyEnabled": "", + "components.Settings.SettingsNetwork.proxyEnabledTip": "", + "components.Settings.SettingsNetwork.proxyHostname": "", + "components.Settings.SettingsNetwork.proxyPassword": "", + "components.Settings.SettingsNetwork.proxyPort": "", + "components.Settings.SettingsNetwork.proxyUser": "", + "components.Settings.SettingsUsers.disabledMediaServerLoginWarning": "", + "components.Settings.SettingsUsers.loginMethodsTip": "", + "components.Settings.SettingsUsers.mediaServerLoginTip": "", + "components.Settings.SonarrModal.animeSeriesType": "", + "components.Settings.SonarrModal.monitorNewItems": "", + "components.Settings.SonarrModal.monitorNewItemsHelp": "", + "components.Settings.SonarrModal.testFirstTags": "", + "components.Settings.SonarrModal.toastSonarrTestFailure": "", + "components.Settings.SonarrModal.toastSonarrTestSuccess": "", + "components.Settings.SonarrModal.validationApiKeyRequired": "", + "components.Settings.SonarrModal.validationApplicationUrl": "", + "components.Settings.SonarrModal.validationBaseUrlLeadingSlash": "", + "components.Settings.SonarrModal.validationBaseUrlTrailingSlash": "", + "components.Settings.SonarrModal.validationHostnameRequired": "", + "components.Settings.SonarrModal.validationPortRequired": "", + "components.Settings.SonarrModal.validationProfileRequired": "", + "components.Settings.SonarrModal.validationRootFolderRequired": "", + "components.Settings.addrule": "", + "components.Settings.cancelscan": "", + "components.Settings.chooseProvider": "", + "components.Settings.clearBlocklistedTagsConfirm": "", + "components.Settings.clickTest": "", + "components.Settings.connectionTestFailed": "", + "components.Settings.copyBlocklistedTagsTip": "", + "components.Settings.currentlibrary": "", + "components.Settings.default4k": "", + "components.Settings.deleteServer": "", + "components.Settings.deleteserverconfirm": "", + "components.Settings.hostname": "", + "components.Settings.importBlocklistedTagsTip": "", + "components.Settings.invalidurlerror": "", + "components.Settings.jellyfinForgotPasswordUrl": "", + "components.Settings.jellyfinSettingsDescription": "", + "components.Settings.jellyfinSyncFailedAutomaticGroupedFolders": "", + "components.Settings.jellyfinSyncFailedGenericError": "", + "components.Settings.jellyfinSyncFailedNoLibrariesFound": "", + "components.Settings.jellyfinlibraries": "", + "components.Settings.jellyfinlibrariesDescription": "", + "components.Settings.jellyfinsettings": "", + "components.Settings.jellyfinsettingsDescription": "", + "components.Settings.librariesRemaining": "", + "components.Settings.manualscan": "", + "components.Settings.manualscanDescription": "", + "components.Settings.manualscanDescriptionJellyfin": "", + "components.Settings.menuJellyfinSettings": "", + "components.Settings.menuJobs": "", + "components.Settings.menuLogs": "", + "components.Settings.menuMetadataProviders": "", + "components.Settings.menuNetwork": "", + "components.Settings.menuNotifications": "", + "components.Settings.metadataSettings": "", + "components.Settings.metadataSettingsSaved": "", + "components.Settings.no": "", + "components.Settings.noDefault4kServer": "", + "components.Settings.noDefaultNon4kServer": "", + "components.Settings.noDefaultServer": "", + "components.Settings.noSpecialCharacters": "", + "components.Settings.notTested": "", + "components.Settings.notificationAgentSettingsDescription": "", + "components.Settings.notificationsettings": "", + "components.Settings.notrunning": "", + "components.Settings.operational": "", + "components.Settings.overrideRules": "", + "components.Setup.setup": "", + "components.Settings.overrideRulesDescription": "", + "components.Settings.plexlibrariesDescription": "", + "components.Settings.plexsettings": "", + "components.Settings.plexsettingsDescription": "", + "components.Settings.port": "", + "components.Settings.providerStatus": "", + "components.Settings.radarrsettings": "", + "components.Settings.restartrequiredTooltip": "", + "components.Settings.saving": "", + "components.Settings.scan": "", + "components.Settings.scanbackground": "", + "components.Settings.searchKeywords": "", + "components.Settings.seriesMetadataProvider": "", + "components.Settings.serverRemote": "", + "components.Settings.serverSecure": "", + "components.Settings.serverpreset": "", + "components.Settings.serverpresetManualMessage": "", + "components.Settings.services": "", + "components.Settings.sonarrsettings": "", + "components.Settings.ssl": "", + "components.Settings.startscan": "", + "components.Settings.starttyping": "", + "components.Settings.syncJellyfin": "", + "components.Settings.syncing": "", + "components.Settings.tautulliApiKey": "", + "components.Settings.tautulliSettings": "", + "components.Settings.tautulliSettingsDescription": "", + "components.Settings.timeout": "", + "components.Settings.tmdbProviderDoesnotWork": "", + "components.Settings.toastPlexConnecting": "", + "components.Settings.toastPlexConnectingFailure": "", + "components.Settings.toastPlexRefreshFailure": "", + "components.Settings.toastPlexRefreshSuccess": "", + "components.Settings.toastTautulliSettingsFailure": "", + "components.Settings.toastTautulliSettingsSuccess": "", + "components.Settings.tvdbProviderDoesnotWork": "", + "components.Settings.urlBase": "", + "components.Settings.validationApiKey": "", + "components.Settings.validationHostnameRequired": "", + "components.Settings.validationPortRequired": "", + "components.Settings.validationUrl": "", + "components.Settings.validationUrlBaseTrailingSlash": "", + "components.Settings.validationUrlTrailingSlash": "", + "components.Settings.valueRequired": "", + "components.Settings.webAppUrl": "", + "components.Settings.webAppUrlTip": "", + "components.Settings.webhook": "", + "components.Settings.webpush": "", + "components.Setup.back": "", + "components.Setup.signinWithPlex": "", + "components.Setup.subtitle": "", + "components.StatusBadge.status4k": "", + "components.StatusChecker.appUpdated": "", + "components.StatusChecker.restartRequiredDescription": "", + "components.TitleCard.addToWatchList": "", + "components.TitleCard.watchlistCancel": "", + "components.TitleCard.watchlistDeleted": "", + "components.TitleCard.watchlistError": "", + "components.TitleCard.watchlistSuccess": "", + "components.TvDetails.removefromwatchlist": "", + "components.TvDetails.rtcriticsscore": "", + "components.TvDetails.watchlistDeleted": "", + "components.TvDetails.watchlistError": "", + "components.TvDetails.watchlistSuccess": "", + "components.UserList.autogeneratepasswordTip": "", + "components.UserList.created": "", + "components.UserList.createlocaluser": "", + "components.UserList.deleteconfirm": "", + "components.UserList.descending": "", + "components.UserList.edituser": "", + "components.UserList.email": "", + "components.UserList.importedPlexUsersNoPassword": "", + "components.UserList.importedUsersNoPassword": "", + "components.UserList.importedfromJellyfin": "", + "components.UserList.importedfromplex": "", + "components.UserList.importfromJellyfin": "", + "components.UserList.importfromJellyfinerror": "", + "components.UserList.importfrommediaserver": "", + "components.UserList.importfromplex": "", + "components.UserList.noJellyfinuserstoimport": "", + "components.UserList.nouserstoimport": "", + "components.UserList.passwordinfodescription": "", + "components.UserList.plexuser": "", + "components.UserList.sortByRequests": "", + "components.UserList.sortByRole": "", + "components.UserList.sortByType": "", + "components.UserList.sortByUser": "", + "components.UserList.toggleSortDirection": "", + "components.UserList.toggleSortDirectionAria": "", + "components.UserList.usercreatedfailedexisting": "", + "components.UserList.usercreatedsuccess": "", + "components.UserList.userdeleted": "", + "components.UserList.userdeleteerror": "", + "components.UserList.userfail": "", + "components.UserList.username": "", + "components.UserList.userssaved": "", + "components.UserList.validationEmail": "", + "components.UserList.validationpasswordminchars": "", + "components.UserProfile.ProfileHeader.joindate": "", + "components.UserProfile.ProfileHeader.profile": "", + "components.UserProfile.ProfileHeader.settings": "", + "components.UserProfile.ProfileHeader.userid": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.description": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorUnauthorized": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorUnknown": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.password": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.passwordRequired": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.save": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.saving": "", + "components.UserProfile.UserSettings.UserGeneralSettings.email": "", + "components.UserProfile.UserSettings.UserGeneralSettings.enableOverride": "", + "components.UserProfile.UserSettings.UserGeneralSettings.general": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.deleteFailed": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.errorUnknown": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.linkedAccounts": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.linkedAccountsHint": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.noLinkedAccounts": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablingwebpusherror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.emailsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.emailsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.notificationsettings": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pgpPublicKey": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverApplicationTokenTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverUserKey": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoversettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoversettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.sendSilently": "", + "components.UserProfile.UserSettings.UserNotificationSettings.sendSilentlyDescription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.sound": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramChatId": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPushbulletAccessToken": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverUserKey": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramChatId": "", + "components.UserProfile.UserSettings.UserPasswordChange.localPasswordDescription": "", + "components.UserProfile.UserSettings.UserPasswordChange.noPasswordSetOwnAccount": "", + "components.UserProfile.UserSettings.UserPasswordChange.nopermissionDescription": "", + "components.UserProfile.UserSettings.UserPasswordChange.toastSettingsFailureVerifyCurrent": "", + "components.UserProfile.UserSettings.UserPasswordChange.toastSettingsSuccess": "", + "components.UserProfile.UserSettings.UserPasswordChange.validationConfirmPassword": "", + "components.UserProfile.UserSettings.UserPasswordChange.validationConfirmPasswordSame": "", + "components.UserProfile.UserSettings.UserPasswordChange.validationCurrentPassword": "", + "components.UserProfile.UserSettings.UserPasswordChange.validationNewPasswordLength": "", + "components.UserProfile.UserSettings.UserPermissions.permissions": "", + "i18n.blocklistSuccess": "", + "i18n.blocklisted": "", + "i18n.collection": "", + "i18n.completed": "", + "i18n.error": "", + "i18n.notificationLocale": "", + "i18n.partiallyblocklisted": "", + "i18n.restartRequired": "", + "components.RequestCard.failedmodify": "", + "components.Settings.Notifications.NotificationsPushbullet.validationTypes": "", + "components.Settings.Notifications.NotificationsPushover.accessToken": "", + "components.Settings.Notifications.NotificationsPushover.accessTokenTip": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersKey": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersRemove": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersTip": "", + "components.Settings.Notifications.NotificationsWebhook.customJson": "", + "components.Settings.Notifications.chatIdTip": "", + "components.Settings.Notifications.validationChatIdRequired": "", + "components.Settings.Notifications.validationEmail": "", + "components.Settings.Notifications.validationMessageThreadId": "", + "components.Settings.OverrideRuleTile.language": "", + "components.Settings.OverrideRuleTile.settings": "", + "components.Settings.RadarrModal.toastRadarrTestSuccess": "", + "components.Settings.SettingsJobsCache.misses": "", + "components.Settings.SettingsMain.discoverRegionTip": "", + "components.Settings.SettingsMain.enableSpecialEpisodes": "", + "components.Settings.SettingsNetwork.docs": "", + "components.Settings.SettingsNetwork.forceIpv4FirstTip": "", + "components.Settings.SonarrModal.seriesType": "", + "components.Settings.copyBlocklistedTags": "", + "components.Settings.copyBlocklistedTagsEmpty": "", + "components.Settings.menuServices": "", + "components.Settings.serviceSettingsDescription": "", + "components.Setup.configemby": "", + "components.Setup.configjellyfin": "", + "components.Setup.configuremediaserver": "", + "components.TvDetails.play": "", + "components.TvDetails.play4k": "", + "components.UserList.importfromplexerror": "", + "components.UserList.localLoginDisabled": "", + "components.UserList.localuser": "", + "components.UserList.newJellyfinsigninenabled": "", + "components.UserList.newplexsigninenabled": "", + "components.UserList.sortBy": "", + "components.UserList.sortByJoined": "", + "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegion": "", + "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegionTip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmoviestip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.region": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pgpPublicKeyTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessToken": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessTokenTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverApplicationToken": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramMessageThreadId": "", + "components.UserProfile.UserSettings.UserNotificationSettings.webpush": "", + "components.UserProfile.UserSettings.UserPasswordChange.confirmpassword": "", + "components.Settings.Notifications.NotificationsGotify.url": "", + "components.Settings.Notifications.webhookUrl": "", + "components.UserList.ascending": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.managedevices": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.type": "", + "components.Settings.SettingsNetwork.proxyBypassFilterTip": "", + "components.Discover.FilterSlideover.status": "", + "components.Settings.SonarrModal.validationNameRequired": "", + "components.Settings.enablessl": "", + "components.Settings.experimentalTooltip": "", + "components.Settings.externalUrl": "", + "components.Settings.failed": "", + "components.Settings.jellyfinSettingsFailure": "", + "components.Settings.jellyfinSettingsSuccess": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverUserKeyTip": "", + "i18n.deleted": "", + "components.Selector.CertificationSelector.errorLoading": "", + "components.Settings.Notifications.NotificationsWebPush.webpushsettingsfailed": "", + "components.Settings.Notifications.validationWebhookRoleId": "", + "components.Settings.OverrideRuleModal.tags": "", + "components.Settings.RadarrModal.tagRequests": "", + "components.Settings.SettingsAbout.supportseerr": "", + "components.Settings.SettingsNetwork.forceIpv4First": "", + "components.Settings.SonarrModal.tagRequests": "", + "components.Settings.yes": "", + "i18n.blocklistError": "", + "components.Settings.SonarrModal.validationApplicationUrlTrailingSlash": "", + "components.Setup.configplex": "", + "components.Settings.SettingsJobsCache.radarr-scan": "", + "components.StatusChecker.appUpdatedDescription": "", + "components.Settings.SettingsMain.toastSettingsSuccess": "", + "components.UserProfile.plexwatchlist": "", + "i18n.removefromBlocklist": "", + "components.Setup.welcome": "", + "components.Settings.Notifications.botUsername": "", + "components.ManageSlideOver.removearr": "", + "components.MovieDetails.downloadstatus": "", + "components.UserList.bulkedit": "", + "components.UserProfile.UserSettings.UserPasswordChange.currentpassword": "", + "components.UserList.deleteuser": "", + "components.MovieDetails.removefromwatchlist": "", + "components.RequestModal.SearchByNameModal.nomatches": "", + "components.Selector.returningSeries": "", + "components.Settings.Notifications.agentenabled": "", + "components.Settings.Notifications.authUser": "", + "components.Settings.Notifications.botAPI": "", + "components.Settings.SettingsAbout.Releases.viewchangelog": "", + "components.Settings.SettingsMain.toastSettingsFailure": "", + "components.Settings.SettingsMain.validationApplicationTitle": "", + "components.Settings.manualscanJellyfin": "", + "components.Login.title": "", + "components.Settings.tip": "", + "components.Selector.CertificationSelector.minRating": "", + "components.UserList.autogeneratepassword": "", + "components.Login.urlBase": "", + "components.Login.validationEmailRequired": "", + "components.Settings.Notifications.NotificationsNtfy.priority": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestFailed": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestSending": "", + "components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestFailed": "", + "components.Settings.Notifications.NotificationsWebhook.agentenabled": "", + "components.Settings.Notifications.embedPoster": "", + "components.Settings.Notifications.validationBotAPIRequired": "", + "components.Settings.OverrideRuleModal.selectQualityProfile": "", + "components.Settings.OverrideRuleModal.selectRootFolder": "", + "components.Settings.SettingsNetwork.trustProxy": "", + "components.Settings.SonarrModal.validationLanguageProfileRequired": "", + "components.Settings.activeProfile": "", + "components.Settings.failedToSaveMetadataSettings": "", + "components.TvDetails.episodeCount": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorExists": "", + "components.UserList.userlist": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramChatIdTipLong": "", + "components.UserProfile.UserSettings.UserPasswordChange.noPasswordSet": "", + "components.UserProfile.UserSettings.UserPasswordChange.toastSettingsFailure": "", + "components.Discover.RecentRequestsSlider.unableToConnect": "", + "components.Settings.toastPlexRefresh": "", + "components.StatusBadge.seasonnumber": "", + "components.Login.tipEmailHasTrailingWhitespace": "", + "components.Settings.Notifications.senderName": "", + "components.Selector.CertificationSelector.noOptions": "", + "components.Login.saving": "", + "components.Settings.Notifications.NotificationsWebhook.supportVariablesTip": "", + "components.Settings.OverrideRuleTile.rootfolder": "", + "components.Settings.general": "", + "components.UserList.mediaServerUser": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.browser": "", + "components.Settings.metadataProviderSelection": "", + "components.UserProfile.UserSettings.menuNotifications": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadId": "" } diff --git a/src/i18n/locale/nb_NO.json b/src/i18n/locale/nb_NO.json index de9acacbe6..a72f9ebed3 100644 --- a/src/i18n/locale/nb_NO.json +++ b/src/i18n/locale/nb_NO.json @@ -962,9 +962,6 @@ "components.ManageSlideOver.alltime": "Totalt", "components.Settings.Notifications.NotificationsPushbullet.channelTag": "Kanal-merke", "components.Settings.tautulliSettingsDescription": "Seerr kan hente avspillingshistorikk for Plex fra Tautulli. Fyll inn innstilingene under for å etablere kommunikasjon med din Tautulli-tjener.", - "components.UserProfile.UserSettings.UserGeneralSettings.discordId": "Discord Bruker ID", - "components.UserProfile.UserSettings.UserGeneralSettings.validationDiscordId": "Du må oppgi en gyldig Bruker ID for Discord", - "components.UserProfile.UserSettings.UserGeneralSettings.discordIdTip": "ID-nummeret til din brukerkonto", "components.Settings.SettingsAbout.appDataPath": "Datakatalog", "components.RequestBlock.languageprofile": "Språkprofil", "components.MovieDetails.digitalrelease": "Digital utgivelse", @@ -1182,5 +1179,494 @@ "components.Blocklist.mediaType": "Type", "components.Blocklist.showAllBlocklisted": "Vil alle skjulte medier", "components.Discover.RecentRequestsSlider.unableToConnect": "Kunne ikke koble til {services}. Noe information kan være utilgjengelig.", - "components.Blocklist.filterBlocklistedTags": "Skjulte tagger" + "components.Blocklist.filterBlocklistedTags": "Skjulte tagger", + "components.Settings.Notifications.useUserLocale": "", + "components.Settings.OverrideRuleModal.conditions": "", + "components.Settings.OverrideRuleModal.conditionsDescription": "", + "components.Settings.OverrideRuleModal.create": "", + "components.Settings.OverrideRuleModal.createrule": "", + "components.Settings.OverrideRuleModal.genres": "", + "components.Settings.OverrideRuleModal.keywords": "", + "components.Settings.OverrideRuleModal.qualityprofile": "", + "components.Settings.RadarrModal.baseUrlHelp": "", + "components.Settings.RadarrModal.externalUrlHelp": "", + "components.Settings.RadarrModal.server4kHelp": "", + "components.Settings.SettingsJobsCache.availability-sync": "", + "components.Settings.SettingsJobsCache.dnscacheflushed": "", + "components.Settings.SettingsJobsCache.dnscachemisses": "", + "components.Settings.SettingsJobsCache.dnscachename": "", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorSeconds": "", + "components.Settings.SettingsJobsCache.flushdnscache": "", + "components.Settings.SettingsJobsCache.hitRate": "", + "components.Settings.SettingsJobsCache.hits": "", + "components.Settings.SettingsJobsCache.image-cache-cleanup": "", + "components.Settings.SettingsJobsCache.imagecacheDescription": "", + "components.Settings.SettingsJobsCache.plex-refresh-token": "", + "components.Settings.SettingsJobsCache.process-blocklisted-tags": "", + "components.Settings.SettingsJobsCache.size": "", + "components.Settings.SettingsJobsCache.usersavatars": "", + "components.Settings.SettingsMain.blocklistLanguageTip": "", + "components.Settings.SettingsMain.blocklistRegion": "", + "components.Settings.SettingsMain.blocklistRegionTip": "", + "components.Settings.SettingsMain.blocklistedTags": "", + "components.Settings.SettingsMain.blocklistedTagsLimit": "", + "components.Settings.SettingsMain.blocklistedTagsLimitTip": "", + "components.Settings.SettingsMain.blocklistedTagsTip": "", + "components.Settings.SettingsMain.cacheImages": "", + "components.Settings.SettingsMain.discoverRegionTip": "", + "components.Settings.SettingsMain.enableSpecialEpisodes": "", + "components.Settings.SettingsMain.hideAvailable": "", + "components.Settings.SettingsMain.hideAvailableTip": "", + "components.Settings.SettingsMain.hideBlocklisted": "", + "components.Settings.SettingsMain.hideBlocklistedTip": "", + "components.Settings.SettingsMain.validationApplicationUrl": "", + "components.Settings.SettingsMain.validationApplicationUrlTrailingSlash": "", + "components.Settings.SettingsMain.validationUrl": "", + "components.Settings.SettingsMain.validationUrlTrailingSlash": "", + "components.Settings.SettingsMain.youtubeUrl": "", + "components.Settings.SettingsMain.youtubeUrlTip": "", + "components.Settings.SettingsNetwork.apiRequestTimeout": "", + "components.Settings.SettingsNetwork.apiRequestTimeoutTip": "", + "components.Settings.SettingsNetwork.csrfProtection": "", + "components.Settings.SettingsNetwork.csrfProtectionHoverTip": "", + "components.Settings.SettingsNetwork.csrfProtectionTip": "", + "components.Settings.SettingsNetwork.dnsCache": "", + "components.Settings.SettingsNetwork.dnsCacheForceMaxTtl": "", + "components.Settings.SettingsNetwork.dnsCacheForceMinTtl": "", + "components.Settings.SettingsNetwork.dnsCacheHoverTip": "", + "components.Settings.SettingsNetwork.dnsCacheTip": "", + "components.Settings.SettingsNetwork.docs": "", + "components.Settings.SettingsNetwork.forceIpv4First": "", + "components.Settings.SettingsNetwork.forceIpv4FirstTip": "", + "components.Settings.SettingsNetwork.network": "", + "components.Settings.SettingsNetwork.networkDisclaimer": "", + "components.Settings.SettingsNetwork.networksettings": "", + "components.Settings.SettingsNetwork.networksettingsDescription": "", + "components.Settings.SettingsNetwork.proxyBypassFilter": "", + "components.Settings.SettingsNetwork.proxyBypassFilterTip": "", + "components.Settings.SettingsNetwork.proxyBypassLocalAddresses": "", + "components.Settings.SettingsNetwork.proxyEnabledTip": "", + "components.Settings.SettingsNetwork.proxyPort": "", + "components.Settings.SettingsNetwork.toastSettingsSuccess": "", + "components.Settings.SettingsNetwork.trustProxyTip": "", + "components.Settings.SettingsNetwork.validationApiRequestTimeout": "", + "components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "", + "components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "", + "components.Settings.SettingsUsers.loginMethods": "", + "components.Settings.SonarrModal.monitorNewItems": "", + "components.Settings.SonarrModal.seriesType": "", + "components.Settings.blocklistedTagImportTitle": "", + "components.Settings.blocklistedTagsText": "", + "components.Settings.chooseProvider": "", + "components.Settings.failedToSaveMetadataSettings": "", + "components.Settings.general": "", + "components.Settings.importBlocklistedTagsTip": "", + "components.Settings.invalidKeyword": "", + "components.Settings.invalidurlerror": "", + "components.Settings.jellyfinSettingsDescription": "", + "components.Settings.jellyfinSettingsFailure": "", + "components.Settings.jellyfinSettingsSuccess": "", + "components.Settings.metadataSettingsSaved": "", + "components.Settings.no": "", + "components.Settings.nooptions": "", + "components.Settings.notTested": "", + "components.Settings.operational": "", + "components.Settings.overrideRules": "", + "components.Settings.overrideRulesDescription": "", + "components.Settings.searchKeywords": "", + "components.Settings.seriesMetadataProvider": "", + "components.Settings.settings": "", + "components.Settings.starttyping": "", + "components.Settings.syncJellyfin": "", + "components.Settings.syncing": "", + "components.Settings.tvdbProviderDoesnotWork": "", + "components.Settings.yes": "", + "components.Setup.back": "", + "components.Setup.configemby": "", + "components.Setup.configuremediaserver": "", + "components.TitleCard.watchlistCancel": "", + "components.TitleCard.watchlistDeleted": "", + "components.TitleCard.watchlistSuccess": "", + "components.TvDetails.Season.noepisodes": "", + "components.UserList.descending": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.save": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.saving": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.title": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.username": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.usernameRequired": "", + "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegionTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablingwebpusherror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingssaved": "", + "components.UserProfile.UserSettings.UserGeneralSettings.mediaServerUser": "", + "components.UserProfile.UserSettings.UserGeneralSettings.save": "", + "components.UserProfile.UserSettings.UserGeneralSettings.saving": "", + "components.UserProfile.UserSettings.UserGeneralSettings.streamingRegion": "", + "components.UserProfile.UserSettings.UserGeneralSettings.streamingRegionTip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmailEmpty": "", + "components.UserProfile.UserSettings.UserGeneralSettings.validationemailformat": "", + "components.UserProfile.UserSettings.UserGeneralSettings.validationemailrequired": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.deleteFailed": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.errorUnknown": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.linkedAccounts": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.linkedAccountsHint": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.noLinkedAccounts": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.noPermissionDescription": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorExists": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorUnauthorized": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.browser": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.created": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.device": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablewebpush": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablewebpush": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablingwebpusherror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.engine": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.managedevices": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.nodevicestoshow": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.operatingsystem": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleted": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleteerror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.type": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.unknown": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeendisabled": "", + "components.UserProfile.UserSettings.UserNotificationSettings.deviceDefault": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdAdd": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdPlaceholder": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdRemove": "", + "components.Login.credentialerror": "", + "components.Login.description": "", + "components.Login.enablessl": "", + "components.MovieDetails.imdbuserscore": "", + "components.Layout.Sidebar.blocklist": "", + "components.Login.title": "", + "components.CollectionDetails.removefromblocklistpartialcount": "", + "components.Layout.UserWarnings.profileIncomplete": "", + "components.Login.adminerror": "", + "components.Login.emailAlreadyTaken": "", + "components.Login.emailtooltip": "", + "components.Login.hostname": "", + "components.Login.initialsignin": "", + "components.Login.initialsigningin": "", + "components.Login.invalidurlerror": "", + "components.Login.jellyfinLocalLoginHint": "", + "components.Login.loginwithapp": "", + "components.Login.noadminerror": "", + "components.Login.orsigninwith": "", + "components.Login.port": "", + "components.Login.save": "", + "components.Login.saveFailed": "", + "components.Login.saving": "", + "components.Login.servertype": "", + "components.Login.signinwithjellyfin": "", + "components.Login.tipUsernameHasTrailingWhitespace": "", + "components.Login.urlBase": "", + "components.Login.validationEmailRequired": "", + "components.Login.validationPortRequired": "", + "components.Login.validationUrlBaseLeadingSlash": "", + "components.Login.validationUrlBaseTrailingSlash": "", + "components.Login.validationUrlTrailingSlash": "", + "components.Login.validationemailformat": "", + "components.Login.validationhostformat": "", + "components.Login.validationhostrequired": "", + "components.Login.validationservertyperequired": "", + "components.Login.validationusernamerequired": "", + "components.ManageSlideOver.manageModalRemoveMediaWarning": "", + "components.MetadataSelector.selectMetdataProvider": "", + "components.MetadataSelector.tmdbLabel": "", + "components.MetadataSelector.tvdbLabel": "", + "components.MovieDetails.play": "", + "components.MovieDetails.play4k": "", + "components.MovieDetails.watchlistSuccess": "", + "components.PermissionEdit.blocklistedItems": "", + "components.PermissionEdit.blocklistedItemsDescription": "", + "components.PermissionEdit.manageblocklist": "", + "components.PermissionEdit.manageblocklistDescription": "", + "components.PermissionEdit.viewblocklistedItems": "", + "components.PermissionEdit.viewblocklistedItemsDescription": "", + "components.RequestList.RequestItem.removearr": "", + "components.RequestList.RequestItem.unknowntitle": "", + "components.RequestList.sortDirection": "", + "components.RequestList.unableToConnect": "", + "components.Selector.CertificationSelector.errorLoading": "", + "components.Selector.CertificationSelector.maxRating": "", + "components.Selector.CertificationSelector.minRating": "", + "components.Selector.nooptions": "", + "components.Selector.planned": "", + "components.Selector.returningSeries": "", + "components.Selector.searchKeywords": "", + "components.Selector.searchStatus": "", + "components.Selector.searchStudios": "", + "components.Selector.searchUsers": "", + "components.Selector.showless": "", + "components.Selector.showmore": "", + "components.Selector.starttyping": "", + "components.Settings.Notifications.NotificationsGotify.validationPriorityRequired": "", + "components.Settings.Notifications.NotificationsNtfy.agentenabled": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestSending": "", + "components.Settings.Notifications.NotificationsPushover.embedPoster": "", + "components.Settings.Notifications.NotificationsPushover.sound": "", + "components.Settings.Notifications.NotificationsSlack.embedPoster": "", + "components.Settings.Notifications.NotificationsWebhook.webhookUrlTip": "", + "components.Settings.Notifications.NotificationsWebPush.embedPoster": "", + "components.Settings.Notifications.messageThreadId": "", + "components.Settings.Notifications.messageThreadIdTip": "", + "components.Settings.Notifications.validationMessageThreadId": "", + "components.Settings.Notifications.validationWebhookRoleId": "", + "components.Settings.Notifications.webhookRoleIdTip": "", + "components.Setup.configjellyfin": "", + "components.Setup.configplex": "", + "components.UserList.username": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.deletesubscription": "", + "components.UserProfile.UserSettings.menuLinkedAccounts": "", + "i18n.partiallyblocklisted": "", + "i18n.removeFromBlocklistSuccess": "", + "i18n.removefromBlocklist": "", + "components.Login.urlBaseHelp": "", + "components.ManageSlideOver.removearr": "", + "components.ManageSlideOver.removearr4k": "", + "components.MovieDetails.openradarr": "", + "components.MovieDetails.openradarr4k": "", + "components.Settings.apiKey": "", + "components.Discover.timeWindowDay": "", + "components.Discover.timeWindowWeek": "", + "components.Settings.Notifications.usePublicLogo": "", + "components.Settings.Notifications.usePublicLogoTip": "", + "components.Settings.OverrideRuleModal.editrule": "", + "components.Settings.OverrideRuleModal.ruleCreated": "", + "components.Settings.OverrideRuleModal.ruleUpdated": "", + "components.Settings.OverrideRuleModal.selectQualityProfile": "", + "components.Settings.OverrideRuleModal.selectRootFolder": "", + "components.Settings.OverrideRuleModal.selectService": "", + "components.Settings.OverrideRuleModal.selecttags": "", + "components.Settings.OverrideRuleModal.serviceDescription": "", + "components.Settings.OverrideRuleModal.settings": "", + "components.Settings.OverrideRuleModal.settingsDescription": "", + "components.Settings.OverrideRuleModal.tags": "", + "components.Settings.OverrideRuleModal.users": "", + "components.Settings.OverrideRuleTile.conditions": "", + "components.Settings.OverrideRuleTile.genre": "", + "components.Settings.OverrideRuleTile.keywords": "", + "components.Settings.OverrideRuleTile.language": "", + "components.Settings.OverrideRuleTile.qualityprofile": "", + "components.Settings.OverrideRuleTile.rootfolder": "", + "components.Settings.OverrideRuleTile.settings": "", + "components.Settings.OverrideRuleTile.tags": "", + "components.Settings.OverrideRuleTile.users": "", + "components.Settings.RadarrModal.apiKeyHelp": "", + "components.Settings.RadarrModal.enableSearchHelp": "", + "components.Settings.RadarrModal.syncEnabledHelp": "", + "components.Settings.RadarrModal.tagRequests": "", + "components.Settings.RadarrModal.tagRequestsInfo": "", + "components.Settings.SettingsJobsCache.dnsCache": "", + "components.Settings.SettingsJobsCache.dnsCacheDescription": "", + "components.Settings.SettingsJobsCache.dnsCacheGlobalStats": "", + "components.Settings.SettingsJobsCache.dnsCacheGlobalStatsDescription": "", + "components.Settings.SettingsJobsCache.dnscacheactiveaddress": "", + "components.Settings.SettingsJobsCache.failures": "", + "components.Settings.SettingsJobsCache.imagecachecount": "", + "components.Settings.SettingsJobsCache.jellyfin-full-scan": "", + "components.Settings.SettingsJobsCache.jellyfin-recently-added-scan": "", + "components.Settings.SettingsJobsCache.misses": "", + "components.Settings.SettingsMain.apikey": "", + "components.Settings.SettingsMain.apikeyCopied": "", + "components.Settings.SettingsMain.applicationurl": "", + "components.Settings.SettingsMain.blocklistLanguage": "", + "components.Settings.SettingsMain.cacheImagesTip": "", + "components.Settings.SettingsMain.discoverRegion": "", + "components.Settings.SettingsMain.generalsettingsDescription": "", + "components.Settings.SettingsNetwork.proxyPassword": "", + "components.Settings.SettingsNetwork.validationProxyPort": "", + "components.Settings.SettingsUsers.atLeastOneAuth": "", + "components.Settings.SettingsUsers.disabledMediaServerLoginWarning": "", + "components.Settings.SettingsUsers.loginMethodsTip": "", + "components.Settings.SettingsUsers.mediaServerLogin": "", + "components.Settings.SettingsUsers.mediaServerLoginTip": "", + "components.Settings.SonarrModal.animeSeriesType": "", + "components.Settings.SonarrModal.apiKeyHelp": "", + "components.Settings.SonarrModal.baseUrlHelp": "", + "components.Settings.SonarrModal.enableSearchHelp": "", + "components.Settings.SonarrModal.externalUrlHelp": "", + "components.Settings.SonarrModal.monitorNewItemsHelp": "", + "components.Settings.SonarrModal.server4kHelp": "", + "components.Settings.SonarrModal.syncEnabledHelp": "", + "components.Settings.SonarrModal.tagRequests": "", + "components.Settings.SonarrModal.tagRequestsInfo": "", + "components.Settings.addrule": "", + "components.Settings.allChosenProvidersAreOperational": "", + "components.Settings.animeMetadataProvider": "", + "components.Settings.blocklistedTagImportInstructions": "", + "components.Settings.jellyfinSyncFailedAutomaticGroupedFolders": "", + "components.Settings.jellyfinSyncFailedNoLibrariesFound": "", + "components.Settings.jellyfinlibraries": "", + "components.Settings.jellyfinlibrariesDescription": "", + "components.Settings.jellyfinsettings": "", + "components.Settings.jellyfinsettingsDescription": "", + "components.Settings.manualscanDescriptionJellyfin": "", + "components.Settings.manualscanJellyfin": "", + "components.Settings.menuJellyfinSettings": "", + "components.Settings.metadataProviderSelection": "", + "components.Settings.noSpecialCharacters": "", + "components.Settings.providerStatus": "", + "components.Setup.librarieserror": "", + "components.Setup.servertype": "", + "components.Setup.signin": "", + "components.Setup.signinWithEmby": "", + "components.Setup.signinWithPlex": "", + "components.Setup.subtitle": "", + "components.StatusBadge.seasonepisodenumber": "", + "components.StatusBadge.seasonnumber": "", + "components.TvDetails.play4k": "", + "components.TvDetails.watchlistDeleted": "", + "components.TvDetails.watchlistSuccess": "", + "components.UserList.ascending": "", + "components.UserList.importedPlexUsersNoPassword": "", + "components.UserList.importedUsersNoPassword": "", + "components.UserList.importedfromJellyfin": "", + "components.UserList.importfromJellyfin": "", + "components.UserList.importfromJellyfinerror": "", + "components.UserList.mediaServerUser": "", + "components.UserList.newJellyfinsigninenabled": "", + "components.UserList.sortBy": "", + "components.UserList.sortByJoined": "", + "components.UserList.sortByRequests": "", + "components.UserList.sortByRole": "", + "components.UserList.sortByType": "", + "components.UserList.sortByUser": "", + "components.UserList.toggleSortDirectionAria": "", + "components.UserList.validationUsername": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.description": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.passwordRequired": "", + "components.UserProfile.UserSettings.UserGeneralSettings.email": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadIdTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramMessageThreadId": "", + "components.UserProfile.UserSettings.UserPasswordChange.localPasswordDescription": "", + "i18n.addToBlocklist": "", + "i18n.blocklist": "", + "i18n.blocklistDuplicateError": "", + "i18n.blocklistError": "", + "i18n.blocklistSuccess": "", + "i18n.blocklisted": "", + "i18n.collection": "", + "i18n.deleted": "", + "i18n.error": "", + "i18n.notificationLocale": "", + "i18n.specials": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeenenabled": "", + "components.RequestCard.failedmodify": "", + "components.RequestCard.unknowntitle": "", + "components.RequestList.RequestItem.failedmodify": "", + "components.Settings.Notifications.NotificationsGotify.priority": "", + "components.Settings.Notifications.NotificationsNtfy.embedPoster": "", + "components.Settings.Notifications.NotificationsNtfy.ntfysettingssaved": "", + "components.Settings.Notifications.webhookRoleId": "", + "components.Settings.OverrideRuleModal.languages": "", + "components.Settings.OverrideRuleModal.notagoptions": "", + "components.Settings.OverrideRuleModal.rootfolder": "", + "components.Settings.SettingsAbout.aboutseerr": "", + "components.Settings.SettingsAbout.contribute": "", + "components.Settings.SettingsAbout.supportseerr": "", + "components.Settings.SettingsJobsCache.dnscachehits": "", + "components.Settings.SettingsMain.locale": "", + "components.Settings.SettingsMain.originallanguage": "", + "components.Settings.SettingsMain.originallanguageTip": "", + "components.Settings.SettingsMain.partialRequestsEnabled": "", + "components.Settings.SettingsMain.streamingRegion": "", + "components.Settings.SettingsMain.streamingRegionTip": "", + "components.Settings.SettingsMain.toastApiKeyFailure": "", + "components.Settings.SettingsMain.toastApiKeySuccess": "", + "components.Settings.clearBlocklistedTagsConfirm": "", + "components.Settings.clickTest": "", + "components.Settings.menuMetadataProviders": "", + "components.Settings.menuNetwork": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingsfailed": "", + "i18n.completed": "", + "components.Settings.Notifications.NotificationsNtfy.ntfysettingsfailed": "", + "components.Settings.SettingsNetwork.proxyHostname": "", + "components.Settings.SettingsNetwork.proxyEnabled": "", + "components.MovieDetails.downloadstatus": "", + "components.Setup.signinWithJellyfin": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorExists": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorUnauthorized": "", + "components.UserProfile.UserSettings.UserNotificationSettings.sound": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadId": "", + "components.Selector.CertificationSelector.noOptions": "", + "components.Selector.CertificationSelector.selectCertification": "", + "components.Selector.searchGenres": "", + "components.Settings.SettingsNetwork.proxySsl": "", + "components.Settings.SettingsNetwork.toastSettingsFailure": "", + "components.TvDetails.play": "", + "components.IssueList.IssueItem.descriptionpreview": "", + "components.Settings.Notifications.NotificationsNtfy.validationNtfyUrl": "", + "components.Settings.Notifications.NotificationsNtfy.validationPriorityRequired": "", + "components.Settings.Notifications.NotificationsPushover.deviceDefault": "", + "components.Settings.Notifications.NotificationsWebhook.customHeaders": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersAdd": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersAuthConflict": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersIncomplete": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersKey": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersRemove": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersTip": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersValue": "", + "components.Settings.Notifications.embedPoster": "", + "components.Settings.Notifications.userEmailRequired": "", + "components.Settings.SettingsJobsCache.imagecache": "", + "components.Discover.updatesuccess": "", + "components.DownloadBlock.formattedTitle": "", + "components.Settings.jellyfinSyncFailedGenericError": "", + "components.Settings.timeout": "", + "components.Settings.tip": "", + "components.Settings.tmdbProviderDoesnotWork": "", + "components.UserList.noJellyfinuserstoimport": "", + "components.MovieDetails.watchlistDeleted": "", + "components.Selector.inProduction": "", + "components.Settings.copyBlocklistedTags": "", + "components.Settings.metadataProviderSettings": "", + "components.RequestList.RequestItem.profileName": "", + "components.Selector.CertificationSelector.selectCountry": "", + "components.Selector.CertificationSelector.starttyping": "", + "components.Selector.canceled": "", + "components.Selector.ended": "", + "components.Selector.pilot": "", + "components.Settings.Notifications.NotificationsNtfy.password": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestFailed": "", + "components.Settings.Notifications.NotificationsNtfy.token": "", + "components.Settings.SettingsJobsCache.dnsNoCacheEntries": "", + "components.Settings.Notifications.NotificationsNtfy.tokenAuth": "", + "components.Settings.Notifications.NotificationsNtfy.topic": "", + "components.Settings.SettingsJobsCache.dnscacheage": "", + "components.Settings.Notifications.NotificationsNtfy.url": "", + "components.Settings.Notifications.NotificationsNtfy.username": "", + "components.Settings.Notifications.NotificationsNtfy.usernamePasswordAuth": "", + "components.Settings.Notifications.NotificationsNtfy.validationNtfyTopic": "", + "components.Settings.save": "", + "components.Settings.Notifications.NotificationsNtfy.validationTypes": "", + "components.Settings.metadataSettings": "", + "components.Settings.OverrideRuleModal.service": "", + "components.UserList.toggleSortDirection": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorUnknown": "", + "components.Settings.SettingsJobsCache.ipv4Fallbacks": "", + "components.Settings.copyBlocklistedTagsEmpty": "", + "components.Settings.copyBlocklistedTagsTip": "", + "components.Settings.failed": "", + "components.Settings.jellyfinSettings": "", + "components.Settings.valueRequired": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.password": "", + "components.Settings.SettingsJobsCache.imagecachesize": "", + "components.Settings.SettingsNetwork.trustProxy": "", + "components.Settings.Notifications.NotificationsNtfy.priority": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestSuccess": "", + "components.Settings.SettingsMain.toastSettingsFailure": "", + "components.Settings.SettingsMain.toastSettingsSuccess": "", + "components.Settings.SettingsNetwork.proxyUser": "", + "components.Settings.connectionTestFailed": "", + "components.Settings.saving": "", + "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegion": "", + "components.Login.validationEmailFormat": "", + "components.Login.tipEmailHasTrailingWhitespace": "", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorDays": "", + "components.Settings.scanbackground": "", + "components.Login.back": "", + "components.Settings.jellyfinForgotPasswordUrl": "", + "components.Settings.Notifications.NotificationsWebhook.supportVariables": "", + "components.Settings.Notifications.NotificationsWebhook.supportVariablesTip": "" } diff --git a/src/i18n/locale/nl.json b/src/i18n/locale/nl.json index 5360809e99..778b32250d 100644 --- a/src/i18n/locale/nl.json +++ b/src/i18n/locale/nl.json @@ -158,7 +158,7 @@ "i18n.tvshows": "Series", "i18n.unavailable": "Niet beschikbaar", "pages.oops": "Oeps", - "pages.returnHome": "Terug naar de startpagina", + "pages.returnHome": "Terug naar startpagina", "components.MovieDetails.MovieCast.fullcast": "Volledige cast", "components.TvDetails.TvCast.fullseriescast": "Volledige cast van de serie", "components.Settings.Notifications.emailsettingssaved": "Instellingen voor e-mailmeldingen opgeslagen!", @@ -441,7 +441,7 @@ "components.UserProfile.UserSettings.UserGeneralSettings.generalsettings": "Algemene instellingen", "components.UserProfile.UserSettings.menuNotifications": "Meldingen", "components.UserProfile.UserSettings.menuGeneralSettings": "Algemeen", - "components.UserProfile.UserSettings.UserNotificationSettings.discordId": "Gebruikers-ID", + "components.UserProfile.UserSettings.UserNotificationSettings.discordId": "Gebruikers-ID's", "components.UserProfile.UserSettings.UserPasswordChange.validationNewPasswordLength": "Wachtwoord is te kort; moet minimaal 8 tekens bevatten", "components.UserProfile.UserSettings.UserPasswordChange.validationNewPassword": "Je moet een nieuw wachtwoord opgeven", "components.UserProfile.UserSettings.UserPasswordChange.validationCurrentPassword": "Je moet jouw huidige wachtwoord opgeven", @@ -475,8 +475,8 @@ "components.Settings.Notifications.NotificationsPushbullet.accessToken": "Toegangstoken", "components.Layout.UserDropdown.settings": "Instellingen", "components.Layout.UserDropdown.myprofile": "Profiel", - "components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "Je moet een geldige gebruikers-ID opgeven", - "components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "Het meercijferige ID-nummer van je gebruikersaccount", + "components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "Ieder ID moet een geldig Discord-gebruikers-id zijn", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "Het meercijferige ID-nummer van je gebruikersaccount. Voor meerdere accounts kun je meerdere gebruikers-id's toevoegen.", "components.CollectionDetails.requestcollection4k": "Collectie aanvragen in 4K", "components.UserProfile.UserSettings.UserGeneralSettings.regionTip": "Inhoud filteren op regionale beschikbaarheid", "components.UserProfile.UserSettings.UserGeneralSettings.region": "Regio van Ontdekken", @@ -566,7 +566,7 @@ "components.Settings.SettingsLogs.filterWarn": "Waarschuwing", "components.Settings.SettingsLogs.filterInfo": "Info", "pages.somethingwentwrong": "Er ging iets mis", - "pages.serviceunavailable": "Service niet beschikbaar", + "pages.serviceunavailable": "Dienst niet beschikbaar", "pages.pagenotfound": "Pagina niet gevonden", "pages.internalservererror": "Interne serverfout", "pages.errormessagewithcode": "{statusCode} - {error}", @@ -613,8 +613,8 @@ "components.RequestModal.QuotaDisplay.notenoughseasonrequests": "Onvoldoende seizoensaanvragen over", "components.RequestModal.QuotaDisplay.movielimit": "{limit, plural, one {film} other {films}}", "components.RequestModal.QuotaDisplay.movie": "film", - "components.RequestModal.QuotaDisplay.allowedRequestsUser": "Deze gebruiker mag elke {days} dagen {limit} {type} aanvragen.", - "components.RequestModal.QuotaDisplay.allowedRequests": "Je mag elke {days} dagen {limit} {type} aanvragen.", + "components.RequestModal.QuotaDisplay.allowedRequestsUser": "Deze gebruiker mag {limit} {type} aanvragen{days, plural, =0 {} one { per dag} other { per {days} dagen}}.", + "components.RequestModal.QuotaDisplay.allowedRequests": "Je kunt {limit} {type} aanvragen{days, plural, =0 {} one { per dag} other { per {days} dagen}}.", "components.QuotaSelector.unlimited": "Onbeperkt", "i18n.view": "Bekijken", "i18n.tvshow": "Serie", @@ -742,7 +742,7 @@ "components.PermissionEdit.requestMovies": "Films aanvragen", "components.UserProfile.UserSettings.UserGeneralSettings.languageDefault": "Standaard ({language})", "components.DownloadBlock.estimatedtime": "Geschat {time}", - "components.Settings.Notifications.NotificationsSlack.webhookUrlTip": "Maak een inkomende webhookintegratie aan", + "components.Settings.Notifications.NotificationsSlack.webhookUrlTip": "Maak een inkomende webhook integratie aan", "components.Settings.Notifications.botApiTip": "Maak een bot om te gebruiken met Seerr", "components.Settings.Notifications.webhookUrlTip": "Maak een webhook-integratie op je server", "components.Settings.Notifications.chatIdTip": "Start een chat met je bot, voeg @get_id_bot toe, en geef het /my_id commando", @@ -756,7 +756,7 @@ "components.Settings.Notifications.encryptionDefault": "STARTTLS gebruiken indien beschikbaar", "components.Settings.Notifications.encryption": "Encryptiemethode", "components.Settings.webAppUrl": "URL van Web App", - "components.Settings.webAppUrlTip": "Stuur gebruikers optioneel naar de web-app op uw server in plaats van de \"gehoste\" web-app", + "components.Settings.webAppUrlTip": "Stuur gebruikers optioneel naar de web-app op uw server in plaats van https://app.plex.tv/desktop", "components.UserList.localLoginDisabled": "De instelling Lokaal aanmelden inschakelen is momenteel uitgeschakeld.", "components.Settings.Notifications.NotificationsWebPush.httpsRequirement": "Om web-pushmeldingen te ontvangen, moet Seerr via HTTPS worden weergegeven.", "components.RequestList.RequestItem.requesteddate": "Aangevraagd", @@ -965,9 +965,6 @@ "components.Settings.toastTautulliSettingsSuccess": "Instellingen Tautulli opgeslagen!", "components.Settings.validationUrl": "Je moet een geldige URL opgeven", "components.Settings.validationUrlBaseLeadingSlash": "URL-basis moet met een schuine streep beginnen", - "components.UserProfile.UserSettings.UserGeneralSettings.discordId": "Gebruikers-ID Discord", - "components.UserProfile.UserSettings.UserGeneralSettings.discordIdTip": "Het meercijferige ID-nummer van je Discord-account", - "components.UserProfile.UserSettings.UserGeneralSettings.validationDiscordId": "Je moet een geldige gebruikers-ID van Discord opgeven", "components.Settings.SettingsAbout.appDataPath": "Gegevensmap", "components.RequestBlock.languageprofile": "Taalprofiel", "components.Settings.SettingsJobsCache.editJobScheduleCurrent": "Huidige frequentie", @@ -1188,7 +1185,7 @@ "components.Discover.tmdbtvstreamingservices": "Streamingdiensten voor series TMDB", "components.Login.validationhostrequired": "{mediaServerName}-URL vereist", "components.Layout.UserWarnings.emailInvalid": "E-mailadres is ongeldig.", - "components.Login.description": "Aangezien dit de eerste keer is dat je je aanmeldt bij {applicationName}, dien je een geldig e-mailadres op te geven.", + "components.Login.description": "Voeg een geldig e-mailadres toe om je profiel te voltooien. Dit wordt gebruikt voor meldingen en lokaal inloggen.", "components.Login.saving": "Toevoegen…", "components.ManageSlideOver.removearr": "Verwijderen van {arr}", "components.Settings.RadarrModal.tagRequests": "Aanvragen taggen", @@ -1361,7 +1358,7 @@ "components.UserProfile.UserSettings.UserLinkedAccountsSettings.linkedAccounts": "Gekoppelde accounts", "components.Settings.OverrideRuleModal.selectQualityProfile": "Kwaliteitsprofiel selecteren", "components.Settings.OverrideRuleTile.users": "Gebruikers", - "components.Setup.librarieserror": "Validatie mislukt. Schakel de bibliotheken opnieuw in en uit om verder te gaan.", + "components.Setup.librarieserror": "Het lukt niet om bibliotheken van je mediaserver te laden. Controleer of deze bereikbaar is en nog steeds correct is geconfigureerd.", "components.UserProfile.UserSettings.UserLinkedAccountsSettings.noPermissionDescription": "Je hebt geen toestemming om de gekoppelde accounts van deze gebruiker aan te passen.", "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadId": "Thread-/topic-ID", "components.UserProfile.UserSettings.LinkJellyfinModal.errorExists": "Dit account is al gekoppeld aan een {applicationName}-gebruiker", @@ -1537,7 +1534,7 @@ "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.engine": "Engine", "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.nodevicestoshow": "Er zijn geen webpush-abonnementen om weer te geven.", "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.type": "type", - "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeendisabled": "Web-push is uitgschakeld.", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeendisabled": "Web-push is uitgeschakeld.", "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeenenabled": "Web-push is ingeschakeld.", "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingsfailed": "Opslaan instellingen web-pushmeldingen mislukt.", "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingssaved": "Opslaan instellingen web-pushmeldingen gelukt!", @@ -1601,7 +1598,7 @@ "components.Settings.SettingsMain.blocklistedTagsLimitTip": "De \"Verwerk geblokkeerde tags\" taak zal maximaal dit aantal pagina's per sortering blokkeren. Grotere aantallen zorgen voor een nauwkeurigere blokkeerlijst, maar nemen meer ruimte in beslag.", "components.Settings.SettingsMain.blocklistedTagsTip": "Automatisch content met tags toevoegen aan de blokkeerlijst met behulp van de \"Geblokkeerde tags verwerken\" taak", "components.Settings.SettingsMain.hideBlocklistedTip": "Verberg geblokkeerde items op de ontdekkingspagina's voor alle gebruikers met de 'Blokkeerlijst beheren' machtiging", - "components.Settings.SettingsNetwork.apiRequestTimeout": "API-verzoek time-out", + "components.Settings.SettingsNetwork.apiRequestTimeout": "Timeout api-verzoek", "components.Settings.SettingsNetwork.apiRequestTimeoutTip": "Maximale wachttijd (in seconden) voor reacties van externe services zoals Radarr/Sonarr. Stel in op 0 voor geen time-out.", "components.Settings.SettingsNetwork.proxyEnabledTip": "Stuur ALLE uitgaande HTTP/HTTPS-verzoeken via een proxyserver (host/poort). Schakelt GEEN HTTPS-, SSL- of certificaatconfiguratie in.", "components.Settings.SettingsNetwork.validationApiRequestTimeout": "U moet een geldige time-outwaarde opgeven", @@ -1618,15 +1615,15 @@ "components.Settings.Notifications.NotificationsWebhook.customHeadersAdd": "Header toevoegen", "components.Settings.Notifications.NotificationsWebhook.customHeadersAuthConflict": "Het is niet mogelijk om zowel een Autorisatie-header als een aangepaste Autorisatie-header te gebruiken. Verwijder er alstublieft een.", "components.Settings.Notifications.NotificationsWebhook.customHeadersIncomplete": "Alle headers moeten zowel een naam als een waarde hebben", - "components.Settings.Notifications.NotificationsWebhook.customHeadersKey": "Header naam", + "components.Settings.Notifications.NotificationsWebhook.customHeadersKey": "Header-naam", "components.Settings.Notifications.NotificationsWebhook.customHeadersRemove": "Verwijder", "components.Settings.Notifications.NotificationsWebhook.customHeadersTip": "Voeg aangepaste HTTP-headers toe die bij webhook-verzoeken moeten worden meegestuurd", - "components.Settings.Notifications.NotificationsWebhook.customHeadersValue": "Header waarde", + "components.Settings.Notifications.NotificationsWebhook.customHeadersValue": "Header-waarde", "components.Settings.SonarrModal.monitorNewItems": "Houd nieuwe seizoenen in de gaten", "components.UserList.ascending": "oplopend", "components.UserList.descending": "aflopend", "components.UserList.sortBy": "Sorteren op {field}", - "components.UserList.sortByJoined": "Sorteren op datum van aanmelding", + "components.UserList.sortByJoined": "Sorteren op aanmelddatum", "components.UserList.sortByRequests": "Sorteren op aantal aanvragen", "components.UserList.sortByRole": "Sorteren op gebruikersrol", "components.UserList.sortByType": "Sorteren op accounttype", @@ -1635,7 +1632,7 @@ "components.UserList.toggleSortDirectionAria": "Sorteerrichting wijzigen", "i18n.error": "Er is iets misgegaan. Probeer het opnieuw.", "components.Login.tipEmailHasTrailingWhitespace": "De email eindigt met een witruimte", - "i18n.partiallyblocklisted": "Gedeeltelijk op de Blocklist", + "i18n.partiallyblocklisted": "Gedeeltelijk geblokkeerd", "components.Layout.UserWarnings.profileIncomplete": "Profiel is niet compleet", "components.Login.emailAlreadyTaken": "Dit e-mailadres is reeds in gebruik.", "components.Login.jellyfinLocalLoginHint": "Als je nog geen e-mailadres ingesteld hebt in jouw profiel, gebruik dan jouw {mediaServerName} gebruikersnaam in de plaats.", @@ -1654,5 +1651,60 @@ "components.UserList.importedPlexUsersNoPassword": "Geïmporteerde gebruikers hebben geen wachtwoord ingesteld voor {applicationTitle}. Als je het inloggen via Plex uitschakelt, moeten ze een wachtwoord instellen via hun profiel of via een link om het wachtwoord opnieuw in te stellen.", "components.UserList.importedUsersNoPassword": "Geïmporteerde gebruikers hebben geen wachtwoord ingesteld voor {applicationTitle}. Als je het inloggen via {mediaServerName} uitschakelt, moeten ze een wachtwoord instellen via hun profiel of via een link om het wachtwoord opnieuw in te stellen.", "components.UserProfile.UserSettings.UserPasswordChange.localPasswordDescription": "Dit wachtwoord wordt gebruikt om in te loggen via het lokale inlogformulier van {applicationTitle}. Het staat los van het wachtwoord voor uw mediaserver.", - "i18n.notificationLocale": "Taal voor meldingen" + "i18n.notificationLocale": "Taal voor meldingen", + "components.CollectionDetails.removefromblocklistpartialcount": "{removeLabel} ({count, plural, one {# film} other {# films}})", + "components.Login.urlBaseHelp": "Als je een Base-URL hebt ingesteld in Jellyfin (Dashboard > Networking), vul die hier in (bijv. /jellyfin). Laat anders leeg.", + "components.Settings.Notifications.useUserLocale": "Landinstellingen van de ontvanger voor meldingen gebruiken", + "components.Settings.RadarrModal.apiKeyHelp": "Vind het in Radarr onder: Settings > General > Security > API Key", + "components.Settings.RadarrModal.baseUrlHelp": "Als je een URL Base heb ingesteld in Radarr (Settings > General > Host), vul die hier in (bijv. /radarr). Laat anders leeg.", + "components.Settings.SettingsMain.blocklistLanguage": "Taal van blokkeerlijst", + "components.Settings.SettingsMain.blocklistLanguageTip": "Taal die voor het scannen van de blokkeerlijstinhoud gebruikt wordt (onafhankelijk van de ontdekken instellingen)", + "components.Settings.SettingsMain.blocklistRegion": "Regio van de blokkeerlijst", + "components.Settings.SettingsMain.blocklistRegionTip": "Regio die voor het scannen van de blokkeerlijstinhoud gebruikt wordt (onafhankelijk van de ontdekken instellingen)", + "components.Settings.SettingsUsers.disabledMediaServerLoginWarning": "Sommige gebruikers hebben mogelijk geen {applicationTitle}-wachtwoord ingesteld. Het uitschakelen van {mediaServerName}-aanmelding kan ervoor zorgen dat zij geen toegang meer hebben. Getroffen gebruikers moeten een wachtwoord instellen via hun profiel of via een link om het wachtwoord opnieuw in te stellen.", + "components.Settings.SonarrModal.apiKeyHelp": "Vind het in Sonarr onder: Settings > General > Security > API Key", + "components.Settings.SonarrModal.baseUrlHelp": "Als je een URL Base hebt ingesteld in Sonarr (Settings > General > Host), vul die hier in (bijv. /radarr). Laat anders leeg.", + "components.Settings.Notifications.usePublicLogo": "Seerr-logo gebruiken in plaats van instantielogo", + "components.Settings.Notifications.usePublicLogoTip": "Als jouw Seerr-instantie niet publiekelijk bereikbaar is, schakel dan deze optie in zodat e-mailclients van buiten jouw netwerk de afbeelding kunnen weergeven. De afbeelding zal worden opgehaald uit de openbare GitHub-repository.", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdAdd": "Gebruikers-ID toevoegen", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdPlaceholder": "Gebruikers-ID Discord", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdRemove": "Verwijderen", + "components.Settings.Notifications.validationWebhookThreadId": "Je moet een geldige Dicord Thread ID aanleveren", + "components.Settings.Notifications.webhookThreadId": "Thread ID", + "components.Common.QuickConnectModal.error": "Foutmelding", + "components.Common.QuickConnectModal.expired": "Code Verlopen", + "components.Common.QuickConnectModal.expiredMessage": "Deze Quick Connect code is verlopen. Probeer het opnieuw.", + "components.Common.QuickConnectModal.tryAgain": "Probeer opnieuw", + "components.Common.QuickConnectModal.waitingForAuth": "Wachten op goedkeuring…", + "components.Login.JellyfinQuickConnectModal.cancel": "Annuleren", + "components.Login.JellyfinQuickConnectModal.instructions": "Vul deze code in in jouw {mediaServerName} app", + "components.Login.JellyfinQuickConnectModal.subtitle": "Inloggen met Quick Connect", + "components.Login.JellyfinQuickConnectModal.title": "Quick Connect", + "components.Login.quickconnect": "Quick Connect", + "components.Login.quickconnecterror": "Quick Connect mislukt. Probeer het opnieuw.", + "components.RequestModal.AdvancedRequester.ignoreQuotaDescription": "Dit verzoek telt niet mee voor de gebruikerslimieten. Gebruik voorzichtig.", + "components.RequestModal.AdvancedRequester.ignoreQuotaTitle": "Gebruikersquota Omzeilen", + "components.UserList.importfromplexnochanges": "Plex import gelukt. Geen gebruikers aangemaakt of vernieuwd.", + "components.UserList.importfromplexsynced": "Plex gebruikers succesvol gesynchroniseerd. Bestaande gebruikers zijn vernieuwend.", + "components.UserProfile.UserSettings.LinkJellyfinModal.quickConnect": "Quick Connect gebruiken", + "components.UserProfile.UserSettings.LinkJellyfinQuickConnectModal.instructions": "Vul deze code in jouw {mediaServerName} app in", + "components.UserProfile.UserSettings.LinkJellyfinQuickConnectModal.subtitle": "Quick Connect", + "components.UserProfile.UserSettings.LinkJellyfinQuickConnectModal.title": "Verbind {mediaServerName} Account", + "components.UserProfile.UserSettings.UserNotificationSettings.discordNotificationsNotEnabled": "De server eigenaar heeft Disorder meldingen niet ingesteld. Deze informatie wordt alleen gebruikt als de server eigenaar de externe service instelt.", + "hooks.useQuickConnect.errorMessage": "Initialiseren van Quick Connect mislukt. Probeer het opnieuw.", + "i18n.sync": "Synchroniseren", + "i18n.syncing": "Synchroniseren…", + "components.UserProfile.UserSettings.LinkJellyfinQuickConnectModal.usePassword": "Gebruik in plaats daarvan een wachtwoord", + "components.ManageSlideOver.clearmediadataerror": "Er is iets misgegaan tijdens het wissen van de mediagegevens.", + "components.ManageSlideOver.removemediaerror": "Er is iets misgegaan tijdens het wissen van de media.", + "components.RequestList.RequestItem.removemediaerror": "Er is iets misgegaan tijdens het verwijderen van de media.", + "components.Settings.SettingsAbout.versionCheckDisabled": "Versiecontrole uitgeschakeld", + "components.Settings.SettingsMain.versionCheck": "Versiecontrole", + "components.Settings.SettingsMain.versionCheckTip": "Automatisch controleren op nieuwe versies op GitHub.", + "components.UserList.syncnotice": "Je kunt nog steeds op Synchroniseren klikken om bestaande Plex-gebruikersgegevens bij te werken, zoals bijgewerkte e-mailadressen of gebruikersnamen.", + "components.Settings.Notifications.webhookThreadIdTip": "Het ID van het threadkanaal waarin meldingen moeten worden geplaatst. Laat dit veld leeg om meldingen in het webhookkanaal te plaatsen", + "components.Settings.Notifications.NotificationsGotify.embedPoster": "Poster invoegen", + "components.Settings.jellyfinSyncFailedConnectionError": "De server {mediaServerName} kan niet worden bereikt. Controleer of de server actief is en bereikbaar is vanuit Seerr.", + "components.Settings.toastPlexSyncFailure": "Het synchroniseren van de Plex-bibliotheken is mislukt.", + "components.Settings.toggleLibraryFailure": "Het is niet gelukt om de bibliotheek bij te werken." } diff --git a/src/i18n/locale/pl.json b/src/i18n/locale/pl.json index 30f4b1b451..353bb14b58 100644 --- a/src/i18n/locale/pl.json +++ b/src/i18n/locale/pl.json @@ -369,9 +369,9 @@ "components.MovieDetails.releasedate": "{releaseCount, plural, one {Data wydania} other {Daty wydania}}", "components.MovieDetails.viewfullcrew": "Zobacz pełną obsadę", "components.PermissionEdit.autoapproveDescription": "Przyznaj automatyczne zatwierdzanie wszystkich próśb o multimedia inne niż 4K.", - "components.RequestButton.approve4krequests": "Zatwierdź {requestCount, plural, one {Prośba} other {{requestCount} Prośby}} 4K", + "components.RequestButton.approve4krequests": "Zatwierdź {requestCount, plural, one {prośbę} few {{requestCount} prośby} other {{requestCount} próśb}} 4K", "components.Settings.RadarrModal.minimumAvailability": "Minimalna dostępność", - "components.RequestButton.decline4krequests": "Odrzuć {requestCount, plural, one {prośby 4K} other {{requestCount} próśb 4K}}", + "components.RequestButton.decline4krequests": "Odrzuć {requestCount, plural, one {prośby 4K} few {{requestCount} prośby 4K} other {{requestCount} próśb 4K}}", "components.NotificationTypeSelector.usermediafailedDescription": "Otrzymuj powiadomienia, gdy prośby o multimedia nie zostaną dodane do Radarr lub Sonarr.", "components.PermissionEdit.advancedrequest": "Zaawansowane prośby", "components.PermissionEdit.advancedrequestDescription": "Przyznaj uprawnienia do modyfikowania zaawansowanych opcji próśb multimedia.", @@ -690,7 +690,7 @@ "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsSuccess": "Ustawienia zostały zapisane pomyślnie!", "components.UserProfile.UserSettings.UserNotificationSettings.notifications": "Powiadomienia", "components.UserProfile.UserSettings.UserNotificationSettings.telegramsettingssaved": "Ustawienia powiadomień Telegram zostały zapisane pomyślnie!", - "components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "Musisz podać prawidłowy identyfikator użytkownika", + "components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "Każde ID użytkownika Discord musi być prawidłowe", "components.UserProfile.UserSettings.UserNotificationSettings.validationPgpPublicKey": "Musisz podać prawidłowy klucz publiczny PGP", "components.UserProfile.UserSettings.UserPermissions.toastSettingsSuccess": "Uprawnienia zostały pomyślnie zapisane!", "components.UserProfile.UserSettings.menuChangePass": "Hasło", @@ -874,7 +874,7 @@ "components.Setup.finish": "Zakończ konfigurację", "components.TvDetails.network": "{networkCount, plural, one {Sieć} other {Sieci}}", "components.UserProfile.UserSettings.UserGeneralSettings.user": "Użytkownik", - "components.Settings.webAppUrlTip": "Opcjonalnie kieruj użytkowników do aplikacji internetowej na Twoim serwerze zamiast do \"hostowanej\" aplikacji internetowej", + "components.Settings.webAppUrlTip": "Opcjonalnie kieruj użytkowników do aplikacji internetowej na twoim serwerze zamiast https://app.plex.tv/desktop", "components.TvDetails.episodeRuntimeMinutes": "{runtime} minut", "components.TvDetails.viewfullcrew": "Zobacz pełną ekipę", "components.UserProfile.UserSettings.UserGeneralSettings.general": "Ogólne", @@ -884,10 +884,10 @@ "components.TvDetails.watchtrailer": "Obejrzyj zwiastun", "components.UserList.importfromplexerror": "Coś poszło nie tak podczas importowania użytkowników Plex.", "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailure": "Coś poszło nie tak podczas zapisywania ustawień.", - "components.UserProfile.UserSettings.UserNotificationSettings.discordId": "Identyfikator użytkownika", + "components.UserProfile.UserSettings.UserNotificationSettings.discordId": "Identyfikatory użytkowników", "components.UserProfile.UserSettings.UserNotificationSettings.sendSilently": "Wyślij po cichu", "components.UserProfile.UserSettings.UserNotificationSettings.telegramsettingsfailed": "Nie udało się zapisać ustawień powiadomień telegram.", - "components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "Wielocyfrowy numer ID powiązany z Twoim kontem użytkownika", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "Wielocyfrowy numer ID powiązany z Twoim kontem użytkownika. Dla wielu konto domowych, możesz dodać więcej niż jedno konto użytkownika Discord.", "components.UserList.importfromplex": "Importuj użytkowników Plex", "i18n.available": "Dostępny", "components.UserList.totalrequests": "Prośby", @@ -962,9 +962,6 @@ "components.UserProfile.recentlywatched": "Ostatnio oglądane", "i18n.import": "Importuj", "i18n.importing": "Importowanie…", - "components.UserProfile.UserSettings.UserGeneralSettings.discordId": "ID użytkownika Discorda", - "components.UserProfile.UserSettings.UserGeneralSettings.discordIdTip": "Wielocyfrowy numer ID powiązany z Twoim kontem użytkownika Discord", - "components.UserProfile.UserSettings.UserGeneralSettings.validationDiscordId": "Musisz podać poprawne ID użytkownika Discord", "components.Settings.SettingsAbout.appDataPath": "Katalog danych", "components.StatusChecker.restartRequiredDescription": "Uruchom ponownie serwer, aby zastosować zaktualizowane ustawienia.", "components.StatusChecker.appUpdated": "Zaktualizowano {applicationTitle}", @@ -1477,7 +1474,7 @@ "i18n.notificationLocale": "Język powiadomień", "components.Settings.tip": "Rada", "components.Settings.valueRequired": "Musisz podać wartość.", - "components.Setup.librarieserror": "Walidacja nie powiodła się. Proszę ponownie przełączyć biblioteki, aby kontynuować.", + "components.Setup.librarieserror": "Nie można załadować bibliotek z serwera multimediów. Sprawdź, czy jest on dostępny i czy nadal jest poprawnie skonfigurowany.", "components.UserList.importedPlexUsersNoPassword": "Zaimportowani użytkownicy nie mają ustawionego hasła w {applicationTitle}. Jeśli wyłączysz logowanie Plex, będą musieli ustawić hasło ze swojego profilu lub za pomocą łącza resetowania hasła.", "components.UserList.importedUsersNoPassword": "Zaimportowani użytkownicy nie mają ustawionego hasła w {applicationTitle}. Jeśli wyłączysz logowanie przez {mediaServerName}, będą musieli ustawić hasło ze swojego profilu lub za pomocą łącza resetowania hasła.", "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegionTip": "Filtruj zawartość według dostępności regionalnej", @@ -1666,5 +1663,48 @@ "components.Settings.starttyping": "Zacznij pisać, aby wyszukać.", "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadIdTip": "Jeśli w czacie grupowym włączono tematy, możesz tutaj podać ID wątku/tematu", "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramMessageThreadId": "ID wątku/tematu musi być dodatnią liczbą całkowitą", - "components.Settings.SettingsMain.blocklistRegionTip": "Region używany do skanowania zawartości czarnej listy (niezależnie od ustawień wykrywania)" + "components.Settings.SettingsMain.blocklistRegionTip": "Region używany do skanowania zawartości czarnej listy (niezależnie od ustawień wykrywania)", + "components.Settings.Notifications.usePublicLogo": "Użyj publicznego logo Seerr zamiast logo instancji", + "components.Settings.Notifications.usePublicLogoTip": "Jeśli twoja instancja Seerr nie jest publicznie dostępna, włącz tę opcję, aby klienci poczty e-mail spoza Twojej sieci mogli wyświetlić obraz. Obraz zostanie pobrany z publicznego repozytorium GitHub.", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdRemove": "Usuń", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdPlaceholder": "ID użytkownika Discord", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdAdd": "Dodaj ID użytkownika", + "components.Settings.Notifications.validationWebhookThreadId": "Musisz podać prawidłowy identyfikator wątku Discord", + "components.Settings.Notifications.webhookThreadId": "ID wątku", + "components.Settings.Notifications.webhookThreadIdTip": "Identyfikator kanału wątku, w którym mają być wysyłane powiadomienia. Pozostaw puste, aby wysyłać powiadomienia w kanale webhook", + "components.UserProfile.UserSettings.UserNotificationSettings.discordNotificationsNotEnabled": "Właściciel serwera nie włączył powiadomień Discord. Informacje te zostaną wykorzystane tylko wtedy, gdy właściciel serwera skonfiguruje usługę zewnętrzną.", + "components.Common.QuickConnectModal.error": "Błąd", + "components.Common.QuickConnectModal.expired": "Kod wygasł", + "components.Common.QuickConnectModal.expiredMessage": "Kod do szybkiego połączenia wygasł. Spróbuj ponownie.", + "components.Common.QuickConnectModal.tryAgain": "Spróbuj ponownie", + "components.Common.QuickConnectModal.waitingForAuth": "Trwa autoryzacja...", + "components.Login.JellyfinQuickConnectModal.cancel": "Anuluj", + "components.Login.JellyfinQuickConnectModal.instructions": "Wprowadź ten kod w swojej aplikacji {mediaServerName}", + "components.Login.JellyfinQuickConnectModal.subtitle": "Zaloguj się z szybkim połączeniem", + "components.Login.JellyfinQuickConnectModal.title": "Szybkie połączenie", + "components.Login.quickconnect": "Szybkie połączenie", + "components.Login.quickconnecterror": "Wystąpił błąd przy próbie szybkiego połączenia. Spróbuj ponownie.", + "components.RequestModal.AdvancedRequester.ignoreQuotaTitle": "Omiń limit użytkownika", + "components.UserProfile.UserSettings.LinkJellyfinModal.quickConnect": "Użyj szybkiego połączenia", + "i18n.sync": "Synchronizacja", + "i18n.syncing": "Synchronizuje…", + "components.UserProfile.UserSettings.LinkJellyfinQuickConnectModal.subtitle": "Szybkie połączenie", + "components.UserProfile.UserSettings.LinkJellyfinQuickConnectModal.title": "Połącz konto z {mediaServerName}", + "components.UserProfile.UserSettings.LinkJellyfinQuickConnectModal.usePassword": "Użyj jednak hasła", + "components.RequestModal.AdvancedRequester.ignoreQuotaDescription": "Ta prośba nie będzie wliczana do limitów przydzielonych użytkownikowi. Zachowaj ostrożność.", + "components.UserList.importfromplexnochanges": "Importowanie z Plex zakończone. Nie utworzono ani nie odświeżono żadnych użytkowników.", + "components.UserList.importfromplexsynced": "Synchronizacja użytkowników Plex przebiegła pomyślnie. Istniejący użytkownicy zostali odświeżeni.", + "components.UserList.syncnotice": "Nadal możesz kliknąć opcję Synchronizuj, aby odświeżyć istniejące konta użytkowników Plex, np. aktualizując adres e-mail lub nazwę użytkownika.", + "components.UserProfile.UserSettings.LinkJellyfinQuickConnectModal.instructions": "Wprowadź ten kod w swojej aplikacji {mediaServerName}", + "hooks.useQuickConnect.errorMessage": "Nie udało się zainicjować szybkiego połączenia. Spróbuj ponownie.", + "components.Settings.SettingsAbout.versionCheckDisabled": "Sprawdzenie wersji wyłączone", + "components.Settings.SettingsMain.versionCheck": "Sprawdzenie wersji", + "components.Settings.SettingsMain.versionCheckTip": "Automatycznie sprawdzaj czy jest nowa wersja na platformie GitHub.", + "components.Settings.jellyfinSyncFailedConnectionError": "Nie można połączyć się z serwerem {mediaServerName}. Sprawdź, czy jest uruchomiony i dostępny z Seerr.", + "components.Settings.toastPlexSyncFailure": "Nie udało się zsynchronizować bibliotek Plex.", + "components.Settings.toggleLibraryFailure": "Nie udało się zaktualizować biblioteki.", + "components.ManageSlideOver.clearmediadataerror": "Wystąpił błąd podczas czyszczenia danych multimediów.", + "components.ManageSlideOver.removemediaerror": "Wystąpił błąd podczas usuwania multimediów.", + "components.RequestList.RequestItem.removemediaerror": "Wystąpił błąd podczas usuwania multimedia.", + "components.Settings.Notifications.NotificationsGotify.embedPoster": "Dołącz plakat" } diff --git a/src/i18n/locale/pt_BR.json b/src/i18n/locale/pt_BR.json index 2cc453066a..69f334436e 100644 --- a/src/i18n/locale/pt_BR.json +++ b/src/i18n/locale/pt_BR.json @@ -957,9 +957,6 @@ "components.Settings.validationUrlTrailingSlash": "A URL não pode terminar com uma barra", "components.UserList.newplexsigninenabled": "A opção Habilitar Novo Método de Início de Sessão do Plex está habilitada. Usuários Plex com acesso à bibliotecas podem se autenticar sem que precisem serem importados.", "i18n.importing": "Importando…", - "components.UserProfile.UserSettings.UserGeneralSettings.discordId": "ID do Usuário Discord", - "components.UserProfile.UserSettings.UserGeneralSettings.discordIdTip": "O número de identificação associado à sua conta Discord", - "components.UserProfile.UserSettings.UserGeneralSettings.validationDiscordId": "Você deve prover um ID válido de usuário Discord", "components.UserProfile.recentlywatched": "Assistidos Recentemente", "i18n.import": "Importar", "components.ManageSlideOver.alltime": "Desde Início", @@ -1488,5 +1485,193 @@ "components.Settings.SettingsNetwork.apiRequestTimeoutTip": "Tempo máximo (em segundos) para aguardar respostas de serviços externos como Radarr/Sonarr. Defina como 0 para nenhum tempo limite.", "components.Settings.SettingsNetwork.proxyEnabledTip": "Envia TODAS as solicitações HTTP/HTTPS de saída por meio de um servidor proxy (host/porta). NÃO habilita HTTPS, SSL ou configuração de certificado.", "components.Settings.SettingsNetwork.validationApiRequestTimeout": "Você deve fornecer um valor de tempo limite válido", - "components.CollectionDetails.removefromblocklistpartialcount": "{removeLabel} {count, plural, one {# filme} other {# filmes}}" + "components.CollectionDetails.removefromblocklistpartialcount": "{removeLabel} {count, plural, one {# filme} other {# filmes}}", + "components.RequestCard.failedmodify": "", + "components.RequestList.RequestItem.failedmodify": "", + "components.Settings.Notifications.NotificationsWebhook.customHeaders": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersAdd": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersAuthConflict": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersIncomplete": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersKey": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersRemove": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersTip": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersValue": "", + "components.Settings.Notifications.useUserLocale": "", + "components.Settings.invalidKeyword": "", + "components.Settings.providerStatus": "", + "components.StatusBadge.seasonnumber": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadIdTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramMessageThreadId": "", + "components.UserProfile.UserSettings.UserPasswordChange.localPasswordDescription": "", + "components.UserProfile.UserSettings.UserGeneralSettings.email": "", + "components.UserProfile.UserSettings.UserGeneralSettings.streamingRegionTip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmail": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.deleteFailed": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.errorUnknown": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.linkedAccounts": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.linkedAccountsHint": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.noLinkedAccounts": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.noPermissionDescription": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorExists": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorUnauthorized": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.device": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablewebpush": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablingwebpusherror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablewebpush": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.nodevicestoshow": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.operatingsystem": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleted": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.type": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdAdd": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdPlaceholder": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdRemove": "", + "components.Layout.UserWarnings.profileIncomplete": "", + "components.Login.emailAlreadyTaken": "", + "components.Login.jellyfinLocalLoginHint": "", + "components.Login.saveFailed": "", + "components.Settings.SettingsMain.blocklistLanguage": "", + "components.Settings.SettingsMain.blocklistLanguageTip": "", + "components.Settings.SettingsMain.blocklistRegion": "", + "components.Settings.SettingsMain.blocklistRegionTip": "", + "components.Settings.SonarrModal.animeSeriesType": "", + "components.Settings.SonarrModal.monitorNewItems": "", + "components.Settings.blocklistedTagImportTitle": "", + "components.Settings.clearBlocklistedTagsConfirm": "", + "components.Settings.metadataProviderSettings": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.engine": "", + "components.Login.urlBaseHelp": "", + "components.Settings.RadarrModal.apiKeyHelp": "", + "components.UserList.sortBy": "", + "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegion": "", + "components.Settings.addrule": "", + "components.UserList.sortByRequests": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.saving": "", + "components.Settings.menuJellyfinSettings": "", + "components.Settings.animeMetadataProvider": "", + "components.Settings.failed": "", + "components.Settings.failedToSaveMetadataSettings": "", + "components.Settings.starttyping": "", + "components.UserProfile.UserSettings.UserGeneralSettings.streamingRegion": "", + "i18n.specials": "", + "components.Settings.Notifications.usePublicLogo": "", + "components.Settings.Notifications.usePublicLogoTip": "", + "components.Settings.RadarrModal.baseUrlHelp": "", + "components.Settings.RadarrModal.enableSearchHelp": "", + "components.Settings.RadarrModal.externalUrlHelp": "", + "components.Settings.RadarrModal.server4kHelp": "", + "components.Settings.RadarrModal.syncEnabledHelp": "", + "components.Settings.SettingsUsers.atLeastOneAuth": "", + "components.Settings.SettingsUsers.disabledMediaServerLoginWarning": "", + "components.Settings.SettingsUsers.loginMethods": "", + "components.Settings.SettingsUsers.loginMethodsTip": "", + "components.Settings.SettingsUsers.mediaServerLogin": "", + "components.Settings.SettingsUsers.mediaServerLoginTip": "", + "components.Settings.SonarrModal.apiKeyHelp": "", + "components.Settings.SonarrModal.baseUrlHelp": "", + "components.Settings.SonarrModal.enableSearchHelp": "", + "components.Settings.SonarrModal.externalUrlHelp": "", + "components.Settings.SonarrModal.monitorNewItemsHelp": "", + "components.Settings.SonarrModal.server4kHelp": "", + "components.Settings.SonarrModal.syncEnabledHelp": "", + "components.Settings.SonarrModal.tagRequests": "", + "components.Settings.allChosenProvidersAreOperational": "", + "components.Settings.blocklistedTagImportInstructions": "", + "components.Settings.clickTest": "", + "components.Settings.connectionTestFailed": "", + "components.Settings.copyBlocklistedTags": "", + "components.Settings.copyBlocklistedTagsEmpty": "", + "components.Settings.copyBlocklistedTagsTip": "", + "components.Settings.general": "", + "components.Settings.importBlocklistedTagsTip": "", + "components.Settings.menuMetadataProviders": "", + "components.Settings.menuNetwork": "", + "components.Settings.metadataSettings": "", + "components.Settings.noSpecialCharacters": "", + "components.Settings.nooptions": "", + "components.Settings.notTested": "", + "components.Settings.overrideRules": "", + "components.Settings.overrideRulesDescription": "", + "components.Settings.seriesMetadataProvider": "", + "components.Settings.settings": "", + "components.Settings.timeout": "", + "components.Settings.tvdbProviderDoesnotWork": "", + "components.Settings.yes": "", + "components.Setup.configemby": "", + "components.Setup.configjellyfin": "", + "components.Setup.configplex": "", + "components.Setup.configuremediaserver": "", + "components.Setup.librarieserror": "", + "components.UserList.ascending": "", + "components.UserList.descending": "", + "components.UserList.importedPlexUsersNoPassword": "", + "components.UserList.importedUsersNoPassword": "", + "components.UserList.sortByJoined": "", + "components.UserList.toggleSortDirectionAria": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.description": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorExists": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorUnauthorized": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorUnknown": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.password": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.passwordRequired": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.usernameRequired": "", + "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegionTip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmailEmpty": "", + "components.UserProfile.UserSettings.menuLinkedAccounts": "", + "i18n.blocklist": "", + "i18n.blocklistDuplicateError": "", + "i18n.blocklistError": "", + "i18n.blocklistSuccess": "", + "i18n.blocklisted": "", + "i18n.completed": "", + "i18n.deleted": "", + "i18n.error": "", + "i18n.notificationLocale": "", + "i18n.partiallyblocklisted": "", + "i18n.addToBlocklist": "", + "components.Discover.timeWindowDay": "", + "components.Discover.timeWindowWeek": "", + "components.Settings.Notifications.NotificationsNtfy.validationPriorityRequired": "", + "components.Settings.SonarrModal.seriesType": "", + "components.Settings.blocklistedTagsText": "", + "components.Settings.metadataProviderSelection": "", + "components.Settings.metadataSettingsSaved": "", + "components.Settings.no": "", + "components.Settings.scanbackground": "", + "components.Settings.searchKeywords": "", + "components.Settings.tip": "", + "components.Settings.tmdbProviderDoesnotWork": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.save": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.managedevices": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadId": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingssaved": "", + "components.Settings.apiKey": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.unknown": "", + "components.Settings.chooseProvider": "", + "components.Settings.operational": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablingwebpusherror": "", + "components.Settings.valueRequired": "", + "components.UserList.sortByRole": "", + "components.UserList.sortByType": "", + "components.UserList.sortByUser": "", + "components.UserList.toggleSortDirection": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.title": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.username": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.browser": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.created": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.deletesubscription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeenenabled": "", + "components.Settings.Notifications.NotificationsNtfy.priority": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeendisabled": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleteerror": "", + "components.Login.tipEmailHasTrailingWhitespace": "", + "components.Login.tipUsernameHasTrailingWhitespace": "", + "i18n.removeFromBlocklistSuccess": "", + "i18n.removefromBlocklist": "", + "components.Common.QuickConnectModal.error": "Erro", + "components.Common.QuickConnectModal.expired": "Código Expirado", + "components.Common.QuickConnectModal.expiredMessage": "Este código de Conexão Rápida expirou. Por favor, tente novamente.", + "components.Common.QuickConnectModal.tryAgain": "Tente novamente", + "components.Common.QuickConnectModal.waitingForAuth": "Aguardando por autorização..." } diff --git a/src/i18n/locale/pt_PT.json b/src/i18n/locale/pt_PT.json index 600b7675e5..b45c9d787e 100644 --- a/src/i18n/locale/pt_PT.json +++ b/src/i18n/locale/pt_PT.json @@ -799,7 +799,6 @@ "components.TvDetails.seasonstitle": "Temporadas", "components.TvDetails.status4k": "4K {status}", "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseries": "Pedir séries automaticamente", - "components.UserProfile.UserSettings.UserGeneralSettings.validationDiscordId": "Deve prover um ID válido de utilizador Discord", "i18n.resolved": "Resolvido", "components.PermissionEdit.createissuesDescription": "Concede permissão para reportar problemas com mídias.", "components.PermissionEdit.manageissuesDescription": "Concede permissão para gerir problemas com mídia.", @@ -814,7 +813,6 @@ "components.PermissionEdit.viewrecent": "Ver Recentemente Adicionados", "components.PermissionEdit.viewrecentDescription": "Concede permissão para ver lista de mídias adicionadas recentemente.", "components.Settings.SettingsJobsCache.editJobScheduleSelectorMinutes": "Cada {jobScheduleMinutes, plural, one {minuto} other {{jobScheduleMinutes} minutos}}", - "components.UserProfile.UserSettings.UserGeneralSettings.discordId": "ID do Utilizador Discord", "i18n.restartRequired": "Reinicialização Necessária", "components.StatusChecker.appUpdated": "{applicationTitle} Atualizado", "components.StatusChecker.restartRequiredDescription": "Por favor, reinicie o servidor para aplicar as novas configurações.", @@ -938,7 +936,6 @@ "components.Settings.validationApiKey": "Deve prover uma chave API válida", "components.Settings.validationUrl": "Deve prover uma URL válida", "components.TvDetails.productioncountries": "{countryCount, plural, one {País} other {Países}} de Produção", - "components.UserProfile.UserSettings.UserGeneralSettings.discordIdTip": "O número de identificação associado à sua conta Discord", "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletsettingsfailed": "Falha ao gravar configurações de notificação via Pushover.", "components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverApplicationToken": "Deve prover uma chave válida de acesso", "components.UserProfile.recentlywatched": "Assistidos Recentemente", @@ -1609,5 +1606,67 @@ "components.Settings.SettingsNetwork.apiRequestTimeout": "Tempo limite da solicitação da API", "components.Settings.SettingsNetwork.apiRequestTimeoutTip": "Tempo máximo (em segundos) para aguardar respostas de serviços externos como o Radarr/Sonarr. Defina como 0 para nenhum tempo limite.", "components.Settings.SettingsNetwork.validationApiRequestTimeout": "Deve fornecer um valor de tempo limite válido", - "components.Settings.SonarrModal.monitorNewItems": "Acompanhe as novas temporadas" + "components.Settings.SonarrModal.monitorNewItems": "Acompanhe as novas temporadas", + "components.Settings.Notifications.usePublicLogo": "", + "components.Settings.Notifications.usePublicLogoTip": "", + "components.Settings.Notifications.useUserLocale": "", + "components.Settings.RadarrModal.apiKeyHelp": "", + "components.Settings.RadarrModal.baseUrlHelp": "", + "components.Settings.RadarrModal.enableSearchHelp": "", + "components.Settings.RadarrModal.externalUrlHelp": "", + "components.Login.tipEmailHasTrailingWhitespace": "", + "components.Login.tipUsernameHasTrailingWhitespace": "", + "components.Settings.RadarrModal.server4kHelp": "", + "components.Settings.RadarrModal.syncEnabledHelp": "", + "components.Settings.SettingsMain.blocklistLanguage": "", + "components.Settings.SettingsMain.blocklistLanguageTip": "", + "components.Settings.SettingsMain.blocklistRegion": "", + "components.Settings.SettingsMain.blocklistRegionTip": "", + "components.Settings.SettingsUsers.disabledMediaServerLoginWarning": "", + "components.Settings.SonarrModal.apiKeyHelp": "", + "components.Settings.SonarrModal.baseUrlHelp": "", + "components.Settings.SonarrModal.externalUrlHelp": "", + "components.Settings.SonarrModal.monitorNewItemsHelp": "", + "components.Settings.SonarrModal.server4kHelp": "", + "components.Settings.SonarrModal.syncEnabledHelp": "", + "components.UserList.ascending": "", + "components.UserList.importedPlexUsersNoPassword": "", + "components.UserList.importedUsersNoPassword": "", + "components.UserList.sortBy": "", + "components.UserList.sortByJoined": "", + "components.UserList.sortByRequests": "", + "components.UserList.sortByRole": "", + "components.UserList.sortByType": "", + "components.UserList.sortByUser": "", + "components.UserList.toggleSortDirection": "", + "components.UserList.toggleSortDirectionAria": "", + "components.UserProfile.UserSettings.UserPasswordChange.localPasswordDescription": "", + "components.RequestCard.failedmodify": "", + "components.Settings.Notifications.NotificationsWebhook.customHeaders": "", + "i18n.notificationLocale": "", + "i18n.partiallyblocklisted": "", + "components.CollectionDetails.removefromblocklistpartialcount": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdAdd": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdPlaceholder": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdRemove": "", + "components.Discover.timeWindowWeek": "", + "components.Layout.UserWarnings.profileIncomplete": "", + "components.Login.emailAlreadyTaken": "", + "components.Login.jellyfinLocalLoginHint": "", + "components.Login.saveFailed": "", + "components.RequestList.RequestItem.failedmodify": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersAdd": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersRemove": "", + "components.Settings.SonarrModal.enableSearchHelp": "", + "components.Login.urlBaseHelp": "", + "i18n.error": "", + "components.Settings.Notifications.NotificationsNtfy.validationPriorityRequired": "", + "components.Discover.timeWindowDay": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersKey": "", + "components.UserList.descending": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersAuthConflict": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersIncomplete": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersTip": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersValue": "", + "components.Settings.Notifications.NotificationsNtfy.priority": "" } diff --git a/src/i18n/locale/ro.json b/src/i18n/locale/ro.json index 6d6d1592b8..fe363cea5f 100644 --- a/src/i18n/locale/ro.json +++ b/src/i18n/locale/ro.json @@ -966,5 +966,707 @@ "components.RequestList.unableToConnect": "Conexiunena cu {services} a eșuat. Unele informații ar putea deveni indisponibile.", "components.Settings.SettingsJobsCache.dnsNoCacheEntries": "Nici o căutare DNS nu a fost stocată în cache încă.", "i18n.removefromBlocklist": "Elimină din lista de materiale blocate", - "i18n.removeFromBlocklistSuccess": "{title}a fost eliminat cu succes din lista de materiale blocate." + "i18n.removeFromBlocklistSuccess": "{title}a fost eliminat cu succes din lista de materiale blocate.", + "components.Settings.Notifications.usePublicLogo": "", + "components.Settings.Notifications.usePublicLogoTip": "", + "components.Settings.Notifications.validationTypes": "", + "components.Settings.Notifications.validationWebhookRoleId": "", + "components.Settings.RadarrModal.apiKeyHelp": "", + "components.Settings.RadarrModal.baseUrlHelp": "", + "components.Settings.RadarrModal.enableSearchHelp": "", + "components.Settings.RadarrModal.externalUrlHelp": "", + "components.Settings.RadarrModal.syncEnabledHelp": "", + "components.Settings.SettingsJobsCache.jellyfin-recently-added-scan": "", + "components.Settings.SettingsJobsCache.misses": "", + "components.Settings.SettingsJobsCache.nextexecution": "", + "components.Settings.SettingsJobsCache.plex-full-scan": "", + "components.Settings.SettingsJobsCache.plex-watchlist-sync": "", + "components.Settings.SettingsJobsCache.process": "", + "components.Settings.SettingsJobsCache.process-blocklisted-tags": "", + "components.Settings.SettingsJobsCache.runnow": "", + "components.Settings.SettingsLogs.copiedLogMessage": "", + "components.Settings.SettingsLogs.copyToClipboard": "", + "components.Settings.SettingsLogs.extraData": "", + "components.Settings.SettingsLogs.filterDebug": "", + "components.Settings.SettingsLogs.filterError": "", + "components.Settings.SettingsLogs.filterWarn": "", + "components.Settings.SettingsMain.applicationTitle": "", + "components.Settings.SettingsMain.applicationurl": "", + "components.Settings.SettingsMain.blocklistLanguage": "", + "components.Settings.SettingsMain.blocklistLanguageTip": "", + "components.Settings.SettingsMain.blocklistRegion": "", + "components.Settings.SettingsMain.blocklistRegionTip": "", + "components.Settings.SettingsMain.blocklistedTags": "", + "components.Settings.SettingsMain.cacheImages": "", + "components.Settings.SettingsMain.generalsettings": "", + "components.Settings.SettingsMain.generalsettingsDescription": "", + "components.Settings.SettingsMain.hideAvailable": "", + "components.Settings.SettingsMain.hideBlocklisted": "", + "components.Settings.SettingsMain.hideBlocklistedTip": "", + "components.Settings.SettingsMain.originallanguageTip": "", + "components.Settings.SettingsMain.validationUrlTrailingSlash": "", + "components.Settings.SettingsMain.youtubeUrl": "", + "components.Settings.SettingsMain.youtubeUrlTip": "", + "components.Settings.SettingsNetwork.apiRequestTimeoutTip": "", + "components.Settings.SettingsNetwork.csrfProtection": "", + "components.Settings.SettingsNetwork.csrfProtectionTip": "", + "components.Settings.SettingsNetwork.dnsCache": "", + "components.Settings.SettingsNetwork.docs": "", + "components.Settings.SettingsNetwork.networksettings": "", + "components.Settings.SettingsNetwork.proxyBypassFilter": "", + "components.Settings.SettingsNetwork.proxyHostname": "", + "components.Settings.SettingsNetwork.proxyPassword": "", + "components.Settings.SettingsNetwork.proxyPort": "", + "components.Settings.SettingsNetwork.proxySsl": "", + "components.Settings.SettingsNetwork.proxyUser": "", + "components.Settings.SettingsNetwork.trustProxyTip": "", + "components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "", + "components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "", + "components.Settings.SettingsNetwork.validationProxyPort": "", + "components.Settings.SettingsUsers.atLeastOneAuth": "", + "components.Settings.SettingsUsers.defaultPermissions": "", + "components.Settings.SettingsUsers.disabledMediaServerLoginWarning": "", + "components.Settings.SonarrModal.animeSeriesType": "", + "components.Settings.SonarrModal.animeTags": "", + "components.Settings.SonarrModal.baseUrlHelp": "", + "components.Settings.SonarrModal.edit4ksonarr": "", + "components.Settings.SonarrModal.editsonarr": "", + "components.Settings.SonarrModal.enableSearch": "", + "components.Settings.SonarrModal.enableSearchHelp": "", + "components.Settings.SonarrModal.externalUrlHelp": "", + "components.Settings.SonarrModal.loadingTags": "", + "components.Settings.SonarrModal.loadinglanguageprofiles": "", + "components.Settings.SonarrModal.loadingprofiles": "", + "components.Settings.SonarrModal.loadingrootfolders": "", + "components.Settings.SonarrModal.monitorNewItemsHelp": "", + "components.Settings.SonarrModal.notagoptions": "", + "components.Settings.SonarrModal.port": "", + "components.Settings.SonarrModal.qualityprofile": "", + "components.Settings.SonarrModal.syncEnabledHelp": "", + "components.Settings.SonarrModal.validationApplicationUrlTrailingSlash": "", + "components.Settings.SonarrModal.validationBaseUrlLeadingSlash": "", + "components.Settings.SonarrModal.validationBaseUrlTrailingSlash": "", + "components.Settings.SonarrModal.validationHostnameRequired": "", + "components.Settings.SonarrModal.validationPortRequired": "", + "components.Settings.SonarrModal.validationProfileRequired": "", + "components.Settings.blocklistedTagsText": "", + "components.Settings.clearBlocklistedTagsConfirm": "", + "components.Settings.jellyfinSettingsDescription": "", + "components.Settings.jellyfinSettingsFailure": "", + "components.Settings.jellyfinSyncFailedAutomaticGroupedFolders": "", + "components.Settings.jellyfinSyncFailedGenericError": "", + "components.Settings.jellyfinSyncFailedNoLibrariesFound": "", + "components.Settings.jellyfinlibraries": "", + "components.Settings.jellyfinlibrariesDescription": "", + "components.Settings.jellyfinsettingsDescription": "", + "components.Settings.manualscanDescriptionJellyfin": "", + "components.Settings.manualscanJellyfin": "", + "components.Settings.mediaTypeSeries": "", + "components.Settings.menuAbout": "", + "components.Settings.menuGeneralSettings": "", + "components.Settings.menuJellyfinSettings": "", + "components.Settings.menuServices": "", + "components.Settings.menuUsers": "", + "components.Settings.metadataProviderSelection": "", + "components.Settings.metadataProviderSettings": "", + "components.Settings.noDefault4kServer": "", + "components.Settings.noDefaultNon4kServer": "", + "components.Settings.noDefaultServer": "", + "components.Settings.noSpecialCharacters": "", + "components.Settings.notificationsettings": "", + "components.Settings.notrunning": "", + "components.Settings.overrideRules": "", + "components.Settings.plexsettingsDescription": "", + "components.Settings.providerStatus": "", + "components.Settings.radarrsettings": "", + "components.Settings.restartrequiredTooltip": "", + "components.Settings.save": "", + "components.Settings.saving": "", + "components.Settings.scanbackground": "", + "components.Settings.scanning": "", + "components.Settings.searchKeywords": "", + "components.Settings.serverLocal": "", + "components.Settings.serverpreset": "", + "components.Settings.serverpresetLoad": "", + "components.Settings.serverpresetManualMessage": "", + "components.Settings.serverpresetRefreshing": "", + "components.Settings.serviceSettingsDescription": "", + "components.Settings.services": "", + "components.Settings.settingUpPlexDescription": "", + "components.Settings.settings": "", + "components.Settings.sonarrsettings": "", + "components.Settings.starttyping": "", + "components.Settings.syncJellyfin": "", + "components.Settings.timeout": "", + "components.Settings.toastPlexRefresh": "", + "components.Settings.toastTautulliSettingsSuccess": "", + "components.Settings.tvdbProviderDoesnotWork": "", + "components.Settings.urlBase": "", + "components.Settings.validationHostnameRequired": "", + "components.Settings.validationPortRequired": "", + "components.Settings.validationUrl": "", + "components.Settings.validationUrlBaseLeadingSlash": "", + "components.Settings.validationUrlBaseTrailingSlash": "", + "components.Settings.validationUrlTrailingSlash": "", + "components.Settings.valueRequired": "", + "components.Setup.continue": "", + "components.Setup.finish": "", + "components.Setup.finishing": "", + "components.Setup.librarieserror": "", + "components.Setup.servertype": "", + "components.Setup.setup": "", + "components.Setup.signin": "", + "components.StatusBadge.managemedia": "", + "components.StatusBadge.openinarr": "", + "components.StatusBadge.playonplex": "", + "components.StatusBadge.seasonepisodenumber": "", + "components.StatusBadge.seasonnumber": "", + "components.StatusBadge.status": "", + "components.StatusChecker.appUpdatedDescription": "", + "components.StatusChecker.restartRequired": "", + "components.StatusChecker.restartRequiredDescription": "", + "components.TvDetails.anime": "", + "components.TvDetails.cast": "", + "components.TvDetails.episodeRuntime": "", + "components.TvDetails.episodeRuntimeMinutes": "", + "components.TvDetails.firstAirDate": "", + "components.TvDetails.manageseries": "", + "components.TvDetails.originallanguage": "", + "components.TvDetails.originaltitle": "", + "components.TvDetails.overview": "", + "components.TvDetails.play": "", + "components.TvDetails.play4k": "", + "components.TvDetails.productioncountries": "", + "components.TvDetails.recommendations": "", + "components.TvDetails.reportissue": "", + "components.TvDetails.rtaudiencescore": "", + "components.TvDetails.seasonnumber": "", + "components.TvDetails.similar": "", + "components.TvDetails.status4k": "", + "components.TvDetails.streamingproviders": "", + "components.TvDetails.tmdbuserscore": "", + "components.TvDetails.viewfullcrew": "", + "components.TvDetails.watchlistDeleted": "", + "components.TvDetails.watchlistError": "", + "components.TvDetails.watchlistSuccess": "", + "components.TvDetails.watchtrailer": "", + "components.UserList.ascending": "", + "components.UserList.autogeneratepassword": "", + "components.UserList.create": "", + "components.UserList.created": "", + "components.UserList.createlocaluser": "", + "components.UserList.creating": "", + "components.UserList.deleteconfirm": "", + "components.UserList.deleteuser": "", + "components.UserList.edituser": "", + "components.UserList.email": "", + "components.UserList.importedPlexUsersNoPassword": "", + "components.UserList.importedUsersNoPassword": "", + "components.UserList.importedfromJellyfin": "", + "components.UserList.importedfromplex": "", + "components.UserList.importfromJellyfin": "", + "components.UserList.importfromJellyfinerror": "", + "components.UserList.importfrommediaserver": "", + "components.UserList.importfromplex": "", + "components.UserList.importfromplexerror": "", + "components.UserList.passwordinfodescription": "", + "components.UserList.plexuser": "", + "components.UserList.role": "", + "components.UserList.sortBy": "", + "components.UserList.sortByJoined": "", + "components.UserList.sortByRole": "", + "components.UserList.sortByUser": "", + "components.UserList.toggleSortDirection": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorUnauthorized": "", + "components.UserProfile.UserSettings.UserGeneralSettings.applanguage": "", + "components.UserProfile.UserSettings.UserGeneralSettings.streamingRegionTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.emailsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.emailsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessTokenTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverApplicationToken": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverApplicationTokenTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverUserKeyTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.sendSilently": "", + "components.UserProfile.UserSettings.UserNotificationSettings.sound": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramChatId": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramChatIdTipLong": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadId": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPushbulletAccessToken": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverApplicationToken": "", + "components.UserProfile.UserSettings.UserNotificationSettings.webpush": "", + "components.UserProfile.UserSettings.UserPasswordChange.localPasswordDescription": "", + "components.UserProfile.totalrequests": "", + "components.UserProfile.unlimited": "", + "i18n.advanced": "", + "i18n.approve": "", + "i18n.approved": "", + "i18n.areyousure": "", + "i18n.available": "", + "i18n.blocklist": "", + "i18n.blocklistDuplicateError": "", + "i18n.blocklistError": "", + "i18n.declined": "", + "i18n.deleting": "", + "i18n.delimitedlist": "", + "i18n.edit": "", + "i18n.error": "", + "i18n.experimental": "", + "i18n.failed": "", + "i18n.import": "", + "i18n.loading": "", + "i18n.notificationLocale": "", + "i18n.partiallyblocklisted": "", + "components.RequestCard.failedmodify": "", + "components.RequestList.RequestItem.failedmodify": "", + "components.Settings.SettingsJobsCache.unknownJob": "", + "components.Settings.SettingsJobsCache.usersavatars": "", + "components.Settings.SettingsNetwork.csrfProtectionHoverTip": "", + "components.TitleCard.cleardata": "", + "components.UserList.password": "", + "components.Settings.Notifications.NotificationsWebPush.embedPoster": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersAuthConflict": "", + "components.Settings.Notifications.useUserLocale": "", + "components.Settings.Notifications.validationMessageThreadId": "", + "components.Settings.RadarrModal.server4kHelp": "", + "components.Settings.SettingsJobsCache.jobScheduleEditFailed": "", + "components.Settings.SettingsJobsCache.jobScheduleEditSaved": "", + "components.Settings.SettingsJobsCache.jobcancelled": "", + "components.Settings.SettingsJobsCache.jobname": "", + "components.Settings.SettingsJobsCache.radarr-scan": "", + "components.Settings.SettingsJobsCache.size": "", + "components.Settings.SettingsJobsCache.sonarr-scan": "", + "components.Settings.SettingsLogs.pauseLogs": "", + "components.Settings.SettingsLogs.resumeLogs": "", + "components.Settings.SettingsLogs.showall": "", + "components.Settings.SettingsLogs.time": "", + "components.Settings.SettingsMain.apikey": "", + "components.Settings.SettingsMain.apikeyCopied": "", + "components.Settings.SettingsMain.blocklistedTagsLimitTip": "", + "components.Settings.SettingsNetwork.validationApiRequestTimeout": "", + "components.Settings.SonarrModal.server4kHelp": "", + "components.Settings.advancedTooltip": "", + "components.Settings.allChosenProvidersAreOperational": "", + "components.Settings.blocklistedTagImportInstructions": "", + "components.Settings.overrideRulesDescription": "", + "components.Settings.plex": "", + "components.Settings.plexlibraries": "", + "components.Settings.ssl": "", + "components.Settings.toastPlexRefreshFailure": "", + "components.Settings.toastTautulliSettingsFailure": "", + "components.Settings.webAppUrl": "", + "components.Settings.webAppUrlTip": "", + "components.Settings.webpush": "", + "components.Settings.yes": "", + "components.Setup.back": "", + "components.Setup.configemby": "", + "components.Setup.configjellyfin": "", + "components.Setup.configplex": "", + "components.Setup.configuremediaserver": "", + "components.Setup.configureservices": "", + "components.Setup.signinMessage": "", + "components.Setup.subtitle": "", + "components.TitleCard.addToWatchList": "", + "components.TitleCard.mediaerror": "", + "components.TitleCard.watchlistError": "", + "components.TitleCard.watchlistSuccess": "", + "components.TvDetails.Season.noepisodes": "", + "components.TvDetails.Season.somethingwentwrong": "", + "components.TvDetails.TvCast.fullseriescast": "", + "components.TvDetails.TvCrew.fullseriescrew": "", + "components.TvDetails.episodeCount": "", + "components.TvDetails.nextAirDate": "", + "components.UserList.localLoginDisabled": "", + "components.UserList.localuser": "", + "components.UserList.toggleSortDirectionAria": "", + "components.UserList.user": "", + "components.UserList.usercreatedfailed": "", + "components.UserList.usercreatedfailedexisting": "", + "components.UserList.userdeleteerror": "", + "components.UserList.userfail": "", + "components.UserList.username": "", + "components.UserList.userssaved": "", + "components.UserList.validationEmail": "", + "components.UserList.validationUsername": "", + "components.UserList.validationpasswordminchars": "", + "components.UserProfile.ProfileHeader.profile": "", + "components.UserProfile.ProfileHeader.settings": "", + "components.UserProfile.ProfileHeader.userid": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorExists": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorUnknown": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.password": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.passwordRequired": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.save": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.saving": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.title": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.usernameRequired": "", + "components.UserProfile.UserSettings.UserGeneralSettings.admin": "", + "components.UserProfile.UserSettings.UserGeneralSettings.displayName": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexuser": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.browser": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.created": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablingwebpusherror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.type": "", + "components.UserProfile.UserSettings.UserNotificationSettings.notifications": "", + "components.UserProfile.UserSettings.UserNotificationSettings.notificationsettings": "", + "i18n.back": "", + "i18n.blocklistSuccess": "", + "i18n.blocklisted": "", + "i18n.cancel": "", + "i18n.collection": "", + "i18n.completed": "", + "i18n.decline": "", + "components.Settings.port": "", + "components.Settings.Notifications.validationUrl": "", + "components.Login.tipUsernameHasTrailingWhitespace": "", + "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegionTip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.languageDefault": "", + "components.UserProfile.UserSettings.UserGeneralSettings.localuser": "", + "components.UserProfile.UserSettings.UserGeneralSettings.mediaServerUser": "", + "components.UserProfile.UserSettings.UserGeneralSettings.originallanguageTip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmoviestip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseries": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseriestip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.regionTip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.role": "", + "components.UserProfile.UserSettings.UserGeneralSettings.save": "", + "components.UserProfile.UserSettings.UserGeneralSettings.saving": "", + "components.UserProfile.UserSettings.UserGeneralSettings.seriesrequestlimit": "", + "components.UserProfile.UserSettings.UserGeneralSettings.streamingRegion": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmailEmpty": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsSuccess": "", + "components.UserProfile.UserSettings.UserGeneralSettings.user": "", + "components.UserProfile.UserSettings.UserGeneralSettings.validationemailformat": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.linkedAccountsHint": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.noPermissionDescription": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorExists": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorUnauthorized": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleteerror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordId": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdAdd": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdPlaceholder": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdRemove": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "", + "components.Layout.UserWarnings.profileIncomplete": "", + "components.Login.emailAlreadyTaken": "", + "components.Login.jellyfinLocalLoginHint": "", + "components.Login.saveFailed": "", + "components.Settings.Notifications.NotificationsNtfy.validationPriorityRequired": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersValue": "", + "components.Settings.Notifications.chatIdTip": "", + "components.Settings.Notifications.discordsettingsfailed": "", + "components.Settings.Notifications.emailsettingssaved": "", + "components.Settings.Notifications.embedPoster": "", + "components.Settings.Notifications.sendSilently": "", + "components.Settings.Notifications.sendSilentlyTip": "", + "components.Settings.Notifications.smtpHost": "", + "components.Settings.SettingsNetwork.networksettingsDescription": "", + "components.Settings.SonarrModal.add": "", + "components.Settings.clickTest": "", + "components.Settings.copyBlocklistedTags": "", + "components.UserList.mediaServerUser": "", + "components.UserList.newJellyfinsigninenabled": "", + "components.UserList.noJellyfinuserstoimport": "", + "components.UserList.nouserstoimport": "", + "components.UserList.owner": "", + "components.UserList.sortByRequests": "", + "components.UserProfile.UserSettings.UserGeneralSettings.email": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pgpPublicKey": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pgpPublicKeyTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessToken": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverUserKey": "", + "components.UserProfile.seriesrequest": "", + "pages.serviceunavailable": "", + "components.Login.urlBaseHelp": "", + "components.Settings.Notifications.discordsettingssaved": "", + "components.Settings.Notifications.emailsender": "", + "components.Settings.Notifications.emailsettingsfailed": "", + "components.Settings.SettingsMain.blocklistedTagsLimit": "", + "components.Settings.SonarrModal.apiKeyHelp": "", + "components.Settings.copyBlocklistedTagsEmpty": "", + "components.Settings.manualscan": "", + "components.Settings.plexlibrariesDescription": "", + "components.Settings.plexsettings": "", + "components.Settings.toastPlexRefreshSuccess": "", + "components.TitleCard.tmdbid": "", + "components.TitleCard.tvdbid": "", + "components.TitleCard.watchlistCancel": "", + "components.TitleCard.watchlistDeleted": "", + "components.TvDetails.removefromwatchlist": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.username": "", + "components.UserProfile.UserSettings.UserNotificationSettings.email": "", + "components.TvDetails.addtowatchlist": "", + "components.Settings.SettingsNetwork.trustProxy": "", + "components.Settings.animeMetadataProvider": "", + "components.Settings.seriesMetadataProvider": "", + "components.TvDetails.overviewunavailable": "", + "i18n.importing": "", + "pages.somethingwentwrong": "", + "i18n.addToBlocklist": "", + "components.Settings.activeProfile": "", + "components.StatusBadge.status4k": "", + "components.Settings.tip": "", + "components.UserList.users": "", + "components.UserList.descending": "", + "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegion": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersAdd": "", + "components.Settings.SettingsLogs.viewdetails": "", + "components.Settings.SonarrModal.default4kserver": "", + "components.Settings.connectionTestFailed": "", + "components.Settings.metadataSettings": "", + "components.Settings.validationApiKey": "", + "components.UserList.totalrequests": "", + "components.UserProfile.UserSettings.UserNotificationSettings.deviceDefault": "", + "components.Login.tipEmailHasTrailingWhitespace": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersIncomplete": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersKey": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersRemove": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersTip": "", + "components.Settings.Notifications.enableMentions": "", + "components.Settings.Notifications.encryption": "", + "components.Settings.Notifications.encryptionDefault": "", + "components.Settings.Notifications.encryptionNone": "", + "components.Settings.Notifications.encryptionOpportunisticTls": "", + "components.Settings.Notifications.encryptionTip": "", + "components.Settings.Notifications.messageThreadId": "", + "components.Settings.Notifications.messageThreadIdTip": "", + "components.Settings.Notifications.pgpPassword": "", + "components.Settings.Notifications.pgpPasswordTip": "", + "components.Settings.Notifications.pgpPrivateKey": "", + "components.Settings.Notifications.pgpPrivateKeyTip": "", + "components.Settings.SettingsMain.blocklistedTagsTip": "", + "components.Settings.SettingsMain.discoverRegion": "", + "components.Settings.SettingsMain.discoverRegionTip": "", + "components.Settings.SettingsMain.enableSpecialEpisodes": "", + "components.Settings.SettingsMain.partialRequestsEnabled": "", + "components.Settings.SettingsMain.streamingRegion": "", + "components.Settings.SettingsMain.streamingRegionTip": "", + "components.Settings.SettingsMain.toastApiKeyFailure": "", + "components.Settings.SettingsMain.toastApiKeySuccess": "", + "components.Settings.SettingsMain.toastSettingsFailure": "", + "components.Settings.SettingsMain.toastSettingsSuccess": "", + "components.Settings.SettingsMain.validationApplicationTitle": "", + "components.Settings.SettingsMain.validationApplicationUrl": "", + "components.Settings.SettingsMain.validationUrl": "", + "components.Settings.SettingsNetwork.apiRequestTimeout": "", + "components.Settings.SettingsNetwork.dnsCacheForceMaxTtl": "", + "components.Settings.SettingsNetwork.dnsCacheForceMinTtl": "", + "components.Settings.SettingsNetwork.toastSettingsFailure": "", + "components.Settings.SettingsNetwork.toastSettingsSuccess": "", + "components.Settings.SettingsNetwork.dnsCacheHoverTip": "", + "components.Settings.SettingsNetwork.dnsCacheTip": "", + "components.Settings.SettingsNetwork.forceIpv4First": "", + "components.Settings.SettingsNetwork.forceIpv4FirstTip": "", + "components.Settings.SettingsNetwork.network": "", + "components.Settings.SettingsNetwork.networkDisclaimer": "", + "components.Settings.SettingsNetwork.proxyBypassFilterTip": "", + "components.Settings.SettingsNetwork.proxyBypassLocalAddresses": "", + "components.Settings.SettingsNetwork.proxyEnabled": "", + "components.Settings.SettingsNetwork.proxyEnabledTip": "", + "components.Settings.SettingsUsers.defaultPermissionsTip": "", + "components.Settings.SettingsUsers.localLogin": "", + "components.Settings.SettingsUsers.localLoginTip": "", + "components.Settings.SettingsUsers.loginMethods": "", + "components.Settings.SettingsUsers.loginMethodsTip": "", + "components.Settings.SettingsUsers.mediaServerLogin": "", + "components.Settings.SettingsUsers.mediaServerLoginTip": "", + "components.Settings.SettingsUsers.movieRequestLimitLabel": "", + "components.Settings.SettingsUsers.newPlexLogin": "", + "components.Settings.SettingsUsers.newPlexLoginTip": "", + "components.Settings.SettingsUsers.toastSettingsFailure": "", + "components.Settings.SettingsUsers.toastSettingsSuccess": "", + "components.Settings.SettingsUsers.tvRequestLimitLabel": "", + "components.Settings.SettingsUsers.userSettings": "", + "components.Settings.SettingsUsers.userSettingsDescription": "", + "components.Settings.SettingsUsers.users": "", + "components.Settings.SonarrModal.animelanguageprofile": "", + "components.Settings.SonarrModal.animequalityprofile": "", + "components.Settings.SonarrModal.animerootfolder": "", + "components.Settings.SonarrModal.apiKey": "", + "components.Settings.SonarrModal.create4ksonarr": "", + "components.Settings.SonarrModal.createsonarr": "", + "components.Settings.SonarrModal.defaultserver": "", + "components.Settings.SonarrModal.languageprofile": "", + "components.Settings.SonarrModal.rootfolder": "", + "components.Settings.SonarrModal.seasonfolders": "", + "components.Settings.SonarrModal.selectLanguageProfile": "", + "components.Settings.SonarrModal.selectQualityProfile": "", + "components.Settings.SonarrModal.selecttags": "", + "components.Settings.SonarrModal.seriesType": "", + "components.Settings.SonarrModal.server4k": "", + "components.Settings.SonarrModal.ssl": "", + "components.Settings.SonarrModal.syncEnabled": "", + "components.Settings.SonarrModal.tagRequests": "", + "components.Settings.SonarrModal.tagRequestsInfo": "", + "components.Settings.SonarrModal.tags": "", + "components.Settings.SonarrModal.testFirstLanguageProfiles": "", + "components.Settings.SonarrModal.testFirstQualityProfiles": "", + "components.Settings.SonarrModal.testFirstRootFolders": "", + "components.Settings.SonarrModal.toastSonarrTestSuccess": "", + "components.Settings.SonarrModal.validationApplicationUrl": "", + "components.Settings.SonarrModal.validationLanguageProfileRequired": "", + "components.Settings.SonarrModal.validationNameRequired": "", + "components.Settings.addradarr": "", + "components.Settings.address": "", + "components.Settings.addrule": "", + "components.Settings.addsonarr": "", + "components.Settings.cancelscan": "", + "components.Settings.chooseProvider": "", + "components.Settings.copyBlocklistedTagsTip": "", + "components.Settings.currentlibrary": "", + "components.Settings.default": "", + "components.Settings.default4k": "", + "components.Settings.deleteServer": "", + "components.Settings.menuLogs": "", + "components.Settings.deleteserverconfirm": "", + "components.Settings.email": "", + "components.Settings.enablessl": "", + "components.Settings.failedToSaveMetadataSettings": "", + "components.Settings.hostname": "", + "components.Settings.importBlocklistedTagsTip": "", + "components.Settings.invalidKeyword": "", + "components.Settings.invalidurlerror": "", + "components.Settings.jellyfinSettings": "", + "components.Settings.jellyfinsettings": "", + "components.Settings.librariesRemaining": "", + "components.Settings.menuJobs": "", + "components.Settings.menuMetadataProviders": "", + "components.Settings.menuNetwork": "", + "components.Settings.menuNotifications": "", + "components.Settings.menuPlexSettings": "", + "components.Settings.tautulliSettingsDescription": "", + "components.Settings.tmdbProviderDoesnotWork": "", + "components.Settings.metadataSettingsSaved": "", + "components.Settings.no": "", + "components.Settings.nooptions": "", + "components.Settings.notTested": "", + "components.Settings.notificationAgentSettingsDescription": "", + "components.Settings.notifications": "", + "components.Settings.scan": "", + "components.Settings.startscan": "", + "components.Settings.syncing": "", + "components.Settings.tautulliApiKey": "", + "components.Settings.tautulliSettings": "", + "components.Settings.toastPlexConnecting": "", + "components.Settings.toastPlexConnectingFailure": "", + "components.Settings.toastPlexConnectingSuccess": "", + "components.Setup.signinWithEmby": "", + "components.Setup.signinWithJellyfin": "", + "components.Setup.signinWithPlex": "", + "components.StatusChecker.appUpdated": "", + "components.StatusChecker.reloadApp": "", + "components.TvDetails.seasonstitle": "", + "components.TvDetails.showtype": "", + "components.UserList.autogeneratepasswordTip": "", + "components.UserList.bulkedit": "", + "components.UserList.newplexsigninenabled": "", + "components.UserList.sortByType": "", + "components.UserList.usercreatedsuccess": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.description": "", + "components.UserProfile.UserSettings.UserGeneralSettings.accounttype": "", + "components.UserProfile.UserSettings.UserGeneralSettings.enableOverride": "", + "components.UserProfile.UserSettings.UserGeneralSettings.general": "", + "components.UserProfile.UserSettings.UserGeneralSettings.generalsettings": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailure": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmail": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.deleteFailed": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.errorUnknown": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.linkedAccounts": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.deletesubscription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.device": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablewebpush": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablewebpush": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.engine": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.managedevices": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.nodevicestoshow": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.operatingsystem": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleted": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.unknown": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeendisabled": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeenenabled": "", + "components.UserProfile.UserSettings.UserGeneralSettings.owner": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoversettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.sendSilentlyDescription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadIdTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPgpPublicKey": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramChatId": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramMessageThreadId": "", + "components.UserList.accounttype": "", + "components.Settings.webhook": "", + "components.Discover.timeWindowDay": "", + "components.Discover.timeWindowWeek": "", + "components.UserProfile.ProfileHeader.joindate": "", + "components.Settings.jellyfinSettingsSuccess": "", + "components.Settings.SettingsJobsCache.jobsDescription": "", + "components.Settings.SettingsJobsCache.jobsandcache": "", + "components.Settings.SettingsJobsCache.jobstarted": "", + "components.Settings.SettingsJobsCache.jobs": "", + "components.Settings.SettingsJobsCache.jobtype": "", + "components.Settings.SettingsLogs.filterInfo": "", + "components.Settings.SettingsLogs.label": "", + "components.Settings.SettingsLogs.level": "", + "components.Settings.SettingsLogs.logDetails": "", + "components.Settings.SettingsLogs.logs": "", + "components.Settings.SettingsLogs.logsDescription": "", + "components.Settings.SettingsLogs.message": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmovies": "", + "components.Settings.SettingsJobsCache.plex-recently-added-scan": "", + "components.Settings.SettingsJobsCache.plex-refresh-token": "", + "components.Settings.SettingsMain.cacheImagesTip": "", + "components.Settings.SonarrModal.externalUrl": "", + "components.Settings.SonarrModal.hostname": "", + "components.Settings.SonarrModal.testFirstTags": "", + "components.Settings.SonarrModal.toastSonarrTestFailure": "", + "components.Settings.apiKey": "", + "components.Settings.SonarrModal.validationApiKeyRequired": "", + "components.Settings.SonarrModal.validationRootFolderRequired": "", + "components.Settings.blocklistedTagImportTitle": "", + "components.Settings.experimentalTooltip": "", + "components.Settings.failed": "", + "components.Settings.general": "", + "components.Settings.jellyfinForgotPasswordUrl": "", + "components.Settings.manualscanDescription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablingwebpusherror": "", + "components.Settings.operational": "", + "components.Setup.welcome": "", + "components.TvDetails.seasons": "", + "i18n.delete": "", + "i18n.deleted": "", + "components.Settings.SettingsMain.hideAvailableTip": "", + "components.Settings.SonarrModal.monitorNewItems": "", + "components.TvDetails.rtcriticsscore": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.noLinkedAccounts": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoversettingsfailed": "", + "components.Settings.externalUrl": "", + "components.UserList.userlist": "", + "components.Settings.is4k": "", + "components.UserList.admin": "", + "components.Settings.SonarrModal.baseUrl": "", + "components.UserList.userdeleted": "", + "components.CollectionDetails.removefromblocklistpartialcount": "", + "components.Settings.Notifications.NotificationsNtfy.priority": "", + "components.Settings.Notifications.NotificationsWebhook.customHeaders": "", + "i18n.close": "", + "components.Settings.serverRemote": "", + "components.Settings.serverSecure": "", + "components.Settings.Notifications.encryptionImplicitTls": "", + "components.Settings.SettingsMain.general": "", + "components.Settings.SettingsMain.validationApplicationUrlTrailingSlash": "", + "components.Settings.SonarrModal.servername": "", + "components.UserProfile.UserSettings.UserGeneralSettings.validationemailrequired": "", + "i18n.all": "", + "i18n.canceling": "", + "i18n.next": "", + "components.Settings.SettingsMain.locale": "", + "components.Settings.SonarrModal.selectRootFolder": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverUserKey": "" } diff --git a/src/i18n/locale/ru.json b/src/i18n/locale/ru.json index ce024733e7..203c0e5e23 100644 --- a/src/i18n/locale/ru.json +++ b/src/i18n/locale/ru.json @@ -628,7 +628,7 @@ "components.UserProfile.UserSettings.UserPasswordChange.validationNewPassword": "Вы должны ввести новый пароль", "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramChatId": "Вы должны предоставить действительный ID чата", "components.UserProfile.UserSettings.UserNotificationSettings.validationPgpPublicKey": "Вы должны предоставить действительный открытый ключ PGP", - "components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "Вы должны предоставить действительный ID пользователя", + "components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "Каждый ID должен быть корректным ID пользователя в Discord", "components.UserList.validationEmail": "Email обязателен", "components.UserList.usercreatedfailedexisting": "Указанный адрес электронной почты уже используется другим пользователем.", "components.TvDetails.streamingproviders": "Сейчас транслируется", @@ -717,8 +717,8 @@ "components.UserProfile.UserSettings.UserNotificationSettings.email": "Электронная почта", "components.UserProfile.UserSettings.UserNotificationSettings.discordsettingssaved": "Настройки уведомлений Discord успешно сохранены!", "components.UserProfile.UserSettings.UserNotificationSettings.discordsettingsfailed": "Не удалось сохранить настройки уведомлений Discord.", - "components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "ID вашей учётной записи", - "components.UserProfile.UserSettings.UserNotificationSettings.discordId": "ID пользователя", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "ID вашей учётной записи. Для нескольких учетных записей в одной семье можно добавить более одного Discord ID.", + "components.UserProfile.UserSettings.UserNotificationSettings.discordId": "ID пользователей", "components.UserProfile.UserSettings.UserGeneralSettings.accounttype": "Тип учётной записи", "components.UserProfile.ProfileHeader.userid": "ID пользователя: {userid}", "components.UserProfile.ProfileHeader.settings": "Редактировать настройки", @@ -1040,8 +1040,6 @@ "components.Settings.SettingsJobsCache.editJobScheduleSelectorSeconds": "Каждую {jobScheduleSeconds, plural, one {секунду} few {секунды} other {{jobScheduleSeconds} секунд}}", "components.StatusChecker.restartRequiredDescription": "Пожалуйста, перезапустите сервер, чтобы применить обновленные настройки.", "components.TvDetails.episodeCount": "{episodeCount, plural, one {# эпизод} few {# эпизода} other {# эпизодов}}", - "components.UserProfile.UserSettings.UserGeneralSettings.discordId": "ID пользователя Discord", - "components.UserProfile.UserSettings.UserGeneralSettings.validationDiscordId": "Вы должны предоставить действительный идентификатор пользователя Discord", "components.PermissionEdit.viewwatchlistsDescription": "Предоставьте разрешение на просмотр списков просмотра Plex других пользователей.", "components.UserList.newplexsigninenabled": "Параметр Включить новый вход в Plex в настоящее время включен. Пользователей Plex с доступом к библиотеке не нужно импортировать для входа.", "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmovies": "Автоматический запрос фильмов", @@ -1152,7 +1150,6 @@ "components.TvDetails.Season.somethingwentwrong": "Что-то пошло не так при получении данных о сериале.", "components.TvDetails.rtaudiencescore": "Рейтинг пользователей Rotten Tomatoes", "components.TvDetails.rtcriticsscore": "Rotten Tomatoes томатометр", - "components.UserProfile.UserSettings.UserGeneralSettings.discordIdTip": "Многозначный идентификационный номер, связанный с вашей учетной записью пользователя Discord", "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmoviestip": "Автоматически запрашивать фильмы из вашего списка просмотра Plex", "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseries": "Автоматический запрос сериалов", "components.UserProfile.plexwatchlist": "Список просмотра Plex", @@ -1666,5 +1663,45 @@ "components.UserList.importedPlexUsersNoPassword": "Импортированные пользователи не имеют пароля. Если вы выключите вход через Plex, им придётся установить пароль в профиле или через ссылку для сброса пароля.", "components.UserList.importedUsersNoPassword": "Импортированные пользователи не имеют пароля для {applicationTitle}. Если вы выключите вход через {mediaServerName}, им нужно будет установить пароль в профиле или через ссылку для сброса пароля.", "components.UserProfile.UserSettings.UserPasswordChange.localPasswordDescription": "Этот пароль будет использоваться для локального входа в {applicationTitle}. Он не связан с паролем от вашего медиа сервера.", - "i18n.notificationLocale": "Язык уведомлений" + "i18n.notificationLocale": "Язык уведомлений", + "components.Settings.Notifications.usePublicLogo": "Использовать логотип Seerr вместо пользовательского логотипа", + "components.Settings.Notifications.usePublicLogoTip": "Если ваш экземпляр Seerr не доступен публично, включите эту опцию, чтобы клиенты электронной почты вне домашней сети могли отобразить изображение. Изображение будет взято из публичного GitHub репозитория.", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdAdd": "Добавить ID пользователя", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdPlaceholder": "ID пользователя Discord", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdRemove": "Удалить", + "components.Settings.Notifications.validationWebhookThreadId": "Вы должны указать действительный ID треда Discord", + "components.Settings.Notifications.webhookThreadId": "ID треда", + "components.Settings.Notifications.webhookThreadIdTip": "ID канала треда, в который будут отправляться уведомления. Оставьте пустым, чтобы отправлять в канал вебхука", + "components.UserProfile.UserSettings.UserNotificationSettings.discordNotificationsNotEnabled": "Владелец сервера не включил уведомления Discord. Эта информация будет использована только в том случае, если владелец сервера настроит внешнюю службу.", + "i18n.sync": "Синхронизировать", + "i18n.syncing": "Синхронизация…", + "components.Common.QuickConnectModal.error": "Ошибка", + "components.Common.QuickConnectModal.expired": "Срок действия кода истёк", + "components.Common.QuickConnectModal.expiredMessage": "Срок кода Быстрого Подключения истёк. Пожалуйста, попробуйте ещё раз.", + "components.Common.QuickConnectModal.tryAgain": "Попробуйте ещё раз", + "components.Common.QuickConnectModal.waitingForAuth": "Ожидание авторизации...", + "components.Login.JellyfinQuickConnectModal.cancel": "Отмена", + "components.Login.JellyfinQuickConnectModal.instructions": "Введите этот код в ваше {mediaServerName} приложение", + "components.Login.JellyfinQuickConnectModal.subtitle": "Войти с помощью Быстрого Подключения", + "components.Login.JellyfinQuickConnectModal.title": "Быстрое Подключение", + "components.Login.quickconnect": "Быстрое Подключение", + "components.Login.quickconnecterror": "Быстрое Подключение не удалось. Пожалуйста, попробуйте ещё раз.", + "components.ManageSlideOver.clearmediadataerror": "Произошла ошибка при очистке медиаданных.", + "components.ManageSlideOver.removemediaerror": "Произошла ошибка при удалении медиа.", + "components.RequestList.RequestItem.removemediaerror": "Произошла ошибка при удалении медиа.", + "components.RequestModal.AdvancedRequester.ignoreQuotaDescription": "Этот запрос не будет учитываться в рамках квоты пользователя. Используйте с осторожностью.", + "components.RequestModal.AdvancedRequester.ignoreQuotaTitle": "Не использовать лимиты пользователя", + "components.Settings.SettingsAbout.versionCheckDisabled": "Проверка версии отключена", + "components.Settings.SettingsMain.versionCheck": "Проверка версии", + "components.Settings.SettingsMain.versionCheckTip": "Автоматическая проверка наличия новых версий на GitHub.", + "components.UserList.syncnotice": "Вы по-прежнему можете нажать кнопку «Синхронизация», чтобы обновить данные существующих пользователей Plex, например, адрес электронной почты или имя пользователя.", + "components.UserProfile.UserSettings.LinkJellyfinModal.quickConnect": "Использовать Быстрое Подключение", + "components.UserProfile.UserSettings.LinkJellyfinQuickConnectModal.instructions": "Введите этот код в ваше {mediaServerName} приложение", + "components.UserProfile.UserSettings.LinkJellyfinQuickConnectModal.subtitle": "Быстрое Подключение", + "components.UserProfile.UserSettings.LinkJellyfinQuickConnectModal.title": "Привязать аккаунт {mediaServerName}", + "components.UserProfile.UserSettings.LinkJellyfinQuickConnectModal.usePassword": "Использовать пароль", + "hooks.useQuickConnect.errorMessage": "Не удалось запустить функцию Быстрого Подключения. Пожалуйста, попробуйте еще раз.", + "components.Settings.Notifications.NotificationsGotify.embedPoster": "Добавлять постер", + "components.UserList.importfromplexnochanges": "Импорт из Plex завершён. Новых либо обновлённых пользователей не было.", + "components.UserList.importfromplexsynced": "Пользователи Plex успешно синхронизированы. Существующие пользователи были обновлены." } diff --git a/src/i18n/locale/sk.json b/src/i18n/locale/sk.json index 0967ef424b..092e3b0617 100644 --- a/src/i18n/locale/sk.json +++ b/src/i18n/locale/sk.json @@ -1 +1,1672 @@ -{} +{ + "components.Settings.metadataSettings": "", + "components.Blocklist.blocklistNotFoundError": "", + "components.CollectionDetails.overview": "", + "components.Blocklist.filterManual": "", + "components.Settings.toastPlexConnecting": "", + "components.Settings.toastPlexConnectingFailure": "", + "components.Settings.toastTautulliSettingsSuccess": "", + "components.CollectionDetails.numberofmovies": "", + "components.Settings.tvdbProviderDoesnotWork": "", + "components.Settings.urlBase": "", + "components.Settings.validationApiKey": "", + "components.Discover.CreateSlider.providetmdbsearch": "", + "components.Settings.valueRequired": "", + "components.Discover.CreateSlider.searchStudios": "", + "i18n.requested": "", + "components.AppDataWarning.dockerVolumeMissingDescription": "", + "components.Blocklist.blocklistSettingsDescription": "", + "components.Blocklist.blocklistdate": "", + "components.Blocklist.blocklistedby": "", + "components.Blocklist.blocklistsettings": "", + "components.Blocklist.filterBlocklistedTags": "", + "components.Discover.DiscoverSliderEdit.deletesuccess": "", + "components.Discover.DiscoverTv.activefilters": "", + "components.Discover.DiscoverTv.discovertv": "", + "components.Blocklist.mediaName": "", + "components.Blocklist.mediaTmdbId": "", + "components.Blocklist.mediaType": "", + "components.Settings.Notifications.usePublicLogo": "", + "components.Settings.Notifications.usePublicLogoTip": "", + "components.Settings.Notifications.validationBotAPIRequired": "", + "components.Settings.Notifications.validationPgpPrivateKey": "", + "components.Settings.Notifications.validationSmtpHostRequired": "", + "components.Settings.Notifications.validationTypes": "", + "components.Settings.Notifications.validationUrl": "", + "components.Settings.Notifications.validationWebhookRoleId": "", + "components.Settings.Notifications.webhookRoleId": "", + "components.Settings.Notifications.webhookRoleIdTip": "", + "components.Settings.Notifications.webhookUrlTip": "", + "components.Settings.OverrideRuleModal.conditions": "", + "components.Settings.OverrideRuleModal.conditionsDescription": "", + "components.Settings.OverrideRuleModal.createrule": "", + "components.Settings.OverrideRuleModal.editrule": "", + "components.Settings.OverrideRuleModal.genres": "", + "components.Settings.OverrideRuleModal.keywords": "", + "components.Settings.OverrideRuleModal.languages": "", + "components.Settings.OverrideRuleModal.notagoptions": "", + "components.Settings.OverrideRuleModal.qualityprofile": "", + "components.Settings.OverrideRuleModal.rootfolder": "", + "components.Settings.OverrideRuleModal.selectQualityProfile": "", + "components.Settings.OverrideRuleModal.selectRootFolder": "", + "components.Settings.OverrideRuleModal.selectService": "", + "components.Settings.OverrideRuleModal.service": "", + "components.Settings.OverrideRuleModal.serviceDescription": "", + "components.Settings.OverrideRuleModal.settings": "", + "components.Settings.OverrideRuleModal.settingsDescription": "", + "components.Settings.OverrideRuleModal.tags": "", + "components.Settings.OverrideRuleModal.users": "", + "components.Settings.OverrideRuleTile.conditions": "", + "components.Settings.OverrideRuleTile.language": "", + "components.Settings.OverrideRuleTile.qualityprofile": "", + "components.Settings.RadarrModal.baseUrl": "", + "components.Settings.RadarrModal.defaultserver": "", + "components.Settings.RadarrModal.port": "", + "components.Settings.RadarrModal.released": "", + "components.Settings.RadarrModal.rootfolder": "", + "components.Settings.RadarrModal.server4kHelp": "", + "components.Settings.RadarrModal.servername": "", + "components.Settings.RadarrModal.ssl": "", + "components.Settings.RadarrModal.toastRadarrTestSuccess": "", + "components.Settings.RadarrModal.validationBaseUrlTrailingSlash": "", + "components.Settings.RadarrModal.validationHostnameRequired": "", + "components.Settings.RadarrModal.validationNameRequired": "", + "components.Settings.RadarrModal.validationProfileRequired": "", + "components.Settings.RadarrModal.validationRootFolderRequired": "", + "components.Settings.SettingsAbout.Releases.currentversion": "", + "components.Settings.SettingsAbout.Releases.latestversion": "", + "components.Settings.SettingsAbout.Releases.versionChangelog": "", + "components.Settings.SettingsAbout.Releases.viewchangelog": "", + "components.Settings.SettingsAbout.contribute": "", + "components.Settings.SettingsAbout.githubdiscussions": "", + "components.Settings.SettingsAbout.outofdate": "", + "components.Settings.SettingsAbout.runningDevelop": "", + "components.Settings.SettingsJobsCache.availability-sync": "", + "components.Settings.SettingsJobsCache.cache": "", + "components.Settings.SettingsJobsCache.cacheDescription": "", + "components.Blocklist.showAllBlocklisted": "", + "components.Settings.SettingsJobsCache.cacheflushed": "", + "components.Settings.SettingsJobsCache.cachehits": "", + "components.Settings.SettingsJobsCache.cachekeys": "", + "components.Settings.SettingsJobsCache.cachevsize": "", + "components.Settings.SettingsJobsCache.canceljob": "", + "components.Settings.SettingsJobsCache.command": "", + "components.Settings.SettingsJobsCache.dnsCacheDescription": "", + "components.Settings.SettingsJobsCache.dnsCacheGlobalStatsDescription": "", + "components.Settings.SettingsJobsCache.dnscacheactiveaddress": "", + "components.Settings.SettingsJobsCache.dnscachemisses": "", + "components.Settings.SettingsJobsCache.dnscachename": "", + "components.Settings.SettingsJobsCache.download-sync": "", + "components.Settings.SettingsJobsCache.download-sync-reset": "", + "components.Settings.SettingsJobsCache.editJobSchedule": "", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorDays": "", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorHours": "", + "components.Settings.SettingsJobsCache.failures": "", + "components.Settings.SettingsJobsCache.image-cache-cleanup": "", + "components.Settings.SettingsJobsCache.imagecacheDescription": "", + "components.Settings.SettingsJobsCache.ipv4Fallbacks": "", + "components.Settings.SettingsJobsCache.jobScheduleEditFailed": "", + "components.Settings.SettingsJobsCache.jobScheduleEditSaved": "", + "components.CollectionDetails.requestcollection": "", + "components.Settings.SettingsJobsCache.jobcancelled": "", + "components.Settings.SettingsJobsCache.jobname": "", + "components.Settings.SettingsJobsCache.jobs": "", + "components.Settings.SettingsJobsCache.jobsDescription": "", + "components.Settings.SettingsJobsCache.jobsandcache": "", + "components.Settings.SettingsJobsCache.jobstarted": "", + "components.Settings.SettingsJobsCache.jobtype": "", + "components.Settings.SettingsJobsCache.misses": "", + "components.Settings.SettingsJobsCache.nextexecution": "", + "components.Settings.SettingsJobsCache.plex-recently-added-scan": "", + "components.Settings.SettingsJobsCache.plex-watchlist-sync": "", + "components.Settings.SettingsJobsCache.radarr-scan": "", + "components.Settings.SettingsJobsCache.runnow": "", + "components.Settings.SettingsJobsCache.sonarr-scan": "", + "components.Settings.SettingsJobsCache.unknownJob": "", + "components.Settings.SettingsJobsCache.usersavatars": "", + "components.Settings.SettingsLogs.copyToClipboard": "", + "components.Settings.SettingsLogs.filterDebug": "", + "components.Settings.SettingsLogs.filterError": "", + "components.Settings.SettingsLogs.filterInfo": "", + "components.Settings.SettingsLogs.filterWarn": "", + "components.Settings.SettingsLogs.level": "", + "components.Settings.SettingsLogs.message": "", + "components.Settings.SettingsLogs.resumeLogs": "", + "components.Settings.SettingsLogs.showall": "", + "components.Settings.SettingsLogs.time": "", + "components.Settings.SettingsLogs.viewdetails": "", + "components.Settings.SettingsMain.apikey": "", + "components.Settings.SettingsMain.apikeyCopied": "", + "components.Settings.SettingsMain.applicationurl": "", + "components.Settings.SettingsMain.blocklistLanguage": "", + "components.Settings.SettingsMain.blocklistLanguageTip": "", + "components.Settings.SettingsMain.blocklistRegion": "", + "components.Settings.SettingsMain.blocklistRegionTip": "", + "components.Settings.SettingsMain.blocklistedTags": "", + "components.Settings.SettingsMain.blocklistedTagsLimitTip": "", + "components.Settings.SettingsMain.blocklistedTagsTip": "", + "components.Settings.SettingsMain.cacheImages": "", + "components.Settings.SettingsNetwork.dnsCache": "", + "components.Settings.SettingsNetwork.dnsCacheForceMaxTtl": "", + "components.Settings.SettingsNetwork.proxyBypassLocalAddresses": "", + "components.Settings.SettingsNetwork.proxyPassword": "", + "components.Settings.SettingsNetwork.toastSettingsSuccess": "", + "components.Settings.SettingsUsers.defaultPermissionsTip": "", + "components.Settings.SettingsUsers.disabledMediaServerLoginWarning": "", + "components.Settings.SettingsUsers.localLogin": "", + "components.Settings.SettingsUsers.localLoginTip": "", + "components.Settings.SettingsUsers.loginMethods": "", + "components.Settings.SettingsUsers.mediaServerLoginTip": "", + "components.Settings.SettingsUsers.movieRequestLimitLabel": "", + "components.Settings.SettingsUsers.newPlexLoginTip": "", + "components.Settings.SettingsUsers.toastSettingsSuccess": "", + "components.Settings.SonarrModal.baseUrl": "", + "components.Settings.SonarrModal.default4kserver": "", + "components.Settings.SonarrModal.loadinglanguageprofiles": "", + "components.Settings.SonarrModal.loadingprofiles": "", + "components.Settings.SonarrModal.loadingrootfolders": "", + "components.Settings.SonarrModal.monitorNewItemsHelp": "", + "components.Settings.SonarrModal.notagoptions": "", + "components.Settings.SonarrModal.rootfolder": "", + "components.Settings.SonarrModal.selectLanguageProfile": "", + "components.Settings.SonarrModal.selectQualityProfile": "", + "components.Settings.SonarrModal.selectRootFolder": "", + "components.Settings.SonarrModal.selecttags": "", + "components.Settings.SonarrModal.seriesType": "", + "components.Settings.SonarrModal.server4k": "", + "components.Settings.SonarrModal.server4kHelp": "", + "components.Settings.SonarrModal.syncEnabledHelp": "", + "components.Settings.SonarrModal.tagRequestsInfo": "", + "components.Settings.SonarrModal.tags": "", + "components.Settings.SonarrModal.testFirstLanguageProfiles": "", + "components.Settings.SonarrModal.testFirstQualityProfiles": "", + "components.Settings.SonarrModal.testFirstRootFolders": "", + "components.Settings.SonarrModal.testFirstTags": "", + "components.Settings.SonarrModal.toastSonarrTestFailure": "", + "components.Settings.SonarrModal.toastSonarrTestSuccess": "", + "components.Settings.SonarrModal.validationApiKeyRequired": "", + "components.Settings.SonarrModal.validationApplicationUrl": "", + "components.Settings.SonarrModal.validationApplicationUrlTrailingSlash": "", + "components.Settings.SonarrModal.validationBaseUrlLeadingSlash": "", + "components.Settings.SonarrModal.validationBaseUrlTrailingSlash": "", + "components.Settings.SonarrModal.validationHostnameRequired": "", + "components.Settings.SonarrModal.validationLanguageProfileRequired": "", + "components.Settings.SonarrModal.validationNameRequired": "", + "components.Settings.SonarrModal.validationProfileRequired": "", + "components.Settings.SonarrModal.validationRootFolderRequired": "", + "components.Settings.activeProfile": "", + "components.Settings.addradarr": "", + "components.Settings.address": "", + "components.Settings.addrule": "", + "components.Settings.allChosenProvidersAreOperational": "", + "components.Settings.animeMetadataProvider": "", + "components.Settings.cancelscan": "", + "components.Settings.chooseProvider": "", + "components.Settings.clearBlocklistedTagsConfirm": "", + "components.Settings.clickTest": "", + "components.Settings.connectionTestFailed": "", + "components.Settings.currentlibrary": "", + "components.Settings.default": "", + "components.Settings.default4k": "", + "components.Settings.deleteServer": "", + "components.Settings.deleteserverconfirm": "", + "components.Settings.email": "", + "components.Settings.enablessl": "", + "components.Settings.experimentalTooltip": "", + "components.Settings.externalUrl": "", + "components.Settings.failedToSaveMetadataSettings": "", + "components.Settings.invalidurlerror": "", + "components.Settings.is4k": "", + "components.Settings.jellyfinForgotPasswordUrl": "", + "components.Settings.jellyfinSettings": "", + "components.Settings.jellyfinSettingsFailure": "", + "components.Settings.jellyfinSettingsSuccess": "", + "components.Settings.jellyfinSyncFailedAutomaticGroupedFolders": "", + "components.Settings.jellyfinSyncFailedGenericError": "", + "components.Settings.jellyfinSyncFailedNoLibrariesFound": "", + "components.Settings.jellyfinlibraries": "", + "components.Settings.jellyfinlibrariesDescription": "", + "components.Settings.jellyfinsettingsDescription": "", + "components.Settings.manualscan": "", + "components.Settings.manualscanDescription": "", + "components.Settings.manualscanDescriptionJellyfin": "", + "components.Settings.mediaTypeMovie": "", + "components.Settings.mediaTypeSeries": "", + "components.Settings.menuAbout": "", + "components.Settings.notificationsettings": "", + "components.Settings.operational": "", + "components.Settings.overrideRules": "", + "components.Settings.overrideRulesDescription": "", + "components.Settings.plex": "", + "components.Settings.plexsettings": "", + "components.Settings.plexsettingsDescription": "", + "components.Settings.port": "", + "components.Settings.providerStatus": "", + "components.Settings.radarrsettings": "", + "components.Settings.restartrequiredTooltip": "", + "components.Settings.scanbackground": "", + "components.Settings.scanning": "", + "components.Settings.searchKeywords": "", + "components.Settings.seriesMetadataProvider": "", + "components.Settings.serverLocal": "", + "components.Settings.serverRemote": "", + "components.Settings.serverSecure": "", + "components.Settings.serverpreset": "", + "components.Settings.serviceSettingsDescription": "", + "components.Settings.settingUpPlexDescription": "", + "components.Settings.settings": "", + "components.Settings.sonarrsettings": "", + "components.Settings.ssl": "", + "components.Settings.startscan": "", + "components.Settings.starttyping": "", + "components.Settings.syncing": "", + "components.Settings.tautulliApiKey": "", + "components.Settings.tautulliSettingsDescription": "", + "components.Settings.timeout": "", + "components.Settings.tip": "", + "components.Settings.tmdbProviderDoesnotWork": "", + "components.Settings.toastPlexConnectingSuccess": "", + "components.Settings.toastPlexRefresh": "", + "components.Settings.validationPortRequired": "", + "components.Settings.validationUrl": "", + "components.Settings.validationUrlBaseLeadingSlash": "", + "components.Settings.validationUrlBaseTrailingSlash": "", + "components.Settings.validationUrlTrailingSlash": "", + "components.Settings.webAppUrl": "", + "components.Settings.webAppUrlTip": "", + "components.Setup.configplex": "", + "components.Setup.configuremediaserver": "", + "components.Setup.configureservices": "", + "components.Setup.finish": "", + "components.Setup.finishing": "", + "components.Setup.servertype": "", + "components.Setup.setup": "", + "components.Setup.signin": "", + "components.Setup.signinWithEmby": "", + "components.Setup.signinWithJellyfin": "", + "components.Setup.signinWithPlex": "", + "components.Setup.subtitle": "", + "components.Setup.welcome": "", + "components.StatusBadge.managemedia": "", + "components.StatusBadge.playonplex": "", + "components.StatusBadge.seasonepisodenumber": "", + "components.StatusBadge.status4k": "", + "components.StatusChecker.appUpdated": "", + "components.StatusChecker.appUpdatedDescription": "", + "components.StatusChecker.reloadApp": "", + "components.StatusChecker.restartRequired": "", + "components.StatusChecker.restartRequiredDescription": "", + "components.TitleCard.addToWatchList": "", + "components.TitleCard.cleardata": "", + "components.CollectionDetails.requestcollection4k": "", + "components.TitleCard.mediaerror": "", + "components.TitleCard.tmdbid": "", + "components.TitleCard.watchlistError": "", + "components.TitleCard.watchlistSuccess": "", + "components.TvDetails.TvCast.fullseriescast": "", + "components.TvDetails.TvCrew.fullseriescrew": "", + "components.TvDetails.firstAirDate": "", + "components.TvDetails.manageseries": "", + "components.TvDetails.network": "", + "components.TvDetails.originallanguage": "", + "components.TvDetails.originaltitle": "", + "components.TvDetails.overviewunavailable": "", + "components.TvDetails.play": "", + "components.TvDetails.play4k": "", + "components.TvDetails.removefromwatchlist": "", + "components.TvDetails.rtcriticsscore": "", + "components.TvDetails.streamingproviders": "", + "components.TvDetails.tmdbuserscore": "", + "components.TvDetails.watchtrailer": "", + "components.UserList.accounttype": "", + "components.UserList.autogeneratepassword": "", + "components.UserList.autogeneratepasswordTip": "", + "components.UserList.deleteconfirm": "", + "components.UserList.deleteuser": "", + "components.UserList.edituser": "", + "components.UserList.email": "", + "components.UserList.importedPlexUsersNoPassword": "", + "components.UserList.importedUsersNoPassword": "", + "components.UserList.importedfromJellyfin": "", + "components.UserList.importfromJellyfinerror": "", + "components.UserList.importfromplexerror": "", + "components.UserList.localuser": "", + "components.UserList.mediaServerUser": "", + "components.UserList.nouserstoimport": "", + "components.UserList.password": "", + "components.Discover.CreateSlider.addSlider": "", + "components.UserList.passwordinfodescription": "", + "components.Discover.CreateSlider.addcustomslider": "", + "components.UserList.role": "", + "components.UserList.sortBy": "", + "components.UserList.sortByRequests": "", + "components.UserList.sortByRole": "", + "components.UserList.sortByUser": "", + "components.UserList.toggleSortDirection": "", + "components.UserList.usercreatedfailedexisting": "", + "components.UserList.userssaved": "", + "components.UserProfile.ProfileHeader.profile": "", + "components.UserProfile.ProfileHeader.settings": "", + "components.UserProfile.ProfileHeader.userid": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorUnauthorized": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.password": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.passwordRequired": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.saving": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.title": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.username": "", + "components.UserProfile.UserSettings.UserGeneralSettings.admin": "", + "components.UserProfile.UserSettings.UserGeneralSettings.applanguage": "", + "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegion": "", + "components.UserProfile.UserSettings.UserGeneralSettings.mediaServerUser": "", + "components.UserProfile.UserSettings.UserGeneralSettings.originallanguage": "", + "components.UserProfile.UserSettings.UserGeneralSettings.originallanguageTip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.owner": "", + "components.UserProfile.UserSettings.UserGeneralSettings.validationemailformat": "", + "components.UserProfile.UserSettings.UserGeneralSettings.validationemailrequired": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.deleteFailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleted": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleteerror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.type": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.unknown": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeendisabled": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeenenabled": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.email": "", + "components.Discover.DiscoverTv.sortTmdbRatingAsc": "", + "components.UserProfile.UserSettings.UserNotificationSettings.emailsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.emailsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.notifications": "", + "components.Discover.CreateSlider.providetmdbstudio": "", + "components.UserProfile.UserSettings.UserNotificationSettings.notificationsettings": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pgpPublicKey": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pgpPublicKeyTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessToken": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessTokenTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverApplicationToken": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverUserKeyTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoversettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.sendSilentlyDescription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramChatId": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramChatIdTipLong": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadId": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPushbulletAccessToken": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramMessageThreadId": "", + "components.UserProfile.UserSettings.UserNotificationSettings.webpush": "", + "components.UserProfile.UserSettings.UserPasswordChange.localPasswordDescription": "", + "components.UserProfile.UserSettings.UserPasswordChange.newpassword": "", + "components.UserProfile.UserSettings.UserPasswordChange.noPasswordSet": "", + "components.UserProfile.UserSettings.UserPasswordChange.nopermissionDescription": "", + "components.UserProfile.UserSettings.UserPasswordChange.password": "", + "components.UserProfile.UserSettings.UserPasswordChange.toastSettingsFailureVerifyCurrent": "", + "components.UserProfile.UserSettings.UserPasswordChange.validationConfirmPassword": "", + "components.UserProfile.UserSettings.UserPasswordChange.validationConfirmPasswordSame": "", + "components.UserProfile.UserSettings.UserPasswordChange.validationCurrentPassword": "", + "components.UserProfile.UserSettings.UserPasswordChange.validationNewPasswordLength": "", + "components.UserProfile.UserSettings.UserPermissions.permissions": "", + "components.UserProfile.UserSettings.UserPermissions.toastSettingsSuccess": "", + "components.UserProfile.UserSettings.UserPermissions.unauthorizedDescription": "", + "components.UserProfile.UserSettings.menuChangePass": "", + "components.UserProfile.UserSettings.menuPermissions": "", + "components.UserProfile.UserSettings.unauthorizedDescription": "", + "components.UserProfile.emptywatchlist": "", + "components.UserProfile.limit": "", + "components.UserProfile.localWatchlist": "", + "components.UserProfile.movierequests": "", + "components.UserProfile.pastdays": "", + "components.UserProfile.plexwatchlist": "", + "components.UserProfile.recentlywatched": "", + "components.UserProfile.requestsperdays": "", + "components.UserProfile.seriesrequest": "", + "components.UserProfile.totalrequests": "", + "components.UserProfile.unlimited": "", + "i18n.addToBlocklist": "", + "i18n.all": "", + "i18n.areyousure": "", + "i18n.available": "", + "i18n.back": "", + "i18n.blocklist": "", + "i18n.blocklistDuplicateError": "", + "i18n.blocklistError": "", + "i18n.blocklistSuccess": "", + "i18n.blocklisted": "", + "i18n.cancel": "", + "i18n.canceling": "", + "i18n.close": "", + "i18n.collection": "", + "i18n.completed": "", + "i18n.decline": "", + "i18n.declined": "", + "i18n.deleted": "", + "i18n.edit": "", + "i18n.import": "", + "i18n.next": "", + "i18n.noresults": "", + "i18n.notificationLocale": "", + "i18n.partiallyblocklisted": "", + "i18n.previous": "", + "i18n.processing": "", + "i18n.removeFromBlocklistSuccess": "", + "i18n.removefromBlocklist": "", + "i18n.request": "", + "i18n.resultsperpage": "", + "i18n.retry": "", + "i18n.save": "", + "i18n.saving": "", + "i18n.settings": "", + "i18n.specials": "", + "i18n.unavailable": "", + "pages.returnHome": "", + "components.Discover.CreateSlider.searchGenres": "", + "components.Discover.CreateSlider.searchKeywords": "", + "components.Discover.DiscoverWatchlist.discoverwatchlist": "", + "components.Discover.FilterSlideover.runtime": "", + "components.Discover.moviegenres": "", + "components.Discover.networks": "", + "components.RequestCard.failedmodify": "", + "components.RequestCard.seasons": "", + "components.RequestCard.tmdbid": "", + "components.RequestCard.tvdbid": "", + "components.RequestCard.unknowntitle": "", + "components.RequestList.RequestItem.cancelRequest": "", + "components.RequestModal.numberofepisodes": "", + "components.RequestModal.pending4krequest": "", + "components.RequestModal.requestCancel": "", + "components.RequestModal.requestSuccess": "", + "components.RequestModal.requestcancelled": "", + "components.RequestModal.requestmovies": "", + "components.RequestModal.requestmovies4k": "", + "components.RequestModal.requestmovietitle": "", + "components.RequestModal.requestseasons": "", + "components.ResetPassword.requestresetlinksuccessmessage": "", + "components.ResetPassword.resetpassword": "", + "components.ResetPassword.resetpasswordsuccessmessage": "", + "components.ResetPassword.validationemailrequired": "", + "components.Search.search": "", + "components.Selector.CertificationSelector.minRating": "", + "components.Selector.CertificationSelector.noOptions": "", + "components.Selector.CertificationSelector.starttyping": "", + "components.Selector.ended": "", + "components.Selector.inProduction": "", + "components.Selector.nooptions": "", + "components.Selector.planned": "", + "components.Selector.returningSeries": "", + "components.Settings.Notifications.NotificationsPushbullet.pushbulletSettingsFailed": "", + "components.Settings.Notifications.NotificationsPushbullet.pushbulletSettingsSaved": "", + "components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestSuccess": "", + "components.Settings.Notifications.NotificationsPushbullet.validationAccessTokenRequired": "", + "components.Settings.RadarrModal.validationApplicationUrl": "", + "components.Settings.SettingsLogs.label": "", + "components.IssueDetails.IssueDescription.deleteissue": "", + "components.IssueDetails.IssueDescription.description": "", + "components.Discover.DiscoverTv.sortTmdbRatingDesc": "", + "components.Discover.DiscoverTvGenre.genreSeries": "", + "components.Discover.DiscoverTvKeyword.keywordSeries": "", + "components.Discover.FilterSlideover.firstAirDate": "", + "components.Discover.FilterSlideover.from": "", + "components.Discover.FilterSlideover.genres": "", + "components.Discover.FilterSlideover.keywords": "", + "components.Discover.FilterSlideover.ratingText": "", + "components.Discover.FilterSlideover.releaseDate": "", + "components.Discover.FilterSlideover.runtimeText": "", + "components.TvDetails.overview": "", + "components.Discover.CreateSlider.slidernameplaceholder": "", + "components.Discover.CreateSlider.starttyping": "", + "components.Discover.DiscoverSliderEdit.enable": "", + "components.Discover.DiscoverTv.sortFirstAirDateDesc": "", + "components.Discover.DiscoverTv.sortPopularityAsc": "", + "components.Discover.DiscoverTv.sortPopularityDesc": "", + "components.Discover.DiscoverTvLanguage.languageSeries": "", + "components.Discover.FilterSlideover.certification": "", + "components.Discover.FilterSlideover.clearfilters": "", + "components.Discover.FilterSlideover.tmdbuservotecount": "", + "components.Discover.FilterSlideover.to": "", + "components.Discover.FilterSlideover.voteCount": "", + "components.Discover.StudioSlider.studios": "", + "components.Discover.TvGenreList.seriesgenres": "", + "components.Discover.RecentRequestsSlider.unableToConnect": "", + "components.Discover.discover": "", + "components.Discover.plexwatchlist": "", + "components.Discover.popularmovies": "", + "components.Discover.populartv": "", + "components.Discover.recentlyAdded": "", + "components.Discover.tvgenres": "", + "components.Discover.upcoming": "", + "components.Discover.upcomingmovies": "", + "components.Discover.updatefailed": "", + "components.Discover.updatesuccess": "", + "components.Settings.Notifications.NotificationsWebPush.httpsRequirement": "", + "components.Settings.Notifications.NotificationsWebPush.toastWebPushTestFailed": "", + "components.Settings.Notifications.NotificationsWebPush.toastWebPushTestSending": "", + "components.Settings.Notifications.NotificationsWebPush.toastWebPushTestSuccess": "", + "components.Settings.Notifications.NotificationsWebPush.webpushsettingsfailed": "", + "components.Settings.Notifications.NotificationsWebhook.authheader": "", + "components.Settings.Notifications.NotificationsWebhook.customHeaders": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersAdd": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersAuthConflict": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersIncomplete": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersKey": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersRemove": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersTip": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersValue": "", + "components.Settings.Notifications.NotificationsWebhook.customJson": "", + "components.Settings.Notifications.NotificationsWebhook.resetPayload": "", + "components.Settings.Notifications.NotificationsWebhook.resetPayloadSuccess": "", + "components.Settings.Notifications.NotificationsWebhook.supportVariables": "", + "components.Settings.Notifications.NotificationsWebhook.supportVariablesTip": "", + "components.Settings.Notifications.NotificationsWebhook.templatevariablehelp": "", + "components.Settings.Notifications.NotificationsWebhook.toastWebhookTestFailed": "", + "components.Settings.Notifications.NotificationsWebhook.toastWebhookTestSuccess": "", + "components.Settings.Notifications.NotificationsWebhook.validationJsonPayloadRequired": "", + "components.Settings.Notifications.NotificationsWebhook.validationTypes": "", + "components.Settings.Notifications.NotificationsWebhook.validationWebhookUrl": "", + "components.Discover.recentrequests": "", + "components.Settings.Notifications.NotificationsWebhook.webhookUrl": "", + "components.Settings.Notifications.NotificationsWebhook.webhookUrlTip": "", + "components.Settings.Notifications.NotificationsWebhook.webhooksettingsfailed": "", + "components.Settings.Notifications.NotificationsWebhook.webhooksettingssaved": "", + "components.Settings.Notifications.useUserLocale": "", + "components.Settings.Notifications.userEmailRequired": "", + "components.Settings.Notifications.validationChatIdRequired": "", + "components.Settings.Notifications.validationPgpPassword": "", + "components.Settings.OverrideRuleTile.users": "", + "components.Settings.RadarrModal.announced": "", + "components.Settings.RadarrModal.apiKey": "", + "components.Settings.RadarrModal.apiKeyHelp": "", + "components.Settings.RadarrModal.baseUrlHelp": "", + "components.Settings.RadarrModal.edit4kradarr": "", + "components.Settings.RadarrModal.editradarr": "", + "components.Settings.RadarrModal.enableSearch": "", + "components.Settings.RadarrModal.enableSearchHelp": "", + "components.Settings.RadarrModal.externalUrlHelp": "", + "components.Settings.RadarrModal.loadingTags": "", + "components.Settings.RadarrModal.loadingprofiles": "", + "components.Settings.RadarrModal.loadingrootfolders": "", + "components.Settings.RadarrModal.minimumAvailability": "", + "components.Discover.resetfailed": "", + "components.Settings.RadarrModal.syncEnabled": "", + "components.Settings.RadarrModal.syncEnabledHelp": "", + "components.Discover.tmdbmoviestreamingservices": "", + "components.Settings.RadarrModal.tagRequests": "", + "components.Settings.RadarrModal.tagRequestsInfo": "", + "components.Settings.RadarrModal.tags": "", + "components.Settings.RadarrModal.testFirstQualityProfiles": "", + "components.Settings.RadarrModal.testFirstRootFolders": "", + "components.Layout.Sidebar.browsetv": "", + "components.Settings.RadarrModal.testFirstTags": "", + "components.Settings.RadarrModal.toastRadarrTestFailure": "", + "components.Settings.SettingsAbout.about": "", + "components.Settings.SettingsAbout.supportseerr": "", + "components.Settings.SettingsAbout.timezone": "", + "components.Settings.SettingsAbout.totalmedia": "", + "components.Settings.SettingsAbout.totalrequests": "", + "components.Settings.SettingsAbout.uptodate": "", + "components.Settings.SettingsJobsCache.jellyfin-full-scan": "", + "components.Settings.SettingsMain.cacheImagesTip": "", + "components.Settings.SettingsMain.hideAvailableTip": "", + "components.Settings.SettingsMain.locale": "", + "components.Settings.SettingsMain.originallanguage": "", + "components.Settings.SettingsMain.toastApiKeyFailure": "", + "components.Settings.SettingsMain.validationUrlTrailingSlash": "", + "components.Settings.SettingsNetwork.csrfProtectionHoverTip": "", + "components.Settings.SettingsNetwork.csrfProtectionTip": "", + "components.Settings.SettingsNetwork.dnsCacheHoverTip": "", + "components.Settings.SettingsNetwork.proxySsl": "", + "components.Settings.SettingsNetwork.proxyUser": "", + "components.Settings.SettingsNetwork.toastSettingsFailure": "", + "components.Settings.SonarrModal.enableSearch": "", + "components.Settings.SonarrModal.externalUrl": "", + "components.Settings.SonarrModal.hostname": "", + "components.Settings.SonarrModal.languageprofile": "", + "components.Settings.SonarrModal.monitorNewItems": "", + "components.Settings.addsonarr": "", + "components.Settings.blocklistedTagsText": "", + "components.Settings.menuNetwork": "", + "components.Settings.menuNotifications": "", + "components.Settings.menuPlexSettings": "", + "components.Settings.metadataProviderSelection": "", + "components.Settings.metadataProviderSettings": "", + "components.Settings.toastPlexRefreshSuccess": "", + "components.Settings.toastTautulliSettingsFailure": "", + "components.Settings.validationHostnameRequired": "", + "components.Settings.webhook": "", + "components.TitleCard.watchlistCancel": "", + "components.TitleCard.watchlistDeleted": "", + "components.UserList.importfrommediaserver": "", + "components.UserList.owner": "", + "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegionTip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.displayName": "", + "components.UserProfile.UserSettings.UserGeneralSettings.save": "", + "components.UserProfile.UserSettings.UserGeneralSettings.saving": "", + "components.RequestModal.season": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorExists": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablewebpush": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverApplicationTokenTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverUserKey": "", + "components.Settings.Notifications.senderName": "", + "components.Settings.Notifications.smtpHost": "", + "components.Settings.Notifications.smtpPort": "", + "components.IssueDetails.problemepisode": "", + "components.UserProfile.UserSettings.UserNotificationSettings.sound": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadIdTip": "", + "components.UserProfile.UserSettings.UserPermissions.toastSettingsFailure": "", + "i18n.requesting": "", + "i18n.resolved": "", + "components.Settings.advancedTooltip": "", + "components.TitleCard.tvdbid": "", + "components.TvDetails.showtype": "", + "components.UserProfile.UserSettings.menuNotifications": "", + "components.Discover.DiscoverTv.sortTitleAsc": "", + "components.Discover.DiscoverTv.sortTitleDesc": "", + "components.Discover.DiscoverWatchlist.watchlist": "", + "components.Discover.FilterSlideover.activefilters": "", + "components.Discover.FilterSlideover.excludeKeywords": "", + "components.Discover.FilterSlideover.filters": "", + "components.Discover.FilterSlideover.originalLanguage": "", + "components.Discover.MovieGenreList.moviegenres": "", + "components.Discover.MovieGenreSlider.moviegenres": "", + "components.Discover.NetworkSlider.networks": "", + "components.Discover.PlexWatchlistSlider.emptywatchlist": "", + "components.Discover.PlexWatchlistSlider.plexwatchlist": "", + "components.Discover.emptywatchlist": "", + "components.Discover.resetwarning": "", + "components.Discover.stopediting": "", + "components.Discover.studios": "", + "components.Discover.tmdbmoviekeyword": "", + "components.IssueDetails.issuepagetitle": "", + "components.IssueDetails.issuetype": "", + "components.IssueDetails.lastupdated": "", + "components.IssueDetails.leavecomment": "", + "components.IssueDetails.nocomments": "", + "components.IssueDetails.openedby": "", + "components.IssueList.IssueItem.episodes": "", + "components.IssueDetails.openin4karr": "", + "components.IssueDetails.openinarr": "", + "components.IssueDetails.play4konplex": "", + "components.IssueDetails.playonplex": "", + "components.IssueDetails.toasteditdescriptionfailed": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestSuccess": "", + "components.Settings.Notifications.NotificationsNtfy.token": "", + "components.Settings.Notifications.NotificationsNtfy.url": "", + "components.Settings.Notifications.NotificationsNtfy.username": "", + "components.Settings.Notifications.NotificationsNtfy.usernamePasswordAuth": "", + "components.Settings.Notifications.NotificationsNtfy.validationNtfyTopic": "", + "components.Settings.Notifications.NotificationsPushbullet.agentEnabled": "", + "components.Settings.OverrideRuleTile.genre": "", + "components.Settings.OverrideRuleTile.keywords": "", + "components.Settings.OverrideRuleTile.settings": "", + "components.Settings.RadarrModal.add": "", + "components.Settings.RadarrModal.externalUrl": "", + "components.RequestList.RequestItem.modifieduserdate": "", + "components.Settings.RadarrModal.selectQualityProfile": "", + "components.Settings.RadarrModal.selectRootFolder": "", + "components.Settings.RadarrModal.selecttags": "", + "components.Settings.RadarrModal.server4k": "", + "components.IssueList.showallissues": "", + "components.Settings.RadarrModal.validationApplicationUrlTrailingSlash": "", + "components.Settings.RadarrModal.validationBaseUrlLeadingSlash": "", + "components.RequestModal.AdvancedRequester.default": "", + "components.Settings.RadarrModal.validationPortRequired": "", + "components.Settings.SettingsAbout.Releases.releases": "", + "components.RequestModal.AdvancedRequester.folder": "", + "components.Settings.SonarrModal.animerootfolder": "", + "components.Settings.SonarrModal.defaultserver": "", + "components.RequestModal.AdvancedRequester.requestas": "", + "components.RequestModal.AdvancedRequester.tags": "", + "components.Settings.hostname": "", + "components.Settings.importBlocklistedTagsTip": "", + "components.Settings.invalidKeyword": "", + "components.TvDetails.productioncountries": "", + "components.ResetPassword.password": "", + "components.Settings.Notifications.NotificationsGotify.validationUrlRequired": "", + "components.Settings.SettingsJobsCache.dnscacheage": "", + "components.Settings.SettingsJobsCache.dnscacheflushed": "", + "components.Settings.SettingsJobsCache.imagecachecount": "", + "components.Settings.SettingsJobsCache.imagecachesize": "", + "components.Settings.SettingsJobsCache.jellyfin-recently-added-scan": "", + "components.Settings.SettingsJobsCache.size": "", + "components.IssueList.sortModified": "", + "components.Settings.SettingsLogs.copiedLogMessage": "", + "components.Settings.SettingsMain.blocklistedTagsLimit": "", + "components.Settings.SettingsMain.generalsettingsDescription": "", + "components.Settings.SettingsMain.hideAvailable": "", + "components.Settings.SettingsNetwork.csrfProtection": "", + "components.Settings.SettingsMain.youtubeUrl": "", + "components.Settings.SettingsMain.youtubeUrlTip": "", + "components.Settings.SettingsNetwork.apiRequestTimeoutTip": "", + "components.Settings.SettingsUsers.loginMethodsTip": "", + "components.TvDetails.episodeRuntime": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPgpPublicKey": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmail": "", + "components.Layout.Sidebar.blocklist": "", + "components.RequestButton.declinerequest4k": "", + "components.Settings.SettingsMain.applicationTitle": "", + "components.Settings.SonarrModal.apiKey": "", + "i18n.retrying": "", + "components.Discover.CreateSlider.providetmdbgenreid": "", + "components.Discover.CreateSlider.providetmdbkeywordid": "", + "components.Discover.CreateSlider.validationDatarequired": "", + "components.Discover.CreateSlider.validationTitlerequired": "", + "components.Discover.DiscoverMovieGenre.genreMovies": "", + "components.Discover.DiscoverMovieKeyword.keywordMovies": "", + "components.Discover.DiscoverMovies.activefilters": "", + "components.Discover.DiscoverMovies.sortReleaseDateDesc": "", + "components.Discover.DiscoverMovies.sortTitleAsc": "", + "components.Discover.DiscoverMovies.sortTitleDesc": "", + "components.Discover.DiscoverMovies.sortTmdbRatingAsc": "", + "components.Discover.DiscoverMovies.sortTmdbRatingDesc": "", + "components.Discover.DiscoverNetwork.networkSeries": "", + "components.Discover.DiscoverSliderEdit.deletefail": "", + "components.Discover.DiscoverTv.sortFirstAirDateAsc": "", + "components.Discover.FilterSlideover.streamingservices": "", + "components.Discover.FilterSlideover.studio": "", + "components.Discover.FilterSlideover.tmdbuserscore": "", + "components.Discover.trending": "", + "components.Discover.upcomingtv": "", + "components.DownloadBlock.estimatedtime": "", + "components.IssueDetails.IssueComment.delete": "", + "components.IssueDetails.IssueComment.edit": "", + "components.IssueDetails.IssueComment.postedby": "", + "components.IssueDetails.allepisodes": "", + "components.IssueDetails.allseasons": "", + "components.IssueDetails.closeissueandcomment": "", + "components.IssueDetails.commentplaceholder": "", + "components.IssueDetails.problemseason": "", + "components.IssueList.IssueItem.issuestatus": "", + "components.IssueList.IssueItem.opened": "", + "components.IssueList.IssueItem.openeduserdate": "", + "components.IssueList.IssueItem.problemepisode": "", + "components.IssueList.IssueItem.seasons": "", + "components.IssueList.IssueItem.unknownissuetype": "", + "components.IssueList.IssueItem.viewissue": "", + "components.IssueModal.CreateIssueModal.allepisodes": "", + "components.IssueModal.CreateIssueModal.allseasons": "", + "components.IssueModal.CreateIssueModal.episode": "", + "components.IssueModal.CreateIssueModal.extras": "", + "components.IssueModal.CreateIssueModal.problemepisode": "", + "components.IssueModal.CreateIssueModal.problemseason": "", + "components.IssueModal.CreateIssueModal.providedetail": "", + "components.IssueModal.CreateIssueModal.reportissue": "", + "components.IssueModal.CreateIssueModal.season": "", + "components.IssueModal.CreateIssueModal.submitissue": "", + "components.IssueModal.CreateIssueModal.toastFailedCreate": "", + "components.IssueModal.CreateIssueModal.toastSuccessCreate": "", + "components.IssueModal.CreateIssueModal.validationMessageRequired": "", + "components.IssueModal.CreateIssueModal.whatswrong": "", + "components.Login.initialsignin": "", + "components.Layout.Sidebar.issues": "", + "components.Layout.Sidebar.requests": "", + "components.Layout.Sidebar.settings": "", + "components.Layout.Sidebar.users": "", + "components.Layout.UserDropdown.MiniQuotaDisplay.movierequests": "", + "components.Layout.UserDropdown.MiniQuotaDisplay.seriesrequests": "", + "components.Layout.UserDropdown.myprofile": "", + "components.Layout.UserDropdown.requests": "", + "components.Layout.UserDropdown.settings": "", + "components.Layout.UserDropdown.signout": "", + "components.Layout.UserWarnings.emailInvalid": "", + "components.Layout.UserWarnings.emailRequired": "", + "components.Layout.UserWarnings.passwordRequired": "", + "components.Login.credentialerror": "", + "components.Login.description": "", + "components.Login.initialsigningin": "", + "components.Login.invalidurlerror": "", + "components.Login.loginerror": "", + "components.Login.orsigninwith": "", + "components.Login.password": "", + "components.Login.urlBase": "", + "components.Login.username": "", + "components.Login.validationEmailFormat": "", + "components.Login.validationUrlTrailingSlash": "", + "components.Login.validationhostformat": "", + "components.Login.validationhostrequired": "", + "components.Login.validationpasswordrequired": "", + "components.Login.validationservertyperequired": "", + "components.Login.validationusernamerequired": "", + "components.ManageSlideOver.alltime": "", + "components.ManageSlideOver.downloadstatus": "", + "components.ManageSlideOver.manageModalClearMediaWarning": "", + "components.ManageSlideOver.manageModalIssues": "", + "components.ManageSlideOver.manageModalNoRequests": "", + "components.ManageSlideOver.manageModalRemoveMediaWarning": "", + "components.ManageSlideOver.manageModalRequests": "", + "components.ManageSlideOver.manageModalTitle": "", + "components.ManageSlideOver.opentautulli": "", + "components.ManageSlideOver.pastdays": "", + "components.MediaSlider.ShowMoreCard.seemore": "", + "components.MetadataSelector.selectMetdataProvider": "", + "components.MetadataSelector.tmdbLabel": "", + "components.MovieDetails.MovieCast.fullcast": "", + "components.MovieDetails.MovieCrew.fullcrew": "", + "components.MovieDetails.addtowatchlist": "", + "components.MovieDetails.budget": "", + "components.MovieDetails.managemovie": "", + "components.MovieDetails.openradarr": "", + "components.MovieDetails.originaltitle": "", + "components.MovieDetails.overviewunavailable": "", + "components.MovieDetails.physicalrelease": "", + "components.MovieDetails.play": "", + "components.MovieDetails.productioncountries": "", + "components.MovieDetails.recommendations": "", + "components.RequestModal.AdvancedRequester.advancedoptions": "", + "components.RequestModal.AdvancedRequester.animenote": "", + "components.RequestModal.AdvancedRequester.destinationserver": "", + "components.RequestModal.AdvancedRequester.qualityprofile": "", + "components.RequestModal.AdvancedRequester.selecttags": "", + "components.RequestModal.requestmovie4ktitle": "", + "components.ResetPassword.emailresetlink": "", + "components.ResetPassword.validationpasswordmatch": "", + "components.ResetPassword.validationpasswordminchars": "", + "components.ResetPassword.validationpasswordrequired": "", + "components.Search.searchresults": "", + "components.Selector.CertificationSelector.maxRating": "", + "components.Settings.Notifications.NotificationsGotify.gotifysettingssaved": "", + "components.Settings.Notifications.NotificationsGotify.priority": "", + "components.Settings.Notifications.NotificationsNtfy.priority": "", + "components.Settings.Notifications.NotificationsPushover.validationUserTokenRequired": "", + "components.Settings.Notifications.NotificationsWebhook.agentenabled": "", + "components.Settings.Notifications.NotificationsWebhook.toastWebhookTestSending": "", + "components.Settings.Notifications.webhookUrl": "", + "components.Settings.OverrideRuleModal.create": "", + "components.Settings.RadarrModal.selectMinimumAvailability": "", + "components.Settings.RadarrModal.validationMinimumAvailabilityRequired": "", + "components.Settings.SettingsJobsCache.cacheksize": "", + "components.Settings.SettingsJobsCache.cachemisses": "", + "components.Settings.SettingsJobsCache.plex-refresh-token": "", + "components.Settings.SettingsMain.hideBlocklistedTip": "", + "components.Settings.SettingsNetwork.proxyPort": "", + "components.Settings.copyBlocklistedTags": "", + "components.Settings.copyBlocklistedTagsEmpty": "", + "components.Settings.copyBlocklistedTagsTip": "", + "components.Settings.toastPlexRefreshFailure": "", + "components.Settings.webpush": "", + "components.Settings.yes": "", + "components.Setup.back": "", + "components.Setup.configemby": "", + "components.Setup.librarieserror": "", + "components.UserProfile.UserSettings.UserGeneralSettings.movierequestlimit": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.managedevices": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletsettingsfailed": "", + "components.UserProfile.UserSettings.menuGeneralSettings": "", + "components.UserProfile.UserSettings.menuLinkedAccounts": "", + "i18n.advanced": "", + "components.UserList.sortByJoined": "", + "components.Discover.CreateSlider.editsuccess": "", + "components.Discover.CreateSlider.needresults": "", + "components.Discover.CreateSlider.nooptions": "", + "components.Layout.VersionStatus.outofdate": "", + "components.NotificationTypeSelector.adminissueresolvedDescription": "", + "components.PermissionEdit.autoapproveMoviesDescription": "", + "components.PermissionEdit.autoapproveSeries": "", + "components.RequestBlock.requestdate": "", + "components.UserProfile.UserSettings.UserGeneralSettings.email": "", + "components.UserProfile.UserSettings.UserGeneralSettings.enableOverride": "", + "components.UserProfile.UserSettings.UserGeneralSettings.general": "", + "components.UserProfile.UserSettings.UserGeneralSettings.generalsettings": "", + "components.UserProfile.UserSettings.UserGeneralSettings.languageDefault": "", + "components.UserProfile.UserSettings.UserGeneralSettings.localuser": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexuser": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmovies": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmoviestip": "", + "components.RequestModal.pendingapproval": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseries": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseriestip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.region": "", + "components.UserProfile.UserSettings.UserGeneralSettings.regionTip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.role": "", + "components.UserProfile.UserSettings.UserGeneralSettings.seriesrequestlimit": "", + "components.UserProfile.UserSettings.UserGeneralSettings.streamingRegion": "", + "components.UserProfile.UserSettings.UserGeneralSettings.streamingRegionTip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmailEmpty": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsSuccess": "", + "components.UserProfile.UserSettings.UserGeneralSettings.user": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.errorUnknown": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.linkedAccounts": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.linkedAccountsHint": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.noLinkedAccounts": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.noPermissionDescription": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorUnauthorized": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.browser": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.created": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.deletesubscription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.device": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablewebpush": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablingwebpusherror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablingwebpusherror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.engine": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.nodevicestoshow": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.operatingsystem": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.deviceDefault": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordId": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdAdd": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdPlaceholder": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdRemove": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "", + "components.Settings.SettingsAbout.Releases.releasedataMissing": "", + "components.Settings.SonarrModal.edit4ksonarr": "", + "components.Settings.SonarrModal.editsonarr": "", + "components.Settings.manualscanJellyfin": "", + "component.BlocklistModal.blocklisting": "", + "components.AirDateBadge.airedrelative": "", + "components.AirDateBadge.airsrelative": "", + "components.Login.tipEmailHasTrailingWhitespace": "", + "components.RequestButton.viewrequest": "", + "components.Settings.Notifications.NotificationsPushover.toastPushoverTestFailed": "", + "components.Settings.Notifications.NotificationsPushover.userTokenTip": "", + "components.Discover.DiscoverSliderEdit.remove": "", + "components.Discover.DiscoverStudio.studioMovies": "", + "components.Discover.RecentlyAddedSlider.recentlyAdded": "", + "components.Discover.TvGenreSlider.tvgenres": "", + "components.DiscoverTvUpcoming.upcomingtv": "", + "components.Discover.createnewslider": "", + "components.Discover.customizediscover": "", + "components.Discover.resetsuccess": "", + "components.IssueDetails.toasteditdescriptionsuccess": "", + "components.IssueDetails.toastissuedeleted": "", + "components.IssueDetails.toastissuedeletefailed": "", + "components.IssueDetails.toaststatusupdated": "", + "components.IssueDetails.toaststatusupdatefailed": "", + "components.Layout.VersionStatus.commitsbehind": "", + "components.Layout.VersionStatus.streamstable": "", + "components.Login.loginwithapp": "", + "components.Login.saving": "", + "components.Login.signinwithplex": "", + "components.RequestButton.declinerequest": "", + "components.Layout.SearchInput.searchPlaceholder": "", + "components.Discover.DiscoverMovies.sortPopularityDesc": "", + "components.Discover.DiscoverMovies.sortReleaseDateAsc": "", + "components.IssueDetails.reopenissue": "", + "components.IssueDetails.reopenissueandcomment": "", + "components.IssueDetails.season": "", + "components.IssueList.IssueItem.issuetype": "", + "components.IssueModal.issueSubtitles": "", + "components.IssueModal.issueVideo": "", + "components.Layout.Sidebar.browsemovies": "", + "components.Login.noadminerror": "", + "components.ManageSlideOver.markallseasons4kavailable": "", + "components.NotificationTypeSelector.mediaautorequestedDescription": "", + "components.Settings.Notifications.toastTelegramTestSending": "", + "components.Settings.SettingsNetwork.validationProxyPort": "", + "components.PermissionEdit.viewrecentDescription": "", + "components.PermissionEdit.viewrequests": "", + "components.PermissionEdit.viewwatchlists": "", + "components.RequestModal.QuotaDisplay.requiredquotaUser": "", + "components.NotificationTypeSelector.notificationTypes": "", + "components.Discover.DiscoverMovies.discovermovies": "", + "components.Login.validationemailformat": "", + "components.ManageSlideOver.markavailable": "", + "components.Settings.Notifications.sendSilentlyTip": "", + "components.Discover.tmdbmoviegenre": "", + "components.Settings.failed": "", + "components.Discover.DiscoverMovieLanguage.languageMovies": "", + "components.IssueList.issues": "", + "components.Login.email": "", + "components.Settings.Notifications.authPass": "", + "components.Settings.RadarrModal.notagoptions": "", + "components.Layout.UserWarnings.profileIncomplete": "", + "components.Layout.VersionStatus.streamdevelop": "", + "components.Login.adminerror": "", + "components.Login.emailAlreadyTaken": "", + "components.Login.jellyfinLocalLoginHint": "", + "components.Login.port": "", + "components.Login.save": "", + "components.Login.saveFailed": "", + "components.Login.servertype": "", + "components.Login.signingin": "", + "components.Login.signinheader": "", + "components.Login.signinwithjellyfin": "", + "components.Login.signinwithoverseerr": "", + "components.Login.tipUsernameHasTrailingWhitespace": "", + "components.Login.title": "", + "components.ManageSlideOver.manageModalClearMedia": "", + "components.ManageSlideOver.manageModalMedia": "", + "components.ManageSlideOver.manageModalMedia4k": "", + "components.ManageSlideOver.mark4kavailable": "", + "components.ManageSlideOver.movie": "", + "components.ManageSlideOver.playedby": "", + "components.ManageSlideOver.plays": "", + "components.ManageSlideOver.removearr4k": "", + "components.MetadataSelector.tvdbLabel": "", + "components.MovieDetails.digitalrelease": "", + "components.MovieDetails.downloadstatus": "", + "components.MovieDetails.tmdbuserscore": "", + "components.MovieDetails.watchlistDeleted": "", + "components.NotificationTypeSelector.adminissuereopenedDescription": "", + "components.NotificationTypeSelector.issuecomment": "", + "components.NotificationTypeSelector.issueresolved": "", + "components.NotificationTypeSelector.mediaAutoApproved": "", + "components.NotificationTypeSelector.mediaapproved": "", + "components.NotificationTypeSelector.mediaapprovedDescription": "", + "components.NotificationTypeSelector.mediaautorequested": "", + "components.NotificationTypeSelector.mediaavailable": "", + "components.NotificationTypeSelector.mediafailedDescription": "", + "components.NotificationTypeSelector.mediarequestedDescription": "", + "components.NotificationTypeSelector.userissuecommentDescription": "", + "components.NotificationTypeSelector.userissuecreatedDescription": "", + "components.NotificationTypeSelector.userissuereopenedDescription": "", + "components.PermissionEdit.autoapproveDescription": "", + "components.PermissionEdit.autoapproveMovies": "", + "components.PermissionEdit.autoapproveSeriesDescription": "", + "components.PermissionEdit.viewissuesDescription": "", + "components.PermissionEdit.viewrecent": "", + "components.PermissionEdit.viewrequestsDescription": "", + "components.PermissionEdit.viewwatchlistsDescription": "", + "components.PersonDetails.crewmember": "", + "components.PersonDetails.lifespan": "", + "components.QuotaSelector.days": "", + "components.RequestBlock.profilechanged": "", + "components.RequestBlock.requestedby": "", + "components.RequestBlock.requestoverrides": "", + "components.RequestBlock.rootfolder": "", + "components.RequestBlock.seasons": "", + "components.RequestBlock.server": "", + "components.RequestCard.cancelrequest": "", + "components.RequestCard.declinerequest": "", + "components.RequestCard.deleterequest": "", + "components.RequestCard.editrequest": "", + "components.RequestCard.failedretry": "", + "components.RequestCard.mediaerror": "", + "components.RequestList.RequestItem.deleterequest": "", + "components.RequestList.RequestItem.editrequest": "", + "components.RequestList.RequestItem.failedmodify": "", + "components.RequestList.RequestItem.failedretry": "", + "components.RequestList.RequestItem.mediaerror": "", + "components.RequestList.RequestItem.profileName": "", + "components.RequestList.RequestItem.seasons": "", + "components.RequestList.RequestItem.tvdbid": "", + "components.RequestList.showallrequests": "", + "components.RequestList.sortDirection": "", + "components.RequestList.sortModified": "", + "components.RequestList.unableToConnect": "", + "components.RequestModal.AdvancedRequester.rootfolder": "", + "components.RequestModal.QuotaDisplay.allowedRequests": "", + "components.RequestModal.QuotaDisplay.allowedRequestsUser": "", + "components.RequestModal.QuotaDisplay.movie": "", + "components.RequestModal.QuotaDisplay.quotaLink": "", + "components.RequestModal.QuotaDisplay.requestsremaining": "", + "components.RequestModal.QuotaDisplay.requiredquota": "", + "components.RequestModal.QuotaDisplay.season": "", + "components.RequestModal.QuotaDisplay.seasonlimit": "", + "components.RequestModal.autoapproval": "", + "components.RequestModal.edit": "", + "components.RequestModal.errorediting": "", + "components.RequestModal.pendingrequest": "", + "components.RequestModal.requestApproved": "", + "components.RequestModal.requestadmin": "", + "components.RequestModal.requestcollection4ktitle": "", + "components.RequestModal.requestcollectiontitle": "", + "components.RequestModal.requestedited": "", + "components.RequestModal.requesterror": "", + "components.RequestModal.requestfrom": "", + "components.RequestModal.requestseasons4k": "", + "components.RequestModal.requestseries4ktitle": "", + "components.RequestModal.requestseriestitle": "", + "components.ResetPassword.confirmpassword": "", + "components.ResetPassword.email": "", + "components.ResetPassword.gobacklogin": "", + "components.Selector.CertificationSelector.errorLoading": "", + "components.Selector.CertificationSelector.selectCertification": "", + "components.Selector.CertificationSelector.selectCountry": "", + "components.Selector.pilot": "", + "components.Selector.searchGenres": "", + "components.Selector.searchKeywords": "", + "components.Selector.searchStatus": "", + "components.Selector.searchStudios": "", + "components.Selector.searchUsers": "", + "components.Selector.showless": "", + "components.Selector.showmore": "", + "components.Selector.starttyping": "", + "components.Settings.Notifications.NotificationsGotify.agentenabled": "", + "components.Settings.Notifications.NotificationsGotify.gotifysettingsfailed": "", + "components.Settings.Notifications.NotificationsGotify.toastGotifyTestFailed": "", + "components.Settings.Notifications.NotificationsGotify.toastGotifyTestSending": "", + "components.Settings.Notifications.NotificationsGotify.toastGotifyTestSuccess": "", + "components.Settings.Notifications.NotificationsGotify.url": "", + "components.Settings.Notifications.NotificationsGotify.validationPriorityRequired": "", + "components.Settings.Notifications.NotificationsGotify.validationTokenRequired": "", + "components.Settings.Notifications.NotificationsGotify.validationTypes": "", + "components.Settings.Notifications.NotificationsGotify.validationUrlTrailingSlash": "", + "components.Settings.Notifications.NotificationsNtfy.embedPoster": "", + "components.Settings.Notifications.NotificationsNtfy.ntfysettingsfailed": "", + "components.Settings.Notifications.NotificationsNtfy.ntfysettingssaved": "", + "components.Settings.Notifications.NotificationsNtfy.password": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestFailed": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestSending": "", + "components.Settings.Notifications.NotificationsNtfy.tokenAuth": "", + "components.Settings.Notifications.NotificationsNtfy.topic": "", + "components.Settings.Notifications.NotificationsNtfy.validationNtfyUrl": "", + "components.Settings.Notifications.NotificationsNtfy.validationPriorityRequired": "", + "components.Settings.Notifications.NotificationsNtfy.validationTypes": "", + "components.Settings.Notifications.NotificationsPushbullet.accessToken": "", + "components.Settings.Notifications.NotificationsPushbullet.accessTokenTip": "", + "components.Settings.Notifications.NotificationsPushbullet.channelTag": "", + "components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestFailed": "", + "components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestSending": "", + "components.Settings.Notifications.NotificationsPushbullet.validationTypes": "", + "components.Settings.Notifications.NotificationsPushover.agentenabled": "", + "components.Settings.Notifications.NotificationsPushover.deviceDefault": "", + "components.Settings.Notifications.NotificationsPushover.embedPoster": "", + "components.Settings.Notifications.NotificationsPushover.validationTypes": "", + "components.Settings.Notifications.NotificationsSlack.agentenabled": "", + "components.Settings.Notifications.NotificationsSlack.embedPoster": "", + "components.Settings.Notifications.NotificationsSlack.slacksettingsfailed": "", + "components.Settings.Notifications.NotificationsSlack.slacksettingssaved": "", + "components.Settings.Notifications.NotificationsSlack.toastSlackTestSending": "", + "components.Settings.Notifications.NotificationsSlack.toastSlackTestSuccess": "", + "components.Settings.Notifications.NotificationsSlack.validationWebhookUrl": "", + "components.Settings.Notifications.NotificationsSlack.webhookUrl": "", + "components.Settings.Notifications.NotificationsSlack.webhookUrlTip": "", + "components.Settings.Notifications.allowselfsigned": "", + "components.Settings.Notifications.authUser": "", + "components.Settings.Notifications.botAPI": "", + "components.Settings.Notifications.botUsernameTip": "", + "components.Settings.Notifications.chatId": "", + "components.Settings.Notifications.chatIdTip": "", + "components.Settings.Notifications.discordsettingsfailed": "", + "components.Settings.Notifications.discordsettingssaved": "", + "components.Settings.Notifications.emailsettingsfailed": "", + "components.Settings.Notifications.emailsettingssaved": "", + "components.Settings.Notifications.embedPoster": "", + "components.Settings.Notifications.enableMentions": "", + "components.Settings.Notifications.encryption": "", + "components.Settings.Notifications.encryptionDefault": "", + "components.Settings.Notifications.encryptionImplicitTls": "", + "components.Settings.Notifications.encryptionNone": "", + "components.Settings.Notifications.encryptionTip": "", + "components.Settings.Notifications.messageThreadId": "", + "components.Settings.Notifications.messageThreadIdTip": "", + "components.Settings.Notifications.pgpPassword": "", + "components.Settings.Notifications.pgpPrivateKeyTip": "", + "components.Settings.Notifications.telegramsettingsfailed": "", + "components.Settings.Notifications.telegramsettingssaved": "", + "components.Settings.Notifications.toastDiscordTestFailed": "", + "components.Settings.Notifications.toastDiscordTestSending": "", + "components.Settings.Notifications.toastEmailTestSuccess": "", + "components.Settings.Notifications.toastTelegramTestSuccess": "", + "components.Settings.Notifications.validationEmail": "", + "components.Settings.Notifications.validationMessageThreadId": "", + "components.Settings.SettingsAbout.aboutseerr": "", + "components.Settings.SettingsJobsCache.process-blocklisted-tags": "", + "components.Settings.SettingsMain.toastApiKeySuccess": "", + "components.Settings.SettingsMain.toastSettingsFailure": "", + "components.Settings.SettingsMain.toastSettingsSuccess": "", + "components.Settings.SettingsMain.validationApplicationTitle": "", + "components.Settings.SettingsMain.validationApplicationUrl": "", + "components.Settings.SettingsNetwork.dnsCacheTip": "", + "components.Settings.SettingsNetwork.network": "", + "components.Settings.SettingsNetwork.networkDisclaimer": "", + "components.Settings.SettingsNetwork.networksettings": "", + "components.Settings.SettingsNetwork.proxyBypassFilter": "", + "components.Settings.SettingsNetwork.proxyBypassFilterTip": "", + "components.Settings.SettingsNetwork.proxyEnabled": "", + "components.Settings.SettingsNetwork.proxyEnabledTip": "", + "components.Settings.SettingsNetwork.trustProxy": "", + "components.Settings.SettingsNetwork.trustProxyTip": "", + "components.Settings.SettingsNetwork.validationApiRequestTimeout": "", + "components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "", + "components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "", + "components.Settings.SettingsUsers.atLeastOneAuth": "", + "components.Settings.SettingsUsers.defaultPermissions": "", + "components.Settings.SettingsUsers.tvRequestLimitLabel": "", + "components.Settings.SettingsUsers.userSettingsDescription": "", + "components.Settings.SettingsUsers.users": "", + "components.Settings.SonarrModal.add": "", + "components.Settings.SonarrModal.animelanguageprofile": "", + "components.Settings.SonarrModal.animequalityprofile": "", + "components.Settings.SonarrModal.apiKeyHelp": "", + "components.Settings.SonarrModal.baseUrlHelp": "", + "components.Settings.SonarrModal.enableSearchHelp": "", + "components.Settings.SonarrModal.externalUrlHelp": "", + "components.Settings.menuGeneralSettings": "", + "components.Settings.menuJellyfinSettings": "", + "components.Settings.tautulliSettings": "", + "components.UserList.importfromplex": "", + "components.PermissionEdit.autoapprove4kSeries": "", + "components.PermissionEdit.manageblocklistDescription": "", + "components.PermissionEdit.request": "", + "components.Settings.SettingsMain.enableSpecialEpisodes": "", + "components.Settings.SettingsMain.general": "", + "i18n.delete": "", + "components.Settings.SettingsMain.hideBlocklisted": "", + "components.Settings.Notifications.NotificationsPushover.accessToken": "", + "components.Settings.Notifications.NotificationsPushover.accessTokenTip": "", + "components.Settings.Notifications.NotificationsPushover.pushoversettingsfailed": "", + "components.Settings.Notifications.NotificationsPushover.toastPushoverTestSending": "", + "components.Settings.Notifications.NotificationsPushover.toastPushoverTestSuccess": "", + "components.Settings.Notifications.NotificationsSlack.toastSlackTestFailed": "", + "components.Settings.Notifications.NotificationsSlack.validationTypes": "", + "components.Settings.Notifications.NotificationsWebPush.agentenabled": "", + "components.Settings.SettingsMain.streamingRegion": "", + "components.Settings.Notifications.botApiTip": "", + "components.Settings.Notifications.botAvatarUrl": "", + "components.Settings.Notifications.botUsername": "", + "components.Settings.Notifications.emailsender": "", + "components.Settings.Notifications.pgpPasswordTip": "", + "components.Settings.Notifications.pgpPrivateKey": "", + "components.Settings.Notifications.sendSilently": "", + "components.Login.urlBaseHelp": "", + "components.Login.validationEmailRequired": "", + "components.Login.validationPortRequired": "", + "components.Login.validationemailrequired": "", + "components.ManageSlideOver.markallseasonsavailable": "", + "components.ManageSlideOver.openarr": "", + "components.ManageSlideOver.tvshow": "", + "components.MovieDetails.imdbuserscore": "", + "components.MovieDetails.mark4kavailable": "", + "components.MovieDetails.play4k": "", + "components.MovieDetails.viewfullcrew": "", + "components.NotificationTypeSelector.issuereopenedDescription": "", + "components.RequestList.RequestItem.unknowntitle": "", + "components.RequestModal.SearchByNameModal.notvdbiddescription": "", + "components.RequestModal.alreadyrequested": "", + "components.RequestModal.approve": "", + "components.RequestModal.cancel": "", + "components.Settings.Notifications.NotificationsPushover.pushoversettingssaved": "", + "components.Settings.Notifications.NotificationsPushover.sound": "", + "components.Settings.Notifications.NotificationsPushover.userToken": "", + "components.Settings.Notifications.NotificationsPushover.validationAccessTokenRequired": "", + "components.Settings.Notifications.NotificationsWebPush.embedPoster": "", + "components.Settings.RadarrModal.create4kradarr": "", + "components.Settings.RadarrModal.createradarr": "", + "components.Settings.SettingsJobsCache.flushcache": "", + "components.Settings.SettingsJobsCache.flushdnscache": "", + "components.Settings.Notifications.toastDiscordTestSuccess": "", + "components.Settings.Notifications.toastEmailTestFailed": "", + "components.Settings.jellyfinSettingsDescription": "", + "components.Settings.Notifications.toastEmailTestSending": "", + "components.Settings.menuServices": "", + "components.Settings.menuUsers": "", + "components.Setup.configjellyfin": "", + "components.Settings.Notifications.toastTelegramTestFailed": "", + "components.Setup.signinMessage": "", + "components.TvDetails.Season.noepisodes": "", + "components.TvDetails.Season.somethingwentwrong": "", + "components.TvDetails.episodeRuntimeMinutes": "", + "components.RequestList.requests": "", + "components.TvDetails.watchlistDeleted": "", + "components.TvDetails.watchlistError": "", + "components.TvDetails.watchlistSuccess": "", + "components.Settings.Notifications.validationSmtpPortRequired": "", + "i18n.approved": "", + "components.Settings.OverrideRuleModal.ruleUpdated": "", + "components.Settings.OverrideRuleModal.selecttags": "", + "components.RequestModal.AdvancedRequester.notagoptions": "", + "components.Settings.RadarrModal.default4kserver": "", + "components.Settings.RadarrModal.hostname": "", + "components.Settings.RadarrModal.inCinemas": "", + "components.Settings.RadarrModal.validationApiKeyRequired": "", + "components.Settings.SettingsAbout.appDataPath": "", + "components.Settings.SettingsAbout.documentation": "", + "components.Settings.SettingsAbout.gettingsupport": "", + "components.Settings.SettingsJobsCache.dnsCache": "", + "components.Settings.SettingsJobsCache.dnscachehits": "", + "components.Settings.SettingsJobsCache.editJobSchedulePrompt": "", + "components.Settings.SettingsJobsCache.editJobScheduleCurrent": "", + "components.Settings.SettingsJobsCache.hitRate": "", + "components.Settings.SettingsJobsCache.imagecache": "", + "components.Settings.SettingsJobsCache.hits": "", + "components.Settings.SettingsLogs.pauseLogs": "", + "components.Settings.SettingsLogs.logsDescription": "", + "components.LanguageSelector.languageServerDefault": "", + "components.MovieDetails.originallanguage": "", + "components.Settings.SettingsMain.streamingRegionTip": "", + "components.Settings.SettingsMain.originallanguageTip": "", + "components.Settings.SettingsNetwork.docs": "", + "components.Settings.Notifications.NotificationsWebPush.webpushsettingssaved": "", + "components.Settings.SettingsUsers.newPlexLogin": "", + "components.RequestModal.QuotaDisplay.quotaLinkUser": "", + "components.Settings.SonarrModal.create4ksonarr": "", + "components.Settings.SonarrModal.loadingTags": "", + "components.Settings.SonarrModal.port": "", + "components.Settings.SonarrModal.qualityprofile": "", + "components.Settings.SonarrModal.seasonfolders": "", + "components.Settings.SonarrModal.servername": "", + "components.Settings.SonarrModal.ssl": "", + "components.Settings.SonarrModal.syncEnabled": "", + "components.Settings.SonarrModal.tagRequests": "", + "components.Settings.apiKey": "", + "components.Settings.blocklistedTagImportInstructions": "", + "components.Settings.blocklistedTagImportTitle": "", + "components.StatusBadge.openinarr": "", + "components.StatusBadge.seasonnumber": "", + "components.StatusBadge.status": "", + "components.TvDetails.addtowatchlist": "", + "components.TvDetails.anime": "", + "components.TvDetails.cast": "", + "components.TvDetails.episodeCount": "", + "components.TvDetails.nextAirDate": "", + "components.TvDetails.recommendations": "", + "components.TvDetails.reportissue": "", + "components.TvDetails.seasons": "", + "components.TvDetails.seasonstitle": "", + "components.TvDetails.similar": "", + "components.TvDetails.status4k": "", + "components.TvDetails.viewfullcrew": "", + "components.UserList.created": "", + "components.UserList.importedfromplex": "", + "components.UserList.newJellyfinsigninenabled": "", + "components.UserList.newplexsigninenabled": "", + "components.UserList.plexuser": "", + "components.RequestModal.SearchByNameModal.nomatches": "", + "components.UserList.totalrequests": "", + "components.UserList.user": "", + "components.UserList.usercreatedfailed": "", + "components.UserList.validationEmail": "", + "components.UserList.validationUsername": "", + "components.UserList.validationpasswordminchars": "", + "components.UserProfile.ProfileHeader.joindate": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.description": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorExists": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorUnknown": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.usernameRequired": "", + "components.UserProfile.UserSettings.UserNotificationSettings.sendSilently": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverApplicationToken": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramChatId": "", + "components.UserProfile.UserSettings.UserPasswordChange.confirmpassword": "", + "components.UserProfile.UserSettings.UserPasswordChange.currentpassword": "", + "components.UserProfile.UserSettings.UserPasswordChange.noPasswordSetOwnAccount": "", + "components.UserProfile.recentrequests": "", + "i18n.deleting": "", + "i18n.delimitedlist": "", + "i18n.error": "", + "i18n.experimental": "", + "components.UserList.bulkedit": "", + "i18n.importing": "", + "i18n.loading": "", + "i18n.movie": "", + "i18n.movies": "", + "i18n.notrequested": "", + "i18n.open": "", + "i18n.partiallyavailable": "", + "i18n.pending": "", + "i18n.restartRequired": "", + "components.NotificationTypeSelector.issuecommentDescription": "", + "i18n.showingresults": "", + "i18n.status": "", + "i18n.test": "", + "i18n.testing": "", + "components.RequestList.RequestItem.tmdbid": "", + "components.RequestModal.AdvancedRequester.languageprofile": "", + "i18n.tvshow": "", + "i18n.tvshows": "", + "components.RequestModal.QuotaDisplay.movielimit": "", + "components.RequestModal.QuotaDisplay.notenoughseasonrequests": "", + "i18n.view": "", + "pages.errormessagewithcode": "", + "pages.internalservererror": "", + "components.Settings.SettingsJobsCache.cachename": "", + "pages.oops": "", + "pages.pagenotfound": "", + "pages.serviceunavailable": "", + "pages.somethingwentwrong": "", + "components.Settings.SettingsNetwork.dnsCacheForceMinTtl": "", + "components.Settings.SonarrModal.createsonarr": "", + "components.Discover.resettodefault": "", + "components.MovieDetails.overview": "", + "components.Settings.Notifications.encryptionOpportunisticTls": "", + "components.Settings.OverrideRuleTile.rootfolder": "", + "components.Settings.OverrideRuleTile.tags": "", + "components.Settings.general": "", + "components.RequestModal.seasonnumber": "", + "components.Settings.Notifications.NotificationsNtfy.agentenabled": "", + "components.Settings.SettingsLogs.logs": "", + "components.Settings.SettingsMain.partialRequestsEnabled": "", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorMinutes": "", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorSeconds": "", + "components.Settings.SettingsNetwork.apiRequestTimeout": "", + "components.Settings.SettingsNetwork.forceIpv4First": "", + "components.Settings.SettingsNetwork.forceIpv4FirstTip": "", + "components.TvDetails.rtaudiencescore": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoversettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverUserKey": "", + "components.Settings.SettingsJobsCache.plex-full-scan": "", + "components.Settings.SettingsJobsCache.process": "", + "i18n.request4k": "", + "components.Discover.CreateSlider.providetmdbnetwork": "", + "components.NotificationTypeSelector.mediaAutoApprovedDescription": "", + "components.PermissionEdit.admin": "", + "components.PermissionEdit.createissues": "", + "components.RequestModal.selectmovies": "", + "components.ResetPassword.passwordreset": "", + "components.Settings.Notifications.NotificationsGotify.token": "", + "components.Settings.OverrideRuleModal.ruleCreated": "", + "components.Settings.SettingsLogs.logDetails": "", + "components.Settings.SettingsNetwork.proxyHostname": "", + "components.Settings.SonarrModal.validationPortRequired": "", + "components.Settings.jellyfinsettings": "", + "components.TvDetails.seasonnumber": "", + "components.UserList.admin": "", + "components.UserList.importfromJellyfin": "", + "components.UserList.userlist": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailure": "", + "component.BlocklistBlock.blocklistdate": "", + "component.BlocklistBlock.blocklistedby": "", + "i18n.failed": "", + "components.Settings.SettingsLogs.extraData": "", + "i18n.approve": "", + "components.Discover.DiscoverMovies.sortPopularityAsc": "", + "components.IssueModal.issueOther": "", + "components.MovieDetails.rtaudiencescore": "", + "components.MovieDetails.rtcriticsscore": "", + "components.MovieDetails.runtime": "", + "components.MovieDetails.showless": "", + "components.MovieDetails.showmore": "", + "components.MovieDetails.similar": "", + "components.MovieDetails.streamingproviders": "", + "components.MovieDetails.studio": "", + "components.MovieDetails.theatricalrelease": "", + "components.MovieDetails.watchlistError": "", + "components.MovieDetails.watchlistSuccess": "", + "components.MovieDetails.watchtrailer": "", + "components.NotificationTypeSelector.adminissuecommentDescription": "", + "components.NotificationTypeSelector.usermediaavailableDescription": "", + "components.NotificationTypeSelector.usermediadeclinedDescription": "", + "components.NotificationTypeSelector.usermediafailedDescription": "", + "components.NotificationTypeSelector.usermediarequestedDescription": "", + "components.PermissionEdit.adminDescription": "", + "components.PermissionEdit.advancedrequest": "", + "components.PermissionEdit.advancedrequestDescription": "", + "components.PermissionEdit.autoapprove": "", + "components.PermissionEdit.autoapprove4kDescription": "", + "components.PermissionEdit.autoapprove4kMovies": "", + "components.PermissionEdit.autoapprove4kMoviesDescription": "", + "components.PermissionEdit.autoapprove4kSeriesDescription": "", + "components.PermissionEdit.autorequest": "", + "components.PermissionEdit.autorequestDescription": "", + "components.PermissionEdit.autorequestMovies": "", + "components.PermissionEdit.autorequestMoviesDescription": "", + "components.PermissionEdit.autorequestSeries": "", + "components.PermissionEdit.autorequestSeriesDescription": "", + "components.PermissionEdit.blocklistedItemsDescription": "", + "components.PermissionEdit.createissuesDescription": "", + "components.PermissionEdit.manageblocklist": "", + "components.PermissionEdit.manageissues": "", + "components.PermissionEdit.managerequests": "", + "components.PermissionEdit.managerequestsDescription": "", + "components.PermissionEdit.request4k": "", + "components.PermissionEdit.request4kDescription": "", + "components.PermissionEdit.request4kMovies": "", + "components.PermissionEdit.request4kMoviesDescription": "", + "components.PermissionEdit.request4kTv": "", + "components.PermissionEdit.request4kTvDescription": "", + "components.QuotaSelector.movieRequests": "", + "components.QuotaSelector.movies": "", + "components.QuotaSelector.seasons": "", + "components.QuotaSelector.tvRequests": "", + "components.QuotaSelector.unlimited": "", + "components.RegionSelector.regionDefault": "", + "components.RegionSelector.regionServerDefault": "", + "components.RequestBlock.approve": "", + "components.RequestBlock.decline": "", + "components.RequestList.sortAdded": "", + "components.RequestBlock.delete": "", + "components.RequestBlock.edit": "", + "components.RequestBlock.languageprofile": "", + "components.RequestBlock.lastmodifiedby": "", + "components.RequestButton.approve4krequests": "", + "components.RequestButton.approverequest": "", + "components.RequestButton.approverequest4k": "", + "components.RequestButton.approverequests": "", + "components.RequestButton.decline4krequests": "", + "components.RequestButton.requestmore": "", + "components.RequestButton.requestmore4k": "", + "components.RequestCard.approverequest": "", + "components.RequestModal.selectseason": "", + "components.Selector.canceled": "", + "components.Settings.SettingsJobsCache.dnsCacheGlobalStats": "", + "components.Setup.continue": "", + "components.Settings.Notifications.agentenabled": "", + "components.Settings.SettingsAbout.Releases.viewongithub": "", + "i18n.usersettings": "", + "components.Settings.SettingsMain.discoverRegion": "", + "components.Settings.SettingsMain.discoverRegionTip": "", + "components.Settings.SettingsMain.generalsettings": "", + "components.Layout.LanguagePicker.displaylanguage": "", + "components.PersonDetails.alsoknownas": "", + "components.PersonDetails.appearsin": "", + "components.Settings.RadarrModal.qualityprofile": "", + "components.Settings.SettingsJobsCache.dnsNoCacheEntries": "", + "components.Settings.noDefaultNon4kServer": "", + "components.Settings.notifications": "", + "components.UserList.localLoginDisabled": "", + "components.UserList.ascending": "", + "components.UserList.create": "", + "components.UserList.createlocaluser": "", + "components.UserList.creating": "", + "components.UserList.descending": "", + "components.UserList.noJellyfinuserstoimport": "", + "components.UserList.sortByType": "", + "components.UserList.toggleSortDirectionAria": "", + "components.UserList.usercreatedsuccess": "", + "components.UserList.userdeleted": "", + "components.UserList.userdeleteerror": "", + "components.UserList.userfail": "", + "components.UserList.username": "", + "components.UserList.users": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.save": "", + "components.UserProfile.UserSettings.UserGeneralSettings.accounttype": "", + "components.Discover.timeWindowDay": "", + "components.Discover.timeWindowWeek": "", + "components.Discover.tmdbnetwork": "", + "components.Discover.tmdbsearch": "", + "components.Discover.tmdbstudio": "", + "components.Discover.tmdbtvgenre": "", + "components.Discover.tmdbtvkeyword": "", + "components.Discover.tmdbtvstreamingservices": "", + "components.DownloadBlock.formattedTitle": "", + "components.IssueDetails.IssueComment.areyousuredelete": "", + "components.IssueDetails.IssueComment.postedbyedited": "", + "components.IssueDetails.IssueComment.validationComment": "", + "components.IssueDetails.IssueDescription.edit": "", + "components.IssueDetails.closeissue": "", + "components.IssueDetails.comments": "", + "components.IssueDetails.deleteissue": "", + "components.IssueDetails.deleteissueconfirm": "", + "components.IssueDetails.episode": "", + "components.IssueDetails.unknownissuetype": "", + "components.IssueList.IssueItem.descriptionpreview": "", + "components.IssueList.sortAdded": "", + "components.IssueModal.CreateIssueModal.toastviewissue": "", + "components.IssueModal.issueAudio": "", + "components.LanguageSelector.originalLanguageDefault": "", + "components.Layout.Sidebar.dashboard": "", + "components.Login.emailtooltip": "", + "components.Login.enablessl": "", + "components.Login.forgotpassword": "", + "components.Login.hostname": "", + "components.Login.signin": "", + "components.Login.validationUrlBaseLeadingSlash": "", + "components.Login.validationUrlBaseTrailingSlash": "", + "components.ManageSlideOver.manageModalAdvanced": "", + "components.ManageSlideOver.openarr4k": "", + "components.ManageSlideOver.removearr": "", + "components.MovieDetails.markavailable": "", + "components.MovieDetails.openradarr4k": "", + "components.MovieDetails.releasedate": "", + "components.MovieDetails.removefromwatchlist": "", + "components.MovieDetails.reportissue": "", + "components.MovieDetails.revenue": "", + "components.NotificationTypeSelector.issuecreated": "", + "components.NotificationTypeSelector.issuecreatedDescription": "", + "components.NotificationTypeSelector.issuereopened": "", + "components.NotificationTypeSelector.issueresolvedDescription": "", + "components.NotificationTypeSelector.mediaavailableDescription": "", + "components.NotificationTypeSelector.mediadeclined": "", + "components.NotificationTypeSelector.mediadeclinedDescription": "", + "components.NotificationTypeSelector.mediafailed": "", + "components.NotificationTypeSelector.mediarequested": "", + "components.NotificationTypeSelector.userissueresolvedDescription": "", + "components.NotificationTypeSelector.usermediaAutoApprovedDescription": "", + "components.NotificationTypeSelector.usermediaapprovedDescription": "", + "components.PermissionEdit.autoapprove4k": "", + "components.PermissionEdit.blocklistedItems": "", + "components.PermissionEdit.manageissuesDescription": "", + "components.PermissionEdit.requestMovies": "", + "components.PermissionEdit.requestTv": "", + "components.PersonDetails.ascharacter": "", + "components.PersonDetails.birthdate": "", + "components.RequestButton.declinerequests": "", + "components.RequestButton.viewrequest4k": "", + "components.RequestList.RequestItem.removearr": "", + "components.RequestList.RequestItem.requested": "", + "components.RequestList.RequestItem.requesteddate": "", + "components.Settings.SettingsMain.validationApplicationUrlTrailingSlash": "", + "components.Settings.SettingsMain.validationUrl": "", + "components.Settings.SettingsNetwork.networksettingsDescription": "", + "components.Settings.nooptions": "", + "components.Settings.notTested": "", + "components.Settings.notrunning": "", + "components.Settings.plexlibraries": "", + "components.Settings.plexlibrariesDescription": "", + "components.Settings.save": "", + "components.Settings.saving": "", + "components.Settings.scan": "", + "components.Settings.serverpresetLoad": "", + "components.Settings.serverpresetManualMessage": "", + "components.Settings.serverpresetRefreshing": "", + "components.Settings.services": "", + "components.Settings.syncJellyfin": "", + "components.UserProfile.UserSettings.UserPasswordChange.toastSettingsFailure": "", + "components.UserProfile.UserSettings.UserPasswordChange.toastSettingsSuccess": "", + "components.UserProfile.UserSettings.UserPasswordChange.validationNewPassword": "", + "components.CollectionDetails.removefromblocklistpartialcount": "", + "components.Discover.CreateSlider.addfail": "", + "components.Discover.CreateSlider.addsuccess": "", + "components.Discover.CreateSlider.editSlider": "", + "components.Discover.CreateSlider.editfail": "", + "components.Discover.FilterSlideover.status": "", + "components.Login.back": "", + "components.MovieDetails.cast": "", + "components.PermissionEdit.requestDescription": "", + "components.PermissionEdit.requestMoviesDescription": "", + "components.PermissionEdit.requestTvDescription": "", + "components.PermissionEdit.users": "", + "components.PermissionEdit.usersDescription": "", + "components.PermissionEdit.viewblocklistedItems": "", + "components.PermissionEdit.viewblocklistedItemsDescription": "", + "components.PermissionEdit.viewissues": "", + "components.RequestList.RequestItem.modified": "", + "components.Settings.SettingsAbout.version": "", + "components.Settings.SettingsUsers.mediaServerLogin": "", + "components.Settings.SettingsUsers.toastSettingsFailure": "", + "components.Settings.SettingsUsers.userSettings": "", + "components.Settings.SonarrModal.animeSeriesType": "", + "components.Settings.SonarrModal.animeTags": "", + "components.Settings.librariesRemaining": "", + "components.Settings.menuJobs": "", + "components.Settings.menuLogs": "", + "components.Settings.menuMetadataProviders": "", + "components.Settings.metadataSettingsSaved": "", + "components.Settings.no": "", + "components.Settings.noDefault4kServer": "", + "components.Settings.noDefaultServer": "", + "components.Settings.noSpecialCharacters": "", + "components.Settings.notificationAgentSettingsDescription": "" +} diff --git a/src/i18n/locale/sl.json b/src/i18n/locale/sl.json index f4da326fae..8fcedac237 100644 --- a/src/i18n/locale/sl.json +++ b/src/i18n/locale/sl.json @@ -55,5 +55,1618 @@ "components.Discover.DiscoverTv.sortTitleDesc": "Naslov (a-ž) ↑", "components.Discover.DiscoverSliderEdit.remove": "Odstrani", "components.Discover.DiscoverTv.sortTmdbRatingAsc": "Ocena TMDB ↓", - "components.Discover.DiscoverTvGenre.genreSeries": "{genre} Series" + "components.Discover.DiscoverTvGenre.genreSeries": "{genre} Series", + "components.IssueList.sortAdded": "", + "components.Login.initialsignin": "", + "components.Settings.Notifications.usePublicLogo": "", + "components.Settings.Notifications.usePublicLogoTip": "", + "components.Settings.OverrideRuleModal.selectQualityProfile": "", + "components.Settings.OverrideRuleModal.service": "", + "components.Settings.OverrideRuleModal.serviceDescription": "", + "components.Settings.OverrideRuleModal.settings": "", + "components.Settings.OverrideRuleModal.settingsDescription": "", + "components.Settings.OverrideRuleModal.tags": "", + "components.Settings.RadarrModal.port": "", + "components.Settings.RadarrModal.released": "", + "components.Settings.RadarrModal.selectMinimumAvailability": "", + "components.Settings.RadarrModal.server4kHelp": "", + "components.Settings.RadarrModal.ssl": "", + "components.Settings.RadarrModal.testFirstTags": "", + "components.Settings.RadarrModal.toastRadarrTestFailure": "", + "components.Settings.RadarrModal.toastRadarrTestSuccess": "", + "components.Settings.RadarrModal.validationApiKeyRequired": "", + "components.Settings.RadarrModal.validationApplicationUrl": "", + "components.Settings.RadarrModal.validationApplicationUrlTrailingSlash": "", + "components.Settings.RadarrModal.validationBaseUrlLeadingSlash": "", + "components.Settings.RadarrModal.validationBaseUrlTrailingSlash": "", + "components.Settings.RadarrModal.validationHostnameRequired": "", + "components.Settings.RadarrModal.validationMinimumAvailabilityRequired": "", + "components.Settings.SettingsAbout.Releases.releasedataMissing": "", + "components.Settings.SettingsAbout.Releases.versionChangelog": "", + "components.Settings.SettingsAbout.Releases.viewchangelog": "", + "components.Settings.SettingsAbout.appDataPath": "", + "components.Settings.SettingsAbout.contribute": "", + "components.Settings.SettingsAbout.documentation": "", + "components.Settings.SettingsAbout.gettingsupport": "", + "components.Settings.SettingsAbout.githubdiscussions": "", + "components.Settings.SettingsJobsCache.cache": "", + "components.Settings.SettingsJobsCache.cacheDescription": "", + "components.Settings.SettingsJobsCache.cachehits": "", + "components.Settings.SettingsJobsCache.cachekeys": "", + "components.Settings.SettingsJobsCache.cacheksize": "", + "components.Settings.SettingsJobsCache.cachemisses": "", + "components.Settings.SettingsJobsCache.cachename": "", + "components.Settings.SettingsJobsCache.canceljob": "", + "components.Settings.SettingsJobsCache.command": "", + "components.Settings.SettingsJobsCache.dnsCacheGlobalStatsDescription": "", + "components.Settings.SettingsJobsCache.dnscacheactiveaddress": "", + "components.Settings.SettingsJobsCache.dnscacheage": "", + "components.Settings.SettingsJobsCache.dnscacheflushed": "", + "components.Settings.SettingsJobsCache.dnscachehits": "", + "components.Settings.SettingsJobsCache.dnscachemisses": "", + "components.Settings.SettingsJobsCache.dnscachename": "", + "components.Settings.SettingsJobsCache.download-sync": "", + "components.Settings.SettingsJobsCache.download-sync-reset": "", + "components.Settings.SettingsJobsCache.editJobSchedule": "", + "components.Settings.SettingsJobsCache.editJobScheduleCurrent": "", + "components.Settings.SettingsJobsCache.editJobSchedulePrompt": "", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorDays": "", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorHours": "", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorMinutes": "", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorSeconds": "", + "components.Settings.SettingsJobsCache.flushcache": "", + "components.Settings.SettingsJobsCache.flushdnscache": "", + "components.Settings.SettingsJobsCache.hitRate": "", + "components.Settings.SettingsJobsCache.hits": "", + "components.Settings.SettingsJobsCache.imagecache": "", + "components.Settings.SettingsJobsCache.imagecacheDescription": "", + "components.Settings.SettingsJobsCache.imagecachecount": "", + "components.Settings.SettingsJobsCache.jellyfin-full-scan": "", + "components.Settings.SettingsJobsCache.jobname": "", + "components.Settings.SettingsJobsCache.jobsDescription": "", + "components.Settings.SettingsJobsCache.jobstarted": "", + "components.Settings.SettingsJobsCache.nextexecution": "", + "components.Settings.SettingsJobsCache.plex-full-scan": "", + "components.Settings.SettingsJobsCache.plex-recently-added-scan": "", + "components.Settings.SettingsJobsCache.plex-refresh-token": "", + "components.Settings.SettingsJobsCache.plex-watchlist-sync": "", + "components.Settings.SettingsJobsCache.radarr-scan": "", + "components.Settings.SettingsJobsCache.runnow": "", + "components.Settings.SettingsMain.blocklistLanguage": "", + "components.Settings.SettingsMain.hideAvailableTip": "", + "components.Settings.SettingsMain.hideBlocklistedTip": "", + "components.Settings.SettingsMain.originallanguage": "", + "components.Settings.SettingsMain.originallanguageTip": "", + "components.Settings.SettingsMain.partialRequestsEnabled": "", + "components.Settings.SettingsMain.toastSettingsFailure": "", + "components.Settings.SettingsMain.validationApplicationTitle": "", + "components.Settings.SettingsMain.validationUrl": "", + "components.Settings.SettingsMain.validationUrlTrailingSlash": "", + "components.Settings.SettingsMain.youtubeUrl": "", + "components.Settings.SettingsMain.youtubeUrlTip": "", + "components.Settings.SettingsNetwork.apiRequestTimeoutTip": "", + "components.Settings.SettingsNetwork.csrfProtection": "", + "components.Settings.SettingsNetwork.csrfProtectionHoverTip": "", + "components.Settings.SettingsNetwork.csrfProtectionTip": "", + "components.Settings.SettingsNetwork.dnsCacheTip": "", + "components.Settings.SettingsNetwork.docs": "", + "components.Settings.SettingsNetwork.forceIpv4First": "", + "components.Settings.SettingsNetwork.forceIpv4FirstTip": "", + "components.Settings.SettingsNetwork.networkDisclaimer": "", + "components.Settings.SettingsNetwork.networksettings": "", + "components.Settings.SettingsNetwork.networksettingsDescription": "", + "components.Settings.SettingsNetwork.proxyBypassFilter": "", + "components.Settings.SettingsNetwork.proxyBypassFilterTip": "", + "components.Settings.SettingsNetwork.proxyBypassLocalAddresses": "", + "components.Settings.SettingsNetwork.proxyEnabled": "", + "components.Settings.SettingsNetwork.proxyEnabledTip": "", + "components.Settings.SettingsNetwork.proxyHostname": "", + "components.Settings.SettingsNetwork.proxyPassword": "", + "components.Settings.SettingsNetwork.proxyPort": "", + "components.Settings.SettingsNetwork.proxySsl": "", + "components.Settings.SettingsNetwork.proxyUser": "", + "components.Settings.SettingsNetwork.toastSettingsFailure": "", + "components.Settings.SettingsNetwork.toastSettingsSuccess": "", + "components.Settings.SettingsNetwork.trustProxy": "", + "components.Settings.SettingsNetwork.trustProxyTip": "", + "components.Settings.SettingsNetwork.validationApiRequestTimeout": "", + "components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "", + "components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "", + "components.Settings.SettingsNetwork.validationProxyPort": "", + "components.Settings.SettingsUsers.atLeastOneAuth": "", + "components.Settings.SettingsUsers.defaultPermissions": "", + "components.Settings.SettingsUsers.defaultPermissionsTip": "", + "components.Settings.SettingsUsers.disabledMediaServerLoginWarning": "", + "components.Settings.SettingsUsers.localLogin": "", + "components.Settings.SettingsUsers.localLoginTip": "", + "components.Settings.SettingsUsers.tvRequestLimitLabel": "", + "components.Settings.SettingsUsers.userSettings": "", + "components.Settings.SettingsUsers.userSettingsDescription": "", + "components.Settings.SonarrModal.add": "", + "components.Settings.SonarrModal.animeSeriesType": "", + "components.Settings.SonarrModal.animelanguageprofile": "", + "components.Settings.SonarrModal.animequalityprofile": "", + "components.Settings.SonarrModal.animerootfolder": "", + "components.Settings.SonarrModal.apiKeyHelp": "", + "components.Settings.SonarrModal.baseUrl": "", + "components.Settings.SonarrModal.baseUrlHelp": "", + "components.Settings.SonarrModal.create4ksonarr": "", + "components.Settings.SonarrModal.createsonarr": "", + "components.Settings.SonarrModal.default4kserver": "", + "components.Settings.SonarrModal.defaultserver": "", + "components.Settings.SonarrModal.edit4ksonarr": "", + "components.Settings.SonarrModal.editsonarr": "", + "components.Settings.SonarrModal.enableSearch": "", + "components.Settings.SonarrModal.enableSearchHelp": "", + "components.Settings.SonarrModal.externalUrl": "", + "components.Settings.SonarrModal.externalUrlHelp": "", + "components.Settings.SonarrModal.hostname": "", + "components.Settings.SonarrModal.languageprofile": "", + "components.Settings.SonarrModal.loadinglanguageprofiles": "", + "components.Settings.SonarrModal.loadingrootfolders": "", + "components.Settings.SonarrModal.syncEnabledHelp": "", + "components.Settings.SonarrModal.tagRequests": "", + "components.Settings.SonarrModal.tagRequestsInfo": "", + "components.Settings.SonarrModal.tags": "", + "components.Settings.SonarrModal.testFirstLanguageProfiles": "", + "components.Settings.SonarrModal.testFirstQualityProfiles": "", + "components.Settings.SonarrModal.testFirstTags": "", + "components.Settings.SonarrModal.toastSonarrTestSuccess": "", + "components.Settings.SonarrModal.validationBaseUrlLeadingSlash": "", + "components.Settings.SonarrModal.validationBaseUrlTrailingSlash": "", + "components.Settings.SonarrModal.validationHostnameRequired": "", + "components.Settings.SonarrModal.validationLanguageProfileRequired": "", + "components.Settings.SonarrModal.validationNameRequired": "", + "components.Settings.SonarrModal.validationPortRequired": "", + "components.Settings.SonarrModal.validationProfileRequired": "", + "components.Settings.activeProfile": "", + "components.Settings.addradarr": "", + "components.Settings.address": "", + "components.Settings.addrule": "", + "components.Settings.addsonarr": "", + "components.Settings.advancedTooltip": "", + "components.Settings.allChosenProvidersAreOperational": "", + "components.Settings.animeMetadataProvider": "", + "components.Settings.apiKey": "", + "components.Settings.blocklistedTagImportInstructions": "", + "components.Settings.blocklistedTagImportTitle": "", + "components.Settings.chooseProvider": "", + "components.Settings.clearBlocklistedTagsConfirm": "", + "components.Settings.clickTest": "", + "components.Settings.connectionTestFailed": "", + "components.Settings.copyBlocklistedTagsEmpty": "", + "components.Settings.copyBlocklistedTagsTip": "", + "components.Settings.email": "", + "components.Settings.enablessl": "", + "components.Settings.experimentalTooltip": "", + "components.Settings.externalUrl": "", + "components.Settings.failed": "", + "components.Settings.failedToSaveMetadataSettings": "", + "components.Settings.general": "", + "components.Settings.importBlocklistedTagsTip": "", + "components.Settings.invalidKeyword": "", + "components.Settings.invalidurlerror": "", + "components.Settings.is4k": "", + "components.Settings.jellyfinForgotPasswordUrl": "", + "components.Settings.jellyfinSettings": "", + "components.Settings.jellyfinSettingsDescription": "", + "components.Settings.jellyfinSettingsSuccess": "", + "components.Settings.jellyfinSyncFailedGenericError": "", + "components.Settings.jellyfinSyncFailedNoLibrariesFound": "", + "components.Settings.jellyfinlibrariesDescription": "", + "components.Settings.jellyfinsettings": "", + "components.Settings.jellyfinsettingsDescription": "", + "components.Settings.manualscan": "", + "components.Settings.manualscanDescription": "", + "components.Settings.menuNetwork": "", + "components.Settings.menuNotifications": "", + "components.Settings.menuPlexSettings": "", + "components.Settings.menuServices": "", + "components.Settings.menuUsers": "", + "components.Settings.metadataProviderSelection": "", + "components.Settings.metadataProviderSettings": "", + "components.Settings.metadataSettingsSaved": "", + "components.Settings.noDefault4kServer": "", + "components.Settings.noDefaultNon4kServer": "", + "components.Settings.noDefaultServer": "", + "components.Settings.noSpecialCharacters": "", + "components.Settings.nooptions": "", + "components.Settings.notTested": "", + "components.Settings.notificationAgentSettingsDescription": "", + "components.Settings.notificationsettings": "", + "components.Settings.notrunning": "", + "components.Settings.operational": "", + "components.Settings.overrideRules": "", + "components.Settings.overrideRulesDescription": "", + "components.Settings.plexlibraries": "", + "components.Settings.plexlibrariesDescription": "", + "components.Settings.plexsettings": "", + "components.Settings.plexsettingsDescription": "", + "components.Settings.port": "", + "components.Settings.providerStatus": "", + "components.Settings.radarrsettings": "", + "components.Settings.saving": "", + "components.Settings.scan": "", + "components.Settings.scanbackground": "", + "components.Settings.searchKeywords": "", + "components.Settings.seriesMetadataProvider": "", + "components.Settings.serverRemote": "", + "components.Settings.serverSecure": "", + "components.Settings.serverpreset": "", + "components.Settings.serverpresetLoad": "", + "components.Settings.services": "", + "components.Settings.settingUpPlexDescription": "", + "components.Settings.sonarrsettings": "", + "components.Settings.ssl": "", + "components.Settings.startscan": "", + "components.Settings.starttyping": "", + "components.Settings.syncJellyfin": "", + "components.Settings.syncing": "", + "components.Settings.tautulliApiKey": "", + "components.Settings.tautulliSettingsDescription": "", + "components.Settings.timeout": "", + "components.Settings.tip": "", + "components.Settings.tmdbProviderDoesnotWork": "", + "components.Settings.toastPlexConnecting": "", + "components.Settings.toastPlexConnectingFailure": "", + "components.Settings.toastPlexConnectingSuccess": "", + "components.Settings.toastPlexRefresh": "", + "components.Settings.toastPlexRefreshFailure": "", + "components.Settings.toastPlexRefreshSuccess": "", + "components.Settings.yes": "", + "components.Setup.continue": "", + "components.Setup.finish": "", + "components.Setup.finishing": "", + "components.Setup.librarieserror": "", + "components.Setup.servertype": "", + "components.Setup.signinWithEmby": "", + "components.Setup.subtitle": "", + "components.StatusBadge.managemedia": "", + "components.StatusBadge.openinarr": "", + "components.StatusBadge.playonplex": "", + "components.StatusBadge.seasonepisodenumber": "", + "components.StatusBadge.seasonnumber": "", + "components.StatusBadge.status": "", + "components.StatusBadge.status4k": "", + "components.StatusChecker.appUpdated": "", + "components.StatusChecker.appUpdatedDescription": "", + "components.StatusChecker.reloadApp": "", + "components.StatusChecker.restartRequired": "", + "components.TitleCard.watchlistCancel": "", + "components.TitleCard.watchlistDeleted": "", + "components.TitleCard.watchlistError": "", + "components.TvDetails.Season.noepisodes": "", + "components.TvDetails.Season.somethingwentwrong": "", + "components.TvDetails.TvCast.fullseriescast": "", + "components.TvDetails.TvCrew.fullseriescrew": "", + "components.RequestCard.failedmodify": "", + "components.RequestCard.mediaerror": "", + "components.RequestCard.tmdbid": "", + "components.RequestCard.tvdbid": "", + "components.RequestList.RequestItem.editrequest": "", + "components.RequestList.RequestItem.failedmodify": "", + "components.RequestList.RequestItem.failedretry": "", + "components.RequestList.RequestItem.mediaerror": "", + "components.RequestModal.pendingapproval": "", + "components.RequestModal.pendingrequest": "", + "components.RequestModal.requestApproved": "", + "components.RequestModal.requestCancel": "", + "components.RequestModal.requestSuccess": "", + "components.ResetPassword.validationpasswordrequired": "", + "components.Search.search": "", + "components.Search.searchresults": "", + "components.Selector.CertificationSelector.errorLoading": "", + "components.Selector.CertificationSelector.noOptions": "", + "components.Selector.CertificationSelector.selectCertification": "", + "components.TvDetails.addtowatchlist": "", + "components.TvDetails.episodeCount": "", + "components.TvDetails.episodeRuntime": "", + "components.TvDetails.episodeRuntimeMinutes": "", + "components.TvDetails.manageseries": "", + "components.TvDetails.network": "", + "components.TvDetails.recommendations": "", + "components.TvDetails.removefromwatchlist": "", + "components.TvDetails.reportissue": "", + "components.TvDetails.rtaudiencescore": "", + "components.TvDetails.rtcriticsscore": "", + "components.Settings.Notifications.NotificationsGotify.toastGotifyTestFailed": "", + "components.Settings.Notifications.NotificationsGotify.toastGotifyTestSending": "", + "components.Settings.Notifications.NotificationsGotify.toastGotifyTestSuccess": "", + "components.TvDetails.seasonnumber": "", + "components.TvDetails.seasons": "", + "components.TvDetails.seasonstitle": "", + "components.TvDetails.showtype": "", + "components.TvDetails.similar": "", + "components.TvDetails.status4k": "", + "components.TvDetails.streamingproviders": "", + "components.TvDetails.tmdbuserscore": "", + "components.TvDetails.viewfullcrew": "", + "components.TvDetails.watchlistDeleted": "", + "components.TvDetails.watchlistError": "", + "components.Settings.Notifications.NotificationsNtfy.ntfysettingsfailed": "", + "components.Settings.Notifications.NotificationsNtfy.ntfysettingssaved": "", + "components.TvDetails.watchlistSuccess": "", + "components.TvDetails.watchtrailer": "", + "components.UserList.accounttype": "", + "components.UserList.admin": "", + "components.UserList.ascending": "", + "components.UserList.create": "", + "components.UserList.created": "", + "components.UserList.createlocaluser": "", + "components.UserList.creating": "", + "components.UserList.descending": "", + "components.UserList.email": "", + "components.UserList.importedPlexUsersNoPassword": "", + "components.UserList.importedUsersNoPassword": "", + "components.UserList.importedfromJellyfin": "", + "components.UserList.importedfromplex": "", + "components.UserList.importfromJellyfin": "", + "components.UserList.importfromJellyfinerror": "", + "components.UserList.importfrommediaserver": "", + "components.UserList.importfromplex": "", + "components.UserList.importfromplexerror": "", + "components.UserList.localLoginDisabled": "", + "components.UserList.localuser": "", + "components.UserList.newJellyfinsigninenabled": "", + "components.UserList.newplexsigninenabled": "", + "components.UserList.passwordinfodescription": "", + "components.UserList.plexuser": "", + "components.Settings.Notifications.NotificationsPushover.validationAccessTokenRequired": "", + "components.UserList.sortBy": "", + "components.UserList.sortByJoined": "", + "components.UserList.sortByRequests": "", + "components.UserList.sortByRole": "", + "components.UserList.sortByType": "", + "components.UserList.sortByUser": "", + "components.UserList.toggleSortDirection": "", + "components.UserList.toggleSortDirectionAria": "", + "components.UserList.totalrequests": "", + "components.UserList.user": "", + "components.UserList.usercreatedfailed": "", + "components.UserList.usercreatedfailedexisting": "", + "components.UserList.userdeleted": "", + "components.UserList.users": "", + "components.UserList.userssaved": "", + "components.UserList.validationEmail": "", + "components.UserList.validationUsername": "", + "components.UserList.validationpasswordminchars": "", + "components.UserProfile.ProfileHeader.profile": "", + "components.UserProfile.ProfileHeader.userid": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.description": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorExists": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorUnauthorized": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorUnknown": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.password": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.passwordRequired": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.save": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.saving": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.title": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.username": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.usernameRequired": "", + "components.UserProfile.UserSettings.UserGeneralSettings.accounttype": "", + "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegion": "", + "components.Settings.Notifications.toastTelegramTestSuccess": "", + "components.Settings.Notifications.userEmailRequired": "", + "components.UserProfile.UserSettings.UserGeneralSettings.displayName": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.emailsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.emailsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.notifications": "", + "components.UserProfile.UserSettings.UserNotificationSettings.notificationsettings": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pgpPublicKeyTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessToken": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessTokenTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverApplicationToken": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverApplicationTokenTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverUserKey": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverUserKeyTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoversettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoversettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadIdTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPushbulletAccessToken": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverApplicationToken": "", + "components.UserProfile.UserSettings.UserNotificationSettings.webpush": "", + "components.UserProfile.UserSettings.UserPasswordChange.confirmpassword": "", + "components.UserProfile.UserSettings.UserPasswordChange.currentpassword": "", + "components.UserProfile.UserSettings.UserPasswordChange.localPasswordDescription": "", + "components.UserProfile.UserSettings.UserPasswordChange.newpassword": "", + "components.UserProfile.UserSettings.UserPasswordChange.noPasswordSet": "", + "components.UserProfile.UserSettings.UserPasswordChange.noPasswordSetOwnAccount": "", + "components.UserProfile.UserSettings.UserPasswordChange.nopermissionDescription": "", + "components.UserProfile.UserSettings.UserPasswordChange.password": "", + "components.UserProfile.UserSettings.UserPasswordChange.toastSettingsFailure": "", + "components.UserProfile.UserSettings.UserPasswordChange.toastSettingsFailureVerifyCurrent": "", + "components.UserProfile.UserSettings.UserPasswordChange.toastSettingsSuccess": "", + "components.UserProfile.UserSettings.UserPasswordChange.validationConfirmPassword": "", + "components.UserProfile.UserSettings.UserPasswordChange.validationCurrentPassword": "", + "components.UserProfile.UserSettings.UserPasswordChange.validationNewPassword": "", + "components.UserProfile.UserSettings.UserPermissions.toastSettingsFailure": "", + "components.UserProfile.UserSettings.UserPermissions.toastSettingsSuccess": "", + "components.UserProfile.UserSettings.UserPermissions.unauthorizedDescription": "", + "components.UserProfile.UserSettings.menuChangePass": "", + "components.UserProfile.UserSettings.menuGeneralSettings": "", + "components.UserProfile.UserSettings.menuLinkedAccounts": "", + "components.UserProfile.movierequests": "", + "components.UserProfile.pastdays": "", + "components.Settings.hostname": "", + "components.UserProfile.recentlywatched": "", + "components.UserProfile.recentrequests": "", + "components.UserProfile.requestsperdays": "", + "components.UserProfile.seriesrequest": "", + "i18n.all": "", + "i18n.approve": "", + "i18n.approved": "", + "i18n.areyousure": "", + "i18n.blocklisted": "", + "i18n.completed": "", + "i18n.deleted": "", + "i18n.deleting": "", + "i18n.delimitedlist": "", + "i18n.edit": "", + "i18n.error": "", + "i18n.experimental": "", + "i18n.failed": "", + "i18n.import": "", + "i18n.importing": "", + "i18n.loading": "", + "i18n.next": "", + "components.Settings.menuJobs": "", + "i18n.notificationLocale": "", + "i18n.notrequested": "", + "i18n.partiallyavailable": "", + "i18n.partiallyblocklisted": "", + "i18n.pending": "", + "i18n.previous": "", + "i18n.processing": "", + "i18n.removeFromBlocklistSuccess": "", + "i18n.removefromBlocklist": "", + "i18n.request": "", + "i18n.request4k": "", + "i18n.requested": "", + "i18n.requesting": "", + "i18n.resolved": "", + "i18n.restartRequired": "", + "i18n.resultsperpage": "", + "i18n.retrying": "", + "i18n.save": "", + "i18n.saving": "", + "i18n.specials": "", + "pages.returnHome": "", + "components.Settings.validationUrl": "", + "components.UserList.noJellyfinuserstoimport": "", + "components.UserList.nouserstoimport": "", + "components.UserList.password": "", + "components.UserList.usercreatedsuccess": "", + "components.UserProfile.UserSettings.UserGeneralSettings.save": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.type": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.unknown": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeendisabled": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "", + "components.UserProfile.plexwatchlist": "", + "i18n.tvshows": "", + "i18n.unavailable": "", + "components.Discover.createnewslider": "", + "components.Discover.customizediscover": "", + "components.Discover.discover": "", + "components.Discover.emptywatchlist": "", + "components.Discover.moviegenres": "", + "components.Discover.networks": "", + "components.Discover.plexwatchlist": "", + "components.Discover.popularmovies": "", + "components.Discover.populartv": "", + "components.Discover.recentlyAdded": "", + "components.Discover.resetfailed": "", + "components.Discover.resetsuccess": "", + "components.Discover.resettodefault": "", + "components.Discover.resetwarning": "", + "components.Discover.stopediting": "", + "components.Discover.tmdbmoviegenre": "", + "components.Discover.tmdbmoviekeyword": "", + "components.Discover.tmdbtvkeyword": "", + "components.Discover.tmdbtvstreamingservices": "", + "components.Discover.tvgenres": "", + "components.Discover.upcoming": "", + "components.Discover.upcomingmovies": "", + "components.Discover.upcomingtv": "", + "components.Discover.updatefailed": "", + "components.Discover.updatesuccess": "", + "components.Settings.Notifications.NotificationsWebPush.agentenabled": "", + "components.Settings.Notifications.NotificationsWebPush.embedPoster": "", + "components.Settings.Notifications.NotificationsWebPush.toastWebPushTestFailed": "", + "components.Settings.Notifications.NotificationsWebPush.toastWebPushTestSending": "", + "components.Settings.Notifications.NotificationsWebPush.toastWebPushTestSuccess": "", + "components.Settings.Notifications.NotificationsWebhook.agentenabled": "", + "components.Settings.Notifications.NotificationsWebhook.authheader": "", + "components.Settings.Notifications.NotificationsWebhook.customHeaders": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersAdd": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersAuthConflict": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersIncomplete": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersKey": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersRemove": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersTip": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersValue": "", + "components.Settings.Notifications.NotificationsWebhook.customJson": "", + "components.Settings.Notifications.NotificationsWebhook.resetPayload": "", + "components.Settings.Notifications.NotificationsWebhook.resetPayloadSuccess": "", + "components.Settings.Notifications.NotificationsWebhook.supportVariables": "", + "components.Settings.Notifications.NotificationsWebhook.supportVariablesTip": "", + "components.Settings.Notifications.NotificationsWebhook.toastWebhookTestSending": "", + "components.Settings.Notifications.NotificationsWebhook.toastWebhookTestSuccess": "", + "components.Settings.Notifications.NotificationsWebhook.validationJsonPayloadRequired": "", + "components.Settings.Notifications.NotificationsWebhook.validationTypes": "", + "components.Settings.Notifications.NotificationsWebhook.validationWebhookUrl": "", + "components.Settings.Notifications.NotificationsWebhook.webhookUrl": "", + "components.Settings.Notifications.NotificationsWebhook.webhookUrlTip": "", + "components.Settings.Notifications.NotificationsWebhook.webhooksettingsfailed": "", + "components.Settings.Notifications.NotificationsWebhook.webhooksettingssaved": "", + "components.Settings.Notifications.useUserLocale": "", + "components.Settings.Notifications.validationBotAPIRequired": "", + "components.Settings.Notifications.validationEmail": "", + "components.Settings.Notifications.validationMessageThreadId": "", + "components.Settings.Notifications.validationPgpPassword": "", + "components.Settings.Notifications.validationPgpPrivateKey": "", + "components.Settings.Notifications.validationSmtpHostRequired": "", + "components.Settings.Notifications.validationSmtpPortRequired": "", + "components.Settings.Notifications.validationTypes": "", + "components.Settings.Notifications.validationUrl": "", + "components.Settings.Notifications.validationWebhookRoleId": "", + "components.Settings.Notifications.webhookRoleId": "", + "components.Settings.OverrideRuleModal.conditionsDescription": "", + "components.Settings.OverrideRuleModal.keywords": "", + "components.Settings.OverrideRuleModal.notagoptions": "", + "components.Settings.OverrideRuleModal.qualityprofile": "", + "components.Settings.OverrideRuleModal.rootfolder": "", + "components.Settings.OverrideRuleModal.ruleCreated": "", + "components.Settings.OverrideRuleModal.ruleUpdated": "", + "components.Settings.OverrideRuleModal.selectRootFolder": "", + "components.Settings.OverrideRuleModal.selectService": "", + "components.Settings.OverrideRuleModal.selecttags": "", + "components.Settings.OverrideRuleModal.users": "", + "components.Settings.OverrideRuleTile.conditions": "", + "components.Settings.OverrideRuleTile.genre": "", + "components.Settings.OverrideRuleTile.keywords": "", + "components.Settings.OverrideRuleTile.language": "", + "components.Settings.OverrideRuleTile.tags": "", + "components.Settings.RadarrModal.apiKeyHelp": "", + "components.Settings.RadarrModal.baseUrlHelp": "", + "components.Settings.RadarrModal.enableSearchHelp": "", + "components.Settings.RadarrModal.externalUrlHelp": "", + "components.Settings.RadarrModal.hostname": "", + "components.Settings.RadarrModal.loadingprofiles": "", + "components.Settings.RadarrModal.loadingrootfolders": "", + "components.Settings.RadarrModal.minimumAvailability": "", + "components.Settings.RadarrModal.notagoptions": "", + "components.Settings.RadarrModal.selectQualityProfile": "", + "components.Settings.RadarrModal.selectRootFolder": "", + "components.Settings.RadarrModal.selecttags": "", + "components.Settings.RadarrModal.server4k": "", + "components.Settings.RadarrModal.servername": "", + "components.Settings.RadarrModal.syncEnabled": "", + "components.Settings.RadarrModal.syncEnabledHelp": "", + "components.Settings.RadarrModal.tagRequestsInfo": "", + "components.Settings.RadarrModal.testFirstQualityProfiles": "", + "components.Settings.RadarrModal.testFirstRootFolders": "", + "components.Settings.SettingsAbout.outofdate": "", + "components.Settings.SettingsAbout.runningDevelop": "", + "components.Settings.SettingsAbout.supportseerr": "", + "components.Settings.SettingsAbout.totalmedia": "", + "components.Settings.SettingsAbout.totalrequests": "", + "components.Settings.SettingsAbout.version": "", + "components.Settings.SettingsJobsCache.availability-sync": "", + "components.Settings.SettingsJobsCache.cachevsize": "", + "components.Settings.SettingsMain.blocklistLanguageTip": "", + "components.Settings.SettingsMain.blocklistRegion": "", + "components.Settings.SettingsMain.blocklistRegionTip": "", + "components.Settings.SettingsMain.general": "", + "components.Settings.SettingsMain.generalsettings": "", + "components.Settings.SettingsMain.generalsettingsDescription": "", + "components.Settings.SettingsMain.hideAvailable": "", + "components.Settings.SettingsMain.hideBlocklisted": "", + "components.Settings.SettingsNetwork.dnsCache": "", + "components.Settings.SettingsNetwork.dnsCacheForceMaxTtl": "", + "components.Settings.SettingsUsers.loginMethods": "", + "components.Settings.SettingsUsers.loginMethodsTip": "", + "components.Settings.SettingsUsers.mediaServerLogin": "", + "components.Settings.SettingsUsers.mediaServerLoginTip": "", + "components.Settings.SettingsUsers.newPlexLogin": "", + "components.Settings.SettingsUsers.newPlexLoginTip": "", + "components.Settings.SettingsUsers.toastSettingsSuccess": "", + "components.Settings.SonarrModal.monitorNewItems": "", + "components.Settings.SonarrModal.monitorNewItemsHelp": "", + "components.Settings.SonarrModal.notagoptions": "", + "components.Settings.SonarrModal.port": "", + "components.Settings.SonarrModal.rootfolder": "", + "components.Settings.SonarrModal.seasonfolders": "", + "components.Settings.SonarrModal.selectLanguageProfile": "", + "components.Settings.SonarrModal.selectQualityProfile": "", + "components.Settings.SonarrModal.selectRootFolder": "", + "components.Settings.SonarrModal.selecttags": "", + "components.Settings.SonarrModal.seriesType": "", + "components.Settings.SonarrModal.server4k": "", + "components.Settings.SonarrModal.server4kHelp": "", + "components.Settings.SonarrModal.servername": "", + "components.Settings.SonarrModal.ssl": "", + "components.Settings.SonarrModal.syncEnabled": "", + "components.Settings.SonarrModal.validationApiKeyRequired": "", + "components.Settings.SonarrModal.validationApplicationUrl": "", + "components.Settings.SonarrModal.validationApplicationUrlTrailingSlash": "", + "components.Settings.currentlibrary": "", + "components.Settings.default": "", + "components.Settings.default4k": "", + "components.Settings.deleteServer": "", + "components.Settings.deleteserverconfirm": "", + "components.Settings.jellyfinSyncFailedAutomaticGroupedFolders": "", + "components.Settings.manualscanDescriptionJellyfin": "", + "components.Settings.manualscanJellyfin": "", + "components.Settings.mediaTypeSeries": "", + "components.Settings.menuAbout": "", + "components.Settings.menuGeneralSettings": "", + "components.Settings.menuJellyfinSettings": "", + "components.Settings.menuLogs": "", + "components.Settings.menuMetadataProviders": "", + "components.Settings.serverpresetManualMessage": "", + "components.Settings.serverpresetRefreshing": "", + "components.Settings.serviceSettingsDescription": "", + "components.Settings.toastTautulliSettingsFailure": "", + "components.Settings.toastTautulliSettingsSuccess": "", + "components.Settings.tvdbProviderDoesnotWork": "", + "components.Settings.validationApiKey": "", + "components.Settings.validationHostnameRequired": "", + "components.Settings.validationPortRequired": "", + "components.Settings.validationUrlBaseLeadingSlash": "", + "components.Settings.validationUrlBaseTrailingSlash": "", + "components.Settings.validationUrlTrailingSlash": "", + "components.Settings.valueRequired": "", + "components.Settings.webAppUrl": "", + "components.Settings.webAppUrlTip": "", + "components.Setup.back": "", + "components.Setup.configemby": "", + "components.Setup.configjellyfin": "", + "components.Setup.configplex": "", + "components.Setup.configuremediaserver": "", + "components.Setup.configureservices": "", + "components.TvDetails.nextAirDate": "", + "components.TvDetails.originallanguage": "", + "components.TvDetails.originaltitle": "", + "components.TvDetails.overview": "", + "components.TvDetails.overviewunavailable": "", + "components.TvDetails.play": "", + "components.TvDetails.play4k": "", + "components.TvDetails.productioncountries": "", + "components.UserList.role": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.device": "", + "components.UserProfile.UserSettings.UserNotificationSettings.sendSilently": "", + "components.UserProfile.UserSettings.UserNotificationSettings.sendSilentlyDescription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.sound": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramChatId": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramChatIdTipLong": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadId": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverUserKey": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramChatId": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramMessageThreadId": "", + "components.UserProfile.totalrequests": "", + "components.UserProfile.unlimited": "", + "i18n.addToBlocklist": "", + "i18n.advanced": "", + "i18n.test": "", + "i18n.tvshow": "", + "i18n.usersettings": "", + "i18n.view": "", + "pages.errormessagewithcode": "", + "pages.oops": "", + "pages.pagenotfound": "", + "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegionTip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.email": "", + "components.UserProfile.UserSettings.UserGeneralSettings.enableOverride": "", + "components.UserProfile.UserSettings.UserGeneralSettings.general": "", + "components.UserProfile.UserSettings.UserGeneralSettings.generalsettings": "", + "components.UserProfile.UserSettings.UserGeneralSettings.languageDefault": "", + "components.UserProfile.UserSettings.UserGeneralSettings.localuser": "", + "components.UserProfile.UserSettings.UserGeneralSettings.mediaServerUser": "", + "components.UserProfile.UserSettings.UserGeneralSettings.movierequestlimit": "", + "components.UserProfile.UserSettings.UserGeneralSettings.originallanguage": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexuser": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmovies": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmoviestip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseries": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseriestip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.region": "", + "components.UserProfile.UserSettings.UserGeneralSettings.regionTip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.role": "", + "components.UserProfile.UserSettings.UserGeneralSettings.saving": "", + "components.UserProfile.UserSettings.UserGeneralSettings.seriesrequestlimit": "", + "components.UserProfile.UserSettings.UserGeneralSettings.streamingRegion": "", + "components.UserProfile.UserSettings.UserGeneralSettings.streamingRegionTip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailure": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmail": "", + "components.UserProfile.UserSettings.UserGeneralSettings.user": "", + "components.UserProfile.UserSettings.UserGeneralSettings.validationemailformat": "", + "components.UserProfile.UserSettings.UserGeneralSettings.validationemailrequired": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.deleteFailed": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.errorUnknown": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.linkedAccountsHint": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.noLinkedAccounts": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorExists": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorUnauthorized": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.created": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.deletesubscription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablewebpush": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablingwebpusherror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablingwebpusherror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.engine": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.managedevices": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.nodevicestoshow": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleted": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleteerror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.deviceDefault": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordId": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdAdd": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdPlaceholder": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdRemove": "", + "components.MovieDetails.similar": "", + "components.Settings.SettingsMain.toastSettingsSuccess": "", + "components.RequestCard.unknowntitle": "", + "components.Discover.FilterSlideover.keywords": "", + "components.IssueList.IssueItem.viewissue": "", + "components.Login.email": "", + "components.Settings.Notifications.NotificationsNtfy.token": "", + "components.Settings.webpush": "", + "components.Discover.recentrequests": "", + "components.Discover.studios": "", + "components.Discover.DiscoverTv.sortTmdbRatingDesc": "", + "components.Discover.DiscoverTvLanguage.languageSeries": "", + "components.Discover.FilterSlideover.ratingText": "", + "components.Discover.FilterSlideover.releaseDate": "", + "components.Discover.FilterSlideover.runtime": "", + "components.Discover.FilterSlideover.streamingservices": "", + "components.Discover.FilterSlideover.studio": "", + "components.Discover.FilterSlideover.tmdbuserscore": "", + "components.Discover.FilterSlideover.tmdbuservotecount": "", + "components.Discover.FilterSlideover.to": "", + "components.Discover.FilterSlideover.voteCount": "", + "components.Discover.MovieGenreList.moviegenres": "", + "components.Discover.MovieGenreSlider.moviegenres": "", + "components.Discover.NetworkSlider.networks": "", + "components.Discover.PlexWatchlistSlider.emptywatchlist": "", + "components.DownloadBlock.formattedTitle": "", + "components.IssueDetails.IssueComment.areyousuredelete": "", + "components.IssueDetails.IssueComment.edit": "", + "components.IssueDetails.IssueComment.postedby": "", + "components.IssueDetails.IssueComment.postedbyedited": "", + "components.IssueDetails.IssueComment.validationComment": "", + "components.IssueDetails.IssueDescription.deleteissue": "", + "components.IssueDetails.IssueDescription.description": "", + "components.IssueDetails.IssueDescription.edit": "", + "components.IssueDetails.allepisodes": "", + "components.IssueDetails.allseasons": "", + "components.IssueDetails.closeissueandcomment": "", + "components.IssueDetails.commentplaceholder": "", + "components.IssueDetails.comments": "", + "components.IssueDetails.deleteissue": "", + "components.IssueDetails.reopenissueandcomment": "", + "components.IssueDetails.toasteditdescriptionfailed": "", + "components.IssueDetails.toasteditdescriptionsuccess": "", + "components.IssueDetails.toastissuedeleted": "", + "components.IssueDetails.toastissuedeletefailed": "", + "components.IssueDetails.toaststatusupdatefailed": "", + "components.IssueList.IssueItem.episodes": "", + "components.IssueList.IssueItem.issuestatus": "", + "components.IssueList.IssueItem.issuetype": "", + "components.IssueList.IssueItem.opened": "", + "components.IssueList.IssueItem.openeduserdate": "", + "components.IssueList.IssueItem.seasons": "", + "components.IssueList.IssueItem.unknownissuetype": "", + "components.IssueList.sortModified": "", + "components.IssueModal.CreateIssueModal.allepisodes": "", + "components.IssueModal.CreateIssueModal.allseasons": "", + "components.IssueModal.CreateIssueModal.episode": "", + "components.IssueModal.CreateIssueModal.extras": "", + "components.IssueModal.CreateIssueModal.problemepisode": "", + "components.IssueModal.CreateIssueModal.problemseason": "", + "components.IssueModal.CreateIssueModal.providedetail": "", + "components.IssueModal.CreateIssueModal.submitissue": "", + "components.IssueModal.CreateIssueModal.toastFailedCreate": "", + "components.Layout.Sidebar.browsetv": "", + "components.Layout.Sidebar.dashboard": "", + "components.Layout.UserDropdown.MiniQuotaDisplay.seriesrequests": "", + "components.Layout.UserDropdown.myprofile": "", + "components.Layout.UserDropdown.requests": "", + "components.Layout.UserDropdown.settings": "", + "components.Layout.UserWarnings.emailInvalid": "", + "components.Layout.UserWarnings.emailRequired": "", + "components.Layout.UserWarnings.passwordRequired": "", + "components.Discover.FilterSlideover.status": "", + "components.IssueList.IssueItem.descriptionpreview": "", + "component.BlocklistModal.blocklisting": "", + "components.Blocklist.blocklistNotFoundError": "", + "components.Blocklist.blocklistSettingsDescription": "", + "components.Blocklist.blocklistedby": "", + "components.Discover.FilterSlideover.filters": "", + "components.Discover.FilterSlideover.firstAirDate": "", + "components.Discover.PlexWatchlistSlider.plexwatchlist": "", + "components.Discover.RecentlyAddedSlider.recentlyAdded": "", + "components.Discover.StudioSlider.studios": "", + "components.IssueDetails.openedby": "", + "components.IssueDetails.openin4karr": "", + "components.IssueDetails.openinarr": "", + "components.IssueDetails.play4konplex": "", + "components.IssueDetails.playonplex": "", + "components.IssueDetails.problemepisode": "", + "components.IssueDetails.problemseason": "", + "components.IssueDetails.reopenissue": "", + "components.IssueModal.CreateIssueModal.reportissue": "", + "components.IssueModal.CreateIssueModal.season": "", + "components.Layout.UserDropdown.signout": "", + "components.Settings.SettingsAbout.timezone": "", + "components.Settings.SettingsJobsCache.jobsandcache": "", + "components.Login.enablessl": "", + "components.DiscoverTvUpcoming.upcomingtv": "", + "components.Discover.RecentRequestsSlider.unableToConnect": "", + "components.CollectionDetails.removefromblocklistpartialcount": "", + "components.Discover.FilterSlideover.originalLanguage": "", + "components.IssueDetails.closeissue": "", + "components.IssueDetails.leavecomment": "", + "components.IssueModal.CreateIssueModal.toastSuccessCreate": "", + "components.IssueModal.CreateIssueModal.toastviewissue": "", + "components.IssueModal.CreateIssueModal.validationMessageRequired": "", + "components.IssueModal.CreateIssueModal.whatswrong": "", + "components.IssueModal.issueAudio": "", + "components.IssueModal.issueVideo": "", + "components.LanguageSelector.languageServerDefault": "", + "components.LanguageSelector.originalLanguageDefault": "", + "components.Layout.Sidebar.browsemovies": "", + "components.Login.emailtooltip": "", + "components.MovieDetails.showless": "", + "components.MovieDetails.showmore": "", + "components.MovieDetails.streamingproviders": "", + "components.Login.validationUrlBaseLeadingSlash": "", + "component.BlocklistBlock.blocklistdate": "", + "components.Blocklist.blocklistdate": "", + "components.Blocklist.blocklistsettings": "", + "components.Blocklist.filterBlocklistedTags": "", + "components.Blocklist.filterManual": "", + "components.Blocklist.mediaName": "", + "components.Blocklist.mediaTmdbId": "", + "components.Blocklist.mediaType": "", + "components.Blocklist.showAllBlocklisted": "", + "components.Discover.timeWindowDay": "", + "components.Discover.timeWindowWeek": "", + "components.IssueDetails.deleteissueconfirm": "", + "components.IssueDetails.episode": "", + "components.IssueDetails.issuepagetitle": "", + "components.IssueDetails.lastupdated": "", + "components.IssueDetails.nocomments": "", + "components.Layout.Sidebar.settings": "", + "components.Layout.Sidebar.users": "", + "components.Settings.Notifications.discordsettingsfailed": "", + "components.Settings.Notifications.pgpPrivateKey": "", + "components.Settings.Notifications.sendSilently": "", + "components.MovieDetails.tmdbuserscore": "", + "components.Settings.settings": "", + "components.Settings.OverrideRuleTile.settings": "", + "components.Layout.Sidebar.blocklist": "", + "components.Settings.serverLocal": "", + "components.Layout.UserWarnings.profileIncomplete": "", + "components.Layout.VersionStatus.commitsbehind": "", + "components.Layout.VersionStatus.outofdate": "", + "components.Layout.VersionStatus.streamstable": "", + "components.Login.adminerror": "", + "components.Login.back": "", + "components.Login.credentialerror": "", + "components.Login.description": "", + "components.Login.emailAlreadyTaken": "", + "components.Login.forgotpassword": "", + "components.Login.initialsigningin": "", + "components.Login.invalidurlerror": "", + "components.Login.jellyfinLocalLoginHint": "", + "components.Login.loginerror": "", + "components.Login.noadminerror": "", + "components.Login.saveFailed": "", + "components.Login.signin": "", + "components.Login.signingin": "", + "components.Login.signinheader": "", + "components.Login.signinwithjellyfin": "", + "components.Login.signinwithoverseerr": "", + "components.Login.signinwithplex": "", + "components.Login.tipEmailHasTrailingWhitespace": "", + "components.Login.tipUsernameHasTrailingWhitespace": "", + "components.Login.title": "", + "components.Login.urlBase": "", + "components.ManageSlideOver.manageModalClearMediaWarning": "", + "components.ManageSlideOver.manageModalIssues": "", + "components.ManageSlideOver.manageModalMedia": "", + "components.ManageSlideOver.manageModalMedia4k": "", + "components.ManageSlideOver.manageModalNoRequests": "", + "components.ManageSlideOver.manageModalRemoveMediaWarning": "", + "components.ManageSlideOver.manageModalRequests": "", + "components.ManageSlideOver.manageModalTitle": "", + "components.ManageSlideOver.mark4kavailable": "", + "components.ManageSlideOver.markallseasons4kavailable": "", + "components.ManageSlideOver.markallseasonsavailable": "", + "components.ManageSlideOver.markavailable": "", + "components.ManageSlideOver.movie": "", + "components.ManageSlideOver.openarr": "", + "components.ManageSlideOver.openarr4k": "", + "components.ManageSlideOver.opentautulli": "", + "components.ManageSlideOver.pastdays": "", + "components.ManageSlideOver.playedby": "", + "components.ManageSlideOver.plays": "", + "components.ManageSlideOver.removearr": "", + "components.ManageSlideOver.removearr4k": "", + "components.ManageSlideOver.tvshow": "", + "components.MediaSlider.ShowMoreCard.seemore": "", + "components.MetadataSelector.tmdbLabel": "", + "components.MetadataSelector.tvdbLabel": "", + "components.MovieDetails.MovieCast.fullcast": "", + "components.MovieDetails.MovieCrew.fullcrew": "", + "components.MovieDetails.addtowatchlist": "", + "components.MovieDetails.budget": "", + "components.MovieDetails.cast": "", + "components.MovieDetails.digitalrelease": "", + "components.MovieDetails.downloadstatus": "", + "components.MovieDetails.mark4kavailable": "", + "components.MovieDetails.openradarr": "", + "components.MovieDetails.openradarr4k": "", + "components.MovieDetails.originallanguage": "", + "components.MovieDetails.originaltitle": "", + "components.MovieDetails.overview": "", + "components.MovieDetails.play": "", + "components.MovieDetails.play4k": "", + "components.MovieDetails.reportissue": "", + "components.MovieDetails.revenue": "", + "components.MovieDetails.rtaudiencescore": "", + "components.MovieDetails.rtcriticsscore": "", + "components.MovieDetails.runtime": "", + "components.MovieDetails.studio": "", + "components.MovieDetails.theatricalrelease": "", + "components.MovieDetails.viewfullcrew": "", + "components.MovieDetails.watchlistDeleted": "", + "components.MovieDetails.watchlistSuccess": "", + "components.MovieDetails.watchtrailer": "", + "components.NotificationTypeSelector.adminissuereopenedDescription": "", + "components.NotificationTypeSelector.issuecomment": "", + "components.NotificationTypeSelector.issuecommentDescription": "", + "components.NotificationTypeSelector.issuecreated": "", + "components.NotificationTypeSelector.issuecreatedDescription": "", + "components.NotificationTypeSelector.issuereopened": "", + "components.NotificationTypeSelector.issuereopenedDescription": "", + "components.NotificationTypeSelector.issueresolved": "", + "components.NotificationTypeSelector.issueresolvedDescription": "", + "components.NotificationTypeSelector.mediaAutoApproved": "", + "components.NotificationTypeSelector.mediaAutoApprovedDescription": "", + "components.NotificationTypeSelector.mediaapproved": "", + "components.NotificationTypeSelector.mediaapprovedDescription": "", + "components.NotificationTypeSelector.mediaautorequested": "", + "components.NotificationTypeSelector.mediaautorequestedDescription": "", + "components.NotificationTypeSelector.mediaavailable": "", + "components.NotificationTypeSelector.mediaavailableDescription": "", + "components.NotificationTypeSelector.mediadeclined": "", + "components.NotificationTypeSelector.mediadeclinedDescription": "", + "components.NotificationTypeSelector.mediafailed": "", + "components.NotificationTypeSelector.mediafailedDescription": "", + "components.NotificationTypeSelector.mediarequested": "", + "components.NotificationTypeSelector.userissuecommentDescription": "", + "components.NotificationTypeSelector.userissuecreatedDescription": "", + "components.NotificationTypeSelector.userissuereopenedDescription": "", + "components.NotificationTypeSelector.usermediaavailableDescription": "", + "components.NotificationTypeSelector.usermediadeclinedDescription": "", + "components.NotificationTypeSelector.usermediafailedDescription": "", + "components.NotificationTypeSelector.usermediarequestedDescription": "", + "components.PermissionEdit.admin": "", + "components.PermissionEdit.adminDescription": "", + "components.PermissionEdit.advancedrequest": "", + "components.PermissionEdit.advancedrequestDescription": "", + "components.PermissionEdit.autoapprove4k": "", + "components.PermissionEdit.autoapprove4kDescription": "", + "components.PermissionEdit.autoapprove4kMovies": "", + "components.PermissionEdit.autoapprove4kMoviesDescription": "", + "components.PermissionEdit.autoapprove4kSeries": "", + "components.PermissionEdit.autoapprove4kSeriesDescription": "", + "components.PermissionEdit.autoapproveDescription": "", + "components.PermissionEdit.autoapproveMoviesDescription": "", + "components.PermissionEdit.autoapproveSeries": "", + "components.PermissionEdit.autoapproveSeriesDescription": "", + "components.PermissionEdit.autorequestDescription": "", + "components.PermissionEdit.autorequestMovies": "", + "components.PermissionEdit.blocklistedItems": "", + "components.PermissionEdit.blocklistedItemsDescription": "", + "components.PermissionEdit.manageblocklist": "", + "components.PermissionEdit.manageblocklistDescription": "", + "components.PermissionEdit.manageissuesDescription": "", + "components.PermissionEdit.managerequests": "", + "components.PermissionEdit.managerequestsDescription": "", + "components.PermissionEdit.request": "", + "components.PermissionEdit.request4k": "", + "components.PermissionEdit.request4kDescription": "", + "components.PermissionEdit.request4kMovies": "", + "components.PermissionEdit.request4kTv": "", + "components.PermissionEdit.request4kTvDescription": "", + "components.PermissionEdit.requestDescription": "", + "components.PermissionEdit.requestMovies": "", + "components.PermissionEdit.requestMoviesDescription": "", + "components.PermissionEdit.requestTv": "", + "components.PermissionEdit.requestTvDescription": "", + "components.PermissionEdit.users": "", + "components.PermissionEdit.usersDescription": "", + "components.PermissionEdit.viewblocklistedItems": "", + "components.PermissionEdit.viewblocklistedItemsDescription": "", + "components.PermissionEdit.viewissues": "", + "components.PermissionEdit.viewissuesDescription": "", + "components.PermissionEdit.viewrecent": "", + "components.PermissionEdit.viewrecentDescription": "", + "components.PermissionEdit.viewrequests": "", + "components.PermissionEdit.viewrequestsDescription": "", + "components.PermissionEdit.viewwatchlists": "", + "components.PermissionEdit.viewwatchlistsDescription": "", + "components.PersonDetails.appearsin": "", + "components.PersonDetails.ascharacter": "", + "components.PersonDetails.birthdate": "", + "components.PersonDetails.crewmember": "", + "components.QuotaSelector.days": "", + "components.QuotaSelector.movieRequests": "", + "components.QuotaSelector.movies": "", + "components.QuotaSelector.seasons": "", + "components.QuotaSelector.tvRequests": "", + "components.QuotaSelector.unlimited": "", + "components.RegionSelector.regionDefault": "", + "components.RegionSelector.regionServerDefault": "", + "components.RequestBlock.approve": "", + "components.RequestBlock.decline": "", + "components.RequestBlock.delete": "", + "components.RequestBlock.edit": "", + "components.RequestBlock.languageprofile": "", + "components.RequestBlock.lastmodifiedby": "", + "components.RequestBlock.profilechanged": "", + "components.RequestBlock.requestdate": "", + "components.RequestBlock.requestedby": "", + "components.RequestBlock.seasons": "", + "components.RequestButton.approve4krequests": "", + "components.RequestButton.approverequest4k": "", + "components.RequestButton.approverequests": "", + "components.RequestButton.decline4krequests": "", + "components.RequestButton.declinerequest": "", + "components.RequestButton.declinerequest4k": "", + "components.RequestButton.declinerequests": "", + "components.RequestButton.requestmore": "", + "components.RequestCard.cancelrequest": "", + "components.RequestCard.deleterequest": "", + "components.RequestCard.editrequest": "", + "components.RequestList.RequestItem.modified": "", + "components.RequestList.RequestItem.modifieduserdate": "", + "components.RequestList.RequestItem.profileName": "", + "components.RequestList.RequestItem.removearr": "", + "components.RequestList.RequestItem.requested": "", + "components.RequestList.RequestItem.requesteddate": "", + "components.RequestList.RequestItem.seasons": "", + "components.RequestList.RequestItem.tmdbid": "", + "components.RequestList.RequestItem.tvdbid": "", + "components.RequestList.RequestItem.unknowntitle": "", + "components.RequestList.requests": "", + "components.RequestList.showallrequests": "", + "components.RequestList.sortAdded": "", + "components.RequestList.sortDirection": "", + "components.RequestList.sortModified": "", + "components.RequestList.unableToConnect": "", + "components.RequestModal.AdvancedRequester.advancedoptions": "", + "components.RequestModal.AdvancedRequester.animenote": "", + "components.RequestModal.AdvancedRequester.default": "", + "components.RequestModal.AdvancedRequester.destinationserver": "", + "components.RequestModal.AdvancedRequester.folder": "", + "components.RequestModal.AdvancedRequester.languageprofile": "", + "components.RequestModal.AdvancedRequester.notagoptions": "", + "components.RequestModal.AdvancedRequester.tags": "", + "components.RequestModal.QuotaDisplay.allowedRequests": "", + "components.RequestModal.QuotaDisplay.allowedRequestsUser": "", + "components.RequestModal.QuotaDisplay.movielimit": "", + "components.RequestModal.QuotaDisplay.notenoughseasonrequests": "", + "components.RequestModal.QuotaDisplay.quotaLink": "", + "components.RequestModal.QuotaDisplay.quotaLinkUser": "", + "components.RequestModal.QuotaDisplay.requestsremaining": "", + "components.RequestModal.QuotaDisplay.requiredquotaUser": "", + "components.RequestModal.SearchByNameModal.nomatches": "", + "components.RequestModal.SearchByNameModal.notvdbiddescription": "", + "components.RequestModal.alreadyrequested": "", + "components.RequestModal.approve": "", + "components.RequestModal.autoapproval": "", + "components.RequestModal.cancel": "", + "components.RequestModal.edit": "", + "components.RequestModal.errorediting": "", + "components.RequestModal.numberofepisodes": "", + "components.RequestModal.pending4krequest": "", + "components.RequestModal.requestadmin": "", + "components.RequestModal.requestcancelled": "", + "components.RequestModal.requestcollection4ktitle": "", + "components.RequestModal.requestedited": "", + "components.RequestModal.requestmovies": "", + "components.RequestModal.requestmovies4k": "", + "components.RequestModal.requestmovietitle": "", + "components.RequestModal.requestseasons": "", + "components.RequestModal.requestseasons4k": "", + "components.RequestModal.requestseries4ktitle": "", + "components.RequestModal.requestseriestitle": "", + "components.RequestModal.season": "", + "components.RequestModal.seasonnumber": "", + "components.RequestModal.selectmovies": "", + "components.RequestModal.selectseason": "", + "components.ResetPassword.confirmpassword": "", + "components.ResetPassword.email": "", + "components.ResetPassword.emailresetlink": "", + "components.ResetPassword.gobacklogin": "", + "components.ResetPassword.password": "", + "components.ResetPassword.passwordreset": "", + "components.ResetPassword.requestresetlinksuccessmessage": "", + "components.ResetPassword.resetpassword": "", + "components.ResetPassword.resetpasswordsuccessmessage": "", + "components.ResetPassword.validationpasswordmatch": "", + "components.ResetPassword.validationpasswordminchars": "", + "components.Selector.CertificationSelector.minRating": "", + "components.Selector.CertificationSelector.selectCountry": "", + "components.Selector.CertificationSelector.starttyping": "", + "components.Selector.ended": "", + "components.Selector.inProduction": "", + "components.Selector.nooptions": "", + "components.Selector.pilot": "", + "components.Selector.planned": "", + "components.Selector.returningSeries": "", + "components.Selector.searchGenres": "", + "components.Selector.searchKeywords": "", + "components.Selector.searchStatus": "", + "components.Selector.searchStudios": "", + "components.Selector.searchUsers": "", + "components.Selector.showless": "", + "components.Settings.Notifications.NotificationsGotify.token": "", + "components.Settings.Notifications.NotificationsGotify.validationPriorityRequired": "", + "components.Settings.Notifications.NotificationsGotify.validationTokenRequired": "", + "components.Settings.Notifications.NotificationsGotify.validationTypes": "", + "components.Settings.Notifications.NotificationsGotify.validationUrlRequired": "", + "components.Settings.Notifications.NotificationsGotify.validationUrlTrailingSlash": "", + "components.Settings.Notifications.NotificationsNtfy.agentenabled": "", + "components.Settings.Notifications.NotificationsNtfy.embedPoster": "", + "components.Settings.Notifications.NotificationsNtfy.password": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestFailed": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestSending": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestSuccess": "", + "components.Settings.Notifications.NotificationsNtfy.tokenAuth": "", + "components.Settings.Notifications.NotificationsNtfy.topic": "", + "components.Settings.Notifications.NotificationsNtfy.url": "", + "components.Settings.Notifications.NotificationsNtfy.username": "", + "components.Settings.Notifications.NotificationsNtfy.usernamePasswordAuth": "", + "components.Settings.Notifications.NotificationsNtfy.validationNtfyTopic": "", + "components.Settings.Notifications.NotificationsNtfy.validationNtfyUrl": "", + "components.Settings.Notifications.NotificationsNtfy.validationPriorityRequired": "", + "components.Settings.Notifications.NotificationsNtfy.validationTypes": "", + "components.Settings.Notifications.NotificationsPushbullet.accessTokenTip": "", + "components.Settings.Notifications.NotificationsPushbullet.agentEnabled": "", + "components.Settings.Notifications.NotificationsPushbullet.channelTag": "", + "components.Settings.Notifications.NotificationsPushbullet.pushbulletSettingsFailed": "", + "components.Settings.Notifications.NotificationsPushbullet.pushbulletSettingsSaved": "", + "components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestFailed": "", + "components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestSending": "", + "components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestSuccess": "", + "components.Settings.Notifications.NotificationsPushover.toastPushoverTestFailed": "", + "components.Settings.Notifications.NotificationsPushover.toastPushoverTestSending": "", + "components.Settings.Notifications.NotificationsPushover.toastPushoverTestSuccess": "", + "components.Settings.Notifications.NotificationsPushover.userToken": "", + "components.Settings.Notifications.NotificationsPushover.userTokenTip": "", + "components.Settings.Notifications.NotificationsPushover.validationTypes": "", + "components.Settings.Notifications.NotificationsPushover.validationUserTokenRequired": "", + "components.Settings.Notifications.NotificationsSlack.agentenabled": "", + "components.Settings.Notifications.NotificationsSlack.embedPoster": "", + "components.Settings.Notifications.NotificationsSlack.slacksettingsfailed": "", + "components.Settings.Notifications.NotificationsSlack.toastSlackTestSending": "", + "components.Settings.Notifications.NotificationsSlack.validationTypes": "", + "components.Settings.Notifications.NotificationsSlack.validationWebhookUrl": "", + "components.Settings.Notifications.NotificationsSlack.webhookUrl": "", + "components.Settings.Notifications.NotificationsSlack.webhookUrlTip": "", + "components.Settings.Notifications.agentenabled": "", + "components.Settings.Notifications.allowselfsigned": "", + "components.Settings.Notifications.botUsernameTip": "", + "components.Settings.Notifications.discordsettingssaved": "", + "components.Settings.Notifications.emailsender": "", + "components.Settings.Notifications.emailsettingsfailed": "", + "components.Settings.Notifications.emailsettingssaved": "", + "components.Settings.Notifications.embedPoster": "", + "components.Settings.Notifications.enableMentions": "", + "components.Settings.Notifications.encryptionDefault": "", + "components.Settings.Notifications.encryptionImplicitTls": "", + "components.Settings.Notifications.encryptionNone": "", + "components.Settings.Notifications.encryptionOpportunisticTls": "", + "components.Settings.Notifications.encryptionTip": "", + "components.Settings.Notifications.messageThreadId": "", + "components.Settings.Notifications.messageThreadIdTip": "", + "components.Settings.Notifications.pgpPassword": "", + "components.Settings.Notifications.pgpPasswordTip": "", + "components.Settings.Notifications.pgpPrivateKeyTip": "", + "components.Settings.Notifications.sendSilentlyTip": "", + "components.Settings.Notifications.senderName": "", + "components.Settings.Notifications.smtpHost": "", + "components.Settings.Notifications.smtpPort": "", + "components.Settings.Notifications.telegramsettingsfailed": "", + "components.Settings.Notifications.telegramsettingssaved": "", + "components.Settings.Notifications.toastDiscordTestFailed": "", + "components.Settings.Notifications.toastDiscordTestSending": "", + "components.Settings.Notifications.toastDiscordTestSuccess": "", + "components.Settings.Notifications.toastEmailTestFailed": "", + "components.Settings.Notifications.toastEmailTestSending": "", + "components.Settings.Notifications.toastEmailTestSuccess": "", + "components.Settings.Notifications.toastTelegramTestFailed": "", + "components.Settings.Notifications.toastTelegramTestSending": "", + "components.Settings.SettingsJobsCache.ipv4Fallbacks": "", + "components.Settings.SonarrModal.animeTags": "", + "components.TvDetails.cast": "", + "i18n.settings": "", + "components.RequestButton.approverequest": "", + "components.UserList.edituser": "", + "components.NotificationTypeSelector.userissueresolvedDescription": "", + "components.NotificationTypeSelector.usermediaAutoApprovedDescription": "", + "components.PermissionEdit.autorequestMoviesDescription": "", + "components.PermissionEdit.autorequestSeriesDescription": "", + "components.PermissionEdit.createissuesDescription": "", + "components.Settings.SonarrModal.qualityprofile": "", + "components.Settings.SonarrModal.testFirstRootFolders": "", + "components.Settings.SonarrModal.validationRootFolderRequired": "", + "components.TitleCard.tmdbid": "", + "components.RequestButton.requestmore4k": "", + "components.RequestButton.viewrequest": "", + "components.RequestButton.viewrequest4k": "", + "components.UserList.mediaServerUser": "", + "components.RequestCard.declinerequest": "", + "components.UserProfile.UserSettings.UserPasswordChange.validationConfirmPasswordSame": "", + "i18n.cancel": "", + "components.RequestList.RequestItem.cancelRequest": "", + "i18n.canceling": "", + "components.RequestList.RequestItem.deleterequest": "", + "components.Login.urlBaseHelp": "", + "components.Login.username": "", + "components.Login.validationEmailFormat": "", + "components.Login.validationEmailRequired": "", + "components.Login.validationPortRequired": "", + "components.Login.validationUrlBaseTrailingSlash": "", + "components.Login.validationUrlTrailingSlash": "", + "components.Login.validationemailrequired": "", + "components.Login.validationhostrequired": "", + "components.Login.validationpasswordrequired": "", + "components.Login.validationservertyperequired": "", + "components.Login.validationusernamerequired": "", + "components.ManageSlideOver.downloadstatus": "", + "components.ManageSlideOver.manageModalAdvanced": "", + "components.ManageSlideOver.manageModalClearMedia": "", + "components.MovieDetails.productioncountries": "", + "components.MovieDetails.removefromwatchlist": "", + "components.NotificationTypeSelector.mediarequestedDescription": "", + "components.NotificationTypeSelector.notificationTypes": "", + "components.PermissionEdit.request4kMoviesDescription": "", + "components.RequestBlock.requestoverrides": "", + "components.RequestBlock.rootfolder": "", + "components.RequestBlock.server": "", + "components.RequestModal.AdvancedRequester.qualityprofile": "", + "components.RequestModal.AdvancedRequester.rootfolder": "", + "components.RequestModal.AdvancedRequester.selecttags": "", + "components.RequestModal.requesterror": "", + "components.RequestModal.requestfrom": "", + "components.RequestModal.requestmovie4ktitle": "", + "components.Selector.canceled": "", + "components.Selector.showmore": "", + "components.Settings.Notifications.NotificationsGotify.agentenabled": "", + "components.Settings.Notifications.NotificationsGotify.gotifysettingsfailed": "", + "components.Settings.Notifications.NotificationsGotify.gotifysettingssaved": "", + "components.Settings.Notifications.NotificationsGotify.priority": "", + "components.Settings.Notifications.NotificationsPushbullet.validationAccessTokenRequired": "", + "components.Settings.Notifications.NotificationsPushbullet.validationTypes": "", + "components.Settings.Notifications.NotificationsPushover.accessToken": "", + "components.Settings.Notifications.NotificationsPushover.accessTokenTip": "", + "components.Settings.Notifications.NotificationsPushover.agentenabled": "", + "components.Settings.Notifications.NotificationsPushover.deviceDefault": "", + "components.Settings.Notifications.NotificationsPushover.embedPoster": "", + "components.Settings.Notifications.NotificationsPushover.pushoversettingsfailed": "", + "components.Settings.Notifications.NotificationsPushover.pushoversettingssaved": "", + "components.Settings.Notifications.NotificationsPushover.sound": "", + "components.Settings.Notifications.NotificationsSlack.slacksettingssaved": "", + "components.Settings.Notifications.NotificationsSlack.toastSlackTestFailed": "", + "components.Settings.Notifications.NotificationsWebPush.webpushsettingsfailed": "", + "components.Settings.Notifications.chatId": "", + "components.Settings.Notifications.validationChatIdRequired": "", + "components.Settings.RadarrModal.loadingTags": "", + "components.Settings.SettingsMain.validationApplicationUrlTrailingSlash": "", + "components.Settings.SonarrModal.toastSonarrTestFailure": "", + "components.Settings.copyBlocklistedTags": "", + "components.Settings.plex": "", + "components.TitleCard.watchlistSuccess": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.linkedAccounts": "", + "i18n.showingresults": "", + "components.RequestModal.QuotaDisplay.requiredquota": "", + "components.Settings.mediaTypeMovie": "", + "components.RequestModal.QuotaDisplay.season": "", + "components.RequestModal.QuotaDisplay.seasonlimit": "", + "components.Selector.CertificationSelector.maxRating": "", + "components.Settings.Notifications.encryption": "", + "components.Settings.RadarrModal.validationNameRequired": "", + "components.Settings.SettingsUsers.movieRequestLimitLabel": "", + "components.Settings.webhook": "", + "components.Settings.Notifications.botAPI": "", + "components.Settings.SettingsJobsCache.image-cache-cleanup": "", + "components.Discover.FilterSlideover.from": "", + "components.Discover.FilterSlideover.genres": "", + "components.Settings.Notifications.authPass": "", + "components.Settings.OverrideRuleModal.languages": "", + "components.UserProfile.ProfileHeader.joindate": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pgpPublicKey": "", + "components.Layout.VersionStatus.streamdevelop": "", + "components.UserList.userlist": "", + "component.BlocklistBlock.blocklistedby": "", + "components.Settings.cancelscan": "", + "components.RequestCard.approverequest": "", + "components.RequestCard.failedretry": "", + "components.RequestCard.seasons": "", + "components.Settings.SettingsAbout.uptodate": "", + "components.Discover.DiscoverTvKeyword.keywordSeries": "", + "components.IssueDetails.toaststatusupdated": "", + "components.IssueList.showallissues": "", + "components.IssueModal.issueOther": "", + "components.Login.password": "", + "components.MetadataSelector.selectMetdataProvider": "", + "components.MovieDetails.imdbuserscore": "", + "components.MovieDetails.watchlistError": "", + "components.PermissionEdit.autoapproveMovies": "", + "components.PersonDetails.lifespan": "", + "components.Settings.OverrideRuleTile.rootfolder": "", + "components.Settings.SettingsUsers.toastSettingsFailure": "", + "components.NotificationTypeSelector.adminissuecommentDescription": "", + "components.MovieDetails.markavailable": "", + "components.PermissionEdit.manageissues": "", + "components.RequestModal.QuotaDisplay.movie": "", + "components.IssueModal.issueSubtitles": "", + "components.MovieDetails.recommendations": "", + "components.RequestModal.requestcollectiontitle": "", + "components.Settings.Notifications.NotificationsGotify.url": "", + "components.Settings.Notifications.NotificationsWebhook.templatevariablehelp": "", + "components.Settings.Notifications.NotificationsWebhook.toastWebhookTestFailed": "", + "components.Settings.Notifications.NotificationsWebPush.httpsRequirement": "", + "components.Settings.Notifications.NotificationsWebPush.webpushsettingssaved": "", + "components.Settings.Notifications.webhookRoleIdTip": "", + "components.Settings.Notifications.webhookUrl": "", + "components.Settings.Notifications.webhookUrlTip": "", + "components.Settings.OverrideRuleModal.conditions": "", + "components.Settings.OverrideRuleModal.create": "", + "components.Settings.OverrideRuleModal.createrule": "", + "components.Settings.OverrideRuleModal.editrule": "", + "components.Settings.OverrideRuleTile.qualityprofile": "", + "components.Settings.SettingsJobsCache.process-blocklisted-tags": "", + "components.Settings.OverrideRuleTile.users": "", + "components.Settings.RadarrModal.announced": "", + "components.Settings.RadarrModal.baseUrl": "", + "components.Settings.RadarrModal.default4kserver": "", + "components.Settings.RadarrModal.defaultserver": "", + "components.Settings.RadarrModal.edit4kradarr": "", + "components.Settings.RadarrModal.editradarr": "", + "components.Settings.RadarrModal.enableSearch": "", + "components.Settings.RadarrModal.tagRequests": "", + "components.Settings.RadarrModal.validationPortRequired": "", + "components.Settings.RadarrModal.validationProfileRequired": "", + "components.Settings.RadarrModal.validationRootFolderRequired": "", + "components.Settings.SettingsAbout.Releases.currentversion": "", + "components.Settings.SettingsAbout.Releases.latestversion": "", + "components.Settings.SettingsAbout.Releases.viewongithub": "", + "components.Settings.SettingsAbout.about": "", + "components.Settings.SettingsJobsCache.imagecachesize": "", + "components.Settings.SettingsJobsCache.size": "", + "components.Settings.SettingsJobsCache.sonarr-scan": "", + "components.Settings.SettingsMain.blocklistedTagsLimit": "", + "components.Settings.SettingsJobsCache.unknownJob": "", + "components.Settings.SettingsJobsCache.usersavatars": "", + "components.Settings.SettingsLogs.copiedLogMessage": "", + "components.Settings.SettingsLogs.copyToClipboard": "", + "components.Settings.SettingsLogs.extraData": "", + "components.Settings.SettingsLogs.filterDebug": "", + "components.Settings.SettingsLogs.filterError": "", + "components.Settings.SettingsLogs.filterInfo": "", + "components.Settings.SettingsLogs.filterWarn": "", + "components.Settings.SettingsLogs.label": "", + "components.Settings.SettingsLogs.level": "", + "components.Settings.SettingsLogs.logDetails": "", + "components.Settings.SettingsLogs.logs": "", + "components.Settings.SettingsLogs.logsDescription": "", + "components.Settings.SettingsLogs.message": "", + "components.Settings.SettingsLogs.pauseLogs": "", + "components.Settings.SettingsLogs.resumeLogs": "", + "components.Settings.SettingsLogs.showall": "", + "components.Settings.SettingsLogs.time": "", + "components.Settings.SettingsLogs.viewdetails": "", + "components.Settings.SettingsMain.apikey": "", + "components.Settings.SettingsMain.apikeyCopied": "", + "components.Settings.SettingsMain.applicationTitle": "", + "components.Settings.SettingsMain.applicationurl": "", + "components.Settings.SettingsMain.blocklistedTags": "", + "components.Settings.SettingsMain.blocklistedTagsLimitTip": "", + "components.Settings.SettingsMain.blocklistedTagsTip": "", + "components.Settings.SettingsMain.discoverRegionTip": "", + "components.Settings.SettingsNetwork.dnsCacheForceMinTtl": "", + "components.Settings.SettingsNetwork.dnsCacheHoverTip": "", + "components.Settings.SettingsMain.enableSpecialEpisodes": "", + "components.Settings.SettingsMain.streamingRegion": "", + "components.Settings.SettingsMain.streamingRegionTip": "", + "components.Settings.SettingsMain.toastApiKeyFailure": "", + "components.Settings.SettingsMain.toastApiKeySuccess": "", + "components.Settings.SettingsMain.validationApplicationUrl": "", + "components.Settings.SettingsNetwork.network": "", + "components.Settings.save": "", + "components.UserList.owner": "", + "components.UserProfile.ProfileHeader.settings": "", + "components.Settings.RadarrModal.externalUrl": "", + "components.IssueList.issues": "", + "components.Settings.tautulliSettings": "", + "components.PermissionEdit.autoapprove": "", + "i18n.noresults": "", + "components.Login.validationhostformat": "", + "components.Settings.blocklistedTagsText": "", + "components.Settings.scanning": "", + "components.TitleCard.mediaerror": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmailEmpty": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsSuccess": "", + "components.UserProfile.UserSettings.UserNotificationSettings.email": "", + "components.UserProfile.UserSettings.UserPasswordChange.validationNewPasswordLength": "", + "i18n.movie": "", + "i18n.movies": "", + "components.Settings.SettingsJobsCache.dnsNoCacheEntries": "", + "components.Settings.SettingsMain.locale": "", + "components.Settings.SonarrModal.loadingTags": "", + "components.Settings.librariesRemaining": "", + "components.UserProfile.UserSettings.UserGeneralSettings.owner": "", + "i18n.close": "", + "pages.serviceunavailable": "", + "components.Settings.RadarrModal.inCinemas": "", + "components.Settings.metadataSettings": "", + "components.TvDetails.anime": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.noPermissionDescription": "", + "components.UserList.deleteconfirm": "", + "components.UserProfile.UserSettings.UserGeneralSettings.admin": "", + "components.UserProfile.UserSettings.UserGeneralSettings.applanguage": "", + "components.UserProfile.UserSettings.UserGeneralSettings.originallanguageTip": "", + "i18n.available": "", + "i18n.back": "", + "i18n.blocklist": "", + "i18n.blocklistDuplicateError": "", + "i18n.blocklistError": "", + "i18n.blocklistSuccess": "", + "components.DownloadBlock.estimatedtime": "", + "components.Layout.SearchInput.searchPlaceholder": "", + "components.Layout.LanguagePicker.displaylanguage": "", + "components.Login.port": "", + "components.Login.save": "", + "components.Login.saving": "", + "components.Login.servertype": "", + "components.Login.validationemailformat": "", + "components.ManageSlideOver.alltime": "", + "components.MovieDetails.releasedate": "", + "components.NotificationTypeSelector.adminissueresolvedDescription": "", + "components.PersonDetails.alsoknownas": "", + "components.Settings.RadarrModal.tags": "", + "components.Settings.SettingsJobsCache.process": "", + "components.Settings.SonarrModal.loadingprofiles": "", + "components.Settings.jellyfinlibraries": "", + "components.Settings.restartrequiredTooltip": "", + "components.TitleCard.tvdbid": "", + "components.UserList.userdeleteerror": "", + "components.UserList.userfail": "", + "components.UserList.username": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablewebpush": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPgpPublicKey": "", + "i18n.collection": "", + "i18n.decline": "", + "i18n.declined": "", + "i18n.delete": "", + "i18n.testing": "", + "pages.somethingwentwrong": "", + "components.Settings.SettingsJobsCache.failures": "", + "components.RequestModal.AdvancedRequester.requestas": "", + "components.Settings.SettingsNetwork.apiRequestTimeout": "", + "components.Settings.jellyfinSettingsFailure": "", + "components.Settings.notifications": "", + "components.Discover.TvGenreList.seriesgenres": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.operatingsystem": "", + "components.MovieDetails.managemovie": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "", + "components.PermissionEdit.autorequestSeries": "", + "i18n.retry": "", + "components.ResetPassword.validationemailrequired": "", + "components.Selector.starttyping": "", + "components.Settings.SonarrModal.apiKey": "", + "components.Settings.SettingsJobsCache.jobtype": "", + "components.UserProfile.UserSettings.UserPermissions.permissions": "", + "components.Settings.Notifications.NotificationsNtfy.priority": "", + "components.Settings.SettingsMain.cacheImagesTip": "", + "components.Settings.SettingsMain.discoverRegion": "", + "components.Settings.Notifications.NotificationsSlack.toastSlackTestSuccess": "", + "components.Settings.Notifications.authUser": "", + "components.Settings.Notifications.botApiTip": "", + "components.Settings.Notifications.botAvatarUrl": "", + "components.Settings.Notifications.botUsername": "", + "components.Settings.Notifications.chatIdTip": "", + "components.Settings.OverrideRuleModal.genres": "", + "components.Settings.SettingsAbout.aboutseerr": "", + "components.Settings.SettingsJobsCache.dnsCache": "", + "components.Settings.SettingsJobsCache.dnsCacheDescription": "", + "components.Settings.SettingsJobsCache.dnsCacheGlobalStats": "", + "components.Settings.SettingsJobsCache.jellyfin-recently-added-scan": "", + "components.Settings.SettingsJobsCache.jobScheduleEditFailed": "", + "components.Settings.SettingsJobsCache.jobScheduleEditSaved": "", + "components.Settings.SettingsJobsCache.jobcancelled": "", + "components.Settings.SettingsJobsCache.jobs": "", + "components.Settings.SettingsJobsCache.misses": "", + "components.Settings.SettingsUsers.users": "", + "components.Setup.signin": "", + "components.Setup.signinMessage": "", + "components.Setup.signinWithJellyfin": "", + "components.Setup.signinWithPlex": "", + "components.Setup.welcome": "", + "components.StatusChecker.restartRequiredDescription": "", + "components.TitleCard.addToWatchList": "", + "components.TitleCard.cleardata": "", + "components.UserList.autogeneratepassword": "", + "components.UserList.autogeneratepasswordTip": "", + "components.UserList.bulkedit": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.browser": "", + "components.UserProfile.UserSettings.menuNotifications": "", + "components.UserProfile.UserSettings.menuPermissions": "", + "components.UserProfile.UserSettings.unauthorizedDescription": "", + "components.UserProfile.emptywatchlist": "", + "components.UserProfile.limit": "", + "components.UserProfile.localWatchlist": "", + "components.IssueDetails.IssueComment.delete": "", + "components.IssueDetails.season": "", + "components.Login.hostname": "", + "components.Discover.TvGenreSlider.tvgenres": "", + "components.Discover.tmdbmoviestreamingservices": "", + "components.Discover.tmdbnetwork": "", + "components.Discover.tmdbsearch": "", + "components.Discover.tmdbstudio": "", + "components.Discover.tmdbtvgenre": "", + "components.IssueDetails.issuetype": "", + "components.IssueList.IssueItem.problemepisode": "", + "components.Login.loginwithapp": "", + "components.Login.orsigninwith": "", + "components.Settings.SettingsJobsCache.cacheflushed": "", + "components.Layout.Sidebar.requests": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeenenabled": "", + "components.NotificationTypeSelector.usermediaapprovedDescription": "", + "components.PermissionEdit.createissues": "", + "components.Settings.SettingsMain.cacheImages": "", + "components.Settings.no": "", + "components.Settings.urlBase": "", + "components.UserList.deleteuser": "", + "i18n.status": "", + "components.Discover.DiscoverWatchlist.discoverwatchlist": "", + "components.Discover.DiscoverWatchlist.watchlist": "", + "components.Discover.FilterSlideover.activefilters": "", + "components.Discover.FilterSlideover.certification": "", + "components.Discover.FilterSlideover.clearfilters": "", + "components.Discover.FilterSlideover.excludeKeywords": "", + "components.PermissionEdit.autorequest": "", + "components.Settings.RadarrModal.add": "", + "components.Settings.RadarrModal.apiKey": "", + "components.Settings.RadarrModal.create4kradarr": "", + "components.Settings.RadarrModal.createradarr": "", + "components.Settings.RadarrModal.qualityprofile": "", + "components.Settings.RadarrModal.rootfolder": "", + "components.Settings.SettingsAbout.Releases.releases": "", + "components.Settings.Notifications.NotificationsPushbullet.accessToken": "", + "components.MovieDetails.overviewunavailable": "", + "components.MovieDetails.physicalrelease": "", + "i18n.open": "", + "pages.internalservererror": "", + "components.Discover.FilterSlideover.runtimeText": "", + "components.IssueDetails.unknownissuetype": "", + "components.Layout.UserDropdown.MiniQuotaDisplay.movierequests": "", + "components.Setup.setup": "", + "components.Layout.Sidebar.issues": "", + "components.Discover.trending": "", + "components.TvDetails.firstAirDate": "" } diff --git a/src/i18n/locale/sq.json b/src/i18n/locale/sq.json index a066adf904..1190c5103a 100644 --- a/src/i18n/locale/sq.json +++ b/src/i18n/locale/sq.json @@ -704,8 +704,6 @@ "components.UserProfile.UserSettings.UserGeneralSettings.admin": "Administrator", "components.UserProfile.UserSettings.UserGeneralSettings.applanguage": "Shfaq Gjuhën", "components.UserProfile.UserSettings.UserGeneralSettings.displayName": "Emri i shfaqur", - "components.UserProfile.UserSettings.UserGeneralSettings.discordId": "ID e përdoruesit të Diskordit", - "components.UserProfile.UserSettings.UserGeneralSettings.discordIdTip": "Numri i identifikimit me shumë shifra i lidhur me llogarinë tuaj të përdoruesit Discord", "components.UserProfile.UserSettings.UserGeneralSettings.enableOverride": "Tejkalo limitin global", "components.UserProfile.UserSettings.UserGeneralSettings.general": "Gjeneral", "components.UserProfile.UserSettings.UserGeneralSettings.generalsettings": "Cilësimet e përgjithshme", @@ -721,7 +719,6 @@ "components.UserProfile.UserSettings.UserGeneralSettings.region": "Rajoni i Zbulimit", "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailure": "Diçka shkoi keq duke ruajtur cilësimet.", "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsSuccess": "Cilësimet u ruajtën me sukses!", - "components.UserProfile.UserSettings.UserGeneralSettings.validationDiscordId": "Ju duhet të siguroni një ID të vlefshme të përdoruesit Discord", "components.UserProfile.UserSettings.UserNotificationSettings.discordId": "ID e përdoruesit", "components.UserProfile.UserSettings.UserGeneralSettings.seriesrequestlimit": "Limiti i kërkesës së serisë", "components.UserProfile.UserSettings.UserNotificationSettings.emailsettingsfailed": "Cilësimet e njoftimit me email nuk u ruajtën.", @@ -1003,5 +1000,673 @@ "components.RequestBlock.lastmodifiedby": "Ndryshuar së fundi nga", "components.RequestBlock.requestdate": "Data e Kërkesës", "components.RequestBlock.requestedby": "Kërkuar nga", - "components.RequestCard.approverequest": "Mirato Kërkesën" + "components.RequestCard.approverequest": "Mirato Kërkesën", + "components.Discover.DiscoverMovies.sortTitleDesc": "", + "components.Settings.Notifications.usePublicLogo": "", + "components.Settings.Notifications.usePublicLogoTip": "", + "components.Settings.Notifications.validationWebhookRoleId": "", + "components.Settings.Notifications.webhookRoleId": "", + "components.Settings.Notifications.webhookRoleIdTip": "", + "components.Settings.OverrideRuleModal.conditions": "", + "components.Settings.OverrideRuleModal.conditionsDescription": "", + "components.Settings.OverrideRuleModal.create": "", + "components.Settings.OverrideRuleModal.createrule": "", + "components.Settings.OverrideRuleModal.editrule": "", + "components.Settings.OverrideRuleModal.genres": "", + "components.Settings.OverrideRuleModal.keywords": "", + "components.Settings.OverrideRuleModal.languages": "", + "components.Settings.OverrideRuleModal.notagoptions": "", + "components.Settings.OverrideRuleModal.qualityprofile": "", + "components.Settings.OverrideRuleModal.rootfolder": "", + "components.Settings.OverrideRuleModal.ruleCreated": "", + "components.Settings.OverrideRuleModal.ruleUpdated": "", + "components.Settings.OverrideRuleModal.selectQualityProfile": "", + "components.Settings.OverrideRuleModal.selectRootFolder": "", + "components.Settings.OverrideRuleModal.selectService": "", + "components.Settings.OverrideRuleModal.selecttags": "", + "components.Settings.OverrideRuleModal.service": "", + "components.Settings.OverrideRuleModal.serviceDescription": "", + "components.Settings.OverrideRuleModal.settings": "", + "components.Settings.OverrideRuleModal.settingsDescription": "", + "components.Settings.OverrideRuleModal.tags": "", + "components.Settings.OverrideRuleModal.users": "", + "components.Settings.OverrideRuleTile.conditions": "", + "components.Settings.OverrideRuleTile.genre": "", + "components.Settings.OverrideRuleTile.language": "", + "components.Settings.OverrideRuleTile.qualityprofile": "", + "components.Settings.OverrideRuleTile.rootfolder": "", + "components.Settings.OverrideRuleTile.settings": "", + "components.Settings.OverrideRuleTile.tags": "", + "components.Settings.OverrideRuleTile.users": "", + "components.Settings.RadarrModal.enableSearchHelp": "", + "components.Settings.RadarrModal.externalUrlHelp": "", + "components.Settings.SettingsAbout.aboutseerr": "", + "components.Settings.SettingsAbout.contribute": "", + "components.Settings.SettingsJobsCache.dnsCache": "", + "components.Settings.SettingsJobsCache.dnsCacheDescription": "", + "components.Settings.SettingsJobsCache.dnsCacheGlobalStats": "", + "components.Settings.SettingsJobsCache.dnsCacheGlobalStatsDescription": "", + "components.Settings.SettingsJobsCache.dnscacheactiveaddress": "", + "components.Settings.SettingsJobsCache.dnscachemisses": "", + "components.Settings.SettingsJobsCache.dnscachename": "", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorSeconds": "", + "components.Settings.SettingsJobsCache.failures": "", + "components.Settings.SettingsJobsCache.flushdnscache": "", + "components.Settings.SettingsJobsCache.hitRate": "", + "components.Settings.SettingsJobsCache.hits": "", + "components.Settings.SettingsJobsCache.imagecache": "", + "components.Settings.SettingsJobsCache.ipv4Fallbacks": "", + "components.Settings.SettingsJobsCache.jellyfin-full-scan": "", + "components.RequestCard.failedmodify": "", + "components.RequestCard.tvdbid": "", + "components.RequestCard.unknowntitle": "", + "components.RequestList.RequestItem.failedmodify": "", + "components.Selector.CertificationSelector.selectCountry": "", + "components.Selector.ended": "", + "components.Settings.Notifications.NotificationsNtfy.validationTypes": "", + "components.Settings.OverrideRuleTile.keywords": "", + "components.Settings.SettingsJobsCache.plex-watchlist-sync": "", + "components.Settings.SettingsJobsCache.process-blocklisted-tags": "", + "components.Settings.SettingsJobsCache.usersavatars": "", + "components.Settings.SettingsMain.apikeyCopied": "", + "components.Settings.SettingsMain.applicationTitle": "", + "components.Settings.SettingsMain.applicationurl": "", + "components.Settings.SettingsMain.blocklistLanguage": "", + "components.Settings.SettingsMain.blocklistLanguageTip": "", + "components.Settings.SettingsMain.blocklistRegion": "", + "components.Settings.SettingsMain.blocklistRegionTip": "", + "components.Settings.SettingsMain.blocklistedTags": "", + "components.Settings.SettingsMain.blocklistedTagsLimit": "", + "components.Settings.SettingsMain.blocklistedTagsLimitTip": "", + "components.Settings.SettingsMain.blocklistedTagsTip": "", + "components.Settings.SettingsMain.cacheImages": "", + "components.Settings.SettingsMain.discoverRegion": "", + "components.Settings.SettingsMain.discoverRegionTip": "", + "components.Settings.SettingsMain.general": "", + "components.Settings.SettingsMain.generalsettings": "", + "components.Settings.SettingsMain.generalsettingsDescription": "", + "components.Settings.SettingsMain.hideAvailable": "", + "components.Settings.SettingsMain.hideAvailableTip": "", + "components.Settings.SettingsMain.hideBlocklisted": "", + "components.Settings.SettingsMain.hideBlocklistedTip": "", + "components.Settings.SettingsMain.locale": "", + "components.Settings.SettingsMain.originallanguage": "", + "components.Settings.SettingsMain.streamingRegionTip": "", + "components.Settings.SettingsMain.toastApiKeyFailure": "", + "components.Settings.SettingsMain.toastApiKeySuccess": "", + "components.Settings.SettingsMain.toastSettingsFailure": "", + "components.Settings.SettingsMain.toastSettingsSuccess": "", + "components.Settings.SettingsMain.validationApplicationTitle": "", + "components.Settings.SettingsMain.validationApplicationUrl": "", + "components.Settings.SettingsMain.validationApplicationUrlTrailingSlash": "", + "components.Settings.SettingsMain.validationUrlTrailingSlash": "", + "components.Settings.SettingsMain.youtubeUrl": "", + "components.Settings.SettingsMain.youtubeUrlTip": "", + "components.Settings.SettingsNetwork.apiRequestTimeoutTip": "", + "components.Settings.SettingsNetwork.csrfProtection": "", + "components.Settings.SettingsNetwork.csrfProtectionHoverTip": "", + "components.Settings.SettingsNetwork.csrfProtectionTip": "", + "components.Settings.SettingsNetwork.dnsCache": "", + "components.Settings.SettingsNetwork.dnsCacheForceMaxTtl": "", + "components.Settings.SettingsNetwork.dnsCacheForceMinTtl": "", + "components.Settings.SettingsNetwork.dnsCacheHoverTip": "", + "components.Settings.SettingsNetwork.dnsCacheTip": "", + "components.Settings.SettingsNetwork.forceIpv4First": "", + "components.Settings.SettingsNetwork.forceIpv4FirstTip": "", + "components.Settings.SettingsNetwork.networksettingsDescription": "", + "components.Settings.SettingsNetwork.proxyBypassFilter": "", + "components.Settings.SettingsNetwork.proxyBypassLocalAddresses": "", + "components.Settings.SettingsNetwork.proxyEnabledTip": "", + "components.Settings.SettingsNetwork.proxyHostname": "", + "components.Settings.SettingsNetwork.proxyPassword": "", + "components.Settings.SettingsNetwork.proxyPort": "", + "components.Settings.SettingsNetwork.proxySsl": "", + "components.Settings.SettingsNetwork.proxyUser": "", + "components.Settings.SettingsNetwork.toastSettingsFailure": "", + "components.Settings.SettingsNetwork.toastSettingsSuccess": "", + "components.Settings.SettingsNetwork.trustProxy": "", + "components.Settings.SettingsNetwork.trustProxyTip": "", + "components.Settings.SettingsNetwork.validationApiRequestTimeout": "", + "components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "", + "components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "", + "components.Settings.SettingsNetwork.validationProxyPort": "", + "components.Settings.SettingsUsers.atLeastOneAuth": "", + "components.Settings.SettingsUsers.disabledMediaServerLoginWarning": "", + "components.Settings.SettingsUsers.loginMethods": "", + "components.Settings.SettingsUsers.mediaServerLoginTip": "", + "components.Settings.SonarrModal.monitorNewItemsHelp": "", + "components.Settings.SonarrModal.seriesType": "", + "components.Settings.SonarrModal.server4kHelp": "", + "components.Settings.SonarrModal.syncEnabledHelp": "", + "components.Settings.SonarrModal.tagRequests": "", + "components.Settings.SonarrModal.tagRequestsInfo": "", + "components.Settings.addrule": "", + "components.Settings.advancedTooltip": "", + "components.Settings.allChosenProvidersAreOperational": "", + "components.Settings.animeMetadataProvider": "", + "components.Settings.apiKey": "", + "components.Settings.blocklistedTagImportInstructions": "", + "components.Settings.blocklistedTagImportTitle": "", + "components.Settings.blocklistedTagsText": "", + "components.Settings.chooseProvider": "", + "components.Settings.deleteServer": "", + "components.Settings.experimentalTooltip": "", + "components.Settings.failed": "", + "components.Settings.failedToSaveMetadataSettings": "", + "components.Settings.invalidKeyword": "", + "components.Settings.jellyfinSettingsFailure": "", + "components.Settings.jellyfinSettingsSuccess": "", + "components.Settings.syncing": "", + "components.Settings.valueRequired": "", + "components.Setup.configemby": "", + "components.Setup.configjellyfin": "", + "components.Setup.configplex": "", + "components.Setup.configuremediaserver": "", + "components.Setup.librarieserror": "", + "components.Setup.servertype": "", + "components.TitleCard.watchlistError": "", + "components.TitleCard.watchlistSuccess": "", + "components.TvDetails.Season.somethingwentwrong": "", + "components.TvDetails.removefromwatchlist": "", + "components.TvDetails.reportissue": "", + "components.TvDetails.tmdbuserscore": "", + "components.TvDetails.watchlistDeleted": "", + "components.TvDetails.watchlistError": "", + "components.UserList.descending": "", + "components.UserList.importedPlexUsersNoPassword": "", + "components.UserList.importfromJellyfin": "", + "components.UserList.importfromJellyfinerror": "", + "components.UserList.noJellyfinuserstoimport": "", + "components.UserList.sortBy": "", + "components.UserList.sortByJoined": "", + "components.UserList.sortByRequests": "", + "components.UserList.sortByRole": "", + "components.UserList.sortByType": "", + "components.UserList.sortByUser": "", + "components.UserList.toggleSortDirection": "", + "components.UserList.toggleSortDirectionAria": "", + "components.UserList.username": "", + "components.UserList.validationUsername": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.description": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorExists": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorUnauthorized": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorUnknown": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.saving": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.title": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.username": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.usernameRequired": "", + "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegion": "", + "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegionTip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.email": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmovies": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.browser": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.created": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.deletesubscription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.device": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeenenabled": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.sound": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadId": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadIdTip": "", + "components.UserList.newJellyfinsigninenabled": "", + "components.UserProfile.UserSettings.menuLinkedAccounts": "", + "components.UserProfile.emptywatchlist": "", + "i18n.blocklist": "", + "i18n.blocklistDuplicateError": "", + "i18n.blocklistError": "", + "i18n.blocklistSuccess": "", + "i18n.blocklisted": "", + "i18n.collection": "", + "i18n.deleted": "", + "i18n.restartRequired": "", + "components.UserProfile.UserSettings.UserGeneralSettings.validationemailrequired": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.deleteFailed": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.errorUnknown": "", + "components.Discover.resettodefault": "", + "components.Discover.resetwarning": "", + "components.Discover.studios": "", + "components.Discover.tmdbmoviegenre": "", + "components.Discover.tmdbmoviekeyword": "", + "components.Discover.tmdbmoviestreamingservices": "", + "components.Discover.tmdbnetwork": "", + "components.Discover.tmdbsearch": "", + "components.Discover.tmdbstudio": "", + "components.Discover.tmdbtvgenre": "", + "components.Discover.tmdbtvkeyword": "", + "components.Discover.tmdbtvstreamingservices": "", + "components.Discover.tvgenres": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersKey": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersRemove": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersTip": "", + "components.Settings.Notifications.NotificationsWebhook.supportVariables": "", + "components.Settings.Notifications.NotificationsWebhook.supportVariablesTip": "", + "components.Settings.Notifications.NotificationsWebhook.webhookUrlTip": "", + "components.Settings.Notifications.useUserLocale": "", + "components.Settings.Notifications.validationMessageThreadId": "", + "components.Settings.RadarrModal.apiKeyHelp": "", + "components.Settings.RadarrModal.baseUrlHelp": "", + "components.Settings.RadarrModal.server4kHelp": "", + "components.Settings.RadarrModal.syncEnabledHelp": "", + "components.Settings.RadarrModal.tagRequests": "", + "components.Settings.SettingsJobsCache.dnscacheflushed": "", + "components.Settings.SettingsMain.originallanguageTip": "", + "components.Settings.SettingsMain.partialRequestsEnabled": "", + "components.Settings.SettingsMain.streamingRegion": "", + "components.Settings.SettingsNetwork.network": "", + "components.Settings.SettingsNetwork.networkDisclaimer": "", + "components.Settings.SettingsNetwork.networksettings": "", + "components.Settings.SonarrModal.animeSeriesType": "", + "components.Settings.SonarrModal.apiKeyHelp": "", + "components.Settings.SonarrModal.baseUrlHelp": "", + "components.Settings.SonarrModal.enableSearchHelp": "", + "components.Settings.SonarrModal.externalUrlHelp": "", + "components.Settings.clearBlocklistedTagsConfirm": "", + "components.Settings.clickTest": "", + "components.Settings.connectionTestFailed": "", + "components.Settings.copyBlocklistedTags": "", + "components.Settings.copyBlocklistedTagsTip": "", + "components.Settings.jellyfinSyncFailedAutomaticGroupedFolders": "", + "components.Settings.jellyfinlibraries": "", + "components.Settings.jellyfinlibrariesDescription": "", + "components.Settings.jellyfinsettings": "", + "components.Settings.jellyfinsettingsDescription": "", + "components.Settings.manualscanJellyfin": "", + "components.Settings.menuJellyfinSettings": "", + "components.Settings.menuMetadataProviders": "", + "components.Settings.metadataProviderSelection": "", + "components.Settings.metadataProviderSettings": "", + "components.Settings.metadataSettings": "", + "components.Settings.metadataSettingsSaved": "", + "components.Settings.noSpecialCharacters": "", + "components.Settings.nooptions": "", + "components.Settings.notTested": "", + "components.Settings.operational": "", + "components.Settings.overrideRules": "", + "components.Settings.overrideRulesDescription": "", + "components.Settings.save": "", + "components.Settings.saving": "", + "components.Settings.scanbackground": "", + "components.Settings.searchKeywords": "", + "components.Settings.seriesMetadataProvider": "", + "components.Settings.settings": "", + "components.Settings.starttyping": "", + "components.Settings.syncJellyfin": "", + "components.Settings.tvdbProviderDoesnotWork": "", + "components.Setup.signin": "", + "components.Setup.signinWithEmby": "", + "components.Setup.signinWithJellyfin": "", + "components.Setup.subtitle": "", + "components.StatusBadge.openinarr": "", + "components.StatusChecker.appUpdatedDescription": "", + "components.StatusChecker.restartRequired": "", + "components.TitleCard.tmdbid": "", + "components.TitleCard.tvdbid": "", + "components.TitleCard.watchlistDeleted": "", + "components.TvDetails.addtowatchlist": "", + "components.TvDetails.episodeCount": "", + "components.TvDetails.play": "", + "components.TvDetails.play4k": "", + "components.TvDetails.rtcriticsscore": "", + "components.TvDetails.seasonnumber": "", + "components.UserList.importedUsersNoPassword": "", + "components.UserList.importedfromJellyfin": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.password": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.passwordRequired": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.save": "", + "components.UserProfile.UserSettings.UserGeneralSettings.saving": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramMessageThreadId": "", + "components.UserProfile.UserSettings.UserPasswordChange.localPasswordDescription": "", + "components.UserProfile.localWatchlist": "", + "i18n.addToBlocklist": "", + "i18n.notificationLocale": "", + "components.UserProfile.UserSettings.UserGeneralSettings.mediaServerUser": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmoviestip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseries": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseriestip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.streamingRegion": "", + "components.UserProfile.UserSettings.UserGeneralSettings.streamingRegionTip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmail": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmailEmpty": "", + "components.UserProfile.UserSettings.UserGeneralSettings.validationemailformat": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.linkedAccounts": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.linkedAccountsHint": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.noLinkedAccounts": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.noPermissionDescription": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorUnauthorized": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablewebpush": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablingwebpusherror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablewebpush": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablingwebpusherror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.managedevices": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.nodevicestoshow": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.operatingsystem": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleted": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeendisabled": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdAdd": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdPlaceholder": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdRemove": "", + "components.Settings.RadarrModal.tagRequestsInfo": "", + "components.Discover.CreateSlider.addSlider": "", + "components.Discover.CreateSlider.addcustomslider": "", + "components.Discover.CreateSlider.addsuccess": "", + "components.Discover.CreateSlider.editSlider": "", + "components.Discover.CreateSlider.editfail": "", + "components.Discover.CreateSlider.editsuccess": "", + "components.Discover.CreateSlider.needresults": "", + "components.Discover.CreateSlider.nooptions": "", + "components.Discover.CreateSlider.providetmdbgenreid": "", + "components.Discover.CreateSlider.providetmdbkeywordid": "", + "components.Discover.CreateSlider.providetmdbstudio": "", + "components.Discover.CreateSlider.searchGenres": "", + "components.Discover.CreateSlider.searchKeywords": "", + "components.Discover.CreateSlider.searchStudios": "", + "components.Discover.CreateSlider.starttyping": "", + "components.Discover.CreateSlider.validationDatarequired": "", + "components.Discover.CreateSlider.validationTitlerequired": "", + "components.Discover.DiscoverMovies.activefilters": "", + "components.Discover.DiscoverMovies.sortPopularityAsc": "", + "components.Discover.DiscoverMovies.sortPopularityDesc": "", + "components.Discover.DiscoverMovies.sortReleaseDateAsc": "", + "components.Discover.DiscoverMovies.sortReleaseDateDesc": "", + "components.Discover.DiscoverMovies.sortTmdbRatingAsc": "", + "components.Discover.DiscoverMovies.sortTmdbRatingDesc": "", + "components.Discover.DiscoverSliderEdit.remove": "", + "components.Discover.DiscoverTv.activefilters": "", + "components.Discover.DiscoverTv.sortFirstAirDateDesc": "", + "components.Discover.DiscoverTv.sortPopularityAsc": "", + "components.Discover.DiscoverTv.sortTitleAsc": "", + "components.Discover.DiscoverTv.sortTitleDesc": "", + "components.Discover.DiscoverTv.sortTmdbRatingAsc": "", + "components.Discover.DiscoverTv.sortTmdbRatingDesc": "", + "components.Discover.DiscoverTvKeyword.keywordSeries": "", + "components.Discover.FilterSlideover.activefilters": "", + "components.Discover.FilterSlideover.clearfilters": "", + "components.Discover.FilterSlideover.filters": "", + "components.Discover.FilterSlideover.firstAirDate": "", + "components.Discover.FilterSlideover.from": "", + "components.Discover.FilterSlideover.genres": "", + "components.Discover.FilterSlideover.keywords": "", + "components.Discover.FilterSlideover.originalLanguage": "", + "components.Discover.FilterSlideover.ratingText": "", + "components.Discover.FilterSlideover.releaseDate": "", + "components.Discover.FilterSlideover.runtime": "", + "components.Discover.FilterSlideover.studio": "", + "components.Discover.FilterSlideover.tmdbuservotecount": "", + "components.Discover.FilterSlideover.voteCount": "", + "components.Discover.PlexWatchlistSlider.emptywatchlist": "", + "components.Discover.PlexWatchlistSlider.plexwatchlist": "", + "components.Discover.RecentlyAddedSlider.recentlyAdded": "", + "components.Layout.Sidebar.browsetv": "", + "components.Login.initialsignin": "", + "components.Login.hostname": "", + "components.Login.invalidurlerror": "", + "components.Login.port": "", + "components.Login.servertype": "", + "components.Login.loginwithapp": "", + "components.Login.noadminerror": "", + "components.Login.orsigninwith": "", + "components.Discover.FilterSlideover.certification": "", + "components.Discover.FilterSlideover.excludeKeywords": "", + "component.BlocklistBlock.blocklistdate": "", + "component.BlocklistBlock.blocklistedby": "", + "component.BlocklistModal.blocklisting": "", + "components.Blocklist.blocklistNotFoundError": "", + "components.Blocklist.blocklistSettingsDescription": "", + "components.Blocklist.blocklistdate": "", + "components.Blocklist.blocklistedby": "", + "components.Blocklist.filterBlocklistedTags": "", + "components.Blocklist.filterManual": "", + "components.Blocklist.mediaName": "", + "components.Blocklist.mediaTmdbId": "", + "components.Blocklist.mediaType": "", + "components.Blocklist.showAllBlocklisted": "", + "components.Layout.Sidebar.blocklist": "", + "components.CollectionDetails.removefromblocklistpartialcount": "", + "components.Discover.DiscoverMovieKeyword.keywordMovies": "", + "components.Discover.FilterSlideover.to": "", + "components.Discover.moviegenres": "", + "components.Discover.networks": "", + "components.Discover.resetsuccess": "", + "components.IssueList.IssueItem.descriptionpreview": "", + "components.Selector.CertificationSelector.starttyping": "", + "components.Discover.DiscoverMovies.discovermovies": "", + "components.Discover.DiscoverSliderEdit.deletefail": "", + "components.Discover.DiscoverSliderEdit.deletesuccess": "", + "components.Discover.DiscoverSliderEdit.enable": "", + "components.Discover.DiscoverTv.sortPopularityDesc": "", + "components.Discover.FilterSlideover.streamingservices": "", + "components.Discover.updatefailed": "", + "components.DownloadBlock.formattedTitle": "", + "components.Layout.Sidebar.browsemovies": "", + "components.Layout.UserWarnings.emailInvalid": "", + "components.Layout.UserWarnings.emailRequired": "", + "components.Layout.UserWarnings.passwordRequired": "", + "components.DiscoverTvUpcoming.upcomingtv": "", + "components.Selector.CertificationSelector.errorLoading": "", + "components.Selector.CertificationSelector.maxRating": "", + "components.Discover.RecentRequestsSlider.unableToConnect": "", + "components.Discover.FilterSlideover.status": "", + "components.Settings.timeout": "", + "components.Blocklist.blocklistsettings": "", + "components.Discover.CreateSlider.providetmdbnetwork": "", + "components.Discover.CreateSlider.providetmdbsearch": "", + "components.Discover.DiscoverTv.discovertv": "", + "components.Discover.DiscoverTv.sortFirstAirDateAsc": "", + "components.Discover.FilterSlideover.runtimeText": "", + "components.Discover.resetfailed": "", + "components.Layout.UserWarnings.profileIncomplete": "", + "components.Login.back": "", + "components.Login.description": "", + "components.Login.emailAlreadyTaken": "", + "components.Login.emailtooltip": "", + "components.Login.enablessl": "", + "components.Login.initialsigningin": "", + "components.Login.jellyfinLocalLoginHint": "", + "components.Login.save": "", + "components.Login.saveFailed": "", + "components.Login.saving": "", + "components.Login.urlBase": "", + "components.Login.username": "", + "components.Login.validationUrlBaseLeadingSlash": "", + "components.Login.validationUrlBaseTrailingSlash": "", + "components.Login.validationUrlTrailingSlash": "", + "components.Login.validationservertyperequired": "", + "components.Login.validationusernamerequired": "", + "components.ManageSlideOver.manageModalRemoveMediaWarning": "", + "components.MetadataSelector.selectMetdataProvider": "", + "components.MovieDetails.addtowatchlist": "", + "components.MovieDetails.downloadstatus": "", + "components.MovieDetails.imdbuserscore": "", + "components.MovieDetails.openradarr": "", + "components.MovieDetails.openradarr4k": "", + "components.MovieDetails.play": "", + "components.MovieDetails.play4k": "", + "components.MovieDetails.removefromwatchlist": "", + "components.MovieDetails.watchlistDeleted": "", + "components.MovieDetails.watchlistError": "", + "components.MovieDetails.watchlistSuccess": "", + "components.PermissionEdit.blocklistedItems": "", + "components.PermissionEdit.blocklistedItemsDescription": "", + "components.PermissionEdit.manageblocklist": "", + "components.PermissionEdit.manageblocklistDescription": "", + "components.PermissionEdit.viewblocklistedItemsDescription": "", + "components.RequestCard.cancelrequest": "", + "components.RequestCard.declinerequest": "", + "components.RequestList.RequestItem.profileName": "", + "components.RequestList.RequestItem.removearr": "", + "components.RequestList.RequestItem.tmdbid": "", + "components.RequestList.RequestItem.tvdbid": "", + "components.RequestList.RequestItem.unknowntitle": "", + "components.RequestList.sortDirection": "", + "components.RequestList.unableToConnect": "", + "components.RequestModal.pending4krequest": "", + "components.RequestModal.requestcollection4ktitle": "", + "components.RequestModal.requestmovietitle": "", + "components.RequestModal.requestseries4ktitle": "", + "components.RequestModal.requestseriestitle": "", + "components.Selector.CertificationSelector.minRating": "", + "components.Selector.CertificationSelector.selectCertification": "", + "components.Selector.canceled": "", + "components.Selector.inProduction": "", + "components.Selector.nooptions": "", + "components.Selector.pilot": "", + "components.Selector.planned": "", + "components.Selector.returningSeries": "", + "components.Selector.searchKeywords": "", + "components.Selector.searchUsers": "", + "components.Settings.Notifications.NotificationsGotify.validationPriorityRequired": "", + "components.Settings.Notifications.NotificationsNtfy.password": "", + "components.Settings.Notifications.NotificationsNtfy.priority": "", + "components.Settings.Notifications.NotificationsNtfy.token": "", + "components.Settings.Notifications.NotificationsNtfy.tokenAuth": "", + "components.Settings.Notifications.NotificationsNtfy.topic": "", + "components.Settings.Notifications.NotificationsNtfy.url": "", + "components.Settings.Notifications.NotificationsNtfy.username": "", + "components.Settings.Notifications.NotificationsPushover.deviceDefault": "", + "components.Settings.Notifications.NotificationsPushover.embedPoster": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersValue": "", + "components.Settings.Notifications.embedPoster": "", + "components.Settings.Notifications.messageThreadId": "", + "components.Settings.Notifications.messageThreadIdTip": "", + "components.Login.tipEmailHasTrailingWhitespace": "", + "components.Login.tipUsernameHasTrailingWhitespace": "", + "components.Login.validationhostformat": "", + "components.Login.urlBaseHelp": "", + "components.Login.validationEmailRequired": "", + "components.Login.validationPortRequired": "", + "components.Login.validationhostrequired": "", + "components.ManageSlideOver.removearr": "", + "components.ManageSlideOver.removearr4k": "", + "components.MetadataSelector.tmdbLabel": "", + "components.RequestCard.editrequest": "", + "components.RequestCard.tmdbid": "", + "components.Settings.Notifications.NotificationsNtfy.usernamePasswordAuth": "", + "components.Settings.Notifications.NotificationsNtfy.validationNtfyTopic": "", + "components.Settings.Notifications.NotificationsNtfy.validationNtfyUrl": "", + "components.Settings.Notifications.NotificationsNtfy.validationPriorityRequired": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersIncomplete": "", + "components.Settings.SettingsJobsCache.availability-sync": "", + "components.Settings.SettingsJobsCache.misses": "", + "components.Settings.SettingsNetwork.proxyEnabled": "", + "components.Settings.copyBlocklistedTagsEmpty": "", + "components.StatusChecker.reloadApp": "", + "components.TvDetails.status4k": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleteerror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.unknown": "", + "i18n.partiallyblocklisted": "", + "components.Settings.yes": "", + "components.Settings.manualscanDescriptionJellyfin": "", + "components.Setup.signinWithPlex": "", + "i18n.error": "", + "i18n.removeFromBlocklistSuccess": "", + "i18n.removefromBlocklist": "", + "i18n.specials": "", + "components.RequestModal.SearchByNameModal.nomatches": "", + "components.Settings.general": "", + "components.Settings.jellyfinSettings": "", + "components.Settings.jellyfinSyncFailedNoLibrariesFound": "", + "components.Setup.back": "", + "components.Login.adminerror": "", + "components.Discover.CreateSlider.addfail": "", + "i18n.completed": "", + "components.Settings.SettingsNetwork.docs": "", + "components.Settings.SettingsMain.apikey": "", + "components.StatusBadge.managemedia": "", + "components.StatusChecker.appUpdated": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorExists": "", + "components.Discover.FilterSlideover.tmdbuserscore": "", + "components.Settings.SettingsMain.enableSpecialEpisodes": "", + "components.TitleCard.watchlistCancel": "", + "components.UserProfile.UserSettings.UserNotificationSettings.deviceDefault": "", + "components.Settings.Notifications.NotificationsPushover.sound": "", + "components.Settings.Notifications.userEmailRequired": "", + "components.Discover.createnewslider": "", + "components.Discover.customizediscover": "", + "components.Settings.jellyfinSyncFailedGenericError": "", + "components.Login.title": "", + "components.TitleCard.mediaerror": "", + "components.Settings.SettingsAbout.supportseerr": "", + "components.Settings.SettingsJobsCache.imagecacheDescription": "", + "components.Settings.SettingsJobsCache.imagecachecount": "", + "components.Settings.SettingsJobsCache.imagecachesize": "", + "components.Settings.SettingsMain.validationUrl": "", + "components.Settings.restartrequiredTooltip": "", + "components.StatusChecker.restartRequiredDescription": "", + "components.TitleCard.addToWatchList": "", + "components.TitleCard.cleardata": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.type": "", + "components.Settings.SettingsJobsCache.dnsNoCacheEntries": "", + "components.Settings.SettingsJobsCache.dnscacheage": "", + "components.Settings.SettingsMain.cacheImagesTip": "", + "components.Settings.invalidurlerror": "", + "components.Settings.SonarrModal.monitorNewItems": "", + "components.TvDetails.manageseries": "", + "components.TvDetails.watchlistSuccess": "", + "components.UserList.ascending": "", + "components.UserProfile.UserSettings.UserGeneralSettings.save": "", + "components.Discover.timeWindowDay": "", + "components.Discover.timeWindowWeek": "", + "components.Discover.updatesuccess": "", + "components.Login.credentialerror": "", + "components.Login.validationEmailFormat": "", + "components.Login.validationemailformat": "", + "components.MetadataSelector.tvdbLabel": "", + "components.PermissionEdit.viewblocklistedItems": "", + "components.RequestModal.requestcollectiontitle": "", + "components.RequestModal.requestmovie4ktitle": "", + "components.Selector.CertificationSelector.noOptions": "", + "components.Settings.Notifications.NotificationsWebhook.customHeaders": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersAdd": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersAuthConflict": "", + "components.Settings.SettingsJobsCache.dnscachehits": "", + "components.Settings.SettingsJobsCache.editJobScheduleCurrent": "", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorDays": "", + "components.Settings.SettingsJobsCache.image-cache-cleanup": "", + "components.Settings.SettingsJobsCache.jellyfin-recently-added-scan": "", + "components.Settings.SettingsNetwork.proxyBypassFilterTip": "", + "components.Settings.importBlocklistedTagsTip": "", + "components.Settings.jellyfinForgotPasswordUrl": "", + "components.Discover.CreateSlider.slidernameplaceholder": "", + "components.Settings.jellyfinSettingsDescription": "", + "components.Settings.tip": "", + "components.StatusBadge.playonplex": "", + "components.StatusBadge.seasonepisodenumber": "", + "components.StatusBadge.seasonnumber": "", + "components.TvDetails.rtaudiencescore": "", + "components.TvDetails.seasonstitle": "", + "components.Settings.SettingsNetwork.apiRequestTimeout": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.engine": "", + "components.Settings.menuNetwork": "", + "components.Settings.SettingsLogs.viewdetails": "", + "components.Settings.SettingsUsers.mediaServerLogin": "", + "components.UserProfile.plexwatchlist": "", + "components.Login.signinwithjellyfin": "", + "components.Selector.searchGenres": "", + "components.Selector.searchStatus": "", + "components.Selector.searchStudios": "", + "components.Selector.showless": "", + "components.Selector.showmore": "", + "components.Selector.starttyping": "", + "components.Settings.Notifications.NotificationsGotify.priority": "", + "components.Settings.Notifications.NotificationsNtfy.agentenabled": "", + "components.Settings.Notifications.NotificationsNtfy.embedPoster": "", + "components.Settings.Notifications.NotificationsNtfy.ntfysettingsfailed": "", + "components.Settings.Notifications.NotificationsNtfy.ntfysettingssaved": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestFailed": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestSending": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestSuccess": "", + "components.Settings.Notifications.NotificationsSlack.embedPoster": "", + "components.Settings.Notifications.NotificationsWebPush.embedPoster": "", + "components.Settings.tmdbProviderDoesnotWork": "", + "components.UserList.mediaServerUser": "", + "components.Settings.SettingsUsers.loginMethodsTip": "", + "components.TvDetails.Season.noepisodes": "", + "components.Settings.SettingsJobsCache.size": "", + "components.Settings.no": "", + "components.Discover.DiscoverMovies.sortTitleAsc": "", + "components.Discover.stopediting": "", + "components.Settings.providerStatus": "", + "components.Settings.SettingsJobsCache.plex-refresh-token": "" } diff --git a/src/i18n/locale/sr.json b/src/i18n/locale/sr.json index 59d6241d6d..3d872a6f9a 100644 --- a/src/i18n/locale/sr.json +++ b/src/i18n/locale/sr.json @@ -614,5 +614,1059 @@ "components.Discover.DiscoverMovies.activefilters": "{count, plural, one {# Active Filter} остали {# Active Filters}}", "components.Discover.DiscoverMovies.sortPopularityAsc": "Популарност растућа", "components.Discover.DiscoverMovies.sortPopularityDesc": "Популарност опадајућа", - "components.Discover.DiscoverMovies.sortReleaseDateAsc": "Датум изласка растући" + "components.Discover.DiscoverMovies.sortReleaseDateAsc": "Датум изласка растући", + "components.Settings.Notifications.usePublicLogo": "", + "components.Settings.Notifications.usePublicLogoTip": "", + "components.Settings.Notifications.validationTypes": "", + "components.Settings.Notifications.webhookRoleId": "", + "components.Settings.OverrideRuleTile.conditions": "", + "components.Settings.OverrideRuleTile.genre": "", + "components.Settings.OverrideRuleTile.keywords": "", + "components.Settings.OverrideRuleTile.language": "", + "components.Settings.OverrideRuleTile.qualityprofile": "", + "components.Settings.RadarrModal.apiKeyHelp": "", + "components.Settings.RadarrModal.baseUrlHelp": "", + "components.Settings.RadarrModal.selecttags": "", + "components.Settings.RadarrModal.syncEnabledHelp": "", + "components.Settings.SettingsAbout.Releases.releasedataMissing": "", + "components.Settings.SettingsJobsCache.dnscachemisses": "", + "components.Settings.SettingsJobsCache.dnscachename": "", + "components.Settings.SettingsJobsCache.download-sync-reset": "", + "components.Settings.SettingsJobsCache.editJobScheduleCurrent": "", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorDays": "", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorMinutes": "", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorSeconds": "", + "components.Settings.SettingsJobsCache.failures": "", + "components.Settings.SettingsJobsCache.hitRate": "", + "components.Settings.SettingsJobsCache.hits": "", + "components.Settings.SettingsJobsCache.image-cache-cleanup": "", + "components.Settings.SettingsJobsCache.imagecacheDescription": "", + "components.Settings.SettingsJobsCache.imagecachesize": "", + "components.Settings.SettingsJobsCache.jobsDescription": "", + "components.Settings.SettingsJobsCache.misses": "", + "components.Settings.SettingsJobsCache.plex-recently-added-scan": "", + "components.Settings.SettingsJobsCache.plex-refresh-token": "", + "components.Settings.SettingsJobsCache.process-blocklisted-tags": "", + "components.Settings.SettingsLogs.copiedLogMessage": "", + "components.Settings.SettingsLogs.copyToClipboard": "", + "components.Settings.SettingsLogs.logsDescription": "", + "components.Settings.SettingsLogs.viewdetails": "", + "components.Settings.SettingsMain.apikey": "", + "components.Settings.SettingsMain.apikeyCopied": "", + "components.Settings.SettingsMain.blocklistLanguage": "", + "components.Settings.SettingsMain.blocklistLanguageTip": "", + "components.Settings.SettingsMain.blocklistRegion": "", + "components.Settings.SettingsMain.blocklistRegionTip": "", + "components.Settings.SettingsMain.blocklistedTags": "", + "components.Settings.SettingsMain.blocklistedTagsLimit": "", + "components.Settings.SettingsMain.blocklistedTagsLimitTip": "", + "components.Settings.SettingsMain.blocklistedTagsTip": "", + "components.Settings.SettingsMain.generalsettingsDescription": "", + "components.Settings.SettingsMain.hideAvailableTip": "", + "components.Settings.SettingsMain.hideBlocklistedTip": "", + "components.Settings.SettingsMain.locale": "", + "components.Settings.SettingsMain.originallanguage": "", + "components.Settings.SettingsMain.originallanguageTip": "", + "components.Settings.SettingsMain.partialRequestsEnabled": "", + "components.Settings.SettingsMain.streamingRegion": "", + "components.Settings.SettingsMain.streamingRegionTip": "", + "components.Settings.SettingsMain.toastApiKeyFailure": "", + "components.Settings.SettingsMain.toastSettingsSuccess": "", + "components.Settings.SettingsMain.validationApplicationUrl": "", + "components.Settings.SettingsMain.validationApplicationUrlTrailingSlash": "", + "components.Settings.SettingsMain.validationUrl": "", + "components.Settings.SettingsMain.validationUrlTrailingSlash": "", + "components.Settings.SettingsNetwork.csrfProtection": "", + "components.Settings.SettingsNetwork.dnsCacheTip": "", + "components.Settings.SettingsNetwork.networkDisclaimer": "", + "components.Settings.SettingsNetwork.proxyBypassLocalAddresses": "", + "components.Settings.SettingsNetwork.proxyEnabledTip": "", + "components.Settings.SettingsNetwork.trustProxyTip": "", + "components.Settings.SettingsNetwork.validationApiRequestTimeout": "", + "components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "", + "components.Settings.SettingsUsers.atLeastOneAuth": "", + "components.Settings.SettingsUsers.disabledMediaServerLoginWarning": "", + "components.Settings.SettingsUsers.newPlexLoginTip": "", + "components.Settings.SettingsUsers.toastSettingsFailure": "", + "components.Settings.SettingsUsers.tvRequestLimitLabel": "", + "components.Settings.SettingsUsers.userSettingsDescription": "", + "components.Settings.SonarrModal.apiKeyHelp": "", + "components.Settings.SonarrModal.baseUrlHelp": "", + "components.Settings.SonarrModal.enableSearchHelp": "", + "components.Settings.SonarrModal.externalUrlHelp": "", + "components.Settings.SonarrModal.monitorNewItemsHelp": "", + "components.Settings.SonarrModal.seriesType": "", + "components.Settings.SonarrModal.testFirstLanguageProfiles": "", + "components.Settings.SonarrModal.testFirstTags": "", + "components.Settings.SonarrModal.validationApplicationUrlTrailingSlash": "", + "components.Settings.SonarrModal.validationBaseUrlLeadingSlash": "", + "components.Settings.SonarrModal.validationBaseUrlTrailingSlash": "", + "components.Settings.addrule": "", + "components.Settings.advancedTooltip": "", + "components.RequestCard.failedmodify": "", + "components.RequestList.RequestItem.failedmodify": "", + "components.Settings.allChosenProvidersAreOperational": "", + "components.RequestModal.approve": "", + "components.Settings.animeMetadataProvider": "", + "components.Settings.blocklistedTagImportInstructions": "", + "components.Settings.blocklistedTagImportTitle": "", + "components.Settings.blocklistedTagsText": "", + "components.Settings.clearBlocklistedTagsConfirm": "", + "components.Settings.copyBlocklistedTagsEmpty": "", + "components.Settings.copyBlocklistedTagsTip": "", + "components.Settings.deleteServer": "", + "components.Settings.experimentalTooltip": "", + "components.Settings.failed": "", + "components.Settings.failedToSaveMetadataSettings": "", + "components.Settings.general": "", + "components.Settings.manualscanDescriptionJellyfin": "", + "components.Settings.metadataSettings": "", + "components.Settings.noDefault4kServer": "", + "components.Settings.noDefaultServer": "", + "components.Settings.noSpecialCharacters": "", + "components.Settings.nooptions": "", + "components.Settings.notTested": "", + "components.Settings.notificationAgentSettingsDescription": "", + "components.Settings.overrideRules": "", + "components.Settings.overrideRulesDescription": "", + "components.Settings.restartrequiredTooltip": "", + "components.Settings.save": "", + "components.Settings.saving": "", + "components.Settings.serverpresetLoad": "", + "components.Settings.Notifications.NotificationsPushover.sound": "", + "components.Settings.serviceSettingsDescription": "", + "components.Settings.settingUpPlexDescription": "", + "components.Settings.webAppUrlTip": "", + "components.Settings.webpush": "", + "components.Settings.yes": "", + "components.Setup.back": "", + "components.Setup.configemby": "", + "components.Setup.configplex": "", + "components.Setup.librarieserror": "", + "components.Setup.signinWithEmby": "", + "components.Setup.signinWithJellyfin": "", + "components.Setup.signinWithPlex": "", + "components.Setup.subtitle": "", + "components.StatusBadge.openinarr": "", + "components.StatusBadge.seasonepisodenumber": "", + "components.StatusBadge.seasonnumber": "", + "components.StatusChecker.appUpdated": "", + "components.StatusChecker.appUpdatedDescription": "", + "components.StatusChecker.reloadApp": "", + "components.StatusChecker.restartRequired": "", + "components.StatusChecker.restartRequiredDescription": "", + "components.TitleCard.cleardata": "", + "components.TitleCard.mediaerror": "", + "components.TvDetails.addtowatchlist": "", + "components.TvDetails.episodeCount": "", + "components.TvDetails.play": "", + "components.TvDetails.play4k": "", + "components.TvDetails.removefromwatchlist": "", + "components.TvDetails.rtaudiencescore": "", + "components.Settings.menuJellyfinSettings": "", + "components.Settings.menuMetadataProviders": "", + "components.TvDetails.seasonnumber": "", + "components.TvDetails.seasons": "", + "components.TvDetails.seasonstitle": "", + "components.TvDetails.status4k": "", + "components.TvDetails.streamingproviders": "", + "components.TvDetails.viewfullcrew": "", + "components.TvDetails.watchlistSuccess": "", + "components.UserList.edituser": "", + "components.UserList.importedPlexUsersNoPassword": "", + "components.UserList.importedUsersNoPassword": "", + "components.UserList.importedfromJellyfin": "", + "components.UserList.importedfromplex": "", + "components.UserList.importfromJellyfin": "", + "components.UserList.importfromJellyfinerror": "", + "components.UserList.importfrommediaserver": "", + "components.UserList.importfromplex": "", + "components.UserList.importfromplexerror": "", + "components.UserList.localLoginDisabled": "", + "components.UserList.mediaServerUser": "", + "components.UserList.passwordinfodescription": "", + "components.UserList.sortBy": "", + "components.UserList.sortByJoined": "", + "components.UserList.sortByRequests": "", + "components.UserList.sortByRole": "", + "components.UserList.sortByType": "", + "components.UserList.sortByUser": "", + "components.UserList.toggleSortDirection": "", + "components.UserList.toggleSortDirectionAria": "", + "components.UserProfile.ProfileHeader.userid": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.description": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorExists": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorUnknown": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.password": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.passwordRequired": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.save": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.saving": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.title": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.username": "", + "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegion": "", + "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegionTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.emailsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessTokenTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadIdTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPgpPublicKey": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPushbulletAccessToken": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverApplicationToken": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverUserKey": "", + "components.UserProfile.UserSettings.UserPasswordChange.localPasswordDescription": "", + "components.UserProfile.UserSettings.UserPasswordChange.noPasswordSet": "", + "components.UserProfile.UserSettings.UserPasswordChange.nopermissionDescription": "", + "components.UserProfile.UserSettings.UserPasswordChange.toastSettingsFailure": "", + "components.UserProfile.UserSettings.UserPasswordChange.validationConfirmPasswordSame": "", + "components.UserProfile.UserSettings.UserPermissions.toastSettingsFailure": "", + "components.UserProfile.UserSettings.UserPermissions.toastSettingsSuccess": "", + "components.UserProfile.UserSettings.UserPermissions.unauthorizedDescription": "", + "components.UserProfile.UserSettings.unauthorizedDescription": "", + "components.UserProfile.emptywatchlist": "", + "components.UserProfile.localWatchlist": "", + "components.UserProfile.recentlywatched": "", + "components.UserProfile.seriesrequest": "", + "i18n.areyousure": "", + "i18n.blocklist": "", + "i18n.blocklistDuplicateError": "", + "i18n.blocklistError": "", + "i18n.collection": "", + "i18n.completed": "", + "i18n.deleted": "", + "i18n.error": "", + "i18n.notificationLocale": "", + "i18n.partiallyblocklisted": "", + "i18n.removeFromBlocklistSuccess": "", + "i18n.removefromBlocklist": "", + "components.Discover.RecentRequestsSlider.unableToConnect": "", + "components.Discover.RecentlyAddedSlider.recentlyAdded": "", + "components.Discover.resetfailed": "", + "components.Discover.resettodefault": "", + "components.Discover.resetwarning": "", + "components.Discover.stopediting": "", + "components.Discover.studios": "", + "components.Discover.tmdbmoviegenre": "", + "components.Discover.tmdbmoviekeyword": "", + "components.Discover.tmdbmoviestreamingservices": "", + "components.Discover.tmdbnetwork": "", + "components.Discover.tmdbsearch": "", + "components.Discover.tmdbstudio": "", + "components.Discover.tmdbtvkeyword": "", + "components.Discover.tvgenres": "", + "components.Discover.updatefailed": "", + "components.Discover.updatesuccess": "", + "components.Settings.Notifications.NotificationsWebPush.embedPoster": "", + "components.Settings.Notifications.NotificationsWebPush.toastWebPushTestFailed": "", + "components.Settings.Notifications.NotificationsWebPush.toastWebPushTestSending": "", + "components.Settings.Notifications.NotificationsWebPush.toastWebPushTestSuccess": "", + "components.Settings.Notifications.NotificationsWebPush.webpushsettingsfailed": "", + "components.Settings.Notifications.NotificationsWebPush.webpushsettingssaved": "", + "components.Settings.Notifications.NotificationsWebhook.customHeaders": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersAdd": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersAuthConflict": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersIncomplete": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersKey": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersRemove": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersTip": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersValue": "", + "components.Settings.Notifications.NotificationsWebhook.resetPayload": "", + "components.Settings.Notifications.NotificationsWebhook.supportVariables": "", + "components.Settings.Notifications.NotificationsWebhook.supportVariablesTip": "", + "components.Settings.Notifications.NotificationsWebhook.toastWebhookTestFailed": "", + "components.Settings.Notifications.NotificationsWebhook.toastWebhookTestSending": "", + "components.Settings.Notifications.NotificationsWebhook.webhookUrlTip": "", + "components.Settings.Notifications.useUserLocale": "", + "components.Settings.Notifications.validationBotAPIRequired": "", + "components.Settings.Notifications.validationMessageThreadId": "", + "components.Settings.Notifications.validationPgpPassword": "", + "components.Settings.Notifications.webhookRoleIdTip": "", + "components.Settings.Notifications.webhookUrlTip": "", + "components.Settings.OverrideRuleModal.conditions": "", + "components.Settings.OverrideRuleModal.conditionsDescription": "", + "components.Settings.OverrideRuleModal.createrule": "", + "components.Settings.OverrideRuleModal.editrule": "", + "components.Settings.OverrideRuleModal.genres": "", + "components.Settings.OverrideRuleModal.selectQualityProfile": "", + "components.Settings.OverrideRuleModal.selectRootFolder": "", + "components.Settings.OverrideRuleModal.selectService": "", + "components.Settings.OverrideRuleModal.selecttags": "", + "components.Settings.OverrideRuleModal.serviceDescription": "", + "components.Settings.OverrideRuleModal.settings": "", + "components.Settings.OverrideRuleModal.settingsDescription": "", + "components.Settings.OverrideRuleModal.users": "", + "components.Settings.OverrideRuleTile.rootfolder": "", + "components.Settings.OverrideRuleTile.settings": "", + "components.Settings.OverrideRuleTile.tags": "", + "components.Settings.RadarrModal.default4kserver": "", + "components.Settings.RadarrModal.enableSearch": "", + "components.Settings.RadarrModal.enableSearchHelp": "", + "components.Settings.RadarrModal.externalUrlHelp": "", + "components.Settings.RadarrModal.server4kHelp": "", + "components.Settings.RadarrModal.tagRequests": "", + "components.Settings.RadarrModal.tagRequestsInfo": "", + "components.Settings.RadarrModal.validationBaseUrlTrailingSlash": "", + "components.Settings.SettingsAbout.runningDevelop": "", + "components.Settings.SettingsJobsCache.availability-sync": "", + "components.Settings.SettingsJobsCache.cacheflushed": "", + "components.Settings.SettingsJobsCache.dnsNoCacheEntries": "", + "components.Settings.SettingsJobsCache.dnscacheflushed": "", + "components.Settings.SettingsJobsCache.ipv4Fallbacks": "", + "components.Settings.SettingsJobsCache.jellyfin-full-scan": "", + "components.Settings.SettingsJobsCache.jellyfin-recently-added-scan": "", + "components.Settings.SettingsMain.applicationurl": "", + "components.Settings.SettingsUsers.mediaServerLoginTip": "", + "components.Settings.SonarrModal.syncEnabledHelp": "", + "components.Settings.SonarrModal.tagRequests": "", + "components.Settings.SonarrModal.tagRequestsInfo": "", + "components.Settings.importBlocklistedTagsTip": "", + "components.Settings.invalidKeyword": "", + "components.Settings.jellyfinSyncFailedGenericError": "", + "components.Settings.jellyfinlibraries": "", + "components.Settings.jellyfinlibrariesDescription": "", + "components.Settings.jellyfinsettings": "", + "components.Settings.jellyfinsettingsDescription": "", + "components.Settings.manualscanJellyfin": "", + "components.Settings.menuNetwork": "", + "components.Settings.metadataProviderSelection": "", + "components.Settings.metadataProviderSettings": "", + "components.Settings.settings": "", + "components.Settings.syncJellyfin": "", + "components.Settings.toastTautulliSettingsSuccess": "", + "components.Settings.tvdbProviderDoesnotWork": "", + "components.Settings.validationApiKey": "", + "components.Settings.validationUrlTrailingSlash": "", + "components.Settings.valueRequired": "", + "components.Settings.webAppUrl": "", + "components.Setup.configuremediaserver": "", + "components.UserList.autogeneratepasswordTip": "", + "components.UserList.usercreatedfailed": "", + "components.UserList.usercreatedsuccess": "", + "components.UserList.userfail": "", + "components.UserProfile.UserSettings.UserGeneralSettings.languageDefault": "", + "components.UserProfile.UserSettings.UserGeneralSettings.validationemailrequired": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.operatingsystem": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleted": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleteerror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pgpPublicKey": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessToken": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverApplicationToken": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverApplicationTokenTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverUserKeyTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoversettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.sound": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramChatIdTipLong": "", + "i18n.addToBlocklist": "", + "i18n.blocklisted": "", + "components.UserProfile.UserSettings.UserGeneralSettings.enableOverride": "", + "components.UserProfile.UserSettings.UserGeneralSettings.mediaServerUser": "", + "components.UserProfile.UserSettings.UserGeneralSettings.movierequestlimit": "", + "components.UserProfile.UserSettings.UserGeneralSettings.originallanguageTip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmovies": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmoviestip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseries": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseriestip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.regionTip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.save": "", + "components.UserProfile.UserSettings.UserGeneralSettings.saving": "", + "components.UserProfile.UserSettings.UserGeneralSettings.seriesrequestlimit": "", + "components.UserProfile.UserSettings.UserGeneralSettings.streamingRegionTip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailure": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmail": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmailEmpty": "", + "components.UserProfile.UserSettings.UserGeneralSettings.validationemailformat": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.errorUnknown": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.linkedAccountsHint": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorExists": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorUnauthorized": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.browser": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.created": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.deletesubscription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.device": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablewebpush": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablingwebpusherror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablewebpush": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablingwebpusherror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.engine": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.managedevices": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.nodevicestoshow": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.unknown": "", + "components.UserProfile.UserSettings.UserNotificationSettings.deviceDefault": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdAdd": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdPlaceholder": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdRemove": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "", + "components.Discover.FilterSlideover.clearfilters": "", + "components.Discover.DiscoverMovies.sortTmdbRatingDesc": "", + "components.Settings.Notifications.NotificationsPushbullet.channelTag": "", + "components.Discover.DiscoverSliderEdit.deletefail": "", + "components.Discover.DiscoverSliderEdit.deletesuccess": "", + "components.Discover.DiscoverSliderEdit.enable": "", + "components.Discover.DiscoverTv.activefilters": "", + "components.Discover.DiscoverTv.discovertv": "", + "components.Discover.DiscoverTv.sortFirstAirDateAsc": "", + "components.Discover.DiscoverTv.sortFirstAirDateDesc": "", + "components.Discover.DiscoverTv.sortPopularityAsc": "", + "components.Discover.DiscoverTv.sortTitleAsc": "", + "components.Discover.DiscoverTv.sortTitleDesc": "", + "components.Discover.DiscoverWatchlist.discoverwatchlist": "", + "components.Discover.DiscoverWatchlist.watchlist": "", + "components.Discover.FilterSlideover.activefilters": "", + "components.Discover.FilterSlideover.filters": "", + "components.Discover.FilterSlideover.firstAirDate": "", + "components.Discover.FilterSlideover.from": "", + "components.Discover.FilterSlideover.genres": "", + "components.Discover.FilterSlideover.keywords": "", + "components.Discover.FilterSlideover.originalLanguage": "", + "components.Discover.FilterSlideover.ratingText": "", + "components.Discover.FilterSlideover.releaseDate": "", + "components.Discover.FilterSlideover.runtime": "", + "components.Discover.FilterSlideover.runtimeText": "", + "components.Discover.FilterSlideover.streamingservices": "", + "components.Discover.PlexWatchlistSlider.emptywatchlist": "", + "components.Discover.PlexWatchlistSlider.plexwatchlist": "", + "components.DownloadBlock.formattedTitle": "", + "components.IssueDetails.IssueComment.postedbyedited": "", + "components.IssueDetails.closeissue": "", + "components.IssueDetails.closeissueandcomment": "", + "components.IssueDetails.openedby": "", + "components.IssueDetails.problemepisode": "", + "components.IssueDetails.problemseason": "", + "components.IssueDetails.reopenissue": "", + "components.IssueDetails.reopenissueandcomment": "", + "components.IssueDetails.toasteditdescriptionfailed": "", + "components.IssueDetails.toasteditdescriptionsuccess": "", + "components.IssueDetails.toastissuedeleted": "", + "components.IssueDetails.toastissuedeletefailed": "", + "components.IssueDetails.toaststatusupdated": "", + "components.IssueList.IssueItem.openeduserdate": "", + "components.IssueModal.CreateIssueModal.season": "", + "components.IssueModal.CreateIssueModal.submitissue": "", + "components.IssueModal.CreateIssueModal.toastFailedCreate": "", + "components.IssueModal.CreateIssueModal.toastSuccessCreate": "", + "components.Layout.VersionStatus.streamdevelop": "", + "components.PermissionEdit.adminDescription": "", + "components.RequestModal.requestApproved": "", + "components.Discover.FilterSlideover.status": "", + "components.Discover.FilterSlideover.certification": "", + "components.Discover.FilterSlideover.excludeKeywords": "", + "component.BlocklistModal.blocklisting": "", + "components.Blocklist.blocklistNotFoundError": "", + "components.Blocklist.blocklistSettingsDescription": "", + "components.Blocklist.blocklistdate": "", + "components.Blocklist.blocklistedby": "", + "components.Blocklist.blocklistsettings": "", + "components.Blocklist.filterBlocklistedTags": "", + "components.Blocklist.filterManual": "", + "components.Blocklist.mediaName": "", + "components.Blocklist.mediaTmdbId": "", + "components.Blocklist.mediaType": "", + "components.Blocklist.showAllBlocklisted": "", + "components.Discover.timeWindowWeek": "", + "components.Discover.DiscoverMovies.sortTmdbRatingAsc": "", + "components.Discover.DiscoverSliderEdit.remove": "", + "components.Discover.DiscoverTv.sortTmdbRatingAsc": "", + "components.Discover.DiscoverTv.sortTmdbRatingDesc": "", + "components.Discover.createnewslider": "", + "components.Discover.customizediscover": "", + "components.Discover.emptywatchlist": "", + "components.Discover.networks": "", + "components.Discover.plexwatchlist": "", + "components.Discover.resetsuccess": "", + "components.IssueDetails.IssueComment.postedby": "", + "components.IssueList.IssueItem.problemepisode": "", + "components.IssueList.IssueItem.seasons": "", + "components.IssueList.IssueItem.viewissue": "", + "components.IssueList.showallissues": "", + "components.IssueList.sortAdded": "", + "components.IssueModal.CreateIssueModal.problemseason": "", + "components.IssueModal.CreateIssueModal.whatswrong": "", + "components.LanguageSelector.languageServerDefault": "", + "components.LanguageSelector.originalLanguageDefault": "", + "components.Layout.LanguagePicker.displaylanguage": "", + "components.Layout.Sidebar.browsemovies": "", + "components.Layout.Sidebar.browsetv": "", + "components.Layout.UserDropdown.requests": "", + "components.NotificationTypeSelector.mediaAutoApprovedDescription": "", + "components.NotificationTypeSelector.notificationTypes": "", + "components.Settings.SettingsMain.toastSettingsFailure": "", + "components.Settings.syncing": "", + "components.TvDetails.productioncountries": "", + "components.Settings.jellyfinSyncFailedNoLibrariesFound": "", + "components.Setup.configjellyfin": "", + "components.Settings.SettingsMain.discoverRegionTip": "", + "components.DiscoverTvUpcoming.upcomingtv": "", + "components.Settings.OverrideRuleModal.keywords": "", + "components.Settings.OverrideRuleModal.languages": "", + "components.Settings.OverrideRuleModal.ruleUpdated": "", + "components.Settings.Notifications.embedPoster": "", + "components.Settings.clickTest": "", + "components.Settings.connectionTestFailed": "", + "component.BlocklistBlock.blocklistedby": "", + "components.CollectionDetails.removefromblocklistpartialcount": "", + "components.IssueDetails.toaststatusupdatefailed": "", + "components.IssueList.IssueItem.episodes": "", + "components.IssueModal.CreateIssueModal.allepisodes": "", + "components.IssueModal.CreateIssueModal.allseasons": "", + "components.IssueModal.CreateIssueModal.episode": "", + "components.IssueModal.CreateIssueModal.problemepisode": "", + "components.IssueModal.CreateIssueModal.providedetail": "", + "components.IssueModal.CreateIssueModal.reportissue": "", + "components.IssueModal.CreateIssueModal.toastviewissue": "", + "components.IssueModal.CreateIssueModal.validationMessageRequired": "", + "components.MovieDetails.productioncountries": "", + "components.Settings.RadarrModal.inCinemas": "", + "components.Settings.RadarrModal.loadingTags": "", + "components.Login.enablessl": "", + "components.Selector.pilot": "", + "components.Selector.searchUsers": "", + "components.IssueList.IssueItem.descriptionpreview": "", + "components.Settings.Notifications.NotificationsNtfy.topic": "", + "components.Settings.Notifications.NotificationsNtfy.url": "", + "components.Layout.UserDropdown.MiniQuotaDisplay.movierequests": "", + "components.PermissionEdit.blocklistedItems": "", + "components.Layout.Sidebar.blocklist": "", + "components.TvDetails.Season.noepisodes": "", + "components.Layout.UserDropdown.MiniQuotaDisplay.seriesrequests": "", + "components.Layout.UserWarnings.profileIncomplete": "", + "components.Login.emailAlreadyTaken": "", + "components.Login.jellyfinLocalLoginHint": "", + "components.Login.saveFailed": "", + "components.Login.tipEmailHasTrailingWhitespace": "", + "components.Login.tipUsernameHasTrailingWhitespace": "", + "components.Login.validationUrlBaseLeadingSlash": "", + "components.Login.validationUrlBaseTrailingSlash": "", + "components.Login.validationUrlTrailingSlash": "", + "components.MetadataSelector.selectMetdataProvider": "", + "components.MetadataSelector.tmdbLabel": "", + "components.MetadataSelector.tvdbLabel": "", + "components.MovieDetails.MovieCrew.fullcrew": "", + "components.MovieDetails.addtowatchlist": "", + "components.MovieDetails.rtcriticsscore": "", + "components.MovieDetails.streamingproviders": "", + "components.NotificationTypeSelector.issuereopened": "", + "components.NotificationTypeSelector.mediaavailableDescription": "", + "components.NotificationTypeSelector.mediarequestedDescription": "", + "components.PermissionEdit.advancedrequestDescription": "", + "components.PermissionEdit.autorequestMoviesDescription": "", + "components.PermissionEdit.autorequestSeries": "", + "components.PermissionEdit.blocklistedItemsDescription": "", + "components.PermissionEdit.manageblocklist": "", + "components.PermissionEdit.manageissuesDescription": "", + "components.PermissionEdit.request4kMovies": "", + "components.PermissionEdit.request4kTv": "", + "components.PermissionEdit.viewissuesDescription": "", + "components.PersonDetails.alsoknownas": "", + "components.PersonDetails.lifespan": "", + "components.QuotaSelector.seasons": "", + "components.RequestBlock.approve": "", + "components.RequestBlock.decline": "", + "components.RequestBlock.requestdate": "", + "components.RequestBlock.requestoverrides": "", + "components.RequestButton.viewrequest4k": "", + "components.RequestCard.approverequest": "", + "components.RequestCard.editrequest": "", + "components.RequestCard.tmdbid": "", + "components.RequestCard.unknowntitle": "", + "components.RequestModal.QuotaDisplay.allowedRequests": "", + "components.RequestModal.QuotaDisplay.allowedRequestsUser": "", + "components.RequestModal.QuotaDisplay.requiredquotaUser": "", + "components.RequestModal.QuotaDisplay.seasonlimit": "", + "components.RequestModal.SearchByNameModal.nomatches": "", + "components.RequestModal.SearchByNameModal.notvdbiddescription": "", + "components.RequestModal.errorediting": "", + "components.RequestModal.pendingapproval": "", + "components.RequestModal.requestcancelled": "", + "components.RequestModal.requestcollection4ktitle": "", + "components.RequestModal.requestcollectiontitle": "", + "components.RequestModal.requestedited": "", + "components.RequestModal.requesterror": "", + "components.RequestModal.requestmovies": "", + "components.RequestModal.requestmovies4k": "", + "components.RequestModal.requestseasons4k": "", + "components.RequestModal.requestseries4ktitle": "", + "components.RequestModal.requestseriestitle": "", + "components.ResetPassword.emailresetlink": "", + "components.ResetPassword.requestresetlinksuccessmessage": "", + "components.ResetPassword.validationpasswordmatch": "", + "components.Selector.CertificationSelector.errorLoading": "", + "components.Selector.CertificationSelector.maxRating": "", + "components.Selector.CertificationSelector.minRating": "", + "components.Selector.searchKeywords": "", + "components.Selector.searchStatus": "", + "components.Selector.searchStudios": "", + "components.Selector.showless": "", + "components.Selector.showmore": "", + "components.Selector.starttyping": "", + "components.Settings.Notifications.NotificationsGotify.agentenabled": "", + "components.Settings.Notifications.NotificationsGotify.gotifysettingsfailed": "", + "components.Settings.Notifications.NotificationsGotify.priority": "", + "components.Settings.Notifications.NotificationsGotify.toastGotifyTestFailed": "", + "components.Settings.Notifications.NotificationsGotify.validationPriorityRequired": "", + "components.Settings.Notifications.NotificationsGotify.validationTokenRequired": "", + "components.Settings.Notifications.NotificationsGotify.validationTypes": "", + "components.Settings.Notifications.NotificationsGotify.validationUrlRequired": "", + "components.Settings.Notifications.NotificationsGotify.validationUrlTrailingSlash": "", + "components.Settings.Notifications.NotificationsNtfy.agentenabled": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestSending": "", + "components.Settings.Notifications.NotificationsNtfy.token": "", + "components.Settings.Notifications.NotificationsNtfy.username": "", + "components.Settings.Notifications.NotificationsNtfy.usernamePasswordAuth": "", + "components.Settings.Notifications.NotificationsNtfy.validationNtfyTopic": "", + "components.Settings.Notifications.NotificationsNtfy.validationNtfyUrl": "", + "components.Settings.Notifications.NotificationsNtfy.validationPriorityRequired": "", + "components.Settings.Notifications.NotificationsNtfy.validationTypes": "", + "components.Settings.Notifications.NotificationsPushbullet.accessTokenTip": "", + "components.Settings.Notifications.NotificationsPushbullet.pushbulletSettingsFailed": "", + "components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestFailed": "", + "components.Settings.Notifications.NotificationsPushover.accessToken": "", + "components.Settings.Notifications.NotificationsPushover.deviceDefault": "", + "components.Settings.Notifications.NotificationsPushover.embedPoster": "", + "components.Settings.Notifications.NotificationsPushover.pushoversettingsfailed": "", + "components.Settings.Notifications.NotificationsPushover.pushoversettingssaved": "", + "components.Settings.Notifications.NotificationsPushover.userToken": "", + "components.Settings.Notifications.NotificationsPushover.validationAccessTokenRequired": "", + "components.Settings.Notifications.NotificationsPushover.validationTypes": "", + "components.Settings.Notifications.NotificationsPushover.validationUserTokenRequired": "", + "components.Settings.Notifications.NotificationsSlack.embedPoster": "", + "components.Settings.Notifications.NotificationsSlack.toastSlackTestFailed": "", + "components.Settings.Notifications.NotificationsSlack.toastSlackTestSuccess": "", + "components.Settings.Notifications.NotificationsSlack.validationTypes": "", + "components.Settings.Notifications.NotificationsSlack.validationWebhookUrl": "", + "components.Settings.Notifications.NotificationsSlack.webhookUrlTip": "", + "components.Settings.Notifications.allowselfsigned": "", + "components.Settings.Notifications.botAPI": "", + "components.Settings.Notifications.botAvatarUrl": "", + "components.Settings.Notifications.chatIdTip": "", + "components.Settings.Notifications.enableMentions": "", + "components.Settings.Notifications.encryption": "", + "components.Settings.Notifications.encryptionDefault": "", + "components.Settings.Notifications.encryptionImplicitTls": "", + "components.Settings.Notifications.encryptionTip": "", + "components.Settings.Notifications.messageThreadId": "", + "components.Settings.Notifications.pgpPrivateKey": "", + "components.Settings.Notifications.pgpPrivateKeyTip": "", + "components.Settings.Notifications.sendSilentlyTip": "", + "components.Settings.Notifications.telegramsettingsfailed": "", + "components.Settings.Notifications.toastDiscordTestFailed": "", + "components.Settings.Notifications.toastDiscordTestSuccess": "", + "components.Settings.Notifications.toastEmailTestSending": "", + "components.Settings.Notifications.toastEmailTestSuccess": "", + "components.Settings.Notifications.toastTelegramTestFailed": "", + "components.Settings.Notifications.toastTelegramTestSuccess": "", + "components.Settings.Notifications.userEmailRequired": "", + "components.Settings.SettingsAbout.aboutseerr": "", + "components.Settings.SettingsJobsCache.imagecache": "", + "components.Settings.SettingsJobsCache.imagecachecount": "", + "components.Settings.SettingsNetwork.validationProxyPort": "", + "components.Settings.SonarrModal.server4kHelp": "", + "components.Settings.scanbackground": "", + "components.Settings.searchKeywords": "", + "components.Settings.seriesMetadataProvider": "", + "components.Settings.tmdbProviderDoesnotWork": "", + "components.TvDetails.watchlistDeleted": "", + "components.TvDetails.watchlistError": "", + "components.UserList.validationUsername": "", + "components.UserList.validationpasswordminchars": "", + "components.UserProfile.UserSettings.UserGeneralSettings.streamingRegion": "", + "components.UserProfile.UserSettings.UserNotificationSettings.emailsettingsfailed": "", + "components.UserProfile.UserSettings.UserPasswordChange.toastSettingsFailureVerifyCurrent": "", + "components.Discover.FilterSlideover.studio": "", + "components.Discover.FilterSlideover.tmdbuservotecount": "", + "components.Discover.tmdbtvstreamingservices": "", + "components.Discover.moviegenres": "", + "components.Login.urlBaseHelp": "", + "components.Login.username": "", + "components.Login.validationEmailFormat": "", + "components.Login.validationEmailRequired": "", + "components.Login.validationPortRequired": "", + "components.ManageSlideOver.removearr": "", + "components.PermissionEdit.viewrecent": "", + "components.PermissionEdit.viewrecentDescription": "", + "components.PermissionEdit.viewrequestsDescription": "", + "components.PermissionEdit.viewwatchlistsDescription": "", + "components.Selector.CertificationSelector.noOptions": "", + "components.Selector.CertificationSelector.selectCertification": "", + "components.Selector.CertificationSelector.selectCountry": "", + "components.Selector.CertificationSelector.starttyping": "", + "components.Selector.canceled": "", + "components.Selector.ended": "", + "components.Selector.inProduction": "", + "components.Selector.planned": "", + "components.Selector.searchGenres": "", + "components.Settings.Notifications.NotificationsPushbullet.validationTypes": "", + "components.Settings.Notifications.NotificationsSlack.slacksettingsfailed": "", + "components.Settings.Notifications.NotificationsSlack.slacksettingssaved": "", + "components.Settings.Notifications.NotificationsWebhook.templatevariablehelp": "", + "components.Settings.Notifications.messageThreadIdTip": "", + "components.Settings.Notifications.pgpPasswordTip": "", + "components.Settings.SettingsMain.hideBlocklisted": "", + "components.Settings.SonarrModal.animeSeriesType": "", + "components.Settings.copyBlocklistedTags": "", + "components.Settings.operational": "", + "components.Settings.starttyping": "", + "components.TitleCard.watchlistCancel": "", + "components.TitleCard.watchlistDeleted": "", + "components.UserList.ascending": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorUnauthorized": "", + "i18n.blocklistSuccess": "", + "components.Settings.Notifications.NotificationsPushbullet.pushbulletSettingsSaved": "", + "components.Settings.Notifications.toastDiscordTestSending": "", + "components.Settings.Notifications.toastEmailTestFailed": "", + "components.Settings.Notifications.toastTelegramTestSending": "", + "components.Settings.Notifications.validationPgpPrivateKey": "", + "components.Settings.Notifications.validationUrl": "", + "components.Settings.RadarrModal.validationApplicationUrlTrailingSlash": "", + "components.Settings.RadarrModal.validationBaseUrlLeadingSlash": "", + "components.Settings.jellyfinForgotPasswordUrl": "", + "components.Settings.jellyfinSettings": "", + "components.Layout.VersionStatus.outofdate": "", + "components.Login.credentialerror": "", + "components.Login.description": "", + "components.MovieDetails.play": "", + "components.Layout.UserWarnings.emailInvalid": "", + "components.Login.loginwithapp": "", + "components.Login.save": "", + "components.UserList.username": "", + "components.Discover.FilterSlideover.tmdbuserscore": "", + "components.Discover.FilterSlideover.to": "", + "components.Discover.FilterSlideover.voteCount": "", + "components.PermissionEdit.request4kDescription": "", + "components.UserProfile.UserSettings.UserGeneralSettings.email": "", + "components.QuotaSelector.days": "", + "components.PermissionEdit.viewwatchlists": "", + "components.RequestModal.QuotaDisplay.movielimit": "", + "components.RequestModal.requestmovie4ktitle": "", + "components.NotificationTypeSelector.issuereopenedDescription": "", + "components.Settings.Notifications.NotificationsGotify.toastGotifyTestSuccess": "", + "components.Settings.Notifications.NotificationsGotify.token": "", + "components.Settings.Notifications.NotificationsGotify.url": "", + "components.Settings.Notifications.NotificationsPushover.accessTokenTip": "", + "components.Settings.Notifications.NotificationsWebPush.httpsRequirement": "", + "components.Settings.Notifications.botApiTip": "", + "components.ManageSlideOver.playedby": "", + "components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "", + "components.Layout.UserWarnings.emailRequired": "", + "components.MovieDetails.removefromwatchlist": "", + "components.Layout.UserWarnings.passwordRequired": "", + "components.ManageSlideOver.manageModalClearMediaWarning": "", + "components.ManageSlideOver.manageModalIssues": "", + "components.ManageSlideOver.manageModalMedia4k": "", + "components.RequestModal.QuotaDisplay.quotaLink": "", + "components.TvDetails.episodeRuntime": "", + "components.Selector.returningSeries": "", + "components.Settings.SonarrModal.notagoptions": "", + "components.Settings.metadataSettingsSaved": "", + "components.Settings.Notifications.NotificationsGotify.gotifysettingssaved": "", + "components.Settings.Notifications.NotificationsWebhook.resetPayloadSuccess": "", + "components.Settings.OverrideRuleModal.tags": "", + "components.Settings.SettingsJobsCache.flushdnscache": "", + "components.UserList.descending": "", + "component.BlocklistBlock.blocklistdate": "", + "components.Settings.Notifications.validationWebhookRoleId": "", + "components.Selector.nooptions": "", + "components.Settings.SettingsNetwork.trustProxy": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.usernameRequired": "", + "components.IssueList.sortModified": "", + "components.NotificationTypeSelector.userissuecommentDescription": "", + "components.TitleCard.tvdbid": "", + "components.Settings.OverrideRuleModal.notagoptions": "", + "components.RequestButton.requestmore4k": "", + "components.Settings.RadarrModal.testFirstTags": "", + "components.RequestCard.cancelrequest": "", + "components.RequestCard.declinerequest": "", + "components.ManageSlideOver.manageModalNoRequests": "", + "components.Settings.SettingsMain.youtubeUrl": "", + "components.RequestList.RequestItem.editrequest": "", + "components.TitleCard.tmdbid": "", + "components.TvDetails.manageseries": "", + "components.UserProfile.pastdays": "", + "components.Settings.SettingsNetwork.apiRequestTimeout": "", + "components.Discover.DiscoverTv.sortPopularityDesc": "", + "components.Settings.SettingsAbout.appDataPath": "", + "components.Layout.VersionStatus.commitsbehind": "", + "components.Settings.SettingsJobsCache.dnscacheactiveaddress": "", + "components.Layout.VersionStatus.streamstable": "", + "components.Login.adminerror": "", + "components.Login.back": "", + "components.Login.emailtooltip": "", + "components.Login.hostname": "", + "components.Login.initialsignin": "", + "components.Login.initialsigningin": "", + "components.Login.invalidurlerror": "", + "components.Login.noadminerror": "", + "components.Login.orsigninwith": "", + "components.Login.port": "", + "components.Login.saving": "", + "components.Login.servertype": "", + "components.Login.signinwithjellyfin": "", + "components.Settings.jellyfinSyncFailedAutomaticGroupedFolders": "", + "components.TvDetails.Season.somethingwentwrong": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.type": "", + "components.UserProfile.UserSettings.menuLinkedAccounts": "", + "components.Login.title": "", + "components.Login.urlBase": "", + "components.Settings.SettingsJobsCache.size": "", + "components.Settings.SettingsMain.generalsettings": "", + "components.Settings.SettingsMain.hideAvailable": "", + "components.Login.validationemailformat": "", + "components.Login.validationhostformat": "", + "components.Login.validationhostrequired": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramChatId": "", + "components.UserProfile.UserSettings.UserNotificationSettings.webpush": "", + "components.UserProfile.limit": "", + "components.Settings.SonarrModal.monitorNewItems": "", + "components.Settings.apiKey": "", + "components.Settings.invalidurlerror": "", + "components.TitleCard.addToWatchList": "", + "components.Login.validationservertyperequired": "", + "components.Login.validationusernamerequired": "", + "components.UserProfile.plexwatchlist": "", + "components.ManageSlideOver.manageModalClearMedia": "", + "components.RequestModal.QuotaDisplay.requiredquota": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.deleteFailed": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.linkedAccounts": "", + "components.ManageSlideOver.manageModalRemoveMediaWarning": "", + "components.ManageSlideOver.manageModalTitle": "", + "components.ManageSlideOver.markallseasons4kavailable": "", + "components.ManageSlideOver.markallseasonsavailable": "", + "components.ManageSlideOver.markavailable": "", + "components.ManageSlideOver.openarr": "", + "components.ManageSlideOver.openarr4k": "", + "components.ManageSlideOver.opentautulli": "", + "components.ManageSlideOver.pastdays": "", + "components.ManageSlideOver.plays": "", + "components.ManageSlideOver.removearr4k": "", + "components.MovieDetails.digitalrelease": "", + "components.MovieDetails.downloadstatus": "", + "components.MovieDetails.imdbuserscore": "", + "components.MovieDetails.managemovie": "", + "components.MovieDetails.mark4kavailable": "", + "components.MovieDetails.markavailable": "", + "components.MovieDetails.openradarr": "", + "components.MovieDetails.openradarr4k": "", + "components.MovieDetails.reportissue": "", + "components.MovieDetails.rtaudiencescore": "", + "components.MovieDetails.theatricalrelease": "", + "components.MovieDetails.tmdbuserscore": "", + "components.MovieDetails.watchlistDeleted": "", + "components.MovieDetails.watchlistError": "", + "components.MovieDetails.watchlistSuccess": "", + "components.NotificationTypeSelector.adminissuecommentDescription": "", + "components.NotificationTypeSelector.adminissuereopenedDescription": "", + "components.NotificationTypeSelector.adminissueresolvedDescription": "", + "components.NotificationTypeSelector.issuecomment": "", + "components.Discover.timeWindowDay": "", + "components.Discover.tmdbtvgenre": "", + "components.IssueDetails.deleteissueconfirm": "", + "components.MovieDetails.physicalrelease": "", + "components.NotificationTypeSelector.issuecommentDescription": "", + "components.NotificationTypeSelector.issuecreatedDescription": "", + "components.NotificationTypeSelector.issueresolved": "", + "components.RequestList.RequestItem.removearr": "", + "components.NotificationTypeSelector.issueresolvedDescription": "", + "components.NotificationTypeSelector.mediaAutoApproved": "", + "components.RequestModal.QuotaDisplay.requestsremaining": "", + "components.RequestModal.edit": "", + "components.RequestModal.pending4krequest": "", + "components.RequestModal.requestmovietitle": "", + "components.NotificationTypeSelector.mediaapprovedDescription": "", + "components.NotificationTypeSelector.mediaautorequested": "", + "components.Settings.Notifications.NotificationsNtfy.embedPoster": "", + "components.Settings.Notifications.NotificationsNtfy.ntfysettingsfailed": "", + "components.Settings.Notifications.NotificationsNtfy.ntfysettingssaved": "", + "components.Settings.Notifications.NotificationsNtfy.password": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestFailed": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestSuccess": "", + "components.Settings.Notifications.NotificationsNtfy.tokenAuth": "", + "components.Settings.Notifications.NotificationsPushover.userTokenTip": "", + "components.NotificationTypeSelector.mediaautorequestedDescription": "", + "components.Settings.Notifications.NotificationsWebhook.validationTypes": "", + "components.Settings.Notifications.NotificationsWebhook.toastWebhookTestSuccess": "", + "components.Settings.Notifications.NotificationsWebhook.webhooksettingsfailed": "", + "components.Settings.Notifications.NotificationsWebhook.webhooksettingssaved": "", + "components.NotificationTypeSelector.mediadeclinedDescription": "", + "components.NotificationTypeSelector.mediafailed": "", + "components.Settings.OverrideRuleModal.qualityprofile": "", + "components.Settings.OverrideRuleModal.rootfolder": "", + "components.Settings.OverrideRuleModal.ruleCreated": "", + "components.Settings.OverrideRuleModal.service": "", + "components.NotificationTypeSelector.userissuecreatedDescription": "", + "components.NotificationTypeSelector.userissuereopenedDescription": "", + "components.NotificationTypeSelector.userissueresolvedDescription": "", + "components.NotificationTypeSelector.usermediaAutoApprovedDescription": "", + "components.NotificationTypeSelector.usermediaapprovedDescription": "", + "components.NotificationTypeSelector.usermediaavailableDescription": "", + "components.Settings.SettingsJobsCache.cacheDescription": "", + "components.Settings.SettingsJobsCache.cachevsize": "", + "components.Settings.SettingsJobsCache.dnsCache": "", + "components.Settings.SettingsJobsCache.dnsCacheDescription": "", + "components.Settings.SettingsJobsCache.dnsCacheGlobalStats": "", + "components.Settings.SettingsJobsCache.dnsCacheGlobalStatsDescription": "", + "components.Settings.SettingsJobsCache.dnscachehits": "", + "components.Settings.SettingsJobsCache.plex-full-scan": "", + "components.Settings.SettingsJobsCache.plex-watchlist-sync": "", + "components.Settings.SettingsJobsCache.usersavatars": "", + "components.Settings.SettingsLogs.showall": "", + "components.Settings.SettingsMain.cacheImages": "", + "components.Settings.SettingsMain.cacheImagesTip": "", + "components.Settings.SettingsMain.discoverRegion": "", + "components.Settings.SettingsMain.enableSpecialEpisodes": "", + "components.Settings.SettingsMain.general": "", + "components.NotificationTypeSelector.usermediadeclinedDescription": "", + "components.Settings.SettingsMain.youtubeUrlTip": "", + "components.NotificationTypeSelector.usermediafailedDescription": "", + "components.Settings.SettingsNetwork.dnsCacheForceMaxTtl": "", + "components.Settings.SettingsNetwork.dnsCacheForceMinTtl": "", + "components.Settings.SettingsNetwork.dnsCacheHoverTip": "", + "components.Settings.SettingsNetwork.docs": "", + "components.Settings.SettingsNetwork.csrfProtectionHoverTip": "", + "components.Settings.SettingsNetwork.csrfProtectionTip": "", + "components.Settings.SettingsNetwork.dnsCache": "", + "components.Settings.SettingsUsers.defaultPermissionsTip": "", + "components.Settings.SettingsUsers.localLoginTip": "", + "components.Settings.SettingsUsers.loginMethods": "", + "components.Settings.SettingsUsers.loginMethodsTip": "", + "components.Settings.SettingsUsers.mediaServerLogin": "", + "components.NotificationTypeSelector.usermediarequestedDescription": "", + "components.Settings.jellyfinSettingsDescription": "", + "components.Settings.jellyfinSettingsFailure": "", + "components.Settings.jellyfinSettingsSuccess": "", + "components.PermissionEdit.autoapprove4kDescription": "", + "components.PermissionEdit.autoapproveDescription": "", + "components.Settings.noDefaultNon4kServer": "", + "components.PermissionEdit.autoapproveMoviesDescription": "", + "components.PermissionEdit.autoapproveSeriesDescription": "", + "components.PermissionEdit.autorequest": "", + "components.PermissionEdit.autorequestDescription": "", + "components.PermissionEdit.autorequestMovies": "", + "components.Settings.providerStatus": "", + "components.PermissionEdit.autorequestSeriesDescription": "", + "components.PermissionEdit.createissues": "", + "components.PermissionEdit.createissuesDescription": "", + "components.Settings.timeout": "", + "components.Settings.tip": "", + "components.Settings.validationUrlBaseLeadingSlash": "", + "components.Settings.validationUrlBaseTrailingSlash": "", + "components.TitleCard.watchlistError": "", + "components.TitleCard.watchlistSuccess": "", + "components.TvDetails.TvCrew.fullseriescrew": "", + "components.TvDetails.firstAirDate": "", + "components.TvDetails.originaltitle": "", + "components.PermissionEdit.manageblocklistDescription": "", + "components.PermissionEdit.manageissues": "", + "components.UserList.noJellyfinuserstoimport": "", + "components.PermissionEdit.managerequestsDescription": "", + "components.PermissionEdit.request4kMoviesDescription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadId": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramMessageThreadId": "", + "components.UserProfile.UserSettings.UserPasswordChange.noPasswordSetOwnAccount": "", + "components.PermissionEdit.request4kTvDescription": "", + "components.PermissionEdit.requestDescription": "", + "components.PermissionEdit.requestMovies": "", + "components.PermissionEdit.requestMoviesDescription": "", + "components.PermissionEdit.requestTv": "", + "components.PermissionEdit.requestTvDescription": "", + "components.PermissionEdit.usersDescription": "", + "components.PermissionEdit.viewblocklistedItems": "", + "components.PermissionEdit.viewblocklistedItemsDescription": "", + "components.PermissionEdit.viewissues": "", + "components.QuotaSelector.movieRequests": "", + "components.QuotaSelector.movies": "", + "components.QuotaSelector.tvRequests": "", + "components.RequestBlock.delete": "", + "components.RequestBlock.edit": "", + "components.RequestBlock.lastmodifiedby": "", + "components.Settings.SettingsNetwork.apiRequestTimeoutTip": "", + "components.Settings.SettingsNetwork.network": "", + "components.Settings.SettingsNetwork.networksettings": "", + "components.RequestBlock.requestedby": "", + "components.Settings.SettingsNetwork.networksettingsDescription": "", + "components.Settings.SettingsNetwork.proxyBypassFilter": "", + "components.Settings.SettingsNetwork.proxyBypassFilterTip": "", + "components.RequestButton.approve4krequests": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverUserKey": "", + "components.RequestButton.approverequest4k": "", + "components.RequestButton.approverequests": "", + "components.RequestButton.decline4krequests": "", + "components.RequestCard.failedretry": "", + "components.RequestCard.mediaerror": "", + "components.RequestCard.tvdbid": "", + "components.RequestList.RequestItem.cancelRequest": "", + "components.RequestList.RequestItem.failedretry": "", + "components.RequestList.RequestItem.mediaerror": "", + "components.RequestList.RequestItem.modifieduserdate": "", + "components.Settings.SettingsJobsCache.dnscacheage": "", + "components.RequestList.RequestItem.profileName": "", + "components.RequestList.RequestItem.tmdbid": "", + "components.RequestList.RequestItem.tvdbid": "", + "components.RequestList.RequestItem.unknowntitle": "", + "components.RequestList.showallrequests": "", + "components.RequestList.sortDirection": "", + "components.RequestList.unableToConnect": "", + "components.RequestModal.AdvancedRequester.notagoptions": "", + "components.RequestModal.AdvancedRequester.selecttags": "", + "components.RequestModal.QuotaDisplay.notenoughseasonrequests": "", + "components.Settings.Notifications.NotificationsNtfy.priority": "", + "components.Settings.Notifications.NotificationsPushover.toastPushoverTestFailed": "", + "components.Settings.Notifications.NotificationsPushover.toastPushoverTestSuccess": "", + "components.Settings.SettingsNetwork.forceIpv4First": "", + "components.Settings.SettingsNetwork.forceIpv4FirstTip": "", + "components.Settings.SettingsUsers.localLogin": "", + "components.Settings.SonarrModal.default4kserver": "", + "components.Settings.urlBase": "", + "components.StatusBadge.managemedia": "", + "components.TvDetails.rtcriticsscore": "", + "components.UserList.newJellyfinsigninenabled": "", + "components.UserList.newplexsigninenabled": "", + "components.RequestModal.QuotaDisplay.quotaLinkUser": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.noLinkedAccounts": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.noPermissionDescription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeendisabled": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeenenabled": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoversettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "", + "components.Settings.RadarrModal.notagoptions": "", + "components.Settings.SettingsAbout.supportseerr": "", + "components.Settings.SettingsMain.toastApiKeySuccess": "", + "components.TvDetails.reportissue": "", + "components.Settings.Notifications.telegramsettingssaved": "", + "components.Settings.OverrideRuleModal.create": "", + "components.Settings.OverrideRuleTile.users": "", + "components.TvDetails.tmdbuserscore": "", + "components.Discover.DiscoverTvKeyword.keywordSeries": "", + "components.NotificationTypeSelector.mediarequested": "", + "components.NotificationTypeSelector.mediafailedDescription": "", + "components.RequestButton.declinerequest4k": "", + "components.RequestButton.declinerequests": "", + "components.RequestModal.AdvancedRequester.animenote": "", + "components.Settings.Notifications.botUsernameTip": "", + "components.Settings.SettingsAbout.contribute": "", + "components.Settings.SettingsMain.validationApplicationTitle": "", + "components.Settings.SettingsUsers.movieRequestLimitLabel": "", + "components.Settings.SettingsUsers.newPlexLogin": "", + "components.Settings.chooseProvider": "", + "components.Setup.servertype": "", + "components.Settings.no": "", + "components.Settings.SettingsMain.applicationTitle": "", + "components.Settings.SettingsNetwork.proxyEnabled": "", + "components.Settings.SettingsNetwork.proxyHostname": "", + "components.Settings.SettingsNetwork.proxyPassword": "", + "components.Settings.SettingsNetwork.proxyPort": "", + "components.Settings.SettingsNetwork.proxySsl": "", + "components.Settings.SettingsNetwork.proxyUser": "", + "components.Settings.SettingsNetwork.toastSettingsFailure": "", + "components.Settings.SettingsNetwork.toastSettingsSuccess": "", + "i18n.restartRequired": "", + "i18n.specials": "", + "components.MovieDetails.play4k": "", + "components.NotificationTypeSelector.issuecreated": "", + "components.Setup.signin": "" } diff --git a/src/i18n/locale/sv.json b/src/i18n/locale/sv.json index 7fb2dc67d4..d4adbbaa0c 100644 --- a/src/i18n/locale/sv.json +++ b/src/i18n/locale/sv.json @@ -163,7 +163,7 @@ "pages.oops": "Hoppsan", "i18n.unavailable": "Otillgänglig", "i18n.tvshows": "Serier", - "i18n.processing": "Behandlar", + "i18n.processing": "Behandlas", "i18n.pending": "Väntande", "i18n.partiallyavailable": "Delvis tillgänglig", "i18n.movies": "Filmer", @@ -442,14 +442,14 @@ "components.UserProfile.UserSettings.UserPasswordChange.currentpassword": "Nuvarande lösenord", "components.UserProfile.UserSettings.UserPasswordChange.confirmpassword": "Bekräfta lösenord", "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramChatId": "Du måste ange ett giltigt chatt-ID", - "components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "Du måste ange ett giltigt användar-ID", + "components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "Varje ID måste vara ett giltigt Discord-användar-ID", "components.UserProfile.UserSettings.UserNotificationSettings.telegramChatIdTipLong": "Starta en chatt, lägg till @get_id_bot, och kör kommandot /my_id", "components.UserProfile.UserSettings.UserNotificationSettings.telegramChatId": "Chatt-ID", "components.UserProfile.UserSettings.UserNotificationSettings.sendSilentlyDescription": "Skicka meddelanden utan ljud", "components.UserProfile.UserSettings.UserNotificationSettings.sendSilently": "Skicka tyst", "components.UserProfile.UserSettings.UserNotificationSettings.notificationsettings": "Meddelandeinställningar", - "components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "Det flersiffriga ID-numret som är kopplat till ditt användarkonto", - "components.UserProfile.UserSettings.UserNotificationSettings.discordId": "Användar-ID", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "Det flersiffriga ID-numret som är kopplat till ditt användarkonto. Du kan lägga till fler än ett Discord-användar-ID om flera konton ska ta emot meddelanden.", + "components.UserProfile.UserSettings.UserNotificationSettings.discordId": "Användar-ID:n", "components.UserProfile.UserSettings.UserGeneralSettings.user": "Användare", "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsSuccess": "Inställningarna har sparats!", "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailure": "Något gick fel när inställningarna sparades.", @@ -689,7 +689,7 @@ "components.UserProfile.UserSettings.UserPasswordChange.noPasswordSet": "Det här användarkontot har för närvarande inget lösenord. Konfigurera ett lösenord nedan så att det här kontot kan logga in som en \"lokal användare.\"", "components.Settings.serviceSettingsDescription": "Konfigurera dina {serverType}-instanser nedan. Du kan ansluta flera {serverType}-instanser, men endast två av dem kan markeras som standard (en icke-4K och en 4K). Administratörer kan åsidosätta instansen som används för att bearbeta nya förfrågningar före godkännande.", "components.Settings.noDefaultServer": "Minst en {serverType}-instans måste markeras som standard för att {mediaType}-förfrågningar ska kunna bearbetas.", - "components.Settings.noDefaultNon4kServer": "Om du bara har en enda {serverType}-instans för både icke-4K- och 4K-innehåll (eller om du bara har 4K-innehåll) bör din {serverType}-instans INTE betecknas som en 4K-instans.", + "components.Settings.noDefaultNon4kServer": "Om du bara har en {serverType}-server för både innehåll som inte är i 4K och innehåll i 4K (eller om du bara laddar ner innehåll i 4K), ska din {serverType}-server inte anges som en 4K-server.", "components.Settings.mediaTypeSeries": "serie", "components.Settings.mediaTypeMovie": "film", "components.Settings.SettingsAbout.uptodate": "Uppdaterad", @@ -755,7 +755,7 @@ "components.Settings.Notifications.NotificationsPushover.accessTokenTip": "Registrera en applikation för användning med Seerr", "components.Settings.Notifications.NotificationsPushbullet.accessTokenTip": "Skapa en token från dina kontoinställningar", "components.DownloadBlock.estimatedtime": "Beräknad {time}", - "components.Settings.webAppUrlTip": "Alternativt skicka användare till den lokala webbappen istället för den \"hostade\" webbappen", + "components.Settings.webAppUrlTip": "Om du vill kan du istället hänvisa användare till webbappen på din server istället för https://app.plex.tv/desktop", "components.Settings.webAppUrl": "Webbapp-URL", "components.UserList.localLoginDisabled": "Inställningen Aktivera lokal inloggning är för närvarande inaktiverad.", "components.Settings.Notifications.NotificationsWebPush.httpsRequirement": "För att kunna ta emot webbpushmeddelanden måste Seerr presenteras över HTTPS.", @@ -965,9 +965,6 @@ "components.UserProfile.recentlywatched": "Nyligen sett", "components.Settings.validationUrlBaseTrailingSlash": "URL-basen får inte sluta med ett snedstreck", "components.Settings.validationUrlTrailingSlash": "URL får inte sluta med ett snedstreck", - "components.UserProfile.UserSettings.UserGeneralSettings.validationDiscordId": "Du måste ange ett giltigt Discord användar-ID", - "components.UserProfile.UserSettings.UserGeneralSettings.discordId": "Discord användar-ID", - "components.UserProfile.UserSettings.UserGeneralSettings.discordIdTip": "Det flersiffriga ID-numret som är kopplat till ditt Discord-användarkonto", "components.Settings.SettingsAbout.appDataPath": "Datakatalog", "components.RequestBlock.languageprofile": "Språkprofil", "components.StatusChecker.reloadApp": "Ladda om {applicationTitle}", @@ -1292,7 +1289,7 @@ "components.Setup.configjellyfin": "Konfigurera Jellyfin", "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegionTip": "Filtrera innehåll efter regional tillgänglighet", "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmailEmpty": "En annan användare har detta användarnamn. Du måste ange en e-postadress", - "components.Setup.librarieserror": "Valideringen misslyckades. För att fortsätta, slå av/på biblioteken igen.", + "components.Setup.librarieserror": "Det gick inte att läsa in bibliotek från din mediaserver. Kontrollera att den är nåbar och fortfarande korrekt konfigurerad.", "components.Settings.SettingsNetwork.forceIpv4FirstTip": "Tvinga Seerr att använda IPv4-adresser först, istället för IPv6", "components.Settings.SettingsNetwork.toastSettingsSuccess": "Inställningarna sparade!", "components.UserList.validationUsername": "Du måste ange ett användarnamn", @@ -1666,5 +1663,51 @@ "components.UserList.importedUsersNoPassword": "Importerade användare har inget lösenord för {applicationTitle} angivet. Om du inaktiverar inloggning via {mediaServerName} måste de ange ett lösenord i sin profil eller via en länk för återställning av lösenord.", "components.UserProfile.UserSettings.UserPasswordChange.localPasswordDescription": "Detta lösenord gäller för lokal inloggning med {applicationTitle}. Det är inte samma som ditt lösenord till medieservern.", "components.Settings.Notifications.useUserLocale": "Använd mottagarens valda språk", - "i18n.notificationLocale": "Språk för meddelanden" + "i18n.notificationLocale": "Språk för meddelanden", + "components.Settings.Notifications.usePublicLogo": "Använd Seerrs offentliga logotyp istället för instansens logotyp", + "components.Settings.Notifications.usePublicLogoTip": "Om din Seerr-instans inte är offentligt åtkomlig aktiverar du det här alternativet så att e-postklienter utanför ditt nätverk kan visa bilden. Bilden hämtas från det offentliga GitHub-repot.", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdAdd": "Lägg till användar-ID", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdPlaceholder": "Discord-användar-ID", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdRemove": "Ta bort", + "components.Settings.Notifications.validationWebhookThreadId": "Du måste ange ett giltigt Discord-tråd-ID", + "components.Settings.Notifications.webhookThreadId": "Tråd-ID", + "components.Settings.Notifications.webhookThreadIdTip": "ID:t för kanalen där meddelanden ska publiceras. Lämna tomt för att publicera i webhook-kanalen", + "components.UserProfile.UserSettings.UserNotificationSettings.discordNotificationsNotEnabled": "Discord-meddelanden har inte aktiverats av serverägaren. Informationen används bara om serverägaren konfigurerar en extern tjänst.", + "components.RequestModal.AdvancedRequester.ignoreQuotaDescription": "Den här begäran räknas inte mot användarens kvotgränser. Använd med försiktighet.", + "components.RequestModal.AdvancedRequester.ignoreQuotaTitle": "Åsidosätt användarens kvot", + "components.Common.QuickConnectModal.error": "Fel", + "components.Common.QuickConnectModal.expired": "Koden har gått ut", + "components.Common.QuickConnectModal.expiredMessage": "Denna Quick Connect-kod har gått ut. Försök igen.", + "components.Common.QuickConnectModal.tryAgain": "Försök igen", + "components.Common.QuickConnectModal.waitingForAuth": "Väntar på auktorisering...", + "components.Login.JellyfinQuickConnectModal.cancel": "Avbryt", + "components.Login.JellyfinQuickConnectModal.instructions": "Ange den här koden i din {mediaServerName}-app", + "components.Login.JellyfinQuickConnectModal.subtitle": "Logga in med Quick Connect", + "components.Login.JellyfinQuickConnectModal.title": "Quick Connect", + "components.Login.quickconnect": "Quick Connect", + "components.Login.quickconnecterror": "Quick Connect misslyckades. Försök igen.", + "components.UserList.importfromplexnochanges": "Importen från Plex är klar. Inga användare skapades eller uppdaterades.", + "components.UserList.importfromplexsynced": "Plex-användare synkroniserades. Befintliga användare uppdaterades.", + "components.UserList.syncnotice": "Du kan fortfarande klicka på Synkronisera för att uppdatera befintliga Plex-användare, till exempel om deras e-postadress eller användarnamn har ändrats.", + "components.UserProfile.UserSettings.LinkJellyfinModal.quickConnect": "Använd Quick Connect", + "components.UserProfile.UserSettings.LinkJellyfinQuickConnectModal.instructions": "Ange den här koden i din {mediaServerName}-app", + "components.UserProfile.UserSettings.LinkJellyfinQuickConnectModal.subtitle": "Quick Connect", + "components.UserProfile.UserSettings.LinkJellyfinQuickConnectModal.title": "Länka {mediaServerName}-konto", + "components.UserProfile.UserSettings.LinkJellyfinQuickConnectModal.usePassword": "Använd lösenord istället", + "hooks.useQuickConnect.errorMessage": "Det gick inte att initiera Quick Connect. Försök igen.", + "i18n.sync": "Synkronisera", + "i18n.syncing": "Sykroniserar…", + "components.Settings.SettingsAbout.versionCheckDisabled": "Versionskontroll inaktiverad", + "components.Settings.SettingsMain.versionCheck": "Versionskontroll", + "components.Settings.SettingsMain.versionCheckTip": "Sök automatiskt efter nya versioner på GitHub.", + "components.ManageSlideOver.clearmediadataerror": "Något gick fel när mediadatan rensades.", + "components.ManageSlideOver.removemediaerror": "Något gick fel när median togs bort.", + "components.RequestList.RequestItem.removemediaerror": "Något gick fel när median togs bort.", + "components.Settings.Notifications.NotificationsGotify.embedPoster": "Inkludera affisch", + "components.Settings.jellyfinSyncFailedConnectionError": "Det gick inte att nå servern {mediaServerName}. Kontrollera att den körs och kan nås från Seerr.", + "components.Settings.toastPlexSyncFailure": "Misslyckades med att synkronisera Plex-bibliotek.", + "components.Settings.toggleLibraryFailure": "Misslyckades med att uppdatera biblioteket.", + "components.Settings.Notifications.NotificationsNtfy.tags": "Taggar", + "components.Settings.Notifications.NotificationsNtfy.tagsPlaceholder": "eyes,warning", + "components.UserList.searchUsers": "Sök…" } diff --git a/src/i18n/locale/tr.json b/src/i18n/locale/tr.json index c8ae7d9b3e..ddb3379231 100644 --- a/src/i18n/locale/tr.json +++ b/src/i18n/locale/tr.json @@ -1,182 +1,182 @@ { - "components.Discover.DiscoverTv.sortTmdbRatingAsc": "TMBD Derecelendirmesi (Artan)", + "components.Discover.DiscoverTv.sortTmdbRatingAsc": "TMDB Puanı Artan", "components.Discover.moviegenres": "Film Türleri", - "components.Discover.CreateSlider.editsuccess": "Slider düzenlendi ve keşfet özelleştirme ayarları kaydedildi.", + "components.Discover.CreateSlider.editsuccess": "Kaydırıcı düzenlendi ve keşfet özelleştirme ayarları kaydedildi.", "components.Discover.FilterSlideover.studio": "Stüdyo", "components.CollectionDetails.numberofmovies": "{count} Film", - "components.Discover.PlexWatchlistSlider.emptywatchlist": "Plex İzleme Listenize eklenen içerikler burada gözükeceklerdir.", - "components.Discover.CreateSlider.slidernameplaceholder": "Slider İsmi", - "components.Discover.RecentlyAddedSlider.recentlyAdded": "Yakın Zamanda Eklenenler", + "components.Discover.PlexWatchlistSlider.emptywatchlist": "Plex İzleme Listesi'ne eklenen içerikler burada görünecek.", + "components.Discover.CreateSlider.slidernameplaceholder": "Kaydırıcı Adı", + "components.Discover.RecentlyAddedSlider.recentlyAdded": "Son Eklenenler", "components.Discover.FilterSlideover.keywords": "Anahtar Kelimeler", - "components.Discover.FilterSlideover.ratingText": "{minValue} ile {maxValue} arasında ki değerlendirmeler", - "components.Discover.DiscoverTv.sortFirstAirDateAsc": "İlk Yayınlanma Tarihi (Yeni)", - "components.AppDataWarning.dockerVolumeMissingDescription": "Bağlanmış {appDataPath} dizini düzgün bir şekilde yapılandırılmamış. Tüm veriler konteyner yeniden başlatıldığında veya durdurulduğunda temizlenecektir.", + "components.Discover.FilterSlideover.ratingText": "{minValue} ile {maxValue} arasındaki puanlar", + "components.Discover.DiscoverTv.sortFirstAirDateAsc": "İlk Yayın Tarihi Artan", + "components.AppDataWarning.dockerVolumeMissingDescription": "{appDataPath} birim bağlantısı düzgün yapılandırılmamış. Kapsayıcı durdurulduğunda veya yeniden başlatıldığında tüm veriler silinecektir.", "components.Discover.DiscoverTvKeyword.keywordSeries": "{keywordTitle} Dizileri", - "components.Discover.DiscoverMovies.sortPopularityDesc": "Popülerlik (Azalan)", + "components.Discover.DiscoverMovies.sortPopularityDesc": "Popülerlik Azalan", "components.Discover.StudioSlider.studios": "Stüdyolar", - "components.AirDateBadge.airsrelative": "Yayınlanıyor {relativeTime}", - "components.Discover.DiscoverTv.sortTmdbRatingDesc": "TMBD Derecelendirmesi (Azalan)", - "components.Discover.customizediscover": "Keşfet'i Özelleştir", - "components.Discover.emptywatchlist": "Plex İzleme Listenize eklenen içerikler burada gözükeceklerdir.", + "components.AirDateBadge.airsrelative": "{relativeTime} yayınlanıyor", + "components.Discover.DiscoverTv.sortTmdbRatingDesc": "TMDB Puanı Azalan", + "components.Discover.customizediscover": "Keşfeti Özelleştir", + "components.Discover.emptywatchlist": "Plex İzleme Listesi'ne eklenen içerikler burada görünecek.", "components.Discover.populartv": "Popüler Diziler", - "components.CollectionDetails.overview": "Özet", - "components.Discover.DiscoverMovies.activefilters": "Filtrele", - "components.Discover.DiscoverMovies.sortTmdbRatingAsc": "TMBD Derecelendirmesi (Artan)", + "components.CollectionDetails.overview": "Genel Bakış", + "components.Discover.DiscoverMovies.activefilters": "{count, plural, one {# Aktif Filtre} other {# Aktif Filtre}}", + "components.Discover.DiscoverMovies.sortTmdbRatingAsc": "TMDB Puanı Artan", "components.Discover.DiscoverTvGenre.genreSeries": "{genre} Dizileri", - "components.AirDateBadge.airedrelative": "Yayınlandı {relativeTime}", - "components.Discover.FilterSlideover.clearfilters": "Filtreleri Temizle", - "components.Discover.CreateSlider.searchStudios": "Stüdyolarda Ara.…", - "components.Discover.DiscoverMovies.sortReleaseDateDesc": "Çıkış Tarihi (Azalan)", - "components.Discover.CreateSlider.providetmdbnetwork": "TMDB Ağ Kimliğini Sağlayın", + "components.AirDateBadge.airedrelative": "{relativeTime} yayınlandı", + "components.Discover.FilterSlideover.clearfilters": "Aktif Filtreleri Temizle", + "components.Discover.CreateSlider.searchStudios": "Stüdyoları ara…", + "components.Discover.DiscoverMovies.sortReleaseDateDesc": "Çıkış Tarihi Azalan", + "components.Discover.CreateSlider.providetmdbnetwork": "TMDB Ağ Kimliği girin", "components.Discover.MovieGenreSlider.moviegenres": "Film Türleri", - "components.Discover.networks": "İnternet Yayınları", - "components.Discover.CreateSlider.addfail": "Slider oluşturulamadı.", - "components.CollectionDetails.requestcollection": "Koleksiyonu Talep Et", + "components.Discover.networks": "Kanallar", + "components.Discover.CreateSlider.addfail": "Yeni kaydırıcı oluşturulamadı.", + "components.CollectionDetails.requestcollection": "Koleksiyon Talep Et", "components.Discover.TvGenreSlider.tvgenres": "Dizi Türleri", "components.Discover.DiscoverMovieGenre.genreMovies": "{genre} Filmleri", - "components.Discover.DiscoverMovieLanguage.languageMovies": "{language} Filmler", + "components.Discover.DiscoverMovieLanguage.languageMovies": "{language} Filmleri", "components.Discover.TvGenreList.seriesgenres": "Dizi Türleri", - "components.Discover.DiscoverTv.activefilters": "{count, plural, one {# Filtre} other {# Filtre}}", - "components.Discover.DiscoverTvLanguage.languageSeries": "{language} Diziler", - "components.Discover.DiscoverMovies.sortPopularityAsc": "Popülerlik (Artan)", - "components.Discover.NetworkSlider.networks": "İnternet Yayınları", + "components.Discover.DiscoverTv.activefilters": "{count, plural, one {# Aktif Filtre} other {# Aktif Filtre}}", + "components.Discover.DiscoverTvLanguage.languageSeries": "{language} Dizileri", + "components.Discover.DiscoverMovies.sortPopularityAsc": "Popülerlik Artan", + "components.Discover.NetworkSlider.networks": "Kanallar", "components.Discover.FilterSlideover.streamingservices": "Yayın Hizmetleri", - "components.Discover.CreateSlider.needresults": "Seçtiğin etiketler en az 1 adet sonuç göstermelidir.", + "components.Discover.CreateSlider.needresults": "En az 1 sonuç olması gerekiyor.", "components.Discover.popularmovies": "Popüler Filmler", - "components.Discover.FilterSlideover.activefilters": "{count, plural, one {# Filtre} other {# Filtre}}", + "components.Discover.FilterSlideover.activefilters": "{count, plural, one {# Aktif Filtre} other {# Aktif Filtre}}", "components.Discover.plexwatchlist": "İzleme Listeniz", - "components.Discover.CreateSlider.addcustomslider": "Özel Slider Oluştur", - "components.Discover.FilterSlideover.tmdbuserscore": "TMBD Kullanıcı Skoru", - "components.Discover.DiscoverTv.sortPopularityAsc": "Popülerlik (Artan)", - "components.Discover.CreateSlider.editSlider": "Slider'ı düzenle", - "components.Discover.DiscoverTv.sortTitleAsc": "İsim (A-Z) (Artan)", - "components.Discover.CreateSlider.validationDatarequired": "Bir veri değeri sağlamalısınız.", - "components.Discover.DiscoverTv.sortFirstAirDateDesc": "İlk Yayınlanma Tarihi (Eski)", + "components.Discover.CreateSlider.addcustomslider": "Özel Kaydırıcı Oluştur", + "components.Discover.FilterSlideover.tmdbuserscore": "TMDB Kullanıcı Puanı", + "components.Discover.DiscoverTv.sortPopularityAsc": "Popülerlik Artan", + "components.Discover.CreateSlider.editSlider": "Kaydırıcıyı Düzenle", + "components.Discover.DiscoverTv.sortTitleAsc": "Başlık (A-Z) Artan", + "components.Discover.CreateSlider.validationDatarequired": "Bir veri değeri girmelisiniz.", + "components.Discover.DiscoverTv.sortFirstAirDateDesc": "İlk Yayın Tarihi Azalan", "components.Discover.DiscoverWatchlist.discoverwatchlist": "İzleme Listeniz", "components.Discover.FilterSlideover.releaseDate": "Çıkış Tarihi", "components.Discover.DiscoverTv.discovertv": "Diziler", - "components.Discover.recentlyAdded": "Yakın Zamanda Eklenenler", - "components.Discover.DiscoverSliderEdit.deletefail": "Slider silinemedi.", - "components.Discover.CreateSlider.providetmdbstudio": "TMBD'den Stüdyo etiketi seç", + "components.Discover.recentlyAdded": "Son Eklenenler", + "components.Discover.DiscoverSliderEdit.deletefail": "Kaydırıcı silinemedi.", + "components.Discover.CreateSlider.providetmdbstudio": "TMDB Stüdyo Kimliği girin", "components.Discover.FilterSlideover.runtime": "Süre", - "components.Discover.FilterSlideover.from": "Tarihinden", - "components.Discover.DiscoverMovies.sortTitleDesc": "İsim (Z-A) (Azalan)", + "components.Discover.FilterSlideover.from": "Başlangıç", + "components.Discover.DiscoverMovies.sortTitleDesc": "Başlık (Z-A) Azalan", "components.Discover.DiscoverStudio.studioMovies": "{studio} Filmleri", - "components.Discover.DiscoverTv.sortPopularityDesc": "Popülerlik (Azalan)", - "components.Discover.CreateSlider.searchGenres": "Türlerde ara…", - "components.Discover.CreateSlider.editfail": "Slider düzenlenirken hata oluştu.", - "components.Discover.CreateSlider.starttyping": "Aramak için yazmaya başla.", - "components.Discover.createnewslider": "Yeni Slider Oluştur", + "components.Discover.DiscoverTv.sortPopularityDesc": "Popülerlik Azalan", + "components.Discover.CreateSlider.searchGenres": "Türleri ara…", + "components.Discover.CreateSlider.editfail": "Kaydırıcı düzenlenemedi.", + "components.Discover.CreateSlider.starttyping": "Aramak için yazmaya başlayın.", + "components.Discover.createnewslider": "Yeni Kaydırıcı Oluştur", "components.Discover.FilterSlideover.filters": "Filtreler", - "components.Discover.DiscoverWatchlist.watchlist": "Plex İzleme Listen", + "components.Discover.DiscoverWatchlist.watchlist": "Plex İzleme Listesi", "components.Discover.discover": "Keşfet", "components.Discover.DiscoverSliderEdit.enable": "Görünürlüğü Değiştir", - "components.Discover.CreateSlider.addSlider": "Slider'ı Ekle", - "components.CollectionDetails.requestcollection4k": "Koleksiyonu 4K Kalitesiyle Talep Et", + "components.Discover.CreateSlider.addSlider": "Kaydırıcı Ekle", + "components.CollectionDetails.requestcollection4k": "4K Koleksiyon Talep Et", "components.Discover.FilterSlideover.firstAirDate": "İlk Yayın Tarihi", "components.Discover.CreateSlider.providetmdbsearch": "Bir arama sorgusu girin", "components.Discover.DiscoverNetwork.networkSeries": "{network} Dizileri", - "components.Discover.CreateSlider.providetmdbkeywordid": "TMBD'den Anahtar Kelime etiketi seç", + "components.Discover.CreateSlider.providetmdbkeywordid": "Bir TMDB Anahtar Kelime Kimliği girin", "components.Discover.DiscoverMovieKeyword.keywordMovies": "{keywordTitle} Filmleri", - "components.Discover.FilterSlideover.runtimeText": "{minValue}-{maxValue} dakika oynatma süresi", - "components.Discover.CreateSlider.validationTitlerequired": "Bir başlık belirtmeniz gerekmektedir.", + "components.Discover.FilterSlideover.runtimeText": "{minValue}-{maxValue} dakika süre", + "components.Discover.CreateSlider.validationTitlerequired": "Bir başlık girmelisiniz.", "components.Discover.PlexWatchlistSlider.plexwatchlist": "İzleme Listeniz", - "components.Discover.DiscoverMovies.sortReleaseDateAsc": "Çıkış Tarihi (Artan)", + "components.Discover.DiscoverMovies.sortReleaseDateAsc": "Çıkış Tarihi Artan", "components.Discover.FilterSlideover.genres": "Türler", - "components.Discover.FilterSlideover.originalLanguage": "Orijinal Dili", - "components.Discover.CreateSlider.nooptions": "Sonuç Bulunamadı.", - "components.Discover.DiscoverMovies.sortTmdbRatingDesc": "TMBD Derecelendirmesi (Azalan)", - "components.Discover.CreateSlider.searchKeywords": "Anahtar Kelimeler ara…", - "components.Discover.FilterSlideover.tmdbuservotecount": "TMBD Oy Kullanan Kullanıcı Sayısı", - "components.Discover.CreateSlider.addsuccess": "Yeni slider oluşturuldu ve keşfet sekmesinin ayarları kaydedildi.", - "components.Discover.DiscoverSliderEdit.deletesuccess": "Slider başarıyla silindi.", + "components.Discover.FilterSlideover.originalLanguage": "Orijinal Dil", + "components.Discover.CreateSlider.nooptions": "Sonuç bulunamadı.", + "components.Discover.DiscoverMovies.sortTmdbRatingDesc": "TMDB Puanı Azalan", + "components.Discover.CreateSlider.searchKeywords": "Anahtar kelimeleri ara…", + "components.Discover.FilterSlideover.tmdbuservotecount": "TMDB Kullanıcı Oy Sayısı", + "components.Discover.CreateSlider.addsuccess": "Yeni kaydırıcı oluşturuldu ve keşfet özelleştirme ayarları kaydedildi.", + "components.Discover.DiscoverSliderEdit.deletesuccess": "Kaydırıcı başarıyla silindi.", "components.Discover.DiscoverMovies.discovermovies": "Filmler", "components.Discover.MovieGenreList.moviegenres": "Film Türleri", - "components.Discover.DiscoverMovies.sortTitleAsc": "İsim (A-Z) (Artan)", - "components.Discover.FilterSlideover.voteCount": "{minValue} ile {maxValue} sayıları arasında oya sahip olanlar", - "components.Discover.CreateSlider.providetmdbgenreid": "TMBD'den Tür etiketi seç", - "components.Discover.DiscoverTv.sortTitleDesc": "İsim (Z-A) (Azalan)", - "components.Discover.recentrequests": "Yakın Zamandaki Talepler", + "components.Discover.DiscoverMovies.sortTitleAsc": "Başlık (A-Z) Artan", + "components.Discover.FilterSlideover.voteCount": "{minValue} ile {maxValue} arasındaki oy sayısı", + "components.Discover.CreateSlider.providetmdbgenreid": "Bir TMDB Tür Kimliği girin", + "components.Discover.DiscoverTv.sortTitleDesc": "Başlık (Z-A) Azalan", + "components.Discover.recentrequests": "Son Talepler", "components.Discover.DiscoverSliderEdit.remove": "Kaldır", - "components.Discover.FilterSlideover.to": "Tarihine", - "components.Discover.resetfailed": "Keşfet özelleştirme ayarlarını sıfırlarken bir sorun oluştu.", - "components.Discover.resetsuccess": "Keşfet ayarları başarıyla sıfırlandı.", - "components.Discover.resettodefault": "Varsayılana Dön", - "components.Discover.resetwarning": "Tüm Slider'ları varsayılana sıfırla. Bu aynı zamanda tüm özel Slider'ları da silecektir!", - "components.Discover.stopediting": "Vazgeç", + "components.Discover.FilterSlideover.to": "Bitiş", + "components.Discover.resetfailed": "Keşfet özelleştirme ayarları sıfırlanırken bir sorun oluştu.", + "components.Discover.resetsuccess": "Keşfet özelleştirme ayarları başarıyla sıfırlandı.", + "components.Discover.resettodefault": "Varsayılana Sıfırla", + "components.Discover.resetwarning": "Tüm kaydırıcıları varsayılana sıfırla. Bu işlem özel kaydırıcıları da silecektir!", + "components.Discover.stopediting": "Düzenlemeyi Durdur", "components.Discover.studios": "Stüdyolar", - "components.Discover.tmdbmoviegenre": "TMBD Film Türleri", - "components.Discover.tmdbmoviekeyword": "TMDB Film Anahtar Kelimeleri", - "components.Discover.tmdbmoviestreamingservices": "TMDB Dijital Platform Filmleri", - "components.Discover.tmdbnetwork": "TMBD İnternet Yayınları", - "components.Discover.tmdbsearch": "TMBD Arama", - "components.Discover.tmdbstudio": "TMBD Stüdyolar", - "components.Discover.tmdbtvkeyword": "TMBD Dizi Anahtar Kelimeleri", - "components.Discover.tmdbtvstreamingservices": "TMBD Dijital Platform Dizileri", - "components.Discover.trending": "Popüler", + "components.Discover.tmdbmoviegenre": "TMDB Film Türü", + "components.Discover.tmdbmoviekeyword": "TMDB Film Anahtar Kelimesi", + "components.Discover.tmdbmoviestreamingservices": "TMDB Film Yayın Hizmetleri", + "components.Discover.tmdbnetwork": "TMDB Kanalı", + "components.Discover.tmdbsearch": "TMDB Araması", + "components.Discover.tmdbstudio": "TMDB Stüdyosu", + "components.Discover.tmdbtvkeyword": "TMDB Dizi Anahtar Kelimesi", + "components.Discover.tmdbtvstreamingservices": "TMDB TV Yayın Hizmetleri", + "components.Discover.trending": "Trend", "components.Discover.tvgenres": "Dizi Türleri", - "components.Discover.upcoming": "Gösterime Girecek Filmler", - "components.Discover.upcomingmovies": "Gösterime Girecek Filmler", + "components.Discover.upcoming": "Yaklaşan Filmler", + "components.Discover.upcomingmovies": "Yaklaşan Filmler", "components.Discover.upcomingtv": "Yaklaşan Diziler", - "components.Discover.updatesuccess": "Keşfet ayarları güncellendi.", - "components.DownloadBlock.estimatedtime": "Tahmini Bitiş: {time}", + "components.Discover.updatesuccess": "Keşfet özelleştirme ayarları güncellendi.", + "components.DownloadBlock.estimatedtime": "Tahminen {time}", "components.DownloadBlock.formattedTitle": "{title}: Sezon {seasonNumber} Bölüm {episodeNumber}", "components.IssueDetails.IssueComment.delete": "Yorumu Sil", "components.IssueDetails.IssueComment.edit": "Yorumu Düzenle", - "components.IssueDetails.IssueComment.postedby": "{relativeTime} Tarihinde, {username} tarafından gönderildi", - "components.IssueDetails.IssueComment.postedbyedited": "{relativeTime} Tarihinde, {username} tarafından gönderildi (Düzenlenmiş)", + "components.IssueDetails.IssueComment.postedby": "{relativeTime} tarihinde {username} tarafından gönderildi", + "components.IssueDetails.IssueComment.postedbyedited": "{relativeTime} tarihinde {username} tarafından gönderildi (Düzenlendi)", "components.IssueDetails.IssueComment.validationComment": "Bir mesaj girmelisiniz", - "components.IssueDetails.IssueDescription.deleteissue": "Sorun Bildirimini Sil", + "components.IssueDetails.IssueDescription.deleteissue": "Sorunu Sil", "components.IssueDetails.IssueDescription.description": "Açıklama", "components.IssueDetails.IssueDescription.edit": "Açıklamayı Düzenle", "components.IssueDetails.allepisodes": "Tüm Bölümler", "components.IssueDetails.allseasons": "Tüm Sezonlar", - "components.IssueDetails.closeissue": "Sorun Bildirimini Kapat", + "components.IssueDetails.closeissue": "Sorunu Kapat", "components.IssueDetails.commentplaceholder": "Yorum ekle…", "components.IssueDetails.comments": "Yorumlar", - "components.IssueDetails.deleteissue": "Sorun Bildirimini Sil", + "components.IssueDetails.deleteissue": "Sorunu Sil", "components.IssueDetails.episode": "Bölüm {episodeNumber}", "components.IssueDetails.issuepagetitle": "Sorun", "components.IssueDetails.issuetype": "Tür", - "components.IssueDetails.lastupdated": "En Son Güncelleme Zamanı", - "components.IssueDetails.leavecomment": "Yorum Ekle", + "components.IssueDetails.lastupdated": "Son Güncelleme", + "components.IssueDetails.leavecomment": "Yorum", "components.IssueDetails.nocomments": "Yorum yok.", - "components.IssueDetails.openin4karr": "{arr} ile 4K Aç", - "components.IssueDetails.openinarr": "{arr} ile Aç", - "components.IssueDetails.play4konplex": "{mediaServerName} ile 4K Oynat", - "components.IssueDetails.playonplex": "{mediaServerName} ile Oynat", - "components.IssueDetails.problemepisode": "Sorundan Etkilenen Bölümler", - "components.IssueDetails.problemseason": "Sorundan Etkilenen Sezonlar", - "components.IssueDetails.reopenissue": "Sorun Bildirisini Yeniden Aç", + "components.IssueDetails.openin4karr": "4K {arr}'da Aç", + "components.IssueDetails.openinarr": "{arr}'da Aç", + "components.IssueDetails.play4konplex": "{mediaServerName}'da 4K Oynat", + "components.IssueDetails.playonplex": "{mediaServerName}'da Oynat", + "components.IssueDetails.problemepisode": "Etkilenen Bölüm", + "components.IssueDetails.problemseason": "Etkilenen Sezon", + "components.IssueDetails.reopenissue": "Sorunu Yeniden Aç", "components.IssueDetails.season": "Sezon {seasonNumber}", - "components.IssueDetails.toasteditdescriptionfailed": "Sorun açıklamasını düzenlerken beklenmedik bir hata oluştu.", + "components.IssueDetails.toasteditdescriptionfailed": "Sorun açıklaması düzenlenirken bir sorun oluştu.", "components.IssueDetails.toasteditdescriptionsuccess": "Sorun açıklaması başarıyla düzenlendi!", - "components.IssueDetails.toastissuedeletefailed": "Sorun açıklaması silinirken beklenmedik bir hata oluştu.", - "components.IssueDetails.toaststatusupdated": "Sorun başarıyla güncellendi!", + "components.IssueDetails.toastissuedeletefailed": "Sorun silinirken bir hata oluştu.", + "components.IssueDetails.toaststatusupdated": "Sorun durumu başarıyla güncellendi!", "components.IssueDetails.unknownissuetype": "Bilinmiyor", - "components.IssueList.IssueItem.episodes": "{episodeCount} Bölüm", + "components.IssueList.IssueItem.episodes": "{episodeCount, plural, one {Bölüm} other {Bölümler}}", "components.IssueList.IssueItem.issuestatus": "Durum", "components.IssueList.IssueItem.issuetype": "Tür", - "components.IssueList.IssueItem.opened": "Açıldı", - "components.IssueList.IssueItem.openeduserdate": "{user } tarafından, {date}", - "components.IssueList.IssueItem.problemepisode": "Sorundan Etkilenen Bölümler", - "components.IssueList.IssueItem.seasons": "{seasonCount} Sezon", + "components.IssueList.IssueItem.opened": "Açılma", + "components.IssueList.IssueItem.openeduserdate": "{date} tarihinde {user} tarafından", + "components.IssueList.IssueItem.problemepisode": "Etkilenen Bölüm", + "components.IssueList.IssueItem.seasons": "{seasonCount, plural, one {Sezon} other {Sezonlar}}", "components.IssueList.IssueItem.unknownissuetype": "Bilinmiyor", - "components.IssueList.IssueItem.viewissue": "Sorunu Gör", + "components.IssueList.IssueItem.viewissue": "Sorunu Görüntüle", "components.IssueList.issues": "Sorunlar", "components.IssueList.showallissues": "Tüm Sorunları Göster", - "components.IssueList.sortAdded": "En Son Eklenen", - "components.IssueList.sortModified": "En Son Düzenlenen", + "components.IssueList.sortAdded": "En Son", + "components.IssueList.sortModified": "Son Değiştirilen", "components.IssueModal.CreateIssueModal.allseasons": "Tüm Sezonlar", "components.IssueModal.CreateIssueModal.episode": "Bölüm {episodeNumber}", "components.IssueModal.CreateIssueModal.extras": "Ekstralar", - "components.IssueModal.CreateIssueModal.problemepisode": "Sorundan Etkilenen Bölümler", - "components.IssueModal.CreateIssueModal.problemseason": "Sorundan Etkilenen Sezonlar", + "components.IssueModal.CreateIssueModal.problemepisode": "Etkilenen Bölüm", + "components.IssueModal.CreateIssueModal.problemseason": "Etkilenen Sezon", "components.IssueModal.CreateIssueModal.season": "Sezon {seasonNumber}", - "components.IssueModal.CreateIssueModal.submitissue": "Sorunu Bildir", - "components.IssueModal.CreateIssueModal.toastSuccessCreate": "{title} için oluşturduğunuz hata bildirisi gönderilmiştir!", - "components.IssueModal.CreateIssueModal.toastviewissue": "Sorunu Gör", - "components.IssueModal.CreateIssueModal.validationMessageRequired": "Lütfen açıklamayı doldurunuz", + "components.IssueModal.CreateIssueModal.submitissue": "Sorunu Gönder", + "components.IssueModal.CreateIssueModal.toastSuccessCreate": "{title} için sorun raporu başarıyla gönderildi!", + "components.IssueModal.CreateIssueModal.toastviewissue": "Sorunu Görüntüle", + "components.IssueModal.CreateIssueModal.validationMessageRequired": "Bir açıklama girmelisiniz", "components.IssueModal.CreateIssueModal.whatswrong": "Sorun nedir?", "components.IssueModal.issueAudio": "Ses", "components.IssueModal.issueOther": "Diğer", @@ -198,99 +198,99 @@ "components.Layout.UserDropdown.requests": "Talepler", "components.Layout.UserDropdown.settings": "Ayarlar", "components.Layout.UserDropdown.signout": "Çıkış Yap", - "components.Layout.UserWarnings.emailRequired": "Bir e-mail adresi gereklidir.", - "components.Layout.UserWarnings.passwordRequired": "Bir şifre girmeniz gerekmektedir.", - "components.Layout.VersionStatus.outofdate": "Eski Sürüm", - "components.Layout.VersionStatus.streamdevelop": "Seerr Geliştirme Sürümü", - "components.Layout.VersionStatus.streamstable": "Seerr Stabil Sürüm", - "components.Login.credentialerror": "Kullanıcı adı ya da şifre yanlış.", - "components.Login.description": "{applicationName} uygulamasına ilk defa giriş yaptığınız için geçerli bir e-mail adresi eklemeniz gerekmektedir.", - "components.Login.email": "E-mail Adresi", - "components.Login.forgotpassword": "Şifrenizi mi unuttunuz?", + "components.Layout.UserWarnings.emailRequired": "Bir e-posta adresi gereklidir.", + "components.Layout.UserWarnings.passwordRequired": "Bir şifre gereklidir.", + "components.Layout.VersionStatus.outofdate": "Güncel Değil", + "components.Layout.VersionStatus.streamdevelop": "Seerr Geliştirme", + "components.Layout.VersionStatus.streamstable": "Seerr Kararlı", + "components.Login.credentialerror": "Kullanıcı adı veya şifre hatalı.", + "components.Login.description": "Profilinizi tamamlamak için geçerli bir e-posta adresi ekleyin. Bu adres bildirimler ve yerel giriş için kullanılacaktır.", + "components.Login.email": "E-posta Adresi", + "components.Login.forgotpassword": "Şifremi Unuttum?", "components.Login.initialsignin": "Bağlan", "components.Login.initialsigningin": "Bağlanıyor…", "components.Login.password": "Şifre", "components.Login.save": "Ekle", "components.Login.saving": "Ekleniyor…", - "components.Login.signin": "Oturum Aç", - "components.Login.signingin": "Oturum Açılıyor…", - "components.Login.signinwithjellyfin": "{mediaServerName} hesabını kullan", - "components.Login.signinwithoverseerr": "{applicationTitle} hesabını kullan", - "components.Login.signinwithplex": "Plex hesabını kullan", - "components.Login.title": "E-mail Ekle", + "components.Login.signin": "Giriş Yap", + "components.Login.signingin": "Giriş Yapılıyor…", + "components.Login.signinwithjellyfin": "{mediaServerName} hesabınızı kullanın", + "components.Login.signinwithoverseerr": "{applicationTitle} hesabınızı kullanın", + "components.Login.signinwithplex": "Plex hesabınızı kullanın", + "components.Login.title": "E-posta Ekle", "components.Login.username": "Kullanıcı Adı", - "components.Login.validationEmailFormat": "Geçersiz e-mail", - "components.Login.validationemailformat": "Geçerli bir e-mail adresi gereklidir", - "components.Login.validationemailrequired": "Geçerli bir e-mail adresi sağlamalısınız", - "components.Login.validationhostformat": "Geçerli bir URl gereklidir", - "components.Login.validationhostrequired": "{mediaServerName} URL'si gereklidir", - "components.Login.validationusernamerequired": "Kullanıcı Adı girmeniz gereklidir", + "components.Login.validationEmailFormat": "Geçersiz e-posta", + "components.Login.validationemailformat": "Geçerli bir e-posta gereklidir", + "components.Login.validationemailrequired": "Geçerli bir e-posta adresi girmelisiniz", + "components.Login.validationhostformat": "Geçerli bir URL gereklidir", + "components.Login.validationhostrequired": "{mediaServerName} URL gereklidir", + "components.Login.validationusernamerequired": "Kullanıcı adı gereklidir", "components.ManageSlideOver.alltime": "Tüm Zamanlar", - "components.ManageSlideOver.downloadstatus": "İndirilenler", + "components.ManageSlideOver.downloadstatus": "İndirmeler", "components.ManageSlideOver.manageModalAdvanced": "Gelişmiş", - "components.ManageSlideOver.manageModalClearMedia": "Veriyi Temizle", - "components.Discover.tmdbtvgenre": "TMBD Dizi Türleri", - "components.Discover.updatefailed": "Keşfet ayarları güncellenirken beklenmedik bir hata oluştu.", - "components.IssueDetails.IssueComment.areyousuredelete": "Bu yorumu silmek istediğinize emin misiniz?", - "components.IssueDetails.closeissueandcomment": "Yorum ekleyerek Kapat", - "components.IssueDetails.deleteissueconfirm": "Bu hata bildirisini silmek istediğinize emin misiniz?", - "components.IssueDetails.openedby": "#{issueId} {relativeTime} tarihinde {username} tarafından açıldı", - "components.IssueDetails.reopenissueandcomment": "Bir Yorumla beraber Yeniden Aç", - "components.IssueDetails.toastissuedeleted": "Sorun Bildirisi başarıyla silindi!", - "components.IssueDetails.toaststatusupdatefailed": "Sorun açıklaması güncellenirken beklenmedik bir hata oluştu.", + "components.ManageSlideOver.manageModalClearMedia": "Verileri Temizle", + "components.Discover.tmdbtvgenre": "TMDB Dizi Türü", + "components.Discover.updatefailed": "Keşfet özelleştirme ayarları güncellenirken bir sorun oluştu.", + "components.IssueDetails.IssueComment.areyousuredelete": "Bu yorumu silmek istediğinizden emin misiniz?", + "components.IssueDetails.closeissueandcomment": "Yorum ile Kapat", + "components.IssueDetails.deleteissueconfirm": "Bu sorunu silmek istediğinizden emin misiniz?", + "components.IssueDetails.openedby": "#{issueId}, {relativeTime} tarihinde {username} tarafından açıldı", + "components.IssueDetails.reopenissueandcomment": "Yorum ile Yeniden Aç", + "components.IssueDetails.toastissuedeleted": "Sorun başarıyla silindi!", + "components.IssueDetails.toaststatusupdatefailed": "Sorun durumu güncellenirken bir hata oluştu.", "components.IssueModal.CreateIssueModal.allepisodes": "Tüm Bölümler", - "components.IssueModal.CreateIssueModal.providedetail": "Lütfen karşılaştığınız hatayı detaylı bir biçimde açıklayınız.", + "components.IssueModal.CreateIssueModal.providedetail": "Lütfen yaşadığınız sorunu ayrıntılı olarak açıklayın.", "components.IssueModal.CreateIssueModal.reportissue": "Sorun Bildir", "components.Layout.LanguagePicker.displaylanguage": "Görüntüleme Dili", - "components.Login.validationpasswordrequired": "Bir şifre girmeniz gereklidir", - "components.IssueModal.CreateIssueModal.toastFailedCreate": "Sorunu bildirirken beklenmedik bir hata oluştu.", - "components.Layout.UserWarnings.emailInvalid": "E-mail adresi geçersiz.", - "components.Login.signinheader": "Devam etmek için oturum aç", - "components.Login.validationEmailRequired": "Bir e-mail adresi girmelisiniz", - "components.Layout.VersionStatus.commitsbehind": "{commitsBehind} {commitsBehind, plural, one {commit} other {commits}} behind", - "components.Login.emailtooltip": "E-mail adresi {mediaServerName} uygulamasıyla ilişik olmak zorunda değildir.", - "components.Login.loginerror": "Oturum açarken bir hata oluştu.", - "components.ManageSlideOver.manageModalClearMediaWarning": "* Bu işlem geri döndürülemez bir biçimde {mediaType} ile alakalı her türlü talepte dahil olmak üzere tüm verileri silecektir. Eğer ki bu öğe {mediaServerName} kütüphanenizde bulunuyorsa, tüm medya bilgileri bir sonra ki taramada yeniden oluşturulucaktır.", - "components.ManageSlideOver.manageModalIssues": "Sorun Bildir", - "components.ManageSlideOver.manageModalMedia": "İçerik", - "components.ManageSlideOver.manageModalMedia4k": "4K İçerik", - "components.ManageSlideOver.manageModalNoRequests": "Hiç talep yok.", - "components.ManageSlideOver.manageModalRemoveMediaWarning": "* Bu işlem geri döndürülemez bir biçimde {arr}'dan {mediaType} öğesini ve dosyalarını kaldıracaktır.", + "components.Login.validationpasswordrequired": "Bir şifre girmelisiniz", + "components.IssueModal.CreateIssueModal.toastFailedCreate": "Sorun gönderilirken bir hata oluştu.", + "components.Layout.UserWarnings.emailInvalid": "E-posta adresi geçersiz.", + "components.Login.signinheader": "Devam etmek için giriş yapın", + "components.Login.validationEmailRequired": "Bir e-posta adresi girmelisiniz", + "components.Layout.VersionStatus.commitsbehind": "{commitsBehind} {commitsBehind, plural, one {commit} other {commit}} geride", + "components.Login.emailtooltip": "Adresin {mediaServerName} örneğinizle ilişkilendirilmesi gerekmez.", + "components.Login.loginerror": "Giriş yapılmaya çalışılırken bir sorun oluştu.", + "components.ManageSlideOver.manageModalClearMediaWarning": "* Bu işlem, tüm talepler dahil bu {mediaType} için tüm verileri geri alınamaz şekilde kaldıracaktır. Bu öğe {mediaServerName} kütüphanenizde mevcutsa, medya bilgileri bir sonraki taramada yeniden oluşturulacaktır.", + "components.ManageSlideOver.manageModalIssues": "Açık Sorunlar", + "components.ManageSlideOver.manageModalMedia": "Medya", + "components.ManageSlideOver.manageModalMedia4k": "4K Medya", + "components.ManageSlideOver.manageModalNoRequests": "Talep yok.", + "components.ManageSlideOver.manageModalRemoveMediaWarning": "* Bu işlem, tüm dosyalar dahil bu {mediaType}'ı {arr}'dan geri alınamaz şekilde kaldıracaktır.", "components.ManageSlideOver.manageModalRequests": "Talepler", - "components.ManageSlideOver.manageModalTitle": "{mediaType} içeriğini yönet", - "components.ManageSlideOver.markallseasons4kavailable": "Tüm Sezonları 4K Oynatılabilir Olarak İşaretle", - "components.ManageSlideOver.mark4kavailable": "4K Oynatılabilir Olarak İşaretle", - "components.ManageSlideOver.markallseasonsavailable": "Tüm Sezonları Oynatılabilir Olarak İşaretle", - "components.ManageSlideOver.markavailable": "Oynatılabilir Olarak İşaretle", - "components.ManageSlideOver.movie": "Film", + "components.ManageSlideOver.manageModalTitle": "{mediaType} Yönet", + "components.ManageSlideOver.markallseasons4kavailable": "Tüm Sezonları 4K Kullanılabilir İşaretle", + "components.ManageSlideOver.mark4kavailable": "4K Olarak Kullanılabilir İşaretle", + "components.ManageSlideOver.markallseasonsavailable": "Tüm Sezonları Kullanılabilir İşaretle", + "components.ManageSlideOver.markavailable": "Kullanılabilir Olarak İşaretle", + "components.ManageSlideOver.movie": "film", "components.ManageSlideOver.openarr": "{arr}'da Aç", - "components.ManageSlideOver.openarr4k": "{arr}'da 4K Olarak Aç", - "components.ManageSlideOver.opentautulli": "Tautuli'de Aç", - "components.ManageSlideOver.pastdays": "{days, number} Gün Geçti", - "components.ManageSlideOver.playedby": "Tarafından Oynatıldı", - "components.ManageSlideOver.plays": "{playCount, number} oynatma", + "components.ManageSlideOver.openarr4k": "4K {arr}'da Aç", + "components.ManageSlideOver.opentautulli": "Tautulli'de Aç", + "components.ManageSlideOver.pastdays": "Son {days, number} Gün", + "components.ManageSlideOver.playedby": "Oynatılan Kullanıcı", + "components.ManageSlideOver.plays": "{playCount, number} {playCount, plural, one {oynatma} other {oynatma}}", "components.ManageSlideOver.removearr": "{arr}'dan Kaldır", "components.ManageSlideOver.tvshow": "dizi", - "components.MediaSlider.ShowMoreCard.seemore": "Daha Fazla", - "components.MovieDetails.MovieCast.fullcast": "Tüm Oyuncular", - "components.MovieDetails.MovieCrew.fullcrew": "Tüm Ekip", + "components.MediaSlider.ShowMoreCard.seemore": "Daha Fazla Gör", + "components.MovieDetails.MovieCast.fullcast": "Tam Kadro", + "components.MovieDetails.MovieCrew.fullcrew": "Tam Ekip", "components.MovieDetails.budget": "Bütçe", "components.MovieDetails.cast": "Oyuncular", - "components.MovieDetails.digitalrelease": "Dijital Sürüm", + "components.MovieDetails.digitalrelease": "Dijital Çıkış", "components.MovieDetails.downloadstatus": "İndirme Durumu", "components.MovieDetails.managemovie": "Filmi Yönet", - "components.MovieDetails.mark4kavailable": "4K Oynatılabilir Olarak İşaretle", - "components.MovieDetails.markavailable": "Oynatılabilir Olarak İşaretle", + "components.MovieDetails.mark4kavailable": "4K Olarak Kullanılabilir İşaretle", + "components.MovieDetails.markavailable": "Kullanılabilir Olarak İşaretle", "components.MovieDetails.openradarr": "Filmi Radarr'da Aç", "components.MovieDetails.openradarr4k": "Filmi 4K Radarr'da Aç", "components.MovieDetails.originallanguage": "Orijinal Dil", "components.MovieDetails.originaltitle": "Orijinal Başlık", - "components.MovieDetails.overview": "Özet", - "components.MovieDetails.physicalrelease": "Fiziksel Sürüm", - "components.MovieDetails.play": "İçeriği {mediaServerName} üzerinde oynat", - "components.MovieDetails.play4k": "İçeriği {mediaServerName} üzerinde 4K oynat", - "components.MovieDetails.recommendations": "Önerilenler", - "components.MovieDetails.releasedate": "{releaseCount, plural, one {Yayın Tarihi} other {Yayın Tarihleri}}", + "components.MovieDetails.overview": "Genel Bakış", + "components.MovieDetails.physicalrelease": "Fiziksel Çıkış", + "components.MovieDetails.play": "{mediaServerName}'da Oynat", + "components.MovieDetails.play4k": "{mediaServerName}'da 4K Oynat", + "components.MovieDetails.recommendations": "Öneriler", + "components.MovieDetails.releasedate": "{releaseCount, plural, one {Çıkış Tarihi} other {Çıkış Tarihleri}}", "components.MovieDetails.reportissue": "Sorun Bildir", "components.MovieDetails.revenue": "Hasılat", "components.MovieDetails.rtaudiencescore": "Rotten Tomatoes İzleyici Puanı", @@ -298,510 +298,510 @@ "components.MovieDetails.runtime": "{minutes} dakika", "components.MovieDetails.showmore": "Daha Fazla Göster", "components.MovieDetails.showless": "Daha Az Göster", - "components.MovieDetails.similar": "Benzer İçerikler", + "components.MovieDetails.similar": "Benzer Yapımlar", "components.MovieDetails.studio": "{studioCount, plural, one {Stüdyo} other {Stüdyolar}}", - "components.MovieDetails.theatricalrelease": "Sinemaya Çıkışı", - "components.MovieDetails.tmdbuserscore": "TMBD Kullanıcı Skoru", - "components.MovieDetails.viewfullcrew": "Tüm Ekibi Gör", + "components.MovieDetails.theatricalrelease": "Sinema Gösterimi", + "components.MovieDetails.tmdbuserscore": "TMDB Kullanıcı Puanı", + "components.MovieDetails.viewfullcrew": "Tam Ekibi Görüntüle", "components.MovieDetails.watchtrailer": "Fragmanı İzle", - "components.NotificationTypeSelector.adminissuereopenedDescription": "Sorun bildirileri kullanıcılar tarafından tekrardan açıldığında bildirim gönder.", - "components.NotificationTypeSelector.adminissueresolvedDescription": "Sorun bildirileri kullanıcılar tarafından çözüldüğünde bildirim gönder.", - "components.NotificationTypeSelector.issuecomment": "Sorun Bildirim Yorumları", + "components.NotificationTypeSelector.adminissuereopenedDescription": "Diğer kullanıcılar sorunları yeniden açtığında bildirim alın.", + "components.NotificationTypeSelector.adminissueresolvedDescription": "Diğer kullanıcılar sorunları çözdüğünde bildirim alın.", + "components.NotificationTypeSelector.issuecomment": "Sorun Yorumu", "components.NotificationTypeSelector.issuecreatedDescription": "Sorunlar bildirildiğinde bildirim gönder.", "components.NotificationTypeSelector.issuereopened": "Sorun Yeniden Açıldı", "components.NotificationTypeSelector.issueresolved": "Sorun Çözüldü", "components.NotificationTypeSelector.issueresolvedDescription": "Sorunlar çözüldüğünde bildirim gönder.", - "components.NotificationTypeSelector.mediaAutoApproved": "Otomatik Kabul Edilen Talepler", - "components.NotificationTypeSelector.mediaAutoApprovedDescription": "Kullanıcıların içerik talepleri otomatik olarak kabul edilenler için bildirim gönder.", - "components.NotificationTypeSelector.mediaapproved": "Kabul Edilen Talepler", - "components.NotificationTypeSelector.mediaautorequested": "Talep Otomatik Olarak Gönderildi", - "components.NotificationTypeSelector.mediaautorequestedDescription": "Kullanıcıların otomatik içerik talepleri eğer ki sizin İzleme Listenizden bir içerik içeriyor ise bildirim gönder.", - "components.NotificationTypeSelector.mediaavailable": "İzlenebilir İçerik", - "components.NotificationTypeSelector.mediaavailableDescription": "İçerik izlenebilir olduğunda bildirim gönder.", - "components.NotificationTypeSelector.mediadeclined": "Reddedilen Talepler", - "components.NotificationTypeSelector.mediafailed": "İşlemdeyken İptal Olan Talepler", - "components.NotificationTypeSelector.mediafailedDescription": "İçerik talepleri Sonarr'a ya da Radarr'a eklenirken hata oluşursa bildirim gönder.", - "components.NotificationTypeSelector.mediarequested": "Doğrulama Bekleyen Talepler", + "components.NotificationTypeSelector.mediaAutoApproved": "Talep Otomatik Onaylandı", + "components.NotificationTypeSelector.mediaAutoApprovedDescription": "Kullanıcılar otomatik olarak onaylanan yeni medya talepleri gönderdiğinde bildirim gönder.", + "components.NotificationTypeSelector.mediaapproved": "Talep Onaylandı", + "components.NotificationTypeSelector.mediaautorequested": "Talep Otomatik Gönderildi", + "components.NotificationTypeSelector.mediaautorequestedDescription": "İzleme Listenizdeki öğeler için yeni medya talepleri otomatik olarak gönderildiğinde bildirim alın.", + "components.NotificationTypeSelector.mediaavailable": "Talep Kullanılabilir", + "components.NotificationTypeSelector.mediaavailableDescription": "Medya talepleri kullanılabilir olduğunda bildirim gönder.", + "components.NotificationTypeSelector.mediadeclined": "Talep Reddedildi", + "components.NotificationTypeSelector.mediafailed": "Talep İşleme Başarısız", + "components.NotificationTypeSelector.mediafailedDescription": "Medya talepleri Radarr veya Sonarr'a eklenemediğinde bildirim gönder.", + "components.NotificationTypeSelector.mediarequested": "Talep Onay Bekliyor", "components.NotificationTypeSelector.notificationTypes": "Bildirim Türleri", - "components.NotificationTypeSelector.userissuecommentDescription": "Gönderdiğiniz hata bildirimine yorum geldiğinde bildirim gönder.", - "components.NotificationTypeSelector.userissuecreatedDescription": "Diğer kullanıcılar hata bildirdiklerinde bildirim gönder.", - "components.NotificationTypeSelector.userissuereopenedDescription": "Gönderdiğiniz hata bildirimi yeniden açıldığında bildirim gönder.", - "components.NotificationTypeSelector.userissueresolvedDescription": "Gönderdiğiniz hata bildirimi çözüldüğünde bildirim gönder.", - "components.NotificationTypeSelector.usermediaapprovedDescription": "Gönderdiğiniz içerik talepleri kabul edildiğinde bildirim gönder.", - "components.NotificationTypeSelector.usermediaavailableDescription": "Gönderdiğiniz içerik talepleri izlenebilir olduğunda bildirim gönder.", - "components.NotificationTypeSelector.usermediadeclinedDescription": "Gönderdiğiniz içerik talepleri reddedildiğinde bildirim gönder.", - "components.NotificationTypeSelector.usermediarequestedDescription": "Diğer kullanıcılar doğrulama gerektiren içerik talepleri gönderdiğinde bildirim gönder.", + "components.NotificationTypeSelector.userissuecommentDescription": "Bildirdiğiniz sorunlara yeni yorumlar geldiğinde bildirim alın.", + "components.NotificationTypeSelector.userissuecreatedDescription": "Diğer kullanıcılar sorun bildirdiğinde bildirim alın.", + "components.NotificationTypeSelector.userissuereopenedDescription": "Bildirdiğiniz sorunlar yeniden açıldığında bildirim alın.", + "components.NotificationTypeSelector.userissueresolvedDescription": "Bildirdiğiniz sorunlar çözüldüğünde bildirim alın.", + "components.NotificationTypeSelector.usermediaapprovedDescription": "Medya talepleriniz onaylandığında bildirim alın.", + "components.NotificationTypeSelector.usermediaavailableDescription": "Medya talepleriniz kullanılabilir olduğunda bildirim alın.", + "components.NotificationTypeSelector.usermediadeclinedDescription": "Medya talepleriniz reddedildiğinde bildirim alın.", + "components.NotificationTypeSelector.usermediarequestedDescription": "Diğer kullanıcılar onay gerektiren yeni medya talepleri gönderdiğinde bildirim alın.", "components.PermissionEdit.admin": "Yönetici", - "components.PermissionEdit.adminDescription": "Tam yönetici erişimi. Diğer tüm izin denetlemelerini görmezden gelir.", + "components.PermissionEdit.adminDescription": "Tam yönetici erişimi. Diğer tüm izin kontrollerini atlar.", "components.PermissionEdit.advancedrequest": "Gelişmiş Talepler", - "components.PermissionEdit.advancedrequestDescription": "Gelişmiş talep ayarlarını düzenleyebilme izni ver.", - "components.PermissionEdit.autoapprove": "Otomatik Onay", - "components.PermissionEdit.autoapprove4kMovies": "4K Filmler İçin Otomatik Onay", - "components.PermissionEdit.autoapprove4kMoviesDescription": "4K Film taleplerinin otomatik onaylanmasına izin ver.", - "components.PermissionEdit.autoapprove4kSeries": "4K Diziler İçin Otomatik Onay", - "components.PermissionEdit.autoapprove4kSeriesDescription": "4K Dizi taleplerinin otomatik onaylanmasına izin ver.", - "components.PermissionEdit.autoapproveDescription": "4K olmayan içerik taleplerinin otomatik onaylanması iznini ver.", - "components.PermissionEdit.autoapproveMovies": "Filmler İçin Otomatik Onay", - "components.PermissionEdit.autoapproveSeries": "Diziler İçin Otomatik Onay", - "components.PermissionEdit.autoapproveSeriesDescription": "4K olmayan dizi taleplerinin otomatik onaylanması iznini ver.", - "components.PermissionEdit.autorequest": "Otomatize Talep", - "components.PermissionEdit.autorequestMovies": "Filmleri Otomatize Talep Ett", - "components.PermissionEdit.autorequestMoviesDescription": "Kullanıcının Plex İzleme listesinde olan ve 4K olmayan filmler için otomatik olarak talep etme iznini ver.", - "components.PermissionEdit.autorequestSeries": "Dizileri Otomatize Talep Ett", - "components.PermissionEdit.autorequestSeriesDescription": "Kullanıcının Plex İzleme listesinde olan ve 4K olmayan diziler için otomatik olarak talep etme iznini ver.", - "components.PermissionEdit.createissues": "Sorun Bildirme", - "components.PermissionEdit.manageissues": "Sorunları Yönetme", - "components.PermissionEdit.manageissuesDescription": "İçeriklerde ki hataları yönetme iznini ver.", + "components.PermissionEdit.advancedrequestDescription": "Gelişmiş medya talebi seçeneklerini değiştirme izni ver.", + "components.PermissionEdit.autoapprove": "Otomatik Onayla", + "components.PermissionEdit.autoapprove4kMovies": "4K Filmleri Otomatik Onayla", + "components.PermissionEdit.autoapprove4kMoviesDescription": "4K film talepleri için otomatik onay ver.", + "components.PermissionEdit.autoapprove4kSeries": "4K Dizileri Otomatik Onayla", + "components.PermissionEdit.autoapprove4kSeriesDescription": "4K dizi talepleri için otomatik onay ver.", + "components.PermissionEdit.autoapproveDescription": "4K olmayan tüm medya talepleri için otomatik onay ver.", + "components.PermissionEdit.autoapproveMovies": "Filmleri Otomatik Onayla", + "components.PermissionEdit.autoapproveSeries": "Dizileri Otomatik Onayla", + "components.PermissionEdit.autoapproveSeriesDescription": "4K olmayan dizi talepleri için otomatik onay ver.", + "components.PermissionEdit.autorequest": "Otomatik Talep", + "components.PermissionEdit.autorequestMovies": "Filmleri Otomatik Talep Et", + "components.PermissionEdit.autorequestMoviesDescription": "Plex İzleme Listesi aracılığıyla 4K olmayan filmler için otomatik talep gönderme izni ver.", + "components.PermissionEdit.autorequestSeries": "Dizileri Otomatik Talep Et", + "components.PermissionEdit.autorequestSeriesDescription": "Plex İzleme Listesi aracılığıyla 4K olmayan diziler için otomatik talep gönderme izni ver.", + "components.PermissionEdit.createissues": "Sorun Bildir", + "components.PermissionEdit.manageissues": "Sorunları Yönet", + "components.PermissionEdit.manageissuesDescription": "Medya sorunlarını yönetme izni ver.", "components.PermissionEdit.managerequests": "Talepleri Yönet", - "components.PermissionEdit.request": "Talep", - "components.PermissionEdit.request4k": "4K Talep Etme", - "components.PermissionEdit.request4kDescription": "4K içerik talep edilmesine izin ver.", - "components.PermissionEdit.request4kMovies": "4K Filmleri Talep Etme", - "components.PermissionEdit.request4kMoviesDescription": "4K film talep edilmesine izin ver.", - "components.PermissionEdit.request4kTv": "4K Dizi Talep Etme", - "components.PermissionEdit.request4kTvDescription": "4K dizi talep edilmesine izin ver.", - "components.PermissionEdit.requestDescription": "4K olmayan içeriklerin talep edilmesine izin ver.", - "components.PermissionEdit.requestMovies": "Film Talep Etme", - "components.PermissionEdit.requestMoviesDescription": "4K olmayan filmlerin talep edilmesine izin ver.", - "components.PermissionEdit.requestTv": "Dizi Talep Etme", - "components.PermissionEdit.requestTvDescription": "4K olmayan dizilerin talep edilmesine izin ver.", - "components.PermissionEdit.users": "Kullanıcıları Yönetme", - "components.PermissionEdit.viewissues": "Sorunları Görme", - "components.PermissionEdit.viewissuesDescription": "İçeriklerle alakalı dieğr kullanıcıların raporladığı hataları görme iznini ver.", - "components.PermissionEdit.viewrequests": "Talepleri Görme", - "components.PermissionEdit.viewrequestsDescription": "Diğer kullanıcılar tarafından istenen içerikleri görme iznini ver.", - "components.PermissionEdit.viewwatchlists": "{mediaServerName} İzleme Listelerini Görme", - "components.PersonDetails.alsoknownas": "Nam-ı Diğer: {names}", - "components.PersonDetails.appearsin": "Yer Aldığı İçerikler", - "components.PersonDetails.ascharacter": "{character} rolüyle", - "components.PersonDetails.birthdate": "Doğumu {birthdate}", - "components.PersonDetails.crewmember": "Bilinen İşleri", + "components.PermissionEdit.request": "Talep Et", + "components.PermissionEdit.request4k": "4K Talep Et", + "components.PermissionEdit.request4kDescription": "4K medya için talep gönderme izni ver.", + "components.PermissionEdit.request4kMovies": "4K Film Talep Et", + "components.PermissionEdit.request4kMoviesDescription": "4K film için talep gönderme izni ver.", + "components.PermissionEdit.request4kTv": "4K Dizi Talep Et", + "components.PermissionEdit.request4kTvDescription": "4K dizi için talep gönderme izni ver.", + "components.PermissionEdit.requestDescription": "4K olmayan medya için talep gönderme izni ver.", + "components.PermissionEdit.requestMovies": "Film Talep Et", + "components.PermissionEdit.requestMoviesDescription": "4K olmayan film için talep gönderme izni ver.", + "components.PermissionEdit.requestTv": "Dizi Talep Et", + "components.PermissionEdit.requestTvDescription": "4K olmayan dizi için talep gönderme izni ver.", + "components.PermissionEdit.users": "Kullanıcıları Yönet", + "components.PermissionEdit.viewissues": "Sorunları Görüntüle", + "components.PermissionEdit.viewissuesDescription": "Diğer kullanıcıların bildirdiği medya sorunlarını görüntüleme izni ver.", + "components.PermissionEdit.viewrequests": "Talepleri Görüntüle", + "components.PermissionEdit.viewrequestsDescription": "Diğer kullanıcıların gönderdiği medya taleplerini görüntüleme izni ver.", + "components.PermissionEdit.viewwatchlists": "{mediaServerName} İzleme Listelerini Görüntüle", + "components.PersonDetails.alsoknownas": "Diğer Adları: {names}", + "components.PersonDetails.appearsin": "Görünümler", + "components.PersonDetails.ascharacter": "{character} olarak", + "components.PersonDetails.birthdate": "{birthdate} doğumlu", + "components.PersonDetails.crewmember": "Ekip", "components.PersonDetails.lifespan": "{birthdate} – {deathdate}", - "components.QuotaSelector.days": "{count} gün", - "components.QuotaSelector.movies": "{count} film", - "components.QuotaSelector.seasons": "{count} sezon", - "components.QuotaSelector.tvRequests": "{quotaLimit} {sezon} per {quotaDays} {gün}", + "components.QuotaSelector.days": "{count, plural, one {gün} other {gün}}", + "components.QuotaSelector.movies": "{count, plural, one {film} other {film}}", + "components.QuotaSelector.seasons": "{count, plural, one {sezon} other {sezon}}", + "components.QuotaSelector.tvRequests": "{quotaLimit} {quotaDays} {days} başına {seasons}", "components.QuotaSelector.unlimited": "Sınırsız", "components.RegionSelector.regionDefault": "Tüm Bölgeler", "components.RegionSelector.regionServerDefault": "Varsayılan ({region})", - "components.RequestBlock.approve": "Talebi Kabul Et", + "components.RequestBlock.approve": "Talebi Onayla", "components.RequestBlock.decline": "Talebi Reddet", "components.RequestBlock.delete": "Talebi Sil", "components.RequestBlock.edit": "Talebi Düzenle", - "components.RequestBlock.languageprofile": "Dil Seçeneği", - "components.RequestBlock.profilechanged": "Kalite Seçeneği", - "components.RequestBlock.requestdate": "Talep Etme Tarihi", + "components.RequestBlock.languageprofile": "Dil Profili", + "components.RequestBlock.profilechanged": "Kalite Profili", + "components.RequestBlock.requestdate": "Talep Tarihi", "components.RequestBlock.requestedby": "Talep Eden", - "components.RequestBlock.requestoverrides": "Geçersiz Talep", - "components.RequestBlock.rootfolder": "Ana Klasör", - "components.RequestBlock.seasons": "{seasonCount, plural, one {Sezon} other {Sezon}}", + "components.RequestBlock.requestoverrides": "Talep Geçersiz Kılmaları", + "components.RequestBlock.rootfolder": "Kök Klasör", + "components.RequestBlock.seasons": "{seasonCount, plural, one {Sezon} other {Sezonlar}}", "components.RequestBlock.server": "Hedef Sunucu", - "components.RequestButton.approverequest": "Talebi Kabul Et", - "components.RequestButton.approverequest4k": "4K Talebi Kabul Et", - "components.RequestButton.approverequests": "{requestCount, plural, one {Talebi} other {{requestCount} Talebi}} Kabul Et", + "components.RequestButton.approverequest": "Talebi Onayla", + "components.RequestButton.approverequest4k": "4K Talebi Onayla", + "components.RequestButton.approverequests": "{requestCount, plural, one {Talebi} other {{requestCount} Talebi}} Onayla", "components.RequestButton.declinerequest": "Talebi Reddet", "components.RequestButton.declinerequest4k": "4K Talebi Reddet", "components.RequestButton.declinerequests": "{requestCount, plural, one {Talebi} other {{requestCount} Talebi}} Reddet", - "components.RequestButton.requestmore4k": "Daha Fazla 4K Talep Et", - "components.RequestButton.viewrequest": "Talepleri Gör", - "components.RequestButton.viewrequest4k": "4K Talepleri Gör", - "components.RequestCard.approverequest": "Talebi Kabul Et", + "components.RequestButton.requestmore4k": "4K Olarak Daha Fazla Talep Et", + "components.RequestButton.viewrequest": "Talebi Görüntüle", + "components.RequestButton.viewrequest4k": "4K Talebi Görüntüle", + "components.RequestCard.approverequest": "Talebi Onayla", "components.RequestCard.cancelrequest": "Talebi İptal Et", "components.RequestCard.declinerequest": "Talebi Reddet", "components.RequestCard.deleterequest": "Talebi Sil", - "components.RequestCard.failedretry": "Tekrardan talep gönderirken bir hata oluştu.", + "components.RequestCard.failedretry": "Talep yeniden denenirken bir sorun oluştu.", "components.RequestCard.mediaerror": "{mediaType} Bulunamadı", - "components.RequestCard.seasons": "{seasonCount} Sezon", - "components.RequestCard.tmdbid": "TMDB ID'si", - "components.RequestCard.tvdbid": "TheTVDB ID'si", - "components.RequestCard.unknowntitle": "Bilinmeyen İçerik", + "components.RequestCard.seasons": "{seasonCount, plural, one {Sezon} other {Sezonlar}}", + "components.RequestCard.tmdbid": "TMDB Kimliği", + "components.RequestCard.tvdbid": "TheTVDB Kimliği", + "components.RequestCard.unknowntitle": "Bilinmeyen Başlık", "components.RequestList.RequestItem.deleterequest": "Talebi Sil", "components.RequestList.RequestItem.editrequest": "Talebi Düzenle", - "components.RequestList.RequestItem.failedretry": "Tekrardan talep gönderirken bir hata oluştu.", + "components.RequestList.RequestItem.failedretry": "Talep yeniden denenirken bir sorun oluştu.", "components.RequestList.RequestItem.mediaerror": "{mediaType} Bulunamadı", - "components.RequestList.RequestItem.modified": "Son Düzenleme:", - "components.RequestList.RequestItem.modifieduserdate": "{user } tarafından, {date}", - "components.RequestList.RequestItem.requested": "Talep Edilme:", - "components.RequestList.RequestItem.seasons": "{seasonCount, plural, one {Sezon} other {Sezon}}", - "components.RequestList.RequestItem.tmdbid": "TMDB ID'si", - "components.RequestList.RequestItem.tvdbid": "TheTVDB ID'si", - "components.RequestList.RequestItem.unknowntitle": "Bilinmeyen İçerik", + "components.RequestList.RequestItem.modified": "Değiştirildi", + "components.RequestList.RequestItem.modifieduserdate": "{date} tarihinde {user} tarafından", + "components.RequestList.RequestItem.requested": "Talep Edildi", + "components.RequestList.RequestItem.seasons": "{seasonCount, plural, one {Sezon} other {Sezonlar}}", + "components.RequestList.RequestItem.tmdbid": "TMDB Kimliği", + "components.RequestList.RequestItem.tvdbid": "TheTVDB Kimliği", + "components.RequestList.RequestItem.unknowntitle": "Bilinmeyen Başlık", "components.RequestList.requests": "Talepler", "components.RequestList.showallrequests": "Tüm Talepleri Göster", - "components.RequestList.sortAdded": "En Son Eklenen", + "components.RequestList.sortAdded": "En Son", "components.RequestModal.AdvancedRequester.advancedoptions": "Gelişmiş", - "components.RequestModal.AdvancedRequester.animenote": "* Bu dizi bir animedir.", + "components.RequestModal.AdvancedRequester.animenote": "* Bu dizi bir anime.", "components.RequestModal.AdvancedRequester.default": "{name} (Varsayılan)", "components.RequestModal.AdvancedRequester.destinationserver": "Hedef Sunucu", "components.RequestModal.AdvancedRequester.folder": "{path} ({space})", - "components.RequestModal.AdvancedRequester.languageprofile": "Dil Seçeneği", + "components.RequestModal.AdvancedRequester.languageprofile": "Dil Profili", "components.RequestModal.AdvancedRequester.notagoptions": "Etiket yok.", - "components.RequestModal.AdvancedRequester.qualityprofile": "Kalite Seçeneği", - "components.RequestModal.AdvancedRequester.rootfolder": "Ana Klasör", - "components.RequestModal.AdvancedRequester.selecttags": "Etiketleri seç", + "components.RequestModal.AdvancedRequester.qualityprofile": "Kalite Profili", + "components.RequestModal.AdvancedRequester.rootfolder": "Kök Klasör", + "components.RequestModal.AdvancedRequester.selecttags": "Etiket seç", "components.RequestModal.AdvancedRequester.tags": "Etiketler", - "components.RequestModal.QuotaDisplay.movie": "Film", - "components.RequestModal.QuotaDisplay.movielimit": "{limit, plural, one {Film} other {Film}}", - "components.RequestModal.QuotaDisplay.notenoughseasonrequests": "Sezon isteteme limitiniz yetmemektedir", - "components.RequestModal.QuotaDisplay.quotaLink": "Talep limitlerinizin özetini profil sayfanızda görüntüleyebilirsiniz.", - "components.RequestModal.QuotaDisplay.requestsremaining": "{type} {remaining, plural, one {talep} other {talep}} limitin {remaining, plural, =0 {kalmadı} other {#}}", - "components.RequestModal.QuotaDisplay.requiredquotaUser": "Bu kullanıcının bu dizinin {sezonlarını} talep edebilmesi için en az {seasons, plural, one {sezon talebi} other {sezon talebi}} kadar talep göndermesi gerekiyor.", - "components.RequestModal.QuotaDisplay.season": "Sezon", + "components.RequestModal.QuotaDisplay.movie": "film", + "components.RequestModal.QuotaDisplay.movielimit": "{limit, plural, one {film} other {film}}", + "components.RequestModal.QuotaDisplay.notenoughseasonrequests": "Yeterli sezon talebi kalmadı", + "components.RequestModal.QuotaDisplay.quotaLink": "Talep limitlerinin özetini profil sayfanızda görüntüleyebilirsiniz.", + "components.RequestModal.QuotaDisplay.requestsremaining": "{remaining, plural, =0 {Hiç} other {#}} {type} {remaining, plural, one {talep} other {talep}} kaldı", + "components.RequestModal.QuotaDisplay.requiredquotaUser": "Bu kullanıcının bu dizi için talep gönderebilmesi için en az {seasons} {seasons, plural, one {sezon talebi} other {sezon talebi}} kalması gerekiyor.", + "components.RequestModal.QuotaDisplay.season": "sezon", "components.RequestModal.QuotaDisplay.seasonlimit": "{limit, plural, one {sezon} other {sezon}}", - "components.RequestModal.SearchByNameModal.nomatches": "Bu dizi için uygun bir eşleşme bulamadık.", - "components.RequestModal.SearchByNameModal.notvdbiddescription": "Bu diziyi otomatik olarak eşleştiremedik, lütfen aşağıdan bir eşleşme seçin.", + "components.RequestModal.SearchByNameModal.nomatches": "Bu dizi için eşleşme bulunamadı.", + "components.RequestModal.SearchByNameModal.notvdbiddescription": "Bu dizi otomatik olarak eşleştirilemedi. Lütfen aşağıdan doğru eşleşmeyi seçin.", "components.RequestModal.alreadyrequested": "Zaten Talep Edildi", - "components.RequestModal.approve": "Talebi Kabul Et", - "components.RequestModal.autoapproval": "Otomatik Kabul", + "components.RequestModal.approve": "Talebi Onayla", + "components.RequestModal.autoapproval": "Otomatik Onay", "components.RequestModal.cancel": "Talebi İptal Et", "components.RequestModal.edit": "Talebi Düzenle", - "components.RequestModal.errorediting": "Talep düzenlenirken beklenmedik bir hatayla karşılaşıldı.", - "components.RequestModal.numberofepisodes": "# Bölümleri", - "components.RequestModal.pending4krequest": "4K Talep Beklemede", - "components.RequestModal.pendingapproval": "Gönderdiğiniz talep onaylama aşamasında.", - "components.RequestModal.pendingrequest": "Talep Beklemede", - "components.RequestModal.requestCancel": "{title} için gönderdiğiniz talep reddedildi.", + "components.RequestModal.errorediting": "Talep düzenlenirken bir sorun oluştu.", + "components.RequestModal.numberofepisodes": "Bölüm Sayısı", + "components.RequestModal.pending4krequest": "Bekleyen 4K Talebi", + "components.RequestModal.pendingapproval": "Talebiniz onay bekliyor.", + "components.RequestModal.pendingrequest": "Bekleyen Talep", + "components.RequestModal.requestCancel": "{title} talebi iptal edildi.", "components.RequestModal.requestSuccess": "{title} başarıyla talep edildi!", - "components.RequestModal.requestcancelled": "{title} için gönderdiğiniz talep reddedildi.", - "components.RequestModal.requestcollection4ktitle": "Koleksiyonu 4K Kalitesiyle Talep Et", - "components.RequestModal.requestcollectiontitle": "Koleksiyonu Talep Et", - "components.RequestModal.requestedited": "{title} için gönderilen talep başarıyla düzenlendi!", - "components.RequestModal.requestfrom": "{username} kullanıcısının talepleri onay bekliyor.", - "components.RequestModal.requestmovie4ktitle": "Filmi 4K Talep Et", - "components.RequestModal.requestmovies": "{count} Filmi Talep Et", - "components.RequestModal.requestmovietitle": "Filmi Talep Et", - "components.RequestModal.requestseasons4k": "{seasonCount} Sezonu 4K Talep Et", - "components.RequestModal.requestseries4ktitle": "Diziyi 4K Talep Et", - "components.RequestModal.requestseriestitle": "Diziyi Talep Et", + "components.RequestModal.requestcancelled": "{title} talebi iptal edildi.", + "components.RequestModal.requestcollection4ktitle": "4K Koleksiyon Talep Et", + "components.RequestModal.requestcollectiontitle": "Koleksiyon Talep Et", + "components.RequestModal.requestedited": "{title} talebi başarıyla düzenlendi!", + "components.RequestModal.requestfrom": "{username} kullanıcısının talebi onay bekliyor.", + "components.RequestModal.requestmovie4ktitle": "4K Film Talep Et", + "components.RequestModal.requestmovies": "{count} {count, plural, one {Film} other {Film}} Talep Et", + "components.RequestModal.requestmovietitle": "Film Talep Et", + "components.RequestModal.requestseasons4k": "{seasonCount} {seasonCount, plural, one {Sezon} other {Sezon}} 4K Olarak Talep Et", + "components.RequestModal.requestseries4ktitle": "4K Dizi Talep Et", + "components.RequestModal.requestseriestitle": "Dizi Talep Et", "components.RequestModal.season": "Sezon", - "components.RequestModal.seasonnumber": "{number}. Sezon", - "components.RequestModal.selectmovies": "Film(leri) Seç", - "components.RequestModal.selectseason": "Dizi(leri) Seç", - "components.ResetPassword.confirmpassword": "Şifreyi Doğrula", - "components.ResetPassword.email": "E-mail Adresi", - "components.ResetPassword.emailresetlink": "E-mail Kurtarma Linki", + "components.RequestModal.seasonnumber": "Sezon {number}", + "components.RequestModal.selectmovies": "Film Seç", + "components.RequestModal.selectseason": "Sezon Seç", + "components.ResetPassword.confirmpassword": "Şifreyi Onayla", + "components.ResetPassword.email": "E-posta Adresi", + "components.ResetPassword.emailresetlink": "Kurtarma Bağlantısı Gönder", "components.ResetPassword.password": "Şifre", - "components.ResetPassword.passwordreset": "Şifreyi Sıfırla", + "components.ResetPassword.passwordreset": "Şifre Sıfırlama", "components.ResetPassword.resetpassword": "Şifrenizi sıfırlayın", - "components.ResetPassword.resetpasswordsuccessmessage": "Şifre sıfırlama başarılı!", - "components.ResetPassword.validationemailrequired": "Geçerli bir e-mail adresi sağlamalısınız", - "components.ResetPassword.validationpasswordmatch": "Girdiğiniz şifreler uyuşmuyor", - "components.ResetPassword.validationpasswordminchars": "Girdiğiniz şifre çok kısa, en az 8 karakterden oluşmalıdır", - "components.ResetPassword.validationpasswordrequired": "Bir şifre girmeniz gereklidir", + "components.ResetPassword.resetpasswordsuccessmessage": "Şifre başarıyla sıfırlandı!", + "components.ResetPassword.validationemailrequired": "Geçerli bir e-posta adresi girmelisiniz", + "components.ResetPassword.validationpasswordmatch": "Şifreler eşleşmelidir", + "components.ResetPassword.validationpasswordminchars": "Şifre çok kısa; en az 8 karakter olmalıdır", + "components.ResetPassword.validationpasswordrequired": "Bir şifre girmelisiniz", "components.Search.search": "Ara", "components.Search.searchresults": "Arama Sonuçları", - "components.Selector.nooptions": "Sonuç Bulunamadı.", - "components.Selector.searchGenres": "Türleri seç…", - "components.Selector.searchKeywords": "Anahtar kelimeleri arat…", - "components.Selector.searchStudios": "Stüdyolarda ara…", + "components.Selector.nooptions": "Sonuç bulunamadı.", + "components.Selector.searchGenres": "Tür seç…", + "components.Selector.searchKeywords": "Anahtar kelime ara…", + "components.Selector.searchStudios": "Stüdyo ara…", "components.Selector.showless": "Daha Az Göster", "components.Selector.showmore": "Daha Fazla Göster", - "components.Selector.starttyping": "Aramak için yazmaya başla.", + "components.Selector.starttyping": "Aramak için yazmaya başlayın.", "components.Settings.Notifications.NotificationsGotify.gotifysettingsfailed": "Gotify bildirim ayarları kaydedilemedi.", - "components.Settings.Notifications.NotificationsGotify.gotifysettingssaved": "Gotify bildirim ayarları kaydedildi!", - "components.Settings.Notifications.NotificationsGotify.toastGotifyTestFailed": "Gotify deneme bildirimi gönderilemedi.", - "components.Settings.Notifications.NotificationsGotify.toastGotifyTestSuccess": "Gotify deneme bildirimi gönderildi!", - "components.Settings.Notifications.NotificationsGotify.token": "Uygulama Token'i", - "components.Settings.Notifications.NotificationsGotify.url": "Sunucu URL'si", - "components.Settings.Notifications.NotificationsGotify.validationTokenRequired": "Bir uygulama Token'i girmeniz gerekmektedir", + "components.Settings.Notifications.NotificationsGotify.gotifysettingssaved": "Gotify bildirim ayarları başarıyla kaydedildi!", + "components.Settings.Notifications.NotificationsGotify.toastGotifyTestFailed": "Gotify test bildirimi gönderilemedi.", + "components.Settings.Notifications.NotificationsGotify.toastGotifyTestSuccess": "Gotify test bildirimi gönderildi!", + "components.Settings.Notifications.NotificationsGotify.token": "Uygulama Tokeni", + "components.Settings.Notifications.NotificationsGotify.url": "Sunucu URL", + "components.Settings.Notifications.NotificationsGotify.validationTokenRequired": "Bir uygulama tokeni girmelisiniz", "components.Settings.Notifications.NotificationsGotify.validationUrlRequired": "Geçerli bir URL girmelisiniz", "components.ManageSlideOver.removearr4k": "4K {arr}'dan Kaldır", - "components.NotificationTypeSelector.adminissuecommentDescription": "Diğer kullanıcılar hata bildirilerine yorum yaptıklarında bildirim gönder.", - "components.NotificationTypeSelector.mediadeclinedDescription": "Talep reddedildiğinde bildirim gönder.", - "components.PermissionEdit.createissuesDescription": "İçeriklerde ki hataları bildirme iznini ver.", - "components.PermissionEdit.viewrecentDescription": "Yakın zamanda eklenen içerikleri görme iznini ver.", - "components.RequestBlock.lastmodifiedby": "Son Düzenlenleyen", - "components.RequestButton.decline4krequests": "{requestCount, plural, one {4K Talebi} Reddet other {{requestCount} 4K Requests}}", + "components.NotificationTypeSelector.adminissuecommentDescription": "Diğer kullanıcılar sorunlara yorum yaptığında bildirim alın.", + "components.NotificationTypeSelector.mediadeclinedDescription": "Medya talepleri reddedildiğinde bildirim gönder.", + "components.PermissionEdit.createissuesDescription": "Medya sorunlarını bildirme izni ver.", + "components.PermissionEdit.viewrecentDescription": "Son eklenen medya listesini görüntüleme izni ver.", + "components.RequestBlock.lastmodifiedby": "Son Değiştiren", + "components.RequestButton.decline4krequests": "{requestCount, plural, one {4K Talebi} other {{requestCount} 4K Talebi}} Reddet", "components.RequestButton.requestmore": "Daha Fazla Talep Et", "components.RequestCard.editrequest": "Talebi Düzenle", - "components.RequestModal.requestApproved": "{title} için gönderdiğiniz talep onaylandı!", - "components.Settings.Notifications.NotificationsGotify.toastGotifyTestSending": "Gotify deneme bildirimi gönderiliyor…", - "components.Settings.Notifications.NotificationsGotify.validationTypes": "En azından bir adet bildirim türü seçmelisiniz", - "components.MovieDetails.imdbuserscore": "IMDB Kullanıcı Puanı", - "components.MovieDetails.overviewunavailable": "Özet mevcut değil.", - "components.MovieDetails.productioncountries": "Yapımcı {countryCount, plural, one {Ülke} other {Ülkeler}}", - "components.MovieDetails.streamingproviders": "İçeriğin Erişilebilir Olduğu Platformlar", - "components.NotificationTypeSelector.issuecommentDescription": "Sorun bildirilerine yeni yorum geldiğinde bildirim gönder.", - "components.NotificationTypeSelector.issuecreated": "Sorun Bildirimleri", + "components.RequestModal.requestApproved": "{title} talebi onaylandı!", + "components.Settings.Notifications.NotificationsGotify.toastGotifyTestSending": "Gotify test bildirimi gönderiliyor…", + "components.Settings.Notifications.NotificationsGotify.validationTypes": "En az bir bildirim türü seçmelisiniz", + "components.MovieDetails.imdbuserscore": "IMDB Kullanıcı Puanı – oy sayısı: {formattedCount}", + "components.MovieDetails.overviewunavailable": "Genel bakış mevcut değil.", + "components.MovieDetails.productioncountries": "Yapım {countryCount, plural, one {Ülkesi} other {Ülkeleri}}", + "components.MovieDetails.streamingproviders": "Şu Anda Yayınlanan Yer", + "components.NotificationTypeSelector.issuecommentDescription": "Sorunlara yeni yorumlar geldiğinde bildirim gönder.", + "components.NotificationTypeSelector.issuecreated": "Sorun Bildirildi", "components.NotificationTypeSelector.issuereopenedDescription": "Sorunlar yeniden açıldığında bildirim gönder.", - "components.NotificationTypeSelector.mediaapprovedDescription": "Kullanıcıların içerik talepleri elle kabul edildiğinde bildirim gönder.", - "components.NotificationTypeSelector.mediarequestedDescription": "Kullanıcılar doğrulama gerektiren içerik talepleri gönderdiğinde bildirim gönder.", - "components.NotificationTypeSelector.usermediaAutoApprovedDescription": "Diğer kullanıcılar otomatize içerik talepleri gönderdiğinde bildirim gönder.", - "components.NotificationTypeSelector.usermediafailedDescription": "Gönderdiğiniz içerik talepleri Sonarr ya da Radarr'a eklenemediğinde bildirim gönder.", - "components.PermissionEdit.autoapprove4k": "4K İçin Otomatik Onay", - "components.PermissionEdit.autoapprove4kDescription": "4K İçerik taleplerinin otomatik onaylanması iznini ver.", - "components.PermissionEdit.autoapproveMoviesDescription": "4K olmayan film taleplerinin otomatik onaylanması iznini ver.", - "components.PermissionEdit.autorequestDescription": "Kullanıcının Plex İzleme listesinde olan içerikler için otomatik olarak talep etme iznini ver.", - "components.PermissionEdit.managerequestsDescription": "İçerik taleplerini yönetme iznini ver. Bu izne sahip kullanıcının gönderdiği tüm talepler otomatik olarak kabul edilir.", - "components.PermissionEdit.viewrecent": "Yakın Zamanda Eklenenleri Görme", - "components.RequestModal.AdvancedRequester.requestas": "Talep Eden:", - "components.RequestModal.QuotaDisplay.allowedRequests": "{limit} {type} talep etme limitin var, her {days} günde bir bu limit yenilenecektir.", - "components.RequestModal.QuotaDisplay.allowedRequestsUser": "Bu kullanıcının {limit} {type} talep etme limiti var, her {days} günde bir bu limit yenilenecektir.", - "components.PermissionEdit.usersDescription": "Kullanıcıları yönetme iznini ver. Bu izne sahip kullanıcılar Yönetici yetkisine sahip kişileri düzenleyemezler ya da Yönetici yetkisi veremezler.", - "components.PermissionEdit.viewwatchlistsDescription": "Diğer kullanıcıların {mediaServerName} İzleme Listelerini görme iznini ver.", + "components.NotificationTypeSelector.mediaapprovedDescription": "Medya talepleri manuel olarak onaylandığında bildirim gönder.", + "components.NotificationTypeSelector.mediarequestedDescription": "Kullanıcılar onay gerektiren yeni medya talepleri gönderdiğinde bildirim gönder.", + "components.NotificationTypeSelector.usermediaAutoApprovedDescription": "Diğer kullanıcılar otomatik onaylanan yeni medya talepleri gönderdiğinde bildirim alın.", + "components.NotificationTypeSelector.usermediafailedDescription": "Medya talepleri Radarr veya Sonarr'a eklenemediğinde bildirim alın.", + "components.PermissionEdit.autoapprove4k": "4K Otomatik Onayla", + "components.PermissionEdit.autoapprove4kDescription": "Tüm 4K medya talepleri için otomatik onay ver.", + "components.PermissionEdit.autoapproveMoviesDescription": "4K olmayan film talepleri için otomatik onay ver.", + "components.PermissionEdit.autorequestDescription": "Plex İzleme Listesi aracılığıyla 4K olmayan medya için otomatik talep gönderme izni ver.", + "components.PermissionEdit.managerequestsDescription": "Medya taleplerini yönetme izni ver. Bu izne sahip kullanıcıların tüm talepleri otomatik olarak onaylanır.", + "components.PermissionEdit.viewrecent": "Son Eklenenleri Görüntüle", + "components.RequestModal.AdvancedRequester.requestas": "Şu Kişi Olarak Talep Et", + "components.RequestModal.QuotaDisplay.allowedRequests": "{limit} {type} talep etmenize izin verilmektedir{days, plural, =0 {} one { her gün} other { her {days} günde}}.", + "components.RequestModal.QuotaDisplay.allowedRequestsUser": "Bu kullanıcının {limit} {type} talep etmesine izin verilmektedir{days, plural, =0 {} one { her gün} other { her {days} günde}}.", + "components.PermissionEdit.usersDescription": "Kullanıcıları yönetme izni ver. Bu izne sahip kullanıcılar, Yönetici ayrıcalığına sahip kullanıcıları düzenleyemez veya bu ayrıcalığı veremez.", + "components.PermissionEdit.viewwatchlistsDescription": "Diğer kullanıcıların {mediaServerName} İzleme Listelerini görüntüleme izni ver.", "components.RequestList.RequestItem.cancelRequest": "Talebi İptal Et", - "components.RequestList.sortModified": "En Son Düzenlenen", + "components.RequestList.sortModified": "Son Değiştirilen", "components.RequestModal.QuotaDisplay.quotaLinkUser": "Bu kullanıcının talep limitlerinin özetini profil sayfasında görüntüleyebilirsiniz.", - "components.RequestModal.requesterror": "Talebi gönderirken beklenmedik bir hata oluştu.", - "components.RequestModal.requestseasons": "{seasonCount} Sezonu Talep Et", + "components.RequestModal.requesterror": "Talep gönderilirken bir sorun oluştu.", + "components.RequestModal.requestseasons": "{seasonCount} {seasonCount, plural, one {Sezon} other {Sezon}} Talep Et", "components.Settings.Notifications.NotificationsGotify.agentenabled": "Aracıyı Etkinleştir", - "components.QuotaSelector.movieRequests": "{quotaLimit} {Film} per {quotaDays} {gün}", - "components.RequestList.RequestItem.requesteddate": "Talep Edildi", - "components.RequestModal.requestadmin": "Bu talep otomatik olarak onaylanacaktır.", - "components.RequestButton.approve4krequests": "{requestCount, plural, one {4K Talebi} Kabul Et other {{requestCount} 4K Requests}}", - "components.RequestModal.QuotaDisplay.requiredquota": "Bu dizinin {sezonlarını} istetebilmek için en azından {seasons, plural, one {sezon talebi} other {sezon talepleri}} kadar talep göndermeniz gerekiyor.", - "components.RequestModal.requestmovies4k": "{count} Filmi 4K Talep Et", - "components.ResetPassword.gobacklogin": "Oturum Açma Sayfasına Dön", - "components.ResetPassword.requestresetlinksuccessmessage": "Eğer ki girilen e-mail adresi gerçek bir kişiyle ilişkiliyse şifre sıfırlama linki gönderilecektir.", - "components.Settings.Notifications.NotificationsGotify.validationUrlTrailingSlash": "URL'nizin sonunda slash (eğik çizgi) olmamalıdır", - "components.Settings.Notifications.NotificationsPushbullet.accessToken": "Erişim Token'i", - "components.Settings.Notifications.NotificationsPushbullet.accessTokenTip": "Hesap Ayarlarınızdan bir token oluşturun", + "components.QuotaSelector.movieRequests": "{quotaLimit} {quotaDays} {days} başına {movies}", + "components.RequestList.RequestItem.requesteddate": "Talep Tarihi", + "components.RequestModal.requestadmin": "Bu talep otomatik olarak onaylanacak.", + "components.RequestButton.approve4krequests": "{requestCount, plural, one {4K Talebi} other {{requestCount} 4K Talebi}} Onayla", + "components.RequestModal.QuotaDisplay.requiredquota": "Bu dizi için talep gönderebilmek için en az {seasons} {seasons, plural, one {sezon talebi} other {sezon talebi}} kalması gerekiyor.", + "components.RequestModal.requestmovies4k": "{count} {count, plural, one {Film} other {Film}} 4K Olarak Talep Et", + "components.ResetPassword.gobacklogin": "Giriş Sayfasına Dön", + "components.ResetPassword.requestresetlinksuccessmessage": "Girilen e-posta adresi geçerli bir kullanıcıyla ilişkiliyse şifre sıfırlama bağlantısı gönderilecektir.", + "components.Settings.Notifications.NotificationsGotify.validationUrlTrailingSlash": "URL eğik çizgi ile bitmemelidir", + "components.Settings.Notifications.NotificationsPushbullet.accessToken": "Erişim Tokeni", + "components.Settings.Notifications.NotificationsPushbullet.accessTokenTip": "Hesap Ayarları'ndan bir token oluşturun", "components.Settings.Notifications.NotificationsPushbullet.agentEnabled": "Aracıyı Etkinleştir", "components.Settings.Notifications.NotificationsPushbullet.channelTag": "Kanal Etiketi", "components.Settings.Notifications.NotificationsPushbullet.pushbulletSettingsFailed": "Pushbullet bildirim ayarları kaydedilemedi.", - "components.Settings.Notifications.NotificationsPushbullet.pushbulletSettingsSaved": "Pushbullet bildirim ayarları kaydedildi!", - "components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestSending": "Pushbullet deneme bildirimi gönderiliyor…", - "components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestSuccess": "Pushbullet deneme bildirimi gönderildi!", - "components.Settings.Notifications.NotificationsPushbullet.validationAccessTokenRequired": "Bir erişim token'i sağlamalısınız", - "components.Settings.Notifications.NotificationsPushbullet.validationTypes": "En azından bir adet bildirim türü seçmelisiniz", - "components.Settings.Notifications.NotificationsPushover.accessToken": "Uygulama API'sinin Token'i", - "components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestFailed": "Pushbullet deneme bildirimi gönderilemedi.", + "components.Settings.Notifications.NotificationsPushbullet.pushbulletSettingsSaved": "Pushbullet bildirim ayarları başarıyla kaydedildi!", + "components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestSending": "Pushbullet test bildirimi gönderiliyor…", + "components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestSuccess": "Pushbullet test bildirimi gönderildi!", + "components.Settings.Notifications.NotificationsPushbullet.validationAccessTokenRequired": "Bir erişim tokeni girmelisiniz", + "components.Settings.Notifications.NotificationsPushbullet.validationTypes": "En az bir bildirim türü seçmelisiniz", + "components.Settings.Notifications.NotificationsPushover.accessToken": "Uygulama API Tokeni", + "components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestFailed": "Pushbullet test bildirimi gönderilemedi.", "components.Settings.RadarrModal.released": "Yayınlandı", - "components.Settings.RadarrModal.rootfolder": "Ana Klasör", + "components.Settings.RadarrModal.rootfolder": "Kök Klasör", "components.Settings.Notifications.NotificationsPushover.agentenabled": "Aracıyı Etkinleştir", "components.Settings.Notifications.NotificationsPushover.deviceDefault": "Cihaz Varsayılanı", "components.Settings.Notifications.NotificationsPushover.pushoversettingsfailed": "Pushover bildirim ayarları kaydedilemedi.", - "components.Settings.Notifications.NotificationsPushover.pushoversettingssaved": "Pushover bildirim ayarları kaydedildi!", + "components.Settings.Notifications.NotificationsPushover.pushoversettingssaved": "Pushover bildirim ayarları başarıyla kaydedildi!", "components.Settings.Notifications.NotificationsPushover.sound": "Bildirim Sesi", - "components.Settings.Notifications.NotificationsPushover.toastPushoverTestFailed": "Pushover deneme bildirimi gönderilemedi.", - "components.Settings.Notifications.NotificationsPushover.accessTokenTip": "Seerr ile kullanılmak üzere bir uygulama kaydedin", - "components.Settings.Notifications.NotificationsPushover.toastPushoverTestSuccess": "Pushover deneme bildirimi gönderildi!", - "components.Settings.Notifications.NotificationsPushover.validationAccessTokenRequired": "Geçerli bir uygulama token'i sağlamalısınız", - "components.Settings.Notifications.NotificationsPushover.validationTypes": "En azından bir adet bildirim türü seçmelisiniz", - "components.Settings.Notifications.NotificationsPushover.userToken": "Kullanıcı ya da Grup Kimliği", - "components.Settings.Notifications.NotificationsPushover.validationUserTokenRequired": "Geçerli bir kullanıcı ya da grup kimliği sağlamalısınız", + "components.Settings.Notifications.NotificationsPushover.toastPushoverTestFailed": "Pushover test bildirimi gönderilemedi.", + "components.Settings.Notifications.NotificationsPushover.accessTokenTip": "Seerr ile kullanmak için bir uygulama kaydedin", + "components.Settings.Notifications.NotificationsPushover.toastPushoverTestSuccess": "Pushover test bildirimi gönderildi!", + "components.Settings.Notifications.NotificationsPushover.validationAccessTokenRequired": "Geçerli bir uygulama tokeni girmelisiniz", + "components.Settings.Notifications.NotificationsPushover.validationTypes": "En az bir bildirim türü seçmelisiniz", + "components.Settings.Notifications.NotificationsPushover.userToken": "Kullanıcı veya Grup Anahtarı", + "components.Settings.Notifications.NotificationsPushover.validationUserTokenRequired": "Geçerli bir kullanıcı veya grup anahtarı girmelisiniz", "components.Settings.Notifications.NotificationsSlack.agentenabled": "Aracıyı Etkinleştir", "components.Settings.Notifications.NotificationsSlack.slacksettingsfailed": "Slack bildirim ayarları kaydedilemedi.", - "components.Settings.Notifications.NotificationsSlack.toastSlackTestFailed": "Slacxk deneme bildirimi gönderilemedi.", - "components.Settings.Notifications.NotificationsSlack.toastSlackTestSending": "Slack deneme bildirimi gönderiliyor…", - "components.Settings.Notifications.NotificationsSlack.toastSlackTestSuccess": "Slack deneme bildirimi gönderildi!", - "components.Settings.Notifications.NotificationsSlack.validationTypes": "En azından bir adet bildirim türü seçmelisiniz", + "components.Settings.Notifications.NotificationsSlack.toastSlackTestFailed": "Slack test bildirimi gönderilemedi.", + "components.Settings.Notifications.NotificationsSlack.toastSlackTestSending": "Slack test bildirimi gönderiliyor…", + "components.Settings.Notifications.NotificationsSlack.toastSlackTestSuccess": "Slack test bildirimi gönderildi!", + "components.Settings.Notifications.NotificationsSlack.validationTypes": "En az bir bildirim türü seçmelisiniz", "components.Settings.Notifications.NotificationsSlack.validationWebhookUrl": "Geçerli bir URL girmelisiniz", - "components.Settings.Notifications.NotificationsSlack.webhookUrl": "Webhook URL'si", + "components.Settings.Notifications.NotificationsSlack.webhookUrl": "Webhook URL", "components.Settings.Notifications.NotificationsWebPush.agentenabled": "Aracıyı Etkinleştir", - "components.Settings.Notifications.NotificationsWebPush.httpsRequirement": "Web push bildirimlerini alabilmek için Seerr'in HTTPS üzerinden sunulması gerekir.", - "components.Settings.Notifications.NotificationsWebPush.toastWebPushTestFailed": "Web deneme bildirimi gönderilemedi.", - "components.Settings.Notifications.NotificationsWebPush.toastWebPushTestSending": "Web deneme bildirimi gönderiliyor…", - "components.Settings.Notifications.NotificationsWebPush.toastWebPushTestSuccess": "Web deneme bildirimi gönderildi!", - "components.Settings.Notifications.NotificationsWebPush.webpushsettingssaved": "Web bildirim ayarları kaydedildi!", + "components.Settings.Notifications.NotificationsWebPush.httpsRequirement": "Web push bildirimleri alabilmek için Seerr'ın HTTPS üzerinden sunulması gerekir.", + "components.Settings.Notifications.NotificationsWebPush.toastWebPushTestFailed": "Web push test bildirimi gönderilemedi.", + "components.Settings.Notifications.NotificationsWebPush.toastWebPushTestSending": "Web push test bildirimi gönderiliyor…", + "components.Settings.Notifications.NotificationsWebPush.toastWebPushTestSuccess": "Web push test bildirimi gönderildi!", + "components.Settings.Notifications.NotificationsWebPush.webpushsettingssaved": "Web push bildirim ayarları başarıyla kaydedildi!", "components.Settings.Notifications.NotificationsWebhook.agentenabled": "Aracıyı Etkinleştir", - "components.Settings.Notifications.NotificationsWebhook.authheader": "Yetkilendirme Üstbilgisi (Header'ı)", - "components.Settings.Notifications.NotificationsWebhook.customJson": "JSON Payload'I", - "components.Settings.Notifications.NotificationsWebhook.resetPayload": "Varsayılan Ayarlara Sıfırla", - "components.Settings.Notifications.NotificationsWebhook.resetPayloadSuccess": "JSON Payload'ı başarıyla sıfırlandı!", - "components.Settings.Notifications.NotificationsWebhook.templatevariablehelp": "Şablon Değişkeni İçin Yardım", - "components.Settings.Notifications.NotificationsWebhook.toastWebhookTestSending": "Webhook deneme bildirimi gönderiliyor…", - "components.Settings.Notifications.NotificationsWebhook.toastWebhookTestSuccess": "Webhook deneme bildirimi gönderildi!", - "components.Settings.Notifications.NotificationsWebhook.validationJsonPayloadRequired": "Geçerli bir JSON Payload'ı sağlamalısınız", - "components.Settings.Notifications.NotificationsWebhook.validationTypes": "En azından bir adet bildirim türü seçmelisiniz", - "components.Settings.Notifications.NotificationsWebhook.webhookUrl": "Webhook URL'si", + "components.Settings.Notifications.NotificationsWebhook.authheader": "Yetkilendirme Başlığı", + "components.Settings.Notifications.NotificationsWebhook.customJson": "JSON Yükü", + "components.Settings.Notifications.NotificationsWebhook.resetPayload": "Varsayılana Sıfırla", + "components.Settings.Notifications.NotificationsWebhook.resetPayloadSuccess": "JSON yükü başarıyla sıfırlandı!", + "components.Settings.Notifications.NotificationsWebhook.templatevariablehelp": "Şablon Değişken Yardımı", + "components.Settings.Notifications.NotificationsWebhook.toastWebhookTestSending": "Webhook test bildirimi gönderiliyor…", + "components.Settings.Notifications.NotificationsWebhook.toastWebhookTestSuccess": "Webhook test bildirimi gönderildi!", + "components.Settings.Notifications.NotificationsWebhook.validationJsonPayloadRequired": "Geçerli bir JSON yükü girmelisiniz", + "components.Settings.Notifications.NotificationsWebhook.validationTypes": "En az bir bildirim türü seçmelisiniz", + "components.Settings.Notifications.NotificationsWebhook.webhookUrl": "Webhook URL", "components.Settings.Notifications.NotificationsWebhook.webhooksettingsfailed": "Webhook bildirim ayarları kaydedilemedi.", - "components.Settings.Notifications.NotificationsWebhook.webhooksettingssaved": "Webhook bildirim ayarları kaydedildi!", + "components.Settings.Notifications.NotificationsWebhook.webhooksettingssaved": "Webhook bildirim ayarları başarıyla kaydedildi!", "components.Settings.Notifications.agentenabled": "Aracıyı Etkinleştir", - "components.Settings.Notifications.allowselfsigned": "Kendinden İmzalı Sertifikaları Kabul Et", - "components.Settings.Notifications.authPass": "SMPT Şifresi", - "components.Settings.Notifications.botAPI": "Bot Yetkilendirme Token'i", - "components.Settings.Notifications.botApiTip": "Seerr ile kullanılmak üzere bir bot oluşturun", - "components.Settings.Notifications.botAvatarUrl": "Bot Profil Resmi URL'si", + "components.Settings.Notifications.allowselfsigned": "Öz İmzalı Sertifikalara İzin Ver", + "components.Settings.Notifications.authPass": "SMTP Şifresi", + "components.Settings.Notifications.botAPI": "Bot Yetkilendirme Tokeni", + "components.Settings.Notifications.botApiTip": "Seerr ile kullanmak için bir bot oluşturun", + "components.Settings.Notifications.botAvatarUrl": "Bot Avatar URL", "components.Settings.Notifications.botUsername": "Bot Kullanıcı Adı", - "components.Settings.Notifications.chatId": "Sohbet ID'si", - "components.Settings.Notifications.chatIdTip": "Botunuzla sohbet etmek için @get_id_bot isimiyle ekleyin, /my_id komutunu kullanın ve sohbeti başlatın", + "components.Settings.Notifications.chatId": "Sohbet Kimliği", + "components.Settings.Notifications.chatIdTip": "Botunuzla bir sohbet başlatın, @get_id_bot'u ekleyin ve /my_id komutunu kullanın", "components.Settings.Notifications.discordsettingsfailed": "Discord bildirim ayarları kaydedilemedi.", "components.Settings.Notifications.emailsender": "Gönderen Adresi", - "components.Settings.Notifications.emailsettingsfailed": "E-mail bildirim ayarları kaydedilemedi.", - "components.Settings.Notifications.emailsettingssaved": "E-mail bildirim ayarları kaydedildi!", - "components.Settings.Notifications.enableMentions": "Bahsetmeleri Etkinleştirin", + "components.Settings.Notifications.emailsettingsfailed": "E-posta bildirim ayarları kaydedilemedi.", + "components.Settings.Notifications.emailsettingssaved": "E-posta bildirim ayarları başarıyla kaydedildi!", + "components.Settings.Notifications.enableMentions": "Bahsetmeleri Etkinleştir", "components.Settings.Notifications.encryption": "Şifreleme Yöntemi", - "components.Settings.Notifications.encryptionDefault": "Mümkünse STARTTLS kullan", - "components.Settings.Notifications.encryptionImplicitTls": "Örtülü (Implict) TLS Kullan", - "components.Settings.Notifications.encryptionNone": "Hiçbiri", - "components.Settings.Notifications.encryptionTip": "Genel olarak Örtülü (Implict) TLS 465 numaralı portu kullanır, STARTTLS ise 587 numaralı portu kullanır", + "components.Settings.Notifications.encryptionDefault": "Mümkünse STARTTLS Kullan", + "components.Settings.Notifications.encryptionImplicitTls": "Örtük TLS Kullan", + "components.Settings.Notifications.encryptionNone": "Yok", + "components.Settings.Notifications.encryptionTip": "Çoğu durumda Örtük TLS port 465, STARTTLS ise port 587 kullanır", "components.Settings.Notifications.pgpPassword": "PGP Şifresi", - "components.Settings.Notifications.pgpPrivateKey": "PGP Gizli Anahtar", - "components.Settings.Notifications.pgpPrivateKeyTip": "OpenPGP kullanarak e-mailleri imzala ve şifrele", + "components.Settings.Notifications.pgpPrivateKey": "PGP Özel Anahtarı", + "components.Settings.Notifications.pgpPrivateKeyTip": "OpenPGP kullanarak şifrelenmiş e-posta mesajlarını imzalayın", "components.Settings.Notifications.sendSilently": "Sessizce Gönder", - "components.Settings.Notifications.sendSilentlyTip": "Bildirimler sessiz gönderilir", - "components.Settings.Notifications.senderName": "Gönderen İsmi", - "components.Settings.Notifications.smtpHost": "SMPT Sunucu Adresi", - "components.Settings.Notifications.smtpPort": "SMPT Port'u", + "components.Settings.Notifications.sendSilentlyTip": "Bildirimleri ses olmadan gönder", + "components.Settings.Notifications.senderName": "Gönderen Adı", + "components.Settings.Notifications.smtpHost": "SMTP Sunucusu", + "components.Settings.Notifications.smtpPort": "SMTP Portu", "components.Settings.Notifications.telegramsettingsfailed": "Telegram bildirim ayarları kaydedilemedi.", - "components.Settings.Notifications.telegramsettingssaved": "Telegram bildirim ayarları kaydedildi!", - "components.Settings.Notifications.toastDiscordTestSuccess": "Discord deneme bildirimi gönderildi!", - "components.Settings.Notifications.toastEmailTestFailed": "E-mail deneme bildirimi gönderilemedi.", - "components.Settings.Notifications.toastEmailTestSuccess": "E-mail deneme bildirimi gönderildi!", + "components.Settings.Notifications.telegramsettingssaved": "Telegram bildirim ayarları başarıyla kaydedildi!", + "components.Settings.Notifications.toastDiscordTestSuccess": "Discord test bildirimi gönderildi!", + "components.Settings.Notifications.toastEmailTestFailed": "E-posta test bildirimi gönderilemedi.", + "components.Settings.Notifications.toastEmailTestSuccess": "E-posta test bildirimi gönderildi!", "components.Settings.Notifications.toastTelegramTestFailed": "Telegram test bildirimi gönderilemedi.", - "components.Settings.Notifications.toastTelegramTestSending": "Telegram deneme bildirimi gönderiliyor…", - "components.Settings.Notifications.toastTelegramTestSuccess": "Telegram deneme bildirimi gönderildi!", - "components.Settings.Notifications.userEmailRequired": "Kullanıcı e-mail'ini zorunlu tut", - "components.Settings.Notifications.validationChatIdRequired": "Geçerli bir sohbet ID'si sağlamalısın", - "components.Settings.Notifications.validationEmail": "Geçerli bir e-mail adresi sağlamalısın", - "components.Settings.Notifications.validationPgpPassword": "Bir PGP şifresi sağlamalısın", - "components.Settings.Notifications.validationPgpPrivateKey": "Geçerli bir PGP gizli anahtarı sağlamalısın", - "components.Settings.Notifications.validationSmtpPortRequired": "Geçerli bir port numarası girmelisin", - "components.Settings.Notifications.validationTypes": "En azından bir adet bildirim türü seçmelisiniz", + "components.Settings.Notifications.toastTelegramTestSending": "Telegram test bildirimi gönderiliyor…", + "components.Settings.Notifications.toastTelegramTestSuccess": "Telegram test bildirimi gönderildi!", + "components.Settings.Notifications.userEmailRequired": "Kullanıcı e-postası zorunlu", + "components.Settings.Notifications.validationChatIdRequired": "Geçerli bir sohbet kimliği girmelisiniz", + "components.Settings.Notifications.validationEmail": "Geçerli bir e-posta adresi girmelisiniz", + "components.Settings.Notifications.validationPgpPassword": "Bir PGP şifresi girmelisiniz", + "components.Settings.Notifications.validationPgpPrivateKey": "Geçerli bir PGP özel anahtarı girmelisiniz", + "components.Settings.Notifications.validationSmtpPortRequired": "Geçerli bir port numarası girmelisiniz", + "components.Settings.Notifications.validationTypes": "En az bir bildirim türü seçmelisiniz", "components.Settings.Notifications.validationUrl": "Geçerli bir URL girmelisiniz", - "components.Settings.Notifications.webhookUrl": "Webhook URL'si", + "components.Settings.Notifications.webhookUrl": "Webhook URL", "components.Settings.RadarrModal.add": "Sunucu Ekle", - "components.Settings.RadarrModal.announced": "Duyurulmuş", + "components.Settings.RadarrModal.announced": "Duyuruldu", "components.Settings.RadarrModal.apiKey": "API Anahtarı", - "components.Settings.RadarrModal.baseUrl": "Temel URL Adresi", - "components.Settings.RadarrModal.create4kradarr": "Yeni Bir 4K Radarr Sunucusu Ekle", - "components.Settings.RadarrModal.createradarr": "Yeni Bir Radarr Sunucusu Ekle", + "components.Settings.RadarrModal.baseUrl": "URL Tabanı", + "components.Settings.RadarrModal.create4kradarr": "Yeni 4K Radarr Sunucusu Ekle", + "components.Settings.RadarrModal.createradarr": "Yeni Radarr Sunucusu Ekle", "components.Settings.RadarrModal.default4kserver": "Varsayılan 4K Sunucu", "components.Settings.RadarrModal.defaultserver": "Varsayılan Sunucu", "components.Settings.RadarrModal.edit4kradarr": "4K Radarr Sunucusunu Düzenle", "components.Settings.RadarrModal.editradarr": "Radarr Sunucusunu Düzenle", "components.Settings.RadarrModal.enableSearch": "Otomatik Aramayı Etkinleştir", "components.Settings.RadarrModal.externalUrl": "Harici URL", - "components.Settings.RadarrModal.hostname": "Domain ya da IP Adresi", - "components.Settings.RadarrModal.inCinemas": "Sinemalarda", + "components.Settings.RadarrModal.hostname": "Sunucu Adı veya IP Adresi", + "components.Settings.RadarrModal.inCinemas": "Sinemada", "components.Settings.RadarrModal.loadingTags": "Etiketler yükleniyor…", - "components.Settings.RadarrModal.loadingrootfolders": "Ana klasör yükleniyor…", - "components.Settings.RadarrModal.minimumAvailability": "Asgari Erişilebilirlik Ayarı", + "components.Settings.RadarrModal.loadingrootfolders": "Kök klasörler yükleniyor…", + "components.Settings.RadarrModal.minimumAvailability": "Minimum Kullanılabilirlik", "components.Settings.RadarrModal.notagoptions": "Etiket yok.", "components.Settings.RadarrModal.port": "Port", - "components.Settings.RadarrModal.qualityprofile": "Kalite Seçeneği", - "components.Settings.RadarrModal.selectQualityProfile": "Kaliteyi seç", - "components.Settings.RadarrModal.selectRootFolder": "Ana klasörü seç", - "components.Settings.RadarrModal.selecttags": "Etiketleri seç", + "components.Settings.RadarrModal.qualityprofile": "Kalite Profili", + "components.Settings.RadarrModal.selectQualityProfile": "Kalite profili seç", + "components.Settings.RadarrModal.selectRootFolder": "Kök klasör seç", + "components.Settings.RadarrModal.selecttags": "Etiket seç", "components.Settings.RadarrModal.server4k": "4K Sunucu", - "components.Settings.RadarrModal.servername": "Sunucu İsmi", + "components.Settings.RadarrModal.servername": "Sunucu Adı", "components.Settings.RadarrModal.ssl": "SSL Kullan", "components.Settings.RadarrModal.syncEnabled": "Taramayı Etkinleştir", - "components.Settings.RadarrModal.tagRequests": "Etiket Talepleri", + "components.Settings.RadarrModal.tagRequests": "Talepleri Etiketle", "components.Settings.RadarrModal.tags": "Etiketler", - "components.Settings.RadarrModal.testFirstQualityProfiles": "Kalite seçeneklerini görüntülemek için bağlantıyı test et", - "components.Settings.RadarrModal.testFirstRootFolders": "Ana klasörleri görüntülemek için bağlantıyı test et", - "components.Settings.RadarrModal.testFirstTags": "Etiketleri görüntülemek için bağlantıyı test et", + "components.Settings.RadarrModal.testFirstQualityProfiles": "Kalite profillerini yüklemek için bağlantıyı test edin", + "components.Settings.RadarrModal.testFirstRootFolders": "Kök klasörleri yüklemek için bağlantıyı test edin", + "components.Settings.RadarrModal.testFirstTags": "Etiketleri yüklemek için bağlantıyı test edin", "components.Settings.RadarrModal.toastRadarrTestFailure": "Radarr'a bağlanılamadı.", - "components.Settings.RadarrModal.toastRadarrTestSuccess": "Radarr'a başarıyla bağlanıldı!", - "components.Settings.RadarrModal.validationApplicationUrl": "Geçerli bir URL adresi girmelisiniz", - "components.Settings.RadarrModal.validationApplicationUrlTrailingSlash": "URL'nizin sonunda slash (eğik çizgi) olmamalıdır", - "components.Settings.RadarrModal.validationBaseUrlLeadingSlash": "URL'nizin başında slash (eğik çizgi) olmalıdır", - "components.Settings.RadarrModal.validationBaseUrlTrailingSlash": "Temel URL slash (eğik çizgi) ile bitmemelidir", - "components.Settings.RadarrModal.validationMinimumAvailabilityRequired": "Asgari erişilebilirlik ayarı seçmelisin", - "components.Settings.RadarrModal.validationNameRequired": "Bir sunucu ismi girmelisin", - "components.Settings.RadarrModal.validationPortRequired": "Geçerli bir port numarası girmelisin", - "components.Settings.RadarrModal.validationProfileRequired": "Bir kalite seçeneği seçmelisin", - "components.Settings.RadarrModal.validationRootFolderRequired": "Bir ana klasör seçmelisin", - "components.Settings.SettingsAbout.Releases.currentversion": "Mevcut Sürüm", - "components.Settings.SettingsAbout.Releases.latestversion": "Güncel Sürüm", - "components.Settings.SettingsAbout.Releases.releases": "Yayınlanan Sürümler", - "components.Settings.SettingsAbout.Releases.versionChangelog": "{version} Sürüm Notları", - "components.Settings.SettingsAbout.Releases.viewchangelog": "Sürüm Notlarını Gör", - "components.Settings.SettingsAbout.Releases.viewongithub": "Github'da Görüntüle", + "components.Settings.RadarrModal.toastRadarrTestSuccess": "Radarr bağlantısı başarıyla kuruldu!", + "components.Settings.RadarrModal.validationApplicationUrl": "Geçerli bir URL girmelisiniz", + "components.Settings.RadarrModal.validationApplicationUrlTrailingSlash": "URL eğik çizgi ile bitmemelidir", + "components.Settings.RadarrModal.validationBaseUrlLeadingSlash": "URL tabanı eğik çizgi ile başlamalıdır", + "components.Settings.RadarrModal.validationBaseUrlTrailingSlash": "URL tabanı eğik çizgi ile bitmemelidir", + "components.Settings.RadarrModal.validationMinimumAvailabilityRequired": "Bir minimum kullanılabilirlik seçmelisiniz", + "components.Settings.RadarrModal.validationNameRequired": "Bir sunucu adı girmelisiniz", + "components.Settings.RadarrModal.validationPortRequired": "Geçerli bir port numarası girmelisiniz", + "components.Settings.RadarrModal.validationProfileRequired": "Bir kalite profili seçmelisiniz", + "components.Settings.RadarrModal.validationRootFolderRequired": "Bir kök klasör seçmelisiniz", + "components.Settings.SettingsAbout.Releases.currentversion": "Mevcut", + "components.Settings.SettingsAbout.Releases.latestversion": "En Son", + "components.Settings.SettingsAbout.Releases.releases": "Sürümler", + "components.Settings.SettingsAbout.Releases.versionChangelog": "{version} Değişiklik Günlüğü", + "components.Settings.SettingsAbout.Releases.viewchangelog": "Değişiklik Günlüğünü Görüntüle", + "components.Settings.SettingsAbout.Releases.viewongithub": "GitHub'da Görüntüle", "components.Settings.SettingsAbout.about": "Hakkında", "components.Settings.SettingsAbout.appDataPath": "Veri Dizini", - "components.Settings.SettingsAbout.documentation": "Dökümantasyon", + "components.Settings.SettingsAbout.documentation": "Belgeler", "components.Settings.SettingsAbout.gettingsupport": "Destek Al", "components.Settings.SettingsAbout.githubdiscussions": "GitHub Tartışmaları", - "components.Settings.SettingsAbout.timezone": "Zaman Dilimi", - "components.Settings.SettingsAbout.totalmedia": "Toplam İçerik", - "components.Settings.SettingsAbout.totalrequests": "Toplam Talep", + "components.Settings.SettingsAbout.timezone": "Saat Dilimi", + "components.Settings.SettingsAbout.totalmedia": "Toplam Medya", + "components.Settings.SettingsAbout.totalrequests": "Toplam Talepler", "components.Settings.SettingsAbout.uptodate": "Güncel", "components.Settings.SettingsAbout.version": "Sürüm", - "components.Settings.SettingsJobsCache.availability-sync": "İçerik Kullanılabilirliğini Eşitle", - "components.Settings.SettingsJobsCache.cache": "Önbelleğe Al", - "components.Settings.SettingsJobsCache.cacheDescription": "Seerr, performansı optimize etmek ve gereksiz API çağrıları yapmaktan kaçınmak için harici API uç noktalarına gelen istekleri önbelleğe alır.", + "components.Settings.SettingsJobsCache.availability-sync": "Medya Kullanılabilirlik Senkronizasyonu", + "components.Settings.SettingsJobsCache.cache": "Önbellek", + "components.Settings.SettingsJobsCache.cacheDescription": "Seerr, performansı optimize etmek ve gereksiz API çağrılarını önlemek için harici API uç noktalarına yapılan istekleri önbelleğe alır.", "components.Settings.SettingsJobsCache.cacheflushed": "{cachename} önbelleği temizlendi.", - "components.Settings.SettingsJobsCache.cachehits": "Tutulan", + "components.Settings.SettingsJobsCache.cachehits": "İsabet", "components.Settings.SettingsJobsCache.cachekeys": "Toplam Anahtar", "components.Settings.SettingsJobsCache.cacheksize": "Anahtar Boyutu", - "components.Settings.SettingsJobsCache.cachemisses": "Kayıplar", - "components.Settings.SettingsJobsCache.cachename": "Önbellek İsmi", - "components.Settings.SettingsJobsCache.canceljob": "İşlemi Durdur", + "components.Settings.SettingsJobsCache.cachemisses": "Kaçırma", + "components.Settings.SettingsJobsCache.cachename": "Önbellek Adı", + "components.Settings.SettingsJobsCache.canceljob": "Görevi İptal Et", "components.Settings.SettingsJobsCache.command": "Komut", - "components.Settings.SettingsJobsCache.download-sync": "İndirilenleri Eşitle", - "components.Settings.SettingsJobsCache.download-sync-reset": "İndirilenler Eşitlemesini Sıfırla", - "components.Settings.SettingsJobsCache.editJobSchedule": "İşlemi Düzenle", - "components.Settings.SettingsJobsCache.editJobScheduleCurrent": "Mevcut Crontab", - "components.Settings.SettingsJobsCache.editJobSchedulePrompt": "Yeni Crontab", - "components.Settings.SettingsJobsCache.editJobScheduleSelectorMinutes": "Her {jobScheduleMinutes, plural, one {dakikada} other {{jobScheduleMinutes} dakika}}", - "components.Settings.SettingsJobsCache.editJobScheduleSelectorSeconds": "Her {jobScheduleSeconds, plural, one {saniyede} other {{jobScheduleSeconds} saniye}}", + "components.Settings.SettingsJobsCache.download-sync": "İndirme Senkronizasyonu", + "components.Settings.SettingsJobsCache.download-sync-reset": "İndirme Senkronizasyonu Sıfırlama", + "components.Settings.SettingsJobsCache.editJobSchedule": "Görevi Değiştir", + "components.Settings.SettingsJobsCache.editJobScheduleCurrent": "Mevcut Sıklık", + "components.Settings.SettingsJobsCache.editJobSchedulePrompt": "Yeni Sıklık", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorMinutes": "Her {jobScheduleMinutes, plural, one {dakika} other {{jobScheduleMinutes} dakikada bir}}", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorSeconds": "Her {jobScheduleSeconds, plural, one {saniye} other {{jobScheduleSeconds} saniyede bir}}", "components.Settings.SettingsJobsCache.flushcache": "Önbelleği Temizle", - "components.Settings.SettingsJobsCache.image-cache-cleanup": "Görüntü Önbellek Temizleme", - "components.Settings.SettingsJobsCache.imagecache": "Görüntü Önbelleği", - "components.Settings.SettingsJobsCache.imagecachecount": "Önbelleğe Alınan Görüntüler", + "components.Settings.SettingsJobsCache.image-cache-cleanup": "Resim Önbelleği Temizleme", + "components.Settings.SettingsJobsCache.imagecache": "Resim Önbelleği", + "components.Settings.SettingsJobsCache.imagecachecount": "Önbelleğe Alınan Resimler", "components.Settings.SettingsJobsCache.imagecachesize": "Toplam Önbellek Boyutu", - "components.Settings.SettingsJobsCache.jellyfin-full-scan": "Tam Jellyfin Kütüphane Taraması", - "components.Settings.SettingsJobsCache.jellyfin-recently-added-scan": "Jellyfin En Son Eklenenler Taraması", - "components.Settings.SonarrModal.animerootfolder": "Anime Ana Dizini", - "components.Settings.SettingsJobsCache.jobScheduleEditSaved": "İşlem başarıyla düzenlendi!", + "components.Settings.SettingsJobsCache.jellyfin-full-scan": "Jellyfin Tam Kütüphane Taraması", + "components.Settings.SettingsJobsCache.jellyfin-recently-added-scan": "Jellyfin Son Eklenenler Taraması", + "components.Settings.SonarrModal.animerootfolder": "Anime Kök Klasörü", + "components.Settings.SettingsJobsCache.jobScheduleEditSaved": "Görev başarıyla düzenlendi!", "components.Settings.SettingsJobsCache.jobcancelled": "{jobname} iptal edildi.", - "components.Settings.SettingsJobsCache.jobname": "İşlem İsmi", - "components.Settings.SettingsJobsCache.jobsandcache": "İşlemler & Önbellek", - "components.Settings.SettingsJobsCache.jobstarted": "{jobname} başladı.", + "components.Settings.SettingsJobsCache.jobname": "Görev Adı", + "components.Settings.SettingsJobsCache.jobsandcache": "Görevler ve Önbellek", + "components.Settings.SettingsJobsCache.jobstarted": "{jobname} başlatıldı.", "components.Settings.SettingsJobsCache.jobtype": "Tür", - "components.Settings.SettingsJobsCache.nextexecution": "Bir Sonraki İşlem", - "components.Settings.SettingsJobsCache.plex-full-scan": "Tam Plex Kütüphane Taraması", - "components.Settings.SettingsJobsCache.plex-recently-added-scan": "Plex En Son Eklenenler Taraması", + "components.Settings.SettingsJobsCache.nextexecution": "Sonraki Çalışma", + "components.Settings.SettingsJobsCache.plex-full-scan": "Plex Tam Kütüphane Taraması", + "components.Settings.SettingsJobsCache.plex-recently-added-scan": "Plex Son Eklenenler Taraması", "components.Settings.SettingsJobsCache.plex-watchlist-sync": "Plex İzleme Listesi Senkronizasyonu", - "components.Settings.SettingsJobsCache.process": "Süreç", + "components.Settings.SettingsJobsCache.process": "İşle", "components.Settings.SettingsJobsCache.radarr-scan": "Radarr Taraması", - "components.Settings.SettingsJobsCache.runnow": "Şimdi Başlat", + "components.Settings.SettingsJobsCache.runnow": "Şimdi Çalıştır", "components.Settings.SettingsJobsCache.sonarr-scan": "Sonarr Taraması", - "components.Settings.SettingsJobsCache.unknownJob": "Bilinmeyen İşlem", - "components.Settings.SettingsLogs.copiedLogMessage": "Loglar panoya kopyalandı.", + "components.Settings.SettingsJobsCache.unknownJob": "Bilinmeyen Görev", + "components.Settings.SettingsLogs.copiedLogMessage": "Günlük mesajı panoya kopyalandı.", "components.Settings.SettingsLogs.copyToClipboard": "Panoya Kopyala", - "components.Settings.SettingsLogs.extraData": "İlave Veri", - "components.Settings.SettingsLogs.filterDebug": "Sorun Ayıkla", - "components.Settings.SettingsLogs.filterError": "Sorun", - "components.Settings.SettingsLogs.filterInfo": "Bilgilendirme", + "components.Settings.SettingsLogs.extraData": "Ek Veri", + "components.Settings.SettingsLogs.filterDebug": "Hata Ayıklama", + "components.Settings.SettingsLogs.filterError": "Hata", + "components.Settings.SettingsLogs.filterInfo": "Bilgi", "components.Settings.SettingsLogs.filterWarn": "Uyarı", "components.Settings.SettingsLogs.label": "Etiket", "components.Settings.SettingsLogs.level": "Önem Derecesi", - "components.Settings.SettingsLogs.logDetails": "Log Detayları", - "components.Settings.SettingsLogs.logs": "Loglar", + "components.Settings.SettingsLogs.logDetails": "Günlük Ayrıntıları", + "components.Settings.SettingsLogs.logs": "Günlükler", "components.Settings.SettingsLogs.message": "Mesaj", - "components.Settings.SettingsLogs.pauseLogs": "Durdur", - "components.Settings.SettingsLogs.resumeLogs": "Devam Ettir", - "components.Settings.SettingsLogs.showall": "Tüm Logları Göster", - "components.Settings.SettingsLogs.time": "Tarih Bilgisi", - "components.Settings.SettingsLogs.viewdetails": "Detayları Görüntüle", + "components.Settings.SettingsLogs.pauseLogs": "Duraklat", + "components.Settings.SettingsLogs.resumeLogs": "Devam Et", + "components.Settings.SettingsLogs.showall": "Tüm Günlükleri Göster", + "components.Settings.SettingsLogs.time": "Zaman Damgası", + "components.Settings.SettingsLogs.viewdetails": "Ayrıntıları Görüntüle", "components.Settings.SettingsMain.apikey": "API Anahtarı", "components.Settings.SettingsMain.applicationTitle": "Uygulama Başlığı", - "components.Settings.SettingsMain.applicationurl": "Uygulama URL'si", - "components.Settings.SettingsMain.cacheImages": "Resim Önbelleğe Alma'yı Etkinleştir", + "components.Settings.SettingsMain.applicationurl": "Uygulama URL", + "components.Settings.SettingsMain.cacheImages": "Resim Önbelleğini Etkinleştir", "components.Settings.SonarrModal.apiKey": "API Anahtarı", "components.Settings.SettingsMain.general": "Genel", "components.Settings.SettingsMain.generalsettings": "Genel Ayarlar", - "components.Settings.SettingsMain.hideAvailable": "Kullanılabilir İçerikleri Gizle", - "components.Settings.SettingsMain.locale": "Uygulama Dili", + "components.Settings.SettingsMain.hideAvailable": "Kullanılabilir Medyayı Gizle", + "components.Settings.SettingsMain.locale": "Görüntüleme Dili", "components.Settings.SettingsMain.originallanguage": "Keşfet Dili", - "components.Settings.SettingsMain.originallanguageTip": "İçerikleri orijinal dillerine göre filtrele", - "components.Settings.SettingsMain.partialRequestsEnabled": "Kısmi Dizi Taleplerini Kabul Et", - "components.Settings.SettingsMain.toastApiKeyFailure": "Yeni API anahtarı denenirken beklenmedik bir hata oluştu.", - "components.Settings.SettingsMain.toastSettingsFailure": "Ayarlar kaydedilirken beklenmedik bir hata oluştu.", + "components.Settings.SettingsMain.originallanguageTip": "İçeriği orijinal dile göre filtrele", + "components.Settings.SettingsMain.partialRequestsEnabled": "Kısmi Dizi Taleplerini İzin Ver", + "components.Settings.SettingsMain.toastApiKeyFailure": "Yeni API anahtarı oluşturulurken bir sorun oluştu.", + "components.Settings.SettingsMain.toastSettingsFailure": "Ayarlar kaydedilirken bir sorun oluştu.", "components.Settings.SettingsMain.toastSettingsSuccess": "Ayarlar başarıyla kaydedildi!", "components.Settings.SettingsMain.validationApplicationTitle": "Bir uygulama başlığı girmelisiniz", - "components.Settings.SettingsMain.validationApplicationUrl": "Geçerli bir URL adresi girmelisiniz", - "components.Settings.SettingsMain.validationApplicationUrlTrailingSlash": "URL'nizin sonunda slash (eğik çizgi) olmamalıdır", - "components.Settings.SettingsUsers.defaultPermissions": "Öntanımlı İzinler", - "components.Settings.SettingsUsers.defaultPermissionsTip": "Yeni kayıt yapmış kullanıcılara verilen izinler", - "components.Settings.SettingsUsers.localLogin": "Local (Yerel) Oturum Açmayı Etkinleştir", - "components.Settings.SettingsUsers.movieRequestLimitLabel": "Genel Film Talep Etme Sınırı", - "components.Settings.SettingsUsers.newPlexLogin": "{mediaServerName}'den Kayıtsız Oturum Açmayı Etkinleştir", - "components.Settings.SettingsUsers.newPlexLoginTip": "{mediaServerName} kullanıcılarının içe aktarılmadan oturum açmalarına izin ver", - "components.Settings.SettingsUsers.toastSettingsFailure": "Ayarlar kaydedilirken beklenmedik bir hata oluştu.", + "components.Settings.SettingsMain.validationApplicationUrl": "Geçerli bir URL girmelisiniz", + "components.Settings.SettingsMain.validationApplicationUrlTrailingSlash": "URL eğik çizgi ile bitmemelidir", + "components.Settings.SettingsUsers.defaultPermissions": "Varsayılan İzinler", + "components.Settings.SettingsUsers.defaultPermissionsTip": "Yeni kullanıcılara atanan başlangıç izinleri", + "components.Settings.SettingsUsers.localLogin": "Yerel Girişi Etkinleştir", + "components.Settings.SettingsUsers.movieRequestLimitLabel": "Global Film Talep Limiti", + "components.Settings.SettingsUsers.newPlexLogin": "Yeni {mediaServerName} Girişini Etkinleştir", + "components.Settings.SettingsUsers.newPlexLoginTip": "{mediaServerName} kullanıcılarının önceden içe aktarılmadan giriş yapmasına izin ver", + "components.Settings.SettingsUsers.toastSettingsFailure": "Ayarlar kaydedilirken bir sorun oluştu.", "components.Settings.SettingsUsers.toastSettingsSuccess": "Kullanıcı ayarları başarıyla kaydedildi!", - "components.Settings.SettingsUsers.tvRequestLimitLabel": "Genel Dizi Talep Etme Sınırı", + "components.Settings.SettingsUsers.tvRequestLimitLabel": "Global Dizi Talep Limiti", "components.Settings.SettingsUsers.userSettings": "Kullanıcı Ayarları", "components.Settings.SettingsUsers.users": "Kullanıcılar", "components.Settings.SonarrModal.add": "Sunucu Ekle", - "components.Settings.SonarrModal.animeSeriesType": "Anime Dizisi Türü", + "components.Settings.SonarrModal.animeSeriesType": "Anime Dizi Türü", "components.Settings.SonarrModal.animeTags": "Anime Etiketleri", - "components.Settings.SonarrModal.animelanguageprofile": "Anime Dil Seçenekleri", - "components.Settings.SonarrModal.animequalityprofile": "Anime Kalite Seçenekleri", + "components.Settings.SonarrModal.animelanguageprofile": "Anime Dil Profili", + "components.Settings.SonarrModal.animequalityprofile": "Anime Kalite Profili", "components.Settings.SonarrModal.create4ksonarr": "Yeni 4K Sonarr Sunucusu Ekle", "components.Settings.SonarrModal.createsonarr": "Yeni Sonarr Sunucusu Ekle", "components.Settings.SonarrModal.default4kserver": "Varsayılan 4K Sunucu", @@ -809,392 +809,391 @@ "components.Settings.SonarrModal.edit4ksonarr": "4K Sonarr Sunucusunu Düzenle", "components.Settings.SonarrModal.enableSearch": "Otomatik Aramayı Etkinleştir", "components.Settings.SonarrModal.externalUrl": "Harici URL", - "components.Settings.SonarrModal.hostname": "Domain ya da IP Adresi", - "components.Settings.SonarrModal.languageprofile": "Dil Seçeneği", + "components.Settings.SonarrModal.hostname": "Sunucu Adı veya IP Adresi", + "components.Settings.SonarrModal.languageprofile": "Dil Profili", "components.Settings.SonarrModal.loadingTags": "Etiketler yükleniyor…", - "components.Settings.SonarrModal.loadingprofiles": "Kalite seçenekleri yükleniyor…", - "components.Settings.SonarrModal.loadingrootfolders": "Ana klasör yükleniyor…", + "components.Settings.SonarrModal.loadingprofiles": "Kalite profilleri yükleniyor…", + "components.Settings.SonarrModal.loadingrootfolders": "Kök klasörler yükleniyor…", "components.Settings.SonarrModal.notagoptions": "Etiket yok.", "components.Settings.SonarrModal.port": "Port", - "components.Settings.SonarrModal.qualityprofile": "Kalite Seçeneği", - "components.Settings.SonarrModal.rootfolder": "Ana Klasör", + "components.Settings.SonarrModal.qualityprofile": "Kalite Profili", + "components.Settings.SonarrModal.rootfolder": "Kök Klasör", "components.Settings.SonarrModal.seasonfolders": "Sezon Klasörleri", - "components.Settings.SonarrModal.selectLanguageProfile": "Dil seçeneğini seç", - "components.Settings.SonarrModal.selectQualityProfile": "Kaliteyi seç", - "components.Settings.SonarrModal.selectRootFolder": "Ana klasörü seç", - "components.Settings.SonarrModal.selecttags": "Etiketleri seç", - "components.Settings.SonarrModal.seriesType": "Dizinin Türü", + "components.Settings.SonarrModal.selectLanguageProfile": "Dil profili seç", + "components.Settings.SonarrModal.selectQualityProfile": "Kalite profili seç", + "components.Settings.SonarrModal.selectRootFolder": "Kök klasör seç", + "components.Settings.SonarrModal.selecttags": "Etiket seç", + "components.Settings.SonarrModal.seriesType": "Dizi Türü", "components.Settings.SonarrModal.server4k": "4K Sunucu", "components.Settings.SonarrModal.ssl": "SSL Kullan", "components.Settings.SonarrModal.syncEnabled": "Taramayı Etkinleştir", - "components.Settings.SonarrModal.tagRequests": "Etiket Talepleri", + "components.Settings.SonarrModal.tagRequests": "Talepleri Etiketle", "components.Settings.SonarrModal.tags": "Etiketler", - "components.Settings.SonarrModal.testFirstLanguageProfiles": "Dil seçeneklerini görmek için bağlantıyı test et", - "components.Settings.SonarrModal.testFirstQualityProfiles": "Kalite seçeneklerini görmek için bağlantıyı test et", - "components.Settings.SonarrModal.testFirstRootFolders": "Ana klasörleri yüklemek için bağlantıyı test et", - "components.Settings.SonarrModal.testFirstTags": "Etiketleri yüklemek için bağlantıyı test et", + "components.Settings.SonarrModal.testFirstLanguageProfiles": "Dil profillerini yüklemek için bağlantıyı test edin", + "components.Settings.SonarrModal.testFirstQualityProfiles": "Kalite profillerini yüklemek için bağlantıyı test edin", + "components.Settings.SonarrModal.testFirstRootFolders": "Kök klasörleri yüklemek için bağlantıyı test edin", + "components.Settings.SonarrModal.testFirstTags": "Etiketleri yüklemek için bağlantıyı test edin", "components.Settings.SonarrModal.toastSonarrTestFailure": "Sonarr'a bağlanılamadı.", "components.Settings.SonarrModal.validationApiKeyRequired": "Bir API anahtarı girmelisiniz", - "components.Settings.SonarrModal.validationApplicationUrl": "Geçerli bir URL adresi girmelisiniz", - "components.Settings.SonarrModal.validationApplicationUrlTrailingSlash": "URL'nizin sonunda slash (eğik çizgi) olmamalıdır", - "components.Settings.SonarrModal.validationBaseUrlLeadingSlash": "Temel URL'nizin başında slash (eğik çizgi) olmalıdır", - "components.Settings.SonarrModal.validationHostnameRequired": "Geçerli bir sunucu adresi girmelisin", - "components.Settings.SonarrModal.validationLanguageProfileRequired": "Bir dil seçeneği seçmelisin", - "components.Settings.SonarrModal.validationPortRequired": "Geçerli bir port numarası girmelisin", - "components.Settings.SonarrModal.validationProfileRequired": "Bir kalite seçeneği girmelisin", - "components.Settings.SonarrModal.validationRootFolderRequired": "Bir ana klasör seçmelisin", - "components.Settings.activeProfile": "Etkin Profil", + "components.Settings.SonarrModal.validationApplicationUrl": "Geçerli bir URL girmelisiniz", + "components.Settings.SonarrModal.validationApplicationUrlTrailingSlash": "URL eğik çizgi ile bitmemelidir", + "components.Settings.SonarrModal.validationBaseUrlLeadingSlash": "Temel URL eğik çizgi ile başlamalıdır", + "components.Settings.SonarrModal.validationHostnameRequired": "Geçerli bir sunucu adı veya IP adresi girmelisiniz", + "components.Settings.SonarrModal.validationLanguageProfileRequired": "Bir dil profili seçmelisiniz", + "components.Settings.SonarrModal.validationPortRequired": "Geçerli bir port numarası girmelisiniz", + "components.Settings.SonarrModal.validationProfileRequired": "Bir kalite profili seçmelisiniz", + "components.Settings.SonarrModal.validationRootFolderRequired": "Bir kök klasör seçmelisiniz", + "components.Settings.activeProfile": "Aktif Profil", "components.Settings.addradarr": "Radarr Sunucusu Ekle", "components.Settings.address": "Adres", "components.Settings.addsonarr": "Sonarr Sunucusu Ekle", "components.Settings.cancelscan": "Taramayı İptal Et", "components.Settings.currentlibrary": "Mevcut Kütüphane: {name}", - "components.Settings.default": "Öntanımlı", - "components.Settings.default4k": "Öntanımlı 4K", + "components.Settings.default": "Varsayılan", + "components.Settings.default4k": "Varsayılan 4K", "components.Settings.deleteServer": "{serverType} Sunucusunu Sil", - "components.Settings.deleteserverconfirm": "Bu sunucuyu silmek istediğinize emin misiniz?", - "components.Settings.email": "E-mail", + "components.Settings.deleteserverconfirm": "Bu sunucuyu silmek istediğinizden emin misiniz?", + "components.Settings.email": "E-posta", "components.Settings.enablessl": "SSL Kullan", "components.Settings.externalUrl": "Harici URL", - "components.Settings.hostname": "Domain ya da IP Adresi", + "components.Settings.hostname": "Sunucu Adı veya IP Adresi", "components.Settings.is4k": "4K", "components.Settings.jellyfinSettings": "{mediaServerName} Ayarları", - "components.Settings.jellyfinSettingsFailure": "{mediaServerName} ayarları kaydedilirken beklenmedik bir hata oluştu.", + "components.Settings.jellyfinSettingsFailure": "{mediaServerName} ayarları kaydedilirken bir sorun oluştu.", "components.Settings.jellyfinSettingsSuccess": "{mediaServerName} ayarları başarıyla kaydedildi!", - "components.Settings.jellyfinlibraries": "{mediaServerName} Kütüphaneler", - "components.Settings.jellyfinlibrariesDescription": "{mediaServerName} medya sunucunuzda içerik taraması yapılacak. Eğer hiç bir kütüphane listelenmediyse 'Kütüphaneleri Eşitle' butonunu kullanın.", + "components.Settings.jellyfinlibraries": "{mediaServerName} Kütüphaneleri", + "components.Settings.jellyfinlibrariesDescription": "{mediaServerName}'ın başlıkları taradığı kütüphaneler. Kütüphane listelenmiyorsa aşağıdaki düğmeye tıklayın.", "components.Settings.jellyfinsettings": "{mediaServerName} Ayarları", - "components.Settings.jellyfinsettingsDescription": "{mediaServerName} sunucunuz için ayarları düzenleyin. {mediaServerName} sunucunuzun kütüphaneleri içlerinde ki içerikleri tespiti için taranacaktır.", + "components.Settings.jellyfinsettingsDescription": "{mediaServerName} sunucunuzun ayarlarını yapılandırın. {mediaServerName}, hangi içeriklerin mevcut olduğunu görmek için {mediaServerName} kütüphanelerinizi tarar.", "components.Settings.librariesRemaining": "Kalan Kütüphaneler: {count}", - "components.Settings.manualscan": "Kütüphaneleri Manuel Tara", - "components.Settings.manualscanDescriptionJellyfin": "Normalde rutin olarak bu işlem 24 saatte bir yapılır. Seerr {mediaServerName} sunucunuzun en son eklenenlerini sıklıkla kontrol eder. Eğer ki bu Seerr'ı ilk yapılandırışınız ise tek seferlik manuel kütüphane taraması yapmanız önerilir!", - "components.Settings.mediaTypeMovie": "Film", + "components.Settings.manualscan": "Manuel Kütüphane Taraması", + "components.Settings.manualscanDescriptionJellyfin": "Normalde bu işlem yalnızca 24 saatte bir çalışır. Seerr, {mediaServerName} sunucunuza son eklenenler için daha sık bakacaktır. Seerr'ı ilk kez yapılandırıyorsanız bir defaya mahsus tam manuel kütüphane taraması önerilir!", + "components.Settings.mediaTypeMovie": "film", "components.Settings.menuAbout": "Hakkında", "components.Settings.menuGeneralSettings": "Genel", "components.Settings.menuJellyfinSettings": "{mediaServerName}", - "components.Settings.menuLogs": "Loglar", + "components.Settings.menuLogs": "Günlükler", "components.Settings.menuNotifications": "Bildirimler", "components.Settings.menuPlexSettings": "Plex", - "components.Settings.menuServices": "Servisler", + "components.Settings.menuServices": "Hizmetler", "components.Settings.menuUsers": "Kullanıcılar", "components.Settings.mediaTypeSeries": "dizi", - "components.Settings.noDefaultServer": "Talebin işlenebilmesi için en azından bir {serverType} sunucusunun {mediaType} içerikleri için öntanımlı olarak işaretlenmesi gerekmektedir.", - "components.Settings.notificationAgentSettingsDescription": "Bildirim aracılarını yapılandırın ve etkinleştirin", + "components.Settings.noDefaultServer": "{mediaType} taleplerinin işlenebilmesi için en az bir {serverType} sunucusu varsayılan olarak işaretlenmelidir.", + "components.Settings.notificationAgentSettingsDescription": "Bildirim aracılarını yapılandırın ve etkinleştirin.", "components.Settings.notifications": "Bildirimler", "components.Settings.notificationsettings": "Bildirim Ayarları", "components.Settings.notrunning": "Çalışmıyor", "components.Settings.plex": "Plex", - "components.Settings.plexlibraries": "Plex Kütüphaneler", - "components.Settings.plexlibrariesDescription": "Seerr'in başlıklar için taradığı kütüphaneler. Plex bağlantı ayarlarınızı kurun ve kaydedin, ardından hiçbir kütüphane listelenmemişse aşağıdaki düğmeye tıklayın.", + "components.Settings.plexlibraries": "Plex Kütüphaneleri", + "components.Settings.plexlibrariesDescription": "Seerr'ın başlıkları taradığı kütüphaneler. Plex bağlantı ayarlarınızı yapılandırıp kaydedin, ardından kütüphane listelenmiyorsa aşağıdaki düğmeye tıklayın.", "components.Settings.plexsettings": "Plex Ayarları", "components.Settings.port": "Port", "components.Settings.radarrsettings": "Radarr Ayarları", - "components.Settings.restartrequiredTooltip": "Bu ayarda yapılan değişikliklerin etkili olması için Seerr'in yeniden başlatılması gerekir", + "components.Settings.restartrequiredTooltip": "Bu ayardaki değişikliklerin geçerli olabilmesi için Seerr yeniden başlatılmalıdır", "components.Settings.save": "Değişiklikleri Kaydet", "components.Settings.saving": "Kaydediliyor…", - "components.Settings.scan": "Kütüphaneleri Eşitle", - "components.Settings.scanning": "Eşitleniyor…", - "components.Settings.serverLocal": "local (yerel)", - "components.Settings.serverRemote": "remote (uzak)", + "components.Settings.scan": "Kütüphaneleri Senkronize Et", + "components.Settings.scanning": "Senkronize Ediliyor…", + "components.Settings.serverLocal": "yerel", + "components.Settings.serverRemote": "uzak", "components.Settings.serverSecure": "güvenli", "components.Settings.serverpreset": "Sunucu", - "components.Settings.serverpresetLoad": "Kullanılabilir suncuuları yüklemek için butona bas", - "components.Settings.serverpresetManualMessage": "Özel ayarlamalar", - "components.Settings.serverpresetRefreshing": "Sunucular getiriliyor…", - "components.Settings.Notifications.NotificationsSlack.slacksettingssaved": "Slack bildirim ayarları kaydedildi!", - "components.Settings.Notifications.authUser": "SMPT Kullanıcı Adı", - "components.Settings.Notifications.pgpPasswordTip": "OpenPGP kullanarak e-mailleri imzala ve şifrele", + "components.Settings.serverpresetLoad": "Mevcut sunucuları yüklemek için düğmeye basın", + "components.Settings.serverpresetManualMessage": "Manuel yapılandırma", + "components.Settings.serverpresetRefreshing": "Sunucular alınıyor…", + "components.Settings.Notifications.NotificationsSlack.slacksettingssaved": "Slack bildirim ayarları başarıyla kaydedildi!", + "components.Settings.Notifications.authUser": "SMTP Kullanıcı Adı", + "components.Settings.Notifications.pgpPasswordTip": "OpenPGP kullanarak şifrelenmiş e-posta mesajlarını imzalayın", "components.Settings.Notifications.toastDiscordTestFailed": "Discord test bildirimi gönderilemedi.", - "components.Settings.RadarrModal.selectMinimumAvailability": "Asgari erişilebilirlik ayarını seç", - "components.Settings.RadarrModal.validationHostnameRequired": "Geçerli bir sunucu adresi girmelisin", - "components.Settings.SettingsMain.cacheImagesTip": "Dış kaynaklardan alınan resimleri önbelleğe al (depolama kullanımını arttıracaktır)", - "components.Settings.Notifications.NotificationsSlack.webhookUrlTip": "Bir Webhook entegrasyonu oluştur", - "components.Settings.Notifications.discordsettingssaved": "Discord bildirim ayarları kaydedildi!", - "components.Settings.Notifications.encryptionOpportunisticTls": "Her daim STARTTLS kullan", - "components.Settings.Notifications.toastDiscordTestSending": "Discord deneme bildirimi gönderiliyor…", - "components.Settings.Notifications.webhookUrlTip": "Sunucunuz için bir Webhook entegrasyonu oluştur", - "components.Settings.SettingsAbout.outofdate": "Eski Sürüm", - "components.Settings.SettingsAbout.runningDevelop": "Seerr'in yalnızca geliştirmeye katkıda bulunan veya en son testlere yardımcı olan kişiler için önerilen develop dalını çalıştırıyorsunuz.", + "components.Settings.RadarrModal.selectMinimumAvailability": "Minimum kullanılabilirlik seç", + "components.Settings.RadarrModal.validationHostnameRequired": "Geçerli bir sunucu adı veya IP adresi girmelisiniz", + "components.Settings.SettingsMain.cacheImagesTip": "Harici kaynaklı resimleri önbelleğe al (önemli miktarda disk alanı gerektirir)", + "components.Settings.Notifications.NotificationsSlack.webhookUrlTip": "Bir Gelen Webhook entegrasyonu oluşturun", + "components.Settings.Notifications.discordsettingssaved": "Discord bildirim ayarları başarıyla kaydedildi!", + "components.Settings.Notifications.encryptionOpportunisticTls": "Her Zaman STARTTLS Kullan", + "components.Settings.Notifications.toastDiscordTestSending": "Discord test bildirimi gönderiliyor…", + "components.Settings.Notifications.webhookUrlTip": "Sunucunuzda bir webhook entegrasyonu oluşturun", + "components.Settings.SettingsAbout.outofdate": "Güncel Değil", + "components.Settings.SettingsAbout.runningDevelop": "Seerr'ın develop dalını çalıştırıyorsunuz; bu yalnızca geliştirmeye katkıda bulunanlar veya en yeni sürümleri test edenler için önerilir.", "components.Settings.SettingsJobsCache.cachevsize": "Değer Boyutu", - "components.Settings.SettingsJobsCache.imagecacheDescription": "Ayarlarda etkinleştirildiğinde, Seerr önceden yapılandırılmış harici kaynaklardan gelen görüntüleri proxy'leyecek ve önbelleğe alacaktır. Önbelleğe alınan görüntüler yapılandırma klasörünüze kaydedilir. Dosyaları {appDataPath}/cache/images konumunda bulabilirsiniz.", - "components.Settings.SettingsUsers.userSettingsDescription": "Genel ve varsayılan kullanıcı ayarlarını yapılandırın.", + "components.Settings.SettingsJobsCache.imagecacheDescription": "Ayarlarda etkinleştirildiğinde Seerr, önceden yapılandırılmış harici kaynaklardan gelen resimleri proxy üzerinden önbelleğe alır. Önbelleğe alınan resimler yapılandırma klasörünüze kaydedilir. Dosyaları {appDataPath}/cache/images konumunda bulabilirsiniz.", + "components.Settings.SettingsUsers.userSettingsDescription": "Global ve varsayılan kullanıcı ayarlarını yapılandırın.", "components.Settings.SonarrModal.editsonarr": "Sonarr Sunucusunu Düzenle", - "components.Settings.SonarrModal.tagRequestsInfo": "Otomatik olarak isteyenin kullanıcı adını ek etiketlere ekle", - "components.Settings.SonarrModal.toastSonarrTestSuccess": "Sonarr'a başarıyla bağlanıldı!", - "components.Settings.experimentalTooltip": "Bu ayarı etkinleştirmek uygulamanın beklenmedik davranışlarda bulunmasına sebep olabilir", - "components.Settings.noDefaultNon4kServer": "Eğer ki hem 4K hem de 4K-olmayan içerikler için tek tip bir {serverType} sunucusu kullanıyorsan (ya da sadece 4K içerikleri indiriyorsan) {serverType} sunucunu KESİNLİKLE 4K olarak ayarlamaman gerekmektedir.", - "components.Settings.Notifications.NotificationsWebPush.webpushsettingsfailed": "Web bildirim ayarları kaydedilemedi.", - "components.Settings.Notifications.validationBotAPIRequired": "Bir bot yetkilendirme token'i sağlamalısın", - "components.Settings.RadarrModal.loadingprofiles": "Kalite seçenekleri yükleniyor…", - "components.Settings.RadarrModal.tagRequestsInfo": "Otomatik olarak isteyenin kullanıcı adını ek etiketlere ekle", + "components.Settings.SonarrModal.tagRequestsInfo": "Talep sahibinin kullanıcı kimliği ve görünen adıyla otomatik olarak ek etiket ekle", + "components.Settings.SonarrModal.toastSonarrTestSuccess": "Sonarr bağlantısı başarıyla kuruldu!", + "components.Settings.experimentalTooltip": "Bu ayarı etkinleştirmek beklenmedik uygulama davranışlarına yol açabilir", + "components.Settings.noDefaultNon4kServer": "Yalnızca tek bir {serverType} sunucunuz varsa hem 4K olmayan hem de 4K içerikler için (veya yalnızca 4K içerik indiriyorsanız), {serverType} sunucunuz 4K sunucu olarak belirlenmemelidir.", + "components.Settings.Notifications.NotificationsWebPush.webpushsettingsfailed": "Web push bildirim ayarları kaydedilemedi.", + "components.Settings.Notifications.validationBotAPIRequired": "Bir bot yetkilendirme tokeni girmelisiniz", + "components.Settings.RadarrModal.loadingprofiles": "Kalite profilleri yükleniyor…", + "components.Settings.RadarrModal.tagRequestsInfo": "Talep sahibinin kullanıcı kimliği ve görünen adıyla otomatik olarak ek etiket ekle", "components.Settings.RadarrModal.validationApiKeyRequired": "Bir API anahtarı girmelisiniz", - "components.Settings.SettingsAbout.Releases.releasedataMissing": "Çıkışıyla ilgili veri henüz mevcut değildir.", - "components.Settings.SettingsJobsCache.editJobScheduleSelectorHours": "Her {jobScheduleHours, plural, one {saatte} other {{jobScheduleHours} hours}}", - "components.Settings.SettingsJobsCache.jobScheduleEditFailed": "İşlem kaydedilirken beklenmedik bir hata oluştu.", - "components.Settings.SettingsJobsCache.jobs": "İşlemler", - "components.Settings.SettingsMain.generalsettingsDescription": "Seerr için genel ve varsayılan ayarları yapılandırın.", - "components.Settings.SonarrModal.validationNameRequired": "Bir sunucu ismi girmelisin", - "components.Settings.jellyfinSettingsDescription": "Talep Etğe bağlı olarak {mediaServerName} medya sunucunuz için dahili ve harici uç noktaları yapılandırabilirsiniz. Çoğu durumda, harici URL dahili URL'den farklıdır. Farklı bir parola sıfırlama sayfasına yönlendirmek istemeniz durumunda {mediaServerName} medya sunucunuz için özel bir parola sıfırlama URL'si de ayarlanabilir. Ayrıca, daha önce otomatik olarak oluşturulan Jellyfin API anahtarını da değiştirebilirsiniz..", - "components.Settings.noDefault4kServer": "Kullanıcıların 4K {mediaType} talepleri yapabilmesi için 4K {serverType} sunucusu öntanımlı olarak işaretlenmelidir.", - "components.Settings.plexsettingsDescription": "Plex sunucunuz için ayarları yapılandırın. Seerr, içerik kullanılabilirliğini belirlemek için Plex kitaplıklarınızı tarar.", + "components.Settings.SettingsAbout.Releases.releasedataMissing": "Sürüm verileri şu anda kullanılamıyor.", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorHours": "Her {jobScheduleHours, plural, one {saat} other {{jobScheduleHours} saatte bir}}", + "components.Settings.SettingsJobsCache.jobScheduleEditFailed": "Görev kaydedilirken bir sorun oluştu.", + "components.Settings.SettingsJobsCache.jobs": "Görevler", + "components.Settings.SettingsMain.generalsettingsDescription": "Seerr için global ve varsayılan ayarları yapılandırın.", + "components.Settings.SonarrModal.validationNameRequired": "Bir sunucu adı girmelisiniz", + "components.Settings.jellyfinSettingsDescription": "İsteğe bağlı olarak {mediaServerName} sunucunuz için dahili ve harici uç noktaları yapılandırın. Çoğu durumda harici URL dahili URL'den farklıdır. {mediaServerName} girişi için özel bir şifre sıfırlama URL'si de ayarlanabilir. Daha önce otomatik olarak oluşturulan Jellyfin API anahtarını da değiştirebilirsiniz.", + "components.Settings.noDefault4kServer": "Kullanıcıların 4K {mediaType} talepleri gönderebilmesi için bir 4K {serverType} sunucusu varsayılan olarak işaretlenmelidir.", + "components.Settings.plexsettingsDescription": "Plex sunucunuzun ayarlarını yapılandırın. Seerr, içerik kullanılabilirliğini belirlemek için Plex kütüphanelerinizi tarar.", "components.Settings.Notifications.NotificationsWebhook.validationWebhookUrl": "Geçerli bir URL girmelisiniz", - "components.Settings.Notifications.botUsernameTip": "Kullanıcıların botunuzla sohbet başlatmalarına ve kendi bildirimlerini yapılandırmalarına izin verin", - "components.Settings.Notifications.validationSmtpHostRequired": "Geçerli bir sunucu adresi girmelisin", - "components.Settings.SettingsUsers.localLoginTip": "Kullanıcıların e-posta adreslerini ve parolalarını kullanarak oturum açmalarına izin verin", - "components.Settings.advancedTooltip": "Bu ayarın yanlış yapılandırılması işlevselliğin bozulmasına neden olabilir", - "components.Settings.Notifications.NotificationsPushover.toastPushoverTestSending": "Pushover deneme bildirimi gönderiliyor…", - "components.Settings.Notifications.NotificationsPushover.userTokenTip": "30 karakterlik kullanıcı ya da grup kimliği", - "components.Settings.Notifications.NotificationsWebhook.toastWebhookTestFailed": "Webhook deneme bildirimi gönderilemedi.", - "components.Settings.Notifications.toastEmailTestSending": "E-mail deneme bildirimi gönderiliyor…", + "components.Settings.Notifications.botUsernameTip": "Kullanıcıların botunuzla sohbet başlatmasına ve kendi bildirimlerini yapılandırmasına izin verin", + "components.Settings.Notifications.validationSmtpHostRequired": "Geçerli bir sunucu adı veya IP adresi girmelisiniz", + "components.Settings.SettingsUsers.localLoginTip": "Kullanıcıların e-posta adresi ve şifresiyle giriş yapmasına izin ver", + "components.Settings.advancedTooltip": "Bu ayarı yanlış yapılandırmak işlevselliği bozabilir", + "components.Settings.Notifications.NotificationsPushover.toastPushoverTestSending": "Pushover test bildirimi gönderiliyor…", + "components.Settings.Notifications.NotificationsPushover.userTokenTip": "30 karakterli kullanıcı veya grup tanımlayıcınız", + "components.Settings.Notifications.NotificationsWebhook.toastWebhookTestFailed": "Webhook test bildirimi gönderilemedi.", + "components.Settings.Notifications.toastEmailTestSending": "E-posta test bildirimi gönderiliyor…", "components.Settings.SettingsMain.toastApiKeySuccess": "Yeni API anahtarı başarıyla oluşturuldu!", - "components.Settings.SettingsJobsCache.jobsDescription": "Seerr, belirli bakım görevlerini düzenli olarak zamanlanmış işler olarak gerçekleştirir, ancak bunlar aşağıda manuel olarak da tetiklenebilir. Bir işi manuel olarak çalıştırmak, zamanlamasını değiştirmez.", - "components.Settings.SettingsLogs.logsDescription": "Bu günlükleri doğrudan stdout üzerinden veya {appDataPath}/logs/seerr.log dizininde görüntüleyebilirsiniz.", - "components.Settings.SonarrModal.baseUrl": "Temel URL Adresi", - "components.Settings.SonarrModal.loadinglanguageprofiles": "Dil seçenekleri yükleniyor…", - "components.Settings.SonarrModal.servername": "Sunucu İsmi", - "components.Settings.SonarrModal.validationBaseUrlTrailingSlash": "Temel URL slash (eğik çizgi) ile bitmemelidir", - "components.Settings.manualscanDescription": "Normalde, bu yalnızca her 24 saatte bir çalıştırılır. Seerr, Plex sunucunuzun son eklenenlerini sık sık kontrol edecektir. Plex'i ilk kez yapılandırıyorsanız, tek seferlik tam manuel kütüphane taraması önerilir!", - "components.Settings.manualscanJellyfin": "Kütüphaneleri Elle Tara", - "components.Settings.menuJobs": "İşlemler & Önbellek", - "components.Settings.serviceSettingsDescription": "{serverType} sunucu(lar)ınızı aşağıdan yapılandırın. Birden fazla {serverType} sunucusuna bağlanabilirsiniz, ancak sadece iki tanesi varsayılan olarak tanımlanabilir (bir adet 4K ve bir adet 4K dışı). Yöneticiler sunucuları her daim mutlak bir biçimde yönetebilirler.", - "components.Settings.services": "Servisler", - "components.Settings.settingUpPlexDescription": "Plex'i kurmak için, ayrıntıları manuel olarak girebilir veya plex.tv adresinden alınan bir sunucuyu seçebilirsiniz. Kullanılabilir sunucuların listesini almak için açılır menünün sağındaki düğmeyi kullanın.", + "components.Settings.SettingsJobsCache.jobsDescription": "Seerr belirli bakım görevlerini düzenli aralıklarla gerçekleştirir, ancak bunlar aşağıdan manuel olarak da tetiklenebilir. Bir görevi manuel olarak çalıştırmak zamanlamasını değiştirmez.", + "components.Settings.SettingsLogs.logsDescription": "Bu günlükleri ayrıca stdout aracılığıyla veya {appDataPath}/logs/seerr.log dosyasında görüntüleyebilirsiniz.", + "components.Settings.SonarrModal.baseUrl": "URL Tabanı", + "components.Settings.SonarrModal.loadinglanguageprofiles": "Dil profilleri yükleniyor…", + "components.Settings.SonarrModal.servername": "Sunucu Adı", + "components.Settings.SonarrModal.validationBaseUrlTrailingSlash": "Temel URL eğik çizgi ile bitmemelidir", + "components.Settings.manualscanDescription": "Normalde bu işlem yalnızca 24 saatte bir çalışır. Seerr, Plex sunucunuza son eklenenler için daha sık bakacaktır. Plex'i ilk kez yapılandırıyorsanız bir defaya mahsus tam manuel kütüphane taraması önerilir!", + "components.Settings.manualscanJellyfin": "Manuel Kütüphane Taraması", + "components.Settings.menuJobs": "Görevler ve Önbellek", + "components.Settings.serviceSettingsDescription": "Aşağıda {serverType} sunucularınızı yapılandırın. Birden fazla {serverType} sunucusu bağlayabilirsiniz, ancak yalnızca ikisi varsayılan olarak işaretlenebilir (biri 4K olmayan, biri 4K). Yöneticiler, onaydan önce yeni talepleri işlemek için kullanılan sunucuyu geçersiz kılabilir.", + "components.Settings.services": "Hizmetler", + "components.Settings.settingUpPlexDescription": "Plex'i kurmak için ayrıntıları manuel olarak girebilir veya plex.tv'ten alınan bir sunucu seçebilirsiniz. Mevcut sunucuların listesini getirmek için açılır menünün sağındaki düğmeye basın.", "components.Settings.ssl": "SSL", - "components.Settings.startscan": "Taramaya Başla", - "components.Settings.syncJellyfin": "Kütüphaneleri Eşitle", + "components.Settings.startscan": "Taramayı Başlat", + "components.Settings.syncJellyfin": "Kütüphaneleri Senkronize Et", "components.Settings.tautulliApiKey": "API Anahtarı", "components.Settings.tautulliSettings": "Tautulli Ayarları", "components.Settings.timeout": "Zaman Aşımı", - "components.Settings.toastPlexConnecting": "Plex'e yeniden bağlanılıyor…", + "components.Settings.toastPlexConnecting": "Plex'e bağlanmaya çalışılıyor…", "components.Settings.toastPlexConnectingFailure": "Plex'e bağlanılamadı.", - "components.Settings.toastPlexConnectingSuccess": "Plex'e başarıyla bağlanıldı!", - "components.Settings.toastPlexRefresh": "Plex'den sunucu listesi alınıyor…", - "components.Settings.toastPlexRefreshFailure": "Plex'ten sunucu listesi alınamadı.", - "components.Settings.toastPlexRefreshSuccess": "Plex'ten sunucu listesi başarıyla alınıldı!", - "components.Settings.toastTautulliSettingsFailure": "Tautulli ayarlarınızı kaydederken beklenmedik bir hatayla karşılaşıldı.", + "components.Settings.toastPlexConnectingSuccess": "Plex bağlantısı başarıyla kuruldu!", + "components.Settings.toastPlexRefresh": "Plex'ten sunucu listesi alınıyor…", + "components.Settings.toastPlexRefreshFailure": "Plex sunucu listesi alınamadı.", + "components.Settings.toastPlexRefreshSuccess": "Plex sunucu listesi başarıyla alındı!", + "components.Settings.toastTautulliSettingsFailure": "Tautulli ayarları kaydedilirken bir sorun oluştu.", "components.Settings.toastTautulliSettingsSuccess": "Tautulli ayarları başarıyla kaydedildi!", - "components.Settings.urlBase": "Temel URL Adresi", - "components.Settings.validationHostnameRequired": "Geçerli bir sunucu adresi girmelisin", - "components.Settings.validationPortRequired": "Geçerli bir port numarası girmelisin", - "components.Settings.validationUrl": "Geçerli bir URL adresi girmelisiniz", - "components.Settings.validationUrlBaseTrailingSlash": "Temel URL slash (eğik çizgi) ile bitmemelidir", - "components.Settings.validationUrlTrailingSlash": "URL'nizin sonunda slash (eğik çizgi) olmamalıdır", - "components.Settings.webAppUrl": "Web Uygulaması Linki", + "components.Settings.urlBase": "URL Tabanı", + "components.Settings.validationHostnameRequired": "Geçerli bir sunucu adı veya IP adresi girmelisiniz", + "components.Settings.validationPortRequired": "Geçerli bir port numarası girmelisiniz", + "components.Settings.validationUrl": "Geçerli bir URL girmelisiniz", + "components.Settings.validationUrlBaseTrailingSlash": "URL tabanı eğik çizgi ile bitmemelidir", + "components.Settings.validationUrlTrailingSlash": "URL eğik çizgi ile bitmemelidir", + "components.Settings.webAppUrl": "Web Uygulaması URL", "components.Settings.webhook": "Webhook", - "components.Settings.webpush": "Web Bildirimi", - "components.Setup.configuremediaserver": "Medya Sunucusunu Düzenleyin", - "components.Setup.configureservices": "Servisleri Düzenleyin", - "components.Setup.continue": "Devam Et", - "components.Setup.finish": "Kurulumu Bitir", - "components.Setup.finishing": "Son Adımlar…", + "components.Settings.webpush": "Web Push", + "components.Setup.configuremediaserver": "Medya Sunucusunu Yapılandır", + "components.Setup.configureservices": "Hizmetleri Yapılandır", + "components.Setup.continue": "Devam", + "components.Setup.finish": "Kurulumu Tamamla", + "components.Setup.finishing": "Tamamlanıyor…", "components.Setup.setup": "Kurulum", - "components.Setup.signin": "Oturum Açın", - "components.Setup.signinMessage": "Oturum açarak devam edin", + "components.Setup.signin": "Giriş Yap", + "components.Setup.signinMessage": "Başlamak için giriş yapın", "components.Setup.signinWithPlex": "Plex bilgilerinizi girin", - "components.Setup.welcome": "Jellyseer'a Hoşgeldiniz", - "components.StatusBadge.managemedia": "{mediaType} içeriğini yönet", + "components.Setup.welcome": "Seerr'a Hoş Geldiniz", + "components.StatusBadge.managemedia": "{mediaType} Yönet", "components.StatusBadge.openinarr": "{arr}'da Aç", "components.StatusBadge.seasonepisodenumber": "S{seasonNumber}B{episodeNumber}", "components.StatusBadge.status": "{status}", "components.StatusBadge.status4k": "4K {status}", "components.StatusChecker.appUpdated": "{applicationTitle} Güncellendi", - "components.StatusChecker.reloadApp": "{applicationTitle}'ı Yenile", - "components.StatusChecker.restartRequired": "Sunucuyu Yeniden Başlatmalısınız", - "components.StatusChecker.restartRequiredDescription": "Güncellenen ayarların etkinleşmesi için sunucuyu yeniden başlatın.", + "components.StatusChecker.reloadApp": "{applicationTitle}'ı Yeniden Yükle", + "components.StatusChecker.restartRequired": "Sunucu Yeniden Başlatması Gerekiyor", + "components.StatusChecker.restartRequiredDescription": "Güncellenen ayarları uygulamak için lütfen sunucuyu yeniden başlatın.", "components.TitleCard.addToWatchList": "İzleme listesine ekle", - "components.TitleCard.cleardata": "Veriyi Temizle", + "components.TitleCard.cleardata": "Verileri Temizle", "components.TitleCard.mediaerror": "{mediaType} Bulunamadı", - "components.TitleCard.tmdbid": "TMDB ID'si", - "components.TitleCard.watchlistCancel": "{title} için olan izleme listesi iptal edildi.", + "components.TitleCard.tmdbid": "TMDB Kimliği", + "components.TitleCard.watchlistCancel": "{title} için izleme listesi iptal edildi.", "components.TitleCard.watchlistError": "Bir şeyler ters gitti. Lütfen tekrar deneyin.", "components.TitleCard.watchlistSuccess": "{title} izleme listesine başarıyla eklendi!", - "components.TvDetails.Season.noepisodes": "Bölüm listesi mevcut değil.", - "components.TvDetails.Season.somethingwentwrong": "Sezon verisi alınırken bir şeyler ters gitti.", - "components.TvDetails.TvCast.fullseriescast": "Dizinin Tüm Oyuncuları", - "components.TvDetails.TvCrew.fullseriescrew": "Dizinin Tam Ekibi", + "components.TvDetails.Season.noepisodes": "Bölüm listesi kullanılamıyor.", + "components.TvDetails.Season.somethingwentwrong": "Sezon verileri alınırken bir sorun oluştu.", + "components.TvDetails.TvCast.fullseriescast": "Tam Dizi Kadrosu", + "components.TvDetails.TvCrew.fullseriescrew": "Tam Dizi Ekibi", "components.TvDetails.anime": "Anime", "components.TvDetails.cast": "Oyuncular", - "components.TvDetails.episodeRuntime": "Bölümün Süresi", + "components.TvDetails.episodeRuntime": "Bölüm Süresi", "components.TvDetails.episodeRuntimeMinutes": "{runtime} dakika", "components.TvDetails.firstAirDate": "İlk Yayın Tarihi", - "components.TvDetails.manageseries": "Diziyi Düzenle", - "components.TvDetails.nextAirDate": "Sonraki Çıkış tarihi", - "components.TvDetails.originallanguage": "Orijinal Dili", + "components.TvDetails.manageseries": "Diziyi Yönet", + "components.TvDetails.nextAirDate": "Sonraki Yayın Tarihi", + "components.TvDetails.originallanguage": "Orijinal Dil", "components.TvDetails.originaltitle": "Orijinal Başlık", - "components.TvDetails.overview": "Özet", - "components.TvDetails.play": "{mediaServerName} ile Oynat", - "components.TvDetails.play4k": "İçeriği {mediaServerName} içinde 4K oynat", - "components.TvDetails.recommendations": "Önerilenler", + "components.TvDetails.overview": "Genel Bakış", + "components.TvDetails.play": "{mediaServerName}'da Oynat", + "components.TvDetails.play4k": "{mediaServerName}'da 4K Oynat", + "components.TvDetails.recommendations": "Öneriler", "components.TvDetails.reportissue": "Sorun Bildir", "components.TvDetails.rtaudiencescore": "Rotten Tomatoes İzleyici Puanı", "components.TvDetails.rtcriticsscore": "Rotten Tomatoes Tomatometer", "components.TvDetails.seasonnumber": "Sezon {seasonNumber}", - "components.TvDetails.network": "{networkCount} Yayın Platformu", - "components.TvDetails.seasons": "{seasonCount} # Sezon", - "components.TvDetails.tmdbuserscore": "TMBD Kullanıcı Skoru", - "components.TvDetails.viewfullcrew": "Tüm Ekibi Gör", + "components.TvDetails.network": "{networkCount, plural, one {Kanal} other {Kanallar}}", + "components.TvDetails.seasons": "{seasonCount, plural, one {# Sezon} other {# Sezon}}", + "components.TvDetails.tmdbuserscore": "TMDB Kullanıcı Puanı", + "components.TvDetails.viewfullcrew": "Tam Ekibi Görüntüle", "components.TvDetails.watchtrailer": "Fragmanı İzle", "components.UserList.accounttype": "Tür", "components.UserList.admin": "Yönetici", - "components.UserList.autogeneratepassword": "Rastgele Bir Parola Oluştur", + "components.UserList.autogeneratepassword": "Otomatik Şifre Oluştur", "components.UserList.bulkedit": "Toplu Düzenle", "components.UserList.create": "Oluştur", - "components.UserList.created": "Katılma", + "components.UserList.created": "Katıldı", "components.UserList.createlocaluser": "Yerel Kullanıcı Oluştur", "components.UserList.creating": "Oluşturuluyor…", "components.UserList.deleteuser": "Kullanıcıyı Sil", - "components.UserList.edituser": "Kullanıcının İzinlerini Düzenle", - "components.UserList.email": "E-mail Adresi", - "components.UserList.importedfromJellyfin": "{userCount} {mediaServerName} kullanıcısı başarıyla içeri aktarıldı!", - "components.UserList.importedfromplex": "{userCount} Plex kullanıcısı başarıyla içeri aktarıldı!", + "components.UserList.edituser": "Kullanıcı İzinlerini Düzenle", + "components.UserList.email": "E-posta Adresi", + "components.UserList.importedfromJellyfin": "{userCount} {mediaServerName} {userCount, plural, one {kullanıcı} other {kullanıcı}} başarıyla içe aktarıldı!", + "components.UserList.importedfromplex": "{userCount} Plex {userCount, plural, one {kullanıcı} other {kullanıcı}} başarıyla içe aktarıldı!", "components.UserList.importfromJellyfin": "{mediaServerName} Kullanıcılarını İçe Aktar", - "components.UserList.importfrommediaserver": "{mediaServerName} Kullanıcılarını İçeri Aktar", - "components.UserList.importfromplex": "Plex Kullanıcılarını İçeri Aktar", - "components.UserList.importfromplexerror": "Plex kullanıcıları içeri aktarılırken bir şeyler ters gitti.", - "components.UserList.localLoginDisabled": "Yerel Oturum Açma seçeneği şu anda etin değil.", + "components.UserList.importfrommediaserver": "{mediaServerName} Kullanıcılarını İçe Aktar", + "components.UserList.importfromplex": "Plex Kullanıcılarını İçe Aktar", + "components.UserList.importfromplexerror": "Plex kullanıcıları içe aktarılırken bir sorun oluştu.", + "components.UserList.localLoginDisabled": "Yerel Girişi Etkinleştir ayarı şu anda devre dışı.", "components.UserList.localuser": "Yerel Kullanıcı", "components.UserList.mediaServerUser": "{mediaServerName} Kullanıcısı", - "components.TitleCard.watchlistDeleted": "{title} İzleme listenizden başarıyla çıkarıldı!", + "components.TitleCard.watchlistDeleted": "{title} izleme listesinden başarıyla kaldırıldı!", "components.Settings.sonarrsettings": "Sonarr Ayarları", - "components.Settings.syncing": "Eşitleniyor", - "components.Settings.tautulliSettingsDescription": "İsteğe bağlı olarak Tautulli sunucunuz için ayarları yapılandırın. Seerr, Plex medyanız için izleme geçmişi verilerini Tautulli'den alır.", + "components.Settings.syncing": "Senkronize Ediliyor", + "components.Settings.tautulliSettingsDescription": "İsteğe bağlı olarak Tautulli sunucunuzun ayarlarını yapılandırın. Seerr, Plex medyanız için izleme geçmişi verilerini Tautulli'den çeker.", "components.Settings.validationApiKey": "Bir API anahtarı girmelisiniz", - "components.Settings.validationUrlBaseLeadingSlash": "URL'nizin başında slash (eğik çizgi) olmalıdır", - "components.Settings.webAppUrlTip": "İsteğe bağlı olarak kullanıcıları \"hosted\" web uygulaması yerine sunucunuzdaki web uygulamasına yönlendirin", + "components.Settings.validationUrlBaseLeadingSlash": "URL tabanı eğik çizgi ile başlamalıdır", + "components.Settings.webAppUrlTip": "İsteğe bağlı olarak kullanıcıları \"barındırılan\" web uygulaması yerine sunucunuzdaki web uygulamasına yönlendirin", "components.Setup.signinWithJellyfin": "Jellyfin bilgilerinizi girin", - "components.StatusBadge.playonplex": "{mediaServerName} ile Oynat", - "components.StatusChecker.appUpdatedDescription": "Lütfen aşağıda ki butona tıklayarak uygulamayı yenileyin.", - "components.TitleCard.tvdbid": "TheTVDB ID'si", + "components.StatusBadge.playonplex": "{mediaServerName}'da Oynat", + "components.StatusChecker.appUpdatedDescription": "Uygulamayı yeniden yüklemek için aşağıdaki düğmeye tıklayın.", + "components.TitleCard.tvdbid": "TheTVDB Kimliği", "components.TvDetails.similar": "Benzer Diziler", - "components.TvDetails.overviewunavailable": "Özet mevcut değil.", + "components.TvDetails.overviewunavailable": "Genel bakış mevcut değil.", "components.TvDetails.seasonstitle": "Sezonlar", - "components.UserList.autogeneratepasswordTip": "Kullanıcıya otomatik oluşturulmuş parolayı e-mail olarak gönder", - "components.TvDetails.episodeCount": "{episodeCount} # Bölüm", - "components.UserList.importfromJellyfinerror": "{mediaServerName} kullanıcıları içeri aktarılırken bir şeyler ters gitti.", - "components.TvDetails.showtype": "Dizinin Türü", - "components.TvDetails.productioncountries": "Yapımcı {countryCount, plural, one {Ülke} other {Ülkeler}}", + "components.UserList.autogeneratepasswordTip": "Sunucu tarafından oluşturulan bir şifreyi kullanıcıya e-posta ile gönder", + "components.TvDetails.episodeCount": "{episodeCount, plural, one {# Bölüm} other {# Bölüm}}", + "components.UserList.importfromJellyfinerror": "{mediaServerName} kullanıcıları içe aktarılırken bir sorun oluştu.", + "components.TvDetails.showtype": "Dizi Türü", + "components.TvDetails.productioncountries": "Yapım {countryCount, plural, one {Ülkesi} other {Ülkeleri}}", "components.TvDetails.status4k": "4K {status}", - "components.UserList.deleteconfirm": "Bu kullanıcıyı silmek istediğinizden emin misiniz? Tüm talep verisi temelli olarak silinecektir.", - "components.TvDetails.streamingproviders": "İçertiğin Erişilebilir Olduğu Platformlar", - "components.UserList.newplexsigninenabled": "Yeni Plex Kullanıcılarının Oturum Açmasına İzin Ver ayarı etkin durumdadır. Plex kullanıcılarından kütüphane erişimine sahip olanlarının oturum açabilmesi için içeri aktarılması gerekmez.", - "components.UserList.newJellyfinsigninenabled": "Yeni {mediaServerName} Kullanıcılarının Oturum Açmasına İzin Ver ayarı etkin durumdadır. {mediaServerName} kullanıcılarından kütüphane erişimine sahip olanlarının oturum açabilmesi için içeri aktarılması gerekmez.", - "components.UserList.noJellyfinuserstoimport": "İçeri aktarılacak {mediaServerName} kullanıcısı mevcut değil.", - "components.UserList.nouserstoimport": "İçeri aktarılacak Plex kullanıcısı mevcut değil.", + "components.UserList.deleteconfirm": "Bu kullanıcıyı silmek istediğinizden emin misiniz? Tüm talep verileri kalıcı olarak kaldırılacaktır.", + "components.TvDetails.streamingproviders": "Şu Anda Yayınlanan Yer", + "components.UserList.newplexsigninenabled": "Yeni Plex Girişini Etkinleştir ayarı şu anda etkin. Kütüphane erişimi olan Plex kullanıcılarının giriş yapmak için içe aktarılmasına gerek yok.", + "components.UserList.newJellyfinsigninenabled": "Yeni {mediaServerName} Girişini Etkinleştir ayarı şu anda etkin. Kütüphane erişimi olan {mediaServerName} kullanıcılarının giriş yapmak için içe aktarılmasına gerek yok.", + "components.UserList.noJellyfinuserstoimport": "İçe aktarılacak {mediaServerName} kullanıcısı yok.", + "components.UserList.nouserstoimport": "İçe aktarılacak Plex kullanıcısı yok.", "components.UserList.owner": "Sahip", "components.UserList.password": "Şifre", - "components.UserList.passwordinfodescription": "Bir uygulama URL'si ayarlayın ve otomatik şifre oluşturulabilmesi için e-mail bildirimlerini aktif edin.", + "components.UserList.passwordinfodescription": "Otomatik şifre oluşturmaya izin vermek için bir uygulama URL'si yapılandırın ve e-posta bildirimlerini etkinleştirin.", "components.UserList.plexuser": "Plex Kullanıcısı", "components.UserList.role": "Rol", "components.UserList.totalrequests": "Talepler", "components.UserList.user": "Kullanıcı", "components.UserList.usercreatedsuccess": "Kullanıcı başarıyla oluşturuldu!", "components.UserList.userdeleted": "Kullanıcı başarıyla silindi!", - "components.UserList.userdeleteerror": "Kullanıcıyı silerken bir şeyler ters gitti.", - "components.UserList.userfail": "Kullanıcının izinleri kaydedilirken bir şeyler ters gitti.", + "components.UserList.userdeleteerror": "Kullanıcı silinirken bir sorun oluştu.", + "components.UserList.userfail": "Kullanıcı izinleri kaydedilirken bir sorun oluştu.", "components.UserList.userlist": "Kullanıcı Listesi", "components.UserList.users": "Kullanıcılar", - "components.UserList.userssaved": "Kullanıcının izni başarıyla kaydedildi!", - "components.UserList.validationpasswordminchars": "Girdiğiniz şifre çok kısa, en az 8 karakterden oluşmalıdır", + "components.UserList.userssaved": "Kullanıcı izinleri başarıyla kaydedildi!", + "components.UserList.validationpasswordminchars": "Şifre çok kısa; en az 8 karakter olmalıdır", "components.UserProfile.ProfileHeader.joindate": "{joindate} tarihinde katıldı", "components.UserProfile.ProfileHeader.profile": "Profili Görüntüle", "components.UserProfile.ProfileHeader.settings": "Ayarları Düzenle", "components.UserProfile.ProfileHeader.userid": "Kullanıcı Kimliği: {userid}", - "components.UserProfile.UserSettings.UserGeneralSettings.accounttype": "Hesabın Türü", + "components.UserProfile.UserSettings.UserGeneralSettings.accounttype": "Hesap Türü", "components.UserProfile.UserSettings.UserGeneralSettings.admin": "Yönetici", - "components.UserProfile.UserSettings.UserGeneralSettings.discordId": "Discord Kullanıcı Kimliği", - "components.UserProfile.UserSettings.UserGeneralSettings.displayName": "Görünen İsmi", - "components.UserProfile.UserSettings.UserGeneralSettings.email": "E-mail", - "components.UserProfile.UserSettings.UserGeneralSettings.enableOverride": "Genel Limiti Geçersiz Kıl", + "components.UserProfile.UserSettings.UserGeneralSettings.displayName": "Görünen Ad", + "components.UserProfile.UserSettings.UserGeneralSettings.email": "E-posta", + "components.UserProfile.UserSettings.UserGeneralSettings.enableOverride": "Global Limiti Geçersiz Kıl", "components.UserProfile.UserSettings.UserGeneralSettings.general": "Genel", "components.UserProfile.UserSettings.UserGeneralSettings.generalsettings": "Genel Ayarlar", "components.UserProfile.UserSettings.UserGeneralSettings.languageDefault": "Varsayılan ({language})", "components.UserProfile.UserSettings.UserGeneralSettings.localuser": "Yerel Kullanıcı", "components.UserProfile.UserSettings.UserGeneralSettings.mediaServerUser": "{mediaServerName} Kullanıcısı", - "components.UserProfile.UserSettings.UserGeneralSettings.movierequestlimit": "Film Talep Etme Limiti", + "components.UserProfile.UserSettings.UserGeneralSettings.movierequestlimit": "Film Talep Limiti", "components.UserProfile.UserSettings.UserGeneralSettings.originallanguage": "Keşfet Dili", - "components.UserProfile.UserSettings.UserGeneralSettings.originallanguageTip": "İçerikleri orijinal dillerine göre filtrele", + "components.UserProfile.UserSettings.UserGeneralSettings.originallanguageTip": "İçeriği orijinal dile göre filtrele", "components.UserProfile.UserSettings.UserGeneralSettings.owner": "Sahip", "components.UserProfile.UserSettings.UserGeneralSettings.plexuser": "Plex Kullanıcısı", - "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmovies": "Filmleri Otomatize Talep Ett", - "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseries": "Dizileri Otomatize Talep Ett", - "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseriestip": "Plex İzleme Listenizde'ki dizileri otomatik olarak istet", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmovies": "Filmleri Otomatik Talep Et", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseries": "Dizileri Otomatik Talep Et", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseriestip": "Plex İzleme Listenizdeki dizileri otomatik olarak talep et", "components.UserProfile.UserSettings.UserGeneralSettings.region": "Keşfet Bölgesi", - "components.UserProfile.UserSettings.UserGeneralSettings.regionTip": "İçerikleri bölgesel erişilebilirliklerine göre filtrele", + "components.UserProfile.UserSettings.UserGeneralSettings.regionTip": "İçeriği bölgesel kullanılabilirliğe göre filtrele", "components.UserProfile.UserSettings.UserGeneralSettings.save": "Değişiklikleri Kaydet", - "components.UserProfile.UserSettings.UserGeneralSettings.seriesrequestlimit": "Dizi Talep Etme Limiti", - "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailure": "Ayarlar kaydedilirken beklenmedik bir hata oluştu.", + "components.UserProfile.UserSettings.UserGeneralSettings.seriesrequestlimit": "Dizi Talep Limiti", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailure": "Ayarlar kaydedilirken bir sorun oluştu.", "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsSuccess": "Ayarlar başarıyla kaydedildi!", "components.UserProfile.UserSettings.UserGeneralSettings.user": "Kullanıcı", "components.UserProfile.UserSettings.UserNotificationSettings.deviceDefault": "Cihaz Varsayılanı", - "components.UserProfile.UserSettings.UserNotificationSettings.discordId": "Kullanıcı Kimliği", + "components.UserProfile.UserSettings.UserNotificationSettings.discordId": "Kullanıcı Kimlikleri", "components.UserProfile.UserSettings.UserNotificationSettings.discordsettingsfailed": "Discord bildirim ayarları kaydedilemedi.", - "components.UserProfile.UserSettings.UserNotificationSettings.discordsettingssaved": "Discord bildirim ayarları kaydedildi!", - "components.UserProfile.UserSettings.UserNotificationSettings.email": "E-mail", - "components.UserProfile.UserSettings.UserNotificationSettings.emailsettingsfailed": "E-mail bildirim ayarları kaydedilemedi.", - "components.UserProfile.UserSettings.UserNotificationSettings.emailsettingssaved": "E-mail bildirim ayarları kaydedildi!", + "components.UserProfile.UserSettings.UserNotificationSettings.discordsettingssaved": "Discord bildirim ayarları başarıyla kaydedildi!", + "components.UserProfile.UserSettings.UserNotificationSettings.email": "E-posta", + "components.UserProfile.UserSettings.UserNotificationSettings.emailsettingsfailed": "E-posta bildirim ayarları kaydedilemedi.", + "components.UserProfile.UserSettings.UserNotificationSettings.emailsettingssaved": "E-posta bildirim ayarları başarıyla kaydedildi!", "components.UserProfile.UserSettings.UserNotificationSettings.notifications": "Bildirimler", "components.UserProfile.UserSettings.UserNotificationSettings.notificationsettings": "Bildirim Ayarları", - "components.UserProfile.UserSettings.UserNotificationSettings.pgpPublicKey": "Açık (Public) PGP Anahtarı", - "components.UserProfile.UserSettings.UserNotificationSettings.pgpPublicKeyTip": "E-mail mesajlarını OpenPGP kullanarak şifrele", - "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessToken": "Erişim Token'i", - "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessTokenTip": "Hesap Ayarlarınızdan bir token oluşturun", + "components.UserProfile.UserSettings.UserNotificationSettings.pgpPublicKey": "PGP Genel Anahtarı", + "components.UserProfile.UserSettings.UserNotificationSettings.pgpPublicKeyTip": "OpenPGP kullanarak e-posta mesajlarını şifrele", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessToken": "Erişim Tokeni", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessTokenTip": "Hesap Ayarları'ndan bir token oluşturun", "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletsettingsfailed": "Pushbullet bildirim ayarları kaydedilemedi.", - "components.UserProfile.UserSettings.UserNotificationSettings.pushoverApplicationToken": "Uygulama API'sinin Token'i", - "components.UserProfile.UserSettings.UserNotificationSettings.pushoverApplicationTokenTip": "{applicationTitle} ile kullanabilmeniz için bir uygulama oluşturun", - "components.UserProfile.UserSettings.UserNotificationSettings.pushoverUserKey": "Kullanıcı ya da Grup Kimliği", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverApplicationToken": "Uygulama API Tokeni", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverApplicationTokenTip": "{applicationTitle} ile kullanmak için bir uygulama kaydedin", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverUserKey": "Kullanıcı veya Grup Anahtarı", "components.UserProfile.UserSettings.UserNotificationSettings.pushoversettingsfailed": "Pushover bildirim ayarları kaydedilemedi.", - "components.UserProfile.UserSettings.UserNotificationSettings.pushoversettingssaved": "Pushover bildirim ayarları kaydedildi!", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoversettingssaved": "Pushover bildirim ayarları başarıyla kaydedildi!", "components.UserProfile.UserSettings.UserNotificationSettings.sendSilently": "Sessizce Gönder", - "components.UserProfile.UserSettings.UserNotificationSettings.sendSilentlyDescription": "Bildirimler sessiz gönderilir", + "components.UserProfile.UserSettings.UserNotificationSettings.sendSilentlyDescription": "Bildirimleri ses olmadan gönder", "components.UserProfile.UserSettings.UserNotificationSettings.sound": "Bildirim Sesi", - "components.UserProfile.UserSettings.UserNotificationSettings.telegramChatId": "Sohbet ID'si", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramChatId": "Sohbet Kimliği", "components.UserProfile.UserSettings.menuNotifications": "Bildirimler", "components.UserProfile.UserSettings.UserNotificationSettings.telegramsettingsfailed": "Telegram bildirim ayarları kaydedilemedi.", - "components.UserProfile.UserSettings.UserNotificationSettings.telegramsettingssaved": "Telegram bildirim ayarları kaydedildi!", - "components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "Geçerli bir kullanıcı kimliği sağlamalısınız", - "components.UserProfile.UserSettings.UserNotificationSettings.validationPgpPublicKey": "Geçerli bir PGP açık (public) anahtarı sağlamalısın", - "components.UserProfile.UserSettings.UserNotificationSettings.validationPushbulletAccessToken": "Bir erişim token'i sağlamalısınız", - "components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverUserKey": "Geçerli bir kullanıcı ya da grup kimliği sağlamalısınız", - "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramChatId": "Geçerli bir sohbet ID'si sağlamalısın", - "components.UserProfile.UserSettings.UserNotificationSettings.webpush": "Web Bildirimi", - "components.UserProfile.UserSettings.UserPasswordChange.confirmpassword": "Şifreyi Doğrula", - "components.UserProfile.UserSettings.UserPasswordChange.currentpassword": "Kullandığınız Şifre", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramsettingssaved": "Telegram bildirim ayarları başarıyla kaydedildi!", + "components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "Girilen her kimlik, geçerli bir Discord kullanıcı kimliği olmalıdır", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPgpPublicKey": "Geçerli bir PGP genel anahtarı girmelisiniz", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPushbulletAccessToken": "Bir erişim tokeni girmelisiniz", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverUserKey": "Geçerli bir kullanıcı veya grup anahtarı girmelisiniz", + "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramChatId": "Geçerli bir sohbet kimliği girmelisiniz", + "components.UserProfile.UserSettings.UserNotificationSettings.webpush": "Web Push", + "components.UserProfile.UserSettings.UserPasswordChange.confirmpassword": "Şifreyi Onayla", + "components.UserProfile.UserSettings.UserPasswordChange.currentpassword": "Mevcut Şifre", "components.UserProfile.UserSettings.UserPasswordChange.newpassword": "Yeni Şifre", - "components.UserProfile.UserSettings.UserPasswordChange.noPasswordSetOwnAccount": "Hesabın ayarlanmış bir şifreye sahip değil. Aşağıdan e-mail adresinizi kullanarak bir şifre belirlerseniz \"yerel kullanıcı\" olarak oturum açabilirsiniz.", - "components.UserProfile.UserSettings.UserPasswordChange.nopermissionDescription": "Bu kullanıcının şifresini değiştirme yetkisine sahip değilsin.", + "components.UserProfile.UserSettings.UserPasswordChange.noPasswordSetOwnAccount": "Hesabınızın şu an belirlenmemiş bir şifresi var. E-posta adresinizle \"yerel kullanıcı\" olarak giriş yapabilmek için aşağıda bir şifre belirleyin.", + "components.UserProfile.UserSettings.UserPasswordChange.nopermissionDescription": "Bu kullanıcının şifresini değiştirme izniniz yok.", "components.UserProfile.UserSettings.UserPasswordChange.password": "Şifre", - "components.UserProfile.UserSettings.UserPasswordChange.toastSettingsFailureVerifyCurrent": "Şifre kaydedilirken bir hata oluştu, şu anda kullandığınız şifreyi doğru girdiğinizden emin olun.", + "components.UserProfile.UserSettings.UserPasswordChange.toastSettingsFailureVerifyCurrent": "Şifre kaydedilirken bir sorun oluştu. Mevcut şifrenizi doğru girdiniz mi?", "components.UserProfile.UserSettings.UserPasswordChange.toastSettingsSuccess": "Şifre başarıyla kaydedildi!", - "components.UserProfile.UserSettings.UserPasswordChange.validationConfirmPassword": "Yeni şifreyi doğrulamanız gerekmektedir", - "components.UserProfile.UserSettings.UserPasswordChange.validationConfirmPasswordSame": "Girdiğiniz şifreler uyuşmuyor", - "components.UserProfile.UserSettings.UserPasswordChange.validationCurrentPassword": "Mevcut olarak kullandığınız şifrenizi girmediniz", - "components.UserProfile.UserSettings.UserPasswordChange.validationNewPassword": "Yeni bir şifre girmediniz", + "components.UserProfile.UserSettings.UserPasswordChange.validationConfirmPassword": "Yeni şifreyi onaylamalısınız", + "components.UserProfile.UserSettings.UserPasswordChange.validationConfirmPasswordSame": "Şifreler eşleşmelidir", + "components.UserProfile.UserSettings.UserPasswordChange.validationCurrentPassword": "Mevcut şifrenizi girmelisiniz", + "components.UserProfile.UserSettings.UserPasswordChange.validationNewPassword": "Yeni bir şifre girmelisiniz", "components.UserProfile.UserSettings.UserPermissions.permissions": "İzinler", - "components.UserProfile.UserSettings.UserPermissions.toastSettingsFailure": "Ayarlar kaydedilirken beklenmedik bir hata oluştu.", + "components.UserProfile.UserSettings.UserPermissions.toastSettingsFailure": "Ayarlar kaydedilirken bir sorun oluştu.", "components.UserProfile.UserSettings.UserPermissions.toastSettingsSuccess": "İzinler başarıyla kaydedildi!", - "components.UserProfile.UserSettings.UserPermissions.unauthorizedDescription": "Kendi izinlerini düzenleyemezsin.", + "components.UserProfile.UserSettings.UserPermissions.unauthorizedDescription": "Kendi izinlerinizi değiştiremezsiniz.", "components.UserProfile.UserSettings.menuChangePass": "Şifre", "components.UserProfile.UserSettings.menuGeneralSettings": "Genel", - "components.UserProfile.UserSettings.unauthorizedDescription": "Bu kullanıcının ayarlarını düzenleme yetkin yok.", - "components.UserProfile.limit": "{limit} limitinizden {remaining} adet kaldı", - "components.UserProfile.localWatchlist": "{username} kullanıcısının İzleme Listesi", + "components.UserProfile.UserSettings.unauthorizedDescription": "Bu kullanıcının ayarlarını değiştirme izniniz yok.", + "components.UserProfile.limit": "{limit} tanesinden {remaining} kaldı", + "components.UserProfile.localWatchlist": "{username}'in İzleme Listesi", "components.UserProfile.movierequests": "Film Talepleri", - "components.UserProfile.pastdays": "{type} (Yenilenmesine {days} gün kaldı)", - "components.UserProfile.plexwatchlist": "Plex İzleme Listen", - "components.UserProfile.recentlywatched": "Yakın Zamanda İzlendi", - "components.UserProfile.recentrequests": "Yakın Zamandaki Talepler", - "components.UserProfile.requestsperdays": "{limit}ı", + "components.UserProfile.pastdays": "{type} (son {days} gün)", + "components.UserProfile.plexwatchlist": "Plex İzleme Listesi", + "components.UserProfile.recentlywatched": "Son İzlenenler", + "components.UserProfile.recentrequests": "Son Talepler", + "components.UserProfile.requestsperdays": "{limit} kaldı", "components.UserProfile.seriesrequest": "Dizi Talepleri", - "components.UserProfile.totalrequests": "Toplam Talep", + "components.UserProfile.totalrequests": "Toplam Talepler", "components.UserProfile.unlimited": "Sınırsız", "i18n.advanced": "Gelişmiş", - "i18n.all": "Hepsi", - "i18n.approve": "Kabul Et", + "i18n.all": "Tümü", + "i18n.approve": "Onayla", "i18n.areyousure": "Emin misiniz?", "i18n.available": "Kullanılabilir", "i18n.back": "Geri", - "i18n.cancel": "İptal Et", - "i18n.canceling": "İptal Ediliyor…", + "i18n.cancel": "İptal", + "i18n.canceling": "İptal ediliyor…", "i18n.close": "Kapat", "i18n.collection": "Koleksiyon", "i18n.decline": "Reddet", @@ -1205,233 +1204,469 @@ "i18n.experimental": "Deneysel", "i18n.failed": "Başarısız", "i18n.import": "İçe Aktar", - "i18n.importing": "İçe Aktarılıyor…", + "i18n.importing": "İçe aktarılıyor…", "i18n.loading": "Yükleniyor…", "i18n.movie": "Film", "i18n.movies": "Filmler", "i18n.next": "Sonraki", - "i18n.noresults": "Sonuç Bulunamadı.", - "i18n.open": "Açık", - "i18n.partiallyavailable": "Kısmen Mevcut", + "i18n.noresults": "Sonuç bulunamadı.", + "i18n.open": "Aç", + "i18n.partiallyavailable": "Kısmen Kullanılabilir", "i18n.pending": "Beklemede", "i18n.previous": "Önceki", "i18n.processing": "İşleniyor", "i18n.request": "Talep Et", - "i18n.request4k": "4K Talep Et", + "i18n.request4k": "4K Olarak Talep Et", "i18n.requested": "Talep Edildi", - "i18n.requesting": "Talep Ediliyor…", + "i18n.requesting": "Talep ediliyor…", "i18n.resolved": "Çözüldü", - "i18n.resultsperpage": "Sayfa'da {pageSize} sonuç gösteriliyor", - "i18n.retry": "Yeniden Dene", - "i18n.retrying": "Yeniden Deneniyor…", + "i18n.resultsperpage": "Sayfa başına {pageSize} sonuç göster", + "i18n.retry": "Tekrar Dene", + "i18n.retrying": "Tekrar deneniyor…", "i18n.save": "Değişiklikleri Kaydet", "i18n.saving": "Kaydediliyor…", "i18n.settings": "Ayarlar", - "i18n.showingresults": "{from} ile {to} arasından toplam {total} sonuç var", + "i18n.showingresults": "{total} sonuçtan {from} ile {to} arası gösteriliyor", "i18n.status": "Durum", - "i18n.test": "Test Et", - "i18n.testing": "Test Ediliyor…", + "i18n.test": "Test", + "i18n.testing": "Test ediliyor…", "i18n.tvshow": "Dizi", "i18n.tvshows": "Diziler", - "i18n.unavailable": "Mevcut Değil", + "i18n.unavailable": "Kullanılamıyor", "i18n.usersettings": "Kullanıcı Ayarları", "i18n.view": "Görüntüle", "pages.errormessagewithcode": "{statusCode} - {error}", - "pages.internalservererror": "Dahili Sunucu Sorunsı", - "pages.oops": "Eyvah", + "pages.internalservererror": "Dahili Sunucu Hatası", + "pages.oops": "Hata", "pages.pagenotfound": "Sayfa Bulunamadı", - "pages.returnHome": "Anasayfaya Dön", - "pages.serviceunavailable": "Hizmet Erişilebilir Değil", + "pages.returnHome": "Ana Sayfaya Dön", + "pages.serviceunavailable": "Hizmet Kullanılamıyor", "pages.somethingwentwrong": "Bir Şeyler Ters Gitti", - "components.UserProfile.UserSettings.UserGeneralSettings.validationDiscordId": "Geçerli bir Discord ID'si sunmalısın", - "components.UserProfile.UserSettings.UserPasswordChange.toastSettingsFailure": "Şifre kaydedilirken bir şeyler ters gitti.", + "components.UserProfile.UserSettings.UserPasswordChange.toastSettingsFailure": "Şifre kaydedilirken bir sorun oluştu.", "components.UserProfile.UserSettings.menuPermissions": "İzinler", - "i18n.approved": "Kabul Edildi", + "i18n.approved": "Onaylandı", "i18n.deleting": "Siliniyor…", "i18n.notrequested": "Talep Edilmedi", - "components.UserList.usercreatedfailed": "Kullanıcı oluşturulurken bir şeyler ters gitti.", - "components.UserList.usercreatedfailedexisting": "Verilen e-mail adresi halihazırda başka bir kulllanıcı tarafından kullanılmaktadır.", - "components.UserList.validationEmail": "E-posta gerekli", - "components.UserProfile.UserSettings.UserGeneralSettings.applanguage": "Uygulama Dili", - "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmoviestip": "Plex İzleme Listenizde'ki filmleri otomatik olarak talep Et", - "components.UserProfile.UserSettings.UserGeneralSettings.role": "Rolü", + "components.UserList.usercreatedfailed": "Kullanıcı oluşturulurken bir sorun oluştu.", + "components.UserList.usercreatedfailedexisting": "Girilen e-posta adresi başka bir kullanıcı tarafından kullanılıyor.", + "components.UserList.validationEmail": "E-posta gereklidir", + "components.UserProfile.UserSettings.UserGeneralSettings.applanguage": "Görüntüleme Dili", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmoviestip": "Plex İzleme Listenizdeki filmleri otomatik olarak talep et", + "components.UserProfile.UserSettings.UserGeneralSettings.role": "Rol", "components.UserProfile.UserSettings.UserGeneralSettings.saving": "Kaydediliyor…", - "components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "Hesabınız ile bağıntılı numaralardan oluşan kullanıcı ID'niz", - "components.UserProfile.UserSettings.UserNotificationSettings.pushoverUserKeyTip": "30 karakterlik kullanıcı ya da grup kimliği", - "components.UserProfile.emptywatchlist": "Plex İzleme Listenize eklenen içerikler burada gözükeceklerdir.", - "i18n.restartRequired": "Yeniden Başlatma Gereklidir", - "components.UserProfile.UserSettings.UserGeneralSettings.discordIdTip": "Discord kullanıcı hesabınızla ilişkili çok haneli kimlik numarası", - "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletsettingssaved": "Pushbullet bildirim ayarları kaydedildi!", - "components.UserProfile.UserSettings.UserNotificationSettings.telegramChatIdTipLong": "Bir sohbet başlatın ve @get_id_bot ID'li botunuzu ekleyin. Son olarak /my_id komutunu kullanın", - "components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverApplicationToken": "Geçerli bir uygulama token'i sağlamalısınız", - "components.UserProfile.UserSettings.UserPasswordChange.noPasswordSet": "Bu kullanıcının henüz ayarlanmış bir şifresi yok. Aşağıdan bir şifre ayarlayarak bu hesabın \"yerel kullanıcı\" olarak oturum açabilmesini sağlayın.", - "components.UserProfile.UserSettings.UserPasswordChange.validationNewPasswordLength": "Girdiğiniz şifre çok kısa, en az 8 karakterden oluşmalıdır", - "components.MovieDetails.addtowatchlist": "İzleme Listeme Ekle", - "components.MovieDetails.watchlistDeleted": "{title} İzleme listesinden başarıyla kaldırıldı!", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "Hesabınızla ilişkilendirilmiş çok haneli Discord kullanıcı kimlik numarasıdır. Aynı hanede birden fazla hesap kullanılıyorsa, birden fazla Discord kullanıcı kimliği ekleyebilirsiniz.", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverUserKeyTip": "30 karakterli kullanıcı veya grup tanımlayıcınız", + "components.UserProfile.emptywatchlist": "Plex İzleme Listesi'ne eklenen içerikler burada görünecek.", + "i18n.restartRequired": "Yeniden Başlatma Gerekiyor", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletsettingssaved": "Pushbullet bildirim ayarları başarıyla kaydedildi!", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramChatIdTipLong": "Bir sohbet başlatın, @get_id_bot'u ekleyin ve /my_id komutunu kullanın", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverApplicationToken": "Geçerli bir uygulama tokeni girmelisiniz", + "components.UserProfile.UserSettings.UserPasswordChange.noPasswordSet": "Bu kullanıcı hesabının şu an belirlenmemiş bir şifresi var. Bu hesabın \"yerel kullanıcı\" olarak giriş yapabilmesi için aşağıda bir şifre belirleyin.", + "components.UserProfile.UserSettings.UserPasswordChange.validationNewPasswordLength": "Şifre çok kısa; en az 8 karakter olmalıdır", + "components.MovieDetails.addtowatchlist": "İzleme Listesine Ekle", + "components.MovieDetails.watchlistDeleted": "{title} izleme listesinden başarıyla kaldırıldı!", "components.TvDetails.watchlistSuccess": "{title} izleme listesine başarıyla eklendi!", "components.MovieDetails.removefromwatchlist": "İzleme Listesinden Kaldır", - "components.TvDetails.addtowatchlist": "İzleme Listeme Ekle", - "components.UserProfile.UserSettings.UserGeneralSettings.validationemailformat": "Geçerli bir e-posta gerekli", + "components.TvDetails.addtowatchlist": "İzleme Listesine Ekle", + "components.UserProfile.UserSettings.UserGeneralSettings.validationemailformat": "Geçerli bir e-posta gereklidir", "components.RequestList.RequestItem.profileName": "Profil", "components.TvDetails.removefromwatchlist": "İzleme Listesinden Kaldır", - "components.Login.adminerror": "Oturum açmak için bir yönetici hesabı kullanmalısınız.", + "components.Login.adminerror": "Giriş yapmak için bir yönetici hesabı kullanmalısınız.", "components.Login.enablessl": "SSL Kullan", "components.Login.hostname": "{mediaServerName} URL", "components.Login.invalidurlerror": "{mediaServerName} sunucusuna bağlanılamıyor.", "components.Login.port": "Port", "components.Login.urlBase": "URL Tabanı", - "components.Login.validationPortRequired": "Geçerli bir port numarası sağlamalısınız", - "components.Login.validationUrlBaseLeadingSlash": "URL tabanının başında eğik çizgi olmalıdır", - "components.Login.validationUrlBaseTrailingSlash": "URL tabanı eğik çizgiyle bitmemelidir", - "components.Login.validationUrlTrailingSlash": "URL, eğik çizgiyle bitmemelidir", + "components.Login.validationPortRequired": "Geçerli bir port numarası girmelisiniz", + "components.Login.validationUrlBaseLeadingSlash": "URL tabanı eğik çizgi ile başlamalıdır", + "components.Login.validationUrlBaseTrailingSlash": "URL tabanı eğik çizgi ile bitmemelidir", + "components.Login.validationUrlTrailingSlash": "URL eğik çizgi ile bitmemelidir", "components.MovieDetails.watchlistError": "Bir şeyler ters gitti. Lütfen tekrar deneyin.", "components.MovieDetails.watchlistSuccess": "{title} izleme listesine başarıyla eklendi!", "components.Settings.invalidurlerror": "{mediaServerName} sunucusuna bağlanılamıyor.", - "components.Settings.jellyfinForgotPasswordUrl": "Parola Sıfırlama URL'si", - "components.Settings.jellyfinSyncFailedAutomaticGroupedFolders": "Otomatik Kitaplık Gruplandırması ile özel kimlik doğrulaması desteklenmiyor", - "components.Settings.jellyfinSyncFailedGenericError": "Kütüphaneler senkronize edilirken bir hata oluştu", - "components.Settings.jellyfinSyncFailedNoLibrariesFound": "Hiç kütüphane bulunamadı", - "components.TvDetails.watchlistDeleted": "{title} İzleme listesinden başarıyla kaldırıldı!", + "components.Settings.jellyfinForgotPasswordUrl": "Şifremi Unuttum URL", + "components.Settings.jellyfinSyncFailedAutomaticGroupedFolders": "Otomatik Kütüphane Gruplama ile özel kimlik doğrulama desteklenmiyor", + "components.Settings.jellyfinSyncFailedGenericError": "Kütüphaneler senkronize edilirken bir sorun oluştu", + "components.Settings.jellyfinSyncFailedNoLibrariesFound": "Hiçbir kütüphane bulunamadı", + "components.TvDetails.watchlistDeleted": "{title} izleme listesinden başarıyla kaldırıldı!", "components.TvDetails.watchlistError": "Bir şeyler ters gitti. Lütfen tekrar deneyin.", - "components.UserList.username": "Kullanıcı adı", - "components.UserList.validationUsername": "Kullanıcı adınızı sağlamalısınız", - "components.UserProfile.UserSettings.UserGeneralSettings.validationemailrequired": "E-posta gerekli", - "components.Login.back": "Geri git", + "components.UserList.username": "Kullanıcı Adı", + "components.UserList.validationUsername": "Bir kullanıcı adı girmelisiniz", + "components.UserProfile.UserSettings.UserGeneralSettings.validationemailrequired": "E-posta gereklidir", + "components.Login.back": "Geri dön", "components.Login.servertype": "Sunucu Türü", - "components.Setup.configemby": "Emby İle Devam Et", + "components.Setup.configemby": "Emby'yi Yapılandır", "components.Login.validationservertyperequired": "Lütfen bir sunucu türü seçin", - "components.Selector.canceled": "İptal Edilen", - "components.Selector.returningSeries": "Geri Dönecek Diziler", - "components.Setup.back": "Geri Dön", - "components.Selector.ended": "Biten", - "components.Selector.inProduction": "Çekimde", + "components.Selector.canceled": "İptal Edildi", + "components.Selector.returningSeries": "Devam Eden Dizi", + "components.Setup.back": "Geri dön", + "components.Selector.ended": "Sona Erdi", + "components.Selector.inProduction": "Yapımda", "components.Selector.pilot": "Pilot", - "components.Selector.planned": "Planlanan", - "components.Selector.searchStatus": "Durum seçin...", - "components.Setup.configjellyfin": "Jellyfin İle Devam Et", - "components.Setup.configplex": "Plex İle Devam Et", - "components.Setup.servertype": "Sunucu Türünü Seçin", + "components.Selector.planned": "Planlandı", + "components.Selector.searchStatus": "Durum seç...", + "components.Setup.configjellyfin": "Jellyfin'i Yapılandır", + "components.Setup.configplex": "Plex'i Yapılandır", + "components.Setup.servertype": "Sunucu Türü Seçin", "components.Setup.signinWithEmby": "Emby bilgilerinizi girin", "components.Setup.subtitle": "Medya sunucunuzu seçerek başlayın", "components.StatusBadge.seasonnumber": "S{seasonNumber}", "components.Discover.FilterSlideover.status": "Durum", "components.Settings.Notifications.webhookRoleId": "Bildirim Rol Kimliği", - "components.Settings.SettingsJobsCache.plex-refresh-token": "Plex Token'ını Yenile", + "components.Settings.SettingsJobsCache.plex-refresh-token": "Plex Token Yenileme", "components.Settings.SettingsMain.discoverRegion": "Keşfet Bölgesi", "components.Settings.SettingsMain.discoverRegionTip": "İçeriği bölgesel kullanılabilirliğe göre filtrele", - "components.Settings.SettingsMain.streamingRegion": "Dijital Yayın Bölgesi", - "components.Settings.SettingsMain.streamingRegionTip": "Bölgesel kullanılabilirliğe göre dijital yayınları göster", + "components.Settings.SettingsMain.streamingRegion": "Yayın Bölgesi", + "components.Settings.SettingsMain.streamingRegionTip": "Yayın sitelerini bölgesel kullanılabilirliğe göre göster", "components.Settings.apiKey": "API anahtarı", "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegion": "Keşfet Bölgesi", - "components.UserProfile.UserSettings.UserGeneralSettings.streamingRegion": "Dijital Yayın Bölgesi", - "components.UserProfile.UserSettings.UserGeneralSettings.streamingRegionTip": "Bölgesel kullanılabilirliğe göre dijital yayınları göster", - "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmailEmpty": "Başka bir kullanıcı zaten bu kullanıcı adına sahip. Bir e-posta ayarlamalısınız", + "components.UserProfile.UserSettings.UserGeneralSettings.streamingRegion": "Yayın Bölgesi", + "components.UserProfile.UserSettings.UserGeneralSettings.streamingRegionTip": "Yayın sitelerini bölgesel kullanılabilirliğe göre göster", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmailEmpty": "Başka bir kullanıcı bu kullanıcı adını kullanıyor. Bir e-posta belirlemelisiniz", "i18n.specials": "Özel Bölümler", "components.Settings.Notifications.webhookRoleIdTip": "Webhook mesajında bahsedilecek rol kimliği. Bahsetmeleri devre dışı bırakmak için boş bırakın", - "components.RequestList.RequestItem.removearr": "{arr}'dan kaldır", - "components.Settings.SettingsJobsCache.usersavatars": "Kullanıcıların Avatarları", - "components.Settings.scanbackground": "Tarama arka planda devam edecek. Kurulum sürecine devam edebilirsiniz.", + "components.RequestList.RequestItem.removearr": "{arr}'dan Kaldır", + "components.Settings.SettingsJobsCache.usersavatars": "Kullanıcı Avatarları", + "components.Settings.scanbackground": "Tarama arka planda çalışacaktır. Bu arada kurulum sürecine devam edebilirsiniz.", "components.Settings.tip": "İpucu", "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmail": "Bu e-posta zaten alınmış!", "components.RequestList.sortDirection": "Sıralama Yönünü Değiştir", - "components.Settings.Notifications.validationWebhookRoleId": "Geçerli bir Discord Rol Kimliği sağlamalısınız", + "components.Settings.Notifications.validationWebhookRoleId": "Geçerli bir Discord Rol Kimliği girmelisiniz", "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegionTip": "İçeriği bölgesel kullanılabilirliğe göre filtrele", "components.Settings.SettingsNetwork.toastSettingsFailure": "Ayarlar kaydedilirken bir sorun oluştu.", - "components.Settings.OverrideRuleModal.conditionsDescription": "Parametre değişikliklerini uygulamadan önce koşulları belirtir. Kuralların uygulanması için her alan doğrulanmalıdır (VE işlem). Bir alan, özelliklerinden herhangi biri eşleşirse doğrulanmış kabul edilir (VEYA işlem).", - "components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorUnauthorized": "Kullanılan kimlik bilgileriniz ile Plex'e bağlanılamıyor", - "components.Settings.Notifications.messageThreadIdTip": "Grup sohbetinizde konular etkinleştirilmişse, burada bir konu başlığı/konu kimliği belirtebilirsiniz", - "components.Settings.SettingsNetwork.trustProxyTip": "Seerr'in proxy arkasındaki istemci IP adreslerini doğru şekilde kaydetmesine izin ver", - "components.Selector.searchUsers": "Kullanıcıları seçin…", - "components.UserProfile.UserSettings.UserLinkedAccountsSettings.linkedAccountsHint": "Bu harici hesaplar {applicationName} hesabınıza bağlıdır.", + "components.Settings.OverrideRuleModal.conditionsDescription": "Parametre değişikliklerini uygulamadan önce koşulları belirler. Kuralların uygulanabilmesi için her alan doğrulanmalıdır (VE işlemi). Özelliklerinden herhangi biri eşleşirse bir alan doğrulanmış sayılır (VEYA işlemi).", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorUnauthorized": "Kimlik bilgilerinizle Plex'e bağlanılamıyor", + "components.Settings.Notifications.messageThreadIdTip": "Grup sohbetinizde konular etkinleştirilmişse burada bir iş parçacığı/konu kimliği belirtebilirsiniz", + "components.Settings.SettingsNetwork.trustProxyTip": "Seerr'ın bir proxy arkasındaki istemci IP adreslerini doğru şekilde kaydetmesine izin ver", + "components.Selector.searchUsers": "Kullanıcı seç…", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.linkedAccountsHint": "Bu harici hesaplar {applicationName} hesabınıza bağlı.", "components.DiscoverTvUpcoming.upcomingtv": "Yaklaşan Diziler", - "components.Login.loginwithapp": "{appName} ile giriş yapın", - "components.Login.noadminerror": "Sunucuda yönetici kullanıcısı bulunamadı.", - "components.Login.orsigninwith": "veya giriş yapın", - "components.Settings.Notifications.messageThreadId": "Konu/Konu Kimliği", - "components.Settings.Notifications.validationMessageThreadId": "Konu/konu kimliği pozitif bir tam sayı olmalıdır", + "components.Login.loginwithapp": "{appName} ile Giriş Yap", + "components.Login.noadminerror": "Sunucuda yönetici kullanıcı bulunamadı.", + "components.Login.orsigninwith": "Ya da şununla giriş yap", + "components.Settings.Notifications.messageThreadId": "İş Parçacığı/Konu Kimliği", + "components.Settings.Notifications.validationMessageThreadId": "İş parçacığı/konu kimliği pozitif tam sayı olmalıdır", "components.Settings.OverrideRuleModal.conditions": "Koşullar", "components.Settings.OverrideRuleModal.create": "Kural oluştur", "components.Settings.OverrideRuleModal.createrule": "Yeni Geçersiz Kılma Kuralı", "components.Settings.OverrideRuleModal.editrule": "Geçersiz Kılma Kuralını Düzenle", "components.Settings.OverrideRuleModal.genres": "Türler", - "components.Settings.OverrideRuleModal.keywords": "Anahtar kelimeler", + "components.Settings.OverrideRuleModal.keywords": "Anahtar Kelimeler", "components.Settings.OverrideRuleModal.languages": "Diller", "components.Settings.OverrideRuleModal.notagoptions": "Etiket yok.", "components.Settings.OverrideRuleModal.qualityprofile": "Kalite Profili", "components.Settings.OverrideRuleModal.rootfolder": "Kök Klasör", "components.Settings.OverrideRuleModal.ruleCreated": "Geçersiz kılma kuralı başarıyla oluşturuldu!", "components.Settings.OverrideRuleModal.ruleUpdated": "Geçersiz kılma kuralı başarıyla güncellendi!", - "components.Settings.OverrideRuleModal.selectRootFolder": "Kök klasörü seç", - "components.Settings.OverrideRuleModal.selectQualityProfile": "Kalite profilini seç", + "components.Settings.OverrideRuleModal.selectRootFolder": "Kök klasör seç", + "components.Settings.OverrideRuleModal.selectQualityProfile": "Kalite profili seç", "components.Settings.OverrideRuleModal.selecttags": "Etiket seç", - "components.Settings.OverrideRuleModal.serviceDescription": "Bu kuralı seçili servise uygula.", - "components.Settings.OverrideRuleModal.service": "Servis", - "components.Settings.OverrideRuleModal.selectService": "Servis seç", + "components.Settings.OverrideRuleModal.serviceDescription": "Bu kuralı seçilen hizmete uygula.", + "components.Settings.OverrideRuleModal.service": "Hizmet", + "components.Settings.OverrideRuleModal.selectService": "Hizmet seç", "components.Settings.OverrideRuleModal.settings": "Ayarlar", - "components.Settings.OverrideRuleModal.settingsDescription": "Yukarıdaki koşullar sağlandığında hangi ayarların değiştirileceğini belirtir.", + "components.Settings.OverrideRuleModal.settingsDescription": "Yukarıdaki koşullar sağlandığında hangi ayarların değiştirileceğini belirler.", "components.Settings.OverrideRuleModal.tags": "Etiketler", "components.Settings.OverrideRuleModal.users": "Kullanıcılar", "components.Settings.OverrideRuleTile.conditions": "Koşullar", "components.Settings.OverrideRuleTile.genre": "Tür", - "components.Settings.OverrideRuleTile.keywords": "Anahtar kelimeler", + "components.Settings.OverrideRuleTile.keywords": "Anahtar Kelimeler", "components.Settings.OverrideRuleTile.language": "Dil", "components.Settings.OverrideRuleTile.qualityprofile": "Kalite Profili", "components.Settings.OverrideRuleTile.rootfolder": "Kök Klasör", "components.Settings.OverrideRuleTile.settings": "Ayarlar", "components.Settings.OverrideRuleTile.tags": "Etiketler", "components.Settings.OverrideRuleTile.users": "Kullanıcılar", - "components.Settings.SettingsMain.enableSpecialEpisodes": "Özel Bölüm Taleplerine İzin Ver", + "components.Settings.SettingsMain.enableSpecialEpisodes": "Özel Bölüm Taleplerini İzin Ver", "components.Settings.SettingsNetwork.csrfProtection": "CSRF Korumasını Etkinleştir", "components.Settings.SettingsNetwork.csrfProtectionHoverTip": "Ne yaptığınızı bilmiyorsanız bu ayarı ETKİNLEŞTİRMEYİN!", - "components.Settings.SettingsNetwork.csrfProtectionTip": "Harici API erişimini salt okunur olarak ayarlayın (HTTPS gerektirir)", - "components.Settings.SettingsNetwork.docs": "belgeleme", - "components.Settings.SettingsNetwork.forceIpv4FirstTip": "Seerr'in IPv6 yerine öncelikli olarak IPv4 adreslerine bağlanmasını zorunlu kılın", - "components.Settings.SettingsNetwork.forceIpv4First": "Öncelike IPv4 Bağlantısını Zorla", + "components.Settings.SettingsNetwork.csrfProtectionTip": "Harici API erişimini salt okunur olarak ayarla (HTTPS gerektirir)", + "components.Settings.SettingsNetwork.docs": "belgeler", + "components.Settings.SettingsNetwork.forceIpv4FirstTip": "Seerr'ı IPv6 yerine önce IPv4 adreslerini çözümlemeye zorla", + "components.Settings.SettingsNetwork.forceIpv4First": "Önce IPv4 Çözümlemesini Zorla", "components.Settings.SettingsNetwork.network": "Ağ", - "components.Settings.SettingsNetwork.networkDisclaimer": "Bu ayarlar yerine konteynerinizden/sisteminizden gelen ağ parametreleri kullanılmalıdır. Daha fazla bilgi için {docs}'a bakın.", + "components.Settings.SettingsNetwork.networkDisclaimer": "Bu ayarlar yerine kapsayıcınızdaki/sisteminizden gelen ağ parametreleri kullanılmalıdır. Daha fazla bilgi için {docs}'a bakın.", "components.Settings.SettingsNetwork.networksettings": "Ağ Ayarları", "components.Settings.SettingsNetwork.networksettingsDescription": "Seerr örneğiniz için ağ ayarlarını yapılandırın.", - "components.Settings.SettingsNetwork.proxyBypassFilterTip": "Alt etki alanları için ayırıcı olarak ',' ve joker karakter olarak '*.' kullanın", - "components.Settings.SettingsNetwork.proxyBypassLocalAddresses": "Yerel Adresler için Proxy Kullanma", + "components.Settings.SettingsNetwork.proxyBypassFilterTip": "Ayırıcı olarak ',' ve alt alan adları için joker karakter olarak '*.' kullanın", + "components.Settings.SettingsNetwork.proxyBypassLocalAddresses": "Yerel Adresler için Proxy'yi Atla", "components.Settings.SettingsNetwork.proxyEnabled": "HTTP(S) Proxy", - "components.Settings.SettingsNetwork.proxyHostname": "Proxy Ana Bilgisayar Adı", + "components.Settings.SettingsNetwork.proxyHostname": "Proxy Sunucu Adı", "components.Settings.SettingsNetwork.proxyPassword": "Proxy Şifresi", - "components.Settings.SettingsNetwork.proxyPort": "Proxy Bağlantı Noktası", - "components.Settings.SettingsNetwork.proxySsl": "Proxy İçin SSL Kullanın", + "components.Settings.SettingsNetwork.proxyPort": "Proxy Portu", + "components.Settings.SettingsNetwork.proxySsl": "Proxy için SSL Kullan", "components.Settings.SettingsNetwork.proxyUser": "Proxy Kullanıcı Adı", - "components.Settings.SettingsNetwork.proxyBypassFilter": "Proxy Tarafından Yoksayılan Adresler", + "components.Settings.SettingsNetwork.proxyBypassFilter": "Proxy'den Muaf Adresler", "components.Settings.SettingsNetwork.toastSettingsSuccess": "Ayarlar başarıyla kaydedildi!", "components.Settings.SettingsNetwork.trustProxy": "Proxy Desteğini Etkinleştir", - "components.Settings.SettingsNetwork.validationProxyPort": "Geçerli bir bağlantı noktası sağlamalısınız", + "components.Settings.SettingsNetwork.validationProxyPort": "Geçerli bir port girmelisiniz", "components.Settings.SettingsUsers.atLeastOneAuth": "En az bir kimlik doğrulama yöntemi seçilmelidir.", "components.Settings.SettingsUsers.loginMethods": "Giriş Yöntemleri", - "components.Settings.SettingsUsers.loginMethodsTip": "Kullanıcılar için oturum açma yöntemlerini yapılandırın.", - "components.Settings.SettingsUsers.mediaServerLogin": "{mediaServerName} ile Oturum Açmayı Etkinleştir", - "components.Settings.SettingsUsers.mediaServerLoginTip": "Kullanıcıların {mediaServerName} hesaplarını kullanarak oturum açmalarına izin ver", + "components.Settings.SettingsUsers.loginMethodsTip": "Kullanıcılar için giriş yöntemlerini yapılandırın.", + "components.Settings.SettingsUsers.mediaServerLogin": "{mediaServerName} Girişini Etkinleştir", + "components.Settings.SettingsUsers.mediaServerLoginTip": "Kullanıcıların {mediaServerName} hesaplarıyla giriş yapmasına izin ver", "components.Settings.addrule": "Yeni Geçersiz Kılma Kuralı", "components.Settings.menuNetwork": "Ağ", "components.Settings.overrideRules": "Geçersiz Kılma Kuralları", - "components.Settings.overrideRulesDescription": "Geçersiz kılma kuralları, bir talep kuralla eşleşirse değiştirilecek özellikleri belirtmenize olanak tanır.", - "components.Setup.librarieserror": "Doğrulama başarısız oldu. Devam etmek için lütfen kütüphaneleri tekrar değiştirin.", + "components.Settings.overrideRulesDescription": "Geçersiz kılma kuralları, bir talep kuralla eşleştiğinde değiştirilecek özellikleri belirlemenizi sağlar.", + "components.Setup.librarieserror": "Doğrulama başarısız. Devam etmek için lütfen kütüphaneleri tekrar açıp kapatın.", "components.UserProfile.UserSettings.LinkJellyfinModal.description": "Hesabınızı {applicationName} ile bağlamak için {mediaServerName} kimlik bilgilerinizi girin.", "components.UserProfile.UserSettings.LinkJellyfinModal.errorExists": "Bu hesap zaten bir {applicationName} kullanıcısına bağlı", - "components.UserProfile.UserSettings.LinkJellyfinModal.errorUnauthorized": "Kullanılan kimlik bilgileriniz ile {mediaServerName} sunucusuna bağlanılamadı", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorUnauthorized": "Kimlik bilgilerinizle {mediaServerName}'a bağlanılamıyor", "components.UserProfile.UserSettings.LinkJellyfinModal.errorUnknown": "Bilinmeyen bir hata oluştu", "components.UserProfile.UserSettings.LinkJellyfinModal.password": "Şifre", - "components.UserProfile.UserSettings.LinkJellyfinModal.save": "Bağlantı", + "components.UserProfile.UserSettings.LinkJellyfinModal.save": "Bağla", "components.UserProfile.UserSettings.LinkJellyfinModal.saving": "Ekleniyor…", "components.UserProfile.UserSettings.LinkJellyfinModal.title": "{mediaServerName} Hesabını Bağla", - "components.UserProfile.UserSettings.LinkJellyfinModal.username": "Kullanıcı adı", - "components.UserProfile.UserSettings.LinkJellyfinModal.usernameRequired": "Kullanıcı adı sağlamalısınız", - "components.UserProfile.UserSettings.UserLinkedAccountsSettings.deleteFailed": "Bağlantılı hesap silinemiyor.", + "components.UserProfile.UserSettings.LinkJellyfinModal.username": "Kullanıcı Adı", + "components.UserProfile.UserSettings.LinkJellyfinModal.usernameRequired": "Bir kullanıcı adı girmelisiniz", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.deleteFailed": "Bağlı hesap silinemedi.", "components.UserProfile.UserSettings.UserLinkedAccountsSettings.errorUnknown": "Bilinmeyen bir hata oluştu", - "components.UserProfile.UserSettings.UserLinkedAccountsSettings.linkedAccounts": "Bağlantılı Hesaplar", - "components.UserProfile.UserSettings.UserLinkedAccountsSettings.noLinkedAccounts": "Hesabınıza bağlı herhangi bir harici hesabınız bulunmamaktadır.", - "components.UserProfile.UserSettings.UserLinkedAccountsSettings.noPermissionDescription": "Bu kullanıcının bağlantılı hesaplarını değiştirme yetkiniz yok.", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.linkedAccounts": "Bağlı Hesaplar", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.noLinkedAccounts": "Hesabınıza bağlı harici hesap yok.", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.noPermissionDescription": "Bu kullanıcının bağlı hesaplarını değiştirme izniniz yok.", "components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorExists": "Bu hesap zaten bir Plex kullanıcısına bağlı", - "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadId": "Konu/Konu Kimliği", - "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadIdTip": "Grup sohbetinizde konular etkinleştirilmişse, burada bir konu başlığının/konunun kimliğini belirtebilirsiniz", - "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramMessageThreadId": "Konu/konu kimliği pozitif bir tam sayı olmalıdır", - "components.UserProfile.UserSettings.menuLinkedAccounts": "Bağlantılı Hesaplar", - "components.UserProfile.UserSettings.LinkJellyfinModal.passwordRequired": "Şifre sağlamanız gerekiyor" + "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadId": "İş Parçacığı/Konu Kimliği", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadIdTip": "Grup sohbetinizde konular etkinleştirilmişse burada bir iş parçacığı/konu kimliği belirtebilirsiniz", + "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramMessageThreadId": "İş parçacığı/konu kimliği pozitif tam sayı olmalıdır", + "components.UserProfile.UserSettings.menuLinkedAccounts": "Bağlı Hesaplar", + "components.UserProfile.UserSettings.LinkJellyfinModal.passwordRequired": "Bir şifre girmelisiniz", + "component.BlocklistBlock.blocklistdate": "Engelleme tarihi", + "component.BlocklistBlock.blocklistedby": "Engelleyen", + "component.BlocklistModal.blocklisting": "Engelleme", + "components.Blocklist.blocklistNotFoundError": "{title} engelleme listesinde değil.", + "components.Blocklist.blocklistSettingsDescription": "Engellenen medyayı yönetin.", + "components.Blocklist.blocklistdate": "tarih", + "components.Blocklist.blocklistedby": "{date} tarihinde {user} tarafından", + "components.Blocklist.blocklistsettings": "Engelleme Listesi Ayarları", + "components.Blocklist.filterBlocklistedTags": "Engellenen Etiketler", + "components.Blocklist.filterManual": "Manuel", + "components.Blocklist.mediaName": "Ad", + "components.Blocklist.mediaTmdbId": "TMDB Kimliği", + "components.Blocklist.mediaType": "Tür", + "components.Blocklist.showAllBlocklisted": "Tüm Engellenen Medyayı Göster", + "components.CollectionDetails.removefromblocklistpartialcount": "{removeLabel} ({count, plural, one {# film} other {# film}})", + "components.Discover.FilterSlideover.certification": "İçerik Derecelendirmesi", + "components.Discover.FilterSlideover.excludeKeywords": "Anahtar Kelimeleri Hariç Tut", + "components.Discover.RecentRequestsSlider.unableToConnect": "{services} hizmetine bağlanılamıyor. Bazı bilgiler kullanılamayabilir.", + "components.Discover.timeWindowDay": "Günlük", + "components.Discover.timeWindowWeek": "Haftalık", + "components.IssueList.IssueItem.descriptionpreview": "Sorun Açıklaması", + "components.Layout.Sidebar.blocklist": "Engelleme Listesi", + "components.Layout.UserWarnings.profileIncomplete": "Profil eksik", + "components.Login.emailAlreadyTaken": "Bu e-posta adresi zaten kullanımda.", + "components.Login.jellyfinLocalLoginHint": "Profilinizde bir e-posta adresi ayarlamadıysanız {mediaServerName} kullanıcı adınızı kullanın.", + "components.Login.saveFailed": "Kaydedilirken bir sorun oluştu.", + "components.Login.tipEmailHasTrailingWhitespace": "E-posta sonda boşlukla bitiyor", + "components.Login.tipUsernameHasTrailingWhitespace": "Kullanıcı adı sonda boşlukla bitiyor", + "components.Login.urlBaseHelp": "Jellyfin'de bir Temel URL ayarladıysanız (Pano > Ağ), buraya girin (ör. /jellyfin). Aksi takdirde boş bırakın.", + "components.MetadataSelector.selectMetdataProvider": "Bir meta veri sağlayıcısı seçin", + "components.MetadataSelector.tmdbLabel": "The Movie Database (TMDB)", + "components.MetadataSelector.tvdbLabel": "TheTVDB", + "components.PermissionEdit.blocklistedItems": "Medyayı engelle.", + "components.PermissionEdit.blocklistedItemsDescription": "Medyayı engelleme listesine ekleme izni ver.", + "components.PermissionEdit.manageblocklist": "Engelleme Listesini Yönet", + "components.PermissionEdit.manageblocklistDescription": "Engellenen medyayı yönetme izni ver.", + "components.PermissionEdit.viewblocklistedItems": "Engellenen medyayı görüntüle.", + "components.PermissionEdit.viewblocklistedItemsDescription": "Engellenen medyayı görüntüleme izni ver.", + "components.RequestCard.failedmodify": "Talep değiştirilirken bir sorun oluştu.", + "components.RequestList.RequestItem.failedmodify": "Talep değiştirilirken bir sorun oluştu.", + "components.RequestList.unableToConnect": "{services} hizmetine bağlanılamıyor. Bazı bilgiler kullanılamayabilir.", + "components.Selector.CertificationSelector.errorLoading": "Sertifikasyonlar yüklenemedi", + "components.Selector.CertificationSelector.maxRating": "Maksimum derecelendirme", + "components.Selector.CertificationSelector.minRating": "Minimum derecelendirme", + "components.Selector.CertificationSelector.noOptions": "Seçenek mevcut değil", + "components.Selector.CertificationSelector.selectCertification": "Bir sertifikasyon seçin", + "components.Selector.CertificationSelector.selectCountry": "Bir ülke seçin", + "components.Selector.CertificationSelector.starttyping": "Aramak için yazmaya başlayın.", + "components.Settings.Notifications.NotificationsGotify.priority": "Öncelik", + "components.Settings.Notifications.NotificationsGotify.validationPriorityRequired": "Bir öncelik numarası belirlemelisiniz", + "components.Settings.Notifications.NotificationsNtfy.agentenabled": "Aracıyı Etkinleştir", + "components.Settings.Notifications.NotificationsNtfy.embedPoster": "Poster Göm", + "components.Settings.Notifications.NotificationsNtfy.ntfysettingsfailed": "Ntfy bildirim ayarları kaydedilemedi.", + "components.Settings.Notifications.NotificationsNtfy.ntfysettingssaved": "Ntfy bildirim ayarları başarıyla kaydedildi!", + "components.Settings.Notifications.NotificationsNtfy.password": "Şifre", + "components.Settings.Notifications.NotificationsNtfy.priority": "Öncelik", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestFailed": "Ntfy test bildirimi gönderilemedi.", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestSending": "Ntfy test bildirimi gönderiliyor…", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestSuccess": "Ntfy test bildirimi gönderildi!", + "components.Settings.Notifications.NotificationsNtfy.token": "Token", + "components.Settings.Notifications.NotificationsNtfy.tokenAuth": "Token kimlik doğrulaması", + "components.Settings.Notifications.NotificationsNtfy.topic": "Konu", + "components.Settings.Notifications.NotificationsNtfy.url": "Sunucu kök URL", + "components.Settings.Notifications.NotificationsNtfy.username": "Kullanıcı Adı", + "components.Settings.Notifications.NotificationsNtfy.usernamePasswordAuth": "Kullanıcı Adı + Şifre kimlik doğrulaması", + "components.Settings.Notifications.NotificationsNtfy.validationNtfyTopic": "Bir konu girmelisiniz", + "components.Settings.Notifications.NotificationsNtfy.validationNtfyUrl": "Geçerli bir URL girmelisiniz", + "components.Settings.Notifications.NotificationsNtfy.validationPriorityRequired": "1 ile 5 arasında bir öncelik girmelisiniz", + "components.Settings.Notifications.NotificationsNtfy.validationTypes": "En az bir bildirim türü seçmelisiniz", + "components.Settings.Notifications.NotificationsPushover.embedPoster": "Poster Göm", + "components.Settings.Notifications.NotificationsSlack.embedPoster": "Poster Göm", + "components.Settings.Notifications.NotificationsWebPush.embedPoster": "Poster Göm", + "components.Settings.Notifications.NotificationsWebhook.customHeaders": "Özel Başlıklar", + "components.Settings.Notifications.NotificationsWebhook.customHeadersAdd": "Başlık Ekle", + "components.Settings.Notifications.NotificationsWebhook.customHeadersAuthConflict": "Yetkilendirme Başlığı ve özel Yetkilendirme başlığı birlikte kullanılamaz. Lütfen birini kaldırın.", + "components.Settings.Notifications.NotificationsWebhook.customHeadersIncomplete": "Tüm başlıkların hem adı hem de değeri olmalıdır", + "components.Settings.Notifications.NotificationsWebhook.customHeadersKey": "Başlık Adı", + "components.Settings.Notifications.NotificationsWebhook.customHeadersRemove": "Kaldır", + "components.Settings.Notifications.NotificationsWebhook.customHeadersTip": "Webhook isteklerine eklenecek özel HTTP başlıkları ekleyin", + "components.Settings.Notifications.NotificationsWebhook.customHeadersValue": "Başlık Değeri", + "components.Settings.Notifications.NotificationsWebhook.supportVariables": "URL Değişkenlerini Destekle", + "components.Settings.Notifications.NotificationsWebhook.supportVariablesTip": "Kullanılabilir değişkenler webhook şablon değişkenleri bölümünde belgelenmiştir", + "components.Settings.Notifications.NotificationsWebhook.webhookUrlTip": "Test Bildirim URL'si gerçek webhook URL'si yerine {testUrl} olarak ayarlanmıştır.", + "components.Settings.Notifications.embedPoster": "Poster Göm", + "components.Settings.Notifications.usePublicLogo": "Örnek logosu yerine genel Seerr logosunu kullan", + "components.Settings.Notifications.usePublicLogoTip": "Seerr örneğiniz herkese açık değilse, ağınız dışındaki e-posta istemcilerinin resmi gösterebilmesi için bu seçeneği etkinleştirin. Resim genel GitHub deposundan çekilecektir.", + "components.Settings.Notifications.useUserLocale": "Bildirim Alıcısının Dilini Kullan", + "components.Settings.RadarrModal.apiKeyHelp": "Radarr'da bulun: Ayarlar > Genel > Güvenlik > API Anahtarı", + "components.Settings.RadarrModal.baseUrlHelp": "Radarr'da bir URL Tabanı ayarladıysanız (Ayarlar > Genel > Sunucu), buraya girin (ör. /radarr). Aksi takdirde boş bırakın.", + "components.Settings.RadarrModal.enableSearchHelp": "Bir talep onaylandığında Radarr'da otomatik olarak arama başlat.", + "components.Settings.RadarrModal.externalUrlHelp": "Sunucu adı ağınız dışından erişilemediğinde medya sayfalarındaki tıklanabilir bağlantılar için.", + "components.Settings.RadarrModal.server4kHelp": "Yalnızca ayrı bir 4K örneğiniz varsa. Tek sunucu için işaretsiz bırakın.", + "components.Settings.RadarrModal.syncEnabledHelp": "Kullanıcıların zaten mevcut içerikleri talep edememesi için Radarr'ı mevcut medya ve talep durumu açısından tara.", + "components.Settings.SettingsAbout.aboutseerr": "Seerr Hakkında", + "components.Settings.SettingsAbout.contribute": "Katkıda Bulun", + "components.Settings.SettingsAbout.supportseerr": "Seerr'ı Destekle", + "components.Settings.SettingsJobsCache.dnsCache": "DNS Önbelleği", + "components.Settings.SettingsJobsCache.dnsCacheDescription": "Seerr, performansı optimize etmek ve gereksiz API çağrılarını önlemek için DNS aramalarını önbelleğe alır.", + "components.Settings.SettingsJobsCache.dnsCacheGlobalStats": "Global DNS Önbellek İstatistikleri", + "components.Settings.SettingsJobsCache.dnsCacheGlobalStatsDescription": "Bu istatistikler tüm DNS önbellek girişleri genelinde toplanmıştır.", + "components.Settings.SettingsJobsCache.dnsNoCacheEntries": "Henüz önbelleğe alınmış DNS araması yok.", + "components.Settings.SettingsJobsCache.dnscacheactiveaddress": "Aktif Adres", + "components.Settings.SettingsJobsCache.dnscacheage": "Yaş", + "components.Settings.SettingsJobsCache.dnscacheflushed": "{hostname} dns önbelleği temizlendi.", + "components.Settings.SettingsJobsCache.dnscachehits": "İsabet", + "components.Settings.SettingsJobsCache.dnscachemisses": "Kaçırma", + "components.Settings.SettingsJobsCache.dnscachename": "Sunucu Adı", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorDays": "Her {jobScheduleDays, plural, one {gün} other {{jobScheduleDays} günde bir}}", + "components.Settings.SettingsJobsCache.failures": "Başarısızlıklar", + "components.Settings.SettingsJobsCache.flushdnscache": "DNS Önbelleğini Temizle", + "components.Settings.SettingsJobsCache.hitRate": "İsabet Oranı", + "components.Settings.SettingsJobsCache.hits": "İsabet", + "components.Settings.SettingsJobsCache.ipv4Fallbacks": "IPv4 Yedekleri", + "components.Settings.SettingsJobsCache.misses": "Kaçırma", + "components.Settings.SettingsJobsCache.process-blocklisted-tags": "Engellenen Etiketleri İşle", + "components.Settings.SettingsJobsCache.size": "Boyut", + "components.Settings.SettingsMain.apikeyCopied": "API anahtarı panoya kopyalandı.", + "components.Settings.SettingsMain.blocklistLanguage": "Engelleme Listesi Dili", + "components.Settings.SettingsMain.blocklistLanguageTip": "Engelleme listesi içerik taraması için kullanılan dil (keşfet ayarlarından bağımsız)", + "components.Settings.SettingsMain.blocklistRegion": "Engelleme Listesi Bölgesi", + "components.Settings.SettingsMain.blocklistRegionTip": "Engelleme listesi içerik taraması için kullanılan bölge (keşfet ayarlarından bağımsız)", + "components.Settings.SettingsMain.blocklistedTags": "Etiketli İçerikleri Engelle", + "components.Settings.SettingsMain.blocklistedTagsLimit": "Etiket Başına Engellenen İçerik Sınırı", + "components.Settings.SettingsMain.blocklistedTagsLimitTip": "\"Engellenen Etiketleri İşle\" görevi her sıralamaya bu kadar sayfa kadar engelleme uygular. Daha büyük sayılar daha doğru bir engelleme listesi oluşturur ancak daha fazla alan kullanır.", + "components.Settings.SettingsMain.blocklistedTagsTip": "\"Engellenen Etiketleri İşle\" görevi kullanılarak etiketli içerikleri otomatik olarak engelleme listesine ekle", + "components.Settings.SettingsMain.hideAvailableTip": "Kullanılabilir medyayı arama sonuçlarından değil, keşfet sayfalarından gizle", + "components.Settings.SettingsMain.hideBlocklisted": "Engellenen Öğeleri Gizle", + "components.Settings.SettingsMain.hideBlocklistedTip": "\"Engelleme Listesini Yönet\" iznine sahip tüm kullanıcılar için engellenen öğeleri keşfet sayfalarından gizle", + "components.Settings.SettingsMain.validationUrl": "Geçerli bir URL girmelisiniz", + "components.Settings.SettingsMain.validationUrlTrailingSlash": "URL eğik çizgi ile bitmemelidir", + "components.Settings.SettingsMain.youtubeUrl": "YouTube URL", + "components.Settings.SettingsMain.youtubeUrlTip": "Kendi barındırdığınız bir YouTube örneği kullanılıyorsa YouTube videoları için temel URL.", + "components.Settings.SettingsNetwork.apiRequestTimeout": "API İstek Zaman Aşımı", + "components.Settings.SettingsNetwork.apiRequestTimeoutTip": "Radarr/Sonarr gibi harici hizmetlerden yanıt beklemek için maksimum süre (saniye). Zaman aşımı olmaması için 0 girin.", + "components.Settings.SettingsNetwork.dnsCache": "DNS Önbelleği", + "components.Settings.SettingsNetwork.dnsCacheForceMaxTtl": "DNS Önbelleği Maksimum TTL", + "components.Settings.SettingsNetwork.dnsCacheForceMinTtl": "DNS Önbelleği Minimum TTL", + "components.Settings.SettingsNetwork.dnsCacheHoverTip": "DNS aramalarında sorun yaşıyorsanız bunu ETKİNLEŞTİRMEYİN", + "components.Settings.SettingsNetwork.dnsCacheTip": "Performansı optimize etmek ve gereksiz API çağrılarını önlemek için DNS aramalarının önbelleğe alınmasını etkinleştir", + "components.Settings.SettingsNetwork.proxyEnabledTip": "Tüm giden HTTP/HTTPS isteklerini bir proxy sunucusu üzerinden gönder (host/port). HTTPS, SSL veya sertifika yapılandırmasını etkinleştirmez.", + "components.Settings.SettingsNetwork.validationApiRequestTimeout": "Geçerli bir zaman aşımı değeri girmelisiniz", + "components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "Geçerli bir maksimum TTL girmelisiniz", + "components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "Geçerli bir minimum TTL girmelisiniz", + "components.Settings.SettingsUsers.disabledMediaServerLoginWarning": "Bazı kullanıcıların {applicationTitle} şifresi belirlenmemiş olabilir. {mediaServerName} girişini devre dışı bırakmak onları kilitleyebilir. Etkilenen kullanıcıların profillerinden veya şifre sıfırlama bağlantısıyla şifre belirlemeleri gerekecektir.", + "components.Settings.SonarrModal.apiKeyHelp": "Sonarr'da bulun: Ayarlar > Genel > Güvenlik > API Anahtarı", + "components.Settings.SonarrModal.baseUrlHelp": "Sonarr'da bir URL Tabanı ayarladıysanız (Ayarlar > Genel > Sunucu), buraya girin (ör. /sonarr). Aksi takdirde boş bırakın.", + "components.Settings.SonarrModal.enableSearchHelp": "Bir talep onaylandığında Sonarr'da otomatik olarak arama başlat.", + "components.Settings.SonarrModal.externalUrlHelp": "Sunucu adı ağınız dışından erişilemediğinde medya sayfalarındaki tıklanabilir bağlantılar için.", + "components.Settings.SonarrModal.monitorNewItems": "Yeni Sezonları İzle", + "components.Settings.SonarrModal.monitorNewItemsHelp": "Bir dizi eklendiğinde Sonarr'ın yeni sezonları izleyip izlemeyeceği (Tümü) veya izlemeyeceği (Hiçbiri).", + "components.Settings.SonarrModal.server4kHelp": "Yalnızca ayrı bir 4K örneğiniz varsa. Tek sunucu için işaretsiz bırakın.", + "components.Settings.SonarrModal.syncEnabledHelp": "Kullanıcıların zaten mevcut içerikleri talep edememesi için Sonarr'ı mevcut medya ve talep durumu açısından tara.", + "components.Settings.allChosenProvidersAreOperational": "Seçilen tüm meta veri sağlayıcıları çalışıyor", + "components.Settings.animeMetadataProvider": "Anime meta veri sağlayıcısı", + "components.Settings.blocklistedTagImportInstructions": "Engelleme listesi etiket yapılandırmasını aşağıya yapıştırın.", + "components.Settings.blocklistedTagImportTitle": "Engellenen Etiket Yapılandırmasını İçe Aktar", + "components.Settings.blocklistedTagsText": "Engellenen Etiketler", + "components.Settings.chooseProvider": "Farklı içerik türleri için meta veri sağlayıcılarını seçin", + "components.Settings.clearBlocklistedTagsConfirm": "Engellenen etiketleri temizlemek istediğinizden emin misiniz?", + "components.Settings.clickTest": "Meta veri sağlayıcılarıyla bağlantıyı kontrol etmek için \"Test\" düğmesine tıklayın", + "components.Settings.connectionTestFailed": "Bağlantı testi başarısız", + "components.Settings.copyBlocklistedTags": "Engellenen etiketler panoya kopyalandı.", + "components.Settings.copyBlocklistedTagsEmpty": "Kopyalanacak bir şey yok", + "components.Settings.copyBlocklistedTagsTip": "Engellenen etiket yapılandırmasını kopyala", + "components.Settings.failed": "Çalışmıyor", + "components.Settings.failedToSaveMetadataSettings": "Meta veri sağlayıcısı ayarları kaydedilemedi", + "components.Settings.general": "Genel", + "components.Settings.importBlocklistedTagsTip": "Engellenen etiket yapılandırmasını içe aktar", + "components.Settings.invalidKeyword": "{keywordId} geçerli bir TMDB anahtar kelimesi değil.", + "components.Settings.menuMetadataProviders": "Meta Veri Sağlayıcıları", + "components.Settings.metadataProviderSelection": "Meta Veri Sağlayıcısı Seçimi", + "components.Settings.metadataProviderSettings": "Meta Veri Sağlayıcıları", + "components.Settings.metadataSettings": "Meta veri sağlayıcısı ayarları", + "components.Settings.metadataSettingsSaved": "Meta veri sağlayıcısı ayarları kaydedildi", + "components.Settings.no": "Hayır", + "components.Settings.noSpecialCharacters": "Yapılandırma, virgülle ayrılmış TMDB anahtar kelime kimliklerinden oluşmalı ve virgülle başlayıp bitmemelidir.", + "components.Settings.nooptions": "Sonuç bulunamadı.", + "components.Settings.notTested": "Test Edilmedi", + "components.Settings.operational": "Çalışıyor", + "components.Settings.providerStatus": "Meta Veri Sağlayıcısı Durumu", + "components.Settings.searchKeywords": "Anahtar kelime ara…", + "components.Settings.seriesMetadataProvider": "Dizi meta veri sağlayıcısı", + "components.Settings.settings": "Ayarlar", + "components.Settings.starttyping": "Aramak için yazmaya başlayın.", + "components.Settings.tmdbProviderDoesnotWork": "TMDB sağlayıcısı çalışmıyor, lütfen başka bir meta veri sağlayıcısı seçin", + "components.Settings.tvdbProviderDoesnotWork": "TVDB sağlayıcısı çalışmıyor, lütfen başka bir meta veri sağlayıcısı seçin", + "components.Settings.valueRequired": "Bir değer girmelisiniz.", + "components.Settings.yes": "Evet", + "components.UserList.ascending": "artan", + "components.UserList.descending": "azalan", + "components.UserList.importedPlexUsersNoPassword": "İçe aktarılan kullanıcıların {applicationTitle} şifresi belirlenmemiş. Plex girişini devre dışı bırakırsanız, profillerinden veya şifre sıfırlama bağlantısıyla şifre belirlemeleri gerekecektir.", + "components.UserList.importedUsersNoPassword": "İçe aktarılan kullanıcıların {applicationTitle} şifresi belirlenmemiş. {mediaServerName} girişini devre dışı bırakırsanız, profillerinden veya şifre sıfırlama bağlantısıyla şifre belirlemeleri gerekecektir.", + "components.UserList.sortBy": "{field} alanına göre sırala", + "components.UserList.sortByJoined": "Katılım tarihine göre sırala", + "components.UserList.sortByRequests": "Talep sayısına göre sırala", + "components.UserList.sortByRole": "Kullanıcı rolüne göre sırala", + "components.UserList.sortByType": "Hesap türüne göre sırala", + "components.UserList.sortByUser": "Kullanıcı adına göre sırala", + "components.UserList.toggleSortDirection": "Tekrar tıklayarak {direction} sırala", + "components.UserList.toggleSortDirectionAria": "Sıralama yönünü değiştir", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": "Aktif Abonelik", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.browser": "Tarayıcı", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.created": "Oluşturuldu", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.deletesubscription": "Aboneliği Sil", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.device": "Cihaz", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablewebpush": "Web push'u devre dışı bırak", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablingwebpusherror": "Web push devre dışı bırakılırken bir sorun oluştu.", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablewebpush": "Web push'u etkinleştir", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablingwebpusherror": "Web push etkinleştirilirken bir sorun oluştu.", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.engine": "Motor", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.managedevices": "Cihazları Yönet", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.nodevicestoshow": "Gösterilecek web push aboneliğiniz yok.", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.operatingsystem": "İşletim Sistemi", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleted": "Abonelik silindi.", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleteerror": "Kullanıcı aboneliği silinirken bir sorun oluştu.", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.type": "tür", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.unknown": "Bilinmiyor", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeendisabled": "Web push devre dışı bırakıldı.", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeenenabled": "Web push etkinleştirildi.", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingsfailed": "Web push bildirim ayarları kaydedilemedi.", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingssaved": "Web push bildirim ayarları başarıyla kaydedildi!", + "components.UserProfile.UserSettings.UserPasswordChange.localPasswordDescription": "Bu şifre {applicationTitle} yerel giriş formuyla giriş yapmak için kullanılır. Medya sunucusu şifrenizden ayrıdır.", + "i18n.addToBlocklist": "Engelleme Listesine Ekle", + "i18n.blocklist": "Engelleme Listesi", + "i18n.blocklistDuplicateError": "{title} zaten engelleme listesinde.", + "i18n.blocklistError": "Bir şeyler ters gitti. Lütfen tekrar deneyin.", + "i18n.blocklistSuccess": "{title} başarıyla engelleme listesine eklendi.", + "i18n.blocklisted": "Engellendi", + "i18n.completed": "Tamamlandı", + "i18n.deleted": "Silindi", + "i18n.error": "Bir şeyler ters gitti. Lütfen tekrar deneyin.", + "i18n.notificationLocale": "Bildirim Dili", + "i18n.partiallyblocklisted": "Kısmen Engellendi", + "i18n.removeFromBlocklistSuccess": "{title} engelleme listesinden başarıyla kaldırıldı.", + "i18n.removefromBlocklist": "Engelleme Listesinden Kaldır", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdAdd": "Kullanıcı Kimliği Ekle", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdPlaceholder": "Discord Kullanıcı Kimliği", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdRemove": "Kaldır" } diff --git a/src/i18n/locale/uk.json b/src/i18n/locale/uk.json index b899392b1b..10d958294b 100644 --- a/src/i18n/locale/uk.json +++ b/src/i18n/locale/uk.json @@ -914,8 +914,6 @@ "components.UserProfile.UserSettings.UserGeneralSettings.accounttype": "Тип облікового запису", "components.UserProfile.UserSettings.UserGeneralSettings.admin": "Адміністратор", "components.UserProfile.UserSettings.UserGeneralSettings.applanguage": "Мова інтерфейсу", - "components.UserProfile.UserSettings.UserGeneralSettings.discordId": "ID користувача Discord", - "components.UserProfile.UserSettings.UserGeneralSettings.discordIdTip": "багатозначний ідентифікаційний номер, пов’язаний із вашим обліковим записом користувача Discord", "components.UserProfile.UserSettings.UserGeneralSettings.displayName": "Відображуване ім'я", "components.UserProfile.UserSettings.UserGeneralSettings.enableOverride": "Перевизначити глобальні обмеження", "components.UserProfile.UserSettings.UserGeneralSettings.general": "Загальне", @@ -938,7 +936,6 @@ "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailure": "Щось пішло не так при збереженні налаштувань.", "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsSuccess": "Налаштування успішно збережено!", "components.UserProfile.UserSettings.UserGeneralSettings.user": "Користувач", - "components.UserProfile.UserSettings.UserGeneralSettings.validationDiscordId": "Ви повинні надати дійсний ідентифікатор користувача Discord", "components.UserProfile.UserSettings.UserNotificationSettings.discordId": "ID користувача", "components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "ID вашого облікового запису", "components.UserProfile.UserSettings.UserNotificationSettings.discordsettingsfailed": "Не вдалося зберегти налаштування повідомлень Discord.", @@ -1551,5 +1548,125 @@ "components.Settings.SettingsNetwork.forceIpv4FirstTip": "Змусити Jellyseerr спершу резолвити адреси IPv4 замість IPv6", "components.Settings.SettingsNetwork.networkDisclaimer": "Параметри мережі з вашого контейнера/системи слід використовувати замість цих налаштувань. Дивіться {docs} для отримання додаткової інформації.", "components.Settings.SettingsNetwork.networksettingsDescription": "Налаштуйте параметри мережі для вашого екземпляра Jellyseerr.", - "components.Settings.SettingsNetwork.proxyBypassFilter": "Ігноровані адреси проксі" + "components.Settings.SettingsNetwork.proxyBypassFilter": "Ігноровані адреси проксі", + "components.Settings.Notifications.usePublicLogo": "", + "components.Settings.Notifications.usePublicLogoTip": "", + "components.Settings.RadarrModal.apiKeyHelp": "", + "components.Settings.RadarrModal.baseUrlHelp": "", + "components.Settings.RadarrModal.enableSearchHelp": "", + "components.Settings.RadarrModal.externalUrlHelp": "", + "components.Settings.RadarrModal.server4kHelp": "", + "components.Settings.RadarrModal.syncEnabledHelp": "", + "components.Settings.SettingsAbout.supportseerr": "", + "components.Settings.SettingsJobsCache.dnsNoCacheEntries": "", + "components.Settings.SettingsJobsCache.process-blocklisted-tags": "", + "components.Settings.SettingsMain.blocklistRegionTip": "", + "components.Settings.SettingsMain.blocklistedTags": "", + "components.Settings.SettingsMain.blocklistedTagsLimit": "", + "components.Settings.SettingsMain.blocklistedTagsLimitTip": "", + "components.Settings.SettingsMain.blocklistedTagsTip": "", + "components.Settings.SettingsNetwork.apiRequestTimeout": "", + "components.Settings.SettingsNetwork.validationApiRequestTimeout": "", + "components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "", + "components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "", + "components.Settings.SettingsUsers.disabledMediaServerLoginWarning": "", + "components.Settings.SonarrModal.apiKeyHelp": "", + "components.Settings.SonarrModal.baseUrlHelp": "", + "components.Settings.SonarrModal.monitorNewItemsHelp": "", + "components.Settings.SonarrModal.server4kHelp": "", + "components.Settings.SonarrModal.syncEnabledHelp": "", + "components.Settings.blocklistedTagImportInstructions": "", + "components.Settings.blocklistedTagImportTitle": "", + "components.Settings.blocklistedTagsText": "", + "components.Settings.clearBlocklistedTagsConfirm": "", + "components.Settings.copyBlocklistedTags": "", + "components.Settings.copyBlocklistedTagsTip": "", + "components.RequestCard.failedmodify": "", + "components.UserList.sortBy": "", + "components.UserList.toggleSortDirection": "", + "components.UserList.toggleSortDirectionAria": "", + "components.UserProfile.UserSettings.UserPasswordChange.localPasswordDescription": "", + "i18n.addToBlocklist": "", + "i18n.blocklistDuplicateError": "", + "i18n.blocklistError": "", + "i18n.blocklisted": "", + "i18n.partiallyblocklisted": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersAuthConflict": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersIncomplete": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersKey": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersRemove": "", + "components.Settings.Notifications.useUserLocale": "", + "components.Settings.SettingsMain.hideBlocklisted": "", + "components.Settings.SonarrModal.enableSearchHelp": "", + "components.Settings.SonarrModal.externalUrlHelp": "", + "components.UserList.descending": "", + "components.UserList.importedPlexUsersNoPassword": "", + "components.UserList.importedUsersNoPassword": "", + "components.UserList.sortByRequests": "", + "components.UserList.sortByRole": "", + "components.UserList.sortByType": "", + "components.UserList.sortByUser": "", + "i18n.notificationLocale": "", + "i18n.removeFromBlocklistSuccess": "", + "i18n.removefromBlocklist": "", + "components.Blocklist.blocklistdate": "", + "components.Blocklist.filterManual": "", + "components.Blocklist.mediaName": "", + "components.PermissionEdit.manageblocklistDescription": "", + "components.Discover.timeWindowWeek": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersTip": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersValue": "", + "i18n.error": "", + "component.BlocklistBlock.blocklistdate": "", + "component.BlocklistBlock.blocklistedby": "", + "components.Blocklist.blocklistedby": "", + "components.Blocklist.blocklistsettings": "", + "components.Blocklist.filterBlocklistedTags": "", + "components.CollectionDetails.removefromblocklistpartialcount": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdAdd": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdPlaceholder": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdRemove": "", + "components.Layout.UserWarnings.profileIncomplete": "", + "components.Login.emailAlreadyTaken": "", + "components.Login.jellyfinLocalLoginHint": "", + "components.Login.saveFailed": "", + "components.Login.tipUsernameHasTrailingWhitespace": "", + "components.PermissionEdit.blocklistedItems": "", + "components.PermissionEdit.blocklistedItemsDescription": "", + "components.PermissionEdit.manageblocklist": "", + "components.PermissionEdit.viewblocklistedItems": "", + "components.PermissionEdit.viewblocklistedItemsDescription": "", + "components.RequestList.RequestItem.failedmodify": "", + "components.RequestList.unableToConnect": "", + "components.Settings.Notifications.NotificationsNtfy.validationPriorityRequired": "", + "components.Settings.Notifications.NotificationsWebhook.customHeaders": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersAdd": "", + "components.Settings.SettingsAbout.aboutseerr": "", + "components.Settings.SettingsAbout.contribute": "", + "components.Settings.SettingsMain.blocklistLanguage": "", + "components.Settings.SettingsMain.blocklistLanguageTip": "", + "components.Settings.SettingsMain.blocklistRegion": "", + "components.Settings.SettingsNetwork.apiRequestTimeoutTip": "", + "components.Settings.SettingsNetwork.proxyEnabledTip": "", + "i18n.blocklistSuccess": "", + "components.UserList.sortByJoined": "", + "components.Blocklist.mediaTmdbId": "", + "components.Blocklist.mediaType": "", + "component.BlocklistModal.blocklisting": "", + "components.Blocklist.blocklistNotFoundError": "", + "components.Blocklist.blocklistSettingsDescription": "", + "components.Blocklist.showAllBlocklisted": "", + "components.Layout.Sidebar.blocklist": "", + "components.Login.tipEmailHasTrailingWhitespace": "", + "components.Settings.SettingsMain.hideBlocklistedTip": "", + "components.Login.urlBaseHelp": "", + "components.Settings.importBlocklistedTagsTip": "", + "components.Settings.SonarrModal.monitorNewItems": "", + "components.UserList.ascending": "", + "i18n.blocklist": "", + "components.Discover.timeWindowDay": "", + "components.Settings.Notifications.NotificationsNtfy.priority": "", + "components.Settings.copyBlocklistedTagsEmpty": "", + "components.Discover.RecentRequestsSlider.unableToConnect": "" } diff --git a/src/i18n/locale/vi.json b/src/i18n/locale/vi.json index 3eb7b44b4f..7b47a28aa8 100644 --- a/src/i18n/locale/vi.json +++ b/src/i18n/locale/vi.json @@ -33,5 +33,1640 @@ "components.Discover.DiscoverMovies.activefilters": "{count, plural, one {Dùng # bộ lọc} other {Dùng # bộ lọc}}", "components.Discover.DiscoverMovies.discovermovies": "Phim", "components.Discover.DiscoverMovies.sortPopularityAsc": "Phổ biến sắp xếp tăng dần", - "components.Discover.DiscoverMovies.sortPopularityDesc": "Phổ biến sắp xếp giảm dần" + "components.Discover.DiscoverMovies.sortPopularityDesc": "Phổ biến sắp xếp giảm dần", + "components.Discover.FilterSlideover.genres": "Thể loại", + "components.Settings.Notifications.usePublicLogo": "", + "components.Settings.Notifications.usePublicLogoTip": "", + "components.Settings.Notifications.validationBotAPIRequired": "", + "components.Settings.Notifications.validationChatIdRequired": "", + "components.Settings.Notifications.validationTypes": "", + "components.Settings.Notifications.validationUrl": "", + "components.Settings.Notifications.validationWebhookRoleId": "", + "components.Settings.Notifications.webhookRoleId": "", + "components.Settings.Notifications.webhookRoleIdTip": "", + "components.Settings.OverrideRuleModal.conditions": "", + "components.Settings.OverrideRuleModal.languages": "", + "components.Settings.OverrideRuleModal.notagoptions": "", + "components.Settings.OverrideRuleModal.qualityprofile": "", + "components.Settings.OverrideRuleModal.rootfolder": "", + "components.Settings.OverrideRuleModal.ruleCreated": "", + "components.Settings.OverrideRuleModal.selectQualityProfile": "", + "components.Settings.OverrideRuleModal.selectRootFolder": "", + "components.Settings.OverrideRuleModal.selectService": "", + "components.Settings.OverrideRuleModal.selecttags": "", + "components.Settings.OverrideRuleModal.service": "", + "components.Settings.OverrideRuleTile.language": "", + "components.Settings.OverrideRuleTile.settings": "", + "components.Settings.OverrideRuleTile.users": "", + "components.Settings.RadarrModal.add": "", + "components.Settings.RadarrModal.announced": "", + "components.Settings.RadarrModal.apiKey": "", + "components.Settings.RadarrModal.baseUrlHelp": "", + "components.Settings.RadarrModal.createradarr": "", + "components.Settings.RadarrModal.default4kserver": "", + "components.Settings.RadarrModal.defaultserver": "", + "components.Settings.RadarrModal.edit4kradarr": "", + "components.Settings.RadarrModal.editradarr": "", + "components.Settings.RadarrModal.enableSearchHelp": "", + "components.Settings.RadarrModal.externalUrlHelp": "", + "components.Settings.RadarrModal.hostname": "", + "components.Settings.RadarrModal.loadingTags": "", + "components.Settings.RadarrModal.selectQualityProfile": "", + "components.Settings.RadarrModal.selectRootFolder": "", + "components.Settings.RadarrModal.selecttags": "", + "components.Settings.RadarrModal.servername": "", + "components.Settings.RadarrModal.ssl": "", + "components.Settings.RadarrModal.testFirstQualityProfiles": "", + "components.Settings.RadarrModal.testFirstRootFolders": "", + "components.Settings.RadarrModal.testFirstTags": "", + "components.Settings.RadarrModal.toastRadarrTestFailure": "", + "components.Settings.RadarrModal.toastRadarrTestSuccess": "", + "components.Settings.RadarrModal.validationApiKeyRequired": "", + "components.Settings.RadarrModal.validationBaseUrlTrailingSlash": "", + "components.Settings.RadarrModal.validationHostnameRequired": "", + "components.Settings.RadarrModal.validationMinimumAvailabilityRequired": "", + "components.Settings.RadarrModal.validationNameRequired": "", + "components.Settings.RadarrModal.validationPortRequired": "", + "components.Settings.RadarrModal.validationProfileRequired": "", + "components.Settings.RadarrModal.validationRootFolderRequired": "", + "components.Settings.SettingsAbout.Releases.currentversion": "", + "components.Settings.SettingsAbout.Releases.releasedataMissing": "", + "components.Settings.SettingsAbout.Releases.releases": "", + "components.Settings.SettingsAbout.Releases.versionChangelog": "", + "components.Settings.SettingsAbout.Releases.viewchangelog": "", + "components.Settings.SettingsAbout.Releases.viewongithub": "", + "components.Settings.SettingsAbout.about": "", + "components.Settings.SettingsAbout.aboutseerr": "", + "components.Settings.SettingsAbout.appDataPath": "", + "components.Settings.SettingsAbout.contribute": "", + "components.Settings.SettingsAbout.runningDevelop": "", + "components.Settings.SettingsAbout.supportseerr": "", + "components.Settings.SettingsAbout.timezone": "", + "components.Settings.SettingsAbout.totalrequests": "", + "components.Settings.SettingsAbout.uptodate": "", + "components.Settings.SettingsAbout.version": "", + "components.Settings.SettingsJobsCache.availability-sync": "", + "components.Settings.SettingsJobsCache.cache": "", + "components.Settings.SettingsJobsCache.dnscacheflushed": "", + "components.Settings.SettingsJobsCache.dnscachehits": "", + "components.Settings.SettingsJobsCache.dnscachemisses": "", + "components.Settings.SettingsJobsCache.download-sync-reset": "", + "components.Settings.SettingsJobsCache.editJobSchedule": "", + "components.Settings.SettingsJobsCache.editJobScheduleCurrent": "", + "components.Settings.SettingsJobsCache.editJobSchedulePrompt": "", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorDays": "", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorHours": "", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorMinutes": "", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorSeconds": "", + "components.Settings.SettingsJobsCache.failures": "", + "components.Settings.SettingsJobsCache.flushcache": "", + "components.Settings.SettingsJobsCache.hits": "", + "components.Settings.SettingsJobsCache.image-cache-cleanup": "", + "components.Settings.SettingsJobsCache.imagecache": "", + "components.Settings.SettingsJobsCache.imagecacheDescription": "", + "components.Settings.SettingsJobsCache.imagecachecount": "", + "components.Settings.SettingsJobsCache.imagecachesize": "", + "components.Settings.SettingsJobsCache.ipv4Fallbacks": "", + "components.Settings.SettingsJobsCache.jellyfin-full-scan": "", + "components.Settings.SettingsJobsCache.jellyfin-recently-added-scan": "", + "components.Settings.SettingsJobsCache.jobScheduleEditFailed": "", + "components.Settings.SettingsJobsCache.jobcancelled": "", + "components.Settings.SettingsJobsCache.jobname": "", + "components.Settings.SettingsJobsCache.jobs": "", + "components.Settings.SettingsJobsCache.plex-recently-added-scan": "", + "components.Settings.SettingsJobsCache.plex-refresh-token": "", + "components.Settings.SettingsJobsCache.plex-watchlist-sync": "", + "components.Settings.SettingsJobsCache.process": "", + "components.Settings.SettingsJobsCache.radarr-scan": "", + "components.Settings.SettingsJobsCache.size": "", + "components.Settings.SettingsJobsCache.sonarr-scan": "", + "components.Settings.SettingsJobsCache.unknownJob": "", + "components.Settings.SettingsLogs.copiedLogMessage": "", + "components.Settings.SettingsLogs.copyToClipboard": "", + "components.Settings.SettingsLogs.extraData": "", + "components.Settings.SettingsLogs.filterError": "", + "components.Settings.SettingsLogs.logsDescription": "", + "components.Settings.SettingsMain.applicationurl": "", + "components.Settings.SettingsMain.blocklistLanguage": "", + "components.Settings.SettingsMain.blocklistLanguageTip": "", + "components.Settings.SettingsMain.blocklistRegion": "", + "components.Settings.SettingsMain.blocklistRegionTip": "", + "components.Settings.SettingsMain.blocklistedTags": "", + "components.Settings.SettingsMain.blocklistedTagsLimit": "", + "components.Settings.SettingsMain.blocklistedTagsLimitTip": "", + "components.Settings.SettingsMain.blocklistedTagsTip": "", + "components.Settings.SettingsMain.cacheImages": "", + "components.Settings.SettingsMain.cacheImagesTip": "", + "components.Settings.SettingsMain.discoverRegion": "", + "components.Settings.SettingsMain.discoverRegionTip": "", + "components.Settings.SettingsMain.generalsettings": "", + "components.Settings.SettingsMain.generalsettingsDescription": "", + "components.Settings.SettingsMain.hideAvailable": "", + "components.Settings.SettingsMain.hideAvailableTip": "", + "components.Settings.SettingsMain.hideBlocklisted": "", + "components.Settings.SettingsMain.locale": "", + "components.Settings.SettingsMain.originallanguageTip": "", + "components.Settings.SettingsMain.partialRequestsEnabled": "", + "components.Settings.SettingsMain.streamingRegionTip": "", + "components.Settings.SettingsMain.toastApiKeyFailure": "", + "components.Settings.SettingsMain.toastSettingsFailure": "", + "components.Settings.SettingsMain.toastSettingsSuccess": "", + "components.Settings.SettingsMain.validationApplicationTitle": "", + "components.Settings.SettingsMain.validationApplicationUrl": "", + "components.Settings.SettingsMain.validationApplicationUrlTrailingSlash": "", + "components.Settings.SettingsMain.validationUrl": "", + "components.Settings.SettingsMain.validationUrlTrailingSlash": "", + "components.Settings.SettingsMain.youtubeUrl": "", + "components.Settings.SettingsMain.youtubeUrlTip": "", + "components.Settings.SettingsNetwork.apiRequestTimeout": "", + "components.Settings.SettingsNetwork.apiRequestTimeoutTip": "", + "components.Settings.SettingsNetwork.csrfProtection": "", + "components.Settings.SettingsNetwork.csrfProtectionHoverTip": "", + "components.Settings.SettingsNetwork.dnsCache": "", + "components.Settings.SettingsNetwork.dnsCacheForceMinTtl": "", + "components.Settings.SettingsNetwork.dnsCacheHoverTip": "", + "components.Settings.SettingsNetwork.dnsCacheTip": "", + "components.Settings.SettingsNetwork.docs": "", + "components.Settings.SettingsNetwork.forceIpv4First": "", + "components.Settings.SettingsNetwork.forceIpv4FirstTip": "", + "components.Settings.SettingsNetwork.network": "", + "components.Settings.SettingsNetwork.networkDisclaimer": "", + "components.Settings.SettingsNetwork.networksettingsDescription": "", + "components.Settings.SettingsNetwork.proxyBypassFilter": "", + "components.Settings.SettingsNetwork.proxyBypassFilterTip": "", + "components.Settings.SettingsNetwork.proxyBypassLocalAddresses": "", + "components.Settings.SettingsNetwork.proxyEnabledTip": "", + "components.Settings.SettingsNetwork.proxyPort": "", + "components.Settings.SettingsNetwork.proxyUser": "", + "components.Settings.SettingsNetwork.toastSettingsFailure": "", + "components.Settings.SettingsNetwork.toastSettingsSuccess": "", + "components.Settings.SettingsNetwork.validationProxyPort": "", + "components.Settings.SettingsUsers.defaultPermissions": "", + "components.Settings.SettingsUsers.defaultPermissionsTip": "", + "components.Settings.SettingsUsers.disabledMediaServerLoginWarning": "", + "components.Settings.SettingsUsers.localLogin": "", + "components.Settings.SettingsUsers.localLoginTip": "", + "components.Settings.SettingsUsers.loginMethodsTip": "", + "components.Settings.SettingsUsers.mediaServerLogin": "", + "components.Settings.SettingsUsers.mediaServerLoginTip": "", + "components.Settings.SettingsUsers.newPlexLogin": "", + "components.Settings.SettingsUsers.newPlexLoginTip": "", + "components.Settings.SettingsUsers.toastSettingsFailure": "", + "components.Settings.SettingsUsers.userSettingsDescription": "", + "components.Settings.SonarrModal.animeSeriesType": "", + "components.Settings.SonarrModal.animeTags": "", + "components.Settings.SonarrModal.animelanguageprofile": "", + "components.Settings.SonarrModal.animerootfolder": "", + "components.Settings.SonarrModal.apiKey": "", + "components.Settings.SonarrModal.baseUrl": "", + "components.Settings.SonarrModal.baseUrlHelp": "", + "components.Settings.SonarrModal.createsonarr": "", + "components.Settings.SonarrModal.enableSearchHelp": "", + "components.Settings.SonarrModal.externalUrl": "", + "components.Settings.SonarrModal.externalUrlHelp": "", + "components.Settings.SonarrModal.hostname": "", + "components.Settings.SonarrModal.languageprofile": "", + "components.Settings.SonarrModal.loadingTags": "", + "components.Settings.SonarrModal.loadinglanguageprofiles": "", + "components.Settings.SonarrModal.loadingprofiles": "", + "components.Settings.SonarrModal.loadingrootfolders": "", + "components.Settings.SonarrModal.rootfolder": "", + "components.Settings.SonarrModal.seasonfolders": "", + "components.Settings.SonarrModal.selectLanguageProfile": "", + "components.Settings.SonarrModal.selectQualityProfile": "", + "components.Settings.SonarrModal.selectRootFolder": "", + "components.Settings.SonarrModal.seriesType": "", + "components.Settings.SonarrModal.server4k": "", + "components.Settings.SonarrModal.server4kHelp": "", + "components.Settings.SonarrModal.servername": "", + "components.Settings.SonarrModal.ssl": "", + "components.Settings.SonarrModal.syncEnabled": "", + "components.Settings.SonarrModal.syncEnabledHelp": "", + "components.Settings.SonarrModal.tagRequests": "", + "components.Settings.SonarrModal.tagRequestsInfo": "", + "components.Settings.SonarrModal.tags": "", + "components.Settings.SonarrModal.testFirstLanguageProfiles": "", + "components.Settings.SonarrModal.testFirstQualityProfiles": "", + "components.Settings.SonarrModal.testFirstTags": "", + "components.Settings.SonarrModal.toastSonarrTestFailure": "", + "components.Settings.SonarrModal.toastSonarrTestSuccess": "", + "components.Settings.SonarrModal.validationApiKeyRequired": "", + "components.Settings.SonarrModal.validationApplicationUrl": "", + "components.Settings.SonarrModal.validationApplicationUrlTrailingSlash": "", + "components.Settings.SonarrModal.validationHostnameRequired": "", + "components.Settings.SonarrModal.validationLanguageProfileRequired": "", + "components.Settings.SonarrModal.validationProfileRequired": "", + "components.Settings.SonarrModal.validationRootFolderRequired": "", + "components.Settings.address": "", + "components.Settings.addrule": "", + "components.Settings.addsonarr": "", + "components.Settings.advancedTooltip": "", + "components.Settings.apiKey": "", + "components.Settings.blocklistedTagImportInstructions": "", + "components.Settings.chooseProvider": "", + "components.Settings.clearBlocklistedTagsConfirm": "", + "components.Settings.clickTest": "", + "components.Settings.connectionTestFailed": "", + "components.Settings.copyBlocklistedTags": "", + "components.Settings.copyBlocklistedTagsEmpty": "", + "components.Settings.copyBlocklistedTagsTip": "", + "components.Settings.default": "", + "components.Settings.default4k": "", + "components.Settings.deleteServer": "", + "components.Settings.deleteserverconfirm": "", + "components.Settings.enablessl": "", + "components.Settings.experimentalTooltip": "", + "components.Settings.failed": "", + "components.RequestCard.failedmodify": "", + "components.RequestCard.unknowntitle": "", + "components.Settings.failedToSaveMetadataSettings": "", + "components.Settings.general": "", + "components.Settings.hostname": "", + "components.Settings.importBlocklistedTagsTip": "", + "components.Settings.invalidurlerror": "", + "components.Settings.jellyfinForgotPasswordUrl": "", + "components.Settings.jellyfinSettings": "", + "components.RequestModal.QuotaDisplay.quotaLink": "", + "components.RequestModal.QuotaDisplay.quotaLinkUser": "", + "components.Settings.jellyfinSettingsDescription": "", + "components.Settings.jellyfinSettingsFailure": "", + "components.Settings.jellyfinSettingsSuccess": "", + "components.Settings.jellyfinSyncFailedAutomaticGroupedFolders": "", + "components.Settings.jellyfinSyncFailedNoLibrariesFound": "", + "components.Settings.jellyfinsettingsDescription": "", + "components.Settings.librariesRemaining": "", + "components.Settings.manualscanDescription": "", + "components.RequestModal.requestApproved": "", + "components.RequestModal.requestCancel": "", + "components.Settings.manualscanDescriptionJellyfin": "", + "components.Settings.manualscanJellyfin": "", + "components.Settings.mediaTypeMovie": "", + "components.Settings.mediaTypeSeries": "", + "components.Settings.menuAbout": "", + "components.Settings.metadataProviderSettings": "", + "components.RequestModal.requestseasons": "", + "components.Settings.metadataSettings": "", + "components.Settings.noDefaultServer": "", + "components.Settings.notificationAgentSettingsDescription": "", + "components.Settings.notifications": "", + "components.RequestModal.requestseasons4k": "", + "components.RequestModal.requestseries4ktitle": "", + "components.RequestModal.requestseriestitle": "", + "components.RequestModal.season": "", + "components.RequestModal.seasonnumber": "", + "components.Settings.notificationsettings": "", + "components.Settings.overrideRules": "", + "components.Settings.overrideRulesDescription": "", + "components.Settings.plexlibraries": "", + "components.Settings.plexlibrariesDescription": "", + "components.Selector.CertificationSelector.selectCountry": "", + "components.Selector.CertificationSelector.starttyping": "", + "components.Selector.ended": "", + "components.Selector.inProduction": "", + "components.Settings.plexsettings": "", + "components.Settings.port": "", + "components.Settings.providerStatus": "", + "components.Settings.saving": "", + "components.Settings.scanbackground": "", + "components.Settings.searchKeywords": "", + "components.Settings.seriesMetadataProvider": "", + "components.Settings.serverLocal": "", + "components.Settings.serverSecure": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestSending": "", + "components.Settings.serverpreset": "", + "components.Settings.serverpresetLoad": "", + "components.Settings.serverpresetManualMessage": "", + "components.Settings.serverpresetRefreshing": "", + "components.Settings.serviceSettingsDescription": "", + "components.Settings.services": "", + "components.Settings.settingUpPlexDescription": "", + "components.Settings.startscan": "", + "components.Settings.starttyping": "", + "components.Settings.syncJellyfin": "", + "components.Settings.tautulliApiKey": "", + "components.Settings.tautulliSettings": "", + "components.Settings.tautulliSettingsDescription": "", + "components.Settings.Notifications.NotificationsPushbullet.channelTag": "", + "components.Settings.tmdbProviderDoesnotWork": "", + "components.Settings.toastPlexConnecting": "", + "components.Settings.toastPlexConnectingFailure": "", + "components.Settings.toastPlexConnectingSuccess": "", + "components.Settings.toastPlexRefresh": "", + "components.Settings.toastPlexRefreshSuccess": "", + "components.Settings.toastTautulliSettingsFailure": "", + "components.Settings.toastTautulliSettingsSuccess": "", + "components.Settings.urlBase": "", + "components.Settings.validationPortRequired": "", + "components.Settings.validationUrl": "", + "components.Settings.validationUrlBaseLeadingSlash": "", + "components.Settings.validationUrlBaseTrailingSlash": "", + "components.Settings.valueRequired": "", + "components.Settings.webAppUrl": "", + "components.Settings.webAppUrlTip": "", + "components.Settings.webhook": "", + "components.Settings.webpush": "", + "components.Settings.yes": "", + "components.Setup.configemby": "", + "components.Setup.configplex": "", + "components.Setup.configuremediaserver": "", + "components.Setup.finish": "", + "components.Setup.finishing": "", + "components.Setup.librarieserror": "", + "components.Settings.Notifications.NotificationsPushbullet.validationTypes": "", + "components.Setup.servertype": "", + "components.Setup.setup": "", + "components.Settings.Notifications.NotificationsPushover.accessToken": "", + "components.Setup.signin": "", + "components.Setup.signinMessage": "", + "components.Setup.signinWithEmby": "", + "components.Setup.signinWithJellyfin": "", + "components.Setup.subtitle": "", + "components.Setup.welcome": "", + "components.StatusBadge.managemedia": "", + "components.StatusBadge.playonplex": "", + "components.StatusBadge.seasonepisodenumber": "", + "components.StatusBadge.seasonnumber": "", + "components.StatusBadge.status4k": "", + "components.StatusChecker.appUpdated": "", + "components.StatusChecker.appUpdatedDescription": "", + "components.StatusChecker.reloadApp": "", + "components.StatusChecker.restartRequired": "", + "components.StatusChecker.restartRequiredDescription": "", + "components.TitleCard.addToWatchList": "", + "components.TitleCard.cleardata": "", + "components.TitleCard.mediaerror": "", + "components.TitleCard.tmdbid": "", + "components.TitleCard.tvdbid": "", + "components.TitleCard.watchlistCancel": "", + "components.TitleCard.watchlistDeleted": "", + "components.TitleCard.watchlistSuccess": "", + "components.Settings.Notifications.NotificationsPushover.agentenabled": "", + "components.TvDetails.Season.somethingwentwrong": "", + "components.TvDetails.TvCast.fullseriescast": "", + "components.TvDetails.TvCrew.fullseriescrew": "", + "components.TvDetails.addtowatchlist": "", + "components.TvDetails.anime": "", + "components.TvDetails.cast": "", + "components.TvDetails.episodeCount": "", + "components.TvDetails.episodeRuntime": "", + "components.TvDetails.episodeRuntimeMinutes": "", + "components.TvDetails.overviewunavailable": "", + "components.TvDetails.play4k": "", + "components.TvDetails.productioncountries": "", + "components.TvDetails.recommendations": "", + "components.TvDetails.removefromwatchlist": "", + "components.TvDetails.reportissue": "", + "components.TvDetails.rtaudiencescore": "", + "components.TvDetails.rtcriticsscore": "", + "components.TvDetails.seasons": "", + "components.TvDetails.seasonstitle": "", + "components.TvDetails.showtype": "", + "components.TvDetails.similar": "", + "components.TvDetails.status4k": "", + "components.TvDetails.streamingproviders": "", + "components.TvDetails.tmdbuserscore": "", + "components.TvDetails.watchlistDeleted": "", + "components.TvDetails.watchlistError": "", + "components.TvDetails.watchlistSuccess": "", + "components.TvDetails.watchtrailer": "", + "components.UserList.accounttype": "", + "components.UserList.admin": "", + "components.UserList.autogeneratepassword": "", + "components.UserList.autogeneratepasswordTip": "", + "components.UserList.create": "", + "components.UserList.created": "", + "components.UserList.createlocaluser": "", + "components.UserList.creating": "", + "components.UserList.deleteconfirm": "", + "components.UserList.descending": "", + "components.UserList.edituser": "", + "components.UserList.email": "", + "components.UserList.importedPlexUsersNoPassword": "", + "components.UserList.importedUsersNoPassword": "", + "components.UserList.importfromplexerror": "", + "components.UserList.localuser": "", + "components.UserList.mediaServerUser": "", + "components.UserList.newJellyfinsigninenabled": "", + "components.UserList.newplexsigninenabled": "", + "components.UserList.noJellyfinuserstoimport": "", + "components.UserList.nouserstoimport": "", + "components.UserList.password": "", + "components.UserList.passwordinfodescription": "", + "components.UserList.plexuser": "", + "components.UserList.sortByRequests": "", + "components.UserList.sortByRole": "", + "components.UserList.sortByType": "", + "components.UserList.sortByUser": "", + "components.UserList.toggleSortDirection": "", + "components.UserList.toggleSortDirectionAria": "", + "components.UserList.user": "", + "components.UserList.usercreatedsuccess": "", + "components.Settings.Notifications.allowselfsigned": "", + "components.UserList.userdeleted": "", + "components.UserList.userfail": "", + "components.UserList.userlist": "", + "components.UserList.users": "", + "components.UserList.validationpasswordminchars": "", + "components.UserProfile.ProfileHeader.joindate": "", + "components.UserProfile.ProfileHeader.profile": "", + "components.UserProfile.ProfileHeader.settings": "", + "components.UserProfile.ProfileHeader.userid": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorExists": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorUnknown": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.passwordRequired": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.title": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.username": "", + "components.UserProfile.UserSettings.UserGeneralSettings.accounttype": "", + "components.UserProfile.UserSettings.UserGeneralSettings.admin": "", + "components.UserProfile.UserSettings.UserGeneralSettings.applanguage": "", + "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegionTip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.enableOverride": "", + "components.Settings.Notifications.validationMessageThreadId": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailure": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmail": "", + "components.UserProfile.UserSettings.UserGeneralSettings.validationemailformat": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.emailsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.emailsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.notificationsettings": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pgpPublicKey": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pgpPublicKeyTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessToken": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessTokenTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverApplicationTokenTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverUserKey": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverUserKeyTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoversettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoversettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramChatId": "", + "components.Settings.RadarrModal.tagRequestsInfo": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPgpPublicKey": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPushbulletAccessToken": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverApplicationToken": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramMessageThreadId": "", + "components.UserProfile.UserSettings.UserNotificationSettings.webpush": "", + "components.UserProfile.UserSettings.UserPasswordChange.confirmpassword": "", + "components.UserProfile.UserSettings.UserPasswordChange.localPasswordDescription": "", + "components.UserProfile.UserSettings.UserPasswordChange.newpassword": "", + "components.UserProfile.UserSettings.UserPasswordChange.noPasswordSet": "", + "components.UserProfile.UserSettings.UserPasswordChange.noPasswordSetOwnAccount": "", + "components.UserProfile.UserSettings.UserPasswordChange.toastSettingsFailure": "", + "components.UserProfile.UserSettings.UserPasswordChange.toastSettingsFailureVerifyCurrent": "", + "components.UserProfile.UserSettings.UserPasswordChange.toastSettingsSuccess": "", + "components.UserProfile.UserSettings.UserPasswordChange.validationConfirmPasswordSame": "", + "components.UserProfile.UserSettings.UserPasswordChange.validationCurrentPassword": "", + "components.UserProfile.UserSettings.UserPasswordChange.validationNewPassword": "", + "components.UserProfile.UserSettings.UserPasswordChange.validationNewPasswordLength": "", + "components.UserProfile.UserSettings.UserPermissions.permissions": "", + "components.UserProfile.UserSettings.UserPermissions.toastSettingsFailure": "", + "components.Settings.SettingsAbout.totalmedia": "", + "components.UserProfile.UserSettings.UserPermissions.toastSettingsSuccess": "", + "components.UserProfile.UserSettings.UserPermissions.unauthorizedDescription": "", + "components.UserProfile.UserSettings.menuChangePass": "", + "components.UserProfile.UserSettings.menuGeneralSettings": "", + "components.UserProfile.UserSettings.menuLinkedAccounts": "", + "components.UserProfile.UserSettings.menuNotifications": "", + "components.UserProfile.UserSettings.menuPermissions": "", + "components.UserProfile.UserSettings.unauthorizedDescription": "", + "components.UserProfile.emptywatchlist": "", + "components.UserProfile.recentlywatched": "", + "components.UserProfile.requestsperdays": "", + "components.UserProfile.seriesrequest": "", + "components.UserProfile.unlimited": "", + "i18n.advanced": "", + "i18n.approve": "", + "i18n.approved": "", + "i18n.blocklistError": "", + "i18n.blocklistSuccess": "", + "i18n.blocklisted": "", + "i18n.delete": "", + "i18n.deleted": "", + "i18n.deleting": "", + "i18n.delimitedlist": "", + "i18n.edit": "", + "i18n.error": "", + "i18n.failed": "", + "i18n.import": "", + "i18n.notificationLocale": "", + "i18n.restartRequired": "", + "i18n.resultsperpage": "", + "i18n.saving": "", + "i18n.settings": "", + "i18n.view": "Xem", + "pages.internalservererror": "", + "pages.oops": "", + "pages.pagenotfound": "", + "pages.returnHome": "Trở về trang chủ", + "pages.serviceunavailable": "", + "components.Settings.SettingsJobsCache.usersavatars": "", + "components.Settings.SettingsUsers.toastSettingsSuccess": "", + "components.Settings.SettingsUsers.tvRequestLimitLabel": "", + "components.Settings.is4k": "", + "components.Settings.menuServices": "", + "components.Settings.menuUsers": "", + "components.Settings.metadataSettingsSaved": "", + "components.Settings.no": "", + "components.Settings.radarrsettings": "", + "components.Settings.restartrequiredTooltip": "", + "components.Settings.save": "", + "components.Settings.scanning": "", + "components.TvDetails.nextAirDate": "", + "components.TvDetails.originallanguage": "", + "components.UserList.localLoginDisabled": "", + "components.UserList.owner": "", + "components.Discover.emptywatchlist": "", + "components.Discover.moviegenres": "", + "components.Discover.plexwatchlist": "", + "components.Discover.popularmovies": "", + "components.Discover.populartv": "", + "components.Discover.recentlyAdded": "", + "components.Discover.recentrequests": "", + "components.Discover.resetfailed": "", + "components.Discover.resetsuccess": "", + "components.Discover.resettodefault": "", + "components.Discover.tmdbmoviegenre": "", + "components.Discover.tmdbmoviekeyword": "", + "components.Discover.tmdbmoviestreamingservices": "", + "components.Discover.tvgenres": "", + "components.Discover.upcoming": "", + "components.Discover.upcomingmovies": "", + "components.DiscoverTvUpcoming.upcomingtv": "", + "components.Settings.Notifications.NotificationsWebhook.agentenabled": "", + "components.Settings.Notifications.NotificationsWebhook.customHeaders": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersAdd": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersAuthConflict": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersIncomplete": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersKey": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersRemove": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersTip": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersValue": "", + "components.Settings.Notifications.NotificationsWebhook.customJson": "", + "components.Settings.Notifications.NotificationsWebhook.resetPayload": "", + "components.Settings.Notifications.NotificationsWebhook.resetPayloadSuccess": "", + "components.Settings.Notifications.NotificationsWebhook.supportVariables": "", + "components.Settings.Notifications.NotificationsWebhook.supportVariablesTip": "", + "components.Settings.Notifications.NotificationsWebhook.templatevariablehelp": "", + "components.Settings.Notifications.NotificationsWebhook.toastWebhookTestFailed": "", + "components.Settings.Notifications.NotificationsWebhook.toastWebhookTestSuccess": "", + "components.Settings.Notifications.NotificationsWebhook.validationJsonPayloadRequired": "", + "components.Settings.Notifications.NotificationsWebhook.validationWebhookUrl": "", + "components.Settings.Notifications.NotificationsWebhook.webhookUrl": "", + "components.Settings.Notifications.NotificationsWebhook.webhookUrlTip": "", + "components.Settings.Notifications.NotificationsWebhook.webhooksettingsfailed": "", + "components.Settings.Notifications.NotificationsWebhook.webhooksettingssaved": "", + "components.Settings.Notifications.useUserLocale": "", + "components.Settings.Notifications.validationSmtpPortRequired": "", + "components.Settings.OverrideRuleModal.conditionsDescription": "", + "components.Settings.OverrideRuleModal.create": "", + "components.Settings.OverrideRuleModal.createrule": "", + "components.Settings.OverrideRuleModal.keywords": "", + "components.Settings.OverrideRuleModal.serviceDescription": "", + "components.Settings.OverrideRuleModal.settings": "", + "components.Settings.OverrideRuleModal.settingsDescription": "", + "components.Settings.OverrideRuleModal.tags": "", + "components.Settings.OverrideRuleTile.keywords": "", + "components.Settings.OverrideRuleTile.qualityprofile": "", + "components.Settings.OverrideRuleTile.rootfolder": "", + "components.Settings.OverrideRuleTile.tags": "", + "components.Settings.RadarrModal.inCinemas": "", + "components.Settings.RadarrModal.loadingprofiles": "", + "components.Settings.RadarrModal.loadingrootfolders": "", + "components.Settings.RadarrModal.minimumAvailability": "", + "components.Settings.RadarrModal.notagoptions": "", + "components.Settings.RadarrModal.port": "", + "components.Settings.RadarrModal.qualityprofile": "", + "components.Settings.RadarrModal.released": "", + "components.Settings.RadarrModal.rootfolder": "", + "components.Settings.RadarrModal.selectMinimumAvailability": "", + "components.Settings.RadarrModal.server4k": "", + "components.Settings.RadarrModal.server4kHelp": "", + "components.Settings.RadarrModal.syncEnabled": "", + "components.Settings.RadarrModal.syncEnabledHelp": "", + "components.Settings.RadarrModal.tags": "", + "components.Settings.SettingsJobsCache.cacheDescription": "", + "components.Settings.SettingsJobsCache.cacheksize": "", + "components.Settings.SettingsJobsCache.cachemisses": "", + "components.Settings.SettingsJobsCache.cachevsize": "", + "components.Settings.SettingsJobsCache.canceljob": "", + "components.Settings.SettingsJobsCache.dnsCacheDescription": "", + "components.Settings.SettingsJobsCache.dnscachename": "", + "components.Settings.SettingsJobsCache.download-sync": "", + "components.Settings.SettingsJobsCache.jobsDescription": "", + "components.Settings.SettingsJobsCache.jobsandcache": "", + "components.Settings.SettingsJobsCache.jobstarted": "", + "components.Settings.SettingsJobsCache.misses": "", + "components.Settings.SettingsJobsCache.plex-full-scan": "", + "components.Settings.SettingsLogs.filterInfo": "", + "components.Settings.SettingsLogs.filterWarn": "", + "components.Settings.SettingsLogs.label": "", + "components.Settings.SettingsLogs.level": "", + "components.Settings.SettingsLogs.pauseLogs": "", + "components.Settings.SettingsLogs.time": "", + "components.Settings.SettingsLogs.viewdetails": "", + "components.Settings.SettingsMain.apikeyCopied": "", + "components.Settings.SettingsMain.applicationTitle": "", + "components.Settings.SettingsNetwork.dnsCacheForceMaxTtl": "", + "components.Settings.SonarrModal.apiKeyHelp": "", + "components.Settings.SonarrModal.create4ksonarr": "", + "components.Settings.blocklistedTagImportTitle": "", + "components.Settings.noSpecialCharacters": "", + "components.Settings.notTested": "", + "components.Settings.validationApiKey": "", + "components.Settings.validationHostnameRequired": "", + "components.TvDetails.firstAirDate": "", + "components.TvDetails.manageseries": "", + "components.TvDetails.network": "", + "components.TvDetails.originaltitle": "", + "components.UserList.importedfromJellyfin": "", + "components.UserList.importfromJellyfin": "", + "components.UserList.importfrommediaserver": "", + "components.UserList.importfromplex": "", + "components.UserProfile.UserSettings.UserGeneralSettings.generalsettings": "", + "components.UserProfile.UserSettings.UserGeneralSettings.languageDefault": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.managedevices": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.nodevicestoshow": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.operatingsystem": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.unknown": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramChatIdTipLong": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadIdTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "", + "components.UserProfile.limit": "", + "components.UserProfile.localWatchlist": "", + "components.UserProfile.movierequests": "", + "components.UserProfile.pastdays": "", + "components.UserProfile.UserSettings.UserGeneralSettings.general": "", + "components.UserProfile.UserSettings.UserGeneralSettings.movierequestlimit": "", + "components.UserProfile.UserSettings.UserGeneralSettings.originallanguage": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmoviestip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseries": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseriestip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.region": "", + "components.UserProfile.UserSettings.UserGeneralSettings.regionTip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.save": "", + "components.UserProfile.UserSettings.UserGeneralSettings.saving": "", + "components.UserProfile.UserSettings.UserGeneralSettings.seriesrequestlimit": "", + "components.UserProfile.UserSettings.UserGeneralSettings.streamingRegion": "", + "components.UserProfile.UserSettings.UserGeneralSettings.streamingRegionTip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmailEmpty": "", + "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsSuccess": "", + "components.UserProfile.UserSettings.UserGeneralSettings.validationemailrequired": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.deleteFailed": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.errorUnknown": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.linkedAccounts": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.linkedAccountsHint": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.noLinkedAccounts": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.browser": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablewebpush": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablingwebpusherror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablewebpush": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleted": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleteerror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeendisabled": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeenenabled": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingsfailed": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordId": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdAdd": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdPlaceholder": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdRemove": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "", + "components.Settings.email": "", + "components.Settings.externalUrl": "", + "components.Settings.OverrideRuleModal.genres": "", + "components.Settings.SettingsLogs.resumeLogs": "", + "components.Settings.SettingsUsers.userSettings": "", + "i18n.all": "", + "components.Settings.syncing": "", + "components.Discover.DiscoverMovies.sortReleaseDateAsc": "", + "components.Discover.DiscoverMovies.sortTitleDesc": "", + "components.Discover.DiscoverMovies.sortTmdbRatingAsc": "", + "components.Discover.DiscoverMovies.sortTmdbRatingDesc": "", + "components.Discover.DiscoverNetwork.networkSeries": "", + "components.Discover.DiscoverSliderEdit.deletefail": "", + "components.Discover.DiscoverSliderEdit.enable": "", + "components.Discover.DiscoverTv.activefilters": "", + "components.Discover.DiscoverTv.discovertv": "", + "components.Discover.tmdbnetwork": "", + "components.IssueDetails.lastupdated": "", + "components.IssueDetails.openin4karr": "", + "components.IssueDetails.openinarr": "", + "components.IssueDetails.play4konplex": "", + "components.IssueModal.CreateIssueModal.toastSuccessCreate": "", + "components.IssueModal.CreateIssueModal.toastviewissue": "", + "components.IssueModal.CreateIssueModal.validationMessageRequired": "", + "components.IssueModal.CreateIssueModal.whatswrong": "", + "components.IssueModal.issueOther": "", + "components.IssueModal.issueVideo": "", + "components.LanguageSelector.languageServerDefault": "", + "components.LanguageSelector.originalLanguageDefault": "", + "components.Layout.LanguagePicker.displaylanguage": "", + "components.Layout.Sidebar.browsemovies": "", + "components.Layout.Sidebar.browsetv": "", + "components.Layout.UserDropdown.MiniQuotaDisplay.seriesrequests": "", + "components.Layout.UserDropdown.myprofile": "", + "components.Layout.UserDropdown.requests": "", + "components.Layout.SearchInput.searchPlaceholder": "", + "components.Layout.VersionStatus.outofdate": "", + "components.Selector.showless": "", + "components.Settings.Notifications.authPass": "", + "components.Discover.DiscoverSliderEdit.remove": "", + "components.Discover.DiscoverTvLanguage.languageSeries": "", + "components.Discover.DiscoverWatchlist.discoverwatchlist": "Danh sách theo dõi của bạn", + "components.Discover.FilterSlideover.activefilters": "", + "components.Discover.FilterSlideover.certification": "Đánh giá nội dung", + "components.Discover.FilterSlideover.ratingText": "Xếp hạng nằm trong khoảng từ {minValue} đến {maxValue}", + "components.Discover.FilterSlideover.runtime": "Thời gian chạy", + "components.Discover.FilterSlideover.runtimeText": "", + "components.Discover.FilterSlideover.status": "Trạng thái", + "components.Discover.FilterSlideover.streamingservices": "Dịch vụ phát trực tuyến", + "components.Discover.PlexWatchlistSlider.emptywatchlist": "", + "components.Discover.RecentlyAddedSlider.recentlyAdded": "", + "components.Discover.RecentRequestsSlider.unableToConnect": "", + "components.Discover.StudioSlider.studios": "", + "components.Discover.TvGenreSlider.tvgenres": "", + "components.Discover.createnewslider": "", + "components.Discover.customizediscover": "", + "components.Discover.discover": "", + "components.Layout.Sidebar.users": "", + "components.NotificationTypeSelector.mediadeclined": "", + "components.Settings.plex": "", + "components.IssueDetails.IssueDescription.deleteissue": "", + "components.IssueDetails.IssueDescription.description": "", + "components.IssueDetails.IssueDescription.edit": "", + "components.IssueDetails.allseasons": "", + "components.IssueDetails.closeissue": "", + "components.IssueDetails.closeissueandcomment": "", + "components.IssueDetails.commentplaceholder": "", + "components.IssueDetails.comments": "", + "components.IssueDetails.deleteissue": "", + "components.IssueDetails.deleteissueconfirm": "", + "components.IssueDetails.episode": "", + "components.IssueDetails.issuetype": "", + "components.IssueDetails.nocomments": "", + "components.IssueDetails.openedby": "", + "components.IssueList.IssueItem.seasons": "", + "components.IssueList.IssueItem.unknownissuetype": "", + "components.IssueList.IssueItem.viewissue": "", + "components.IssueList.issues": "", + "components.IssueList.sortAdded": "", + "components.IssueList.sortModified": "", + "components.IssueModal.CreateIssueModal.allepisodes": "", + "components.IssueModal.CreateIssueModal.allseasons": "", + "components.IssueModal.CreateIssueModal.episode": "", + "components.IssueModal.CreateIssueModal.extras": "", + "components.IssueModal.CreateIssueModal.problemepisode": "", + "components.IssueModal.CreateIssueModal.problemseason": "", + "components.IssueModal.CreateIssueModal.reportissue": "", + "components.IssueModal.CreateIssueModal.season": "", + "components.IssueModal.CreateIssueModal.submitissue": "", + "components.Layout.Sidebar.dashboard": "", + "components.Layout.Sidebar.issues": "", + "components.Layout.Sidebar.requests": "", + "components.Layout.Sidebar.settings": "", + "components.Layout.UserDropdown.signout": "", + "components.Layout.UserWarnings.emailInvalid": "", + "components.Layout.UserWarnings.emailRequired": "", + "components.Layout.UserWarnings.passwordRequired": "", + "components.Settings.SettingsNetwork.trustProxyTip": "", + "components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "", + "components.UserList.validationUsername": "", + "i18n.loading": "", + "i18n.movie": "", + "i18n.movies": "", + "i18n.noresults": "", + "i18n.partiallyavailable": "", + "i18n.pending": "", + "i18n.previous": "", + "i18n.processing": "", + "i18n.request": "", + "components.Setup.back": "", + "components.Discover.timeWindowDay": "", + "components.Discover.timeWindowWeek": "", + "components.Blocklist.mediaType": "Loại", + "components.TvDetails.viewfullcrew": "", + "components.ManageSlideOver.removearr4k": "", + "components.NotificationTypeSelector.mediaapproved": "", + "components.PermissionEdit.advancedrequest": "", + "components.PermissionEdit.manageblocklist": "", + "components.ResetPassword.passwordreset": "", + "components.Settings.Notifications.NotificationsNtfy.password": "", + "components.Settings.Notifications.NotificationsPushbullet.accessToken": "", + "components.Settings.RadarrModal.baseUrl": "", + "components.Settings.RadarrModal.externalUrl": "", + "components.Settings.invalidKeyword": "", + "components.Settings.menuMetadataProviders": "", + "components.Layout.UserWarnings.profileIncomplete": "", + "components.Layout.VersionStatus.commitsbehind": "", + "components.Layout.VersionStatus.streamdevelop": "", + "components.Layout.VersionStatus.streamstable": "", + "components.Login.adminerror": "", + "components.Login.credentialerror": "", + "components.Login.description": "", + "components.Login.email": "", + "components.Login.emailAlreadyTaken": "", + "components.Login.emailtooltip": "", + "components.Login.enablessl": "", + "components.Login.forgotpassword": "", + "components.Login.jellyfinLocalLoginHint": "", + "components.Login.noadminerror": "", + "components.Login.orsigninwith": "", + "components.Login.password": "", + "components.Login.port": "", + "components.Login.save": "", + "components.Login.saveFailed": "", + "components.Login.saving": "", + "components.Login.servertype": "", + "components.Login.signin": "", + "components.Login.signingin": "", + "components.Login.signinheader": "", + "components.Login.signinwithjellyfin": "", + "components.Login.signinwithoverseerr": "", + "components.Login.title": "", + "components.Login.urlBase": "", + "components.Login.validationusernamerequired": "", + "components.ManageSlideOver.manageModalClearMediaWarning": "", + "components.ManageSlideOver.manageModalMedia": "", + "components.ManageSlideOver.manageModalMedia4k": "", + "components.ManageSlideOver.manageModalNoRequests": "", + "components.ManageSlideOver.manageModalRemoveMediaWarning": "", + "components.ManageSlideOver.manageModalRequests": "", + "components.ManageSlideOver.manageModalTitle": "", + "components.ManageSlideOver.mark4kavailable": "", + "components.ManageSlideOver.markallseasons4kavailable": "", + "components.ManageSlideOver.markallseasonsavailable": "", + "components.ManageSlideOver.markavailable": "", + "components.ManageSlideOver.openarr": "", + "components.ManageSlideOver.openarr4k": "", + "components.ManageSlideOver.opentautulli": "", + "components.ManageSlideOver.pastdays": "", + "components.ManageSlideOver.playedby": "", + "components.ManageSlideOver.plays": "", + "components.MovieDetails.addtowatchlist": "", + "components.MovieDetails.budget": "", + "components.MovieDetails.cast": "", + "components.MovieDetails.digitalrelease": "", + "components.MovieDetails.downloadstatus": "", + "components.MovieDetails.overviewunavailable": "", + "components.MovieDetails.play": "", + "components.MovieDetails.play4k": "", + "components.MovieDetails.productioncountries": "", + "components.MovieDetails.recommendations": "", + "components.MovieDetails.releasedate": "", + "components.MovieDetails.removefromwatchlist": "", + "components.MovieDetails.reportissue": "", + "components.MovieDetails.revenue": "", + "components.MovieDetails.runtime": "", + "components.MovieDetails.showless": "", + "components.MovieDetails.showmore": "", + "components.MovieDetails.similar": "", + "components.MovieDetails.streamingproviders": "", + "components.MovieDetails.studio": "", + "components.MovieDetails.theatricalrelease": "", + "components.MovieDetails.tmdbuserscore": "", + "components.MovieDetails.viewfullcrew": "", + "components.MovieDetails.watchlistDeleted": "", + "components.MovieDetails.watchlistError": "", + "components.MovieDetails.watchlistSuccess": "", + "components.MovieDetails.watchtrailer": "", + "components.NotificationTypeSelector.adminissuereopenedDescription": "", + "components.NotificationTypeSelector.adminissueresolvedDescription": "", + "components.NotificationTypeSelector.issuecomment": "", + "components.NotificationTypeSelector.issuecommentDescription": "", + "components.NotificationTypeSelector.issuecreated": "", + "components.NotificationTypeSelector.mediaAutoApprovedDescription": "", + "components.NotificationTypeSelector.mediaautorequested": "", + "components.NotificationTypeSelector.mediaautorequestedDescription": "", + "components.NotificationTypeSelector.mediadeclinedDescription": "", + "components.NotificationTypeSelector.mediafailed": "", + "components.NotificationTypeSelector.mediarequested": "", + "components.NotificationTypeSelector.mediarequestedDescription": "", + "components.NotificationTypeSelector.notificationTypes": "", + "components.NotificationTypeSelector.userissuecommentDescription": "", + "components.NotificationTypeSelector.userissuecreatedDescription": "", + "components.NotificationTypeSelector.userissuereopenedDescription": "", + "components.NotificationTypeSelector.userissueresolvedDescription": "", + "components.NotificationTypeSelector.usermediaapprovedDescription": "", + "components.NotificationTypeSelector.usermediaavailableDescription": "", + "components.NotificationTypeSelector.usermediadeclinedDescription": "", + "components.NotificationTypeSelector.usermediafailedDescription": "", + "components.NotificationTypeSelector.usermediarequestedDescription": "", + "components.PermissionEdit.advancedrequestDescription": "", + "components.PermissionEdit.autoapprove": "", + "components.PermissionEdit.autoapprove4k": "", + "components.PermissionEdit.autoapprove4kDescription": "", + "components.PermissionEdit.autoapprove4kMovies": "", + "components.PermissionEdit.autoapprove4kMoviesDescription": "", + "components.PermissionEdit.autoapprove4kSeries": "", + "components.PermissionEdit.autoapprove4kSeriesDescription": "", + "components.PermissionEdit.autoapproveDescription": "", + "components.PermissionEdit.autoapproveMoviesDescription": "", + "components.PermissionEdit.autoapproveSeries": "", + "components.PermissionEdit.autoapproveSeriesDescription": "", + "components.PermissionEdit.autorequestMovies": "", + "components.PermissionEdit.autorequestMoviesDescription": "", + "components.PermissionEdit.autorequestSeries": "", + "components.PermissionEdit.autorequestSeriesDescription": "", + "components.PermissionEdit.blocklistedItems": "", + "components.PermissionEdit.createissuesDescription": "", + "components.PermissionEdit.manageissues": "", + "components.PermissionEdit.manageissuesDescription": "", + "components.PermissionEdit.managerequests": "", + "components.PermissionEdit.managerequestsDescription": "", + "components.PermissionEdit.request": "", + "components.PermissionEdit.request4k": "", + "components.PermissionEdit.request4kMovies": "", + "components.PermissionEdit.request4kTv": "", + "components.PermissionEdit.viewrecentDescription": "", + "components.PermissionEdit.viewrequests": "", + "components.PermissionEdit.viewrequestsDescription": "", + "components.RequestBlock.lastmodifiedby": "", + "components.RequestBlock.profilechanged": "", + "components.RequestBlock.requestdate": "", + "components.RequestBlock.requestedby": "", + "components.RequestBlock.requestoverrides": "", + "components.RequestBlock.seasons": "", + "components.RequestBlock.server": "", + "components.RequestButton.approve4krequests": "", + "components.RequestButton.approverequest": "", + "components.RequestButton.approverequest4k": "", + "components.RequestButton.approverequests": "", + "components.RequestButton.decline4krequests": "", + "components.RequestButton.declinerequest": "", + "components.RequestButton.declinerequest4k": "", + "components.RequestButton.declinerequests": "", + "components.RequestButton.requestmore": "", + "components.RequestButton.viewrequest": "", + "components.RequestButton.viewrequest4k": "", + "components.RequestCard.cancelrequest": "", + "components.RequestCard.editrequest": "", + "components.RequestCard.failedretry": "", + "components.RequestCard.mediaerror": "", + "components.RequestCard.seasons": "", + "components.RequestCard.tmdbid": "", + "components.RequestCard.tvdbid": "", + "components.RequestList.RequestItem.cancelRequest": "", + "components.RequestList.RequestItem.deleterequest": "", + "components.RequestList.RequestItem.editrequest": "", + "components.RequestList.RequestItem.failedmodify": "", + "components.RequestList.RequestItem.failedretry": "", + "components.RequestList.RequestItem.mediaerror": "", + "components.RequestList.RequestItem.modified": "", + "components.RequestList.RequestItem.modifieduserdate": "", + "components.RequestList.RequestItem.profileName": "", + "components.RequestList.RequestItem.removearr": "", + "components.RequestList.RequestItem.seasons": "", + "components.RequestList.RequestItem.tmdbid": "", + "components.RequestList.RequestItem.tvdbid": "", + "components.RequestList.RequestItem.unknowntitle": "", + "components.RequestList.requests": "", + "components.RequestList.showallrequests": "", + "components.RequestList.sortAdded": "", + "components.RequestList.sortDirection": "", + "components.RequestList.sortModified": "", + "components.RequestList.unableToConnect": "", + "components.RequestModal.AdvancedRequester.advancedoptions": "", + "components.RequestModal.AdvancedRequester.animenote": "", + "components.RequestModal.AdvancedRequester.default": "", + "components.RequestModal.AdvancedRequester.destinationserver": "", + "components.RequestModal.AdvancedRequester.folder": "", + "components.RequestModal.AdvancedRequester.languageprofile": "", + "components.RequestModal.AdvancedRequester.qualityprofile": "", + "components.RequestModal.AdvancedRequester.requestas": "", + "components.RequestModal.AdvancedRequester.rootfolder": "", + "components.RequestModal.AdvancedRequester.selecttags": "", + "components.RequestModal.AdvancedRequester.tags": "", + "components.RequestModal.QuotaDisplay.allowedRequests": "", + "components.RequestModal.QuotaDisplay.allowedRequestsUser": "", + "components.RequestModal.QuotaDisplay.movie": "", + "components.RequestModal.QuotaDisplay.requestsremaining": "", + "components.RequestModal.QuotaDisplay.requiredquota": "", + "components.RequestModal.QuotaDisplay.requiredquotaUser": "", + "components.RequestModal.QuotaDisplay.seasonlimit": "", + "components.RequestModal.SearchByNameModal.nomatches": "", + "components.RequestModal.SearchByNameModal.notvdbiddescription": "", + "components.RequestModal.approve": "", + "components.RequestModal.autoapproval": "", + "components.RequestModal.cancel": "", + "components.RequestModal.edit": "", + "components.RequestModal.errorediting": "", + "components.RequestModal.numberofepisodes": "", + "components.RequestModal.pendingapproval": "", + "components.RequestModal.pendingrequest": "", + "components.RequestModal.requestadmin": "", + "components.RequestModal.requestcancelled": "", + "components.RequestModal.requestcollection4ktitle": "", + "components.RequestModal.requestcollectiontitle": "", + "components.RequestModal.requestedited": "", + "components.RequestModal.requesterror": "", + "components.RequestModal.requestmovies": "", + "components.RequestModal.requestmovies4k": "", + "components.RequestModal.requestmovietitle": "", + "components.RequestModal.selectmovies": "", + "components.RequestModal.selectseason": "", + "components.ResetPassword.confirmpassword": "", + "components.ResetPassword.emailresetlink": "", + "components.ResetPassword.gobacklogin": "", + "components.ResetPassword.requestresetlinksuccessmessage": "", + "components.ResetPassword.resetpassword": "", + "components.ResetPassword.resetpasswordsuccessmessage": "", + "components.ResetPassword.validationpasswordmatch": "", + "components.ResetPassword.validationpasswordminchars": "", + "components.ResetPassword.validationpasswordrequired": "", + "components.Search.search": "", + "components.Search.searchresults": "", + "components.Selector.CertificationSelector.errorLoading": "", + "components.Selector.CertificationSelector.maxRating": "", + "components.Selector.CertificationSelector.minRating": "", + "components.Selector.CertificationSelector.noOptions": "", + "components.Selector.CertificationSelector.selectCertification": "", + "components.Selector.nooptions": "", + "components.Selector.pilot": "", + "components.Selector.planned": "", + "components.Selector.returningSeries": "", + "components.Selector.searchGenres": "", + "components.Selector.searchKeywords": "", + "components.Selector.searchStatus": "", + "components.Selector.searchStudios": "", + "components.Selector.searchUsers": "", + "components.Selector.showmore": "", + "components.Selector.starttyping": "", + "components.Settings.Notifications.NotificationsGotify.agentenabled": "", + "components.Settings.Notifications.NotificationsGotify.gotifysettingssaved": "", + "components.Settings.Notifications.NotificationsGotify.toastGotifyTestSending": "", + "components.Settings.Notifications.NotificationsGotify.toastGotifyTestSuccess": "", + "components.Settings.Notifications.NotificationsGotify.url": "", + "components.Settings.Notifications.NotificationsGotify.validationPriorityRequired": "", + "components.Settings.Notifications.NotificationsGotify.validationTokenRequired": "", + "components.Settings.Notifications.NotificationsGotify.validationTypes": "", + "components.Settings.Notifications.NotificationsGotify.validationUrlRequired": "", + "components.Settings.Notifications.NotificationsGotify.validationUrlTrailingSlash": "", + "components.Settings.Notifications.NotificationsNtfy.agentenabled": "", + "components.Settings.Notifications.NotificationsNtfy.embedPoster": "", + "components.Settings.Notifications.NotificationsNtfy.ntfysettingsfailed": "", + "components.Settings.Notifications.NotificationsNtfy.ntfysettingssaved": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestFailed": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestSuccess": "", + "components.Settings.Notifications.NotificationsNtfy.token": "", + "components.Settings.Notifications.NotificationsNtfy.tokenAuth": "", + "components.Settings.Notifications.NotificationsNtfy.topic": "", + "components.Settings.Notifications.NotificationsNtfy.url": "", + "components.Settings.Notifications.NotificationsNtfy.username": "", + "components.Settings.Notifications.NotificationsNtfy.usernamePasswordAuth": "", + "components.Settings.Notifications.NotificationsNtfy.validationNtfyTopic": "", + "components.Settings.Notifications.NotificationsNtfy.validationNtfyUrl": "", + "components.Settings.Notifications.NotificationsNtfy.validationPriorityRequired": "", + "components.Settings.Notifications.NotificationsNtfy.validationTypes": "", + "components.Settings.Notifications.NotificationsPushbullet.accessTokenTip": "", + "components.Settings.Notifications.NotificationsPushbullet.agentEnabled": "", + "components.Settings.Notifications.NotificationsPushbullet.pushbulletSettingsFailed": "", + "components.Settings.Notifications.NotificationsPushbullet.pushbulletSettingsSaved": "", + "components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestFailed": "", + "components.Settings.Notifications.NotificationsPushbullet.validationAccessTokenRequired": "", + "components.Settings.Notifications.NotificationsPushover.accessTokenTip": "", + "components.Settings.Notifications.NotificationsPushover.deviceDefault": "", + "components.Settings.Notifications.NotificationsPushover.embedPoster": "", + "components.Settings.Notifications.NotificationsPushover.pushoversettingsfailed": "", + "components.Settings.Notifications.NotificationsPushover.pushoversettingssaved": "", + "components.Settings.Notifications.NotificationsPushover.sound": "", + "components.Settings.Notifications.NotificationsPushover.toastPushoverTestFailed": "", + "components.Settings.Notifications.NotificationsPushover.toastPushoverTestSending": "", + "components.Settings.Notifications.NotificationsPushover.toastPushoverTestSuccess": "", + "components.Settings.Notifications.NotificationsPushover.userTokenTip": "", + "components.Settings.Notifications.NotificationsPushover.validationAccessTokenRequired": "", + "components.Settings.Notifications.NotificationsPushover.validationTypes": "", + "components.Settings.Notifications.NotificationsSlack.agentenabled": "", + "components.Settings.Notifications.NotificationsSlack.slacksettingsfailed": "", + "components.Settings.Notifications.NotificationsSlack.slacksettingssaved": "", + "components.Settings.Notifications.NotificationsSlack.validationTypes": "", + "components.Settings.Notifications.NotificationsSlack.validationWebhookUrl": "", + "components.Settings.Notifications.NotificationsSlack.webhookUrlTip": "", + "components.Settings.Notifications.NotificationsWebPush.httpsRequirement": "", + "components.Settings.Notifications.NotificationsWebPush.toastWebPushTestFailed": "", + "components.Settings.Notifications.NotificationsWebPush.toastWebPushTestSending": "", + "components.Settings.Notifications.botAPI": "", + "components.Settings.Notifications.botUsernameTip": "", + "components.Settings.Notifications.chatId": "", + "components.Settings.Notifications.chatIdTip": "", + "components.Settings.Notifications.discordsettingsfailed": "", + "components.Settings.Notifications.discordsettingssaved": "", + "components.Settings.Notifications.emailsettingsfailed": "", + "components.Settings.Notifications.emailsettingssaved": "", + "components.Settings.Notifications.embedPoster": "", + "components.Settings.Notifications.enableMentions": "", + "components.Settings.Notifications.encryption": "", + "components.Settings.Notifications.encryptionImplicitTls": "", + "components.Settings.Notifications.encryptionNone": "", + "components.Settings.Notifications.messageThreadId": "", + "components.Settings.Notifications.pgpPasswordTip": "", + "components.Settings.Notifications.pgpPrivateKeyTip": "", + "components.Settings.Notifications.sendSilently": "", + "components.Settings.Notifications.sendSilentlyTip": "", + "components.Settings.Notifications.senderName": "", + "components.Settings.Notifications.telegramsettingsfailed": "", + "components.Settings.Notifications.telegramsettingssaved": "", + "components.Settings.Notifications.toastDiscordTestFailed": "", + "components.Settings.Notifications.toastDiscordTestSending": "", + "components.Settings.Notifications.toastDiscordTestSuccess": "", + "components.Settings.Notifications.toastEmailTestFailed": "", + "components.Settings.Notifications.toastTelegramTestFailed": "", + "components.Settings.Notifications.toastTelegramTestSending": "", + "components.Settings.Notifications.toastTelegramTestSuccess": "", + "components.Settings.Notifications.userEmailRequired": "", + "components.Settings.Notifications.validationEmail": "", + "components.Settings.Notifications.validationPgpPrivateKey": "", + "components.Settings.Notifications.validationSmtpHostRequired": "", + "components.Settings.RadarrModal.apiKeyHelp": "", + "components.Settings.RadarrModal.validationApplicationUrl": "", + "components.Settings.RadarrModal.validationApplicationUrlTrailingSlash": "", + "components.Settings.RadarrModal.validationBaseUrlLeadingSlash": "", + "components.Settings.SettingsJobsCache.command": "", + "components.Settings.SettingsJobsCache.dnsCache": "", + "components.Settings.SettingsJobsCache.nextexecution": "", + "components.Settings.SettingsMain.enableSpecialEpisodes": "", + "components.Settings.SettingsNetwork.trustProxy": "", + "components.Settings.SettingsNetwork.validationApiRequestTimeout": "", + "components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "", + "components.Settings.operational": "", + "components.Settings.settings": "", + "components.Setup.configureservices": "", + "components.Setup.continue": "", + "components.UserProfile.UserSettings.UserGeneralSettings.localuser": "", + "components.UserProfile.UserSettings.UserGeneralSettings.mediaServerUser": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablingwebpusherror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.engine": "", + "components.UserProfile.UserSettings.UserNotificationSettings.sendSilently": "", + "components.UserProfile.UserSettings.UserNotificationSettings.sound": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorUnauthorized": "", + "components.UserProfile.recentrequests": "", + "components.Discover.DiscoverSliderEdit.deletesuccess": "", + "components.Discover.DiscoverStudio.studioMovies": "", + "components.Discover.DiscoverTv.sortTmdbRatingAsc": "Xếp hạng TMDB (Tăng dần)", + "components.Discover.FilterSlideover.filters": "Bộ lọc", + "components.Discover.FilterSlideover.releaseDate": "Ngày phát hành", + "components.Discover.NetworkSlider.networks": "Mạng", + "components.Discover.TvGenreList.seriesgenres": "", + "components.Discover.networks": "", + "components.IssueDetails.issuepagetitle": "", + "components.IssueModal.CreateIssueModal.toastFailedCreate": "", + "components.Login.back": "", + "components.Login.signinwithplex": "", + "components.Login.validationservertyperequired": "", + "components.ManageSlideOver.movie": "", + "components.RequestModal.pending4krequest": "", + "components.ResetPassword.password": "", + "components.NotificationTypeSelector.usermediaAutoApprovedDescription": "", + "components.Login.urlBaseHelp": "", + "components.Login.username": "", + "components.Login.validationEmailFormat": "", + "components.Login.validationEmailRequired": "", + "components.Login.validationPortRequired": "", + "components.Login.validationUrlBaseLeadingSlash": "", + "components.Login.validationemailformat": "", + "components.Login.validationemailrequired": "", + "components.Login.validationhostformat": "", + "components.Login.validationhostrequired": "", + "components.Login.validationpasswordrequired": "", + "components.ManageSlideOver.alltime": "", + "components.ManageSlideOver.downloadstatus": "", + "components.ManageSlideOver.manageModalAdvanced": "", + "components.ManageSlideOver.manageModalClearMedia": "", + "components.ManageSlideOver.manageModalIssues": "", + "components.MovieDetails.imdbuserscore": "", + "components.MovieDetails.managemovie": "", + "components.MovieDetails.openradarr4k": "", + "components.MovieDetails.originallanguage": "", + "components.MovieDetails.originaltitle": "", + "components.MovieDetails.overview": "", + "components.MovieDetails.physicalrelease": "", + "components.NotificationTypeSelector.mediaAutoApproved": "", + "components.NotificationTypeSelector.mediaapprovedDescription": "", + "components.PermissionEdit.autorequest": "", + "components.PermissionEdit.autorequestDescription": "", + "components.RequestCard.declinerequest": "", + "components.RequestCard.deleterequest": "", + "components.RequestModal.QuotaDisplay.movielimit": "", + "components.RequestModal.QuotaDisplay.notenoughseasonrequests": "", + "components.RequestModal.requestfrom": "", + "components.RequestModal.requestmovie4ktitle": "", + "components.Settings.Notifications.NotificationsGotify.priority": "", + "components.Settings.Notifications.NotificationsGotify.toastGotifyTestFailed": "", + "components.Settings.Notifications.NotificationsSlack.toastSlackTestFailed": "", + "components.Settings.Notifications.NotificationsSlack.toastSlackTestSending": "", + "components.Settings.Notifications.NotificationsSlack.toastSlackTestSuccess": "", + "components.Settings.Notifications.NotificationsWebPush.toastWebPushTestSuccess": "", + "components.Settings.Notifications.NotificationsWebPush.webpushsettingsfailed": "", + "components.Settings.Notifications.NotificationsWebPush.webpushsettingssaved": "", + "components.Settings.Notifications.agentenabled": "", + "components.Settings.Notifications.botAvatarUrl": "", + "components.Settings.Notifications.botUsername": "", + "components.Settings.Notifications.toastEmailTestSending": "", + "components.Settings.Notifications.toastEmailTestSuccess": "", + "components.Settings.OverrideRuleModal.users": "", + "components.Settings.OverrideRuleTile.conditions": "", + "components.Settings.OverrideRuleTile.genre": "", + "components.Settings.SettingsJobsCache.dnsCacheGlobalStats": "", + "components.Settings.SettingsJobsCache.process-blocklisted-tags": "", + "components.Settings.SettingsMain.hideBlocklistedTip": "", + "components.Settings.SettingsNetwork.proxyEnabled": "", + "components.Settings.SettingsUsers.movieRequestLimitLabel": "", + "components.Settings.SonarrModal.monitorNewItemsHelp": "", + "components.Settings.SonarrModal.notagoptions": "", + "components.Settings.SonarrModal.qualityprofile": "", + "components.Settings.blocklistedTagsText": "", + "components.RequestBlock.rootfolder": "", + "components.Settings.menuGeneralSettings": "", + "components.Settings.menuJellyfinSettings": "", + "components.Settings.menuJobs": "", + "components.Settings.menuLogs": "", + "components.Settings.menuNetwork": "", + "components.Settings.noDefault4kServer": "", + "components.Settings.noDefaultNon4kServer": "", + "components.Settings.nooptions": "", + "components.Settings.validationUrlTrailingSlash": "", + "components.TitleCard.watchlistError": "", + "components.TvDetails.Season.noepisodes": "", + "components.UserList.userssaved": "", + "components.UserList.validationEmail": "", + "components.UserProfile.UserSettings.UserGeneralSettings.originallanguageTip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.owner": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexuser": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.noPermissionDescription": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorExists": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.email": "", + "components.UserProfile.UserSettings.UserPasswordChange.nopermissionDescription": "", + "i18n.decline": "", + "i18n.importing": "", + "i18n.notrequested": "", + "i18n.partiallyblocklisted": "", + "i18n.removeFromBlocklistSuccess": "", + "i18n.request4k": "", + "i18n.requested": "", + "i18n.requesting": "", + "i18n.resolved": "", + "pages.errormessagewithcode": "", + "components.TvDetails.play": "", + "components.RequestModal.AdvancedRequester.notagoptions": "", + "components.RequestModal.alreadyrequested": "", + "components.RequestModal.requestSuccess": "", + "components.ResetPassword.email": "", + "components.ResetPassword.validationemailrequired": "", + "components.Settings.Notifications.NotificationsGotify.gotifysettingsfailed": "", + "components.Settings.Notifications.NotificationsSlack.webhookUrl": "", + "components.Settings.Notifications.NotificationsWebhook.authheader": "", + "components.Settings.Notifications.NotificationsWebhook.validationTypes": "", + "components.Settings.Notifications.NotificationsWebPush.agentenabled": "", + "components.Settings.Notifications.authUser": "", + "components.Settings.Notifications.smtpPort": "", + "components.Settings.OverrideRuleModal.editrule": "", + "components.Settings.SettingsAbout.Releases.latestversion": "", + "components.Settings.SettingsJobsCache.cachehits": "", + "components.Settings.SettingsLogs.showall": "", + "components.Settings.SettingsUsers.users": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.saving": "", + "components.UserProfile.UserSettings.UserGeneralSettings.displayName": "", + "components.UserProfile.UserSettings.UserNotificationSettings.deviceDefault": "", + "components.UserProfile.totalrequests": "", + "i18n.open": "", + "i18n.removefromBlocklist": "", + "i18n.save": "", + "components.Discover.DiscoverMovies.sortReleaseDateDesc": "", + "components.Discover.DiscoverMovies.sortTitleAsc": "", + "components.Discover.FilterSlideover.to": "", + "components.Discover.resetwarning": "", + "components.Discover.stopediting": "", + "components.Discover.studios": "", + "components.IssueDetails.leavecomment": "", + "components.IssueModal.CreateIssueModal.providedetail": "", + "components.IssueModal.issueAudio": "", + "components.IssueModal.issueSubtitles": "", + "components.MovieDetails.rtaudiencescore": "", + "components.MovieDetails.rtcriticsscore": "", + "components.NotificationTypeSelector.mediaavailable": "", + "components.NotificationTypeSelector.mediaavailableDescription": "", + "components.PermissionEdit.admin": "", + "components.PermissionEdit.adminDescription": "", + "components.PermissionEdit.request4kTvDescription": "", + "components.PermissionEdit.requestDescription": "", + "components.PermissionEdit.requestMovies": "", + "components.PermissionEdit.requestMoviesDescription": "", + "components.PermissionEdit.requestTv": "", + "components.Settings.SettingsJobsCache.cachekeys": "", + "components.Settings.SettingsJobsCache.cachename": "", + "components.Settings.SettingsLogs.filterDebug": "", + "component.BlocklistBlock.blocklistdate": "", + "component.BlocklistBlock.blocklistedby": "", + "component.BlocklistModal.blocklisting": "", + "components.Blocklist.blocklistNotFoundError": "", + "components.Blocklist.blocklistSettingsDescription": "", + "components.Blocklist.blocklistdate": "ngày", + "components.Blocklist.blocklistedby": "", + "components.Blocklist.blocklistsettings": "", + "components.Blocklist.filterBlocklistedTags": "", + "components.Blocklist.filterManual": "", + "components.Blocklist.mediaName": "Tên", + "components.Blocklist.mediaTmdbId": "", + "components.Blocklist.showAllBlocklisted": "", + "components.Layout.Sidebar.blocklist": "", + "components.Login.tipEmailHasTrailingWhitespace": "", + "components.Login.tipUsernameHasTrailingWhitespace": "", + "components.ManageSlideOver.tvshow": "", + "components.MediaSlider.ShowMoreCard.seemore": "", + "components.MetadataSelector.selectMetdataProvider": "", + "components.MetadataSelector.tmdbLabel": "", + "components.MetadataSelector.tvdbLabel": "", + "components.MovieDetails.MovieCast.fullcast": "", + "components.MovieDetails.markavailable": "", + "components.MovieDetails.openradarr": "", + "components.NotificationTypeSelector.issuecreatedDescription": "", + "components.NotificationTypeSelector.issuereopened": "", + "components.NotificationTypeSelector.issuereopenedDescription": "", + "components.NotificationTypeSelector.issueresolved": "", + "components.NotificationTypeSelector.issueresolvedDescription": "", + "components.NotificationTypeSelector.mediafailedDescription": "", + "components.PermissionEdit.blocklistedItemsDescription": "", + "components.PermissionEdit.manageblocklistDescription": "", + "components.PermissionEdit.request4kDescription": "", + "components.PermissionEdit.requestTvDescription": "", + "components.PermissionEdit.request4kMoviesDescription": "", + "components.PermissionEdit.users": "", + "components.PermissionEdit.usersDescription": "", + "components.PermissionEdit.viewblocklistedItems": "", + "components.PermissionEdit.viewblocklistedItemsDescription": "", + "components.PermissionEdit.viewissues": "", + "components.PermissionEdit.viewissuesDescription": "", + "components.PermissionEdit.viewrecent": "", + "components.PermissionEdit.viewwatchlists": "", + "components.PermissionEdit.viewwatchlistsDescription": "", + "components.PersonDetails.alsoknownas": "", + "components.PersonDetails.lifespan": "", + "components.PersonDetails.appearsin": "", + "components.PersonDetails.ascharacter": "", + "components.PersonDetails.birthdate": "", + "components.PersonDetails.crewmember": "", + "components.QuotaSelector.days": "", + "components.QuotaSelector.movieRequests": "", + "components.QuotaSelector.movies": "", + "components.QuotaSelector.tvRequests": "", + "components.QuotaSelector.unlimited": "", + "components.RequestList.RequestItem.requested": "", + "components.RequestList.RequestItem.requesteddate": "", + "components.Settings.Notifications.NotificationsWebhook.toastWebhookTestSending": "", + "components.Settings.Notifications.encryptionDefault": "", + "components.Settings.Notifications.encryptionOpportunisticTls": "", + "components.Settings.Notifications.encryptionTip": "", + "components.Settings.Notifications.messageThreadIdTip": "", + "components.Settings.Notifications.pgpPrivateKey": "", + "components.Settings.Notifications.smtpHost": "", + "components.Settings.Notifications.validationPgpPassword": "", + "components.Settings.SettingsJobsCache.flushdnscache": "", + "components.Settings.SettingsJobsCache.hitRate": "", + "components.Settings.SonarrModal.validationBaseUrlLeadingSlash": "", + "components.Settings.allChosenProvidersAreOperational": "", + "components.Settings.animeMetadataProvider": "", + "components.Settings.cancelscan": "", + "components.Settings.currentlibrary": "", + "components.Settings.jellyfinlibraries": "", + "components.Settings.manualscan": "", + "components.Settings.metadataProviderSelection": "", + "components.StatusBadge.openinarr": "", + "components.TvDetails.overview": "", + "components.TvDetails.seasonnumber": "", + "components.UserList.role": "", + "components.Discover.DiscoverTv.sortFirstAirDateAsc": "", + "components.Discover.DiscoverTv.sortFirstAirDateDesc": "", + "components.Discover.DiscoverTv.sortPopularityAsc": "Độ phổ biến tăng dần", + "components.Discover.DiscoverTv.sortPopularityDesc": "Độ phổ biến giảm dần", + "components.Discover.DiscoverTv.sortTitleAsc": "Tiêu đề (A-Z) tăng dần", + "components.Discover.DiscoverTv.sortTitleDesc": "Tiêu đề (A-Z) giảm dần", + "components.Discover.DiscoverTv.sortTmdbRatingDesc": "Xếp hạng TMDB (Giảm dần)", + "components.Discover.DiscoverTvGenre.genreSeries": "{genre} Series", + "components.Discover.DiscoverTvKeyword.keywordSeries": "{keywordTitle} Series", + "components.Discover.FilterSlideover.clearfilters": "Xóa các bộ lọc đang hoạt động", + "components.Discover.FilterSlideover.excludeKeywords": "Loại trừ từ khóa", + "components.Discover.FilterSlideover.firstAirDate": "Ngày phát sóng đầu tiên", + "components.Discover.FilterSlideover.from": "Từ", + "components.Discover.FilterSlideover.keywords": "Từ khóa", + "components.Discover.FilterSlideover.originalLanguage": "Ngôn ngữ gốc", + "components.Discover.FilterSlideover.studio": "", + "components.Discover.FilterSlideover.tmdbuserscore": "", + "components.Discover.FilterSlideover.tmdbuservotecount": "", + "components.Discover.FilterSlideover.voteCount": "", + "components.Discover.MovieGenreList.moviegenres": "", + "components.Discover.MovieGenreSlider.moviegenres": "Thể loại phim", + "components.Discover.PlexWatchlistSlider.plexwatchlist": "", + "components.Discover.tmdbsearch": "", + "components.Discover.tmdbstudio": "", + "components.Discover.tmdbtvgenre": "", + "components.Discover.tmdbtvkeyword": "", + "components.Discover.tmdbtvstreamingservices": "", + "components.Discover.trending": "", + "components.Discover.upcomingtv": "", + "components.Discover.updatefailed": "", + "components.Discover.updatesuccess": "", + "components.DownloadBlock.estimatedtime": "", + "components.DownloadBlock.formattedTitle": "", + "components.IssueDetails.IssueComment.areyousuredelete": "", + "components.IssueDetails.IssueComment.delete": "", + "components.IssueDetails.IssueComment.edit": "", + "components.IssueDetails.IssueComment.postedby": "", + "components.IssueDetails.IssueComment.postedbyedited": "", + "components.IssueDetails.IssueComment.validationComment": "", + "components.IssueDetails.allepisodes": "", + "components.IssueDetails.playonplex": "", + "components.IssueDetails.problemepisode": "", + "components.IssueDetails.problemseason": "", + "components.IssueDetails.reopenissue": "", + "components.IssueDetails.reopenissueandcomment": "", + "components.IssueDetails.season": "", + "components.IssueDetails.toasteditdescriptionfailed": "", + "components.IssueDetails.toasteditdescriptionsuccess": "", + "components.IssueDetails.toastissuedeleted": "", + "components.IssueDetails.toaststatusupdated": "", + "components.IssueDetails.toastissuedeletefailed": "", + "components.IssueDetails.toaststatusupdatefailed": "", + "components.IssueDetails.unknownissuetype": "", + "components.IssueList.IssueItem.descriptionpreview": "", + "components.IssueList.IssueItem.episodes": "", + "components.IssueList.IssueItem.issuestatus": "", + "components.IssueList.IssueItem.issuetype": "", + "components.IssueList.IssueItem.opened": "", + "components.IssueList.IssueItem.openeduserdate": "", + "components.IssueList.IssueItem.problemepisode": "", + "components.Layout.UserDropdown.settings": "", + "components.NotificationTypeSelector.adminissuecommentDescription": "", + "components.PermissionEdit.autoapproveMovies": "", + "components.Settings.Notifications.NotificationsWebPush.embedPoster": "", + "components.Settings.Notifications.botApiTip": "", + "components.Settings.Notifications.webhookUrl": "", + "components.Settings.SettingsNetwork.proxyPassword": "", + "components.Settings.SonarrModal.animequalityprofile": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverUserKey": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramChatId": "", + "components.UserProfile.UserSettings.UserPasswordChange.currentpassword": "", + "i18n.areyousure": "", + "i18n.available": "", + "i18n.back": "", + "i18n.collection": "", + "i18n.completed": "", + "components.Layout.UserDropdown.MiniQuotaDisplay.movierequests": "", + "components.Login.hostname": "", + "components.Login.initialsignin": "", + "components.Login.initialsigningin": "", + "components.Login.invalidurlerror": "", + "components.Login.loginerror": "", + "components.Login.loginwithapp": "", + "components.Login.validationUrlBaseTrailingSlash": "", + "components.Login.validationUrlTrailingSlash": "", + "components.QuotaSelector.seasons": "", + "components.RegionSelector.regionDefault": "", + "components.RegionSelector.regionServerDefault": "", + "components.RequestBlock.approve": "", + "components.RequestBlock.decline": "", + "components.RequestBlock.delete": "", + "components.RequestBlock.edit": "", + "components.RequestBlock.languageprofile": "", + "components.Settings.Notifications.emailsender": "", + "components.Settings.SettingsMain.streamingRegion": "", + "components.Settings.SettingsNetwork.networksettings": "", + "components.Settings.toastPlexRefreshFailure": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletsettingsfailed": "", + "components.ManageSlideOver.removearr": "", + "components.MovieDetails.mark4kavailable": "", + "components.Settings.Notifications.NotificationsGotify.token": "", + "components.Settings.SettingsJobsCache.dnscacheage": "", + "components.Settings.SettingsNetwork.proxyHostname": "", + "components.Settings.RadarrModal.create4kradarr": "", + "components.Settings.RadarrModal.enableSearch": "", + "components.Settings.SettingsJobsCache.cacheflushed": "", + "components.Settings.SettingsJobsCache.dnsCacheGlobalStatsDescription": "", + "components.Settings.SettingsJobsCache.dnscacheactiveaddress": "", + "components.Setup.configjellyfin": "", + "components.UserProfile.plexwatchlist": "", + "components.Settings.SettingsLogs.logDetails": "", + "components.Settings.SettingsLogs.logs": "", + "components.Settings.SettingsLogs.message": "", + "components.Settings.SettingsMain.originallanguage": "", + "components.Settings.SettingsNetwork.csrfProtectionTip": "", + "components.Settings.SettingsUsers.atLeastOneAuth": "", + "components.Settings.SettingsUsers.loginMethods": "", + "components.Settings.SonarrModal.validationBaseUrlTrailingSlash": "", + "components.Settings.SonarrModal.validationPortRequired": "", + "components.UserList.username": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.usernameRequired": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorUnauthorized": "", + "components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncmovies": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.type": "", + "components.UserProfile.UserSettings.UserNotificationSettings.pushoverApplicationToken": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadId": "", + "components.UserProfile.UserSettings.UserPasswordChange.password": "", + "components.UserProfile.UserSettings.UserPasswordChange.validationConfirmPassword": "", + "i18n.blocklistDuplicateError": "", + "components.Settings.SettingsMain.general": "", + "components.Settings.scan": "", + "i18n.experimental": "", + "i18n.next": "", + "i18n.showingresults": "", + "i18n.status": "Trạng thái", + "i18n.test": "Kiểm tra", + "i18n.testing": "", + "i18n.tvshow": "", + "i18n.tvshows": "", + "i18n.unavailable": "", + "i18n.usersettings": "", + "i18n.specials": "", + "components.PermissionEdit.createissues": "", + "components.RequestCard.approverequest": "", + "components.Settings.RadarrModal.tagRequests": "", + "i18n.retry": "", + "components.Settings.SettingsJobsCache.jobtype": "", + "components.StatusBadge.status": "", + "components.RequestButton.requestmore4k": "", + "components.RequestModal.QuotaDisplay.season": "", + "i18n.cancel": "", + "components.Settings.SettingsAbout.documentation": "", + "components.Settings.SettingsAbout.gettingsupport": "", + "components.Settings.SettingsAbout.githubdiscussions": "", + "components.Settings.SettingsAbout.outofdate": "", + "components.Settings.SettingsJobsCache.runnow": "", + "components.Settings.SettingsMain.toastApiKeySuccess": "", + "components.Settings.SonarrModal.enableSearch": "", + "components.Settings.SonarrModal.validationNameRequired": "", + "components.Settings.jellyfinSyncFailedGenericError": "", + "components.Settings.jellyfinlibrariesDescription": "", + "components.Settings.jellyfinsettings": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.password": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.save": "", + "components.UserProfile.UserSettings.UserGeneralSettings.email": "", + "components.UserProfile.UserSettings.UserGeneralSettings.user": "", + "components.Settings.SettingsJobsCache.dnsNoCacheEntries": "", + "components.Settings.SettingsJobsCache.jobScheduleEditSaved": "", + "components.Settings.SettingsMain.apikey": "", + "components.Settings.SonarrModal.add": "", + "components.Settings.SonarrModal.port": "", + "components.Settings.SonarrModal.testFirstRootFolders": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.description": "", + "components.UserProfile.UserSettings.UserGeneralSettings.role": "", + "components.Settings.SonarrModal.monitorNewItems": "", + "components.Settings.SonarrModal.selecttags": "", + "components.UserList.ascending": "", + "components.UserList.deleteuser": "", + "components.Settings.menuNotifications": "", + "components.Settings.menuPlexSettings": "", + "components.Settings.plexsettingsDescription": "", + "components.UserList.userdeleteerror": "", + "components.Settings.tip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.created": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.deletesubscription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.device": "", + "components.UserProfile.UserSettings.UserNotificationSettings.sendSilentlyDescription": "", + "i18n.canceling": "", + "i18n.close": "", + "i18n.addToBlocklist": "", + "components.Settings.Notifications.NotificationsNtfy.priority": "", + "components.MovieDetails.MovieCrew.fullcrew": "", + "components.Settings.Notifications.NotificationsPushover.validationUserTokenRequired": "", + "components.Settings.Notifications.NotificationsSlack.embedPoster": "", + "components.Settings.Notifications.pgpPassword": "", + "components.Settings.Notifications.webhookUrlTip": "", + "components.Settings.SettingsNetwork.proxySsl": "", + "components.Settings.SonarrModal.defaultserver": "", + "components.Settings.SonarrModal.edit4ksonarr": "", + "components.Settings.SonarrModal.editsonarr": "", + "components.Settings.SonarrModal.default4kserver": "", + "components.Settings.sonarrsettings": "", + "components.UserList.bulkedit": "", + "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegion": "", + "components.UserProfile.UserSettings.UserNotificationSettings.notifications": "", + "i18n.retrying": "", + "components.IssueList.showallissues": "", + "components.Selector.canceled": "", + "components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestSending": "", + "components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestSuccess": "", + "components.Settings.OverrideRuleModal.ruleUpdated": "", + "components.Settings.activeProfile": "", + "components.Settings.addradarr": "", + "components.Settings.notrunning": "", + "components.Settings.serverRemote": "", + "components.Settings.timeout": "", + "components.Settings.tvdbProviderDoesnotWork": "", + "components.Setup.signinWithPlex": "", + "components.UserList.importedfromplex": "", + "components.UserList.importfromJellyfinerror": "", + "i18n.blocklist": "", + "components.CollectionDetails.removefromblocklistpartialcount": "", + "components.Discover.DiscoverWatchlist.watchlist": "", + "components.Settings.Notifications.NotificationsPushover.userToken": "", + "components.Settings.ssl": "", + "components.UserList.sortBy": "", + "components.UserList.sortByJoined": "", + "components.UserList.totalrequests": "", + "components.UserList.usercreatedfailed": "", + "components.UserList.usercreatedfailedexisting": "", + "i18n.declined": "", + "pages.somethingwentwrong": "" } diff --git a/src/i18n/locale/zh_Hans.json b/src/i18n/locale/zh_Hans.json index 8330cb1eed..63efa33f46 100644 --- a/src/i18n/locale/zh_Hans.json +++ b/src/i18n/locale/zh_Hans.json @@ -848,9 +848,7 @@ "components.Settings.SettingsJobsCache.editJobScheduleSelectorHours": "每 {jobScheduleHours, plural, one {hour} other {{jobScheduleHours} hours}}", "components.Settings.tautulliSettings": "Tautulli 设置", "components.Settings.tautulliSettingsDescription": "可选配置您的 Tautulli 服务器设置。Seerr 会从 Tautulli 获取您的 Plex 媒体观看历史数据。", - "components.UserProfile.UserSettings.UserGeneralSettings.discordId": "Discord 用户ID", "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletsettingsfailed": "Pushbullet 通知设置保存失败。", - "components.UserProfile.UserSettings.UserGeneralSettings.discordIdTip": "您的Discord 用户 ID ", "components.IssueList.IssueItem.unknownissuetype": "未知", "components.IssueList.IssueItem.viewissue": "查看问题", "components.IssueList.issues": "问题", @@ -963,7 +961,6 @@ "components.Settings.SettingsJobsCache.editJobSchedule": "编辑作业", "components.Settings.SettingsJobsCache.jobScheduleEditFailed": "保存作业设置时出了点问题。", "components.Settings.validationUrl": "请输入有效的网址", - "components.UserProfile.UserSettings.UserGeneralSettings.validationDiscordId": "请输入有效的 Discord 用户 ID", "components.UserProfile.UserSettings.UserNotificationSettings.pushoverUserKey": "用户或群组令牌", "components.UserProfile.UserSettings.UserNotificationSettings.pushoversettingssaved": "Pushover 通知设置保存成功!", "components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverUserKey": "请输入有效的用户或群组令牌", @@ -1459,5 +1456,217 @@ "components.RequestList.unableToConnect": "无法连接到 {services},部分信息可能不可用。", "components.Blocklist.filterBlocklistedTags": "被屏蔽的标签", "i18n.removefromBlocklist": "从黑名单中移除", - "components.Settings.Notifications.NotificationsWebhook.customHeadersTip": "添加自定义 HTTP 标头以包含在 webhook 请求中" + "components.Settings.Notifications.NotificationsWebhook.customHeadersTip": "添加自定义 HTTP 标头以包含在 webhook 请求中", + "components.Settings.Notifications.usePublicLogo": "", + "components.Settings.Notifications.usePublicLogoTip": "", + "components.Settings.RadarrModal.baseUrlHelp": "", + "components.Settings.RadarrModal.enableSearchHelp": "", + "components.Settings.RadarrModal.externalUrlHelp": "", + "components.Settings.RadarrModal.server4kHelp": "", + "components.Settings.RadarrModal.syncEnabledHelp": "", + "components.Settings.SettingsAbout.contribute": "", + "components.Settings.SettingsAbout.supportseerr": "", + "components.Settings.SettingsJobsCache.dnsCache": "", + "components.Settings.SettingsJobsCache.dnsCacheDescription": "", + "components.Settings.SettingsJobsCache.dnsCacheGlobalStats": "", + "components.Settings.SettingsJobsCache.dnsCacheGlobalStatsDescription": "", + "components.Settings.SettingsJobsCache.dnscachehits": "", + "components.Settings.SettingsJobsCache.dnscachemisses": "", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorDays": "", + "components.Settings.SettingsJobsCache.hitRate": "", + "components.Settings.SettingsJobsCache.hits": "", + "components.Settings.SettingsJobsCache.process-blocklisted-tags": "", + "components.Settings.SettingsJobsCache.size": "", + "components.Settings.SettingsMain.blocklistLanguage": "", + "components.Settings.SettingsMain.blocklistLanguageTip": "", + "components.Settings.SettingsMain.blocklistRegion": "", + "components.Settings.SettingsMain.blocklistRegionTip": "", + "components.Settings.SettingsMain.blocklistedTagsTip": "", + "components.Settings.SettingsMain.hideAvailableTip": "", + "components.Settings.SettingsMain.hideBlocklisted": "", + "components.Settings.SettingsMain.validationUrl": "", + "components.Settings.SettingsMain.youtubeUrl": "", + "components.Settings.SettingsMain.youtubeUrlTip": "", + "components.Settings.SettingsNetwork.apiRequestTimeout": "", + "components.Settings.SettingsNetwork.apiRequestTimeoutTip": "", + "components.Settings.SettingsNetwork.dnsCacheForceMaxTtl": "", + "components.Settings.SettingsNetwork.dnsCacheForceMinTtl": "", + "components.Settings.SettingsNetwork.dnsCacheHoverTip": "", + "components.Settings.SettingsNetwork.dnsCacheTip": "", + "components.Settings.SettingsNetwork.proxyEnabledTip": "", + "components.Settings.SettingsNetwork.validationApiRequestTimeout": "", + "components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "", + "components.Settings.SettingsUsers.disabledMediaServerLoginWarning": "", + "components.Settings.SonarrModal.apiKeyHelp": "", + "components.Settings.SonarrModal.baseUrlHelp": "", + "components.Settings.SonarrModal.enableSearchHelp": "", + "components.Settings.SonarrModal.externalUrlHelp": "", + "components.Settings.SonarrModal.monitorNewItems": "", + "components.Settings.SonarrModal.monitorNewItemsHelp": "", + "components.Settings.SonarrModal.server4kHelp": "", + "components.Settings.blocklistedTagImportInstructions": "", + "components.Settings.blocklistedTagImportTitle": "", + "components.Settings.chooseProvider": "", + "components.Settings.operational": "", + "components.Settings.searchKeywords": "", + "components.Settings.seriesMetadataProvider": "", + "components.UserList.sortBy": "", + "components.UserList.sortByJoined": "", + "components.UserList.sortByRequests": "", + "components.UserList.sortByRole": "", + "components.RequestCard.failedmodify": "", + "components.RequestList.RequestItem.failedmodify": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.browser": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.created": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.unknown": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeendisabled": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeenenabled": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingsfailed": "", + "components.UserProfile.UserSettings.UserPasswordChange.localPasswordDescription": "", + "components.Settings.SettingsJobsCache.misses": "", + "i18n.completed": "", + "i18n.notificationLocale": "", + "i18n.removeFromBlocklistSuccess": "", + "components.Settings.tmdbProviderDoesnotWork": "", + "components.Discover.timeWindowWeek": "", + "components.Settings.Notifications.NotificationsWebPush.embedPoster": "", + "components.Settings.Notifications.NotificationsWebhook.customHeaders": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersAdd": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersAuthConflict": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersIncomplete": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersKey": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersRemove": "", + "components.Settings.Notifications.NotificationsWebhook.supportVariables": "", + "components.Settings.Notifications.NotificationsWebhook.supportVariablesTip": "", + "components.Settings.Notifications.NotificationsWebhook.webhookUrlTip": "", + "components.Settings.Notifications.useUserLocale": "", + "components.Settings.RadarrModal.apiKeyHelp": "", + "components.Settings.SettingsJobsCache.dnscacheactiveaddress": "", + "components.Settings.SettingsMain.blocklistedTags": "", + "components.Settings.SettingsMain.blocklistedTagsLimit": "", + "components.Settings.SettingsMain.blocklistedTagsLimitTip": "", + "components.Settings.SettingsMain.hideBlocklistedTip": "", + "components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "", + "components.Settings.SonarrModal.syncEnabledHelp": "", + "components.Settings.metadataProviderSelection": "", + "components.UserList.importedPlexUsersNoPassword": "", + "components.UserList.importedUsersNoPassword": "", + "i18n.blocklist": "", + "i18n.blocklistDuplicateError": "", + "i18n.blocklistSuccess": "", + "i18n.blocklisted": "", + "components.Discover.FilterSlideover.certification": "", + "components.Login.tipEmailHasTrailingWhitespace": "", + "components.Login.tipUsernameHasTrailingWhitespace": "", + "components.CollectionDetails.removefromblocklistpartialcount": "", + "components.Settings.Notifications.embedPoster": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleted": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingssaved": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdAdd": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdPlaceholder": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdRemove": "", + "components.Blocklist.mediaName": "", + "components.Blocklist.mediaTmdbId": "", + "components.PermissionEdit.blocklistedItems": "", + "components.Layout.UserWarnings.profileIncomplete": "", + "components.Login.emailAlreadyTaken": "", + "components.Login.jellyfinLocalLoginHint": "", + "components.Login.saveFailed": "", + "components.Selector.CertificationSelector.maxRating": "", + "components.Selector.CertificationSelector.minRating": "", + "components.Selector.CertificationSelector.noOptions": "", + "components.Settings.Notifications.NotificationsGotify.priority": "", + "components.Settings.SettingsJobsCache.dnscachename": "", + "components.Settings.SettingsJobsCache.failures": "", + "i18n.blocklistError": "", + "components.Login.urlBaseHelp": "", + "components.Selector.CertificationSelector.starttyping": "", + "components.Settings.no": "", + "components.UserList.descending": "", + "components.Settings.Notifications.NotificationsNtfy.password": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersValue": "", + "components.Settings.allChosenProvidersAreOperational": "", + "components.Settings.nooptions": "", + "components.UserList.toggleSortDirectionAria": "", + "components.Settings.SettingsNetwork.dnsCache": "", + "components.PermissionEdit.blocklistedItemsDescription": "", + "i18n.addToBlocklist": "", + "components.Settings.Notifications.NotificationsNtfy.agentenabled": "", + "components.Settings.Notifications.NotificationsNtfy.embedPoster": "", + "components.Settings.Notifications.NotificationsNtfy.ntfysettingsfailed": "", + "components.Settings.Notifications.NotificationsNtfy.ntfysettingssaved": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestSending": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestSuccess": "", + "components.Settings.Notifications.NotificationsNtfy.tokenAuth": "", + "components.Settings.Notifications.NotificationsNtfy.topic": "", + "components.Settings.Notifications.NotificationsNtfy.url": "", + "components.Settings.Notifications.NotificationsNtfy.username": "", + "components.Settings.Notifications.NotificationsNtfy.usernamePasswordAuth": "", + "components.Settings.Notifications.NotificationsNtfy.validationNtfyTopic": "", + "components.Settings.Notifications.NotificationsNtfy.validationNtfyUrl": "", + "components.Settings.Notifications.NotificationsNtfy.validationPriorityRequired": "", + "components.Settings.Notifications.NotificationsNtfy.validationTypes": "", + "components.Settings.Notifications.NotificationsPushover.embedPoster": "", + "components.Settings.animeMetadataProvider": "", + "components.Settings.blocklistedTagsText": "", + "components.Settings.clearBlocklistedTagsConfirm": "", + "components.Settings.copyBlocklistedTags": "", + "components.Settings.copyBlocklistedTagsEmpty": "", + "components.Settings.copyBlocklistedTagsTip": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.engine": "", + "components.Settings.importBlocklistedTagsTip": "", + "components.Settings.menuMetadataProviders": "", + "components.Settings.metadataProviderSettings": "", + "components.Settings.metadataSettings": "", + "components.Settings.metadataSettingsSaved": "", + "components.Settings.noSpecialCharacters": "", + "components.Settings.settings": "", + "components.Settings.starttyping": "", + "components.Settings.tvdbProviderDoesnotWork": "", + "components.Settings.valueRequired": "", + "components.Settings.yes": "", + "components.UserList.sortByType": "", + "components.UserList.sortByUser": "", + "components.UserList.toggleSortDirection": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.deletesubscription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.device": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablewebpush": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablingwebpusherror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablewebpush": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.operatingsystem": "", + "i18n.deleted": "", + "i18n.partiallyblocklisted": "", + "components.Settings.SettingsAbout.aboutseerr": "", + "components.MetadataSelector.tmdbLabel": "", + "components.Selector.CertificationSelector.selectCertification": "", + "components.Settings.Notifications.NotificationsSlack.embedPoster": "", + "components.Settings.SettingsJobsCache.dnscacheflushed": "", + "components.Settings.SettingsMain.validationUrlTrailingSlash": "", + "components.Settings.providerStatus": "", + "i18n.error": "", + "components.Discover.timeWindowDay": "", + "components.Settings.failed": "", + "components.Settings.SettingsJobsCache.ipv4Fallbacks": "", + "components.Settings.SettingsJobsCache.dnsNoCacheEntries": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablingwebpusherror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.managedevices": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.nodevicestoshow": "", + "components.Settings.clickTest": "", + "components.Settings.connectionTestFailed": "", + "components.UserList.ascending": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleteerror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.type": "", + "components.MetadataSelector.tvdbLabel": "", + "components.Settings.failedToSaveMetadataSettings": "", + "components.Settings.general": "", + "components.Settings.invalidKeyword": "", + "components.Settings.SettingsJobsCache.dnscacheage": "", + "components.Settings.Notifications.NotificationsNtfy.priority": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestFailed": "", + "components.Settings.Notifications.NotificationsNtfy.token": "", + "components.Settings.Notifications.NotificationsGotify.validationPriorityRequired": "", + "components.Settings.SettingsMain.apikeyCopied": "", + "components.Settings.notTested": "", + "components.Settings.SettingsJobsCache.flushdnscache": "" } diff --git a/src/i18n/locale/zh_Hant.json b/src/i18n/locale/zh_Hant.json index 29550455b0..5767bd2974 100644 --- a/src/i18n/locale/zh_Hant.json +++ b/src/i18n/locale/zh_Hant.json @@ -962,9 +962,6 @@ "components.Settings.validationUrl": "請輸入有效的網址", "components.UserList.newplexsigninenabled": "允許新的 Plex 登入 的設定目前已啟用。還沒匯入的 Plex 使用者也能登入。", "components.Settings.tautulliSettingsDescription": "關於 Tautulli 伺服器的設定。Jellyseerr 會從 Tautulli 獲取 Plex 媒體的觀看歷史記錄。", - "components.UserProfile.UserSettings.UserGeneralSettings.discordId": "Discord 使用者 ID", - "components.UserProfile.UserSettings.UserGeneralSettings.discordIdTip": "您的Discord 使用者 ID 號碼", - "components.UserProfile.UserSettings.UserGeneralSettings.validationDiscordId": "請輸入有效的 Discord 使用者 ID", "components.Settings.SettingsAbout.appDataPath": "資料目錄", "components.RequestBlock.languageprofile": "語言設定", "components.StatusChecker.appUpdated": "{applicationTitle} 軟體已更新", @@ -1235,5 +1232,441 @@ "i18n.blocklistError": "啊!出了點問題,請再試一次。", "i18n.blocklistDuplicateError": "{title} 已經在封鎖清單中", "i18n.blocklist": "封鎖", - "i18n.addToBlocklist": "加到封鎖清單" + "i18n.addToBlocklist": "加到封鎖清單", + "components.Settings.Notifications.usePublicLogo": "", + "components.Settings.Notifications.usePublicLogoTip": "", + "components.Settings.Notifications.validationMessageThreadId": "", + "components.Settings.Notifications.validationWebhookRoleId": "", + "components.Settings.Notifications.webhookRoleId": "", + "components.Settings.Notifications.webhookRoleIdTip": "", + "components.Settings.OverrideRuleModal.conditionsDescription": "", + "components.Settings.OverrideRuleModal.create": "", + "components.Settings.OverrideRuleModal.createrule": "", + "components.Settings.OverrideRuleModal.editrule": "", + "components.Settings.OverrideRuleModal.genres": "", + "components.Settings.OverrideRuleModal.keywords": "", + "components.Settings.OverrideRuleModal.notagoptions": "", + "components.Settings.OverrideRuleModal.qualityprofile": "", + "components.Settings.OverrideRuleModal.rootfolder": "", + "components.Settings.OverrideRuleModal.ruleCreated": "", + "components.Settings.OverrideRuleModal.ruleUpdated": "", + "components.Settings.OverrideRuleModal.selectQualityProfile": "", + "components.Settings.OverrideRuleModal.selectRootFolder": "", + "components.Settings.OverrideRuleModal.selectService": "", + "components.Settings.OverrideRuleModal.selecttags": "", + "components.Settings.OverrideRuleModal.service": "", + "components.Settings.OverrideRuleModal.serviceDescription": "", + "components.Settings.OverrideRuleModal.settingsDescription": "", + "components.Settings.OverrideRuleModal.tags": "", + "components.Settings.OverrideRuleModal.users": "", + "components.Settings.OverrideRuleTile.conditions": "", + "components.Settings.OverrideRuleTile.genre": "", + "components.Settings.OverrideRuleTile.qualityprofile": "", + "components.Settings.OverrideRuleTile.rootfolder": "", + "components.Settings.OverrideRuleTile.settings": "", + "components.Settings.OverrideRuleTile.tags": "", + "components.Settings.OverrideRuleTile.users": "", + "components.Settings.RadarrModal.apiKeyHelp": "", + "components.Settings.RadarrModal.server4kHelp": "", + "components.Settings.RadarrModal.syncEnabledHelp": "", + "components.RequestCard.failedmodify": "", + "components.RequestList.RequestItem.failedmodify": "", + "components.Settings.SettingsJobsCache.dnsCache": "", + "components.Settings.SettingsJobsCache.dnsCacheDescription": "", + "components.Settings.SettingsJobsCache.dnsCacheGlobalStats": "", + "components.Settings.SettingsJobsCache.dnsCacheGlobalStatsDescription": "", + "components.Settings.SettingsJobsCache.editJobScheduleSelectorDays": "", + "components.Settings.SettingsJobsCache.failures": "", + "components.Settings.SettingsJobsCache.hitRate": "", + "components.Settings.SettingsJobsCache.hits": "", + "components.Settings.SettingsJobsCache.jellyfin-recently-added-scan": "", + "components.Settings.SettingsJobsCache.misses": "", + "components.Settings.SettingsJobsCache.plex-refresh-token": "", + "components.Settings.SettingsJobsCache.process-blocklisted-tags": "", + "components.Selector.searchUsers": "", + "components.Settings.SettingsJobsCache.usersavatars": "", + "components.Settings.SettingsMain.blocklistLanguage": "", + "components.Settings.SettingsMain.blocklistLanguageTip": "", + "components.Settings.SettingsMain.blocklistRegion": "", + "components.Settings.SettingsMain.blocklistedTags": "", + "components.Settings.SettingsMain.blocklistedTagsLimit": "", + "components.Settings.SettingsMain.blocklistedTagsTip": "", + "components.Settings.SettingsMain.discoverRegion": "", + "components.Settings.SettingsMain.discoverRegionTip": "", + "components.Settings.Notifications.NotificationsPushover.embedPoster": "", + "components.Settings.SettingsMain.enableSpecialEpisodes": "", + "components.Settings.SettingsMain.hideAvailableTip": "", + "components.Settings.SettingsMain.hideBlocklisted": "", + "components.Settings.SettingsMain.hideBlocklistedTip": "", + "components.Settings.SettingsMain.streamingRegion": "", + "components.Settings.SettingsMain.streamingRegionTip": "", + "components.Settings.SettingsNetwork.apiRequestTimeoutTip": "", + "components.Settings.SettingsNetwork.csrfProtectionHoverTip": "", + "components.Settings.SettingsNetwork.csrfProtectionTip": "", + "components.Settings.SettingsNetwork.dnsCacheForceMaxTtl": "", + "components.Settings.SettingsNetwork.dnsCacheForceMinTtl": "", + "components.Settings.SettingsNetwork.dnsCacheHoverTip": "", + "components.Settings.SettingsNetwork.dnsCacheTip": "", + "components.Settings.SettingsNetwork.docs": "", + "components.Settings.SettingsNetwork.forceIpv4First": "", + "components.Settings.SettingsNetwork.network": "", + "components.Settings.SettingsNetwork.proxyBypassFilterTip": "", + "components.Settings.SettingsNetwork.proxyBypassLocalAddresses": "", + "components.Settings.SettingsNetwork.proxyEnabledTip": "", + "components.Settings.SettingsNetwork.proxyHostname": "", + "components.Settings.SettingsNetwork.proxyUser": "", + "components.Settings.SettingsNetwork.toastSettingsFailure": "", + "components.Settings.SettingsNetwork.trustProxyTip": "", + "components.Settings.SettingsNetwork.validationApiRequestTimeout": "", + "components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "", + "components.Settings.SettingsNetwork.validationProxyPort": "", + "components.Settings.SettingsUsers.atLeastOneAuth": "", + "components.Settings.SettingsUsers.disabledMediaServerLoginWarning": "", + "components.Settings.SonarrModal.apiKeyHelp": "", + "components.Settings.SonarrModal.baseUrlHelp": "", + "components.Settings.SonarrModal.externalUrlHelp": "", + "components.Settings.SonarrModal.monitorNewItems": "", + "components.Settings.SonarrModal.server4kHelp": "", + "components.Settings.SonarrModal.syncEnabledHelp": "", + "components.Settings.addrule": "", + "components.Settings.allChosenProvidersAreOperational": "", + "components.Settings.animeMetadataProvider": "", + "components.Settings.blocklistedTagImportInstructions": "", + "components.Settings.blocklistedTagImportTitle": "", + "components.Settings.blocklistedTagsText": "", + "components.Settings.chooseProvider": "", + "components.Settings.clearBlocklistedTagsConfirm": "", + "components.Settings.clickTest": "", + "components.Settings.copyBlocklistedTagsEmpty": "", + "components.Settings.copyBlocklistedTagsTip": "", + "components.Settings.failed": "", + "components.Settings.general": "", + "components.Settings.importBlocklistedTagsTip": "", + "components.Settings.invalidKeyword": "", + "components.Settings.invalidurlerror": "", + "components.Settings.jellyfinForgotPasswordUrl": "", + "components.Settings.jellyfinSettings": "", + "components.Settings.jellyfinSettingsFailure": "", + "components.Settings.jellyfinSyncFailedNoLibrariesFound": "", + "components.Settings.jellyfinlibraries": "", + "components.Settings.jellyfinlibrariesDescription": "", + "components.Settings.jellyfinsettingsDescription": "", + "components.Settings.manualscanDescriptionJellyfin": "", + "components.Settings.menuJellyfinSettings": "", + "components.Settings.menuMetadataProviders": "", + "components.Settings.menuNetwork": "", + "components.Settings.metadataProviderSelection": "", + "components.Settings.metadataProviderSettings": "", + "components.Settings.metadataSettings": "", + "components.Settings.operational": "", + "components.Settings.save": "", + "components.Settings.scanbackground": "", + "components.Settings.searchKeywords": "", + "components.Settings.seriesMetadataProvider": "", + "components.Settings.settings": "", + "components.Settings.starttyping": "", + "components.Settings.syncing": "", + "components.Settings.tvdbProviderDoesnotWork": "", + "components.Setup.back": "", + "components.Setup.configemby": "", + "components.Setup.configjellyfin": "", + "components.TvDetails.addtowatchlist": "", + "components.TvDetails.removefromwatchlist": "", + "components.TvDetails.watchlistSuccess": "", + "components.UserList.ascending": "", + "components.UserList.importedPlexUsersNoPassword": "", + "components.UserList.importedUsersNoPassword": "", + "components.UserList.importedfromJellyfin": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.description": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorExists": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorUnauthorized": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.save": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.linkedAccounts": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablingwebpusherror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadIdTip": "", + "components.UserProfile.UserSettings.UserPasswordChange.localPasswordDescription": "", + "i18n.specials": "", + "components.Settings.jellyfinsettings": "", + "components.UserList.sortByUser": "", + "components.UserList.toggleSortDirection": "", + "components.UserList.toggleSortDirectionAria": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.nodevicestoshow": "", + "components.Discover.timeWindowWeek": "", + "components.Settings.Notifications.NotificationsWebhook.customHeaders": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersAdd": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersAuthConflict": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersIncomplete": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersKey": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersRemove": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersValue": "", + "components.Settings.Notifications.NotificationsWebhook.supportVariables": "", + "components.Settings.Notifications.NotificationsWebhook.supportVariablesTip": "", + "components.Settings.Notifications.NotificationsWebhook.webhookUrlTip": "", + "components.Settings.Notifications.useUserLocale": "", + "components.Settings.OverrideRuleModal.conditions": "", + "components.Settings.RadarrModal.enableSearchHelp": "", + "components.Settings.RadarrModal.externalUrlHelp": "", + "components.Settings.SettingsAbout.contribute": "", + "components.Settings.SettingsJobsCache.size": "", + "components.Settings.SettingsMain.validationUrl": "", + "components.Settings.SettingsMain.validationUrlTrailingSlash": "", + "components.Settings.SettingsMain.youtubeUrl": "", + "components.Settings.SettingsMain.youtubeUrlTip": "", + "components.Settings.SettingsNetwork.apiRequestTimeout": "", + "components.Settings.SettingsNetwork.csrfProtection": "", + "components.Settings.SonarrModal.monitorNewItemsHelp": "", + "components.Settings.metadataSettingsSaved": "", + "components.Settings.noSpecialCharacters": "", + "components.Settings.notTested": "", + "components.Settings.overrideRules": "", + "components.Settings.overrideRulesDescription": "", + "components.Settings.valueRequired": "", + "components.Settings.yes": "", + "components.Setup.configplex": "", + "components.Setup.configuremediaserver": "", + "components.Setup.signin": "", + "components.Setup.signinWithEmby": "", + "components.TitleCard.watchlistDeleted": "", + "components.TitleCard.watchlistError": "", + "components.TitleCard.watchlistSuccess": "", + "components.UserList.descending": "", + "components.UserList.importfrommediaserver": "", + "components.UserList.mediaServerUser": "", + "components.UserList.noJellyfinuserstoimport": "", + "components.UserList.sortByRole": "", + "components.UserList.sortByType": "", + "components.UserList.username": "", + "components.UserList.validationUsername": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.password": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.title": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.username": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.usernameRequired": "", + "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegionTip": "", + "components.UserProfile.UserSettings.UserGeneralSettings.email": "", + "components.UserProfile.UserSettings.UserGeneralSettings.mediaServerUser": "", + "components.UserProfile.UserSettings.UserGeneralSettings.validationemailformat": "", + "components.UserProfile.UserSettings.UserGeneralSettings.validationemailrequired": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.deleteFailed": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.errorUnknown": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.linkedAccountsHint": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.noLinkedAccounts": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.noPermissionDescription": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorExists": "", + "components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorUnauthorized": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.browser": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.created": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.deletesubscription": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.device": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablewebpush": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablewebpush": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablingwebpusherror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.managedevices": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.operatingsystem": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleted": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleteerror": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.type": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.unknown": "", + "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramMessageThreadId": "", + "i18n.notificationLocale": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdAdd": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdPlaceholder": "", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdRemove": "", + "components.ManageSlideOver.removearr": "", + "components.ManageSlideOver.removearr4k": "", + "components.Login.invalidurlerror": "", + "components.IssueList.IssueItem.descriptionpreview": "", + "components.Blocklist.mediaName": "", + "components.Login.tipUsernameHasTrailingWhitespace": "", + "components.Login.save": "", + "components.TitleCard.watchlistCancel": "", + "components.Login.port": "", + "components.Selector.pilot": "", + "components.Discover.timeWindowDay": "", + "components.Login.tipEmailHasTrailingWhitespace": "", + "components.Settings.saving": "", + "components.Login.loginwithapp": "", + "components.Discover.FilterSlideover.excludeKeywords": "", + "components.CollectionDetails.removefromblocklistpartialcount": "", + "components.Login.saving": "", + "components.Settings.providerStatus": "", + "components.Blocklist.mediaTmdbId": "", + "components.Blocklist.mediaType": "", + "components.Layout.UserWarnings.profileIncomplete": "", + "components.Login.emailAlreadyTaken": "", + "components.Login.jellyfinLocalLoginHint": "", + "components.Login.noadminerror": "", + "components.Login.orsigninwith": "", + "components.Login.saveFailed": "", + "components.Login.servertype": "", + "components.Login.urlBase": "", + "components.Login.validationEmailFormat": "", + "components.Login.validationPortRequired": "", + "components.Login.validationUrlBaseLeadingSlash": "", + "components.Login.validationUrlTrailingSlash": "", + "components.ManageSlideOver.manageModalRemoveMediaWarning": "", + "components.MovieDetails.openradarr": "", + "components.MovieDetails.openradarr4k": "", + "components.MovieDetails.removefromwatchlist": "", + "components.PermissionEdit.blocklistedItems": "", + "components.RequestList.sortDirection": "", + "components.RequestList.unableToConnect": "", + "components.Selector.CertificationSelector.errorLoading": "", + "components.Selector.CertificationSelector.maxRating": "", + "components.Selector.CertificationSelector.minRating": "", + "components.Selector.CertificationSelector.noOptions": "", + "components.Selector.CertificationSelector.selectCertification": "", + "components.Selector.CertificationSelector.selectCountry": "", + "components.Selector.ended": "", + "components.Selector.inProduction": "", + "components.Selector.planned": "", + "components.Selector.returningSeries": "", + "components.Settings.Notifications.NotificationsNtfy.agentenabled": "", + "components.Settings.Notifications.NotificationsNtfy.embedPoster": "", + "components.Settings.Notifications.NotificationsNtfy.password": "", + "components.Settings.Notifications.NotificationsNtfy.priority": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestFailed": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestSending": "", + "components.Settings.Notifications.NotificationsNtfy.toastNtfyTestSuccess": "", + "components.Settings.Notifications.NotificationsNtfy.token": "", + "components.Settings.Notifications.NotificationsNtfy.tokenAuth": "", + "components.Settings.Notifications.NotificationsNtfy.topic": "", + "components.Settings.Notifications.NotificationsNtfy.username": "", + "components.Settings.Notifications.NotificationsNtfy.usernamePasswordAuth": "", + "components.Settings.Notifications.NotificationsNtfy.validationNtfyTopic": "", + "components.Settings.Notifications.NotificationsNtfy.validationNtfyUrl": "", + "components.Settings.Notifications.NotificationsNtfy.validationPriorityRequired": "", + "components.Settings.Notifications.NotificationsNtfy.validationTypes": "", + "components.Settings.Notifications.NotificationsSlack.embedPoster": "", + "components.Settings.Notifications.NotificationsWebPush.embedPoster": "", + "components.Settings.Notifications.embedPoster": "", + "components.Settings.Notifications.messageThreadId": "", + "components.Settings.Notifications.messageThreadIdTip": "", + "components.Settings.Notifications.userEmailRequired": "", + "components.Settings.OverrideRuleTile.keywords": "", + "components.Settings.OverrideRuleTile.language": "", + "components.Settings.SettingsJobsCache.jellyfin-full-scan": "", + "components.Settings.SettingsMain.blocklistRegionTip": "", + "components.Settings.SettingsNetwork.dnsCache": "", + "components.Settings.SonarrModal.enableSearchHelp": "", + "components.Settings.jellyfinSettingsDescription": "", + "components.UserProfile.UserSettings.UserGeneralSettings.discoverRegion": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.errorUnknown": "", + "components.Selector.canceled": "", + "components.Login.urlBaseHelp": "", + "components.Selector.CertificationSelector.starttyping": "", + "components.Settings.RadarrModal.baseUrlHelp": "", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramMessageThreadId": "", + "components.MovieDetails.watchlistError": "", + "components.Setup.signinWithJellyfin": "", + "components.Settings.jellyfinSyncFailedGenericError": "", + "components.Discover.FilterSlideover.certification": "", + "components.PermissionEdit.blocklistedItemsDescription": "", + "components.Settings.Notifications.NotificationsWebhook.customHeadersTip": "", + "components.Settings.OverrideRuleModal.settings": "", + "components.Settings.SettingsAbout.aboutseerr": "", + "i18n.error": "", + "components.Settings.SettingsMain.blocklistedTagsLimitTip": "", + "components.Settings.SettingsNetwork.proxyEnabled": "", + "components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "", + "components.Settings.apiKey": "", + "components.Settings.failedToSaveMetadataSettings": "", + "components.Settings.no": "", + "components.Settings.tip": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.passwordRequired": "", + "components.MetadataSelector.selectMetdataProvider": "", + "components.MetadataSelector.tmdbLabel": "", + "components.MetadataSelector.tvdbLabel": "", + "components.PermissionEdit.manageblocklist": "", + "components.PermissionEdit.manageblocklistDescription": "", + "components.PermissionEdit.viewblocklistedItems": "", + "components.PermissionEdit.viewblocklistedItemsDescription": "", + "components.Settings.timeout": "", + "components.Settings.tmdbProviderDoesnotWork": "", + "components.Settings.Notifications.NotificationsGotify.validationPriorityRequired": "", + "components.Settings.Notifications.NotificationsNtfy.ntfysettingsfailed": "", + "components.Settings.Notifications.NotificationsNtfy.ntfysettingssaved": "", + "components.Settings.Notifications.NotificationsNtfy.url": "", + "components.Settings.SettingsJobsCache.flushdnscache": "", + "components.Settings.SettingsNetwork.toastSettingsSuccess": "", + "components.Settings.connectionTestFailed": "", + "components.Settings.copyBlocklistedTags": "", + "components.Settings.jellyfinSettingsSuccess": "", + "components.Settings.jellyfinSyncFailedAutomaticGroupedFolders": "", + "components.Settings.manualscanJellyfin": "", + "components.Setup.servertype": "", + "components.Setup.subtitle": "", + "components.StatusBadge.seasonnumber": "", + "components.UserList.newJellyfinsigninenabled": "", + "components.Discover.FilterSlideover.status": "", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.engine": "", + "components.Login.initialsignin": "", + "components.MovieDetails.watchlistDeleted": "", + "components.Settings.OverrideRuleModal.languages": "", + "components.MovieDetails.watchlistSuccess": "", + "components.Settings.SettingsJobsCache.dnscacheactiveaddress": "", + "components.Settings.SettingsJobsCache.dnsNoCacheEntries": "", + "components.Settings.SettingsJobsCache.dnscacheflushed": "", + "components.Settings.SettingsJobsCache.dnscachehits": "", + "components.Settings.SettingsJobsCache.dnscachemisses": "", + "components.Settings.SettingsJobsCache.dnscachename": "", + "components.Settings.SettingsNetwork.forceIpv4FirstTip": "", + "components.Settings.SettingsNetwork.networksettings": "", + "components.Settings.SettingsNetwork.networksettingsDescription": "", + "components.Settings.SettingsNetwork.proxyBypassFilter": "", + "components.Settings.SettingsNetwork.proxyPassword": "", + "components.Settings.SettingsNetwork.proxyPort": "", + "components.Settings.SettingsNetwork.proxySsl": "", + "components.Setup.librarieserror": "", + "components.Setup.signinWithPlex": "", + "components.TvDetails.play": "", + "components.TvDetails.play4k": "", + "components.MovieDetails.play": "", + "components.TvDetails.watchlistDeleted": "", + "components.TvDetails.watchlistError": "", + "components.UserList.importfromJellyfin": "", + "components.UserList.importfromJellyfinerror": "", + "components.UserList.sortBy": "", + "components.UserList.sortByJoined": "", + "components.UserList.sortByRequests": "", + "components.TitleCard.addToWatchList": "", + "components.Login.title": "", + "components.Settings.SettingsUsers.loginMethods": "", + "components.RequestList.RequestItem.removearr": "", + "components.Selector.searchStatus": "", + "components.Settings.SettingsJobsCache.dnscacheage": "", + "components.Settings.nooptions": "", + "components.Settings.SettingsNetwork.networkDisclaimer": "", + "components.Login.signinwithjellyfin": "", + "components.Login.validationhostrequired": "", + "components.Settings.SettingsUsers.loginMethodsTip": "", + "components.Settings.SettingsUsers.mediaServerLogin": "", + "components.Settings.SettingsUsers.mediaServerLoginTip": "", + "components.UserProfile.UserSettings.LinkJellyfinModal.saving": "", + "components.Settings.SettingsMain.apikeyCopied": "", + "components.Login.validationEmailRequired": "", + "components.Settings.SettingsNetwork.trustProxy": "", + "components.Login.enablessl": "", + "components.Login.hostname": "", + "components.Login.initialsigningin": "", + "components.Settings.syncJellyfin": "", + "i18n.partiallyblocklisted": "", + "components.Login.validationUrlBaseTrailingSlash": "", + "components.Login.validationemailformat": "", + "components.Login.validationhostformat": "", + "components.Login.validationservertyperequired": "", + "components.Login.validationusernamerequired": "", + "components.MovieDetails.addtowatchlist": "", + "components.MovieDetails.downloadstatus": "", + "components.Settings.SettingsAbout.supportseerr": "", + "components.Discover.RecentRequestsSlider.unableToConnect": "", + "components.DiscoverTvUpcoming.upcomingtv": "", + "components.Login.adminerror": "", + "components.Login.back": "", + "components.Login.emailtooltip": "", + "components.Login.description": "", + "components.MovieDetails.play4k": "", + "components.Settings.Notifications.NotificationsGotify.priority": "", + "components.RequestList.RequestItem.profileName": "", + "components.Settings.SettingsJobsCache.ipv4Fallbacks": "" } diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 833ecad66b..6415c53bdc 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -11,6 +11,7 @@ import type { User } from '@app/hooks/useUser'; import { Permission, useUser } from '@app/hooks/useUser'; import '@app/styles/globals.css'; import { polyfillIntl } from '@app/utils/polyfillIntl'; +import { getHostAndPort } from '@app/utils/urlHelper'; import '@fontsource-variable/inter'; import { MediaServerType } from '@server/constants/server'; import type { PublicSettingsResponse } from '@server/interfaces/api/settingsInterfaces'; @@ -243,8 +244,10 @@ CoreApp.getInitialProps = async (initialProps) => { applicationUrl: '', hideAvailable: false, hideBlocklisted: false, + hideRequested: false, movie4kEnabled: false, series4kEnabled: false, + bookAudioEnabled: false, localLogin: true, mediaServerLogin: true, discoverRegion: '', @@ -253,6 +256,8 @@ CoreApp.getInitialProps = async (initialProps) => { mediaServerType: MediaServerType.NOT_CONFIGURED, partialRequestsEnabled: true, enableSpecialEpisodes: false, + syncBookFormatRequests: false, + seriesRequestsEnabled: true, cacheImages: false, vapidPublic: '', enablePushRegistration: false, @@ -260,15 +265,14 @@ CoreApp.getInitialProps = async (initialProps) => { emailEnabled: false, newPlexLogin: true, youtubeUrl: '', + versionCheck: true, plexClientIdentifier: '', }; if (ctx.res) { // Check if app is initialized and redirect if necessary const response = await axios.get( - `http://${process.env.HOST || 'localhost'}:${ - process.env.PORT || 5055 - }/api/v1/settings/public` + `http://${getHostAndPort()}/api/v1/settings/public` ); currentSettings = response.data; @@ -286,9 +290,7 @@ CoreApp.getInitialProps = async (initialProps) => { try { // Attempt to get the user by running a request to the local api const response = await axios.get( - `http://${process.env.HOST || 'localhost'}:${ - process.env.PORT || 5055 - }/api/v1/auth/me`, + `http://${getHostAndPort()}/api/v1/auth/me`, { headers: ctx.req && ctx.req.headers.cookie diff --git a/src/pages/_document.tsx b/src/pages/_document.tsx index e3e03eb0d0..6afc71344c 100644 --- a/src/pages/_document.tsx +++ b/src/pages/_document.tsx @@ -1,6 +1,8 @@ import type { DocumentContext, DocumentInitialProps } from 'next/document'; import Document, { Head, Html, Main, NextScript } from 'next/document'; +import type { JSX } from 'react'; + class MyDocument extends Document { static async getInitialProps( ctx: DocumentContext diff --git a/src/pages/author/[authorId]/index.tsx b/src/pages/author/[authorId]/index.tsx new file mode 100644 index 0000000000..71bbeeba40 --- /dev/null +++ b/src/pages/author/[authorId]/index.tsx @@ -0,0 +1,8 @@ +import AuthorDetails from '@app/components/AuthorDetails'; +import type { NextPage } from 'next'; + +const MoviePage: NextPage = () => { + return ; +}; + +export default MoviePage; diff --git a/src/pages/book/[bookId]/index.tsx b/src/pages/book/[bookId]/index.tsx new file mode 100644 index 0000000000..d1699f9409 --- /dev/null +++ b/src/pages/book/[bookId]/index.tsx @@ -0,0 +1,39 @@ +import BookDetails from '@app/components/BookDetails'; +import type { BookDetails as BookDetailsType } from '@server/models/Book'; +import axios from 'axios'; +import type { GetServerSideProps, NextPage } from 'next'; + +interface BookPageProps { + book?: BookDetailsType; +} + +const BookPage: NextPage = ({ book }) => { + return ; +}; + +export const getServerSideProps: GetServerSideProps = async ( + ctx +) => { + try { + const response = await axios.get( + `http://${process.env.HOST || 'localhost'}:${ + process.env.PORT || 5055 + }/api/v1/book/${ctx.query.bookId}`, + { + headers: ctx.req?.headers?.cookie + ? { cookie: ctx.req.headers.cookie } + : undefined, + } + ); + + return { + props: { + book: response.data, + }, + }; + } catch { + return { props: {} }; + } +}; + +export default BookPage; diff --git a/src/pages/collection/[collectionId]/index.tsx b/src/pages/collection/[collectionId]/index.tsx index e555e41e59..ff8bbbd898 100644 --- a/src/pages/collection/[collectionId]/index.tsx +++ b/src/pages/collection/[collectionId]/index.tsx @@ -1,4 +1,5 @@ import CollectionDetails from '@app/components/CollectionDetails'; +import { getHostAndPort } from '@app/utils/urlHelper'; import type { Collection } from '@server/models/Collection'; import axios from 'axios'; import type { GetServerSideProps, NextPage } from 'next'; @@ -15,9 +16,7 @@ export const getServerSideProps: GetServerSideProps< CollectionPageProps > = async (ctx) => { const response = await axios.get( - `http://${process.env.HOST || 'localhost'}:${ - process.env.PORT || 5055 - }/api/v1/collection/${ctx.query.collectionId}`, + `http://${getHostAndPort()}/api/v1/collection/${ctx.query.collectionId}`, { headers: ctx.req?.headers?.cookie ? { cookie: ctx.req.headers.cookie } diff --git a/src/pages/discover/books/index.tsx b/src/pages/discover/books/index.tsx new file mode 100644 index 0000000000..8d500d2adf --- /dev/null +++ b/src/pages/discover/books/index.tsx @@ -0,0 +1,8 @@ +import DiscoverBooks from '@app/components/Discover/DiscoverBooks'; +import type { NextPage } from 'next'; + +const DiscoverBooksPage: NextPage = () => { + return ; +}; + +export default DiscoverBooksPage; diff --git a/src/pages/movie/[movieId]/index.tsx b/src/pages/movie/[movieId]/index.tsx index d9788a2023..24fd3a826f 100644 --- a/src/pages/movie/[movieId]/index.tsx +++ b/src/pages/movie/[movieId]/index.tsx @@ -1,4 +1,5 @@ import MovieDetails from '@app/components/MovieDetails'; +import { getHostAndPort } from '@app/utils/urlHelper'; import type { MovieDetails as MovieDetailsType } from '@server/models/Movie'; import axios from 'axios'; import type { GetServerSideProps, NextPage } from 'next'; @@ -15,9 +16,7 @@ export const getServerSideProps: GetServerSideProps = async ( ctx ) => { const response = await axios.get( - `http://${process.env.HOST || 'localhost'}:${ - process.env.PORT || 5055 - }/api/v1/movie/${ctx.query.movieId}`, + `http://${getHostAndPort()}/api/v1/movie/${ctx.query.movieId}`, { headers: ctx.req?.headers?.cookie ? { cookie: ctx.req.headers.cookie } diff --git a/src/pages/series/[seriesId]/index.tsx b/src/pages/series/[seriesId]/index.tsx new file mode 100644 index 0000000000..698e173fa5 --- /dev/null +++ b/src/pages/series/[seriesId]/index.tsx @@ -0,0 +1,40 @@ +import SeriesDetails from '@app/components/SeriesDetails'; +import type { Series } from '@server/models/Series'; +import type { GetServerSideProps, NextPage } from 'next'; + +interface SeriesPageProps { + series?: Series; +} + +const SeriesPage: NextPage = ({ series }) => { + return ; +}; + +export const getServerSideProps: GetServerSideProps = async ( + ctx +) => { + try { + const res = await fetch( + `http://localhost:${process.env.PORT || 5055}/api/v1/series/${ + ctx.query.seriesId + }`, + { + headers: ctx.req?.headers?.cookie + ? { cookie: ctx.req.headers.cookie } + : undefined, + } + ); + if (!res.ok) throw new Error(); + const series: Series = await res.json(); + + return { + props: { + series, + }, + }; + } catch { + return { props: {} }; + } +}; + +export default SeriesPage; diff --git a/src/pages/tv/[tvId]/index.tsx b/src/pages/tv/[tvId]/index.tsx index 9659d82ca8..599a7985e0 100644 --- a/src/pages/tv/[tvId]/index.tsx +++ b/src/pages/tv/[tvId]/index.tsx @@ -1,4 +1,5 @@ import TvDetails from '@app/components/TvDetails'; +import { getHostAndPort } from '@app/utils/urlHelper'; import type { TvDetails as TvDetailsType } from '@server/models/Tv'; import axios from 'axios'; import type { GetServerSideProps, NextPage } from 'next'; @@ -15,9 +16,7 @@ export const getServerSideProps: GetServerSideProps = async ( ctx ) => { const response = await axios.get( - `http://${process.env.HOST || 'localhost'}:${ - process.env.PORT || 5055 - }/api/v1/tv/${ctx.query.tvId}`, + `http://${getHostAndPort()}/api/v1/tv/${ctx.query.tvId}`, { headers: ctx.req?.headers?.cookie ? { cookie: ctx.req.headers.cookie } diff --git a/src/utils/mediaTypeRoutes.ts b/src/utils/mediaTypeRoutes.ts new file mode 100644 index 0000000000..dc48fb60d2 --- /dev/null +++ b/src/utils/mediaTypeRoutes.ts @@ -0,0 +1,11 @@ +const BOOK_ROUTES = [ + '/book', + '/books', + '/author', + '/series', + '/discover/books', +] as const; + +export function isBookRoute(pathname: string): boolean { + return BOOK_ROUTES.some((route) => pathname.startsWith(route)); +} diff --git a/src/utils/pushSubscriptionHelpers.ts b/src/utils/pushSubscriptionHelpers.ts index 0a11d5dffc..4ded3565ae 100644 --- a/src/utils/pushSubscriptionHelpers.ts +++ b/src/utils/pushSubscriptionHelpers.ts @@ -69,7 +69,7 @@ export const verifyAndResubscribePushSubscription = async ( userId: number | undefined, currentSettings: PublicSettingsResponse ): Promise => { - if (!userId) { + if (!('serviceWorker' in navigator) || !userId) { return false; } @@ -80,37 +80,37 @@ export const verifyAndResubscribePushSubscription = async ( return true; } - if (subscription) { + if (!currentSettings.enablePushRegistration) { return false; } - if (currentSettings.enablePushRegistration) { - try { - const oldEndpoint = await unsubscribeToPushNotifications(userId); - - await subscribeToPushNotifications(userId, currentSettings); - - if (oldEndpoint) { - try { - await axios.delete( - `/api/v1/user/${userId}/pushSubscription/${encodeURIComponent( - oldEndpoint - )}` - ); - } catch { - // Ignore errors when deleting old endpoint (it might not exist) - } + try { + // If a browser-level subscription exists but doesn't belong to this + // user (or has stale VAPID keys), tear it down before resubscribing. + const oldEndpoint = subscription + ? await unsubscribeToPushNotifications(userId) + : null; + + await subscribeToPushNotifications(userId, currentSettings); + + if (oldEndpoint) { + try { + await axios.delete( + `/api/v1/user/${userId}/pushSubscription/${encodeURIComponent( + oldEndpoint + )}` + ); + } catch { + // Ignore errors when deleting old endpoint (it might not exist) } - - return true; - } catch (error) { - throw new Error(`[SW] Resubscribe failed: ${error.message}`, { - cause: error, - }); } - } - return false; + return true; + } catch (error) { + throw new Error(`[SW] Resubscribe failed: ${error.message}`, { + cause: error, + }); + } }; export const subscribeToPushNotifications = async ( diff --git a/src/utils/refreshIntervalHelper.test.ts b/src/utils/refreshIntervalHelper.test.ts new file mode 100644 index 0000000000..34c83c97b2 --- /dev/null +++ b/src/utils/refreshIntervalHelper.test.ts @@ -0,0 +1,127 @@ +import { MediaType } from '@server/constants/media'; +import type { DownloadingItem } from '@server/lib/downloadtracker'; +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import { getRequestDownloadStatus } from './refreshIntervalHelper'; + +const createDownloadItem = ( + overrides: Partial & Pick +): DownloadingItem => ({ + mediaType: MediaType.TV, + externalId: 1, + size: 1000, + sizeLeft: 500, + status: 'downloading', + timeLeft: '00:05:00', + estimatedCompletionTime: new Date('2026-01-01T00:00:00Z'), + title: 'Test Show', + ...overrides, +}); + +const getRequestStatus = ( + downloadStatus: DownloadingItem[] | undefined, + seasonNumbers: number[] +) => { + const downloadItem = getRequestDownloadStatus(downloadStatus, seasonNumbers); + + return { + downloadItem, + inProgress: downloadItem.length > 0, + }; +}; + +describe('getRequestDownloadStatus', () => { + it('returns an empty array when downloadStatus is undefined', () => { + const status = getRequestStatus(undefined, [1]); + + assert.deepEqual(status.downloadItem, []); + assert.equal(status.inProgress, false); + }); + + it('returns all items when seasonNumbers is empty', () => { + const movieDownload = createDownloadItem({ + downloadId: 'movie-1', + mediaType: MediaType.MOVIE, + }); + const tvDownload = createDownloadItem({ + downloadId: 'tv-1', + episode: { + id: 1, + seasonNumber: 2, + episodeNumber: 3, + absoluteEpisodeNumber: 3, + }, + }); + const downloads = [movieDownload, tvDownload]; + + const status = getRequestStatus(downloads, []); + + assert.deepEqual(status.downloadItem, downloads); + assert.equal(status.inProgress, true); + }); + + it('returns only items whose episode season matches the request seasons', () => { + const seasonOne = createDownloadItem({ + downloadId: 's1', + episode: { + id: 1, + seasonNumber: 1, + episodeNumber: 1, + absoluteEpisodeNumber: 1, + }, + }); + const seasonThree = createDownloadItem({ + downloadId: 's3', + episode: { + id: 3, + seasonNumber: 3, + episodeNumber: 1, + absoluteEpisodeNumber: 10, + }, + }); + const downloads = [seasonOne, seasonThree]; + + const status = getRequestStatus(downloads, [3]); + + assert.deepEqual(status.downloadItem, [seasonThree]); + assert.equal(status.inProgress, true); + }); + + it('returns an empty array when no items match the request seasons', () => { + const seasonThree = createDownloadItem({ + downloadId: 's3', + episode: { + id: 3, + seasonNumber: 3, + episodeNumber: 1, + absoluteEpisodeNumber: 10, + }, + }); + + const status = getRequestStatus([seasonThree], [1, 2]); + + assert.deepEqual(status.downloadItem, []); + assert.equal(status.inProgress, false); + }); + + it('excludes items without episode metadata when filtering by season', () => { + const withoutEpisode = createDownloadItem({ + downloadId: 'missing-episode', + }); + const seasonTwo = createDownloadItem({ + downloadId: 's2', + episode: { + id: 2, + seasonNumber: 2, + episodeNumber: 4, + absoluteEpisodeNumber: 4, + }, + }); + const downloads = [withoutEpisode, seasonTwo]; + + const status = getRequestStatus(downloads, [2]); + + assert.deepEqual(status.downloadItem, [seasonTwo]); + assert.equal(status.inProgress, true); + }); +}); diff --git a/src/utils/refreshIntervalHelper.ts b/src/utils/refreshIntervalHelper.ts index bdf67abc55..b427cd7e68 100644 --- a/src/utils/refreshIntervalHelper.ts +++ b/src/utils/refreshIntervalHelper.ts @@ -1,5 +1,20 @@ import type { DownloadingItem } from '@server/lib/downloadtracker'; +export const getRequestDownloadStatus = ( + downloadStatus: DownloadingItem[] | undefined, + seasonNumbers: number[] +): DownloadingItem[] => { + const items = downloadStatus ?? []; + + if (!seasonNumbers.length) { + return items; + } + + return items.filter( + (item) => item.episode && seasonNumbers.includes(item.episode.seasonNumber) + ); +}; + export const refreshIntervalHelper = ( downloadItem: { downloadStatus: DownloadingItem[] | undefined; diff --git a/src/utils/requestBookFormats.ts b/src/utils/requestBookFormats.ts new file mode 100644 index 0000000000..86118dae5c --- /dev/null +++ b/src/utils/requestBookFormats.ts @@ -0,0 +1,41 @@ +import type { RequestOverrides } from '@app/components/RequestModal/AdvancedRequester'; +import type { MediaRequest } from '@server/entity/MediaRequest'; +import axios from 'axios'; +import { mutate } from 'swr'; + +export interface BookFormatRequestOutcome { + is4k: boolean; + request?: MediaRequest; +} + +export const requestBookFormats = async ( + mediaId: number, + formats: boolean[], + overridesFor?: (is4k: boolean) => RequestOverrides | undefined +): Promise => { + const results = await Promise.allSettled( + formats.map((is4k) => { + const overrides = overridesFor?.(is4k); + + return axios.post('/api/v1/request', { + mediaId, + mediaType: 'book', + is4k, + serverId: overrides?.server, + profileId: overrides?.profile, + metadataProfileId: overrides?.metadataProfile, + rootFolder: overrides?.folder, + userId: overrides?.user?.id, + tags: overrides?.tags, + }); + }) + ); + + mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0'); + mutate('/api/v1/request/count'); + + return results.map((result, index) => ({ + is4k: formats[index], + request: result.status === 'fulfilled' ? result.value.data : undefined, + })); +}; diff --git a/src/utils/urlHelper.ts b/src/utils/urlHelper.ts new file mode 100644 index 0000000000..c79550a905 --- /dev/null +++ b/src/utils/urlHelper.ts @@ -0,0 +1,12 @@ +import { Address6 } from 'ip-address'; + +export function getHostAndPort(): string { + let host = process.env.HOST || 'localhost'; + if (Address6.isValid(host)) { + // If host is an IPv6 literal it needs to be placed in square brackets + host = `[${host}]`; + } + + const port = Number(process.env.PORT) || 5055; + return `${host}:${port}`; +} diff --git a/tsconfig.json b/tsconfig.json index 69e6d122d9..8fe25cc0b9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,10 +9,10 @@ "noEmit": true, "esModuleInterop": true, "module": "esnext", - "moduleResolution": "node", + "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "preserve", + "jsx": "react-jsx", "strictPropertyInitialization": false, "experimentalDecorators": true, "emitDecoratorMetadata": true,