diff --git a/.all-contributorsrc b/.all-contributorsrc index 91017932..8779bc16 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -7,7 +7,7 @@ "badgeTemplate": "\"All-orange.svg\"/>", "contributorsPerLine": 7, "projectName": "jellyseerr", - "projectOwner": "Fallenbagel", + "projectOwner": "fallenbagel", "repoType": "github", "repoHost": "https://github.com", "skipCi": true, @@ -439,6 +439,177 @@ "contributions": [ "code" ] + }, + { + "login": "M0NsTeRRR", + "name": "Ludovic Ortega", + "avatar_url": "https://avatars.githubusercontent.com/u/37785089?v=4", + "profile": "https://github.com/M0NsTeRRR", + "contributions": [ + "security" + ] + }, + { + "login": "j0srisk", + "name": "Joseph Risk", + "avatar_url": "https://avatars.githubusercontent.com/u/18372584?v=4", + "profile": "http://josephrisk.com", + "contributions": [ + "code" + ] + }, + { + "login": "Loetwiek", + "name": "Loetwiek", + "avatar_url": "https://avatars.githubusercontent.com/u/79059734?v=4", + "profile": "https://github.com/Loetwiek", + "contributions": [ + "code" + ] + }, + { + "login": "Fuochi", + "name": "Fuochi", + "avatar_url": "https://avatars.githubusercontent.com/u/4720478?v=4", + "profile": "https://github.com/Fuochi", + "contributions": [ + "doc" + ] + }, + { + "login": "demrich", + "name": "David Emrich", + "avatar_url": "https://avatars.githubusercontent.com/u/30092389?v=4", + "profile": "https://github.com/demrich", + "contributions": [ + "code" + ] + }, + { + "login": "maxnatamo", + "name": "Max T. Kristiansen", + "avatar_url": "https://avatars.githubusercontent.com/u/5898152?v=4", + "profile": "https://maxtrier.dk", + "contributions": [ + "code" + ] + }, + { + "login": "DamsDev1", + "name": "Damien Fajole", + "avatar_url": "https://avatars.githubusercontent.com/u/60252259?v=4", + "profile": "https://damsdev.me", + "contributions": [ + "code" + ] + }, + { + "login": "AhmedNSidd", + "name": "Ahmed Siddiqui", + "avatar_url": "https://avatars.githubusercontent.com/u/36286128?v=4", + "profile": "https://github.com/AhmedNSidd", + "contributions": [ + "code" + ] + }, + { + "login": "Zariel", + "name": "Chris Bannister", + "avatar_url": "https://avatars.githubusercontent.com/u/2213?v=4", + "profile": "https://github.com/Zariel", + "contributions": [ + "code" + ] + }, + { + "login": "C4J3", + "name": "Joe", + "avatar_url": "https://avatars.githubusercontent.com/u/13005453?v=4", + "profile": "https://github.com/C4J3", + "contributions": [ + "doc" + ] + }, + { + "login": "guillaumearnx", + "name": "Guillaume ARNOUX", + "avatar_url": "https://avatars.githubusercontent.com/u/37373941?v=4", + "profile": "https://me.garnx.fr", + "contributions": [ + "code" + ] + }, + { + "login": "dr-carrot", + "name": "dr-carrot", + "avatar_url": "https://avatars.githubusercontent.com/u/17272571?v=4", + "profile": "https://github.com/dr-carrot", + "contributions": [ + "code" + ] + }, + { + "login": "gageorsburn", + "name": "Gage Orsburn", + "avatar_url": "https://avatars.githubusercontent.com/u/4692734?v=4", + "profile": "https://github.com/gageorsburn", + "contributions": [ + "code" + ] + }, + { + "login": "GkhnGRBZ", + "name": "GkhnGRBZ", + "avatar_url": "https://avatars.githubusercontent.com/u/127258824?v=4", + "profile": "https://github.com/GkhnGRBZ", + "contributions": [ + "code" + ] + }, + { + "login": "benhaney", + "name": "Ben Haney", + "avatar_url": "https://avatars.githubusercontent.com/u/31331498?v=4", + "profile": "http://benhaney.com", + "contributions": [ + "code" + ] + }, + { + "login": "Wunderharke", + "name": "Wunderharke", + "avatar_url": "https://avatars.githubusercontent.com/u/5105672?v=4", + "profile": "https://github.com/Wunderharke", + "contributions": [ + "doc" + ] + }, + { + "login": "methbkts", + "name": "Metin Bektas", + "avatar_url": "https://avatars.githubusercontent.com/u/30674934?v=4", + "profile": "https://github.com/methbkts", + "contributions": [ + "infra" + ] + }, + { + "login": "andrewkolda", + "name": "andrewkolda", + "avatar_url": "https://avatars.githubusercontent.com/u/158614532?v=4", + "profile": "https://github.com/andrewkolda", + "contributions": [ + "design" + ] + }, + { + "login": "ishanjain28", + "name": "Ishan Jain", + "avatar_url": "https://avatars.githubusercontent.com/u/7921368?v=4", + "profile": "https://ishanjain.me", + "contributions": [ + "code" + ] } ] } diff --git a/.dockerignore b/.dockerignore index 21a5da86..5a009f2a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -18,7 +18,7 @@ config/logs/* config/*.json dist Dockerfile* -docker-compose.yml +compose.yaml docs LICENSE node_modules diff --git a/.gitattributes b/.gitattributes index eb5d2314..d9863caf 100644 --- a/.gitattributes +++ b/.gitattributes @@ -40,7 +40,7 @@ docs export-ignore .all-contributorsrc export-ignore .editorconfig export-ignore Dockerfile.local export-ignore -docker-compose.yml export-ignore +compose.yaml export-ignore stylelint.config.js export-ignore public/os_logo_filled.png export-ignore diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index a98da750..94a91e93 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -1,6 +1,6 @@ name: 🐛 Bug Report description: Report a problem -labels: ['type:bug', 'awaiting-triage'] +labels: ['bug', 'awaiting triage'] body: - type: markdown attributes: @@ -55,6 +55,16 @@ body: - tablet validations: required: true + - type: dropdown + id: database + attributes: + options: + - SQLite (default) + - PostgreSQL + label: Database + description: Which database backend are you using? + validations: + required: true - type: input id: device attributes: diff --git a/.github/ISSUE_TEMPLATE/enhancement.yml b/.github/ISSUE_TEMPLATE/enhancement.yml index 35a7adbd..4327a8f6 100644 --- a/.github/ISSUE_TEMPLATE/enhancement.yml +++ b/.github/ISSUE_TEMPLATE/enhancement.yml @@ -1,6 +1,6 @@ name: ✨ Feature Request description: Suggest an idea -labels: ['type:enhancement', 'awaiting-triage'] +labels: ['enhancement', 'awaiting triage'] body: - type: markdown attributes: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bd7baea3..44b07fd9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,8 +12,8 @@ jobs: test: name: Lint & Test Build if: github.event_name == 'pull_request' - runs-on: ubuntu-22.04 - container: node:20-alpine + runs-on: ubuntu-24.04 + container: node:22-alpine steps: - name: Checkout uses: actions/checkout@v4 @@ -43,15 +43,23 @@ jobs: - name: Build run: pnpm build - build_and_push: + build: name: Build & Publish Docker Images if: github.ref == 'refs/heads/develop' && !contains(github.event.head_commit.message, '[skip ci]') - runs-on: ubuntu-22.04 + strategy: + matrix: + include: + - runner: ubuntu-24.04 + platform: linux/amd64 + - runner: ubuntu-24.04-arm + platform: linux/arm64 + runs-on: ${{ matrix.runner }} + outputs: + digest-amd64: ${{ steps.set_outputs.outputs.digest-amd64 }} + digest-arm64: ${{ steps.set_outputs.outputs.digest-arm64 }} steps: - name: Checkout uses: actions/checkout@v4 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Log in to Docker Hub @@ -70,24 +78,77 @@ jobs: echo "OWNER_LC=${OWNER,,}" >>${GITHUB_ENV} env: OWNER: ${{ github.repository_owner }} - - name: Build and push + - name: Docker metadata + id: meta + uses: docker/metadata-action@v4 + with: + images: | + fallenbagel/jellyseerr + ghcr.io/${{ env.OWNER_LC }}/jellyseerr + tags: | + type=ref,event=branch + type=sha,prefix=,suffix=,format=short + - name: Build and push by digest + id: build uses: docker/build-push-action@v5 with: context: . file: ./Dockerfile - platforms: linux/amd64,linux/arm64 + platforms: ${{ matrix.platform }} push: true build-args: | COMMIT_TAG=${{ github.sha }} - tags: | - fallenbagel/jellyseerr:develop - ghcr.io/${{ env.OWNER_LC }}/jellyseerr:develop + outputs: | + type=image,push-by-digest=true,name=fallenbagel/jellyseerr,push=true + type=image,push-by-digest=true,name=ghcr.io/${{ env.OWNER_LC }}/jellyseerr,push=true + cache-from: type=gha,scope=${{ matrix.platform }} + cache-to: type=gha,mode=max,scope=${{ matrix.platform }} + provenance: false + - name: Set outputs + id: set_outputs + run: | + platform="${{ matrix.platform == 'linux/amd64' && 'amd64' || 'arm64' }}" + echo "digest-${platform}=${{ steps.build.outputs.digest }}" >> $GITHUB_OUTPUT + + merge_and_push: + name: Create and Push Multi-arch Manifest + needs: build + runs-on: ubuntu-24.04 + steps: + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Set lower case owner name + run: | + echo "OWNER_LC=${OWNER,,}" >>${GITHUB_ENV} + env: + OWNER: ${{ github.repository_owner }} + - name: Create and push manifest + run: | + docker manifest create fallenbagel/jellyseerr:develop \ + --amend fallenbagel/jellyseerr@${{ needs.build.outputs.digest-amd64 }} \ + --amend fallenbagel/jellyseerr@${{ needs.build.outputs.digest-arm64 }} + docker manifest push fallenbagel/jellyseerr:develop + + # GHCR manifest + docker manifest create ghcr.io/${{ env.OWNER_LC }}/jellyseerr:develop \ + --amend ghcr.io/${{ env.OWNER_LC }}/jellyseerr@${{ needs.build.outputs.digest-amd64 }} \ + --amend ghcr.io/${{ env.OWNER_LC }}/jellyseerr@${{ needs.build.outputs.digest-arm64 }} + docker manifest push ghcr.io/${{ env.OWNER_LC }}/jellyseerr:develop discord: name: Send Discord Notification - needs: build_and_push + needs: merge_and_push if: always() && github.event_name != 'pull_request' && !contains(github.event.head_commit.message, '[skip ci]') - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - name: Get Build Job Status uses: technote-space/workflow-conclusion-action@v3 diff --git a/.github/workflows/cypress.yml b/.github/workflows/cypress.yml index a7e3c68a..4dc75901 100644 --- a/.github/workflows/cypress.yml +++ b/.github/workflows/cypress.yml @@ -17,7 +17,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 - name: Pnpm Setup uses: pnpm/action-setup@v4 with: diff --git a/.github/workflows/helm.yml b/.github/workflows/helm.yml new file mode 100644 index 00000000..131e6102 --- /dev/null +++ b/.github/workflows/helm.yml @@ -0,0 +1,135 @@ +name: Release Charts + +on: + push: + branches: + - develop + +jobs: + package-helm-chart: + name: Package helm chart + runs-on: ubuntu-latest + permissions: + contents: read + packages: read + outputs: + has_artifacts: ${{ steps.check-artifacts.outputs.has_artifacts }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install helm + uses: azure/setup-helm@v4 + + - name: Install Oras + uses: oras-project/setup-oras@v1 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Package helm charts + run: | + mkdir -p ./.cr-release-packages + for chart_path in ./charts/*; do + if [ -d "$chart_path" ] && [ -f "$chart_path/Chart.yaml" ]; then + chart_name=$(grep '^name:' "$chart_path/Chart.yaml" | awk '{print $2}') + # get current version + current_version=$(grep '^version:' "$chart_path/Chart.yaml" | awk '{print $2}') + # try to get current release version + set +e + oras discover ghcr.io/${GITHUB_REPOSITORY@L}/${chart_name}:${current_version} + oras_exit_code=$? + set -e + + if [ $oras_exit_code -ne 0 ]; then + helm dependency build "$chart_path" + helm package "$chart_path" --destination ./.cr-release-packages + else + echo "No version change for $chart_name. Skipping." + fi + else + echo "Skipping $chart_name: Not a valid Helm chart" + fi + done + + - name: Check if artifacts exist + id: check-artifacts + run: | + if ls .cr-release-packages/* >/dev/null 2>&1; then + echo "has_artifacts=true" >> $GITHUB_OUTPUT + else + echo "has_artifacts=false" >> $GITHUB_OUTPUT + fi + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + if: steps.check-artifacts.outputs.has_artifacts == 'true' + with: + name: artifacts + include-hidden-files: true + path: .cr-release-packages/ + + publish: + name: Publish to ghcr.io + runs-on: ubuntu-latest + permissions: + packages: write # needed for pushing to github registry + id-token: write # needed for signing the images with GitHub OIDC Token + needs: [package-helm-chart] + if: needs.package-helm-chart.outputs.has_artifacts == 'true' + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install helm + uses: azure/setup-helm@v4 + + - name: Install Oras + uses: oras-project/setup-oras@v1 + + - name: Install Cosign + uses: sigstore/cosign-installer@v3 + + - name: Downloads artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts + path: .cr-release-packages/ + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Push charts to GHCR + env: + COSIGN_YES: true + run: | + for chart_path in `find .cr-release-packages -name '*.tgz' -print`; do + # push chart to OCI + chart_release_file=$(basename "$chart_path") + chart_name=${chart_release_file%-*} + helm push ${chart_path} oci://ghcr.io/${GITHUB_REPOSITORY@L} |& tee helm-push-output.log + chart_digest=$(awk -F "[, ]+" '/Digest/{print $NF}' < helm-push-output.log) + # sign chart + cosign sign "ghcr.io/${GITHUB_REPOSITORY@L}/${chart_name}@${chart_digest}" + # push artifacthub-repo.yml to OCI + oras push \ + ghcr.io/${GITHUB_REPOSITORY@L}/${chart_name}:artifacthub.io \ + --config /dev/null:application/vnd.cncf.artifacthub.config.v1+yaml \ + charts/$chart_name/artifacthub-repo.yml:application/vnd.cncf.artifacthub.repository-metadata.layer.v1.yaml \ + |& tee oras-push-output.log + artifacthub_digest=$(grep "Digest:" oras-push-output.log | awk '{print $2}') + # sign artifacthub-repo.yml + cosign sign "ghcr.io/${GITHUB_REPOSITORY@L}/${chart_name}:artifacthub.io@${artifacthub_digest}" + done diff --git a/.github/workflows/lint-helm-charts.yml b/.github/workflows/lint-helm-charts.yml new file mode 100644 index 00000000..e46a11ee --- /dev/null +++ b/.github/workflows/lint-helm-charts.yml @@ -0,0 +1,33 @@ +name: Lint and Test Charts + +on: + pull_request: + branches: + - develop + paths: + - '.github/workflows/lint-helm-charts.yml' + - 'charts/**' +jobs: + lint-test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up Helm + uses: azure/setup-helm@v4.2.0 + - name: Ensure documentation is updated + uses: docker://jnorwood/helm-docs:v1.14.2 + - name: Set up chart-testing + uses: helm/chart-testing-action@v2.6.1 + - 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" + fi + - name: Run chart-testing + if: steps.list-changed.outputs.changed == 'true' + run: ct lint --target-branch ${{ github.event.repository.default_branch }} --validate-maintainers=false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 465e8404..d0c36976 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,7 +16,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx @@ -26,6 +26,12 @@ jobs: with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_TOKEN }} + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GH_TOKEN }} - name: Pnpm Setup uses: pnpm/action-setup@v4 with: diff --git a/.github/workflows/test-docs-deploy.yml b/.github/workflows/test-docs-deploy.yml index bba30545..5526af09 100644 --- a/.github/workflows/test-docs-deploy.yml +++ b/.github/workflows/test-docs-deploy.yml @@ -4,7 +4,7 @@ on: pull_request: branches: - develop - path: + paths: - 'docs/**' - 'gen-docs/**' diff --git a/.gitignore b/.gitignore index 9a8925ab..c417acb0 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,7 @@ yarn-error.log* # database config/db/*.sqlite3* config/settings.json +config/settings.old.json # logs config/logs/*.log* diff --git a/.prettierignore b/.prettierignore index e7f72ab6..c2e778c1 100644 --- a/.prettierignore +++ b/.prettierignore @@ -9,3 +9,6 @@ pnpm-lock.yaml src/assets/ public/ docs/ + +# helm charts +**/charts diff --git a/.prettierrc.js b/.prettierrc.js index 1de1f8bf..10da08eb 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -15,5 +15,11 @@ module.exports = { rangeEnd: 0, // default: Infinity }, }, + { + files: 'charts/**', + options: { + rangeEnd: 0, // default: Infinity + }, + }, ], }; diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c5f768c2..ab5f59da 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,7 @@ All help is welcome and greatly appreciated! If you would like to contribute to - HTML/Typescript/Javascript editor - [VSCode](https://code.visualstudio.com/) is recommended. Upon opening the project, a few extensions will be automatically recommended for install. -- [NodeJS](https://nodejs.org/en/download/) (Node 20.x) +- [NodeJS](https://nodejs.org/en/download/) (Node 22.x) - [Pnpm](https://pnpm.io/cli/install) - [Git](https://git-scm.com/downloads) @@ -48,11 +48,11 @@ All help is welcome and greatly appreciated! If you would like to contribute to 4. Run the development environment: ```bash - pnpm + pnpm install pnpm dev ``` - - Alternatively, you can use [Docker](https://www.docker.com/) with `docker-compose up -d`. This method does not require installing NodeJS or Yarn on your machine directly. + - Alternatively, you can use [Docker](https://www.docker.com/) with `docker compose up -d`. This method does not require installing NodeJS or Yarn on your machine directly. 5. Create your patch and test your changes. @@ -101,6 +101,46 @@ We use [Weblate](https://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-f Translation status +## Migrations + +If you are adding a new feature that requires a database migration, you will need to create 2 migrations: one for SQLite and one for PostgreSQL. Here is how you could do it: + +1. Create a PostgreSQL database or use an existing one: + +```bash +sudo docker run --name postgres-jellyseerr -e POSTGRES_PASSWORD=postgres -d -p 127.0.0.1:5432:5432/tcp postgres:latest +``` + +2. Reset the SQLite database and the PostgreSQL database: + +```bash +rm config/db/db.* +rm config/settings.* +PGPASSWORD=postgres sudo docker exec -it postgres-jellyseerr /usr/bin/psql -h 127.0.0.1 -U postgres -c "DROP DATABASE IF EXISTS jellyseerr;" +PGPASSWORD=postgres sudo docker exec -it postgres-jellyseerr /usr/bin/psql -h 127.0.0.1 -U postgres -c "CREATE DATABASE jellyseerr;" +``` + +3. Checkout the `develop` branch and create the original database for SQLite and PostgreSQL so that TypeORM can automatically generate the migrations: + +```bash +git checkout develop +pnpm i +rm -r .next dist; pnpm build +pnpm start +DB_TYPE="postgres" DB_USER=postgres DB_PASS=postgres pnpm start +``` + +(You can shutdown the server once the message "Server ready on 5055" appears) + +4. Let TypeORM generate the migrations: + +```bash +git checkout -b your-feature-branch +pnpm i +pnpm migration:generate server/migration/sqlite/YourMigrationName +DB_TYPE="postgres" DB_USER=postgres DB_PASS=postgres pnpm migration:generate server/migration/postgres/YourMigrationName +``` + ## Attribution This contribution guide was inspired by the [Next.js](https://github.com/vercel/next.js), [Radarr](https://github.com/Radarr/Radarr), and [Overseerr](https://github.com/sct/Overseerr) contribution guides. diff --git a/Dockerfile b/Dockerfile index 0f761e33..96fecbe9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20-alpine AS BUILD_IMAGE +FROM node:22-alpine AS BUILD_IMAGE WORKDIR /app @@ -14,7 +14,7 @@ RUN \ ;; \ esac -RUN npm install --global pnpm +RUN npm install --global pnpm@9 COPY package.json pnpm-lock.yaml postinstall-win.js ./ RUN CYPRESS_INSTALL_BINARY=0 pnpm install --frozen-lockfile @@ -29,14 +29,14 @@ RUN pnpm build # remove development dependencies RUN pnpm prune --prod --ignore-scripts -RUN rm -rf src server .next/cache +RUN rm -rf src server .next/cache charts gen-docs docs RUN touch config/DOCKER RUN echo "{\"commitTag\": \"${COMMIT_TAG}\"}" > committag.json -FROM node:20-alpine +FROM node:22-alpine # Metadata for Github Package Registry LABEL org.opencontainers.image.source="https://github.com/Fallenbagel/jellyseerr" @@ -45,7 +45,7 @@ WORKDIR /app RUN apk add --no-cache tzdata tini && rm -rf /tmp/* -RUN npm install -g pnpm +RUN npm install -g pnpm@9 # copy from build image COPY --from=BUILD_IMAGE /app ./ diff --git a/Dockerfile.local b/Dockerfile.local index b9322e53..666fa74f 100644 --- a/Dockerfile.local +++ b/Dockerfile.local @@ -1,9 +1,9 @@ -FROM node:20-alpine +FROM node:22-alpine COPY . /app WORKDIR /app -Run npm install --global pnpm +RUN npm install --global pnpm@9 RUN pnpm install diff --git a/README.md b/README.md index 4f0fa356..70463b60 100644 --- a/README.md +++ b/README.md @@ -8,20 +8,20 @@

Discord Docker pulls -Translation status +Translation status GitHub -All Contributors +All Contributors -**Jellyseerr** is a free and open source software application for managing requests for your media library. -It is a fork of [Overseerr](https://github.com/sct/overseerr) built to bring support for [Jellyfin](https://github.com/jellyfin/jellyfin) & [Emby](https://github.com/MediaBrowser/Emby) media servers! +**Jellyseerr** is a free and open source software application for managing requests for your media library. It integrates with the media server of your choice: [Jellyfin](https://jellyfin.org), [Plex](https://plex.tv), and [Emby](https://emby.media/). In addition, it integrates with your existing services, such as **[Sonarr](https://sonarr.tv/)**, **[Radarr](https://radarr.video/)**. ## Current Features -- Full Jellyfin/Emby/Plex integration including authentication with user import & management -- Supports Movies, Shows and Mixed Libraries -- Ability to change email addresses for smtp purposes +- Full Jellyfin/Emby/Plex integration including authentication with user import & management. +- Support for **PostgreSQL** and **SQLite** databases. +- Supports Movies, Shows and Mixed Libraries. +- Ability to change email addresses for SMTP purposes. - Easy integration with your existing services. Currently, Jellyseerr supports Sonarr and Radarr. More to come! - Jellyfin/Emby/Plex library scan, to keep track of the titles which are already available. - Customizable request system, which allows users to request individual seasons or movies in a friendly, easy-to-use interface. @@ -29,8 +29,7 @@ It is a fork of [Overseerr](https://github.com/sct/overseerr) built to bring sup - Granular permission system. - Support for various notification agents. - Mobile-friendly design, for when you need to approve requests on the go! - - (Upcoming Features include: Multiple Server Instances, and much more!) +- Support for watchlisting & blacklisting media. With more features on the way! Check out our [issue tracker](https://github.com/fallenbagel/jellyseerr/issues) to see the features which have already been requested. @@ -87,65 +86,90 @@ Thanks goes to these wonderful people from Overseerr ([emoji key](https://allcon - - - - - - - + + + + + + + - - - + + + - - + + - + - - - + + + - + - - - + + + - + - + - - - + + + - + - + - - - + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Fallenbagel
Fallenbagel

💻 🚧
Sean
Sean

🌍 💻
notfakie
notfakie

💻
Mohamed Jumail
Mohamed Jumail

👀
Shilong Jiang
Shilong Jiang

💻
Boring Dragon
Boring Dragon

💻
Samuel Bartík
Samuel Bartík

💻
Fallenbagel
Fallenbagel

💻 🚧
Sean
Sean

🌍 💻
notfakie
notfakie

💻
Mohamed Jumail
Mohamed Jumail

👀
Shilong Jiang
Shilong Jiang

💻
Boring Dragon
Boring Dragon

💻
Samuel Bartík
Samuel Bartík

💻
Thegan Govender
Thegan Govender

💻
jab416171
jab416171

📖
Nicolai Van der Storm
Nicolai Van der Storm

💻
Thegan Govender
Thegan Govender

💻
jab416171
jab416171

📖
Nicolai Van der Storm
Nicolai Van der Storm

💻
Smexhy
Smexhy

🌍
dd060606
dd060606

💻
Daniel
Daniel

💻
dd060606
dd060606

💻
Daniel
Daniel

💻
undone37
undone37

🌍
Chechu García
Chechu García

🌍
Dimitri
Dimitri

🌍
andrey4korop
andrey4korop

💻 🌍
andrey4korop
andrey4korop

💻 🌍
Geoffrey Coulaud
Geoffrey Coulaud

🌍
Pikachu920
Pikachu920

💻
Maxim Yalagin
Maxim Yalagin

💻
Jesse Boswell
Jesse Boswell

💻
Pikachu920
Pikachu920

💻
Maxim Yalagin
Maxim Yalagin

💻
Jesse Boswell
Jesse Boswell

💻
d-fendrich
d-fendrich

🌍
David Fernández Alcoba
David Fernández Alcoba

💻
David Fernández Alcoba
David Fernández Alcoba

💻
Gauvino
Gauvino

🌍
EthanArmbrust
EthanArmbrust

💻
Eduardo
Eduardo

📖
RickLuiken
RickLuiken

💻
EthanArmbrust
EthanArmbrust

💻
Eduardo
Eduardo

📖
RickLuiken
RickLuiken

💻
Br33ce
Br33ce

🌍
Athfan Khaleel
Athfan Khaleel

📖
Athfan Khaleel
Athfan Khaleel

📖
Michael Dallinger
Michael Dallinger

🌍
Janek
Janek

📖
Janek
Janek

📖
Aleksa Siriški
Aleksa Siriški

🚇
Danish Humair
Danish Humair

💻
Stephen Harris
Stephen Harris

📖
Joshua M. Boniface
Joshua M. Boniface

💻
Danish Humair
Danish Humair

💻
Stephen Harris
Stephen Harris

📖
Joshua M. Boniface
Joshua M. Boniface

💻
Gauthier
Gauthier

💻
Gauthier
Gauthier

💻
Kara
Kara

🚇
Joaquin Olivero
Joaquin Olivero

💻
Joaquin Olivero
Joaquin Olivero

💻
Julian Behr
Julian Behr

🌍
ThowZzy
ThowZzy

💻
Joseph Risk
Joseph Risk

💻
Loetwiek
Loetwiek

💻
ThowZzy
ThowZzy

💻
Joseph Risk
Joseph Risk

💻
Loetwiek
Loetwiek

💻
Fuochi
Fuochi

📖
Fuochi
Fuochi

📖
Nir Israel Hen
Nir Israel Hen

🌍
Baraa
Baraa

💻
Francisco Sales
Francisco Sales

💻
Oliver Laing
Oliver Laing

💻
Baraa
Baraa

💻
Francisco Sales
Francisco Sales

💻
Oliver Laing
Oliver Laing

💻
Ludovic Ortega
Ludovic Ortega

🛡️
Joseph Risk
Joseph Risk

💻
Loetwiek
Loetwiek

💻
Fuochi
Fuochi

📖
David Emrich
David Emrich

💻
Max T. Kristiansen
Max T. Kristiansen

💻
Damien Fajole
Damien Fajole

💻
Ahmed Siddiqui
Ahmed Siddiqui

💻
Chris Bannister
Chris Bannister

💻
Joe
Joe

📖
Guillaume ARNOUX
Guillaume ARNOUX

💻
dr-carrot
dr-carrot

💻
Gage Orsburn
Gage Orsburn

💻
GkhnGRBZ
GkhnGRBZ

💻
Ben Haney
Ben Haney

💻
Wunderharke
Wunderharke

📖
Metin Bektas
Metin Bektas

🚇
andrewkolda
andrewkolda

🎨
Ishan Jain
Ishan Jain

💻
@@ -290,6 +314,12 @@ Thanks goes to these wonderful people from Overseerr ([emoji key](https://allcon Joseph Risk
Joseph Risk

💻 Loetwiek
Loetwiek

💻 Fuochi
Fuochi

📖 + David Emrich
David Emrich

💻 + Max T. Kristiansen
Max T. Kristiansen

💻 + Damien Fajole
Damien Fajole

💻 + + + Ahmed Siddiqui
Ahmed Siddiqui

💻 diff --git a/charts/jellyseerr-chart/.helmignore b/charts/jellyseerr-chart/.helmignore new file mode 100644 index 00000000..e8232ed5 --- /dev/null +++ b/charts/jellyseerr-chart/.helmignore @@ -0,0 +1,25 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ +# go template +*.gotmpl diff --git a/charts/jellyseerr-chart/Chart.yaml b/charts/jellyseerr-chart/Chart.yaml new file mode 100644 index 00000000..dd595adf --- /dev/null +++ b/charts/jellyseerr-chart/Chart.yaml @@ -0,0 +1,13 @@ +apiVersion: v2 +kubeVersion: ">=1.23.0-0" +name: jellyseerr-chart +description: Jellyseerr helm chart for Kubernetes +type: application +version: 2.1.1 +appVersion: "2.3.0" +maintainers: + - name: Jellyseerr + url: https://github.com/Fallenbagel/jellyseerr +sources: + - https://github.com/Fallenbagel/jellyseerr/tree/main/charts/jellyseerr +home: https://github.com/Fallenbagel/jellyseerr diff --git a/charts/jellyseerr-chart/README.md b/charts/jellyseerr-chart/README.md new file mode 100644 index 00000000..678ba00f --- /dev/null +++ b/charts/jellyseerr-chart/README.md @@ -0,0 +1,67 @@ +# jellyseerr-chart + +![Version: 2.1.1](https://img.shields.io/badge/Version-2.1.1-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 2.3.0](https://img.shields.io/badge/AppVersion-2.3.0-informational?style=flat-square) + +Jellyseerr helm chart for Kubernetes + +**Homepage:** + +## Maintainers + +| Name | Email | Url | +| ---- | ------ | --- | +| Jellyseerr | | | + +## Source Code + +* + +## Requirements + +Kubernetes: `>=1.23.0-0` + +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| affinity | object | `{}` | | +| config | object | `{"persistence":{"accessModes":["ReadWriteOnce"],"annotations":{},"name":"","size":"5Gi","volumeName":""}}` | Creating PVC to store configuration | +| config.persistence.accessModes | list | `["ReadWriteOnce"]` | Access modes of persistent disk | +| config.persistence.annotations | object | `{}` | Annotations for PVCs | +| config.persistence.name | string | `""` | Config name | +| config.persistence.size | string | `"5Gi"` | Size of persistent disk | +| config.persistence.volumeName | string | `""` | Name of the permanent volume to reference in the claim. Can be used to bind to existing volumes. | +| extraEnv | list | `[]` | Environment variables to add to the jellyseerr pods | +| extraEnvFrom | list | `[]` | Environment variables from secrets or configmaps to add to the jellyseerr pods | +| fullnameOverride | string | `""` | | +| image.pullPolicy | string | `"IfNotPresent"` | | +| image.registry | string | `"ghcr.io"` | | +| image.repository | string | `"fallenbagel/jellyseerr"` | | +| image.sha | string | `""` | | +| image.tag | string | `""` | Overrides the image tag whose default is the chart appVersion. | +| imagePullSecrets | list | `[]` | | +| ingress.annotations | object | `{}` | | +| ingress.enabled | bool | `false` | | +| ingress.hosts[0].host | string | `"chart-example.local"` | | +| ingress.hosts[0].paths[0].path | string | `"/"` | | +| ingress.hosts[0].paths[0].pathType | string | `"ImplementationSpecific"` | | +| ingress.ingressClassName | string | `""` | | +| ingress.tls | list | `[]` | | +| nameOverride | string | `""` | | +| nodeSelector | object | `{}` | | +| podAnnotations | object | `{}` | | +| podLabels | object | `{}` | | +| podSecurityContext | object | `{}` | | +| replicaCount | int | `1` | | +| resources | object | `{}` | | +| securityContext | object | `{}` | | +| service.port | int | `80` | | +| service.type | string | `"ClusterIP"` | | +| 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 | +| serviceAccount.name | string | `""` | If not set and create is true, a name is generated using the fullname template | +| strategy | object | `{"type":"Recreate"}` | Deployment strategy | +| tolerations | list | `[]` | | +| volumeMounts | list | `[]` | Additional volumeMounts on the output Deployment definition. | +| volumes | list | `[]` | Additional volumes on the output Deployment definition. | diff --git a/charts/jellyseerr-chart/README.md.gotmpl b/charts/jellyseerr-chart/README.md.gotmpl new file mode 100644 index 00000000..c58fe7d5 --- /dev/null +++ b/charts/jellyseerr-chart/README.md.gotmpl @@ -0,0 +1,17 @@ +{{ template "chart.header" . }} + +{{ template "chart.deprecationWarning" . }} + +{{ template "chart.badgesSection" . }} + +{{ template "chart.description" . }} + +{{ template "chart.homepageLine" . }} + +{{ template "chart.maintainersSection" . }} + +{{ template "chart.sourcesSection" . }} + +{{ template "chart.requirementsSection" . }} + +{{ template "chart.valuesSection" . }} diff --git a/charts/jellyseerr-chart/artifacthub-repo.yml b/charts/jellyseerr-chart/artifacthub-repo.yml new file mode 100644 index 00000000..849fcf8d --- /dev/null +++ b/charts/jellyseerr-chart/artifacthub-repo.yml @@ -0,0 +1 @@ +repositoryID: c6b3f2dc-444c-4e37-b397-6a5ff563ee8b diff --git a/charts/jellyseerr-chart/templates/NOTES.txt b/charts/jellyseerr-chart/templates/NOTES.txt new file mode 100644 index 00000000..aa8a44b6 --- /dev/null +++ b/charts/jellyseerr-chart/templates/NOTES.txt @@ -0,0 +1,5 @@ +*********************************************************************** + Welcome to {{ .Chart.Name }} + Chart version: {{ .Chart.Version }} + App version: {{ .Chart.AppVersion }} +*********************************************************************** \ No newline at end of file diff --git a/charts/jellyseerr-chart/templates/_helpers.tpl b/charts/jellyseerr-chart/templates/_helpers.tpl new file mode 100644 index 00000000..bb4b4ef6 --- /dev/null +++ b/charts/jellyseerr-chart/templates/_helpers.tpl @@ -0,0 +1,70 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "jellyseerr.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "jellyseerr.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "jellyseerr.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "jellyseerr.labels" -}} +helm.sh/chart: {{ include "jellyseerr.chart" . }} +{{ include "jellyseerr.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/part-of: {{ .Chart.Name }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "jellyseerr.selectorLabels" -}} +app.kubernetes.io/name: {{ include "jellyseerr.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "jellyseerr.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "jellyseerr.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{/* +Create the name of the pvc config to use +*/}} +{{- define "jellyseerr.configPersistenceName" -}} +{{- default (printf "%s-config" (include "jellyseerr.fullname" .)) .Values.config.persistence.name }} +{{- end }} \ No newline at end of file diff --git a/charts/jellyseerr-chart/templates/deployment.yaml b/charts/jellyseerr-chart/templates/deployment.yaml new file mode 100644 index 00000000..447ecca2 --- /dev/null +++ b/charts/jellyseerr-chart/templates/deployment.yaml @@ -0,0 +1,89 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "jellyseerr.fullname" . }} + labels: + {{- include "jellyseerr.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + strategy: + type: {{ .Values.strategy.type }} + selector: + matchLabels: + {{- include "jellyseerr.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "jellyseerr.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "jellyseerr.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + {{- if .Values.image.sha }} + image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}@sha256:{{ .Values.image.sha }}" + {{- else }} + image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + {{- end }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: 5055 + protocol: TCP + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.extraEnv }} + env: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.extraEnvFrom }} + envFrom: + {{- toYaml . | nindent 12 }} + {{- end }} + volumeMounts: + - name: config + mountPath: /app/config + {{- with .Values.volumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + volumes: + - name: config + persistentVolumeClaim: + claimName: {{ include "jellyseerr.configPersistenceName" . }} + {{- with .Values.volumes }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/charts/jellyseerr-chart/templates/ingress.yaml b/charts/jellyseerr-chart/templates/ingress.yaml new file mode 100644 index 00000000..85f1125a --- /dev/null +++ b/charts/jellyseerr-chart/templates/ingress.yaml @@ -0,0 +1,41 @@ +{{- if .Values.ingress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "jellyseerr.fullname" . }} + labels: + {{- include "jellyseerr.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.ingress.ingressClassName }} + ingressClassName: {{ .Values.ingress.ingressClassName }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + pathType: {{ .pathType }} + backend: + service: + name: {{ include "jellyseerr.fullname" $ }} + port: + number: {{ $.Values.service.port }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/jellyseerr-chart/templates/persistentvolumeclaim.yaml b/charts/jellyseerr-chart/templates/persistentvolumeclaim.yaml new file mode 100644 index 00000000..bf0d6422 --- /dev/null +++ b/charts/jellyseerr-chart/templates/persistentvolumeclaim.yaml @@ -0,0 +1,20 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "jellyseerr.configPersistenceName" . }} + labels: + {{- include "jellyseerr.labels" . | nindent 4 }} +spec: + {{- with .Values.config.persistence.accessModes }} + accessModes: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- if .Values.config.persistence.volumeName }} + volumeName: {{ .Values.config.persistence.volumeName }} + {{- end }} + {{- with .Values.config.persistence.storageClass }} + storageClassName: {{ if (eq "-" .) }}""{{ else }}{{ . }}{{ end }} + {{- end }} + resources: + requests: + storage: "{{ .Values.config.persistence.size }}" \ No newline at end of file diff --git a/charts/jellyseerr-chart/templates/service.yaml b/charts/jellyseerr-chart/templates/service.yaml new file mode 100644 index 00000000..5c915e3b --- /dev/null +++ b/charts/jellyseerr-chart/templates/service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "jellyseerr.fullname" . }} + labels: + {{- include "jellyseerr.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "jellyseerr.selectorLabels" . | nindent 4 }} + ipFamilyPolicy: PreferDualStack diff --git a/charts/jellyseerr-chart/templates/serviceaccount.yaml b/charts/jellyseerr-chart/templates/serviceaccount.yaml new file mode 100644 index 00000000..6a2dcfd0 --- /dev/null +++ b/charts/jellyseerr-chart/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "jellyseerr.serviceAccountName" . }} + labels: + {{- include "jellyseerr.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/charts/jellyseerr-chart/templates/tests/test-connection.yaml b/charts/jellyseerr-chart/templates/tests/test-connection.yaml new file mode 100644 index 00000000..6adc5d30 --- /dev/null +++ b/charts/jellyseerr-chart/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "jellyseerr.fullname" . }}-test-connection" + labels: + {{- include "jellyseerr.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "jellyseerr.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/charts/jellyseerr-chart/values.yaml b/charts/jellyseerr-chart/values.yaml new file mode 100644 index 00000000..861ad361 --- /dev/null +++ b/charts/jellyseerr-chart/values.yaml @@ -0,0 +1,114 @@ +replicaCount: 1 + +image: + registry: ghcr.io + repository: fallenbagel/jellyseerr + pullPolicy: IfNotPresent + # -- Overrides the image tag whose default is the chart appVersion. + tag: "" + sha: "" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +# -- Deployment strategy +strategy: + type: Recreate + +# -- Environment variables to add to the jellyseerr pods +extraEnv: [] +# -- Environment variables from secrets or configmaps to add to the jellyseerr pods +extraEnvFrom: [] + +serviceAccount: + # -- Specifies whether a service account should be created + create: true + # -- Automatically mount a ServiceAccount's API credentials? + automount: true + # -- Annotations to add to the service account + annotations: {} + # -- The name of the service account to use. + # -- If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} +podLabels: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 80 + +# -- Creating PVC to store configuration +config: + persistence: + # -- Size of persistent disk + size: 5Gi + # -- Annotations for PVCs + annotations: {} + # -- Access modes of persistent disk + accessModes: + - ReadWriteOnce + # -- Config name + name: "" + # -- Name of the permanent volume to reference in the claim. + # Can be used to bind to existing volumes. + volumeName: "" + +ingress: + enabled: false + ingressClassName: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +# -- Additional volumes on the output Deployment definition. +volumes: [] +# - name: foo +# secret: +# secretName: mysecret +# optional: false + +# -- Additional volumeMounts on the output Deployment definition. +volumeMounts: [] +# - name: foo +# mountPath: "/etc/foo" +# readOnly: true + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/docker-compose.yml b/compose.yaml similarity index 93% rename from docker-compose.yml rename to compose.yaml index 91b76e1e..94705357 100644 --- a/docker-compose.yml +++ b/compose.yaml @@ -1,4 +1,3 @@ -version: '3' services: jellyseerr: build: diff --git a/cypress/config/settings.cypress.json b/cypress/config/settings.cypress.json index 45e38a29..e49d8888 100644 --- a/cypress/config/settings.cypress.json +++ b/cypress/config/settings.cypress.json @@ -16,11 +16,15 @@ "hideAvailable": false, "localLogin": true, "newPlexLogin": true, - "region": "", + "discoverRegion": "", + "streamingRegion": "", "originalLanguage": "", "trustProxy": false, "mediaServerType": 1, "partialRequestsEnabled": true, + "enableSpecialEpisodes": false, + "forceIpv4First": false, + "dnsServers": "", "locale": "en" }, "plex": { @@ -75,6 +79,7 @@ "types": 0, "options": { "webhookUrl": "", + "webhookRoleId": "", "enableMentions": true } }, @@ -98,6 +103,7 @@ "options": { "botAPI": "", "chatId": "", + "messageThreadId": "", "sendSilently": false } }, diff --git a/cypress/e2e/settings/general-settings.cy.ts b/cypress/e2e/settings/general-settings.cy.ts index bcfce1a3..59649ed6 100644 --- a/cypress/e2e/settings/general-settings.cy.ts +++ b/cypress/e2e/settings/general-settings.cy.ts @@ -13,10 +13,10 @@ describe('General Settings', () => { }); it('modifies setting that requires restart', () => { - cy.visit('/settings'); + cy.visit('/settings/network'); cy.get('#trustProxy').click(); - cy.get('[data-testid=settings-main-form]').submit(); + cy.get('[data-testid=settings-network-form]').submit(); cy.get('[data-testid=modal-title]').should( 'contain', 'Server Restart Required' @@ -26,7 +26,7 @@ describe('General Settings', () => { cy.get('[data-testid=modal-title]').should('not.exist'); cy.get('[type=checkbox]#trustProxy').click(); - cy.get('[data-testid=settings-main-form]').submit(); + cy.get('[data-testid=settings-network-form]').submit(); cy.get('[data-testid=modal-title]').should('not.exist'); }); }); diff --git a/docker-compose.postgres.yaml b/docker-compose.postgres.yaml new file mode 100644 index 00000000..2ed270f3 --- /dev/null +++ b/docker-compose.postgres.yaml @@ -0,0 +1,38 @@ +--- +version: '3.8' +services: + jellyseerr: + build: + context: . + dockerfile: Dockerfile.local + ports: + - '5055:5055' + environment: + DB_TYPE: 'postgres' # Which DB engine to use. The default is "sqlite". To use postgres, this needs to be set to "postgres" + DB_HOST: 'postgres' # The host (url) of the database + DB_PORT: '5432' # The port to connect to + DB_USER: 'jellyseerr' # Username used to connect to the database + DB_PASS: 'jellyseerr' # Password of the user used to connect to the database + DB_NAME: 'jellyseerr' # The name of the database to connect to + DB_LOG_QUERIES: 'false' # Whether to log the DB queries for debugging + DB_USE_SSL: 'false' # Whether to enable ssl for database connection + volumes: + - .:/app:rw,cached + - /app/node_modules + - /app/.next + depends_on: + - postgres + links: + - postgres + postgres: + image: postgres + environment: + POSTGRES_USER: jellyseerr + POSTGRES_PASSWORD: jellyseerr + POSTGRES_DB: jellyseerr + ports: + - '5432:5432' + volumes: + - postgres:/var/lib/postgresql/data +volumes: + postgres: diff --git a/docs/README.md b/docs/README.md index a441434d..c2f4ff32 100644 --- a/docs/README.md +++ b/docs/README.md @@ -7,30 +7,34 @@ sidebar_position: 1 Welcome to the Jellyseerr Documentation. +**Jellyseerr** is a free and open source software application for managing requests for your media library. It integrates with the media server of your choice: [Jellyfin](https://jellyfin.org), [Plex](https://plex.tv), and [Emby](https://emby.media/). In addition, it integrates with your existing services, such as **[Sonarr](https://sonarr.tv/)**, **[Radarr](https://radarr.video/)**. + ## Features - **Full Jellyfin/Emby/Plex integration**. Login and manage user access with Jellyfin/Emby/Plex. - **Syncs to your Jellyfin/Emby/Plex library** to show what titles you already have. +- Supports Movies, Shows and Mixed Libraries. - **Integrates with Sonarr and Radarr**. With more services to come in the future. +- Optionally set **Override rules** for requests to match with your defined conditions. - **Easy to use request system** allowing users to request individual seasons or movies in a friendly, clean UI. - **Simple request management UI**. Don't dig through the app to approve recent requests. - **Mobile-friendly design**, for when you need to approve requests on the go. - Granular permission system. - Localization into other languages. +- Support for **PostgreSQL** and **SQLite** databases. +- Support for various notification agents. +- Easily **Watchlist** or **Blacklist** media. - More features to come! ## Motivation -The primary motivation for starting this project was to add support for Jellyfin and Emby to Overseerr. As Overseerr is an incredibly performant and easy-to-use application, we wanted to bring that same experience to Jellyfin and Emby users. Thus, **Jellyseerr** was born. - -This application is designed to be a **one-stop-shop** for all your media requests. It is designed to be a **simple, easy-to-use** application that allows users to request media to be added to your Jellyfin/Emby/Plex server. +The primary motivation for starting Jellyseerr was to bring Jellyfin and Emby support to Overseerr. However, over time, **Jellyseerr** has evolved into its own distinct application with unique features. Designed as a one-stop shop for media requests, it offers a simple, easy-to-use experience for managing requests on Jellyfin, Emby, and Plex servers. ## We need your help! -[Jellyseerr](https://github.com/Fallenbagel/jellyseerr) is a fork of Overseerr, with a heavy focus on Jellyfin and Emby integration. -[Overseerr](https://github.com/sct/overseerr) is an ambitious project where the original developers/contributors have already poured a lot of work into, and we wanted to build on top of that. +[Jellyseerr](https://github.com/Fallenbagel/jellyseerr) is an ambitious project where developers/contributors poured a lot of work into, and that builds on top of [Overseerr](https://github.com/sct/overseerr). And we have a lot more to do as well. -We also have poured a lot of work into this project, and we have a lot more to do as well. We need your valuable feedback and help to find and fix bugs. Also, with Jellyseerr being an open-source project, anyone is welcome to contribute. We also encourage you to contribute to Overseerr as well. +We value your feedback and support in identifying and fixing bugs to make Jellyseerr even better. As an open-source project, we welcome contributions from everyone. While Jellyseerr has diverged from Overseerr and evolved into its own unique application, we still encourage contributions to Overseerr, as it played a crucial role in inspiring what Jellyseerr has become today. Contribution includes building new features, patching bugs, translating the application, or even just writing documentation. diff --git a/docs/extending-jellyseerr/database-config.mdx b/docs/extending-jellyseerr/database-config.mdx new file mode 100644 index 00000000..d6c4442a --- /dev/null +++ b/docs/extending-jellyseerr/database-config.mdx @@ -0,0 +1,82 @@ +--- +title: Configuring the Database (Advanced) +description: Configure the database for Jellyseerr +sidebar_position: 2 +--- +# Configuring the Database + +Jellyseerr supports SQLite and PostgreSQL. The database connection can be configured using the following environment variables: + +## SQLite Options + +If you want to use SQLite, you can simply set the `DB_TYPE` environment variable to `sqlite`. This is the default configuration so even if you don't set any other options, SQLite will be used. + +```dotenv +DB_TYPE="sqlite" # Which DB engine to use, either "sqlite" or "postgres". The default is "sqlite". +CONFIG_DIRECTORY="config" # (optional) The path to the config directory where the db file is stored. The default is "config". +DB_LOG_QUERIES="false" # (optional) Whether to log the DB queries for debugging. The default is "false". +``` + +## PostgreSQL Options + +### TCP Connection + +If your PostgreSQL server is configured to accept TCP connections, you can specify the host and port using the `DB_HOST` and `DB_PORT` environment variables. This is useful for remote connections where the server uses a network host and port. + +```dotenv +DB_TYPE="postgres" # Which DB engine to use, either "sqlite" or "postgres". The default is "sqlite". +DB_HOST="localhost" # (optional) The host (URL) of the database. The default is "localhost". +DB_PORT="5432" # (optional) The port to connect to. The default is "5432". +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="jellyseerr" # (optional) The name of the database to connect to. The default is "jellyseerr". +DB_LOG_QUERIES="false" # (optional) Whether to log the DB queries for debugging. The default is "false". +``` + +### Unix Socket Connection + +If your PostgreSQL server is configured to accept Unix socket connections, you can specify the path to the socket directory using the `DB_SOCKET_PATH` environment variable. This is useful for local connections where the server uses a Unix socket. + +```dotenv +DB_TYPE="postgres" # Which DB engine to use, either "sqlite" or "postgres". The default is "sqlite". +DB_SOCKET_PATH="/var/run/postgresql" # (required) The path to the PostgreSQL Unix socket directory. +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="jellyseerr" # (optional) The name of the database to connect to. The default is "jellyseerr". +DB_LOG_QUERIES="false" # (optional) Whether to log the DB queries for debugging. The default is "false". +``` + +### SSL configuration + +The following options can be used to further configure ssl. Certificates can be provided as a string or a file path, with the string version taking precedence. + +```dotenv +DB_USE_SSL="false" # (optional) Whether to enable ssl for database connection. This must be "true" to use the other ssl options. The default is "false". +DB_SSL_REJECT_UNAUTHORIZED="true" # (optional) Whether to reject ssl connections with unverifiable certificates i.e. self-signed certificates without providing the below settings. The default is "true". +DB_SSL_CA= # (optional) The CA certificate to verify the connection, provided as a string. The default is "". +DB_SSL_CA_FILE= # (optional) The path to a CA certificate to verify the connection. The default is "". +DB_SSL_KEY= # (optional) The private key for the connection in PEM format, provided as a string. The default is "". +DB_SSL_KEY_FILE= # (optinal) Path to the private key for the connection in PEM format. The default is "". +DB_SSL_CERT= # (optional) Certificate chain in pem format for the private key, provided as a string. The default is "". +DB_SSL_CERT_FILE= # (optional) Path to certificate chain in pem format for the private key. The default is "". +``` +--- + +### Migrating from SQLite to PostgreSQL + +1. Set up your PostgreSQL database and configure Jellyseerr to use it +2. Run Jellyseerr to create the tables in the PostgreSQL database +3. Stop Jellyseerr +4. Run the following command to export the data from the SQLite database and import it into the PostgreSQL database: +:::info +Edit the postgres connection string to match your setup. + +If you don't have or don't want to use docker, you can build the working pgloader version [in this PR](https://github.com/dimitri/pgloader/pull/1531) from source and use the same options as below. +::: +:::caution +The most recent release of pgloader has an issue quoting the table columns. Use the version in the docker container to avoid this issue. +::: +```bash +docker run --rm -v config/db.sqlite3:/db.sqlite3:ro ghcr.io/ralgar/pgloader:pr-1531 pgloader --with "quote identifiers" --with "data only" /db.sqlite3 postgresql://{{DB_USER}}:{{DB_PASS}}@{{DB_HOST}}:{{DB_PORT}}/{{DB_NAME}} + ``` +5. Start Jellyseerr diff --git a/docs/extending-jellyseerr/reverse-proxy.mdx b/docs/extending-jellyseerr/reverse-proxy.mdx index 1ac36545..505389ac 100644 --- a/docs/extending-jellyseerr/reverse-proxy.mdx +++ b/docs/extending-jellyseerr/reverse-proxy.mdx @@ -95,6 +95,8 @@ location ^~ /jellyseerr { sub_filter '/api/v1' '/$app/api/v1'; sub_filter '/login/plex/loading' '/$app/login/plex/loading'; sub_filter '/images/' '/$app/images/'; + sub_filter '/imageproxy/' '/$app/imageproxy/'; + sub_filter '/avatarproxy/' '/$app/avatarproxy/'; sub_filter '/android-' '/$app/android-'; sub_filter '/apple-' '/$app/apple-'; sub_filter '/favicon' '/$app/favicon'; @@ -190,7 +192,7 @@ Caddy will automatically obtain and renew SSL certificates for your domain. ## Traefik (v2) -Add the following labels to the Jellyseerr service in your `docker-compose.yml` file: +Add the following labels to the Jellyseerr service in your `compose.yaml` file: ```yaml labels: diff --git a/docs/getting-started/aur.mdx b/docs/getting-started/aur.mdx index a67a0b24..025118c8 100644 --- a/docs/getting-started/aur.mdx +++ b/docs/getting-started/aur.mdx @@ -6,6 +6,10 @@ sidebar_position: 4 # AUR (Arch User Repository) +:::note Disclaimer +This AUR package is not maintained by us but by a third party. Please refer to the maintainer for any issues. +::: + :::info This method is not recommended for most users. It is intended for advanced users who are using Arch Linux or an Arch-based distribution. ::: diff --git a/docs/getting-started/buildfromsource.mdx b/docs/getting-started/buildfromsource.mdx index 5b39912c..9e139dcd 100644 --- a/docs/getting-started/buildfromsource.mdx +++ b/docs/getting-started/buildfromsource.mdx @@ -6,55 +6,20 @@ sidebar_position: 2 # Build from Source (Advanced) :::warning This method is not recommended for most users. It is intended for advanced users who are familiar with managing their own server infrastructure. + +Refer to [Configuring Databases](/extending-jellyseerr/database-config#postgresql-options) for details on how to configure your database. ::: import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; ### Prerequisites - - - - [Node.js 18.x](https://nodejs.org/en/download/) - - [Yarn 1.x](https://classic.yarnpkg.com/lang/en/docs/install) - - [Git](https://git-scm.com/downloads) - - - - - [Node.js 20.x](https://nodejs.org/en/download/) + - [Node.js 22.x](https://nodejs.org/en/download/) - [Pnpm 9.x](https://pnpm.io/installation) - [Git](https://git-scm.com/downloads) - - - ## Unix (Linux, macOS) ### Installation - - -1. Assuming you want the working directory to be `/opt/jellyseerr`, create the directory and navigate to it: -```bash -sudo mkdir -p /opt/jellyseerr && cd /opt/jellyseerr -``` -2. Clone the Jellyseerr repository and checkout the latest release: -```bash -git clone https://github.com/Fallenbagel/jellyseerr.git -cd jellyseerr -git checkout main -``` -3. Install the dependencies: -```bash -CYPRESS_INSTALL_BINARY=0 yarn install --frozen-lockfile --network-timeout 1000000 -``` -4. Build the project: -```bash -yarn build -``` -5. Start Jellyseerr: -```bash -yarn start -``` - - 1. Assuming you want the working directory to be `/opt/jellyseerr`, create the directory and navigate to it: ```bash sudo mkdir -p /opt/jellyseerr && cd /opt/jellyseerr @@ -63,7 +28,7 @@ sudo mkdir -p /opt/jellyseerr && cd /opt/jellyseerr ```bash git clone https://github.com/Fallenbagel/jellyseerr.git cd jellyseerr -git checkout develop # by default, you are on the develop branch so this step is not necessary +git checkout main ``` 3. Install the dependencies: ```bash @@ -77,8 +42,6 @@ pnpm build ```bash pnpm start ``` - - :::info You can now access Jellyseerr by visiting `http://localhost:5055` in your web browser. @@ -97,9 +60,6 @@ PORT=5055 ## specify on which interface to listen, by default jellyseerr listens on all interfaces #HOST=127.0.0.1 -## Uncomment if your media server is emby instead of jellyfin. -# JELLYFIN_TYPE=emby - ## Uncomment if you want to force Node.js to resolve IPv4 before IPv6 (advanced users only) # FORCE_IPV4_FIRST=true ``` @@ -234,33 +194,6 @@ pm2 status jellyseerr ## Windows ### Installation - - -1. Assuming you want the working directory to be `C:\jellyseerr`, create the directory and navigate to it: -```powershell -mkdir C:\jellyseerr -cd C:\jellyseerr -``` -2. Clone the Jellyseerr repository and checkout the latest release: -```powershell -git clone https://github.com/Fallenbagel/jellyseerr.git . -git checkout main -``` -3. Install the dependencies: -```powershell -npm install -g win-node-env -set CYPRESS_INSTALL_BINARY=0 && yarn install --frozen-lockfile --network-timeout 1000000 -``` -4. Build the project: -```powershell -yarn build -``` -5. Start Jellyseerr: -```powershell -yarn start -``` - - 1. Assuming you want the working directory to be `C:\jellyseerr`, create the directory and navigate to it: ```powershell mkdir C:\jellyseerr @@ -269,7 +202,7 @@ cd C:\jellyseerr 2. Clone the Jellyseerr repository and checkout the develop branch: ```powershell git clone https://github.com/Fallenbagel/jellyseerr.git . -git checkout develop # by default, you are on the develop branch so this step is not necessary +git checkout main ``` 3. Install the dependencies: ```powershell @@ -284,8 +217,6 @@ pnpm build ```powershell pnpm start ``` - - :::tip You can add the environment variables to a `.env` file in the Jellyseerr directory. @@ -313,6 +244,7 @@ node dist/index.js - Set the trigger to "When the computer starts" - Set the action to "Start a program" - Set the program/script to the path of the `start-jellyseerr.bat` file +- Set the "Start in" to the jellyseerr directory. - Click "Finish" Now, Jellyseerr will start when the computer boots up in the background. diff --git a/docs/getting-started/docker.mdx b/docs/getting-started/docker.mdx index 3d8d690c..3d411e02 100644 --- a/docs/getting-started/docker.mdx +++ b/docs/getting-started/docker.mdx @@ -7,6 +7,8 @@ sidebar_position: 1 :::info This is the recommended method for most users. Details on how to install Docker can be found on the [official Docker website](https://docs.docker.com/get-docker/). + +Refer to [Configuring Databases](/extending-jellyseerr/database-config#postgresql-options) for details on how to configure your database. ::: ## Unix (Linux, macOS) @@ -71,7 +73,7 @@ You could also use [diun](https://github.com/crazy-max/diun) to receive notifica For details on how to use Docker Compose, please [review the official Compose documentation](https://docs.docker.com/compose/reference/). #### Installation: -Define the `jellyseerr` service in your `docker-compose.yml` as follows: +Define the `jellyseerr` service in your `compose.yaml` as follows: ```yaml --- services: @@ -94,17 +96,17 @@ If you are using emby, make sure to set the `JELLYFIN_TYPE` environment variable Then, start all services defined in the Compose file: ```bash -docker-compose up -d +docker compose up -d ``` #### Updating: Pull the latest image: ```bash -docker-compose pull jellyseerr +docker compose pull jellyseerr ``` Then, restart all services defined in the Compose file: ```bash -docker-compose up -d +docker compose up -d ``` :::tip You may alternatively use a third-party mechanism like [dockge](https://github.com/louislam/dockge) to manage your docker compose files. @@ -145,6 +147,16 @@ Then, create and start the Jellyseerr container: ```bash docker run -d --name jellyseerr -e LOG_LEVEL=debug -e TZ=Asia/Tashkent -p 5055:5055 -v "jellyseerr-data:/app/config" --restart unless-stopped fallenbagel/jellyseerr:latest +``` + +#### Updating: +Pull the latest image: +```bash +docker compose pull jellyseerr +``` +Then, restart all services defined in the Compose file: +```bash +docker compose up -d ``` @@ -167,6 +179,16 @@ services: volumes: jellyseerr-data: external: true +``` + +#### Updating: +Pull the latest image: +```bash +docker compose pull jellyseerr +``` +Then, restart all services defined in the Compose file: +```bash +docker compose up -d ``` @@ -185,3 +207,6 @@ Docker on Windows works differently than it does on Linux; it runs Docker inside **If you must run Docker on Windows, you should put the `/app/config` directory mount inside the VM and not on the Windows host.** (This also applies to other containers with SQLite databases.) Named volumes, like in the example commands above, are automatically mounted inside the VM. Therefore the warning on the setup about the `/app/config` folder being incorrectly mounted page should be ignored. +::: + + diff --git a/docs/getting-started/kubernetes.mdx b/docs/getting-started/kubernetes.mdx new file mode 100644 index 00000000..c3e36ef1 --- /dev/null +++ b/docs/getting-started/kubernetes.mdx @@ -0,0 +1,21 @@ +--- +title: Kubernetes +description: Install Jellyseerr in Kubernetes +sidebar_position: 5 +--- +# Kubernetes +:::info +This method is not recommended for most users. It is intended for advanced users who are using Kubernetes. +::: + +## Installation +```console +helm install jellyseerr oci://ghcr.io/fallenbagel/jellyseerr/jellyseerr-chart +``` +Helm values can be found in the Jellyseerr repository under [charts/jellyseerr-chart/README.md](https://github.com/Fallenbagel/jellyseerr/tree/develop/charts/jellyseerr-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/fallenbagel/jellyseerr/jellyseerr-chart:[tag] --certificate-identity=https://github.com/Fallenbagel/jellyseerr/.github/workflows/helm.yml@refs/heads/main --certificate-oidc-issuer=https://token.ac +tions.githubusercontent.com +``` \ No newline at end of file diff --git a/docs/getting-started/nixpkg.mdx b/docs/getting-started/nixpkg.mdx index b7d955e9..e35fd184 100644 --- a/docs/getting-started/nixpkg.mdx +++ b/docs/getting-started/nixpkg.mdx @@ -6,6 +6,8 @@ sidebar_position: 3 import { JellyseerrVersion, NixpkgVersion } from '@site/src/components/JellyseerrVersion'; import Admonition from '@theme/Admonition'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; # Nix Package Manager (Advanced) :::info @@ -13,22 +15,55 @@ This method is not recommended for most users. It is intended for advanced users ::: export const VersionMismatchWarning = () => { - const jellyseerrVersion = JellyseerrVersion(); - const nixpkgVersion = NixpkgVersion(); + let jellyseerrVersion = null; + let nixpkgVersions = null; + try { + jellyseerrVersion = JellyseerrVersion(); + nixpkgVersions = NixpkgVersion(); + } catch (err) { + return ( + + Failed to load version information. Error: {err.message || JSON.stringify(err)} + + ); + } - const isUpToDate = jellyseerrVersion === nixpkgVersion; + if (!nixpkgVersions || nixpkgVersions.error) { + return ( + + Failed to fetch Nixpkg versions: {nixpkgVersions?.error || 'Unknown error'} + + ); + } + + const isUnstableUpToDate = jellyseerrVersion === nixpkgVersions.unstable; + const isStableUpToDate = jellyseerrVersion === nixpkgVersions.stable; return ( - <> - {!isUpToDate ? ( - - The upstream Jellyseerr Nix Package (v{nixpkgVersion}) is not up-to-date. If you want to use Jellyseerr v{jellyseerrVersion}, you will need to override the package derivation. - - ) : ( - - The upstream Jellyseerr Nix Package (v{nixpkgVersion}) is up-to-date with Jellyseerr v{jellyseerrVersion}. - - )} + <> + {!isStableUpToDate ? ( + + The{' '} + + upstream Jellyseerr Nix Package (v{nixpkgVersions.stable}) + {' '} + is not up-to-date. If you want to use Jellyseerr v{jellyseerrVersion},{' '} + {isUnstableUpToDate ? ( + <> + consider using the{' '} + + unstable package + {' '} + instead. + + ) : ( + <> + you will need to{' '} + override the package derivation. + + )} + +) : null} ); }; @@ -48,6 +83,8 @@ To get up and running with jellyseerr using Nix, you can add the following to yo If you want more advanced configuration options, you can use the following: + + ```nix { config, pkgs, ... }: @@ -56,53 +93,20 @@ If you want more advanced configuration options, you can use the following: enable = true; port = 5055; openFirewall = true; + package = pkgs.jellyseerr; # Use the unstable package if stable is not up-to-date }; } ``` - -After adding the configuration to your `configuration.nix`, you can run the following command to install jellyseerr: - -```bash -nixos-rebuild switch -``` -After rebuild is complete jellyseerr should be running, verify that it is with the following command. -```bash -systemctl status jellyseerr -``` - -:::info -You can now access Jellyseerr by visiting `http://localhost:5055` in your web browser. -::: - - - -import CodeBlock from '@theme/CodeBlock'; - -## Overriding the package derivation -export const VersionMatch = () => { - const jellyseerrVersion = JellyseerrVersion(); - const nixpkgVersion = NixpkgVersion(); - - const code = `{ config, pkgs, ... }: + + +In order to use postgres, you will need to add override the default module of jellyseerr with the following as the current default module is not compatible with postgres: +```nix { - nixpkgs.config.packageOverrides = pkgs: { - jellyseerr = pkgs.jellyseerr.overrideAttrs (oldAttrs: rec { - version = "${jellyseerrVersion}"; - - src = pkgs.fetchFromGitHub { - rev = "v\${version}"; - sha256 = pkgs.lib.fakeSha256; - }; - - offlineCache = pkgs.fetchYarnDeps { - sha256 = pkgs.lib.fakeSha256; - }; - }); - }; -}`; - - const module = `{ config, pkgs, lib, ... }: - + config, + pkgs, + lib, + ... +}: with lib; let cfg = config.services.jellyseerr; @@ -113,28 +117,65 @@ in disabledModules = [ "services/misc/jellyseerr.nix" ]; options.services.jellyseerr = { - enable = mkEnableOption (mdDoc ''Jellyseerr, a requests manager for Jellyfin''); + enable = mkEnableOption ''Jellyseerr, a requests manager for Jellyfin''; openFirewall = mkOption { type = types.bool; default = false; - description = mdDoc ''Open port in the firewall for the Jellyseerr web interface.''; + description = ''Open port in the firewall for the Jellyseerr web interface.''; }; port = mkOption { type = types.port; default = 5055; - description = mdDoc ''The port which the Jellyseerr web UI should listen to.''; + description = ''The port which the Jellyseerr web UI should listen to.''; }; package = mkOption { - type = types.package; - default = pkgs.jellyseerr; - defaultText = literalExpression "pkgs.jellyseerr"; - description = lib.mdDoc '' - Jellyseerr package to use. - ''; + type = types.package; + default = pkgs.jellyseerr; + defaultText = literalExpression "pkgs.jellyseerr"; + description = '' + Jellyseerr package to use. + ''; + }; + + databaseConfig = mkOption { + type = types.attrsOf types.str; + default = { + type = "sqlite"; + configDirectory = "config"; + logQueries = "false"; }; + description = '' + Database configuration. For "sqlite", only "type", "configDirectory", and "logQueries" are relevant. + For "postgres", include host, port, user, pass, name, and optionally socket. + Example: + { + type = "postgres"; + socket = "/run/postgresql"; + user = "jellyseerr"; + name = "jellyseerr"; + logQueries = "false"; + } + or + { + type = "postgres"; + host = "localhost"; + port = "5432"; + user = "dbuser"; + pass = "password"; + name = "jellyseerr"; + logQueries = "false"; + } + or + { + type = "sqlite"; + configDirectory = "config"; + logQueries = "false"; + } + ''; + }; }; config = mkIf cfg.enable { @@ -142,14 +183,29 @@ in description = "Jellyseerr, a requests manager for Jellyfin"; after = [ "network.target" ]; wantedBy = [ "multi-user.target" ]; - environment.PORT = toString cfg.port; + environment = + let + dbConfig = cfg.databaseConfig; + in + { + PORT = toString cfg.port; + DB_TYPE = toString dbConfig.type; + CONFIG_DIRECTORY = toString dbConfig.configDirectory or ""; + DB_LOG_QUERIES = toString dbConfig.logQueries; + DB_HOST = if dbConfig.type == "postgres" && !(hasAttr "socket" dbConfig) then toString dbConfig.host or "" else ""; + DB_PORT = if dbConfig.type == "postgres" && !(hasAttr "socket" dbConfig) then toString dbConfig.port or "" else ""; + DB_SOCKET_PATH = if dbConfig.type == "postgres" && hasAttr "socket" dbConfig then toString dbConfig.socket or "" else ""; + DB_USER = if dbConfig.type == "postgres" then toString dbConfig.user or "" else ""; + DB_PASS = if dbConfig.type == "postgres" then toString dbConfig.pass or "" else ""; + DB_NAME = if dbConfig.type == "postgres" then toString dbConfig.name or "" else ""; + }; serviceConfig = { Type = "exec"; StateDirectory = "jellyseerr"; - WorkingDirectory = "\${cfg.package}/libexec/jellyseerr/deps/jellyseerr"; + WorkingDirectory = "${cfg.package}/libexec/jellyseerr"; DynamicUser = true; - ExecStart = "\${cfg.package}/bin/jellyseerr"; - BindPaths = [ "/var/lib/jellyseerr/:\${cfg.package}/libexec/jellyseerr/deps/jellyseerr/config/" ]; + ExecStart = "${cfg.package}/bin/jellyseerr"; + BindPaths = [ "/var/lib/jellyseerr/:${cfg.package}/libexec/jellyseerr/config/" ]; Restart = "on-failure"; ProtectHome = true; ProtectSystem = "strict"; @@ -169,57 +225,47 @@ in }; }; - networking.firewall = mkIf cfg.openFirewall { - allowedTCPPorts = [ cfg.port ]; - }; + networking.firewall = mkIf cfg.openFirewall { allowedTCPPorts = [ cfg.port ]; }; }; -}`; - - const configuration = `{ config, pkgs, ... }: +} +``` +Then, import the module into your `configuration.nix`: +```nix +{ config, pkgs, ... }: { - imports = [ ./jellyseerr-module.nix ] + imports = [ ./modules/jellyseerr.nix ]; - services.jellyseerr = { - enable = true; - port = 5055; - openFirewall = true; - package = (pkgs.callPackage (import ../../../pkgs/jellyseerr) { }); + services.jellyseerr = { + enable = true; + port = 5055; + openFirewall = true; + package = pkgs.unstable.jellyseerr; # use the unstable package if stable is not up-to-date + databaseConfig = { + type = "postgres"; + host = "localhost"; # or socket: "/run/postgresql" + port = "5432"; # if using socket, this is not needed + user = "jellyseerr"; + pass = "jellyseerr"; + name = "jellyseerr"; + logQueries = "false"; }; -}`; + } +} +``` + + - const isUpToDate = jellyseerrVersion === nixpkgVersion; +After adding the configuration to your `configuration.nix`, you can run the following command to install jellyseerr: - return ( - <> - {isUpToDate ? ( - <> -

The latest version of Jellyseerr ({jellyseerrVersion}) and the Jellyseerr nixpkg package version ({nixpkgVersion}) is up-to-date.

-

There is no need to override the package derivation.

- - ) : ( - <> -

The latest version of Jellyseerr ({jellyseerrVersion}) and the Jellyseerr nixpkg version (v{nixpkgVersion}) is out-of-date. - If you want to use Jellyseerr v{jellyseerrVersion}, you will need to override the package derivation.

-

In order to override the package derivation:

-
    -
  1. Grab the latest nixpkg derivation for Jellyseerr
  2. -
  3. Grab the latest package.json for Jellyseerr
  4. -
  5. Add it to the same directory as the nixpkg derivation
  6. -
  7. Update the `src` and `offlineCache` attributes in the nixpkg derivation:
  8. - {code} - You can replace the sha256 with the actual hash that nixos-rebuild outputs when you run the command. -
  9. Grab this module and import it in your `configuration.nix`
  10. - {module} - We are using a custom module because the upstream module does not have a package option. -
  11. Call the new package in your `configuration.nix`
  12. - {configuration} -
- - )} - - ); +```bash +nixos-rebuild switch +``` +After rebuild is complete jellyseerr should be running, verify that it is with the following command. +```bash +systemctl status jellyseerr +``` -}; - - +:::info +You can now access Jellyseerr by visiting `http://localhost:5055` in your web browser. +::: diff --git a/docs/troubleshooting.mdx b/docs/troubleshooting.mdx new file mode 100644 index 00000000..2f3fed66 --- /dev/null +++ b/docs/troubleshooting.mdx @@ -0,0 +1,158 @@ +--- +title: Troubleshooting +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +## [TMDB] failed to retrieve/fetch XXX + +### Option 1: Change your DNS servers + +This error often comes from your Internet Service Provider (ISP) blocking TMDB API. The ISP may block the DNS resolution to the TMDB API hostname. + +To fix this, you can change your DNS servers to a public DNS service like Google's DNS or Cloudflare's DNS: + + + + +Add the following to your `docker run` command to use Google's DNS: +```bash +--dns=8.8.8.8 +``` +or for Cloudflare's DNS: +```bash +--dns=1.1.1.1 +``` + + + + + +Add the following to your `compose.yaml` to use Google's DNS: +```yaml +--- +services: + jellyseerr: + dns: + - 8.8.8.8 +``` +or for Cloudflare's DNS: +```yaml +--- +services: + jellyseerr: + dns: + - 1.1.1.1 +``` + + + + + +1. Open the Control Panel. +2. Click on Network and Internet. +3. Click on Network and Sharing Center. +4. Click on Change adapter settings. +5. Right-click the network interface connected to the internet and select Properties. +6. Select Internet Protocol Version 4 (TCP/IPv4) and click Properties. +7. Select Use the following DNS server addresses and enter `8.8.8.8` for Google's DNS or `1.1.1.1` for Cloudflare's DNS. + + + + + +1. Open a terminal. +2. Edit the `/etc/resolv.conf` file with your favorite text editor. +3. Add the following line to use Google's DNS: + ```bash + nameserver 8.8.8.8 + ``` + or for Cloudflare's DNS: + + ```bash + nameserver 1.1.1.1 + ``` + + + + +### Option 2: Force IPV4 resolution first + +Sometimes there are configuration issues with IPV6 that prevent the hostname resolution from working correctly. + +You can try to force the resolution to use IPV4 first by setting the `FORCE_IPV4_FIRST` environment variable to `true`: + + + + +Add the following to your `docker run` command: +```bash +-e "FORCE_IPV4_FIRST=true" +``` + + + + + +Add the following to your `compose.yaml`: +```yaml +--- +services: + jellyseerr: + environment: + - FORCE_IPV4_FIRST=true +``` + + + + +### Option 3: Use Jellyseerr through a proxy + +If you can't change your DNS servers or force IPV4 resolution, you can use Jellyseerr through a proxy. + +In some places (like China), the ISP blocks not only the DNS resolution but also the connection to the TMDB API. + +You can configure Jellyseerr to use a proxy with the [HTTP(S) Proxy](/using-jellyseerr/settings/general#https-proxy) setting. + +### Option 4: Check that your server can reach TMDB API + +Make sure that your server can reach the TMDB API by running the following command: + + + + +```bash +docker exec -it jellyseerr sh -c "apk update && apk add curl && curl -L https://api.themoviedb.org" +``` + + + + + +```bash +docker compose exec jellyseerr sh -c "apk update && apk add curl && curl -L https://api.themoviedb.org" +``` + + + + +In a terminal: +```bash +curl -L https://api.themoviedb.org +``` + + + + +In a PowerShell window: +```powershell +(Invoke-WebRequest -Uri "https://api.themoviedb.org" -Method Get).Content +``` + + + + + +If you can't get a response, then your server can't reach the TMDB API. +This is usually due to a network configuration issue or a firewall blocking the connection. diff --git a/docs/using-jellyseerr/backups.md b/docs/using-jellyseerr/backups.md new file mode 100644 index 00000000..47224b22 --- /dev/null +++ b/docs/using-jellyseerr/backups.md @@ -0,0 +1,93 @@ +--- +title: Backups +description: Understand which data you should back up. +sidebar_position: 4 +--- + +# Which data does Jellyseerr save and where? + +## Settings + +All configurations from the **Settings** panel in the Jellyseerr web UI are saved, including integrations with Radarr, Sonarr, Jellyfin, Plex, and notification settings. +These settings are stored in the `settings.json` file located in the Jellyseerr data folder. + +## User Data + +Apart from the settings, all other data—including user accounts, media requests, blacklist etc. are stored in the database (either SQLite or PostgreSQL). + +# Backup + +### SQLite + +If your backup system uses filesystem snapshots (such as Kubernetes with Volsync), you can directly back up the Jellyseerr data folder. +Otherwise, you need to stop the Jellyseerr application and back up the `config` folder. + +For advanced users, it's possible to back up the database without stopping the application by using the [SQLite CLI](https://www.sqlite.org/download.html). Run the following command to create a backup: + +```bash +sqlite3 db/db.sqlite3 ".backup '/tmp/jellyseerr_db.sqlite3.bak'" +``` + +Then, copy the `/tmp/jellyseerr_dump.sqlite3.bak` file to your desired backup location. + +### PostgreSQL + +You can back up the `config` folder and dump the PostgreSQL database without stopping the Jellyseerr application. + +Install [postgresql-client](https://www.postgresql.org/download/) and run the following command to create a backup (just replace the placeholders): + +:::info +Depending on how your PostgreSQL instance is configured, you may need to add these options to the command below. + + -h, --host=HOSTNAME database server host or socket directory + + -p, --port=PORT database server port number +::: + +```bash +pg_dump -U -d -f /tmp/jellyseerr_db.sql +``` + +# Restore + +### SQLite + +After restoring your `db/db.sqlite3` file and, optionally, the `settings.json` file, the `config` folder structure should look like this: + +``` +. +├── cache <-- Optional +├── db +│ └── db.sqlite3 +├── logs <-- Optional +└── settings.json <-- Optional (required if you want to avoid reconfiguring Jellyseerr) +``` + +Once the files are restored, start the Jellyseerr application. + +### PostgreSQL + +Install the [PostgreSQL client](https://www.postgresql.org/download/) and restore the PostgreSQL database using the following command (replace the placeholders accordingly): + +:::info +Depending on how your PostgreSQL instance is configured, you may need to add these options to the command below. + + -h, --host=HOSTNAME database server host or socket directory + + -p, --port=PORT database server port number +::: + +```bash +pg_restore -U -d /tmp/jellyseerr_db.sql +``` + +Optionally, restore the `settings.json` file. The `config` folder structure should look like this: + +``` +. +├── cache <-- Optional +├── logs <-- Optional +└── settings.json <-- Optional (required if you want to avoid reconfiguring Jellyseerr) +``` + +Once the database and files are restored, start the Jellyseerr application. diff --git a/docs/using-jellyseerr/notifications/discord.md b/docs/using-jellyseerr/notifications/discord.md index 016de30e..b39e283a 100644 --- a/docs/using-jellyseerr/notifications/discord.md +++ b/docs/using-jellyseerr/notifications/discord.md @@ -18,6 +18,10 @@ Users can optionally opt-in to being mentioned in Discord notifications by confi You can find the webhook URL in the Discord application, at **Server Settings → Integrations → Webhooks**. +### Notification Role ID (optional) + +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). + ### 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! diff --git a/docs/using-jellyseerr/settings/general.md b/docs/using-jellyseerr/settings/general.md index 9cec9b9f..08601d2a 100644 --- a/docs/using-jellyseerr/settings/general.md +++ b/docs/using-jellyseerr/settings/general.md @@ -12,6 +12,8 @@ This is your Jellyseerr API key, which can be used to integrate Jellyseerr with If you need to generate a new API key for any reason, simply click the button to the right of the text box. +If you want to set the API key, rather than letting it be randomly generated, you can use the API_KEY environment variable. Whatever that variable is set to will be your API key. + ## Application Title If you aren't a huge fan of the name "Jellyseerr" and would like to display something different to your users, you can customize the application title! @@ -56,9 +58,9 @@ You should enable this if you are having issues with loading images directly fro Set the default display language for Jellyseerr. Users can override this setting in their user settings. -## Discover Region & Discover Language +## Discover Region, Discover Language & Streaming Region -These settings filter content shown on the "Discover" home page based on regional availability and original language, respectively. Users can override these global settings by configuring these same options in their user settings. +These settings filter content shown on the "Discover" home page based on regional availability and original language, respectively. The Streaming Region filters the available streaming providers on the media page. Users can override these global settings by configuring these same options in their user settings. ## Hide Available Media diff --git a/docs/using-jellyseerr/users/editing-users.md b/docs/using-jellyseerr/users/editing-users.md index 13ddb760..8a04c3ad 100644 --- a/docs/using-jellyseerr/users/editing-users.md +++ b/docs/using-jellyseerr/users/editing-users.md @@ -35,7 +35,7 @@ Users can override the [global display language](/using-jellyseerr/settings/gene ### Discover Region & Discover Language -Users can override the [global filter settings](/using-jellyseerr/settings/general#discover-region-and-discover-language) to suit their own preferences. +Users can override the [global filter settings](/using-jellyseerr/settings/general#discover-region-discover-language--streaming-region) to suit their own preferences. ### Movie Request Limit & Series Request Limit diff --git a/gen-docs/docusaurus.config.ts b/gen-docs/docusaurus.config.ts index 1aed7043..637741b0 100644 --- a/gen-docs/docusaurus.config.ts +++ b/gen-docs/docusaurus.config.ts @@ -11,7 +11,7 @@ const config: Config = { baseUrl: '/', trailingSlash: false, - organizationName: 'Fallenbagel', + organizationName: 'fallenbagel', projectName: 'Jellyseerr', deploymentBranch: 'gh-pages', @@ -32,7 +32,7 @@ const config: Config = { routeBasePath: '/', path: '../docs', editUrl: - 'https://github.com/Fallenbagel/jellyseerr/edit/develop/docs/', + 'https://github.com/fallenbagel/jellyseerr/edit/develop/docs/', }, blog: false, pages: false, @@ -70,7 +70,7 @@ const config: Config = { }, items: [ { - href: 'https://github.com/Fallenbagel/jellyseerr', + href: 'https://github.com/fallenbagel/jellyseerr', label: 'GitHub', position: 'right', }, diff --git a/gen-docs/package.json b/gen-docs/package.json index 951dbc18..4be20686 100644 --- a/gen-docs/package.json +++ b/gen-docs/package.json @@ -47,6 +47,6 @@ ] }, "engines": { - "node": ">=18.0" + "node": ">=22.0" } } diff --git a/gen-docs/src/components/JellyseerrVersion/index.tsx b/gen-docs/src/components/JellyseerrVersion/index.tsx index 0c4857e5..355f7444 100644 --- a/gen-docs/src/components/JellyseerrVersion/index.tsx +++ b/gen-docs/src/components/JellyseerrVersion/index.tsx @@ -26,25 +26,37 @@ export const JellyseerrVersion = () => { }; export const NixpkgVersion = () => { - const [version, setVersion] = useState(null); + const [versions, setVersions] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const fetchVersion = async () => { try { - const url = - 'https://raw.githubusercontent.com/NixOS/nixpkgs/nixos-unstable/pkgs/servers/jellyseerr/default.nix'; - const response = await fetch(url); - const data = await response.text(); + const unstableUrl = + 'https://raw.githubusercontent.com/NixOS/nixpkgs/refs/heads/nixos-unstable/pkgs/by-name/je/jellyseerr/package.nix'; + const stableUrl = + 'https://raw.githubusercontent.com/NixOS/nixpkgs/refs/heads/nixos-24.11/pkgs/servers/jellyseerr/default.nix'; + + const [unstableResponse, stableResponse] = await Promise.all([ + fetch(unstableUrl), + fetch(stableUrl), + ]); + + const unstableData = await unstableResponse.text(); + const stableData = await stableResponse.text(); const versionRegex = /version\s*=\s*"([^"]+)"/; - const match = data.match(versionRegex); - if (match && match[1]) { - setVersion(match[1]); - } else { - setError('0.0.0'); - } + + const unstableMatch = unstableData.match(versionRegex); + const stableMatch = stableData.match(versionRegex); + + const unstableVersion = + unstableMatch && unstableMatch[1] ? unstableMatch[1] : '0.0.0'; + const stableVersion = + stableMatch && stableMatch[1] ? stableMatch[1] : '0.0.0'; + + setVersions({ unstable: unstableVersion, stable: stableVersion }); setLoading(false); } catch (err) { setError(err.message); @@ -63,5 +75,5 @@ export const NixpkgVersion = () => { return { error }; } - return version; + return versions; }; diff --git a/next.config.js b/next.config.js index 35a316c6..597cba32 100644 --- a/next.config.js +++ b/next.config.js @@ -10,7 +10,8 @@ module.exports = { remotePatterns: [ { hostname: 'gravatar.com' }, { hostname: 'image.tmdb.org' }, - { hostname: '*', protocol: 'https' }, + { hostname: 'artworks.thetvdb.com' }, + { hostname: 'plex.tv' }, ], }, webpack(config) { diff --git a/overseerr-api.yml b/overseerr-api.yml index d2403538..641ce5d7 100644 --- a/overseerr-api.yml +++ b/overseerr-api.yml @@ -38,6 +38,8 @@ tags: description: Endpoints related to getting service (Radarr/Sonarr) details. - name: watchlist description: Collection of media to watch later + - name: blacklist + description: Blacklisted media from discovery page. servers: - url: '{server}/api/v1' variables: @@ -46,6 +48,19 @@ servers: components: schemas: + Blacklist: + type: object + properties: + tmdbId: + type: number + example: 1 + title: + type: string + media: + $ref: '#/components/schemas/MediaInfo' + userId: + type: number + example: 1 Watchlist: type: object properties: @@ -128,10 +143,12 @@ components: properties: locale: type: string - region: + discoverRegion: type: string originalLanguage: type: string + streamingRegion: + type: string MainSettings: type: object properties: @@ -147,12 +164,6 @@ components: applicationUrl: type: string example: https://os.example.com - trustProxy: - type: boolean - example: true - csrfProtection: - type: boolean - example: false hideAvailable: type: boolean example: false @@ -171,6 +182,24 @@ components: defaultPermissions: type: number example: 32 + enableSpecialEpisodes: + type: boolean + example: false + NetworkSettings: + type: object + properties: + csrfProtection: + type: boolean + example: false + forceIpv4First: + type: boolean + example: false + dnsServers: + type: string + example: '1.1.1.1' + trustProxy: + type: boolean + example: true PlexLibrary: type: object properties: @@ -1258,6 +1287,8 @@ components: type: string webhookUrl: type: string + webhookRoleId: + type: string enableMentions: type: boolean SlackSettings: @@ -1319,6 +1350,8 @@ components: type: string chatId: type: string + messageThreadId: + type: string sendSilently: type: boolean PushbulletSettings: @@ -1802,6 +1835,9 @@ components: telegramChatId: type: string nullable: true + telegramMessageThreadId: + type: string + nullable: true telegramSendSilently: type: boolean nullable: true @@ -1915,6 +1951,11 @@ components: type: string native_name: type: string + OverrideRule: + type: object + properties: + id: + type: string securitySchemes: cookieAuth: type: apiKey @@ -1973,6 +2014,9 @@ paths: appDataPath: type: string example: /app/config + appDataPermissions: + type: boolean + example: true /settings/main: get: summary: Get main settings @@ -2004,6 +2048,37 @@ paths: application/json: schema: $ref: '#/components/schemas/MainSettings' + /settings/network: + get: + summary: Get network settings + description: Retrieves all network settings in a JSON object. + tags: + - settings + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/MainSettings' + post: + summary: Update network settings + description: Updates network settings with the provided values. + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/NetworkSettings' + responses: + '200': + description: 'Values were sucessfully updated' + content: + application/json: + schema: + $ref: '#/components/schemas/NetworkSettings' /settings/main/regenerate: post: summary: Get main settings with newly-generated API key @@ -2775,6 +2850,15 @@ paths: imageCount: type: number example: 123 + avatar: + type: object + properties: + size: + type: number + example: 123456 + imageCount: + type: number + example: 123 apiCaches: type: array items: @@ -3726,6 +3810,11 @@ paths: type: string enum: [created, updated, requests, displayname] default: created + - in: query + name: q + required: false + schema: + type: string responses: '200': description: A JSON array of all users @@ -3842,7 +3931,7 @@ paths: schema: type: object properties: - jellyfinIds: + jellyfinUserIds: type: array items: type: string @@ -4042,6 +4131,109 @@ paths: restricted: type: boolean example: false + /blacklist: + get: + summary: Returns blacklisted items + description: Returns list of all blacklisted media + tags: + - settings + parameters: + - in: query + name: take + schema: + type: number + nullable: true + example: 25 + - in: query + name: skip + schema: + type: number + nullable: true + example: 0 + - in: query + name: search + schema: + type: string + nullable: true + example: dune + responses: + '200': + description: Blacklisted items returned + content: + application/json: + schema: + type: object + properties: + pageInfo: + $ref: '#/components/schemas/PageInfo' + results: + type: array + items: + type: object + properties: + user: + $ref: '#/components/schemas/User' + createdAt: + type: string + example: 2024-04-21T01:55:44.000Z + id: + type: number + example: 1 + mediaType: + type: string + example: movie + title: + type: string + example: Dune + tmdbId: + type: number + example: 438631 + post: + summary: Add media to blacklist + tags: + - blacklist + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Blacklist' + responses: + '201': + description: Item succesfully blacklisted + '412': + description: Item has already been blacklisted + /blacklist/{tmdbId}: + get: + summary: Get media from blacklist + tags: + - blacklist + parameters: + - in: path + name: tmdbId + description: tmdbId ID + required: true + example: '1' + schema: + type: string + responses: + '200': + description: Blacklist details in JSON + delete: + summary: Remove media from blacklist + tags: + - blacklist + parameters: + - in: path + name: tmdbId + description: tmdbId ID + required: true + example: '1' + schema: + type: string + responses: + '204': + description: Succesfully removed media item /watchlist: post: summary: Add media to watchlist @@ -5304,6 +5496,13 @@ paths: type: string enum: [added, modified] default: added + - in: query + name: sortDirection + schema: + type: string + enum: [asc, desc] + nullable: true + default: desc - in: query name: requestedBy schema: @@ -5354,7 +5553,7 @@ paths: - type: array items: type: number - minimum: 1 + minimum: 0 - type: string enum: [all] is4k: @@ -5460,7 +5659,7 @@ paths: type: array items: type: number - minimum: 1 + minimum: 0 is4k: type: boolean example: false @@ -6824,6 +7023,74 @@ paths: type: array items: $ref: '#/components/schemas/WatchProviderDetails' + /overrideRule: + get: + summary: Get override rules + description: Returns a list of all override rules with their conditions and settings + tags: + - overriderule + responses: + '200': + description: Override rules returned + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/OverrideRule' + post: + summary: Create override rule + description: Creates a new Override Rule from the request body. + tags: + - overriderule + responses: + '200': + description: 'Values were successfully created' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/OverrideRule' + /overrideRule/{ruleId}: + put: + summary: Update override rule + description: Updates an Override Rule from the request body. + tags: + - overriderule + parameters: + - in: path + name: ruleId + required: true + schema: + type: number + responses: + '200': + description: 'Values were successfully updated' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/OverrideRule' + delete: + summary: Delete override rule by ID + description: Deletes the override rule with the provided ruleId. + tags: + - overriderule + parameters: + - in: path + name: ruleId + required: true + schema: + type: number + responses: + '200': + description: Override rule successfully deleted + content: + application/json: + schema: + $ref: '#/components/schemas/OverrideRule' security: - cookieAuth: [] - apiKey: [] diff --git a/package.json b/package.json index 426d774d..6e6500ed 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@supercharge/request-ip": "1.2.0", "@svgr/webpack": "6.5.1", "@tanem/react-nprogress": "5.0.30", + "@types/wink-jaro-distance": "^2.0.2", "ace-builds": "1.15.2", "bcrypt": "5.1.0", "bowser": "2.11.0", @@ -62,12 +63,14 @@ "formik": "^2.4.6", "gravatar-url": "3.1.0", "lodash": "4.17.21", + "mime": "3", "next": "^14.2.4", "node-cache": "5.1.2", "node-gyp": "9.3.1", "node-schedule": "2.1.1", "nodemailer": "6.9.1", "openpgp": "5.7.0", + "pg": "8.11.0", "plex-api": "5.3.2", "pug": "3.0.2", "react": "^18.3.1", @@ -92,8 +95,10 @@ "sqlite3": "5.1.4", "swagger-ui-express": "4.6.2", "swr": "2.2.5", - "typeorm": "0.3.12", + "typeorm": "0.3.11", + "undici": "^6.20.1", "web-push": "3.5.0", + "wink-jaro-distance": "^2.0.0", "winston": "3.8.2", "winston-daily-rotate-file": "4.7.1", "xml2js": "0.4.23", @@ -119,7 +124,8 @@ "@types/express": "4.17.17", "@types/express-session": "1.17.6", "@types/lodash": "4.14.191", - "@types/node": "20.14.8", + "@types/mime": "3", + "@types/node": "22.10.5", "@types/node-schedule": "2.1.0", "@types/nodemailer": "6.4.7", "@types/react": "^18.3.3", @@ -165,7 +171,7 @@ "typescript": "4.9.5" }, "engines": { - "node": "^20.0.0", + "node": "^22.0.0", "pnpm": "^9.0.0" }, "overrides": { @@ -231,7 +237,8 @@ "COMMIT_TAG": "$GIT_SHA" }, "imageNames": [ - "fallenbagel/jellyseerr" + "fallenbagel/jellyseerr", + "ghcr.io/fallenbagel/jellyseerr" ], "platforms": [ "linux/amd64", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ea593fea..de1247df 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,6 +38,9 @@ importers: '@tanem/react-nprogress': specifier: 5.0.30 version: 5.0.30(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/wink-jaro-distance': + specifier: ^2.0.2 + version: 2.0.2 ace-builds: specifier: 1.15.2 version: 1.15.2 @@ -49,7 +52,7 @@ importers: version: 2.11.0 connect-typeorm: specifier: 1.1.4 - version: 1.1.4(typeorm@0.3.12(sqlite3@5.1.4(encoding@0.1.13))(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5))) + version: 1.1.4(typeorm@0.3.11(pg@8.11.0)(sqlite3@5.1.4(encoding@0.1.13))(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.10.5)(typescript@4.9.5))) cookie-parser: specifier: 1.4.6 version: 1.4.6 @@ -98,6 +101,9 @@ importers: lodash: specifier: 4.17.21 version: 4.17.21 + mime: + specifier: '3' + version: 3.0.0 next: specifier: ^14.2.4 version: 14.2.4(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -116,6 +122,9 @@ importers: openpgp: specifier: 5.7.0 version: 5.7.0 + pg: + specifier: 8.11.0 + version: 8.11.0 plex-api: specifier: 5.3.2 version: 5.3.2 @@ -154,7 +163,7 @@ importers: version: 5.7.0(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-spring: specifier: 9.7.1 - version: 9.7.1(@react-three/fiber@8.16.8(react-dom@18.3.1(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(encoding@0.1.13)(react@18.3.1))(react@18.3.1)(three@0.165.0))(konva@9.3.12)(react-dom@18.3.1(react@18.3.1))(react-konva@18.2.10(konva@9.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(encoding@0.1.13)(react@18.3.1))(react-zdog@1.2.2)(react@18.3.1)(three@0.165.0)(zdog@1.1.3) + version: 9.7.1(@react-three/fiber@8.16.8(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(encoding@0.1.13)(react@18.3.1))(react@18.3.1)(three@0.165.0))(konva@9.3.12)(react-dom@18.3.1(react@18.3.1))(react-konva@18.2.10(@types/react@18.3.3)(konva@9.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(encoding@0.1.13)(react@18.3.1))(react-zdog@1.2.2)(react@18.3.1)(three@0.165.0)(zdog@1.1.3) react-tailwindcss-datepicker-sct: specifier: 1.3.4 version: 1.3.4(dayjs@1.11.7)(react@18.3.1) @@ -189,11 +198,17 @@ importers: specifier: 2.2.5 version: 2.2.5(react@18.3.1) typeorm: - specifier: 0.3.12 - version: 0.3.12(sqlite3@5.1.4(encoding@0.1.13))(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5)) + specifier: 0.3.11 + version: 0.3.11(pg@8.11.0)(sqlite3@5.1.4(encoding@0.1.13))(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.10.5)(typescript@4.9.5)) + undici: + specifier: ^6.20.1 + version: 6.20.1 web-push: specifier: 3.5.0 version: 3.5.0 + wink-jaro-distance: + specifier: ^2.0.0 + version: 2.0.0 winston: specifier: 3.8.2 version: 3.8.2 @@ -233,13 +248,13 @@ importers: version: 10.0.1(semantic-release@19.0.5(encoding@0.1.13)) '@tailwindcss/aspect-ratio': specifier: 0.4.2 - version: 0.4.2(tailwindcss@3.2.7(postcss@8.4.21)(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5))) + version: 0.4.2(tailwindcss@3.2.7(postcss@8.4.21)(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.10.5)(typescript@4.9.5))) '@tailwindcss/forms': specifier: 0.5.3 - version: 0.5.3(tailwindcss@3.2.7(postcss@8.4.21)(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5))) + version: 0.5.3(tailwindcss@3.2.7(postcss@8.4.21)(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.10.5)(typescript@4.9.5))) '@tailwindcss/typography': specifier: 0.5.9 - version: 0.5.9(tailwindcss@3.2.7(postcss@8.4.21)(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5))) + version: 0.5.9(tailwindcss@3.2.7(postcss@8.4.21)(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.10.5)(typescript@4.9.5))) '@types/bcrypt': specifier: 5.0.0 version: 5.0.0 @@ -264,9 +279,12 @@ importers: '@types/lodash': specifier: 4.14.191 version: 4.14.191 + '@types/mime': + specifier: '3' + version: 3.0.4 '@types/node': - specifier: 20.14.8 - version: 20.14.8 + specifier: 22.10.5 + version: 22.10.5 '@types/node-schedule': specifier: 2.1.0 version: 2.1.0 @@ -314,7 +332,7 @@ importers: version: 10.4.13(postcss@8.4.21) commitizen: specifier: 4.3.0 - version: 4.3.0(@types/node@20.14.8)(typescript@4.9.5) + version: 4.3.0(@types/node@22.10.5)(typescript@4.9.5) copyfiles: specifier: 2.4.1 version: 2.4.1 @@ -326,7 +344,7 @@ importers: version: 12.7.0 cz-conventional-changelog: specifier: 3.3.0 - version: 3.3.0(@types/node@20.14.8)(typescript@4.9.5) + version: 3.3.0(@types/node@22.10.5)(typescript@4.9.5) eslint: specifier: 8.35.0 version: 8.35.0 @@ -383,10 +401,10 @@ importers: version: 1.0.1(semantic-release@19.0.5(encoding@0.1.13)) tailwindcss: specifier: 3.2.7 - version: 3.2.7(postcss@8.4.21)(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5)) + version: 3.2.7(postcss@8.4.21)(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.10.5)(typescript@4.9.5)) ts-node: specifier: 10.9.1 - version: 10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5) + version: 10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.10.5)(typescript@4.9.5) tsc-alias: specifier: 1.8.2 version: 1.8.2 @@ -410,22 +428,42 @@ packages: resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==} engines: {node: '>=6.9.0'} + '@babel/code-frame@7.26.2': + resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} + engines: {node: '>=6.9.0'} + '@babel/compat-data@7.24.7': resolution: {integrity: sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==} engines: {node: '>=6.9.0'} + '@babel/compat-data@7.26.3': + resolution: {integrity: sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g==} + engines: {node: '>=6.9.0'} + '@babel/core@7.24.7': resolution: {integrity: sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==} engines: {node: '>=6.9.0'} + '@babel/core@7.26.0': + resolution: {integrity: sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==} + engines: {node: '>=6.9.0'} + '@babel/generator@7.24.7': resolution: {integrity: sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==} engines: {node: '>=6.9.0'} + '@babel/generator@7.26.3': + resolution: {integrity: sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==} + engines: {node: '>=6.9.0'} + '@babel/helper-annotate-as-pure@7.24.7': resolution: {integrity: sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==} engines: {node: '>=6.9.0'} + '@babel/helper-annotate-as-pure@7.25.9': + resolution: {integrity: sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==} + engines: {node: '>=6.9.0'} + '@babel/helper-builder-binary-assignment-operator-visitor@7.24.7': resolution: {integrity: sha512-xZeCVVdwb4MsDBkkyZ64tReWYrLRHlMN72vP7Bdm3OUOuyFZExhsHUUnuWnm2/XOlAJzR0LfPpB56WXZn0X/lA==} engines: {node: '>=6.9.0'} @@ -434,23 +472,44 @@ packages: resolution: {integrity: sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==} engines: {node: '>=6.9.0'} + '@babel/helper-compilation-targets@7.25.9': + resolution: {integrity: sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==} + engines: {node: '>=6.9.0'} + '@babel/helper-create-class-features-plugin@7.24.7': resolution: {integrity: sha512-kTkaDl7c9vO80zeX1rJxnuRpEsD5tA81yh11X1gQo+PhSti3JS+7qeZo9U4RHobKRiFPKaGK3svUAeb8D0Q7eg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 + '@babel/helper-create-class-features-plugin@7.25.9': + resolution: {integrity: sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + '@babel/helper-create-regexp-features-plugin@7.24.7': resolution: {integrity: sha512-03TCmXy2FtXJEZfbXDTSqq1fRJArk7lX9DOFC/47VthYcxyIOx+eXQmdo6DOQvrbpIix+KfXwvuXdFDZHxt+rA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 + '@babel/helper-create-regexp-features-plugin@7.26.3': + resolution: {integrity: sha512-G7ZRb40uUgdKOQqPLjfD12ZmGA54PzqDFUv2BKImnC9QIfGhIHKvVML0oN8IUiDq4iRqpq74ABpvOaerfWdong==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + '@babel/helper-define-polyfill-provider@0.6.2': resolution: {integrity: sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + '@babel/helper-define-polyfill-provider@0.6.3': + resolution: {integrity: sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + '@babel/helper-environment-visitor@7.24.7': resolution: {integrity: sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==} engines: {node: '>=6.9.0'} @@ -467,36 +526,70 @@ packages: resolution: {integrity: sha512-LGeMaf5JN4hAT471eJdBs/GK1DoYIJ5GCtZN/EsL6KUiiDZOvO/eKE11AMZJa2zP4zk4qe9V2O/hxAmkRc8p6w==} engines: {node: '>=6.9.0'} + '@babel/helper-member-expression-to-functions@7.25.9': + resolution: {integrity: sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==} + engines: {node: '>=6.9.0'} + '@babel/helper-module-imports@7.24.7': resolution: {integrity: sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==} engines: {node: '>=6.9.0'} + '@babel/helper-module-imports@7.25.9': + resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==} + engines: {node: '>=6.9.0'} + '@babel/helper-module-transforms@7.24.7': resolution: {integrity: sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 + '@babel/helper-module-transforms@7.26.0': + resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + '@babel/helper-optimise-call-expression@7.24.7': resolution: {integrity: sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A==} engines: {node: '>=6.9.0'} + '@babel/helper-optimise-call-expression@7.25.9': + resolution: {integrity: sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==} + engines: {node: '>=6.9.0'} + '@babel/helper-plugin-utils@7.24.7': resolution: {integrity: sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==} engines: {node: '>=6.9.0'} + '@babel/helper-plugin-utils@7.25.9': + resolution: {integrity: sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==} + engines: {node: '>=6.9.0'} + '@babel/helper-remap-async-to-generator@7.24.7': resolution: {integrity: sha512-9pKLcTlZ92hNZMQfGCHImUpDOlAgkkpqalWEeftW5FBya75k8Li2ilerxkM/uBEj01iBZXcCIB/bwvDYgWyibA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 + '@babel/helper-remap-async-to-generator@7.25.9': + resolution: {integrity: sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + '@babel/helper-replace-supers@7.24.7': resolution: {integrity: sha512-qTAxxBM81VEyoAY0TtLrx1oAEJc09ZK67Q9ljQToqCnA+55eNwCORaxlKyu+rNfX86o8OXRUSNUnrtsAZXM9sg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 + '@babel/helper-replace-supers@7.25.9': + resolution: {integrity: sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + '@babel/helper-simple-access@7.24.7': resolution: {integrity: sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==} engines: {node: '>=6.9.0'} @@ -505,6 +598,10 @@ packages: resolution: {integrity: sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ==} engines: {node: '>=6.9.0'} + '@babel/helper-skip-transparent-expression-wrappers@7.25.9': + resolution: {integrity: sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==} + engines: {node: '>=6.9.0'} + '@babel/helper-split-export-declaration@7.24.7': resolution: {integrity: sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==} engines: {node: '>=6.9.0'} @@ -513,22 +610,42 @@ packages: resolution: {integrity: sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==} engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.25.9': + resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.24.7': resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.25.9': + resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.24.7': resolution: {integrity: sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.25.9': + resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} + engines: {node: '>=6.9.0'} + '@babel/helper-wrap-function@7.24.7': resolution: {integrity: sha512-N9JIYk3TD+1vq/wn77YnJOqMtfWhNewNE+DJV4puD2X7Ew9J4JvrzrFDfTfyv5EgEXVy9/Wt8QiOErzEmv5Ifw==} engines: {node: '>=6.9.0'} + '@babel/helper-wrap-function@7.25.9': + resolution: {integrity: sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==} + engines: {node: '>=6.9.0'} + '@babel/helpers@7.24.7': resolution: {integrity: sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==} engines: {node: '>=6.9.0'} + '@babel/helpers@7.26.0': + resolution: {integrity: sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==} + engines: {node: '>=6.9.0'} + '@babel/highlight@7.24.7': resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==} engines: {node: '>=6.9.0'} @@ -538,6 +655,11 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.26.3': + resolution: {integrity: sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.24.7': resolution: {integrity: sha512-TiT1ss81W80eQsN+722OaeQMY/G4yTb4G9JrqeiDADs3N8lbPMGldWi9x8tyqCW5NLx1Jh2AvkE6r6QvEltMMQ==} engines: {node: '>=6.9.0'} @@ -576,8 +698,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-proposal-export-default-from@7.24.7': - resolution: {integrity: sha512-CcmFwUJ3tKhLjPdt4NP+SHMshebytF8ZTYOv5ZDpkzq2sin80Wb5vJrGt8fhPrORQCfoSa0LAxC/DW+GAC5+Hw==} + '@babel/plugin-proposal-export-default-from@7.25.9': + resolution: {integrity: sha512-ykqgwNfSnNOB+C8fV5X4mG3AVmvu+WVxcaU9xHHtBb7PCrPeweMmPjGsn8eMaeJg6SJuoUuZENeeSWaarWqonQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -651,8 +773,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-export-default-from@7.24.7': - resolution: {integrity: sha512-bTPz4/635WQ9WhwsyPdxUJDVpsi/X9BMmy/8Rf/UAlOO4jSql4CxUCjWI5PiM+jG+c4LVPTScoTw80geFj9+Bw==} + '@babel/plugin-syntax-export-default-from@7.25.9': + resolution: {integrity: sha512-9MhJ/SMTsVqsd69GyQg89lYR4o9T+oDGv5F6IsigxxqFVOyR/IflDLYP8WDI1l8fkhNGGktqkvL5qwNCtGEpgQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -662,8 +784,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-flow@7.24.7': - resolution: {integrity: sha512-9G8GYT/dxn/D1IIKOUBmGX0mnmj46mGH9NnZyJLwtCpgh5f7D2VbuKodb+2s9m1Yavh1s7ASQN8lf0eqrb1LTw==} + '@babel/plugin-syntax-flow@7.26.0': + resolution: {integrity: sha512-B+O2DnPc0iG+YXFqOxv2WNuNU97ToWjOomUQ78DouOENWUaM5sVrmet9mcomUGQFwpJd//gvUagXBSdzO1fRKg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -696,6 +818,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-jsx@7.25.9': + resolution: {integrity: sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-logical-assignment-operators@7.10.4': resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} peerDependencies: @@ -744,6 +872,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-typescript@7.25.9': + resolution: {integrity: sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-unicode-sets-regex@7.18.6': resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} engines: {node: '>=6.9.0'} @@ -756,6 +890,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-arrow-functions@7.25.9': + resolution: {integrity: sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-async-generator-functions@7.24.7': resolution: {integrity: sha512-o+iF77e3u7ZS4AoAuJvapz9Fm001PuD2V3Lp6OSE4FYQke+cSewYtnek+THqGRWyQloRCyvWL1OkyfNEl9vr/g==} engines: {node: '>=6.9.0'} @@ -768,6 +908,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-async-to-generator@7.25.9': + resolution: {integrity: sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-block-scoped-functions@7.24.7': resolution: {integrity: sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ==} engines: {node: '>=6.9.0'} @@ -780,6 +926,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-block-scoping@7.25.9': + resolution: {integrity: sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-class-properties@7.24.7': resolution: {integrity: sha512-vKbfawVYayKcSeSR5YYzzyXvsDFWU2mD8U5TFeXtbCPLFUqe7GyCgvO6XDHzje862ODrOwy6WCPmKeWHbCFJ4w==} engines: {node: '>=6.9.0'} @@ -798,18 +950,36 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-classes@7.25.9': + resolution: {integrity: sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-computed-properties@7.24.7': resolution: {integrity: sha512-25cS7v+707Gu6Ds2oY6tCkUwsJ9YIDbggd9+cu9jzzDgiNq7hR/8dkzxWfKWnTic26vsI3EsCXNd4iEB6e8esQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-computed-properties@7.25.9': + resolution: {integrity: sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-destructuring@7.24.7': resolution: {integrity: sha512-19eJO/8kdCQ9zISOf+SEUJM/bAUIsvY3YDnXZTupUCQ8LgrWnsG/gFB9dvXqdXnRXMAM8fvt7b0CBKQHNGy1mw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-destructuring@7.25.9': + resolution: {integrity: sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-dotall-regex@7.24.7': resolution: {integrity: sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw==} engines: {node: '>=6.9.0'} @@ -840,8 +1010,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-flow-strip-types@7.24.7': - resolution: {integrity: sha512-cjRKJ7FobOH2eakx7Ja+KpJRj8+y+/SiB3ooYm/n2UJfxu0oEaOoxOinitkJcPqv9KxS0kxTGPUaR7L2XcXDXA==} + '@babel/plugin-transform-flow-strip-types@7.25.9': + resolution: {integrity: sha512-/VVukELzPDdci7UUsWQaSkhgnjIWXnIyRpM02ldxaVoFK96c41So8JcKT3m0gYjyv7j5FNPGS5vfELrWalkbDA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -858,6 +1028,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-function-name@7.25.9': + resolution: {integrity: sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-json-strings@7.24.7': resolution: {integrity: sha512-2yFnBGDvRuxAaE/f0vfBKvtnvvqU8tGpMHqMNpTN2oWMKIR3NqFkjaAgGwawhqK/pIN2T3XdjGPdaG0vDhOBGw==} engines: {node: '>=6.9.0'} @@ -870,6 +1046,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-literals@7.25.9': + resolution: {integrity: sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-logical-assignment-operators@7.24.7': resolution: {integrity: sha512-4D2tpwlQ1odXmTEIFWy9ELJcZHqrStlzK/dAOWYyxX3zT0iXQB6banjgeOJQXzEc4S0E0a5A+hahxPaEFYftsw==} engines: {node: '>=6.9.0'} @@ -894,6 +1076,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-modules-commonjs@7.26.3': + resolution: {integrity: sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-modules-systemjs@7.24.7': resolution: {integrity: sha512-GYQE0tW7YoaN13qFh3O1NCY4MPkUiAH3fiF7UcV/I3ajmDKEdG3l+UOcbAm4zUE3gnvUU+Eni7XrVKo9eO9auw==} engines: {node: '>=6.9.0'} @@ -912,6 +1100,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0 + '@babel/plugin-transform-named-capturing-groups-regex@7.25.9': + resolution: {integrity: sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + '@babel/plugin-transform-new-target@7.24.7': resolution: {integrity: sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA==} engines: {node: '>=6.9.0'} @@ -960,18 +1154,36 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-parameters@7.25.9': + resolution: {integrity: sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-private-methods@7.24.7': resolution: {integrity: sha512-COTCOkG2hn4JKGEKBADkA8WNb35TGkkRbI5iT845dB+NyqgO8Hn+ajPbSnIQznneJTa3d30scb6iz/DhH8GsJQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-private-methods@7.25.9': + resolution: {integrity: sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-private-property-in-object@7.24.7': resolution: {integrity: sha512-9z76mxwnwFxMyxZWEgdgECQglF2Q7cFLm0kMf8pGwt+GSJsY0cONKj/UuO4bOH0w/uAel3ekS4ra5CEAyJRmDA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-private-property-in-object@7.25.9': + resolution: {integrity: sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-property-literals@7.24.7': resolution: {integrity: sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA==} engines: {node: '>=6.9.0'} @@ -990,20 +1202,26 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-react-display-name@7.25.9': + resolution: {integrity: sha512-KJfMlYIUxQB1CJfO3e0+h0ZHWOTLCPP115Awhaz8U0Zpq36Gl/cXlpoyMRnUWlhNUBAzldnCiAZNvCDj7CrKxQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-react-jsx-development@7.24.7': resolution: {integrity: sha512-QG9EnzoGn+Qar7rxuW+ZOsbWOt56FvvI93xInqsZDC5fsekx1AlIO4KIJ5M+D0p0SqSH156EpmZyXq630B8OlQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-react-jsx-self@7.24.7': - resolution: {integrity: sha512-fOPQYbGSgH0HUp4UJO4sMBFjY6DuWq+2i8rixyUMb3CdGixs/gccURvYOAhajBdKDoGajFr3mUq5rH3phtkGzw==} + '@babel/plugin-transform-react-jsx-self@7.25.9': + resolution: {integrity: sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-react-jsx-source@7.24.7': - resolution: {integrity: sha512-J2z+MWzZHVOemyLweMqngXrgGC42jQ//R0KdxqkIz/OrbVIIlhFI3WigZ5fO+nwFvBlncr4MGapd8vTyc7RPNQ==} + '@babel/plugin-transform-react-jsx-source@7.25.9': + resolution: {integrity: sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -1014,6 +1232,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-react-jsx@7.25.9': + resolution: {integrity: sha512-s5XwpQYCqGerXl+Pu6VDL3x0j2d82eiV77UJ8a2mDHAW7j9SWRqQ2y1fNo1Z74CdcYipl5Z41zvjj4Nfzq36rw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-react-pure-annotations@7.24.7': resolution: {integrity: sha512-PLgBVk3fzbmEjBJ/u8kFzOqS9tUeDjiaWud/rRym/yjCo/M9cASPlnrd2ZmmZpQT40fOOrvR8jh+n8jikrOhNA==} engines: {node: '>=6.9.0'} @@ -1032,8 +1256,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-runtime@7.24.7': - resolution: {integrity: sha512-YqXjrk4C+a1kZjewqt+Mmu2UuV1s07y8kqcUf4qYLnoqemhR4gRQikhdAhSVJioMjVTu6Mo6pAbaypEA3jY6fw==} + '@babel/plugin-transform-runtime@7.25.9': + resolution: {integrity: sha512-nZp7GlEl+yULJrClz0SwHPqir3lc0zsPrDHQUcxGspSL7AKrexNSEfTbfqnDNJUO13bgKyfuOLMF8Xqtu8j3YQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -1044,18 +1268,36 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-shorthand-properties@7.25.9': + resolution: {integrity: sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-spread@7.24.7': resolution: {integrity: sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-spread@7.25.9': + resolution: {integrity: sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-sticky-regex@7.24.7': resolution: {integrity: sha512-kHPSIJc9v24zEml5geKg9Mjx5ULpfncj0wRpYtxbvKyTtHCYDkVE3aHQ03FrpEo4gEe2vrJJS1Y9CJTaThA52g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-sticky-regex@7.25.9': + resolution: {integrity: sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-template-literals@7.24.7': resolution: {integrity: sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw==} engines: {node: '>=6.9.0'} @@ -1074,6 +1316,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-typescript@7.26.3': + resolution: {integrity: sha512-6+5hpdr6mETwSKjmJUdYw0EIkATiQhnELWlE3kJFBwSg/BGIVwVaVbX+gOXBCdc7Ln1RXZxyWGecIXhUfnl7oA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-unicode-escapes@7.24.7': resolution: {integrity: sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw==} engines: {node: '>=6.9.0'} @@ -1092,6 +1340,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-unicode-regex@7.25.9': + resolution: {integrity: sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-unicode-sets-regex@7.24.7': resolution: {integrity: sha512-2G8aAvF4wy1w/AGZkemprdGMRg5o6zPNhbHVImRz3lss55TYCBd6xStN19rt8XJHq20sqV0JbyWjOWwQRwV/wg==} engines: {node: '>=6.9.0'} @@ -1104,8 +1358,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/preset-flow@7.24.7': - resolution: {integrity: sha512-NL3Lo0NorCU607zU3NwRyJbpaB6E3t0xtd3LfAQKDfkeX4/ggcDXvkmkW42QWT5owUeW/jAe4hn+2qvkV1IbfQ==} + '@babel/preset-flow@7.25.9': + resolution: {integrity: sha512-EASHsAhE+SSlEzJ4bzfusnXSHiU+JfAYzj+jbw2vgQKgq5HrUr8qs+vgtiEL5dOH6sEweI+PNt2D7AqrDSHyqQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -1127,8 +1381,14 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/register@7.24.6': - resolution: {integrity: sha512-WSuFCc2wCqMeXkz/i3yfAAsxwWflEgbVkZzivgAmXl/MxrXeoYFZOOPllbC8R8WTF7u61wSRQtDVZ1879cdu6w==} + '@babel/preset-typescript@7.26.0': + resolution: {integrity: sha512-NMk1IGZ5I/oHhoXEElcm+xUnL/szL6xflkFZmoEU9xj1qSJXpiS7rsspYo92B4DRCDvZn2erT5LdsCeXAKNCkg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/register@7.25.9': + resolution: {integrity: sha512-8D43jXtGsYmEeDvm4MWHYUpWf8iiXgWYx3fW7E7Wb7Oe6FWqJPl5K6TuFW0dOwNZzEE5rjlaSJYH9JjrUKJszA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -1140,18 +1400,34 @@ packages: resolution: {integrity: sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==} engines: {node: '>=6.9.0'} + '@babel/runtime@7.26.0': + resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==} + engines: {node: '>=6.9.0'} + '@babel/template@7.24.7': resolution: {integrity: sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==} engines: {node: '>=6.9.0'} + '@babel/template@7.25.9': + resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} + engines: {node: '>=6.9.0'} + '@babel/traverse@7.24.7': resolution: {integrity: sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==} engines: {node: '>=6.9.0'} + '@babel/traverse@7.26.4': + resolution: {integrity: sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==} + engines: {node: '>=6.9.0'} + '@babel/types@7.24.7': resolution: {integrity: sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==} engines: {node: '>=6.9.0'} + '@babel/types@7.26.3': + resolution: {integrity: sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==} + engines: {node: '>=6.9.0'} + '@colors/colors@1.5.0': resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} @@ -1630,6 +1906,10 @@ packages: resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} engines: {node: '>=6.0.0'} + '@jridgewell/gen-mapping@0.3.8': + resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} + engines: {node: '>=6.0.0'} + '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -1644,6 +1924,9 @@ packages: '@jridgewell/sourcemap-codec@1.4.15': resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} @@ -2710,8 +2993,8 @@ packages: '@swc/helpers@0.5.5': resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} - '@swc/types@0.1.9': - resolution: {integrity: sha512-qKnCno++jzcJ4lM4NTfYifm1EFSCeIfKiAHAfkENZAV5Kl9PjJIyd2yeeVv6c/2CckuLyv2NmRC5pv6pm2WQBg==} + '@swc/types@0.1.17': + resolution: {integrity: sha512-V5gRru+aD8YVyCOMAjMpWR1Ui577DD5KSJsHP8RAxopAH22jFz6GZd/qxqjO6MJHQhcsjvjOFXyDhyLQUnMveQ==} '@tailwindcss/aspect-ratio@0.4.2': resolution: {integrity: sha512-8QPrypskfBa7QIMuKHg2TA7BqES6vhBrDLOv8Unb6FcFyd3TjKbc6lcmb9UPQHxfl24sXoJ41ux/H7qQQvfaSQ==} @@ -2848,6 +3131,9 @@ 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==} @@ -2869,18 +3155,18 @@ packages: '@types/node@14.18.63': resolution: {integrity: sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==} - '@types/node@17.0.36': - resolution: {integrity: sha512-V3orv+ggDsWVHP99K3JlwtH20R7J4IhI1Kksgc+64q5VxgfRkQG8Ws3MFm/FZOKDYGy9feGFlZ70/HpCNe9QaA==} + '@types/node@17.0.45': + resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==} - '@types/node@18.19.39': - resolution: {integrity: sha512-nPwTRDKUctxw3di5b4TfT3I0sWDiWoPQCZjXhvdkINntwr8lcoVCKsTgnXeRubKIlfnV+eN/HYk6Jb40tbcEAQ==} - - '@types/node@20.14.8': - resolution: {integrity: sha512-DO+2/jZinXfROG7j7WKFn/3C6nFwxy2lLpgLjEXJz+0XKphZlTLJ14mo8Vfg8X5BWN6XjyESXq+LcYdT7tR3bA==} + '@types/node@18.19.70': + resolution: {integrity: sha512-RE+K0+KZoEpDUbGGctnGdkrLFwi1eYKTlIHNl2Um98mUkGsm1u2Ff6Ltd0e8DktTtC98uy7rSj+hO8t/QuLoVQ==} '@types/node@20.5.1': resolution: {integrity: sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg==} + '@types/node@22.10.5': + resolution: {integrity: sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==} + '@types/nodemailer@6.4.15': resolution: {integrity: sha512-0EBJxawVNjPkng1zm2vopRctuWVCxk34JcIlRuXSf54habUWdz1FB7wHDqOqvDa8Mtpt0Q3LTXQkAs2LNyK5jQ==} @@ -2899,6 +3185,9 @@ packages: '@types/prop-types@15.7.12': resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} + '@types/prop-types@15.7.14': + resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==} + '@types/qs@6.9.15': resolution: {integrity: sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==} @@ -2911,8 +3200,10 @@ packages: '@types/react-reconciler@0.26.7': resolution: {integrity: sha512-mBDYl8x+oyPX/VBb3E638N0B7xG+SPk/EAMcVPeexqus/5aTpTphQi0curhhshOqRrc9t6OPoJfEUkbymse/lQ==} - '@types/react-reconciler@0.28.8': - resolution: {integrity: sha512-SN9c4kxXZonFhbX4hJrZy37yw9e7EIxcpHCxQv5JUS18wDE5ovkQKlqQEkufdJCCMfuI9BnjUJvhYeJ9x5Ra7g==} + '@types/react-reconciler@0.28.9': + resolution: {integrity: sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==} + peerDependencies: + '@types/react': '*' '@types/react-transition-group@4.4.10': resolution: {integrity: sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==} @@ -2920,6 +3211,9 @@ packages: '@types/react-transition-group@4.4.5': resolution: {integrity: sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==} + '@types/react@18.3.18': + resolution: {integrity: sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==} + '@types/react@18.3.3': resolution: {integrity: sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==} @@ -2959,8 +3253,11 @@ packages: '@types/web-push@3.3.2': resolution: {integrity: sha512-JxWGVL/m7mWTIg4mRYO+A6s0jPmBkr4iJr39DqJpRJAc+jrPiEe1/asmkwerzRon8ZZDxaZJpsxpv0Z18Wo9gw==} - '@types/webxr@0.5.17': - resolution: {integrity: sha512-JYcclaQIlisHRXM9dMF7SeVvQ54kcYc7QK1eKCExCTLKWnZDxP4cp/rXH4Uoa1j5+5oQJ0Cc2sZC/PWiiG4q2g==} + '@types/webxr@0.5.20': + resolution: {integrity: sha512-JGpU6qiIJQKUuVSKx1GtQnHJGxRjtfGIhzO2ilq43VZZS//f1h1Sgexbdk+Lq+7569a6EYhOWrUpIruR/1Enmg==} + + '@types/wink-jaro-distance@2.0.2': + resolution: {integrity: sha512-Q79orp7qA/g/uLdFmqd5MtEa0ZfJW5X1WXikAu8IVHt24IrHWrcTNYNdPpLK5mwVg34C6FQnrv/DMtcUhjE/zA==} '@types/xml2js@0.4.11': resolution: {integrity: sha512-JdigeAKmCyoJUiQljjr7tQG3if9NkqGUgwEUqBvV0N7LM4HyQk7UXCnusRa1lnvXAEYJ8mw8GtZWioagNztOwA==} @@ -2974,8 +3271,8 @@ packages: '@types/yargs@15.0.19': resolution: {integrity: sha512-2XUaGVmyQjgyAZldf0D0c14vvo/yv0MhQBSTJcejMMaitsn3nxCB6TmH4G0ZQf+uxROOa9mpanoSm8h6SG/1ZA==} - '@types/yargs@17.0.32': - resolution: {integrity: sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==} + '@types/yargs@17.0.33': + resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} @@ -3136,6 +3433,11 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + acorn@8.14.0: + resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} + engines: {node: '>=0.4.0'} + hasBin: true + agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} @@ -3417,16 +3719,31 @@ packages: peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + babel-plugin-polyfill-corejs2@0.4.12: + resolution: {integrity: sha512-CPWT6BwvhrTO2d8QVorhTCQw9Y43zOu7G9HigcfxvepOU6b8o3tcWad6oVgZIsZCTt42FFv97aA7ZJsbM4+8og==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + babel-plugin-polyfill-corejs3@0.10.4: resolution: {integrity: sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + babel-plugin-polyfill-corejs3@0.10.6: + resolution: {integrity: sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + babel-plugin-polyfill-regenerator@0.6.2: resolution: {integrity: sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + babel-plugin-polyfill-regenerator@0.6.3: + resolution: {integrity: sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + babel-plugin-syntax-jsx@6.18.0: resolution: {integrity: sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==} @@ -3506,6 +3823,11 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + browserslist@4.24.3: + resolution: {integrity: sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} @@ -3518,6 +3840,10 @@ packages: buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + buffer-writer@2.0.0: + resolution: {integrity: sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==} + engines: {node: '>=4'} + buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} @@ -3528,10 +3854,6 @@ packages: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} - bytes@3.0.0: - resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==} - engines: {node: '>= 0.8'} - bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -3594,6 +3916,9 @@ packages: caniuse-lite@1.0.30001636: resolution: {integrity: sha512-bMg2vmr8XBsbL6Lr0UHXy/21m84FTxDLWn2FSqMd5PrlbMxwJlQnC2YWYxVgp66PZE+BBNF2jYQUBKCo1FDeZg==} + caniuse-lite@1.0.30001690: + resolution: {integrity: sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==} + cardinal@2.1.1: resolution: {integrity: sha512-JSr5eOgoEymtYHBjNWyjrMqet9Am2miJhlfKNdqLp6zoeAh0KN5dRAcxlecj5mAJrmQomgiOBj35xHLrFjqBpw==} hasBin: true @@ -3813,8 +4138,8 @@ packages: resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} engines: {node: '>= 0.6'} - compression@1.7.4: - resolution: {integrity: sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==} + compression@1.7.5: + resolution: {integrity: sha512-bQJ0YRck5ak3LgtnpKkiabX5pNF7tMUh1BSy2ZBOTh0Dim0BUu6aPPwByIns6/A5Prh8PufSPerMDUklpzes2Q==} engines: {node: '>= 0.8.0'} computed-style@0.1.4: @@ -4099,6 +4424,9 @@ packages: core-js-compat@3.37.1: resolution: {integrity: sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==} + core-js-compat@3.39.0: + resolution: {integrity: sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==} + core-util-is@1.0.2: resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} @@ -4255,10 +4583,6 @@ packages: resolution: {integrity: sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==} engines: {node: '>=0.11'} - date-fns@2.30.0: - resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} - engines: {node: '>=0.11'} - dateformat@3.0.3: resolution: {integrity: sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==} @@ -4296,6 +4620,15 @@ packages: supports-color: optional: true + debug@4.4.0: + resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + decamelize-keys@1.1.1: resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} engines: {node: '>=0.10.0'} @@ -4496,6 +4829,9 @@ packages: electron-to-chromium@1.4.810: resolution: {integrity: sha512-Kaxhu4T7SJGpRQx99tq216gCq2nMxJo+uuT6uzz9l8TVN2stL7M06MIIXAtr9jsrLs2Glflgf2vMQRepxawOdQ==} + electron-to-chromium@1.5.77: + resolution: {integrity: sha512-AnJSrt5JpRVgY6dgd5yccguLc5A7oMSF0Kt3fcW+Hp5WTuFbl5upeSFZbMZYy2o7jhmIhU8Ekrd82GhyXUqUUg==} + email-templates@9.0.0: resolution: {integrity: sha512-ap0p38jAq8FMy86Jp2b3hyCFDUA9utWfOuyKPWhrknmHrrT3n94viGcQIAsaQtUZGaJP/0dJ9w//XqvaZV/yYQ==} engines: {node: '>=10.0.0'} @@ -4525,6 +4861,10 @@ packages: resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} engines: {node: '>= 0.8'} + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + encoding-japanese@2.0.0: resolution: {integrity: sha512-++P0RhebUC8MJAwJOsT93dT+5oc5oPImp1HubZpAuCZ5kTLnhuuBhKHj2jJeO/Gj93idPBWmIuQ9QWMe5rX3pQ==} engines: {node: '>=8.10.0'} @@ -4562,8 +4902,8 @@ packages: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} - envinfo@7.13.0: - resolution: {integrity: sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q==} + envinfo@7.14.0: + resolution: {integrity: sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg==} engines: {node: '>=4'} hasBin: true @@ -4618,6 +4958,10 @@ packages: resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} engines: {node: '>=6'} + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + escape-goat@3.0.0: resolution: {integrity: sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==} engines: {node: '>=10'} @@ -4852,6 +5196,9 @@ packages: resolution: {integrity: sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==} engines: {node: '>=0.10.0'} + exponential-backoff@3.1.1: + resolution: {integrity: sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==} + express-openapi-validator@4.13.8: resolution: {integrity: sha512-89/sdkq+BKBuIyykaMl/vR9grFc3WFUPTjFo0THHbu+5g+q8rA7fKeoMfz+h84yOQIBcztmJ5ZJdk5uhEls31A==} @@ -4906,6 +5253,10 @@ packages: resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} engines: {node: '>=8.6.0'} + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} @@ -4916,8 +5267,8 @@ packages: resolution: {integrity: sha512-FChq8hbz65WMj4rstcQsFB0O7Cy++nmbNfLYnD9cYv2cRn8EG6k/MGn9kO/tjO66t09DLDugj3yL+V2o6Qftrg==} engines: {node: '>=10.0'} - fast-xml-parser@4.4.0: - resolution: {integrity: sha512-kLY3jFlwIYwBNDojclKsNAC12sfD6NwW74QB2CoNGPvtVxjliYehVunB3HYyNi+n4Tt1dAcgwYvmKF/Z18flqg==} + fast-xml-parser@4.5.1: + resolution: {integrity: sha512-y655CeyUQ+jj7KBbYMc4FG01V8ZQqjN+gDYGJ50RtfsUB8iG9AmwmwoAgeKLJdmueKKMrH1RJ7yXHTSoczdv5w==} hasBin: true fastq@1.17.1: @@ -5007,8 +5358,8 @@ packages: flow-enums-runtime@0.0.6: resolution: {integrity: sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==} - flow-parser@0.238.0: - resolution: {integrity: sha512-VE7XSv1epljsIN2YeBnxCmGJihpNIAnLLu/pPOdA+Gkso7qDltJwUi6vfHjgxdBbjSdAuPGnhuOHJUQG+yYwIg==} + flow-parser@0.258.0: + resolution: {integrity: sha512-/f3ui3WaPTRUtqnWaGzf/f352hn4VhqGOiuSVkgaW6SbHNp5EwdDoh6BF3zB9A6kcWhCpg/0x0A3aXU+KXugAA==} engines: {node: '>=0.4.0'} fn.name@1.1.0: @@ -5286,14 +5637,14 @@ packages: hermes-estree@0.19.1: resolution: {integrity: sha512-daLGV3Q2MKk8w4evNMKwS8zBE/rcpA800nu1Q5kM08IKijoSnPe9Uo1iIxzPKRkn95IxxsgBMPeYHt3VG4ej2g==} - hermes-estree@0.20.1: - resolution: {integrity: sha512-SQpZK4BzR48kuOg0v4pb3EAGNclzIlqMj3Opu/mu7bbAoFw6oig6cEt/RAi0zTFW/iW6Iz9X9ggGuZTAZ/yZHg==} + hermes-estree@0.23.1: + resolution: {integrity: sha512-eT5MU3f5aVhTqsfIReZ6n41X5sYn4IdQL0nvz6yO+MMlPxw49aSARHLg/MSehQftyjnrE8X6bYregzSumqc6cg==} hermes-parser@0.19.1: resolution: {integrity: sha512-Vp+bXzxYJWrpEuJ/vXxUsLnt0+y4q9zyi4zUlkLqD8FKv4LjIfOvP69R/9Lty3dCyKh0E2BU7Eypqr63/rKT/A==} - hermes-parser@0.20.1: - resolution: {integrity: sha512-BL5P83cwCogI8D7rrDCgsFY0tdYUtmFP9XaXtl2IQjC+2Xo+4okjfXintlTxcIwl4qeGddEl28Z11kbVIw0aNA==} + hermes-parser@0.23.1: + resolution: {integrity: sha512-oxl5h2DkFW83hT4DAUJorpah8ou4yvmweUzLJmmr6YV2cezduCdlil1AvU/a/xSsAFo4WUcNA4GoV5Bvq6JffA==} hermes-profile-transformer@0.0.6: resolution: {integrity: sha512-cnN7bQUm65UWOy6cbGcCcZ3rpwW8Q/j4OP5aWRhEry4Z2t2aR1cjrbp0BS+KiBN0smvP1caBgAuxutvyvJILzQ==} @@ -5380,8 +5731,8 @@ packages: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} - https-proxy-agent@7.0.4: - resolution: {integrity: sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==} + https-proxy-agent@7.0.5: + resolution: {integrity: sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==} engines: {node: '>= 14'} human-signals@1.1.1: @@ -5429,8 +5780,8 @@ packages: resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} engines: {node: '>= 4'} - image-size@1.1.1: - resolution: {integrity: sha512-541xKlUw6jr/6gGuk92F+mYM5zaFAc5ahphvkqvNe2bQ6gVBkd6bfrmVJ2t4KDAfikAYZyIqTnktX3i6/aQDrQ==} + image-size@1.2.0: + resolution: {integrity: sha512-4S8fwbO6w3GeCVN6OPtA9I5IGKkcDMPcKndtUlpJuCwu7JLjtj7JZpwqLuyY2nrmQT3AWsCJLSKPsc2mPBSl3w==} engines: {node: '>=16.x'} hasBin: true @@ -5552,6 +5903,10 @@ packages: resolution: {integrity: sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A==} engines: {node: '>= 0.4'} + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + is-data-view@1.0.1: resolution: {integrity: sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==} engines: {node: '>= 0.4'} @@ -5848,6 +6203,16 @@ packages: engines: {node: '>=4'} hasBin: true + jsesc@3.0.2: + resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} + engines: {node: '>=6'} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -6304,61 +6669,61 @@ packages: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} engines: {node: '>= 0.6'} - metro-babel-transformer@0.80.9: - resolution: {integrity: sha512-d76BSm64KZam1nifRZlNJmtwIgAeZhZG3fi3K+EmPOlrR8rDtBxQHDSN3fSGeNB9CirdTyabTMQCkCup6BXFSQ==} + metro-babel-transformer@0.80.12: + resolution: {integrity: sha512-YZziRs0MgA3pzCkkvOoQRXjIoVjvrpi/yRlJnObyIvMP6lFdtyG4nUGIwGY9VXnBvxmXD6mPY2e+NSw6JAyiRg==} engines: {node: '>=18'} - metro-cache-key@0.80.9: - resolution: {integrity: sha512-hRcYGhEiWIdM87hU0fBlcGr+tHDEAT+7LYNCW89p5JhErFt/QaAkVx4fb5bW3YtXGv5BTV7AspWPERoIb99CXg==} + metro-cache-key@0.80.12: + resolution: {integrity: sha512-o4BspKnugg/pE45ei0LGHVuBJXwRgruW7oSFAeSZvBKA/sGr0UhOGY3uycOgWInnS3v5yTTfiBA9lHlNRhsvGA==} engines: {node: '>=18'} - metro-cache@0.80.9: - resolution: {integrity: sha512-ujEdSI43QwI+Dj2xuNax8LMo8UgKuXJEdxJkzGPU6iIx42nYa1byQ+aADv/iPh5sh5a//h5FopraW5voXSgm2w==} + metro-cache@0.80.12: + resolution: {integrity: sha512-p5kNHh2KJ0pbQI/H7ZBPCEwkyNcSz7OUkslzsiIWBMPQGFJ/xArMwkV7I+GJcWh+b4m6zbLxE5fk6fqbVK1xGA==} engines: {node: '>=18'} - metro-config@0.80.9: - resolution: {integrity: sha512-28wW7CqS3eJrunRGnsibWldqgwRP9ywBEf7kg+uzUHkSFJNKPM1K3UNSngHmH0EZjomizqQA2Zi6/y6VdZMolg==} + metro-config@0.80.12: + resolution: {integrity: sha512-4rwOWwrhm62LjB12ytiuR5NgK1ZBNr24/He8mqCsC+HXZ+ATbrewLNztzbAZHtFsrxP4D4GLTGgh96pCpYLSAQ==} engines: {node: '>=18'} - metro-core@0.80.9: - resolution: {integrity: sha512-tbltWQn+XTdULkGdzHIxlxk4SdnKxttvQQV3wpqqFbHDteR4gwCyTR2RyYJvxgU7HELfHtrVbqgqAdlPByUSbg==} + metro-core@0.80.12: + resolution: {integrity: sha512-QqdJ/yAK+IpPs2HU/h5v2pKEdANBagSsc6DRSjnwSyJsCoHlmyJKCaCJ7KhWGx+N4OHxh37hoA8fc2CuZbx0Fw==} engines: {node: '>=18'} - metro-file-map@0.80.9: - resolution: {integrity: sha512-sBUjVtQMHagItJH/wGU9sn3k2u0nrCl0CdR4SFMO1tksXLKbkigyQx4cbpcyPVOAmGTVuy3jyvBlELaGCAhplQ==} + metro-file-map@0.80.12: + resolution: {integrity: sha512-sYdemWSlk66bWzW2wp79kcPMzwuG32x1ZF3otI0QZTmrnTaaTiGyhE66P1z6KR4n2Eu5QXiABa6EWbAQv0r8bw==} engines: {node: '>=18'} - metro-minify-terser@0.80.9: - resolution: {integrity: sha512-FEeCeFbkvvPuhjixZ1FYrXtO0araTpV6UbcnGgDUpH7s7eR5FG/PiJz3TsuuPP/HwCK19cZtQydcA2QrCw446A==} + metro-minify-terser@0.80.12: + resolution: {integrity: sha512-muWzUw3y5k+9083ZoX9VaJLWEV2Jcgi+Oan0Mmb/fBNMPqP9xVDuy4pOMn/HOiGndgfh/MK7s4bsjkyLJKMnXQ==} engines: {node: '>=18'} - metro-resolver@0.80.9: - resolution: {integrity: sha512-wAPIjkN59BQN6gocVsAvvpZ1+LQkkqUaswlT++cJafE/e54GoVkMNCmrR4BsgQHr9DknZ5Um/nKueeN7kaEz9w==} + metro-resolver@0.80.12: + resolution: {integrity: sha512-PR24gYRZnYHM3xT9pg6BdbrGbM/Cu1TcyIFBVlAk7qDAuHkUNQ1nMzWumWs+kwSvtd9eZGzHoucGJpTUEeLZAw==} engines: {node: '>=18'} - metro-runtime@0.80.9: - resolution: {integrity: sha512-8PTVIgrVcyU+X/rVCy/9yxNlvXsBCk5JwwkbAm/Dm+Abo6NBGtNjWF0M1Xo/NWCb4phamNWcD7cHdR91HhbJvg==} + metro-runtime@0.80.12: + resolution: {integrity: sha512-LIx7+92p5rpI0i6iB4S4GBvvLxStNt6fF0oPMaUd1Weku7jZdfkCZzmrtDD9CSQ6EPb0T9NUZoyXIxlBa3wOCw==} engines: {node: '>=18'} - metro-source-map@0.80.9: - resolution: {integrity: sha512-RMn+XS4VTJIwMPOUSj61xlxgBvPeY4G6s5uIn6kt6HB6A/k9ekhr65UkkDD7WzHYs3a9o869qU8tvOZvqeQzgw==} + metro-source-map@0.80.12: + resolution: {integrity: sha512-o+AXmE7hpvM8r8MKsx7TI21/eerYYy2DCDkWfoBkv+jNkl61khvDHlQn0cXZa6lrcNZiZkl9oHSMcwLLIrFmpw==} engines: {node: '>=18'} - metro-symbolicate@0.80.9: - resolution: {integrity: sha512-Ykae12rdqSs98hg41RKEToojuIW85wNdmSe/eHUgMkzbvCFNVgcC0w3dKZEhSsqQOXapXRlLtHkaHLil0UD/EA==} + metro-symbolicate@0.80.12: + resolution: {integrity: sha512-/dIpNdHksXkGHZXARZpL7doUzHqSNxgQ8+kQGxwpJuHnDhGkENxB5PS2QBaTDdEcmyTMjS53CN1rl9n1gR6fmw==} engines: {node: '>=18'} hasBin: true - metro-transform-plugins@0.80.9: - resolution: {integrity: sha512-UlDk/uc8UdfLNJhPbF3tvwajyuuygBcyp+yBuS/q0z3QSuN/EbLllY3rK8OTD9n4h00qZ/qgxGv/lMFJkwP4vg==} + metro-transform-plugins@0.80.12: + resolution: {integrity: sha512-WQWp00AcZvXuQdbjQbx1LzFR31IInlkCDYJNRs6gtEtAyhwpMMlL2KcHmdY+wjDO9RPcliZ+Xl1riOuBecVlPA==} engines: {node: '>=18'} - metro-transform-worker@0.80.9: - resolution: {integrity: sha512-c/IrzMUVnI0hSVVit4TXzt3A1GiUltGVlzCmLJWxNrBGHGrJhvgePj38+GXl1Xf4Fd4vx6qLUkKMQ3ux73bFLQ==} + metro-transform-worker@0.80.12: + resolution: {integrity: sha512-KAPFN1y3eVqEbKLx1I8WOarHPqDMUa8WelWxaJCNKO/yHCP26zELeqTJvhsQup+8uwB6EYi/sp0b6TGoh6lOEA==} engines: {node: '>=18'} - metro@0.80.9: - resolution: {integrity: sha512-Bc57Xf3GO2Xe4UWQsBj/oW6YfLPABEu8jfDVDiNmJvoQW4CO34oDPuYKe4KlXzXhcuNsqOtSxpbjCRRVjhhREg==} + metro@0.80.12: + resolution: {integrity: sha512-1UsH5FzJd9quUsD1qY+zUG4JY3jo3YEMxbMYH9jT6NK3j4iORhlwTK8fYTfAUBhDKjgLfKjAh7aoazNE23oIRA==} engines: {node: '>=18'} hasBin: true @@ -6429,10 +6794,18 @@ packages: resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==} engines: {node: '>=8.6'} + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} + mime-db@1.53.0: + resolution: {integrity: sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==} + engines: {node: '>= 0.6'} + mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} @@ -6545,11 +6918,6 @@ packages: engines: {node: '>=10'} hasBin: true - mkdirp@2.1.6: - resolution: {integrity: sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==} - engines: {node: '>=10'} - hasBin: true - modify-values@1.0.1: resolution: {integrity: sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==} engines: {node: '>=0.10.0'} @@ -6617,6 +6985,10 @@ packages: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} + negotiator@0.6.4: + resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} + engines: {node: '>= 0.6'} + neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} @@ -6697,6 +7069,9 @@ packages: node-releases@2.0.14: resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} + node-releases@2.0.19: + resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + node-schedule@2.1.1: resolution: {integrity: sha512-OXdegQq03OmXEjt2hZP33W2YPs/E5BcFQks46+G2gAxs4gHOIVD1u7EqlYLYSKsaIpyKCK9Gbk0ta1/gjRSMRQ==} engines: {node: '>=6'} @@ -6863,8 +7238,8 @@ packages: oauth-sign@0.9.0: resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==} - ob1@0.80.9: - resolution: {integrity: sha512-v9yOxowkZbxWhKOaaTyLjIm1aLy4ebMNcSn4NYJKOAI/Qv+SkfEfszpLr2GIxsccmb2Y2HA9qtsqiIJ80ucpVA==} + ob1@0.80.12: + resolution: {integrity: sha512-VMArClVT6LkhUGpnuEoBuyjG9rzUyEzg4PDkav6wK1cLhOK02gPCYFxoiB4mqVnrMhDpIzJcrGNAMVi9P+hXrw==} engines: {node: '>=18'} object-assign@4.1.1: @@ -7047,6 +7422,9 @@ packages: resolution: {integrity: sha512-wpgERjNkLrBiFmkMEjuZJEWKKDrNfHCKA1OhyN1wg1FrLkULbviEy6py1AyJUgZ72YWFbZ38FIpnqvVqAlDUwA==} engines: {node: '>=8'} + packet-reader@1.0.0: + resolution: {integrity: sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -7138,9 +7516,46 @@ packages: performance-now@2.1.0: resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + pg-cloudflare@1.1.1: + resolution: {integrity: sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==} + + pg-connection-string@2.7.0: + resolution: {integrity: sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-pool@3.7.0: + resolution: {integrity: sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.7.0: + resolution: {integrity: sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg@8.11.0: + resolution: {integrity: sha512-meLUVPn2TWgJyLmy7el3fQQVwft4gU5NGyvV0XbD41iU9Jbg8lCH4zexhIkihDzVHJStlt6r088G6/fWeNjhXA==} + engines: {node: '>= 8.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + picocolors@1.0.1: resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} @@ -7243,6 +7658,22 @@ packages: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.0: + resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -7530,8 +7961,8 @@ packages: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 - react-devtools-core@5.2.0: - resolution: {integrity: sha512-vZK+/gvxxsieAoAyYaiRIVFxlajb7KXhgBDV7OsoMzaAE+IqGpoxusBjIgq5ibqA2IloKu0p9n7tE68z1xs18A==} + react-devtools-core@5.3.2: + resolution: {integrity: sha512-crr9HkVrDiJ0A4zot89oS0Cgv0Oa4OG1Em4jit3P3ZxZSKPMYyMjfwMqgcJna9o625g8oN87rBm8SWWrSTBZxg==} react-dom@18.3.1: resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} @@ -7721,9 +8152,6 @@ packages: reflect-metadata@0.1.13: resolution: {integrity: sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==} - reflect-metadata@0.1.14: - resolution: {integrity: sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==} - reflect.getprototypeof@1.0.6: resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==} engines: {node: '>= 0.4'} @@ -7732,6 +8160,10 @@ packages: resolution: {integrity: sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==} engines: {node: '>=4'} + regenerate-unicode-properties@10.2.0: + resolution: {integrity: sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==} + engines: {node: '>=4'} + regenerate@1.4.2: resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} @@ -7756,10 +8188,21 @@ packages: resolution: {integrity: sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==} engines: {node: '>=4'} + regexpu-core@6.2.0: + resolution: {integrity: sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==} + engines: {node: '>=4'} + registry-auth-token@5.0.2: resolution: {integrity: sha512-o/3ikDxtXaA59BmZuZrJZDJv8NMDGSj+6j6XaeBmHw8eY1i1qd9+6H+LjVvQXx3HN6aRCGa1cUdJ9RaJZUugnQ==} engines: {node: '>=14'} + regjsgen@0.8.0: + resolution: {integrity: sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==} + + regjsparser@0.12.0: + resolution: {integrity: sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==} + hasBin: true + regjsparser@0.9.1: resolution: {integrity: sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==} hasBin: true @@ -7831,6 +8274,11 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve@1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + engines: {node: '>= 0.4'} + hasBin: true + resolve@1.22.8: resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} hasBin: true @@ -7986,10 +8434,19 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + engines: {node: '>=10'} + hasBin: true + send@0.18.0: resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} engines: {node: '>= 0.8.0'} + send@0.19.0: + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} + engines: {node: '>= 0.8.0'} + serialize-error@2.1.0: resolution: {integrity: sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==} engines: {node: '>=0.10.0'} @@ -7998,6 +8455,10 @@ packages: resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==} engines: {node: '>= 0.8.0'} + serve-static@1.16.2: + resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} + engines: {node: '>= 0.8.0'} + set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} @@ -8043,8 +8504,9 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - shell-quote@1.8.1: - resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==} + shell-quote@1.8.2: + resolution: {integrity: sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==} + engines: {node: '>= 0.4'} side-channel@1.0.6: resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} @@ -8156,6 +8618,10 @@ packages: split2@3.2.2: resolution: {integrity: sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==} + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + split@1.0.1: resolution: {integrity: sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==} @@ -8329,6 +8795,7 @@ packages: sudo-prompt@9.2.1: resolution: {integrity: sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} @@ -8404,8 +8871,8 @@ packages: resolution: {integrity: sha512-biM9brNqxSc04Ee71hzFbryD11nX7VPhQQY32AdDmjFvodsRFz/3ufeoTZ6uYkRFfGo188tENcASNs3vTdsM0w==} engines: {node: '>=10'} - terser@5.31.1: - resolution: {integrity: sha512-37upzU1+viGvuFtBo9NPufCb9dwM0+l9hMxYyWfBA+fbwrPqNJAhbZ6W47bBFnZHKHTUBnMvi87434qq+qnxOg==} + terser@5.37.0: + resolution: {integrity: sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==} engines: {node: '>=10'} hasBin: true @@ -8582,6 +9049,9 @@ packages: tslib@2.6.3: resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==} + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsscmp@1.0.6: resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==} engines: {node: '>=0.6.x'} @@ -8661,8 +9131,8 @@ packages: typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} - typeorm@0.3.12: - resolution: {integrity: sha512-sYSxBmCf1nJLLTcYtwqZ+lQIRtLPyUoO93rHTOKk9vJCyT4UfRtU7oRsJvfvKP3nnZTD1hzz2SEy2zwPEN6OyA==} + typeorm@0.3.11: + resolution: {integrity: sha512-pzdOyWbVuz/z8Ww6gqvBW4nylsM0KLdUCDExr2gR20/x1khGSVxQkjNV/3YqliG90jrWzrknYbYscpk8yxFJVg==} engines: {node: '>= 12.9.0'} hasBin: true peerDependencies: @@ -8673,7 +9143,7 @@ packages: ioredis: ^5.0.4 mongodb: ^3.6.0 mssql: ^7.3.0 - mysql2: ^2.2.5 || ^3.0.1 + mysql2: ^2.2.5 oracledb: ^5.1.0 pg: ^8.5.1 pg-native: ^3.0.0 @@ -8759,6 +9229,13 @@ packages: undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + undici-types@6.20.0: + resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} + + undici@6.20.1: + resolution: {integrity: sha512-AjQF1QsmqfJys+LXfGTNum+qw4S88CojRInG/6t31W/1fk6G59s92bnAvGz5Cmur+kQv2SURXEvvudLmbrE8QA==} + engines: {node: '>=18.17'} + unicode-canonical-property-names-ecmascript@2.0.0: resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==} engines: {node: '>=4'} @@ -8774,6 +9251,10 @@ packages: resolution: {integrity: sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==} engines: {node: '>=4'} + unicode-match-property-value-ecmascript@2.2.0: + resolution: {integrity: sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==} + engines: {node: '>=4'} + unicode-property-aliases-ecmascript@2.1.0: resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} engines: {node: '>=4'} @@ -8846,6 +9327,12 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + update-browserslist-db@1.1.1: + resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -8989,6 +9476,9 @@ packages: wide-align@1.1.5: resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + wink-jaro-distance@2.0.0: + resolution: {integrity: sha512-9bcUaXCi9N8iYpGWbFkf83OsBkg17r4hEyxusEzl+nnReLRPqxhB9YNeRn3g54SYnVRNXP029lY3HDsbdxTAuA==} + winston-daily-rotate-file@4.7.1: resolution: {integrity: sha512-7LGPiYGBPNyGHLn9z33i96zx/bd71pjBn9tqQzO3I4Tayv94WPmBNwKC7CO1wPHdP9uvu+Md/1nr6VSH9h0iaA==} engines: {node: '>=8'} @@ -9103,6 +9593,11 @@ packages: engines: {node: '>= 14'} hasBin: true + yaml@2.7.0: + resolution: {integrity: sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==} + engines: {node: '>= 14'} + hasBin: true + yamljs@0.3.0: resolution: {integrity: sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==} hasBin: true @@ -9180,8 +9675,16 @@ snapshots: '@babel/highlight': 7.24.7 picocolors: 1.0.1 + '@babel/code-frame@7.26.2': + dependencies: + '@babel/helper-validator-identifier': 7.25.9 + js-tokens: 4.0.0 + picocolors: 1.1.1 + '@babel/compat-data@7.24.7': {} + '@babel/compat-data@7.26.3': {} + '@babel/core@7.24.7': dependencies: '@ampproject/remapping': 2.3.0 @@ -9202,6 +9705,26 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/core@7.26.0': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.3 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helpers': 7.26.0 + '@babel/parser': 7.26.3 + '@babel/template': 7.25.9 + '@babel/traverse': 7.26.4 + '@babel/types': 7.26.3 + convert-source-map: 2.0.0 + debug: 4.4.0 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + '@babel/generator@7.24.7': dependencies: '@babel/types': 7.24.7 @@ -9209,10 +9732,22 @@ snapshots: '@jridgewell/trace-mapping': 0.3.25 jsesc: 2.5.2 + '@babel/generator@7.26.3': + dependencies: + '@babel/parser': 7.26.3 + '@babel/types': 7.26.3 + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 3.1.0 + '@babel/helper-annotate-as-pure@7.24.7': dependencies: '@babel/types': 7.24.7 + '@babel/helper-annotate-as-pure@7.25.9': + dependencies: + '@babel/types': 7.26.3 + '@babel/helper-builder-binary-assignment-operator-visitor@7.24.7': dependencies: '@babel/traverse': 7.24.7 @@ -9228,6 +9763,14 @@ snapshots: lru-cache: 5.1.1 semver: 6.3.1 + '@babel/helper-compilation-targets@7.25.9': + dependencies: + '@babel/compat-data': 7.26.3 + '@babel/helper-validator-option': 7.25.9 + browserslist: 4.24.3 + lru-cache: 5.1.1 + semver: 6.3.1 + '@babel/helper-create-class-features-plugin@7.24.7(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 @@ -9243,6 +9786,32 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-create-class-features-plugin@7.25.9(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-member-expression-to-functions': 7.25.9 + '@babel/helper-optimise-call-expression': 7.25.9 + '@babel/helper-replace-supers': 7.25.9(@babel/core@7.24.7) + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + '@babel/traverse': 7.26.4 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-create-class-features-plugin@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-member-expression-to-functions': 7.25.9 + '@babel/helper-optimise-call-expression': 7.25.9 + '@babel/helper-replace-supers': 7.25.9(@babel/core@7.26.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + '@babel/traverse': 7.26.4 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + '@babel/helper-create-regexp-features-plugin@7.24.7(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 @@ -9250,6 +9819,13 @@ snapshots: regexpu-core: 5.3.2 semver: 6.3.1 + '@babel/helper-create-regexp-features-plugin@7.26.3(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-annotate-as-pure': 7.25.9 + regexpu-core: 6.2.0 + semver: 6.3.1 + '@babel/helper-define-polyfill-provider@0.6.2(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 @@ -9261,6 +9837,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-define-polyfill-provider@0.6.3(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + debug: 4.4.0 + lodash.debounce: 4.0.8 + resolve: 1.22.10 + transitivePeerDependencies: + - supports-color + '@babel/helper-environment-visitor@7.24.7': dependencies: '@babel/types': 7.24.7 @@ -9281,6 +9868,13 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-member-expression-to-functions@7.25.9': + dependencies: + '@babel/traverse': 7.26.4 + '@babel/types': 7.26.3 + transitivePeerDependencies: + - supports-color + '@babel/helper-module-imports@7.24.7': dependencies: '@babel/traverse': 7.24.7 @@ -9288,6 +9882,13 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-module-imports@7.25.9': + dependencies: + '@babel/traverse': 7.26.4 + '@babel/types': 7.26.3 + transitivePeerDependencies: + - supports-color + '@babel/helper-module-transforms@7.24.7(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 @@ -9299,12 +9900,36 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-module-transforms@7.26.0(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + '@babel/traverse': 7.26.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + '@babel/traverse': 7.26.4 + transitivePeerDependencies: + - supports-color + '@babel/helper-optimise-call-expression@7.24.7': dependencies: '@babel/types': 7.24.7 + '@babel/helper-optimise-call-expression@7.25.9': + dependencies: + '@babel/types': 7.26.3 + '@babel/helper-plugin-utils@7.24.7': {} + '@babel/helper-plugin-utils@7.25.9': {} + '@babel/helper-remap-async-to-generator@7.24.7(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 @@ -9314,6 +9939,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-remap-async-to-generator@7.25.9(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-wrap-function': 7.25.9 + '@babel/traverse': 7.26.4 + transitivePeerDependencies: + - supports-color + '@babel/helper-replace-supers@7.24.7(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 @@ -9323,6 +9957,24 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-replace-supers@7.25.9(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-member-expression-to-functions': 7.25.9 + '@babel/helper-optimise-call-expression': 7.25.9 + '@babel/traverse': 7.26.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-replace-supers@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-member-expression-to-functions': 7.25.9 + '@babel/helper-optimise-call-expression': 7.25.9 + '@babel/traverse': 7.26.4 + transitivePeerDependencies: + - supports-color + '@babel/helper-simple-access@7.24.7': dependencies: '@babel/traverse': 7.24.7 @@ -9337,16 +9989,29 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-skip-transparent-expression-wrappers@7.25.9': + dependencies: + '@babel/traverse': 7.26.4 + '@babel/types': 7.26.3 + transitivePeerDependencies: + - supports-color + '@babel/helper-split-export-declaration@7.24.7': dependencies: '@babel/types': 7.24.7 '@babel/helper-string-parser@7.24.7': {} + '@babel/helper-string-parser@7.25.9': {} + '@babel/helper-validator-identifier@7.24.7': {} + '@babel/helper-validator-identifier@7.25.9': {} + '@babel/helper-validator-option@7.24.7': {} + '@babel/helper-validator-option@7.25.9': {} + '@babel/helper-wrap-function@7.24.7': dependencies: '@babel/helper-function-name': 7.24.7 @@ -9356,11 +10021,24 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-wrap-function@7.25.9': + dependencies: + '@babel/template': 7.25.9 + '@babel/traverse': 7.26.4 + '@babel/types': 7.26.3 + transitivePeerDependencies: + - supports-color + '@babel/helpers@7.24.7': dependencies: '@babel/template': 7.24.7 '@babel/types': 7.24.7 + '@babel/helpers@7.26.0': + dependencies: + '@babel/template': 7.25.9 + '@babel/types': 7.26.3 + '@babel/highlight@7.24.7': dependencies: '@babel/helper-validator-identifier': 7.24.7 @@ -9372,6 +10050,10 @@ snapshots: dependencies: '@babel/types': 7.24.7 + '@babel/parser@7.26.3': + dependencies: + '@babel/types': 7.26.3 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.24.7(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 @@ -9402,8 +10084,8 @@ snapshots: dependencies: '@babel/core': 7.24.7 '@babel/helper-environment-visitor': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/helper-remap-async-to-generator': 7.24.7(@babel/core@7.24.7) + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-remap-async-to-generator': 7.25.9(@babel/core@7.24.7) '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.7) transitivePeerDependencies: - supports-color @@ -9411,59 +10093,81 @@ snapshots: '@babel/plugin-proposal-class-properties@7.18.6(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 - '@babel/helper-create-class-features-plugin': 7.24.7(@babel/core@7.24.7) - '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.24.7) + '@babel/helper-plugin-utils': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-proposal-export-default-from@7.24.7(@babel/core@7.24.7)': + '@babel/plugin-proposal-class-properties@7.18.6(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-export-default-from@7.25.9(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-export-default-from': 7.24.7(@babel/core@7.24.7) + '@babel/helper-plugin-utils': 7.25.9 '@babel/plugin-proposal-logical-assignment-operators@7.20.7(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-plugin-utils': 7.25.9 '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.7) '@babel/plugin-proposal-nullish-coalescing-operator@7.18.6(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-plugin-utils': 7.25.9 '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.7) + '@babel/plugin-proposal-nullish-coalescing-operator@7.18.6(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-proposal-numeric-separator@7.18.6(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-plugin-utils': 7.25.9 '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.7) '@babel/plugin-proposal-object-rest-spread@7.20.7(@babel/core@7.24.7)': dependencies: - '@babel/compat-data': 7.24.7 + '@babel/compat-data': 7.26.3 '@babel/core': 7.24.7 - '@babel/helper-compilation-targets': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.7) - '@babel/plugin-transform-parameters': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-parameters': 7.25.9(@babel/core@7.24.7) '@babel/plugin-proposal-optional-catch-binding@7.18.6(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-plugin-utils': 7.25.9 '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.7) '@babel/plugin-proposal-optional-chaining@7.21.0(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.7) transitivePeerDependencies: - supports-color + '@babel/plugin-proposal-optional-chaining@7.21.0(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.26.0) + transitivePeerDependencies: + - supports-color + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 @@ -9488,20 +10192,25 @@ snapshots: '@babel/core': 7.24.7 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-export-default-from@7.24.7(@babel/core@7.24.7)': + '@babel/plugin-syntax-export-default-from@7.25.9(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-plugin-utils': 7.25.9 '@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-flow@7.24.7(@babel/core@7.24.7)': + '@babel/plugin-syntax-flow@7.26.0(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-flow@7.26.0(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 '@babel/plugin-syntax-import-assertions@7.24.7(@babel/core@7.24.7)': dependencies: @@ -9528,6 +10237,16 @@ snapshots: '@babel/core': 7.24.7 '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 @@ -9538,6 +10257,11 @@ snapshots: '@babel/core': 7.24.7 '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 @@ -9558,6 +10282,11 @@ snapshots: '@babel/core': 7.24.7 '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 @@ -9573,6 +10302,16 @@ snapshots: '@babel/core': 7.24.7 '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-typescript@7.25.9(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-typescript@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 @@ -9584,6 +10323,11 @@ snapshots: '@babel/core': 7.24.7 '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-transform-arrow-functions@7.25.9(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/plugin-transform-async-generator-functions@7.24.7(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 @@ -9603,6 +10347,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-async-to-generator@7.25.9(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-remap-async-to-generator': 7.25.9(@babel/core@7.24.7) + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-block-scoped-functions@7.24.7(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 @@ -9613,6 +10366,11 @@ snapshots: '@babel/core': 7.24.7 '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-transform-block-scoping@7.25.9(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/plugin-transform-class-properties@7.24.7(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 @@ -9644,17 +10402,40 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-classes@7.25.9(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-replace-supers': 7.25.9(@babel/core@7.24.7) + '@babel/traverse': 7.26.4 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-computed-properties@7.24.7(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 '@babel/helper-plugin-utils': 7.24.7 '@babel/template': 7.24.7 + '@babel/plugin-transform-computed-properties@7.25.9(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/template': 7.25.9 + '@babel/plugin-transform-destructuring@7.24.7(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-transform-destructuring@7.25.9(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/plugin-transform-dotall-regex@7.24.7(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 @@ -9686,11 +10467,17 @@ snapshots: '@babel/helper-plugin-utils': 7.24.7 '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.24.7) - '@babel/plugin-transform-flow-strip-types@7.24.7(@babel/core@7.24.7)': + '@babel/plugin-transform-flow-strip-types@7.25.9(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-flow': 7.24.7(@babel/core@7.24.7) + '@babel/helper-plugin-utils': 7.25.9 + '@babel/plugin-syntax-flow': 7.26.0(@babel/core@7.24.7) + + '@babel/plugin-transform-flow-strip-types@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/plugin-syntax-flow': 7.26.0(@babel/core@7.26.0) '@babel/plugin-transform-for-of@7.24.7(@babel/core@7.24.7)': dependencies: @@ -9707,6 +10494,15 @@ snapshots: '@babel/helper-function-name': 7.24.7 '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-transform-function-name@7.25.9(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/traverse': 7.26.4 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-json-strings@7.24.7(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 @@ -9718,6 +10514,11 @@ snapshots: '@babel/core': 7.24.7 '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-transform-literals@7.25.9(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/plugin-transform-logical-assignment-operators@7.24.7(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 @@ -9746,6 +10547,22 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-modules-commonjs@7.26.3(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.24.7) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-commonjs@7.26.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-modules-systemjs@7.24.7(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 @@ -9770,6 +10587,12 @@ snapshots: '@babel/helper-create-regexp-features-plugin': 7.24.7(@babel/core@7.24.7) '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-transform-named-capturing-groups-regex@7.25.9(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-create-regexp-features-plugin': 7.26.3(@babel/core@7.24.7) + '@babel/helper-plugin-utils': 7.25.9 + '@babel/plugin-transform-new-target@7.24.7(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 @@ -9823,6 +10646,11 @@ snapshots: '@babel/core': 7.24.7 '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-transform-parameters@7.25.9(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/plugin-transform-private-methods@7.24.7(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 @@ -9831,6 +10659,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-private-methods@7.25.9(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.24.7) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-private-property-in-object@7.24.7(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 @@ -9841,6 +10677,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-private-property-in-object@7.25.9(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.24.7) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-property-literals@7.24.7(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 @@ -9856,6 +10701,11 @@ snapshots: '@babel/core': 7.24.7 '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-transform-react-display-name@7.25.9(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/plugin-transform-react-jsx-development@7.24.7(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 @@ -9863,15 +10713,15 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-react-jsx-self@7.24.7(@babel/core@7.24.7)': + '@babel/plugin-transform-react-jsx-self@7.25.9(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-react-jsx-source@7.24.7(@babel/core@7.24.7)': + '@babel/plugin-transform-react-jsx-source@7.25.9(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-plugin-utils': 7.25.9 '@babel/plugin-transform-react-jsx@7.24.7(@babel/core@7.24.7)': dependencies: @@ -9884,6 +10734,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.24.7) + '@babel/types': 7.26.3 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-react-pure-annotations@7.24.7(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 @@ -9901,14 +10762,14 @@ snapshots: '@babel/core': 7.24.7 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-runtime@7.24.7(@babel/core@7.24.7)': + '@babel/plugin-transform-runtime@7.25.9(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 - '@babel/helper-module-imports': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - babel-plugin-polyfill-corejs2: 0.4.11(@babel/core@7.24.7) - babel-plugin-polyfill-corejs3: 0.10.4(@babel/core@7.24.7) - babel-plugin-polyfill-regenerator: 0.6.2(@babel/core@7.24.7) + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + babel-plugin-polyfill-corejs2: 0.4.12(@babel/core@7.24.7) + babel-plugin-polyfill-corejs3: 0.10.6(@babel/core@7.24.7) + babel-plugin-polyfill-regenerator: 0.6.3(@babel/core@7.24.7) semver: 6.3.1 transitivePeerDependencies: - supports-color @@ -9918,6 +10779,11 @@ snapshots: '@babel/core': 7.24.7 '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-transform-shorthand-properties@7.25.9(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/plugin-transform-spread@7.24.7(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 @@ -9926,11 +10792,24 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-spread@7.25.9(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-sticky-regex@7.24.7(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-transform-sticky-regex@7.25.9(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/plugin-transform-template-literals@7.24.7(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 @@ -9951,6 +10830,28 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-typescript@7.26.3(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.24.7) + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.24.7) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-typescript@7.26.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.0) + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-unicode-escapes@7.24.7(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 @@ -9968,6 +10869,12 @@ snapshots: '@babel/helper-create-regexp-features-plugin': 7.24.7(@babel/core@7.24.7) '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-transform-unicode-regex@7.25.9(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-create-regexp-features-plugin': 7.26.3(@babel/core@7.24.7) + '@babel/helper-plugin-utils': 7.25.9 + '@babel/plugin-transform-unicode-sets-regex@7.24.7(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 @@ -10061,12 +10968,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/preset-flow@7.24.7(@babel/core@7.24.7)': + '@babel/preset-flow@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/helper-validator-option': 7.24.7 - '@babel/plugin-transform-flow-strip-types': 7.24.7(@babel/core@7.24.7) + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-validator-option': 7.25.9 + '@babel/plugin-transform-flow-strip-types': 7.25.9(@babel/core@7.26.0) '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.24.7)': dependencies: @@ -10098,9 +11005,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/register@7.24.6(@babel/core@7.24.7)': + '@babel/preset-typescript@7.26.0(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.24.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-validator-option': 7.25.9 + '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-modules-commonjs': 7.26.3(@babel/core@7.26.0) + '@babel/plugin-transform-typescript': 7.26.3(@babel/core@7.26.0) + transitivePeerDependencies: + - supports-color + + '@babel/register@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 clone-deep: 4.0.1 find-cache-dir: 2.1.0 make-dir: 2.1.0 @@ -10113,12 +11031,22 @@ snapshots: dependencies: regenerator-runtime: 0.14.1 + '@babel/runtime@7.26.0': + dependencies: + regenerator-runtime: 0.14.1 + '@babel/template@7.24.7': dependencies: '@babel/code-frame': 7.24.7 '@babel/parser': 7.24.7 '@babel/types': 7.24.7 + '@babel/template@7.25.9': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/parser': 7.26.3 + '@babel/types': 7.26.3 + '@babel/traverse@7.24.7': dependencies: '@babel/code-frame': 7.24.7 @@ -10134,12 +11062,29 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/traverse@7.26.4': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.3 + '@babel/parser': 7.26.3 + '@babel/template': 7.25.9 + '@babel/types': 7.26.3 + debug: 4.4.0 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + '@babel/types@7.24.7': dependencies: '@babel/helper-string-parser': 7.24.7 '@babel/helper-validator-identifier': 7.24.7 to-fast-properties: 2.0.0 + '@babel/types@7.26.3': + dependencies: + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + '@colors/colors@1.5.0': {} '@colors/colors@1.6.0': {} @@ -10226,7 +11171,7 @@ snapshots: - '@swc/core' - '@swc/wasm' - '@commitlint/load@19.2.0(@types/node@20.14.8)(typescript@4.9.5)': + '@commitlint/load@19.2.0(@types/node@22.10.5)(typescript@4.9.5)': dependencies: '@commitlint/config-validator': 19.0.3 '@commitlint/execute-rule': 19.0.0 @@ -10234,7 +11179,7 @@ snapshots: '@commitlint/types': 19.0.3 chalk: 5.3.0 cosmiconfig: 9.0.0(typescript@4.9.5) - cosmiconfig-typescript-loader: 5.0.0(@types/node@20.14.8)(cosmiconfig@9.0.0(typescript@4.9.5))(typescript@4.9.5) + cosmiconfig-typescript-loader: 5.0.0(@types/node@22.10.5)(cosmiconfig@9.0.0(typescript@4.9.5))(typescript@4.9.5) lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 lodash.uniq: 4.5.0 @@ -10592,7 +11537,7 @@ snapshots: dependencies: '@formatjs/icu-messageformat-parser': 2.3.0 '@types/json-stable-stringify': 1.0.36 - '@types/node': 17.0.36 + '@types/node': 17.0.45 chalk: 4.1.2 json-stable-stringify: 1.1.1 tslib: 2.6.3 @@ -10743,14 +11688,14 @@ snapshots: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.14.8 + '@types/node': 22.10.5 jest-mock: 29.7.0 '@jest/fake-timers@29.7.0': dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 20.14.8 + '@types/node': 22.10.5 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -10763,7 +11708,7 @@ snapshots: dependencies: '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 20.14.8 + '@types/node': 22.10.5 '@types/yargs': 15.0.19 chalk: 4.1.2 @@ -10772,8 +11717,8 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 20.14.8 - '@types/yargs': 17.0.32 + '@types/node': 22.10.5 + '@types/yargs': 17.0.33 chalk: 4.1.2 '@jridgewell/gen-mapping@0.3.5': @@ -10782,17 +11727,25 @@ snapshots: '@jridgewell/sourcemap-codec': 1.4.15 '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/gen-mapping@0.3.8': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/set-array@1.2.1': {} '@jridgewell/source-map@0.3.6': dependencies: - '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/gen-mapping': 0.3.8 '@jridgewell/trace-mapping': 0.3.25 '@jridgewell/sourcemap-codec@1.4.15': {} + '@jridgewell/sourcemap-codec@1.5.0': {} + '@jridgewell/trace-mapping@0.3.25': dependencies: '@jridgewell/resolve-uri': 3.1.2 @@ -10836,7 +11789,7 @@ snapshots: nopt: 5.0.0 npmlog: 5.0.1 rimraf: 3.0.2 - semver: 7.6.2 + semver: 7.3.8 tar: 6.2.1 transitivePeerDependencies: - encoding @@ -10911,13 +11864,13 @@ snapshots: '@npmcli/fs@1.1.1': dependencies: '@gar/promisify': 1.1.3 - semver: 7.6.2 + semver: 7.3.8 optional: true '@npmcli/fs@2.1.2': dependencies: '@gar/promisify': 1.1.3 - semver: 7.6.2 + semver: 7.3.8 '@npmcli/move-file@1.1.2': dependencies: @@ -11505,7 +12458,7 @@ snapshots: '@react-native-community/cli-tools': 13.6.8(encoding@0.1.13) chalk: 4.1.2 execa: 5.1.1 - fast-glob: 3.3.2 + fast-glob: 3.3.3 transitivePeerDependencies: - encoding @@ -11515,14 +12468,14 @@ snapshots: chalk: 4.1.2 cosmiconfig: 5.2.1 deepmerge: 4.3.1 - fast-glob: 3.3.2 + fast-glob: 3.3.3 joi: 17.13.3 transitivePeerDependencies: - encoding '@react-native-community/cli-debugger-ui@13.6.8': dependencies: - serve-static: 1.15.0 + serve-static: 1.16.2 transitivePeerDependencies: - supports-color @@ -11536,15 +12489,15 @@ snapshots: chalk: 4.1.2 command-exists: 1.2.9 deepmerge: 4.3.1 - envinfo: 7.13.0 + envinfo: 7.14.0 execa: 5.1.1 hermes-profile-transformer: 0.0.6 node-stream-zip: 1.15.0 ora: 5.4.1 - semver: 7.6.2 + semver: 7.6.3 strip-ansi: 5.2.0 wcwidth: 1.0.1 - yaml: 2.4.5 + yaml: 2.7.0 transitivePeerDependencies: - encoding @@ -11562,8 +12515,8 @@ snapshots: '@react-native-community/cli-tools': 13.6.8(encoding@0.1.13) chalk: 4.1.2 execa: 5.1.1 - fast-glob: 3.3.2 - fast-xml-parser: 4.4.0 + fast-glob: 3.3.3 + fast-xml-parser: 4.5.1 logkitty: 0.7.1 transitivePeerDependencies: - encoding @@ -11573,8 +12526,8 @@ snapshots: '@react-native-community/cli-tools': 13.6.8(encoding@0.1.13) chalk: 4.1.2 execa: 5.1.1 - fast-glob: 3.3.2 - fast-xml-parser: 4.4.0 + fast-glob: 3.3.3 + fast-xml-parser: 4.5.1 ora: 5.4.1 transitivePeerDependencies: - encoding @@ -11589,12 +12542,12 @@ snapshots: dependencies: '@react-native-community/cli-debugger-ui': 13.6.8 '@react-native-community/cli-tools': 13.6.8(encoding@0.1.13) - compression: 1.7.4 + compression: 1.7.5 connect: 3.7.0 errorhandler: 1.5.1 nocache: 3.0.4 pretty-format: 26.6.2 - serve-static: 1.15.0 + serve-static: 1.16.2 ws: 6.2.3 transitivePeerDependencies: - bufferutil @@ -11612,8 +12565,8 @@ snapshots: node-fetch: 2.7.0(encoding@0.1.13) open: 6.4.0 ora: 5.4.1 - semver: 7.6.2 - shell-quote: 1.8.1 + semver: 7.6.3 + shell-quote: 1.8.2 sudo-prompt: 9.2.1 transitivePeerDependencies: - encoding @@ -11640,7 +12593,7 @@ snapshots: fs-extra: 8.1.0 graceful-fs: 4.2.11 prompts: 2.4.2 - semver: 7.6.2 + semver: 7.6.3 transitivePeerDependencies: - bufferutil - encoding @@ -11661,7 +12614,7 @@ snapshots: '@babel/core': 7.24.7 '@babel/plugin-proposal-async-generator-functions': 7.20.7(@babel/core@7.24.7) '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.24.7) - '@babel/plugin-proposal-export-default-from': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-proposal-export-default-from': 7.25.9(@babel/core@7.24.7) '@babel/plugin-proposal-logical-assignment-operators': 7.20.7(@babel/core@7.24.7) '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6(@babel/core@7.24.7) '@babel/plugin-proposal-numeric-separator': 7.18.6(@babel/core@7.24.7) @@ -11669,35 +12622,35 @@ snapshots: '@babel/plugin-proposal-optional-catch-binding': 7.18.6(@babel/core@7.24.7) '@babel/plugin-proposal-optional-chaining': 7.21.0(@babel/core@7.24.7) '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.24.7) - '@babel/plugin-syntax-export-default-from': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-syntax-flow': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-syntax-export-default-from': 7.25.9(@babel/core@7.24.7) + '@babel/plugin-syntax-flow': 7.26.0(@babel/core@7.24.7) '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.7) '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.7) - '@babel/plugin-transform-arrow-functions': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-async-to-generator': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-block-scoping': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-classes': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-computed-properties': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-destructuring': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-flow-strip-types': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-function-name': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-literals': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-modules-commonjs': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-named-capturing-groups-regex': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-parameters': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-private-methods': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-private-property-in-object': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-react-display-name': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-react-jsx': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-react-jsx-self': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-react-jsx-source': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-runtime': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-shorthand-properties': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-spread': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-sticky-regex': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-typescript': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-unicode-regex': 7.24.7(@babel/core@7.24.7) - '@babel/template': 7.24.7 + '@babel/plugin-transform-arrow-functions': 7.25.9(@babel/core@7.24.7) + '@babel/plugin-transform-async-to-generator': 7.25.9(@babel/core@7.24.7) + '@babel/plugin-transform-block-scoping': 7.25.9(@babel/core@7.24.7) + '@babel/plugin-transform-classes': 7.25.9(@babel/core@7.24.7) + '@babel/plugin-transform-computed-properties': 7.25.9(@babel/core@7.24.7) + '@babel/plugin-transform-destructuring': 7.25.9(@babel/core@7.24.7) + '@babel/plugin-transform-flow-strip-types': 7.25.9(@babel/core@7.24.7) + '@babel/plugin-transform-function-name': 7.25.9(@babel/core@7.24.7) + '@babel/plugin-transform-literals': 7.25.9(@babel/core@7.24.7) + '@babel/plugin-transform-modules-commonjs': 7.26.3(@babel/core@7.24.7) + '@babel/plugin-transform-named-capturing-groups-regex': 7.25.9(@babel/core@7.24.7) + '@babel/plugin-transform-parameters': 7.25.9(@babel/core@7.24.7) + '@babel/plugin-transform-private-methods': 7.25.9(@babel/core@7.24.7) + '@babel/plugin-transform-private-property-in-object': 7.25.9(@babel/core@7.24.7) + '@babel/plugin-transform-react-display-name': 7.25.9(@babel/core@7.24.7) + '@babel/plugin-transform-react-jsx': 7.25.9(@babel/core@7.24.7) + '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.24.7) + '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.24.7) + '@babel/plugin-transform-runtime': 7.25.9(@babel/core@7.24.7) + '@babel/plugin-transform-shorthand-properties': 7.25.9(@babel/core@7.24.7) + '@babel/plugin-transform-spread': 7.25.9(@babel/core@7.24.7) + '@babel/plugin-transform-sticky-regex': 7.25.9(@babel/core@7.24.7) + '@babel/plugin-transform-typescript': 7.26.3(@babel/core@7.24.7) + '@babel/plugin-transform-unicode-regex': 7.25.9(@babel/core@7.24.7) + '@babel/template': 7.25.9 '@react-native/babel-plugin-codegen': 0.74.84(@babel/preset-env@7.24.7(@babel/core@7.24.7)) babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.24.7) react-refresh: 0.14.2 @@ -11707,7 +12660,7 @@ snapshots: '@react-native/codegen@0.74.84(@babel/preset-env@7.24.7(@babel/core@7.24.7))': dependencies: - '@babel/parser': 7.24.7 + '@babel/parser': 7.26.3 '@babel/preset-env': 7.24.7(@babel/core@7.24.7) glob: 7.2.3 hermes-parser: 0.19.1 @@ -11726,9 +12679,9 @@ snapshots: '@react-native/metro-babel-transformer': 0.74.84(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7)) chalk: 4.1.2 execa: 5.1.1 - metro: 0.80.9(encoding@0.1.13) - metro-config: 0.80.9(encoding@0.1.13) - metro-core: 0.80.9 + metro: 0.80.12 + metro-config: 0.80.12 + metro-core: 0.80.12 node-fetch: 2.7.0(encoding@0.1.13) querystring: 0.2.1 readline: 1.3.0 @@ -11754,7 +12707,7 @@ snapshots: nullthrows: 1.1.1 open: 7.4.2 selfsigned: 2.4.1 - serve-static: 1.15.0 + serve-static: 1.16.2 temp-dir: 2.0.0 ws: 6.2.3 transitivePeerDependencies: @@ -11801,7 +12754,7 @@ snapshots: '@react-spring/types': 9.7.3 react: 18.3.1 - '@react-spring/konva@9.7.3(konva@9.3.12)(react-konva@18.2.10(konva@9.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': + '@react-spring/konva@9.7.3(konva@9.3.12)(react-konva@18.2.10(@types/react@18.3.3)(konva@9.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': dependencies: '@react-spring/animated': 9.7.3(react@18.3.1) '@react-spring/core': 9.7.3(react@18.3.1) @@ -11809,7 +12762,7 @@ snapshots: '@react-spring/types': 9.7.3 konva: 9.3.12 react: 18.3.1 - react-konva: 18.2.10(konva@9.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-konva: 18.2.10(@types/react@18.3.3)(konva@9.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@react-spring/native@9.7.3(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(encoding@0.1.13)(react@18.3.1))(react@18.3.1)': dependencies: @@ -11825,13 +12778,13 @@ snapshots: '@react-spring/types': 9.7.3 react: 18.3.1 - '@react-spring/three@9.7.3(@react-three/fiber@8.16.8(react-dom@18.3.1(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(encoding@0.1.13)(react@18.3.1))(react@18.3.1)(three@0.165.0))(react@18.3.1)(three@0.165.0)': + '@react-spring/three@9.7.3(@react-three/fiber@8.16.8(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(encoding@0.1.13)(react@18.3.1))(react@18.3.1)(three@0.165.0))(react@18.3.1)(three@0.165.0)': dependencies: '@react-spring/animated': 9.7.3(react@18.3.1) '@react-spring/core': 9.7.3(react@18.3.1) '@react-spring/shared': 9.7.3(react@18.3.1) '@react-spring/types': 9.7.3 - '@react-three/fiber': 8.16.8(react-dom@18.3.1(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(encoding@0.1.13)(react@18.3.1))(react@18.3.1)(three@0.165.0) + '@react-three/fiber': 8.16.8(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(encoding@0.1.13)(react@18.3.1))(react@18.3.1)(three@0.165.0) react: 18.3.1 three: 0.165.0 @@ -12063,14 +13016,14 @@ snapshots: '@swc/helpers': 0.5.11 react: 18.3.1 - '@react-three/fiber@8.16.8(react-dom@18.3.1(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(encoding@0.1.13)(react@18.3.1))(react@18.3.1)(three@0.165.0)': + '@react-three/fiber@8.16.8(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(encoding@0.1.13)(react@18.3.1))(react@18.3.1)(three@0.165.0)': dependencies: - '@babel/runtime': 7.24.7 + '@babel/runtime': 7.26.0 '@types/react-reconciler': 0.26.7 - '@types/webxr': 0.5.17 + '@types/webxr': 0.5.20 base64-js: 1.5.1 buffer: 6.0.3 - its-fine: 1.2.5(react@18.3.1) + its-fine: 1.2.5(@types/react@18.3.3)(react@18.3.1) react: 18.3.1 react-reconciler: 0.27.0(react@18.3.1) react-use-measure: 2.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -12081,6 +13034,8 @@ snapshots: optionalDependencies: react-dom: 18.3.1(react@18.3.1) react-native: 0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(encoding@0.1.13)(react@18.3.1) + transitivePeerDependencies: + - '@types/react' '@react-types/breadcrumbs@3.7.5(react@18.3.1)': dependencies: @@ -12218,7 +13173,7 @@ snapshots: '@rnx-kit/chromium-edge-launcher@1.0.0': dependencies: - '@types/node': 18.19.39 + '@types/node': 18.19.70 escape-string-regexp: 4.0.0 is-wsl: 2.2.0 lighthouse-logger: 1.4.2 @@ -12301,7 +13256,7 @@ snapshots: fs-extra: 11.2.0 globby: 11.1.0 http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.4 + https-proxy-agent: 7.0.5 issue-parser: 6.0.0 lodash: 4.17.21 mime: 3.0.0 @@ -12326,7 +13281,7 @@ snapshots: read-pkg: 5.2.0 registry-auth-token: 5.0.2 semantic-release: 19.0.5(encoding@0.1.13) - semver: 7.6.2 + semver: 7.3.8 tempy: 1.0.1 '@semantic-release/release-notes-generator@10.0.3(semantic-release@19.0.5(encoding@0.1.13))': @@ -12489,7 +13444,7 @@ snapshots: '@swc/core@1.6.5(@swc/helpers@0.5.11)': dependencies: '@swc/counter': 0.1.3 - '@swc/types': 0.1.9 + '@swc/types': 0.1.17 optionalDependencies: '@swc/core-darwin-arm64': 1.6.5 '@swc/core-darwin-x64': 1.6.5 @@ -12514,26 +13469,26 @@ snapshots: '@swc/counter': 0.1.3 tslib: 2.6.3 - '@swc/types@0.1.9': + '@swc/types@0.1.17': dependencies: '@swc/counter': 0.1.3 - '@tailwindcss/aspect-ratio@0.4.2(tailwindcss@3.2.7(postcss@8.4.21)(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5)))': + '@tailwindcss/aspect-ratio@0.4.2(tailwindcss@3.2.7(postcss@8.4.21)(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.10.5)(typescript@4.9.5)))': dependencies: - tailwindcss: 3.2.7(postcss@8.4.21)(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5)) + tailwindcss: 3.2.7(postcss@8.4.21)(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.10.5)(typescript@4.9.5)) - '@tailwindcss/forms@0.5.3(tailwindcss@3.2.7(postcss@8.4.21)(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5)))': + '@tailwindcss/forms@0.5.3(tailwindcss@3.2.7(postcss@8.4.21)(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.10.5)(typescript@4.9.5)))': dependencies: mini-svg-data-uri: 1.4.4 - tailwindcss: 3.2.7(postcss@8.4.21)(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5)) + tailwindcss: 3.2.7(postcss@8.4.21)(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.10.5)(typescript@4.9.5)) - '@tailwindcss/typography@0.5.9(tailwindcss@3.2.7(postcss@8.4.21)(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5)))': + '@tailwindcss/typography@0.5.9(tailwindcss@3.2.7(postcss@8.4.21)(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.10.5)(typescript@4.9.5)))': dependencies: lodash.castarray: 4.4.0 lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 postcss-selector-parser: 6.0.10 - tailwindcss: 3.2.7(postcss@8.4.21)(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5)) + tailwindcss: 3.2.7(postcss@8.4.21)(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.10.5)(typescript@4.9.5)) '@tanem/react-nprogress@5.0.30(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: @@ -12559,20 +13514,20 @@ snapshots: '@types/bcrypt@5.0.0': dependencies: - '@types/node': 17.0.36 + '@types/node': 22.10.5 '@types/body-parser@1.19.5': dependencies: '@types/connect': 3.4.38 - '@types/node': 17.0.36 + '@types/node': 22.10.5 '@types/connect@3.4.38': dependencies: - '@types/node': 17.0.36 + '@types/node': 22.10.5 '@types/conventional-commits-parser@5.0.0': dependencies: - '@types/node': 17.0.36 + '@types/node': 22.10.5 optional: true '@types/cookie-parser@1.4.3': @@ -12608,7 +13563,7 @@ snapshots: '@types/express-serve-static-core@4.19.5': dependencies: - '@types/node': 17.0.36 + '@types/node': 22.10.5 '@types/qs': 6.9.15 '@types/range-parser': 1.2.7 '@types/send': 0.17.4 @@ -12670,6 +13625,8 @@ snapshots: '@types/mime@1.3.5': {} + '@types/mime@3.0.4': {} + '@types/minimatch@3.0.5': {} '@types/minimist@1.2.5': {} @@ -12682,33 +13639,33 @@ snapshots: '@types/node-forge@1.3.11': dependencies: - '@types/node': 20.14.8 + '@types/node': 22.10.5 '@types/node-schedule@2.1.0': dependencies: - '@types/node': 17.0.36 + '@types/node': 22.10.5 '@types/node@14.18.63': {} - '@types/node@17.0.36': {} + '@types/node@17.0.45': {} - '@types/node@18.19.39': - dependencies: - undici-types: 5.26.5 - - '@types/node@20.14.8': + '@types/node@18.19.70': dependencies: undici-types: 5.26.5 '@types/node@20.5.1': {} + '@types/node@22.10.5': + dependencies: + undici-types: 6.20.0 + '@types/nodemailer@6.4.15': dependencies: - '@types/node': 17.0.36 + '@types/node': 22.10.5 '@types/nodemailer@6.4.7': dependencies: - '@types/node': 17.0.36 + '@types/node': 22.10.5 '@types/normalize-package-data@2.4.4': {} @@ -12718,6 +13675,8 @@ snapshots: '@types/prop-types@15.7.12': {} + '@types/prop-types@15.7.14': {} + '@types/qs@6.9.15': {} '@types/range-parser@1.2.7': {} @@ -12728,9 +13687,9 @@ snapshots: '@types/react-reconciler@0.26.7': dependencies: - '@types/react': 18.3.3 + '@types/react': 18.3.18 - '@types/react-reconciler@0.28.8': + '@types/react-reconciler@0.28.9(@types/react@18.3.3)': dependencies: '@types/react': 18.3.3 @@ -12742,6 +13701,11 @@ snapshots: dependencies: '@types/react': 18.3.3 + '@types/react@18.3.18': + dependencies: + '@types/prop-types': 15.7.14 + csstype: 3.1.3 + '@types/react@18.3.3': dependencies: '@types/prop-types': 15.7.12 @@ -12756,12 +13720,12 @@ snapshots: '@types/send@0.17.4': dependencies: '@types/mime': 1.3.5 - '@types/node': 17.0.36 + '@types/node': 22.10.5 '@types/serve-static@1.15.7': dependencies: '@types/http-errors': 2.0.4 - '@types/node': 17.0.36 + '@types/node': 22.10.5 '@types/send': 0.17.4 '@types/sinonjs__fake-timers@8.1.1': {} @@ -12781,13 +13745,15 @@ snapshots: '@types/web-push@3.3.2': dependencies: - '@types/node': 17.0.36 + '@types/node': 22.10.5 - '@types/webxr@0.5.17': {} + '@types/webxr@0.5.20': {} + + '@types/wink-jaro-distance@2.0.2': {} '@types/xml2js@0.4.11': dependencies: - '@types/node': 17.0.36 + '@types/node': 22.10.5 '@types/yamljs@0.2.31': {} @@ -12797,13 +13763,13 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@types/yargs@17.0.32': + '@types/yargs@17.0.33': dependencies: '@types/yargs-parser': 21.0.3 '@types/yauzl@2.10.3': dependencies: - '@types/node': 17.0.36 + '@types/node': 22.10.5 optional: true '@types/yup@0.29.14': {} @@ -12887,7 +13853,7 @@ snapshots: debug: 4.3.5(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 - semver: 7.6.2 + semver: 7.3.8 tsutils: 3.21.0(typescript@4.9.5) optionalDependencies: typescript: 4.9.5 @@ -12993,6 +13959,8 @@ snapshots: acorn@8.12.0: {} + acorn@8.14.0: {} + agent-base@6.0.2: dependencies: debug: 4.3.5(supports-color@8.1.1) @@ -13223,7 +14191,7 @@ snapshots: ast-types@0.15.2: dependencies: - tslib: 2.6.3 + tslib: 2.8.1 astral-regex@1.0.0: {} @@ -13265,9 +14233,9 @@ snapshots: dependencies: deep-equal-json: 1.0.0 - babel-core@7.0.0-bridge.0(@babel/core@7.24.7): + babel-core@7.0.0-bridge.0(@babel/core@7.26.0): dependencies: - '@babel/core': 7.24.7 + '@babel/core': 7.26.0 babel-plugin-emotion@10.2.2: dependencies: @@ -13305,6 +14273,15 @@ snapshots: transitivePeerDependencies: - supports-color + babel-plugin-polyfill-corejs2@0.4.12(@babel/core@7.24.7): + dependencies: + '@babel/compat-data': 7.26.3 + '@babel/core': 7.24.7 + '@babel/helper-define-polyfill-provider': 0.6.3(@babel/core@7.24.7) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + babel-plugin-polyfill-corejs3@0.10.4(@babel/core@7.24.7): dependencies: '@babel/core': 7.24.7 @@ -13313,6 +14290,14 @@ snapshots: transitivePeerDependencies: - supports-color + babel-plugin-polyfill-corejs3@0.10.6(@babel/core@7.24.7): + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-define-polyfill-provider': 0.6.3(@babel/core@7.24.7) + core-js-compat: 3.39.0 + transitivePeerDependencies: + - supports-color + babel-plugin-polyfill-regenerator@0.6.2(@babel/core@7.24.7): dependencies: '@babel/core': 7.24.7 @@ -13320,11 +14305,18 @@ snapshots: transitivePeerDependencies: - supports-color + babel-plugin-polyfill-regenerator@0.6.3(@babel/core@7.24.7): + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-define-polyfill-provider': 0.6.3(@babel/core@7.24.7) + transitivePeerDependencies: + - supports-color + babel-plugin-syntax-jsx@6.18.0: {} babel-plugin-transform-flow-enums@0.0.2(@babel/core@7.24.7): dependencies: - '@babel/plugin-syntax-flow': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-syntax-flow': 7.26.0(@babel/core@7.24.7) transitivePeerDependencies: - '@babel/core' @@ -13413,6 +14405,13 @@ snapshots: node-releases: 2.0.14 update-browserslist-db: 1.0.16(browserslist@4.23.1) + browserslist@4.24.3: + dependencies: + caniuse-lite: 1.0.30001690 + electron-to-chromium: 1.5.77 + node-releases: 2.0.19 + update-browserslist-db: 1.1.1(browserslist@4.24.3) + bser@2.1.1: dependencies: node-int64: 0.4.0 @@ -13423,6 +14422,8 @@ snapshots: buffer-from@1.1.2: {} + buffer-writer@2.0.0: {} + buffer@5.7.1: dependencies: base64-js: 1.5.1 @@ -13437,8 +14438,6 @@ snapshots: dependencies: streamsearch: 1.1.0 - bytes@3.0.0: {} - bytes@3.1.2: {} cacache@15.3.0: @@ -13528,6 +14527,8 @@ snapshots: caniuse-lite@1.0.30001636: {} + caniuse-lite@1.0.30001690: {} + cardinal@2.1.1: dependencies: ansicolors: 0.3.2 @@ -13616,7 +14617,7 @@ snapshots: chrome-launcher@0.15.2: dependencies: - '@types/node': 20.14.8 + '@types/node': 22.10.5 escape-string-regexp: 4.0.0 is-wsl: 2.2.0 lighthouse-logger: 1.4.2 @@ -13752,10 +14753,10 @@ snapshots: commander@9.5.0: {} - commitizen@4.3.0(@types/node@20.14.8)(typescript@4.9.5): + commitizen@4.3.0(@types/node@22.10.5)(typescript@4.9.5): dependencies: cachedir: 2.3.0 - cz-conventional-changelog: 3.3.0(@types/node@20.14.8)(typescript@4.9.5) + cz-conventional-changelog: 3.3.0(@types/node@22.10.5)(typescript@4.9.5) dedent: 0.7.0 detect-indent: 6.1.0 find-node-modules: 2.1.3 @@ -13783,16 +14784,16 @@ snapshots: compressible@2.0.18: dependencies: - mime-db: 1.52.0 + mime-db: 1.53.0 - compression@1.7.4: + compression@1.7.5: dependencies: - accepts: 1.3.8 - bytes: 3.0.0 + bytes: 3.1.2 compressible: 2.0.18 debug: 2.6.9 + negotiator: 0.6.4 on-headers: 1.0.2 - safe-buffer: 5.1.2 + safe-buffer: 5.2.1 vary: 1.1.2 transitivePeerDependencies: - supports-color @@ -13813,13 +14814,13 @@ snapshots: ini: 1.3.8 proto-list: 1.2.4 - connect-typeorm@1.1.4(typeorm@0.3.12(sqlite3@5.1.4(encoding@0.1.13))(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5))): + connect-typeorm@1.1.4(typeorm@0.3.11(pg@8.11.0)(sqlite3@5.1.4(encoding@0.1.13))(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.10.5)(typescript@4.9.5))): dependencies: '@types/debug': 0.0.31 '@types/express-session': 1.17.6 debug: 4.3.5(supports-color@8.1.1) express-session: 1.18.0 - typeorm: 0.3.12(sqlite3@5.1.4(encoding@0.1.13))(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5)) + typeorm: 0.3.11(pg@8.11.0)(sqlite3@5.1.4(encoding@0.1.13))(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.10.5)(typescript@4.9.5)) transitivePeerDependencies: - supports-color @@ -13948,6 +14949,10 @@ snapshots: dependencies: browserslist: 4.23.1 + core-js-compat@3.39.0: + dependencies: + browserslist: 4.24.3 + core-util-is@1.0.2: {} core-util-is@1.0.3: {} @@ -13959,9 +14964,9 @@ snapshots: ts-node: 10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.5.1)(typescript@5.5.2) typescript: 5.5.2 - cosmiconfig-typescript-loader@5.0.0(@types/node@20.14.8)(cosmiconfig@9.0.0(typescript@4.9.5))(typescript@4.9.5): + cosmiconfig-typescript-loader@5.0.0(@types/node@22.10.5)(cosmiconfig@9.0.0(typescript@4.9.5))(typescript@4.9.5): dependencies: - '@types/node': 20.14.8 + '@types/node': 22.10.5 cosmiconfig: 9.0.0(typescript@4.9.5) jiti: 1.21.6 typescript: 4.9.5 @@ -14128,16 +15133,16 @@ snapshots: untildify: 4.0.0 yauzl: 2.10.0 - cz-conventional-changelog@3.3.0(@types/node@20.14.8)(typescript@4.9.5): + cz-conventional-changelog@3.3.0(@types/node@22.10.5)(typescript@4.9.5): dependencies: chalk: 2.4.2 - commitizen: 4.3.0(@types/node@20.14.8)(typescript@4.9.5) + commitizen: 4.3.0(@types/node@22.10.5)(typescript@4.9.5) conventional-commit-types: 3.0.0 lodash.map: 4.6.0 longest: 2.0.1 word-wrap: 1.2.5 optionalDependencies: - '@commitlint/load': 19.2.0(@types/node@20.14.8)(typescript@4.9.5) + '@commitlint/load': 19.2.0(@types/node@22.10.5)(typescript@4.9.5) transitivePeerDependencies: - '@types/node' - typescript @@ -14170,10 +15175,6 @@ snapshots: date-fns@2.29.3: {} - date-fns@2.30.0: - dependencies: - '@babel/runtime': 7.24.7 - dateformat@3.0.3: {} dayjs@1.11.11: {} @@ -14204,6 +15205,10 @@ snapshots: optionalDependencies: supports-color: 8.1.1 + debug@4.4.0: + dependencies: + ms: 2.1.3 + decamelize-keys@1.1.1: dependencies: decamelize: 1.2.0 @@ -14411,6 +15416,8 @@ snapshots: electron-to-chromium@1.4.810: {} + electron-to-chromium@1.5.77: {} + email-templates@9.0.0(encoding@0.1.13)(handlebars@4.7.8)(mustache@4.2.0)(pug@3.0.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(underscore@1.13.6): dependencies: '@ladjs/i18n': 7.2.6 @@ -14492,6 +15499,8 @@ snapshots: encodeurl@1.0.2: {} + encodeurl@2.0.0: {} + encoding-japanese@2.0.0: {} encoding-japanese@2.1.0: {} @@ -14527,7 +15536,7 @@ snapshots: env-paths@2.2.1: {} - envinfo@7.13.0: {} + envinfo@7.14.0: {} err-code@2.0.3: {} @@ -14650,6 +15659,8 @@ snapshots: escalade@3.1.2: {} + escalade@3.2.0: {} + escape-goat@3.0.0: {} escape-html@1.0.3: {} @@ -14697,7 +15708,7 @@ snapshots: debug: 4.3.5(supports-color@8.1.1) enhanced-resolve: 5.17.0 eslint: 8.35.0 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.35.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.35.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.35.0))(eslint@8.35.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.35.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.35.0) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.54.0(eslint@8.35.0)(typescript@4.9.5))(eslint@8.35.0) fast-glob: 3.3.2 get-tsconfig: 4.7.5 @@ -14719,7 +15730,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.35.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.35.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.35.0))(eslint@8.35.0): + eslint-module-utils@2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.35.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.35.0): dependencies: debug: 3.2.7(supports-color@5.5.0) optionalDependencies: @@ -15024,6 +16035,8 @@ snapshots: dependencies: homedir-polyfill: 1.0.3 + exponential-backoff@3.1.1: {} + express-openapi-validator@4.13.8: dependencies: '@types/multer': 1.4.11 @@ -15141,6 +16154,14 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.7 + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + fast-json-stable-stringify@2.1.0: {} fast-levenshtein@2.0.6: {} @@ -15149,7 +16170,7 @@ snapshots: dependencies: boolean: 3.2.0 - fast-xml-parser@4.4.0: + fast-xml-parser@4.5.1: dependencies: strnum: 1.0.5 @@ -15272,7 +16293,7 @@ snapshots: flow-enums-runtime@0.0.6: {} - flow-parser@0.238.0: {} + flow-parser@0.258.0: {} fn.name@1.1.0: {} @@ -15586,15 +16607,15 @@ snapshots: hermes-estree@0.19.1: {} - hermes-estree@0.20.1: {} + hermes-estree@0.23.1: {} hermes-parser@0.19.1: dependencies: hermes-estree: 0.19.1 - hermes-parser@0.20.1: + hermes-parser@0.23.1: dependencies: - hermes-estree: 0.20.1 + hermes-estree: 0.23.1 hermes-profile-transformer@0.0.6: dependencies: @@ -15728,7 +16749,7 @@ snapshots: transitivePeerDependencies: - supports-color - https-proxy-agent@7.0.4: + https-proxy-agent@7.0.5: dependencies: agent-base: 7.1.1 debug: 4.3.5(supports-color@8.1.1) @@ -15776,7 +16797,7 @@ snapshots: ignore@5.3.1: {} - image-size@1.1.1: + image-size@1.2.0: dependencies: queue: 6.0.2 @@ -15907,6 +16928,10 @@ snapshots: dependencies: hasown: 2.0.2 + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + is-data-view@1.0.1: dependencies: is-typed-array: 1.1.13 @@ -16067,10 +17092,12 @@ snapshots: reflect.getprototypeof: 1.0.6 set-function-name: 2.0.2 - its-fine@1.2.5(react@18.3.1): + its-fine@1.2.5(@types/react@18.3.3)(react@18.3.1): dependencies: - '@types/react-reconciler': 0.28.8 + '@types/react-reconciler': 0.28.9(@types/react@18.3.3) react: 18.3.1 + transitivePeerDependencies: + - '@types/react' jackspeak@2.3.6: dependencies: @@ -16085,7 +17112,7 @@ snapshots: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.14.8 + '@types/node': 22.10.5 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -16093,12 +17120,12 @@ snapshots: jest-message-util@29.7.0: dependencies: - '@babel/code-frame': 7.24.7 + '@babel/code-frame': 7.26.2 '@jest/types': 29.6.3 '@types/stack-utils': 2.0.3 chalk: 4.1.2 graceful-fs: 4.2.11 - micromatch: 4.0.7 + micromatch: 4.0.8 pretty-format: 29.7.0 slash: 3.0.0 stack-utils: 2.0.6 @@ -16106,13 +17133,13 @@ snapshots: jest-mock@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 20.14.8 + '@types/node': 22.10.5 jest-util: 29.7.0 jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 20.14.8 + '@types/node': 22.10.5 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -16129,7 +17156,7 @@ snapshots: jest-worker@29.7.0: dependencies: - '@types/node': 20.14.8 + '@types/node': 22.10.5 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -16170,21 +17197,21 @@ snapshots: jscodeshift@0.14.0(@babel/preset-env@7.24.7(@babel/core@7.24.7)): dependencies: - '@babel/core': 7.24.7 - '@babel/parser': 7.24.7 - '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.24.7) - '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6(@babel/core@7.24.7) - '@babel/plugin-proposal-optional-chaining': 7.21.0(@babel/core@7.24.7) - '@babel/plugin-transform-modules-commonjs': 7.24.7(@babel/core@7.24.7) + '@babel/core': 7.26.0 + '@babel/parser': 7.26.3 + '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.26.0) + '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6(@babel/core@7.26.0) + '@babel/plugin-proposal-optional-chaining': 7.21.0(@babel/core@7.26.0) + '@babel/plugin-transform-modules-commonjs': 7.26.3(@babel/core@7.26.0) '@babel/preset-env': 7.24.7(@babel/core@7.24.7) - '@babel/preset-flow': 7.24.7(@babel/core@7.24.7) - '@babel/preset-typescript': 7.24.7(@babel/core@7.24.7) - '@babel/register': 7.24.6(@babel/core@7.24.7) - babel-core: 7.0.0-bridge.0(@babel/core@7.24.7) + '@babel/preset-flow': 7.25.9(@babel/core@7.26.0) + '@babel/preset-typescript': 7.26.0(@babel/core@7.26.0) + '@babel/register': 7.25.9(@babel/core@7.26.0) + babel-core: 7.0.0-bridge.0(@babel/core@7.26.0) chalk: 4.1.2 - flow-parser: 0.238.0 + flow-parser: 0.258.0 graceful-fs: 4.2.11 - micromatch: 4.0.7 + micromatch: 4.0.8 neo-async: 2.6.2 node-dir: 0.1.17 recast: 0.21.5 @@ -16197,6 +17224,10 @@ snapshots: jsesc@2.5.2: {} + jsesc@3.0.2: {} + + jsesc@3.1.0: {} + json-buffer@3.0.1: {} json-parse-better-errors@1.0.2: {} @@ -16733,50 +17764,56 @@ snapshots: methods@1.1.2: {} - metro-babel-transformer@0.80.9: + metro-babel-transformer@0.80.12: dependencies: - '@babel/core': 7.24.7 - hermes-parser: 0.20.1 + '@babel/core': 7.26.0 + flow-enums-runtime: 0.0.6 + hermes-parser: 0.23.1 nullthrows: 1.1.1 transitivePeerDependencies: - supports-color - metro-cache-key@0.80.9: {} - - metro-cache@0.80.9: + metro-cache-key@0.80.12: dependencies: - metro-core: 0.80.9 - rimraf: 3.0.2 + flow-enums-runtime: 0.0.6 - metro-config@0.80.9(encoding@0.1.13): + metro-cache@0.80.12: + dependencies: + exponential-backoff: 3.1.1 + flow-enums-runtime: 0.0.6 + metro-core: 0.80.12 + + metro-config@0.80.12: dependencies: connect: 3.7.0 cosmiconfig: 5.2.1 + flow-enums-runtime: 0.0.6 jest-validate: 29.7.0 - metro: 0.80.9(encoding@0.1.13) - metro-cache: 0.80.9 - metro-core: 0.80.9 - metro-runtime: 0.80.9 + metro: 0.80.12 + metro-cache: 0.80.12 + metro-core: 0.80.12 + metro-runtime: 0.80.12 transitivePeerDependencies: - bufferutil - - encoding - supports-color - utf-8-validate - metro-core@0.80.9: + metro-core@0.80.12: dependencies: + flow-enums-runtime: 0.0.6 lodash.throttle: 4.1.1 - metro-resolver: 0.80.9 + metro-resolver: 0.80.12 - metro-file-map@0.80.9: + metro-file-map@0.80.12: dependencies: anymatch: 3.1.3 debug: 2.6.9 fb-watchman: 2.0.2 + flow-enums-runtime: 0.0.6 graceful-fs: 4.2.11 invariant: 2.2.4 jest-worker: 29.7.0 - micromatch: 4.0.7 + micromatch: 4.0.8 node-abort-controller: 3.1.1 nullthrows: 1.1.1 walker: 1.0.8 @@ -16785,33 +17822,39 @@ snapshots: transitivePeerDependencies: - supports-color - metro-minify-terser@0.80.9: + metro-minify-terser@0.80.12: dependencies: - terser: 5.31.1 + flow-enums-runtime: 0.0.6 + terser: 5.37.0 - metro-resolver@0.80.9: {} - - metro-runtime@0.80.9: + metro-resolver@0.80.12: dependencies: - '@babel/runtime': 7.24.7 + flow-enums-runtime: 0.0.6 - metro-source-map@0.80.9: + metro-runtime@0.80.12: dependencies: - '@babel/traverse': 7.24.7 - '@babel/types': 7.24.7 + '@babel/runtime': 7.26.0 + flow-enums-runtime: 0.0.6 + + metro-source-map@0.80.12: + dependencies: + '@babel/traverse': 7.26.4 + '@babel/types': 7.26.3 + flow-enums-runtime: 0.0.6 invariant: 2.2.4 - metro-symbolicate: 0.80.9 + metro-symbolicate: 0.80.12 nullthrows: 1.1.1 - ob1: 0.80.9 + ob1: 0.80.12 source-map: 0.5.7 vlq: 1.0.1 transitivePeerDependencies: - supports-color - metro-symbolicate@0.80.9: + metro-symbolicate@0.80.12: dependencies: + flow-enums-runtime: 0.0.6 invariant: 2.2.4 - metro-source-map: 0.80.9 + metro-source-map: 0.80.12 nullthrows: 1.1.1 source-map: 0.5.7 through2: 2.0.5 @@ -16819,45 +17862,46 @@ snapshots: transitivePeerDependencies: - supports-color - metro-transform-plugins@0.80.9: + metro-transform-plugins@0.80.12: dependencies: - '@babel/core': 7.24.7 - '@babel/generator': 7.24.7 - '@babel/template': 7.24.7 - '@babel/traverse': 7.24.7 + '@babel/core': 7.26.0 + '@babel/generator': 7.26.3 + '@babel/template': 7.25.9 + '@babel/traverse': 7.26.4 + flow-enums-runtime: 0.0.6 nullthrows: 1.1.1 transitivePeerDependencies: - supports-color - metro-transform-worker@0.80.9(encoding@0.1.13): + metro-transform-worker@0.80.12: dependencies: - '@babel/core': 7.24.7 - '@babel/generator': 7.24.7 - '@babel/parser': 7.24.7 - '@babel/types': 7.24.7 - metro: 0.80.9(encoding@0.1.13) - metro-babel-transformer: 0.80.9 - metro-cache: 0.80.9 - metro-cache-key: 0.80.9 - metro-minify-terser: 0.80.9 - metro-source-map: 0.80.9 - metro-transform-plugins: 0.80.9 + '@babel/core': 7.26.0 + '@babel/generator': 7.26.3 + '@babel/parser': 7.26.3 + '@babel/types': 7.26.3 + flow-enums-runtime: 0.0.6 + metro: 0.80.12 + metro-babel-transformer: 0.80.12 + metro-cache: 0.80.12 + metro-cache-key: 0.80.12 + metro-minify-terser: 0.80.12 + metro-source-map: 0.80.12 + metro-transform-plugins: 0.80.12 nullthrows: 1.1.1 transitivePeerDependencies: - bufferutil - - encoding - supports-color - utf-8-validate - metro@0.80.9(encoding@0.1.13): + metro@0.80.12: dependencies: - '@babel/code-frame': 7.24.7 - '@babel/core': 7.24.7 - '@babel/generator': 7.24.7 - '@babel/parser': 7.24.7 - '@babel/template': 7.24.7 - '@babel/traverse': 7.24.7 - '@babel/types': 7.24.7 + '@babel/code-frame': 7.26.2 + '@babel/core': 7.26.0 + '@babel/generator': 7.26.3 + '@babel/parser': 7.26.3 + '@babel/template': 7.25.9 + '@babel/traverse': 7.26.4 + '@babel/types': 7.26.3 accepts: 1.3.8 chalk: 4.1.2 ci-info: 2.0.0 @@ -16865,29 +17909,28 @@ snapshots: debug: 2.6.9 denodeify: 1.2.1 error-stack-parser: 2.1.4 + flow-enums-runtime: 0.0.6 graceful-fs: 4.2.11 - hermes-parser: 0.20.1 - image-size: 1.1.1 + hermes-parser: 0.23.1 + image-size: 1.2.0 invariant: 2.2.4 jest-worker: 29.7.0 jsc-safe-url: 0.2.4 lodash.throttle: 4.1.1 - metro-babel-transformer: 0.80.9 - metro-cache: 0.80.9 - metro-cache-key: 0.80.9 - metro-config: 0.80.9(encoding@0.1.13) - metro-core: 0.80.9 - metro-file-map: 0.80.9 - metro-resolver: 0.80.9 - metro-runtime: 0.80.9 - metro-source-map: 0.80.9 - metro-symbolicate: 0.80.9 - metro-transform-plugins: 0.80.9 - metro-transform-worker: 0.80.9(encoding@0.1.13) + metro-babel-transformer: 0.80.12 + metro-cache: 0.80.12 + metro-cache-key: 0.80.12 + metro-config: 0.80.12 + metro-core: 0.80.12 + metro-file-map: 0.80.12 + metro-resolver: 0.80.12 + metro-runtime: 0.80.12 + metro-source-map: 0.80.12 + metro-symbolicate: 0.80.12 + metro-transform-plugins: 0.80.12 + metro-transform-worker: 0.80.12 mime-types: 2.1.35 - node-fetch: 2.7.0(encoding@0.1.13) nullthrows: 1.1.1 - rimraf: 3.0.2 serialize-error: 2.1.0 source-map: 0.5.7 strip-ansi: 6.0.1 @@ -16896,7 +17939,6 @@ snapshots: yargs: 17.7.2 transitivePeerDependencies: - bufferutil - - encoding - supports-color - utf-8-validate @@ -17038,8 +18080,15 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + mime-db@1.52.0: {} + mime-db@1.53.0: {} + mime-types@2.1.35: dependencies: mime-db: 1.52.0 @@ -17138,8 +18187,6 @@ snapshots: mkdirp@1.0.4: {} - mkdirp@2.1.6: {} - modify-values@1.0.1: {} moment@2.30.1: {} @@ -17201,6 +18248,8 @@ snapshots: negotiator@0.6.3: {} + negotiator@0.6.4: {} + neo-async@2.6.2: {} nerf-dart@1.0.0: {} @@ -17269,7 +18318,7 @@ snapshots: nopt: 5.0.0 npmlog: 6.0.2 rimraf: 3.0.2 - semver: 7.6.2 + semver: 7.3.8 tar: 6.2.1 which: 2.0.2 transitivePeerDependencies: @@ -17297,6 +18346,8 @@ snapshots: node-releases@2.0.14: {} + node-releases@2.0.19: {} + node-schedule@2.1.1: dependencies: cron-parser: 4.9.0 @@ -17348,7 +18399,7 @@ snapshots: dependencies: hosted-git-info: 4.1.0 is-core-module: 2.14.0 - semver: 7.6.2 + semver: 7.3.8 validate-npm-package-license: 3.0.4 normalize-path@3.0.0: {} @@ -17393,7 +18444,9 @@ snapshots: oauth-sign@0.9.0: {} - ob1@0.80.9: {} + ob1@0.80.12: + dependencies: + flow-enums-runtime: 0.0.6 object-assign@4.1.1: {} @@ -17578,6 +18631,8 @@ snapshots: dependencies: p-timeout: 3.2.0 + packet-reader@1.0.0: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -17656,8 +18711,47 @@ snapshots: performance-now@2.1.0: {} + pg-cloudflare@1.1.1: + optional: true + + pg-connection-string@2.7.0: {} + + pg-int8@1.0.1: {} + + pg-pool@3.7.0(pg@8.11.0): + dependencies: + pg: 8.11.0 + + pg-protocol@1.7.0: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.0 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg@8.11.0: + dependencies: + buffer-writer: 2.0.0 + packet-reader: 1.0.0 + pg-connection-string: 2.7.0 + pg-pool: 3.7.0(pg@8.11.0) + pg-protocol: 1.7.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.1.1 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + picocolors@1.0.1: {} + picocolors@1.1.1: {} + picomatch@2.3.1: {} pidtree@0.6.0: {} @@ -17716,13 +18810,13 @@ snapshots: camelcase-css: 2.0.1 postcss: 8.4.21 - postcss-load-config@3.1.4(postcss@8.4.21)(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5)): + postcss-load-config@3.1.4(postcss@8.4.21)(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.10.5)(typescript@4.9.5)): dependencies: lilconfig: 2.1.0 yaml: 1.10.2 optionalDependencies: postcss: 8.4.21 - ts-node: 10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5) + ts-node: 10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.10.5)(typescript@4.9.5) postcss-nested@6.0.0(postcss@8.4.21): dependencies: @@ -17753,6 +18847,16 @@ snapshots: picocolors: 1.0.1 source-map-js: 1.2.0 + postgres-array@2.0.0: {} + + postgres-bytea@1.0.0: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + prelude-ls@1.2.1: {} prettier-linter-helpers@1.0.0: @@ -18046,9 +19150,9 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-devtools-core@5.2.0: + react-devtools-core@5.3.2: dependencies: - shell-quote: 1.8.1 + shell-quote: 1.8.2 ws: 7.5.10 transitivePeerDependencies: - bufferutil @@ -18090,15 +19194,17 @@ snapshots: react-is@18.3.1: {} - react-konva@18.2.10(konva@9.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-konva@18.2.10(@types/react@18.3.3)(konva@9.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@types/react-reconciler': 0.28.8 - its-fine: 1.2.5(react@18.3.1) + '@types/react-reconciler': 0.28.9(@types/react@18.3.3) + its-fine: 1.2.5(@types/react@18.3.3)(react@18.3.1) konva: 9.3.12 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) react-reconciler: 0.29.2(react@18.3.1) scheduler: 0.23.2 + transitivePeerDependencies: + - '@types/react' react-markdown@8.0.5(@types/react@18.3.3)(react@18.3.1): dependencies: @@ -18146,14 +19252,14 @@ snapshots: jest-environment-node: 29.7.0 jsc-android: 250231.0.0 memoize-one: 5.2.1 - metro-runtime: 0.80.9 - metro-source-map: 0.80.9 + metro-runtime: 0.80.12 + metro-source-map: 0.80.12 mkdirp: 0.5.6 nullthrows: 1.1.1 pretty-format: 26.6.2 promise: 8.3.0 react: 18.3.1 - react-devtools-core: 5.2.0 + react-devtools-core: 5.3.2 react-refresh: 0.14.2 react-shallow-renderer: 16.15.0(react@18.3.1) regenerator-runtime: 0.13.11 @@ -18225,12 +19331,12 @@ snapshots: react: 18.3.1 react-is: 18.3.1 - react-spring@9.7.1(@react-three/fiber@8.16.8(react-dom@18.3.1(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(encoding@0.1.13)(react@18.3.1))(react@18.3.1)(three@0.165.0))(konva@9.3.12)(react-dom@18.3.1(react@18.3.1))(react-konva@18.2.10(konva@9.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(encoding@0.1.13)(react@18.3.1))(react-zdog@1.2.2)(react@18.3.1)(three@0.165.0)(zdog@1.1.3): + react-spring@9.7.1(@react-three/fiber@8.16.8(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(encoding@0.1.13)(react@18.3.1))(react@18.3.1)(three@0.165.0))(konva@9.3.12)(react-dom@18.3.1(react@18.3.1))(react-konva@18.2.10(@types/react@18.3.3)(konva@9.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(encoding@0.1.13)(react@18.3.1))(react-zdog@1.2.2)(react@18.3.1)(three@0.165.0)(zdog@1.1.3): dependencies: '@react-spring/core': 9.7.3(react@18.3.1) - '@react-spring/konva': 9.7.3(konva@9.3.12)(react-konva@18.2.10(konva@9.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + '@react-spring/konva': 9.7.3(konva@9.3.12)(react-konva@18.2.10(@types/react@18.3.3)(konva@9.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) '@react-spring/native': 9.7.3(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) - '@react-spring/three': 9.7.3(@react-three/fiber@8.16.8(react-dom@18.3.1(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(encoding@0.1.13)(react@18.3.1))(react@18.3.1)(three@0.165.0))(react@18.3.1)(three@0.165.0) + '@react-spring/three': 9.7.3(@react-three/fiber@8.16.8(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(encoding@0.1.13)(react@18.3.1))(react@18.3.1)(three@0.165.0))(react@18.3.1)(three@0.165.0) '@react-spring/web': 9.7.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@react-spring/zdog': 9.7.3(react-dom@18.3.1(react@18.3.1))(react-zdog@1.2.2)(react@18.3.1)(zdog@1.1.3) react: 18.3.1 @@ -18348,7 +19454,7 @@ snapshots: ast-types: 0.15.2 esprima: 4.0.1 source-map: 0.6.1 - tslib: 2.6.3 + tslib: 2.8.1 redent@3.0.0: dependencies: @@ -18361,8 +19467,6 @@ snapshots: reflect-metadata@0.1.13: {} - reflect-metadata@0.1.14: {} - reflect.getprototypeof@1.0.6: dependencies: call-bind: 1.0.7 @@ -18377,6 +19481,10 @@ snapshots: dependencies: regenerate: 1.4.2 + regenerate-unicode-properties@10.2.0: + dependencies: + regenerate: 1.4.2 + regenerate@1.4.2: {} regenerator-runtime@0.13.11: {} @@ -18405,10 +19513,25 @@ snapshots: unicode-match-property-ecmascript: 2.0.0 unicode-match-property-value-ecmascript: 2.1.0 + regexpu-core@6.2.0: + dependencies: + regenerate: 1.4.2 + regenerate-unicode-properties: 10.2.0 + regjsgen: 0.8.0 + regjsparser: 0.12.0 + unicode-match-property-ecmascript: 2.0.0 + unicode-match-property-value-ecmascript: 2.2.0 + registry-auth-token@5.0.2: dependencies: '@pnpm/npm-conf': 2.2.2 + regjsgen@0.8.0: {} + + regjsparser@0.12.0: + dependencies: + jsesc: 3.0.2 + regjsparser@0.9.1: dependencies: jsesc: 0.5.0 @@ -18495,6 +19618,12 @@ snapshots: resolve-pkg-maps@1.0.0: {} + resolve@1.22.10: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + resolve@1.22.8: dependencies: is-core-module: 2.14.0 @@ -18665,6 +19794,8 @@ snapshots: semver@7.6.2: {} + semver@7.6.3: {} + send@0.18.0: dependencies: debug: 2.6.9 @@ -18683,6 +19814,24 @@ snapshots: transitivePeerDependencies: - supports-color + send@0.19.0: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + serialize-error@2.1.0: {} serve-static@1.15.0: @@ -18694,6 +19843,15 @@ snapshots: transitivePeerDependencies: - supports-color + serve-static@1.16.2: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.0 + transitivePeerDependencies: + - supports-color + set-blocking@2.0.0: {} set-function-length@1.2.2: @@ -18763,7 +19921,7 @@ snapshots: shebang-regex@3.0.0: {} - shell-quote@1.8.1: {} + shell-quote@1.8.2: {} side-channel@1.0.6: dependencies: @@ -18884,6 +20042,8 @@ snapshots: dependencies: readable-stream: 3.6.2 + split2@4.2.0: {} + split@1.0.1: dependencies: through: 2.3.8 @@ -19114,7 +20274,7 @@ snapshots: react: 18.3.1 use-sync-external-store: 1.2.2(react@18.3.1) - tailwindcss@3.2.7(postcss@8.4.21)(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5)): + tailwindcss@3.2.7(postcss@8.4.21)(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.10.5)(typescript@4.9.5)): dependencies: arg: 5.0.2 chokidar: 3.6.0 @@ -19133,7 +20293,7 @@ snapshots: postcss: 8.4.21 postcss-import: 14.1.0(postcss@8.4.21) postcss-js: 4.0.1(postcss@8.4.21) - postcss-load-config: 3.1.4(postcss@8.4.21)(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5)) + postcss-load-config: 3.1.4(postcss@8.4.21)(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.10.5)(typescript@4.9.5)) postcss-nested: 6.0.0(postcss@8.4.21) postcss-selector-parser: 6.1.0 postcss-value-parser: 4.2.0 @@ -19167,10 +20327,10 @@ snapshots: type-fest: 0.16.0 unique-string: 2.0.0 - terser@5.31.1: + terser@5.37.0: dependencies: '@jridgewell/source-map': 0.3.6 - acorn: 8.12.0 + acorn: 8.14.0 commander: 2.20.3 source-map-support: 0.5.21 @@ -19271,14 +20431,14 @@ snapshots: dependencies: typescript: 4.9.5 - ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5): + ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.10.5)(typescript@4.9.5): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 20.14.8 + '@types/node': 22.10.5 acorn: 8.12.0 acorn-walk: 8.3.3 arg: 4.1.3 @@ -19339,6 +20499,8 @@ snapshots: tslib@2.6.3: {} + tslib@2.8.1: {} + tsscmp@1.0.6: {} tsutils@3.21.0(typescript@4.9.5): @@ -19420,28 +20582,29 @@ snapshots: typedarray@0.0.6: {} - typeorm@0.3.12(sqlite3@5.1.4(encoding@0.1.13))(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5)): + typeorm@0.3.11(pg@8.11.0)(sqlite3@5.1.4(encoding@0.1.13))(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.10.5)(typescript@4.9.5)): dependencies: '@sqltools/formatter': 1.2.5 app-root-path: 3.1.0 buffer: 6.0.3 chalk: 4.1.2 cli-highlight: 2.1.11 - date-fns: 2.30.0 + date-fns: 2.29.3 debug: 4.3.5(supports-color@8.1.1) dotenv: 16.4.5 - glob: 8.1.0 + glob: 7.2.3 js-yaml: 4.1.0 - mkdirp: 2.1.6 - reflect-metadata: 0.1.14 + mkdirp: 1.0.4 + reflect-metadata: 0.1.13 sha.js: 2.4.11 tslib: 2.6.3 - uuid: 9.0.1 + uuid: 8.3.2 xml2js: 0.4.23 yargs: 17.7.2 optionalDependencies: + pg: 8.11.0 sqlite3: 5.1.4(encoding@0.1.13) - ts-node: 10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5) + ts-node: 10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.10.5)(typescript@4.9.5) transitivePeerDependencies: - supports-color @@ -19475,6 +20638,10 @@ snapshots: undici-types@5.26.5: {} + undici-types@6.20.0: {} + + undici@6.20.1: {} + unicode-canonical-property-names-ecmascript@2.0.0: {} unicode-emoji-utils@1.2.0: @@ -19488,6 +20655,8 @@ snapshots: unicode-match-property-value-ecmascript@2.1.0: {} + unicode-match-property-value-ecmascript@2.2.0: {} + unicode-property-aliases-ecmascript@2.1.0: {} unified@10.1.2: @@ -19565,6 +20734,12 @@ snapshots: escalade: 3.1.2 picocolors: 1.0.1 + update-browserslist-db@1.1.1(browserslist@4.24.3): + dependencies: + browserslist: 4.24.3 + escalade: 3.2.0 + picocolors: 1.1.1 + uri-js@4.4.1: dependencies: punycode: 2.3.1 @@ -19744,6 +20919,8 @@ snapshots: dependencies: string-width: 4.2.3 + wink-jaro-distance@2.0.0: {} + winston-daily-rotate-file@4.7.1(winston@3.8.2): dependencies: file-stream-rotator: 0.6.1 @@ -19852,6 +21029,8 @@ snapshots: yaml@2.4.5: {} + yaml@2.7.0: {} + yamljs@0.3.0: dependencies: argparse: 1.0.10 diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 00000000..1f53798b --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / diff --git a/server/api/externalapi.ts b/server/api/externalapi.ts index 75140bf0..767cb420 100644 --- a/server/api/externalapi.ts +++ b/server/api/externalapi.ts @@ -1,3 +1,5 @@ +import { MediaServerType } from '@server/constants/server'; +import { getSettings } from '@server/lib/settings'; import type { RateLimitOptions } from '@server/utils/rateLimit'; import rateLimit from '@server/utils/rateLimit'; import type NodeCache from 'node-cache'; @@ -33,13 +35,32 @@ class ExternalAPI { this.fetch = fetch; } - this.baseUrl = baseUrl; - this.params = params; + const url = new URL(baseUrl); + + const settings = getSettings(); + this.defaultHeaders = { 'Content-Type': 'application/json', Accept: 'application/json', + ...((url.username || url.password) && { + Authorization: `Basic ${Buffer.from( + `${url.username}:${url.password}` + ).toString('base64')}`, + }), + ...(settings.main.mediaServerType === MediaServerType.EMBY && { + 'Accept-Encoding': 'gzip', + }), ...options.headers, }; + + if (url.username || url.password) { + url.username = ''; + url.password = ''; + baseUrl = url.toString(); + } + + this.baseUrl = baseUrl; + this.params = params; this.cache = options.nodeCache; } @@ -49,10 +70,13 @@ class ExternalAPI { ttl?: number, config?: RequestInit ): Promise { + const headers = { ...this.defaultHeaders, ...config?.headers }; const cacheKey = this.serializeCacheKey(endpoint, { ...this.params, ...params, + headers, }); + const cachedItem = this.cache?.get(cacheKey); if (cachedItem) { return cachedItem; @@ -61,10 +85,7 @@ class ExternalAPI { const url = this.formatUrl(endpoint, params); const response = await this.fetch(url, { ...config, - headers: { - ...this.defaultHeaders, - ...config?.headers, - }, + headers, }); if (!response.ok) { const text = await response.text(); @@ -77,7 +98,7 @@ class ExternalAPI { } const data = await this.getDataFromResponse(response); - if (this.cache) { + if (this.cache && ttl !== 0) { this.cache.set(cacheKey, data, ttl ?? DEFAULT_TTL); } @@ -91,10 +112,13 @@ class ExternalAPI { ttl?: number, config?: RequestInit ): Promise { + const headers = { ...this.defaultHeaders, ...config?.headers }; const cacheKey = this.serializeCacheKey(endpoint, { config: { ...this.params, ...params }, + headers, data, }); + const cachedItem = this.cache?.get(cacheKey); if (cachedItem) { return cachedItem; @@ -134,7 +158,7 @@ class ExternalAPI { } const resData = await this.getDataFromResponse(response); - if (this.cache) { + if (this.cache && ttl !== 0) { this.cache.set(cacheKey, resData, ttl ?? DEFAULT_TTL); } @@ -148,10 +172,13 @@ class ExternalAPI { ttl?: number, config?: RequestInit ): Promise { + const headers = { ...this.defaultHeaders, ...config?.headers }; const cacheKey = this.serializeCacheKey(endpoint, { config: { ...this.params, ...params }, data, + headers, }); + const cachedItem = this.cache?.get(cacheKey); if (cachedItem) { return cachedItem; @@ -161,10 +188,7 @@ class ExternalAPI { const response = await this.fetch(url, { method: 'PUT', ...config, - headers: { - ...this.defaultHeaders, - ...config?.headers, - }, + headers, body: JSON.stringify(data), }); if (!response.ok) { @@ -178,7 +202,7 @@ class ExternalAPI { } const resData = await this.getDataFromResponse(response); - if (this.cache) { + if (this.cache && ttl !== 0) { this.cache.set(cacheKey, resData, ttl ?? DEFAULT_TTL); } @@ -220,9 +244,11 @@ class ExternalAPI { config?: RequestInit, overwriteBaseUrl?: string ): Promise { + const headers = { ...this.defaultHeaders, ...config?.headers }; const cacheKey = this.serializeCacheKey(endpoint, { ...this.params, ...params, + headers, }); const cachedItem = this.cache?.get(cacheKey); @@ -237,10 +263,7 @@ class ExternalAPI { const url = this.formatUrl(endpoint, params, overwriteBaseUrl); this.fetch(url, { ...config, - headers: { - ...this.defaultHeaders, - ...config?.headers, - }, + headers, }).then(async (response) => { if (!response.ok) { const text = await response.text(); @@ -263,10 +286,7 @@ class ExternalAPI { const url = this.formatUrl(endpoint, params, overwriteBaseUrl); const response = await this.fetch(url, { ...config, - headers: { - ...this.defaultHeaders, - ...config?.headers, - }, + headers, }); if (!response.ok) { const text = await response.text(); @@ -286,6 +306,14 @@ class ExternalAPI { return data; } + protected removeCache(endpoint: string, options?: Record) { + const cacheKey = this.serializeCacheKey(endpoint, { + ...this.params, + ...options, + }); + this.cache?.del(cacheKey); + } + private formatUrl( endpoint: string, params?: Record, @@ -310,13 +338,13 @@ class ExternalAPI { private serializeCacheKey( endpoint: string, - params?: Record + options?: Record ) { - if (!params) { + if (!options) { return `${this.baseUrl}${endpoint}`; } - return `${this.baseUrl}${endpoint}${JSON.stringify(params)}`; + return `${this.baseUrl}${endpoint}${JSON.stringify(options)}`; } private async getDataFromResponse(response: Response) { diff --git a/server/api/jellyfin.ts b/server/api/jellyfin.ts index 9f739fae..8a5aac7b 100644 --- a/server/api/jellyfin.ts +++ b/server/api/jellyfin.ts @@ -144,36 +144,26 @@ class JellyfinAPI extends ExternalAPI { try { return await authenticate(true); } catch (e) { - logger.debug(`Failed to authenticate with headers: ${e.message}`, { + logger.debug('Failed to authenticate with headers', { label: 'Jellyfin API', + error: e.cause.message ?? e.cause.statusText, ip: ClientIP, }); + + if (!e.cause.status) { + throw new ApiError(404, ApiErrorCode.InvalidUrl); + } + + if (e.cause.status === 401) { + throw new ApiError(e.cause.status, ApiErrorCode.InvalidCredentials); + } } try { return await authenticate(false); } catch (e) { - const status = e.cause?.status; - - const networkErrorCodes = new Set([ - 'ECONNREFUSED', - 'EHOSTUNREACH', - 'ENOTFOUND', - 'ETIMEDOUT', - 'ECONNRESET', - 'EADDRINUSE', - 'ENETDOWN', - 'ENETUNREACH', - 'EPIPE', - 'ECONNABORTED', - 'EPROTO', - 'EHOSTDOWN', - 'EAI_AGAIN', - 'ERR_INVALID_URL', - ]); - - if (networkErrorCodes.has(e.code) || status === 404) { - throw new ApiError(status, ApiErrorCode.InvalidUrl); + if (e.cause.status === 401) { + throw new ApiError(e.cause.status, ApiErrorCode.InvalidCredentials); } } @@ -196,7 +186,16 @@ class JellyfinAPI extends ExternalAPI { throw new ApiError(e.cause?.status, ApiErrorCode.InvalidCredentials); } } else { - throw new ApiError(401, ApiErrorCode.InvalidCredentials); + logger.error( + 'Something went wrong while authenticating with the Jellyfin server', + { + label: 'Jellyfin API', + error: e.cause.message ?? e.cause.statusText, + ip: ClientIP, + } + ); + + throw new ApiError(e.cause.status, ApiErrorCode.Unknown); } } @@ -224,8 +223,8 @@ class JellyfinAPI extends ExternalAPI { return serverResponse.ServerName; } catch (e) { logger.error( - `Something went wrong while getting the server name from the Jellyfin server: ${e.message}`, - { label: 'Jellyfin API' } + 'Something went wrong while getting the server name from the Jellyfin server', + { label: 'Jellyfin API', error: e.cause.message ?? e.cause.statusText } ); throw new ApiError(e.cause?.status, ApiErrorCode.Unknown); @@ -239,8 +238,8 @@ class JellyfinAPI extends ExternalAPI { return { users: userReponse }; } catch (e) { logger.error( - `Something went wrong while getting the account from the Jellyfin server: ${e.message}`, - { label: 'Jellyfin API' } + 'Something went wrong while getting the account from the Jellyfin server', + { label: 'Jellyfin API', error: e.cause.message ?? e.cause.statusText } ); throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken); @@ -255,8 +254,8 @@ class JellyfinAPI extends ExternalAPI { return userReponse; } catch (e) { logger.error( - `Something went wrong while getting the account from the Jellyfin server: ${e.message}`, - { label: 'Jellyfin API' } + 'Something went wrong while getting the account from the Jellyfin server', + { label: 'Jellyfin API', error: e.cause.message ?? e.cause.statusText } ); throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken); @@ -279,8 +278,11 @@ class JellyfinAPI extends ExternalAPI { return this.mapLibraries(mediaFolderResponse.Items); } catch (e) { logger.error( - `Something went wrong while getting libraries from the Jellyfin server: ${e.message}`, - { label: 'Jellyfin API' } + 'Something went wrong while getting libraries from the Jellyfin server', + { + label: 'Jellyfin API', + error: e.cause.message ?? e.cause.statusText, + } ); return []; @@ -334,8 +336,8 @@ class JellyfinAPI extends ExternalAPI { ); } catch (e) { logger.error( - `Something went wrong while getting library content from the Jellyfin server: ${e.message}`, - { label: 'Jellyfin API' } + 'Something went wrong while getting library content from the Jellyfin server', + { label: 'Jellyfin API', error: e.cause.message ?? e.cause.statusText } ); throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken); @@ -355,8 +357,8 @@ class JellyfinAPI extends ExternalAPI { return itemResponse; } catch (e) { logger.error( - `Something went wrong while getting library content from the Jellyfin server: ${e.message}`, - { label: 'Jellyfin API' } + 'Something went wrong while getting library content from the Jellyfin server', + { label: 'Jellyfin API', error: e.cause.message ?? e.cause.statusText } ); throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken); @@ -380,8 +382,8 @@ class JellyfinAPI extends ExternalAPI { } logger.error( - `Something went wrong while getting library content from the Jellyfin server: ${e.message}`, - { label: 'Jellyfin API' } + 'Something went wrong while getting library content from the Jellyfin server', + { label: 'Jellyfin API', error: e.cause.message ?? e.cause.statusText } ); throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken); } @@ -394,8 +396,8 @@ class JellyfinAPI extends ExternalAPI { return seasonResponse.Items; } catch (e) { logger.error( - `Something went wrong while getting the list of seasons from the Jellyfin server: ${e.message}`, - { label: 'Jellyfin API' } + 'Something went wrong while getting the list of seasons from the Jellyfin server', + { label: 'Jellyfin API', error: e.cause.message ?? e.cause.statusText } ); throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken); @@ -419,8 +421,8 @@ class JellyfinAPI extends ExternalAPI { ); } catch (e) { logger.error( - `Something went wrong while getting the list of episodes from the Jellyfin server: ${e.message}`, - { label: 'Jellyfin API' } + 'Something went wrong while getting the list of episodes from the Jellyfin server', + { label: 'Jellyfin API', error: e.cause.message ?? e.cause.statusText } ); throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken); @@ -436,8 +438,8 @@ class JellyfinAPI extends ExternalAPI { ).AccessToken; } catch (e) { logger.error( - `Something went wrong while creating an API key the Jellyfin server: ${e.message}`, - { label: 'Jellyfin API' } + 'Something went wrong while creating an API key from the Jellyfin server', + { label: 'Jellyfin API', error: e.cause.message ?? e.cause.statusText } ); throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken); diff --git a/server/api/plexapi.ts b/server/api/plexapi.ts index f6b8f3cb..10d5d1d2 100644 --- a/server/api/plexapi.ts +++ b/server/api/plexapi.ts @@ -180,7 +180,7 @@ class PlexAPI { settings.plex.libraries = []; } - settings.save(); + await settings.save(); } public async getLibraryContents( diff --git a/server/api/plextv.ts b/server/api/plextv.ts index 27bed196..92bffa80 100644 --- a/server/api/plextv.ts +++ b/server/api/plextv.ts @@ -3,6 +3,7 @@ import type { PlexDevice } from '@server/interfaces/api/plexInterfaces'; import cacheManager from '@server/lib/cache'; import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; +import { randomUUID } from 'node:crypto'; import xml2js from 'xml2js'; interface PlexAccountResponse { @@ -127,6 +128,11 @@ export interface PlexWatchlistItem { title: string; } +export interface PlexWatchlistCache { + etag: string; + response: WatchlistResponse; +} + class PlexTvAPI extends ExternalAPI { private authToken: string; @@ -261,6 +267,11 @@ class PlexTvAPI extends ExternalAPI { items: PlexWatchlistItem[]; }> { try { + const watchlistCache = cacheManager.getCache('plexwatchlist'); + let cachedWatchlist = watchlistCache.data.get( + this.authToken + ); + const params = new URLSearchParams({ 'X-Plex-Container-Start': offset.toString(), 'X-Plex-Container-Size': size.toString(), @@ -268,42 +279,62 @@ class PlexTvAPI extends ExternalAPI { const response = await this.fetch( `https://metadata.provider.plex.tv/library/sections/watchlist/all?${params.toString()}`, { - headers: this.defaultHeaders, + headers: { + ...this.defaultHeaders, + ...(cachedWatchlist?.etag + ? { 'If-None-Match': cachedWatchlist.etag } + : {}), + }, } ); const data = (await response.json()) as WatchlistResponse; + // If we don't recieve HTTP 304, the watchlist has been updated and we need to update the cache. + if (response.status >= 200 && response.status <= 299) { + cachedWatchlist = { + etag: response.headers.get('etag') ?? '', + response: data, + }; + + watchlistCache.data.set( + this.authToken, + cachedWatchlist + ); + } + const watchlistDetails = await Promise.all( - (data.MediaContainer.Metadata ?? []).map(async (watchlistItem) => { - const detailedResponse = await this.getRolling( - `/library/metadata/${watchlistItem.ratingKey}`, - {}, - undefined, - {}, - 'https://metadata.provider.plex.tv' - ); + (cachedWatchlist?.response.MediaContainer.Metadata ?? []).map( + async (watchlistItem) => { + const detailedResponse = await this.getRolling( + `/library/metadata/${watchlistItem.ratingKey}`, + {}, + undefined, + {}, + 'https://metadata.provider.plex.tv' + ); - const metadata = detailedResponse.MediaContainer.Metadata[0]; + const metadata = detailedResponse.MediaContainer.Metadata[0]; - const tmdbString = metadata.Guid.find((guid) => - guid.id.startsWith('tmdb') - ); - const tvdbString = metadata.Guid.find((guid) => - guid.id.startsWith('tvdb') - ); + 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, - }; - }) + 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 filteredList = watchlistDetails.filter((detail) => detail.tmdbId); @@ -311,7 +342,7 @@ class PlexTvAPI extends ExternalAPI { return { offset, size, - totalSize: data.MediaContainer.totalSize, + totalSize: cachedWatchlist?.response.MediaContainer.totalSize ?? 0, items: filteredList, }; } catch (e) { @@ -327,6 +358,29 @@ class PlexTvAPI extends ExternalAPI { }; } } + + public async pingToken() { + try { + const data: { pong: unknown } = await this.get( + '/api/v2/ping', + {}, + undefined, + { + headers: { + 'X-Plex-Client-Identifier': randomUUID(), + }, + } + ); + if (!data?.pong) { + throw new Error('No pong response'); + } + } catch (e) { + logger.error('Failed to ping token', { + label: 'Plex Refresh Token', + errorMessage: e.message, + }); + } + } } export default PlexTvAPI; diff --git a/server/api/rating/rottentomatoes.ts b/server/api/rating/rottentomatoes.ts index e86c2488..bfded767 100644 --- a/server/api/rating/rottentomatoes.ts +++ b/server/api/rating/rottentomatoes.ts @@ -1,6 +1,7 @@ import ExternalAPI from '@server/api/externalapi'; import cacheManager from '@server/lib/cache'; import { getSettings } from '@server/lib/settings'; +import jaro from 'wink-jaro-distance'; interface RTAlgoliaSearchResponse { results: { @@ -15,7 +16,7 @@ interface RTAlgoliaHit { tmsId: string; type: string; title: string; - titles: string[]; + titles?: string[]; description: string; releaseYear: number; rating: string; @@ -24,9 +25,9 @@ interface RTAlgoliaHit { isEmsSearchable: boolean; rtId: number; vanity: string; - aka: string[]; + aka?: string[]; posterImageUrl: string; - rottenTomatoes: { + rottenTomatoes?: { audienceScore: number; criticsIconUrl: string; wantToSeeCount: number; @@ -47,6 +48,47 @@ export interface RTRating { url: string; } +// Tunables +const INEXACT_TITLE_FACTOR = 0.25; +const ALTERNATE_TITLE_FACTOR = 0.8; +const PER_YEAR_PENALTY = 0.4; +const MINIMUM_SCORE = 0.175; + +// Normalization for title comparisons. +// Lowercase and strip non-alphanumeric (unicode-aware). +const norm = (s: string): string => + s.toLowerCase().replace(/[^\p{L}\p{N} ]/gu, ''); + +// Title similarity. 1 if exact, quarter-jaro otherwise. +const similarity = (a: string, b: string): number => + a === b ? 1 : jaro(a, b).similarity * INEXACT_TITLE_FACTOR; + +// Gets the best similarity score between the searched title and all alternate +// titles of the search result. Non-main titles are penalized. +const t_score = ({ title, titles, aka }: RTAlgoliaHit, s: string): number => { + const f = (t: string, i: number) => + similarity(norm(t), norm(s)) * (i ? ALTERNATE_TITLE_FACTOR : 1); + return Math.max(...[title].concat(aka || [], titles || []).map(f)); +}; + +// Year difference to score: 0 -> 1.0, 1 -> 0.6, 2 -> 0.2, 3+ -> 0.0 +const y_score = (r: RTAlgoliaHit, y?: number): number => + y ? Math.max(0, 1 - Math.abs(r.releaseYear - y) * PER_YEAR_PENALTY) : 1; + +// Cut score in half if result has no ratings. +const extra_score = (r: RTAlgoliaHit): number => (r.rottenTomatoes ? 1 : 0.5); + +// Score search result as product of all subscores +const score = (r: RTAlgoliaHit, name: string, year?: number): number => + t_score(r, name) * y_score(r, year) * extra_score(r); + +// Score each search result and return the highest scoring result, if any +const best = (rs: RTAlgoliaHit[], name: string, year?: number): RTAlgoliaHit => + rs + .map((r) => ({ score: score(r, name, year), result: r })) + .filter(({ score }) => score > MINIMUM_SCORE) + .sort(({ score: a }, { score: b }) => b - a)[0]?.result; + /** * This is a best-effort API. The Rotten Tomatoes API is technically * private and getting access costs money/requires approval. @@ -90,47 +132,21 @@ class RottenTomatoes extends ExternalAPI { year: number ): Promise { try { + const filters = encodeURIComponent('isEmsSearchable=1 AND type:"movie"'); const data = await this.post('/queries', { requests: [ { indexName: 'content_rt', - query: name, - params: 'filters=isEmsSearchable%20%3D%201&hitsPerPage=20', + query: name.replace(/\bthe\b ?/gi, ''), + params: `filters=${filters}&hitsPerPage=20`, }, ], }); const contentResults = data.results.find((r) => r.index === 'content_rt'); + const movie = best(contentResults?.hits || [], name, year); - if (!contentResults) { - return null; - } - - // First, attempt to match exact name and year - let movie = contentResults.hits.find( - (movie) => movie.releaseYear === year && movie.title === name - ); - - // If we don't find a movie, try to match partial name and year - if (!movie) { - movie = contentResults.hits.find( - (movie) => movie.releaseYear === year && movie.title.includes(name) - ); - } - - // If we still dont find a movie, try to match just on year - if (!movie) { - movie = contentResults.hits.find((movie) => movie.releaseYear === year); - } - - // One last try, try exact name match only - if (!movie) { - movie = contentResults.hits.find((movie) => movie.title === name); - } - - if (!movie) { - return null; - } + if (!movie?.rottenTomatoes) return null; return { title: movie.title, @@ -158,33 +174,21 @@ class RottenTomatoes extends ExternalAPI { year?: number ): Promise { try { + const filters = encodeURIComponent('isEmsSearchable=1 AND type:"tv"'); const data = await this.post('/queries', { requests: [ { indexName: 'content_rt', query: name, - params: 'filters=isEmsSearchable%20%3D%201&hitsPerPage=20', + params: `filters=${filters}&hitsPerPage=20`, }, ], }); const contentResults = data.results.find((r) => r.index === 'content_rt'); + const tvshow = best(contentResults?.hits || [], name, year); - if (!contentResults) { - return null; - } - - let tvshow: RTAlgoliaHit | undefined = contentResults.hits[0]; - - if (year) { - tvshow = contentResults.hits.find( - (series) => series.releaseYear === year - ); - } - - if (!tvshow) { - return null; - } + if (!tvshow?.rottenTomatoes) return null; return { title: tvshow.title, diff --git a/server/api/servarr/base.ts b/server/api/servarr/base.ts index ae024b6e..8b0d5ca0 100644 --- a/server/api/servarr/base.ts +++ b/server/api/servarr/base.ts @@ -157,9 +157,13 @@ class ServarrBase extends ExternalAPI { public getQueue = async (): Promise<(QueueItem & QueueItemAppendT)[]> => { try { - const data = await this.get>(`/queue`, { - includeEpisode: 'true', - }); + const data = await this.get>( + `/queue`, + { + includeEpisode: 'true', + }, + 0 + ); return data.records; } catch (e) { @@ -193,15 +197,24 @@ class ServarrBase extends ExternalAPI { } }; + async refreshMonitoredDownloads(): Promise { + await this.runCommand('RefreshMonitoredDownloads', {}); + } + protected async runCommand( commandName: string, options: Record ): Promise { try { - await this.post(`/command`, { - name: commandName, - ...options, - }); + await this.post( + `/command`, + { + name: commandName, + ...options, + }, + {}, + 0 + ); } catch (e) { throw new Error(`[${this.apiName}] Failed to run command: ${e.message}`); } diff --git a/server/api/servarr/radarr.ts b/server/api/servarr/radarr.ts index 51d30037..f3bf3faa 100644 --- a/server/api/servarr/radarr.ts +++ b/server/api/servarr/radarr.ts @@ -230,6 +230,23 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> { throw new Error(`[Radarr] Failed to remove movie: ${e.message}`); } }; + + public clearCache = ({ + tmdbId, + externalId, + }: { + tmdbId?: number | null; + externalId?: number | null; + }) => { + if (tmdbId) { + this.removeCache('/movie/lookup', { + term: `tmdb:${tmdbId}`, + }); + } + if (externalId) { + this.removeCache(`/movie/${externalId}`); + } + }; } export default RadarrAPI; diff --git a/server/api/servarr/sonarr.ts b/server/api/servarr/sonarr.ts index 67c9dd2a..5590c9ac 100644 --- a/server/api/servarr/sonarr.ts +++ b/server/api/servarr/sonarr.ts @@ -303,10 +303,10 @@ class SonarrAPI extends ServarrBase<{ }); try { - await this.runCommand('SeriesSearch', { seriesId }); + await this.runCommand('MissingEpisodeSearch', { seriesId }); } catch (e) { logger.error( - 'Something went wrong while executing Sonarr series search.', + 'Something went wrong while executing Sonarr missing episode search.', { label: 'Sonarr API', errorMessage: e.message, @@ -353,6 +353,30 @@ class SonarrAPI extends ServarrBase<{ throw new Error(`[Radarr] Failed to remove serie: ${e.message}`); } }; + + public clearCache = ({ + tvdbId, + externalId, + title, + }: { + tvdbId?: number | null; + externalId?: number | null; + title?: string | null; + }) => { + if (tvdbId) { + this.removeCache('/series/lookup', { + term: `tvdb:${tvdbId}`, + }); + } + if (externalId) { + this.removeCache(`/series/${externalId}`); + } + if (title) { + this.removeCache('/series/lookup', { + term: title, + }); + } + }; } export default SonarrAPI; diff --git a/server/api/themoviedb/index.ts b/server/api/themoviedb/index.ts index 6f13ec08..5cf449ea 100644 --- a/server/api/themoviedb/index.ts +++ b/server/api/themoviedb/index.ts @@ -99,16 +99,16 @@ interface DiscoverTvOptions { } class TheMovieDb extends ExternalAPI { - private region?: string; + private discoverRegion?: string; private originalLanguage?: string; constructor({ - region, + discoverRegion, originalLanguage, - }: { region?: string; originalLanguage?: string } = {}) { + }: { discoverRegion?: string; originalLanguage?: string } = {}) { super( 'https://api.themoviedb.org/3', { - api_key: 'db55323b8d3e4154498498a75642b381', + api_key: '431a8708161bcd1f1fbe7536137e61ed', }, { nodeCache: cacheManager.getCache('tmdb').data, @@ -118,7 +118,7 @@ class TheMovieDb extends ExternalAPI { }, } ); - this.region = region; + this.discoverRegion = discoverRegion; this.originalLanguage = originalLanguage; } @@ -469,7 +469,7 @@ class TheMovieDb extends ExternalAPI { page: page.toString(), include_adult: includeAdult ? 'true' : 'false', language, - region: this.region || '', + region: this.discoverRegion || '', with_original_language: originalLanguage && originalLanguage !== 'all' ? originalLanguage @@ -541,7 +541,7 @@ class TheMovieDb extends ExternalAPI { sort_by: sortBy, page: page.toString(), language, - region: this.region || '', + region: this.discoverRegion || '', // Set our release date values, but check if one is set and not the other, // so we can force a past date or a future date. TMDB Requires both values if one is set! 'first_air_date.gte': @@ -594,7 +594,7 @@ class TheMovieDb extends ExternalAPI { { page: page.toString(), language, - region: this.region || '', + region: this.discoverRegion || '', originalLanguage: this.originalLanguage || '', } ); @@ -620,7 +620,7 @@ class TheMovieDb extends ExternalAPI { { page: page.toString(), language, - region: this.region || '', + region: this.discoverRegion || '', } ); diff --git a/server/constants/error.ts b/server/constants/error.ts index ac18c3ec..664f02c9 100644 --- a/server/constants/error.ts +++ b/server/constants/error.ts @@ -2,7 +2,9 @@ export enum ApiErrorCode { InvalidUrl = 'INVALID_URL', InvalidCredentials = 'INVALID_CREDENTIALS', InvalidAuthToken = 'INVALID_AUTH_TOKEN', + InvalidEmail = 'INVALID_EMAIL', NotAdmin = 'NOT_ADMIN', + NoAdminUser = 'NO_ADMIN_USER', SyncErrorGroupedFolders = 'SYNC_ERROR_GROUPED_FOLDERS', SyncErrorNoLibraries = 'SYNC_ERROR_NO_LIBRARIES', Unknown = 'UNKNOWN', diff --git a/server/constants/media.ts b/server/constants/media.ts index de2bf834..dbcfbd34 100644 --- a/server/constants/media.ts +++ b/server/constants/media.ts @@ -16,4 +16,5 @@ export enum MediaStatus { PROCESSING, PARTIALLY_AVAILABLE, AVAILABLE, + BLACKLISTED, } diff --git a/server/datasource.ts b/server/datasource.ts index d4eadaa1..0c5b9ad3 100644 --- a/server/datasource.ts +++ b/server/datasource.ts @@ -1,7 +1,43 @@ -import 'reflect-metadata'; +import fs from 'fs'; +import type { TlsOptions } from 'tls'; import type { DataSourceOptions, EntityTarget, Repository } from 'typeorm'; import { DataSource } from 'typeorm'; +const DB_SSL_PREFIX = 'DB_SSL_'; + +function boolFromEnv(envVar: string, defaultVal = false) { + if (process.env[envVar]) { + return process.env[envVar]?.toLowerCase() === 'true'; + } + return defaultVal; +} + +function stringOrReadFileFromEnv(envVar: string): Buffer | string | undefined { + if (process.env[envVar]) { + return process.env[envVar]; + } + const filePath = process.env[`${envVar}_FILE`]; + if (filePath) { + return fs.readFileSync(filePath); + } + return undefined; +} + +function buildSslConfig(): TlsOptions | undefined { + if (process.env.DB_USE_SSL?.toLowerCase() !== 'true') { + return undefined; + } + return { + rejectUnauthorized: boolFromEnv( + `${DB_SSL_PREFIX}REJECT_UNAUTHORIZED`, + true + ), + ca: stringOrReadFileFromEnv(`${DB_SSL_PREFIX}CA`), + key: stringOrReadFileFromEnv(`${DB_SSL_PREFIX}KEY`), + cert: stringOrReadFileFromEnv(`${DB_SSL_PREFIX}CERT`), + }; +} + const devConfig: DataSourceOptions = { type: 'sqlite', database: process.env.CONFIG_DIRECTORY @@ -9,10 +45,10 @@ const devConfig: DataSourceOptions = { : 'config/db/db.sqlite3', synchronize: true, migrationsRun: false, - logging: false, + logging: boolFromEnv('DB_LOG_QUERIES'), enableWAL: true, entities: ['server/entity/**/*.ts'], - migrations: ['server/migration/**/*.ts'], + migrations: ['server/migration/sqlite/**/*.ts'], subscribers: ['server/subscriber/**/*.ts'], }; @@ -23,16 +59,60 @@ const prodConfig: DataSourceOptions = { : 'config/db/db.sqlite3', synchronize: false, migrationsRun: false, - logging: false, + logging: boolFromEnv('DB_LOG_QUERIES'), enableWAL: true, entities: ['dist/entity/**/*.js'], - migrations: ['dist/migration/**/*.js'], + migrations: ['dist/migration/sqlite/**/*.js'], subscribers: ['dist/subscriber/**/*.js'], }; -const dataSource = new DataSource( - process.env.NODE_ENV !== 'production' ? devConfig : prodConfig -); +const postgresDevConfig: DataSourceOptions = { + type: 'postgres', + host: process.env.DB_SOCKET_PATH || process.env.DB_HOST, + port: process.env.DB_SOCKET_PATH + ? undefined + : parseInt(process.env.DB_PORT ?? '5432'), + username: process.env.DB_USER, + password: process.env.DB_PASS, + database: process.env.DB_NAME ?? 'jellyseerr', + ssl: buildSslConfig(), + synchronize: false, + migrationsRun: true, + logging: boolFromEnv('DB_LOG_QUERIES'), + entities: ['server/entity/**/*.ts'], + migrations: ['server/migration/postgres/**/*.ts'], + subscribers: ['server/subscriber/**/*.ts'], +}; + +const postgresProdConfig: DataSourceOptions = { + type: 'postgres', + host: process.env.DB_SOCKET_PATH || process.env.DB_HOST, + port: process.env.DB_SOCKET_PATH + ? undefined + : parseInt(process.env.DB_PORT ?? '5432'), + username: process.env.DB_USER, + password: process.env.DB_PASS, + database: process.env.DB_NAME ?? 'jellyseerr', + ssl: buildSslConfig(), + synchronize: false, + migrationsRun: false, + logging: boolFromEnv('DB_LOG_QUERIES'), + entities: ['dist/entity/**/*.js'], + migrations: ['dist/migration/postgres/**/*.js'], + subscribers: ['dist/subscriber/**/*.js'], +}; + +export const isPgsql = process.env.DB_TYPE === 'postgres'; + +function getDataSource(): DataSourceOptions { + if (process.env.NODE_ENV === 'production') { + return isPgsql ? postgresProdConfig : prodConfig; + } else { + return isPgsql ? postgresDevConfig : devConfig; + } +} + +const dataSource = new DataSource(getDataSource()); export const getRepository = ( target: EntityTarget diff --git a/server/entity/Blacklist.ts b/server/entity/Blacklist.ts new file mode 100644 index 00000000..4ce3a86e --- /dev/null +++ b/server/entity/Blacklist.ts @@ -0,0 +1,95 @@ +import { MediaStatus, type MediaType } from '@server/constants/media'; +import { getRepository } from '@server/datasource'; +import Media from '@server/entity/Media'; +import { User } from '@server/entity/User'; +import type { BlacklistItem } from '@server/interfaces/api/blacklistInterfaces'; +import { + Column, + CreateDateColumn, + Entity, + Index, + JoinColumn, + ManyToOne, + OneToOne, + PrimaryGeneratedColumn, + Unique, +} from 'typeorm'; +import type { ZodNumber, ZodOptional, ZodString } from 'zod'; + +@Entity() +@Unique(['tmdbId']) +export class Blacklist implements BlacklistItem { + @PrimaryGeneratedColumn() + public id: number; + + @Column({ type: 'varchar' }) + public mediaType: MediaType; + + @Column({ nullable: true, type: 'varchar' }) + title?: string; + + @Column() + @Index() + public tmdbId: number; + + @ManyToOne(() => User, (user) => user.id, { + eager: true, + }) + user: User; + + @OneToOne(() => Media, (media) => media.blacklist, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public media: Media; + + @CreateDateColumn() + public createdAt: Date; + + constructor(init?: Partial) { + Object.assign(this, init); + } + + public static async addToBlacklist({ + blacklistRequest, + }: { + blacklistRequest: { + mediaType: MediaType; + title?: ZodOptional['_output']; + tmdbId: ZodNumber['_output']; + }; + }): Promise { + const blacklist = new this({ + ...blacklistRequest, + }); + + const mediaRepository = getRepository(Media); + let media = await mediaRepository.findOne({ + where: { + tmdbId: blacklistRequest.tmdbId, + }, + }); + + const blacklistRepository = getRepository(this); + + await blacklistRepository.save(blacklist); + + if (!media) { + media = new Media({ + tmdbId: blacklistRequest.tmdbId, + status: MediaStatus.BLACKLISTED, + status4k: MediaStatus.BLACKLISTED, + mediaType: blacklistRequest.mediaType, + blacklist: Promise.resolve(blacklist), + }); + + await mediaRepository.save(media); + } else { + media.blacklist = Promise.resolve(blacklist); + media.status = MediaStatus.BLACKLISTED; + media.status4k = MediaStatus.BLACKLISTED; + + await mediaRepository.save(media); + } + } +} diff --git a/server/entity/Media.ts b/server/entity/Media.ts index 723eb213..1941162a 100644 --- a/server/entity/Media.ts +++ b/server/entity/Media.ts @@ -3,12 +3,14 @@ import SonarrAPI from '@server/api/servarr/sonarr'; import { MediaStatus, MediaType } from '@server/constants/media'; import { MediaServerType } from '@server/constants/server'; import { getRepository } from '@server/datasource'; +import { Blacklist } from '@server/entity/Blacklist'; 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 { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; +import { DbAwareColumn } from '@server/utils/DbColumnHelper'; import { getHostname } from '@server/utils/getHostname'; import { AfterLoad, @@ -17,6 +19,7 @@ import { Entity, Index, OneToMany, + OneToOne, PrimaryGeneratedColumn, UpdateDateColumn, } from 'typeorm'; @@ -40,6 +43,10 @@ class Media { finalIds = tmdbIds; } + if (finalIds.length === 0) { + return []; + } + const media = await mediaRepository .createQueryBuilder('media') .leftJoinAndSelect( @@ -66,7 +73,7 @@ class Media { try { const media = await mediaRepository.findOne({ - where: { tmdbId: id, mediaType }, + where: { tmdbId: id, mediaType: mediaType }, relations: { requests: true, issues: true }, }); @@ -116,16 +123,32 @@ class Media { @OneToMany(() => Issue, (issue) => issue.media, { cascade: true }) public issues: Issue[]; + @OneToOne(() => Blacklist, (blacklist) => blacklist.media) + public blacklist: Promise; + @CreateDateColumn() public createdAt: Date; @UpdateDateColumn() public updatedAt: Date; - @Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' }) + /** + * The `lastSeasonChange` column stores the date and time when the media was added to the library. + * It needs to be database-aware because SQLite supports `datetime` while PostgreSQL supports `timestamp with timezone (timestampz)`. + */ + @DbAwareColumn({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' }) public lastSeasonChange: Date; - @Column({ type: 'datetime', nullable: true }) + /** + * The `mediaAddedAt` column stores the date and time when the media was added to the library. + * It needs to be database-aware because SQLite supports `datetime` while PostgreSQL supports `timestamp with timezone (timestampz)`. + * This column is nullable because it can be null when the media is not yet synced to the library. + */ + @DbAwareColumn({ + type: 'datetime', + default: () => 'CURRENT_TIMESTAMP', + nullable: true, + }) public mediaAddedAt: Date; @Column({ nullable: true, type: 'int' }) @@ -224,7 +247,7 @@ class Media { this.mediaUrl = `${jellyfinHost}/web/index.html#!/${pageName}?id=${this.jellyfinMediaId}&context=home&serverId=${serverId}`; } if (this.jellyfinMediaId4k) { - this.mediaUrl4k = `${jellyfinHost}/web/index.html#!/${pageName}?id=${this.jellyfinMediaId}&context=home&serverId=${serverId}`; + this.mediaUrl4k = `${jellyfinHost}/web/index.html#!/${pageName}?id=${this.jellyfinMediaId4k}&context=home&serverId=${serverId}`; } } } diff --git a/server/entity/MediaRequest.ts b/server/entity/MediaRequest.ts index ba67ab7b..b758da97 100644 --- a/server/entity/MediaRequest.ts +++ b/server/entity/MediaRequest.ts @@ -7,12 +7,14 @@ import type { import SonarrAPI from '@server/api/servarr/sonarr'; 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 { Permission } from '@server/lib/permissions'; @@ -40,6 +42,7 @@ export class RequestPermissionError extends Error {} export class QuotaRestrictedError extends Error {} export class DuplicateMediaRequestError extends Error {} export class NoSeasonsAvailableError extends Error {} +export class BlacklistedMediaError extends Error {} type MediaRequestOptions = { isAutoRequest?: boolean; @@ -56,6 +59,7 @@ export class MediaRequest { const mediaRepository = getRepository(Media); const requestRepository = getRepository(MediaRequest); const userRepository = getRepository(User); + const settings = getSettings(); let requestUser = user; @@ -143,6 +147,16 @@ export class MediaRequest { mediaType: requestBody.mediaType, }); } else { + if (media.status === MediaStatus.BLACKLISTED) { + logger.warn('Request for media blocked due to being blacklisted', { + tmdbId: tmdbMedia.id, + mediaType: requestBody.mediaType, + label: 'Media Request', + }); + + throw new BlacklistedMediaError('This media is blacklisted.'); + } + if (media.status === MediaStatus.UNKNOWN && !requestBody.is4k) { media.status = MediaStatus.PENDING; } @@ -194,6 +208,134 @@ 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; + + 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; + }); + + // 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)), + ]), + ]; + } + + logger.debug('Override rule applied.', { + label: 'Media Request', + overrides: prioritizedRule, + }); + } + } + if (requestBody.mediaType === MediaType.MOVIE) { await mediaRepository.save(media); @@ -232,9 +374,9 @@ export class MediaRequest { : undefined, is4k: requestBody.is4k, serverId: requestBody.serverId, - profileId: requestBody.profileId, - rootFolder: requestBody.rootFolder, - tags: requestBody.tags, + profileId: profileId, + rootFolder: rootFolder, + tags: tags, isAutoRequest: options.isAutoRequest ?? false, }); @@ -244,12 +386,14 @@ export class MediaRequest { const tmdbMediaShow = tmdbMedia as Awaited< ReturnType >; - const requestedSeasons = + let requestedSeasons = requestBody.seasons === 'all' - ? tmdbMediaShow.seasons - .map((season) => season.season_number) - .filter((sn) => sn > 0) + ? tmdbMediaShow.seasons.map((season) => season.season_number) : (requestBody.seasons as number[]); + if (!settings.main.enableSpecialEpisodes) { + requestedSeasons = requestedSeasons.filter((sn) => sn > 0); + } + let existingSeasons: number[] = []; // We need to check existing requests on this title to make sure we don't double up on seasons that were @@ -335,10 +479,10 @@ export class MediaRequest { : undefined, is4k: requestBody.is4k, serverId: requestBody.serverId, - profileId: requestBody.profileId, - rootFolder: requestBody.rootFolder, + profileId: profileId, + rootFolder: rootFolder, languageProfileId: requestBody.languageProfileId, - tags: requestBody.tags, + tags: tags, seasons: finalSeasons.map( (sn) => new SeasonRequest({ @@ -575,10 +719,15 @@ export class MediaRequest { // Do not update the status if the item is already partially available or available media[this.is4k ? 'status4k' : 'status'] !== MediaStatus.AVAILABLE && media[this.is4k ? 'status4k' : 'status'] !== - MediaStatus.PARTIALLY_AVAILABLE + MediaStatus.PARTIALLY_AVAILABLE && + media[this.is4k ? 'status4k' : 'status'] !== MediaStatus.PROCESSING ) { - media[this.is4k ? 'status4k' : 'status'] = MediaStatus.PROCESSING; - mediaRepository.save(media); + const statusField = this.is4k ? 'status4k' : 'status'; + + await mediaRepository.update( + { id: this.media.id }, + { [statusField]: MediaStatus.PROCESSING } + ); } if ( @@ -848,7 +997,7 @@ export class MediaRequest { const requestRepository = getRepository(MediaRequest); this.status = MediaRequestStatus.FAILED; - requestRepository.save(this); + await requestRepository.save(this); logger.warn( 'Something went wrong sending movie request to Radarr, marking status as FAILED', @@ -861,6 +1010,14 @@ export class MediaRequest { ); this.sendNotification(media, Notification.MEDIA_FAILED); + }) + .finally(() => { + radarr.clearCache({ + tmdbId: movie.id, + externalId: this.is4k + ? media.externalServiceId4k + : media.externalServiceId, + }); }); logger.info('Sent request to Radarr', { label: 'Media Request', @@ -1118,18 +1275,23 @@ export class MediaRequest { throw new Error('Media data not found'); } - media[this.is4k ? 'externalServiceId4k' : 'externalServiceId'] = - sonarrSeries.id; - media[this.is4k ? 'externalServiceSlug4k' : 'externalServiceSlug'] = - sonarrSeries.titleSlug; - media[this.is4k ? 'serviceId4k' : 'serviceId'] = sonarrSettings?.id; - await mediaRepository.save(media); + const updateFields = { + [this.is4k ? 'externalServiceId4k' : 'externalServiceId']: + sonarrSeries.id, + [this.is4k ? 'externalServiceSlug4k' : 'externalServiceSlug']: + sonarrSeries.titleSlug, + [this.is4k ? 'serviceId4k' : 'serviceId']: sonarrSettings?.id, + }; + + await mediaRepository.update({ id: this.media.id }, updateFields); }) .catch(async () => { const requestRepository = getRepository(MediaRequest); - this.status = MediaRequestStatus.FAILED; - requestRepository.save(this); + await requestRepository.update( + { id: this.id }, + { status: MediaRequestStatus.FAILED } + ); logger.warn( 'Something went wrong sending series request to Sonarr, marking status as FAILED', @@ -1142,6 +1304,15 @@ export class MediaRequest { ); this.sendNotification(media, Notification.MEDIA_FAILED); + }) + .finally(() => { + sonarr.clearCache({ + tvdbId, + externalId: this.is4k + ? media.externalServiceId4k + : media.externalServiceId, + title: series.name, + }); }); logger.info('Sent request to Sonarr', { label: 'Media Request', diff --git a/server/entity/OverrideRule.ts b/server/entity/OverrideRule.ts new file mode 100644 index 00000000..bf137343 --- /dev/null +++ b/server/entity/OverrideRule.ts @@ -0,0 +1,52 @@ +import { + Column, + CreateDateColumn, + Entity, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm'; + +@Entity() +class OverrideRule { + @PrimaryGeneratedColumn() + public id: number; + + @Column({ type: 'int', nullable: true }) + public radarrServiceId?: number; + + @Column({ type: 'int', nullable: true }) + public sonarrServiceId?: number; + + @Column({ nullable: true }) + public users?: string; + + @Column({ nullable: true }) + public genre?: string; + + @Column({ nullable: true }) + public language?: string; + + @Column({ nullable: true }) + public keywords?: string; + + @Column({ type: 'int', nullable: true }) + public profileId?: number; + + @Column({ nullable: true }) + public rootFolder?: string; + + @Column({ nullable: true }) + public tags?: string; + + @CreateDateColumn() + public createdAt: Date; + + @UpdateDateColumn() + public updatedAt: Date; + + constructor(init?: Partial) { + Object.assign(this, init); + } +} + +export default OverrideRule; diff --git a/server/entity/Season.ts b/server/entity/Season.ts index 44a83d97..d488a5c1 100644 --- a/server/entity/Season.ts +++ b/server/entity/Season.ts @@ -23,7 +23,9 @@ class Season { @Column({ type: 'int', default: MediaStatus.UNKNOWN }) public status4k: MediaStatus; - @ManyToOne(() => Media, (media) => media.seasons, { onDelete: 'CASCADE' }) + @ManyToOne(() => Media, (media) => media.seasons, { + onDelete: 'CASCADE', + }) public media: Promise; @CreateDateColumn() diff --git a/server/entity/User.ts b/server/entity/User.ts index e4c8314c..c8753bfe 100644 --- a/server/entity/User.ts +++ b/server/entity/User.ts @@ -83,13 +83,13 @@ export class User { @Column({ nullable: true }) public jellyfinUserId?: string; - @Column({ nullable: true }) + @Column({ nullable: true, select: false }) public jellyfinDeviceId?: string; - @Column({ nullable: true }) + @Column({ nullable: true, select: false }) public jellyfinAuthToken?: string; - @Column({ nullable: true }) + @Column({ nullable: true, select: false }) public plexToken?: string; @Column({ type: 'integer', default: 0 }) diff --git a/server/entity/UserSettings.ts b/server/entity/UserSettings.ts index ea4a7d33..82671fe3 100644 --- a/server/entity/UserSettings.ts +++ b/server/entity/UserSettings.ts @@ -31,7 +31,10 @@ export class UserSettings { public locale?: string; @Column({ nullable: true }) - public region?: string; + public discoverRegion?: string; + + @Column({ nullable: true }) + public streamingRegion?: string; @Column({ nullable: true }) public originalLanguage?: string; @@ -57,6 +60,9 @@ export class UserSettings { @Column({ nullable: true }) public telegramChatId?: string; + @Column({ nullable: true }) + public telegramMessageThreadId?: string; + @Column({ nullable: true }) public telegramSendSilently?: boolean; diff --git a/server/index.ts b/server/index.ts index ef20674d..e4e872ab 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1,5 +1,5 @@ import PlexAPI from '@server/api/plexapi'; -import dataSource, { getRepository } from '@server/datasource'; +import dataSource, { getRepository, isPgsql } from '@server/datasource'; import DiscoverSlider from '@server/entity/DiscoverSlider'; import { Session } from '@server/entity/Session'; import { User } from '@server/entity/User'; @@ -19,8 +19,11 @@ import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; import clearCookies from '@server/middleware/clearcookies'; import routes from '@server/routes'; +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 restartFlag from '@server/utils/restartFlag'; import { getClientIp } from '@supercharge/request-ip'; import { TypeormStore } from 'connect-typeorm/out'; @@ -38,11 +41,6 @@ import path from 'path'; import swaggerUi from 'swagger-ui-express'; import YAML from 'yamljs'; -if (process.env.forceIpv4First === 'true') { - dns.setDefaultResultOrder('ipv4first'); - net.setDefaultAutoSelectFamily(false); -} - const API_SPEC_PATH = path.join(__dirname, '../overseerr-api.yml'); logger.info(`Starting Overseerr version ${getAppVersion()}`); @@ -50,6 +48,12 @@ const dev = process.env.NODE_ENV !== 'production'; const app = next({ dev }); const handle = app.getRequestHandler(); +if (!appDataPermissions()) { + logger.error( + 'Something went wrong while checking config folder! Please ensure the config folder is set up properly.\nhttps://docs.jellyseerr.dev/getting-started' + ); +} + app .prepare() .then(async () => { @@ -57,14 +61,38 @@ app // Run migrations in production if (process.env.NODE_ENV === 'production') { - await dbConnection.query('PRAGMA foreign_keys=OFF'); - await dbConnection.runMigrations(); - await dbConnection.query('PRAGMA foreign_keys=ON'); + if (isPgsql) { + await dbConnection.runMigrations(); + } else { + await dbConnection.query('PRAGMA foreign_keys=OFF'); + await dbConnection.runMigrations(); + await dbConnection.query('PRAGMA foreign_keys=ON'); + } } // Load Settings const settings = await getSettings().load(); - restartFlag.initializeSettings(settings.main); + restartFlag.initializeSettings(settings); + + // Check if we force IPv4 first + if ( + process.env.forceIpv4First === 'true' || + settings.network.forceIpv4First + ) { + dns.setDefaultResultOrder('ipv4first'); + net.setDefaultAutoSelectFamily(false); + } + + if (settings.network.dnsServers.trim() !== '') { + dns.setServers( + settings.network.dnsServers.split(',').map((server) => server.trim()) + ); + } + + // Register HTTP proxy + if (settings.network.proxy.enabled) { + await createCustomProxyAgent(settings.network.proxy); + } // Migrate library types if ( @@ -118,7 +146,7 @@ app await DiscoverSlider.bootstrapSliders(); const server = express(); - if (settings.main.trustProxy) { + if (settings.network.trustProxy) { server.enable('trust proxy'); } server.use(cookieParser()); @@ -139,7 +167,7 @@ app next(); } }); - if (settings.main.csrfProtection) { + if (settings.network.csrfProtection) { server.use( csurf({ cookie: { @@ -169,12 +197,12 @@ app cookie: { maxAge: 1000 * 60 * 60 * 24 * 30, httpOnly: true, - sameSite: settings.main.csrfProtection ? 'strict' : 'lax', + sameSite: settings.network.csrfProtection ? 'strict' : 'lax', secure: 'auto', }, store: new TypeormStore({ cleanupLimit: 2, - ttl: 1000 * 60 * 60 * 24 * 30, + ttl: 60 * 60 * 24 * 30, }).connect(sessionRespository) as Store, }) ); @@ -202,6 +230,7 @@ app // Do not set cookies so CDNs can cache them server.use('/imageproxy', clearCookies, imageproxy); + server.use('/avatarproxy', clearCookies, avatarproxy); server.get('*', (req, res) => handle(req, res)); server.use( diff --git a/server/interfaces/api/blacklistInterfaces.ts b/server/interfaces/api/blacklistInterfaces.ts new file mode 100644 index 00000000..99e56585 --- /dev/null +++ b/server/interfaces/api/blacklistInterfaces.ts @@ -0,0 +1,14 @@ +import type { User } from '@server/entity/User'; +import type { PaginatedResponse } from '@server/interfaces/api/common'; + +export interface BlacklistItem { + tmdbId: number; + mediaType: 'movie' | 'tv'; + title?: string; + createdAt?: Date; + user: User; +} + +export interface BlacklistResultsResponse extends PaginatedResponse { + results: BlacklistItem[]; +} diff --git a/server/interfaces/api/discoverInterfaces.ts b/server/interfaces/api/discoverInterfaces.ts index 89cb7426..6738cbb5 100644 --- a/server/interfaces/api/discoverInterfaces.ts +++ b/server/interfaces/api/discoverInterfaces.ts @@ -5,6 +5,7 @@ export interface GenreSliderItem { } export interface WatchlistItem { + id: number; ratingKey: string; tmdbId: number; mediaType: 'movie' | 'tv'; diff --git a/server/interfaces/api/overrideRuleInterfaces.ts b/server/interfaces/api/overrideRuleInterfaces.ts new file mode 100644 index 00000000..5ae61a68 --- /dev/null +++ b/server/interfaces/api/overrideRuleInterfaces.ts @@ -0,0 +1,3 @@ +import type OverrideRule from '@server/entity/OverrideRule'; + +export type OverrideRuleResultsResponse = OverrideRule[]; diff --git a/server/interfaces/api/settingsInterfaces.ts b/server/interfaces/api/settingsInterfaces.ts index 1bf40cdb..017eef85 100644 --- a/server/interfaces/api/settingsInterfaces.ts +++ b/server/interfaces/api/settingsInterfaces.ts @@ -32,10 +32,12 @@ export interface PublicSettingsResponse { localLogin: boolean; movie4kEnabled: boolean; series4kEnabled: boolean; - region: string; + discoverRegion: string; + streamingRegion: string; originalLanguage: string; mediaServerType: number; partialRequestsEnabled: boolean; + enableSpecialEpisodes: boolean; cacheImages: boolean; vapidPublic: string; enablePushRegistration: boolean; @@ -58,7 +60,7 @@ export interface CacheItem { export interface CacheResponse { apiCaches: CacheItem[]; - imageCache: Record<'tmdb', { size: number; imageCount: number }>; + imageCache: Record<'tmdb' | 'avatar', { size: number; imageCount: number }>; } export interface StatusResponse { diff --git a/server/interfaces/api/userSettingsInterfaces.ts b/server/interfaces/api/userSettingsInterfaces.ts index 53b6729c..32776461 100644 --- a/server/interfaces/api/userSettingsInterfaces.ts +++ b/server/interfaces/api/userSettingsInterfaces.ts @@ -5,7 +5,8 @@ export interface UserSettingsGeneralResponse { email?: string; discordId?: string; locale?: string; - region?: string; + discoverRegion?: string; + streamingRegion?: string; originalLanguage?: string; movieQuotaLimit?: number; movieQuotaDays?: number; @@ -33,6 +34,7 @@ export interface UserSettingsNotificationsResponse { telegramEnabled?: boolean; telegramBotUsername?: string; telegramChatId?: string; + telegramMessageThreadId?: string; telegramSendSilently?: boolean; webPushEnabled?: boolean; notificationTypes: Partial; diff --git a/server/job/schedule.ts b/server/job/schedule.ts index b358130c..df0cd917 100644 --- a/server/job/schedule.ts +++ b/server/job/schedule.ts @@ -2,6 +2,7 @@ import { MediaServerType } from '@server/constants/server'; import availabilitySync from '@server/lib/availabilitySync'; import downloadTracker from '@server/lib/downloadtracker'; import ImageProxy from '@server/lib/imageproxy'; +import refreshToken from '@server/lib/refreshToken'; import { jellyfinFullScanner, jellyfinRecentScanner, @@ -13,7 +14,6 @@ import type { JobId } from '@server/lib/settings'; import { getSettings } from '@server/lib/settings'; import watchlistSync from '@server/lib/watchlistsync'; import logger from '@server/logger'; -import random from 'lodash/random'; import schedule from 'node-schedule'; interface ScheduledJob { @@ -70,6 +70,35 @@ export const startJobs = (): void => { running: () => plexFullScanner.status().running, cancelFn: () => plexFullScanner.cancel(), }); + + scheduledJobs.push({ + id: 'plex-refresh-token', + name: 'Plex Refresh Token', + type: 'process', + interval: 'fixed', + cronSchedule: jobs['plex-refresh-token'].schedule, + job: schedule.scheduleJob(jobs['plex-refresh-token'].schedule, () => { + logger.info('Starting scheduled job: Plex Refresh Token', { + label: 'Jobs', + }); + refreshToken.run(); + }), + }); + + // Watchlist Sync + scheduledJobs.push({ + id: 'plex-watchlist-sync', + name: 'Plex Watchlist Sync', + type: 'process', + interval: 'seconds', + cronSchedule: jobs['plex-watchlist-sync'].schedule, + job: schedule.scheduleJob(jobs['plex-watchlist-sync'].schedule, () => { + logger.info('Starting scheduled job: Plex Watchlist Sync', { + label: 'Jobs', + }); + watchlistSync.syncWatchlist(); + }), + }); } else if ( mediaServerType === MediaServerType.JELLYFIN || mediaServerType === MediaServerType.EMBY @@ -112,31 +141,6 @@ export const startJobs = (): void => { }); } - // Watchlist Sync - const watchlistSyncJob: ScheduledJob = { - id: 'plex-watchlist-sync', - name: 'Plex Watchlist Sync', - type: 'process', - interval: 'fixed', - cronSchedule: jobs['plex-watchlist-sync'].schedule, - job: schedule.scheduleJob(new Date(Date.now() + 1000 * 60 * 20), () => { - logger.info('Starting scheduled job: Plex Watchlist Sync', { - label: 'Jobs', - }); - watchlistSync.syncWatchlist(); - }), - }; - - // To help alleviate load on Plex's servers, we will add some fuzziness to the next schedule - // after each run - watchlistSyncJob.job.on('run', () => { - watchlistSyncJob.job.schedule( - new Date(Math.floor(Date.now() + 1000 * 60 * random(14, 24, true))) - ); - }); - - scheduledJobs.push(watchlistSyncJob); - // Run full radarr scan every 24 hours scheduledJobs.push({ id: 'radarr-scan', @@ -227,6 +231,9 @@ export const startJobs = (): void => { }); // Clean TMDB image cache ImageProxy.clearCache('tmdb'); + + // Clean users avatar image cache + ImageProxy.clearCache('avatar'); }), }); diff --git a/server/lib/cache.ts b/server/lib/cache.ts index 011205e7..51d0e08f 100644 --- a/server/lib/cache.ts +++ b/server/lib/cache.ts @@ -8,7 +8,8 @@ export type AvailableCacheIds = | 'imdb' | 'github' | 'plexguid' - | 'plextv'; + | 'plextv' + | 'plexwatchlist'; const DEFAULT_TTL = 300; const DEFAULT_CHECK_PERIOD = 120; @@ -68,6 +69,7 @@ class CacheManager { stdTtl: 86400 * 7, // 1 week cache checkPeriod: 60, }), + plexwatchlist: new Cache('plexwatchlist', 'Plex Watchlist'), }; public getCache(id: AvailableCacheIds): Cache { diff --git a/server/lib/downloadtracker.ts b/server/lib/downloadtracker.ts index e948c580..b96bfca8 100644 --- a/server/lib/downloadtracker.ts +++ b/server/lib/downloadtracker.ts @@ -85,6 +85,7 @@ class DownloadTracker { }); try { + await radarr.refreshMonitoredDownloads(); const queueItems = await radarr.getQueue(); this.radarrServers[server.id] = queueItems.map((item) => ({ @@ -162,6 +163,7 @@ class DownloadTracker { }); try { + await sonarr.refreshMonitoredDownloads(); const queueItems = await sonarr.getQueue(); this.sonarrServers[server.id] = queueItems.map((item) => ({ diff --git a/server/lib/imageproxy.ts b/server/lib/imageproxy.ts index 195e96b9..04e320a0 100644 --- a/server/lib/imageproxy.ts +++ b/server/lib/imageproxy.ts @@ -3,6 +3,7 @@ import type { RateLimitOptions } from '@server/utils/rateLimit'; import rateLimit from '@server/utils/rateLimit'; import { createHash } from 'crypto'; import { promises } from 'fs'; +import mime from 'mime/lite'; import path, { join } from 'path'; type ImageResponse = { @@ -11,7 +12,7 @@ type ImageResponse = { curRevalidate: number; isStale: boolean; etag: string; - extension: string; + extension: string | null; cacheKey: string; cacheMiss: boolean; }; @@ -27,29 +28,45 @@ class ImageProxy { let deletedImages = 0; const cacheDirectory = path.join(baseCacheDirectory, key); - const files = await promises.readdir(cacheDirectory); + try { + const files = await promises.readdir(cacheDirectory); - for (const file of files) { - const filePath = path.join(cacheDirectory, file); - const stat = await promises.lstat(filePath); + for (const file of files) { + const filePath = path.join(cacheDirectory, file); + const stat = await promises.lstat(filePath); - if (stat.isDirectory()) { - const imageFiles = await promises.readdir(filePath); + if (stat.isDirectory()) { + const imageFiles = await promises.readdir(filePath); - for (const imageFile of imageFiles) { - const [, expireAtSt] = imageFile.split('.'); - const expireAt = Number(expireAtSt); - const now = Date.now(); + for (const imageFile of imageFiles) { + const [, expireAtSt] = imageFile.split('.'); + const expireAt = Number(expireAtSt); + const now = Date.now(); - if (now > expireAt) { - await promises.rm(path.join(filePath, imageFile)); - deletedImages += 1; + if (now > expireAt) { + await promises.rm(path.join(filePath), { + recursive: true, + }); + deletedImages += 1; + } } } } + } catch (e) { + if (e.code === 'ENOENT') { + logger.error('Directory not found', { + label: 'Image Cache', + message: e.message, + }); + } else { + logger.error('Failed to read directory', { + label: 'Image Cache', + message: e.message, + }); + } } - logger.info(`Cleared ${deletedImages} stale image(s) from cache`, { + logger.info(`Cleared ${deletedImages} stale image(s) from cache '${key}'`, { label: 'Image Cache', }); } @@ -69,39 +86,56 @@ class ImageProxy { } private static async getDirectorySize(dir: string): Promise { - const files = await promises.readdir(dir, { - withFileTypes: true, - }); + try { + const files = await promises.readdir(dir, { + withFileTypes: true, + }); - const paths = files.map(async (file) => { - const path = join(dir, file.name); + const paths = files.map(async (file) => { + const path = join(dir, file.name); - if (file.isDirectory()) return await ImageProxy.getDirectorySize(path); + if (file.isDirectory()) return await ImageProxy.getDirectorySize(path); - if (file.isFile()) { - const { size } = await promises.stat(path); + if (file.isFile()) { + const { size } = await promises.stat(path); - return size; + return size; + } + + return 0; + }); + + return (await Promise.all(paths)) + .flat(Infinity) + .reduce((i, size) => i + size, 0); + } catch (e) { + if (e.code === 'ENOENT') { + return 0; } + } - return 0; - }); - - return (await Promise.all(paths)) - .flat(Infinity) - .reduce((i, size) => i + size, 0); + return 0; } private static async getImageCount(dir: string) { - const files = await promises.readdir(dir); + try { + const files = await promises.readdir(dir); - return files.length; + return files.length; + } catch (e) { + if (e.code === 'ENOENT') { + return 0; + } + } + + return 0; } private fetch: typeof fetch; private cacheVersion; private key; private baseUrl; + private headers: HeadersInit | null = null; constructor( key: string, @@ -109,6 +143,7 @@ class ImageProxy { options: { cacheVersion?: number; rateLimitOptions?: RateLimitOptions; + headers?: HeadersInit; } = {} ) { this.cacheVersion = options.cacheVersion ?? 1; @@ -122,9 +157,13 @@ class ImageProxy { } else { this.fetch = fetch; } + this.headers = options.headers || null; } - public async getImage(path: string): Promise { + public async getImage( + path: string, + fallbackPath?: string + ): Promise { const cacheKey = this.getCacheKey(path); const imageResponse = await this.get(cacheKey); @@ -133,7 +172,11 @@ class ImageProxy { const newImage = await this.set(path, cacheKey); if (!newImage) { - throw new Error('Failed to load image'); + if (fallbackPath) { + return await this.getImage(fallbackPath); + } else { + throw new Error('Failed to load image'); + } } return newImage; @@ -147,6 +190,27 @@ class ImageProxy { return imageResponse; } + public async clearCachedImage(path: string) { + // find cacheKey + const cacheKey = this.getCacheKey(path); + + try { + const directory = join(this.getCacheDirectory(), cacheKey); + const files = await promises.readdir(directory); + + await promises.rm(directory, { recursive: true }); + + logger.info(`Cleared ${files[0]} from cache 'avatar'`, { + label: 'Image Cache', + }); + } catch (e) { + logger.error('Failed to clear cached image', { + label: 'Image Cache', + message: e.message, + }); + } + } + private async get(cacheKey: string): Promise { try { const directory = join(this.getCacheDirectory(), cacheKey); @@ -187,16 +251,30 @@ class ImageProxy { const directory = join(this.getCacheDirectory(), cacheKey); const href = this.baseUrl + - (this.baseUrl.endsWith('/') ? '' : '/') + + (this.baseUrl.length > 0 + ? this.baseUrl.endsWith('/') + ? '' + : '/' + : '') + (path.startsWith('/') ? path.slice(1) : path); - const response = await this.fetch(href); + const response = await this.fetch(href, { + headers: this.headers || undefined, + }); + if (!response.ok) { + return null; + } const arrayBuffer = await response.arrayBuffer(); const buffer = Buffer.from(arrayBuffer); - const extension = path.split('.').pop() ?? ''; - const maxAge = Number( + const extension = mime.getExtension( + response.headers.get('content-type') ?? '' + ); + + let maxAge = Number( (response.headers.get('cache-control') ?? '0').split('=')[1] ); + + if (!maxAge) maxAge = 86400; const expireAt = Date.now() + maxAge * 1000; const etag = (response.headers.get('etag') ?? '').replace(/"/g, ''); @@ -232,7 +310,7 @@ class ImageProxy { private async writeToCacheDir( dir: string, - extension: string, + extension: string | null, maxAge: number, expireAt: number, buffer: Buffer, diff --git a/server/lib/notifications/agents/discord.ts b/server/lib/notifications/agents/discord.ts index e949e3e1..8eb1d99d 100644 --- a/server/lib/notifications/agents/discord.ts +++ b/server/lib/notifications/agents/discord.ts @@ -291,6 +291,10 @@ class DiscordAgent } } + if (settings.options.webhookRoleId) { + userMentions.push(`<@&${settings.options.webhookRoleId}>`); + } + const response = await fetch(settings.options.webhookUrl, { method: 'POST', headers: { diff --git a/server/lib/notifications/agents/telegram.ts b/server/lib/notifications/agents/telegram.ts index a66f9710..db12b494 100644 --- a/server/lib/notifications/agents/telegram.ts +++ b/server/lib/notifications/agents/telegram.ts @@ -17,6 +17,7 @@ interface TelegramMessagePayload { text: string; parse_mode: string; chat_id: string; + message_thread_id: string; disable_notification: boolean; } @@ -25,6 +26,7 @@ interface TelegramPhotoPayload { caption: string; parse_mode: string; chat_id: string; + message_thread_id: string; disable_notification: boolean; } @@ -182,6 +184,7 @@ class TelegramAgent body: JSON.stringify({ ...notificationPayload, chat_id: settings.options.chatId, + message_thread_id: settings.options.messageThreadId, disable_notification: !!settings.options.sendSilently, } as TelegramMessagePayload | TelegramPhotoPayload), }); @@ -233,6 +236,8 @@ class TelegramAgent body: JSON.stringify({ ...notificationPayload, chat_id: payload.notifyUser.settings.telegramChatId, + message_thread_id: + payload.notifyUser.settings.telegramMessageThreadId, disable_notification: !!payload.notifyUser.settings.telegramSendSilently, } as TelegramMessagePayload | TelegramPhotoPayload), @@ -296,6 +301,7 @@ class TelegramAgent body: JSON.stringify({ ...notificationPayload, chat_id: user.settings.telegramChatId, + message_thread_id: user.settings.telegramMessageThreadId, disable_notification: !!user.settings?.telegramSendSilently, } as TelegramMessagePayload | TelegramPhotoPayload), }); diff --git a/server/lib/permissions.ts b/server/lib/permissions.ts index 4a4a90d8..bc477169 100644 --- a/server/lib/permissions.ts +++ b/server/lib/permissions.ts @@ -27,6 +27,8 @@ export enum Permission { AUTO_REQUEST_TV = 33554432, RECENT_VIEW = 67108864, WATCHLIST_VIEW = 134217728, + MANAGE_BLACKLIST = 268435456, + VIEW_BLACKLIST = 1073741824, } export interface PermissionCheckOptions { diff --git a/server/lib/refreshToken.ts b/server/lib/refreshToken.ts new file mode 100644 index 00000000..ac7bd346 --- /dev/null +++ b/server/lib/refreshToken.ts @@ -0,0 +1,37 @@ +import PlexTvAPI from '@server/api/plextv'; +import { getRepository } from '@server/datasource'; +import { User } from '@server/entity/User'; +import logger from '@server/logger'; + +class RefreshToken { + public async run() { + const userRepository = getRepository(User); + + const users = await userRepository + .createQueryBuilder('user') + .addSelect('user.plexToken') + .where("user.plexToken != ''") + .getMany(); + + for (const user of users) { + await this.refreshUserToken(user); + } + } + + private async refreshUserToken(user: User) { + if (!user.plexToken) { + logger.warn('Skipping user refresh token for user without plex token', { + label: 'Plex Refresh Token', + user: user.displayName, + }); + return; + } + + const plexTvApi = new PlexTvAPI(user.plexToken); + plexTvApi.pingToken(); + } +} + +const refreshToken = new RefreshToken(); + +export default refreshToken; diff --git a/server/lib/scanners/jellyfin/index.ts b/server/lib/scanners/jellyfin/index.ts index f48de70e..b4816ae5 100644 --- a/server/lib/scanners/jellyfin/index.ts +++ b/server/lib/scanners/jellyfin/index.ts @@ -210,14 +210,27 @@ class JellyfinScanner { return; } - if (metadata.ProviderIds.Tvdb) { - tvShow = await this.tmdb.getShowByTvdbId({ - tvdbId: Number(metadata.ProviderIds.Tvdb), - }); - } else if (metadata.ProviderIds.Tmdb) { - tvShow = await this.tmdb.getTvShow({ - tvId: Number(metadata.ProviderIds.Tmdb), - }); + if (metadata.ProviderIds.Tmdb) { + try { + tvShow = await this.tmdb.getTvShow({ + tvId: Number(metadata.ProviderIds.Tmdb), + }); + } catch { + this.log('Unable to find TMDb ID for this title.', 'debug', { + jellyfinitem, + }); + } + } + if (!tvShow && metadata.ProviderIds.Tvdb) { + try { + tvShow = await this.tmdb.getShowByTvdbId({ + tvdbId: Number(metadata.ProviderIds.Tvdb), + }); + } catch { + this.log('Unable to find TVDb ID for this title.', 'debug', { + jellyfinitem, + }); + } } if (tvShow) { @@ -491,7 +504,13 @@ class JellyfinScanner { } }); } else { - this.log(`failed show: ${metadata.Name}`); + this.log( + `No information found for the show: ${metadata.Name}`, + 'debug', + { + jellyfinitem, + } + ); } } catch (e) { this.log( diff --git a/server/lib/scanners/plex/index.ts b/server/lib/scanners/plex/index.ts index f074872b..9dee904a 100644 --- a/server/lib/scanners/plex/index.ts +++ b/server/lib/scanners/plex/index.ts @@ -129,7 +129,7 @@ class PlexScanner }); settings.plex.libraries = newLibraries; - settings.save(); + await settings.save(); } } else { for (const library of this.libraries) { @@ -277,8 +277,11 @@ class PlexScanner const seasons = tvShow.seasons; const processableSeasons: ProcessableSeason[] = []; + const settings = getSettings(); - const filteredSeasons = seasons.filter((sn) => sn.season_number !== 0); + const filteredSeasons = settings.main.enableSpecialEpisodes + ? seasons + : seasons.filter((sn) => sn.season_number !== 0); for (const season of filteredSeasons) { const matchedPlexSeason = metadata.Children?.Metadata.find( diff --git a/server/lib/scanners/sonarr/index.ts b/server/lib/scanners/sonarr/index.ts index 3256c948..7a6e95c0 100644 --- a/server/lib/scanners/sonarr/index.ts +++ b/server/lib/scanners/sonarr/index.ts @@ -102,11 +102,12 @@ class SonarrScanner } const tmdbId = tvShow.id; + const settings = getSettings(); const filteredSeasons = sonarrSeries.seasons.filter( (sn) => - sn.seasonNumber !== 0 && - tvShow.seasons.find((s) => s.season_number === sn.seasonNumber) + tvShow.seasons.find((s) => s.season_number === sn.seasonNumber) && + (!settings.main.enableSpecialEpisodes ? sn.seasonNumber !== 0 : true) ); for (const season of filteredSeasons) { diff --git a/server/lib/settings/index.ts b/server/lib/settings/index.ts index 8c55d6c3..258dfe2f 100644 --- a/server/lib/settings/index.ts +++ b/server/lib/settings/index.ts @@ -2,7 +2,7 @@ import { MediaServerType } from '@server/constants/server'; import { Permission } from '@server/lib/permissions'; import { runMigrations } from '@server/lib/settings/migrator'; import { randomUUID } from 'crypto'; -import fs from 'fs'; +import fs from 'fs/promises'; import { merge } from 'lodash'; import path from 'path'; import webpush from 'web-push'; @@ -76,6 +76,7 @@ export interface DVRSettings { syncEnabled: boolean; preventSearch: boolean; tagRequests: boolean; + overrideRule: number[]; } export interface RadarrSettings extends DVRSettings { @@ -99,11 +100,21 @@ interface Quota { quotaDays?: number; } +export interface ProxySettings { + enabled: boolean; + hostname: string; + port: number; + useSsl: boolean; + user: string; + password: string; + bypassFilter: string; + bypassLocalAddresses: boolean; +} + export interface MainSettings { apiKey: string; applicationTitle: string; applicationUrl: string; - csrfProtection: boolean; cacheImages: boolean; defaultPermissions: number; defaultQuotas: { @@ -113,14 +124,23 @@ export interface MainSettings { hideAvailable: boolean; localLogin: boolean; newPlexLogin: boolean; - region: string; + discoverRegion: string; + streamingRegion: string; originalLanguage: string; - trustProxy: boolean; mediaServerType: number; partialRequestsEnabled: boolean; + enableSpecialEpisodes: boolean; locale: string; } +export interface NetworkSettings { + csrfProtection: boolean; + forceIpv4First: boolean; + dnsServers: string; + trustProxy: boolean; + proxy: ProxySettings; +} + interface PublicSettings { initialized: boolean; } @@ -132,13 +152,15 @@ interface FullPublicSettings extends PublicSettings { localLogin: boolean; movie4kEnabled: boolean; series4kEnabled: boolean; - region: string; + discoverRegion: string; + streamingRegion: string; originalLanguage: string; mediaServerType: number; jellyfinExternalHost?: string; jellyfinForgotPasswordUrl?: string; jellyfinServerName?: string; partialRequestsEnabled: boolean; + enableSpecialEpisodes: boolean; cacheImages: boolean; vapidPublic: string; enablePushRegistration: boolean; @@ -158,6 +180,7 @@ export interface NotificationAgentDiscord extends NotificationAgentConfig { botUsername?: string; botAvatarUrl?: string; webhookUrl: string; + webhookRoleId?: string; enableMentions: boolean; }; } @@ -198,6 +221,7 @@ export interface NotificationAgentTelegram extends NotificationAgentConfig { botUsername?: string; botAPI: string; chatId: string; + messageThreadId: string; sendSilently: boolean; }; } @@ -269,6 +293,7 @@ export type JobId = | 'plex-recently-added-scan' | 'plex-full-scan' | 'plex-watchlist-sync' + | 'plex-refresh-token' | 'radarr-scan' | 'sonarr-scan' | 'download-sync' @@ -291,6 +316,7 @@ export interface AllSettings { public: PublicSettings; notifications: NotificationSettings; jobs: Record; + network: NetworkSettings; } const SETTINGS_PATH = process.env.CONFIG_DIRECTORY @@ -309,7 +335,6 @@ class Settings { apiKey: '', applicationTitle: 'Jellyseerr', applicationUrl: '', - csrfProtection: false, cacheImages: false, defaultPermissions: Permission.REQUEST, defaultQuotas: { @@ -319,11 +344,12 @@ class Settings { hideAvailable: false, localLogin: true, newPlexLogin: true, - region: '', + discoverRegion: '', + streamingRegion: '', originalLanguage: '', - trustProxy: false, mediaServerType: MediaServerType.NOT_CONFIGURED, partialRequestsEnabled: true, + enableSpecialEpisodes: false, locale: 'en', }, plex: { @@ -372,6 +398,7 @@ class Settings { types: 0, options: { webhookUrl: '', + webhookRoleId: '', enableMentions: true, }, }, @@ -395,6 +422,7 @@ class Settings { options: { botAPI: '', chatId: '', + messageThreadId: '', sendSilently: false, }, }, @@ -445,7 +473,10 @@ class Settings { schedule: '0 0 3 * * *', }, 'plex-watchlist-sync': { - schedule: '0 */10 * * * *', + schedule: '0 */3 * * * *', + }, + 'plex-refresh-token': { + schedule: '0 0 5 * * *', }, 'radarr-scan': { schedule: '0 0 4 * * *', @@ -472,6 +503,22 @@ class Settings { schedule: '0 0 5 * * *', }, }, + network: { + csrfProtection: false, + trustProxy: false, + forceIpv4First: false, + dnsServers: '', + proxy: { + enabled: false, + hostname: '', + port: 8080, + useSsl: false, + user: '', + password: '', + bypassFilter: '', + bypassLocalAddresses: true, + }, + }, }; if (initialSettings) { this.data = merge(this.data, initialSettings); @@ -479,10 +526,6 @@ class Settings { } get main(): MainSettings { - if (!this.data.main.apiKey) { - this.data.main.apiKey = this.generateApiKey(); - this.save(); - } return this.data.main; } @@ -552,10 +595,12 @@ class Settings { series4kEnabled: this.data.sonarr.some( (sonarr) => sonarr.is4k && sonarr.isDefault ), - region: this.data.main.region, + 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, cacheImages: this.data.main.cacheImages, vapidPublic: this.vapidPublic, enablePushRegistration: this.data.notifications.agents.webpush.enabled, @@ -583,43 +628,37 @@ class Settings { this.data.jobs = data; } - get clientId(): string { - if (!this.data.clientId) { - this.data.clientId = randomUUID(); - this.save(); - } + get network(): NetworkSettings { + return this.data.network; + } + set network(data: NetworkSettings) { + this.data.network = data; + } + + get clientId(): string { return this.data.clientId; } get vapidPublic(): string { - this.generateVapidKeys(); - return this.data.vapidPublic; } get vapidPrivate(): string { - this.generateVapidKeys(); - return this.data.vapidPrivate; } - public regenerateApiKey(): MainSettings { + public async regenerateApiKey(): Promise { this.main.apiKey = this.generateApiKey(); - this.save(); + await this.save(); return this.main; } private generateApiKey(): string { - return Buffer.from(`${Date.now()}${randomUUID()}`).toString('base64'); - } - - private generateVapidKeys(force = false): void { - if (!this.data.vapidPublic || !this.data.vapidPrivate || force) { - const vapidKeys = webpush.generateVAPIDKeys(); - this.data.vapidPrivate = vapidKeys.privateKey; - this.data.vapidPublic = vapidKeys.publicKey; - this.save(); + if (process.env.API_KEY) { + return process.env.API_KEY; + } else { + return Buffer.from(`${Date.now()}${randomUUID()}`).toString('base64'); } } @@ -637,24 +676,50 @@ class Settings { return this; } - if (!fs.existsSync(SETTINGS_PATH)) { - this.save(); + let data; + try { + data = await fs.readFile(SETTINGS_PATH, 'utf-8'); + } catch { + await this.save(); } - const data = fs.readFileSync(SETTINGS_PATH, 'utf-8'); if (data) { const parsedJson = JSON.parse(data); - this.data = await runMigrations(parsedJson); - - this.data = merge(this.data, parsedJson); - - this.save(); + const migratedData = await runMigrations(parsedJson, SETTINGS_PATH); + this.data = merge(this.data, migratedData); } + + // generate keys and ids if it's missing + let change = false; + if (!this.data.main.apiKey) { + this.data.main.apiKey = this.generateApiKey(); + change = true; + } else if (process.env.API_KEY) { + if (this.main.apiKey != process.env.API_KEY) { + this.main.apiKey = process.env.API_KEY; + } + } + if (!this.data.clientId) { + this.data.clientId = randomUUID(); + change = true; + } + if (!this.data.vapidPublic || !this.data.vapidPrivate) { + const vapidKeys = webpush.generateVAPIDKeys(); + this.data.vapidPrivate = vapidKeys.privateKey; + this.data.vapidPublic = vapidKeys.publicKey; + change = true; + } + if (change) { + await this.save(); + } + return this; } - public save(): void { - fs.writeFileSync(SETTINGS_PATH, JSON.stringify(this.data, undefined, ' ')); + public async save(): Promise { + const tmp = SETTINGS_PATH + '.tmp'; + await fs.writeFile(tmp, JSON.stringify(this.data, undefined, ' ')); + await fs.rename(tmp, SETTINGS_PATH); } } diff --git a/server/lib/settings/migrations/0001_migrate_hostname.ts b/server/lib/settings/migrations/0001_migrate_hostname.ts index c514ac2d..ddc8211c 100644 --- a/server/lib/settings/migrations/0001_migrate_hostname.ts +++ b/server/lib/settings/migrations/0001_migrate_hostname.ts @@ -1,15 +1,14 @@ import type { AllSettings } from '@server/lib/settings'; const migrateHostname = (settings: any): AllSettings => { - const oldJellyfinSettings = settings.jellyfin; - if (oldJellyfinSettings && oldJellyfinSettings.hostname) { - const { hostname } = oldJellyfinSettings; + if (settings.jellyfin?.hostname) { + const { hostname } = settings.jellyfin; const protocolMatch = hostname.match(/^(https?):\/\//i); const useSsl = protocolMatch && protocolMatch[1].toLowerCase() === 'https'; const remainingUrl = hostname.replace(/^(https?):\/\//i, ''); const urlMatch = remainingUrl.match(/^([^:]+)(:([0-9]+))?(\/.*)?$/); - delete oldJellyfinSettings.hostname; + delete settings.jellyfin.hostname; if (urlMatch) { const [, ip, , port, urlBase] = urlMatch; settings.jellyfin = { @@ -21,9 +20,7 @@ const migrateHostname = (settings: any): AllSettings => { }; } } - if (settings.jellyfin && settings.jellyfin.hostname) { - delete settings.jellyfin.hostname; - } + return settings; }; diff --git a/server/lib/settings/migrations/0002_migrate_apitokens.ts b/server/lib/settings/migrations/0002_migrate_apitokens.ts index 46340433..0149c3e3 100644 --- a/server/lib/settings/migrations/0002_migrate_apitokens.ts +++ b/server/lib/settings/migrations/0002_migrate_apitokens.ts @@ -27,8 +27,14 @@ const migrateApiTokens = async (settings: any): Promise => { admin.jellyfinDeviceId ); jellyfinClient.setUserId(admin.jellyfinUserId ?? ''); - const apiKey = await jellyfinClient.createApiToken('Jellyseerr'); - settings.jellyfin.apiKey = apiKey; + try { + const apiKey = await jellyfinClient.createApiToken('Jellyseerr'); + settings.jellyfin.apiKey = apiKey; + } catch { + throw new Error( + "Failed to create Jellyfin API token from admin account. Please check your network configuration or edit your settings.json by adding an 'apiKey' field inside of the 'jellyfin' section to fix this issue." + ); + } } return settings; }; diff --git a/server/lib/settings/migrations/0004_migrate_region_setting.ts b/server/lib/settings/migrations/0004_migrate_region_setting.ts new file mode 100644 index 00000000..a140e078 --- /dev/null +++ b/server/lib/settings/migrations/0004_migrate_region_setting.ts @@ -0,0 +1,24 @@ +import type { AllSettings } from '@server/lib/settings'; + +const migrateRegionSetting = (settings: any): AllSettings => { + if ( + settings.main.discoverRegion !== undefined && + settings.main.streamingRegion !== undefined + ) { + return settings; + } + + const oldRegion = settings.main.region; + if (oldRegion) { + settings.main.discoverRegion = oldRegion; + settings.main.streamingRegion = oldRegion; + } else { + settings.main.discoverRegion = ''; + settings.main.streamingRegion = 'US'; + } + delete settings.main.region; + + return settings; +}; + +export default migrateRegionSetting; diff --git a/server/lib/settings/migrations/0005_migrate_network_settings.ts b/server/lib/settings/migrations/0005_migrate_network_settings.ts new file mode 100644 index 00000000..a6ad4844 --- /dev/null +++ b/server/lib/settings/migrations/0005_migrate_network_settings.ts @@ -0,0 +1,33 @@ +import type { AllSettings } from '@server/lib/settings'; + +const migrateNetworkSettings = (settings: any): AllSettings => { + if (settings.network) { + return settings; + } + const newSettings = { ...settings }; + newSettings.network = { + ...settings.network, + csrfProtection: settings.main.csrfProtection ?? false, + trustProxy: settings.main.trustProxy ?? false, + forceIpv4First: settings.main.forceIpv4First ?? false, + dnsServers: settings.main.dnsServers ?? '', + proxy: settings.main.proxy ?? { + enabled: false, + hostname: '', + port: 8080, + useSsl: false, + user: '', + password: '', + bypassFilter: '', + bypassLocalAddresses: true, + }, + }; + delete settings.main.csrfProtection; + delete settings.main.trustProxy; + delete settings.main.forceIpv4First; + delete settings.main.dnsServers; + delete settings.main.proxy; + return newSettings; +}; + +export default migrateNetworkSettings; diff --git a/server/lib/settings/migrator.ts b/server/lib/settings/migrator.ts index 856016e1..80114000 100644 --- a/server/lib/settings/migrator.ts +++ b/server/lib/settings/migrator.ts @@ -1,30 +1,100 @@ import type { AllSettings } from '@server/lib/settings'; import logger from '@server/logger'; -import fs from 'fs'; +import fs from 'fs/promises'; import path from 'path'; const migrationsDir = path.join(__dirname, 'migrations'); export const runMigrations = async ( - settings: AllSettings + settings: AllSettings, + SETTINGS_PATH: string ): Promise => { - const migrations = fs - .readdirSync(migrationsDir) - .filter((file) => file.endsWith('.js') || file.endsWith('.ts')) - // eslint-disable-next-line @typescript-eslint/no-var-requires - .map((file) => require(path.join(migrationsDir, file)).default); - let migrated = settings; try { + // we read old backup and create a backup of currents settings + const BACKUP_PATH = SETTINGS_PATH.replace('.json', '.old.json'); + let oldBackup: string | null = null; + try { + oldBackup = await fs.readFile(BACKUP_PATH, 'utf-8'); + } catch { + /* empty */ + } + await fs.writeFile(BACKUP_PATH, JSON.stringify(settings, undefined, ' ')); + + const migrations = (await fs.readdir(migrationsDir)).filter( + (file) => file.endsWith('.js') || file.endsWith('.ts') + ); + + const settingsBefore = JSON.stringify(migrated); + for (const migration of migrations) { - migrated = await migration(migrated); + try { + logger.debug(`Checking migration '${migration}'...`, { + label: 'Settings Migrator', + }); + const { default: migrationFn } = await import( + path.join(migrationsDir, migration) + ); + const newSettings = await migrationFn(structuredClone(migrated)); + if (JSON.stringify(migrated) !== JSON.stringify(newSettings)) { + logger.debug(`Migration '${migration}' has been applied.`, { + label: 'Settings Migrator', + }); + } + migrated = newSettings; + } catch (e) { + // we stop jellyseerr if the migration failed + logger.error( + `Error while running migration '${migration}': ${e.message}`, + { + label: 'Settings Migrator', + } + ); + logger.error( + 'A common cause for this error is a permission issue with your configuration folder, a network issue or a corrupted database.', + { + label: 'Settings Migrator', + } + ); + process.exit(); + } + } + + const settingsAfter = JSON.stringify(migrated); + + if (settingsBefore !== settingsAfter) { + // a migration occured + // we check that the new config will be saved + await fs.writeFile( + SETTINGS_PATH, + JSON.stringify(migrated, undefined, ' ') + ); + const fileSaved = JSON.parse(await fs.readFile(SETTINGS_PATH, 'utf-8')); + if (JSON.stringify(fileSaved) !== settingsAfter) { + // something went wrong while saving file + throw new Error('Unable to save settings after migration.'); + } + } else if (oldBackup) { + // no migration occured + // we save the old backup (to avoid settings.json and settings.old.json being the same) + await fs.writeFile(BACKUP_PATH, oldBackup.toString()); } } catch (e) { + // we stop jellyseerr if the migration failed logger.error( `Something went wrong while running settings migrations: ${e.message}`, - { label: 'Settings Migrator' } + { + label: 'Settings Migrator', + } ); + logger.error( + 'A common cause for this issue is a permission error of your configuration folder.', + { + label: 'Settings Migrator', + } + ); + process.exit(); } return migrated; diff --git a/server/lib/watchlistsync.ts b/server/lib/watchlistsync.ts index 2d198451..4919bf70 100644 --- a/server/lib/watchlistsync.ts +++ b/server/lib/watchlistsync.ts @@ -62,7 +62,7 @@ class WatchlistSync { const plexTvApi = new PlexTvAPI(user.plexToken); - const response = await plexTvApi.getWatchlist({ size: 200 }); + const response = await plexTvApi.getWatchlist({ size: 20 }); const mediaItems = await Media.getRelatedMedia( user, diff --git a/server/migration/postgres/1734786061496-InitialMigration.ts b/server/migration/postgres/1734786061496-InitialMigration.ts new file mode 100644 index 00000000..592c56fb --- /dev/null +++ b/server/migration/postgres/1734786061496-InitialMigration.ts @@ -0,0 +1,195 @@ +import type { MigrationInterface, QueryRunner } from 'typeorm'; + +export class InitialMigration1734786061496 implements MigrationInterface { + name = 'InitialMigration1734786061496'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "blacklist" ("id" SERIAL NOT NULL, "mediaType" character varying NOT NULL, "title" character varying, "tmdbId" integer NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "userId" integer, "mediaId" integer, CONSTRAINT "UQ_6bbafa28411e6046421991ea21c" UNIQUE ("tmdbId"), CONSTRAINT "REL_62b7ade94540f9f8d8bede54b9" UNIQUE ("mediaId"), CONSTRAINT "PK_04dc42a96bf0914cda31b579702" PRIMARY KEY ("id"))` + ); + await queryRunner.query( + `CREATE INDEX "IDX_6bbafa28411e6046421991ea21" ON "blacklist" ("tmdbId") ` + ); + await queryRunner.query( + `CREATE TABLE "season_request" ("id" SERIAL NOT NULL, "seasonNumber" integer NOT NULL, "status" integer NOT NULL DEFAULT '1', "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "requestId" integer, CONSTRAINT "PK_4811e502081543bf620f1fa4328" PRIMARY KEY ("id"))` + ); + await queryRunner.query( + `CREATE TABLE "media_request" ("id" SERIAL NOT NULL, "status" integer NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "type" character varying NOT NULL, "is4k" boolean NOT NULL DEFAULT false, "serverId" integer, "profileId" integer, "rootFolder" character varying, "languageProfileId" integer, "tags" text, "isAutoRequest" boolean NOT NULL DEFAULT false, "mediaId" integer NOT NULL, "requestedById" integer, "modifiedById" integer, CONSTRAINT "PK_f8334500e8e12db87536558c66c" PRIMARY KEY ("id"))` + ); + await queryRunner.query( + `CREATE TABLE "season" ("id" SERIAL NOT NULL, "seasonNumber" integer NOT NULL, "status" integer NOT NULL DEFAULT '1', "status4k" integer NOT NULL DEFAULT '1', "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "mediaId" integer NOT NULL, CONSTRAINT "PK_8ac0d081dbdb7ab02d166bcda9f" PRIMARY KEY ("id"))` + ); + await queryRunner.query( + `CREATE TABLE "media" ("id" SERIAL NOT NULL, "mediaType" character varying NOT NULL, "tmdbId" integer NOT NULL, "tvdbId" integer, "imdbId" character varying, "status" integer NOT NULL DEFAULT '1', "status4k" integer NOT NULL DEFAULT '1', "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "lastSeasonChange" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "mediaAddedAt" TIMESTAMP WITH TIME ZONE DEFAULT now(), "serviceId" integer, "serviceId4k" integer, "externalServiceId" integer, "externalServiceId4k" integer, "externalServiceSlug" character varying, "externalServiceSlug4k" character varying, "ratingKey" character varying, "ratingKey4k" character varying, "jellyfinMediaId" character varying, "jellyfinMediaId4k" character varying, CONSTRAINT "UQ_41a289eb1fa489c1bc6f38d9c3c" UNIQUE ("tvdbId"), CONSTRAINT "PK_f4e0fcac36e050de337b670d8bd" PRIMARY KEY ("id"))` + ); + await queryRunner.query( + `CREATE INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5" ON "media" ("tmdbId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_41a289eb1fa489c1bc6f38d9c3" ON "media" ("tvdbId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_7ff2d11f6a83cb52386eaebe74" ON "media" ("imdbId") ` + ); + await queryRunner.query( + `CREATE TABLE "watchlist" ("id" SERIAL NOT NULL, "ratingKey" character varying NOT NULL, "mediaType" character varying NOT NULL, "title" character varying NOT NULL, "tmdbId" integer NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "requestedById" integer, "mediaId" integer NOT NULL, CONSTRAINT "UNIQUE_USER_DB" UNIQUE ("tmdbId", "requestedById"), CONSTRAINT "PK_0c8c0dbcc8d379117138e71ad5b" PRIMARY KEY ("id"))` + ); + await queryRunner.query( + `CREATE INDEX "IDX_939f205946256cc0d2a1ac51a8" ON "watchlist" ("tmdbId") ` + ); + await queryRunner.query( + `CREATE TABLE "user_push_subscription" ("id" SERIAL NOT NULL, "endpoint" character varying NOT NULL, "p256dh" character varying NOT NULL, "auth" character varying NOT NULL, "userId" integer, CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "PK_397020e7be9a4086cc798e0bb63" PRIMARY KEY ("id"))` + ); + await queryRunner.query( + `CREATE TABLE "user_settings" ("id" SERIAL NOT NULL, "locale" character varying NOT NULL DEFAULT '', "discoverRegion" character varying, "streamingRegion" character varying, "originalLanguage" character varying, "pgpKey" character varying, "discordId" character varying, "pushbulletAccessToken" character varying, "pushoverApplicationToken" character varying, "pushoverUserKey" character varying, "pushoverSound" character varying, "telegramChatId" character varying, "telegramSendSilently" boolean, "watchlistSyncMovies" boolean, "watchlistSyncTv" boolean, "notificationTypes" text, "userId" integer, CONSTRAINT "REL_986a2b6d3c05eb4091bb8066f7" UNIQUE ("userId"), CONSTRAINT "PK_00f004f5922a0744d174530d639" PRIMARY KEY ("id"))` + ); + await queryRunner.query( + `CREATE TABLE "user" ("id" SERIAL NOT NULL, "email" character varying NOT NULL, "plexUsername" character varying, "jellyfinUsername" character varying, "username" character varying, "password" character varying, "resetPasswordGuid" character varying, "recoveryLinkExpirationDate" date, "userType" integer NOT NULL DEFAULT '1', "plexId" integer, "jellyfinUserId" character varying, "jellyfinDeviceId" character varying, "jellyfinAuthToken" character varying, "plexToken" character varying, "permissions" integer NOT NULL DEFAULT '0', "avatar" character varying NOT NULL, "movieQuotaLimit" integer, "movieQuotaDays" integer, "tvQuotaLimit" integer, "tvQuotaDays" integer, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"), CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id"))` + ); + await queryRunner.query( + `CREATE TABLE "issue_comment" ("id" SERIAL NOT NULL, "message" text NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "userId" integer, "issueId" integer, CONSTRAINT "PK_2ad05784e2ae661fa409e5e0248" PRIMARY KEY ("id"))` + ); + await queryRunner.query( + `CREATE TABLE "issue" ("id" SERIAL NOT NULL, "issueType" integer NOT NULL, "status" integer NOT NULL DEFAULT '1', "problemSeason" integer NOT NULL DEFAULT '0', "problemEpisode" integer NOT NULL DEFAULT '0', "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "mediaId" integer, "createdById" integer, "modifiedById" integer, CONSTRAINT "PK_f80e086c249b9f3f3ff2fd321b7" PRIMARY KEY ("id"))` + ); + await queryRunner.query( + `CREATE TABLE "discover_slider" ("id" SERIAL NOT NULL, "type" integer NOT NULL, "order" integer NOT NULL, "isBuiltIn" boolean NOT NULL DEFAULT false, "enabled" boolean NOT NULL DEFAULT true, "title" character varying, "data" character varying, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_20a71a098d04bae448e4d51db23" PRIMARY KEY ("id"))` + ); + await queryRunner.query( + `CREATE TABLE "session" ("expiredAt" bigint NOT NULL, "id" character varying(255) NOT NULL, "json" text NOT NULL, CONSTRAINT "PK_f55da76ac1c3ac420f444d2ff11" PRIMARY KEY ("id"))` + ); + await queryRunner.query( + `CREATE INDEX "IDX_28c5d1d16da7908c97c9bc2f74" ON "session" ("expiredAt") ` + ); + await queryRunner.query( + `ALTER TABLE "blacklist" ADD CONSTRAINT "FK_53c1ab62c3e5875bc3ac474823e" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE "blacklist" ADD CONSTRAINT "FK_62b7ade94540f9f8d8bede54b99" FOREIGN KEY ("mediaId") REFERENCES "media"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE "season_request" ADD CONSTRAINT "FK_6f14737e346d6b27d8e50d2157a" FOREIGN KEY ("requestId") REFERENCES "media_request"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE "media_request" ADD CONSTRAINT "FK_a1aa713f41c99e9d10c48da75a0" FOREIGN KEY ("mediaId") REFERENCES "media"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE "media_request" ADD CONSTRAINT "FK_6997bee94720f1ecb7f31137095" FOREIGN KEY ("requestedById") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE "media_request" ADD CONSTRAINT "FK_f4fc4efa14c3ba2b29c4525fa15" FOREIGN KEY ("modifiedById") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE "season" ADD CONSTRAINT "FK_087099b39600be695591da9a49c" FOREIGN KEY ("mediaId") REFERENCES "media"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE "watchlist" ADD CONSTRAINT "FK_ae34e6b153a90672eb9dc4857d7" FOREIGN KEY ("requestedById") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE "watchlist" ADD CONSTRAINT "FK_6641da8d831b93dfcb429f8b8bc" FOREIGN KEY ("mediaId") REFERENCES "media"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE "user_push_subscription" ADD CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE "user_settings" ADD CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE "issue_comment" ADD CONSTRAINT "FK_707b033c2d0653f75213614789d" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE "issue_comment" ADD CONSTRAINT "FK_180710fead1c94ca499c57a7d42" FOREIGN KEY ("issueId") REFERENCES "issue"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE "issue" ADD CONSTRAINT "FK_276e20d053f3cff1645803c95d8" FOREIGN KEY ("mediaId") REFERENCES "media"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE "issue" ADD CONSTRAINT "FK_10b17b49d1ee77e7184216001e0" FOREIGN KEY ("createdById") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE "issue" ADD CONSTRAINT "FK_da88a1019c850d1a7b143ca02e5" FOREIGN KEY ("modifiedById") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "issue" DROP CONSTRAINT "FK_da88a1019c850d1a7b143ca02e5"` + ); + await queryRunner.query( + `ALTER TABLE "issue" DROP CONSTRAINT "FK_10b17b49d1ee77e7184216001e0"` + ); + await queryRunner.query( + `ALTER TABLE "issue" DROP CONSTRAINT "FK_276e20d053f3cff1645803c95d8"` + ); + await queryRunner.query( + `ALTER TABLE "issue_comment" DROP CONSTRAINT "FK_180710fead1c94ca499c57a7d42"` + ); + await queryRunner.query( + `ALTER TABLE "issue_comment" DROP CONSTRAINT "FK_707b033c2d0653f75213614789d"` + ); + await queryRunner.query( + `ALTER TABLE "user_settings" DROP CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78"` + ); + await queryRunner.query( + `ALTER TABLE "user_push_subscription" DROP CONSTRAINT "FK_03f7958328e311761b0de675fbe"` + ); + await queryRunner.query( + `ALTER TABLE "watchlist" DROP CONSTRAINT "FK_6641da8d831b93dfcb429f8b8bc"` + ); + await queryRunner.query( + `ALTER TABLE "watchlist" DROP CONSTRAINT "FK_ae34e6b153a90672eb9dc4857d7"` + ); + await queryRunner.query( + `ALTER TABLE "season" DROP CONSTRAINT "FK_087099b39600be695591da9a49c"` + ); + await queryRunner.query( + `ALTER TABLE "media_request" DROP CONSTRAINT "FK_f4fc4efa14c3ba2b29c4525fa15"` + ); + await queryRunner.query( + `ALTER TABLE "media_request" DROP CONSTRAINT "FK_6997bee94720f1ecb7f31137095"` + ); + await queryRunner.query( + `ALTER TABLE "media_request" DROP CONSTRAINT "FK_a1aa713f41c99e9d10c48da75a0"` + ); + await queryRunner.query( + `ALTER TABLE "season_request" DROP CONSTRAINT "FK_6f14737e346d6b27d8e50d2157a"` + ); + await queryRunner.query( + `ALTER TABLE "blacklist" DROP CONSTRAINT "FK_62b7ade94540f9f8d8bede54b99"` + ); + await queryRunner.query( + `ALTER TABLE "blacklist" DROP CONSTRAINT "FK_53c1ab62c3e5875bc3ac474823e"` + ); + await queryRunner.query( + `DROP INDEX "public"."IDX_28c5d1d16da7908c97c9bc2f74"` + ); + await queryRunner.query(`DROP TABLE "session"`); + await queryRunner.query(`DROP TABLE "discover_slider"`); + await queryRunner.query(`DROP TABLE "issue"`); + await queryRunner.query(`DROP TABLE "issue_comment"`); + await queryRunner.query(`DROP TABLE "user"`); + await queryRunner.query(`DROP TABLE "user_settings"`); + await queryRunner.query(`DROP TABLE "user_push_subscription"`); + await queryRunner.query( + `DROP INDEX "public"."IDX_939f205946256cc0d2a1ac51a8"` + ); + await queryRunner.query(`DROP TABLE "watchlist"`); + await queryRunner.query( + `DROP INDEX "public"."IDX_7ff2d11f6a83cb52386eaebe74"` + ); + await queryRunner.query( + `DROP INDEX "public"."IDX_41a289eb1fa489c1bc6f38d9c3"` + ); + await queryRunner.query( + `DROP INDEX "public"."IDX_7157aad07c73f6a6ae3bbd5ef5"` + ); + await queryRunner.query(`DROP TABLE "media"`); + await queryRunner.query(`DROP TABLE "season"`); + await queryRunner.query(`DROP TABLE "media_request"`); + await queryRunner.query(`DROP TABLE "season_request"`); + await queryRunner.query( + `DROP INDEX "public"."IDX_6bbafa28411e6046421991ea21"` + ); + await queryRunner.query(`DROP TABLE "blacklist"`); + } +} diff --git a/server/migration/postgres/1734786596045-AddTelegramMessageThreadId.ts b/server/migration/postgres/1734786596045-AddTelegramMessageThreadId.ts new file mode 100644 index 00000000..a1b89b12 --- /dev/null +++ b/server/migration/postgres/1734786596045-AddTelegramMessageThreadId.ts @@ -0,0 +1,19 @@ +import type { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddTelegramMessageThreadId1734786596045 + implements MigrationInterface +{ + name = 'AddTelegramMessageThreadId1734786596045'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "user_settings" ADD "telegramMessageThreadId" character varying` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "user_settings" DROP COLUMN "telegramMessageThreadId"` + ); + } +} diff --git a/server/migration/postgres/1734805738349-AddOverrideRules.ts b/server/migration/postgres/1734805738349-AddOverrideRules.ts new file mode 100644 index 00000000..b9cc4721 --- /dev/null +++ b/server/migration/postgres/1734805738349-AddOverrideRules.ts @@ -0,0 +1,15 @@ +import type { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddOverrideRules1734805738349 implements MigrationInterface { + name = 'AddOverrideRules1734805738349'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "override_rule" ("id" SERIAL NOT NULL, "radarrServiceId" integer, "sonarrServiceId" integer, "users" character varying, "genre" character varying, "language" character varying, "keywords" character varying, "profileId" integer, "rootFolder" character varying, "tags" character varying, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_657f810c7b20a4fce45aee8f182" PRIMARY KEY ("id"))` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "override_rule"`); + } +} diff --git a/server/migration/postgres/1734809898562-FixNullFields.ts b/server/migration/postgres/1734809898562-FixNullFields.ts new file mode 100644 index 00000000..b36cbac9 --- /dev/null +++ b/server/migration/postgres/1734809898562-FixNullFields.ts @@ -0,0 +1,65 @@ +import type { MigrationInterface, QueryRunner } from 'typeorm'; + +export class FixNullFields1734809898562 implements MigrationInterface { + name = 'FixNullFields1734809898562'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "watchlist" DROP CONSTRAINT "FK_6641da8d831b93dfcb429f8b8bc"` + ); + await queryRunner.query( + `ALTER TABLE "watchlist" ALTER COLUMN "mediaId" DROP NOT NULL` + ); + await queryRunner.query( + `ALTER TABLE "media_request" DROP CONSTRAINT "FK_a1aa713f41c99e9d10c48da75a0"` + ); + await queryRunner.query( + `ALTER TABLE "media_request" ALTER COLUMN "mediaId" DROP NOT NULL` + ); + await queryRunner.query( + `ALTER TABLE "season" DROP CONSTRAINT "FK_087099b39600be695591da9a49c"` + ); + await queryRunner.query( + `ALTER TABLE "season" ALTER COLUMN "mediaId" DROP NOT NULL` + ); + await queryRunner.query( + `ALTER TABLE "watchlist" ADD CONSTRAINT "FK_6641da8d831b93dfcb429f8b8bc" FOREIGN KEY ("mediaId") REFERENCES "media"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE "media_request" ADD CONSTRAINT "FK_a1aa713f41c99e9d10c48da75a0" FOREIGN KEY ("mediaId") REFERENCES "media"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE "season" ADD CONSTRAINT "FK_087099b39600be695591da9a49c" FOREIGN KEY ("mediaId") REFERENCES "media"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "season" DROP CONSTRAINT "FK_087099b39600be695591da9a49c"` + ); + await queryRunner.query( + `ALTER TABLE "media_request" DROP CONSTRAINT "FK_a1aa713f41c99e9d10c48da75a0"` + ); + await queryRunner.query( + `ALTER TABLE "watchlist" DROP CONSTRAINT "FK_6641da8d831b93dfcb429f8b8bc"` + ); + await queryRunner.query( + `ALTER TABLE "season" ALTER COLUMN "mediaId" SET NOT NULL` + ); + await queryRunner.query( + `ALTER TABLE "season" ADD CONSTRAINT "FK_087099b39600be695591da9a49c" FOREIGN KEY ("mediaId") REFERENCES "media"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE "media_request" ALTER COLUMN "mediaId" SET NOT NULL` + ); + await queryRunner.query( + `ALTER TABLE "media_request" ADD CONSTRAINT "FK_a1aa713f41c99e9d10c48da75a0" FOREIGN KEY ("mediaId") REFERENCES "media"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE "watchlist" ALTER COLUMN "mediaId" SET NOT NULL` + ); + await queryRunner.query( + `ALTER TABLE "watchlist" ADD CONSTRAINT "FK_6641da8d831b93dfcb429f8b8bc" FOREIGN KEY ("mediaId") REFERENCES "media"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ); + } +} diff --git a/server/migration/1603944374840-InitialMigration.ts b/server/migration/sqlite/1603944374840-InitialMigration.ts similarity index 100% rename from server/migration/1603944374840-InitialMigration.ts rename to server/migration/sqlite/1603944374840-InitialMigration.ts diff --git a/server/migration/1605085519544-SeasonStatus.ts b/server/migration/sqlite/1605085519544-SeasonStatus.ts similarity index 100% rename from server/migration/1605085519544-SeasonStatus.ts rename to server/migration/sqlite/1605085519544-SeasonStatus.ts diff --git a/server/migration/1606730060700-CascadeMigration.ts b/server/migration/sqlite/1606730060700-CascadeMigration.ts similarity index 100% rename from server/migration/1606730060700-CascadeMigration.ts rename to server/migration/sqlite/1606730060700-CascadeMigration.ts diff --git a/server/migration/1607928251245-DropImdbIdConstraint.ts b/server/migration/sqlite/1607928251245-DropImdbIdConstraint.ts similarity index 100% rename from server/migration/1607928251245-DropImdbIdConstraint.ts rename to server/migration/sqlite/1607928251245-DropImdbIdConstraint.ts diff --git a/server/migration/1608217312474-AddUserRequestDeleteCascades.ts b/server/migration/sqlite/1608217312474-AddUserRequestDeleteCascades.ts similarity index 100% rename from server/migration/1608217312474-AddUserRequestDeleteCascades.ts rename to server/migration/sqlite/1608217312474-AddUserRequestDeleteCascades.ts diff --git a/server/migration/1608477467935-AddLastSeasonChangeMedia.ts b/server/migration/sqlite/1608477467935-AddLastSeasonChangeMedia.ts similarity index 100% rename from server/migration/1608477467935-AddLastSeasonChangeMedia.ts rename to server/migration/sqlite/1608477467935-AddLastSeasonChangeMedia.ts diff --git a/server/migration/1608477467936-ForceDropImdbUniqueConstraint.ts b/server/migration/sqlite/1608477467936-ForceDropImdbUniqueConstraint.ts similarity index 100% rename from server/migration/1608477467936-ForceDropImdbUniqueConstraint.ts rename to server/migration/sqlite/1608477467936-ForceDropImdbUniqueConstraint.ts diff --git a/server/migration/1609236552057-RemoveTmdbIdUniqueConstraint.ts b/server/migration/sqlite/1609236552057-RemoveTmdbIdUniqueConstraint.ts similarity index 100% rename from server/migration/1609236552057-RemoveTmdbIdUniqueConstraint.ts rename to server/migration/sqlite/1609236552057-RemoveTmdbIdUniqueConstraint.ts diff --git a/server/migration/1610070934506-LocalUsers.ts b/server/migration/sqlite/1610070934506-LocalUsers.ts similarity index 100% rename from server/migration/1610070934506-LocalUsers.ts rename to server/migration/sqlite/1610070934506-LocalUsers.ts diff --git a/server/migration/1610370640747-Add4kStatusFields.ts b/server/migration/sqlite/1610370640747-Add4kStatusFields.ts similarity index 100% rename from server/migration/1610370640747-Add4kStatusFields.ts rename to server/migration/sqlite/1610370640747-Add4kStatusFields.ts diff --git a/server/migration/1610522845513-AddMediaAddedFieldToMedia.ts b/server/migration/sqlite/1610522845513-AddMediaAddedFieldToMedia.ts similarity index 100% rename from server/migration/1610522845513-AddMediaAddedFieldToMedia.ts rename to server/migration/sqlite/1610522845513-AddMediaAddedFieldToMedia.ts diff --git a/server/migration/1611508672722-AddDisplayNameToUser.ts b/server/migration/sqlite/1611508672722-AddDisplayNameToUser.ts similarity index 100% rename from server/migration/1611508672722-AddDisplayNameToUser.ts rename to server/migration/sqlite/1611508672722-AddDisplayNameToUser.ts diff --git a/server/migration/1611757511674-SonarrRadarrSyncServiceFields.ts b/server/migration/sqlite/1611757511674-SonarrRadarrSyncServiceFields.ts similarity index 100% rename from server/migration/1611757511674-SonarrRadarrSyncServiceFields.ts rename to server/migration/sqlite/1611757511674-SonarrRadarrSyncServiceFields.ts diff --git a/server/migration/1611801511397-AddRatingKeysToMedia.ts b/server/migration/sqlite/1611801511397-AddRatingKeysToMedia.ts similarity index 100% rename from server/migration/1611801511397-AddRatingKeysToMedia.ts rename to server/migration/sqlite/1611801511397-AddRatingKeysToMedia.ts diff --git a/server/migration/1612482778137-AddResetPasswordGuidAndExpiryDate.ts b/server/migration/sqlite/1612482778137-AddResetPasswordGuidAndExpiryDate.ts similarity index 100% rename from server/migration/1612482778137-AddResetPasswordGuidAndExpiryDate.ts rename to server/migration/sqlite/1612482778137-AddResetPasswordGuidAndExpiryDate.ts diff --git a/server/migration/1612571545781-AddLanguageProfileId.ts b/server/migration/sqlite/1612571545781-AddLanguageProfileId.ts similarity index 100% rename from server/migration/1612571545781-AddLanguageProfileId.ts rename to server/migration/sqlite/1612571545781-AddLanguageProfileId.ts diff --git a/server/migration/1613379909641-AddJellyfinUserParams.ts b/server/migration/sqlite/1613379909641-AddJellyfinUserParams.ts similarity index 100% rename from server/migration/1613379909641-AddJellyfinUserParams.ts rename to server/migration/sqlite/1613379909641-AddJellyfinUserParams.ts diff --git a/server/migration/1613412948344-ServerTypeEnum.ts b/server/migration/sqlite/1613412948344-ServerTypeEnum.ts similarity index 100% rename from server/migration/1613412948344-ServerTypeEnum.ts rename to server/migration/sqlite/1613412948344-ServerTypeEnum.ts diff --git a/server/migration/1613615266968-CreateUserSettings.ts b/server/migration/sqlite/1613615266968-CreateUserSettings.ts similarity index 100% rename from server/migration/1613615266968-CreateUserSettings.ts rename to server/migration/sqlite/1613615266968-CreateUserSettings.ts diff --git a/server/migration/1613670041760-AddJellyfinDeviceId.ts b/server/migration/sqlite/1613670041760-AddJellyfinDeviceId.ts similarity index 100% rename from server/migration/1613670041760-AddJellyfinDeviceId.ts rename to server/migration/sqlite/1613670041760-AddJellyfinDeviceId.ts diff --git a/server/migration/1613955393450-UpdateUserSettingsRegions.ts b/server/migration/sqlite/1613955393450-UpdateUserSettingsRegions.ts similarity index 100% rename from server/migration/1613955393450-UpdateUserSettingsRegions.ts rename to server/migration/sqlite/1613955393450-UpdateUserSettingsRegions.ts diff --git a/server/migration/1614334195680-AddTelegramSettingsToUserSettings.ts b/server/migration/sqlite/1614334195680-AddTelegramSettingsToUserSettings.ts similarity index 100% rename from server/migration/1614334195680-AddTelegramSettingsToUserSettings.ts rename to server/migration/sqlite/1614334195680-AddTelegramSettingsToUserSettings.ts diff --git a/server/migration/1615333940450-AddPGPToUserSettings.ts b/server/migration/sqlite/1615333940450-AddPGPToUserSettings.ts similarity index 100% rename from server/migration/1615333940450-AddPGPToUserSettings.ts rename to server/migration/sqlite/1615333940450-AddPGPToUserSettings.ts diff --git a/server/migration/1616576677254-AddUserQuotaFields.ts b/server/migration/sqlite/1616576677254-AddUserQuotaFields.ts similarity index 100% rename from server/migration/1616576677254-AddUserQuotaFields.ts rename to server/migration/sqlite/1616576677254-AddUserQuotaFields.ts diff --git a/server/migration/1617624225464-CreateTagsFieldonMediaRequest.ts b/server/migration/sqlite/1617624225464-CreateTagsFieldonMediaRequest.ts similarity index 100% rename from server/migration/1617624225464-CreateTagsFieldonMediaRequest.ts rename to server/migration/sqlite/1617624225464-CreateTagsFieldonMediaRequest.ts diff --git a/server/migration/1617730837489-AddUserSettingsNotificationAgentsField.ts b/server/migration/sqlite/1617730837489-AddUserSettingsNotificationAgentsField.ts similarity index 100% rename from server/migration/1617730837489-AddUserSettingsNotificationAgentsField.ts rename to server/migration/sqlite/1617730837489-AddUserSettingsNotificationAgentsField.ts diff --git a/server/migration/1618912653565-CreateUserPushSubscriptions.ts b/server/migration/sqlite/1618912653565-CreateUserPushSubscriptions.ts similarity index 100% rename from server/migration/1618912653565-CreateUserPushSubscriptions.ts rename to server/migration/sqlite/1618912653565-CreateUserPushSubscriptions.ts diff --git a/server/migration/1619239659754-AddUserSettingsLocale.ts b/server/migration/sqlite/1619239659754-AddUserSettingsLocale.ts similarity index 100% rename from server/migration/1619239659754-AddUserSettingsLocale.ts rename to server/migration/sqlite/1619239659754-AddUserSettingsLocale.ts diff --git a/server/migration/1619339817343-AddUserSettingsNotificationTypes.ts b/server/migration/sqlite/1619339817343-AddUserSettingsNotificationTypes.ts similarity index 100% rename from server/migration/1619339817343-AddUserSettingsNotificationTypes.ts rename to server/migration/sqlite/1619339817343-AddUserSettingsNotificationTypes.ts diff --git a/server/migration/1634904083966-AddIssues.ts b/server/migration/sqlite/1634904083966-AddIssues.ts similarity index 100% rename from server/migration/1634904083966-AddIssues.ts rename to server/migration/sqlite/1634904083966-AddIssues.ts diff --git a/server/migration/1635079863457-AddPushbulletPushoverUserSettings.ts b/server/migration/sqlite/1635079863457-AddPushbulletPushoverUserSettings.ts similarity index 100% rename from server/migration/1635079863457-AddPushbulletPushoverUserSettings.ts rename to server/migration/sqlite/1635079863457-AddPushbulletPushoverUserSettings.ts diff --git a/server/migration/1660632269368-AddWatchlistSyncUserSetting.ts b/server/migration/sqlite/1660632269368-AddWatchlistSyncUserSetting.ts similarity index 100% rename from server/migration/1660632269368-AddWatchlistSyncUserSetting.ts rename to server/migration/sqlite/1660632269368-AddWatchlistSyncUserSetting.ts diff --git a/server/migration/1660714479373-AddMediaRequestIsAutoRequestedField.ts b/server/migration/sqlite/1660714479373-AddMediaRequestIsAutoRequestedField.ts similarity index 100% rename from server/migration/1660714479373-AddMediaRequestIsAutoRequestedField.ts rename to server/migration/sqlite/1660714479373-AddMediaRequestIsAutoRequestedField.ts diff --git a/server/migration/1672041273674-AddDiscoverSlider.ts b/server/migration/sqlite/1672041273674-AddDiscoverSlider.ts similarity index 100% rename from server/migration/1672041273674-AddDiscoverSlider.ts rename to server/migration/sqlite/1672041273674-AddDiscoverSlider.ts diff --git a/server/migration/1682608634546-AddWatchlists.ts b/server/migration/sqlite/1682608634546-AddWatchlists.ts similarity index 100% rename from server/migration/1682608634546-AddWatchlists.ts rename to server/migration/sqlite/1682608634546-AddWatchlists.ts diff --git a/server/migration/1697393491630-AddUserPushoverSound.ts b/server/migration/sqlite/1697393491630-AddUserPushoverSound.ts similarity index 100% rename from server/migration/1697393491630-AddUserPushoverSound.ts rename to server/migration/sqlite/1697393491630-AddUserPushoverSound.ts diff --git a/server/migration/sqlite/1699901142442-AddBlacklist.ts b/server/migration/sqlite/1699901142442-AddBlacklist.ts new file mode 100644 index 00000000..eb096270 --- /dev/null +++ b/server/migration/sqlite/1699901142442-AddBlacklist.ts @@ -0,0 +1,20 @@ +import type { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddBlacklist1699901142442 implements MigrationInterface { + name = 'AddBlacklist1699901142442'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "blacklist" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "title" varchar, "tmdbId" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')),"userId" integer, "mediaId" integer,CONSTRAINT "UQ_6bbafa28411e6046421991ea21c" UNIQUE ("tmdbId", "userId"))` + ); + + await queryRunner.query( + `CREATE INDEX "IDX_6bbafa28411e6046421991ea21" ON "blacklist" ("tmdbId") ` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "blacklist"`); + await queryRunner.query(`DROP INDEX "IDX_6bbafa28411e6046421991ea21"`); + } +} diff --git a/server/migration/sqlite/1727907530757-AddUserSettingsStreamingRegion.ts b/server/migration/sqlite/1727907530757-AddUserSettingsStreamingRegion.ts new file mode 100644 index 00000000..bd7a183b --- /dev/null +++ b/server/migration/sqlite/1727907530757-AddUserSettingsStreamingRegion.ts @@ -0,0 +1,53 @@ +import type { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddUserSettingsStreamingRegion1727907530757 + implements MigrationInterface +{ + name = 'AddUserSettingsStreamingRegion1727907530757'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "temporary_user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "notificationTypes" text, "discordId" varchar, "userId" integer, "originalLanguage" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "pgpKey" varchar, "locale" varchar NOT NULL DEFAULT (''), "pushbulletAccessToken" varchar, "pushoverApplicationToken" varchar, "pushoverUserKey" varchar, "watchlistSyncMovies" boolean, "watchlistSyncTv" boolean, "pushoverSound" varchar, CONSTRAINT "UQ_986a2b6d3c05eb4091bb8066f78" 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", "notificationTypes", "discordId", "userId", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "watchlistSyncMovies", "watchlistSyncTv", "pushoverSound") SELECT "id", "notificationTypes", "discordId", "userId", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "watchlistSyncMovies", "watchlistSyncTv", "pushoverSound" FROM "user_settings"` + ); + await queryRunner.query(`DROP TABLE "user_settings"`); + await queryRunner.query( + `ALTER TABLE "temporary_user_settings" RENAME TO "user_settings"` + ); + await queryRunner.query( + `CREATE TABLE "temporary_user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "notificationTypes" text, "discordId" varchar, "userId" integer, "originalLanguage" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "pgpKey" varchar, "locale" varchar NOT NULL DEFAULT (''), "pushbulletAccessToken" varchar, "pushoverApplicationToken" varchar, "pushoverUserKey" varchar, "watchlistSyncMovies" boolean, "watchlistSyncTv" boolean, "pushoverSound" varchar, "discoverRegion" varchar, "streamingRegion" varchar, CONSTRAINT "UQ_986a2b6d3c05eb4091bb8066f78" 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", "notificationTypes", "discordId", "userId", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "watchlistSyncMovies", "watchlistSyncTv", "pushoverSound") SELECT "id", "notificationTypes", "discordId", "userId", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "watchlistSyncMovies", "watchlistSyncTv", "pushoverSound" 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, "notificationTypes" text, "discordId" varchar, "userId" integer, "originalLanguage" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "pgpKey" varchar, "locale" varchar NOT NULL DEFAULT (''), "pushbulletAccessToken" varchar, "pushoverApplicationToken" varchar, "pushoverUserKey" varchar, "watchlistSyncMovies" boolean, "watchlistSyncTv" boolean, "pushoverSound" varchar, CONSTRAINT "UQ_986a2b6d3c05eb4091bb8066f78" 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", "notificationTypes", "discordId", "userId", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "watchlistSyncMovies", "watchlistSyncTv", "pushoverSound") SELECT "id", "notificationTypes", "discordId", "userId", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "watchlistSyncMovies", "watchlistSyncTv", "pushoverSound" FROM "temporary_user_settings"` + ); + await queryRunner.query(`DROP TABLE "temporary_user_settings"`); + 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, "notificationTypes" text, "discordId" varchar, "userId" integer, "originalLanguage" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "pgpKey" varchar, "locale" varchar NOT NULL DEFAULT (''), "pushbulletAccessToken" varchar, "pushoverApplicationToken" varchar, "pushoverUserKey" varchar, "watchlistSyncMovies" boolean, "watchlistSyncTv" boolean, "pushoverSound" varchar, "region" varchar, CONSTRAINT "UQ_986a2b6d3c05eb4091bb8066f78" 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", "notificationTypes", "discordId", "userId", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "watchlistSyncMovies", "watchlistSyncTv", "pushoverSound") SELECT "id", "notificationTypes", "discordId", "userId", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "watchlistSyncMovies", "watchlistSyncTv", "pushoverSound" FROM "temporary_user_settings"` + ); + await queryRunner.query(`DROP TABLE "temporary_user_settings"`); + } +} diff --git a/server/migration/sqlite/1734287582736-AddTelegramMessageThreadId.ts b/server/migration/sqlite/1734287582736-AddTelegramMessageThreadId.ts new file mode 100644 index 00000000..94a76b99 --- /dev/null +++ b/server/migration/sqlite/1734287582736-AddTelegramMessageThreadId.ts @@ -0,0 +1,33 @@ +import type { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddTelegramMessageThreadId1734287582736 + implements MigrationInterface +{ + name = 'AddTelegramMessageThreadId1734287582736'; + + 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, "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 "temporary_user_settings"("id", "locale", "discoverRegion", "streamingRegion", "originalLanguage", "pgpKey", "discordId", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "pushoverSound", "telegramChatId", "telegramSendSilently", "watchlistSyncMovies", "watchlistSyncTv", "notificationTypes", "userId") SELECT "id", "locale", "discoverRegion", "streamingRegion", "originalLanguage", "pgpKey", "discordId", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "pushoverSound", "telegramChatId", "telegramSendSilently", "watchlistSyncMovies", "watchlistSyncTv", "notificationTypes", "userId" 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, 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") SELECT "id", "locale", "discoverRegion", "streamingRegion", "originalLanguage", "pgpKey", "discordId", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "pushoverSound", "telegramChatId", "telegramSendSilently", "watchlistSyncMovies", "watchlistSyncTv", "notificationTypes", "userId" FROM "temporary_user_settings"` + ); + await queryRunner.query(`DROP TABLE "temporary_user_settings"`); + } +} diff --git a/server/migration/sqlite/1734805733535-AddOverrideRules.ts b/server/migration/sqlite/1734805733535-AddOverrideRules.ts new file mode 100644 index 00000000..692dc875 --- /dev/null +++ b/server/migration/sqlite/1734805733535-AddOverrideRules.ts @@ -0,0 +1,15 @@ +import type { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddOverrideRules1734805733535 implements MigrationInterface { + name = 'AddOverrideRules1734805733535'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "override_rule" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "radarrServiceId" integer, "sonarrServiceId" integer, "users" varchar, "genre" varchar, "language" varchar, "keywords" varchar, "profileId" integer, "rootFolder" varchar, "tags" varchar, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')))` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "override_rule"`); + } +} diff --git a/server/routes/auth.ts b/server/routes/auth.ts index cd931c25..cbfbc3f7 100644 --- a/server/routes/auth.ts +++ b/server/routes/auth.ts @@ -14,7 +14,6 @@ import { ApiError } from '@server/types/error'; import { getHostname } from '@server/utils/getHostname'; import * as EmailValidator from 'email-validator'; import { Router } from 'express'; -import gravatarUrl from 'gravatar-url'; import net from 'net'; const authRoutes = Router(); @@ -88,7 +87,7 @@ authRoutes.post('/plex', async (req, res, next) => { }); settings.main.mediaServerType = MediaServerType.PLEX; - settings.save(); + await settings.save(); startJobs(); await userRepository.save(user); @@ -261,11 +260,10 @@ authRoutes.post('/jellyfin', async (req, res, next) => { urlBase: body.urlBase, }); - const { externalHostname } = getSettings().jellyfin; - // Try to find deviceId that corresponds to jellyfin user, else generate a new one let user = await userRepository.findOne({ where: { jellyfinUsername: body.username }, + select: { id: true, jellyfinDeviceId: true }, }); let deviceId = ''; @@ -280,11 +278,6 @@ authRoutes.post('/jellyfin', async (req, res, next) => { // First we need to attempt to log the user in to jellyfin const jellyfinserver = new JellyfinAPI(hostname ?? '', undefined, deviceId); - const jellyfinHost = - externalHostname && externalHostname.length > 0 - ? externalHostname - : hostname; - const ip = req.ip; let clientIp; @@ -307,62 +300,84 @@ authRoutes.post('/jellyfin', async (req, res, next) => { where: { jellyfinUserId: account.User.Id }, }); - if (!user && !(await userRepository.count())) { + const missingAdminUser = !user && !(await userRepository.count()); + if ( + missingAdminUser || + settings.main.mediaServerType === MediaServerType.NOT_CONFIGURED + ) { // Check if user is admin on jellyfin if (account.User.Policy.IsAdministrator === false) { throw new ApiError(403, ApiErrorCode.NotAdmin); } - logger.info( - 'Sign-in attempt from Jellyfin user with access to the media server; creating initial admin user for Overseerr', - { - label: 'API', - ip: req.ip, - jellyfinUsername: account.User.Name, - } - ); + if ( + body.serverType !== MediaServerType.JELLYFIN && + body.serverType !== MediaServerType.EMBY + ) { + throw new ApiError(500, ApiErrorCode.NoAdminUser); + } + settings.main.mediaServerType = body.serverType; - // User doesn't exist, and there are no users in the database, we'll create the user - // with admin permissions - switch (body.serverType) { - case MediaServerType.EMBY: - settings.main.mediaServerType = MediaServerType.EMBY; - user = new User({ - email: body.email || account.User.Name, + if (missingAdminUser) { + logger.info( + 'Sign-in attempt from Jellyfin user with access to the media server; creating initial admin user for Jellyseerr', + { + label: 'API', + ip: req.ip, jellyfinUsername: account.User.Name, - jellyfinUserId: account.User.Id, - jellyfinDeviceId: deviceId, - jellyfinAuthToken: account.AccessToken, - permissions: Permission.ADMIN, - avatar: account.User.PrimaryImageTag - ? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90` - : gravatarUrl(body.email || account.User.Name, { - default: 'mm', - size: 200, - }), - userType: UserType.EMBY, - }); - break; - case MediaServerType.JELLYFIN: - settings.main.mediaServerType = MediaServerType.JELLYFIN; - user = new User({ - email: body.email || account.User.Name, + } + ); + + // User doesn't exist, and there are no users in the database, we'll create the user + // with admin permissions + + user = new User({ + id: 1, + email: body.email || account.User.Name, + jellyfinUsername: account.User.Name, + jellyfinUserId: account.User.Id, + jellyfinDeviceId: deviceId, + jellyfinAuthToken: account.AccessToken, + permissions: Permission.ADMIN, + avatar: `/avatarproxy/${account.User.Id}`, + userType: + body.serverType === MediaServerType.JELLYFIN + ? UserType.JELLYFIN + : UserType.EMBY, + }); + + await userRepository.save(user); + } else { + logger.info( + 'Sign-in attempt from Jellyfin user with access to the media server; editing admin user for Jellyseerr', + { + label: 'API', + ip: req.ip, jellyfinUsername: account.User.Name, - jellyfinUserId: account.User.Id, - jellyfinDeviceId: deviceId, - jellyfinAuthToken: account.AccessToken, - permissions: Permission.ADMIN, - avatar: account.User.PrimaryImageTag - ? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90` - : gravatarUrl(body.email || account.User.Name, { - default: 'mm', - size: 200, - }), - userType: UserType.JELLYFIN, - }); - break; - default: - throw new Error('select_server_type'); + } + ); + + // User alread exist but settings.json is not configured, we'll edit the admin user + + user = await userRepository.findOne({ + where: { id: 1 }, + }); + if (!user) { + throw new Error('Unable to find admin user to edit'); + } + user.email = body.email || account.User.Name; + user.jellyfinUsername = account.User.Name; + user.jellyfinUserId = account.User.Id; + user.jellyfinDeviceId = deviceId; + user.jellyfinAuthToken = account.AccessToken; + user.permissions = Permission.ADMIN; + user.avatar = `/avatarproxy/${account.User.Id}`; + user.userType = + body.serverType === MediaServerType.JELLYFIN + ? UserType.JELLYFIN + : UserType.EMBY; + + await userRepository.save(user); } // Create an API key on Jellyfin from this admin user @@ -382,10 +397,8 @@ authRoutes.post('/jellyfin', async (req, res, next) => { settings.jellyfin.urlBase = body.urlBase ?? ''; settings.jellyfin.useSsl = body.useSsl ?? false; settings.jellyfin.apiKey = apiKey; - settings.save(); + await settings.save(); startJobs(); - - await userRepository.save(user); } // User already exists, let's update their information else if (account.User.Id === user?.jellyfinUserId) { @@ -405,15 +418,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => { jellyfinUsername: account.User.Name, } ); - // Update the users avatar with their jellyfin profile pic (incase it changed) - if (account.User.PrimaryImageTag) { - user.avatar = `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`; - } else { - user.avatar = gravatarUrl(user.email || account.User.Name, { - default: 'mm', - size: 200, - }); - } + user.avatar = `/avatarproxy/${account.User.Id}`; user.jellyfinUsername = account.User.Name; if (user.username === account.User.Name) { @@ -451,17 +456,13 @@ authRoutes.post('/jellyfin', async (req, res, next) => { jellyfinUserId: account.User.Id, jellyfinDeviceId: deviceId, permissions: settings.main.defaultPermissions, - avatar: account.User.PrimaryImageTag - ? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90` - : gravatarUrl(body.email || account.User.Name, { - default: 'mm', - size: 200, - }), + avatar: `/avatarproxy/${account.User.Id}`, userType: settings.main.mediaServerType === MediaServerType.JELLYFIN ? UserType.JELLYFIN : UserType.EMBY, }); + //initialize Jellyfin/Emby users with local login const passedExplicitPassword = body.password && body.password.length > 0; if (passedExplicitPassword) { @@ -533,6 +534,22 @@ authRoutes.post('/jellyfin', async (req, res, next) => { message: e.errorCode, }); + case ApiErrorCode.NoAdminUser: + logger.warn( + 'Failed login attempt from user without admin permissions and no admin user exists', + { + label: 'Auth', + account: { + ip: req.ip, + email: body.username, + }, + } + ); + return next({ + status: e.statusCode, + message: e.errorCode, + }); + default: logger.error(e.message, { label: 'Auth' }); return next({ diff --git a/server/routes/avatarproxy.ts b/server/routes/avatarproxy.ts new file mode 100644 index 00000000..5938fa94 --- /dev/null +++ b/server/routes/avatarproxy.ts @@ -0,0 +1,92 @@ +import { MediaServerType } from '@server/constants/server'; +import { getRepository } from '@server/datasource'; +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 { Router } from 'express'; +import gravatarUrl from 'gravatar-url'; + +const router = Router(); + +let _avatarImageProxy: ImageProxy | null = null; +async 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; + const authToken = getSettings().jellyfin.apiKey; + _avatarImageProxy = new ImageProxy('avatar', '', { + headers: { + 'X-Emby-Authorization': `MediaBrowser Client="Jellyseerr", Device="Jellyseerr", DeviceId="${deviceId}", Version="${getAppVersion()}", Token="${authToken}"`, + }, + }); + } + return _avatarImageProxy; +} + +router.get('/:jellyfinUserId', async (req, res) => { + 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 user = await getRepository(User).findOne({ + where: { jellyfinUserId: req.params.jellyfinUserId }, + }); + + const fallbackUrl = gravatarUrl(user?.email || 'none', { + default: 'mm', + size: 200, + }); + + const setttings = getSettings(); + const jellyfinAvatarUrl = + setttings.main.mediaServerType === MediaServerType.JELLYFIN + ? `${getHostname()}/UserImage?UserId=${req.params.jellyfinUserId}` + : `${getHostname()}/Users/${ + req.params.jellyfinUserId + }/Images/Primary?quality=90`; + + let imageData = await avatarImageCache.getImage( + jellyfinAvatarUrl, + fallbackUrl + ); + + if (imageData.meta.extension === 'json') { + // this is a 404 + imageData = await avatarImageCache.getImage(fallbackUrl); + } + + res.writeHead(200, { + 'Content-Type': `image/${imageData.meta.extension}`, + 'Content-Length': imageData.imageBuffer.length, + 'Cache-Control': `public, max-age=${imageData.meta.curRevalidate}`, + 'OS-Cache-Key': imageData.meta.cacheKey, + 'OS-Cache-Status': imageData.meta.cacheMiss ? 'MISS' : 'HIT', + }); + + res.end(imageData.imageBuffer); + } catch (e) { + logger.error('Failed to proxy avatar image', { + errorMessage: e.message, + }); + } +}); + +export default router; diff --git a/server/routes/blacklist.ts b/server/routes/blacklist.ts new file mode 100644 index 00000000..bb2dafe8 --- /dev/null +++ b/server/routes/blacklist.ts @@ -0,0 +1,171 @@ +import { MediaType } from '@server/constants/media'; +import { getRepository } from '@server/datasource'; +import { Blacklist } from '@server/entity/Blacklist'; +import Media from '@server/entity/Media'; +import type { BlacklistResultsResponse } from '@server/interfaces/api/blacklistInterfaces'; +import { Permission } from '@server/lib/permissions'; +import logger from '@server/logger'; +import { isAuthenticated } from '@server/middleware/auth'; +import { Router } from 'express'; +import { EntityNotFoundError, QueryFailedError } from 'typeorm'; +import { z } from 'zod'; + +const blacklistRoutes = Router(); + +export const blacklistAdd = z.object({ + tmdbId: z.coerce.number(), + mediaType: z.nativeEnum(MediaType), + title: z.coerce.string().optional(), + user: z.coerce.number(), +}); + +blacklistRoutes.get( + '/', + isAuthenticated([Permission.MANAGE_BLACKLIST, Permission.VIEW_BLACKLIST], { + type: 'or', + }), + async (req, res, next) => { + const pageSize = req.query.take ? Number(req.query.take) : 25; + const skip = req.query.skip ? Number(req.query.skip) : 0; + const search = (req.query.search as string) ?? ''; + + try { + let query = getRepository(Blacklist) + .createQueryBuilder('blacklist') + .leftJoinAndSelect('blacklist.user', 'user'); + + if (search.length > 0) { + query = query.where('blacklist.title like :title', { + title: `%${search}%`, + }); + } + + const [blacklistedItems, itemsCount] = await query + .orderBy('blacklist.createdAt', 'DESC') + .take(pageSize) + .skip(skip) + .getManyAndCount(); + + return res.status(200).json({ + pageInfo: { + pages: Math.ceil(itemsCount / pageSize), + pageSize, + results: itemsCount, + page: Math.ceil(skip / pageSize) + 1, + }, + results: blacklistedItems, + } as BlacklistResultsResponse); + } catch (error) { + logger.error('Something went wrong while retrieving blacklisted items', { + label: 'Blacklist', + errorMessage: error.message, + }); + return next({ + status: 500, + message: 'Unable to retrieve blacklisted items.', + }); + } + } +); + +blacklistRoutes.get( + '/:id', + isAuthenticated([Permission.MANAGE_BLACKLIST], { + type: 'or', + }), + async (req, res, next) => { + try { + const blacklisteRepository = getRepository(Blacklist); + + const blacklistItem = await blacklisteRepository.findOneOrFail({ + where: { tmdbId: Number(req.params.id) }, + }); + + return res.status(200).send(blacklistItem); + } catch (e) { + if (e instanceof EntityNotFoundError) { + return next({ + status: 401, + message: e.message, + }); + } + return next({ status: 500, message: e.message }); + } + } +); + +blacklistRoutes.post( + '/', + isAuthenticated([Permission.MANAGE_BLACKLIST], { + type: 'or', + }), + async (req, res, next) => { + try { + const values = blacklistAdd.parse(req.body); + + await Blacklist.addToBlacklist({ + blacklistRequest: values, + }); + + return res.status(201).send(); + } catch (error) { + if (!(error instanceof Error)) { + return; + } + + if (error instanceof QueryFailedError) { + switch (error.driverError.errno) { + case 19: + return next({ status: 412, message: 'Item already blacklisted' }); + default: + logger.warn('Something wrong with data blacklist', { + tmdbId: req.body.tmdbId, + mediaType: req.body.mediaType, + label: 'Blacklist', + }); + return next({ status: 409, message: 'Something wrong' }); + } + } + + return next({ status: 500, message: error.message }); + } + } +); + +blacklistRoutes.delete( + '/:id', + isAuthenticated([Permission.MANAGE_BLACKLIST], { + type: 'or', + }), + async (req, res, next) => { + try { + const blacklisteRepository = getRepository(Blacklist); + + const blacklistItem = await blacklisteRepository.findOneOrFail({ + where: { tmdbId: Number(req.params.id) }, + }); + + await blacklisteRepository.remove(blacklistItem); + + const mediaRepository = getRepository(Media); + + const mediaItem = await mediaRepository.findOneOrFail({ + where: { tmdbId: Number(req.params.id) }, + }); + + await mediaRepository.remove(mediaItem); + + return res.status(204).send(); + } catch (e) { + if (e instanceof EntityNotFoundError) { + return next({ + status: 401, + message: e.message, + }); + } + return next({ status: 500, message: e.message }); + } + } +); + +export default blacklistRoutes; diff --git a/server/routes/discover.ts b/server/routes/discover.ts index 55a844ad..4bb12740 100644 --- a/server/routes/discover.ts +++ b/server/routes/discover.ts @@ -29,12 +29,12 @@ import { z } from 'zod'; export const createTmdbWithRegionLanguage = (user?: User): TheMovieDb => { const settings = getSettings(); - const region = - user?.settings?.region === 'all' + const discoverRegion = + user?.settings?.streamingRegion === 'all' ? '' - : user?.settings?.region - ? user?.settings?.region - : settings.main.region; + : user?.settings?.streamingRegion + ? user?.settings?.streamingRegion + : settings.main.discoverRegion; const originalLanguage = user?.settings?.originalLanguage === 'all' @@ -44,7 +44,7 @@ export const createTmdbWithRegionLanguage = (user?: User): TheMovieDb => { : settings.main.originalLanguage; return new TheMovieDb({ - region, + discoverRegion, originalLanguage, }); }; @@ -875,6 +875,7 @@ discoverRoutes.get, WatchlistResponse>( totalPages: Math.ceil(watchlist.totalSize / itemsPerPage), totalResults: watchlist.totalSize, results: watchlist.items.map((item) => ({ + id: item.tmdbId, ratingKey: item.ratingKey, title: item.title, mediaType: item.type === 'show' ? 'tv' : 'movie', diff --git a/server/routes/index.ts b/server/routes/index.ts index 12434256..f064e603 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -15,14 +15,20 @@ import { checkUser, isAuthenticated } from '@server/middleware/auth'; import { mapWatchProviderDetails } from '@server/models/common'; import { mapProductionCompany } from '@server/models/Movie'; import { mapNetwork } from '@server/models/Tv'; +import overrideRuleRoutes from '@server/routes/overrideRule'; import settingsRoutes from '@server/routes/settings'; import watchlistRoutes from '@server/routes/watchlist'; -import { appDataPath, appDataStatus } from '@server/utils/appDataVolume'; +import { + appDataPath, + appDataPermissions, + appDataStatus, +} from '@server/utils/appDataVolume'; import { getAppVersion, getCommitTag } from '@server/utils/appVersion'; import restartFlag from '@server/utils/restartFlag'; import { isPerson } from '@server/utils/typeHelpers'; import { Router } from 'express'; import authRoutes from './auth'; +import blacklistRoutes from './blacklist'; import collectionRoutes from './collection'; import discoverRoutes, { createTmdbWithRegionLanguage } from './discover'; import issueRoutes from './issue'; @@ -92,6 +98,7 @@ router.get('/status/appdata', (_req, res) => { return res.status(200).json({ appData: appDataStatus(), appDataPath: appDataPath(), + appDataPermissions: appDataPermissions(), }); }); @@ -144,6 +151,7 @@ router.use('/search', isAuthenticated(), searchRoutes); router.use('/discover', isAuthenticated(), discoverRoutes); router.use('/request', isAuthenticated(), requestRoutes); router.use('/watchlist', isAuthenticated(), watchlistRoutes); +router.use('/blacklist', isAuthenticated(), blacklistRoutes); router.use('/movie', isAuthenticated(), movieRoutes); router.use('/tv', isAuthenticated(), tvRoutes); router.use('/media', isAuthenticated(), mediaRoutes); @@ -153,6 +161,11 @@ 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.get('/regions', isAuthenticated(), async (req, res, next) => { const tmdb = new TheMovieDb(); diff --git a/server/routes/overrideRule.ts b/server/routes/overrideRule.ts new file mode 100644 index 00000000..912a68aa --- /dev/null +++ b/server/routes/overrideRule.ts @@ -0,0 +1,136 @@ +import { getRepository } from '@server/datasource'; +import OverrideRule from '@server/entity/OverrideRule'; +import type { OverrideRuleResultsResponse } from '@server/interfaces/api/overrideRuleInterfaces'; +import { Permission } from '@server/lib/permissions'; +import { isAuthenticated } from '@server/middleware/auth'; +import { Router } from 'express'; + +const overrideRuleRoutes = Router(); + +overrideRuleRoutes.get( + '/', + isAuthenticated(Permission.ADMIN), + async (req, res, next) => { + const overrideRuleRepository = getRepository(OverrideRule); + + try { + const rules = await overrideRuleRepository.find({}); + + return res.status(200).json(rules as OverrideRuleResultsResponse); + } catch (e) { + next({ status: 404, message: e.message }); + } + } +); + +overrideRuleRoutes.post< + Record, + OverrideRule, + { + users?: string; + genre?: string; + language?: string; + keywords?: string; + profileId?: number; + rootFolder?: string; + tags?: string; + radarrServiceId?: number; + sonarrServiceId?: number; + } +>('/', isAuthenticated(Permission.ADMIN), async (req, res, next) => { + const overrideRuleRepository = getRepository(OverrideRule); + + try { + const rule = new OverrideRule({ + users: req.body.users, + genre: req.body.genre, + language: req.body.language, + keywords: req.body.keywords, + profileId: req.body.profileId, + rootFolder: req.body.rootFolder, + tags: req.body.tags, + radarrServiceId: req.body.radarrServiceId, + sonarrServiceId: req.body.sonarrServiceId, + }); + + const newRule = await overrideRuleRepository.save(rule); + + return res.status(200).json(newRule); + } catch (e) { + next({ status: 404, message: e.message }); + } +}); + +overrideRuleRoutes.put< + { ruleId: string }, + OverrideRule, + { + users?: string; + genre?: string; + language?: string; + keywords?: string; + profileId?: number; + rootFolder?: string; + tags?: string; + radarrServiceId?: number; + sonarrServiceId?: number; + } +>('/:ruleId', isAuthenticated(Permission.ADMIN), async (req, res, next) => { + const overrideRuleRepository = getRepository(OverrideRule); + + try { + const rule = await overrideRuleRepository.findOne({ + where: { + id: Number(req.params.ruleId), + }, + }); + + if (!rule) { + return next({ status: 404, message: 'Override Rule not found.' }); + } + + rule.users = req.body.users; + rule.genre = req.body.genre; + rule.language = req.body.language; + rule.keywords = req.body.keywords; + rule.profileId = req.body.profileId; + rule.rootFolder = req.body.rootFolder; + rule.tags = req.body.tags; + rule.radarrServiceId = req.body.radarrServiceId; + rule.sonarrServiceId = req.body.sonarrServiceId; + + const newRule = await overrideRuleRepository.save(rule); + + return res.status(200).json(newRule); + } catch (e) { + next({ status: 404, message: e.message }); + } +}); + +overrideRuleRoutes.delete<{ ruleId: string }, OverrideRule>( + '/:ruleId', + isAuthenticated(Permission.ADMIN), + async (req, res, next) => { + const overrideRuleRepository = getRepository(OverrideRule); + + try { + const rule = await overrideRuleRepository.findOne({ + where: { + id: Number(req.params.ruleId), + }, + }); + + if (!rule) { + return next({ status: 404, message: 'Override Rule not found.' }); + } + + await overrideRuleRepository.remove(rule); + + return res.status(200).json(rule); + } catch (e) { + next({ status: 404, message: e.message }); + } + } +); + +export default overrideRuleRoutes; diff --git a/server/routes/request.ts b/server/routes/request.ts index 94ae8384..89e5352f 100644 --- a/server/routes/request.ts +++ b/server/routes/request.ts @@ -8,6 +8,7 @@ import { import { getRepository } from '@server/datasource'; import Media from '@server/entity/Media'; import { + BlacklistedMediaError, DuplicateMediaRequestError, MediaRequest, NoSeasonsAvailableError, @@ -93,6 +94,7 @@ requestRoutes.get, RequestResultsResponse>( } let sortFilter: string; + let sortDirection: 'ASC' | 'DESC'; switch (req.query.sort) { case 'modified': @@ -102,6 +104,14 @@ requestRoutes.get, RequestResultsResponse>( sortFilter = 'request.id'; } + switch (req.query.sortDirection) { + case 'asc': + sortDirection = 'ASC'; + break; + default: + sortDirection = 'DESC'; + } + let query = getRepository(MediaRequest) .createQueryBuilder('request') .leftJoinAndSelect('request.media', 'media') @@ -112,7 +122,7 @@ requestRoutes.get, RequestResultsResponse>( requestStatus: statusFilter, }) .andWhere( - '((request.is4k = 0 AND media.status IN (:...mediaStatus)) OR (request.is4k = 1 AND media.status4k IN (:...mediaStatus)))', + '((request.is4k = false AND media.status IN (:...mediaStatus)) OR (request.is4k = true AND media.status4k IN (:...mediaStatus)))', { mediaStatus: mediaStatusFilter, } @@ -141,7 +151,7 @@ requestRoutes.get, RequestResultsResponse>( } const [requests, requestCount] = await query - .orderBy(sortFilter, 'DESC') + .orderBy(sortFilter, sortDirection) .take(pageSize) .skip(skip) .getManyAndCount(); @@ -158,7 +168,7 @@ requestRoutes.get, RequestResultsResponse>( return { id: sonarrSetting.id, - profiles: await sonarr.getProfiles(), + profiles: await sonarr.getProfiles().catch(() => undefined), }; }) ); @@ -173,7 +183,7 @@ requestRoutes.get, RequestResultsResponse>( return { id: radarrSetting.id, - profiles: await radarr.getProfiles(), + profiles: await radarr.getProfiles().catch(() => undefined), }; }) ); @@ -184,7 +194,7 @@ requestRoutes.get, RequestResultsResponse>( case MediaType.MOVIE: { const profileName = radarrServers .find((serverr) => serverr.id === r.serverId) - ?.profiles.find((profile) => profile.id === r.profileId)?.name; + ?.profiles?.find((profile) => profile.id === r.profileId)?.name; return { ...r, @@ -196,7 +206,7 @@ requestRoutes.get, RequestResultsResponse>( ...r, profileName: sonarrServers .find((serverr) => serverr.id === r.serverId) - ?.profiles.find((profile) => profile.id === r.profileId)?.name, + ?.profiles?.find((profile) => profile.id === r.profileId)?.name, }; } } @@ -243,6 +253,8 @@ requestRoutes.post( return next({ status: 409, message: error.message }); case NoSeasonsAvailableError: return next({ status: 202, message: error.message }); + case BlacklistedMediaError: + return next({ status: 403, message: error.message }); default: return next({ status: 500, message: error.message }); } diff --git a/server/routes/service.ts b/server/routes/service.ts index 083e1eb5..8f6c92b0 100644 --- a/server/routes/service.ts +++ b/server/routes/service.ts @@ -123,9 +123,13 @@ serviceRoutes.get<{ sonarrId: string }>( }); try { + const systemStatus = await sonarr.getSystemStatus(); + const sonarrMajorVersion = Number(systemStatus.version.split('.')[0]); + const profiles = await sonarr.getProfiles(); const rootFolders = await sonarr.getRootFolders(); - const languageProfiles = await sonarr.getLanguageProfiles(); + const languageProfiles = + sonarrMajorVersion <= 3 ? await sonarr.getLanguageProfiles() : null; const tags = await sonarr.getTags(); return res.status(200).json({ diff --git a/server/routes/settings/index.ts b/server/routes/settings/index.ts index 30898d2a..018f7b46 100644 --- a/server/routes/settings/index.ts +++ b/server/routes/settings/index.ts @@ -32,7 +32,6 @@ import { getHostname } from '@server/utils/getHostname'; import { Router } from 'express'; import rateLimit from 'express-rate-limit'; import fs from 'fs'; -import gravatarUrl from 'gravatar-url'; import { escapeRegExp, merge, omit, set, sortBy } from 'lodash'; import { rescheduleJob } from 'node-schedule'; import path from 'path'; @@ -70,19 +69,34 @@ settingsRoutes.get('/main', (req, res, next) => { res.status(200).json(filteredMainSettings(req.user, settings.main)); }); -settingsRoutes.post('/main', (req, res) => { +settingsRoutes.post('/main', async (req, res) => { const settings = getSettings(); settings.main = merge(settings.main, req.body); - settings.save(); + await settings.save(); return res.status(200).json(settings.main); }); -settingsRoutes.post('/main/regenerate', (req, res, next) => { +settingsRoutes.get('/network', (req, res) => { const settings = getSettings(); - const main = settings.regenerateApiKey(); + res.status(200).json(settings.network); +}); + +settingsRoutes.post('/network', async (req, res) => { + const settings = getSettings(); + + settings.network = merge(settings.network, req.body); + await settings.save(); + + return res.status(200).json(settings.network); +}); + +settingsRoutes.post('/main/regenerate', async (req, res, next) => { + const settings = getSettings(); + + const main = await settings.regenerateApiKey(); if (!req.user) { return next({ status: 500, message: 'User missing from request.' }); @@ -119,7 +133,7 @@ settingsRoutes.post('/plex', async (req, res, next) => { settings.plex.machineId = result.MediaContainer.machineIdentifier; settings.plex.name = result.MediaContainer.friendlyName; - settings.save(); + await settings.save(); } catch (e) { logger.error('Something went wrong testing Plex connection', { label: 'API', @@ -232,7 +246,7 @@ settingsRoutes.get('/plex/library', async (req, res) => { ...library, enabled: enabledLibraries.includes(library.id), })); - settings.save(); + await settings.save(); return res.status(200).json(settings.plex.libraries); }); @@ -283,7 +297,7 @@ settingsRoutes.post('/jellyfin', async (req, res, next) => { Object.assign(settings.jellyfin, req.body); settings.jellyfin.serverId = result.Id; settings.jellyfin.name = result.ServerName; - settings.save(); + await settings.save(); } catch (e) { if (e instanceof ApiError) { logger.error('Something went wrong testing Jellyfin connection', { @@ -371,17 +385,12 @@ settingsRoutes.get('/jellyfin/library', async (req, res, next) => { ...library, enabled: enabledLibraries.includes(library.id), })); - settings.save(); + await settings.save(); return res.status(200).json(settings.jellyfin.libraries); }); settingsRoutes.get('/jellyfin/users', async (req, res) => { const settings = getSettings(); - const { externalHostname } = settings.jellyfin; - const jellyfinHost = - externalHostname && externalHostname.length > 0 - ? externalHostname - : getHostname(); const userRepository = getRepository(User); const admin = await userRepository.findOneOrFail({ @@ -400,9 +409,7 @@ settingsRoutes.get('/jellyfin/users', async (req, res) => { const users = resp.users.map((user) => ({ username: user.Name, id: user.Id, - thumb: user.PrimaryImageTag - ? `${jellyfinHost}/Users/${user.Id}/Images/Primary/?tag=${user.PrimaryImageTag}&quality=90` - : gravatarUrl(user.Name, { default: 'mm', size: 200 }), + thumb: `/avatarproxy/${user.Id}`, email: user.Name, })); @@ -442,7 +449,7 @@ settingsRoutes.post('/tautulli', async (req, res, next) => { throw new Error('Tautulli version not supported'); } - settings.save(); + await settings.save(); } catch (e) { logger.error('Something went wrong testing Tautulli connection', { label: 'API', @@ -703,7 +710,7 @@ settingsRoutes.post<{ jobId: JobId }>( settingsRoutes.post<{ jobId: JobId }>( '/jobs/:jobId/schedule', - (req, res, next) => { + async (req, res, next) => { const scheduledJob = scheduledJobs.find( (job) => job.id === req.params.jobId ); @@ -717,7 +724,7 @@ settingsRoutes.post<{ jobId: JobId }>( if (result) { settings.jobs[scheduledJob.id].schedule = req.body.schedule; - settings.save(); + await settings.save(); scheduledJob.cronSchedule = req.body.schedule; @@ -746,11 +753,13 @@ settingsRoutes.get('/cache', async (_req, res) => { })); const tmdbImageCache = await ImageProxy.getImageStats('tmdb'); + const avatarImageCache = await ImageProxy.getImageStats('avatar'); return res.status(200).json({ apiCaches, imageCache: { tmdb: tmdbImageCache, + avatar: avatarImageCache, }, }); }); @@ -772,11 +781,11 @@ settingsRoutes.post<{ cacheId: AvailableCacheIds }>( settingsRoutes.post( '/initialize', isAuthenticated(Permission.ADMIN), - (_req, res) => { + async (_req, res) => { const settings = getSettings(); settings.public.initialized = true; - settings.save(); + await settings.save(); return res.status(200).json(settings.public); } diff --git a/server/routes/settings/notifications.ts b/server/routes/settings/notifications.ts index be2fd89a..5b2e1715 100644 --- a/server/routes/settings/notifications.ts +++ b/server/routes/settings/notifications.ts @@ -31,11 +31,11 @@ notificationRoutes.get('/discord', (_req, res) => { res.status(200).json(settings.notifications.agents.discord); }); -notificationRoutes.post('/discord', (req, res) => { +notificationRoutes.post('/discord', async (req, res) => { const settings = getSettings(); settings.notifications.agents.discord = req.body; - settings.save(); + await settings.save(); res.status(200).json(settings.notifications.agents.discord); }); @@ -65,11 +65,11 @@ notificationRoutes.get('/slack', (_req, res) => { res.status(200).json(settings.notifications.agents.slack); }); -notificationRoutes.post('/slack', (req, res) => { +notificationRoutes.post('/slack', async (req, res) => { const settings = getSettings(); settings.notifications.agents.slack = req.body; - settings.save(); + await settings.save(); res.status(200).json(settings.notifications.agents.slack); }); @@ -99,11 +99,11 @@ notificationRoutes.get('/telegram', (_req, res) => { res.status(200).json(settings.notifications.agents.telegram); }); -notificationRoutes.post('/telegram', (req, res) => { +notificationRoutes.post('/telegram', async (req, res) => { const settings = getSettings(); settings.notifications.agents.telegram = req.body; - settings.save(); + await settings.save(); res.status(200).json(settings.notifications.agents.telegram); }); @@ -133,11 +133,11 @@ notificationRoutes.get('/pushbullet', (_req, res) => { res.status(200).json(settings.notifications.agents.pushbullet); }); -notificationRoutes.post('/pushbullet', (req, res) => { +notificationRoutes.post('/pushbullet', async (req, res) => { const settings = getSettings(); settings.notifications.agents.pushbullet = req.body; - settings.save(); + await settings.save(); res.status(200).json(settings.notifications.agents.pushbullet); }); @@ -167,11 +167,11 @@ notificationRoutes.get('/pushover', (_req, res) => { res.status(200).json(settings.notifications.agents.pushover); }); -notificationRoutes.post('/pushover', (req, res) => { +notificationRoutes.post('/pushover', async (req, res) => { const settings = getSettings(); settings.notifications.agents.pushover = req.body; - settings.save(); + await settings.save(); res.status(200).json(settings.notifications.agents.pushover); }); @@ -201,11 +201,11 @@ notificationRoutes.get('/email', (_req, res) => { res.status(200).json(settings.notifications.agents.email); }); -notificationRoutes.post('/email', (req, res) => { +notificationRoutes.post('/email', async (req, res) => { const settings = getSettings(); settings.notifications.agents.email = req.body; - settings.save(); + await settings.save(); res.status(200).json(settings.notifications.agents.email); }); @@ -235,11 +235,11 @@ notificationRoutes.get('/webpush', (_req, res) => { res.status(200).json(settings.notifications.agents.webpush); }); -notificationRoutes.post('/webpush', (req, res) => { +notificationRoutes.post('/webpush', async (req, res) => { const settings = getSettings(); settings.notifications.agents.webpush = req.body; - settings.save(); + await settings.save(); res.status(200).json(settings.notifications.agents.webpush); }); @@ -284,7 +284,7 @@ notificationRoutes.get('/webhook', (_req, res) => { res.status(200).json(response); }); -notificationRoutes.post('/webhook', (req, res, next) => { +notificationRoutes.post('/webhook', async (req, res, next) => { const settings = getSettings(); try { JSON.parse(req.body.options.jsonPayload); @@ -300,7 +300,7 @@ notificationRoutes.post('/webhook', (req, res, next) => { authHeader: req.body.options.authHeader, }, }; - settings.save(); + await settings.save(); res.status(200).json(settings.notifications.agents.webhook); } catch (e) { @@ -351,11 +351,11 @@ notificationRoutes.get('/lunasea', (_req, res) => { res.status(200).json(settings.notifications.agents.lunasea); }); -notificationRoutes.post('/lunasea', (req, res) => { +notificationRoutes.post('/lunasea', async (req, res) => { const settings = getSettings(); settings.notifications.agents.lunasea = req.body; - settings.save(); + await settings.save(); res.status(200).json(settings.notifications.agents.lunasea); }); @@ -385,11 +385,11 @@ notificationRoutes.get('/gotify', (_req, res) => { res.status(200).json(settings.notifications.agents.gotify); }); -notificationRoutes.post('/gotify', (req, res) => { +notificationRoutes.post('/gotify', async (req, res) => { const settings = getSettings(); settings.notifications.agents.gotify = req.body; - settings.save(); + await settings.save(); res.status(200).json(settings.notifications.agents.gotify); }); diff --git a/server/routes/settings/radarr.ts b/server/routes/settings/radarr.ts index c2b0a6f5..efa58665 100644 --- a/server/routes/settings/radarr.ts +++ b/server/routes/settings/radarr.ts @@ -12,7 +12,7 @@ radarrRoutes.get('/', (_req, res) => { res.status(200).json(settings.radarr); }); -radarrRoutes.post('/', (req, res) => { +radarrRoutes.post('/', async (req, res) => { const settings = getSettings(); const newRadarr = req.body as RadarrSettings; @@ -31,7 +31,7 @@ radarrRoutes.post('/', (req, res) => { } settings.radarr = [...settings.radarr, newRadarr]; - settings.save(); + await settings.save(); return res.status(201).json(newRadarr); }); @@ -76,7 +76,7 @@ radarrRoutes.post< radarrRoutes.put<{ id: string }, RadarrSettings, RadarrSettings>( '/:id', - (req, res, next) => { + async (req, res, next) => { const settings = getSettings(); const radarrIndex = settings.radarr.findIndex( @@ -102,7 +102,7 @@ radarrRoutes.put<{ id: string }, RadarrSettings, RadarrSettings>( ...req.body, id: Number(req.params.id), } as RadarrSettings; - settings.save(); + await settings.save(); return res.status(200).json(settings.radarr[radarrIndex]); } @@ -134,7 +134,7 @@ radarrRoutes.get<{ id: string }>('/:id/profiles', async (req, res, next) => { ); }); -radarrRoutes.delete<{ id: string }>('/:id', (req, res, next) => { +radarrRoutes.delete<{ id: string }>('/:id', async (req, res, next) => { const settings = getSettings(); const radarrIndex = settings.radarr.findIndex( @@ -146,7 +146,7 @@ radarrRoutes.delete<{ id: string }>('/:id', (req, res, next) => { } const removed = settings.radarr.splice(radarrIndex, 1); - settings.save(); + await settings.save(); return res.status(200).json(removed[0]); }); diff --git a/server/routes/settings/sonarr.ts b/server/routes/settings/sonarr.ts index 358d0700..84bf4d79 100644 --- a/server/routes/settings/sonarr.ts +++ b/server/routes/settings/sonarr.ts @@ -12,7 +12,7 @@ sonarrRoutes.get('/', (_req, res) => { res.status(200).json(settings.sonarr); }); -sonarrRoutes.post('/', (req, res) => { +sonarrRoutes.post('/', async (req, res) => { const settings = getSettings(); const newSonarr = req.body as SonarrSettings; @@ -31,7 +31,7 @@ sonarrRoutes.post('/', (req, res) => { } settings.sonarr = [...settings.sonarr, newSonarr]; - settings.save(); + await settings.save(); return res.status(201).json(newSonarr); }); @@ -43,13 +43,14 @@ sonarrRoutes.post('/test', async (req, res, next) => { url: SonarrAPI.buildUrl(req.body, '/api/v3'), }); - const urlBase = await sonarr - .getSystemStatus() - .then((value) => value.urlBase) - .catch(() => req.body.baseUrl); + const systemStatus = await sonarr.getSystemStatus(); + const sonarrMajorVersion = Number(systemStatus.version.split('.')[0]); + + const urlBase = systemStatus.urlBase; const profiles = await sonarr.getProfiles(); const folders = await sonarr.getRootFolders(); - const languageProfiles = await sonarr.getLanguageProfiles(); + const languageProfiles = + sonarrMajorVersion <= 3 ? await sonarr.getLanguageProfiles() : null; const tags = await sonarr.getTags(); return res.status(200).json({ @@ -72,7 +73,7 @@ sonarrRoutes.post('/test', async (req, res, next) => { } }); -sonarrRoutes.put<{ id: string }>('/:id', (req, res) => { +sonarrRoutes.put<{ id: string }>('/:id', async (req, res) => { const settings = getSettings(); const sonarrIndex = settings.sonarr.findIndex( @@ -100,12 +101,12 @@ sonarrRoutes.put<{ id: string }>('/:id', (req, res) => { ...req.body, id: Number(req.params.id), } as SonarrSettings; - settings.save(); + await settings.save(); return res.status(200).json(settings.sonarr[sonarrIndex]); }); -sonarrRoutes.delete<{ id: string }>('/:id', (req, res) => { +sonarrRoutes.delete<{ id: string }>('/:id', async (req, res) => { const settings = getSettings(); const sonarrIndex = settings.sonarr.findIndex( @@ -119,7 +120,7 @@ sonarrRoutes.delete<{ id: string }>('/:id', (req, res) => { } const removed = settings.sonarr.splice(sonarrIndex, 1); - settings.save(); + await settings.save(); return res.status(200).json(removed[0]); }); diff --git a/server/routes/user/index.ts b/server/routes/user/index.ts index da9b649c..c9bc9834 100644 --- a/server/routes/user/index.ts +++ b/server/routes/user/index.ts @@ -2,6 +2,7 @@ import JellyfinAPI from '@server/api/jellyfin'; import PlexTvAPI from '@server/api/plextv'; import TautulliAPI from '@server/api/tautulli'; import { MediaType } from '@server/constants/media'; +import { MediaServerType } from '@server/constants/server'; import { UserType } from '@server/constants/user'; import { getRepository } from '@server/datasource'; import Media from '@server/entity/Media'; @@ -33,8 +34,16 @@ router.get('/', async (req, res, next) => { try { const pageSize = req.query.take ? Number(req.query.take) : 10; const skip = req.query.skip ? Number(req.query.skip) : 0; + const q = req.query.q ? req.query.q.toString().toLowerCase() : ''; let query = getRepository(User).createQueryBuilder('user'); + if (q) { + query = query.where( + 'LOWER(user.username) LIKE :q OR LOWER(user.email) LIKE :q OR LOWER(user.plexUsername) LIKE :q OR LOWER(user.jellyfinUsername) LIKE :q', + { q: `%${q}%` } + ); + } + switch (req.query.sort) { case 'updated': query = query.orderBy('user.updatedAt', 'DESC'); @@ -44,7 +53,7 @@ router.get('/', async (req, res, next) => { `CASE WHEN (user.username IS NULL OR user.username = '') THEN ( CASE WHEN (user.plexUsername IS NULL OR user.plexUsername = '') THEN ( CASE WHEN (user.jellyfinUsername IS NULL OR user.jellyfinUsername = '') THEN - user.email + "user"."email" ELSE LOWER(user.jellyfinUsername) END) @@ -61,11 +70,11 @@ router.get('/', async (req, res, next) => { query = query .addSelect((subQuery) => { return subQuery - .select('COUNT(request.id)', 'requestCount') + .select('COUNT(request.id)', 'request_count') .from(MediaRequest, 'request') .where('request.requestedBy.id = user.id'); - }, 'requestCount') - .orderBy('requestCount', 'DESC'); + }, 'request_count') + .orderBy('request_count', 'DESC'); break; default: query = query.orderBy('user.id', 'ASC'); @@ -515,12 +524,6 @@ router.post( //const jellyfinUsersResponse = await jellyfinClient.getUsers(); const createdUsers: User[] = []; - const { externalHostname } = getSettings().jellyfin; - - const jellyfinHost = - externalHostname && externalHostname.length > 0 - ? externalHostname - : hostname; jellyfinClient.setUserId(admin.jellyfinUserId ?? ''); const jellyfinUsers = await jellyfinClient.getUsers(); @@ -544,13 +547,11 @@ router.post( ).toString('base64'), email: jellyfinUser?.Name, permissions: settings.main.defaultPermissions, - avatar: jellyfinUser?.PrimaryImageTag - ? `${jellyfinHost}/Users/${jellyfinUser.Id}/Images/Primary/?tag=${jellyfinUser.PrimaryImageTag}&quality=90` - : gravatarUrl(jellyfinUser?.Name ?? '', { - default: 'mm', - size: 200, - }), - userType: UserType.JELLYFIN, + avatar: `/avatarproxy/${jellyfinUser?.Id}`, + userType: + settings.main.mediaServerType === MediaServerType.JELLYFIN + ? UserType.JELLYFIN + : UserType.EMBY, }); await userRepository.save(newUser); @@ -771,6 +772,7 @@ router.get<{ id: string }, WatchlistResponse>( totalPages: Math.ceil(watchlist.totalSize / itemsPerPage), totalResults: watchlist.totalSize, results: watchlist.items.map((item) => ({ + id: item.tmdbId, ratingKey: item.ratingKey, title: item.title, mediaType: item.type === 'show' ? 'tv' : 'movie', diff --git a/server/routes/user/usersettings.ts b/server/routes/user/usersettings.ts index 9669cb18..24ca976b 100644 --- a/server/routes/user/usersettings.ts +++ b/server/routes/user/usersettings.ts @@ -1,3 +1,5 @@ +import { ApiErrorCode } from '@server/constants/error'; +import { UserType } from '@server/constants/user'; import { getRepository } from '@server/datasource'; import { User } from '@server/entity/User'; import { UserSettings } from '@server/entity/UserSettings'; @@ -9,6 +11,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 { ApiError } from '@server/types/error'; import { Router } from 'express'; import { canMakePermissionsChange } from '.'; @@ -54,7 +57,8 @@ userSettingsRoutes.get<{ id: string }, UserSettingsGeneralResponse>( email: user.email, discordId: user.settings?.discordId, locale: user.settings?.locale, - region: user.settings?.region, + discoverRegion: user.settings?.discoverRegion, + streamingRegion: user.settings?.streamingRegion, originalLanguage: user.settings?.originalLanguage, movieQuotaLimit: user.movieQuotaLimit, movieQuotaDays: user.movieQuotaDays, @@ -97,10 +101,36 @@ userSettingsRoutes.post< }); } + const oldEmail = user.email; + const oldUsername = user.username; user.username = req.body.username; if (user.jellyfinUsername) { user.email = req.body.email || user.jellyfinUsername || user.email; } + // Edge case for local users, because they have no Jellyfin username to fall back on + // if the email is not provided + if (user.userType === UserType.LOCAL) { + if (req.body.email) { + user.email = req.body.email; + if ( + !user.username && + user.email !== oldEmail && + !oldEmail.includes('@') + ) { + user.username = oldEmail; + } + } else if (req.body.username) { + user.email = oldUsername || user.email; + user.username = req.body.username; + } + } + + const existingUser = await userRepository.findOne({ + where: { email: user.email }, + }); + if (oldEmail !== user.email && existingUser) { + throw new ApiError(400, ApiErrorCode.InvalidEmail); + } // Update quota values only if the user has the correct permissions if ( @@ -118,7 +148,8 @@ userSettingsRoutes.post< user: req.user, discordId: req.body.discordId, locale: req.body.locale, - region: req.body.region, + discoverRegion: req.body.discoverRegion, + streamingRegion: req.body.streamingRegion, originalLanguage: req.body.originalLanguage, watchlistSyncMovies: req.body.watchlistSyncMovies, watchlistSyncTv: req.body.watchlistSyncTv, @@ -126,7 +157,8 @@ userSettingsRoutes.post< } else { user.settings.discordId = req.body.discordId; user.settings.locale = req.body.locale; - user.settings.region = req.body.region; + user.settings.discoverRegion = req.body.discoverRegion; + user.settings.streamingRegion = req.body.streamingRegion; user.settings.originalLanguage = req.body.originalLanguage; user.settings.watchlistSyncMovies = req.body.watchlistSyncMovies; user.settings.watchlistSyncTv = req.body.watchlistSyncTv; @@ -138,14 +170,22 @@ userSettingsRoutes.post< username: savedUser.username, discordId: savedUser.settings?.discordId, locale: savedUser.settings?.locale, - region: savedUser.settings?.region, + discoverRegion: savedUser.settings?.discoverRegion, + streamingRegion: savedUser.settings?.streamingRegion, originalLanguage: savedUser.settings?.originalLanguage, watchlistSyncMovies: savedUser.settings?.watchlistSyncMovies, watchlistSyncTv: savedUser.settings?.watchlistSyncTv, email: savedUser.email, }); } catch (e) { - next({ status: 500, message: e.message }); + if (e.errorCode) { + return next({ + status: e.statusCode, + message: e.errorCode, + }); + } else { + return next({ status: 500, message: e.message }); + } } }); @@ -283,6 +323,7 @@ userSettingsRoutes.get<{ id: string }, UserSettingsNotificationsResponse>( telegramEnabled: settings.telegram.enabled, telegramBotUsername: settings.telegram.options.botUsername, telegramChatId: user.settings?.telegramChatId, + telegramMessageThreadId: user.settings?.telegramMessageThreadId, telegramSendSilently: user.settings?.telegramSendSilently, webPushEnabled: settings.webpush.enabled, notificationTypes: user.settings?.notificationTypes ?? {}, @@ -325,6 +366,7 @@ userSettingsRoutes.post<{ id: string }, UserSettingsNotificationsResponse>( pushoverApplicationToken: req.body.pushoverApplicationToken, pushoverUserKey: req.body.pushoverUserKey, telegramChatId: req.body.telegramChatId, + telegramMessageThreadId: req.body.telegramMessageThreadId, telegramSendSilently: req.body.telegramSendSilently, notificationTypes: req.body.notificationTypes, }); @@ -337,6 +379,8 @@ userSettingsRoutes.post<{ id: string }, UserSettingsNotificationsResponse>( user.settings.pushoverUserKey = req.body.pushoverUserKey; user.settings.pushoverSound = req.body.pushoverSound; user.settings.telegramChatId = req.body.telegramChatId; + user.settings.telegramMessageThreadId = + req.body.telegramMessageThreadId; user.settings.telegramSendSilently = req.body.telegramSendSilently; user.settings.notificationTypes = Object.assign( {}, @@ -355,6 +399,7 @@ userSettingsRoutes.post<{ id: string }, UserSettingsNotificationsResponse>( pushoverUserKey: user.settings.pushoverUserKey, pushoverSound: user.settings.pushoverSound, telegramChatId: user.settings.telegramChatId, + telegramMessageThreadId: user.settings.telegramMessageThreadId, telegramSendSilently: user.settings.telegramSendSilently, notificationTypes: user.settings.notificationTypes, }); diff --git a/server/utils/DbColumnHelper.ts b/server/utils/DbColumnHelper.ts new file mode 100644 index 00000000..1c030b92 --- /dev/null +++ b/server/utils/DbColumnHelper.ts @@ -0,0 +1,20 @@ +import { isPgsql } from '@server/datasource'; +import type { ColumnOptions, ColumnType } from 'typeorm'; +import { Column } from 'typeorm'; +const pgTypeMapping: { [key: string]: ColumnType } = { + datetime: 'timestamp with time zone', +}; + +export function resolveDbType(pgType: ColumnType): ColumnType { + if (isPgsql && pgType.toString() in pgTypeMapping) { + return pgTypeMapping[pgType.toString()]; + } + return pgType; +} + +export function DbAwareColumn(columnOptions: ColumnOptions) { + if (columnOptions.type) { + columnOptions.type = resolveDbType(columnOptions.type); + } + return Column(columnOptions); +} diff --git a/server/utils/appDataVolume.ts b/server/utils/appDataVolume.ts index 73c80b2c..837f7f66 100644 --- a/server/utils/appDataVolume.ts +++ b/server/utils/appDataVolume.ts @@ -1,4 +1,4 @@ -import { existsSync } from 'fs'; +import { accessSync, existsSync } from 'fs'; import path from 'path'; const CONFIG_PATH = process.env.CONFIG_DIRECTORY @@ -14,3 +14,12 @@ export const appDataStatus = (): boolean => { export const appDataPath = (): string => { return CONFIG_PATH; }; + +export const appDataPermissions = (): boolean => { + try { + accessSync(CONFIG_PATH); + return true; + } catch (err) { + return false; + } +}; diff --git a/server/utils/customProxyAgent.ts b/server/utils/customProxyAgent.ts new file mode 100644 index 00000000..96ea7fed --- /dev/null +++ b/server/utils/customProxyAgent.ts @@ -0,0 +1,112 @@ +import type { ProxySettings } from '@server/lib/settings'; +import logger from '@server/logger'; +import type { Dispatcher } from 'undici'; +import { Agent, ProxyAgent, setGlobalDispatcher } from 'undici'; + +export default async function createCustomProxyAgent( + proxySettings: ProxySettings +) { + const defaultAgent = new Agent({ keepAliveTimeout: 5000 }); + + const skipUrl = (url: string) => { + const hostname = new URL(url).hostname; + + if (proxySettings.bypassLocalAddresses && isLocalAddress(hostname)) { + return true; + } + + for (const address of proxySettings.bypassFilter.split(',')) { + const trimmedAddress = address.trim(); + if (!trimmedAddress) { + continue; + } + + if (trimmedAddress.startsWith('*')) { + const domain = trimmedAddress.slice(1); + if (hostname.endsWith(domain)) { + return true; + } + } else if (hostname === trimmedAddress) { + return true; + } + } + + return false; + }; + + const noProxyInterceptor = ( + dispatch: Dispatcher['dispatch'] + ): Dispatcher['dispatch'] => { + return (opts, handler) => { + const url = opts.origin?.toString(); + return url && skipUrl(url) + ? defaultAgent.dispatch(opts, handler) + : dispatch(opts, handler); + }; + }; + + const token = + proxySettings.user && proxySettings.password + ? `Basic ${Buffer.from( + `${proxySettings.user}:${proxySettings.password}` + ).toString('base64')}` + : undefined; + + try { + const proxyAgent = new ProxyAgent({ + uri: + (proxySettings.useSsl ? 'https://' : 'http://') + + proxySettings.hostname + + ':' + + proxySettings.port, + token, + interceptors: { + Client: [noProxyInterceptor], + }, + keepAliveTimeout: 5000, + }); + + setGlobalDispatcher(proxyAgent); + } catch (e) { + logger.error('Failed to connect to the proxy: ' + e.message, { + label: 'Proxy', + }); + setGlobalDispatcher(defaultAgent); + return; + } + + try { + const res = await fetch('https://www.google.com', { method: 'HEAD' }); + if (res.ok) { + logger.debug('HTTP(S) proxy connected successfully', { label: 'Proxy' }); + } else { + logger.error('Proxy responded, but with a non-OK status: ' + res.status, { + label: 'Proxy', + }); + setGlobalDispatcher(defaultAgent); + } + } catch (e) { + logger.error( + 'Failed to connect to the proxy: ' + e.message + ': ' + e.cause, + { label: 'Proxy' } + ); + setGlobalDispatcher(defaultAgent); + } +} + +function isLocalAddress(hostname: string) { + if (hostname === 'localhost' || hostname === '127.0.0.1') { + return true; + } + + const privateIpRanges = [ + /^10\./, // 10.x.x.x + /^172\.(1[6-9]|2[0-9]|3[0-1])\./, // 172.16.x.x - 172.31.x.x + /^192\.168\./, // 192.168.x.x + ]; + if (privateIpRanges.some((regex) => regex.test(hostname))) { + return true; + } + + return false; +} diff --git a/server/utils/restartFlag.ts b/server/utils/restartFlag.ts index 387ec5ce..24282a09 100644 --- a/server/utils/restartFlag.ts +++ b/server/utils/restartFlag.ts @@ -1,19 +1,25 @@ -import type { MainSettings } from '@server/lib/settings'; +import type { AllSettings, NetworkSettings } from '@server/lib/settings'; import { getSettings } from '@server/lib/settings'; class RestartFlag { - private settings: MainSettings; + private networkSettings: NetworkSettings; - public initializeSettings(settings: MainSettings): void { - this.settings = { ...settings }; + public initializeSettings(settings: AllSettings): void { + this.networkSettings = { + ...settings.network, + proxy: { ...settings.network.proxy }, + }; } public isSet(): boolean { - const settings = getSettings().main; + const networkSettings = getSettings().network; return ( - this.settings.csrfProtection !== settings.csrfProtection || - this.settings.trustProxy !== settings.trustProxy + this.networkSettings.csrfProtection !== networkSettings.csrfProtection || + this.networkSettings.trustProxy !== networkSettings.trustProxy || + this.networkSettings.proxy.enabled !== networkSettings.proxy.enabled || + this.networkSettings.forceIpv4First !== networkSettings.forceIpv4First || + this.networkSettings.dnsServers !== networkSettings.dnsServers ); } } diff --git a/src/assets/services/jellyfin.svg b/src/assets/services/jellyfin.svg index 4c93218a..7983d340 100644 --- a/src/assets/services/jellyfin.svg +++ b/src/assets/services/jellyfin.svg @@ -1,24 +1,61 @@ diff --git a/src/components/Blacklist/index.tsx b/src/components/Blacklist/index.tsx new file mode 100644 index 00000000..a752e95f --- /dev/null +++ b/src/components/Blacklist/index.tsx @@ -0,0 +1,420 @@ +import Badge from '@app/components/Common/Badge'; +import Button from '@app/components/Common/Button'; +import CachedImage from '@app/components/Common/CachedImage'; +import ConfirmButton from '@app/components/Common/ConfirmButton'; +import Header from '@app/components/Common/Header'; +import LoadingSpinner from '@app/components/Common/LoadingSpinner'; +import PageTitle from '@app/components/Common/PageTitle'; +import useDebouncedState from '@app/hooks/useDebouncedState'; +import { useUpdateQueryParams } from '@app/hooks/useUpdateQueryParams'; +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 { + ChevronLeftIcon, + ChevronRightIcon, + MagnifyingGlassIcon, + TrashIcon, +} from '@heroicons/react/24/solid'; +import type { + BlacklistItem, + BlacklistResultsResponse, +} from '@server/interfaces/api/blacklistInterfaces'; +import type { MovieDetails } from '@server/models/Movie'; +import type { TvDetails } from '@server/models/Tv'; +import Link from 'next/link'; +import { useRouter } from 'next/router'; +import type { ChangeEvent } from 'react'; +import { useState } from 'react'; +import { useInView } from 'react-intersection-observer'; +import { FormattedRelativeTime, useIntl } from 'react-intl'; +import { useToasts } from 'react-toast-notifications'; +import useSWR from 'swr'; + +const messages = defineMessages('components.Blacklist', { + blacklistsettings: 'Blacklist Settings', + blacklistSettingsDescription: 'Manage blacklisted media.', + mediaName: 'Name', + mediaType: 'Type', + mediaTmdbId: 'tmdb Id', + blacklistdate: 'date', + blacklistedby: '{date} by {user}', + blacklistNotFoundError: '{title} is not blacklisted.', +}); + +const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => { + return (movie as MovieDetails).title !== undefined; +}; + +const Blacklist = () => { + const [currentPageSize, setCurrentPageSize] = useState(10); + const [searchFilter, debouncedSearchFilter, setSearchFilter] = + useDebouncedState(''); + const router = useRouter(); + const intl = useIntl(); + + const page = router.query.page ? Number(router.query.page) : 1; + const pageIndex = page - 1; + const updateQueryParams = useUpdateQueryParams({ page: page.toString() }); + + const { + data, + error, + mutate: revalidate, + } = useSWR( + `/api/v1/blacklist/?take=${currentPageSize} + &skip=${pageIndex * currentPageSize} + ${debouncedSearchFilter ? `&search=${debouncedSearchFilter}` : ''}`, + { + refreshInterval: 0, + revalidateOnFocus: false, + } + ); + + // check if there's no data and no errors in the table + // so as to show a spinner inside the table and not refresh the whole component + if (!data && error) { + return ; + } + + const searchItem = (e: ChangeEvent) => { + // Remove the "page" query param from the URL + // so that the "skip" query param on line 62 is empty + // and the search returns results without skipping items + if (router.query.page) router.replace(router.basePath); + + setSearchFilter(e.target.value as string); + }; + + const hasNextPage = data && data.pageInfo.pages > pageIndex + 1; + const hasPrevPage = pageIndex > 0; + + return ( + <> + +
{intl.formatMessage(globalMessages.blacklist)}
+ +
+
+ + + + searchItem(e)} + /> +
+
+ + {!data ? ( + + ) : data.results.length === 0 ? ( +
+ + {intl.formatMessage(globalMessages.noresults)} + +
+ ) : ( + data.results.map((item: BlacklistItem) => { + return ( +
+ +
+ ); + }) + )} + +
+ +
+ + ); +}; + +export default Blacklist; + +interface BlacklistedItemProps { + item: BlacklistItem; + revalidateList: () => void; +} + +const BlacklistedItem = ({ item, revalidateList }: BlacklistedItemProps) => { + const [isUpdating, setIsUpdating] = useState(false); + const { addToast } = useToasts(); + const { ref, inView } = useInView({ + triggerOnce: true, + }); + const intl = useIntl(); + const { hasPermission } = useUser(); + + const url = + item.mediaType === 'movie' + ? `/api/v1/movie/${item.tmdbId}` + : `/api/v1/tv/${item.tmdbId}`; + const { data: title, error } = useSWR( + inView ? url : null + ); + + if (!title && !error) { + return ( +
+ ); + } + + const removeFromBlacklist = async (tmdbId: number, title?: string) => { + setIsUpdating(true); + + const res = await fetch('/api/v1/blacklist/' + tmdbId, { + method: 'DELETE', + }); + + if (res.status === 204) { + addToast( + + {intl.formatMessage(globalMessages.removeFromBlacklistSuccess, { + title, + strong: (msg: React.ReactNode) => {msg}, + })} + , + { appearance: 'success', autoDismiss: true } + ); + } else { + addToast(intl.formatMessage(globalMessages.blacklistError), { + appearance: 'error', + autoDismiss: true, + }); + } + + revalidateList(); + setIsUpdating(false); + }; + + return ( +
+ {title && title.backdropPath && ( +
+ +
+
+ )} +
+
+ + + +
+
+ {title && + (isMovie(title) + ? title.releaseDate + : title.firstAirDate + )?.slice(0, 4)} +
+ + + {title && (isMovie(title) ? title.title : title.name)} + + +
+
+ +
+
+ Status + + {intl.formatMessage(globalMessages.blacklisted)} + +
+ + {item.createdAt && ( +
+ + {intl.formatMessage(globalMessages.blacklisted)} + + + {intl.formatMessage(messages.blacklistedby, { + date: ( + + ), + user: ( + + + + + {item.user.displayName} + + + + ), + })} + +
+ )} +
+ {item.mediaType === 'movie' ? ( +
+
+ {intl.formatMessage(globalMessages.movie)} +
+
+ ) : ( +
+
+ {intl.formatMessage(globalMessages.tvshow)} +
+
+ )} +
+
+
+
+ {hasPermission(Permission.MANAGE_BLACKLIST) && ( + + removeFromBlacklist( + item.tmdbId, + title && (isMovie(title) ? title.title : title.name) + ) + } + confirmText={intl.formatMessage( + isUpdating ? globalMessages.deleting : globalMessages.areyousure + )} + className={`w-full ${ + isUpdating ? 'pointer-events-none opacity-50' : '' + }`} + > + + + {intl.formatMessage(globalMessages.removefromBlacklist)} + + + )} +
+
+ ); +}; diff --git a/src/components/BlacklistBlock/index.tsx b/src/components/BlacklistBlock/index.tsx new file mode 100644 index 00000000..8d619aa3 --- /dev/null +++ b/src/components/BlacklistBlock/index.tsx @@ -0,0 +1,138 @@ +import Badge from '@app/components/Common/Badge'; +import Button from '@app/components/Common/Button'; +import LoadingSpinner from '@app/components/Common/LoadingSpinner'; +import Tooltip from '@app/components/Common/Tooltip'; +import { useUser } from '@app/hooks/useUser'; +import globalMessages from '@app/i18n/globalMessages'; +import defineMessages from '@app/utils/defineMessages'; +import { CalendarIcon, TrashIcon, UserIcon } from '@heroicons/react/24/solid'; +import type { Blacklist } from '@server/entity/Blacklist'; +import Link from 'next/link'; +import { useState } from 'react'; +import { useIntl } from 'react-intl'; +import { useToasts } from 'react-toast-notifications'; +import useSWR from 'swr'; + +const messages = defineMessages('component.BlacklistBlock', { + blacklistedby: 'Blacklisted By', + blacklistdate: 'Blacklisted date', +}); + +interface BlacklistBlockProps { + tmdbId: number; + onUpdate?: () => void; + onDelete?: () => void; +} + +const BlacklistBlock = ({ + tmdbId, + onUpdate, + onDelete, +}: BlacklistBlockProps) => { + const { user } = useUser(); + const intl = useIntl(); + const [isUpdating, setIsUpdating] = useState(false); + const { addToast } = useToasts(); + const { data } = useSWR(`/api/v1/blacklist/${tmdbId}`); + + const removeFromBlacklist = async (tmdbId: number, title?: string) => { + setIsUpdating(true); + + const res = await fetch('/api/v1/blacklist/' + tmdbId, { + method: 'DELETE', + }); + + if (res.status === 204) { + addToast( + + {intl.formatMessage(globalMessages.removeFromBlacklistSuccess, { + title, + strong: (msg: React.ReactNode) => {msg}, + })} + , + { appearance: 'success', autoDismiss: true } + ); + } else { + addToast(intl.formatMessage(globalMessages.blacklistError), { + appearance: 'error', + autoDismiss: true, + }); + } + + onUpdate && onUpdate(); + onDelete && onDelete(); + + setIsUpdating(false); + }; + + if (!data) { + return ( + <> + + + ); + } + + return ( +
+
+
+
+ + + + + + + {data.user.displayName} + + + +
+
+
+ + + +
+
+
+
+
+ + {intl.formatMessage(globalMessages.blacklisted)} + +
+
+
+ + + + + {intl.formatDate(data.createdAt, { + year: 'numeric', + month: 'long', + day: 'numeric', + })} + +
+
+
+ ); +}; + +export default BlacklistBlock; diff --git a/src/components/BlacklistModal/index.tsx b/src/components/BlacklistModal/index.tsx new file mode 100644 index 00000000..4ef1a7b6 --- /dev/null +++ b/src/components/BlacklistModal/index.tsx @@ -0,0 +1,94 @@ +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 { MovieDetails } from '@server/models/Movie'; +import type { TvDetails } from '@server/models/Tv'; +import { useEffect, useState } from 'react'; +import { useIntl } from 'react-intl'; + +interface BlacklistModalProps { + tmdbId: number; + type: 'movie' | 'tv' | 'collection'; + show: boolean; + onComplete?: () => void; + onCancel?: () => void; + isUpdating?: boolean; +} + +const messages = defineMessages('component.BlacklistModal', { + blacklisting: 'Blacklisting', +}); + +const isMovie = ( + movie: MovieDetails | TvDetails | null +): movie is MovieDetails => { + if (!movie) return false; + return (movie as MovieDetails).title !== undefined; +}; + +const BlacklistModal = ({ + tmdbId, + type, + show, + onComplete, + onCancel, + isUpdating, +}: BlacklistModalProps) => { + const intl = useIntl(); + const [data, setData] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + (async () => { + if (!show) return; + try { + setError(null); + const response = await fetch(`/api/v1/${type}/${tmdbId}`); + if (!response.ok) { + throw new Error(); + } + const result = await response.json(); + setData(result); + } catch (err) { + setError(err); + } + })(); + }, [show, tmdbId, type]); + + return ( + + + + ); +}; + +export default BlacklistModal; diff --git a/src/components/CollectionDetails/index.tsx b/src/components/CollectionDetails/index.tsx index 7afa28e4..d9c9a813 100644 --- a/src/components/CollectionDetails/index.tsx +++ b/src/components/CollectionDetails/index.tsx @@ -183,6 +183,11 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => { ); } + const blacklistVisibility = hasPermission( + [Permission.MANAGE_BLACKLIST, Permission.VIEW_BLACKLIST], + { type: 'or' } + ); + return (
{ {data.backdropPath && (
{
{ sliderKey="collection-movies" isLoading={false} isEmpty={data.parts.length === 0} - items={data.parts.map((title) => ( - - ))} + items={data.parts + .filter((title) => { + if (!blacklistVisibility) + return title.mediaInfo?.status !== MediaStatus.BLACKLISTED; + return title; + }) + .map((title) => ( + + ))} />
diff --git a/src/components/Common/ButtonWithDropdown/index.tsx b/src/components/Common/ButtonWithDropdown/index.tsx index b0d314d1..bf98cdae 100644 --- a/src/components/Common/ButtonWithDropdown/index.tsx +++ b/src/components/Common/ButtonWithDropdown/index.tsx @@ -2,7 +2,11 @@ import useClickOutside from '@app/hooks/useClickOutside'; import { withProperties } from '@app/utils/typeHelpers'; import { Transition } from '@headlessui/react'; import { ChevronDownIcon } from '@heroicons/react/24/solid'; -import type { AnchorHTMLAttributes, ButtonHTMLAttributes } from 'react'; +import type { + AnchorHTMLAttributes, + ButtonHTMLAttributes, + RefObject, +} from 'react'; import { Fragment, useRef, useState } from 'react'; interface DropdownItemProps extends AnchorHTMLAttributes { @@ -35,23 +39,33 @@ const DropdownItem = ({ ); }; -interface ButtonWithDropdownProps - extends ButtonHTMLAttributes { +interface ButtonWithDropdownProps { text: React.ReactNode; dropdownIcon?: React.ReactNode; buttonType?: 'primary' | 'ghost'; } +interface ButtonProps + extends ButtonHTMLAttributes, + ButtonWithDropdownProps { + as?: 'button'; +} +interface AnchorProps + extends AnchorHTMLAttributes, + ButtonWithDropdownProps { + as: 'a'; +} const ButtonWithDropdown = ({ + as, text, children, dropdownIcon, className, buttonType = 'primary', ...props -}: ButtonWithDropdownProps) => { +}: ButtonProps | AnchorProps) => { const [isOpen, setIsOpen] = useState(false); - const buttonRef = useRef(null); + const buttonRef = useRef(null); useClickOutside(buttonRef, () => setIsOpen(false)); const styleClasses = { @@ -78,16 +92,28 @@ const ButtonWithDropdown = ({ return ( - + {as === 'a' ? ( + } + {...(props as AnchorHTMLAttributes)} + > + {text} + + ) : ( + + )} {children && ( - )} - {data?.mediaInfo.status4k !== MediaStatus.AVAILABLE && - settings.currentSettings.series4kEnabled && ( + {hasPermission(Permission.ADMIN) && + data?.mediaInfo && + data.mediaInfo.status !== MediaStatus.BLACKLISTED && ( +
+

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

+
+ {data?.mediaInfo.status !== MediaStatus.AVAILABLE && ( )} -
- deleteMedia()} - confirmText={intl.formatMessage(globalMessages.areyousure)} - className="w-full" - > - - - {intl.formatMessage(messages.manageModalClearMedia)} - - -
- {intl.formatMessage(messages.manageModalClearMediaWarning, { - mediaType: intl.formatMessage( - mediaType === 'movie' ? messages.movie : messages.tvshow - ), - mediaServerName: - settings.currentSettings.mediaServerType === - MediaServerType.EMBY - ? 'Emby' - : settings.currentSettings.mediaServerType === - MediaServerType.PLEX - ? 'Plex' - : 'Jellyfin', - })} + {data?.mediaInfo.status4k !== MediaStatus.AVAILABLE && + settings.currentSettings.series4kEnabled && ( + + )} +
+ deleteMedia()} + confirmText={intl.formatMessage(globalMessages.areyousure)} + className="w-full" + > + + + {intl.formatMessage(messages.manageModalClearMedia)} + + +
+ {intl.formatMessage(messages.manageModalClearMediaWarning, { + mediaType: intl.formatMessage( + mediaType === 'movie' ? messages.movie : messages.tvshow + ), + mediaServerName: + settings.currentSettings.mediaServerType === + MediaServerType.EMBY + ? 'Emby' + : settings.currentSettings.mediaServerType === + MediaServerType.PLEX + ? 'Plex' + : 'Jellyfin', + })} +
-
- )} + )}
); diff --git a/src/components/MediaSlider/index.tsx b/src/components/MediaSlider/index.tsx index 56e0afc8..006f0df9 100644 --- a/src/components/MediaSlider/index.tsx +++ b/src/components/MediaSlider/index.tsx @@ -3,8 +3,10 @@ import PersonCard from '@app/components/PersonCard'; import Slider from '@app/components/Slider'; import TitleCard from '@app/components/TitleCard'; import useSettings from '@app/hooks/useSettings'; +import { useUser } from '@app/hooks/useUser'; import { ArrowRightCircleIcon } from '@heroicons/react/24/outline'; import { MediaStatus } from '@server/constants/media'; +import { Permission } from '@server/lib/permissions'; import type { MovieResult, PersonResult, @@ -41,6 +43,7 @@ const MediaSlider = ({ onNewTitles, }: MediaSliderProps) => { const settings = useSettings(); + const { hasPermission } = useUser(); const { data, error, setSize, size } = useSWRInfinite( (pageIndex: number, previousPageData: MixedResult | null) => { if (previousPageData && pageIndex + 1 > previousPageData.totalPages) { @@ -90,50 +93,65 @@ const MediaSlider = ({ return null; } - const finalTitles = titles.slice(0, 20).map((title) => { - switch (title.mediaType) { - case 'movie': + const blacklistVisibility = hasPermission( + [Permission.MANAGE_BLACKLIST, Permission.VIEW_BLACKLIST], + { type: 'or' } + ); + + const finalTitles = titles + .slice(0, 20) + .filter((title) => { + if (!blacklistVisibility) return ( - 0} - /> + (title as TvResult | MovieResult).mediaInfo?.status !== + MediaStatus.BLACKLISTED ); - case 'tv': - return ( - 0} - /> - ); - case 'person': - return ( - - ); - } - }); + return title; + }) + .map((title) => { + switch (title.mediaType) { + case 'movie': + return ( + 0} + /> + ); + case 'tv': + return ( + 0} + /> + ); + case 'person': + return ( + + ); + } + }); if (linkUrl && titles.length > 20) { finalTitles.push( diff --git a/src/components/MovieDetails/index.tsx b/src/components/MovieDetails/index.tsx index e4bc991e..72a598f0 100644 --- a/src/components/MovieDetails/index.tsx +++ b/src/components/MovieDetails/index.tsx @@ -5,6 +5,7 @@ import RTRotten from '@app/assets/rt_rotten.svg'; import ImdbLogo from '@app/assets/services/imdb.svg'; import Spinner from '@app/assets/spinner.svg'; import TmdbLogo from '@app/assets/tmdb_logo.svg'; +import BlacklistModal from '@app/components/BlacklistModal'; import Button from '@app/components/Common/Button'; import CachedImage from '@app/components/Common/CachedImage'; import LoadingSpinner from '@app/components/Common/LoadingSpinner'; @@ -35,15 +36,16 @@ import { CloudIcon, CogIcon, ExclamationTriangleIcon, + EyeSlashIcon, FilmIcon, + MinusCircleIcon, PlayIcon, + StarIcon, TicketIcon, } from '@heroicons/react/24/outline'; import { ChevronDoubleDownIcon, ChevronDoubleUpIcon, - MinusCircleIcon, - StarIcon, } from '@heroicons/react/24/solid'; import { type RatingResponse } from '@server/api/ratings'; import { IssueStatus } from '@server/constants/issue'; @@ -55,7 +57,7 @@ import 'country-flag-icons/3x2/flags.css'; import { uniqBy } from 'lodash'; import Link from 'next/link'; import { useRouter } from 'next/router'; -import { useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { useIntl } from 'react-intl'; import { useToasts } from 'react-toast-notifications'; import useSWR from 'swr'; @@ -100,7 +102,7 @@ const messages = defineMessages('components.MovieDetails', { watchlistSuccess: '{title} added to watchlist successfully!', watchlistDeleted: '{title} Removed from watchlist successfully!', - watchlistError: 'Something went wrong try again.', + watchlistError: 'Something went wrong. Please try again.', removefromwatchlist: 'Remove From Watchlist', addtowatchlist: 'Add To Watchlist', }); @@ -125,6 +127,9 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => { const [toggleWatchlist, setToggleWatchlist] = useState( !movie?.onUserWatchlist ); + const [isBlacklistUpdating, setIsBlacklistUpdating] = + useState(false); + const [showBlacklistModal, setShowBlacklistModal] = useState(false); const { addToast } = useToasts(); const { @@ -155,6 +160,11 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => { setShowManager(router.query.manage == '1' ? true : false); }, [router.query.manage]); + const closeBlacklistModal = useCallback( + () => setShowBlacklistModal(false), + [] + ); + const { mediaUrl: plexUrl, mediaUrl4k: plexUrl4k } = useDeepLinks({ mediaUrl: data?.mediaInfo?.mediaUrl, mediaUrl4k: data?.mediaInfo?.mediaUrl4k, @@ -180,7 +190,7 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => { }) ) { mediaLinks.push({ - text: getAvalaibleMediaServerName(), + text: getAvailableMediaServerName(), url: plexUrl, svg: , }); @@ -194,7 +204,7 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => { }) ) { mediaLinks.push({ - text: getAvalaible4kMediaServerName(), + text: getAvailable4kMediaServerName(), url: plexUrl4k, svg: , }); @@ -212,14 +222,14 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => { }); } - const region = user?.settings?.region - ? user.settings.region - : settings.currentSettings.region - ? settings.currentSettings.region + const discoverRegion = user?.settings?.discoverRegion + ? user.settings.discoverRegion + : settings.currentSettings.discoverRegion + ? settings.currentSettings.discoverRegion : 'US'; const releases = data.releases.results.find( - (r) => r.iso_3166_1 === region + (r) => r.iso_3166_1 === discoverRegion )?.release_dates; // Release date types: @@ -272,11 +282,17 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => { ); } + const streamingRegion = user?.settings?.streamingRegion + ? user.settings.streamingRegion + : settings.currentSettings.streamingRegion + ? settings.currentSettings.streamingRegion + : 'US'; const streamingProviders = - data?.watchProviders?.find((provider) => provider.iso_3166_1 === region) - ?.flatrate ?? []; + data?.watchProviders?.find( + (provider) => provider.iso_3166_1 === streamingRegion + )?.flatrate ?? []; - function getAvalaibleMediaServerName() { + function getAvailableMediaServerName() { if (settings.currentSettings.mediaServerType === MediaServerType.EMBY) { return intl.formatMessage(messages.play, { mediaServerName: 'Emby' }); } @@ -288,7 +304,7 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => { return intl.formatMessage(messages.play, { mediaServerName: 'Jellyfin' }); } - function getAvalaible4kMediaServerName() { + function getAvailable4kMediaServerName() { if (settings.currentSettings.mediaServerType === MediaServerType.EMBY) { return intl.formatMessage(messages.play, { mediaServerName: 'Emby' }); } @@ -374,6 +390,60 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => { } }; + const onClickHideItemBtn = async (): Promise => { + setIsBlacklistUpdating(true); + + const res = await fetch('/api/v1/blacklist', { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + tmdbId: movie?.id, + mediaType: 'movie', + title: movie?.title, + user: user?.id, + }), + }); + + if (res.status === 201) { + addToast( + + {intl.formatMessage(globalMessages.blacklistSuccess, { + title: movie?.title, + strong: (msg: React.ReactNode) => {msg}, + })} + , + { appearance: 'success', autoDismiss: true } + ); + + revalidate(); + } else if (res.status === 412) { + addToast( + + {intl.formatMessage(globalMessages.blacklistDuplicateError, { + title: movie?.title, + strong: (msg: React.ReactNode) => {msg}, + })} + , + { appearance: 'info', autoDismiss: true } + ); + } else { + addToast(intl.formatMessage(globalMessages.blacklistError), { + appearance: 'error', + autoDismiss: true, + }); + } + + setIsBlacklistUpdating(false); + closeBlacklistModal(); + }; + + const showHideButton = hasPermission([Permission.MANAGE_BLACKLIST], { + type: 'or', + }); + return (
{ {data.backdropPath && (
{ revalidate={() => revalidate()} show={showManager} /> +
{
- <> - {toggleWatchlist ? ( - + {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.BLACKLISTED && ( + - - ) : ( - - )} - + {data?.mediaInfo?.status !== MediaStatus.BLACKLISTED && ( + <> + {toggleWatchlist ? ( + + + + ) : ( + + + + )} + + )} {
{
{(!!data.voteCount || (ratingData?.rt?.criticsRating && - !!ratingData?.rt?.criticsScore) || + typeof ratingData?.rt?.criticsScore === 'number') || (ratingData?.rt?.audienceRating && !!ratingData?.rt?.audienceScore) || ratingData?.imdb?.criticsScore) && (
{ratingData?.rt?.criticsRating && - !!ratingData?.rt?.criticsScore && ( + typeof ratingData?.rt?.criticsScore === 'number' && ( @@ -961,14 +1063,26 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
)} {!!streamingProviders.length && ( -
+
{intl.formatMessage(messages.streamingproviders)} - + {streamingProviders.map((p) => { return ( - - {p.name} - + + + + + ); })} diff --git a/src/components/PermissionEdit/index.tsx b/src/components/PermissionEdit/index.tsx index a220335b..5a861de8 100644 --- a/src/components/PermissionEdit/index.tsx +++ b/src/components/PermissionEdit/index.tsx @@ -78,6 +78,13 @@ export const messages = defineMessages('components.PermissionEdit', { viewwatchlists: 'View {mediaServerName} Watchlists', viewwatchlistsDescription: "Grant permission to view other users' {mediaServerName} Watchlists.", + manageblacklist: 'Manage Blacklist', + manageblacklistDescription: 'Grant permission to manage blacklisted media.', + blacklistedItems: 'Blacklist media.', + blacklistedItemsDescription: 'Grant permission to blacklist media.', + viewblacklistedItems: 'View blacklisted media.', + viewblacklistedItemsDescription: + 'Grant permission to view blacklisted media.', }); interface PermissionEditProps { @@ -332,6 +339,22 @@ export const PermissionEdit = ({ }, ], }, + { + id: 'manageblacklist', + name: intl.formatMessage(messages.manageblacklist), + description: intl.formatMessage(messages.manageblacklistDescription), + permission: Permission.MANAGE_BLACKLIST, + children: [ + { + id: 'viewblacklisteditems', + name: intl.formatMessage(messages.viewblacklistedItems), + description: intl.formatMessage( + messages.viewblacklistedItemsDescription + ), + permission: Permission.VIEW_BLACKLIST, + }, + ], + }, ]; return ( diff --git a/src/components/PersonCard/index.tsx b/src/components/PersonCard/index.tsx index 5263a372..4d79a469 100644 --- a/src/components/PersonCard/index.tsx +++ b/src/components/PersonCard/index.tsx @@ -51,6 +51,7 @@ const PersonCard = ({ {profilePath ? (
{ {data.profilePath && (
void; } @@ -30,6 +31,7 @@ const RegionSelector = ({ isUserSetting = false, disableAll = false, watchProviders = false, + regionType = 'discover', onChange, }: RegionSelectorProps) => { const { currentSettings } = useSettings(); @@ -63,6 +65,11 @@ const RegionSelector = ({ sortedRegions?.find((region) => region.iso_3166_1 === regionCode)?.name ?? regionCode; + const regionValue = + regionType === 'discover' + ? currentSettings.discoverRegion + : currentSettings.streamingRegion; + useEffect(() => { if (regions && value) { if (value === 'all') { @@ -97,14 +104,12 @@ const RegionSelector = ({ countries.includes(selectedRegion?.iso_3166_1)) || (isUserSetting && !selectedRegion && - currentSettings.region && - countries.includes(currentSettings.region))) && ( + regionValue && + countries.includes(regionValue))) && ( @@ -114,8 +119,8 @@ const RegionSelector = ({ ? regionName(selectedRegion.iso_3166_1) : isUserSetting && selectedRegion?.iso_3166_1 !== 'all' ? intl.formatMessage(messages.regionServerDefault, { - region: currentSettings.region - ? regionName(currentSettings.region) + region: regionValue + ? regionName(regionValue) : intl.formatMessage(messages.regionDefault), }) : intl.formatMessage(messages.regionDefault)} @@ -148,8 +153,8 @@ const RegionSelector = ({ @@ -160,8 +165,8 @@ const RegionSelector = ({ } block truncate`} > {intl.formatMessage(messages.regionServerDefault, { - region: currentSettings.region - ? regionName(currentSettings.region) + region: regionValue + ? regionName(regionValue) : intl.formatMessage(messages.regionDefault), })} diff --git a/src/components/RequestBlock/index.tsx b/src/components/RequestBlock/index.tsx index 3c317465..b63a24dd 100644 --- a/src/components/RequestBlock/index.tsx +++ b/src/components/RequestBlock/index.tsx @@ -247,7 +247,11 @@ const RequestBlock = ({ request, onUpdate }: RequestBlockProps) => { key={`season-${season.id}`} className="mb-1 mr-2 inline-block" > - {season.seasonNumber} + + {season.seasonNumber === 0 + ? intl.formatMessage(globalMessages.specials) + : season.seasonNumber} + ))}
diff --git a/src/components/RequestButton/index.tsx b/src/components/RequestButton/index.tsx index cf27e55d..cbe04fe3 100644 --- a/src/components/RequestButton/index.tsx +++ b/src/components/RequestButton/index.tsx @@ -300,6 +300,7 @@ const RequestButton = ({ }) && media && media.status !== MediaStatus.AVAILABLE && + media.status !== MediaStatus.BLACKLISTED && !isShowComplete ) { buttons.push({ @@ -345,6 +346,7 @@ const RequestButton = ({ }) && media && media.status4k !== MediaStatus.AVAILABLE && + media.status !== MediaStatus.BLACKLISTED && !is4kShowComplete && settings.currentSettings.series4kEnabled ) { diff --git a/src/components/RequestCard/index.tsx b/src/components/RequestCard/index.tsx index eb78806f..7f08044e 100644 --- a/src/components/RequestCard/index.tsx +++ b/src/components/RequestCard/index.tsx @@ -5,6 +5,7 @@ import Tooltip from '@app/components/Common/Tooltip'; import RequestModal from '@app/components/RequestModal'; import StatusBadge from '@app/components/StatusBadge'; import useDeepLinks from '@app/hooks/useDeepLinks'; +import useSettings from '@app/hooks/useSettings'; import { Permission, useUser } from '@app/hooks/useUser'; import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; @@ -22,7 +23,6 @@ import type { MediaRequest } from '@server/entity/MediaRequest'; import type { NonFunctionProperties } from '@server/interfaces/api/common'; import type { MovieDetails } from '@server/models/Movie'; import type { TvDetails } from '@server/models/Tv'; -import Image from 'next/image'; import Link from 'next/link'; import { useEffect, useState } from 'react'; import { useInView } from 'react-intersection-observer'; @@ -116,7 +116,8 @@ const RequestCardError = ({ requestData }: RequestCardErrorProps) => { className="group flex items-center" > - { + const settings = useSettings(); const { ref, inView } = useInView({ triggerOnce: true, }); @@ -346,6 +348,7 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => { {title.backdropPath && (
{ className="group flex items-center" > - { {intl.formatMessage(messages.seasons, { seasonCount: - title.seasons.filter((season) => season.seasonNumber !== 0) - .length === request.seasons.length + (settings.currentSettings.enableSpecialEpisodes + ? title.seasons.length + : title.seasons.filter( + (season) => season.seasonNumber !== 0 + ).length) === request.seasons.length ? 0 : request.seasons.length, })} @@ -418,7 +425,11 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
{request.seasons.map((season) => ( - {season.seasonNumber} + + {season.seasonNumber === 0 + ? intl.formatMessage(globalMessages.specials) + : season.seasonNumber} + ))}
@@ -603,6 +614,7 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => { className="w-20 flex-shrink-0 scale-100 transform-gpu cursor-pointer overflow-hidden rounded-md shadow-sm transition duration-300 hover:scale-105 hover:shadow-md sm:w-28" > - - { + const settings = useSettings(); const { ref, inView } = useInView({ triggerOnce: true, }); @@ -342,6 +346,18 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => { revalidateList(); }; + const deleteMediaFile = async () => { + if (request.media) { + await fetch(`/api/v1/media/${request.media.id}/file`, { + method: 'DELETE', + }); + await fetch(`/api/v1/media/${request.media.id}`, { + method: 'DELETE', + }); + revalidateList(); + } + }; + const retryRequest = async () => { setRetrying(true); @@ -406,6 +422,7 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => { {title.backdropPath && (
{ className="relative h-auto w-12 flex-shrink-0 scale-100 transform-gpu overflow-hidden rounded-md transition duration-300 hover:scale-105" > { {intl.formatMessage(messages.seasons, { seasonCount: - title.seasons.filter( - (season) => season.seasonNumber !== 0 - ).length === request.seasons.length + (settings.currentSettings.enableSpecialEpisodes + ? title.seasons.length + : title.seasons.filter( + (season) => season.seasonNumber !== 0 + ).length) === request.seasons.length ? 0 : request.seasons.length, })} @@ -475,7 +495,11 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
{request.seasons.map((season) => ( - {season.seasonNumber} + + {season.seasonNumber === 0 + ? intl.formatMessage(globalMessages.specials) + : season.seasonNumber} + ))}
@@ -557,7 +581,8 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => { className="group flex items-center truncate" > - { className="group flex items-center truncate" > - { )} {requestData.status !== MediaRequestStatus.PENDING && hasPermission(Permission.MANAGE_REQUESTS) && ( - deleteRequest()} - confirmText={intl.formatMessage(globalMessages.areyousure)} - className="w-full" - > - - {intl.formatMessage(messages.deleterequest)} - + <> + deleteRequest()} + confirmText={intl.formatMessage(globalMessages.areyousure)} + className="w-full" + > + + {intl.formatMessage(messages.deleterequest)} + + deleteMediaFile()} + confirmText={intl.formatMessage(globalMessages.areyousure)} + className="w-full" + > + + + {intl.formatMessage(messages.removearr, { + arr: request.type === 'movie' ? 'Radarr' : 'Sonarr', + })} + + + )} {requestData.status === MediaRequestStatus.PENDING && hasPermission(Permission.MANAGE_REQUESTS) && ( diff --git a/src/components/RequestList/index.tsx b/src/components/RequestList/index.tsx index 8d56d56e..6cdf5b0b 100644 --- a/src/components/RequestList/index.tsx +++ b/src/components/RequestList/index.tsx @@ -2,13 +2,16 @@ import Button from '@app/components/Common/Button'; import Header from '@app/components/Common/Header'; import LoadingSpinner from '@app/components/Common/LoadingSpinner'; import PageTitle from '@app/components/Common/PageTitle'; +import Tooltip from '@app/components/Common/Tooltip'; import RequestItem from '@app/components/RequestList/RequestItem'; import { useUpdateQueryParams } from '@app/hooks/useUpdateQueryParams'; import { useUser } from '@app/hooks/useUser'; import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; import { - BarsArrowDownIcon, + ArrowDownIcon, + ArrowUpIcon, + Bars3BottomLeftIcon, ChevronLeftIcon, ChevronRightIcon, FunnelIcon, @@ -25,6 +28,7 @@ const messages = defineMessages('components.RequestList', { showallrequests: 'Show All Requests', sortAdded: 'Most Recent', sortModified: 'Last Modified', + sortDirection: 'Toggle Sort Direction', }); enum Filter { @@ -39,6 +43,8 @@ enum Filter { type Sort = 'added' | 'modified'; +type SortDirection = 'asc' | 'desc'; + const RequestList = () => { const router = useRouter(); const intl = useIntl(); @@ -48,6 +54,8 @@ const RequestList = () => { const { user: currentUser } = useUser(); const [currentFilter, setCurrentFilter] = useState(Filter.PENDING); const [currentSort, setCurrentSort] = useState('added'); + const [currentSortDirection, setCurrentSortDirection] = + useState('desc'); const [currentPageSize, setCurrentPageSize] = useState(10); const page = router.query.page ? Number(router.query.page) : 1; @@ -61,7 +69,7 @@ const RequestList = () => { } = useSWR( `/api/v1/request?take=${currentPageSize}&skip=${ pageIndex * currentPageSize - }&filter=${currentFilter}&sort=${currentSort}${ + }&filter=${currentFilter}&sort=${currentSort}&sortDirection=${currentSortDirection}${ router.pathname.startsWith('/profile') ? `&requestedBy=${currentUser?.id}` : router.query.userId @@ -80,6 +88,9 @@ const RequestList = () => { setCurrentFilter(filterSettings.currentFilter); setCurrentSort(filterSettings.currentSort); setCurrentPageSize(filterSettings.currentPageSize); + if (['asc', 'desc'].includes(filterSettings.currentSortDirection)) { + setCurrentSortDirection(filterSettings.currentSortDirection); + } } // If filter value is provided in query, use that instead @@ -95,10 +106,11 @@ const RequestList = () => { JSON.stringify({ currentFilter, currentSort, + currentSortDirection, currentPageSize, }) ); - }, [currentFilter, currentSort, currentPageSize]); + }, [currentFilter, currentSort, currentSortDirection, currentPageSize]); if (!data && !error) { return ; @@ -182,7 +194,7 @@ const RequestList = () => {
- + + + +
diff --git a/src/components/RequestModal/AdvancedRequester/index.tsx b/src/components/RequestModal/AdvancedRequester/index.tsx index 18aff5e2..ad11db82 100644 --- a/src/components/RequestModal/AdvancedRequester/index.tsx +++ b/src/components/RequestModal/AdvancedRequester/index.tsx @@ -1,4 +1,5 @@ /* eslint-disable react-hooks/exhaustive-deps */ +import CachedImage from '@app/components/Common/CachedImage'; import { SmallLoadingSpinner } from '@app/components/Common/LoadingSpinner'; import type { User } from '@app/hooks/useUser'; import { Permission, useUser } from '@app/hooks/useUser'; @@ -14,7 +15,6 @@ import type { import type { UserResultsResponse } from '@server/interfaces/api/userInterfaces'; import { hasPermission } from '@server/lib/permissions'; import { isEqual } from 'lodash'; -import Image from 'next/image'; import { useEffect, useMemo, useState } from 'react'; import { useIntl } from 'react-intl'; import Select from 'react-select'; @@ -561,7 +561,8 @@ const AdvancedRequester = ({ - - { - return (data?.parts ?? []).map((part) => part.id); + return (data?.parts ?? []) + .filter((part) => part.mediaInfo?.status !== MediaStatus.BLACKLISTED) + .map((part) => part.id); }; const getAllRequestedParts = (): number[] => { @@ -248,6 +250,11 @@ const CollectionRequestModal = ({ { type: 'or' } ); + const blacklistVisibility = hasPermission( + [Permission.MANAGE_BLACKLIST, Permission.VIEW_BLACKLIST], + { type: 'or' } + ); + return ( - {data?.parts.map((part) => { - const partRequest = getPartRequest(part.id); - const partMedia = - part.mediaInfo && - part.mediaInfo[is4k ? 'status4k' : 'status'] !== - MediaStatus.UNKNOWN - ? part.mediaInfo - : undefined; + {data?.parts + .filter((part) => { + if (!blacklistVisibility) + return ( + part.mediaInfo?.status !== MediaStatus.BLACKLISTED + ); + return part; + }) + .map((part) => { + const partRequest = getPartRequest(part.id); + const partMedia = + part.mediaInfo && + part.mediaInfo[is4k ? 'status4k' : 'status'] !== + MediaStatus.UNKNOWN + ? part.mediaInfo + : undefined; - return ( - - - togglePart(part.id)} - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === 'Space') { - togglePart(part.id); - } - }} - className={`relative inline-flex h-5 w-10 flex-shrink-0 cursor-pointer items-center justify-center pt-2 focus:outline-none ${ - !!partMedia || - partRequest || - (quota?.movie.limit && - currentlyRemaining <= 0 && - !isSelectedPart(part.id)) - ? 'opacity-50' - : '' + return ( + + - - - - -
- togglePart(part.id)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === 'Space') { + togglePart(part.id); + } }} - width={600} - height={900} - /> -
-
-
- {part.releaseDate?.slice(0, 4)} + className={`relative inline-flex h-5 w-10 flex-shrink-0 cursor-pointer items-center justify-center pt-2 focus:outline-none ${ + (!!partMedia && + partMedia.status !== + MediaStatus.BLACKLISTED) || + partRequest || + (quota?.movie.limit && + currentlyRemaining <= 0 && + !isSelectedPart(part.id)) + ? 'opacity-50' + : '' + }`} + > + + + + + +
+
-
- {part.title} +
+
+ {part.releaseDate?.slice(0, 4)} +
+
+ {part.title} +
-
- - - {!partMedia && !partRequest && ( - - {intl.formatMessage(globalMessages.notrequested)} - - )} - {!partMedia && - partRequest?.status === - MediaRequestStatus.PENDING && ( - - {intl.formatMessage(globalMessages.pending)} + + + {!partMedia && !partRequest && ( + + {intl.formatMessage( + globalMessages.notrequested + )} )} - {((!partMedia && - partRequest?.status === - MediaRequestStatus.APPROVED) || - partMedia?.[is4k ? 'status4k' : 'status'] === - MediaStatus.PROCESSING) && ( - - {intl.formatMessage(globalMessages.requested)} - - )} - {partMedia?.[is4k ? 'status4k' : 'status'] === - MediaStatus.AVAILABLE && ( - - {intl.formatMessage(globalMessages.available)} - - )} - - - ); - })} + {!partMedia && + partRequest?.status === + MediaRequestStatus.PENDING && ( + + {intl.formatMessage(globalMessages.pending)} + + )} + {((!partMedia && + partRequest?.status === + MediaRequestStatus.APPROVED) || + partMedia?.[is4k ? 'status4k' : 'status'] === + MediaStatus.PROCESSING) && ( + + {intl.formatMessage(globalMessages.requested)} + + )} + {partMedia?.[is4k ? 'status4k' : 'status'] === + MediaStatus.AVAILABLE && ( + + {intl.formatMessage(globalMessages.available)} + + )} + {partMedia?.status === MediaStatus.BLACKLISTED && ( + + {intl.formatMessage(globalMessages.blacklisted)} + + )} + + + ); + })}
diff --git a/src/components/RequestModal/SearchByNameModal/index.tsx b/src/components/RequestModal/SearchByNameModal/index.tsx index 0ef7e55b..1b86b614 100644 --- a/src/components/RequestModal/SearchByNameModal/index.tsx +++ b/src/components/RequestModal/SearchByNameModal/index.tsx @@ -88,14 +88,14 @@ const SearchByNameModal = ({ tvdbId === item.tvdbId ? 'ring ring-indigo-500' : '' } `} > -
+
{item.title}
diff --git a/src/components/RequestModal/TvRequestModal.tsx b/src/components/RequestModal/TvRequestModal.tsx index 71750678..7480578d 100644 --- a/src/components/RequestModal/TvRequestModal.tsx +++ b/src/components/RequestModal/TvRequestModal.tsx @@ -42,7 +42,6 @@ const messages = defineMessages('components.RequestModal', { season: 'Season', numberofepisodes: '# of Episodes', seasonnumber: 'Season {number}', - extras: 'Extras', errorediting: 'Something went wrong while editing the request.', requestedited: 'Request for {title} edited successfully!', requestApproved: 'Request for {title} approved!', @@ -254,11 +253,13 @@ const TvRequestModal = ({ }; const getAllSeasons = (): number[] => { - return (data?.seasons ?? []) - .filter( - (season) => season.seasonNumber !== 0 && season.episodeCount !== 0 - ) - .map((season) => season.seasonNumber); + let allSeasons = (data?.seasons ?? []).filter( + (season) => season.episodeCount !== 0 + ); + if (!settings.currentSettings.enableSpecialEpisodes) { + allSeasons = allSeasons.filter((season) => season.seasonNumber > 0); + } + return allSeasons.map((season) => season.seasonNumber); }; const getAllRequestedSeasons = (): number[] => { @@ -582,7 +583,9 @@ const TvRequestModal = ({ {data?.seasons .filter( (season) => - season.seasonNumber !== 0 && season.episodeCount !== 0 + (!settings.currentSettings.enableSpecialEpisodes + ? season.seasonNumber !== 0 + : true) && season.episodeCount !== 0 ) .map((season) => { const seasonRequest = getSeasonRequest( @@ -660,7 +663,7 @@ const TvRequestModal = ({ {season.seasonNumber === 0 - ? intl.formatMessage(messages.extras) + ? intl.formatMessage(globalMessages.specials) : intl.formatMessage(messages.seasonnumber, { number: season.seasonNumber, })} diff --git a/src/components/Selector/index.tsx b/src/components/Selector/index.tsx index a371b7f9..6c831909 100644 --- a/src/components/Selector/index.tsx +++ b/src/components/Selector/index.tsx @@ -13,6 +13,7 @@ import type { TmdbKeywordSearchResponse, } from '@server/api/themoviedb/interfaces'; import type { GenreSliderItem } from '@server/interfaces/api/discoverInterfaces'; +import type { UserResultsResponse } from '@server/interfaces/api/userInterfaces'; import type { Keyword, ProductionCompany, @@ -29,6 +30,7 @@ const messages = defineMessages('components.Selector', { searchKeywords: 'Search keywords…', searchGenres: 'Select genres…', searchStudios: 'Search studios…', + searchUsers: 'Select users…', starttyping: 'Starting typing to search.', nooptions: 'No results.', showmore: 'Show More', @@ -374,7 +376,11 @@ export const WatchProviderSelector = ({ const { currentSettings } = useSettings(); const [showMore, setShowMore] = useState(false); const [watchRegion, setWatchRegion] = useState( - region ? region : currentSettings.region ? currentSettings.region : 'US' + region + ? region + : currentSettings.discoverRegion + ? currentSettings.discoverRegion + : 'US' ); const [activeProvider, setActiveProvider] = useState( activeProviders ?? [] @@ -437,7 +443,7 @@ export const WatchProviderSelector = ({ key={`prodiver-${provider.id}`} >
- +
+ +
{isActive && (
@@ -482,7 +486,7 @@ export const WatchProviderSelector = ({ key={`prodiver-${provider.id}`} >
- +
+ +
{isActive && (
@@ -546,3 +548,77 @@ export const WatchProviderSelector = ({ ); }; + +export const UserSelector = ({ + isMulti, + defaultValue, + onChange, +}: BaseSelectorMultiProps | BaseSelectorSingleProps) => { + const intl = useIntl(); + const [defaultDataValue, setDefaultDataValue] = useState< + { label: string; value: number }[] | null + >(null); + + useEffect(() => { + const loadUsers = async (): Promise => { + if (!defaultValue) { + return; + } + + const users = defaultValue.split(','); + + const res = await fetch(`/api/v1/user`); + if (!res.ok) { + throw new Error('Network response was not ok'); + } + const response: UserResultsResponse = await res.json(); + + const genreData = users + .filter((u) => response.results.find((user) => user.id === Number(u))) + .map((u) => response.results.find((user) => user.id === Number(u))) + .map((u) => ({ + label: u?.displayName ?? '', + value: u?.id ?? 0, + })); + + setDefaultDataValue(genreData); + }; + + loadUsers(); + }, [defaultValue]); + + const loadUserOptions = async (inputValue: string) => { + const res = await fetch( + `/api/v1/user${inputValue ? `?q=${encodeURIComponent(inputValue)}` : ''}` + ); + if (!res.ok) throw new Error(); + const results: UserResultsResponse = await res.json(); + + return results.results + .map((result) => ({ + label: result.displayName, + value: result.id, + })) + .filter(({ label }) => + label.toLowerCase().includes(inputValue.toLowerCase()) + ); + }; + + return ( + { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + onChange(value as any); + }} + /> + ); +}; diff --git a/src/components/Settings/Notifications/NotificationsDiscord.tsx b/src/components/Settings/Notifications/NotificationsDiscord.tsx index b62263fb..82ac6840 100644 --- a/src/components/Settings/Notifications/NotificationsDiscord.tsx +++ b/src/components/Settings/Notifications/NotificationsDiscord.tsx @@ -19,12 +19,16 @@ const messages = defineMessages('components.Settings.Notifications', { webhookUrl: 'Webhook URL', webhookUrlTip: 'Create a webhook integration in your server', + webhookRoleId: 'Notification Role ID', + webhookRoleIdTip: + 'The role ID to mention in the webhook message. Leave empty to disable mentions', discordsettingssaved: 'Discord notification settings saved successfully!', discordsettingsfailed: 'Discord notification settings failed to save.', toastDiscordTestSending: 'Sending Discord test notification…', toastDiscordTestSuccess: 'Discord test notification sent!', toastDiscordTestFailed: 'Discord test notification failed to send.', validationUrl: 'You must provide a valid URL', + validationWebhookRoleId: 'You must provide a valid Discord Role ID', validationTypes: 'You must select at least one notification type', enableMentions: 'Enable Mentions', }); @@ -53,6 +57,12 @@ const NotificationsDiscord = () => { otherwise: Yup.string().nullable(), }) .url(intl.formatMessage(messages.validationUrl)), + webhookRoleId: Yup.string() + .nullable() + .matches( + /^\d{17,19}$/, + intl.formatMessage(messages.validationWebhookRoleId) + ), }); if (!data && !error) { @@ -67,6 +77,7 @@ const NotificationsDiscord = () => { botUsername: data?.options.botUsername, botAvatarUrl: data?.options.botAvatarUrl, webhookUrl: data.options.webhookUrl, + webhookRoleId: data?.options.webhookRoleId, enableMentions: data?.options.enableMentions, }} validationSchema={NotificationsDiscordSchema} @@ -84,6 +95,7 @@ const NotificationsDiscord = () => { botUsername: values.botUsername, botAvatarUrl: values.botAvatarUrl, webhookUrl: values.webhookUrl, + webhookRoleId: values.webhookRoleId, enableMentions: values.enableMentions, }, }), @@ -141,6 +153,7 @@ const NotificationsDiscord = () => { botUsername: values.botUsername, botAvatarUrl: values.botAvatarUrl, webhookUrl: values.webhookUrl, + webhookRoleId: values.webhookRoleId, enableMentions: values.enableMentions, }, }), @@ -254,6 +267,21 @@ const NotificationsDiscord = () => { )}
+
+ +
+
+ +
+ {errors.webhookRoleId && + touched.webhookRoleId && + typeof errors.webhookRoleId === 'string' && ( +
{errors.webhookRoleId}
+ )} +
+
+
+ +
+
+ +
+ {errors.messageThreadId && + touched.messageThreadId && + typeof errors.messageThreadId === 'string' && ( +
{errors.messageThreadId}
+ )} +
+
+ {radarr && ( + <> +

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

+
    + {rules && ( + + )} +
  • +
    + +
    +
  • +
+ + )} ); }} diff --git a/src/components/Settings/SettingsJellyfin.tsx b/src/components/Settings/SettingsJellyfin.tsx index 316dc48e..7c6d02d8 100644 --- a/src/components/Settings/SettingsJellyfin.tsx +++ b/src/components/Settings/SettingsJellyfin.tsx @@ -139,7 +139,10 @@ const SettingsJellyfin: React.FC = ({ ), jellyfinExternalUrl: Yup.string() .nullable() - .url(intl.formatMessage(messages.validationUrl)) + .matches( + /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}(\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&/=]*))?$/i, + intl.formatMessage(messages.validationUrl) + ) .test( 'no-trailing-slash', intl.formatMessage(messages.validationUrlTrailingSlash), @@ -147,7 +150,10 @@ const SettingsJellyfin: React.FC = ({ ), jellyfinForgotPasswordUrl: Yup.string() .nullable() - .url(intl.formatMessage(messages.validationUrl)) + .matches( + /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}(\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&/=]*))?$/i, + intl.formatMessage(messages.validationUrl) + ) .test( 'no-trailing-slash', intl.formatMessage(messages.validationUrlTrailingSlash), diff --git a/src/components/Settings/SettingsJobsCache/index.tsx b/src/components/Settings/SettingsJobsCache/index.tsx index 2916276e..aeba1531 100644 --- a/src/components/Settings/SettingsJobsCache/index.tsx +++ b/src/components/Settings/SettingsJobsCache/index.tsx @@ -58,6 +58,7 @@ const messages: { [messageName: string]: MessageDescriptor } = defineMessages( 'plex-recently-added-scan': 'Plex Recently Added Scan', 'plex-full-scan': 'Plex Full Library Scan', 'plex-watchlist-sync': 'Plex Watchlist Sync', + 'plex-refresh-token': 'Plex Refresh Token', 'jellyfin-full-scan': 'Jellyfin Full Library Scan', 'jellyfin-recently-added-scan': 'Jellyfin Recently Added Scan', 'availability-sync': 'Media Availability Sync', @@ -82,6 +83,7 @@ const messages: { [messageName: string]: MessageDescriptor } = defineMessages( 'When enabled in settings, Jellyseerr will proxy and cache images from pre-configured external sources. Cached images are saved into your config folder. You can find the files in {appDataPath}/cache/images.', imagecachecount: 'Images Cached', imagecachesize: 'Total Cache Size', + usersavatars: "Users' Avatars", } ); @@ -573,6 +575,19 @@ const SettingsJobs = () => { {formatBytes(cacheData?.imageCache.tmdb.size ?? 0)} + + + {intl.formatMessage(messages.usersavatars)} (avatar) + + + {intl.formatNumber( + cacheData?.imageCache.avatar.imageCount ?? 0 + )} + + + {formatBytes(cacheData?.imageCache.avatar.size ?? 0)} + +
diff --git a/src/components/Settings/SettingsLayout.tsx b/src/components/Settings/SettingsLayout.tsx index 6336bad0..dd7cd6fa 100644 --- a/src/components/Settings/SettingsLayout.tsx +++ b/src/components/Settings/SettingsLayout.tsx @@ -13,6 +13,7 @@ const messages = defineMessages('components.Settings', { menuPlexSettings: 'Plex', menuJellyfinSettings: '{mediaServerName}', menuServices: 'Services', + menuNetwork: 'Network', menuNotifications: 'Notifications', menuLogs: 'Logs', menuJobs: 'Jobs & Cache', @@ -53,6 +54,11 @@ const SettingsLayout = ({ children }: SettingsLayoutProps) => { route: '/settings/services', regex: /^\/settings\/services/, }, + { + text: intl.formatMessage(messages.menuNetwork), + route: '/settings/network', + regex: /^\/settings\/network/, + }, { text: intl.formatMessage(messages.menuNotifications), route: '/settings/notifications/email', diff --git a/src/components/Settings/SettingsLogs/index.tsx b/src/components/Settings/SettingsLogs/index.tsx index df819d4c..ef7a396f 100644 --- a/src/components/Settings/SettingsLogs/index.tsx +++ b/src/components/Settings/SettingsLogs/index.tsx @@ -245,7 +245,9 @@ const SettingsLogs = () => {

{intl.formatMessage(messages.logsDescription, { code: (msg: React.ReactNode) => ( - {msg} + + {msg} + ), appDataPath: appData ? appData.appDataPath : '/app/config', })} diff --git a/src/components/Settings/SettingsMain/index.tsx b/src/components/Settings/SettingsMain/index.tsx index f7aac0d9..5ccfaaa2 100644 --- a/src/components/Settings/SettingsMain/index.tsx +++ b/src/components/Settings/SettingsMain/index.tsx @@ -2,7 +2,6 @@ import Button from '@app/components/Common/Button'; import LoadingSpinner from '@app/components/Common/LoadingSpinner'; import PageTitle from '@app/components/Common/PageTitle'; import SensitiveInput from '@app/components/Common/SensitiveInput'; -import Tooltip from '@app/components/Common/Tooltip'; import LanguageSelector from '@app/components/LanguageSelector'; import RegionSelector from '@app/components/RegionSelector'; import CopyButton from '@app/components/Settings/CopyButton'; @@ -31,29 +30,25 @@ const messages = defineMessages('components.Settings.SettingsMain', { apikey: 'API Key', applicationTitle: 'Application Title', applicationurl: 'Application URL', - region: 'Discover Region', - regionTip: 'Filter content by regional availability', + discoverRegion: 'Discover Region', + discoverRegionTip: 'Filter content by regional availability', originallanguage: 'Discover Language', originallanguageTip: 'Filter content by original language', + streamingRegion: 'Streaming Region', + streamingRegionTip: 'Show streaming sites by regional availability', toastApiKeySuccess: 'New API key generated successfully!', toastApiKeyFailure: 'Something went wrong while generating a new API key.', toastSettingsSuccess: 'Settings saved successfully!', toastSettingsFailure: 'Something went wrong while saving settings.', hideAvailable: 'Hide Available Media', - csrfProtection: 'Enable CSRF Protection', - csrfProtectionTip: 'Set external API access to read-only (requires HTTPS)', - csrfProtectionHoverTip: - 'Do NOT enable this setting unless you understand what you are doing!', cacheImages: 'Enable Image Caching', cacheImagesTip: 'Cache externally sourced images (requires a significant amount of disk space)', - trustProxy: 'Enable Proxy Support', - trustProxyTip: - 'Allow Jellyseerr to correctly register client IP addresses behind a proxy', validationApplicationTitle: 'You must provide an application title', validationApplicationUrl: 'You must provide a valid URL', validationApplicationUrlTrailingSlash: 'URL must not end in a trailing slash', partialRequestsEnabled: 'Allow Partial Series Requests', + enableSpecialEpisodes: 'Allow Special Episodes Requests', locale: 'Display Language', }); @@ -76,7 +71,10 @@ const SettingsMain = () => { intl.formatMessage(messages.validationApplicationTitle) ), applicationUrl: Yup.string() - .url(intl.formatMessage(messages.validationApplicationUrl)) + .matches( + /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}(\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&/=]*))?$/i, + intl.formatMessage(messages.validationApplicationUrl) + ) .test( 'no-trailing-slash', intl.formatMessage(messages.validationApplicationUrlTrailingSlash), @@ -129,13 +127,13 @@ const SettingsMain = () => { initialValues={{ applicationTitle: data?.applicationTitle, applicationUrl: data?.applicationUrl, - csrfProtection: data?.csrfProtection, hideAvailable: data?.hideAvailable, locale: data?.locale ?? 'en', - region: data?.region, + discoverRegion: data?.discoverRegion, originalLanguage: data?.originalLanguage, + streamingRegion: data?.streamingRegion || 'US', partialRequestsEnabled: data?.partialRequestsEnabled, - trustProxy: data?.trustProxy, + enableSpecialEpisodes: data?.enableSpecialEpisodes, cacheImages: data?.cacheImages, }} enableReinitialize @@ -150,13 +148,13 @@ const SettingsMain = () => { body: JSON.stringify({ applicationTitle: values.applicationTitle, applicationUrl: values.applicationUrl, - csrfProtection: values.csrfProtection, hideAvailable: values.hideAvailable, locale: values.locale, - region: values.region, + discoverRegion: values.discoverRegion, + streamingRegion: values.streamingRegion, originalLanguage: values.originalLanguage, partialRequestsEnabled: values.partialRequestsEnabled, - trustProxy: values.trustProxy, + enableSpecialEpisodes: values.enableSpecialEpisodes, cacheImages: values.cacheImages, }), }); @@ -266,58 +264,6 @@ const SettingsMain = () => { )}

-
- -
- { - setFieldValue('trustProxy', !values.trustProxy); - }} - /> -
-
-
- -
- - { - setFieldValue( - 'csrfProtection', - !values.csrfProtection - ); - }} - /> - -
-
-