diff --git a/.all-contributorsrc b/.all-contributorsrc index a230a4685..3cf5e765c 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -665,6 +665,78 @@ "contributions": [ "translation" ] + }, + { + "login": "sambartik", + "name": "Samuel Bartík", + "avatar_url": "https://avatars.githubusercontent.com/u/63553146?v=4", + "profile": "https://github.com/sambartik", + "contributions": [ + "code" + ] + }, + { + "login": "frank-cywong", + "name": "Chun Yeung Wong", + "avatar_url": "https://avatars.githubusercontent.com/u/90653148?v=4", + "profile": "https://github.com/frank-cywong", + "contributions": [ + "code" + ] + }, + { + "login": "TheMeanCanEHdian", + "name": "TheMeanCanEHdian", + "avatar_url": "https://avatars.githubusercontent.com/u/16025103?v=4", + "profile": "https://github.com/TheMeanCanEHdian", + "contributions": [ + "code" + ] + }, + { + "login": "Gylesie", + "name": "Gylesie", + "avatar_url": "https://avatars.githubusercontent.com/u/86306812?v=4", + "profile": "https://github.com/Gylesie", + "contributions": [ + "code" + ] + }, + { + "login": "Fhd-pro", + "name": "Fhd-pro", + "avatar_url": "https://avatars.githubusercontent.com/u/82862079?v=4", + "profile": "https://github.com/Fhd-pro", + "contributions": [ + "translation" + ] + }, + { + "login": "PovilasID", + "name": "PovilasID", + "avatar_url": "https://avatars.githubusercontent.com/u/396243?v=4", + "profile": "https://github.com/PovilasID", + "contributions": [ + "translation" + ] + }, + { + "login": "byakurau", + "name": "byakurau", + "avatar_url": "https://avatars.githubusercontent.com/u/1811683?v=4", + "profile": "https://github.com/byakurau", + "contributions": [ + "translation" + ] + }, + { + "login": "miknii", + "name": "miknii", + "avatar_url": "https://avatars.githubusercontent.com/u/109232569?v=4", + "profile": "https://github.com/miknii", + "contributions": [ + "translation" + ] } ], "badgeTemplate": "\"All-orange.svg\"/>", @@ -673,5 +745,5 @@ "projectOwner": "sct", "repoType": "github", "repoHost": "https://github.com", - "skipCi": true + "skipCi": false } diff --git a/.dockerignore b/.dockerignore index 7d669c86d..21a5da869 100644 --- a/.dockerignore +++ b/.dockerignore @@ -26,3 +26,4 @@ public/os_logo_filled.png public/preview.jpg snap stylelint.config.js +cypress diff --git a/.eslintrc.js b/.eslintrc.js index b1c6f4b9f..5af484c53 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -7,6 +7,7 @@ module.exports = { 'plugin:jsx-a11y/recommended', 'plugin:react/recommended', 'plugin:react-hooks/recommended', + 'plugin:react/jsx-runtime', 'prettier', ], parserOptions: { @@ -26,11 +27,21 @@ module.exports = { 'react-hooks/rules-of-hooks': 'error', 'react-hooks/exhaustive-deps': 'warn', '@typescript-eslint/explicit-function-return-type': 'off', - 'prettier/prettier': ['error', { endOfLine: 'auto' }], 'formatjs/no-offset': 'error', 'no-unused-vars': 'off', '@typescript-eslint/no-unused-vars': ['error'], + '@typescript-eslint/array-type': ['error', { default: 'array' }], 'jsx-a11y/no-onchange': 'off', + '@typescript-eslint/consistent-type-imports': [ + 'error', + { + prefer: 'type-imports', + }, + ], + 'no-relative-import-paths/no-relative-import-paths': [ + 'error', + { allowSameFolder: true }, + ], }, overrides: [ { @@ -40,7 +51,7 @@ module.exports = { }, }, ], - plugins: ['jsx-a11y', 'prettier', 'react-hooks', 'formatjs'], + plugins: ['jsx-a11y', 'react-hooks', 'formatjs', 'no-relative-import-paths'], settings: { react: { pragma: 'React', diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 44215bded..d2026ee71 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,7 +3,7 @@ name: Jellyseerr CI on: pull_request: branches: - - "*" + - '*' push: branches: - develop @@ -13,16 +13,18 @@ jobs: name: Lint & Test Build if: github.event_name == 'pull_request' runs-on: ubuntu-20.04 - container: node:16.14-alpine + container: node:16.17-alpine steps: - name: Checkout uses: actions/checkout@v3 - name: Install dependencies env: - HUSKY_SKIP_INSTALL: 1 + HUSKY: 0 run: yarn - name: Lint run: yarn lint + - name: Formatting + run: yarn format:check - name: Build run: yarn build @@ -34,23 +36,29 @@ jobs: - name: Checkout uses: actions/checkout@v3 - name: Set up QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v2 - name: Cache Docker layers - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: /tmp/.buildx-cache key: ${{ runner.os }}-buildx-${{ github.sha }} restore-keys: | ${{ runner.os }}-buildx- - name: Log in to Docker Hub - uses: docker/login-action@v1 + uses: docker/login-action@v2 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_TOKEN }} + - name: Log in to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v3 with: context: . file: ./Dockerfile @@ -77,7 +85,7 @@ jobs: runs-on: ubuntu-20.04 steps: - name: Get Build Job Status - uses: technote-space/workflow-conclusion-action@v2 + uses: technote-space/workflow-conclusion-action@v3 - name: Combine Job Status id: status run: | diff --git a/.github/workflows/cypress.yml b/.github/workflows/cypress.yml new file mode 100644 index 000000000..ecd260dd5 --- /dev/null +++ b/.github/workflows/cypress.yml @@ -0,0 +1,30 @@ +name: Cypress Tests + +on: + pull_request: + branches: + - '*' + push: + branches: + - develop + +jobs: + cypress-run: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Cypress run + uses: cypress-io/github-action@v4 + with: + build: yarn cypress:build + start: yarn start + wait-on: 'http://localhost:5055' + record: true + env: + CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + WITH_MIGRATIONS: true + # Fix test titles in cypress dashboard + COMMIT_INFO_MESSAGE: ${{github.event.pull_request.title}} + COMMIT_INFO_SHA: ${{github.event.pull_request.head.sha}} diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 5d104c7c8..35ae768b7 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -3,7 +3,7 @@ name: Jellyseerr Preview on: push: tags: - - "preview-*" + - 'preview-*' jobs: build_and_push: @@ -16,16 +16,16 @@ jobs: id: get_version run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/} - name: Set up QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v2 - name: Log in to Docker Hub - uses: docker/login-action@v1 + uses: docker/login-action@v2 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_TOKEN }} - name: Build and push - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v3 with: context: . file: ./Dockerfile diff --git a/.github/workflows/private_registery_push.yml b/.github/workflows/private_registery_push.yml deleted file mode 100644 index 376223c77..000000000 --- a/.github/workflows/private_registery_push.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: 'create docker image on pull request and push to private registery' - -on: - pull_request: - branches: - - develop - workflow_dispatch: - -jobs: - build-image: - runs-on: self-hosted - steps: - - - name: Checkout - uses: actions/checkout@v2 - - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 - - - - name: Login to private registery - uses: docker/login-action@v2.0.0 - with: - registry: ${{ secrets.REGISTRY_URL }} - username: ${{ secrets.REGISTRY_USERNAME }} - password: ${{ secrets.REGISTRY_PASSWORD }} - - - - name: Build and push - uses: docker/build-push-action@v2 - with: - context: ./ - file: ./Dockerfile - builder: ${{ steps.buildx.outputs.name }} - push: true - tags: '${{ secrets.REGISTRY_URL }}/fallenbagel/jellyseerr:${{ github.sha }}' - cache-from: 'type=registry,ref=${{ secrets.REGISTRY_URL }}/fallenbagel/jellyseerr:buildcache' - cache-to: 'type=registry,ref=${{ secrets.REGISTRY_URL }}/fallenbagel/jellyseerr:buildcache,mode=max' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 66a1ac47f..8890dcae3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,7 +5,7 @@ on: workflow_dispatch jobs: semantic-release: name: Tag and release latest version - runs-on: self-hosted + runs-on: ubuntu-20.04 env: HUSKY: 0 steps: @@ -18,16 +18,14 @@ jobs: with: node-version: 16 - name: Set up QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v2 - name: Log in to Docker Hub - uses: docker/login-action@v1 + uses: docker/login-action@v2 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_TOKEN }} - - name: Install Yarn - run: npm install -g yarn - name: Install dependencies run: yarn - name: Release @@ -37,6 +35,60 @@ jobs: DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} run: npx semantic-release + build-snap: + name: Build Snap Package (${{ matrix.architecture }}) + needs: semantic-release + runs-on: ubuntu-20.04 + strategy: + fail-fast: false + matrix: + architecture: + - amd64 + - arm64 + - armhf + steps: + - name: Checkout Code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Switch to master branch + run: git checkout master + - name: Pull latest changes + run: git pull + - name: Prepare + id: prepare + run: | + git fetch --prune --tags + if [[ $GITHUB_REF == refs/tags/* || $GITHUB_REF == refs/heads/master ]]; then + echo ::set-output name=RELEASE::stable + else + echo ::set-output name=RELEASE::edge + fi + - name: Set Up QEMU + uses: docker/setup-qemu-action@v1 + with: + image: tonistiigi/binfmt@sha256:df15403e06a03c2f461c1f7938b171fda34a5849eb63a70e2a2109ed5a778bde + - name: Build Snap Package + uses: diddlesnaps/snapcraft-multiarch-action@v1 + id: build + with: + architecture: ${{ matrix.architecture }} + - name: Upload Snap Package + uses: actions/upload-artifact@v2 + with: + name: overseerr-snap-package-${{ matrix.architecture }} + path: ${{ steps.build.outputs.snap }} + - name: Review Snap Package + uses: diddlesnaps/snapcraft-review-tools-action@v1 + with: + snap: ${{ steps.build.outputs.snap }} + - name: Publish Snap Package + uses: snapcore/action-publish@v1 + with: + store_login: ${{ secrets.SNAP_LOGIN }} + snap: ${{ steps.build.outputs.snap }} + release: ${{ steps.prepare.outputs.RELEASE }} + discord: name: Send Discord Notification needs: semantic-release @@ -44,7 +96,7 @@ jobs: runs-on: self-hosted steps: - name: Get Build Job Status - uses: technote-space/workflow-conclusion-action@v2 + uses: technote-space/workflow-conclusion-action@v3 - name: Combine Job Status id: status run: | diff --git a/.github/workflows/snap.yaml b/.github/workflows/snap.yaml new file mode 100644 index 000000000..bf00e04d7 --- /dev/null +++ b/.github/workflows/snap.yaml @@ -0,0 +1,88 @@ +name: Publish Snap + +on: + push: + branches: + - develop + +jobs: + jobs: + name: Job Check + runs-on: ubuntu-20.04 + if: "!contains(github.event.head_commit.message, '[skip ci]')" + steps: + - name: Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.10.0 + with: + access_token: ${{ secrets.GITHUB_TOKEN }} + + build-snap: + name: Build Snap Package (${{ matrix.architecture }}) + needs: jobs + runs-on: ubuntu-20.04 + strategy: + fail-fast: false + matrix: + architecture: + - amd64 + - arm64 + - armhf + steps: + - name: Checkout Code + uses: actions/checkout@v3 + - name: Prepare + id: prepare + run: | + git fetch --prune --unshallow --tags + if [[ $GITHUB_REF == refs/tags/* || $GITHUB_REF == refs/heads/master ]]; then + echo ::set-output name=RELEASE::stable + else + echo ::set-output name=RELEASE::edge + fi + - name: Set Up QEMU + uses: docker/setup-qemu-action@v2 + - name: Build Snap Package + uses: diddlesnaps/snapcraft-multiarch-action@v1 + id: build + with: + architecture: ${{ matrix.architecture }} + - name: Upload Snap Package + uses: actions/upload-artifact@v3 + with: + name: overseerr-snap-package-${{ matrix.architecture }} + path: ${{ steps.build.outputs.snap }} + - name: Review Snap Package + uses: diddlesnaps/snapcraft-review-tools-action@v1 + with: + snap: ${{ steps.build.outputs.snap }} + - name: Publish Snap Package + uses: snapcore/action-publish@v1 + with: + store_login: ${{ secrets.SNAP_LOGIN }} + snap: ${{ steps.build.outputs.snap }} + release: ${{ steps.prepare.outputs.RELEASE }} + + discord: + name: Send Discord Notification + needs: build-snap + if: always() && !contains(github.event.head_commit.message, '[skip ci]') + runs-on: ubuntu-20.04 + steps: + - name: Get Build Job Status + uses: technote-space/workflow-conclusion-action@v3 + - name: Combine Job Status + id: status + run: | + failures=(neutral, skipped, timed_out, action_required) + if [[ ${array[@]} =~ $WORKFLOW_CONCLUSION ]]; then + echo ::set-output name=status::failure + else + echo ::set-output name=status::$WORKFLOW_CONCLUSION + fi + - name: Post Status to Discord + uses: sarisia/actions-status-discord@v1 + with: + webhook: ${{ secrets.DISCORD_WEBHOOK }} + status: ${{ steps.status.outputs.status }} + title: ${{ github.workflow }} + nofail: true diff --git a/.gitignore b/.gitignore index 41a0481fd..70a5d6f2f 100644 --- a/.gitignore +++ b/.gitignore @@ -54,5 +54,16 @@ config/db/db.sqlite3-journal # VS Code .vscode/launch.json +# Cypress +cypress.env.json +cypress/videos +cypress/screenshots + +# ESLint +.eslintcache + +# TS Build Info +tsconfig.tsbuildinfo + # Webstorm .idea diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 000000000..c735fffac --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,5 @@ +module.exports = { + plugins: [require('./merged-prettier-plugin.js')], + singleQuote: true, + trailingComma: 'es5', +}; diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 80a16c644..8dc1918fb 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -11,9 +11,6 @@ // https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode "esbenp.prettier-vscode", - // https://marketplace.visualstudio.com/items?itemName=eg2.vscode-npm-script - "eg2.vscode-npm-script", - // https://marketplace.visualstudio.com/items?itemName=Orta.vscode-jest "Orta.vscode-jest", diff --git a/.vscode/settings.json b/.vscode/settings.json index 26aca34b8..45da7ba67 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -15,8 +15,6 @@ "database": "./config/db/db.sqlite3" } ], - "editor.codeActionsOnSave": { - "source.organizeImports": true - }, - "editor.formatOnSave": true + "editor.formatOnSave": true, + "typescript.preferences.importModuleSpecifier": "non-relative" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 721c260ad..d7e9d1cb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,1272 +1,1197 @@ ## [1.29.1](https://github.com/sct/overseerr/compare/v1.29.0...v1.29.1) (2022-04-06) - ### Bug Fixes -* **auth:** resolve local/password authentication issues ([#2677](https://github.com/sct/overseerr/issues/2677)) ([b75fc7b](https://github.com/sct/overseerr/commit/b75fc7b2384ce760432620faaa92277dcd42b8e1)) +- **auth:** resolve local/password authentication issues ([#2677](https://github.com/sct/overseerr/issues/2677)) ([b75fc7b](https://github.com/sct/overseerr/commit/b75fc7b2384ce760432620faaa92277dcd42b8e1)) # [1.29.0](https://github.com/sct/overseerr/compare/v1.28.0...v1.29.0) (2022-04-01) - ### Bug Fixes -* add Discord ID setting to general user settings page ([#2406](https://github.com/sct/overseerr/issues/2406)) ([eff665e](https://github.com/sct/overseerr/commit/eff665ef4b688aac881408790304b77bd9a31ddb)) -* address unhandled promise rejections & bump node to v16.13 ([#2398](https://github.com/sct/overseerr/issues/2398)) ([8cba486](https://github.com/sct/overseerr/commit/8cba486249fed88232e93a688c8bfe0f6179c589)) -* **css:** rename form-input to form-input-area ([#2613](https://github.com/sct/overseerr/issues/2613)) ([086f0b6](https://github.com/sct/overseerr/commit/086f0b6ce23f607d20c2cec3c73b2e4d1ce9b426)) -* **email:** enclose PGP encryption logic in try/catch ([#2519](https://github.com/sct/overseerr/issues/2519)) ([a76b608](https://github.com/sct/overseerr/commit/a76b608ab796944c0c660e3296a7aca6615d69f3)) -* **frontend:** disable autocomplete on search field ([#2592](https://github.com/sct/overseerr/issues/2592)) ([82d1617](https://github.com/sct/overseerr/commit/82d16177bf763fe8097b4aae326793e3e21e847d)) -* **frontend:** theme-color meta tag ([#2420](https://github.com/sct/overseerr/issues/2420)) ([ff28c9b](https://github.com/sct/overseerr/commit/ff28c9bfebf4a930e2542ee3b3c35f8af4e1b97e)) -* **frontend:** various fixes ([#2524](https://github.com/sct/overseerr/issues/2524)) ([c3dbd0d](https://github.com/sct/overseerr/commit/c3dbd0d6913946e0e1b5308edfbb5ca744740223)) -* **lang:** rename 'Media' notification types for clarity ([#2400](https://github.com/sct/overseerr/issues/2400)) ([399b037](https://github.com/sct/overseerr/commit/399b0379186ed34dcc436bd95330fd1a05fef4b3)) -* **lang:** translations update from Hosted Weblate ([#2625](https://github.com/sct/overseerr/issues/2625)) ([19cdedd](https://github.com/sct/overseerr/commit/19cdedd2a6656b1a852e1cc653bbdb140e978b51)) -* **lang:** translations update from Hosted Weblate ([#2639](https://github.com/sct/overseerr/issues/2639)) ([418a533](https://github.com/sct/overseerr/commit/418a533588bbbdbbbb4caee1ef91d57c1ca35717)) -* **logs:** handle log message nested extra properties ([#2459](https://github.com/sct/overseerr/issues/2459)) ([d777940](https://github.com/sct/overseerr/commit/d7779408d162949b2eafcacefc8eabe53fae229f)) -* **notif:** duplicate notification check logic ([#2424](https://github.com/sct/overseerr/issues/2424)) ([10651ba](https://github.com/sct/overseerr/commit/10651baa675993f7109989bbac67f54661c8693f)) -* **notif:** show event in pop up notification for slack ([#2413](https://github.com/sct/overseerr/issues/2413)) ([d4438c8](https://github.com/sct/overseerr/commit/d4438c82e3753c9b29b6269ad406d263b3fcef4c)), closes [#2408](https://github.com/sct/overseerr/issues/2408) -* **plex:** correctly generate uuid for safari ([#2614](https://github.com/sct/overseerr/issues/2614)) ([d06f2cd](https://github.com/sct/overseerr/commit/d06f2cdb08bfa6f05cf7cec2c408a258fa926b09)) -* **plex:** find TV series in addition to movies from IMDb IDs ([#1830](https://github.com/sct/overseerr/issues/1830)) ([30644f6](https://github.com/sct/overseerr/commit/30644f65ea2e8437676422ae0b083c642a836887)) -* **plex:** include 'Overseerr' in X-Plex-Device-Name header ([#2635](https://github.com/sct/overseerr/issues/2635)) ([d4f9650](https://github.com/sct/overseerr/commit/d4f9650cd07704a97f8b591b7de7351c1e85b825)) -* **plex:** use unique client identifier ([#2602](https://github.com/sct/overseerr/issues/2602)) ([648b346](https://github.com/sct/overseerr/commit/648b346cbe5a941c7e1ec4ddfb276fb0e27ed502)) -* **plex:** user import ([#2442](https://github.com/sct/overseerr/issues/2442)) ([86dff12](https://github.com/sct/overseerr/commit/86dff12cdeef6dca92527dd31757a3a4c7f921bf)) -* **radarr:** correctly check for existing movies ([#2490](https://github.com/sct/overseerr/issues/2490)) ([5d4b06b](https://github.com/sct/overseerr/commit/5d4b06bbcc6cf6d328f6b4a86c4c0f9b0f3aff3e)) -* **radarr:** remove PreDB minimum availability option ([#2386](https://github.com/sct/overseerr/issues/2386)) ([3e5eb4e](https://github.com/sct/overseerr/commit/3e5eb4e148a9f88b871abc4ee1784b870f691534)) -* **requests:** check for existing media of same type when requesting ([#2445](https://github.com/sct/overseerr/issues/2445)) ([eb9ca2e](https://github.com/sct/overseerr/commit/eb9ca2e86f3be3f4ff8ee2e7c4aecdf337d8976d)) -* **sonarr:** monitor existing series upon request approval ([#2553](https://github.com/sct/overseerr/issues/2553)) ([aa062d9](https://github.com/sct/overseerr/commit/aa062d921c425d4b64bfdb28a5f102b0c92f7d87)) -* **sonarr:** only scan seasons that exist in TMDb ([#2523](https://github.com/sct/overseerr/issues/2523)) ([6168185](https://github.com/sct/overseerr/commit/61681857b123802aaeff02a8f61b1ba046c5d333)) -* **tautulli:** fetch additional user history as necessary to return 20 unique media ([#2446](https://github.com/sct/overseerr/issues/2446)) ([7d19de6](https://github.com/sct/overseerr/commit/7d19de6a4af6297be18140ca59402b40f7bbb30b)) - +- add Discord ID setting to general user settings page ([#2406](https://github.com/sct/overseerr/issues/2406)) ([eff665e](https://github.com/sct/overseerr/commit/eff665ef4b688aac881408790304b77bd9a31ddb)) +- address unhandled promise rejections & bump node to v16.13 ([#2398](https://github.com/sct/overseerr/issues/2398)) ([8cba486](https://github.com/sct/overseerr/commit/8cba486249fed88232e93a688c8bfe0f6179c589)) +- **css:** rename form-input to form-input-area ([#2613](https://github.com/sct/overseerr/issues/2613)) ([086f0b6](https://github.com/sct/overseerr/commit/086f0b6ce23f607d20c2cec3c73b2e4d1ce9b426)) +- **email:** enclose PGP encryption logic in try/catch ([#2519](https://github.com/sct/overseerr/issues/2519)) ([a76b608](https://github.com/sct/overseerr/commit/a76b608ab796944c0c660e3296a7aca6615d69f3)) +- **frontend:** disable autocomplete on search field ([#2592](https://github.com/sct/overseerr/issues/2592)) ([82d1617](https://github.com/sct/overseerr/commit/82d16177bf763fe8097b4aae326793e3e21e847d)) +- **frontend:** theme-color meta tag ([#2420](https://github.com/sct/overseerr/issues/2420)) ([ff28c9b](https://github.com/sct/overseerr/commit/ff28c9bfebf4a930e2542ee3b3c35f8af4e1b97e)) +- **frontend:** various fixes ([#2524](https://github.com/sct/overseerr/issues/2524)) ([c3dbd0d](https://github.com/sct/overseerr/commit/c3dbd0d6913946e0e1b5308edfbb5ca744740223)) +- **lang:** rename 'Media' notification types for clarity ([#2400](https://github.com/sct/overseerr/issues/2400)) ([399b037](https://github.com/sct/overseerr/commit/399b0379186ed34dcc436bd95330fd1a05fef4b3)) +- **lang:** translations update from Hosted Weblate ([#2625](https://github.com/sct/overseerr/issues/2625)) ([19cdedd](https://github.com/sct/overseerr/commit/19cdedd2a6656b1a852e1cc653bbdb140e978b51)) +- **lang:** translations update from Hosted Weblate ([#2639](https://github.com/sct/overseerr/issues/2639)) ([418a533](https://github.com/sct/overseerr/commit/418a533588bbbdbbbb4caee1ef91d57c1ca35717)) +- **logs:** handle log message nested extra properties ([#2459](https://github.com/sct/overseerr/issues/2459)) ([d777940](https://github.com/sct/overseerr/commit/d7779408d162949b2eafcacefc8eabe53fae229f)) +- **notif:** duplicate notification check logic ([#2424](https://github.com/sct/overseerr/issues/2424)) ([10651ba](https://github.com/sct/overseerr/commit/10651baa675993f7109989bbac67f54661c8693f)) +- **notif:** show event in pop up notification for slack ([#2413](https://github.com/sct/overseerr/issues/2413)) ([d4438c8](https://github.com/sct/overseerr/commit/d4438c82e3753c9b29b6269ad406d263b3fcef4c)), closes [#2408](https://github.com/sct/overseerr/issues/2408) +- **plex:** correctly generate uuid for safari ([#2614](https://github.com/sct/overseerr/issues/2614)) ([d06f2cd](https://github.com/sct/overseerr/commit/d06f2cdb08bfa6f05cf7cec2c408a258fa926b09)) +- **plex:** find TV series in addition to movies from IMDb IDs ([#1830](https://github.com/sct/overseerr/issues/1830)) ([30644f6](https://github.com/sct/overseerr/commit/30644f65ea2e8437676422ae0b083c642a836887)) +- **plex:** include 'Overseerr' in X-Plex-Device-Name header ([#2635](https://github.com/sct/overseerr/issues/2635)) ([d4f9650](https://github.com/sct/overseerr/commit/d4f9650cd07704a97f8b591b7de7351c1e85b825)) +- **plex:** use unique client identifier ([#2602](https://github.com/sct/overseerr/issues/2602)) ([648b346](https://github.com/sct/overseerr/commit/648b346cbe5a941c7e1ec4ddfb276fb0e27ed502)) +- **plex:** user import ([#2442](https://github.com/sct/overseerr/issues/2442)) ([86dff12](https://github.com/sct/overseerr/commit/86dff12cdeef6dca92527dd31757a3a4c7f921bf)) +- **radarr:** correctly check for existing movies ([#2490](https://github.com/sct/overseerr/issues/2490)) ([5d4b06b](https://github.com/sct/overseerr/commit/5d4b06bbcc6cf6d328f6b4a86c4c0f9b0f3aff3e)) +- **radarr:** remove PreDB minimum availability option ([#2386](https://github.com/sct/overseerr/issues/2386)) ([3e5eb4e](https://github.com/sct/overseerr/commit/3e5eb4e148a9f88b871abc4ee1784b870f691534)) +- **requests:** check for existing media of same type when requesting ([#2445](https://github.com/sct/overseerr/issues/2445)) ([eb9ca2e](https://github.com/sct/overseerr/commit/eb9ca2e86f3be3f4ff8ee2e7c4aecdf337d8976d)) +- **sonarr:** monitor existing series upon request approval ([#2553](https://github.com/sct/overseerr/issues/2553)) ([aa062d9](https://github.com/sct/overseerr/commit/aa062d921c425d4b64bfdb28a5f102b0c92f7d87)) +- **sonarr:** only scan seasons that exist in TMDb ([#2523](https://github.com/sct/overseerr/issues/2523)) ([6168185](https://github.com/sct/overseerr/commit/61681857b123802aaeff02a8f61b1ba046c5d333)) +- **tautulli:** fetch additional user history as necessary to return 20 unique media ([#2446](https://github.com/sct/overseerr/issues/2446)) ([7d19de6](https://github.com/sct/overseerr/commit/7d19de6a4af6297be18140ca59402b40f7bbb30b)) ### Features -* **about:** show config directory ([#2600](https://github.com/sct/overseerr/issues/2600)) ([0c7373c](https://github.com/sct/overseerr/commit/0c7373c7e89a4ff717efaa7d6a5854f7ccd6a8d3)) -* **api:** add additional request counts ([#2426](https://github.com/sct/overseerr/issues/2426)) ([2535edc](https://github.com/sct/overseerr/commit/2535edcc7fd6ec66fd45ad754c03929f1fe94871)) -* **discord:** add 'Enable Mentions' setting ([#1779](https://github.com/sct/overseerr/issues/1779)) ([5f7538a](https://github.com/sct/overseerr/commit/5f7538ae2bf9c6e2feea385cc299bd08df071218)) -* **frontend:** open media management slideover on status badge click ([#2407](https://github.com/sct/overseerr/issues/2407)) ([1f5785d](https://github.com/sct/overseerr/commit/1f5785d6c53b2ca2da67a8ccee72165c052c61a1)) -* **lang:** add Albanian display language ([#2605](https://github.com/sct/overseerr/issues/2605)) ([3d32462](https://github.com/sct/overseerr/commit/3d32462f50b4ced0d9205b79003c35d6d1c948a3)) -* **lang:** translations update from Hosted Weblate ([#2379](https://github.com/sct/overseerr/issues/2379)) ([bd93168](https://github.com/sct/overseerr/commit/bd93168ba1ed650baf4024569bb6a76811a99820)) -* **lang:** translations update from Hosted Weblate ([#2389](https://github.com/sct/overseerr/issues/2389)) ([d2241a4](https://github.com/sct/overseerr/commit/d2241a41877d126a802fc53c925d258af31f34fd)) -* **lang:** translations update from Hosted Weblate ([#2404](https://github.com/sct/overseerr/issues/2404)) ([1b29b15](https://github.com/sct/overseerr/commit/1b29b15d7c9a7ec918cb59116d60e1ae2e797dc4)) -* **lang:** translations update from Hosted Weblate ([#2405](https://github.com/sct/overseerr/issues/2405)) ([879df20](https://github.com/sct/overseerr/commit/879df20022c8c5d9b32858ac5499d3e4369fc064)) -* **lang:** translations update from Hosted Weblate ([#2414](https://github.com/sct/overseerr/issues/2414)) ([88536b1](https://github.com/sct/overseerr/commit/88536b1f9d6e8c1a11e1adf91b85bab4f34b751c)) -* **lang:** translations update from Hosted Weblate ([#2425](https://github.com/sct/overseerr/issues/2425)) ([e9d4b63](https://github.com/sct/overseerr/commit/e9d4b6327b50a005ee6c2c3292b6f107e90fc50c)) -* **lang:** translations update from Hosted Weblate ([#2428](https://github.com/sct/overseerr/issues/2428)) ([f8b1bcc](https://github.com/sct/overseerr/commit/f8b1bccda44371bb6f3f8f4ceeab900b1df3de31)) -* **lang:** translations update from Hosted Weblate ([#2436](https://github.com/sct/overseerr/issues/2436)) ([99c0407](https://github.com/sct/overseerr/commit/99c04072e9f7be8191f25cbcfd5103017b8796eb)) -* **lang:** translations update from Hosted Weblate ([#2452](https://github.com/sct/overseerr/issues/2452)) ([b5bd6ee](https://github.com/sct/overseerr/commit/b5bd6ee78f3d4aa14f0c440d1f2a8323dccfa399)) -* **lang:** translations update from Hosted Weblate ([#2457](https://github.com/sct/overseerr/issues/2457)) ([92b2d32](https://github.com/sct/overseerr/commit/92b2d32d2e1e1d319410a9e357e1304065a77598)) -* **lang:** translations update from Hosted Weblate ([#2489](https://github.com/sct/overseerr/issues/2489)) ([ec08fa6](https://github.com/sct/overseerr/commit/ec08fa67934715ff4a4d618d5b9ff97853913b78)) -* **lang:** translations update from Hosted Weblate ([#2508](https://github.com/sct/overseerr/issues/2508)) ([9f4ae34](https://github.com/sct/overseerr/commit/9f4ae34da76707a40e2c89a50c722ffa1c0327c0)) -* **lang:** translations update from Hosted Weblate ([#2531](https://github.com/sct/overseerr/issues/2531)) ([54b32eb](https://github.com/sct/overseerr/commit/54b32ebfd6b2eb6aeeea98c25939166eda8cc17f)) -* **lang:** translations update from Hosted Weblate ([#2541](https://github.com/sct/overseerr/issues/2541)) ([4549ed3](https://github.com/sct/overseerr/commit/4549ed389e4f25c0946dc01526387e5ac000c3cf)) -* **lang:** translations update from Hosted Weblate ([#2611](https://github.com/sct/overseerr/issues/2611)) ([81c75c8](https://github.com/sct/overseerr/commit/81c75c800edf6d36a1082a291ef7e308f338d005)) -* **lang:** translations update from Hosted Weblate ([#2629](https://github.com/sct/overseerr/issues/2629)) ([1d0cbd2](https://github.com/sct/overseerr/commit/1d0cbd2e761072be0b4b3de461397ad9f9f681f3)) -* **lang:** translations update from Hosted Weblate ([#2645](https://github.com/sct/overseerr/issues/2645)) ([341e3b8](https://github.com/sct/overseerr/commit/341e3b8f0657e09f53ad0b813b051290947343c0)) -* **logs:** use separate json file to parse logs for log viewer ([#2399](https://github.com/sct/overseerr/issues/2399)) ([ce31bef](https://github.com/sct/overseerr/commit/ce31bef8a125c5492f2a1cfef0dcf3d8a4e9ee11)) -* **notif:** add Gotify agent ([#2196](https://github.com/sct/overseerr/issues/2196)) ([e0b6abe](https://github.com/sct/overseerr/commit/e0b6abe4796f5a324c0ff78cff317fcaead671f1)), closes [#2183](https://github.com/sct/overseerr/issues/2183) [#2183](https://github.com/sct/overseerr/issues/2183) [#2077](https://github.com/sct/overseerr/issues/2077) [#2183](https://github.com/sct/overseerr/issues/2183) [#2183](https://github.com/sct/overseerr/issues/2183) [#2183](https://github.com/sct/overseerr/issues/2183) [#2077](https://github.com/sct/overseerr/issues/2077) [#2183](https://github.com/sct/overseerr/issues/2183) [#2183](https://github.com/sct/overseerr/issues/2183) [#2183](https://github.com/sct/overseerr/issues/2183) -* **notif:** add Pushbullet channel tag ([#2198](https://github.com/sct/overseerr/issues/2198)) ([f9200b7](https://github.com/sct/overseerr/commit/f9200b7977208f9b8267ce3a74bd8a86d6f28f7b)) -* **plex:** selective user import ([#2188](https://github.com/sct/overseerr/issues/2188)) ([9cb97db](https://github.com/sct/overseerr/commit/9cb97db13ced5df2dc595cd9033470b1a0750093)) -* **search:** filter search results by year ([#2460](https://github.com/sct/overseerr/issues/2460)) ([72c825d](https://github.com/sct/overseerr/commit/72c825d2a5109688bcc1991a30249284bf281500)) -* **search:** search by id ([#2082](https://github.com/sct/overseerr/issues/2082)) ([b31cdbf](https://github.com/sct/overseerr/commit/b31cdbf074d5dbecbbf6da135a9b686aea9e3c0e)) -* Tautulli integration ([#2230](https://github.com/sct/overseerr/issues/2230)) ([0842c23](https://github.com/sct/overseerr/commit/0842c233d0fc56d44824cad18749492cd52cbed5)) -* **tautulli:** validate upon saving settings ([#2511](https://github.com/sct/overseerr/issues/2511)) ([1dc900d](https://github.com/sct/overseerr/commit/1dc900d5ce9689d179c9d2f554abc74ca50bd9cb)) -* **ui:** add trakt external link ([#2367](https://github.com/sct/overseerr/issues/2367)) ([4e56bae](https://github.com/sct/overseerr/commit/4e56bae98508c1a60aeb3a08560ba1c00acce7e7)) -* verify Plex server access during auth for existing users with Plex IDs ([#2458](https://github.com/sct/overseerr/issues/2458)) ([85bb30e](https://github.com/sct/overseerr/commit/85bb30e252c27047ae367491f0e5bb92a7d52605)) +- **about:** show config directory ([#2600](https://github.com/sct/overseerr/issues/2600)) ([0c7373c](https://github.com/sct/overseerr/commit/0c7373c7e89a4ff717efaa7d6a5854f7ccd6a8d3)) +- **api:** add additional request counts ([#2426](https://github.com/sct/overseerr/issues/2426)) ([2535edc](https://github.com/sct/overseerr/commit/2535edcc7fd6ec66fd45ad754c03929f1fe94871)) +- **discord:** add 'Enable Mentions' setting ([#1779](https://github.com/sct/overseerr/issues/1779)) ([5f7538a](https://github.com/sct/overseerr/commit/5f7538ae2bf9c6e2feea385cc299bd08df071218)) +- **frontend:** open media management slideover on status badge click ([#2407](https://github.com/sct/overseerr/issues/2407)) ([1f5785d](https://github.com/sct/overseerr/commit/1f5785d6c53b2ca2da67a8ccee72165c052c61a1)) +- **lang:** add Albanian display language ([#2605](https://github.com/sct/overseerr/issues/2605)) ([3d32462](https://github.com/sct/overseerr/commit/3d32462f50b4ced0d9205b79003c35d6d1c948a3)) +- **lang:** translations update from Hosted Weblate ([#2379](https://github.com/sct/overseerr/issues/2379)) ([bd93168](https://github.com/sct/overseerr/commit/bd93168ba1ed650baf4024569bb6a76811a99820)) +- **lang:** translations update from Hosted Weblate ([#2389](https://github.com/sct/overseerr/issues/2389)) ([d2241a4](https://github.com/sct/overseerr/commit/d2241a41877d126a802fc53c925d258af31f34fd)) +- **lang:** translations update from Hosted Weblate ([#2404](https://github.com/sct/overseerr/issues/2404)) ([1b29b15](https://github.com/sct/overseerr/commit/1b29b15d7c9a7ec918cb59116d60e1ae2e797dc4)) +- **lang:** translations update from Hosted Weblate ([#2405](https://github.com/sct/overseerr/issues/2405)) ([879df20](https://github.com/sct/overseerr/commit/879df20022c8c5d9b32858ac5499d3e4369fc064)) +- **lang:** translations update from Hosted Weblate ([#2414](https://github.com/sct/overseerr/issues/2414)) ([88536b1](https://github.com/sct/overseerr/commit/88536b1f9d6e8c1a11e1adf91b85bab4f34b751c)) +- **lang:** translations update from Hosted Weblate ([#2425](https://github.com/sct/overseerr/issues/2425)) ([e9d4b63](https://github.com/sct/overseerr/commit/e9d4b6327b50a005ee6c2c3292b6f107e90fc50c)) +- **lang:** translations update from Hosted Weblate ([#2428](https://github.com/sct/overseerr/issues/2428)) ([f8b1bcc](https://github.com/sct/overseerr/commit/f8b1bccda44371bb6f3f8f4ceeab900b1df3de31)) +- **lang:** translations update from Hosted Weblate ([#2436](https://github.com/sct/overseerr/issues/2436)) ([99c0407](https://github.com/sct/overseerr/commit/99c04072e9f7be8191f25cbcfd5103017b8796eb)) +- **lang:** translations update from Hosted Weblate ([#2452](https://github.com/sct/overseerr/issues/2452)) ([b5bd6ee](https://github.com/sct/overseerr/commit/b5bd6ee78f3d4aa14f0c440d1f2a8323dccfa399)) +- **lang:** translations update from Hosted Weblate ([#2457](https://github.com/sct/overseerr/issues/2457)) ([92b2d32](https://github.com/sct/overseerr/commit/92b2d32d2e1e1d319410a9e357e1304065a77598)) +- **lang:** translations update from Hosted Weblate ([#2489](https://github.com/sct/overseerr/issues/2489)) ([ec08fa6](https://github.com/sct/overseerr/commit/ec08fa67934715ff4a4d618d5b9ff97853913b78)) +- **lang:** translations update from Hosted Weblate ([#2508](https://github.com/sct/overseerr/issues/2508)) ([9f4ae34](https://github.com/sct/overseerr/commit/9f4ae34da76707a40e2c89a50c722ffa1c0327c0)) +- **lang:** translations update from Hosted Weblate ([#2531](https://github.com/sct/overseerr/issues/2531)) ([54b32eb](https://github.com/sct/overseerr/commit/54b32ebfd6b2eb6aeeea98c25939166eda8cc17f)) +- **lang:** translations update from Hosted Weblate ([#2541](https://github.com/sct/overseerr/issues/2541)) ([4549ed3](https://github.com/sct/overseerr/commit/4549ed389e4f25c0946dc01526387e5ac000c3cf)) +- **lang:** translations update from Hosted Weblate ([#2611](https://github.com/sct/overseerr/issues/2611)) ([81c75c8](https://github.com/sct/overseerr/commit/81c75c800edf6d36a1082a291ef7e308f338d005)) +- **lang:** translations update from Hosted Weblate ([#2629](https://github.com/sct/overseerr/issues/2629)) ([1d0cbd2](https://github.com/sct/overseerr/commit/1d0cbd2e761072be0b4b3de461397ad9f9f681f3)) +- **lang:** translations update from Hosted Weblate ([#2645](https://github.com/sct/overseerr/issues/2645)) ([341e3b8](https://github.com/sct/overseerr/commit/341e3b8f0657e09f53ad0b813b051290947343c0)) +- **logs:** use separate json file to parse logs for log viewer ([#2399](https://github.com/sct/overseerr/issues/2399)) ([ce31bef](https://github.com/sct/overseerr/commit/ce31bef8a125c5492f2a1cfef0dcf3d8a4e9ee11)) +- **notif:** add Gotify agent ([#2196](https://github.com/sct/overseerr/issues/2196)) ([e0b6abe](https://github.com/sct/overseerr/commit/e0b6abe4796f5a324c0ff78cff317fcaead671f1)), closes [#2183](https://github.com/sct/overseerr/issues/2183) [#2183](https://github.com/sct/overseerr/issues/2183) [#2077](https://github.com/sct/overseerr/issues/2077) [#2183](https://github.com/sct/overseerr/issues/2183) [#2183](https://github.com/sct/overseerr/issues/2183) [#2183](https://github.com/sct/overseerr/issues/2183) [#2077](https://github.com/sct/overseerr/issues/2077) [#2183](https://github.com/sct/overseerr/issues/2183) [#2183](https://github.com/sct/overseerr/issues/2183) [#2183](https://github.com/sct/overseerr/issues/2183) +- **notif:** add Pushbullet channel tag ([#2198](https://github.com/sct/overseerr/issues/2198)) ([f9200b7](https://github.com/sct/overseerr/commit/f9200b7977208f9b8267ce3a74bd8a86d6f28f7b)) +- **plex:** selective user import ([#2188](https://github.com/sct/overseerr/issues/2188)) ([9cb97db](https://github.com/sct/overseerr/commit/9cb97db13ced5df2dc595cd9033470b1a0750093)) +- **search:** filter search results by year ([#2460](https://github.com/sct/overseerr/issues/2460)) ([72c825d](https://github.com/sct/overseerr/commit/72c825d2a5109688bcc1991a30249284bf281500)) +- **search:** search by id ([#2082](https://github.com/sct/overseerr/issues/2082)) ([b31cdbf](https://github.com/sct/overseerr/commit/b31cdbf074d5dbecbbf6da135a9b686aea9e3c0e)) +- Tautulli integration ([#2230](https://github.com/sct/overseerr/issues/2230)) ([0842c23](https://github.com/sct/overseerr/commit/0842c233d0fc56d44824cad18749492cd52cbed5)) +- **tautulli:** validate upon saving settings ([#2511](https://github.com/sct/overseerr/issues/2511)) ([1dc900d](https://github.com/sct/overseerr/commit/1dc900d5ce9689d179c9d2f554abc74ca50bd9cb)) +- **ui:** add trakt external link ([#2367](https://github.com/sct/overseerr/issues/2367)) ([4e56bae](https://github.com/sct/overseerr/commit/4e56bae98508c1a60aeb3a08560ba1c00acce7e7)) +- verify Plex server access during auth for existing users with Plex IDs ([#2458](https://github.com/sct/overseerr/issues/2458)) ([85bb30e](https://github.com/sct/overseerr/commit/85bb30e252c27047ae367491f0e5bb92a7d52605)) # [1.28.0](https://github.com/sct/overseerr/compare/v1.27.0...v1.28.0) (2022-01-01) - ### Bug Fixes -* add missing route guards to issues pages ([#2235](https://github.com/sct/overseerr/issues/2235)) ([c79dc9f](https://github.com/sct/overseerr/commit/c79dc9f70f512dbec0e3460ee78dbc9feccfbbb1)) -* allow basic HTTP auth in hostname validation ([#2307](https://github.com/sct/overseerr/issues/2307)) ([d48a7ba](https://github.com/sct/overseerr/commit/d48a7ba518f9c79d70e499037cb730eb3efe2c08)) -* **docker:** explicitly install python3 ([#2273](https://github.com/sct/overseerr/issues/2273)) [skip ci] ([f1cd087](https://github.com/sct/overseerr/commit/f1cd0878a5c74bddc864f5f8ce9e2f041bdde5ec)) -* **email:** use decrypted private key ([#2232](https://github.com/sct/overseerr/issues/2232)) ([8d29685](https://github.com/sct/overseerr/commit/8d2968572a569ed77a4d7c14ae1dc69935fa847e)) -* **frontend:** more issues-related fixes ([#2234](https://github.com/sct/overseerr/issues/2234)) ([3ec4a9c](https://github.com/sct/overseerr/commit/3ec4a9c76e1f31bee5c8801b389721bf8e5884e0)) -* **frontend:** setup page backdrops ([#2251](https://github.com/sct/overseerr/issues/2251)) ([78a8091](https://github.com/sct/overseerr/commit/78a8091bcd29a7cf50cc7c493c28710389817adf)) -* **frontend:** use consistent formatting & strings ([#2231](https://github.com/sct/overseerr/issues/2231)) ([2164471](https://github.com/sct/overseerr/commit/216447121b686b6d01a31b95ec0c8eb005f6b103)) -* handle Plex library settings migration failure gracefully ([#2254](https://github.com/sct/overseerr/issues/2254)) ([ed53810](https://github.com/sct/overseerr/commit/ed53810fb33f70722361c67d176ff4edf531ba45)) -* **issues:** only allow edit of own comments & do not allow non-admin delete of issues with comments ([#2248](https://github.com/sct/overseerr/issues/2248)) ([bba09d6](https://github.com/sct/overseerr/commit/bba09d69c1bc55c2f35db5a7986e7c935cc9619c)) -* **lang:** add missing string ([#2370](https://github.com/sct/overseerr/issues/2370)) ([d36c1d2](https://github.com/sct/overseerr/commit/d36c1d29295020efb76bac21a443b6f9049802f3)) -* **lang:** string edits ([#2229](https://github.com/sct/overseerr/issues/2229)) ([ab20c21](https://github.com/sct/overseerr/commit/ab20c21184639e1c7725f7cae96249c6fa157351)) -* **lang:** translations update from Weblate ([#2212](https://github.com/sct/overseerr/issues/2212)) ([85aec4f](https://github.com/sct/overseerr/commit/85aec4f8925746ebae9bcc99d8480b78ccfd851e)) -* **logs:** handle unexpected log messages ([#2303](https://github.com/sct/overseerr/issues/2303)) ([f284e4a](https://github.com/sct/overseerr/commit/f284e4ab978e502d2cc08e76226a8ebac91bb48f)) -* **logs:** lazily parse log message label ([#2359](https://github.com/sct/overseerr/issues/2359)) ([5af06bd](https://github.com/sct/overseerr/commit/5af06bd87226fbc6176b0c5e362824793165a34e)) -* **notif:** correct issue notif action URLs ([#2333](https://github.com/sct/overseerr/issues/2333)) ([dc7f959](https://github.com/sct/overseerr/commit/dc7f959cb422a8d89bcebc78377f1513412e542c)) -* **notif:** only send MEDIA_AVAILABLE notifications for non-declined requests ([#2343](https://github.com/sct/overseerr/issues/2343)) ([fcb0dcf](https://github.com/sct/overseerr/commit/fcb0dcf5be64bf9ca814bfe119586908922099c5)) -* **requests:** do not fail request edits if acting user lacks Manage Users permission ([#2338](https://github.com/sct/overseerr/issues/2338)) ([91bfff7](https://github.com/sct/overseerr/commit/91bfff71b7c05c9b9aad2c95282533eefbb6b2e7)) -* secure session cookie ([#2308](https://github.com/sct/overseerr/issues/2308)) ([7f330af](https://github.com/sct/overseerr/commit/7f330aff2e1d3546e8dd1a3e4b037b9beb1cc7f0)) -* **servarr:** handle baseurl error when testing connection ([#2294](https://github.com/sct/overseerr/issues/2294)) ([93b5ea2](https://github.com/sct/overseerr/commit/93b5ea20ca590996f6dc90713a76800180d0621c)) -* **servarr:** handle servaarr server being unavailable when scanning downloads ([#2358](https://github.com/sct/overseerr/issues/2358)) ([488874f](https://github.com/sct/overseerr/commit/488874fc17e4e4719e90d383b83b1e1a5217213b)) -* sort collection parts by release date ([#2368](https://github.com/sct/overseerr/issues/2368)) ([1b3797c](https://github.com/sct/overseerr/commit/1b3797cf6e6ef6b3d8c81e644382f6e3f68cfaaa)) -* **ui:** request badge styling in request list ([#2302](https://github.com/sct/overseerr/issues/2302)) ([f2375c9](https://github.com/sct/overseerr/commit/f2375c902b79dcb1f349500862775ae57ea7d406)) - +- add missing route guards to issues pages ([#2235](https://github.com/sct/overseerr/issues/2235)) ([c79dc9f](https://github.com/sct/overseerr/commit/c79dc9f70f512dbec0e3460ee78dbc9feccfbbb1)) +- allow basic HTTP auth in hostname validation ([#2307](https://github.com/sct/overseerr/issues/2307)) ([d48a7ba](https://github.com/sct/overseerr/commit/d48a7ba518f9c79d70e499037cb730eb3efe2c08)) +- **docker:** explicitly install python3 ([#2273](https://github.com/sct/overseerr/issues/2273)) [skip ci] ([f1cd087](https://github.com/sct/overseerr/commit/f1cd0878a5c74bddc864f5f8ce9e2f041bdde5ec)) +- **email:** use decrypted private key ([#2232](https://github.com/sct/overseerr/issues/2232)) ([8d29685](https://github.com/sct/overseerr/commit/8d2968572a569ed77a4d7c14ae1dc69935fa847e)) +- **frontend:** more issues-related fixes ([#2234](https://github.com/sct/overseerr/issues/2234)) ([3ec4a9c](https://github.com/sct/overseerr/commit/3ec4a9c76e1f31bee5c8801b389721bf8e5884e0)) +- **frontend:** setup page backdrops ([#2251](https://github.com/sct/overseerr/issues/2251)) ([78a8091](https://github.com/sct/overseerr/commit/78a8091bcd29a7cf50cc7c493c28710389817adf)) +- **frontend:** use consistent formatting & strings ([#2231](https://github.com/sct/overseerr/issues/2231)) ([2164471](https://github.com/sct/overseerr/commit/216447121b686b6d01a31b95ec0c8eb005f6b103)) +- handle Plex library settings migration failure gracefully ([#2254](https://github.com/sct/overseerr/issues/2254)) ([ed53810](https://github.com/sct/overseerr/commit/ed53810fb33f70722361c67d176ff4edf531ba45)) +- **issues:** only allow edit of own comments & do not allow non-admin delete of issues with comments ([#2248](https://github.com/sct/overseerr/issues/2248)) ([bba09d6](https://github.com/sct/overseerr/commit/bba09d69c1bc55c2f35db5a7986e7c935cc9619c)) +- **lang:** add missing string ([#2370](https://github.com/sct/overseerr/issues/2370)) ([d36c1d2](https://github.com/sct/overseerr/commit/d36c1d29295020efb76bac21a443b6f9049802f3)) +- **lang:** string edits ([#2229](https://github.com/sct/overseerr/issues/2229)) ([ab20c21](https://github.com/sct/overseerr/commit/ab20c21184639e1c7725f7cae96249c6fa157351)) +- **lang:** translations update from Weblate ([#2212](https://github.com/sct/overseerr/issues/2212)) ([85aec4f](https://github.com/sct/overseerr/commit/85aec4f8925746ebae9bcc99d8480b78ccfd851e)) +- **logs:** handle unexpected log messages ([#2303](https://github.com/sct/overseerr/issues/2303)) ([f284e4a](https://github.com/sct/overseerr/commit/f284e4ab978e502d2cc08e76226a8ebac91bb48f)) +- **logs:** lazily parse log message label ([#2359](https://github.com/sct/overseerr/issues/2359)) ([5af06bd](https://github.com/sct/overseerr/commit/5af06bd87226fbc6176b0c5e362824793165a34e)) +- **notif:** correct issue notif action URLs ([#2333](https://github.com/sct/overseerr/issues/2333)) ([dc7f959](https://github.com/sct/overseerr/commit/dc7f959cb422a8d89bcebc78377f1513412e542c)) +- **notif:** only send MEDIA_AVAILABLE notifications for non-declined requests ([#2343](https://github.com/sct/overseerr/issues/2343)) ([fcb0dcf](https://github.com/sct/overseerr/commit/fcb0dcf5be64bf9ca814bfe119586908922099c5)) +- **requests:** do not fail request edits if acting user lacks Manage Users permission ([#2338](https://github.com/sct/overseerr/issues/2338)) ([91bfff7](https://github.com/sct/overseerr/commit/91bfff71b7c05c9b9aad2c95282533eefbb6b2e7)) +- secure session cookie ([#2308](https://github.com/sct/overseerr/issues/2308)) ([7f330af](https://github.com/sct/overseerr/commit/7f330aff2e1d3546e8dd1a3e4b037b9beb1cc7f0)) +- **servarr:** handle baseurl error when testing connection ([#2294](https://github.com/sct/overseerr/issues/2294)) ([93b5ea2](https://github.com/sct/overseerr/commit/93b5ea20ca590996f6dc90713a76800180d0621c)) +- **servarr:** handle servaarr server being unavailable when scanning downloads ([#2358](https://github.com/sct/overseerr/issues/2358)) ([488874f](https://github.com/sct/overseerr/commit/488874fc17e4e4719e90d383b83b1e1a5217213b)) +- sort collection parts by release date ([#2368](https://github.com/sct/overseerr/issues/2368)) ([1b3797c](https://github.com/sct/overseerr/commit/1b3797cf6e6ef6b3d8c81e644382f6e3f68cfaaa)) +- **ui:** request badge styling in request list ([#2302](https://github.com/sct/overseerr/issues/2302)) ([f2375c9](https://github.com/sct/overseerr/commit/f2375c902b79dcb1f349500862775ae57ea7d406)) ### Features -* add production countries to movie/TV detail pages ([#2170](https://github.com/sct/overseerr/issues/2170)) ([30b20df](https://github.com/sct/overseerr/commit/30b20df37a9604ba1c066f89e54a5482a09575ea)) -* add quotas, advanced options, and toggles to collection request modal ([#1742](https://github.com/sct/overseerr/issues/1742)) ([af40212](https://github.com/sct/overseerr/commit/af40212a738f8d6d9a5bf26dc20c0c87780d6020)) -* **frontend:** add Discovery+ to network slider ([#2345](https://github.com/sct/overseerr/issues/2345)) ([2ded8f5](https://github.com/sct/overseerr/commit/2ded8f5484168bd7b8f45124d9ebdd296a5708d5)) -* issues ([#2180](https://github.com/sct/overseerr/issues/2180)) ([e402c42](https://github.com/sct/overseerr/commit/e402c42aaa7d795cd724856a2e23615bb1a3695d)) -* **lang:** add Polish display language ([#2261](https://github.com/sct/overseerr/issues/2261)) ([c760cea](https://github.com/sct/overseerr/commit/c760ceaa5f36c77fa3ce320fae1b4597d2d8b976)) -* **lang:** translated using Weblate (Chinese (Traditional)) ([#2272](https://github.com/sct/overseerr/issues/2272)) ([d401e33](https://github.com/sct/overseerr/commit/d401e33249cbbca6e707479e5f0207e298ef3248)) -* **lang:** translations update from Hosted Weblate ([#2277](https://github.com/sct/overseerr/issues/2277)) ([92732fc](https://github.com/sct/overseerr/commit/92732fcb42c56242d16daab00e2d38740b92dea0)) -* **lang:** translations update from Hosted Weblate ([#2315](https://github.com/sct/overseerr/issues/2315)) ([6245be1](https://github.com/sct/overseerr/commit/6245be1e10dda67c869b59522c1290e7c100145f)) -* **lang:** translations update from Hosted Weblate ([#2320](https://github.com/sct/overseerr/issues/2320)) ([68112fa](https://github.com/sct/overseerr/commit/68112faefbd64d5c71d3eff21620767f88ccfc34)) -* **lang:** translations update from Hosted Weblate ([#2325](https://github.com/sct/overseerr/issues/2325)) ([febf067](https://github.com/sct/overseerr/commit/febf0677b880d2fed2822ce510db7cbb0826a920)) -* **lang:** translations update from Hosted Weblate ([#2336](https://github.com/sct/overseerr/issues/2336)) ([3f7ef7a](https://github.com/sct/overseerr/commit/3f7ef7af97a807ef38041f4f2642b565aa33d066)) -* **lang:** translations update from Hosted Weblate ([#2341](https://github.com/sct/overseerr/issues/2341)) ([33fe0bd](https://github.com/sct/overseerr/commit/33fe0bdd1e00da40e85b4e4b4780134b31a105d2)) -* **lang:** translations update from Hosted Weblate ([#2346](https://github.com/sct/overseerr/issues/2346)) ([50dc934](https://github.com/sct/overseerr/commit/50dc9341dd98cb2d8ef3ef6471882a5a9b060afa)) -* **lang:** translations update from Hosted Weblate ([#2364](https://github.com/sct/overseerr/issues/2364)) ([d437cc2](https://github.com/sct/overseerr/commit/d437cc25392e9c0881888371ffabc82892a1b15c)) -* **lang:** translations update from Hosted Weblate ([#2366](https://github.com/sct/overseerr/issues/2366)) ([cc2b2bc](https://github.com/sct/overseerr/commit/cc2b2bc7a8ecd89e1feb38a907596b16df9bf0fc)) -* **lang:** translations update from Hosted Weblate ([#2374](https://github.com/sct/overseerr/issues/2374)) ([b9bedac](https://github.com/sct/overseerr/commit/b9bedac7d7ba85223ecf1d9b93b96e2a490d571a)) -* **lang:** translations update from Weblate ([#2226](https://github.com/sct/overseerr/issues/2226)) ([62b3dc5](https://github.com/sct/overseerr/commit/62b3dc5471c28f4d0e4399cb3bc8bfab94cff5ea)) -* **lang:** translations update from Weblate ([#2241](https://github.com/sct/overseerr/issues/2241)) ([2b0b8e0](https://github.com/sct/overseerr/commit/2b0b8e05d9c95ff9218cea858a920a2815871186)) -* **lang:** translations update from Weblate ([#2244](https://github.com/sct/overseerr/issues/2244)) ([0828b00](https://github.com/sct/overseerr/commit/0828b008badc8b512316799a6787bb7c403658d5)) -* **lang:** translations update from Weblate ([#2247](https://github.com/sct/overseerr/issues/2247)) ([8c49309](https://github.com/sct/overseerr/commit/8c49309c35c31f7bcd0b84b0a307febc16842f68)) -* **lang:** translations update from Weblate ([#2252](https://github.com/sct/overseerr/issues/2252)) ([99d5000](https://github.com/sct/overseerr/commit/99d50004e58f6b4594df0a171f6bc668635ec50c)) -* **lang:** translations update from Weblate ([#2265](https://github.com/sct/overseerr/issues/2265)) ([b1b367a](https://github.com/sct/overseerr/commit/b1b367aac625ed3eb865832c94c2352e5a5c40f5)) -* **notif:** 4K media notifications ([#2324](https://github.com/sct/overseerr/issues/2324)) ([88a8c1a](https://github.com/sct/overseerr/commit/88a8c1aa596e1113d6da52e5e8cbe443abc6384f)) -* **notif:** add Pushbullet and Pushover agents to user notification settings ([#1740](https://github.com/sct/overseerr/issues/1740)) ([aeb7a48](https://github.com/sct/overseerr/commit/aeb7a48d72cec3fa2b857030aad3eaa0a457a896)) -* **notif:** issue notifications ([#2242](https://github.com/sct/overseerr/issues/2242)) ([c9ffac3](https://github.com/sct/overseerr/commit/c9ffac33f7c04d926f8c45295703689d42fe87af)) -* **search:** close search bar when hitting return ([#2260](https://github.com/sct/overseerr/issues/2260)) ([b423dc1](https://github.com/sct/overseerr/commit/b423dc167d12f0ba49f902876bceb2e876e35f58)) -* **ui:** allow admins to edit & approve request from advanced request modal ([#2067](https://github.com/sct/overseerr/issues/2067)) ([340f1a2](https://github.com/sct/overseerr/commit/340f1a211952bd2e8f40f0ea4622b52dbe934e85)) +- add production countries to movie/TV detail pages ([#2170](https://github.com/sct/overseerr/issues/2170)) ([30b20df](https://github.com/sct/overseerr/commit/30b20df37a9604ba1c066f89e54a5482a09575ea)) +- add quotas, advanced options, and toggles to collection request modal ([#1742](https://github.com/sct/overseerr/issues/1742)) ([af40212](https://github.com/sct/overseerr/commit/af40212a738f8d6d9a5bf26dc20c0c87780d6020)) +- **frontend:** add Discovery+ to network slider ([#2345](https://github.com/sct/overseerr/issues/2345)) ([2ded8f5](https://github.com/sct/overseerr/commit/2ded8f5484168bd7b8f45124d9ebdd296a5708d5)) +- issues ([#2180](https://github.com/sct/overseerr/issues/2180)) ([e402c42](https://github.com/sct/overseerr/commit/e402c42aaa7d795cd724856a2e23615bb1a3695d)) +- **lang:** add Polish display language ([#2261](https://github.com/sct/overseerr/issues/2261)) ([c760cea](https://github.com/sct/overseerr/commit/c760ceaa5f36c77fa3ce320fae1b4597d2d8b976)) +- **lang:** translated using Weblate (Chinese (Traditional)) ([#2272](https://github.com/sct/overseerr/issues/2272)) ([d401e33](https://github.com/sct/overseerr/commit/d401e33249cbbca6e707479e5f0207e298ef3248)) +- **lang:** translations update from Hosted Weblate ([#2277](https://github.com/sct/overseerr/issues/2277)) ([92732fc](https://github.com/sct/overseerr/commit/92732fcb42c56242d16daab00e2d38740b92dea0)) +- **lang:** translations update from Hosted Weblate ([#2315](https://github.com/sct/overseerr/issues/2315)) ([6245be1](https://github.com/sct/overseerr/commit/6245be1e10dda67c869b59522c1290e7c100145f)) +- **lang:** translations update from Hosted Weblate ([#2320](https://github.com/sct/overseerr/issues/2320)) ([68112fa](https://github.com/sct/overseerr/commit/68112faefbd64d5c71d3eff21620767f88ccfc34)) +- **lang:** translations update from Hosted Weblate ([#2325](https://github.com/sct/overseerr/issues/2325)) ([febf067](https://github.com/sct/overseerr/commit/febf0677b880d2fed2822ce510db7cbb0826a920)) +- **lang:** translations update from Hosted Weblate ([#2336](https://github.com/sct/overseerr/issues/2336)) ([3f7ef7a](https://github.com/sct/overseerr/commit/3f7ef7af97a807ef38041f4f2642b565aa33d066)) +- **lang:** translations update from Hosted Weblate ([#2341](https://github.com/sct/overseerr/issues/2341)) ([33fe0bd](https://github.com/sct/overseerr/commit/33fe0bdd1e00da40e85b4e4b4780134b31a105d2)) +- **lang:** translations update from Hosted Weblate ([#2346](https://github.com/sct/overseerr/issues/2346)) ([50dc934](https://github.com/sct/overseerr/commit/50dc9341dd98cb2d8ef3ef6471882a5a9b060afa)) +- **lang:** translations update from Hosted Weblate ([#2364](https://github.com/sct/overseerr/issues/2364)) ([d437cc2](https://github.com/sct/overseerr/commit/d437cc25392e9c0881888371ffabc82892a1b15c)) +- **lang:** translations update from Hosted Weblate ([#2366](https://github.com/sct/overseerr/issues/2366)) ([cc2b2bc](https://github.com/sct/overseerr/commit/cc2b2bc7a8ecd89e1feb38a907596b16df9bf0fc)) +- **lang:** translations update from Hosted Weblate ([#2374](https://github.com/sct/overseerr/issues/2374)) ([b9bedac](https://github.com/sct/overseerr/commit/b9bedac7d7ba85223ecf1d9b93b96e2a490d571a)) +- **lang:** translations update from Weblate ([#2226](https://github.com/sct/overseerr/issues/2226)) ([62b3dc5](https://github.com/sct/overseerr/commit/62b3dc5471c28f4d0e4399cb3bc8bfab94cff5ea)) +- **lang:** translations update from Weblate ([#2241](https://github.com/sct/overseerr/issues/2241)) ([2b0b8e0](https://github.com/sct/overseerr/commit/2b0b8e05d9c95ff9218cea858a920a2815871186)) +- **lang:** translations update from Weblate ([#2244](https://github.com/sct/overseerr/issues/2244)) ([0828b00](https://github.com/sct/overseerr/commit/0828b008badc8b512316799a6787bb7c403658d5)) +- **lang:** translations update from Weblate ([#2247](https://github.com/sct/overseerr/issues/2247)) ([8c49309](https://github.com/sct/overseerr/commit/8c49309c35c31f7bcd0b84b0a307febc16842f68)) +- **lang:** translations update from Weblate ([#2252](https://github.com/sct/overseerr/issues/2252)) ([99d5000](https://github.com/sct/overseerr/commit/99d50004e58f6b4594df0a171f6bc668635ec50c)) +- **lang:** translations update from Weblate ([#2265](https://github.com/sct/overseerr/issues/2265)) ([b1b367a](https://github.com/sct/overseerr/commit/b1b367aac625ed3eb865832c94c2352e5a5c40f5)) +- **notif:** 4K media notifications ([#2324](https://github.com/sct/overseerr/issues/2324)) ([88a8c1a](https://github.com/sct/overseerr/commit/88a8c1aa596e1113d6da52e5e8cbe443abc6384f)) +- **notif:** add Pushbullet and Pushover agents to user notification settings ([#1740](https://github.com/sct/overseerr/issues/1740)) ([aeb7a48](https://github.com/sct/overseerr/commit/aeb7a48d72cec3fa2b857030aad3eaa0a457a896)) +- **notif:** issue notifications ([#2242](https://github.com/sct/overseerr/issues/2242)) ([c9ffac3](https://github.com/sct/overseerr/commit/c9ffac33f7c04d926f8c45295703689d42fe87af)) +- **search:** close search bar when hitting return ([#2260](https://github.com/sct/overseerr/issues/2260)) ([b423dc1](https://github.com/sct/overseerr/commit/b423dc167d12f0ba49f902876bceb2e876e35f58)) +- **ui:** allow admins to edit & approve request from advanced request modal ([#2067](https://github.com/sct/overseerr/issues/2067)) ([340f1a2](https://github.com/sct/overseerr/commit/340f1a211952bd2e8f40f0ea4622b52dbe934e85)) # [1.27.0](https://github.com/sct/overseerr/compare/v1.26.1...v1.27.0) (2021-10-19) - ### Bug Fixes -* **api:** return queried user's requests instead of own requests ([#2174](https://github.com/sct/overseerr/issues/2174)) ([0edb1f4](https://github.com/sct/overseerr/commit/0edb1f452b6ff4a49ae2bde15f7273769788cf4f)) -* **api:** use query builder for user requests endpoint ([#2119](https://github.com/sct/overseerr/issues/2119)) ([a20f395](https://github.com/sct/overseerr/commit/a20f395c94c97dd7ddbc25590f15def2c9bf13c9)) -* apply request overrides iff override & selected servers match ([#2164](https://github.com/sct/overseerr/issues/2164)) ([50ce198](https://github.com/sct/overseerr/commit/50ce198471b1a3777a183d68904bbfb39ebd4523)) -* **email:** do not attempt to display logo if app URL not configured ([#2125](https://github.com/sct/overseerr/issues/2125)) ([b3b421a](https://github.com/sct/overseerr/commit/b3b421a67408a4a48d23c15341fcdf7aaf19b25a)) -* **frontend:** notification type validation ([#2207](https://github.com/sct/overseerr/issues/2207)) ([2f204b9](https://github.com/sct/overseerr/commit/2f204b995269a53ae36f2a8733f27ae6ab70da5a)) -* **scripts:** update migration scripts ([#2208](https://github.com/sct/overseerr/issues/2208)) [skip ci] ([d0ac74e](https://github.com/sct/overseerr/commit/d0ac74ea4bbfcf3d25d30cbd422d9df1c1259a18)) -* **ui:** refinements for 'About' page ([#2173](https://github.com/sct/overseerr/issues/2173)) ([084a842](https://github.com/sct/overseerr/commit/084a842a4f9b6caaed22edbe77bc9e414bc1f387)) - +- **api:** return queried user's requests instead of own requests ([#2174](https://github.com/sct/overseerr/issues/2174)) ([0edb1f4](https://github.com/sct/overseerr/commit/0edb1f452b6ff4a49ae2bde15f7273769788cf4f)) +- **api:** use query builder for user requests endpoint ([#2119](https://github.com/sct/overseerr/issues/2119)) ([a20f395](https://github.com/sct/overseerr/commit/a20f395c94c97dd7ddbc25590f15def2c9bf13c9)) +- apply request overrides iff override & selected servers match ([#2164](https://github.com/sct/overseerr/issues/2164)) ([50ce198](https://github.com/sct/overseerr/commit/50ce198471b1a3777a183d68904bbfb39ebd4523)) +- **email:** do not attempt to display logo if app URL not configured ([#2125](https://github.com/sct/overseerr/issues/2125)) ([b3b421a](https://github.com/sct/overseerr/commit/b3b421a67408a4a48d23c15341fcdf7aaf19b25a)) +- **frontend:** notification type validation ([#2207](https://github.com/sct/overseerr/issues/2207)) ([2f204b9](https://github.com/sct/overseerr/commit/2f204b995269a53ae36f2a8733f27ae6ab70da5a)) +- **scripts:** update migration scripts ([#2208](https://github.com/sct/overseerr/issues/2208)) [skip ci] ([d0ac74e](https://github.com/sct/overseerr/commit/d0ac74ea4bbfcf3d25d30cbd422d9df1c1259a18)) +- **ui:** refinements for 'About' page ([#2173](https://github.com/sct/overseerr/issues/2173)) ([084a842](https://github.com/sct/overseerr/commit/084a842a4f9b6caaed22edbe77bc9e414bc1f387)) ### Features -* display release dates for theatrical, digital, and physical release types ([#1492](https://github.com/sct/overseerr/issues/1492)) ([a4dca23](https://github.com/sct/overseerr/commit/a4dca2356b7605026f7bc45b691496e765c3328c)) -* dynamically fetch login screen backdrop images ([#2206](https://github.com/sct/overseerr/issues/2206)) ([3486d0b](https://github.com/sct/overseerr/commit/3486d0bf5520cbdff60bd8fd023caed76c452973)) -* **frontend:** add Hulu to network slider ([#2204](https://github.com/sct/overseerr/issues/2204)) ([1e402f7](https://github.com/sct/overseerr/commit/1e402f710b53c11855aab0abdb4b12c51c30b022)) -* **jobs:** allow modifying job schedules ([#1440](https://github.com/sct/overseerr/issues/1440)) ([82614ca](https://github.com/sct/overseerr/commit/82614ca4410782a12d65b4c0a6526ff064be1241)) -* **lang:** add Czech and Danish display languages ([#2176](https://github.com/sct/overseerr/issues/2176)) ([8d8db6c](https://github.com/sct/overseerr/commit/8d8db6cf5d98d4e498a31db339d02f8a98057c8d)) -* **lang:** translations update from Weblate ([#2101](https://github.com/sct/overseerr/issues/2101)) ([c73cf7b](https://github.com/sct/overseerr/commit/c73cf7b19cbc19e97a777c0facb9264fb0113093)) -* **lang:** translations update from Weblate ([#2179](https://github.com/sct/overseerr/issues/2179)) ([e3312ce](https://github.com/sct/overseerr/commit/e3312cef33821c8cb76a4a63bd565c78d67b3e0b)) -* **lang:** translations update from Weblate ([#2185](https://github.com/sct/overseerr/issues/2185)) ([dce10f7](https://github.com/sct/overseerr/commit/dce10f743f52cb04036e2cdaee280e26a81b253b)) -* **lang:** translations update from Weblate ([#2202](https://github.com/sct/overseerr/issues/2202)) ([492d8e3](https://github.com/sct/overseerr/commit/492d8e3daa5fb99aa9df2a18978085d5ddd581e7)) -* **lang:** translations update from Weblate ([#2210](https://github.com/sct/overseerr/issues/2210)) ([0a6ef6c](https://github.com/sct/overseerr/commit/0a6ef6cc81376f7a02f1483109be7ae4ab851c48)) -* **plex-scan:** plex scanner improvements ([#2105](https://github.com/sct/overseerr/issues/2105)) ([afda9c7](https://github.com/sct/overseerr/commit/afda9c7dc222137b0e6654a6beb4737cf2c1752e)) -* **servarr:** auto fill base url when testing service if missing ([#1995](https://github.com/sct/overseerr/issues/1995)) ([739f667](https://github.com/sct/overseerr/commit/739f667b54d8dec258b74d0cd8fd8b3b88dcf8d5)) -* **ui:** link processing/requested status badges to service URL ([#1761](https://github.com/sct/overseerr/issues/1761)) ([032c14a](https://github.com/sct/overseerr/commit/032c14a22680f62f8106943297b081b68645ce61)) +- display release dates for theatrical, digital, and physical release types ([#1492](https://github.com/sct/overseerr/issues/1492)) ([a4dca23](https://github.com/sct/overseerr/commit/a4dca2356b7605026f7bc45b691496e765c3328c)) +- dynamically fetch login screen backdrop images ([#2206](https://github.com/sct/overseerr/issues/2206)) ([3486d0b](https://github.com/sct/overseerr/commit/3486d0bf5520cbdff60bd8fd023caed76c452973)) +- **frontend:** add Hulu to network slider ([#2204](https://github.com/sct/overseerr/issues/2204)) ([1e402f7](https://github.com/sct/overseerr/commit/1e402f710b53c11855aab0abdb4b12c51c30b022)) +- **jobs:** allow modifying job schedules ([#1440](https://github.com/sct/overseerr/issues/1440)) ([82614ca](https://github.com/sct/overseerr/commit/82614ca4410782a12d65b4c0a6526ff064be1241)) +- **lang:** add Czech and Danish display languages ([#2176](https://github.com/sct/overseerr/issues/2176)) ([8d8db6c](https://github.com/sct/overseerr/commit/8d8db6cf5d98d4e498a31db339d02f8a98057c8d)) +- **lang:** translations update from Weblate ([#2101](https://github.com/sct/overseerr/issues/2101)) ([c73cf7b](https://github.com/sct/overseerr/commit/c73cf7b19cbc19e97a777c0facb9264fb0113093)) +- **lang:** translations update from Weblate ([#2179](https://github.com/sct/overseerr/issues/2179)) ([e3312ce](https://github.com/sct/overseerr/commit/e3312cef33821c8cb76a4a63bd565c78d67b3e0b)) +- **lang:** translations update from Weblate ([#2185](https://github.com/sct/overseerr/issues/2185)) ([dce10f7](https://github.com/sct/overseerr/commit/dce10f743f52cb04036e2cdaee280e26a81b253b)) +- **lang:** translations update from Weblate ([#2202](https://github.com/sct/overseerr/issues/2202)) ([492d8e3](https://github.com/sct/overseerr/commit/492d8e3daa5fb99aa9df2a18978085d5ddd581e7)) +- **lang:** translations update from Weblate ([#2210](https://github.com/sct/overseerr/issues/2210)) ([0a6ef6c](https://github.com/sct/overseerr/commit/0a6ef6cc81376f7a02f1483109be7ae4ab851c48)) +- **plex-scan:** plex scanner improvements ([#2105](https://github.com/sct/overseerr/issues/2105)) ([afda9c7](https://github.com/sct/overseerr/commit/afda9c7dc222137b0e6654a6beb4737cf2c1752e)) +- **servarr:** auto fill base url when testing service if missing ([#1995](https://github.com/sct/overseerr/issues/1995)) ([739f667](https://github.com/sct/overseerr/commit/739f667b54d8dec258b74d0cd8fd8b3b88dcf8d5)) +- **ui:** link processing/requested status badges to service URL ([#1761](https://github.com/sct/overseerr/issues/1761)) ([032c14a](https://github.com/sct/overseerr/commit/032c14a22680f62f8106943297b081b68645ce61)) ## [1.26.1](https://github.com/sct/overseerr/compare/v1.26.0...v1.26.1) (2021-09-20) - ### Bug Fixes -* **rt-api:** correctly format movie urls ([4c6009b](https://github.com/sct/overseerr/commit/4c6009bc2c3ff5f657a806363e3bdf7cd83d4261)) +- **rt-api:** correctly format movie urls ([4c6009b](https://github.com/sct/overseerr/commit/4c6009bc2c3ff5f657a806363e3bdf7cd83d4261)) # [1.26.0](https://github.com/sct/overseerr/compare/v1.25.0...v1.26.0) (2021-09-19) - ### Bug Fixes -* **email:** omit links when application URL is not configured ([#1806](https://github.com/sct/overseerr/issues/1806)) ([1133a34](https://github.com/sct/overseerr/commit/1133a34ffdf95c4d036be0264fe7f94f64007e8f)) -* **lang:** minor changes to password reset strings ([#1798](https://github.com/sct/overseerr/issues/1798)) ([a41245c](https://github.com/sct/overseerr/commit/a41245c703688743ec24f9b4a53e70f3340daa0f)) -* **notif:** truncate media overviews ([#1800](https://github.com/sct/overseerr/issues/1800)) ([42e45f3](https://github.com/sct/overseerr/commit/42e45f38e5ede7df0fc4bdb20a970917b2361569)) -* **plex:** do not fail to scan empty libraries ([#1771](https://github.com/sct/overseerr/issues/1771)) ([6789b87](https://github.com/sct/overseerr/commit/6789b8701cb644d9a3f1384f30b3dff707201ef7)) -* **quota:** block multi-season requests that would exceed a user's quota ([#1874](https://github.com/sct/overseerr/issues/1874)) ([8a55f85](https://github.com/sct/overseerr/commit/8a55f85d3ef14ccb83b139acb35d0746431637be)) -* **rt-api:** use rotten-tomatoes 2.0 search api for movies ([a11bb49](https://github.com/sct/overseerr/commit/a11bb49663ec345332c4dd70ddbb49ce230b5c3c)) -* **ui:** center logo on password reset pages ([#1807](https://github.com/sct/overseerr/issues/1807)) ([b8e82b5](https://github.com/sct/overseerr/commit/b8e82b5b4d3cb49ec372e3dce3cd89dff440ffd0)) -* **ui:** change sidebar breakpoint to lg ([#1972](https://github.com/sct/overseerr/issues/1972)) ([70bd9e9](https://github.com/sct/overseerr/commit/70bd9e9308b607206b60a2a36a511de6e397a3db)) -* **ui:** do not allow submission of invalid form inputs ([#1799](https://github.com/sct/overseerr/issues/1799)) ([910d00c](https://github.com/sct/overseerr/commit/910d00c19522a70125bfb5e5081a7ef4000e7f54)) -* **ui:** do not display negative remaining quota ([#1859](https://github.com/sct/overseerr/issues/1859)) ([3841fb0](https://github.com/sct/overseerr/commit/3841fb06ebe1e09250362cc6cb401fdca12eef7f)) -* **ui:** fix notifications settings buttons overflowing ([#1911](https://github.com/sct/overseerr/issues/1911)) ([0ce18b2](https://github.com/sct/overseerr/commit/0ce18b21ca547af6c083c3f248e22b7daf92aef0)) -* **ui:** sort 'Request As' user dropdown by display name ([#2099](https://github.com/sct/overseerr/issues/2099)) ([bb09f8e](https://github.com/sct/overseerr/commit/bb09f8eaf70f6d0c981f31bd5f3c8afb2fe101ab)) -* **webpush:** load user in push sub query ([#1894](https://github.com/sct/overseerr/issues/1894)) ([6f2db6a](https://github.com/sct/overseerr/commit/6f2db6a6ccf299262cf86d91acf639b921f28286)) -* correct logo filename ([#1805](https://github.com/sct/overseerr/issues/1805)) ([f95be83](https://github.com/sct/overseerr/commit/f95be832f95a68b114ff24a65ffa0ebbd71b4121)) - +- **email:** omit links when application URL is not configured ([#1806](https://github.com/sct/overseerr/issues/1806)) ([1133a34](https://github.com/sct/overseerr/commit/1133a34ffdf95c4d036be0264fe7f94f64007e8f)) +- **lang:** minor changes to password reset strings ([#1798](https://github.com/sct/overseerr/issues/1798)) ([a41245c](https://github.com/sct/overseerr/commit/a41245c703688743ec24f9b4a53e70f3340daa0f)) +- **notif:** truncate media overviews ([#1800](https://github.com/sct/overseerr/issues/1800)) ([42e45f3](https://github.com/sct/overseerr/commit/42e45f38e5ede7df0fc4bdb20a970917b2361569)) +- **plex:** do not fail to scan empty libraries ([#1771](https://github.com/sct/overseerr/issues/1771)) ([6789b87](https://github.com/sct/overseerr/commit/6789b8701cb644d9a3f1384f30b3dff707201ef7)) +- **quota:** block multi-season requests that would exceed a user's quota ([#1874](https://github.com/sct/overseerr/issues/1874)) ([8a55f85](https://github.com/sct/overseerr/commit/8a55f85d3ef14ccb83b139acb35d0746431637be)) +- **rt-api:** use rotten-tomatoes 2.0 search api for movies ([a11bb49](https://github.com/sct/overseerr/commit/a11bb49663ec345332c4dd70ddbb49ce230b5c3c)) +- **ui:** center logo on password reset pages ([#1807](https://github.com/sct/overseerr/issues/1807)) ([b8e82b5](https://github.com/sct/overseerr/commit/b8e82b5b4d3cb49ec372e3dce3cd89dff440ffd0)) +- **ui:** change sidebar breakpoint to lg ([#1972](https://github.com/sct/overseerr/issues/1972)) ([70bd9e9](https://github.com/sct/overseerr/commit/70bd9e9308b607206b60a2a36a511de6e397a3db)) +- **ui:** do not allow submission of invalid form inputs ([#1799](https://github.com/sct/overseerr/issues/1799)) ([910d00c](https://github.com/sct/overseerr/commit/910d00c19522a70125bfb5e5081a7ef4000e7f54)) +- **ui:** do not display negative remaining quota ([#1859](https://github.com/sct/overseerr/issues/1859)) ([3841fb0](https://github.com/sct/overseerr/commit/3841fb06ebe1e09250362cc6cb401fdca12eef7f)) +- **ui:** fix notifications settings buttons overflowing ([#1911](https://github.com/sct/overseerr/issues/1911)) ([0ce18b2](https://github.com/sct/overseerr/commit/0ce18b21ca547af6c083c3f248e22b7daf92aef0)) +- **ui:** sort 'Request As' user dropdown by display name ([#2099](https://github.com/sct/overseerr/issues/2099)) ([bb09f8e](https://github.com/sct/overseerr/commit/bb09f8eaf70f6d0c981f31bd5f3c8afb2fe101ab)) +- **webpush:** load user in push sub query ([#1894](https://github.com/sct/overseerr/issues/1894)) ([6f2db6a](https://github.com/sct/overseerr/commit/6f2db6a6ccf299262cf86d91acf639b921f28286)) +- correct logo filename ([#1805](https://github.com/sct/overseerr/issues/1805)) ([f95be83](https://github.com/sct/overseerr/commit/f95be832f95a68b114ff24a65ffa0ebbd71b4121)) ### Features -* list streaming providers on movie/TV detail pages ([#1778](https://github.com/sct/overseerr/issues/1778)) ([98ece67](https://github.com/sct/overseerr/commit/98ece67655a5dffe894974e337a3603afeed0236)) -* **lang:** add Simplified Chinese display language ([#2032](https://github.com/sct/overseerr/issues/2032)) ([590ea7e](https://github.com/sct/overseerr/commit/590ea7e40460e381377b212d00869f191908b41f)) -* **lang:** translated using Weblate (German) ([#1791](https://github.com/sct/overseerr/issues/1791)) ([15f7941](https://github.com/sct/overseerr/commit/15f7941269075b7e12de8bbc0f98418af70df380)) -* **lang:** translations update from Weblate ([#1772](https://github.com/sct/overseerr/issues/1772)) ([6a75a05](https://github.com/sct/overseerr/commit/6a75a05c2348455d5374132a2574d988879d543a)) -* **lang:** translations update from Weblate ([#1796](https://github.com/sct/overseerr/issues/1796)) ([57b52fc](https://github.com/sct/overseerr/commit/57b52fc9cccd3fac93cdb68e36cf652ddbcdf86c)) -* **lang:** translations update from Weblate ([#1910](https://github.com/sct/overseerr/issues/1910)) ([fe89fd5](https://github.com/sct/overseerr/commit/fe89fd5f12460cb1b3acb09fb16b62497ef50f5f)) -* **lang:** translations update from Weblate ([#2058](https://github.com/sct/overseerr/issues/2058)) ([db42c46](https://github.com/sct/overseerr/commit/db42c4678145d2a9676aa71b6773607b696f7cea)) -* **notif:** Restyle HTML email notifications Part 2 ([#1917](https://github.com/sct/overseerr/issues/1917)) ([376149d](https://github.com/sct/overseerr/commit/376149d6ebb4db28d949391115f475afdd4e7d48)) -* **ui:** add 'show more/less...' for studios on movie details page ([#1770](https://github.com/sct/overseerr/issues/1770)) ([680ea0c](https://github.com/sct/overseerr/commit/680ea0c87a9ae143413354680c421d62bccd869d)) -* new logo, who dis? ([#1802](https://github.com/sct/overseerr/issues/1802)) ([beb5637](https://github.com/sct/overseerr/commit/beb5637d9f5c01d773eaee93035b7c195c2ae5f2)) +- list streaming providers on movie/TV detail pages ([#1778](https://github.com/sct/overseerr/issues/1778)) ([98ece67](https://github.com/sct/overseerr/commit/98ece67655a5dffe894974e337a3603afeed0236)) +- **lang:** add Simplified Chinese display language ([#2032](https://github.com/sct/overseerr/issues/2032)) ([590ea7e](https://github.com/sct/overseerr/commit/590ea7e40460e381377b212d00869f191908b41f)) +- **lang:** translated using Weblate (German) ([#1791](https://github.com/sct/overseerr/issues/1791)) ([15f7941](https://github.com/sct/overseerr/commit/15f7941269075b7e12de8bbc0f98418af70df380)) +- **lang:** translations update from Weblate ([#1772](https://github.com/sct/overseerr/issues/1772)) ([6a75a05](https://github.com/sct/overseerr/commit/6a75a05c2348455d5374132a2574d988879d543a)) +- **lang:** translations update from Weblate ([#1796](https://github.com/sct/overseerr/issues/1796)) ([57b52fc](https://github.com/sct/overseerr/commit/57b52fc9cccd3fac93cdb68e36cf652ddbcdf86c)) +- **lang:** translations update from Weblate ([#1910](https://github.com/sct/overseerr/issues/1910)) ([fe89fd5](https://github.com/sct/overseerr/commit/fe89fd5f12460cb1b3acb09fb16b62497ef50f5f)) +- **lang:** translations update from Weblate ([#2058](https://github.com/sct/overseerr/issues/2058)) ([db42c46](https://github.com/sct/overseerr/commit/db42c4678145d2a9676aa71b6773607b696f7cea)) +- **notif:** Restyle HTML email notifications Part 2 ([#1917](https://github.com/sct/overseerr/issues/1917)) ([376149d](https://github.com/sct/overseerr/commit/376149d6ebb4db28d949391115f475afdd4e7d48)) +- **ui:** add 'show more/less...' for studios on movie details page ([#1770](https://github.com/sct/overseerr/issues/1770)) ([680ea0c](https://github.com/sct/overseerr/commit/680ea0c87a9ae143413354680c421d62bccd869d)) +- new logo, who dis? ([#1802](https://github.com/sct/overseerr/issues/1802)) ([beb5637](https://github.com/sct/overseerr/commit/beb5637d9f5c01d773eaee93035b7c195c2ae5f2)) # [1.25.0](https://github.com/sct/overseerr/compare/v1.24.0...v1.25.0) (2021-06-10) - ### Bug Fixes -* **frontend:** add missing route guards to settings pages ([#1700](https://github.com/sct/overseerr/issues/1700)) ([78fc1f7](https://github.com/sct/overseerr/commit/78fc1f7b7d9ef912077066a3605fed6237fb4c8a)) -* **locale:** set locale based on user settings upon login ([#1584](https://github.com/sct/overseerr/issues/1584)) ([f48312e](https://github.com/sct/overseerr/commit/f48312e833ed5d48c41179d0eadbc66d45486d8a)) -* **notif:** include year in Media Available notifications ([#1672](https://github.com/sct/overseerr/issues/1672)) ([11aa712](https://github.com/sct/overseerr/commit/11aa712eb0e8796874c96fbcc9b51b523108e2d4)) -* **plex:** disable library sync if Plex not configured, and disable scan if no libraries ([#1764](https://github.com/sct/overseerr/issues/1764)) ([22238fe](https://github.com/sct/overseerr/commit/22238fe4f711267d001be95942b3151c536e0c18)) -* **plex:** do not fail to import Plex users when Plex Home has managed users ([#1699](https://github.com/sct/overseerr/issues/1699)) ([310cdb3](https://github.com/sct/overseerr/commit/310cdb36df1601bca5e57f0bc796c44111b8435f)) -* **plex:** sync libraries after saving settings ([#1592](https://github.com/sct/overseerr/issues/1592)) ([9749d72](https://github.com/sct/overseerr/commit/9749d723fc0a282b291c06ee68a6e174dcec1c5b)) -* **requests:** appropriately set modifiedBy user for new requests ([#1684](https://github.com/sct/overseerr/issues/1684)) ([a3f04b3](https://github.com/sct/overseerr/commit/a3f04b3f3522d46dc65178bddd1e986426e48050)) -* **requests:** do not prevent duplicate requests if other requests are declined ([de0759c](https://github.com/sct/overseerr/commit/de0759c26a9e857e2b8d7244673625fc79ee4660)) -* **requests:** prevent duplicate movie requests ([126d866](https://github.com/sct/overseerr/commit/126d8665ee2808fc0bc37df4ca61f3e63be096e2)) -* check that application URL and email agent are configured for password reset/generation ([#1724](https://github.com/sct/overseerr/issues/1724)) ([091d66a](https://github.com/sct/overseerr/commit/091d66a1928d3c69a11eab2a789b4639b5ba9817)) -* correctly display error messages ([#1653](https://github.com/sct/overseerr/issues/1653)) ([31cb717](https://github.com/sct/overseerr/commit/31cb7176d286e706575a2dc8003df13f3e737106)) -* handle null values in User email transform ([#1712](https://github.com/sct/overseerr/issues/1712)) ([4a042f1](https://github.com/sct/overseerr/commit/4a042f12be6510ee47de3a7e025497f8d132d6a1)) -* **lang:** only set locale once at page load and move subsequent updates back into Layout ([14756f4](https://github.com/sct/overseerr/commit/14756f4b208c5b201a6e632b43e7a21c5bec6f9c)), closes [#1662](https://github.com/sct/overseerr/issues/1662) -* **locale:** properly restore display language upon page refresh ([#1646](https://github.com/sct/overseerr/issues/1646)) ([e85d1ce](https://github.com/sct/overseerr/commit/e85d1ce94ec45d8f5d086722cfd88e0e2c5b4bb6)) -* **notifications:** default webpush notification agent to enabled for users for settings response ([7520e24](https://github.com/sct/overseerr/commit/7520e24e9287e214dd31224f1201e9b6385fd567)), closes [#1663](https://github.com/sct/overseerr/issues/1663) -* **quotas:** do not count already-requested seasons when editing TV request ([#1649](https://github.com/sct/overseerr/issues/1649)) ([808ccf1](https://github.com/sct/overseerr/commit/808ccf1c6975f853db6dc89f4d9f1f5488dbaae3)) -* **requests:** remove requestedBy user param from existing movie request check ([#1569](https://github.com/sct/overseerr/issues/1569)) ([788f3dc](https://github.com/sct/overseerr/commit/788f3dc435ae224fcc4d4cb2890b1b9b494c64e8)) -* **sensitiveinput:** do not capture enter key input ([#1650](https://github.com/sct/overseerr/issues/1650)) ([bb8d14b](https://github.com/sct/overseerr/commit/bb8d14b5ffd840eff0c2a00e1b5d318677a5ca5f)) -* **sonarr:** do not mark media as failed if there is no season data on TVDB ([#1691](https://github.com/sct/overseerr/issues/1691)) ([0cd7fa0](https://github.com/sct/overseerr/commit/0cd7fa0f1a00d129339be13550a4f694c820a0e9)) -* **tv:** don't show duplicate air date ([#1666](https://github.com/sct/overseerr/issues/1666)) ([e1f5feb](https://github.com/sct/overseerr/commit/e1f5febe7bbf27e77b6f5d057c2c3f7e22898734)) -* **ui:** add clarification to user settings ([#1644](https://github.com/sct/overseerr/issues/1644)) ([2ef57e9](https://github.com/sct/overseerr/commit/2ef57e9b1a5b4d0a1499921f4e26b0b0712d7ded)) -* **ui:** correct horizontal overflow behavior of settings tabs ([#1667](https://github.com/sct/overseerr/issues/1667)) ([e6d5f0a](https://github.com/sct/overseerr/commit/e6d5f0abfebdc24f25d08822b57a8eb7bc48e137)) -* **ui:** hide advanced request options when there is only one choice ([#1591](https://github.com/sct/overseerr/issues/1591)) ([6b26188](https://github.com/sct/overseerr/commit/6b26188d888a1f80bd36a1968e41333bab2af794)) -* **ui:** improve QuotaSelector display of unlimited and singular values ([#1704](https://github.com/sct/overseerr/issues/1704)) ([59b2ec1](https://github.com/sct/overseerr/commit/59b2ec11fa8868bf6873ffa80f4999ae10d65637)) -* perform case-insensitive match for local user email addresses ([#1633](https://github.com/sct/overseerr/issues/1633)) ([928b8a7](https://github.com/sct/overseerr/commit/928b8a71cf361b7bc2b8957c621f5b66c4657b1e)) -* **ui:** apply pointer cursor style for clickable status badges ([#1632](https://github.com/sct/overseerr/issues/1632)) ([6968caa](https://github.com/sct/overseerr/commit/6968caa35a70c172bdd57c984fde6cb6a04a1470)) -* **ui:** remove delete button from request cards ([#1635](https://github.com/sct/overseerr/issues/1635)) ([6b37242](https://github.com/sct/overseerr/commit/6b37242a3f5a3b332d259f4814d235d751ae2491)) -* switch PGP regex to span multiple lines ([#1598](https://github.com/sct/overseerr/issues/1598)) ([d0703aa](https://github.com/sct/overseerr/commit/d0703aa37772759e8e28b5da7187e97e7aadc495)) -* **ui:** hide Plex alert after setup and add local login warning to local user modal ([#1600](https://github.com/sct/overseerr/issues/1600)) ([694d0ff](https://github.com/sct/overseerr/commit/694d0ffcf6b3e3fa00175400fa4217a7d6eb787f)) - +- **frontend:** add missing route guards to settings pages ([#1700](https://github.com/sct/overseerr/issues/1700)) ([78fc1f7](https://github.com/sct/overseerr/commit/78fc1f7b7d9ef912077066a3605fed6237fb4c8a)) +- **locale:** set locale based on user settings upon login ([#1584](https://github.com/sct/overseerr/issues/1584)) ([f48312e](https://github.com/sct/overseerr/commit/f48312e833ed5d48c41179d0eadbc66d45486d8a)) +- **notif:** include year in Media Available notifications ([#1672](https://github.com/sct/overseerr/issues/1672)) ([11aa712](https://github.com/sct/overseerr/commit/11aa712eb0e8796874c96fbcc9b51b523108e2d4)) +- **plex:** disable library sync if Plex not configured, and disable scan if no libraries ([#1764](https://github.com/sct/overseerr/issues/1764)) ([22238fe](https://github.com/sct/overseerr/commit/22238fe4f711267d001be95942b3151c536e0c18)) +- **plex:** do not fail to import Plex users when Plex Home has managed users ([#1699](https://github.com/sct/overseerr/issues/1699)) ([310cdb3](https://github.com/sct/overseerr/commit/310cdb36df1601bca5e57f0bc796c44111b8435f)) +- **plex:** sync libraries after saving settings ([#1592](https://github.com/sct/overseerr/issues/1592)) ([9749d72](https://github.com/sct/overseerr/commit/9749d723fc0a282b291c06ee68a6e174dcec1c5b)) +- **requests:** appropriately set modifiedBy user for new requests ([#1684](https://github.com/sct/overseerr/issues/1684)) ([a3f04b3](https://github.com/sct/overseerr/commit/a3f04b3f3522d46dc65178bddd1e986426e48050)) +- **requests:** do not prevent duplicate requests if other requests are declined ([de0759c](https://github.com/sct/overseerr/commit/de0759c26a9e857e2b8d7244673625fc79ee4660)) +- **requests:** prevent duplicate movie requests ([126d866](https://github.com/sct/overseerr/commit/126d8665ee2808fc0bc37df4ca61f3e63be096e2)) +- check that application URL and email agent are configured for password reset/generation ([#1724](https://github.com/sct/overseerr/issues/1724)) ([091d66a](https://github.com/sct/overseerr/commit/091d66a1928d3c69a11eab2a789b4639b5ba9817)) +- correctly display error messages ([#1653](https://github.com/sct/overseerr/issues/1653)) ([31cb717](https://github.com/sct/overseerr/commit/31cb7176d286e706575a2dc8003df13f3e737106)) +- handle null values in User email transform ([#1712](https://github.com/sct/overseerr/issues/1712)) ([4a042f1](https://github.com/sct/overseerr/commit/4a042f12be6510ee47de3a7e025497f8d132d6a1)) +- **lang:** only set locale once at page load and move subsequent updates back into Layout ([14756f4](https://github.com/sct/overseerr/commit/14756f4b208c5b201a6e632b43e7a21c5bec6f9c)), closes [#1662](https://github.com/sct/overseerr/issues/1662) +- **locale:** properly restore display language upon page refresh ([#1646](https://github.com/sct/overseerr/issues/1646)) ([e85d1ce](https://github.com/sct/overseerr/commit/e85d1ce94ec45d8f5d086722cfd88e0e2c5b4bb6)) +- **notifications:** default webpush notification agent to enabled for users for settings response ([7520e24](https://github.com/sct/overseerr/commit/7520e24e9287e214dd31224f1201e9b6385fd567)), closes [#1663](https://github.com/sct/overseerr/issues/1663) +- **quotas:** do not count already-requested seasons when editing TV request ([#1649](https://github.com/sct/overseerr/issues/1649)) ([808ccf1](https://github.com/sct/overseerr/commit/808ccf1c6975f853db6dc89f4d9f1f5488dbaae3)) +- **requests:** remove requestedBy user param from existing movie request check ([#1569](https://github.com/sct/overseerr/issues/1569)) ([788f3dc](https://github.com/sct/overseerr/commit/788f3dc435ae224fcc4d4cb2890b1b9b494c64e8)) +- **sensitiveinput:** do not capture enter key input ([#1650](https://github.com/sct/overseerr/issues/1650)) ([bb8d14b](https://github.com/sct/overseerr/commit/bb8d14b5ffd840eff0c2a00e1b5d318677a5ca5f)) +- **sonarr:** do not mark media as failed if there is no season data on TVDB ([#1691](https://github.com/sct/overseerr/issues/1691)) ([0cd7fa0](https://github.com/sct/overseerr/commit/0cd7fa0f1a00d129339be13550a4f694c820a0e9)) +- **tv:** don't show duplicate air date ([#1666](https://github.com/sct/overseerr/issues/1666)) ([e1f5feb](https://github.com/sct/overseerr/commit/e1f5febe7bbf27e77b6f5d057c2c3f7e22898734)) +- **ui:** add clarification to user settings ([#1644](https://github.com/sct/overseerr/issues/1644)) ([2ef57e9](https://github.com/sct/overseerr/commit/2ef57e9b1a5b4d0a1499921f4e26b0b0712d7ded)) +- **ui:** correct horizontal overflow behavior of settings tabs ([#1667](https://github.com/sct/overseerr/issues/1667)) ([e6d5f0a](https://github.com/sct/overseerr/commit/e6d5f0abfebdc24f25d08822b57a8eb7bc48e137)) +- **ui:** hide advanced request options when there is only one choice ([#1591](https://github.com/sct/overseerr/issues/1591)) ([6b26188](https://github.com/sct/overseerr/commit/6b26188d888a1f80bd36a1968e41333bab2af794)) +- **ui:** improve QuotaSelector display of unlimited and singular values ([#1704](https://github.com/sct/overseerr/issues/1704)) ([59b2ec1](https://github.com/sct/overseerr/commit/59b2ec11fa8868bf6873ffa80f4999ae10d65637)) +- perform case-insensitive match for local user email addresses ([#1633](https://github.com/sct/overseerr/issues/1633)) ([928b8a7](https://github.com/sct/overseerr/commit/928b8a71cf361b7bc2b8957c621f5b66c4657b1e)) +- **ui:** apply pointer cursor style for clickable status badges ([#1632](https://github.com/sct/overseerr/issues/1632)) ([6968caa](https://github.com/sct/overseerr/commit/6968caa35a70c172bdd57c984fde6cb6a04a1470)) +- **ui:** remove delete button from request cards ([#1635](https://github.com/sct/overseerr/issues/1635)) ([6b37242](https://github.com/sct/overseerr/commit/6b37242a3f5a3b332d259f4814d235d751ae2491)) +- switch PGP regex to span multiple lines ([#1598](https://github.com/sct/overseerr/issues/1598)) ([d0703aa](https://github.com/sct/overseerr/commit/d0703aa37772759e8e28b5da7187e97e7aadc495)) +- **ui:** hide Plex alert after setup and add local login warning to local user modal ([#1600](https://github.com/sct/overseerr/issues/1600)) ([694d0ff](https://github.com/sct/overseerr/commit/694d0ffcf6b3e3fa00175400fa4217a7d6eb787f)) ### Features -* **lang:** add Greek display language ([#1605](https://github.com/sct/overseerr/issues/1605)) ([2241564](https://github.com/sct/overseerr/commit/22415642e8602809e3507e5b13dc2f8de3000003)) -* **lang:** translations update from Weblate ([#1585](https://github.com/sct/overseerr/issues/1585)) ([361ea77](https://github.com/sct/overseerr/commit/361ea77588db3dc04a51dd3a62c73ae1297cdce2)) -* **lang:** translations update from Weblate ([#1603](https://github.com/sct/overseerr/issues/1603)) ([2efa7fa](https://github.com/sct/overseerr/commit/2efa7faf20d05a5fc423e0151c6b46fe6212d096)) -* **lang:** translations update from Weblate ([#1639](https://github.com/sct/overseerr/issues/1639)) ([d22400d](https://github.com/sct/overseerr/commit/d22400dbc9320743498eeb8e6a4dcbccf1a4d52d)) -* **lang:** translations update from Weblate ([#1676](https://github.com/sct/overseerr/issues/1676)) ([8a80571](https://github.com/sct/overseerr/commit/8a805716e3e34ae8d081ad47f9d4cd68f88b0116)) -* **lang:** translations update from Weblate ([#1703](https://github.com/sct/overseerr/issues/1703)) ([6a3649f](https://github.com/sct/overseerr/commit/6a3649f620e518ff07a48c17ce1182aaedff398a)) -* **lang:** translations update from Weblate ([#1727](https://github.com/sct/overseerr/issues/1727)) ([60c3ced](https://github.com/sct/overseerr/commit/60c3ced9e2466568eecde93c88410c87ff0b796f)) -* **lang:** translations update from Weblate ([#1746](https://github.com/sct/overseerr/issues/1746)) ([37a4df6](https://github.com/sct/overseerr/commit/37a4df646cc3e3101360037f1b6f061a734eb5e2)) -* **lang:** translations update from Weblate ([#1768](https://github.com/sct/overseerr/issues/1768)) ([dedf95e](https://github.com/sct/overseerr/commit/dedf95e574a15a708866c381353e58ce3b3a1a61)) -* add display name to create local user modal ([#1631](https://github.com/sct/overseerr/issues/1631)) ([44c3edb](https://github.com/sct/overseerr/commit/44c3edb98568ba15eb525e665115429cfb15d28b)) -* allow users to select notification types ([#1512](https://github.com/sct/overseerr/issues/1512)) ([e605989](https://github.com/sct/overseerr/commit/e60598905b2d6eef7c1872d0c9e92e6d70508ae8)) -* **notif:** prevent manage-request users receiving auto-approve notif from their requests ([#1707](https://github.com/sct/overseerr/issues/1707)) ([#1709](https://github.com/sct/overseerr/issues/1709)) ([9ead8bb](https://github.com/sct/overseerr/commit/9ead8bb1f1680b522550f963502c83e2f99d1e96)) -* **plex:** add support for custom Plex Web App URLs ([#1581](https://github.com/sct/overseerr/issues/1581)) ([a640a91](https://github.com/sct/overseerr/commit/a640a91390f1411637ad379a8253002fdf60480f)) -* **pwa:** add notification badge icon ([#1695](https://github.com/sct/overseerr/issues/1695)) ([9b3b6a9](https://github.com/sct/overseerr/commit/9b3b6a9170b25209e54c74aa9e96659bc2d19edd)) -* **ui:** request list item & request card improvements ([#1532](https://github.com/sct/overseerr/issues/1532)) ([d7b9b1a](https://github.com/sct/overseerr/commit/d7b9b1a525ec6d1d81ad6fe4e55994dd8428988f)) -* **webpush:** add warning to web push settings re: HTTPS requirement ([#1599](https://github.com/sct/overseerr/issues/1599)) ([0c4fb64](https://github.com/sct/overseerr/commit/0c4fb6446be425905a120df5be9a28b052e884c0)) - +- **lang:** add Greek display language ([#1605](https://github.com/sct/overseerr/issues/1605)) ([2241564](https://github.com/sct/overseerr/commit/22415642e8602809e3507e5b13dc2f8de3000003)) +- **lang:** translations update from Weblate ([#1585](https://github.com/sct/overseerr/issues/1585)) ([361ea77](https://github.com/sct/overseerr/commit/361ea77588db3dc04a51dd3a62c73ae1297cdce2)) +- **lang:** translations update from Weblate ([#1603](https://github.com/sct/overseerr/issues/1603)) ([2efa7fa](https://github.com/sct/overseerr/commit/2efa7faf20d05a5fc423e0151c6b46fe6212d096)) +- **lang:** translations update from Weblate ([#1639](https://github.com/sct/overseerr/issues/1639)) ([d22400d](https://github.com/sct/overseerr/commit/d22400dbc9320743498eeb8e6a4dcbccf1a4d52d)) +- **lang:** translations update from Weblate ([#1676](https://github.com/sct/overseerr/issues/1676)) ([8a80571](https://github.com/sct/overseerr/commit/8a805716e3e34ae8d081ad47f9d4cd68f88b0116)) +- **lang:** translations update from Weblate ([#1703](https://github.com/sct/overseerr/issues/1703)) ([6a3649f](https://github.com/sct/overseerr/commit/6a3649f620e518ff07a48c17ce1182aaedff398a)) +- **lang:** translations update from Weblate ([#1727](https://github.com/sct/overseerr/issues/1727)) ([60c3ced](https://github.com/sct/overseerr/commit/60c3ced9e2466568eecde93c88410c87ff0b796f)) +- **lang:** translations update from Weblate ([#1746](https://github.com/sct/overseerr/issues/1746)) ([37a4df6](https://github.com/sct/overseerr/commit/37a4df646cc3e3101360037f1b6f061a734eb5e2)) +- **lang:** translations update from Weblate ([#1768](https://github.com/sct/overseerr/issues/1768)) ([dedf95e](https://github.com/sct/overseerr/commit/dedf95e574a15a708866c381353e58ce3b3a1a61)) +- add display name to create local user modal ([#1631](https://github.com/sct/overseerr/issues/1631)) ([44c3edb](https://github.com/sct/overseerr/commit/44c3edb98568ba15eb525e665115429cfb15d28b)) +- allow users to select notification types ([#1512](https://github.com/sct/overseerr/issues/1512)) ([e605989](https://github.com/sct/overseerr/commit/e60598905b2d6eef7c1872d0c9e92e6d70508ae8)) +- **notif:** prevent manage-request users receiving auto-approve notif from their requests ([#1707](https://github.com/sct/overseerr/issues/1707)) ([#1709](https://github.com/sct/overseerr/issues/1709)) ([9ead8bb](https://github.com/sct/overseerr/commit/9ead8bb1f1680b522550f963502c83e2f99d1e96)) +- **plex:** add support for custom Plex Web App URLs ([#1581](https://github.com/sct/overseerr/issues/1581)) ([a640a91](https://github.com/sct/overseerr/commit/a640a91390f1411637ad379a8253002fdf60480f)) +- **pwa:** add notification badge icon ([#1695](https://github.com/sct/overseerr/issues/1695)) ([9b3b6a9](https://github.com/sct/overseerr/commit/9b3b6a9170b25209e54c74aa9e96659bc2d19edd)) +- **ui:** request list item & request card improvements ([#1532](https://github.com/sct/overseerr/issues/1532)) ([d7b9b1a](https://github.com/sct/overseerr/commit/d7b9b1a525ec6d1d81ad6fe4e55994dd8428988f)) +- **webpush:** add warning to web push settings re: HTTPS requirement ([#1599](https://github.com/sct/overseerr/issues/1599)) ([0c4fb64](https://github.com/sct/overseerr/commit/0c4fb6446be425905a120df5be9a28b052e884c0)) ### Reverts -* **deps:** revert back to typeorm 0.2.32 ([4368c3a](https://github.com/sct/overseerr/commit/4368c3aa4f88425ec08f3b555419e572cfa320e3)) -* **deps:** use 10.1.3 until css import issue is resolved ([2254248](https://github.com/sct/overseerr/commit/2254248abc0f2051a9dd28d9663c7ab1d0b547b6)) -* **requests:** go back to old modifiedBy request values for now ([0918b25](https://github.com/sct/overseerr/commit/0918b254132b0541999486e1f0679d0c0cd65864)) +- **deps:** revert back to typeorm 0.2.32 ([4368c3a](https://github.com/sct/overseerr/commit/4368c3aa4f88425ec08f3b555419e572cfa320e3)) +- **deps:** use 10.1.3 until css import issue is resolved ([2254248](https://github.com/sct/overseerr/commit/2254248abc0f2051a9dd28d9663c7ab1d0b547b6)) +- **requests:** go back to old modifiedBy request values for now ([0918b25](https://github.com/sct/overseerr/commit/0918b254132b0541999486e1f0679d0c0cd65864)) # [1.24.0](https://github.com/sct/overseerr/compare/v1.23.2...v1.24.0) (2021-05-05) - ### Bug Fixes -* **api:** do not try to transform empty values passed to user notificationTypes ([ef3f977](https://github.com/sct/overseerr/commit/ef3f9778aa81f8ed39dcd835d63d94f2248e0204)), closes [#1501](https://github.com/sct/overseerr/issues/1501) -* **backend:** properly set request media status ([#1541](https://github.com/sct/overseerr/issues/1541)) ([b7b55e2](https://github.com/sct/overseerr/commit/b7b55e275cb2f1f61c3057cb8ab4cb1027f6356d)) -* **css:** don't target button globally ([#1510](https://github.com/sct/overseerr/issues/1510)) ([f78b9c1](https://github.com/sct/overseerr/commit/f78b9c1ca9648eb10b010e526d9b9db09648b154)) -* **css:** fix cog icon size on media detail pages ([#1520](https://github.com/sct/overseerr/issues/1520)) ([26ddc03](https://github.com/sct/overseerr/commit/26ddc03b2c01b343c24f1c359b78c587310cc747)) -* **email:** parse sender hostname from application URL ([#1518](https://github.com/sct/overseerr/issues/1518)) ([3baa55c](https://github.com/sct/overseerr/commit/3baa55c690dd9ba39768b8b271595cb6b09fe6da)) -* **lang:** correct overwritten email toast strings ([11a5e8d](https://github.com/sct/overseerr/commit/11a5e8d95bc2a2f16adf1e48d2ef38b508a6ace5)) -* **locale:** default user locale should be the server setting ([#1574](https://github.com/sct/overseerr/issues/1574)) ([549103f](https://github.com/sct/overseerr/commit/549103f6f6d5624201e425df7d7814f0f67863b9)) -* **pwa:** add Discover shortcut and fix/optimize icons ([#1525](https://github.com/sct/overseerr/issues/1525)) ([e1dc62b](https://github.com/sct/overseerr/commit/e1dc62b0a5b64202701aff821837ed11dd3f12db)) -* **radarr:** only process Radarr movies which are either monitored or downloaded ([#1511](https://github.com/sct/overseerr/issues/1511)) ([85899ab](https://github.com/sct/overseerr/commit/85899ab49a27542390e91443531905737224338d)) -* **ui:** add missing margins on button SVGs on Plex Settings page ([#1546](https://github.com/sct/overseerr/issues/1546)) ([5e588be](https://github.com/sct/overseerr/commit/5e588be8127b50dd83477f7f3a65f18de774e8af)) -* **ui:** add user profile links to RequestBlock and change 'ETA' string in DownloadBlock ([#1551](https://github.com/sct/overseerr/issues/1551)) ([e4d0029](https://github.com/sct/overseerr/commit/e4d0029f7b4245b8606e2447c54629def40c7761)) -* **ui:** apply rounded-l-only to SensitiveInput textareas and increase visible text input area ([#1561](https://github.com/sct/overseerr/issues/1561)) ([1123fce](https://github.com/sct/overseerr/commit/1123fce089b86251dcafebf77743d60a6e396bee)) -* **ui:** correct RegionSelector z-index ([#1567](https://github.com/sct/overseerr/issues/1567)) ([e912a00](https://github.com/sct/overseerr/commit/e912a00880f856fa9621e8587ef1cc6513a3d49c)) -* **ui:** correct toasts being in the wrong position on smaller screens ([2ecd9d7](https://github.com/sct/overseerr/commit/2ecd9d7b1391b8fc83e9c12a18bab105e7148f0f)) -* **ui:** default to text input type for SensitiveInputs ([#1568](https://github.com/sct/overseerr/issues/1568)) ([e2acf88](https://github.com/sct/overseerr/commit/e2acf8887cb0456c80308bd1b7f3bbe1930e8cff)) -* **ui:** explicitly specify width/height of Listbox dropdown icon ([#1514](https://github.com/sct/overseerr/issues/1514)) ([802e40a](https://github.com/sct/overseerr/commit/802e40a5dfa00f897f9d5a741718a319f74ff030)) -* **ui:** improve form usability ([#1563](https://github.com/sct/overseerr/issues/1563)) ([26580ea](https://github.com/sct/overseerr/commit/26580eaa218702bc5841718310e340d049c50332)) -* **ui:** show warning if user has both a default non-4K server and a non-default 4K server ([#1478](https://github.com/sct/overseerr/issues/1478)) ([4faddf3](https://github.com/sct/overseerr/commit/4faddf3810e20851c7ae1251ff0187fa13d7b0f6)) -* **webpush:** only prompt user to allow notifications if enabled in user settings ([#1552](https://github.com/sct/overseerr/issues/1552)) ([b05b177](https://github.com/sct/overseerr/commit/b05b177776a5d22bf3b5e93bad4358f4007b879a)) -* correctly fall back to English name in LanguageSelector ([#1537](https://github.com/sct/overseerr/issues/1537)) ([189313e](https://github.com/sct/overseerr/commit/189313e94a16e694d192d157642d77f664fd709b)) -* do not set locale when modifying other users ([#1499](https://github.com/sct/overseerr/issues/1499)) ([4858771](https://github.com/sct/overseerr/commit/48587719e9474139c7bbc2970b1c7d1d17b78a81)) - +- **api:** do not try to transform empty values passed to user notificationTypes ([ef3f977](https://github.com/sct/overseerr/commit/ef3f9778aa81f8ed39dcd835d63d94f2248e0204)), closes [#1501](https://github.com/sct/overseerr/issues/1501) +- **backend:** properly set request media status ([#1541](https://github.com/sct/overseerr/issues/1541)) ([b7b55e2](https://github.com/sct/overseerr/commit/b7b55e275cb2f1f61c3057cb8ab4cb1027f6356d)) +- **css:** don't target button globally ([#1510](https://github.com/sct/overseerr/issues/1510)) ([f78b9c1](https://github.com/sct/overseerr/commit/f78b9c1ca9648eb10b010e526d9b9db09648b154)) +- **css:** fix cog icon size on media detail pages ([#1520](https://github.com/sct/overseerr/issues/1520)) ([26ddc03](https://github.com/sct/overseerr/commit/26ddc03b2c01b343c24f1c359b78c587310cc747)) +- **email:** parse sender hostname from application URL ([#1518](https://github.com/sct/overseerr/issues/1518)) ([3baa55c](https://github.com/sct/overseerr/commit/3baa55c690dd9ba39768b8b271595cb6b09fe6da)) +- **lang:** correct overwritten email toast strings ([11a5e8d](https://github.com/sct/overseerr/commit/11a5e8d95bc2a2f16adf1e48d2ef38b508a6ace5)) +- **locale:** default user locale should be the server setting ([#1574](https://github.com/sct/overseerr/issues/1574)) ([549103f](https://github.com/sct/overseerr/commit/549103f6f6d5624201e425df7d7814f0f67863b9)) +- **pwa:** add Discover shortcut and fix/optimize icons ([#1525](https://github.com/sct/overseerr/issues/1525)) ([e1dc62b](https://github.com/sct/overseerr/commit/e1dc62b0a5b64202701aff821837ed11dd3f12db)) +- **radarr:** only process Radarr movies which are either monitored or downloaded ([#1511](https://github.com/sct/overseerr/issues/1511)) ([85899ab](https://github.com/sct/overseerr/commit/85899ab49a27542390e91443531905737224338d)) +- **ui:** add missing margins on button SVGs on Plex Settings page ([#1546](https://github.com/sct/overseerr/issues/1546)) ([5e588be](https://github.com/sct/overseerr/commit/5e588be8127b50dd83477f7f3a65f18de774e8af)) +- **ui:** add user profile links to RequestBlock and change 'ETA' string in DownloadBlock ([#1551](https://github.com/sct/overseerr/issues/1551)) ([e4d0029](https://github.com/sct/overseerr/commit/e4d0029f7b4245b8606e2447c54629def40c7761)) +- **ui:** apply rounded-l-only to SensitiveInput textareas and increase visible text input area ([#1561](https://github.com/sct/overseerr/issues/1561)) ([1123fce](https://github.com/sct/overseerr/commit/1123fce089b86251dcafebf77743d60a6e396bee)) +- **ui:** correct RegionSelector z-index ([#1567](https://github.com/sct/overseerr/issues/1567)) ([e912a00](https://github.com/sct/overseerr/commit/e912a00880f856fa9621e8587ef1cc6513a3d49c)) +- **ui:** correct toasts being in the wrong position on smaller screens ([2ecd9d7](https://github.com/sct/overseerr/commit/2ecd9d7b1391b8fc83e9c12a18bab105e7148f0f)) +- **ui:** default to text input type for SensitiveInputs ([#1568](https://github.com/sct/overseerr/issues/1568)) ([e2acf88](https://github.com/sct/overseerr/commit/e2acf8887cb0456c80308bd1b7f3bbe1930e8cff)) +- **ui:** explicitly specify width/height of Listbox dropdown icon ([#1514](https://github.com/sct/overseerr/issues/1514)) ([802e40a](https://github.com/sct/overseerr/commit/802e40a5dfa00f897f9d5a741718a319f74ff030)) +- **ui:** improve form usability ([#1563](https://github.com/sct/overseerr/issues/1563)) ([26580ea](https://github.com/sct/overseerr/commit/26580eaa218702bc5841718310e340d049c50332)) +- **ui:** show warning if user has both a default non-4K server and a non-default 4K server ([#1478](https://github.com/sct/overseerr/issues/1478)) ([4faddf3](https://github.com/sct/overseerr/commit/4faddf3810e20851c7ae1251ff0187fa13d7b0f6)) +- **webpush:** only prompt user to allow notifications if enabled in user settings ([#1552](https://github.com/sct/overseerr/issues/1552)) ([b05b177](https://github.com/sct/overseerr/commit/b05b177776a5d22bf3b5e93bad4358f4007b879a)) +- correctly fall back to English name in LanguageSelector ([#1537](https://github.com/sct/overseerr/issues/1537)) ([189313e](https://github.com/sct/overseerr/commit/189313e94a16e694d192d157642d77f664fd709b)) +- do not set locale when modifying other users ([#1499](https://github.com/sct/overseerr/issues/1499)) ([4858771](https://github.com/sct/overseerr/commit/48587719e9474139c7bbc2970b1c7d1d17b78a81)) ### Features -* **email:** replace 'Enable SSL' setting with more descriptive/clear 'Encryption Method' setting ([#1549](https://github.com/sct/overseerr/issues/1549)) ([69ab7cc](https://github.com/sct/overseerr/commit/69ab7cc660bea43b70bdb646eabd3866c1b5a90f)) -* **inputs:** add support for toggling security on input fields ([#1404](https://github.com/sct/overseerr/issues/1404)) ([4fd452d](https://github.com/sct/overseerr/commit/4fd452dd1880f597a0acda812d567e7cb6c16d83)) -* **lang:** translated using Weblate (Spanish) ([#1553](https://github.com/sct/overseerr/issues/1553)) ([e3d5e33](https://github.com/sct/overseerr/commit/e3d5e33ec3e43d36ec832d6ca47f330fc7675088)) -* **lang:** translations update from Weblate ([#1497](https://github.com/sct/overseerr/issues/1497)) ([9a95a07](https://github.com/sct/overseerr/commit/9a95a073916c9968b8ef348d0805d77400ea203a)) -* **lang:** translations update from Weblate ([#1527](https://github.com/sct/overseerr/issues/1527)) ([1a6d4bd](https://github.com/sct/overseerr/commit/1a6d4bddc016f4aaad83b945e103b19be4d0da31)) -* **lang:** translations update from Weblate ([#1558](https://github.com/sct/overseerr/issues/1558)) ([6c9991d](https://github.com/sct/overseerr/commit/6c9991d474a5cd95d9a0a10104bd79d8a9f3ada9)) -* **lang:** translations update from Weblate ([#1566](https://github.com/sct/overseerr/issues/1566)) ([93c441e](https://github.com/sct/overseerr/commit/93c441ef6665291ca3698368e4b093c843726036)) -* add server default locale setting ([#1536](https://github.com/sct/overseerr/issues/1536)) ([f256a44](https://github.com/sct/overseerr/commit/f256a444c57f2d92c1c4918d4ff6e223ef85ecd2)) -* **notif:** add LunaSea agent ([#1495](https://github.com/sct/overseerr/issues/1495)) ([4e6fb00](https://github.com/sct/overseerr/commit/4e6fb00a4a59545817add1544c0b1555078809a4)) -* **notif:** show success/failure toast for test notifications ([#1442](https://github.com/sct/overseerr/issues/1442)) ([079645c](https://github.com/sct/overseerr/commit/079645c2c74edfb7e4f583de2ac72bb9824f6524)) -* **perms:** add separate REQUEST_MOVIE and REQUEST_TV permissions ([#1474](https://github.com/sct/overseerr/issues/1474)) ([91b9e0f](https://github.com/sct/overseerr/commit/91b9e0f67996a442b5c0117fe09e2d69c163fafb)) -* **pwa:** add shortcuts to PWA ([#1509](https://github.com/sct/overseerr/issues/1509)) ([ed99e49](https://github.com/sct/overseerr/commit/ed99e4976dc2700fe84c70af4887c1a431bba92c)) -* add option to only allow Plex sign-in from existing users ([#1496](https://github.com/sct/overseerr/issues/1496)) ([db49b20](https://github.com/sct/overseerr/commit/db49b2024d399d90f2d1500b262374efc42f333c)) -* PWA Support ([#1488](https://github.com/sct/overseerr/issues/1488)) ([28830d4](https://github.com/sct/overseerr/commit/28830d4ef809efa92a5879a81cac11ff52ea3d1f)) +- **email:** replace 'Enable SSL' setting with more descriptive/clear 'Encryption Method' setting ([#1549](https://github.com/sct/overseerr/issues/1549)) ([69ab7cc](https://github.com/sct/overseerr/commit/69ab7cc660bea43b70bdb646eabd3866c1b5a90f)) +- **inputs:** add support for toggling security on input fields ([#1404](https://github.com/sct/overseerr/issues/1404)) ([4fd452d](https://github.com/sct/overseerr/commit/4fd452dd1880f597a0acda812d567e7cb6c16d83)) +- **lang:** translated using Weblate (Spanish) ([#1553](https://github.com/sct/overseerr/issues/1553)) ([e3d5e33](https://github.com/sct/overseerr/commit/e3d5e33ec3e43d36ec832d6ca47f330fc7675088)) +- **lang:** translations update from Weblate ([#1497](https://github.com/sct/overseerr/issues/1497)) ([9a95a07](https://github.com/sct/overseerr/commit/9a95a073916c9968b8ef348d0805d77400ea203a)) +- **lang:** translations update from Weblate ([#1527](https://github.com/sct/overseerr/issues/1527)) ([1a6d4bd](https://github.com/sct/overseerr/commit/1a6d4bddc016f4aaad83b945e103b19be4d0da31)) +- **lang:** translations update from Weblate ([#1558](https://github.com/sct/overseerr/issues/1558)) ([6c9991d](https://github.com/sct/overseerr/commit/6c9991d474a5cd95d9a0a10104bd79d8a9f3ada9)) +- **lang:** translations update from Weblate ([#1566](https://github.com/sct/overseerr/issues/1566)) ([93c441e](https://github.com/sct/overseerr/commit/93c441ef6665291ca3698368e4b093c843726036)) +- add server default locale setting ([#1536](https://github.com/sct/overseerr/issues/1536)) ([f256a44](https://github.com/sct/overseerr/commit/f256a444c57f2d92c1c4918d4ff6e223ef85ecd2)) +- **notif:** add LunaSea agent ([#1495](https://github.com/sct/overseerr/issues/1495)) ([4e6fb00](https://github.com/sct/overseerr/commit/4e6fb00a4a59545817add1544c0b1555078809a4)) +- **notif:** show success/failure toast for test notifications ([#1442](https://github.com/sct/overseerr/issues/1442)) ([079645c](https://github.com/sct/overseerr/commit/079645c2c74edfb7e4f583de2ac72bb9824f6524)) +- **perms:** add separate REQUEST_MOVIE and REQUEST_TV permissions ([#1474](https://github.com/sct/overseerr/issues/1474)) ([91b9e0f](https://github.com/sct/overseerr/commit/91b9e0f67996a442b5c0117fe09e2d69c163fafb)) +- **pwa:** add shortcuts to PWA ([#1509](https://github.com/sct/overseerr/issues/1509)) ([ed99e49](https://github.com/sct/overseerr/commit/ed99e4976dc2700fe84c70af4887c1a431bba92c)) +- add option to only allow Plex sign-in from existing users ([#1496](https://github.com/sct/overseerr/issues/1496)) ([db49b20](https://github.com/sct/overseerr/commit/db49b2024d399d90f2d1500b262374efc42f333c)) +- PWA Support ([#1488](https://github.com/sct/overseerr/issues/1488)) ([28830d4](https://github.com/sct/overseerr/commit/28830d4ef809efa92a5879a81cac11ff52ea3d1f)) ## [1.23.2](https://github.com/sct/overseerr/compare/v1.23.1...v1.23.2) (2021-04-21) - ### Bug Fixes -* **lang:** add missing '4K' from singular case of approve/deny 4K request strings ([#1481](https://github.com/sct/overseerr/issues/1481)) ([a822b01](https://github.com/sct/overseerr/commit/a822b019220e86e362a2570e7024289450b4ed46)) -* **ui:** change 'Disable Auto-Search' checkbox to 'Enable Automatic Search' ([#1476](https://github.com/sct/overseerr/issues/1476)) ([1a311d2](https://github.com/sct/overseerr/commit/1a311d211d78731c9089e66ed5387c1b5afe33c0)) -* better error message when creating a user with an existing email ([f13f1c9](https://github.com/sct/overseerr/commit/f13f1c94515b5bd51382fa18ad96a2ccfd06e50d)), closes [#1441](https://github.com/sct/overseerr/issues/1441) -* set editRequest attribute as necessary, allow users to edit their own pending requests, and show 'View Request' button on series pages ([#1446](https://github.com/sct/overseerr/issues/1446)) ([89455ad](https://github.com/sct/overseerr/commit/89455ad9b783d04d993a0009c351b1096f2b222e)) -* **api:** add check for 4K request perms to request creation endpoint ([#1450](https://github.com/sct/overseerr/issues/1450)) ([4449241](https://github.com/sct/overseerr/commit/4449241a8f63fdaeaa4995aa7ec34127c322b9dd)) -* **notif:** include year in notifications ([#1439](https://github.com/sct/overseerr/issues/1439)) ([4e98f56](https://github.com/sct/overseerr/commit/4e98f567534a650e26b0244990b7ca549cecbe89)) -* **plex:** add support for plex.direct URLs ([#1437](https://github.com/sct/overseerr/issues/1437)) ([db07770](https://github.com/sct/overseerr/commit/db077700e42ab1d2c870213fd55bbdee74002775)) -* **radarr:** search in addition to monitoring existing movies ([#1449](https://github.com/sct/overseerr/issues/1449)) ([3ae7d00](https://github.com/sct/overseerr/commit/3ae7d0098b225562499d7c8a74b8b6c3e8893ad9)) -* **ui:** adjust user list buttons on mobile ([#1452](https://github.com/sct/overseerr/issues/1452)) ([5d1b741](https://github.com/sct/overseerr/commit/5d1b741f55665c528e299a09464dff6d66f72666)) -* **ui:** align icons in user dropdown ([eb5d152](https://github.com/sct/overseerr/commit/eb5d1528869959cdf642e6fefc1a8f4dcf51b84e)) +- **lang:** add missing '4K' from singular case of approve/deny 4K request strings ([#1481](https://github.com/sct/overseerr/issues/1481)) ([a822b01](https://github.com/sct/overseerr/commit/a822b019220e86e362a2570e7024289450b4ed46)) +- **ui:** change 'Disable Auto-Search' checkbox to 'Enable Automatic Search' ([#1476](https://github.com/sct/overseerr/issues/1476)) ([1a311d2](https://github.com/sct/overseerr/commit/1a311d211d78731c9089e66ed5387c1b5afe33c0)) +- better error message when creating a user with an existing email ([f13f1c9](https://github.com/sct/overseerr/commit/f13f1c94515b5bd51382fa18ad96a2ccfd06e50d)), closes [#1441](https://github.com/sct/overseerr/issues/1441) +- set editRequest attribute as necessary, allow users to edit their own pending requests, and show 'View Request' button on series pages ([#1446](https://github.com/sct/overseerr/issues/1446)) ([89455ad](https://github.com/sct/overseerr/commit/89455ad9b783d04d993a0009c351b1096f2b222e)) +- **api:** add check for 4K request perms to request creation endpoint ([#1450](https://github.com/sct/overseerr/issues/1450)) ([4449241](https://github.com/sct/overseerr/commit/4449241a8f63fdaeaa4995aa7ec34127c322b9dd)) +- **notif:** include year in notifications ([#1439](https://github.com/sct/overseerr/issues/1439)) ([4e98f56](https://github.com/sct/overseerr/commit/4e98f567534a650e26b0244990b7ca549cecbe89)) +- **plex:** add support for plex.direct URLs ([#1437](https://github.com/sct/overseerr/issues/1437)) ([db07770](https://github.com/sct/overseerr/commit/db077700e42ab1d2c870213fd55bbdee74002775)) +- **radarr:** search in addition to monitoring existing movies ([#1449](https://github.com/sct/overseerr/issues/1449)) ([3ae7d00](https://github.com/sct/overseerr/commit/3ae7d0098b225562499d7c8a74b8b6c3e8893ad9)) +- **ui:** adjust user list buttons on mobile ([#1452](https://github.com/sct/overseerr/issues/1452)) ([5d1b741](https://github.com/sct/overseerr/commit/5d1b741f55665c528e299a09464dff6d66f72666)) +- **ui:** align icons in user dropdown ([eb5d152](https://github.com/sct/overseerr/commit/eb5d1528869959cdf642e6fefc1a8f4dcf51b84e)) ## [1.23.1](https://github.com/sct/overseerr/compare/v1.23.0...v1.23.1) (2021-04-16) - ### Bug Fixes -* **api:** correctly check if update is available for release versions ([190cbd6](https://github.com/sct/overseerr/commit/190cbd6559c51a02ec09b267891f3033add6afc8)) +- **api:** correctly check if update is available for release versions ([190cbd6](https://github.com/sct/overseerr/commit/190cbd6559c51a02ec09b267891f3033add6afc8)) # [1.23.0](https://github.com/sct/overseerr/compare/v1.22.0...v1.23.0) (2021-04-16) - ### Bug Fixes -* **api:** allow server owner to delete other admin accounts ([2ac6fe7](https://github.com/sct/overseerr/commit/2ac6fe7f6d666d64228d11cde24865acc54c7ce7)) -* **backend:** do not log error when user has no server access ([#1419](https://github.com/sct/overseerr/issues/1419)) ([fc14037](https://github.com/sct/overseerr/commit/fc14037ec1c0b7450d892fa9be8176f5b9ff9d73)) -* **frontend:** add crossorigin attribute to webmanifest link ([#1376](https://github.com/sct/overseerr/issues/1376)) ([82ca2f5](https://github.com/sct/overseerr/commit/82ca2f59349407e3b1b5cd4f321e196f37044df0)) -* **frontend:** autofill with Plex server address ([#1381](https://github.com/sct/overseerr/issues/1381)) ([d9e314b](https://github.com/sct/overseerr/commit/d9e314bad295463d26d8ffe92728f3b5eee4ad05)) -* **frontend:** handle media items/requests no longer having a valid tmdb id ([b5ac2f5](https://github.com/sct/overseerr/commit/b5ac2f5a2c5dda808eca177359f125d6e03d1b0f)), closes [#517](https://github.com/sct/overseerr/issues/517) -* **lang:** remove unused strings & correct manageModalNoRequests strings ([#1413](https://github.com/sct/overseerr/issues/1413)) ([190a5c0](https://github.com/sct/overseerr/commit/190a5c0723d4aeafc4ad6103d52c2042a4eaed0e)) -* **plex:** do not use SSL for local servers ([#1418](https://github.com/sct/overseerr/issues/1418)) ([9233fc0](https://github.com/sct/overseerr/commit/9233fc078579df8a193344ba45bafb0d5c2cb9af)) -* **plex:** use server 'address' returned by Plex API ([#1379](https://github.com/sct/overseerr/issues/1379)) ([33542c9](https://github.com/sct/overseerr/commit/33542c9b2dc53b1e036a7d9571cf467c3d3dc8af)) -* **quotas:** Time value of a quota was being ignored ([d3c6bc1](https://github.com/sct/overseerr/commit/d3c6bc1619c39b1e6225d405efaad5df99a27406)) -* **ui:** allow canceling from request list & hide edit button for own requests ([#1401](https://github.com/sct/overseerr/issues/1401)) ([bed850d](https://github.com/sct/overseerr/commit/bed850dce9ad0d0b52c3c628225aea938164c38b)) -* **ui:** close sidebar on mobile when clicking version status ([ad67381](https://github.com/sct/overseerr/commit/ad673813976669797202c2cefc50274aca84989d)) -* **ui:** correctly set autocomplete attribute for password fields ([#1430](https://github.com/sct/overseerr/issues/1430)) ([4b5e355](https://github.com/sct/overseerr/commit/4b5e355df9e291a5cb550483c7dad6c43f03d3a7)) -* **ui:** dim password field when password generation option is selected ([#1427](https://github.com/sct/overseerr/issues/1427)) ([e8bbd44](https://github.com/sct/overseerr/commit/e8bbd4497a5eab6357fa7b37c9906285b3d1f64f)) -* **ui:** hide alert when email notifs are already configured ([#1335](https://github.com/sct/overseerr/issues/1335)) ([5117987](https://github.com/sct/overseerr/commit/5117987feaed21ccc19e64b04a15f2b77c22b880)) -* fall back to English genre names ([#1352](https://github.com/sct/overseerr/issues/1352)) ([e43106a](https://github.com/sct/overseerr/commit/e43106a434548840acecaf1276a5cebdc30e1345)) -* fix outofdate string & display version status badge in Settings > About ([#1417](https://github.com/sct/overseerr/issues/1417)) ([4eb9209](https://github.com/sct/overseerr/commit/4eb92098ba1f141bf74875ce76816a615763de5f)) -* various fixes for new tags feature ([#1369](https://github.com/sct/overseerr/issues/1369)) ([b4450a3](https://github.com/sct/overseerr/commit/b4450a308c56f767fbaa769d574a1b3f8e221d59)) -* **ui:** link request card status badge to Plex media URL ([#1361](https://github.com/sct/overseerr/issues/1361)) ([7a5c4a3](https://github.com/sct/overseerr/commit/7a5c4a30b5735fe6fbe821a8fcfdb4bcbeca68b3)) - +- **api:** allow server owner to delete other admin accounts ([2ac6fe7](https://github.com/sct/overseerr/commit/2ac6fe7f6d666d64228d11cde24865acc54c7ce7)) +- **backend:** do not log error when user has no server access ([#1419](https://github.com/sct/overseerr/issues/1419)) ([fc14037](https://github.com/sct/overseerr/commit/fc14037ec1c0b7450d892fa9be8176f5b9ff9d73)) +- **frontend:** add crossorigin attribute to webmanifest link ([#1376](https://github.com/sct/overseerr/issues/1376)) ([82ca2f5](https://github.com/sct/overseerr/commit/82ca2f59349407e3b1b5cd4f321e196f37044df0)) +- **frontend:** autofill with Plex server address ([#1381](https://github.com/sct/overseerr/issues/1381)) ([d9e314b](https://github.com/sct/overseerr/commit/d9e314bad295463d26d8ffe92728f3b5eee4ad05)) +- **frontend:** handle media items/requests no longer having a valid tmdb id ([b5ac2f5](https://github.com/sct/overseerr/commit/b5ac2f5a2c5dda808eca177359f125d6e03d1b0f)), closes [#517](https://github.com/sct/overseerr/issues/517) +- **lang:** remove unused strings & correct manageModalNoRequests strings ([#1413](https://github.com/sct/overseerr/issues/1413)) ([190a5c0](https://github.com/sct/overseerr/commit/190a5c0723d4aeafc4ad6103d52c2042a4eaed0e)) +- **plex:** do not use SSL for local servers ([#1418](https://github.com/sct/overseerr/issues/1418)) ([9233fc0](https://github.com/sct/overseerr/commit/9233fc078579df8a193344ba45bafb0d5c2cb9af)) +- **plex:** use server 'address' returned by Plex API ([#1379](https://github.com/sct/overseerr/issues/1379)) ([33542c9](https://github.com/sct/overseerr/commit/33542c9b2dc53b1e036a7d9571cf467c3d3dc8af)) +- **quotas:** Time value of a quota was being ignored ([d3c6bc1](https://github.com/sct/overseerr/commit/d3c6bc1619c39b1e6225d405efaad5df99a27406)) +- **ui:** allow canceling from request list & hide edit button for own requests ([#1401](https://github.com/sct/overseerr/issues/1401)) ([bed850d](https://github.com/sct/overseerr/commit/bed850dce9ad0d0b52c3c628225aea938164c38b)) +- **ui:** close sidebar on mobile when clicking version status ([ad67381](https://github.com/sct/overseerr/commit/ad673813976669797202c2cefc50274aca84989d)) +- **ui:** correctly set autocomplete attribute for password fields ([#1430](https://github.com/sct/overseerr/issues/1430)) ([4b5e355](https://github.com/sct/overseerr/commit/4b5e355df9e291a5cb550483c7dad6c43f03d3a7)) +- **ui:** dim password field when password generation option is selected ([#1427](https://github.com/sct/overseerr/issues/1427)) ([e8bbd44](https://github.com/sct/overseerr/commit/e8bbd4497a5eab6357fa7b37c9906285b3d1f64f)) +- **ui:** hide alert when email notifs are already configured ([#1335](https://github.com/sct/overseerr/issues/1335)) ([5117987](https://github.com/sct/overseerr/commit/5117987feaed21ccc19e64b04a15f2b77c22b880)) +- fall back to English genre names ([#1352](https://github.com/sct/overseerr/issues/1352)) ([e43106a](https://github.com/sct/overseerr/commit/e43106a434548840acecaf1276a5cebdc30e1345)) +- fix outofdate string & display version status badge in Settings > About ([#1417](https://github.com/sct/overseerr/issues/1417)) ([4eb9209](https://github.com/sct/overseerr/commit/4eb92098ba1f141bf74875ce76816a615763de5f)) +- various fixes for new tags feature ([#1369](https://github.com/sct/overseerr/issues/1369)) ([b4450a3](https://github.com/sct/overseerr/commit/b4450a308c56f767fbaa769d574a1b3f8e221d59)) +- **ui:** link request card status badge to Plex media URL ([#1361](https://github.com/sct/overseerr/issues/1361)) ([7a5c4a3](https://github.com/sct/overseerr/commit/7a5c4a30b5735fe6fbe821a8fcfdb4bcbeca68b3)) ### Features -* **lang:** Translations update from Weblate ([#1429](https://github.com/sct/overseerr/issues/1429)) ([a54241c](https://github.com/sct/overseerr/commit/a54241c775705fadc7c044f5312307f28f9a854b)) -* change alpha warning to beta warning ([03fd21b](https://github.com/sct/overseerr/commit/03fd21bebc3ffa34ce983b524d09e74b8ab2d057)) -* **lang:** translated using Weblate (Catalan) ([#1351](https://github.com/sct/overseerr/issues/1351)) ([35c13a8](https://github.com/sct/overseerr/commit/35c13a87467b4deabab3cb2cd1cab1b24ab51875)) -* **lang:** translations update from Weblate ([#1360](https://github.com/sct/overseerr/issues/1360)) ([8ee7693](https://github.com/sct/overseerr/commit/8ee7693a1f00a2f735b2555c7f8180c8a2c6144f)) -* **lang:** translations update from Weblate ([#1416](https://github.com/sct/overseerr/issues/1416)) ([dceca4d](https://github.com/sct/overseerr/commit/dceca4dd97f78f2e3aef678edcd5755c781f5249)) -* add overseerr version and update availability status to sidebar ([ecf1312](https://github.com/sct/overseerr/commit/ecf13123d21d765d67bfa7f9b6509b0f2af62cee)) -* **lang:** translations update from Weblate ([#1388](https://github.com/sct/overseerr/issues/1388)) ([9b199b2](https://github.com/sct/overseerr/commit/9b199b27d806e290cf0551e2d2ede6add61770aa)) -* **lang:** translations update from Weblate ([#1396](https://github.com/sct/overseerr/issues/1396)) ([3daf57e](https://github.com/sct/overseerr/commit/3daf57e9a12e4973dbc56656379ab2dbcb3c2619)) -* **notif:** allow users to enable/disable specific agents ([#1172](https://github.com/sct/overseerr/issues/1172)) ([46c4ee1](https://github.com/sct/overseerr/commit/46c4ee1625cf3e74bd885ecfc254b1e46cf44f29)) -* **webhook:** include requestedBy user in payload ([#1385](https://github.com/sct/overseerr/issues/1385)) ([e605687](https://github.com/sct/overseerr/commit/e60568758097d07f9d4b201ffdf34f0c32ba9cf3)) -* radarr/sonarr tag support ([#1366](https://github.com/sct/overseerr/issues/1366)) ([a306ebc](https://github.com/sct/overseerr/commit/a306ebc2d18317d8dbe4ccd3f24c22f55ffcd6a6)) +- **lang:** Translations update from Weblate ([#1429](https://github.com/sct/overseerr/issues/1429)) ([a54241c](https://github.com/sct/overseerr/commit/a54241c775705fadc7c044f5312307f28f9a854b)) +- change alpha warning to beta warning ([03fd21b](https://github.com/sct/overseerr/commit/03fd21bebc3ffa34ce983b524d09e74b8ab2d057)) +- **lang:** translated using Weblate (Catalan) ([#1351](https://github.com/sct/overseerr/issues/1351)) ([35c13a8](https://github.com/sct/overseerr/commit/35c13a87467b4deabab3cb2cd1cab1b24ab51875)) +- **lang:** translations update from Weblate ([#1360](https://github.com/sct/overseerr/issues/1360)) ([8ee7693](https://github.com/sct/overseerr/commit/8ee7693a1f00a2f735b2555c7f8180c8a2c6144f)) +- **lang:** translations update from Weblate ([#1416](https://github.com/sct/overseerr/issues/1416)) ([dceca4d](https://github.com/sct/overseerr/commit/dceca4dd97f78f2e3aef678edcd5755c781f5249)) +- add overseerr version and update availability status to sidebar ([ecf1312](https://github.com/sct/overseerr/commit/ecf13123d21d765d67bfa7f9b6509b0f2af62cee)) +- **lang:** translations update from Weblate ([#1388](https://github.com/sct/overseerr/issues/1388)) ([9b199b2](https://github.com/sct/overseerr/commit/9b199b27d806e290cf0551e2d2ede6add61770aa)) +- **lang:** translations update from Weblate ([#1396](https://github.com/sct/overseerr/issues/1396)) ([3daf57e](https://github.com/sct/overseerr/commit/3daf57e9a12e4973dbc56656379ab2dbcb3c2619)) +- **notif:** allow users to enable/disable specific agents ([#1172](https://github.com/sct/overseerr/issues/1172)) ([46c4ee1](https://github.com/sct/overseerr/commit/46c4ee1625cf3e74bd885ecfc254b1e46cf44f29)) +- **webhook:** include requestedBy user in payload ([#1385](https://github.com/sct/overseerr/issues/1385)) ([e605687](https://github.com/sct/overseerr/commit/e60568758097d07f9d4b201ffdf34f0c32ba9cf3)) +- radarr/sonarr tag support ([#1366](https://github.com/sct/overseerr/issues/1366)) ([a306ebc](https://github.com/sct/overseerr/commit/a306ebc2d18317d8dbe4ccd3f24c22f55ffcd6a6)) # [1.22.0](https://github.com/sct/overseerr/compare/v1.21.1...v1.22.0) (2021-04-01) - ### Bug Fixes -* **android:** adaptive icons for Android devices ([#1274](https://github.com/sct/overseerr/issues/1274)) ([a65e3d5](https://github.com/sct/overseerr/commit/a65e3d5bb6924cbde30b26ff8acf535e5274efee)) -* **backend:** fix getShowByTvdbId() error message ([#1314](https://github.com/sct/overseerr/issues/1314)) [skip ci] ([fe8d346](https://github.com/sct/overseerr/commit/fe8d34607b07095dce51b29ef7aaae0485573f14)) -* **db:** enable WAL journal mode ([aa205ff](https://github.com/sct/overseerr/commit/aa205ffa975d02ef0be30626e7c946a42679a847)) -* **frontend:** 'Recent Requests' slider should link to request list w/ same filter ([#1235](https://github.com/sct/overseerr/issues/1235)) ([49782c0](https://github.com/sct/overseerr/commit/49782c0b730cce9f0bad14e9c83842b5b0bfe11e)) -* **frontend:** call mutate after changing public settings ([#1302](https://github.com/sct/overseerr/issues/1302)) ([c8f67cf](https://github.com/sct/overseerr/commit/c8f67cf866ada791e4129a0bbae16b9eac41f32e)) -* **frontend:** include language parameter in TMDb links ([#1344](https://github.com/sct/overseerr/issues/1344)) ([1d88be9](https://github.com/sct/overseerr/commit/1d88be9341a8ff9e1f39b02556b489cdbd06392b)) -* **frontend:** redirect from /setup if already initialized ([#1238](https://github.com/sct/overseerr/issues/1238)) ([8016503](https://github.com/sct/overseerr/commit/80165038fd214897e3520a420f971341e7b94865)) -* **frontend:** use correct path to user profile in request modal quota dropdown ([#1307](https://github.com/sct/overseerr/issues/1307)) ([f990585](https://github.com/sct/overseerr/commit/f9905859148088afec53549b81611b07bf19d3b9)) -* **frontend:** use HTTPS to fetch TMDb assets for network/studio sliders ([#1343](https://github.com/sct/overseerr/issues/1343)) ([c886ea6](https://github.com/sct/overseerr/commit/c886ea6c0578cb7532d6c09266a76bfad8598b9d)) -* **frontend:** use next/image to serve login page images ([cbf4519](https://github.com/sct/overseerr/commit/cbf45196b023f60c8e4cf7602c0295f886fe610c)), closes [#1207](https://github.com/sct/overseerr/issues/1207) -* **lang:** allow proper localization of comma-delimited lists ([#1264](https://github.com/sct/overseerr/issues/1264)) ([173408a](https://github.com/sct/overseerr/commit/173408a1f269f09c724843ba087ef3f85b2832ad)) -* **lang:** change 'Extra Data' string to 'Additional Data' ([#1226](https://github.com/sct/overseerr/issues/1226)) ([665e164](https://github.com/sct/overseerr/commit/665e16475f3fa2ea6118340d9ea2d30b98abb238)) -* **lang:** correct mismatched language strings ([#1246](https://github.com/sct/overseerr/issues/1246)) ([8ebc829](https://github.com/sct/overseerr/commit/8ebc8292504cdc57a148ab69bcb4e1514ef018c6)) -* **lang:** correct strings for library sync button & user import toast ([#1252](https://github.com/sct/overseerr/issues/1252)) ([cb5ca7a](https://github.com/sct/overseerr/commit/cb5ca7acf38dcc2e27ec31d88434a11757cdb469)) -* **lang:** edit setting label strings for verb tense consistency ([#1214](https://github.com/sct/overseerr/issues/1214)) ([6d7671d](https://github.com/sct/overseerr/commit/6d7671dd80fea632e5cef29fc0b4968bffe231b0)) -* **lang:** fix overwritten/shared string ([#1212](https://github.com/sct/overseerr/issues/1212)) ([dfd4ff9](https://github.com/sct/overseerr/commit/dfd4ff9229822b0ce79ba322376194cbb6fd233d)) -* **lang:** remove 'requires and' ([#1215](https://github.com/sct/overseerr/issues/1215)) ([cb852fd](https://github.com/sct/overseerr/commit/cb852fded18f53806c23ec6f215385072b2a867b)) -* **lang:** remove unused strings ([#1330](https://github.com/sct/overseerr/issues/1330)) ([13e1595](https://github.com/sct/overseerr/commit/13e1595c6ebff32ca905d9bd3dd781e241545e83)) -* **lang:** UI string edits, round 2 ([#1202](https://github.com/sct/overseerr/issues/1202)) ([ea1863a](https://github.com/sct/overseerr/commit/ea1863ac3a5d3051e07815d07df0d3f2abd9166f)) -* **log:** fix typo in base scanner logging ([#1329](https://github.com/sct/overseerr/issues/1329)) [skip ci] ([b0b04ca](https://github.com/sct/overseerr/commit/b0b04ca1c7218ad5b67d9ec8b3fac5af78a4c132)) -* **logs:** add i18n strings for new log page changes ([8c51c28](https://github.com/sct/overseerr/commit/8c51c28f546b9c2d38ff7f20d59bb08a599e8146)) -* **notifications:** correctly send notifications for users that do not have any user settings yet ([d3a25b9](https://github.com/sct/overseerr/commit/d3a25b935aae35dd97ef0f168ac7e2898126a9a5)), closes [#1324](https://github.com/sct/overseerr/issues/1324) -* **overseerr-api.yml:** fixed pushbullet & webhook API definition refs and descriptions ([#1288](https://github.com/sct/overseerr/issues/1288)) [skip ci] ([3b003b7](https://github.com/sct/overseerr/commit/3b003b770120f7d150c64ff098b626015c030794)) -* **plex:** always send Overseerr for the device name to the plex.tv api ([f7146e4](https://github.com/sct/overseerr/commit/f7146e41899a59f75b963e1cc9dac9eddf24aebe)), closes [#1244](https://github.com/sct/overseerr/issues/1244) -* **ui:** add validation to hostname/IP fields ([#1206](https://github.com/sct/overseerr/issues/1206)) ([f49a024](https://github.com/sct/overseerr/commit/f49a02449c4928aef56cecbf908cf585ea0d4fca)) -* **ui:** better regex matching when parsing logs ([#1225](https://github.com/sct/overseerr/issues/1225)) ([2d737f2](https://github.com/sct/overseerr/commit/2d737f276095a8ca9abea360ef29134e9f639a39)) -* **ui:** button w/ dropdown z-indices ([#1230](https://github.com/sct/overseerr/issues/1230)) ([015671f](https://github.com/sct/overseerr/commit/015671f5be7a9f0f5c38db5a11a4b3c788dfaade)) -* **ui:** center role under title cards on person detail pages ([#1205](https://github.com/sct/overseerr/issues/1205)) ([4a61518](https://github.com/sct/overseerr/commit/4a6151873a3a3c5e45f9817131774a2c52957138)) -* **ui:** correctly enable the request button when partial requests are disabled with no quota ([16a611b](https://github.com/sct/overseerr/commit/16a611b9dfc3c66483640f4f5364646f41d37159)) -* **ui:** correctly paginate request list ([67fbb40](https://github.com/sct/overseerr/commit/67fbb401ac6ba05e58b8dfefd5954b28316254f2)) -* **ui:** correctly show quota display on tv request modal when only series quota is set ([3f1f85a](https://github.com/sct/overseerr/commit/3f1f85a80edfd2a4e9627162ff29ca6bcf2d8583)) -* **ui:** display asterisk indicator on required field labels ([#1236](https://github.com/sct/overseerr/issues/1236)) ([380d361](https://github.com/sct/overseerr/commit/380d36119f19a20ad67f79b3fb5db4036a093cac)) -* **ui:** do not check isValid on Sonarr/Radarr modals for the test button ([0974a4c](https://github.com/sct/overseerr/commit/0974a4c971358b7a64668f9a63fc356234a656c9)) -* **ui:** do not require numeric value in FormattedRelativeTime ([#1234](https://github.com/sct/overseerr/issues/1234)) ([3642b1e](https://github.com/sct/overseerr/commit/3642b1e84a20fef72428b3e240c86d35be8be8a2)) -* **ui:** filter out server options that do not match request type (non-4K or 4K) ([#1183](https://github.com/sct/overseerr/issues/1183)) ([28a6a70](https://github.com/sct/overseerr/commit/28a6a70e1ecc125f4cf4900e599ad0d4d7b55e3b)) -* **ui:** fix label formatting in general user settings ([#1275](https://github.com/sct/overseerr/issues/1275)) ([8546b0e](https://github.com/sct/overseerr/commit/8546b0ef53d232256b62cf08466e692a6971c16b)) -* **ui:** fix regex matching when parsing label from logs ([#1231](https://github.com/sct/overseerr/issues/1231)) ([4a00617](https://github.com/sct/overseerr/commit/4a00617fe47064ea50f95a02f29832a419ab13a3)) -* **ui:** gracefully handle lengthy titles & long words in overviews ([#1338](https://github.com/sct/overseerr/issues/1338)) ([d8bcb99](https://github.com/sct/overseerr/commit/d8bcb99b2fd3b24a5119ba5ff213a640425ff553)) -* **ui:** hide 'show details' button if there are no additional details ([#1254](https://github.com/sct/overseerr/issues/1254)) ([6210f12](https://github.com/sct/overseerr/commit/6210f12e8e9f593d629d22278d78310482ca0cfa)) -* **ui:** increase page size dropdown width when necessary ([#1216](https://github.com/sct/overseerr/issues/1216)) ([75c72b9](https://github.com/sct/overseerr/commit/75c72b987eb52b907ffd8af33f15ecc58213fc12)) -* **ui:** restore saved states of quota override checkboxes ([#1282](https://github.com/sct/overseerr/issues/1282)) ([2059fc1](https://github.com/sct/overseerr/commit/2059fc1cd4d48c7d80e761b7d41b7ec122d82769)) -* **ui:** sort regions & languages by their localized names rather than their TMDb English names ([#1157](https://github.com/sct/overseerr/issues/1157)) ([d76bf32](https://github.com/sct/overseerr/commit/d76bf32c9dcc83ebd0bae979726b1456a9028d8b)) -* **ui:** tweak request list design ([#1201](https://github.com/sct/overseerr/issues/1201)) ([d226fc7](https://github.com/sct/overseerr/commit/d226fc79b8d5f1263d4b80a7a1772074020ec94f)) -* **ui:** use appropriate cursor type for disabled UI elements ([#1184](https://github.com/sct/overseerr/issues/1184)) ([b767a58](https://github.com/sct/overseerr/commit/b767a58b011cc317a889cb8c2889b3210bec5fae)) -* **ui:** use appropriate cursor type for readonly input fields ([#1208](https://github.com/sct/overseerr/issues/1208)) ([9ec2c46](https://github.com/sct/overseerr/commit/9ec2c468cbbcbd41b94bbf9f3cfeb43eed09f36e)) -* **ui:** use correct colspan for 'No results.' message in Settings > Logs ([#1325](https://github.com/sct/overseerr/issues/1325)) ([5c135c9](https://github.com/sct/overseerr/commit/5c135c9974ebfcbdb434dafd459d1035624df6ed)) -* fetch localized person details from TMDb ([#1243](https://github.com/sct/overseerr/issues/1243)) ([1d7a938](https://github.com/sct/overseerr/commit/1d7a938ef8b0b8c20fda5024121de2a217ef4127)) - +- **android:** adaptive icons for Android devices ([#1274](https://github.com/sct/overseerr/issues/1274)) ([a65e3d5](https://github.com/sct/overseerr/commit/a65e3d5bb6924cbde30b26ff8acf535e5274efee)) +- **backend:** fix getShowByTvdbId() error message ([#1314](https://github.com/sct/overseerr/issues/1314)) [skip ci] ([fe8d346](https://github.com/sct/overseerr/commit/fe8d34607b07095dce51b29ef7aaae0485573f14)) +- **db:** enable WAL journal mode ([aa205ff](https://github.com/sct/overseerr/commit/aa205ffa975d02ef0be30626e7c946a42679a847)) +- **frontend:** 'Recent Requests' slider should link to request list w/ same filter ([#1235](https://github.com/sct/overseerr/issues/1235)) ([49782c0](https://github.com/sct/overseerr/commit/49782c0b730cce9f0bad14e9c83842b5b0bfe11e)) +- **frontend:** call mutate after changing public settings ([#1302](https://github.com/sct/overseerr/issues/1302)) ([c8f67cf](https://github.com/sct/overseerr/commit/c8f67cf866ada791e4129a0bbae16b9eac41f32e)) +- **frontend:** include language parameter in TMDb links ([#1344](https://github.com/sct/overseerr/issues/1344)) ([1d88be9](https://github.com/sct/overseerr/commit/1d88be9341a8ff9e1f39b02556b489cdbd06392b)) +- **frontend:** redirect from /setup if already initialized ([#1238](https://github.com/sct/overseerr/issues/1238)) ([8016503](https://github.com/sct/overseerr/commit/80165038fd214897e3520a420f971341e7b94865)) +- **frontend:** use correct path to user profile in request modal quota dropdown ([#1307](https://github.com/sct/overseerr/issues/1307)) ([f990585](https://github.com/sct/overseerr/commit/f9905859148088afec53549b81611b07bf19d3b9)) +- **frontend:** use HTTPS to fetch TMDb assets for network/studio sliders ([#1343](https://github.com/sct/overseerr/issues/1343)) ([c886ea6](https://github.com/sct/overseerr/commit/c886ea6c0578cb7532d6c09266a76bfad8598b9d)) +- **frontend:** use next/image to serve login page images ([cbf4519](https://github.com/sct/overseerr/commit/cbf45196b023f60c8e4cf7602c0295f886fe610c)), closes [#1207](https://github.com/sct/overseerr/issues/1207) +- **lang:** allow proper localization of comma-delimited lists ([#1264](https://github.com/sct/overseerr/issues/1264)) ([173408a](https://github.com/sct/overseerr/commit/173408a1f269f09c724843ba087ef3f85b2832ad)) +- **lang:** change 'Extra Data' string to 'Additional Data' ([#1226](https://github.com/sct/overseerr/issues/1226)) ([665e164](https://github.com/sct/overseerr/commit/665e16475f3fa2ea6118340d9ea2d30b98abb238)) +- **lang:** correct mismatched language strings ([#1246](https://github.com/sct/overseerr/issues/1246)) ([8ebc829](https://github.com/sct/overseerr/commit/8ebc8292504cdc57a148ab69bcb4e1514ef018c6)) +- **lang:** correct strings for library sync button & user import toast ([#1252](https://github.com/sct/overseerr/issues/1252)) ([cb5ca7a](https://github.com/sct/overseerr/commit/cb5ca7acf38dcc2e27ec31d88434a11757cdb469)) +- **lang:** edit setting label strings for verb tense consistency ([#1214](https://github.com/sct/overseerr/issues/1214)) ([6d7671d](https://github.com/sct/overseerr/commit/6d7671dd80fea632e5cef29fc0b4968bffe231b0)) +- **lang:** fix overwritten/shared string ([#1212](https://github.com/sct/overseerr/issues/1212)) ([dfd4ff9](https://github.com/sct/overseerr/commit/dfd4ff9229822b0ce79ba322376194cbb6fd233d)) +- **lang:** remove 'requires and' ([#1215](https://github.com/sct/overseerr/issues/1215)) ([cb852fd](https://github.com/sct/overseerr/commit/cb852fded18f53806c23ec6f215385072b2a867b)) +- **lang:** remove unused strings ([#1330](https://github.com/sct/overseerr/issues/1330)) ([13e1595](https://github.com/sct/overseerr/commit/13e1595c6ebff32ca905d9bd3dd781e241545e83)) +- **lang:** UI string edits, round 2 ([#1202](https://github.com/sct/overseerr/issues/1202)) ([ea1863a](https://github.com/sct/overseerr/commit/ea1863ac3a5d3051e07815d07df0d3f2abd9166f)) +- **log:** fix typo in base scanner logging ([#1329](https://github.com/sct/overseerr/issues/1329)) [skip ci] ([b0b04ca](https://github.com/sct/overseerr/commit/b0b04ca1c7218ad5b67d9ec8b3fac5af78a4c132)) +- **logs:** add i18n strings for new log page changes ([8c51c28](https://github.com/sct/overseerr/commit/8c51c28f546b9c2d38ff7f20d59bb08a599e8146)) +- **notifications:** correctly send notifications for users that do not have any user settings yet ([d3a25b9](https://github.com/sct/overseerr/commit/d3a25b935aae35dd97ef0f168ac7e2898126a9a5)), closes [#1324](https://github.com/sct/overseerr/issues/1324) +- **overseerr-api.yml:** fixed pushbullet & webhook API definition refs and descriptions ([#1288](https://github.com/sct/overseerr/issues/1288)) [skip ci] ([3b003b7](https://github.com/sct/overseerr/commit/3b003b770120f7d150c64ff098b626015c030794)) +- **plex:** always send Overseerr for the device name to the plex.tv api ([f7146e4](https://github.com/sct/overseerr/commit/f7146e41899a59f75b963e1cc9dac9eddf24aebe)), closes [#1244](https://github.com/sct/overseerr/issues/1244) +- **ui:** add validation to hostname/IP fields ([#1206](https://github.com/sct/overseerr/issues/1206)) ([f49a024](https://github.com/sct/overseerr/commit/f49a02449c4928aef56cecbf908cf585ea0d4fca)) +- **ui:** better regex matching when parsing logs ([#1225](https://github.com/sct/overseerr/issues/1225)) ([2d737f2](https://github.com/sct/overseerr/commit/2d737f276095a8ca9abea360ef29134e9f639a39)) +- **ui:** button w/ dropdown z-indices ([#1230](https://github.com/sct/overseerr/issues/1230)) ([015671f](https://github.com/sct/overseerr/commit/015671f5be7a9f0f5c38db5a11a4b3c788dfaade)) +- **ui:** center role under title cards on person detail pages ([#1205](https://github.com/sct/overseerr/issues/1205)) ([4a61518](https://github.com/sct/overseerr/commit/4a6151873a3a3c5e45f9817131774a2c52957138)) +- **ui:** correctly enable the request button when partial requests are disabled with no quota ([16a611b](https://github.com/sct/overseerr/commit/16a611b9dfc3c66483640f4f5364646f41d37159)) +- **ui:** correctly paginate request list ([67fbb40](https://github.com/sct/overseerr/commit/67fbb401ac6ba05e58b8dfefd5954b28316254f2)) +- **ui:** correctly show quota display on tv request modal when only series quota is set ([3f1f85a](https://github.com/sct/overseerr/commit/3f1f85a80edfd2a4e9627162ff29ca6bcf2d8583)) +- **ui:** display asterisk indicator on required field labels ([#1236](https://github.com/sct/overseerr/issues/1236)) ([380d361](https://github.com/sct/overseerr/commit/380d36119f19a20ad67f79b3fb5db4036a093cac)) +- **ui:** do not check isValid on Sonarr/Radarr modals for the test button ([0974a4c](https://github.com/sct/overseerr/commit/0974a4c971358b7a64668f9a63fc356234a656c9)) +- **ui:** do not require numeric value in FormattedRelativeTime ([#1234](https://github.com/sct/overseerr/issues/1234)) ([3642b1e](https://github.com/sct/overseerr/commit/3642b1e84a20fef72428b3e240c86d35be8be8a2)) +- **ui:** filter out server options that do not match request type (non-4K or 4K) ([#1183](https://github.com/sct/overseerr/issues/1183)) ([28a6a70](https://github.com/sct/overseerr/commit/28a6a70e1ecc125f4cf4900e599ad0d4d7b55e3b)) +- **ui:** fix label formatting in general user settings ([#1275](https://github.com/sct/overseerr/issues/1275)) ([8546b0e](https://github.com/sct/overseerr/commit/8546b0ef53d232256b62cf08466e692a6971c16b)) +- **ui:** fix regex matching when parsing label from logs ([#1231](https://github.com/sct/overseerr/issues/1231)) ([4a00617](https://github.com/sct/overseerr/commit/4a00617fe47064ea50f95a02f29832a419ab13a3)) +- **ui:** gracefully handle lengthy titles & long words in overviews ([#1338](https://github.com/sct/overseerr/issues/1338)) ([d8bcb99](https://github.com/sct/overseerr/commit/d8bcb99b2fd3b24a5119ba5ff213a640425ff553)) +- **ui:** hide 'show details' button if there are no additional details ([#1254](https://github.com/sct/overseerr/issues/1254)) ([6210f12](https://github.com/sct/overseerr/commit/6210f12e8e9f593d629d22278d78310482ca0cfa)) +- **ui:** increase page size dropdown width when necessary ([#1216](https://github.com/sct/overseerr/issues/1216)) ([75c72b9](https://github.com/sct/overseerr/commit/75c72b987eb52b907ffd8af33f15ecc58213fc12)) +- **ui:** restore saved states of quota override checkboxes ([#1282](https://github.com/sct/overseerr/issues/1282)) ([2059fc1](https://github.com/sct/overseerr/commit/2059fc1cd4d48c7d80e761b7d41b7ec122d82769)) +- **ui:** sort regions & languages by their localized names rather than their TMDb English names ([#1157](https://github.com/sct/overseerr/issues/1157)) ([d76bf32](https://github.com/sct/overseerr/commit/d76bf32c9dcc83ebd0bae979726b1456a9028d8b)) +- **ui:** tweak request list design ([#1201](https://github.com/sct/overseerr/issues/1201)) ([d226fc7](https://github.com/sct/overseerr/commit/d226fc79b8d5f1263d4b80a7a1772074020ec94f)) +- **ui:** use appropriate cursor type for disabled UI elements ([#1184](https://github.com/sct/overseerr/issues/1184)) ([b767a58](https://github.com/sct/overseerr/commit/b767a58b011cc317a889cb8c2889b3210bec5fae)) +- **ui:** use appropriate cursor type for readonly input fields ([#1208](https://github.com/sct/overseerr/issues/1208)) ([9ec2c46](https://github.com/sct/overseerr/commit/9ec2c468cbbcbd41b94bbf9f3cfeb43eed09f36e)) +- **ui:** use correct colspan for 'No results.' message in Settings > Logs ([#1325](https://github.com/sct/overseerr/issues/1325)) ([5c135c9](https://github.com/sct/overseerr/commit/5c135c9974ebfcbdb434dafd459d1035624df6ed)) +- fetch localized person details from TMDb ([#1243](https://github.com/sct/overseerr/issues/1243)) ([1d7a938](https://github.com/sct/overseerr/commit/1d7a938ef8b0b8c20fda5024121de2a217ef4127)) ### Features -* **frontend:** add apple splash for pwa ([232def9](https://github.com/sct/overseerr/commit/232def972b9156afcbd83592708dbf8b5866ee24)) -* **frontend:** add apple tv+ to network slider ([3dc27ff](https://github.com/sct/overseerr/commit/3dc27ffd9bb054e6cda58872939dbc352877d184)), closes [#1219](https://github.com/sct/overseerr/issues/1219) -* **frontend:** allow selecting multiple original languages ([a908c07](https://github.com/sct/overseerr/commit/a908c07670532b0ca7f766065bb4653ce2376e6f)) -* **lang:** add Catalan to language picker ([#1309](https://github.com/sct/overseerr/issues/1309)) ([77911c0](https://github.com/sct/overseerr/commit/77911c03e98aa3c2c6c062a01c22b030704309c2)) -* **lang:** translations update from Weblate ([#1178](https://github.com/sct/overseerr/issues/1178)) ([3c89010](https://github.com/sct/overseerr/commit/3c89010629bc16f225f1d3936abe9f4e47a0d7c7)) -* **lang:** translations update from Weblate ([#1224](https://github.com/sct/overseerr/issues/1224)) ([c1975b3](https://github.com/sct/overseerr/commit/c1975b33f1115a95068be000b7f479a401f0f0ae)) -* **lang:** translations update from Weblate ([#1237](https://github.com/sct/overseerr/issues/1237)) ([dabd32a](https://github.com/sct/overseerr/commit/dabd32a18b42980059c7a7a7450514ca827a5d3b)) -* **lang:** translations update from Weblate ([#1256](https://github.com/sct/overseerr/issues/1256)) ([e9b1a9e](https://github.com/sct/overseerr/commit/e9b1a9e80e6b8285fa451a8551c5832a850c1746)) -* **lang:** translations update from Weblate ([#1281](https://github.com/sct/overseerr/issues/1281)) ([bec1d3d](https://github.com/sct/overseerr/commit/bec1d3dde834b9a50e24c5894c362e5982ff3bd5)) -* **lang:** translations update from Weblate ([#1305](https://github.com/sct/overseerr/issues/1305)) ([1b129c0](https://github.com/sct/overseerr/commit/1b129c0b3863ea3c5ad34c66b3ace5d09cd4e391)) -* **lang:** translations update from Weblate ([#1313](https://github.com/sct/overseerr/issues/1313)) ([18ce349](https://github.com/sct/overseerr/commit/18ce349faac6ee560b9c92374039954f2365a8d1)) -* **logs:** add copy to clipboard button to logs page ([e2b8745](https://github.com/sct/overseerr/commit/e2b8745fdc192f3d49872625652184005a760885)) -* **notif:** include requested season numbers in notifications ([#1211](https://github.com/sct/overseerr/issues/1211)) ([4ee78ab](https://github.com/sct/overseerr/commit/4ee78ab2fe0359df6baa58f0986687f05a8392a2)) -* **requests:** add request quotas ([#1277](https://github.com/sct/overseerr/issues/1277)) ([6c75c88](https://github.com/sct/overseerr/commit/6c75c8822842514ffd31864992e8d3ce686fea1b)) -* **settings:** logs viewer ([#997](https://github.com/sct/overseerr/issues/997)) ([54429bb](https://github.com/sct/overseerr/commit/54429bbc1d765d0e50486a42749f9bbd4e5b3386)) -* **ui:** add movie/series genre list pages ([#1194](https://github.com/sct/overseerr/issues/1194)) ([6f1a31d](https://github.com/sct/overseerr/commit/6f1a31de473d1a25bc77e0961a52b07050b64c51)) -* **ui:** add option to only allow complete series requests ([#1164](https://github.com/sct/overseerr/issues/1164)) ([36c00fd](https://github.com/sct/overseerr/commit/36c00fde273799a56ec42ce6177ff44fed0904c3)) -* **ui:** Add user requests page ([#936](https://github.com/sct/overseerr/issues/936)) ([a9461f7](https://github.com/sct/overseerr/commit/a9461f760d8112f2ae16183e796f706d3392f8ec)) -* **ui:** allow any value 1-100 for quota limit/days ([#1337](https://github.com/sct/overseerr/issues/1337)) ([f4bed9a](https://github.com/sct/overseerr/commit/f4bed9a63b6b856ebedca9eb7662cd00038d7f7c)) -* **ui:** display movie/series original title ([#1240](https://github.com/sct/overseerr/issues/1240)) ([7230915](https://github.com/sct/overseerr/commit/723091509414465e98d870b3dc943f41b9ac590d)) -* **ui:** experimental status bar style change for ios pwa app ([958cdf9](https://github.com/sct/overseerr/commit/958cdf98fd1cb7c1bdb33aebb6c061750e9ab331)) -* **ui:** store sort order and page size of userlist in localstorage ([#1262](https://github.com/sct/overseerr/issues/1262)) ([f5f8269](https://github.com/sct/overseerr/commit/f5f8269cd28ee792120060f4f38ef09d571fb8d5)) -* add option to cache images locally ([#1213](https://github.com/sct/overseerr/issues/1213)) ([0ca3d43](https://github.com/sct/overseerr/commit/0ca3d4374942b54b59a19d017ab4ae14ba7019c1)) -* genre sliders (experiment) ([#1182](https://github.com/sct/overseerr/issues/1182)) ([1c4515a](https://github.com/sct/overseerr/commit/1c4515a1ae6097f3948aaa0d0ed210831581fd98)) - +- **frontend:** add apple splash for pwa ([232def9](https://github.com/sct/overseerr/commit/232def972b9156afcbd83592708dbf8b5866ee24)) +- **frontend:** add apple tv+ to network slider ([3dc27ff](https://github.com/sct/overseerr/commit/3dc27ffd9bb054e6cda58872939dbc352877d184)), closes [#1219](https://github.com/sct/overseerr/issues/1219) +- **frontend:** allow selecting multiple original languages ([a908c07](https://github.com/sct/overseerr/commit/a908c07670532b0ca7f766065bb4653ce2376e6f)) +- **lang:** add Catalan to language picker ([#1309](https://github.com/sct/overseerr/issues/1309)) ([77911c0](https://github.com/sct/overseerr/commit/77911c03e98aa3c2c6c062a01c22b030704309c2)) +- **lang:** translations update from Weblate ([#1178](https://github.com/sct/overseerr/issues/1178)) ([3c89010](https://github.com/sct/overseerr/commit/3c89010629bc16f225f1d3936abe9f4e47a0d7c7)) +- **lang:** translations update from Weblate ([#1224](https://github.com/sct/overseerr/issues/1224)) ([c1975b3](https://github.com/sct/overseerr/commit/c1975b33f1115a95068be000b7f479a401f0f0ae)) +- **lang:** translations update from Weblate ([#1237](https://github.com/sct/overseerr/issues/1237)) ([dabd32a](https://github.com/sct/overseerr/commit/dabd32a18b42980059c7a7a7450514ca827a5d3b)) +- **lang:** translations update from Weblate ([#1256](https://github.com/sct/overseerr/issues/1256)) ([e9b1a9e](https://github.com/sct/overseerr/commit/e9b1a9e80e6b8285fa451a8551c5832a850c1746)) +- **lang:** translations update from Weblate ([#1281](https://github.com/sct/overseerr/issues/1281)) ([bec1d3d](https://github.com/sct/overseerr/commit/bec1d3dde834b9a50e24c5894c362e5982ff3bd5)) +- **lang:** translations update from Weblate ([#1305](https://github.com/sct/overseerr/issues/1305)) ([1b129c0](https://github.com/sct/overseerr/commit/1b129c0b3863ea3c5ad34c66b3ace5d09cd4e391)) +- **lang:** translations update from Weblate ([#1313](https://github.com/sct/overseerr/issues/1313)) ([18ce349](https://github.com/sct/overseerr/commit/18ce349faac6ee560b9c92374039954f2365a8d1)) +- **logs:** add copy to clipboard button to logs page ([e2b8745](https://github.com/sct/overseerr/commit/e2b8745fdc192f3d49872625652184005a760885)) +- **notif:** include requested season numbers in notifications ([#1211](https://github.com/sct/overseerr/issues/1211)) ([4ee78ab](https://github.com/sct/overseerr/commit/4ee78ab2fe0359df6baa58f0986687f05a8392a2)) +- **requests:** add request quotas ([#1277](https://github.com/sct/overseerr/issues/1277)) ([6c75c88](https://github.com/sct/overseerr/commit/6c75c8822842514ffd31864992e8d3ce686fea1b)) +- **settings:** logs viewer ([#997](https://github.com/sct/overseerr/issues/997)) ([54429bb](https://github.com/sct/overseerr/commit/54429bbc1d765d0e50486a42749f9bbd4e5b3386)) +- **ui:** add movie/series genre list pages ([#1194](https://github.com/sct/overseerr/issues/1194)) ([6f1a31d](https://github.com/sct/overseerr/commit/6f1a31de473d1a25bc77e0961a52b07050b64c51)) +- **ui:** add option to only allow complete series requests ([#1164](https://github.com/sct/overseerr/issues/1164)) ([36c00fd](https://github.com/sct/overseerr/commit/36c00fde273799a56ec42ce6177ff44fed0904c3)) +- **ui:** Add user requests page ([#936](https://github.com/sct/overseerr/issues/936)) ([a9461f7](https://github.com/sct/overseerr/commit/a9461f760d8112f2ae16183e796f706d3392f8ec)) +- **ui:** allow any value 1-100 for quota limit/days ([#1337](https://github.com/sct/overseerr/issues/1337)) ([f4bed9a](https://github.com/sct/overseerr/commit/f4bed9a63b6b856ebedca9eb7662cd00038d7f7c)) +- **ui:** display movie/series original title ([#1240](https://github.com/sct/overseerr/issues/1240)) ([7230915](https://github.com/sct/overseerr/commit/723091509414465e98d870b3dc943f41b9ac590d)) +- **ui:** experimental status bar style change for ios pwa app ([958cdf9](https://github.com/sct/overseerr/commit/958cdf98fd1cb7c1bdb33aebb6c061750e9ab331)) +- **ui:** store sort order and page size of userlist in localstorage ([#1262](https://github.com/sct/overseerr/issues/1262)) ([f5f8269](https://github.com/sct/overseerr/commit/f5f8269cd28ee792120060f4f38ef09d571fb8d5)) +- add option to cache images locally ([#1213](https://github.com/sct/overseerr/issues/1213)) ([0ca3d43](https://github.com/sct/overseerr/commit/0ca3d4374942b54b59a19d017ab4ae14ba7019c1)) +- genre sliders (experiment) ([#1182](https://github.com/sct/overseerr/issues/1182)) ([1c4515a](https://github.com/sct/overseerr/commit/1c4515a1ae6097f3948aaa0d0ed210831581fd98)) ### Reverts -* **ui:** remove local image cache option from settings page ([911faef](https://github.com/sct/overseerr/commit/911faeff562b737a2d18a395fcd90bf354af0cc4)) -* remove experimental tailwind jit compiler until title card hover is fixed ([1df67ba](https://github.com/sct/overseerr/commit/1df67baf9e7cdabc4045a0c115735797e8081bca)) -* **deps:** revert react-intl to 5.13.5 ([e16277c](https://github.com/sct/overseerr/commit/e16277c07d58ddbb749f4a60bc05924f4a5af146)) +- **ui:** remove local image cache option from settings page ([911faef](https://github.com/sct/overseerr/commit/911faeff562b737a2d18a395fcd90bf354af0cc4)) +- remove experimental tailwind jit compiler until title card hover is fixed ([1df67ba](https://github.com/sct/overseerr/commit/1df67baf9e7cdabc4045a0c115735797e8081bca)) +- **deps:** revert react-intl to 5.13.5 ([e16277c](https://github.com/sct/overseerr/commit/e16277c07d58ddbb749f4a60bc05924f4a5af146)) ## [1.21.1](https://github.com/sct/overseerr/compare/v1.21.0...v1.21.1) (2021-03-15) - ### Bug Fixes -* **lang:** translations update from Weblate ([#1155](https://github.com/sct/overseerr/issues/1155)) ([ebc285c](https://github.com/sct/overseerr/commit/ebc285c758f69846e4a5cb74bb42ca5924d166d4)) +- **lang:** translations update from Weblate ([#1155](https://github.com/sct/overseerr/issues/1155)) ([ebc285c](https://github.com/sct/overseerr/commit/ebc285c758f69846e4a5cb74bb42ca5924d166d4)) # [1.21.0](https://github.com/sct/overseerr/compare/v1.20.1...v1.21.0) (2021-03-15) - ### Bug Fixes -* do not allow editing of user settings under certain conditions ([#1168](https://github.com/sct/overseerr/issues/1168)) ([001dcd3](https://github.com/sct/overseerr/commit/001dcd328c8d3b1c417fd7c7ee2aa20183b08eef)) -* **frontend:** check for ID instead of email after initial setup Plex login ([#1097](https://github.com/sct/overseerr/issues/1097)) ([778dda6](https://github.com/sct/overseerr/commit/778dda67d54df87347dd79577ef1bdc88d3c1d3f)) -* **frontend:** check if swr is validating to determine if we should fetch new data ([e5f5bdb](https://github.com/sct/overseerr/commit/e5f5bdb95c62eba31a3321a7457d354f0226bf85)), closes [#719](https://github.com/sct/overseerr/issues/719) -* **frontend:** never hide available content in search results ([d48edeb](https://github.com/sct/overseerr/commit/d48edeb5a9bd8e2edce8bca0fea50e300bb7a1ae)) -* **lang:** add missing i18n strings ([6072e8a](https://github.com/sct/overseerr/commit/6072e8aa9a0f84e50c44a92af303aad15b5f3021)) -* **lang:** edit new Telegram-related strings to conform to style guide ([#1093](https://github.com/sct/overseerr/issues/1093)) ([bdf67e7](https://github.com/sct/overseerr/commit/bdf67e732b6c77cbae768a25edfc9a663ef0108b)) -* **notif:** loosen input validation on Pushover settings ([#1166](https://github.com/sct/overseerr/issues/1166)) ([3148d31](https://github.com/sct/overseerr/commit/3148d312141248653c5d1e42cd2882a67a339163)) -* **notif:** set URL for Discord embeds rather than adding a field for the link ([#1167](https://github.com/sct/overseerr/issues/1167)) ([0bd0912](https://github.com/sct/overseerr/commit/0bd0912613f0db24bd0da4ec956b5119133e35d4)) -* correctly send auto-approval notifictions for series ([8634081](https://github.com/sct/overseerr/commit/8634081c869a2078793ecf06b1b7e249bba0a2f8)) -* **lang:** fix singular form of season count ([#1080](https://github.com/sct/overseerr/issues/1080)) ([b57645d](https://github.com/sct/overseerr/commit/b57645d382361c856281e7a74295afe16c5390f2)) -* **requests:** add plex url to request item ([#1088](https://github.com/sct/overseerr/issues/1088)) ([420038d](https://github.com/sct/overseerr/commit/420038d5ffdd4070df03e5c5cb6ef8d6208fddb5)) -* **sonarr:** correctly search when updating existing sonarr series ([ed0a7fb](https://github.com/sct/overseerr/commit/ed0a7fbdf5122a26fa936e83b76a97c55781782d)), closes [#588](https://github.com/sct/overseerr/issues/588) -* **ui:** add alt prop to studio/network logos & fix blinking text cursor ([#1095](https://github.com/sct/overseerr/issues/1095)) ([0c4637f](https://github.com/sct/overseerr/commit/0c4637f779d8904037b9cbd5fe9166cf05a891c5)) -* **ui:** add link to poster image on request items ([7289872](https://github.com/sct/overseerr/commit/7289872937d5bb94d027424760ee1ceb94095604)) -* **ui:** correct language usage re: "sync" vs. "scan" ([#1079](https://github.com/sct/overseerr/issues/1079)) ([e98f2b9](https://github.com/sct/overseerr/commit/e98f2b96058fb9c5af77be2e8a1bd07fb8fcca06)) -* **ui:** display "Season" vs. "Seasons" as appropriate, and fix request block "Seasons" formatting ([#1127](https://github.com/sct/overseerr/issues/1127)) ([45886cc](https://github.com/sct/overseerr/commit/45886ccef1bee57dc555060a491834567e45b59c)) -* **ui:** request list button sizes ([#1152](https://github.com/sct/overseerr/issues/1152)) ([fc73592](https://github.com/sct/overseerr/commit/fc73592b69c38191f91a68a020868b8e5ec2e2e2)) -* fix language filter link on movie detail pages ([#1142](https://github.com/sct/overseerr/issues/1142)) ([60d453b](https://github.com/sct/overseerr/commit/60d453b0bbba5e2060f72f40d1dde85ec6b05af4)) -* remove language/region filtering on studio/network results ([#1129](https://github.com/sct/overseerr/issues/1129)) ([109aca8](https://github.com/sct/overseerr/commit/109aca8229dc7b81cac314d84591f1c04c12ac2e)) -* **api:** check correct permissions for auto approve when requests are created ([3c1a72b](https://github.com/sct/overseerr/commit/3c1a72b038fd178b4be4dc082cd1496474148d7e)) -* **frontend:** status, requested by, and modified alignment fix ([#1109](https://github.com/sct/overseerr/issues/1109)) ([1a7dc1a](https://github.com/sct/overseerr/commit/1a7dc1acf57888d3d0285b58c1c97a824a232216)) -* **ui:** don't show "Password" user settings tab if current user lacks perms to modify the password ([#1063](https://github.com/sct/overseerr/issues/1063)) ([b146d11](https://github.com/sct/overseerr/commit/b146d11e2ffecedae76472b0491a4662ca4a4a4e)) -* **ui:** fix Radarr logo alignment ([#1068](https://github.com/sct/overseerr/issues/1068)) ([0fa005a](https://github.com/sct/overseerr/commit/0fa005a99cd868b5a235ae9ce65b4c64b05d0f47)) -* **ui:** fix request list UI behavior when season list is too long ([#1106](https://github.com/sct/overseerr/issues/1106)) ([8507691](https://github.com/sct/overseerr/commit/85076919c6ccbf052699b7d5f4ba8b6e5e5af74d)) -* **ui:** improve responsive design on new request list UI ([#1105](https://github.com/sct/overseerr/issues/1105)) ([1f8b03f](https://github.com/sct/overseerr/commit/1f8b03ff6f67ce76051667de05166da54ed3dc89)) -* **ui:** list all movie studios instead of just the first result ([#1110](https://github.com/sct/overseerr/issues/1110)) ([239202d](https://github.com/sct/overseerr/commit/239202d9c11f27410b0fa084bcc4c824b7136081)) -* add correct permission checks to modifying user password/permissions ([ddfc5e6](https://github.com/sct/overseerr/commit/ddfc5e6aa8fc636931f495d6f23d56367466e3b5)) - +- do not allow editing of user settings under certain conditions ([#1168](https://github.com/sct/overseerr/issues/1168)) ([001dcd3](https://github.com/sct/overseerr/commit/001dcd328c8d3b1c417fd7c7ee2aa20183b08eef)) +- **frontend:** check for ID instead of email after initial setup Plex login ([#1097](https://github.com/sct/overseerr/issues/1097)) ([778dda6](https://github.com/sct/overseerr/commit/778dda67d54df87347dd79577ef1bdc88d3c1d3f)) +- **frontend:** check if swr is validating to determine if we should fetch new data ([e5f5bdb](https://github.com/sct/overseerr/commit/e5f5bdb95c62eba31a3321a7457d354f0226bf85)), closes [#719](https://github.com/sct/overseerr/issues/719) +- **frontend:** never hide available content in search results ([d48edeb](https://github.com/sct/overseerr/commit/d48edeb5a9bd8e2edce8bca0fea50e300bb7a1ae)) +- **lang:** add missing i18n strings ([6072e8a](https://github.com/sct/overseerr/commit/6072e8aa9a0f84e50c44a92af303aad15b5f3021)) +- **lang:** edit new Telegram-related strings to conform to style guide ([#1093](https://github.com/sct/overseerr/issues/1093)) ([bdf67e7](https://github.com/sct/overseerr/commit/bdf67e732b6c77cbae768a25edfc9a663ef0108b)) +- **notif:** loosen input validation on Pushover settings ([#1166](https://github.com/sct/overseerr/issues/1166)) ([3148d31](https://github.com/sct/overseerr/commit/3148d312141248653c5d1e42cd2882a67a339163)) +- **notif:** set URL for Discord embeds rather than adding a field for the link ([#1167](https://github.com/sct/overseerr/issues/1167)) ([0bd0912](https://github.com/sct/overseerr/commit/0bd0912613f0db24bd0da4ec956b5119133e35d4)) +- correctly send auto-approval notifictions for series ([8634081](https://github.com/sct/overseerr/commit/8634081c869a2078793ecf06b1b7e249bba0a2f8)) +- **lang:** fix singular form of season count ([#1080](https://github.com/sct/overseerr/issues/1080)) ([b57645d](https://github.com/sct/overseerr/commit/b57645d382361c856281e7a74295afe16c5390f2)) +- **requests:** add plex url to request item ([#1088](https://github.com/sct/overseerr/issues/1088)) ([420038d](https://github.com/sct/overseerr/commit/420038d5ffdd4070df03e5c5cb6ef8d6208fddb5)) +- **sonarr:** correctly search when updating existing sonarr series ([ed0a7fb](https://github.com/sct/overseerr/commit/ed0a7fbdf5122a26fa936e83b76a97c55781782d)), closes [#588](https://github.com/sct/overseerr/issues/588) +- **ui:** add alt prop to studio/network logos & fix blinking text cursor ([#1095](https://github.com/sct/overseerr/issues/1095)) ([0c4637f](https://github.com/sct/overseerr/commit/0c4637f779d8904037b9cbd5fe9166cf05a891c5)) +- **ui:** add link to poster image on request items ([7289872](https://github.com/sct/overseerr/commit/7289872937d5bb94d027424760ee1ceb94095604)) +- **ui:** correct language usage re: "sync" vs. "scan" ([#1079](https://github.com/sct/overseerr/issues/1079)) ([e98f2b9](https://github.com/sct/overseerr/commit/e98f2b96058fb9c5af77be2e8a1bd07fb8fcca06)) +- **ui:** display "Season" vs. "Seasons" as appropriate, and fix request block "Seasons" formatting ([#1127](https://github.com/sct/overseerr/issues/1127)) ([45886cc](https://github.com/sct/overseerr/commit/45886ccef1bee57dc555060a491834567e45b59c)) +- **ui:** request list button sizes ([#1152](https://github.com/sct/overseerr/issues/1152)) ([fc73592](https://github.com/sct/overseerr/commit/fc73592b69c38191f91a68a020868b8e5ec2e2e2)) +- fix language filter link on movie detail pages ([#1142](https://github.com/sct/overseerr/issues/1142)) ([60d453b](https://github.com/sct/overseerr/commit/60d453b0bbba5e2060f72f40d1dde85ec6b05af4)) +- remove language/region filtering on studio/network results ([#1129](https://github.com/sct/overseerr/issues/1129)) ([109aca8](https://github.com/sct/overseerr/commit/109aca8229dc7b81cac314d84591f1c04c12ac2e)) +- **api:** check correct permissions for auto approve when requests are created ([3c1a72b](https://github.com/sct/overseerr/commit/3c1a72b038fd178b4be4dc082cd1496474148d7e)) +- **frontend:** status, requested by, and modified alignment fix ([#1109](https://github.com/sct/overseerr/issues/1109)) ([1a7dc1a](https://github.com/sct/overseerr/commit/1a7dc1acf57888d3d0285b58c1c97a824a232216)) +- **ui:** don't show "Password" user settings tab if current user lacks perms to modify the password ([#1063](https://github.com/sct/overseerr/issues/1063)) ([b146d11](https://github.com/sct/overseerr/commit/b146d11e2ffecedae76472b0491a4662ca4a4a4e)) +- **ui:** fix Radarr logo alignment ([#1068](https://github.com/sct/overseerr/issues/1068)) ([0fa005a](https://github.com/sct/overseerr/commit/0fa005a99cd868b5a235ae9ce65b4c64b05d0f47)) +- **ui:** fix request list UI behavior when season list is too long ([#1106](https://github.com/sct/overseerr/issues/1106)) ([8507691](https://github.com/sct/overseerr/commit/85076919c6ccbf052699b7d5f4ba8b6e5e5af74d)) +- **ui:** improve responsive design on new request list UI ([#1105](https://github.com/sct/overseerr/issues/1105)) ([1f8b03f](https://github.com/sct/overseerr/commit/1f8b03ff6f67ce76051667de05166da54ed3dc89)) +- **ui:** list all movie studios instead of just the first result ([#1110](https://github.com/sct/overseerr/issues/1110)) ([239202d](https://github.com/sct/overseerr/commit/239202d9c11f27410b0fa084bcc4c824b7136081)) +- add correct permission checks to modifying user password/permissions ([ddfc5e6](https://github.com/sct/overseerr/commit/ddfc5e6aa8fc636931f495d6f23d56367466e3b5)) ### Features -* add tagline, episode runtime, genres list to media details & clean/refactor CSS into globals ([#1160](https://github.com/sct/overseerr/issues/1160)) ([2f2e002](https://github.com/sct/overseerr/commit/2f2e00237d43bdab85bfadc3c4f2fbcdde4c2e90)) -* **docker:** add tini to docker image ([#1017](https://github.com/sct/overseerr/issues/1017)) ([1629d02](https://github.com/sct/overseerr/commit/1629d02f3d8368bfd5f6fed05382974ae6fce51f)) -* **email:** add pgp support ([#1138](https://github.com/sct/overseerr/issues/1138)) ([9e5adeb](https://github.com/sct/overseerr/commit/9e5adeb610bdc4800ff536412d0ae8a11fb4338d)) -* **frontend:** add loading bar indicator ([#1170](https://github.com/sct/overseerr/issues/1170)) ([3d6b343](https://github.com/sct/overseerr/commit/3d6b3434138fec49c58f2bf74f781d5e2fc2911f)) -* **lang:** localize job names ([#1043](https://github.com/sct/overseerr/issues/1043)) ([594aad9](https://github.com/sct/overseerr/commit/594aad9d3ae9b323677f3af8c434d7664526593d)) -* **lang:** translations update from Weblate ([#1051](https://github.com/sct/overseerr/issues/1051)) ([69bf817](https://github.com/sct/overseerr/commit/69bf817f598babed99964f073259f827b60bd014)) -* **lang:** Translations update from Weblate ([#1131](https://github.com/sct/overseerr/issues/1131)) ([e4686d6](https://github.com/sct/overseerr/commit/e4686d664b52448e32488ff1c4236f72e01e9a29)) -* **notif:** add "Media Automatically Approved" notification type ([#1137](https://github.com/sct/overseerr/issues/1137)) ([f7d2723](https://github.com/sct/overseerr/commit/f7d2723fab2c30564fd23945709cd39b178a6eef)) -* **notif:** add settings for Discord bot username & avatar URL ([#1113](https://github.com/sct/overseerr/issues/1113)) ([3384eb1](https://github.com/sct/overseerr/commit/3384eb1c479114c0246cb22f9a933aa79fb95fcf)) -* **notif:** include poster image in Telegram notifications ([#1112](https://github.com/sct/overseerr/issues/1112)) ([48387e5](https://github.com/sct/overseerr/commit/48387e5b2f26c0c33acd436c6e1cf902d6c32101)) -* **scan:** add support for new plex tv agent ([#1144](https://github.com/sct/overseerr/issues/1144)) ([a51d2a2](https://github.com/sct/overseerr/commit/a51d2a24d51d092a0c6da608e3322f19a37c2d28)) -* **ui:** add user ID to profile header ([6e95c8b](https://github.com/sct/overseerr/commit/6e95c8b7a10e3467bfd2c3df84ccf886fe01ca5c)) -* add genre/studio/network view to Discover results ([#1067](https://github.com/sct/overseerr/issues/1067)) ([f28112f](https://github.com/sct/overseerr/commit/f28112f057df2589f31ae0d0b14e8b50e479fdb7)) -* add language-filtered Discover pages ([#1111](https://github.com/sct/overseerr/issues/1111)) ([7501161](https://github.com/sct/overseerr/commit/75011610e57f03098c8be9375d0c9ba1e3647e9b)) -* add studio/network sliders to discover ([1c6914f](https://github.com/sct/overseerr/commit/1c6914f5ce5c0d171c4609813915b50233a8e3ad)) -* **telegram:** add support for individual chat notifications ([#1027](https://github.com/sct/overseerr/issues/1027)) ([f6d00d8](https://github.com/sct/overseerr/commit/f6d00d8d1559879189f83739193c6e2acafde51d)) -* **ui:** display "Owner" role instead of "Admin" for user ID 1 ([#1050](https://github.com/sct/overseerr/issues/1050)) ([1b55d2d](https://github.com/sct/overseerr/commit/1b55d2dfbc06d900e7370a4ddfd81789a25bf00c)) -* **ui:** display season count on TV details page ([#1078](https://github.com/sct/overseerr/issues/1078)) ([4365231](https://github.com/sct/overseerr/commit/436523139e8f1594c352b17032734b4498d3994f)) -* **ui:** in Settings > Services, make Radarr/Sonarr server names and logos clickable links ([#1008](https://github.com/sct/overseerr/issues/1008)) ([6a1e389](https://github.com/sct/overseerr/commit/6a1e3891aa5f84b6adb1e475a6658a8cd4e34c22)) -* **ui:** request list redesign ([#1099](https://github.com/sct/overseerr/issues/1099)) ([cd21865](https://github.com/sct/overseerr/commit/cd21865c4d5be00c13c372e0b7a058f61ec855a2)) +- add tagline, episode runtime, genres list to media details & clean/refactor CSS into globals ([#1160](https://github.com/sct/overseerr/issues/1160)) ([2f2e002](https://github.com/sct/overseerr/commit/2f2e00237d43bdab85bfadc3c4f2fbcdde4c2e90)) +- **docker:** add tini to docker image ([#1017](https://github.com/sct/overseerr/issues/1017)) ([1629d02](https://github.com/sct/overseerr/commit/1629d02f3d8368bfd5f6fed05382974ae6fce51f)) +- **email:** add pgp support ([#1138](https://github.com/sct/overseerr/issues/1138)) ([9e5adeb](https://github.com/sct/overseerr/commit/9e5adeb610bdc4800ff536412d0ae8a11fb4338d)) +- **frontend:** add loading bar indicator ([#1170](https://github.com/sct/overseerr/issues/1170)) ([3d6b343](https://github.com/sct/overseerr/commit/3d6b3434138fec49c58f2bf74f781d5e2fc2911f)) +- **lang:** localize job names ([#1043](https://github.com/sct/overseerr/issues/1043)) ([594aad9](https://github.com/sct/overseerr/commit/594aad9d3ae9b323677f3af8c434d7664526593d)) +- **lang:** translations update from Weblate ([#1051](https://github.com/sct/overseerr/issues/1051)) ([69bf817](https://github.com/sct/overseerr/commit/69bf817f598babed99964f073259f827b60bd014)) +- **lang:** Translations update from Weblate ([#1131](https://github.com/sct/overseerr/issues/1131)) ([e4686d6](https://github.com/sct/overseerr/commit/e4686d664b52448e32488ff1c4236f72e01e9a29)) +- **notif:** add "Media Automatically Approved" notification type ([#1137](https://github.com/sct/overseerr/issues/1137)) ([f7d2723](https://github.com/sct/overseerr/commit/f7d2723fab2c30564fd23945709cd39b178a6eef)) +- **notif:** add settings for Discord bot username & avatar URL ([#1113](https://github.com/sct/overseerr/issues/1113)) ([3384eb1](https://github.com/sct/overseerr/commit/3384eb1c479114c0246cb22f9a933aa79fb95fcf)) +- **notif:** include poster image in Telegram notifications ([#1112](https://github.com/sct/overseerr/issues/1112)) ([48387e5](https://github.com/sct/overseerr/commit/48387e5b2f26c0c33acd436c6e1cf902d6c32101)) +- **scan:** add support for new plex tv agent ([#1144](https://github.com/sct/overseerr/issues/1144)) ([a51d2a2](https://github.com/sct/overseerr/commit/a51d2a24d51d092a0c6da608e3322f19a37c2d28)) +- **ui:** add user ID to profile header ([6e95c8b](https://github.com/sct/overseerr/commit/6e95c8b7a10e3467bfd2c3df84ccf886fe01ca5c)) +- add genre/studio/network view to Discover results ([#1067](https://github.com/sct/overseerr/issues/1067)) ([f28112f](https://github.com/sct/overseerr/commit/f28112f057df2589f31ae0d0b14e8b50e479fdb7)) +- add language-filtered Discover pages ([#1111](https://github.com/sct/overseerr/issues/1111)) ([7501161](https://github.com/sct/overseerr/commit/75011610e57f03098c8be9375d0c9ba1e3647e9b)) +- add studio/network sliders to discover ([1c6914f](https://github.com/sct/overseerr/commit/1c6914f5ce5c0d171c4609813915b50233a8e3ad)) +- **telegram:** add support for individual chat notifications ([#1027](https://github.com/sct/overseerr/issues/1027)) ([f6d00d8](https://github.com/sct/overseerr/commit/f6d00d8d1559879189f83739193c6e2acafde51d)) +- **ui:** display "Owner" role instead of "Admin" for user ID 1 ([#1050](https://github.com/sct/overseerr/issues/1050)) ([1b55d2d](https://github.com/sct/overseerr/commit/1b55d2dfbc06d900e7370a4ddfd81789a25bf00c)) +- **ui:** display season count on TV details page ([#1078](https://github.com/sct/overseerr/issues/1078)) ([4365231](https://github.com/sct/overseerr/commit/436523139e8f1594c352b17032734b4498d3994f)) +- **ui:** in Settings > Services, make Radarr/Sonarr server names and logos clickable links ([#1008](https://github.com/sct/overseerr/issues/1008)) ([6a1e389](https://github.com/sct/overseerr/commit/6a1e3891aa5f84b6adb1e475a6658a8cd4e34c22)) +- **ui:** request list redesign ([#1099](https://github.com/sct/overseerr/issues/1099)) ([cd21865](https://github.com/sct/overseerr/commit/cd21865c4d5be00c13c372e0b7a058f61ec855a2)) ## [1.20.1](https://github.com/sct/overseerr/compare/v1.20.0...v1.20.1) (2021-02-28) - ### Bug Fixes -* **notif:** escape application title in Telegram notifications ([#1012](https://github.com/sct/overseerr/issues/1012)) ([5560abf](https://github.com/sct/overseerr/commit/5560abf459b0350ff30b5e71d4208418fc8f3b3e)) -* **notif:** fixed typo in pushover hint ([#1029](https://github.com/sct/overseerr/issues/1029)) ([e9f2fe9](https://github.com/sct/overseerr/commit/e9f2fe910d72fa41bc27673ed43291211c3cac65)) -* **notifications:** correctly send tv auto approval notifications ([537850f](https://github.com/sct/overseerr/commit/537850f414a88df24c78794a2fd68e1e24ff73d1)), closes [#1041](https://github.com/sct/overseerr/issues/1041) -* **plex-sync:** no longer incorrectly sets 4k availability when there isnt any ([3f9a116](https://github.com/sct/overseerr/commit/3f9a116b17d78eeb04f0f125a4f3af6f907c83dd)), closes [#990](https://github.com/sct/overseerr/issues/990) -* **ui:** for server default options, display "All" region/language option instead of empty string ([#1042](https://github.com/sct/overseerr/issues/1042)) ([3fed26c](https://github.com/sct/overseerr/commit/3fed26cfbe74cb662ca531fd37b69f159a051ac1)) -* **ui:** show translated string on sonarr sucesss/failure toast messages ([#1035](https://github.com/sct/overseerr/issues/1035)) ([eefcbcd](https://github.com/sct/overseerr/commit/eefcbcd3ddfa5258ee24dbbbd79de5bf50310f27)) -* **ui:** use country-flag-icons instead of country-flag-emoji for RegionSelector ([#1011](https://github.com/sct/overseerr/issues/1011)) ([abcd7c9](https://github.com/sct/overseerr/commit/abcd7c997584c1310bd8b313ac38f30e335af8d7)) -* add missing default value for settings context ([084917f](https://github.com/sct/overseerr/commit/084917f02d399e2d29bb9927e033c2e6533f586c)) -* added missing language default for ssr context defaults ([9ce88ab](https://github.com/sct/overseerr/commit/9ce88abcc85d744d77172cd2357fdb4ff60dc5e4)) -* allow users to override language/region settings ([69294a7](https://github.com/sct/overseerr/commit/69294a7c4c5bbe55c5cd276786cdfd48ddbff889)), closes [#1013](https://github.com/sct/overseerr/issues/1013) +- **notif:** escape application title in Telegram notifications ([#1012](https://github.com/sct/overseerr/issues/1012)) ([5560abf](https://github.com/sct/overseerr/commit/5560abf459b0350ff30b5e71d4208418fc8f3b3e)) +- **notif:** fixed typo in pushover hint ([#1029](https://github.com/sct/overseerr/issues/1029)) ([e9f2fe9](https://github.com/sct/overseerr/commit/e9f2fe910d72fa41bc27673ed43291211c3cac65)) +- **notifications:** correctly send tv auto approval notifications ([537850f](https://github.com/sct/overseerr/commit/537850f414a88df24c78794a2fd68e1e24ff73d1)), closes [#1041](https://github.com/sct/overseerr/issues/1041) +- **plex-sync:** no longer incorrectly sets 4k availability when there isnt any ([3f9a116](https://github.com/sct/overseerr/commit/3f9a116b17d78eeb04f0f125a4f3af6f907c83dd)), closes [#990](https://github.com/sct/overseerr/issues/990) +- **ui:** for server default options, display "All" region/language option instead of empty string ([#1042](https://github.com/sct/overseerr/issues/1042)) ([3fed26c](https://github.com/sct/overseerr/commit/3fed26cfbe74cb662ca531fd37b69f159a051ac1)) +- **ui:** show translated string on sonarr sucesss/failure toast messages ([#1035](https://github.com/sct/overseerr/issues/1035)) ([eefcbcd](https://github.com/sct/overseerr/commit/eefcbcd3ddfa5258ee24dbbbd79de5bf50310f27)) +- **ui:** use country-flag-icons instead of country-flag-emoji for RegionSelector ([#1011](https://github.com/sct/overseerr/issues/1011)) ([abcd7c9](https://github.com/sct/overseerr/commit/abcd7c997584c1310bd8b313ac38f30e335af8d7)) +- add missing default value for settings context ([084917f](https://github.com/sct/overseerr/commit/084917f02d399e2d29bb9927e033c2e6533f586c)) +- added missing language default for ssr context defaults ([9ce88ab](https://github.com/sct/overseerr/commit/9ce88abcc85d744d77172cd2357fdb4ff60dc5e4)) +- allow users to override language/region settings ([69294a7](https://github.com/sct/overseerr/commit/69294a7c4c5bbe55c5cd276786cdfd48ddbff889)), closes [#1013](https://github.com/sct/overseerr/issues/1013) # [1.20.0](https://github.com/sct/overseerr/compare/v1.19.1...v1.20.0) (2021-02-23) - ### Bug Fixes -* **api:** add isAuthenticated middleware to base user route ([8a27c70](https://github.com/sct/overseerr/commit/8a27c7062599ea23dca115e6e6e95a594e1b219a)) -* **api:** sort users requests by most recent ([1798383](https://github.com/sct/overseerr/commit/17983837fc10661a59d29fc1531530fca0d77825)) -* **api:** Use POST instead of GET for API endpoints that mutate state ([#877](https://github.com/sct/overseerr/issues/877)) ([ff0b5ed](https://github.com/sct/overseerr/commit/ff0b5ed44132cc5a0cd178035796d042ba735a8d)) -* **auth:** handle sign-in attempts from emails with no password ([#933](https://github.com/sct/overseerr/issues/933)) ([5e37a96](https://github.com/sct/overseerr/commit/5e37a96bc017471f8dc4cbdd57f2e8c3568bd97f)) -* **frontend:** changed plex, request, and cog buttons to align properly on smaller mobile UIs ([#928](https://github.com/sct/overseerr/issues/928)) ([f1c3358](https://github.com/sct/overseerr/commit/f1c335815f2f17465cdd36ceb223e78a58149b3b)) -* **frontend:** check for id instead of email after logging in ([c4af4c4](https://github.com/sct/overseerr/commit/c4af4c42ab00f1a63a2f5326c9cd8b26c19f4f14)) -* **frontend:** Do not allow user w/ ID 1 to disable 'Admin' permission ([#965](https://github.com/sct/overseerr/issues/965)) ([77b2d9e](https://github.com/sct/overseerr/commit/77b2d9ea22a2f70cff58ac9421f3f6231bc93059)) -* **frontend:** handle empty array of media attributes ([#922](https://github.com/sct/overseerr/issues/922)) ([04fa9f7](https://github.com/sct/overseerr/commit/04fa9f79e2ec90082b3fa15590dd170f7d68ad52)) -* **frontend:** request and cog button would be misaligned without play on plex/watch trailer button ([#956](https://github.com/sct/overseerr/issues/956)) ([e28dfad](https://github.com/sct/overseerr/commit/e28dfadaf57d47887013c31dc5006332473156e3)) -* **frontend:** Update AdvancedRequester to reflect new /user API response ([#970](https://github.com/sct/overseerr/issues/970)) ([b4bac6a](https://github.com/sct/overseerr/commit/b4bac6a9157119a4f234933245944e133c127bd0)) -* **frontend:** use region settings instead of hardcoded 'US' value for movie/TV ratings ([#1006](https://github.com/sct/overseerr/issues/1006)) ([6ecd202](https://github.com/sct/overseerr/commit/6ecd202607cb48d559440da810ecc585e740542b)) -* **lang:** formatMessage should not use an object spread ([8a7fa00](https://github.com/sct/overseerr/commit/8a7fa00164fd5c5501da525baa29be97bac7e7c4)) -* **lang:** Remove unused strings and correct spelling of 'canceling'/'canceled' ([#981](https://github.com/sct/overseerr/issues/981)) ([5b64655](https://github.com/sct/overseerr/commit/5b646557765d1ad75e44e1c0e60e0291313c7746)) -* **login:** fix the gap when 'use your overseer account' was selected ([#870](https://github.com/sct/overseerr/issues/870)) ([d163e29](https://github.com/sct/overseerr/commit/d163e294599c4bd9bdc0a148db15c8e8541410d8)) -* **notif:** Do not HTML-escape email subjects ([#931](https://github.com/sct/overseerr/issues/931)) ([019622a](https://github.com/sct/overseerr/commit/019622aab1b94cc4d71cacbf0dc5cf64b62c8623)) -* **notif:** Remove extra newlines from Telegram notifications ([#973](https://github.com/sct/overseerr/issues/973)) ([bbea522](https://github.com/sct/overseerr/commit/bbea52249950eb98a8d3886f2bd7648a7d669bf4)) -* **plex:** Check Plex server access on user import ([#955](https://github.com/sct/overseerr/issues/955)) ([bdb3cb2](https://github.com/sct/overseerr/commit/bdb3cb202550e34d8951ac2b5015f97f6a5c1ebf)) -* **plex-sync:** get correct Plex metadata for Hama movie items ([#901](https://github.com/sct/overseerr/issues/901)) ([03cecb3](https://github.com/sct/overseerr/commit/03cecb33559e27199c5a174fc86de0c4550fe666)), closes [#898](https://github.com/sct/overseerr/issues/898) -* **requests:** correctly filter requests out for users without view requests permission ([e118501](https://github.com/sct/overseerr/commit/e118501bf1dfa8dada2c57090e62631de620f3dd)) -* **requests:** correctly handle when tvdbid is missing ([#891](https://github.com/sct/overseerr/issues/891)) ([e037ba4](https://github.com/sct/overseerr/commit/e037ba48f173c06b0c9c8b03085edf832d770c06)) -* **search:** Handle search errors and escape * ([#893](https://github.com/sct/overseerr/issues/893)) ([034968e](https://github.com/sct/overseerr/commit/034968e4370eaea726c94730274349c083856813)) -* **services:** update all radarr/sonarr endpoints to use v3 ([da5ca02](https://github.com/sct/overseerr/commit/da5ca02f81fe91070afbda3e1ebc8d869fe39a8f)) -* **sonarr:** use qualityProfileId instad of profileId when adding series ([552a7e3](https://github.com/sct/overseerr/commit/552a7e30da5fc2cc0bd43b5aef79a0225c75d233)) -* **sync:** fix sonarr/plex sync fighting over availability ([9b73423](https://github.com/sct/overseerr/commit/9b73423d49e1e799cd82764a9ade8c75d92a28a2)), closes [#872](https://github.com/sct/overseerr/issues/872) -* **ui:** add fallback for region display name ([f9c83e1](https://github.com/sct/overseerr/commit/f9c83e14e52a57d6865307b3324a61c04a77a541)) -* **ui:** add missing string for default Discover Language & edit string for default Discover Region ([#1004](https://github.com/sct/overseerr/issues/1004)) ([0acad8e](https://github.com/sct/overseerr/commit/0acad8e9fa65a9de6cecac9b6a4a5b2313ba8f06)) -* **ui:** Add tip & validation for Discord ID input ([#966](https://github.com/sct/overseerr/issues/966)) ([e70a4ec](https://github.com/sct/overseerr/commit/e70a4ecae613e045977e262fd7f9643f30985ab7)) -* **ui:** also allow 17 digit discord ids ([57c00c1](https://github.com/sct/overseerr/commit/57c00c1ea71c1229d5a59e1b8dadd84a646772b9)), closes [#971](https://github.com/sct/overseerr/issues/971) -* **ui:** Automatically disable and uncheck user permissions with unmet requirements ([#941](https://github.com/sct/overseerr/issues/941)) ([c9a150b](https://github.com/sct/overseerr/commit/c9a150b1db2adbb305cf1a448489d7a8c14cf1cb)) -* **ui:** change font size in request list/user list dropdowns to prevent zoom on mobile ([fb9c878](https://github.com/sct/overseerr/commit/fb9c878db49c01d13773e8d2f94c93f840be0b82)) -* **ui:** Display 4K download status on 4K status badge ([#988](https://github.com/sct/overseerr/issues/988)) ([40b07c3](https://github.com/sct/overseerr/commit/40b07c35d40c03039e4bfa5ed1e73af7e8aa6a7d)) -* **ui:** Fix card sizes on person detail pages ([#881](https://github.com/sct/overseerr/issues/881)) ([a3042f8](https://github.com/sct/overseerr/commit/a3042f8e1b05a91d98f48a4aecb08e831a48fc56)) -* **ui:** Fix settings navigation horizontal scroll issues ([#987](https://github.com/sct/overseerr/issues/987)) ([8701fb2](https://github.com/sct/overseerr/commit/8701fb20d07773f4cc32e857b68575a813cf7e21)) -* **ui:** fix webhook URL validation regex ([baad19a](https://github.com/sct/overseerr/commit/baad19a2c94728313ee996fe1a0ffc64fbd9aaa3)) -* **ui:** fixed anime language profile typo ([#879](https://github.com/sct/overseerr/issues/879)) ([ee50761](https://github.com/sct/overseerr/commit/ee5076146ef3c5e8baba197a5b397d3c3f575262)) -* **ui:** Handle missing movie/series data ([#862](https://github.com/sct/overseerr/issues/862)) ([7c0ddad](https://github.com/sct/overseerr/commit/7c0ddad653393327226a877692f046d8693ddc66)) -* **ui:** Notification-related string/UI edits and field validation ([#985](https://github.com/sct/overseerr/issues/985)) ([c88fcb2](https://github.com/sct/overseerr/commit/c88fcb2e2d1c4b84527844a80680c15337626e72)) -* **ui:** rename global group class to form-group ([8056187](https://github.com/sct/overseerr/commit/8056187c3c0ea464a8f751aa6347ea1d35c01aac)) -* **ui:** Size cards appropriately based on base font size ([#871](https://github.com/sct/overseerr/issues/871)) ([282f28f](https://github.com/sct/overseerr/commit/282f28f2b9d0cc8c9105d01b43d4e1f730320b8b)) -* **ui/notif:** Custom application title in password-related emails and UI messages ([#979](https://github.com/sct/overseerr/issues/979)) ([4e2706b](https://github.com/sct/overseerr/commit/4e2706b4211b06f364910c327d84c2ceb45b2fe3)) - +- **api:** add isAuthenticated middleware to base user route ([8a27c70](https://github.com/sct/overseerr/commit/8a27c7062599ea23dca115e6e6e95a594e1b219a)) +- **api:** sort users requests by most recent ([1798383](https://github.com/sct/overseerr/commit/17983837fc10661a59d29fc1531530fca0d77825)) +- **api:** Use POST instead of GET for API endpoints that mutate state ([#877](https://github.com/sct/overseerr/issues/877)) ([ff0b5ed](https://github.com/sct/overseerr/commit/ff0b5ed44132cc5a0cd178035796d042ba735a8d)) +- **auth:** handle sign-in attempts from emails with no password ([#933](https://github.com/sct/overseerr/issues/933)) ([5e37a96](https://github.com/sct/overseerr/commit/5e37a96bc017471f8dc4cbdd57f2e8c3568bd97f)) +- **frontend:** changed plex, request, and cog buttons to align properly on smaller mobile UIs ([#928](https://github.com/sct/overseerr/issues/928)) ([f1c3358](https://github.com/sct/overseerr/commit/f1c335815f2f17465cdd36ceb223e78a58149b3b)) +- **frontend:** check for id instead of email after logging in ([c4af4c4](https://github.com/sct/overseerr/commit/c4af4c42ab00f1a63a2f5326c9cd8b26c19f4f14)) +- **frontend:** Do not allow user w/ ID 1 to disable 'Admin' permission ([#965](https://github.com/sct/overseerr/issues/965)) ([77b2d9e](https://github.com/sct/overseerr/commit/77b2d9ea22a2f70cff58ac9421f3f6231bc93059)) +- **frontend:** handle empty array of media attributes ([#922](https://github.com/sct/overseerr/issues/922)) ([04fa9f7](https://github.com/sct/overseerr/commit/04fa9f79e2ec90082b3fa15590dd170f7d68ad52)) +- **frontend:** request and cog button would be misaligned without play on plex/watch trailer button ([#956](https://github.com/sct/overseerr/issues/956)) ([e28dfad](https://github.com/sct/overseerr/commit/e28dfadaf57d47887013c31dc5006332473156e3)) +- **frontend:** Update AdvancedRequester to reflect new /user API response ([#970](https://github.com/sct/overseerr/issues/970)) ([b4bac6a](https://github.com/sct/overseerr/commit/b4bac6a9157119a4f234933245944e133c127bd0)) +- **frontend:** use region settings instead of hardcoded 'US' value for movie/TV ratings ([#1006](https://github.com/sct/overseerr/issues/1006)) ([6ecd202](https://github.com/sct/overseerr/commit/6ecd202607cb48d559440da810ecc585e740542b)) +- **lang:** formatMessage should not use an object spread ([8a7fa00](https://github.com/sct/overseerr/commit/8a7fa00164fd5c5501da525baa29be97bac7e7c4)) +- **lang:** Remove unused strings and correct spelling of 'canceling'/'canceled' ([#981](https://github.com/sct/overseerr/issues/981)) ([5b64655](https://github.com/sct/overseerr/commit/5b646557765d1ad75e44e1c0e60e0291313c7746)) +- **login:** fix the gap when 'use your overseer account' was selected ([#870](https://github.com/sct/overseerr/issues/870)) ([d163e29](https://github.com/sct/overseerr/commit/d163e294599c4bd9bdc0a148db15c8e8541410d8)) +- **notif:** Do not HTML-escape email subjects ([#931](https://github.com/sct/overseerr/issues/931)) ([019622a](https://github.com/sct/overseerr/commit/019622aab1b94cc4d71cacbf0dc5cf64b62c8623)) +- **notif:** Remove extra newlines from Telegram notifications ([#973](https://github.com/sct/overseerr/issues/973)) ([bbea522](https://github.com/sct/overseerr/commit/bbea52249950eb98a8d3886f2bd7648a7d669bf4)) +- **plex:** Check Plex server access on user import ([#955](https://github.com/sct/overseerr/issues/955)) ([bdb3cb2](https://github.com/sct/overseerr/commit/bdb3cb202550e34d8951ac2b5015f97f6a5c1ebf)) +- **plex-sync:** get correct Plex metadata for Hama movie items ([#901](https://github.com/sct/overseerr/issues/901)) ([03cecb3](https://github.com/sct/overseerr/commit/03cecb33559e27199c5a174fc86de0c4550fe666)), closes [#898](https://github.com/sct/overseerr/issues/898) +- **requests:** correctly filter requests out for users without view requests permission ([e118501](https://github.com/sct/overseerr/commit/e118501bf1dfa8dada2c57090e62631de620f3dd)) +- **requests:** correctly handle when tvdbid is missing ([#891](https://github.com/sct/overseerr/issues/891)) ([e037ba4](https://github.com/sct/overseerr/commit/e037ba48f173c06b0c9c8b03085edf832d770c06)) +- **search:** Handle search errors and escape \* ([#893](https://github.com/sct/overseerr/issues/893)) ([034968e](https://github.com/sct/overseerr/commit/034968e4370eaea726c94730274349c083856813)) +- **services:** update all radarr/sonarr endpoints to use v3 ([da5ca02](https://github.com/sct/overseerr/commit/da5ca02f81fe91070afbda3e1ebc8d869fe39a8f)) +- **sonarr:** use qualityProfileId instad of profileId when adding series ([552a7e3](https://github.com/sct/overseerr/commit/552a7e30da5fc2cc0bd43b5aef79a0225c75d233)) +- **sync:** fix sonarr/plex sync fighting over availability ([9b73423](https://github.com/sct/overseerr/commit/9b73423d49e1e799cd82764a9ade8c75d92a28a2)), closes [#872](https://github.com/sct/overseerr/issues/872) +- **ui:** add fallback for region display name ([f9c83e1](https://github.com/sct/overseerr/commit/f9c83e14e52a57d6865307b3324a61c04a77a541)) +- **ui:** add missing string for default Discover Language & edit string for default Discover Region ([#1004](https://github.com/sct/overseerr/issues/1004)) ([0acad8e](https://github.com/sct/overseerr/commit/0acad8e9fa65a9de6cecac9b6a4a5b2313ba8f06)) +- **ui:** Add tip & validation for Discord ID input ([#966](https://github.com/sct/overseerr/issues/966)) ([e70a4ec](https://github.com/sct/overseerr/commit/e70a4ecae613e045977e262fd7f9643f30985ab7)) +- **ui:** also allow 17 digit discord ids ([57c00c1](https://github.com/sct/overseerr/commit/57c00c1ea71c1229d5a59e1b8dadd84a646772b9)), closes [#971](https://github.com/sct/overseerr/issues/971) +- **ui:** Automatically disable and uncheck user permissions with unmet requirements ([#941](https://github.com/sct/overseerr/issues/941)) ([c9a150b](https://github.com/sct/overseerr/commit/c9a150b1db2adbb305cf1a448489d7a8c14cf1cb)) +- **ui:** change font size in request list/user list dropdowns to prevent zoom on mobile ([fb9c878](https://github.com/sct/overseerr/commit/fb9c878db49c01d13773e8d2f94c93f840be0b82)) +- **ui:** Display 4K download status on 4K status badge ([#988](https://github.com/sct/overseerr/issues/988)) ([40b07c3](https://github.com/sct/overseerr/commit/40b07c35d40c03039e4bfa5ed1e73af7e8aa6a7d)) +- **ui:** Fix card sizes on person detail pages ([#881](https://github.com/sct/overseerr/issues/881)) ([a3042f8](https://github.com/sct/overseerr/commit/a3042f8e1b05a91d98f48a4aecb08e831a48fc56)) +- **ui:** Fix settings navigation horizontal scroll issues ([#987](https://github.com/sct/overseerr/issues/987)) ([8701fb2](https://github.com/sct/overseerr/commit/8701fb20d07773f4cc32e857b68575a813cf7e21)) +- **ui:** fix webhook URL validation regex ([baad19a](https://github.com/sct/overseerr/commit/baad19a2c94728313ee996fe1a0ffc64fbd9aaa3)) +- **ui:** fixed anime language profile typo ([#879](https://github.com/sct/overseerr/issues/879)) ([ee50761](https://github.com/sct/overseerr/commit/ee5076146ef3c5e8baba197a5b397d3c3f575262)) +- **ui:** Handle missing movie/series data ([#862](https://github.com/sct/overseerr/issues/862)) ([7c0ddad](https://github.com/sct/overseerr/commit/7c0ddad653393327226a877692f046d8693ddc66)) +- **ui:** Notification-related string/UI edits and field validation ([#985](https://github.com/sct/overseerr/issues/985)) ([c88fcb2](https://github.com/sct/overseerr/commit/c88fcb2e2d1c4b84527844a80680c15337626e72)) +- **ui:** rename global group class to form-group ([8056187](https://github.com/sct/overseerr/commit/8056187c3c0ea464a8f751aa6347ea1d35c01aac)) +- **ui:** Size cards appropriately based on base font size ([#871](https://github.com/sct/overseerr/issues/871)) ([282f28f](https://github.com/sct/overseerr/commit/282f28f2b9d0cc8c9105d01b43d4e1f730320b8b)) +- **ui/notif:** Custom application title in password-related emails and UI messages ([#979](https://github.com/sct/overseerr/issues/979)) ([4e2706b](https://github.com/sct/overseerr/commit/4e2706b4211b06f364910c327d84c2ceb45b2fe3)) ### Features -* **lang:** translated using Weblate (French) ([#1007](https://github.com/sct/overseerr/issues/1007)) ([970da66](https://github.com/sct/overseerr/commit/970da664b2700b8cd9ad8dce0cbca1d37820eceb)) -* **lang:** translations update from Weblate ([#853](https://github.com/sct/overseerr/issues/853)) ([e156acc](https://github.com/sct/overseerr/commit/e156acc1ae2fa86b4441faacc0b58e1e993e0edc)) -* **lang:** translations update from Weblate ([#986](https://github.com/sct/overseerr/issues/986)) ([4296765](https://github.com/sct/overseerr/commit/4296765ad61bac09c2317b71b763366d328733e4)) -* **notif:** Add Pushbullet notification agent ([#950](https://github.com/sct/overseerr/issues/950)) ([29b97ef](https://github.com/sct/overseerr/commit/29b97ef6d85bbea31dd59b7ad857b0d8ab30bff0)) -* **notif:** Notification improvements ([#914](https://github.com/sct/overseerr/issues/914)) ([2768155](https://github.com/sct/overseerr/commit/2768155bbabe121a4c51fc1472461cd5114c4300)) -* **regions:** add region/original language setting for filtering Discover ([#732](https://github.com/sct/overseerr/issues/732)) ([#942](https://github.com/sct/overseerr/issues/942)) ([b557c06](https://github.com/sct/overseerr/commit/b557c06b0a78f5df5f64a05dc1e4511dae72df4f)) -* **requests:** add language profile support ([#860](https://github.com/sct/overseerr/issues/860)) ([53f6f59](https://github.com/sct/overseerr/commit/53f6f59798fa7e3f95959990a3df555db3c1c51e)) -* **ui:** Add 'Available' filter to request list and remove unused MediaRequestStatus.AVAILABLE enum value ([#905](https://github.com/sct/overseerr/issues/905)) ([9757e3a](https://github.com/sct/overseerr/commit/9757e3ae0c572fb46177e25154b29e0ceced665f)) -* **ui:** Add 'Page Size' setting for request/user list pages ([#957](https://github.com/sct/overseerr/issues/957)) ([621db89](https://github.com/sct/overseerr/commit/621db893281f0280fe773ac7dbdc44434895242c)) -* **ui:** Add separate permissions for 4K auto approval ([#908](https://github.com/sct/overseerr/issues/908)) ([53b7425](https://github.com/sct/overseerr/commit/53b7425f6711e250935e7bb024c38ff6c62e07d9)) -* **ui:** Add sort options to user list ([#913](https://github.com/sct/overseerr/issues/913)) ([ef5d019](https://github.com/sct/overseerr/commit/ef5d019c18d7f6cdbbb1e1b7f8ff7816ed9b117b)) -* **ui:** Add support for requesting collections in 4K ([#968](https://github.com/sct/overseerr/issues/968)) ([139341b](https://github.com/sct/overseerr/commit/139341b0434b41e7c31af36baacd8d65566a6a0c)) -* user profile/settings pages ([#958](https://github.com/sct/overseerr/issues/958)) ([bbb683e](https://github.com/sct/overseerr/commit/bbb683e637386ad8bbeb44dca97aac9cdaf11349)) -* **ui:** added content ratings for tv shows and movie ratings ([#878](https://github.com/sct/overseerr/issues/878)) ([c8b2a57](https://github.com/sct/overseerr/commit/c8b2a57721a51adcc7f90ec1acb48b127991d467)) -* **users:** add reset password flow ([#772](https://github.com/sct/overseerr/issues/772)) ([e5966bd](https://github.com/sct/overseerr/commit/e5966bd3fbfe172f264f4e986ad2aecf29ae1510)) +- **lang:** translated using Weblate (French) ([#1007](https://github.com/sct/overseerr/issues/1007)) ([970da66](https://github.com/sct/overseerr/commit/970da664b2700b8cd9ad8dce0cbca1d37820eceb)) +- **lang:** translations update from Weblate ([#853](https://github.com/sct/overseerr/issues/853)) ([e156acc](https://github.com/sct/overseerr/commit/e156acc1ae2fa86b4441faacc0b58e1e993e0edc)) +- **lang:** translations update from Weblate ([#986](https://github.com/sct/overseerr/issues/986)) ([4296765](https://github.com/sct/overseerr/commit/4296765ad61bac09c2317b71b763366d328733e4)) +- **notif:** Add Pushbullet notification agent ([#950](https://github.com/sct/overseerr/issues/950)) ([29b97ef](https://github.com/sct/overseerr/commit/29b97ef6d85bbea31dd59b7ad857b0d8ab30bff0)) +- **notif:** Notification improvements ([#914](https://github.com/sct/overseerr/issues/914)) ([2768155](https://github.com/sct/overseerr/commit/2768155bbabe121a4c51fc1472461cd5114c4300)) +- **regions:** add region/original language setting for filtering Discover ([#732](https://github.com/sct/overseerr/issues/732)) ([#942](https://github.com/sct/overseerr/issues/942)) ([b557c06](https://github.com/sct/overseerr/commit/b557c06b0a78f5df5f64a05dc1e4511dae72df4f)) +- **requests:** add language profile support ([#860](https://github.com/sct/overseerr/issues/860)) ([53f6f59](https://github.com/sct/overseerr/commit/53f6f59798fa7e3f95959990a3df555db3c1c51e)) +- **ui:** Add 'Available' filter to request list and remove unused MediaRequestStatus.AVAILABLE enum value ([#905](https://github.com/sct/overseerr/issues/905)) ([9757e3a](https://github.com/sct/overseerr/commit/9757e3ae0c572fb46177e25154b29e0ceced665f)) +- **ui:** Add 'Page Size' setting for request/user list pages ([#957](https://github.com/sct/overseerr/issues/957)) ([621db89](https://github.com/sct/overseerr/commit/621db893281f0280fe773ac7dbdc44434895242c)) +- **ui:** Add separate permissions for 4K auto approval ([#908](https://github.com/sct/overseerr/issues/908)) ([53b7425](https://github.com/sct/overseerr/commit/53b7425f6711e250935e7bb024c38ff6c62e07d9)) +- **ui:** Add sort options to user list ([#913](https://github.com/sct/overseerr/issues/913)) ([ef5d019](https://github.com/sct/overseerr/commit/ef5d019c18d7f6cdbbb1e1b7f8ff7816ed9b117b)) +- **ui:** Add support for requesting collections in 4K ([#968](https://github.com/sct/overseerr/issues/968)) ([139341b](https://github.com/sct/overseerr/commit/139341b0434b41e7c31af36baacd8d65566a6a0c)) +- user profile/settings pages ([#958](https://github.com/sct/overseerr/issues/958)) ([bbb683e](https://github.com/sct/overseerr/commit/bbb683e637386ad8bbeb44dca97aac9cdaf11349)) +- **ui:** added content ratings for tv shows and movie ratings ([#878](https://github.com/sct/overseerr/issues/878)) ([c8b2a57](https://github.com/sct/overseerr/commit/c8b2a57721a51adcc7f90ec1acb48b127991d467)) +- **users:** add reset password flow ([#772](https://github.com/sct/overseerr/issues/772)) ([e5966bd](https://github.com/sct/overseerr/commit/e5966bd3fbfe172f264f4e986ad2aecf29ae1510)) ## [1.19.1](https://github.com/sct/overseerr/compare/v1.19.0...v1.19.1) (2021-02-06) - ### Bug Fixes -* **ui:** Fix webhook URL validation regex ([#864](https://github.com/sct/overseerr/issues/864)) ([726f62b](https://github.com/sct/overseerr/commit/726f62b9b69b5078e718f129e26abdf358f5cb06)) +- **ui:** Fix webhook URL validation regex ([#864](https://github.com/sct/overseerr/issues/864)) ([726f62b](https://github.com/sct/overseerr/commit/726f62b9b69b5078e718f129e26abdf358f5cb06)) # [1.19.0](https://github.com/sct/overseerr/compare/v1.18.0...v1.19.0) (2021-02-05) - ### Bug Fixes -* **api:** filter out adult content from combined credits ([3052f12](https://github.com/sct/overseerr/commit/3052f12c91b3ce86128324e3698fff61bbce3f2a)) -* **cache:** use formatted numbers for displaying cache counts ([6c437c5](https://github.com/sct/overseerr/commit/6c437c515fc01b9fe4461968875e23542bae7542)) -* **email:** make image a link to the action url in request template ([ee0a7bd](https://github.com/sct/overseerr/commit/ee0a7bd8c0b3a79c292b0abceb2f780f3889e49f)), closes [#834](https://github.com/sct/overseerr/issues/834) -* **frontend:** add github sponsor link to about page ([7c192d5](https://github.com/sct/overseerr/commit/7c192d54f422a5f2b55750535d2382e313f1d011)) -* **frontend:** correctly show 4k download tracker activity ([a7314f8](https://github.com/sct/overseerr/commit/a7314f876ea528fdec0fb0a2adaa36a01afcdf38)) -* **frontend:** fix possible division by zero in download status ([#839](https://github.com/sct/overseerr/issues/839)) ([c97c96a](https://github.com/sct/overseerr/commit/c97c96a30c50db7735f06c6d2d2f6193fb7da55e)) -* **frontend:** match request button color on titlecards to other request buttons ([5b39911](https://github.com/sct/overseerr/commit/5b39911e024513fab7a62948e653cee08fd166c7)) -* **frontend:** set 4k status on RequestItem when request is for 4k ([a3b00c3](https://github.com/sct/overseerr/commit/a3b00c3458b868506d4158fb24f0369fa5daefc5)) -* **frontend:** use consistent spinner style on TitleCard/Plex Presets ([cf7ebc4](https://github.com/sct/overseerr/commit/cf7ebc488db33725444c428b4244d780ab9d123b)) -* **html:** th elements should be nested under tr, not directly under thead ([#801](https://github.com/sct/overseerr/issues/801)) ([6e9ac27](https://github.com/sct/overseerr/commit/6e9ac275e19d56de8c7a366db970c7321f26fc8a)) -* **lang:** Add missing source strings & remove local user sign-in setting tip ([#828](https://github.com/sct/overseerr/issues/828)) ([c0769d4](https://github.com/sct/overseerr/commit/c0769d4f8f2bad88e4638d8c3cbcc0414b3ef6fb)) -* **lang:** Edit English language strings ([#820](https://github.com/sct/overseerr/issues/820)) ([f54df21](https://github.com/sct/overseerr/commit/f54df214af86d90ea8d7cfcd4e39022215c3568c)) -* **lang:** translate language names & change zh-Hant language code to zh-TW ([#793](https://github.com/sct/overseerr/issues/793)) ([3c5ae36](https://github.com/sct/overseerr/commit/3c5ae360fd179d794a78cc918fe97a09216ca6b2)) -* **notif/ui:** Use custom application title in notifications & sign-in page ([#849](https://github.com/sct/overseerr/issues/849)) ([38c76b5](https://github.com/sct/overseerr/commit/38c76b55e0039c489cb6a4a0a298aa6385406db4)) -* **radarr:** correctly set requested status after sending to radarr (with auto approve) ([ec44841](https://github.com/sct/overseerr/commit/ec448413569ddc2f24bb856d29084169979f9f05)) -* **sonarr-sync:** sonarr sync will no longer set shows with no episodes to partially available ([d20bd53](https://github.com/sct/overseerr/commit/d20bd530edaadc5887b0361358da80153e36505c)), closes [#796](https://github.com/sct/overseerr/issues/796) -* **ui:** Add additional URL & email input validation ([#843](https://github.com/sct/overseerr/issues/843)) ([3f9bfeb](https://github.com/sct/overseerr/commit/3f9bfeb01a67b2b587c7548b02ee826722e65c0f)) -* **ui:** Don't display empty dropdown when no trailer available ([#804](https://github.com/sct/overseerr/issues/804)) ([95c2a21](https://github.com/sct/overseerr/commit/95c2a2169799d96413b47ab24506b330435643eb)) -* **ui:** dont show bulk edit options on user list if there is only one user ([b658ddf](https://github.com/sct/overseerr/commit/b658ddf5cf61b2bb9b93cb1a4ca716cd75e18bb4)) -* **ui:** Dynamically generate path to config in warning message ([#851](https://github.com/sct/overseerr/issues/851)) ([b531a64](https://github.com/sct/overseerr/commit/b531a642f601f4ef9bf39c2f5915402157e55372)) -* **ui:** fix tables extending outside viewport in mobile formats ([e270999](https://github.com/sct/overseerr/commit/e270999745f97c2860f6a5b84e897dc6da8d6001)) -* **ui:** Hide 'Mark 4k as Available' button if 4k not enabled ([#833](https://github.com/sct/overseerr/issues/833)) ([e4a50c3](https://github.com/sct/overseerr/commit/e4a50c33f105b440243885d72a9e96595a525447)) -* **ui:** Limit max width of forms & lists ([#845](https://github.com/sct/overseerr/issues/845)) ([b9d14a9](https://github.com/sct/overseerr/commit/b9d14a9fd0f3c94d8267755147a87fe3b77fa2c3)) -* **ui:** prevent names from getting squished in AdvancedRequester user selector ([06e9411](https://github.com/sct/overseerr/commit/06e941171a1d019fbb178624167c026f6df5271c)) -* **ui:** remove yup validation from display name on user edit page ([63d7e2b](https://github.com/sct/overseerr/commit/63d7e2b39858fcb1cc0819a680eebccded7f4451)) -* **ui:** Restore original port input size ([#814](https://github.com/sct/overseerr/issues/814)) ([1ccafc0](https://github.com/sct/overseerr/commit/1ccafc0ebd368d798f9571b83910336efa317e37)) -* **ui:** show request as option even if there are no radarr/sonarr servers ([b116281](https://github.com/sct/overseerr/commit/b116281196c264b4ec35b07f1b4ffa717e50ade5)) -* **ui:** uniform-size checkboxes, vertically-aligned form labels, and fixes for other UI imperfections/inconsistencies ([#737](https://github.com/sct/overseerr/issues/737)) ([e34fbf7](https://github.com/sct/overseerr/commit/e34fbf72fda34d69b9f25563fa81f88b3c20912a)) -* **ui:** Use minimum char validation message ([#850](https://github.com/sct/overseerr/issues/850)) ([7456bea](https://github.com/sct/overseerr/commit/7456bea2ae600a28cb933278ffb310b63a474d6a)) -* **ui:** validate application url and service external urls ([026795d](https://github.com/sct/overseerr/commit/026795d4c940cb4797d3e68089456a4c3defbb21)) -* **ui:** when PersonCard has no profilePath, correctly position name/role content ([3ffd5ab](https://github.com/sct/overseerr/commit/3ffd5ab0ee8ffa63199d1428e37206f9b59fb7a5)) - +- **api:** filter out adult content from combined credits ([3052f12](https://github.com/sct/overseerr/commit/3052f12c91b3ce86128324e3698fff61bbce3f2a)) +- **cache:** use formatted numbers for displaying cache counts ([6c437c5](https://github.com/sct/overseerr/commit/6c437c515fc01b9fe4461968875e23542bae7542)) +- **email:** make image a link to the action url in request template ([ee0a7bd](https://github.com/sct/overseerr/commit/ee0a7bd8c0b3a79c292b0abceb2f780f3889e49f)), closes [#834](https://github.com/sct/overseerr/issues/834) +- **frontend:** add github sponsor link to about page ([7c192d5](https://github.com/sct/overseerr/commit/7c192d54f422a5f2b55750535d2382e313f1d011)) +- **frontend:** correctly show 4k download tracker activity ([a7314f8](https://github.com/sct/overseerr/commit/a7314f876ea528fdec0fb0a2adaa36a01afcdf38)) +- **frontend:** fix possible division by zero in download status ([#839](https://github.com/sct/overseerr/issues/839)) ([c97c96a](https://github.com/sct/overseerr/commit/c97c96a30c50db7735f06c6d2d2f6193fb7da55e)) +- **frontend:** match request button color on titlecards to other request buttons ([5b39911](https://github.com/sct/overseerr/commit/5b39911e024513fab7a62948e653cee08fd166c7)) +- **frontend:** set 4k status on RequestItem when request is for 4k ([a3b00c3](https://github.com/sct/overseerr/commit/a3b00c3458b868506d4158fb24f0369fa5daefc5)) +- **frontend:** use consistent spinner style on TitleCard/Plex Presets ([cf7ebc4](https://github.com/sct/overseerr/commit/cf7ebc488db33725444c428b4244d780ab9d123b)) +- **html:** th elements should be nested under tr, not directly under thead ([#801](https://github.com/sct/overseerr/issues/801)) ([6e9ac27](https://github.com/sct/overseerr/commit/6e9ac275e19d56de8c7a366db970c7321f26fc8a)) +- **lang:** Add missing source strings & remove local user sign-in setting tip ([#828](https://github.com/sct/overseerr/issues/828)) ([c0769d4](https://github.com/sct/overseerr/commit/c0769d4f8f2bad88e4638d8c3cbcc0414b3ef6fb)) +- **lang:** Edit English language strings ([#820](https://github.com/sct/overseerr/issues/820)) ([f54df21](https://github.com/sct/overseerr/commit/f54df214af86d90ea8d7cfcd4e39022215c3568c)) +- **lang:** translate language names & change zh-Hant language code to zh-TW ([#793](https://github.com/sct/overseerr/issues/793)) ([3c5ae36](https://github.com/sct/overseerr/commit/3c5ae360fd179d794a78cc918fe97a09216ca6b2)) +- **notif/ui:** Use custom application title in notifications & sign-in page ([#849](https://github.com/sct/overseerr/issues/849)) ([38c76b5](https://github.com/sct/overseerr/commit/38c76b55e0039c489cb6a4a0a298aa6385406db4)) +- **radarr:** correctly set requested status after sending to radarr (with auto approve) ([ec44841](https://github.com/sct/overseerr/commit/ec448413569ddc2f24bb856d29084169979f9f05)) +- **sonarr-sync:** sonarr sync will no longer set shows with no episodes to partially available ([d20bd53](https://github.com/sct/overseerr/commit/d20bd530edaadc5887b0361358da80153e36505c)), closes [#796](https://github.com/sct/overseerr/issues/796) +- **ui:** Add additional URL & email input validation ([#843](https://github.com/sct/overseerr/issues/843)) ([3f9bfeb](https://github.com/sct/overseerr/commit/3f9bfeb01a67b2b587c7548b02ee826722e65c0f)) +- **ui:** Don't display empty dropdown when no trailer available ([#804](https://github.com/sct/overseerr/issues/804)) ([95c2a21](https://github.com/sct/overseerr/commit/95c2a2169799d96413b47ab24506b330435643eb)) +- **ui:** dont show bulk edit options on user list if there is only one user ([b658ddf](https://github.com/sct/overseerr/commit/b658ddf5cf61b2bb9b93cb1a4ca716cd75e18bb4)) +- **ui:** Dynamically generate path to config in warning message ([#851](https://github.com/sct/overseerr/issues/851)) ([b531a64](https://github.com/sct/overseerr/commit/b531a642f601f4ef9bf39c2f5915402157e55372)) +- **ui:** fix tables extending outside viewport in mobile formats ([e270999](https://github.com/sct/overseerr/commit/e270999745f97c2860f6a5b84e897dc6da8d6001)) +- **ui:** Hide 'Mark 4k as Available' button if 4k not enabled ([#833](https://github.com/sct/overseerr/issues/833)) ([e4a50c3](https://github.com/sct/overseerr/commit/e4a50c33f105b440243885d72a9e96595a525447)) +- **ui:** Limit max width of forms & lists ([#845](https://github.com/sct/overseerr/issues/845)) ([b9d14a9](https://github.com/sct/overseerr/commit/b9d14a9fd0f3c94d8267755147a87fe3b77fa2c3)) +- **ui:** prevent names from getting squished in AdvancedRequester user selector ([06e9411](https://github.com/sct/overseerr/commit/06e941171a1d019fbb178624167c026f6df5271c)) +- **ui:** remove yup validation from display name on user edit page ([63d7e2b](https://github.com/sct/overseerr/commit/63d7e2b39858fcb1cc0819a680eebccded7f4451)) +- **ui:** Restore original port input size ([#814](https://github.com/sct/overseerr/issues/814)) ([1ccafc0](https://github.com/sct/overseerr/commit/1ccafc0ebd368d798f9571b83910336efa317e37)) +- **ui:** show request as option even if there are no radarr/sonarr servers ([b116281](https://github.com/sct/overseerr/commit/b116281196c264b4ec35b07f1b4ffa717e50ade5)) +- **ui:** uniform-size checkboxes, vertically-aligned form labels, and fixes for other UI imperfections/inconsistencies ([#737](https://github.com/sct/overseerr/issues/737)) ([e34fbf7](https://github.com/sct/overseerr/commit/e34fbf72fda34d69b9f25563fa81f88b3c20912a)) +- **ui:** Use minimum char validation message ([#850](https://github.com/sct/overseerr/issues/850)) ([7456bea](https://github.com/sct/overseerr/commit/7456bea2ae600a28cb933278ffb310b63a474d6a)) +- **ui:** validate application url and service external urls ([026795d](https://github.com/sct/overseerr/commit/026795d4c940cb4797d3e68089456a4c3defbb21)) +- **ui:** when PersonCard has no profilePath, correctly position name/role content ([3ffd5ab](https://github.com/sct/overseerr/commit/3ffd5ab0ee8ffa63199d1428e37206f9b59fb7a5)) ### Features -* **cache:** add cache table and flush cache option to settings ([996bd9f](https://github.com/sct/overseerr/commit/996bd9f14ed0f56767892c169b071be4f0f628d0)) -* **cache:** external API cache ([#786](https://github.com/sct/overseerr/issues/786)) ([20289b5](https://github.com/sct/overseerr/commit/20289b5960a93545cdff9331a1a7b613f382e702)) -* **docker:** Check for /app/config volume mount during setup ([#826](https://github.com/sct/overseerr/issues/826)) ([1e5f88f](https://github.com/sct/overseerr/commit/1e5f88f462b0c69db5f6ab8e0249a5905bc6952a)) -* **frontend:** add TheTVDB external link ([#800](https://github.com/sct/overseerr/issues/800)) ([72cffd7](https://github.com/sct/overseerr/commit/72cffd74a75984ba98c456c0ec006ec378a8dcec)) -* **lang:** add support for Hungarian language ([cfacb15](https://github.com/sct/overseerr/commit/cfacb151b52d08e19d2fcd603fb4bbcd78707cdf)) -* **lang:** translations update from Weblate ([#791](https://github.com/sct/overseerr/issues/791)) ([42295e0](https://github.com/sct/overseerr/commit/42295e076a7579b226d57407a20cb0ba044e9ec1)) -* **lang:** translations update from Weblate ([#819](https://github.com/sct/overseerr/issues/819)) ([9e5e4c2](https://github.com/sct/overseerr/commit/9e5e4c22f5b25df96f47875d599ed8685791382a)) -* **lang:** translations update from Weblate ([#841](https://github.com/sct/overseerr/issues/841)) ([e4f9b8a](https://github.com/sct/overseerr/commit/e4f9b8a9848f3af00e86fc7108c823ed0584609f)) -* **lang:** translations update from Weblate ([#852](https://github.com/sct/overseerr/issues/852)) ([c5be00e](https://github.com/sct/overseerr/commit/c5be00eebfd2b0e65295edbe282cbba22fffa660)) -* **ui:** Add local login setting ([#817](https://github.com/sct/overseerr/issues/817)) ([9d0d5b8](https://github.com/sct/overseerr/commit/9d0d5b86aae025e4647bb664c6412d42192e2fe7)) -* **ui:** added next airing date to TV Shows ([#842](https://github.com/sct/overseerr/issues/842)) ([4eae02a](https://github.com/sct/overseerr/commit/4eae02a7e14e377fd69ddd4a43774cb7e3d1855b)) -* new permission to allow users to see other users requests ([033ba9d](https://github.com/sct/overseerr/commit/033ba9d41bddf6dc1c4512d8404f747e57923bca)), closes [#840](https://github.com/sct/overseerr/issues/840) -* request as another user ([59150f9](https://github.com/sct/overseerr/commit/59150f955f7003672ef19eb9d37156e93b79c97d)) -* **tv:** show cast for the entire show instead of only the last season ([#778](https://github.com/sct/overseerr/issues/778)) ([b239598](https://github.com/sct/overseerr/commit/b239598e64d33b78dc5d7972878840149aff360a)), closes [#775](https://github.com/sct/overseerr/issues/775) -* **ui:** Add custom title functionality ([#825](https://github.com/sct/overseerr/issues/825)) ([35c6bfc](https://github.com/sct/overseerr/commit/35c6bfc0216bf879353b3ee546b439a06c8e6121)) +- **cache:** add cache table and flush cache option to settings ([996bd9f](https://github.com/sct/overseerr/commit/996bd9f14ed0f56767892c169b071be4f0f628d0)) +- **cache:** external API cache ([#786](https://github.com/sct/overseerr/issues/786)) ([20289b5](https://github.com/sct/overseerr/commit/20289b5960a93545cdff9331a1a7b613f382e702)) +- **docker:** Check for /app/config volume mount during setup ([#826](https://github.com/sct/overseerr/issues/826)) ([1e5f88f](https://github.com/sct/overseerr/commit/1e5f88f462b0c69db5f6ab8e0249a5905bc6952a)) +- **frontend:** add TheTVDB external link ([#800](https://github.com/sct/overseerr/issues/800)) ([72cffd7](https://github.com/sct/overseerr/commit/72cffd74a75984ba98c456c0ec006ec378a8dcec)) +- **lang:** add support for Hungarian language ([cfacb15](https://github.com/sct/overseerr/commit/cfacb151b52d08e19d2fcd603fb4bbcd78707cdf)) +- **lang:** translations update from Weblate ([#791](https://github.com/sct/overseerr/issues/791)) ([42295e0](https://github.com/sct/overseerr/commit/42295e076a7579b226d57407a20cb0ba044e9ec1)) +- **lang:** translations update from Weblate ([#819](https://github.com/sct/overseerr/issues/819)) ([9e5e4c2](https://github.com/sct/overseerr/commit/9e5e4c22f5b25df96f47875d599ed8685791382a)) +- **lang:** translations update from Weblate ([#841](https://github.com/sct/overseerr/issues/841)) ([e4f9b8a](https://github.com/sct/overseerr/commit/e4f9b8a9848f3af00e86fc7108c823ed0584609f)) +- **lang:** translations update from Weblate ([#852](https://github.com/sct/overseerr/issues/852)) ([c5be00e](https://github.com/sct/overseerr/commit/c5be00eebfd2b0e65295edbe282cbba22fffa660)) +- **ui:** Add local login setting ([#817](https://github.com/sct/overseerr/issues/817)) ([9d0d5b8](https://github.com/sct/overseerr/commit/9d0d5b86aae025e4647bb664c6412d42192e2fe7)) +- **ui:** added next airing date to TV Shows ([#842](https://github.com/sct/overseerr/issues/842)) ([4eae02a](https://github.com/sct/overseerr/commit/4eae02a7e14e377fd69ddd4a43774cb7e3d1855b)) +- new permission to allow users to see other users requests ([033ba9d](https://github.com/sct/overseerr/commit/033ba9d41bddf6dc1c4512d8404f747e57923bca)), closes [#840](https://github.com/sct/overseerr/issues/840) +- request as another user ([59150f9](https://github.com/sct/overseerr/commit/59150f955f7003672ef19eb9d37156e93b79c97d)) +- **tv:** show cast for the entire show instead of only the last season ([#778](https://github.com/sct/overseerr/issues/778)) ([b239598](https://github.com/sct/overseerr/commit/b239598e64d33b78dc5d7972878840149aff360a)), closes [#775](https://github.com/sct/overseerr/issues/775) +- **ui:** Add custom title functionality ([#825](https://github.com/sct/overseerr/issues/825)) ([35c6bfc](https://github.com/sct/overseerr/commit/35c6bfc0216bf879353b3ee546b439a06c8e6121)) # [1.18.0](https://github.com/sct/overseerr/compare/v1.17.2...v1.18.0) (2021-01-30) - ### Bug Fixes -* **api:** prevent duplicate movie requests ([421f4c1](https://github.com/sct/overseerr/commit/421f4c17f0f206bbe7bfcbf2819014b8c7f55b6a)), closes [#705](https://github.com/sct/overseerr/issues/705) -* **build:** fix sqlite3 build error ([#691](https://github.com/sct/overseerr/issues/691)) ([3a1f6d5](https://github.com/sct/overseerr/commit/3a1f6d5706c8fc100e88425f3d89a26a0325af79)) -* **frontend:** add poster not found image to request card and request list item ([ae9a1b3](https://github.com/sct/overseerr/commit/ae9a1b3e940ac2abf6e842d91f458daab3dd0f0d)) -* **frontend:** add poster not found image to tv details page ([0b05545](https://github.com/sct/overseerr/commit/0b055458d0ddbfd4c87ebf9b0562f161fa3445a3)) -* **frontend:** dont show external links unless slug is set ([946bd2d](https://github.com/sct/overseerr/commit/946bd2db5ecde0748b2e9bc5edfe7ca6000ec3d5)) -* **frontend:** fix server name position on plex settings page ([86efcd8](https://github.com/sct/overseerr/commit/86efcd82c34ad6490f2899ebf6f84cdd1bffc498)) -* **frontend:** fixed mismatched rounded sizing on new login ([5e352c2](https://github.com/sct/overseerr/commit/5e352c201fc2f731ca5f713ecb6901527ef354da)), closes [#721](https://github.com/sct/overseerr/issues/721) -* **ip logging:** add env var for proxy to fix ip logging on failed logins ([#756](https://github.com/sct/overseerr/issues/756)) ([9342a40](https://github.com/sct/overseerr/commit/9342a40bbc03f7fdda23e3876b3a4a81ea8532c0)) -* **lang:** add missing i18n strings for notification settings ([2f75c4c](https://github.com/sct/overseerr/commit/2f75c4c6aed42a15bb47d3652272de8f852ec79f)) -* **notifications:** only send a single notification when standard media becomes available ([b5fd1d5](https://github.com/sct/overseerr/commit/b5fd1d520cd2a7be6e6356a25129e93af1caf542)), closes [#770](https://github.com/sct/overseerr/issues/770) -* **permissions:** use default user permissions when creating a local user ([#713](https://github.com/sct/overseerr/issues/713)) ([660ada0](https://github.com/sct/overseerr/commit/660ada0b2025eb2c06d9054fd0a7b5a632af6af2)) -* **radarr:** fix request bug which made it unable to be added to radarr ([#760](https://github.com/sct/overseerr/issues/760)) ([45a2779](https://github.com/sct/overseerr/commit/45a277964b0c39346d7216873812e0ebe505cb79)) -* **radarr:** return the updated data when updating radarr request ([#765](https://github.com/sct/overseerr/issues/765)) ([0c6d478](https://github.com/sct/overseerr/commit/0c6d4780c355ffe1a951268fb6949491d435bbf1)) -* **requests:** handle when tvdbid is null ([#657](https://github.com/sct/overseerr/issues/657)) ([2da0da8](https://github.com/sct/overseerr/commit/2da0da826ae1d73467bc8a671fda7cc5ca1f14c9)) -* **sonarr-sync:** correctly set series with no seasons to requested status ([3812989](https://github.com/sct/overseerr/commit/3812989a1ce1e07d4af09149008043a6e2e94060)), closes [#762](https://github.com/sct/overseerr/issues/762) -* **sync:** do not update series status if already available and no new seasons ([136d874](https://github.com/sct/overseerr/commit/136d874cba37babf9c0670844b002871710e6d99)), closes [#777](https://github.com/sct/overseerr/issues/777) -* **ui:** Capitalization, punctuation, and grammar inconsistences & errors ([#731](https://github.com/sct/overseerr/issues/731)) ([f05d4a0](https://github.com/sct/overseerr/commit/f05d4a0d0b42905fcaee49b2471bb1f4ee77fffe)) -* lookup movie by imdbid if tmdbid does not exits for plex movie agent ([#711](https://github.com/sct/overseerr/issues/711)) ([e972288](https://github.com/sct/overseerr/commit/e97228899a5936b2525c8060abfa14b5ce31658d)) -* show recently added series even if they are not complete ([d0c830e](https://github.com/sct/overseerr/commit/d0c830e80d389f9e0f48a9b83659331f54630d03)) - +- **api:** prevent duplicate movie requests ([421f4c1](https://github.com/sct/overseerr/commit/421f4c17f0f206bbe7bfcbf2819014b8c7f55b6a)), closes [#705](https://github.com/sct/overseerr/issues/705) +- **build:** fix sqlite3 build error ([#691](https://github.com/sct/overseerr/issues/691)) ([3a1f6d5](https://github.com/sct/overseerr/commit/3a1f6d5706c8fc100e88425f3d89a26a0325af79)) +- **frontend:** add poster not found image to request card and request list item ([ae9a1b3](https://github.com/sct/overseerr/commit/ae9a1b3e940ac2abf6e842d91f458daab3dd0f0d)) +- **frontend:** add poster not found image to tv details page ([0b05545](https://github.com/sct/overseerr/commit/0b055458d0ddbfd4c87ebf9b0562f161fa3445a3)) +- **frontend:** dont show external links unless slug is set ([946bd2d](https://github.com/sct/overseerr/commit/946bd2db5ecde0748b2e9bc5edfe7ca6000ec3d5)) +- **frontend:** fix server name position on plex settings page ([86efcd8](https://github.com/sct/overseerr/commit/86efcd82c34ad6490f2899ebf6f84cdd1bffc498)) +- **frontend:** fixed mismatched rounded sizing on new login ([5e352c2](https://github.com/sct/overseerr/commit/5e352c201fc2f731ca5f713ecb6901527ef354da)), closes [#721](https://github.com/sct/overseerr/issues/721) +- **ip logging:** add env var for proxy to fix ip logging on failed logins ([#756](https://github.com/sct/overseerr/issues/756)) ([9342a40](https://github.com/sct/overseerr/commit/9342a40bbc03f7fdda23e3876b3a4a81ea8532c0)) +- **lang:** add missing i18n strings for notification settings ([2f75c4c](https://github.com/sct/overseerr/commit/2f75c4c6aed42a15bb47d3652272de8f852ec79f)) +- **notifications:** only send a single notification when standard media becomes available ([b5fd1d5](https://github.com/sct/overseerr/commit/b5fd1d520cd2a7be6e6356a25129e93af1caf542)), closes [#770](https://github.com/sct/overseerr/issues/770) +- **permissions:** use default user permissions when creating a local user ([#713](https://github.com/sct/overseerr/issues/713)) ([660ada0](https://github.com/sct/overseerr/commit/660ada0b2025eb2c06d9054fd0a7b5a632af6af2)) +- **radarr:** fix request bug which made it unable to be added to radarr ([#760](https://github.com/sct/overseerr/issues/760)) ([45a2779](https://github.com/sct/overseerr/commit/45a277964b0c39346d7216873812e0ebe505cb79)) +- **radarr:** return the updated data when updating radarr request ([#765](https://github.com/sct/overseerr/issues/765)) ([0c6d478](https://github.com/sct/overseerr/commit/0c6d4780c355ffe1a951268fb6949491d435bbf1)) +- **requests:** handle when tvdbid is null ([#657](https://github.com/sct/overseerr/issues/657)) ([2da0da8](https://github.com/sct/overseerr/commit/2da0da826ae1d73467bc8a671fda7cc5ca1f14c9)) +- **sonarr-sync:** correctly set series with no seasons to requested status ([3812989](https://github.com/sct/overseerr/commit/3812989a1ce1e07d4af09149008043a6e2e94060)), closes [#762](https://github.com/sct/overseerr/issues/762) +- **sync:** do not update series status if already available and no new seasons ([136d874](https://github.com/sct/overseerr/commit/136d874cba37babf9c0670844b002871710e6d99)), closes [#777](https://github.com/sct/overseerr/issues/777) +- **ui:** Capitalization, punctuation, and grammar inconsistences & errors ([#731](https://github.com/sct/overseerr/issues/731)) ([f05d4a0](https://github.com/sct/overseerr/commit/f05d4a0d0b42905fcaee49b2471bb1f4ee77fffe)) +- lookup movie by imdbid if tmdbid does not exits for plex movie agent ([#711](https://github.com/sct/overseerr/issues/711)) ([e972288](https://github.com/sct/overseerr/commit/e97228899a5936b2525c8060abfa14b5ce31658d)) +- show recently added series even if they are not complete ([d0c830e](https://github.com/sct/overseerr/commit/d0c830e80d389f9e0f48a9b83659331f54630d03)) ### Features -* **lang:** translated using Weblate (Dutch) ([059995e](https://github.com/sct/overseerr/commit/059995e0ef3370a3192bd386fa6875ca0f58690a)) -* **lang:** translated using Weblate (French) ([4789583](https://github.com/sct/overseerr/commit/4789583d66305ac7b3d393659b2f3604c0acc576)) -* **lang:** translations update from Weblate ([#727](https://github.com/sct/overseerr/issues/727)) ([71875ef](https://github.com/sct/overseerr/commit/71875efb48246dbb0139ad15a4261a5661fcfe17)) -* **lang:** update languages and fix merge conflict ([083a74a](https://github.com/sct/overseerr/commit/083a74a686d202cce5775bf9752caaa9a626cf45)) -* **ui:** Move PROXY setting to UI ([#782](https://github.com/sct/overseerr/issues/782)) ([f1dd5e7](https://github.com/sct/overseerr/commit/f1dd5e7e12c1f602449c4769173dbce71e3569d0)) -* add manual availability buttons to manage slideover ([67f8aef](https://github.com/sct/overseerr/commit/67f8aef00d98c834b60cb6152ccd5cb7b5709d12)), closes [#672](https://github.com/sct/overseerr/issues/672) -* **media:** add link to the item on plex ([#735](https://github.com/sct/overseerr/issues/735)) ([1d7150c](https://github.com/sct/overseerr/commit/1d7150c24ec5ad347093889bfceab61b664900d5)) -* Radarr & Sonarr Sync ([#734](https://github.com/sct/overseerr/issues/734)) ([ec5fb83](https://github.com/sct/overseerr/commit/ec5fb836785855eb4846fd33b49faeb94c40506a)) -* **frontend:** add option to hide all available items from discovery ([#699](https://github.com/sct/overseerr/issues/699)) ([6c1742e](https://github.com/sct/overseerr/commit/6c1742e94ccfc6c13cf1d25fd9e893ee1f431aae)) -* **lang:** add support for Portuguese (Portugal) language ([e044146](https://github.com/sct/overseerr/commit/e044146aa55109a1eccfde9650b26beb0d5ec9a6)) -* **lang:** translated using Weblate (Dutch) ([6d0f7d4](https://github.com/sct/overseerr/commit/6d0f7d4b50370c420c1017f32d48313074543743)) -* **lang:** translated using Weblate (Italian) ([9aa5c12](https://github.com/sct/overseerr/commit/9aa5c121644518c1fbb308a487c26d8998bb5a36)) -* **lang:** translated using Weblate (Portuguese (Portugal)) ([f001fb3](https://github.com/sct/overseerr/commit/f001fb3b33d4fb749acb70c45b8a55a5bbef570c)) -* **lang:** translated using Weblate (Spanish) ([4f94d22](https://github.com/sct/overseerr/commit/4f94d227fc3096bcb8a1e5cf12fe9222d6c6b711)) -* **login:** add request ip to the failed request log ([#714](https://github.com/sct/overseerr/issues/714)) ([2d31ea9](https://github.com/sct/overseerr/commit/2d31ea940ac0a1a84d2150743798b41ff6490317)) -* **users:** add editable usernames ([#715](https://github.com/sct/overseerr/issues/715)) ([20ca3f2](https://github.com/sct/overseerr/commit/20ca3f2f5fcf4a9eb0d6a8be671bb4fb1f5e6178)) -* pre-populate server info from plex.tv API ([#563](https://github.com/sct/overseerr/issues/563)) ([82ac76b](https://github.com/sct/overseerr/commit/82ac76b0540ba1133cb5384744d2499c2488a4e8)) -* **auth:** Add optional CSRF protection ([#697](https://github.com/sct/overseerr/issues/697)) ([6e25891](https://github.com/sct/overseerr/commit/6e2589178b99f8f32f0ded9a7cfd9921c33e9b60)) -* ability to edit user settings in bulk ([#597](https://github.com/sct/overseerr/issues/597)) ([4b0241c](https://github.com/sct/overseerr/commit/4b0241c3b34d4229f928c21defb10a1c051264d1)) -* **lang:** translated using Weblate (English) ([9bb11af](https://github.com/sct/overseerr/commit/9bb11afc6b4a109ae1e14d41c9fe2b71f19c470a)) -* **lang:** translated using Weblate (German) ([c2a3e8e](https://github.com/sct/overseerr/commit/c2a3e8ed5243925dce991ec7995ae831702dbc7b)) -* **lang:** translated using Weblate (Portuguese (Brazil)) ([32f4916](https://github.com/sct/overseerr/commit/32f4916c4a926097f31ed472aee031536b847bb7)) -* **lang:** translated using Weblate (Portuguese (Brazil)) ([98570c9](https://github.com/sct/overseerr/commit/98570c920e4904a594bb7464161b985094958f84)) -* **notifications:** add option to send notifications for auto-approved requests ([21db367](https://github.com/sct/overseerr/commit/21db3676d1464b63384b04c0c2926cb2a6252e9b)), closes [#267](https://github.com/sct/overseerr/issues/267) +- **lang:** translated using Weblate (Dutch) ([059995e](https://github.com/sct/overseerr/commit/059995e0ef3370a3192bd386fa6875ca0f58690a)) +- **lang:** translated using Weblate (French) ([4789583](https://github.com/sct/overseerr/commit/4789583d66305ac7b3d393659b2f3604c0acc576)) +- **lang:** translations update from Weblate ([#727](https://github.com/sct/overseerr/issues/727)) ([71875ef](https://github.com/sct/overseerr/commit/71875efb48246dbb0139ad15a4261a5661fcfe17)) +- **lang:** update languages and fix merge conflict ([083a74a](https://github.com/sct/overseerr/commit/083a74a686d202cce5775bf9752caaa9a626cf45)) +- **ui:** Move PROXY setting to UI ([#782](https://github.com/sct/overseerr/issues/782)) ([f1dd5e7](https://github.com/sct/overseerr/commit/f1dd5e7e12c1f602449c4769173dbce71e3569d0)) +- add manual availability buttons to manage slideover ([67f8aef](https://github.com/sct/overseerr/commit/67f8aef00d98c834b60cb6152ccd5cb7b5709d12)), closes [#672](https://github.com/sct/overseerr/issues/672) +- **media:** add link to the item on plex ([#735](https://github.com/sct/overseerr/issues/735)) ([1d7150c](https://github.com/sct/overseerr/commit/1d7150c24ec5ad347093889bfceab61b664900d5)) +- Radarr & Sonarr Sync ([#734](https://github.com/sct/overseerr/issues/734)) ([ec5fb83](https://github.com/sct/overseerr/commit/ec5fb836785855eb4846fd33b49faeb94c40506a)) +- **frontend:** add option to hide all available items from discovery ([#699](https://github.com/sct/overseerr/issues/699)) ([6c1742e](https://github.com/sct/overseerr/commit/6c1742e94ccfc6c13cf1d25fd9e893ee1f431aae)) +- **lang:** add support for Portuguese (Portugal) language ([e044146](https://github.com/sct/overseerr/commit/e044146aa55109a1eccfde9650b26beb0d5ec9a6)) +- **lang:** translated using Weblate (Dutch) ([6d0f7d4](https://github.com/sct/overseerr/commit/6d0f7d4b50370c420c1017f32d48313074543743)) +- **lang:** translated using Weblate (Italian) ([9aa5c12](https://github.com/sct/overseerr/commit/9aa5c121644518c1fbb308a487c26d8998bb5a36)) +- **lang:** translated using Weblate (Portuguese (Portugal)) ([f001fb3](https://github.com/sct/overseerr/commit/f001fb3b33d4fb749acb70c45b8a55a5bbef570c)) +- **lang:** translated using Weblate (Spanish) ([4f94d22](https://github.com/sct/overseerr/commit/4f94d227fc3096bcb8a1e5cf12fe9222d6c6b711)) +- **login:** add request ip to the failed request log ([#714](https://github.com/sct/overseerr/issues/714)) ([2d31ea9](https://github.com/sct/overseerr/commit/2d31ea940ac0a1a84d2150743798b41ff6490317)) +- **users:** add editable usernames ([#715](https://github.com/sct/overseerr/issues/715)) ([20ca3f2](https://github.com/sct/overseerr/commit/20ca3f2f5fcf4a9eb0d6a8be671bb4fb1f5e6178)) +- pre-populate server info from plex.tv API ([#563](https://github.com/sct/overseerr/issues/563)) ([82ac76b](https://github.com/sct/overseerr/commit/82ac76b0540ba1133cb5384744d2499c2488a4e8)) +- **auth:** Add optional CSRF protection ([#697](https://github.com/sct/overseerr/issues/697)) ([6e25891](https://github.com/sct/overseerr/commit/6e2589178b99f8f32f0ded9a7cfd9921c33e9b60)) +- ability to edit user settings in bulk ([#597](https://github.com/sct/overseerr/issues/597)) ([4b0241c](https://github.com/sct/overseerr/commit/4b0241c3b34d4229f928c21defb10a1c051264d1)) +- **lang:** translated using Weblate (English) ([9bb11af](https://github.com/sct/overseerr/commit/9bb11afc6b4a109ae1e14d41c9fe2b71f19c470a)) +- **lang:** translated using Weblate (German) ([c2a3e8e](https://github.com/sct/overseerr/commit/c2a3e8ed5243925dce991ec7995ae831702dbc7b)) +- **lang:** translated using Weblate (Portuguese (Brazil)) ([32f4916](https://github.com/sct/overseerr/commit/32f4916c4a926097f31ed472aee031536b847bb7)) +- **lang:** translated using Weblate (Portuguese (Brazil)) ([98570c9](https://github.com/sct/overseerr/commit/98570c920e4904a594bb7464161b985094958f84)) +- **notifications:** add option to send notifications for auto-approved requests ([21db367](https://github.com/sct/overseerr/commit/21db3676d1464b63384b04c0c2926cb2a6252e9b)), closes [#267](https://github.com/sct/overseerr/issues/267) ## [1.17.2](https://github.com/sct/overseerr/compare/v1.17.1...v1.17.2) (2021-01-20) - ### Bug Fixes -* **requests:** allow declined season requests to be re-requested ([e1032ff](https://github.com/sct/overseerr/commit/e1032ff5dfac4a8c9d4da9cf2788c19822343ad9)), closes [#690](https://github.com/sct/overseerr/issues/690) -* **requests:** update requests to approved when parent media is set as available ([78444a9](https://github.com/sct/overseerr/commit/78444a9e643829823162389dee60cca70da56bff)), closes [#688](https://github.com/sct/overseerr/issues/688) +- **requests:** allow declined season requests to be re-requested ([e1032ff](https://github.com/sct/overseerr/commit/e1032ff5dfac4a8c9d4da9cf2788c19822343ad9)), closes [#690](https://github.com/sct/overseerr/issues/690) +- **requests:** update requests to approved when parent media is set as available ([78444a9](https://github.com/sct/overseerr/commit/78444a9e643829823162389dee60cca70da56bff)), closes [#688](https://github.com/sct/overseerr/issues/688) ## [1.17.1](https://github.com/sct/overseerr/compare/v1.17.0...v1.17.1) (2021-01-19) - ### Bug Fixes -* **frontend:** show auto approval on series request modal only with correct permissions ([8927c6d](https://github.com/sct/overseerr/commit/8927c6d2e39dbda2b1121095a7273f5cab1c9b74)), closes [#687](https://github.com/sct/overseerr/issues/687) +- **frontend:** show auto approval on series request modal only with correct permissions ([8927c6d](https://github.com/sct/overseerr/commit/8927c6d2e39dbda2b1121095a7273f5cab1c9b74)), closes [#687](https://github.com/sct/overseerr/issues/687) # [1.17.0](https://github.com/sct/overseerr/compare/v1.16.0...v1.17.0) (2021-01-19) - ### Bug Fixes -* **api:** improve rottentomatoes rating matching for movies ([7db62ab](https://github.com/sct/overseerr/commit/7db62ab824eefc42e6db16e42d52f4266b136f82)), closes [#494](https://github.com/sct/overseerr/issues/494) -* **build:** remove cross import from client to server for UserType ([23624bd](https://github.com/sct/overseerr/commit/23624bd144af5df4c31995b68ce48105b95b20f6)) -* **frontend:** clarify which fields are required in radarr/sonarr modals ([860d71e](https://github.com/sct/overseerr/commit/860d71ed69a69a1a3f74b79290ef471e04f57a6b)), closes [#575](https://github.com/sct/overseerr/issues/575) -* **frontend:** do not show failed media status on request list for declined requests ([00944b1](https://github.com/sct/overseerr/commit/00944b1ec2db8ddc5742448f6448f7364c473a98)), closes [#664](https://github.com/sct/overseerr/issues/664) -* **frontend:** fix button styling on details page on small screen sizes ([d9e0c90](https://github.com/sct/overseerr/commit/d9e0c90e76d80aef0c67318e00e997804805f46e)) -* **frontend:** fix request button height ([a262727](https://github.com/sct/overseerr/commit/a2627270784bdef8644875fa5c5a7349a0b7fd81)) -* **frontend:** request dropdown menu now properly shows up over collection button ([b491be1](https://github.com/sct/overseerr/commit/b491be1b1e7f6aa588274230e695e4c5302b961e)) -* **frontend:** show correct request status on request cards for 4k requests ([1aa0005](https://github.com/sct/overseerr/commit/1aa0005b4298fc1af9c1d0bf1f357738c0fa2673)) -* **lang:** add missing see more i18n string for SeeMoreCard ([d9919ab](https://github.com/sct/overseerr/commit/d9919abb8998d28558ddec35b8e60ab2af75d5b7)) -* **lang:** change email auth user/pass strings to SMTP Username/Password ([a77a2aa](https://github.com/sct/overseerr/commit/a77a2aa3ebb1be353d534db5b07647ac26c60e15)) -* **notifications:** correctly compare seasons before sending series notifications ([f17fa2a](https://github.com/sct/overseerr/commit/f17fa2a2db8144bac89936f588627e8dd37bf54a)) -* **notifications:** only send one available notification for standard media ([fc6f7cc](https://github.com/sct/overseerr/commit/fc6f7ccea586165a30022b6d5554911c66ece6df)) -* **notifications:** send media declined email ([eb6fc8a](https://github.com/sct/overseerr/commit/eb6fc8a19099469794d471db0b48a258c2866633)), closes [#679](https://github.com/sct/overseerr/issues/679) -* **plex-sync:** improve plex sync error handling. add session id to fix stuck runs ([a740b07](https://github.com/sct/overseerr/commit/a740b07f06f892b72a651b928af28ce71cb495ee)) -* **plex-sync:** store plex added date and sort recently added by it ([d688a96](https://github.com/sct/overseerr/commit/d688a967596afcba9799b8133089bebb5add27cf)) -* **requests:** select the correct radarr/sonarr server when sending request to service ([e0d9f89](https://github.com/sct/overseerr/commit/e0d9f891e797c3839f976b75a871903b6f2e55f1)) -* **server:** support absolute paths for CONFIG_DIRECTORY ([51d8fba](https://github.com/sct/overseerr/commit/51d8fba9162b9e148a35ced69e7e035438c8b0f1)) -* **user edit:** fix user edit not being able to be saved ([#651](https://github.com/sct/overseerr/issues/651)) ([b04d00e](https://github.com/sct/overseerr/commit/b04d00ef509d6f13c1f9677b3f318331782c0086)) - +- **api:** improve rottentomatoes rating matching for movies ([7db62ab](https://github.com/sct/overseerr/commit/7db62ab824eefc42e6db16e42d52f4266b136f82)), closes [#494](https://github.com/sct/overseerr/issues/494) +- **build:** remove cross import from client to server for UserType ([23624bd](https://github.com/sct/overseerr/commit/23624bd144af5df4c31995b68ce48105b95b20f6)) +- **frontend:** clarify which fields are required in radarr/sonarr modals ([860d71e](https://github.com/sct/overseerr/commit/860d71ed69a69a1a3f74b79290ef471e04f57a6b)), closes [#575](https://github.com/sct/overseerr/issues/575) +- **frontend:** do not show failed media status on request list for declined requests ([00944b1](https://github.com/sct/overseerr/commit/00944b1ec2db8ddc5742448f6448f7364c473a98)), closes [#664](https://github.com/sct/overseerr/issues/664) +- **frontend:** fix button styling on details page on small screen sizes ([d9e0c90](https://github.com/sct/overseerr/commit/d9e0c90e76d80aef0c67318e00e997804805f46e)) +- **frontend:** fix request button height ([a262727](https://github.com/sct/overseerr/commit/a2627270784bdef8644875fa5c5a7349a0b7fd81)) +- **frontend:** request dropdown menu now properly shows up over collection button ([b491be1](https://github.com/sct/overseerr/commit/b491be1b1e7f6aa588274230e695e4c5302b961e)) +- **frontend:** show correct request status on request cards for 4k requests ([1aa0005](https://github.com/sct/overseerr/commit/1aa0005b4298fc1af9c1d0bf1f357738c0fa2673)) +- **lang:** add missing see more i18n string for SeeMoreCard ([d9919ab](https://github.com/sct/overseerr/commit/d9919abb8998d28558ddec35b8e60ab2af75d5b7)) +- **lang:** change email auth user/pass strings to SMTP Username/Password ([a77a2aa](https://github.com/sct/overseerr/commit/a77a2aa3ebb1be353d534db5b07647ac26c60e15)) +- **notifications:** correctly compare seasons before sending series notifications ([f17fa2a](https://github.com/sct/overseerr/commit/f17fa2a2db8144bac89936f588627e8dd37bf54a)) +- **notifications:** only send one available notification for standard media ([fc6f7cc](https://github.com/sct/overseerr/commit/fc6f7ccea586165a30022b6d5554911c66ece6df)) +- **notifications:** send media declined email ([eb6fc8a](https://github.com/sct/overseerr/commit/eb6fc8a19099469794d471db0b48a258c2866633)), closes [#679](https://github.com/sct/overseerr/issues/679) +- **plex-sync:** improve plex sync error handling. add session id to fix stuck runs ([a740b07](https://github.com/sct/overseerr/commit/a740b07f06f892b72a651b928af28ce71cb495ee)) +- **plex-sync:** store plex added date and sort recently added by it ([d688a96](https://github.com/sct/overseerr/commit/d688a967596afcba9799b8133089bebb5add27cf)) +- **requests:** select the correct radarr/sonarr server when sending request to service ([e0d9f89](https://github.com/sct/overseerr/commit/e0d9f891e797c3839f976b75a871903b6f2e55f1)) +- **server:** support absolute paths for CONFIG_DIRECTORY ([51d8fba](https://github.com/sct/overseerr/commit/51d8fba9162b9e148a35ced69e7e035438c8b0f1)) +- **user edit:** fix user edit not being able to be saved ([#651](https://github.com/sct/overseerr/issues/651)) ([b04d00e](https://github.com/sct/overseerr/commit/b04d00ef509d6f13c1f9677b3f318331782c0086)) ### Features -* **api:** /request/count endpoint ([#682](https://github.com/sct/overseerr/issues/682)) ([192cfd8](https://github.com/sct/overseerr/commit/192cfd8a8ea9ab942d5bb265d42050917a2f5a04)) -* **frontend:** add see more card to media sliders ([587e8db](https://github.com/sct/overseerr/commit/587e8db15e9c19b4c58406e3e4215d8bf87d8762)) -* **frontend:** add template variable help button to custom webhook settings page ([29c5bc4](https://github.com/sct/overseerr/commit/29c5bc40975e7ab0a2e08bb77294f164f0c60769)) -* **lang:** add support for Chinese (Traditional) language ([686c4f7](https://github.com/sct/overseerr/commit/686c4f71bf930625af082ac5e14dc5f79f5c42eb)) -* **lang:** Translations update from Weblate ([#604](https://github.com/sct/overseerr/issues/604)) ([801e765](https://github.com/sct/overseerr/commit/801e76524d6ea0887249f1630402e9c3a3430b44)) -* **login:** add local users functionality ([#591](https://github.com/sct/overseerr/issues/591)) ([492e19d](https://github.com/sct/overseerr/commit/492e19df4014e67dc6a2de5903a33c25e13fcf45)) -* **notifications:** add notification for declined requests ([2f97f61](https://github.com/sct/overseerr/commit/2f97f61a6e8846975774aa16950a39ada2b1a016)), closes [#663](https://github.com/sct/overseerr/issues/663) -* **notifications:** Webhook Notifications ([#632](https://github.com/sct/overseerr/issues/632)) ([a7cc7c5](https://github.com/sct/overseerr/commit/a7cc7c59753dd9649b2ec37eb9d46fe4fa8e1e1c)) -* **requests:** Request Overrides & Request Editing ([#653](https://github.com/sct/overseerr/issues/653)) ([bdb3372](https://github.com/sct/overseerr/commit/bdb33722e6df09dd6d8caa36b104b61c6b8dc00d)) -* **server:** add CONFIG_DIRECTORY env var to control config directory location ([fa8f112](https://github.com/sct/overseerr/commit/fa8f112c31ccb5ee6244f776bc97e76d81958539)) -* 4K Requests ([#559](https://github.com/sct/overseerr/issues/559)) ([6b2df24](https://github.com/sct/overseerr/commit/6b2df24a2e8f96dd2277a814d7e02015d1f80cdc)) -* map AniDB IDs from Hama agent to tvdb/tmdb/imdb IDs ([#538](https://github.com/sct/overseerr/issues/538)) ([0600ac7](https://github.com/sct/overseerr/commit/0600ac7c3a1bc0cdd906634d5f77ea3e99b10e94)), closes [#453](https://github.com/sct/overseerr/issues/453) - +- **api:** /request/count endpoint ([#682](https://github.com/sct/overseerr/issues/682)) ([192cfd8](https://github.com/sct/overseerr/commit/192cfd8a8ea9ab942d5bb265d42050917a2f5a04)) +- **frontend:** add see more card to media sliders ([587e8db](https://github.com/sct/overseerr/commit/587e8db15e9c19b4c58406e3e4215d8bf87d8762)) +- **frontend:** add template variable help button to custom webhook settings page ([29c5bc4](https://github.com/sct/overseerr/commit/29c5bc40975e7ab0a2e08bb77294f164f0c60769)) +- **lang:** add support for Chinese (Traditional) language ([686c4f7](https://github.com/sct/overseerr/commit/686c4f71bf930625af082ac5e14dc5f79f5c42eb)) +- **lang:** Translations update from Weblate ([#604](https://github.com/sct/overseerr/issues/604)) ([801e765](https://github.com/sct/overseerr/commit/801e76524d6ea0887249f1630402e9c3a3430b44)) +- **login:** add local users functionality ([#591](https://github.com/sct/overseerr/issues/591)) ([492e19d](https://github.com/sct/overseerr/commit/492e19df4014e67dc6a2de5903a33c25e13fcf45)) +- **notifications:** add notification for declined requests ([2f97f61](https://github.com/sct/overseerr/commit/2f97f61a6e8846975774aa16950a39ada2b1a016)), closes [#663](https://github.com/sct/overseerr/issues/663) +- **notifications:** Webhook Notifications ([#632](https://github.com/sct/overseerr/issues/632)) ([a7cc7c5](https://github.com/sct/overseerr/commit/a7cc7c59753dd9649b2ec37eb9d46fe4fa8e1e1c)) +- **requests:** Request Overrides & Request Editing ([#653](https://github.com/sct/overseerr/issues/653)) ([bdb3372](https://github.com/sct/overseerr/commit/bdb33722e6df09dd6d8caa36b104b61c6b8dc00d)) +- **server:** add CONFIG_DIRECTORY env var to control config directory location ([fa8f112](https://github.com/sct/overseerr/commit/fa8f112c31ccb5ee6244f776bc97e76d81958539)) +- 4K Requests ([#559](https://github.com/sct/overseerr/issues/559)) ([6b2df24](https://github.com/sct/overseerr/commit/6b2df24a2e8f96dd2277a814d7e02015d1f80cdc)) +- map AniDB IDs from Hama agent to tvdb/tmdb/imdb IDs ([#538](https://github.com/sct/overseerr/issues/538)) ([0600ac7](https://github.com/sct/overseerr/commit/0600ac7c3a1bc0cdd906634d5f77ea3e99b10e94)), closes [#453](https://github.com/sct/overseerr/issues/453) ### Reverts -* **deps:** revert back to next@10.0.3 until sharp optional dependency bug is fixed ([7962964](https://github.com/sct/overseerr/commit/79629645aacc1a042919834da79bff0c1f69c9d6)) +- **deps:** revert back to next@10.0.3 until sharp optional dependency bug is fixed ([7962964](https://github.com/sct/overseerr/commit/79629645aacc1a042919834da79bff0c1f69c9d6)) # [1.16.0](https://github.com/sct/overseerr/compare/v1.15.0...v1.16.0) (2021-01-07) - ### Bug Fixes -* **frontend:** adjust titlecard badge styling ([effc809](https://github.com/sct/overseerr/commit/effc80977a4ed732092254248f82363e52233171)) -* **frontend:** apply same titlecard hover effect to personcard ([67f2b57](https://github.com/sct/overseerr/commit/67f2b57f00216ded3b34965629d6fdd2f16bc25f)) -* **frontend:** only animate titlecard when showDetail is true ([0ab4c3c](https://github.com/sct/overseerr/commit/0ab4c3c36fe2c1ded142b6931111516f7f990a41)) -* **frontend:** use hardware acceleration for titlecard scale ([88810bf](https://github.com/sct/overseerr/commit/88810bf0a4ef74299f6541b60fa91cea3610f99c)) -* **plex-sync:** do not run plex sync if no admin exists ([493d82b](https://github.com/sct/overseerr/commit/493d82b6b066d77609cf66e005fd1f1472b8e011)) - +- **frontend:** adjust titlecard badge styling ([effc809](https://github.com/sct/overseerr/commit/effc80977a4ed732092254248f82363e52233171)) +- **frontend:** apply same titlecard hover effect to personcard ([67f2b57](https://github.com/sct/overseerr/commit/67f2b57f00216ded3b34965629d6fdd2f16bc25f)) +- **frontend:** only animate titlecard when showDetail is true ([0ab4c3c](https://github.com/sct/overseerr/commit/0ab4c3c36fe2c1ded142b6931111516f7f990a41)) +- **frontend:** use hardware acceleration for titlecard scale ([88810bf](https://github.com/sct/overseerr/commit/88810bf0a4ef74299f6541b60fa91cea3610f99c)) +- **plex-sync:** do not run plex sync if no admin exists ([493d82b](https://github.com/sct/overseerr/commit/493d82b6b066d77609cf66e005fd1f1472b8e011)) ### Features -* **lang:** translations update from Weblate ([#495](https://github.com/sct/overseerr/issues/495)) ([b04eda6](https://github.com/sct/overseerr/commit/b04eda6c8a3bfcaa2a14b8a29612fdf690c9fba0)) -* **lang:** Translations update from Weblate ([#580](https://github.com/sct/overseerr/issues/580)) ([2bfe0f2](https://github.com/sct/overseerr/commit/2bfe0f2bf66956763ab26d5c54f26e6c456f59f7)) -* **notifications:** add pushover integration ([#574](https://github.com/sct/overseerr/issues/574)) ([ee5d018](https://github.com/sct/overseerr/commit/ee5d0181fc9a673b27aefd1d09b0a78c3d2e4f55)) +- **lang:** translations update from Weblate ([#495](https://github.com/sct/overseerr/issues/495)) ([b04eda6](https://github.com/sct/overseerr/commit/b04eda6c8a3bfcaa2a14b8a29612fdf690c9fba0)) +- **lang:** Translations update from Weblate ([#580](https://github.com/sct/overseerr/issues/580)) ([2bfe0f2](https://github.com/sct/overseerr/commit/2bfe0f2bf66956763ab26d5c54f26e6c456f59f7)) +- **notifications:** add pushover integration ([#574](https://github.com/sct/overseerr/issues/574)) ([ee5d018](https://github.com/sct/overseerr/commit/ee5d0181fc9a673b27aefd1d09b0a78c3d2e4f55)) # [1.15.0](https://github.com/sct/overseerr/compare/v1.14.1...v1.15.0) (2021-01-04) - ### Bug Fixes -* **api:** return 202 when same seasons are requested again ([5c84702](https://github.com/sct/overseerr/commit/5c847026aad79fcac4d020786ded9f867696c226)) -* **build:** fixes build to include commit tag for app build step ([289864a](https://github.com/sct/overseerr/commit/289864af1a995ce04834bf8a220cc238e1954d19)) -* **docs:** fix typo in build instructions ([#503](https://github.com/sct/overseerr/issues/503)) ([2b27a71](https://github.com/sct/overseerr/commit/2b27a715b07c27200ba1e5e9623629a34389276d)) -* **frontend:** add i18n for request text on titlecard ([a524b9c](https://github.com/sct/overseerr/commit/a524b9c4c8968f6823d33eb270dc26069fe4a725)) -* **frontend:** add localized strings for status checker ([2dcda39](https://github.com/sct/overseerr/commit/2dcda39d40d820419e098bd6f1101eb820e5b42d)) -* **frontend:** center text in movie auto-approve modal on small screens ([#510](https://github.com/sct/overseerr/issues/510)) ([1438b08](https://github.com/sct/overseerr/commit/1438b08cf0b358d79c6688c64be99f1718ec2d23)), closes [#507](https://github.com/sct/overseerr/issues/507) -* **frontend:** change titlecard to only have a request button ([b5a3a7a](https://github.com/sct/overseerr/commit/b5a3a7a89fcaf86dd794dc419711677b53646577)) -* **frontend:** combine duplicate credits on a persons detail page ([d188f6f](https://github.com/sct/overseerr/commit/d188f6ffadff1564c47d5f33138e35498bed29fd)), closes [#504](https://github.com/sct/overseerr/issues/504) -* **frontend:** disable pointer-events on titlecard badges ([ce06879](https://github.com/sct/overseerr/commit/ce0687922a94588b3492e8ddf2e84f54dd1a0d4e)) -* **frontend:** fix count of requests in request list ([f124d73](https://github.com/sct/overseerr/commit/f124d732a2911abdccb5abc11471efe61cc20f7a)) -* **frontend:** fix sliders overflowing on firefox ([67ac9e0](https://github.com/sct/overseerr/commit/67ac9e075f0ca1cfe7e4766d9168815d7ab600fa)), closes [#566](https://github.com/sct/overseerr/issues/566) -* **frontend:** full season request modal fits on a smaller mobile UI ([#535](https://github.com/sct/overseerr/issues/535)) ([12db7a0](https://github.com/sct/overseerr/commit/12db7a065ad566b47d46de4b949343290894f153)) -* **frontend:** handle currentLibrary possibly being null on first manual sync ([93b57a7](https://github.com/sct/overseerr/commit/93b57a76f10a823615ca11ff59f523b67aa30fad)) -* **frontend:** increase titlecard status badge size on larger screens ([ba106c4](https://github.com/sct/overseerr/commit/ba106c447d76db2f9ac70a60c5b38cc60ab554fe)) -* **frontend:** search clear button now correctly triggers routing ([343f466](https://github.com/sct/overseerr/commit/343f466788abc308b91a414ef61bba816ac8875c)) -* **frontend:** set locale cookie expiration to be much longer ([fae4818](https://github.com/sct/overseerr/commit/fae481895736eab81d52eb93788beb00669fb355)) -* **frontend:** show movie/series badges always ([8cbf39a](https://github.com/sct/overseerr/commit/8cbf39a9d12eaee7720fa4721c350c1ef9dee856)) -* **frontend:** update login/setup images ([058fb65](https://github.com/sct/overseerr/commit/058fb65495baa08a0bd4c9e0aef320c6fc7d017b)) -* **holiday:** remove special holiday slider ([8c09033](https://github.com/sct/overseerr/commit/8c0903393cf2cb2a929ba70a8ab6ddcc4cba0574)) -* correctly deal with tmdb id duplicates between movies/series ([721ed9a](https://github.com/sct/overseerr/commit/721ed9a93087a57ae749388bddcacf26022e3df6)), closes [#526](https://github.com/sct/overseerr/issues/526) -* use new commit tag file for app version as well ([d00e470](https://github.com/sct/overseerr/commit/d00e470b55327489b49d770144b7cfdb24045be6)) - +- **api:** return 202 when same seasons are requested again ([5c84702](https://github.com/sct/overseerr/commit/5c847026aad79fcac4d020786ded9f867696c226)) +- **build:** fixes build to include commit tag for app build step ([289864a](https://github.com/sct/overseerr/commit/289864af1a995ce04834bf8a220cc238e1954d19)) +- **docs:** fix typo in build instructions ([#503](https://github.com/sct/overseerr/issues/503)) ([2b27a71](https://github.com/sct/overseerr/commit/2b27a715b07c27200ba1e5e9623629a34389276d)) +- **frontend:** add i18n for request text on titlecard ([a524b9c](https://github.com/sct/overseerr/commit/a524b9c4c8968f6823d33eb270dc26069fe4a725)) +- **frontend:** add localized strings for status checker ([2dcda39](https://github.com/sct/overseerr/commit/2dcda39d40d820419e098bd6f1101eb820e5b42d)) +- **frontend:** center text in movie auto-approve modal on small screens ([#510](https://github.com/sct/overseerr/issues/510)) ([1438b08](https://github.com/sct/overseerr/commit/1438b08cf0b358d79c6688c64be99f1718ec2d23)), closes [#507](https://github.com/sct/overseerr/issues/507) +- **frontend:** change titlecard to only have a request button ([b5a3a7a](https://github.com/sct/overseerr/commit/b5a3a7a89fcaf86dd794dc419711677b53646577)) +- **frontend:** combine duplicate credits on a persons detail page ([d188f6f](https://github.com/sct/overseerr/commit/d188f6ffadff1564c47d5f33138e35498bed29fd)), closes [#504](https://github.com/sct/overseerr/issues/504) +- **frontend:** disable pointer-events on titlecard badges ([ce06879](https://github.com/sct/overseerr/commit/ce0687922a94588b3492e8ddf2e84f54dd1a0d4e)) +- **frontend:** fix count of requests in request list ([f124d73](https://github.com/sct/overseerr/commit/f124d732a2911abdccb5abc11471efe61cc20f7a)) +- **frontend:** fix sliders overflowing on firefox ([67ac9e0](https://github.com/sct/overseerr/commit/67ac9e075f0ca1cfe7e4766d9168815d7ab600fa)), closes [#566](https://github.com/sct/overseerr/issues/566) +- **frontend:** full season request modal fits on a smaller mobile UI ([#535](https://github.com/sct/overseerr/issues/535)) ([12db7a0](https://github.com/sct/overseerr/commit/12db7a065ad566b47d46de4b949343290894f153)) +- **frontend:** handle currentLibrary possibly being null on first manual sync ([93b57a7](https://github.com/sct/overseerr/commit/93b57a76f10a823615ca11ff59f523b67aa30fad)) +- **frontend:** increase titlecard status badge size on larger screens ([ba106c4](https://github.com/sct/overseerr/commit/ba106c447d76db2f9ac70a60c5b38cc60ab554fe)) +- **frontend:** search clear button now correctly triggers routing ([343f466](https://github.com/sct/overseerr/commit/343f466788abc308b91a414ef61bba816ac8875c)) +- **frontend:** set locale cookie expiration to be much longer ([fae4818](https://github.com/sct/overseerr/commit/fae481895736eab81d52eb93788beb00669fb355)) +- **frontend:** show movie/series badges always ([8cbf39a](https://github.com/sct/overseerr/commit/8cbf39a9d12eaee7720fa4721c350c1ef9dee856)) +- **frontend:** update login/setup images ([058fb65](https://github.com/sct/overseerr/commit/058fb65495baa08a0bd4c9e0aef320c6fc7d017b)) +- **holiday:** remove special holiday slider ([8c09033](https://github.com/sct/overseerr/commit/8c0903393cf2cb2a929ba70a8ab6ddcc4cba0574)) +- correctly deal with tmdb id duplicates between movies/series ([721ed9a](https://github.com/sct/overseerr/commit/721ed9a93087a57ae749388bddcacf26022e3df6)), closes [#526](https://github.com/sct/overseerr/issues/526) +- use new commit tag file for app version as well ([d00e470](https://github.com/sct/overseerr/commit/d00e470b55327489b49d770144b7cfdb24045be6)) ### Features -* **email:** add sendername to email notification ([#506](https://github.com/sct/overseerr/issues/506)) ([0185bb1](https://github.com/sct/overseerr/commit/0185bb1a7084c1faeb61fb1c63e34e26732711c8)) -* **frontend:** add clear-field-icon to search field ([#498](https://github.com/sct/overseerr/issues/498)) ([7434a26](https://github.com/sct/overseerr/commit/7434a26f76b5e9f74918f3e1a34443d20ecfcbe4)) -* **frontend:** add documentation link to about page ([c034496](https://github.com/sct/overseerr/commit/c034496f557a031aed35cd28dc7221d8cdf36643)) -* **frontend:** add telegram integration ([#491](https://github.com/sct/overseerr/issues/491)) ([c8d4d67](https://github.com/sct/overseerr/commit/c8d4d674f412082ad9e9da09abd79660365cf728)) -* **frontend:** filter/sorting for request list ([5add44c](https://github.com/sct/overseerr/commit/5add44cfb0379aa6fed7c3b867230292feacc684)), closes [#431](https://github.com/sct/overseerr/issues/431) -* **notifications:** control notifcation types per agent ([8af6a1f](https://github.com/sct/overseerr/commit/8af6a1f566769c583af7dd9e18d162717835b7cc)), closes [#513](https://github.com/sct/overseerr/issues/513) -* status checker to prompt users to reload their frontend when app version changes ([75a4264](https://github.com/sct/overseerr/commit/75a426437a4182e21da13684066966dd5bf8fc5e)) +- **email:** add sendername to email notification ([#506](https://github.com/sct/overseerr/issues/506)) ([0185bb1](https://github.com/sct/overseerr/commit/0185bb1a7084c1faeb61fb1c63e34e26732711c8)) +- **frontend:** add clear-field-icon to search field ([#498](https://github.com/sct/overseerr/issues/498)) ([7434a26](https://github.com/sct/overseerr/commit/7434a26f76b5e9f74918f3e1a34443d20ecfcbe4)) +- **frontend:** add documentation link to about page ([c034496](https://github.com/sct/overseerr/commit/c034496f557a031aed35cd28dc7221d8cdf36643)) +- **frontend:** add telegram integration ([#491](https://github.com/sct/overseerr/issues/491)) ([c8d4d67](https://github.com/sct/overseerr/commit/c8d4d674f412082ad9e9da09abd79660365cf728)) +- **frontend:** filter/sorting for request list ([5add44c](https://github.com/sct/overseerr/commit/5add44cfb0379aa6fed7c3b867230292feacc684)), closes [#431](https://github.com/sct/overseerr/issues/431) +- **notifications:** control notifcation types per agent ([8af6a1f](https://github.com/sct/overseerr/commit/8af6a1f566769c583af7dd9e18d162717835b7cc)), closes [#513](https://github.com/sct/overseerr/issues/513) +- status checker to prompt users to reload their frontend when app version changes ([75a4264](https://github.com/sct/overseerr/commit/75a426437a4182e21da13684066966dd5bf8fc5e)) ## [1.14.1](https://github.com/sct/overseerr/compare/v1.14.0...v1.14.1) (2021-01-02) - ### Bug Fixes -* **holiday:** remove special holiday slider ([22f2037](https://github.com/sct/overseerr/commit/22f2037ea6c5a0ba2ffa4d69f2b7cf42bdcf8575)) +- **holiday:** remove special holiday slider ([22f2037](https://github.com/sct/overseerr/commit/22f2037ea6c5a0ba2ffa4d69f2b7cf42bdcf8575)) # [1.14.0](https://github.com/sct/overseerr/compare/v1.13.0...v1.14.0) (2020-12-25) - ### Bug Fixes -* **frontend:** add margin to ButtonWithDropdown component on movie/tv details page ([06fc98b](https://github.com/sct/overseerr/commit/06fc98b6b958221fa180f57f702c348f15b31f1c)) -* **frontend:** correctly position title card hover section ([#486](https://github.com/sct/overseerr/issues/486)) ([4b7af86](https://github.com/sct/overseerr/commit/4b7af86111a0300e1a137f23fa4ad1639fa55feb)) -* **frontend:** fix missing styles for alert component ([de3d288](https://github.com/sct/overseerr/commit/de3d288949b60d3a3af889d69a62bea2bc799ed7)) -* **frontend:** fix mobile dropdown in notifications settings ([6353cda](https://github.com/sct/overseerr/commit/6353cda5825f442dd539886c7b9ba437edf27ac4)) -* **frontend:** fix scaling titlecard content position ([bd94740](https://github.com/sct/overseerr/commit/bd947409e6e8ff313011b77adc76ccd5f9112c78)) -* **frontend:** improve flex header on movie/tv details page ([d7b1c28](https://github.com/sct/overseerr/commit/d7b1c2840690c144ebf29a360defcbd6fdb21354)) -* **frontend:** invalid dom-nesting title card fix ([#482](https://github.com/sct/overseerr/issues/482)) ([f2ebba7](https://github.com/sct/overseerr/commit/f2ebba7b1df775d33d2af6abc3ee2c9de5f2e57a)), closes [#476](https://github.com/sct/overseerr/issues/476) -* **frontend:** remove vote permission for now ([5d06a34](https://github.com/sct/overseerr/commit/5d06a347311bd10c05d8f58068ca7104e265dcca)) -* **frontend:** sort person detail credits by tmdb votes ([17518db](https://github.com/sct/overseerr/commit/17518dbe7f545100770a892d03d1f8508adc3650)) -* **frontend:** status badge Unavailable renamed to Requested ([ed94a0f](https://github.com/sct/overseerr/commit/ed94a0f335c59de526dd812aea7616313fe002fd)), closes [#374](https://github.com/sct/overseerr/issues/374) -* **frontend:** update titlecard status badge to new requested colors ([8f292d5](https://github.com/sct/overseerr/commit/8f292d538b937ea133175089979ef02599f6fef4)) -* **logs:** rotate logs on a daily basis instead of incrementing log filename ([395cbb2](https://github.com/sct/overseerr/commit/395cbb2be6c62f1d7573593e49a93615eaf22853)) -* improve apple-touch-icon and android app icons ([329a814](https://github.com/sct/overseerr/commit/329a814a8fb791122266c0b04b05848c71d68ba1)) - +- **frontend:** add margin to ButtonWithDropdown component on movie/tv details page ([06fc98b](https://github.com/sct/overseerr/commit/06fc98b6b958221fa180f57f702c348f15b31f1c)) +- **frontend:** correctly position title card hover section ([#486](https://github.com/sct/overseerr/issues/486)) ([4b7af86](https://github.com/sct/overseerr/commit/4b7af86111a0300e1a137f23fa4ad1639fa55feb)) +- **frontend:** fix missing styles for alert component ([de3d288](https://github.com/sct/overseerr/commit/de3d288949b60d3a3af889d69a62bea2bc799ed7)) +- **frontend:** fix mobile dropdown in notifications settings ([6353cda](https://github.com/sct/overseerr/commit/6353cda5825f442dd539886c7b9ba437edf27ac4)) +- **frontend:** fix scaling titlecard content position ([bd94740](https://github.com/sct/overseerr/commit/bd947409e6e8ff313011b77adc76ccd5f9112c78)) +- **frontend:** improve flex header on movie/tv details page ([d7b1c28](https://github.com/sct/overseerr/commit/d7b1c2840690c144ebf29a360defcbd6fdb21354)) +- **frontend:** invalid dom-nesting title card fix ([#482](https://github.com/sct/overseerr/issues/482)) ([f2ebba7](https://github.com/sct/overseerr/commit/f2ebba7b1df775d33d2af6abc3ee2c9de5f2e57a)), closes [#476](https://github.com/sct/overseerr/issues/476) +- **frontend:** remove vote permission for now ([5d06a34](https://github.com/sct/overseerr/commit/5d06a347311bd10c05d8f58068ca7104e265dcca)) +- **frontend:** sort person detail credits by tmdb votes ([17518db](https://github.com/sct/overseerr/commit/17518dbe7f545100770a892d03d1f8508adc3650)) +- **frontend:** status badge Unavailable renamed to Requested ([ed94a0f](https://github.com/sct/overseerr/commit/ed94a0f335c59de526dd812aea7616313fe002fd)), closes [#374](https://github.com/sct/overseerr/issues/374) +- **frontend:** update titlecard status badge to new requested colors ([8f292d5](https://github.com/sct/overseerr/commit/8f292d538b937ea133175089979ef02599f6fef4)) +- **logs:** rotate logs on a daily basis instead of incrementing log filename ([395cbb2](https://github.com/sct/overseerr/commit/395cbb2be6c62f1d7573593e49a93615eaf22853)) +- improve apple-touch-icon and android app icons ([329a814](https://github.com/sct/overseerr/commit/329a814a8fb791122266c0b04b05848c71d68ba1)) ### Features -* **lang:** translations update from Weblate ([#479](https://github.com/sct/overseerr/issues/479)) ([c8c74b0](https://github.com/sct/overseerr/commit/c8c74b0ae54fcc524aa8b2edf5a5c5e5db6c1638)) -* **notifications:** add slack notification agent ([1163e81](https://github.com/sct/overseerr/commit/1163e81adc7da1e8334155ebee5b4672a22143db)), closes [#365](https://github.com/sct/overseerr/issues/365) -* add collections ([#484](https://github.com/sct/overseerr/issues/484)) ([a333a09](https://github.com/sct/overseerr/commit/a333a095820ce3f10857026ba4770a2fffeed7cb)), closes [#418](https://github.com/sct/overseerr/issues/418) -* add separate auto approve permissions for Movies/Series ([4809257](https://github.com/sct/overseerr/commit/480925781691de456abc427fbbba161be11a3a8a)), closes [#268](https://github.com/sct/overseerr/issues/268) -* simple failed request handling ([#474](https://github.com/sct/overseerr/issues/474)) ([02969d5](https://github.com/sct/overseerr/commit/02969d5426245062a2f53475d83c4a8639632c9d)) -* YouTube Movie/TV Trailers ([#454](https://github.com/sct/overseerr/issues/454)) ([e88dc83](https://github.com/sct/overseerr/commit/e88dc83aeba0475e3ad421d5ab130cea4fc9a806)) +- **lang:** translations update from Weblate ([#479](https://github.com/sct/overseerr/issues/479)) ([c8c74b0](https://github.com/sct/overseerr/commit/c8c74b0ae54fcc524aa8b2edf5a5c5e5db6c1638)) +- **notifications:** add slack notification agent ([1163e81](https://github.com/sct/overseerr/commit/1163e81adc7da1e8334155ebee5b4672a22143db)), closes [#365](https://github.com/sct/overseerr/issues/365) +- add collections ([#484](https://github.com/sct/overseerr/issues/484)) ([a333a09](https://github.com/sct/overseerr/commit/a333a095820ce3f10857026ba4770a2fffeed7cb)), closes [#418](https://github.com/sct/overseerr/issues/418) +- add separate auto approve permissions for Movies/Series ([4809257](https://github.com/sct/overseerr/commit/480925781691de456abc427fbbba161be11a3a8a)), closes [#268](https://github.com/sct/overseerr/issues/268) +- simple failed request handling ([#474](https://github.com/sct/overseerr/issues/474)) ([02969d5](https://github.com/sct/overseerr/commit/02969d5426245062a2f53475d83c4a8639632c9d)) +- YouTube Movie/TV Trailers ([#454](https://github.com/sct/overseerr/issues/454)) ([e88dc83](https://github.com/sct/overseerr/commit/e88dc83aeba0475e3ad421d5ab130cea4fc9a806)) # [1.13.0](https://github.com/sct/overseerr/compare/v1.12.1...v1.13.0) (2020-12-23) - ### Bug Fixes -* **api:** correctly return firstAirDate for series in search endpoints ([32b4c99](https://github.com/sct/overseerr/commit/32b4c99950659d9e1da2ffa93c22383c54d0d904)), closes [#462](https://github.com/sct/overseerr/issues/462) -* **email:** correctly log errors when emails fail to send ([0980fa5](https://github.com/sct/overseerr/commit/0980fa54f9fc3bdfae6c57fa5a20ce3b2a88a677)) -* **frontend:** added new Radarr v3 logo ([#471](https://github.com/sct/overseerr/issues/471)) ([3bbc716](https://github.com/sct/overseerr/commit/3bbc716434dc04bfe6b55de9898eb2c0ecb03baa)) -* **frontend:** approve and decline button (in manage panel) will now fit on mobile ([#441](https://github.com/sct/overseerr/issues/441)) ([66ef72d](https://github.com/sct/overseerr/commit/66ef72dd42912d83ea8f86aabb75fbee547f8de9)) -* **frontend:** filter out undefined backdrop paths for person details page ([2e0e4d5](https://github.com/sct/overseerr/commit/2e0e4d5129ed4912415f61eb8d1da41e88ddcaff)) -* **frontend:** show backdrops instead of posters for new person detail design ([9f5f920](https://github.com/sct/overseerr/commit/9f5f920c23007363aa7f53ebef0b61236d4f53ea)) -* clarify full sync runs every 24 hours ([0c8a180](https://github.com/sct/overseerr/commit/0c8a180189b2610bab2fa977d458743d8a60343e)) -* **plex-sync:** match correct tmdb format for movies ([4205e32](https://github.com/sct/overseerr/commit/4205e32ae71bc18c07209f1c82e6af1cb5f01335)) - +- **api:** correctly return firstAirDate for series in search endpoints ([32b4c99](https://github.com/sct/overseerr/commit/32b4c99950659d9e1da2ffa93c22383c54d0d904)), closes [#462](https://github.com/sct/overseerr/issues/462) +- **email:** correctly log errors when emails fail to send ([0980fa5](https://github.com/sct/overseerr/commit/0980fa54f9fc3bdfae6c57fa5a20ce3b2a88a677)) +- **frontend:** added new Radarr v3 logo ([#471](https://github.com/sct/overseerr/issues/471)) ([3bbc716](https://github.com/sct/overseerr/commit/3bbc716434dc04bfe6b55de9898eb2c0ecb03baa)) +- **frontend:** approve and decline button (in manage panel) will now fit on mobile ([#441](https://github.com/sct/overseerr/issues/441)) ([66ef72d](https://github.com/sct/overseerr/commit/66ef72dd42912d83ea8f86aabb75fbee547f8de9)) +- **frontend:** filter out undefined backdrop paths for person details page ([2e0e4d5](https://github.com/sct/overseerr/commit/2e0e4d5129ed4912415f61eb8d1da41e88ddcaff)) +- **frontend:** show backdrops instead of posters for new person detail design ([9f5f920](https://github.com/sct/overseerr/commit/9f5f920c23007363aa7f53ebef0b61236d4f53ea)) +- clarify full sync runs every 24 hours ([0c8a180](https://github.com/sct/overseerr/commit/0c8a180189b2610bab2fa977d458743d8a60343e)) +- **plex-sync:** match correct tmdb format for movies ([4205e32](https://github.com/sct/overseerr/commit/4205e32ae71bc18c07209f1c82e6af1cb5f01335)) ### Features -* **email:** option to allow self signed certificates ([6898357](https://github.com/sct/overseerr/commit/6898357b13a6aa53a55709ea95819c2b3df6784c)) -* **frontend:** adjust person details design and add improved truncate ([1fb7ea7](https://github.com/sct/overseerr/commit/1fb7ea72589d2908ae80a2a688881d4eb3c050e5)) -* **frontend:** first air date added to TV details page ([#470](https://github.com/sct/overseerr/issues/470)) ([a7db01f](https://github.com/sct/overseerr/commit/a7db01fba483ca633a6eb9d39eb085ab9939d4d2)) -* **lang:** translations update from Weblate ([#410](https://github.com/sct/overseerr/issues/410)) ([941fe19](https://github.com/sct/overseerr/commit/941fe1990454439cf05b48ef92bd3493432f8ed8)) -* **logs:** rotate log files if they reach 20MB in size ([22002ab](https://github.com/sct/overseerr/commit/22002ab4c76aace2bb202ac58da605b7a6f75d6d)), closes [#438](https://github.com/sct/overseerr/issues/438) -* **notifications:** include direct links to media in notifications ([659fa50](https://github.com/sct/overseerr/commit/659fa505f0db32262ad0041cddb4daea893e6d65)), closes [#437](https://github.com/sct/overseerr/issues/437) -* **plex-sync:** add support for hama guid's ([ffe9e19](https://github.com/sct/overseerr/commit/ffe9e19c3b99de6af1185900e292da641ff44320)), closes [#453](https://github.com/sct/overseerr/issues/453) +- **email:** option to allow self signed certificates ([6898357](https://github.com/sct/overseerr/commit/6898357b13a6aa53a55709ea95819c2b3df6784c)) +- **frontend:** adjust person details design and add improved truncate ([1fb7ea7](https://github.com/sct/overseerr/commit/1fb7ea72589d2908ae80a2a688881d4eb3c050e5)) +- **frontend:** first air date added to TV details page ([#470](https://github.com/sct/overseerr/issues/470)) ([a7db01f](https://github.com/sct/overseerr/commit/a7db01fba483ca633a6eb9d39eb085ab9939d4d2)) +- **lang:** translations update from Weblate ([#410](https://github.com/sct/overseerr/issues/410)) ([941fe19](https://github.com/sct/overseerr/commit/941fe1990454439cf05b48ef92bd3493432f8ed8)) +- **logs:** rotate log files if they reach 20MB in size ([22002ab](https://github.com/sct/overseerr/commit/22002ab4c76aace2bb202ac58da605b7a6f75d6d)), closes [#438](https://github.com/sct/overseerr/issues/438) +- **notifications:** include direct links to media in notifications ([659fa50](https://github.com/sct/overseerr/commit/659fa505f0db32262ad0041cddb4daea893e6d65)), closes [#437](https://github.com/sct/overseerr/issues/437) +- **plex-sync:** add support for hama guid's ([ffe9e19](https://github.com/sct/overseerr/commit/ffe9e19c3b99de6af1185900e292da641ff44320)), closes [#453](https://github.com/sct/overseerr/issues/453) ## [1.12.1](https://github.com/sct/overseerr/compare/v1.12.0...v1.12.1) (2020-12-22) - ### Bug Fixes -* **migration:** fixes issue migrating away from the unique imdbId constraint ([69fd7a5](https://github.com/sct/overseerr/commit/69fd7a5511215674a5c22ba48627f221da900229)) +- **migration:** fixes issue migrating away from the unique imdbId constraint ([69fd7a5](https://github.com/sct/overseerr/commit/69fd7a5511215674a5c22ba48627f221da900229)) # [1.12.0](https://github.com/sct/overseerr/compare/v1.11.0...v1.12.0) (2020-12-22) - ### Bug Fixes -* **api:** fix cross-imported type crashing build ([f35dae5](https://github.com/sct/overseerr/commit/f35dae56a583a5545375318fa5be994ae1f2557f)) -* **api:** prevent checking first admin account for plex server access ([22006e9](https://github.com/sct/overseerr/commit/22006e9dbde82609440f89bde9a40887b4742682)) -* **frontend:** add name, short_name and start_url to manifest ([#424](https://github.com/sct/overseerr/issues/424)) ([c6836e0](https://github.com/sct/overseerr/commit/c6836e02c810e8adb12c3a4b110f9604cf5b7b81)) -* **frontend:** adjust person card layout to deal with overflowing content ([4891298](https://github.com/sct/overseerr/commit/48912988915ae40606a900a6f1dd23fc25ed567f)), closes [#416](https://github.com/sct/overseerr/issues/416) -* **frontend:** allow more special characters in search input ([5deb64a](https://github.com/sct/overseerr/commit/5deb64a87fd70e97da27a025ad11fb8ace0e0b57)), closes [#430](https://github.com/sct/overseerr/issues/430) -* **logs:** improve logging when adding to sonarr/radarr ([4b50522](https://github.com/sct/overseerr/commit/4b505223b881a750007e3fbc7d4bcb9677d4d412)) -* only run migrations in production ([ab9cef3](https://github.com/sct/overseerr/commit/ab9cef3624b5db1ec03507553a69d33b87857e29)) -* **notifications:** always update the media table when seasons become available ([0916b58](https://github.com/sct/overseerr/commit/0916b58594a00db98c6701fdcaee4f3c3e08904e)) -* **plex-sync:** fixes processing movies using TMDB agent ([764db94](https://github.com/sct/overseerr/commit/764db94f1bd7866309684d5bd56033b21cbc2e0c)), closes [#363](https://github.com/sct/overseerr/issues/363) - +- **api:** fix cross-imported type crashing build ([f35dae5](https://github.com/sct/overseerr/commit/f35dae56a583a5545375318fa5be994ae1f2557f)) +- **api:** prevent checking first admin account for plex server access ([22006e9](https://github.com/sct/overseerr/commit/22006e9dbde82609440f89bde9a40887b4742682)) +- **frontend:** add name, short_name and start_url to manifest ([#424](https://github.com/sct/overseerr/issues/424)) ([c6836e0](https://github.com/sct/overseerr/commit/c6836e02c810e8adb12c3a4b110f9604cf5b7b81)) +- **frontend:** adjust person card layout to deal with overflowing content ([4891298](https://github.com/sct/overseerr/commit/48912988915ae40606a900a6f1dd23fc25ed567f)), closes [#416](https://github.com/sct/overseerr/issues/416) +- **frontend:** allow more special characters in search input ([5deb64a](https://github.com/sct/overseerr/commit/5deb64a87fd70e97da27a025ad11fb8ace0e0b57)), closes [#430](https://github.com/sct/overseerr/issues/430) +- **logs:** improve logging when adding to sonarr/radarr ([4b50522](https://github.com/sct/overseerr/commit/4b505223b881a750007e3fbc7d4bcb9677d4d412)) +- only run migrations in production ([ab9cef3](https://github.com/sct/overseerr/commit/ab9cef3624b5db1ec03507553a69d33b87857e29)) +- **notifications:** always update the media table when seasons become available ([0916b58](https://github.com/sct/overseerr/commit/0916b58594a00db98c6701fdcaee4f3c3e08904e)) +- **plex-sync:** fixes processing movies using TMDB agent ([764db94](https://github.com/sct/overseerr/commit/764db94f1bd7866309684d5bd56033b21cbc2e0c)), closes [#363](https://github.com/sct/overseerr/issues/363) ### Features -* **frontend:** add crew related movies/shows to person details page ([12127a7](https://github.com/sct/overseerr/commit/12127a77633f0e92ae88cbafd49581296f559c33)) -* **frontend:** add full crew page for movies/shows ([604ba2a](https://github.com/sct/overseerr/commit/604ba2a92f1d59489e7fc6dfc011347f8595c123)) -* default user permissions added to settings ([e7ee85c](https://github.com/sct/overseerr/commit/e7ee85c29b5d25c6bff58717eae5e62de4dcef0c)), closes [#388](https://github.com/sct/overseerr/issues/388) -* import users from plex ([#428](https://github.com/sct/overseerr/issues/428)) ([7e8f361](https://github.com/sct/overseerr/commit/7e8f361af711001cfc4dcc06a384b76f9846f90f)), closes [#281](https://github.com/sct/overseerr/issues/281) -* **frontend:** add prioritized crew under overview ([6753d9d](https://github.com/sct/overseerr/commit/6753d9daaafb18672f14fd86f2c1675dcec39b13)), closes [#406](https://github.com/sct/overseerr/issues/406) -* **notifications:** added ability to send test notifications ([44a3054](https://github.com/sct/overseerr/commit/44a305426f3e9829c167a4a73095d0d248641f47)), closes [#309](https://github.com/sct/overseerr/issues/309) - +- **frontend:** add crew related movies/shows to person details page ([12127a7](https://github.com/sct/overseerr/commit/12127a77633f0e92ae88cbafd49581296f559c33)) +- **frontend:** add full crew page for movies/shows ([604ba2a](https://github.com/sct/overseerr/commit/604ba2a92f1d59489e7fc6dfc011347f8595c123)) +- default user permissions added to settings ([e7ee85c](https://github.com/sct/overseerr/commit/e7ee85c29b5d25c6bff58717eae5e62de4dcef0c)), closes [#388](https://github.com/sct/overseerr/issues/388) +- import users from plex ([#428](https://github.com/sct/overseerr/issues/428)) ([7e8f361](https://github.com/sct/overseerr/commit/7e8f361af711001cfc4dcc06a384b76f9846f90f)), closes [#281](https://github.com/sct/overseerr/issues/281) +- **frontend:** add prioritized crew under overview ([6753d9d](https://github.com/sct/overseerr/commit/6753d9daaafb18672f14fd86f2c1675dcec39b13)), closes [#406](https://github.com/sct/overseerr/issues/406) +- **notifications:** added ability to send test notifications ([44a3054](https://github.com/sct/overseerr/commit/44a305426f3e9829c167a4a73095d0d248641f47)), closes [#309](https://github.com/sct/overseerr/issues/309) ### Reverts -* **deps:** revert react-use-clipboard to 1.0.2 ([7083ddf](https://github.com/sct/overseerr/commit/7083ddf18121716e3442acab3506c395fdc351ac)) +- **deps:** revert react-use-clipboard to 1.0.2 ([7083ddf](https://github.com/sct/overseerr/commit/7083ddf18121716e3442acab3506c395fdc351ac)) # [1.11.0](https://github.com/sct/overseerr/compare/v1.10.0...v1.11.0) (2020-12-20) - ### Features -* **frontend:** add language picker to setup/login ([ff2ab29](https://github.com/sct/overseerr/commit/ff2ab29491a80c421525b9a394d6fbbf54914dc2)) -* **frontend:** add support overseerr block to about page ([c128898](https://github.com/sct/overseerr/commit/c128898206d6cbb482de4d8dca53f70b87e4911a)) -* **frontend:** releases added to about page ([b7f5739](https://github.com/sct/overseerr/commit/b7f573903500cc8a62e39afd787bc1da8c09d88b)), closes [#303](https://github.com/sct/overseerr/issues/303) -* **lang:** add support for Italian, Portuguese (Brazil) and Serbian ([108dfc4](https://github.com/sct/overseerr/commit/108dfc4afd31388cb6c9e07deccd168ade8b1574)) -* **lang:** add support for swedish language ([c9fe6cb](https://github.com/sct/overseerr/commit/c9fe6cb0b7ea984d8e4e1cb3f284935c9da7cc2b)) -* **lang:** translations update from Weblate ([#400](https://github.com/sct/overseerr/issues/400)) ([1bd0e64](https://github.com/sct/overseerr/commit/1bd0e646e313ddf77ef331e818e03401fbf64a72)) -* **lang:** translations update from Weblate ([#403](https://github.com/sct/overseerr/issues/403)) ([3778ad8](https://github.com/sct/overseerr/commit/3778ad829c0897de178212b3bde4c0d3b5089161)) +- **frontend:** add language picker to setup/login ([ff2ab29](https://github.com/sct/overseerr/commit/ff2ab29491a80c421525b9a394d6fbbf54914dc2)) +- **frontend:** add support overseerr block to about page ([c128898](https://github.com/sct/overseerr/commit/c128898206d6cbb482de4d8dca53f70b87e4911a)) +- **frontend:** releases added to about page ([b7f5739](https://github.com/sct/overseerr/commit/b7f573903500cc8a62e39afd787bc1da8c09d88b)), closes [#303](https://github.com/sct/overseerr/issues/303) +- **lang:** add support for Italian, Portuguese (Brazil) and Serbian ([108dfc4](https://github.com/sct/overseerr/commit/108dfc4afd31388cb6c9e07deccd168ade8b1574)) +- **lang:** add support for swedish language ([c9fe6cb](https://github.com/sct/overseerr/commit/c9fe6cb0b7ea984d8e4e1cb3f284935c9da7cc2b)) +- **lang:** translations update from Weblate ([#400](https://github.com/sct/overseerr/issues/400)) ([1bd0e64](https://github.com/sct/overseerr/commit/1bd0e646e313ddf77ef331e818e03401fbf64a72)) +- **lang:** translations update from Weblate ([#403](https://github.com/sct/overseerr/issues/403)) ([3778ad8](https://github.com/sct/overseerr/commit/3778ad829c0897de178212b3bde4c0d3b5089161)) # [1.10.0](https://github.com/sct/overseerr/compare/v1.9.1...v1.10.0) (2020-12-19) - ### Bug Fixes -* **email:** fix link to Overseerr in email templates ([816fec1](https://github.com/sct/overseerr/commit/816fec1a83a53edb3b65c3e5e7d0e6e1bd49726d)), closes [#392](https://github.com/sct/overseerr/issues/392) -* **frontend:** adjust padding of search box so placeholder text fits on mobile ([3601d44](https://github.com/sct/overseerr/commit/3601d442db32d3f98f7b050365c11ea8ef9bc4ae)), closes [#393](https://github.com/sct/overseerr/issues/393) -* **frontend:** changed request block for slideover on mobile UI ([#387](https://github.com/sct/overseerr/issues/387)) ([549567a](https://github.com/sct/overseerr/commit/549567a7e9db01933546d9970fc06f17218dfab1)) -* **frontend:** hide Request More button if all current seasons are available ([2a4dd52](https://github.com/sct/overseerr/commit/2a4dd52275007e48f946c3b9e29f1d78da57bdaa)), closes [#343](https://github.com/sct/overseerr/issues/343) -* **frontend:** try not to render broken rottentomatoes data ([a0c5608](https://github.com/sct/overseerr/commit/a0c5608aa0b6c7a4294300589efa9a662163ce48)) - +- **email:** fix link to Overseerr in email templates ([816fec1](https://github.com/sct/overseerr/commit/816fec1a83a53edb3b65c3e5e7d0e6e1bd49726d)), closes [#392](https://github.com/sct/overseerr/issues/392) +- **frontend:** adjust padding of search box so placeholder text fits on mobile ([3601d44](https://github.com/sct/overseerr/commit/3601d442db32d3f98f7b050365c11ea8ef9bc4ae)), closes [#393](https://github.com/sct/overseerr/issues/393) +- **frontend:** changed request block for slideover on mobile UI ([#387](https://github.com/sct/overseerr/issues/387)) ([549567a](https://github.com/sct/overseerr/commit/549567a7e9db01933546d9970fc06f17218dfab1)) +- **frontend:** hide Request More button if all current seasons are available ([2a4dd52](https://github.com/sct/overseerr/commit/2a4dd52275007e48f946c3b9e29f1d78da57bdaa)), closes [#343](https://github.com/sct/overseerr/issues/343) +- **frontend:** try not to render broken rottentomatoes data ([a0c5608](https://github.com/sct/overseerr/commit/a0c5608aa0b6c7a4294300589efa9a662163ce48)) ### Features -* **lang:** translations update from Weblate ([#391](https://github.com/sct/overseerr/issues/391)) ([5f71fb7](https://github.com/sct/overseerr/commit/5f71fb7ee280714275d2ac045c472fcdddd5a2ea)) -* add missing tzdata package to image ([53bede6](https://github.com/sct/overseerr/commit/53bede692d4f0e940dededa63015fe1908129914)), closes [#394](https://github.com/sct/overseerr/issues/394) -* **frontend:** add external links to movie and tv detail pages ([a0024a0](https://github.com/sct/overseerr/commit/a0024a0cbe717d78f53413bb78644c829f143c4d)) -* **lang:** translations update from Weblate ([#380](https://github.com/sct/overseerr/issues/380)) ([8408e19](https://github.com/sct/overseerr/commit/8408e19568b2f239c57e11e2946c75f193d1c22e)) +- **lang:** translations update from Weblate ([#391](https://github.com/sct/overseerr/issues/391)) ([5f71fb7](https://github.com/sct/overseerr/commit/5f71fb7ee280714275d2ac045c472fcdddd5a2ea)) +- add missing tzdata package to image ([53bede6](https://github.com/sct/overseerr/commit/53bede692d4f0e940dededa63015fe1908129914)), closes [#394](https://github.com/sct/overseerr/issues/394) +- **frontend:** add external links to movie and tv detail pages ([a0024a0](https://github.com/sct/overseerr/commit/a0024a0cbe717d78f53413bb78644c829f143c4d)) +- **lang:** translations update from Weblate ([#380](https://github.com/sct/overseerr/issues/380)) ([8408e19](https://github.com/sct/overseerr/commit/8408e19568b2f239c57e11e2946c75f193d1c22e)) ## [1.9.1](https://github.com/sct/overseerr/compare/v1.9.0...v1.9.1) (2020-12-18) - ### Bug Fixes -* change default internal port to 5055 ([#389](https://github.com/sct/overseerr/issues/389)) ([5e5ba40](https://github.com/sct/overseerr/commit/5e5ba4050563f07bff367d2fb31ed7e7fca4291e)) +- change default internal port to 5055 ([#389](https://github.com/sct/overseerr/issues/389)) ([5e5ba40](https://github.com/sct/overseerr/commit/5e5ba4050563f07bff367d2fb31ed7e7fca4291e)) # [1.9.0](https://github.com/sct/overseerr/compare/v1.8.0...v1.9.0) (2020-12-18) - ### Features -* api key regeneration ([6beac73](https://github.com/sct/overseerr/commit/6beac736efcf7b9102e02e43b75d91a9a158cd22)) -* **api:** add movie keyword search ([f88c4a6](https://github.com/sct/overseerr/commit/f88c4a6d4a49f8f3451ba6c85153677f33b7f5f6)) -* **frontend:** add studio/networks to movie/tv details ([4b6ad8a](https://github.com/sct/overseerr/commit/4b6ad8a3871957db4192b603abf38404250cea5d)), closes [#370](https://github.com/sct/overseerr/issues/370) -* **frontend:** added user deletion to the user list ([727fa06](https://github.com/sct/overseerr/commit/727fa06c18febb2a97ca219cc6bf0277ff462acd)), closes [#348](https://github.com/sct/overseerr/issues/348) -* **holiday:** special seasonal slider added to discover :) ([908f635](https://github.com/sct/overseerr/commit/908f63557ca03a1da8b16809ffa2c3acd782d94e)) -* allow to listen server on specific host interface ([#381](https://github.com/sct/overseerr/issues/381)) ([086183b](https://github.com/sct/overseerr/commit/086183b5636aa8d075d01fe59492c3eab0d1345b)), closes [#273](https://github.com/sct/overseerr/issues/273) -* anime profile support ([#384](https://github.com/sct/overseerr/issues/384)) ([0972f40](https://github.com/sct/overseerr/commit/0972f40a4e1fb3b5f02b07ae46b997d71aab9bfb)), closes [#266](https://github.com/sct/overseerr/issues/266) +- api key regeneration ([6beac73](https://github.com/sct/overseerr/commit/6beac736efcf7b9102e02e43b75d91a9a158cd22)) +- **api:** add movie keyword search ([f88c4a6](https://github.com/sct/overseerr/commit/f88c4a6d4a49f8f3451ba6c85153677f33b7f5f6)) +- **frontend:** add studio/networks to movie/tv details ([4b6ad8a](https://github.com/sct/overseerr/commit/4b6ad8a3871957db4192b603abf38404250cea5d)), closes [#370](https://github.com/sct/overseerr/issues/370) +- **frontend:** added user deletion to the user list ([727fa06](https://github.com/sct/overseerr/commit/727fa06c18febb2a97ca219cc6bf0277ff462acd)), closes [#348](https://github.com/sct/overseerr/issues/348) +- **holiday:** special seasonal slider added to discover :) ([908f635](https://github.com/sct/overseerr/commit/908f63557ca03a1da8b16809ffa2c3acd782d94e)) +- allow to listen server on specific host interface ([#381](https://github.com/sct/overseerr/issues/381)) ([086183b](https://github.com/sct/overseerr/commit/086183b5636aa8d075d01fe59492c3eab0d1345b)), closes [#273](https://github.com/sct/overseerr/issues/273) +- anime profile support ([#384](https://github.com/sct/overseerr/issues/384)) ([0972f40](https://github.com/sct/overseerr/commit/0972f40a4e1fb3b5f02b07ae46b997d71aab9bfb)), closes [#266](https://github.com/sct/overseerr/issues/266) # [1.8.0](https://github.com/sct/overseerr/compare/v1.7.0...v1.8.0) (2020-12-17) - ### Features -* **lang:** translations update from Weblate ([#336](https://github.com/sct/overseerr/issues/336)) ([ee84f74](https://github.com/sct/overseerr/commit/ee84f74f8a3558875b41daa539f42d00b949898a)) +- **lang:** translations update from Weblate ([#336](https://github.com/sct/overseerr/issues/336)) ([ee84f74](https://github.com/sct/overseerr/commit/ee84f74f8a3558875b41daa539f42d00b949898a)) # [1.7.0](https://github.com/sct/overseerr/compare/v1.6.0...v1.7.0) (2020-12-17) - ### Bug Fixes -* **email:** do not pass auth object to transport if no auth data present ([d5eb4d8](https://github.com/sct/overseerr/commit/d5eb4d8d438a159266b2de66b6bcdd9440a0c8ef)), closes [#312](https://github.com/sct/overseerr/issues/312) -* **frontend:** add http/https prefix to hostname fields for plex/radarr/sonarr ([ce0266f](https://github.com/sct/overseerr/commit/ce0266f74ea3979b291ff962271a928682892788)), closes [#357](https://github.com/sct/overseerr/issues/357) -* **frontend:** clarify that radarr/sonnarr servers must be tested before profiles/folders appear ([fc12ab8](https://github.com/sct/overseerr/commit/fc12ab84d9482eb3a11f117f8cab6fd48a9401cd)), closes [#326](https://github.com/sct/overseerr/issues/326) [#328](https://github.com/sct/overseerr/issues/328) -* **frontend:** correctly show an unauthorized error when a user fails to login ([18925de](https://github.com/sct/overseerr/commit/18925decafdac518f52a354c594cc378d2529022)), closes [#322](https://github.com/sct/overseerr/issues/322) -* **frontend:** fix tv shows failing to open when firstAirDate is undefined ([c21fa5b](https://github.com/sct/overseerr/commit/c21fa5b5350abdd8e03c077fde7246fa398e176e)), closes [#347](https://github.com/sct/overseerr/issues/347) -* **frontend:** make minimum availability required for Radarr servers ([2fe53ec](https://github.com/sct/overseerr/commit/2fe53ec5a8534e75c7d0cef31a8b46065111e0a7)), closes [#345](https://github.com/sct/overseerr/issues/345) -* **plex-sync:** bundle duplicate ratingKeys to speed up recently added sync ([67146c3](https://github.com/sct/overseerr/commit/67146c33ef7f28d520ba2c50b32673d43f4525c8)), closes [#360](https://github.com/sct/overseerr/issues/360) -* **sonarr.ts, mediarequest.ts:** add missing seasonFolder option ([#358](https://github.com/sct/overseerr/issues/358)) ([e9c899c](https://github.com/sct/overseerr/commit/e9c899ce419d149dde2ad9a0f7d5a2f2545b3ebf)) - +- **email:** do not pass auth object to transport if no auth data present ([d5eb4d8](https://github.com/sct/overseerr/commit/d5eb4d8d438a159266b2de66b6bcdd9440a0c8ef)), closes [#312](https://github.com/sct/overseerr/issues/312) +- **frontend:** add http/https prefix to hostname fields for plex/radarr/sonarr ([ce0266f](https://github.com/sct/overseerr/commit/ce0266f74ea3979b291ff962271a928682892788)), closes [#357](https://github.com/sct/overseerr/issues/357) +- **frontend:** clarify that radarr/sonnarr servers must be tested before profiles/folders appear ([fc12ab8](https://github.com/sct/overseerr/commit/fc12ab84d9482eb3a11f117f8cab6fd48a9401cd)), closes [#326](https://github.com/sct/overseerr/issues/326) [#328](https://github.com/sct/overseerr/issues/328) +- **frontend:** correctly show an unauthorized error when a user fails to login ([18925de](https://github.com/sct/overseerr/commit/18925decafdac518f52a354c594cc378d2529022)), closes [#322](https://github.com/sct/overseerr/issues/322) +- **frontend:** fix tv shows failing to open when firstAirDate is undefined ([c21fa5b](https://github.com/sct/overseerr/commit/c21fa5b5350abdd8e03c077fde7246fa398e176e)), closes [#347](https://github.com/sct/overseerr/issues/347) +- **frontend:** make minimum availability required for Radarr servers ([2fe53ec](https://github.com/sct/overseerr/commit/2fe53ec5a8534e75c7d0cef31a8b46065111e0a7)), closes [#345](https://github.com/sct/overseerr/issues/345) +- **plex-sync:** bundle duplicate ratingKeys to speed up recently added sync ([67146c3](https://github.com/sct/overseerr/commit/67146c33ef7f28d520ba2c50b32673d43f4525c8)), closes [#360](https://github.com/sct/overseerr/issues/360) +- **sonarr.ts, mediarequest.ts:** add missing seasonFolder option ([#358](https://github.com/sct/overseerr/issues/358)) ([e9c899c](https://github.com/sct/overseerr/commit/e9c899ce419d149dde2ad9a0f7d5a2f2545b3ebf)) ### Features -* **frontend:** show alert when there are no default radarr/sonarr servers ([0d088e0](https://github.com/sct/overseerr/commit/0d088e085e68d39455fda21d1fd08ebcaef2c06b)), closes [#344](https://github.com/sct/overseerr/issues/344) +- **frontend:** show alert when there are no default radarr/sonarr servers ([0d088e0](https://github.com/sct/overseerr/commit/0d088e085e68d39455fda21d1fd08ebcaef2c06b)), closes [#344](https://github.com/sct/overseerr/issues/344) # [1.6.0](https://github.com/sct/overseerr/compare/v1.5.0...v1.6.0) (2020-12-16) - ### Bug Fixes -* **api:** accept the api key to perform actions on the api with X-API-Key header ([33f8831](https://github.com/sct/overseerr/commit/33f8831e880dc7fd3f69d951246cada5c6c0ffe7)) -* **api:** filter out libraries that do not have any metadata agent or are not movie/show ([01c179f](https://github.com/sct/overseerr/commit/01c179f762e686a1e5a3d4dab3a5bea53425b575)) -* **api:** only run recently added sync on enabled libraries ([e08fa35](https://github.com/sct/overseerr/commit/e08fa35548bb8644afa8df3124e6f9cc3a2c8f4a)), closes [#259](https://github.com/sct/overseerr/issues/259) -* **api:** set plex libraries to disabled if the name changes ([675060b](https://github.com/sct/overseerr/commit/675060bcdf23acbfd4de2900a65f95e74f4966a5)), closes [#324](https://github.com/sct/overseerr/issues/324) -* **frontend:** adds a tip to plex setup to clarify that syncing runs in the background ([df4ac83](https://github.com/sct/overseerr/commit/df4ac8361f82971ee845f3be217408a9123a0bf3)), closes [#325](https://github.com/sct/overseerr/issues/325) -* **frontend:** aligned movie and tv details ([#331](https://github.com/sct/overseerr/issues/331)) ([db0a5c4](https://github.com/sct/overseerr/commit/db0a5c44f678e76eee7f5582381016306d1f46a2)) -* **frontend:** close sidebar when clicking outside ([#333](https://github.com/sct/overseerr/issues/333)) ([6d7907e](https://github.com/sct/overseerr/commit/6d7907e844a909993d185759d660632f55aeaa35)) -* spelling mistake on the word 'requested' fixed ([#319](https://github.com/sct/overseerr/issues/319)) ([961d110](https://github.com/sct/overseerr/commit/961d1107208069a6fc820a1ba97ffda7336677cb)) - +- **api:** accept the api key to perform actions on the api with X-API-Key header ([33f8831](https://github.com/sct/overseerr/commit/33f8831e880dc7fd3f69d951246cada5c6c0ffe7)) +- **api:** filter out libraries that do not have any metadata agent or are not movie/show ([01c179f](https://github.com/sct/overseerr/commit/01c179f762e686a1e5a3d4dab3a5bea53425b575)) +- **api:** only run recently added sync on enabled libraries ([e08fa35](https://github.com/sct/overseerr/commit/e08fa35548bb8644afa8df3124e6f9cc3a2c8f4a)), closes [#259](https://github.com/sct/overseerr/issues/259) +- **api:** set plex libraries to disabled if the name changes ([675060b](https://github.com/sct/overseerr/commit/675060bcdf23acbfd4de2900a65f95e74f4966a5)), closes [#324](https://github.com/sct/overseerr/issues/324) +- **frontend:** adds a tip to plex setup to clarify that syncing runs in the background ([df4ac83](https://github.com/sct/overseerr/commit/df4ac8361f82971ee845f3be217408a9123a0bf3)), closes [#325](https://github.com/sct/overseerr/issues/325) +- **frontend:** aligned movie and tv details ([#331](https://github.com/sct/overseerr/issues/331)) ([db0a5c4](https://github.com/sct/overseerr/commit/db0a5c44f678e76eee7f5582381016306d1f46a2)) +- **frontend:** close sidebar when clicking outside ([#333](https://github.com/sct/overseerr/issues/333)) ([6d7907e](https://github.com/sct/overseerr/commit/6d7907e844a909993d185759d660632f55aeaa35)) +- spelling mistake on the word 'requested' fixed ([#319](https://github.com/sct/overseerr/issues/319)) ([961d110](https://github.com/sct/overseerr/commit/961d1107208069a6fc820a1ba97ffda7336677cb)) ### Features -* add version to startup logs ([2948f93](https://github.com/sct/overseerr/commit/2948f9360eb484d1d6c0740a840135ca97e7240a)) -* **frontend:** temporary logs page to clear up confusion about it 404ing ([d9788c4](https://github.com/sct/overseerr/commit/d9788c4aa9f87e2eda3f7e3f1adc985f16039552)), closes [#272](https://github.com/sct/overseerr/issues/272) -* **lang:** add support for Spanish language ([6cd2049](https://github.com/sct/overseerr/commit/6cd20491d2a0ceb995c4744eeb92a6e2f57a4893)) -* **lang:** Translations update from Weblate ([#291](https://github.com/sct/overseerr/issues/291)) ([fddbb3c](https://github.com/sct/overseerr/commit/fddbb3cdfe3d50b2835c248556139c769dc2b805)) +- add version to startup logs ([2948f93](https://github.com/sct/overseerr/commit/2948f9360eb484d1d6c0740a840135ca97e7240a)) +- **frontend:** temporary logs page to clear up confusion about it 404ing ([d9788c4](https://github.com/sct/overseerr/commit/d9788c4aa9f87e2eda3f7e3f1adc985f16039552)), closes [#272](https://github.com/sct/overseerr/issues/272) +- **lang:** add support for Spanish language ([6cd2049](https://github.com/sct/overseerr/commit/6cd20491d2a0ceb995c4744eeb92a6e2f57a4893)) +- **lang:** Translations update from Weblate ([#291](https://github.com/sct/overseerr/issues/291)) ([fddbb3c](https://github.com/sct/overseerr/commit/fddbb3cdfe3d50b2835c248556139c769dc2b805)) # [1.5.0](https://github.com/sct/overseerr/compare/v1.4.0...v1.5.0) (2020-12-15) - ### Bug Fixes -* **api:** require package.json directly so typescript doesnt compile it into dist folder ([b9faa64](https://github.com/sct/overseerr/commit/b9faa6486b35aa865019aa8af9d307531054bc1d)) -* **frontend:** add validation for Radarr/Sonarr server name ([b5988f9](https://github.com/sct/overseerr/commit/b5988f9a5ff274e97f208c2726abe76c22c858ee)) -* **frontend:** only show alpha notice to admins ([ff61895](https://github.com/sct/overseerr/commit/ff618956b5d9cf933d867ea979b612c3d8a6f30b)) -* add support for ssl when connecting to plex ([3ba09d0](https://github.com/sct/overseerr/commit/3ba09d07eb0367c41603cd55e7ff41c66fb641c4)), closes [#275](https://github.com/sct/overseerr/issues/275) -* **services:** improve logging for when Radarr movie already exists ([#285](https://github.com/sct/overseerr/issues/285)) ([f998873](https://github.com/sct/overseerr/commit/f998873fc5669a547901f2733c9c785d744d27ca)), closes [#260](https://github.com/sct/overseerr/issues/260) - +- **api:** require package.json directly so typescript doesnt compile it into dist folder ([b9faa64](https://github.com/sct/overseerr/commit/b9faa6486b35aa865019aa8af9d307531054bc1d)) +- **frontend:** add validation for Radarr/Sonarr server name ([b5988f9](https://github.com/sct/overseerr/commit/b5988f9a5ff274e97f208c2726abe76c22c858ee)) +- **frontend:** only show alpha notice to admins ([ff61895](https://github.com/sct/overseerr/commit/ff618956b5d9cf933d867ea979b612c3d8a6f30b)) +- add support for ssl when connecting to plex ([3ba09d0](https://github.com/sct/overseerr/commit/3ba09d07eb0367c41603cd55e7ff41c66fb641c4)), closes [#275](https://github.com/sct/overseerr/issues/275) +- **services:** improve logging for when Radarr movie already exists ([#285](https://github.com/sct/overseerr/issues/285)) ([f998873](https://github.com/sct/overseerr/commit/f998873fc5669a547901f2733c9c785d744d27ca)), closes [#260](https://github.com/sct/overseerr/issues/260) ### Features -* **lang:** add i18n strings for new about page ([900827b](https://github.com/sct/overseerr/commit/900827be97845688e4bea72a8c5d9611a3e9d069)) -* about page initial version ([3f2a04c](https://github.com/sct/overseerr/commit/3f2a04c881bf06b73a952181fa463af84454b0dd)) +- **lang:** add i18n strings for new about page ([900827b](https://github.com/sct/overseerr/commit/900827be97845688e4bea72a8c5d9611a3e9d069)) +- about page initial version ([3f2a04c](https://github.com/sct/overseerr/commit/3f2a04c881bf06b73a952181fa463af84454b0dd)) # [1.4.0](https://github.com/sct/overseerr/compare/v1.3.2...v1.4.0) (2020-12-15) - ### Bug Fixes -* changing parameter name to use correct 'port' [#276](https://github.com/sct/overseerr/issues/276) ([#277](https://github.com/sct/overseerr/issues/277)) ([6d08b10](https://github.com/sct/overseerr/commit/6d08b108200177ca3068c852e60a0df75ce2232a)) -* **services:** include radarr/sonarr baseUrl when adding media ([78af1a3](https://github.com/sct/overseerr/commit/78af1a3e6d00a5645a05e7bf3cf56a59439b6cc9)) - +- changing parameter name to use correct 'port' [#276](https://github.com/sct/overseerr/issues/276) ([#277](https://github.com/sct/overseerr/issues/277)) ([6d08b10](https://github.com/sct/overseerr/commit/6d08b108200177ca3068c852e60a0df75ce2232a)) +- **services:** include radarr/sonarr baseUrl when adding media ([78af1a3](https://github.com/sct/overseerr/commit/78af1a3e6d00a5645a05e7bf3cf56a59439b6cc9)) ### Features -* **lang:** Translations update from Weblate ([#240](https://github.com/sct/overseerr/issues/240)) ([e17c637](https://github.com/sct/overseerr/commit/e17c63748362b6a480693e003ef5eec614dcec43)) +- **lang:** Translations update from Weblate ([#240](https://github.com/sct/overseerr/issues/240)) ([e17c637](https://github.com/sct/overseerr/commit/e17c63748362b6a480693e003ef5eec614dcec43)) ## [1.3.2](https://github.com/sct/overseerr/compare/v1.3.1...v1.3.2) (2020-12-14) - ### Bug Fixes -* **frontend:** convert plex port to a number before posting to the api ([8cb05c4](https://github.com/sct/overseerr/commit/8cb05c413a15a4b74e37ece5e24367d115995b32)) -* **frontend:** converts email smtp port to a number before posting to the api ([2098a2d](https://github.com/sct/overseerr/commit/2098a2d3d2981fd2ae54392aec3ef81327f2858e)), closes [#251](https://github.com/sct/overseerr/issues/251) -* **frontend:** encode special characters in search input to prevent crashing router ([15013d6](https://github.com/sct/overseerr/commit/15013d6c5dbff15704c7c30d261d68a265e7f2d7)), closes [#252](https://github.com/sct/overseerr/issues/252) -* **plex sync:** catch errors that occur during processMovie ([edbbccf](https://github.com/sct/overseerr/commit/edbbccf3ae623430294f1a5c3fd2728dbd42e555)), closes [#244](https://github.com/sct/overseerr/issues/244) [#246](https://github.com/sct/overseerr/issues/246) [#250](https://github.com/sct/overseerr/issues/250) -* **services:** improve logging for adding movies to Radarr ([6c1ee83](https://github.com/sct/overseerr/commit/6c1ee830a183f89bb1fe96a181a7d61684e23b22)) -* **services:** radarr/sonarr will use the correct default server ([0658b79](https://github.com/sct/overseerr/commit/0658b7943e1ab25816db9da34d4c9ea808d9203d)) +- **frontend:** convert plex port to a number before posting to the api ([8cb05c4](https://github.com/sct/overseerr/commit/8cb05c413a15a4b74e37ece5e24367d115995b32)) +- **frontend:** converts email smtp port to a number before posting to the api ([2098a2d](https://github.com/sct/overseerr/commit/2098a2d3d2981fd2ae54392aec3ef81327f2858e)), closes [#251](https://github.com/sct/overseerr/issues/251) +- **frontend:** encode special characters in search input to prevent crashing router ([15013d6](https://github.com/sct/overseerr/commit/15013d6c5dbff15704c7c30d261d68a265e7f2d7)), closes [#252](https://github.com/sct/overseerr/issues/252) +- **plex sync:** catch errors that occur during processMovie ([edbbccf](https://github.com/sct/overseerr/commit/edbbccf3ae623430294f1a5c3fd2728dbd42e555)), closes [#244](https://github.com/sct/overseerr/issues/244) [#246](https://github.com/sct/overseerr/issues/246) [#250](https://github.com/sct/overseerr/issues/250) +- **services:** improve logging for adding movies to Radarr ([6c1ee83](https://github.com/sct/overseerr/commit/6c1ee830a183f89bb1fe96a181a7d61684e23b22)) +- **services:** radarr/sonarr will use the correct default server ([0658b79](https://github.com/sct/overseerr/commit/0658b7943e1ab25816db9da34d4c9ea808d9203d)) ## [1.3.1](https://github.com/sct/overseerr/compare/v1.3.0...v1.3.1) (2020-12-14) - ### Bug Fixes -* **frontend:** also convert activeProfileId to a number for radarr/sonarr submissions ([7bf924f](https://github.com/sct/overseerr/commit/7bf924f7e94a0e0834f41b4ec067ed277c652766)) -* **frontend:** also convert ports to numbers when saving radarr/sonarr servers ([c53dc3b](https://github.com/sct/overseerr/commit/c53dc3b15da522c6e6ab76bbc9d15008a8a9fb9d)) -* **frontend:** new radarr/sonarr ports will be converted to a number before posting ([92c9001](https://github.com/sct/overseerr/commit/92c9001c9d1f2cbd272a5897ea1157d2cadbce2d)) +- **frontend:** also convert activeProfileId to a number for radarr/sonarr submissions ([7bf924f](https://github.com/sct/overseerr/commit/7bf924f7e94a0e0834f41b4ec067ed277c652766)) +- **frontend:** also convert ports to numbers when saving radarr/sonarr servers ([c53dc3b](https://github.com/sct/overseerr/commit/c53dc3b15da522c6e6ab76bbc9d15008a8a9fb9d)) +- **frontend:** new radarr/sonarr ports will be converted to a number before posting ([92c9001](https://github.com/sct/overseerr/commit/92c9001c9d1f2cbd272a5897ea1157d2cadbce2d)) # [1.3.0](https://github.com/sct/overseerr/compare/v1.2.0...v1.3.0) (2020-12-14) - ### Bug Fixes -* **api:** correctly generate clientId on first startup ([5f09e83](https://github.com/sct/overseerr/commit/5f09e83ed870336638d3e9d94fcf55ead928e737)) - +- **api:** correctly generate clientId on first startup ([5f09e83](https://github.com/sct/overseerr/commit/5f09e83ed870336638d3e9d94fcf55ead928e737)) ### Features -* **frontend:** add full cast page for movies and series ([051f1b3](https://github.com/sct/overseerr/commit/051f1b3e899bf749e632743e5c8d45a02b621998)) -* **lang:** translated using Weblate (Dutch) ([1ab3a4b](https://github.com/sct/overseerr/commit/1ab3a4b80a081d7e4a201f1290cd270ed5b38ac7)) -* **lang:** translated using Weblate (English) ([0949c9b](https://github.com/sct/overseerr/commit/0949c9b334b3a4b6c342517a157a9e2b7596f2f0)) -* **lang:** translated using Weblate (French) ([f943701](https://github.com/sct/overseerr/commit/f943701e13c7f0de5a711302597858cc898b16e2)) -* **lang:** translated using Weblate (French) ([30d04ce](https://github.com/sct/overseerr/commit/30d04ce35adc21070cce37ab10384154afda191b)) -* **lang:** translated using Weblate (German) ([7bf9add](https://github.com/sct/overseerr/commit/7bf9addd13a707aac23b64ef3f1733e491d40a4e)) -* **lang:** translated using Weblate (German) ([b6e60a4](https://github.com/sct/overseerr/commit/b6e60a412b30907aea751a4cf1ce0cc8230f9814)) -* **lang:** translated using Weblate (Japanese) ([08e968f](https://github.com/sct/overseerr/commit/08e968fd0097ec7b2a65de064ed5b07e7c49ef39)) -* **lang:** translated using Weblate (Norwegian Bokmål) ([83efb0e](https://github.com/sct/overseerr/commit/83efb0e3d4d96b6a2d2ebdd85d36c9d78c1717b2)) -* **lang:** translated using Weblate (Russian) ([0d8e0d0](https://github.com/sct/overseerr/commit/0d8e0d0352f72fdb65ee8f054371eae08c39fe33)) +- **frontend:** add full cast page for movies and series ([051f1b3](https://github.com/sct/overseerr/commit/051f1b3e899bf749e632743e5c8d45a02b621998)) +- **lang:** translated using Weblate (Dutch) ([1ab3a4b](https://github.com/sct/overseerr/commit/1ab3a4b80a081d7e4a201f1290cd270ed5b38ac7)) +- **lang:** translated using Weblate (English) ([0949c9b](https://github.com/sct/overseerr/commit/0949c9b334b3a4b6c342517a157a9e2b7596f2f0)) +- **lang:** translated using Weblate (French) ([f943701](https://github.com/sct/overseerr/commit/f943701e13c7f0de5a711302597858cc898b16e2)) +- **lang:** translated using Weblate (French) ([30d04ce](https://github.com/sct/overseerr/commit/30d04ce35adc21070cce37ab10384154afda191b)) +- **lang:** translated using Weblate (German) ([7bf9add](https://github.com/sct/overseerr/commit/7bf9addd13a707aac23b64ef3f1733e491d40a4e)) +- **lang:** translated using Weblate (German) ([b6e60a4](https://github.com/sct/overseerr/commit/b6e60a412b30907aea751a4cf1ce0cc8230f9814)) +- **lang:** translated using Weblate (Japanese) ([08e968f](https://github.com/sct/overseerr/commit/08e968fd0097ec7b2a65de064ed5b07e7c49ef39)) +- **lang:** translated using Weblate (Norwegian Bokmål) ([83efb0e](https://github.com/sct/overseerr/commit/83efb0e3d4d96b6a2d2ebdd85d36c9d78c1717b2)) +- **lang:** translated using Weblate (Russian) ([0d8e0d0](https://github.com/sct/overseerr/commit/0d8e0d0352f72fdb65ee8f054371eae08c39fe33)) # [1.2.0](https://github.com/sct/overseerr/compare/v1.1.0...v1.2.0) (2020-12-11) - ### Bug Fixes -* **frontend:** person cards now show correctly in ListView's ([ccb9855](https://github.com/sct/overseerr/commit/ccb98553f104c1aebd33796b7090cc9bbe964bd7)) -* **frontend:** properly remove site overlay when closing modals ([3fa7ff9](https://github.com/sct/overseerr/commit/3fa7ff9858d14d132151f3329164d55d74638f53)) -* **frontend:** switch to using Transition component for modals ([b16fbaf](https://github.com/sct/overseerr/commit/b16fbafa1f3d5e105c0a4ba6f1d66aa064019636)), closes [#220](https://github.com/sct/overseerr/issues/220) -* fix missing personid in Discover ([d8060af](https://github.com/sct/overseerr/commit/d8060afe02574337f51b88cab0a0f824976ac721)) -* missing personId in ListView component ([6502feb](https://github.com/sct/overseerr/commit/6502feb1a5be3c6daab33230814fe74632c87f7e)) -* **frontend:** update overflow issues with seasons + email ([#217](https://github.com/sct/overseerr/issues/217)) ([2d0afb2](https://github.com/sct/overseerr/commit/2d0afb29d37798a626e3f182571ccce43d80063c)), closes [#216](https://github.com/sct/overseerr/issues/216) -* **lang:** fix missing i18n string for agent enabled in email notification page ([42788ad](https://github.com/sct/overseerr/commit/42788adb75f7d23e68327688b1c542dd047e9609)) - +- **frontend:** person cards now show correctly in ListView's ([ccb9855](https://github.com/sct/overseerr/commit/ccb98553f104c1aebd33796b7090cc9bbe964bd7)) +- **frontend:** properly remove site overlay when closing modals ([3fa7ff9](https://github.com/sct/overseerr/commit/3fa7ff9858d14d132151f3329164d55d74638f53)) +- **frontend:** switch to using Transition component for modals ([b16fbaf](https://github.com/sct/overseerr/commit/b16fbafa1f3d5e105c0a4ba6f1d66aa064019636)), closes [#220](https://github.com/sct/overseerr/issues/220) +- fix missing personid in Discover ([d8060af](https://github.com/sct/overseerr/commit/d8060afe02574337f51b88cab0a0f824976ac721)) +- missing personId in ListView component ([6502feb](https://github.com/sct/overseerr/commit/6502feb1a5be3c6daab33230814fe74632c87f7e)) +- **frontend:** update overflow issues with seasons + email ([#217](https://github.com/sct/overseerr/issues/217)) ([2d0afb2](https://github.com/sct/overseerr/commit/2d0afb29d37798a626e3f182571ccce43d80063c)), closes [#216](https://github.com/sct/overseerr/issues/216) +- **lang:** fix missing i18n string for agent enabled in email notification page ([42788ad](https://github.com/sct/overseerr/commit/42788adb75f7d23e68327688b1c542dd047e9609)) ### Features -* **lang:** update language files ([8cd067b](https://github.com/sct/overseerr/commit/8cd067b6e9df1a3c8f4056789436a31177703986)) -* person details page ([d6eb3ae](https://github.com/sct/overseerr/commit/d6eb3ae64ef46bd62145010d3029e272676487c3)) -* **lang:** add nb-NO and de language support to app ([d38b28d](https://github.com/sct/overseerr/commit/d38b28d2061b38366989ff412957a5dee5766c6f)) -* **lang:** add support for dutch language ([df94db0](https://github.com/sct/overseerr/commit/df94db050bf68a925118e0ce865d27178b702f9e)) -* **lang:** add support for russian languge ([8d8e750](https://github.com/sct/overseerr/commit/8d8e7509826514eebc859374d2e1ab212cc442d1)) -* **lang:** added translation using Weblate (Russian) ([887f5dd](https://github.com/sct/overseerr/commit/887f5dd487b61676029652d99cbc5b40213aa22e)) -* **lang:** translated using Weblate (French) ([30a8934](https://github.com/sct/overseerr/commit/30a8934626fa2d47e95b5925d7e4227a0d0aa728)) -* **lang:** translated using Weblate (German) ([44dbb74](https://github.com/sct/overseerr/commit/44dbb745b6216ce19fab4740520785c6414cf367)) -* **lang:** translated using Weblate (Japanese) ([a494507](https://github.com/sct/overseerr/commit/a494507dfeafb0cfd2bd66fb01138522e0e80737)) -* **lang:** translated using Weblate (Russian) ([86cadb8](https://github.com/sct/overseerr/commit/86cadb8283fcab8745b4c09f8429fd9e46708813)) -* **lang:** translations update from Weblate ([#201](https://github.com/sct/overseerr/issues/201)) ([b0c663b](https://github.com/sct/overseerr/commit/b0c663baccd994e234b4d41d86486c3af4906344)) +- **lang:** update language files ([8cd067b](https://github.com/sct/overseerr/commit/8cd067b6e9df1a3c8f4056789436a31177703986)) +- person details page ([d6eb3ae](https://github.com/sct/overseerr/commit/d6eb3ae64ef46bd62145010d3029e272676487c3)) +- **lang:** add nb-NO and de language support to app ([d38b28d](https://github.com/sct/overseerr/commit/d38b28d2061b38366989ff412957a5dee5766c6f)) +- **lang:** add support for dutch language ([df94db0](https://github.com/sct/overseerr/commit/df94db050bf68a925118e0ce865d27178b702f9e)) +- **lang:** add support for russian languge ([8d8e750](https://github.com/sct/overseerr/commit/8d8e7509826514eebc859374d2e1ab212cc442d1)) +- **lang:** added translation using Weblate (Russian) ([887f5dd](https://github.com/sct/overseerr/commit/887f5dd487b61676029652d99cbc5b40213aa22e)) +- **lang:** translated using Weblate (French) ([30a8934](https://github.com/sct/overseerr/commit/30a8934626fa2d47e95b5925d7e4227a0d0aa728)) +- **lang:** translated using Weblate (German) ([44dbb74](https://github.com/sct/overseerr/commit/44dbb745b6216ce19fab4740520785c6414cf367)) +- **lang:** translated using Weblate (Japanese) ([a494507](https://github.com/sct/overseerr/commit/a494507dfeafb0cfd2bd66fb01138522e0e80737)) +- **lang:** translated using Weblate (Russian) ([86cadb8](https://github.com/sct/overseerr/commit/86cadb8283fcab8745b4c09f8429fd9e46708813)) +- **lang:** translations update from Weblate ([#201](https://github.com/sct/overseerr/issues/201)) ([b0c663b](https://github.com/sct/overseerr/commit/b0c663baccd994e234b4d41d86486c3af4906344)) # [1.1.0](https://github.com/sct/overseerr/compare/v1.0.0...v1.1.0) (2020-12-08) - ### Bug Fixes -* fix a few misc unused imports and useless assignments/conditionals ([8e6daf7](https://github.com/sct/overseerr/commit/8e6daf7bd271ce5bebf4a00f5bb1144bd6b60aa5)) -* **frontend:** dont show delete button in request list for users without correct permission ([83fde46](https://github.com/sct/overseerr/commit/83fde46a59c6f1910806a6106b5526b8adbc386c)) -* **frontend:** push updated i18n locale files ([b4002d7](https://github.com/sct/overseerr/commit/b4002d71323a04e7991198cedc263660e872df8d)) - +- fix a few misc unused imports and useless assignments/conditionals ([8e6daf7](https://github.com/sct/overseerr/commit/8e6daf7bd271ce5bebf4a00f5bb1144bd6b60aa5)) +- **frontend:** dont show delete button in request list for users without correct permission ([83fde46](https://github.com/sct/overseerr/commit/83fde46a59c6f1910806a6106b5526b8adbc386c)) +- **frontend:** push updated i18n locale files ([b4002d7](https://github.com/sct/overseerr/commit/b4002d71323a04e7991198cedc263660e872df8d)) ### Features -* generate real api key ([a839370](https://github.com/sct/overseerr/commit/a8393707fec85a9262af5ba8c03d205190b2235b)) -* **frontend:** add i18n strings for request list and request item ([6c4022f](https://github.com/sct/overseerr/commit/6c4022fb236583ad20d4c4c6693c1339e165b4af)) -* **frontend:** initial version of the requests page (no filtering/sorting) ([1ba027b](https://github.com/sct/overseerr/commit/1ba027b4357e078c3f177d9d07208049f0c1ce65)) -* **frontend:** only load request/tmdb cards when in the browser view ([2d51efd](https://github.com/sct/overseerr/commit/2d51efd71612ec969b83c62d6aa0dac6df9391a3)) +- generate real api key ([a839370](https://github.com/sct/overseerr/commit/a8393707fec85a9262af5ba8c03d205190b2235b)) +- **frontend:** add i18n strings for request list and request item ([6c4022f](https://github.com/sct/overseerr/commit/6c4022fb236583ad20d4c4c6693c1339e165b4af)) +- **frontend:** initial version of the requests page (no filtering/sorting) ([1ba027b](https://github.com/sct/overseerr/commit/1ba027b4357e078c3f177d9d07208049f0c1ce65)) +- **frontend:** only load request/tmdb cards when in the browser view ([2d51efd](https://github.com/sct/overseerr/commit/2d51efd71612ec969b83c62d6aa0dac6df9391a3)) # 1.0.0 (2020-12-06) - ### Bug Fixes -* **api:** fix scheduling for plex full sync (maybe) ([7287a6a](https://github.com/sct/overseerr/commit/7287a6a95703b23acc0c4f6eb3beb9ec2295e33f)) -* **frontend:** always show request modal option for tv ([2b46268](https://github.com/sct/overseerr/commit/2b462688243531b4be620a942f59defd4e0534d0)) -* **frontend:** canceled movie request should set parent movie status back to unknown ([#198](https://github.com/sct/overseerr/issues/198)) ([139871f](https://github.com/sct/overseerr/commit/139871f218812a15f742aa66408db12704e0b9b5)) -* **frontend:** close request modals when complete ([85ae499](https://github.com/sct/overseerr/commit/85ae4998f0ba8d4869b9b244f2c440b9df1310d2)) -* **frontend:** dont show runtime if there is no runtime data ([e0c39ae](https://github.com/sct/overseerr/commit/e0c39aeca119b822f2a54ff05a97f91780ddd052)) -* **frontend:** fix missing data for request modal title i18n ([a56fd16](https://github.com/sct/overseerr/commit/a56fd16ab6638d4649fe9f8b9d75e7cae7742f73)) -* **frontend:** fix missing import for ReactNode type in Slider ([b26a234](https://github.com/sct/overseerr/commit/b26a2347e7b0f7ff8720a204d9faefd501ba886c)) -* **frontend:** fix modal design and rename some text for adding servers ([46d99b0](https://github.com/sct/overseerr/commit/46d99b02b1c992c7b8dde2150217ed9ce326b7a5)) -* **frontend:** fix opening popups on safari ([364d9d1](https://github.com/sct/overseerr/commit/364d9d105ca3690fcd5f635485d7c025353bb9f1)) -* **frontend:** fix request card placeholder sizes for mobile ([ef62c67](https://github.com/sct/overseerr/commit/ef62c67480ed52d753ea6db8205f035b2e9da272)) -* **frontend:** show a badge on requestcard for partially available status ([59056c4](https://github.com/sct/overseerr/commit/59056c44f942a37df536ff947b5faccc27f32246)) -* dont cross import SyncStatus type ([e032e38](https://github.com/sct/overseerr/commit/e032e385a5253d215490255c676f42ee48f39428)) -* fix type import from server side crashing build process ([89be56d](https://github.com/sct/overseerr/commit/89be56d8403ebc60c411e7cb357593edd9c79bb2)) -* **frontend:** fix title detail background image to be centered ([b92f64f](https://github.com/sct/overseerr/commit/b92f64fa6e167bc89168d8f5c0f2eb12efa0b6f0)) -* **frontend:** fixed similar/recommendations showing when empty ([#180](https://github.com/sct/overseerr/issues/180)) ([a3ca9b4](https://github.com/sct/overseerr/commit/a3ca9b40c552e6cc5effc2f57f7562ff6f723e42)) -* **frontend:** have tvDetail use the new RequestModal ([6aca826](https://github.com/sct/overseerr/commit/6aca82607b97d4a4ad74e2ea843d52fba4689e6a)) -* **frontend:** reinitalize plex form after data loads ([97e3036](https://github.com/sct/overseerr/commit/97e30367fb5d2d27efc42c1d76b0d051b6f1da76)) -* **frontend:** remove requestId from tilecard request modal component ([61b6152](https://github.com/sct/overseerr/commit/61b6152e8915c99585b944756a61d33b8c8a0307)) -* **frontend:** run initial props for children components after getting the user ([fdf9f38](https://github.com/sct/overseerr/commit/fdf9f38776b6d4c08b3505c03b354639cebb011f)) -* **frontend:** when there were no results in the list view, it would call fetch more infinitely ([c0ce87b](https://github.com/sct/overseerr/commit/c0ce87b6f65bf0ab1301c7ca61090d779709529f)) -* fixed an issue with eslint-prettier on windows ([#32](https://github.com/sct/overseerr/issues/32)) ([b673ea1](https://github.com/sct/overseerr/commit/b673ea1b18ca0f432996bb9e4e5d148af0247170)) -* fixes next.js build to not include server files ([de8ee9b](https://github.com/sct/overseerr/commit/de8ee9ba85e0160b0b472cab44f92c01796efec8)) - +- **api:** fix scheduling for plex full sync (maybe) ([7287a6a](https://github.com/sct/overseerr/commit/7287a6a95703b23acc0c4f6eb3beb9ec2295e33f)) +- **frontend:** always show request modal option for tv ([2b46268](https://github.com/sct/overseerr/commit/2b462688243531b4be620a942f59defd4e0534d0)) +- **frontend:** canceled movie request should set parent movie status back to unknown ([#198](https://github.com/sct/overseerr/issues/198)) ([139871f](https://github.com/sct/overseerr/commit/139871f218812a15f742aa66408db12704e0b9b5)) +- **frontend:** close request modals when complete ([85ae499](https://github.com/sct/overseerr/commit/85ae4998f0ba8d4869b9b244f2c440b9df1310d2)) +- **frontend:** dont show runtime if there is no runtime data ([e0c39ae](https://github.com/sct/overseerr/commit/e0c39aeca119b822f2a54ff05a97f91780ddd052)) +- **frontend:** fix missing data for request modal title i18n ([a56fd16](https://github.com/sct/overseerr/commit/a56fd16ab6638d4649fe9f8b9d75e7cae7742f73)) +- **frontend:** fix missing import for ReactNode type in Slider ([b26a234](https://github.com/sct/overseerr/commit/b26a2347e7b0f7ff8720a204d9faefd501ba886c)) +- **frontend:** fix modal design and rename some text for adding servers ([46d99b0](https://github.com/sct/overseerr/commit/46d99b02b1c992c7b8dde2150217ed9ce326b7a5)) +- **frontend:** fix opening popups on safari ([364d9d1](https://github.com/sct/overseerr/commit/364d9d105ca3690fcd5f635485d7c025353bb9f1)) +- **frontend:** fix request card placeholder sizes for mobile ([ef62c67](https://github.com/sct/overseerr/commit/ef62c67480ed52d753ea6db8205f035b2e9da272)) +- **frontend:** show a badge on requestcard for partially available status ([59056c4](https://github.com/sct/overseerr/commit/59056c44f942a37df536ff947b5faccc27f32246)) +- dont cross import SyncStatus type ([e032e38](https://github.com/sct/overseerr/commit/e032e385a5253d215490255c676f42ee48f39428)) +- fix type import from server side crashing build process ([89be56d](https://github.com/sct/overseerr/commit/89be56d8403ebc60c411e7cb357593edd9c79bb2)) +- **frontend:** fix title detail background image to be centered ([b92f64f](https://github.com/sct/overseerr/commit/b92f64fa6e167bc89168d8f5c0f2eb12efa0b6f0)) +- **frontend:** fixed similar/recommendations showing when empty ([#180](https://github.com/sct/overseerr/issues/180)) ([a3ca9b4](https://github.com/sct/overseerr/commit/a3ca9b40c552e6cc5effc2f57f7562ff6f723e42)) +- **frontend:** have tvDetail use the new RequestModal ([6aca826](https://github.com/sct/overseerr/commit/6aca82607b97d4a4ad74e2ea843d52fba4689e6a)) +- **frontend:** reinitalize plex form after data loads ([97e3036](https://github.com/sct/overseerr/commit/97e30367fb5d2d27efc42c1d76b0d051b6f1da76)) +- **frontend:** remove requestId from tilecard request modal component ([61b6152](https://github.com/sct/overseerr/commit/61b6152e8915c99585b944756a61d33b8c8a0307)) +- **frontend:** run initial props for children components after getting the user ([fdf9f38](https://github.com/sct/overseerr/commit/fdf9f38776b6d4c08b3505c03b354639cebb011f)) +- **frontend:** when there were no results in the list view, it would call fetch more infinitely ([c0ce87b](https://github.com/sct/overseerr/commit/c0ce87b6f65bf0ab1301c7ca61090d779709529f)) +- fixed an issue with eslint-prettier on windows ([#32](https://github.com/sct/overseerr/issues/32)) ([b673ea1](https://github.com/sct/overseerr/commit/b673ea1b18ca0f432996bb9e4e5d148af0247170)) +- fixes next.js build to not include server files ([de8ee9b](https://github.com/sct/overseerr/commit/de8ee9ba85e0160b0b472cab44f92c01796efec8)) ### Features -* add migration for delete cascades on season requests/seasons ([c688cf6](https://github.com/sct/overseerr/commit/c688cf60c710f0cf0b2da5ba6b0c18a2d137e7f9)) -* **api:** email notification agent ([0962392](https://github.com/sct/overseerr/commit/0962392e3930c7fdcb3164b9143cc8faca38bdfa)) -* **frontend:** add french language file ([cd6d8a8](https://github.com/sct/overseerr/commit/cd6d8a8216e7ae183b046d26cd22f3c1dc1d2b35)) -* **frontend:** add translatable strings for request card ([0d2f360](https://github.com/sct/overseerr/commit/0d2f360c22cd9bb50ae04f00a25e5fcc6c21bcdd)) -* **frontend:** added more localized strings ([659a601](https://github.com/sct/overseerr/commit/659a6018777718f7a90141307678d8dadcfd77f8)) -* actually include email templates in built server files ([a28a8b3](https://github.com/sct/overseerr/commit/a28a8b37b0afc79583e4a7191a91f73ff6d3adad)) -* add application url config to main settings ui ([a359672](https://github.com/sct/overseerr/commit/a359672ebafffef742858814f0faa918e0341aa3)) -* add filtering for requests api ([cb9ae25](https://github.com/sct/overseerr/commit/cb9ae25d94f21e97113dfea3ca45c7002089e344)) -* add trending to discover page ([ff8b9d8](https://github.com/sct/overseerr/commit/ff8b9d8e7ed228a153c2da4d237f7a4f99a79321)) -* force setup if app is not initialized ([a99705f](https://github.com/sct/overseerr/commit/a99705f6a5674b436ae28cbc558f4ee6e99ac910)) -* initial user list (no edit/delete yet) and job schedules ([24a0423](https://github.com/sct/overseerr/commit/24a0423f3b14303cfb0e83aef6e9e3bb273c5ba9)) -* manage series slideover added (and approve/decline/delete hooked up) ([236c4e5](https://github.com/sct/overseerr/commit/236c4e5e6126d2424a4badc08b7f7e6d1d70f401)) -* media delete option in manage media slideover ([250f484](https://github.com/sct/overseerr/commit/250f48492c95d74e40d95d3f026d2952157bc6e1)) -* other email notifications for approved/available ([0d73d88](https://github.com/sct/overseerr/commit/0d73d88f35b03e993f305873dc72672003c7d9e5)) -* radarr edit/create modal/backend functionality ([c4ac357](https://github.com/sct/overseerr/commit/c4ac357ef4cdd7a2c610260db46a4f0c325cd785)) -* season creation migration ([978f92a](https://github.com/sct/overseerr/commit/978f92a1c589ac404a3cb1103a68a8a5ffb0dd7d)) -* sonarr edit/delete modal ([3204326](https://github.com/sct/overseerr/commit/320432657e6ccf4d255238098e03590f28267bdb)) -* throw 404 when movie/tv show doesnt exist ([0601b44](https://github.com/sct/overseerr/commit/0601b446873e2eaf042044dd6a995b713586b0cc)) -* **api:** sonarr api wrapper / send to sonarr ([9385592](https://github.com/sct/overseerr/commit/9385592362eeba1dba05c5aa8fc7a2de1d054d74)) -* **frontend:** add header styling to movie/tv recommendation and similar list views ([f5f2545](https://github.com/sct/overseerr/commit/f5f2545520a43daa23e1276d24ff60d794ebbc6e)) -* **frontend:** add links to detail pages from new request card ([6ad3384](https://github.com/sct/overseerr/commit/6ad3384a78f7bcb03f409cce8b35cc61d634d6b2)) -* **frontend:** new design for request card ([93738e1](https://github.com/sct/overseerr/commit/93738e154c41fd11d5c6cf3d35573daf54ead471)) -* **frontend:** update favicon ([886389a](https://github.com/sct/overseerr/commit/886389a361da54c616da3bdfeee9a85e9d12bcf3)) -* notification framework ([d8e542e](https://github.com/sct/overseerr/commit/d8e542e5fe2ed76dcb20fb6dfc5f59430cd4245d)) -* notifications for media_available and media_approved ([a6c5e65](https://github.com/sct/overseerr/commit/a6c5e65bbfc196545471e99fe2e5b7194f9dd387)) -* rotten tomatoes scores on movie/tv details pages ([1694f60](https://github.com/sct/overseerr/commit/1694f60e8aa475ceeb7f170a783ec0ba70bd4bce)) -* upcoming movies on discover ([67290dd](https://github.com/sct/overseerr/commit/67290dd502571a22dcf8559ac07f42e855275bd0)) -* upcoming/trending list views and larger title cards ([94eaaf9](https://github.com/sct/overseerr/commit/94eaaf96b4302a832c52ccb72009b3593452c779)) -* upgrade tailwindcss to 2.0.1 ([fb5c791](https://github.com/sct/overseerr/commit/fb5c791b0b6b7593a472bf01713999a001f92dc7)) -* user edit functionality (managing permissions) ([185ac26](https://github.com/sct/overseerr/commit/185ac2648fd21c4bf9692ac5ac055e9c740065ca)) -* **api:** plex tv sync and recently added sync ([1390cc1](https://github.com/sct/overseerr/commit/1390cc1f130bb3975996e84b12ac833f55f2f753)) -* **frontend:** allow permission check for showing nav items ([0b239f0](https://github.com/sct/overseerr/commit/0b239f0bdfb1394897bce5c50b0d112abfbb4ad7)) -* **frontend:** alpha notice ([33da7e9](https://github.com/sct/overseerr/commit/33da7e9df3a2546b0f208bd3b1d1f268e343cead)) -* **frontend:** buttonWithDropdown component added (no hookups yet) ([4975841](https://github.com/sct/overseerr/commit/4975841b5d4ba4ed1ba8cacaa5a063eeb3b8c311)) -* **frontend:** cancel movie request modal ([1f9cbbf](https://github.com/sct/overseerr/commit/1f9cbbfdf1ac98e54de5b8777c52c7bfc69c7e20)) -* **frontend:** improved settings menu design for mobile ([16221a4](https://github.com/sct/overseerr/commit/16221a46a7d57c77f53aa0186263aa27267d9863)) -* **frontend:** initial Settings design ([8742da0](https://github.com/sct/overseerr/commit/8742da0ebb92d2f78309a998de0f67e788e14376)) -* **frontend:** plex library scan ([1bc3f7b](https://github.com/sct/overseerr/commit/1bc3f7be4b07211563a1e254c28ce51e1bc337a2)) -* **frontend:** plex settings page ([47714b6](https://github.com/sct/overseerr/commit/47714b698cf4351c1ee38bdf0b672d9f0baed03a)) -* **frontend:** radarr delete modal ([877a518](https://github.com/sct/overseerr/commit/877a5184158fb4aa371fa2ea2107032543c9aa37)) -* **frontend:** recently added on discover ([06dc606](https://github.com/sct/overseerr/commit/06dc606bcfeb50b7be1c35ac180c10738bade458)) -* **frontend:** slideover initial work ([14b9cb6](https://github.com/sct/overseerr/commit/14b9cb610c0dcfef939ebec328f371e1cdfb689d)) -* tv request modal status hookup ([5f8114f](https://github.com/sct/overseerr/commit/5f8114f730b067eb710704952824057e7b5b8fbf)) -* **.editorconfig:** add .editorconfig ([b982066](https://github.com/sct/overseerr/commit/b982066327525156f8dd0d32818d3fe7cb28f9c8)) -* **api:** add external ids to movie/tv response ([4aa7431](https://github.com/sct/overseerr/commit/4aa74319e0adcc19041239e57a00bc40fb127826)) -* **api:** add movie details endpoint ([b176148](https://github.com/sct/overseerr/commit/b1761484cb2861329763d51a868f37dd3098760d)) -* **api:** add tmdb discover api wrapper ([#67](https://github.com/sct/overseerr/issues/67)) ([839448f](https://github.com/sct/overseerr/commit/839448fcc8cc14ea83092af82e2ba3d0d92c9b73)) -* **api:** allow plex logins from users who have access to the server ([5147140](https://github.com/sct/overseerr/commit/514714071dfe4be04e607fe6412f5b3f0ef74dd4)) -* **api:** decouple media requests from media info ([8577db1](https://github.com/sct/overseerr/commit/8577db1be16f099d92c6649bbfb15f15e09a2f73)) -* **api:** discover endpoint for movie/tv ([#73](https://github.com/sct/overseerr/issues/73)) ([258bb93](https://github.com/sct/overseerr/commit/258bb93be2acc2ca32eaaefb617a5c326c5943ba)) -* **api:** initial implementation of the auth system ([#30](https://github.com/sct/overseerr/issues/30)) ([5343f35](https://github.com/sct/overseerr/commit/5343f35e5b572fe366a8712b24bd735de30e6170)) -* **api:** plex Sync (Movies) ([1be8b18](https://github.com/sct/overseerr/commit/1be8b183617c3a44ab8d4454a64b43dfe1d877fe)) -* **api:** public settings route ([#57](https://github.com/sct/overseerr/issues/57)) ([c0166e7](https://github.com/sct/overseerr/commit/c0166e7ecb5df110a4167f33338ed6406bf47f41)) -* **api:** radarr api wrapper / send to radarr when requests approved ([#93](https://github.com/sct/overseerr/issues/93)) ([48d62c3](https://github.com/sct/overseerr/commit/48d62c3178488d0d51831155ddd35cc31867db2b)) -* **api:** request api ([#80](https://github.com/sct/overseerr/issues/80)) ([f4c2c47](https://github.com/sct/overseerr/commit/f4c2c47e569e7faea7f99664966cb98b321ce952)) -* **api:** tmdb api wrapper / multi search route ([#62](https://github.com/sct/overseerr/issues/62)) ([c702c17](https://github.com/sct/overseerr/commit/c702c17cee00a52b23f685206e2d5d0c2eddf5a2)) -* **api:** tmdb trending api wrapper ([#68](https://github.com/sct/overseerr/issues/68)) ([ba34e54](https://github.com/sct/overseerr/commit/ba34e54d77d142d211df58d6ce9f53b6e673e004)) -* **api:** tv details endpoint ([a3beeed](https://github.com/sct/overseerr/commit/a3beeede7e72e99c7595673a27e38611ca4bb0cd)) -* **api:** validate plex when settings are saved ([8f6247d](https://github.com/sct/overseerr/commit/8f6247d82160704a3cfb76262696957b27641e87)) -* **api-user:** add basic User Entity and basic routing to fetch all users ([d902ef7](https://github.com/sct/overseerr/commit/d902ef72770712f2f71f33c09bca9ba99a30fc64)) -* **components/plexloginbutton:** added PlexLoginButton ([0abf743](https://github.com/sct/overseerr/commit/0abf743b17c664b58da18bdbf176f4a55ddc4179)) -* **extensions.json:** added recommended extensions for VSCode ([5dc9b51](https://github.com/sct/overseerr/commit/5dc9b510b8049516ad889c9d76a2f84daa0d2718)) -* **frontend:** add cancel request modal for titlecards ([f22f8c5](https://github.com/sct/overseerr/commit/f22f8c5d734be5cc0b1dcca869458a7321cd43a2)) -* **frontend:** approve/decline request well added to movie detail ([8f21358](https://github.com/sct/overseerr/commit/8f21358f797ed55923d90ba43acf1126856e9dfd)) -* **frontend:** basic discover page (only movies) ([#74](https://github.com/sct/overseerr/issues/74)) ([bbfe349](https://github.com/sct/overseerr/commit/bbfe349b52d308620796b37aaf986a0ed1ff0006)) -* **frontend:** design updates for responsive titlecards ([31809d9](https://github.com/sct/overseerr/commit/31809d952c8bafde3f63e2c1d952cc013149940e)) -* **frontend:** discover tv/movies full page ([be0003a](https://github.com/sct/overseerr/commit/be0003a85dc4e91799e85019aeb1110bd524a026)) -* **frontend:** initial search functionality ([#78](https://github.com/sct/overseerr/issues/78)) ([342d1a3](https://github.com/sct/overseerr/commit/342d1a3c75b32b172a51ca7d82fdfde8510abedf)) -* **frontend:** loading spinner ([de84658](https://github.com/sct/overseerr/commit/de84658b48985e24b0f92a1690387f6d59d0bc16)) -* **frontend:** logo updates ([5a43ec5](https://github.com/sct/overseerr/commit/5a43ec5405855deb244e8085484a9d2b743caba6)) -* **frontend:** modal component and basic request hookup ([#91](https://github.com/sct/overseerr/issues/91)) ([626099a](https://github.com/sct/overseerr/commit/626099a2c98fb30d0cb53d8ccf79a6bf75a00059)) -* **frontend:** new dashboard concept ([#82](https://github.com/sct/overseerr/issues/82)) ([eae38bb](https://github.com/sct/overseerr/commit/eae38bb9ec8588856f319387d2f262d7ee3f7e9c)) -* **frontend:** refresh indicator for titlecards / toasts ([4638fae](https://github.com/sct/overseerr/commit/4638fae336edc62a539796b3f55277a238683603)) -* **frontend:** request card / recent requests ([371e433](https://github.com/sct/overseerr/commit/371e43356d2c057e52368c32ffe2af1744311d91)) -* **frontend:** title detail (movie) initial version ([73ce24a](https://github.com/sct/overseerr/commit/73ce24a37bda3713e8cedc44e1ed065bdbc4ee4f)) -* **frontend/api:** beginning of new request modal ([2bf7e10](https://github.com/sct/overseerr/commit/2bf7e10e32718b36799be2feb0a7f9ff54d85744)) -* **frontend/api:** cast included with movie request and cast list on detail page ([04252f8](https://github.com/sct/overseerr/commit/04252f88bbdf51949923586feda582f86ac668ce)) -* **frontend/api:** i18n support ([9131254](https://github.com/sct/overseerr/commit/9131254f3371f12a17de44b6fa8f9bfb0e5c002e)) -* **frontend/api:** movie recommendations/similar request and frontend detail page update ([6398e36](https://github.com/sct/overseerr/commit/6398e3645a1e4ddbb9de9f4fda0a0659b4cac4d0)) -* **frontend/api:** tv details page ([02cbb5b](https://github.com/sct/overseerr/commit/02cbb5b030a3af5d62ab6c4cafdd4d800b4f61f4)) -* **frontend/api:** tv request modal (no status. only request) ([608b966](https://github.com/sct/overseerr/commit/608b96600a926adf16331b36e77789afa5d67069)) -* logout route/sign out button ([#54](https://github.com/sct/overseerr/issues/54)) ([cb9098f](https://github.com/sct/overseerr/commit/cb9098f457f79b71734959fd924b6c72ca77d61d)) -* user avatars from plex ([#53](https://github.com/sct/overseerr/issues/53)) ([e6349c1](https://github.com/sct/overseerr/commit/e6349c13a0eb0489289aa7663fcc64fa7d2906e6)) -* **layout:** created Layout component ([1f497e8](https://github.com/sct/overseerr/commit/1f497e8913146ceb9748d667e638141b2ca4612a)) -* **login component/route:** add: Login Component and Route ([6e47be2](https://github.com/sct/overseerr/commit/6e47be2fa865bcd51582ce30ebee6fd820c5f9dd)) -* **login route conditional:** on login route, do not display layout ([7d179ae](https://github.com/sct/overseerr/commit/7d179ae3b42d8ffae5e1b6e266038793260f1bbe)) -* **pass pageprops to loginpage:** pass page props to loginPage ([1597188](https://github.com/sct/overseerr/commit/159718891fb363001c650ac8b7e1446a1520ce4a)) -* **plex/utils:** added Plex OAuth class ([72f9624](https://github.com/sct/overseerr/commit/72f9624f1db721fe0324b7be9f0f811d2ae02389)) -* bootstrap the basic app structure ([89a6017](https://github.com/sct/overseerr/commit/89a6017c7f6f7637fe249ac0d667a652f44e02bb)) +- add migration for delete cascades on season requests/seasons ([c688cf6](https://github.com/sct/overseerr/commit/c688cf60c710f0cf0b2da5ba6b0c18a2d137e7f9)) +- **api:** email notification agent ([0962392](https://github.com/sct/overseerr/commit/0962392e3930c7fdcb3164b9143cc8faca38bdfa)) +- **frontend:** add french language file ([cd6d8a8](https://github.com/sct/overseerr/commit/cd6d8a8216e7ae183b046d26cd22f3c1dc1d2b35)) +- **frontend:** add translatable strings for request card ([0d2f360](https://github.com/sct/overseerr/commit/0d2f360c22cd9bb50ae04f00a25e5fcc6c21bcdd)) +- **frontend:** added more localized strings ([659a601](https://github.com/sct/overseerr/commit/659a6018777718f7a90141307678d8dadcfd77f8)) +- actually include email templates in built server files ([a28a8b3](https://github.com/sct/overseerr/commit/a28a8b37b0afc79583e4a7191a91f73ff6d3adad)) +- add application url config to main settings ui ([a359672](https://github.com/sct/overseerr/commit/a359672ebafffef742858814f0faa918e0341aa3)) +- add filtering for requests api ([cb9ae25](https://github.com/sct/overseerr/commit/cb9ae25d94f21e97113dfea3ca45c7002089e344)) +- add trending to discover page ([ff8b9d8](https://github.com/sct/overseerr/commit/ff8b9d8e7ed228a153c2da4d237f7a4f99a79321)) +- force setup if app is not initialized ([a99705f](https://github.com/sct/overseerr/commit/a99705f6a5674b436ae28cbc558f4ee6e99ac910)) +- initial user list (no edit/delete yet) and job schedules ([24a0423](https://github.com/sct/overseerr/commit/24a0423f3b14303cfb0e83aef6e9e3bb273c5ba9)) +- manage series slideover added (and approve/decline/delete hooked up) ([236c4e5](https://github.com/sct/overseerr/commit/236c4e5e6126d2424a4badc08b7f7e6d1d70f401)) +- media delete option in manage media slideover ([250f484](https://github.com/sct/overseerr/commit/250f48492c95d74e40d95d3f026d2952157bc6e1)) +- other email notifications for approved/available ([0d73d88](https://github.com/sct/overseerr/commit/0d73d88f35b03e993f305873dc72672003c7d9e5)) +- radarr edit/create modal/backend functionality ([c4ac357](https://github.com/sct/overseerr/commit/c4ac357ef4cdd7a2c610260db46a4f0c325cd785)) +- season creation migration ([978f92a](https://github.com/sct/overseerr/commit/978f92a1c589ac404a3cb1103a68a8a5ffb0dd7d)) +- sonarr edit/delete modal ([3204326](https://github.com/sct/overseerr/commit/320432657e6ccf4d255238098e03590f28267bdb)) +- throw 404 when movie/tv show doesnt exist ([0601b44](https://github.com/sct/overseerr/commit/0601b446873e2eaf042044dd6a995b713586b0cc)) +- **api:** sonarr api wrapper / send to sonarr ([9385592](https://github.com/sct/overseerr/commit/9385592362eeba1dba05c5aa8fc7a2de1d054d74)) +- **frontend:** add header styling to movie/tv recommendation and similar list views ([f5f2545](https://github.com/sct/overseerr/commit/f5f2545520a43daa23e1276d24ff60d794ebbc6e)) +- **frontend:** add links to detail pages from new request card ([6ad3384](https://github.com/sct/overseerr/commit/6ad3384a78f7bcb03f409cce8b35cc61d634d6b2)) +- **frontend:** new design for request card ([93738e1](https://github.com/sct/overseerr/commit/93738e154c41fd11d5c6cf3d35573daf54ead471)) +- **frontend:** update favicon ([886389a](https://github.com/sct/overseerr/commit/886389a361da54c616da3bdfeee9a85e9d12bcf3)) +- notification framework ([d8e542e](https://github.com/sct/overseerr/commit/d8e542e5fe2ed76dcb20fb6dfc5f59430cd4245d)) +- notifications for media_available and media_approved ([a6c5e65](https://github.com/sct/overseerr/commit/a6c5e65bbfc196545471e99fe2e5b7194f9dd387)) +- rotten tomatoes scores on movie/tv details pages ([1694f60](https://github.com/sct/overseerr/commit/1694f60e8aa475ceeb7f170a783ec0ba70bd4bce)) +- upcoming movies on discover ([67290dd](https://github.com/sct/overseerr/commit/67290dd502571a22dcf8559ac07f42e855275bd0)) +- upcoming/trending list views and larger title cards ([94eaaf9](https://github.com/sct/overseerr/commit/94eaaf96b4302a832c52ccb72009b3593452c779)) +- upgrade tailwindcss to 2.0.1 ([fb5c791](https://github.com/sct/overseerr/commit/fb5c791b0b6b7593a472bf01713999a001f92dc7)) +- user edit functionality (managing permissions) ([185ac26](https://github.com/sct/overseerr/commit/185ac2648fd21c4bf9692ac5ac055e9c740065ca)) +- **api:** plex tv sync and recently added sync ([1390cc1](https://github.com/sct/overseerr/commit/1390cc1f130bb3975996e84b12ac833f55f2f753)) +- **frontend:** allow permission check for showing nav items ([0b239f0](https://github.com/sct/overseerr/commit/0b239f0bdfb1394897bce5c50b0d112abfbb4ad7)) +- **frontend:** alpha notice ([33da7e9](https://github.com/sct/overseerr/commit/33da7e9df3a2546b0f208bd3b1d1f268e343cead)) +- **frontend:** buttonWithDropdown component added (no hookups yet) ([4975841](https://github.com/sct/overseerr/commit/4975841b5d4ba4ed1ba8cacaa5a063eeb3b8c311)) +- **frontend:** cancel movie request modal ([1f9cbbf](https://github.com/sct/overseerr/commit/1f9cbbfdf1ac98e54de5b8777c52c7bfc69c7e20)) +- **frontend:** improved settings menu design for mobile ([16221a4](https://github.com/sct/overseerr/commit/16221a46a7d57c77f53aa0186263aa27267d9863)) +- **frontend:** initial Settings design ([8742da0](https://github.com/sct/overseerr/commit/8742da0ebb92d2f78309a998de0f67e788e14376)) +- **frontend:** plex library scan ([1bc3f7b](https://github.com/sct/overseerr/commit/1bc3f7be4b07211563a1e254c28ce51e1bc337a2)) +- **frontend:** plex settings page ([47714b6](https://github.com/sct/overseerr/commit/47714b698cf4351c1ee38bdf0b672d9f0baed03a)) +- **frontend:** radarr delete modal ([877a518](https://github.com/sct/overseerr/commit/877a5184158fb4aa371fa2ea2107032543c9aa37)) +- **frontend:** recently added on discover ([06dc606](https://github.com/sct/overseerr/commit/06dc606bcfeb50b7be1c35ac180c10738bade458)) +- **frontend:** slideover initial work ([14b9cb6](https://github.com/sct/overseerr/commit/14b9cb610c0dcfef939ebec328f371e1cdfb689d)) +- tv request modal status hookup ([5f8114f](https://github.com/sct/overseerr/commit/5f8114f730b067eb710704952824057e7b5b8fbf)) +- **.editorconfig:** add .editorconfig ([b982066](https://github.com/sct/overseerr/commit/b982066327525156f8dd0d32818d3fe7cb28f9c8)) +- **api:** add external ids to movie/tv response ([4aa7431](https://github.com/sct/overseerr/commit/4aa74319e0adcc19041239e57a00bc40fb127826)) +- **api:** add movie details endpoint ([b176148](https://github.com/sct/overseerr/commit/b1761484cb2861329763d51a868f37dd3098760d)) +- **api:** add tmdb discover api wrapper ([#67](https://github.com/sct/overseerr/issues/67)) ([839448f](https://github.com/sct/overseerr/commit/839448fcc8cc14ea83092af82e2ba3d0d92c9b73)) +- **api:** allow plex logins from users who have access to the server ([5147140](https://github.com/sct/overseerr/commit/514714071dfe4be04e607fe6412f5b3f0ef74dd4)) +- **api:** decouple media requests from media info ([8577db1](https://github.com/sct/overseerr/commit/8577db1be16f099d92c6649bbfb15f15e09a2f73)) +- **api:** discover endpoint for movie/tv ([#73](https://github.com/sct/overseerr/issues/73)) ([258bb93](https://github.com/sct/overseerr/commit/258bb93be2acc2ca32eaaefb617a5c326c5943ba)) +- **api:** initial implementation of the auth system ([#30](https://github.com/sct/overseerr/issues/30)) ([5343f35](https://github.com/sct/overseerr/commit/5343f35e5b572fe366a8712b24bd735de30e6170)) +- **api:** plex Sync (Movies) ([1be8b18](https://github.com/sct/overseerr/commit/1be8b183617c3a44ab8d4454a64b43dfe1d877fe)) +- **api:** public settings route ([#57](https://github.com/sct/overseerr/issues/57)) ([c0166e7](https://github.com/sct/overseerr/commit/c0166e7ecb5df110a4167f33338ed6406bf47f41)) +- **api:** radarr api wrapper / send to radarr when requests approved ([#93](https://github.com/sct/overseerr/issues/93)) ([48d62c3](https://github.com/sct/overseerr/commit/48d62c3178488d0d51831155ddd35cc31867db2b)) +- **api:** request api ([#80](https://github.com/sct/overseerr/issues/80)) ([f4c2c47](https://github.com/sct/overseerr/commit/f4c2c47e569e7faea7f99664966cb98b321ce952)) +- **api:** tmdb api wrapper / multi search route ([#62](https://github.com/sct/overseerr/issues/62)) ([c702c17](https://github.com/sct/overseerr/commit/c702c17cee00a52b23f685206e2d5d0c2eddf5a2)) +- **api:** tmdb trending api wrapper ([#68](https://github.com/sct/overseerr/issues/68)) ([ba34e54](https://github.com/sct/overseerr/commit/ba34e54d77d142d211df58d6ce9f53b6e673e004)) +- **api:** tv details endpoint ([a3beeed](https://github.com/sct/overseerr/commit/a3beeede7e72e99c7595673a27e38611ca4bb0cd)) +- **api:** validate plex when settings are saved ([8f6247d](https://github.com/sct/overseerr/commit/8f6247d82160704a3cfb76262696957b27641e87)) +- **api-user:** add basic User Entity and basic routing to fetch all users ([d902ef7](https://github.com/sct/overseerr/commit/d902ef72770712f2f71f33c09bca9ba99a30fc64)) +- **components/plexloginbutton:** added PlexLoginButton ([0abf743](https://github.com/sct/overseerr/commit/0abf743b17c664b58da18bdbf176f4a55ddc4179)) +- **extensions.json:** added recommended extensions for VSCode ([5dc9b51](https://github.com/sct/overseerr/commit/5dc9b510b8049516ad889c9d76a2f84daa0d2718)) +- **frontend:** add cancel request modal for titlecards ([f22f8c5](https://github.com/sct/overseerr/commit/f22f8c5d734be5cc0b1dcca869458a7321cd43a2)) +- **frontend:** approve/decline request well added to movie detail ([8f21358](https://github.com/sct/overseerr/commit/8f21358f797ed55923d90ba43acf1126856e9dfd)) +- **frontend:** basic discover page (only movies) ([#74](https://github.com/sct/overseerr/issues/74)) ([bbfe349](https://github.com/sct/overseerr/commit/bbfe349b52d308620796b37aaf986a0ed1ff0006)) +- **frontend:** design updates for responsive titlecards ([31809d9](https://github.com/sct/overseerr/commit/31809d952c8bafde3f63e2c1d952cc013149940e)) +- **frontend:** discover tv/movies full page ([be0003a](https://github.com/sct/overseerr/commit/be0003a85dc4e91799e85019aeb1110bd524a026)) +- **frontend:** initial search functionality ([#78](https://github.com/sct/overseerr/issues/78)) ([342d1a3](https://github.com/sct/overseerr/commit/342d1a3c75b32b172a51ca7d82fdfde8510abedf)) +- **frontend:** loading spinner ([de84658](https://github.com/sct/overseerr/commit/de84658b48985e24b0f92a1690387f6d59d0bc16)) +- **frontend:** logo updates ([5a43ec5](https://github.com/sct/overseerr/commit/5a43ec5405855deb244e8085484a9d2b743caba6)) +- **frontend:** modal component and basic request hookup ([#91](https://github.com/sct/overseerr/issues/91)) ([626099a](https://github.com/sct/overseerr/commit/626099a2c98fb30d0cb53d8ccf79a6bf75a00059)) +- **frontend:** new dashboard concept ([#82](https://github.com/sct/overseerr/issues/82)) ([eae38bb](https://github.com/sct/overseerr/commit/eae38bb9ec8588856f319387d2f262d7ee3f7e9c)) +- **frontend:** refresh indicator for titlecards / toasts ([4638fae](https://github.com/sct/overseerr/commit/4638fae336edc62a539796b3f55277a238683603)) +- **frontend:** request card / recent requests ([371e433](https://github.com/sct/overseerr/commit/371e43356d2c057e52368c32ffe2af1744311d91)) +- **frontend:** title detail (movie) initial version ([73ce24a](https://github.com/sct/overseerr/commit/73ce24a37bda3713e8cedc44e1ed065bdbc4ee4f)) +- **frontend/api:** beginning of new request modal ([2bf7e10](https://github.com/sct/overseerr/commit/2bf7e10e32718b36799be2feb0a7f9ff54d85744)) +- **frontend/api:** cast included with movie request and cast list on detail page ([04252f8](https://github.com/sct/overseerr/commit/04252f88bbdf51949923586feda582f86ac668ce)) +- **frontend/api:** i18n support ([9131254](https://github.com/sct/overseerr/commit/9131254f3371f12a17de44b6fa8f9bfb0e5c002e)) +- **frontend/api:** movie recommendations/similar request and frontend detail page update ([6398e36](https://github.com/sct/overseerr/commit/6398e3645a1e4ddbb9de9f4fda0a0659b4cac4d0)) +- **frontend/api:** tv details page ([02cbb5b](https://github.com/sct/overseerr/commit/02cbb5b030a3af5d62ab6c4cafdd4d800b4f61f4)) +- **frontend/api:** tv request modal (no status. only request) ([608b966](https://github.com/sct/overseerr/commit/608b96600a926adf16331b36e77789afa5d67069)) +- logout route/sign out button ([#54](https://github.com/sct/overseerr/issues/54)) ([cb9098f](https://github.com/sct/overseerr/commit/cb9098f457f79b71734959fd924b6c72ca77d61d)) +- user avatars from plex ([#53](https://github.com/sct/overseerr/issues/53)) ([e6349c1](https://github.com/sct/overseerr/commit/e6349c13a0eb0489289aa7663fcc64fa7d2906e6)) +- **layout:** created Layout component ([1f497e8](https://github.com/sct/overseerr/commit/1f497e8913146ceb9748d667e638141b2ca4612a)) +- **login component/route:** add: Login Component and Route ([6e47be2](https://github.com/sct/overseerr/commit/6e47be2fa865bcd51582ce30ebee6fd820c5f9dd)) +- **login route conditional:** on login route, do not display layout ([7d179ae](https://github.com/sct/overseerr/commit/7d179ae3b42d8ffae5e1b6e266038793260f1bbe)) +- **pass pageprops to loginpage:** pass page props to loginPage ([1597188](https://github.com/sct/overseerr/commit/159718891fb363001c650ac8b7e1446a1520ce4a)) +- **plex/utils:** added Plex OAuth class ([72f9624](https://github.com/sct/overseerr/commit/72f9624f1db721fe0324b7be9f0f811d2ae02389)) +- bootstrap the basic app structure ([89a6017](https://github.com/sct/overseerr/commit/89a6017c7f6f7637fe249ac0d667a652f44e02bb)) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a016f6d46..96e67c8ac 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -86,7 +86,7 @@ When adding new UI text, please try to adhere to the following guidelines: 1. Be concise and clear, and use as few words as possible to make your point. 2. Use the Oxford comma where appropriate. 3. Use the appropriate Unicode characters for ellipses, arrows, and other special characters/symbols. -4. Capitalize proper nouns, such as Plex, Radarr, Sonarr, Telegram, Slack, Pushover, etc. Be sure to also use the official capitalization for any abbreviations; e.g., TMDb and IMDb have a lowercase 'b', whereas TheTVDB has a capital 'B'. +4. Capitalize proper nouns, such as Plex, Radarr, Sonarr, Telegram, Slack, Pushover, etc. Be sure to also use the official capitalization for any abbreviations; e.g., IMDb has a lowercase 'b', whereas TMDB and TheTVDB have a capital 'B'. 5. Title case headings, button text, and form labels. Note that verbs such as "is" should be capitalized, whereas prepositions like "from" should be lowercase (unless as the first or last word of the string, in which case they are also capitalized). 6. Capitalize the first word in validation error messages, dropdowns, and form "tips." These strings should not end in punctuation. 7. Ensure that toast notification strings are complete sentences ending in punctuation. diff --git a/Dockerfile b/Dockerfile index 8f3ed32c8..851ba4721 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:16.14-alpine AS BUILD_IMAGE +FROM node:16.17-alpine AS BUILD_IMAGE WORKDIR /app @@ -14,7 +14,7 @@ RUN \ esac COPY package.json yarn.lock ./ -RUN yarn install --frozen-lockfile --network-timeout 1000000 +RUN CYPRESS_INSTALL_BINARY=0 yarn install --frozen-lockfile --network-timeout 1000000 COPY . ./ @@ -33,7 +33,7 @@ RUN touch config/DOCKER RUN echo "{\"commitTag\": \"${COMMIT_TAG}\"}" > committag.json -FROM node:16.14-alpine +FROM node:16.17-alpine WORKDIR /app diff --git a/Dockerfile.local b/Dockerfile.local index f0228b6b9..39e0534f3 100644 --- a/Dockerfile.local +++ b/Dockerfile.local @@ -1,4 +1,4 @@ -FROM node:16.14-alpine +FROM node:16.17-alpine COPY . /app WORKDIR /app diff --git a/README.md b/README.md index 03e08ab8b..614dcadb5 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ _The original Overseerr team have been busy and Jellyfin/Emby support aren't on - Jellyfin Support - Emby Support - + (Upcoming Features include: Multiple Server Instances, Music Support, Ability to change email address and much more!) Along with all the existing Overseerr features: diff --git a/cypress.config.ts b/cypress.config.ts new file mode 100644 index 000000000..07b0c8b1d --- /dev/null +++ b/cypress.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'cypress'; + +export default defineConfig({ + projectId: 'onnqy3', + e2e: { + baseUrl: 'http://localhost:5055', + experimentalSessionAndOrigin: true, + }, + env: { + ADMIN_EMAIL: 'admin@seerr.dev', + ADMIN_PASSWORD: 'test1234', + USER_EMAIL: 'friend@seerr.dev', + USER_PASSWORD: 'test1234', + }, + retries: { + runMode: 2, + openMode: 0, + }, +}); diff --git a/cypress/config/settings.cypress.json b/cypress/config/settings.cypress.json new file mode 100644 index 000000000..bb7b661b0 --- /dev/null +++ b/cypress/config/settings.cypress.json @@ -0,0 +1,149 @@ +{ + "clientId": "6919275e-142a-48d8-be6b-93594cbd4626", + "vapidPrivate": "tmnslaO8ZWN6bNbSEv_rolPeBTlNxOwCCAHrM9oZz3M", + "vapidPublic": "BK_EpP8NDm9waor2zn6_S28o3ZYv4kCkJOfYpO3pt3W6jnPmxrgTLANUBNbbyaNatPnSQ12De9CeqSYQrqWzHTs", + "main": { + "apiKey": "testkey", + "applicationTitle": "Overseerr", + "applicationUrl": "", + "csrfProtection": false, + "cacheImages": false, + "defaultPermissions": 32, + "defaultQuotas": { + "movie": {}, + "tv": {} + }, + "hideAvailable": false, + "localLogin": true, + "newPlexLogin": true, + "region": "", + "originalLanguage": "", + "trustProxy": false, + "partialRequestsEnabled": true, + "locale": "en" + }, + "plex": { + "name": "Seerr", + "ip": "192.168.1.1", + "port": 32400, + "useSsl": false, + "libraries": [ + { + "id": "1", + "name": "Movies", + "enabled": true, + "type": "movie" + } + ], + "machineId": "test" + }, + "tautulli": {}, + "radarr": [], + "sonarr": [], + "public": { + "initialized": true + }, + "notifications": { + "agents": { + "email": { + "enabled": false, + "options": { + "emailFrom": "", + "smtpHost": "", + "smtpPort": 587, + "secure": false, + "ignoreTls": false, + "requireTls": false, + "allowSelfSigned": false, + "senderName": "Overseerr" + } + }, + "discord": { + "enabled": false, + "types": 0, + "options": { + "webhookUrl": "", + "enableMentions": true + } + }, + "lunasea": { + "enabled": false, + "types": 0, + "options": { + "webhookUrl": "" + } + }, + "slack": { + "enabled": false, + "types": 0, + "options": { + "webhookUrl": "" + } + }, + "telegram": { + "enabled": false, + "types": 0, + "options": { + "botAPI": "", + "chatId": "", + "sendSilently": false + } + }, + "pushbullet": { + "enabled": false, + "types": 0, + "options": { + "accessToken": "" + } + }, + "pushover": { + "enabled": false, + "types": 0, + "options": { + "accessToken": "", + "userToken": "" + } + }, + "webhook": { + "enabled": false, + "types": 0, + "options": { + "webhookUrl": "", + "jsonPayload": "IntcbiAgICBcIm5vdGlmaWNhdGlvbl90eXBlXCI6IFwie3tub3RpZmljYXRpb25fdHlwZX19XCIsXG4gICAgXCJldmVudFwiOiBcInt7ZXZlbnR9fVwiLFxuICAgIFwic3ViamVjdFwiOiBcInt7c3ViamVjdH19XCIsXG4gICAgXCJtZXNzYWdlXCI6IFwie3ttZXNzYWdlfX1cIixcbiAgICBcImltYWdlXCI6IFwie3tpbWFnZX19XCIsXG4gICAgXCJ7e21lZGlhfX1cIjoge1xuICAgICAgICBcIm1lZGlhX3R5cGVcIjogXCJ7e21lZGlhX3R5cGV9fVwiLFxuICAgICAgICBcInRtZGJJZFwiOiBcInt7bWVkaWFfdG1kYmlkfX1cIixcbiAgICAgICAgXCJ0dmRiSWRcIjogXCJ7e21lZGlhX3R2ZGJpZH19XCIsXG4gICAgICAgIFwic3RhdHVzXCI6IFwie3ttZWRpYV9zdGF0dXN9fVwiLFxuICAgICAgICBcInN0YXR1czRrXCI6IFwie3ttZWRpYV9zdGF0dXM0a319XCJcbiAgICB9LFxuICAgIFwie3tyZXF1ZXN0fX1cIjoge1xuICAgICAgICBcInJlcXVlc3RfaWRcIjogXCJ7e3JlcXVlc3RfaWR9fVwiLFxuICAgICAgICBcInJlcXVlc3RlZEJ5X2VtYWlsXCI6IFwie3tyZXF1ZXN0ZWRCeV9lbWFpbH19XCIsXG4gICAgICAgIFwicmVxdWVzdGVkQnlfdXNlcm5hbWVcIjogXCJ7e3JlcXVlc3RlZEJ5X3VzZXJuYW1lfX1cIixcbiAgICAgICAgXCJyZXF1ZXN0ZWRCeV9hdmF0YXJcIjogXCJ7e3JlcXVlc3RlZEJ5X2F2YXRhcn19XCJcbiAgICB9LFxuICAgIFwie3tpc3N1ZX19XCI6IHtcbiAgICAgICAgXCJpc3N1ZV9pZFwiOiBcInt7aXNzdWVfaWR9fVwiLFxuICAgICAgICBcImlzc3VlX3R5cGVcIjogXCJ7e2lzc3VlX3R5cGV9fVwiLFxuICAgICAgICBcImlzc3VlX3N0YXR1c1wiOiBcInt7aXNzdWVfc3RhdHVzfX1cIixcbiAgICAgICAgXCJyZXBvcnRlZEJ5X2VtYWlsXCI6IFwie3tyZXBvcnRlZEJ5X2VtYWlsfX1cIixcbiAgICAgICAgXCJyZXBvcnRlZEJ5X3VzZXJuYW1lXCI6IFwie3tyZXBvcnRlZEJ5X3VzZXJuYW1lfX1cIixcbiAgICAgICAgXCJyZXBvcnRlZEJ5X2F2YXRhclwiOiBcInt7cmVwb3J0ZWRCeV9hdmF0YXJ9fVwiXG4gICAgfSxcbiAgICBcInt7Y29tbWVudH19XCI6IHtcbiAgICAgICAgXCJjb21tZW50X21lc3NhZ2VcIjogXCJ7e2NvbW1lbnRfbWVzc2FnZX19XCIsXG4gICAgICAgIFwiY29tbWVudGVkQnlfZW1haWxcIjogXCJ7e2NvbW1lbnRlZEJ5X2VtYWlsfX1cIixcbiAgICAgICAgXCJjb21tZW50ZWRCeV91c2VybmFtZVwiOiBcInt7Y29tbWVudGVkQnlfdXNlcm5hbWV9fVwiLFxuICAgICAgICBcImNvbW1lbnRlZEJ5X2F2YXRhclwiOiBcInt7Y29tbWVudGVkQnlfYXZhdGFyfX1cIlxuICAgIH0sXG4gICAgXCJ7e2V4dHJhfX1cIjogW11cbn0i" + } + }, + "webpush": { + "enabled": false, + "options": {} + }, + "gotify": { + "enabled": false, + "types": 0, + "options": { + "url": "", + "token": "" + } + } + } + }, + "jobs": { + "plex-recently-added-scan": { + "schedule": "0 */5 * * * *" + }, + "plex-full-scan": { + "schedule": "0 0 3 * * *" + }, + "radarr-scan": { + "schedule": "0 0 4 * * *" + }, + "sonarr-scan": { + "schedule": "0 30 4 * * *" + }, + "download-sync": { + "schedule": "0 * * * * *" + }, + "download-sync-reset": { + "schedule": "0 0 1 * * *" + } + } + } diff --git a/cypress/e2e/discover.cy.ts b/cypress/e2e/discover.cy.ts new file mode 100644 index 000000000..3489061b0 --- /dev/null +++ b/cypress/e2e/discover.cy.ts @@ -0,0 +1,210 @@ +const clickFirstTitleCardInSlider = (sliderTitle: string): void => { + cy.contains('.slider-header', sliderTitle) + .next('[data-testid=media-slider]') + .find('[data-testid=title-card]') + .first() + .trigger('mouseover') + .find('[data-testid=title-card-title]') + .invoke('text') + .then((text) => { + cy.contains('.slider-header', sliderTitle) + .next('[data-testid=media-slider]') + .find('[data-testid=title-card]') + .first() + .click(); + cy.get('[data-testid=media-title]').should('contain', text); + }); +}; + +describe('Discover', () => { + beforeEach(() => { + cy.loginAsAdmin(); + }); + + it('loads a trending item', () => { + cy.intercept('/api/v1/discover/trending*').as('getTrending'); + cy.visit('/'); + cy.wait('@getTrending'); + clickFirstTitleCardInSlider('Trending'); + }); + + it('loads popular movies', () => { + cy.intercept('/api/v1/discover/movies*').as('getPopularMovies'); + cy.visit('/'); + cy.wait('@getPopularMovies'); + clickFirstTitleCardInSlider('Popular Movies'); + }); + + it('loads upcoming movies', () => { + cy.intercept('/api/v1/discover/movies/upcoming*').as('getUpcomingMovies'); + cy.visit('/'); + cy.wait('@getUpcomingMovies'); + clickFirstTitleCardInSlider('Upcoming Movies'); + }); + + it('loads popular series', () => { + cy.intercept('/api/v1/discover/tv*').as('getPopularTv'); + cy.visit('/'); + cy.wait('@getPopularTv'); + clickFirstTitleCardInSlider('Popular Series'); + }); + + it('loads upcoming series', () => { + cy.intercept('/api/v1/discover/tv/upcoming*').as('getUpcomingSeries'); + cy.visit('/'); + cy.wait('@getUpcomingSeries'); + clickFirstTitleCardInSlider('Upcoming Series'); + }); + + it('displays error for media with invalid TMDB ID', () => { + cy.intercept('GET', '/api/v1/media?*', { + pageInfo: { pages: 1, pageSize: 20, results: 1, page: 1 }, + results: [ + { + downloadStatus: [], + downloadStatus4k: [], + id: 1922, + mediaType: 'movie', + tmdbId: 998814, + tvdbId: null, + imdbId: null, + status: 5, + status4k: 1, + createdAt: '2022-08-18T18:11:13.000Z', + updatedAt: '2022-08-18T19:56:41.000Z', + lastSeasonChange: '2022-08-18T19:56:41.000Z', + mediaAddedAt: '2022-08-18T19:56:41.000Z', + serviceId: null, + serviceId4k: null, + externalServiceId: null, + externalServiceId4k: null, + externalServiceSlug: null, + externalServiceSlug4k: null, + ratingKey: null, + ratingKey4k: null, + seasons: [], + }, + ], + }).as('getMedia'); + + cy.visit('/'); + cy.wait('@getMedia'); + cy.contains('.slider-header', 'Recently Added') + .next('[data-testid=media-slider]') + .find('[data-testid=title-card]') + .first() + .find('[data-testid=title-card-title]') + .contains('Movie Not Found'); + }); + + it('displays error for request with invalid TMDB ID', () => { + cy.intercept('GET', '/api/v1/request?*', { + pageInfo: { pages: 1, pageSize: 10, results: 1, page: 1 }, + results: [ + { + id: 582, + status: 1, + createdAt: '2022-08-18T18:11:13.000Z', + updatedAt: '2022-08-18T18:11:13.000Z', + type: 'movie', + is4k: false, + serverId: null, + profileId: null, + rootFolder: null, + languageProfileId: null, + tags: null, + media: { + downloadStatus: [], + downloadStatus4k: [], + id: 1922, + mediaType: 'movie', + tmdbId: 998814, + tvdbId: null, + imdbId: null, + status: 2, + status4k: 1, + createdAt: '2022-08-18T18:11:13.000Z', + updatedAt: '2022-08-18T18:11:13.000Z', + lastSeasonChange: '2022-08-18T18:11:13.000Z', + mediaAddedAt: null, + serviceId: null, + serviceId4k: null, + externalServiceId: null, + externalServiceId4k: null, + externalServiceSlug: null, + externalServiceSlug4k: null, + ratingKey: null, + ratingKey4k: null, + }, + seasons: [], + modifiedBy: null, + requestedBy: { + permissions: 4194336, + id: 18, + email: 'friend@seerr.dev', + plexUsername: null, + username: '', + recoveryLinkExpirationDate: null, + userType: 2, + avatar: + 'https://gravatar.com/avatar/c77fdc27cab83732b8623d2ea873d330?default=mm&size=200', + movieQuotaLimit: null, + movieQuotaDays: null, + tvQuotaLimit: null, + tvQuotaDays: null, + createdAt: '2022-08-17T04:55:28.000Z', + updatedAt: '2022-08-17T04:55:28.000Z', + requestCount: 1, + displayName: 'friend@seerr.dev', + }, + seasonCount: 0, + }, + ], + }).as('getRequests'); + + cy.visit('/'); + cy.wait('@getRequests'); + cy.contains('.slider-header', 'Recent Requests') + .next('[data-testid=media-slider]') + .find('[data-testid=request-card]') + .first() + .find('[data-testid=request-card-title]') + .contains('Movie Not Found'); + }); + + it('loads plex watchlist', () => { + cy.intercept('/api/v1/discover/watchlist', { + fixture: 'watchlist.json', + }).as('getWatchlist'); + // Wait for one of the watchlist movies to resolve + cy.intercept('/api/v1/movie/361743').as('getTmdbMovie'); + + cy.visit('/'); + + cy.wait('@getWatchlist'); + + const sliderHeader = cy.contains('.slider-header', 'Your Plex Watchlist'); + + sliderHeader.scrollIntoView(); + + cy.wait('@getTmdbMovie'); + // Wait a little longer to make sure the movie component reloaded + cy.wait(500); + + sliderHeader + .next('[data-testid=media-slider]') + .find('[data-testid=title-card]') + .first() + .trigger('mouseover') + .find('[data-testid=title-card-title]') + .invoke('text') + .then((text) => { + cy.contains('.slider-header', 'Plex Watchlist') + .next('[data-testid=media-slider]') + .find('[data-testid=title-card]') + .first() + .click(); + cy.get('[data-testid=media-title]').should('contain', text); + }); + }); +}); diff --git a/cypress/e2e/login.cy.ts b/cypress/e2e/login.cy.ts new file mode 100644 index 000000000..1c9554174 --- /dev/null +++ b/cypress/e2e/login.cy.ts @@ -0,0 +1,13 @@ +describe('Login Page', () => { + it('succesfully logs in as an admin', () => { + cy.loginAsAdmin(); + cy.visit('/'); + cy.contains('Trending'); + }); + + it('succesfully logs in as a local user', () => { + cy.loginAsUser(); + cy.visit('/'); + cy.contains('Trending'); + }); +}); diff --git a/cypress/e2e/movie-details.cy.ts b/cypress/e2e/movie-details.cy.ts new file mode 100644 index 000000000..1d3ecf3f1 --- /dev/null +++ b/cypress/e2e/movie-details.cy.ts @@ -0,0 +1,12 @@ +describe('Movie Details', () => { + it('loads a movie page', () => { + cy.loginAsAdmin(); + // Try to load minions: rise of gru + cy.visit('/movie/438148'); + + cy.get('[data-testid=media-title]').should( + 'contain', + 'Minions: The Rise of Gru (2022)' + ); + }); +}); diff --git a/cypress/e2e/pull-to-refresh.cy.ts b/cypress/e2e/pull-to-refresh.cy.ts new file mode 100644 index 000000000..d56c55897 --- /dev/null +++ b/cypress/e2e/pull-to-refresh.cy.ts @@ -0,0 +1,25 @@ +describe('Pull To Refresh', () => { + beforeEach(() => { + cy.login(Cypress.env('ADMIN_EMAIL'), Cypress.env('ADMIN_PASSWORD')); + cy.viewport(390, 844); + cy.visitMobile('/'); + }); + + it('reloads the current page', () => { + cy.wait(500); + + cy.intercept({ + method: 'GET', + url: '/api/v1/*', + }).as('apiCall'); + + cy.get('.searchbar').swipe('bottom', [190, 400]); + + cy.wait('@apiCall').then((interception) => { + assert.isNotNull( + interception.response.body, + 'API was called and received data' + ); + }); + }); +}); diff --git a/cypress/e2e/settings/general-settings.cy.ts b/cypress/e2e/settings/general-settings.cy.ts new file mode 100644 index 000000000..3717f65b0 --- /dev/null +++ b/cypress/e2e/settings/general-settings.cy.ts @@ -0,0 +1,32 @@ +describe('General Settings', () => { + beforeEach(() => { + cy.loginAsAdmin(); + }); + + it('opens the settings page from the home page', () => { + cy.visit('/'); + + cy.get('[data-testid=sidebar-toggle]').click(); + cy.get('[data-testid=sidebar-menu-settings-mobile]').click(); + + cy.get('.heading').should('contain', 'General Settings'); + }); + + it('modifies setting that requires restart', () => { + cy.visit('/settings'); + + cy.get('#trustProxy').click(); + cy.get('form').submit(); + cy.get('[data-testid=modal-title]').should( + 'contain', + 'Server Restart Required' + ); + + cy.get('[data-testid=modal-ok-button]').click(); + cy.get('[data-testid=modal-title]').should('not.exist'); + + cy.get('[type=checkbox]#trustProxy').click(); + cy.get('form').submit(); + cy.get('[data-testid=modal-title]').should('not.exist'); + }); +}); diff --git a/cypress/e2e/tv-details.cy.ts b/cypress/e2e/tv-details.cy.ts new file mode 100644 index 000000000..5b4bd049a --- /dev/null +++ b/cypress/e2e/tv-details.cy.ts @@ -0,0 +1,28 @@ +describe('TV Details', () => { + it('loads a tv details page', () => { + cy.loginAsAdmin(); + // Try to load stranger things + cy.visit('/tv/66732'); + + cy.get('[data-testid=media-title]').should( + 'contain', + 'Stranger Things (2016)' + ); + }); + + it('shows seasons and expands episodes', () => { + cy.loginAsAdmin(); + + // Try to load stranger things + cy.visit('/tv/66732'); + + // intercept request for season info + cy.intercept('/api/v1/tv/66732/season/4').as('season4'); + + cy.contains('Season 4').should('be.visible').scrollIntoView().click(); + + cy.wait('@season4'); + + cy.contains('Chapter Nine').should('be.visible'); + }); +}); diff --git a/cypress/e2e/user/auto-request-settings.cy.ts b/cypress/e2e/user/auto-request-settings.cy.ts new file mode 100644 index 000000000..e7f5727be --- /dev/null +++ b/cypress/e2e/user/auto-request-settings.cy.ts @@ -0,0 +1,74 @@ +const visitUserEditPage = (email: string): void => { + cy.visit('/users'); + + cy.contains('[data-testid=user-list-row]', email).contains('Edit').click(); +}; + +describe('Auto Request Settings', () => { + beforeEach(() => { + cy.loginAsAdmin(); + }); + + it('should not see watchlist sync settings on an account without permissions', () => { + visitUserEditPage(Cypress.env('USER_EMAIL')); + + cy.contains('Auto-Request Movies').should('not.exist'); + cy.contains('Auto-Request Series').should('not.exist'); + }); + + it('should see watchlist sync settings on an admin account', () => { + visitUserEditPage(Cypress.env('ADMIN_EMAIL')); + + cy.contains('Auto-Request Movies').should('exist'); + cy.contains('Auto-Request Series').should('exist'); + }); + + it('should see auto-request settings after being given permission', () => { + visitUserEditPage(Cypress.env('USER_EMAIL')); + + cy.get('[data-testid=settings-nav-desktop').contains('Permissions').click(); + + cy.get('#autorequest').should('not.be.checked').click(); + + cy.intercept('/api/v1/user/*/settings/permissions').as('userPermissions'); + + cy.contains('Save Changes').click(); + + cy.wait('@userPermissions'); + + cy.reload(); + + cy.get('#autorequest').should('be.checked'); + cy.get('#autorequestmovies').should('be.checked'); + cy.get('#autorequesttv').should('be.checked'); + + cy.get('[data-testid=settings-nav-desktop').contains('General').click(); + + cy.contains('Auto-Request Movies').should('exist'); + cy.contains('Auto-Request Series').should('exist'); + + cy.get('#watchlistSyncMovies').should('not.be.checked').click(); + cy.get('#watchlistSyncTv').should('not.be.checked').click(); + + cy.intercept('/api/v1/user/*/settings/main').as('userMain'); + + cy.contains('Save Changes').click(); + + cy.wait('@userMain'); + + cy.reload(); + + cy.get('#watchlistSyncMovies').should('be.checked').click(); + cy.get('#watchlistSyncTv').should('be.checked').click(); + + cy.contains('Save Changes').click(); + + cy.wait('@userMain'); + + cy.get('[data-testid=settings-nav-desktop').contains('Permissions').click(); + + cy.get('#autorequest').should('be.checked').click(); + + cy.contains('Save Changes').click(); + }); +}); diff --git a/cypress/e2e/user/profile.cy.ts b/cypress/e2e/user/profile.cy.ts new file mode 100644 index 000000000..9cc38d887 --- /dev/null +++ b/cypress/e2e/user/profile.cy.ts @@ -0,0 +1,50 @@ +describe('User Profile', () => { + beforeEach(() => { + cy.loginAsAdmin(); + }); + + it('opens user profile page from the home page', () => { + cy.visit('/'); + + cy.get('[data-testid=user-menu]').click(); + cy.get('[data-testid=user-menu-profile]').click(); + + cy.get('h1').should('contain', Cypress.env('ADMIN_EMAIL')); + }); + + it('loads plex watchlist', () => { + cy.intercept('/api/v1/user/[0-9]*/watchlist', { + fixture: 'watchlist.json', + }).as('getWatchlist'); + // Wait for one of the watchlist movies to resolve + cy.intercept('/api/v1/movie/361743').as('getTmdbMovie'); + + cy.visit('/profile'); + + cy.wait('@getWatchlist'); + + const sliderHeader = cy.contains('.slider-header', 'Plex Watchlist'); + + sliderHeader.scrollIntoView(); + + cy.wait('@getTmdbMovie'); + // Wait a little longer to make sure the movie component reloaded + cy.wait(500); + + sliderHeader + .next('[data-testid=media-slider]') + .find('[data-testid=title-card]') + .first() + .trigger('mouseover') + .find('[data-testid=title-card-title]') + .invoke('text') + .then((text) => { + cy.contains('.slider-header', 'Plex Watchlist') + .next('[data-testid=media-slider]') + .find('[data-testid=title-card]') + .first() + .click(); + cy.get('[data-testid=media-title]').should('contain', text); + }); + }); +}); diff --git a/cypress/e2e/user/user-list.cy.ts b/cypress/e2e/user/user-list.cy.ts new file mode 100644 index 000000000..503bd23f1 --- /dev/null +++ b/cypress/e2e/user/user-list.cy.ts @@ -0,0 +1,70 @@ +const testUser = { + displayName: 'Test User', + emailAddress: 'test@seeerr.dev', + password: 'test1234', +}; + +describe('User List', () => { + beforeEach(() => { + cy.loginAsAdmin(); + }); + + it('opens the user list from the home page', () => { + cy.visit('/'); + + cy.get('[data-testid=sidebar-toggle]').click(); + cy.get('[data-testid=sidebar-menu-users-mobile]').click(); + + cy.get('[data-testid=page-header]').should('contain', 'User List'); + }); + + it('can find the admin user and friend user in the user list', () => { + cy.visit('/users'); + + cy.get('[data-testid=user-list-row]').contains(Cypress.env('ADMIN_EMAIL')); + cy.get('[data-testid=user-list-row]').contains(Cypress.env('USER_EMAIL')); + }); + + it('can create a local user', () => { + cy.visit('/users'); + + cy.contains('Create Local User').click(); + + cy.get('[data-testid=modal-title]').should('contain', 'Create Local User'); + + cy.get('#displayName').type(testUser.displayName); + cy.get('#email').type(testUser.emailAddress); + cy.get('#password').type(testUser.password); + + cy.intercept('/api/v1/user?take=10&skip=0&sort=displayname').as('user'); + + cy.get('[data-testid=modal-ok-button]').click(); + + cy.wait('@user'); + // Wait a little longer for the user list to fully re-render + cy.wait(1000); + + cy.get('[data-testid=user-list-row]').contains(testUser.emailAddress); + }); + + it('can delete the created local test user', () => { + cy.visit('/users'); + + cy.contains('[data-testid=user-list-row]', testUser.emailAddress) + .contains('Delete') + .click(); + + cy.get('[data-testid=modal-title]').should('contain', `Delete User`); + + cy.intercept('/api/v1/user?take=10&skip=0&sort=displayname').as('user'); + + cy.get('[data-testid=modal-ok-button]').should('contain', 'Delete').click(); + + cy.wait('@user'); + cy.wait(1000); + + cy.get('[data-testid=user-list-row]') + .contains(testUser.emailAddress) + .should('not.exist'); + }); +}); diff --git a/cypress/fixtures/watchlist.json b/cypress/fixtures/watchlist.json new file mode 100644 index 000000000..896cef740 --- /dev/null +++ b/cypress/fixtures/watchlist.json @@ -0,0 +1,25 @@ +{ + "page": 1, + "totalPages": 1, + "totalResults": 3, + "results": [ + { + "ratingKey": "5d776be17a53e9001e732ab9", + "title": "Top Gun: Maverick", + "mediaType": "movie", + "tmdbId": 361743 + }, + { + "ratingKey": "5e16338fbc1372003ea68ab3", + "title": "Nope", + "mediaType": "movie", + "tmdbId": 762504 + }, + { + "ratingKey": "5f409b8452f200004161e126", + "title": "Hocus Pocus 2", + "mediaType": "movie", + "tmdbId": 642885 + } + ] +} diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts new file mode 100644 index 000000000..0eb9c869a --- /dev/null +++ b/cypress/support/commands.ts @@ -0,0 +1,35 @@ +/// +import 'cy-mobile-commands'; + +Cypress.Commands.add('login', (email, password) => { + cy.session( + [email, password], + () => { + cy.visit('/login'); + cy.contains('Use your Overseerr account').click(); + + cy.get('[data-testid=email]').type(email); + cy.get('[data-testid=password]').type(password); + + cy.intercept('/api/v1/auth/local').as('localLogin'); + cy.get('[data-testid=local-signin-button]').click(); + + cy.wait('@localLogin'); + + cy.url().should('contain', '/'); + }, + { + validate() { + cy.request('/api/v1/auth/me').its('status').should('eq', 200); + }, + } + ); +}); + +Cypress.Commands.add('loginAsAdmin', () => { + cy.login(Cypress.env('ADMIN_EMAIL'), Cypress.env('ADMIN_PASSWORD')); +}); + +Cypress.Commands.add('loginAsUser', () => { + cy.login(Cypress.env('USER_EMAIL'), Cypress.env('USER_PASSWORD')); +}); diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts new file mode 100644 index 000000000..7a7697cab --- /dev/null +++ b/cypress/support/e2e.ts @@ -0,0 +1,7 @@ +import './commands'; + +before(() => { + if (Cypress.env('SEED_DATABASE')) { + cy.exec('yarn cypress:prepare'); + } +}); diff --git a/cypress/support/index.ts b/cypress/support/index.ts new file mode 100644 index 000000000..857067613 --- /dev/null +++ b/cypress/support/index.ts @@ -0,0 +1,14 @@ +/* eslint-disable @typescript-eslint/no-namespace */ +/// + +declare global { + namespace Cypress { + interface Chainable { + login(email?: string, password?: string): Chainable; + loginAsAdmin(): Chainable; + loginAsUser(): Chainable; + } + } +} + +export {}; diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json new file mode 100644 index 000000000..1b6425b80 --- /dev/null +++ b/cypress/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["es5", "dom"], + "types": ["cypress", "node"], + "resolveJsonModule": true, + "esModuleInterop": true + }, + "include": ["**/*.ts"] +} diff --git a/docs/extending-overseerr/third-party.md b/docs/extending-overseerr/third-party.md index 7ff2bcabf..e1bacfc67 100644 --- a/docs/extending-overseerr/third-party.md +++ b/docs/extending-overseerr/third-party.md @@ -9,7 +9,7 @@ - [LunaSea](https://docs.lunasea.app/modules/overseerr), a self-hosted controller for mobile and macOS - [Requestrr](https://github.com/darkalfx/requestrr/wiki/Configuring-Overseerr), a Discord chatbot - [Doplarr](https://github.com/kiranshila/Doplarr), a Discord request bot -- [Overseerr Assistant](https://github.com/RemiRigal/Overseerr-Assistant), a browser extension for requesting directly from TMDb and IMDb +- [Overseerr Assistant](https://github.com/RemiRigal/Overseerr-Assistant), a browser extension for requesting directly from TMDB and IMDb - [ha-overseerr](https://github.com/vaparr/ha-overseerr), a custom Home Assistant component - [OverCLIrr](https://github.com/WillFantom/OverCLIrr), a command-line tool - [Overseerr Exporter](https://github.com/WillFantom/overseerr-exporter), a Prometheus exporter diff --git a/docs/support/faq.md b/docs/support/faq.md index 56a170941..c638e8636 100644 --- a/docs/support/faq.md +++ b/docs/support/faq.md @@ -45,7 +45,7 @@ Overseerr currently supports the following agents: - New Plex TV - Legacy Plex TV - TheTVDB -- TMDb +- TMDB - [HAMA](https://github.com/ZeroQI/Hama.bundle) Please verify that your library is using one of the agents previously listed. @@ -67,7 +67,7 @@ You can also perform the following to verify the media item has a GUID Overseerr 1. Go to the media item in Plex and **"Get info"** and click on **"View XML"**. 2. Verify that the media item's GUID follows one of the below formats: - 1. TMDb agent `guid="com.plexapp.agents.themoviedb://1705"` + 1. TMDB agent `guid="com.plexapp.agents.themoviedb://1705"` 2. New Plex Movie agent `` 3. TheTVDB agent `guid="com.plexapp.agents.thetvdb://78874/1/1"` 4. Legacy Plex Movie agent `guid="com.plexapp.agents.imdb://tt0765446"` diff --git a/docs/using-overseerr/notifications/webhooks.md b/docs/using-overseerr/notifications/webhooks.md index 37a5c0486..cd2ada314 100644 --- a/docs/using-overseerr/notifications/webhooks.md +++ b/docs/using-overseerr/notifications/webhooks.md @@ -81,7 +81,7 @@ These following special variables are only included in media-related notificatio | Variable | Value | | -------------------- | -------------------------------------------------------------------------------------------------------------- | | `{{media_type}}` | The media type (`movie` or `tv`) | -| `{{media_tmdbid}}` | The media's TMDb ID | +| `{{media_tmdbid}}` | The media's TMDB ID | | `{{media_tvdbid}}` | The media's TheTVDB ID | | `{{media_status}}` | The media's availability status (`UNKNOWN`, `PENDING`, `PROCESSING`, `PARTIALLY_AVAILABLE`, or `AVAILABLE`) | | `{{media_status4k}}` | The media's 4K availability status (`UNKNOWN`, `PENDING`, `PROCESSING`, `PARTIALLY_AVAILABLE`, or `AVAILABLE`) | diff --git a/merged-prettier-plugin.js b/merged-prettier-plugin.js new file mode 100644 index 000000000..6908488f5 --- /dev/null +++ b/merged-prettier-plugin.js @@ -0,0 +1,21 @@ +/* eslint-disable */ +const tailwind = require('prettier-plugin-tailwindcss'); +const organizeImports = require('prettier-plugin-organize-imports'); + +const combinedFormatter = { + ...tailwind, + parsers: { + ...tailwind.parsers, + ...Object.keys(organizeImports.parsers).reduce((acc, key) => { + acc[key] = { + ...tailwind.parsers[key], + preprocess(code, options) { + return organizeImports.parsers[key].preprocess(code, options); + }, + }; + return acc; + }, {}), + }, +}; + +module.exports = combinedFormatter; diff --git a/next.config.js b/next.config.js index 40d899f7a..bf7c70582 100644 --- a/next.config.js +++ b/next.config.js @@ -1,3 +1,6 @@ +/** + * @type {import('next').NextConfig} + */ module.exports = { env: { commitTag: process.env.COMMIT_TAG || 'local', @@ -18,4 +21,7 @@ module.exports = { return config; }, + experimental: { + scrollRestoration: true, + }, }; diff --git a/overseerr-api.yml b/overseerr-api.yml index 24a0ed10e..02f65274e 100644 --- a/overseerr-api.yml +++ b/overseerr-api.yml @@ -1841,14 +1841,14 @@ components: paths: /status: get: - summary: Get Overseerr version - description: Returns the current Overseerr version in a JSON object. + summary: Get Overseerr status + description: Returns the current Overseerr status in a JSON object. security: [] tags: - public responses: '200': - description: Returned version + description: Returned status content: application/json: schema: @@ -1859,6 +1859,12 @@ paths: example: 1.0.0 commitTag: type: string + updateAvailable: + type: boolean + commitsBehind: + type: number + restartRequired: + type: boolean /status/appdata: get: summary: Get application data volume status @@ -2725,6 +2731,12 @@ paths: nullable: true enum: [debug, info, warn, error] default: debug + - in: query + name: search + schema: + type: string + nullable: true + example: plex responses: '200': description: Server log returned @@ -3394,8 +3406,8 @@ paths: name: guid required: true schema: - type: number - example: 1 + type: string + example: '9afef5a7-ec89-4d5f-9397-261e96970b50' responses: '200': description: OK @@ -3759,6 +3771,53 @@ paths: restricted: type: boolean example: false + /user/{userId}/watchlist: + get: + summary: Get user by ID + description: | + Retrieves a user's Plex Watchlist in a JSON object. + tags: + - users + parameters: + - in: path + name: userId + required: true + schema: + type: number + - in: query + name: page + schema: + type: number + example: 1 + default: 1 + responses: + '200': + description: Watchlist data returned + content: + application/json: + schema: + type: object + properties: + page: + type: number + totalPages: + type: number + totalResults: + type: number + results: + type: array + items: + type: object + properties: + tmdbId: + type: number + example: 1 + ratingKey: + type: string + type: + type: string + title: + type: string /user/{userId}/settings/main: get: summary: Get general settings for a user @@ -4650,6 +4709,46 @@ paths: name: type: string example: Genre Name + /discover/watchlist: + get: + summary: Get the Plex watchlist. + tags: + - search + parameters: + - in: query + name: page + schema: + type: number + example: 1 + default: 1 + responses: + '200': + description: Watchlist data returned + content: + application/json: + schema: + type: object + properties: + page: + type: number + totalPages: + type: number + totalResults: + type: number + results: + type: array + items: + type: object + properties: + tmdbId: + type: number + example: 1 + ratingKey: + type: string + type: + type: string + title: + type: string /request: get: summary: Get all requests @@ -4677,7 +4776,16 @@ paths: schema: type: string nullable: true - enum: [all, approved, available, pending, processing, unavailable] + enum: + [ + all, + approved, + available, + pending, + processing, + unavailable, + failed, + ] - in: query name: sort schema: @@ -5597,7 +5705,7 @@ paths: $ref: '#/components/schemas/SonarrSeries' /regions: get: - summary: Regions supported by TMDb + summary: Regions supported by TMDB description: Returns a list of regions in a JSON object. tags: - tmdb @@ -5619,7 +5727,7 @@ paths: example: United States of America /languages: get: - summary: Languages supported by TMDb + summary: Languages supported by TMDB description: Returns a list of languages in a JSON object. tags: - tmdb @@ -5684,7 +5792,7 @@ paths: $ref: '#/components/schemas/ProductionCompany' /genres/movie: get: - summary: Get list of official TMDb movie genres + summary: Get list of official TMDB movie genres description: Returns a list of genres in a JSON array. tags: - tmdb @@ -5712,7 +5820,7 @@ paths: example: Family /genres/tv: get: - summary: Get list of official TMDb movie genres + summary: Get list of official TMDB movie genres description: Returns a list of genres in a JSON array. tags: - tmdb diff --git a/package.json b/package.json index 7d5bb637d..333fdfcef 100644 --- a/package.json +++ b/package.json @@ -3,18 +3,25 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "nodemon -e ts --watch server --watch overseerr-api.yml -e .json,.ts,.yml -x ts-node --files --project server/tsconfig.json server/index.ts", - "build:server": "tsc --project server/tsconfig.json && copyfiles -u 2 server/templates/**/*.{html,pug} dist/templates", + "dev": "nodemon -e ts --watch server --watch overseerr-api.yml -e .json,.ts,.yml -x ts-node -r tsconfig-paths/register --files --project server/tsconfig.json server/index.ts", + "build:server": "tsc --project server/tsconfig.json && copyfiles -u 2 server/templates/**/*.{html,pug} dist/templates && tsc-alias -p server/tsconfig.json", "build:next": "next build", "build": "yarn build:next && yarn build:server", - "lint": "eslint \"./server/**/*.{ts,tsx}\" \"./src/**/*.{ts,tsx}\"", + "lint": "eslint \"./server/**/*.{ts,tsx}\" \"./src/**/*.{ts,tsx}\" --cache", "start": "NODE_ENV=production node dist/index.js", "i18n:extract": "extract-messages -l=en -o src/i18n/locale -d en --flat true --overwriteDefault true \"./src/**/!(*.test).{ts,tsx}\"", - "migration:generate": "ts-node --project server/tsconfig.json ./node_modules/typeorm/cli.js migration:generate", - "migration:create": "ts-node --project server/tsconfig.json ./node_modules/typeorm/cli.js migration:create", - "migration:run": "ts-node --project server/tsconfig.json ./node_modules/typeorm/cli.js migration:run", - "format": "prettier --write .", - "prepare": "husky install" + "migration:generate": "ts-node -r tsconfig-paths/register --project server/tsconfig.json ./node_modules/typeorm/cli.js migration:generate -d server/datasource.ts", + "migration:create": "ts-node -r tsconfig-paths/register --project server/tsconfig.json ./node_modules/typeorm/cli.js migration:create -d server/datasource.ts", + "migration:run": "ts-node -r tsconfig-paths/register --project server/tsconfig.json ./node_modules/typeorm/cli.js migration:run -d server/datasource.ts", + "format": "prettier --loglevel warn --write --cache .", + "format:check": "prettier --check --cache .", + "typecheck": "yarn typecheck:server && yarn typecheck:client", + "typecheck:server": "tsc --project server/tsconfig.json --noEmit", + "typecheck:client": "tsc --noEmit", + "prepare": "husky install", + "cypress:open": "cypress open", + "cypress:prepare": "ts-node -r tsconfig-paths/register --files --project server/tsconfig.json server/scripts/prepareTestDb.ts", + "cypress:build": "yarn build && yarn cypress:prepare" }, "repository": { "type": "git", @@ -22,129 +29,145 @@ }, "license": "MIT", "dependencies": { - "@headlessui/react": "^1.5.0", - "@heroicons/react": "^1.0.6", - "@supercharge/request-ip": "^1.2.0", - "@svgr/webpack": "^6.2.1", - "@tanem/react-nprogress": "^4.0.10", - "ace-builds": "^1.4.14", - "axios": "^0.26.1", - "bcrypt": "^5.0.1", - "bowser": "^2.11.0", - "connect-typeorm": "^1.1.4", - "cookie-parser": "^1.4.6", - "copy-to-clipboard": "^3.3.1", - "country-flag-icons": "^1.4.21", - "csurf": "^1.11.0", - "email-templates": "^8.0.10", - "email-validator": "^2.0.4", - "express": "^4.17.3", - "express-openapi-validator": "^4.13.6", - "express-rate-limit": "^6.3.0", - "express-session": "^1.17.2", - "formik": "^2.2.9", - "gravatar-url": "^3.1.0", - "intl": "^1.2.5", - "lodash": "^4.17.21", - "next": "12.1.0", - "node-cache": "^5.1.2", - "node-gyp": "^9.0.0", - "node-schedule": "^2.1.0", - "nodemailer": "^6.7.2", - "openpgp": "^5.2.0", - "plex-api": "^5.3.2", - "pug": "^3.0.2", - "react": "17.0.2", - "react-ace": "^9.5.0", - "react-animate-height": "^2.0.23", - "react-dom": "17.0.2", - "react-intersection-observer": "^8.33.1", - "react-intl": "5.24.7", - "react-markdown": "^8.0.0", - "react-select": "^5.2.2", - "react-spring": "^9.4.4", - "react-toast-notifications": "^2.5.1", - "react-transition-group": "^4.4.2", - "react-truncate-markup": "^5.1.0", - "react-use-clipboard": "1.0.7", - "reflect-metadata": "^0.1.13", - "secure-random-password": "^0.2.3", - "semver": "^7.3.5", - "sqlite3": "^5.0.2", - "swagger-ui-express": "^4.3.0", - "swr": "^1.2.2", - "typeorm": "0.2.45", - "web-push": "^3.4.5", - "winston": "^3.6.0", - "winston-daily-rotate-file": "^4.6.1", - "xml2js": "^0.4.23", - "yamljs": "^0.3.0", - "yup": "^0.32.11" + "@formatjs/intl-displaynames": "6.0.3", + "@formatjs/intl-locale": "3.0.3", + "@formatjs/intl-pluralrules": "5.0.3", + "@formatjs/intl-utils": "3.8.4", + "@headlessui/react": "0.0.0-insiders.b301f04", + "@heroicons/react": "1.0.6", + "@supercharge/request-ip": "1.2.0", + "@svgr/webpack": "6.3.1", + "@tanem/react-nprogress": "5.0.11", + "ace-builds": "1.9.6", + "axios": "0.27.2", + "axios-rate-limit": "1.3.0", + "bcrypt": "5.0.1", + "bowser": "2.11.0", + "connect-typeorm": "1.1.4", + "cookie-parser": "1.4.6", + "copy-to-clipboard": "3.3.2", + "country-flag-icons": "1.5.5", + "cronstrue": "2.11.0", + "csurf": "1.11.0", + "date-fns": "2.29.1", + "email-templates": "9.0.0", + "email-validator": "2.0.4", + "express": "4.18.1", + "express-openapi-validator": "4.13.8", + "express-rate-limit": "6.5.1", + "express-session": "1.17.3", + "formik": "2.2.9", + "gravatar-url": "3.1.0", + "intl": "1.2.5", + "lodash": "4.17.21", + "next": "12.2.5", + "node-cache": "5.1.2", + "node-gyp": "9.1.0", + "node-schedule": "2.1.0", + "nodemailer": "6.7.8", + "openpgp": "5.4.0", + "plex-api": "5.3.2", + "pug": "3.0.2", + "pulltorefreshjs": "0.1.22", + "react": "18.2.0", + "react-ace": "10.1.0", + "react-animate-height": "2.1.2", + "react-dom": "18.2.0", + "react-intersection-observer": "9.4.0", + "react-intl": "6.0.5", + "react-markdown": "8.0.3", + "react-popper-tooltip": "4.4.2", + "react-select": "5.4.0", + "react-spring": "9.5.2", + "react-toast-notifications": "2.5.1", + "react-truncate-markup": "5.1.2", + "react-use-clipboard": "1.0.8", + "reflect-metadata": "0.1.13", + "secure-random-password": "0.2.3", + "semver": "7.3.7", + "sqlite3": "5.0.11", + "swagger-ui-express": "4.5.0", + "swr": "1.3.0", + "typeorm": "0.3.7", + "web-push": "3.5.0", + "winston": "3.8.1", + "winston-daily-rotate-file": "4.7.1", + "xml2js": "0.4.23", + "yamljs": "0.3.0", + "yup": "0.32.11" }, "devDependencies": { - "@babel/cli": "^7.17.6", - "@commitlint/cli": "^16.2.1", - "@commitlint/config-conventional": "^16.2.1", - "@next/eslint-plugin-next": "^12.1.6", - "@semantic-release/changelog": "^6.0.1", - "@semantic-release/commit-analyzer": "^9.0.2", - "@semantic-release/exec": "^6.0.3", - "@semantic-release/git": "^10.0.1", - "@tailwindcss/aspect-ratio": "^0.4.0", - "@tailwindcss/forms": "^0.5.0", - "@tailwindcss/typography": "^0.5.2", - "@types/bcrypt": "^5.0.0", - "@types/cookie-parser": "^1.4.2", - "@types/country-flag-icons": "^1.2.0", - "@types/csurf": "^1.11.2", - "@types/email-templates": "^8.0.4", - "@types/express": "^4.17.13", - "@types/express-session": "^1.17.4", - "@types/lodash": "^4.14.179", - "@types/node": "^17.0.21", - "@types/node-schedule": "^1.3.2", - "@types/nodemailer": "^6.4.4", - "@types/react": "^17.0.40", - "@types/react-dom": "^17.0.13", - "@types/react-transition-group": "^4.4.4", - "@types/secure-random-password": "^0.2.1", - "@types/semver": "^7.3.9", - "@types/swagger-ui-express": "^4.1.3", - "@types/web-push": "^3.3.2", - "@types/xml2js": "^0.4.9", - "@types/yamljs": "^0.2.31", - "@types/yup": "^0.29.13", - "@typescript-eslint/eslint-plugin": "^5.14.0", - "@typescript-eslint/parser": "^5.14.0", - "autoprefixer": "^10.4.2", - "babel-plugin-react-intl": "^8.2.25", - "babel-plugin-react-intl-auto": "^3.3.0", - "commitizen": "^4.2.4", - "copyfiles": "^2.4.1", - "cz-conventional-changelog": "^3.3.0", - "eslint": "^8.11.0", - "eslint-config-next": "^12.1.0", - "eslint-config-prettier": "^8.5.0", - "eslint-plugin-formatjs": "^3.0.0", - "eslint-plugin-jsx-a11y": "^6.5.1", - "eslint-plugin-prettier": "^4.0.0", - "eslint-plugin-react": "^7.29.3", - "eslint-plugin-react-hooks": "^4.3.0", - "extract-react-intl-messages": "^4.1.1", - "husky": "^7.0.4", - "lint-staged": "^12.3.5", - "nodemon": "^2.0.15", - "postcss": "^8.4.8", - "prettier": "^2.5.1", - "prettier-plugin-tailwindcss": "^0.1.8", - "semantic-release": "^19.0.2", - "semantic-release-docker-buildx": "^1.0.1", - "tailwindcss": "^3.0.23", - "ts-node": "^10.7.0", - "typescript": "^4.6.2" + "@babel/cli": "7.18.10", + "@commitlint/cli": "17.0.3", + "@commitlint/config-conventional": "17.0.3", + "@semantic-release/changelog": "6.0.1", + "@semantic-release/commit-analyzer": "9.0.2", + "@semantic-release/exec": "6.0.3", + "@semantic-release/git": "10.0.1", + "@tailwindcss/aspect-ratio": "0.4.0", + "@tailwindcss/forms": "0.5.2", + "@tailwindcss/typography": "0.5.4", + "@types/bcrypt": "5.0.0", + "@types/cookie-parser": "1.4.3", + "@types/country-flag-icons": "1.2.0", + "@types/csurf": "1.11.2", + "@types/email-templates": "8.0.4", + "@types/express": "4.17.13", + "@types/express-session": "1.17.4", + "@types/lodash": "4.14.183", + "@types/node": "17.0.36", + "@types/node-schedule": "2.1.0", + "@types/nodemailer": "6.4.5", + "@types/pulltorefreshjs": "0.1.5", + "@types/react": "18.0.17", + "@types/react-dom": "18.0.6", + "@types/react-transition-group": "4.4.5", + "@types/secure-random-password": "0.2.1", + "@types/semver": "7.3.12", + "@types/swagger-ui-express": "4.1.3", + "@types/web-push": "3.3.2", + "@types/xml2js": "0.4.11", + "@types/yamljs": "0.2.31", + "@types/yup": "0.29.14", + "@typescript-eslint/eslint-plugin": "5.33.1", + "@typescript-eslint/parser": "5.33.1", + "autoprefixer": "10.4.8", + "babel-plugin-react-intl": "8.2.25", + "babel-plugin-react-intl-auto": "3.3.0", + "commitizen": "4.2.5", + "copyfiles": "2.4.1", + "cy-mobile-commands": "0.3.0", + "cypress": "10.6.0", + "cz-conventional-changelog": "3.3.0", + "eslint": "8.22.0", + "eslint-config-next": "12.2.5", + "eslint-config-prettier": "8.5.0", + "eslint-plugin-formatjs": "4.1.0", + "eslint-plugin-jsx-a11y": "6.6.1", + "eslint-plugin-no-relative-import-paths": "1.4.0", + "eslint-plugin-prettier": "4.2.1", + "eslint-plugin-react": "7.30.1", + "eslint-plugin-react-hooks": "4.6.0", + "extract-react-intl-messages": "4.1.1", + "husky": "8.0.1", + "lint-staged": "12.4.3", + "nodemon": "2.0.19", + "postcss": "8.4.16", + "prettier": "2.7.1", + "prettier-plugin-organize-imports": "3.1.0", + "prettier-plugin-tailwindcss": "0.1.13", + "semantic-release": "19.0.3", + "semantic-release-docker-buildx": "1.0.1", + "tailwindcss": "3.1.8", + "ts-node": "10.9.1", + "tsc-alias": "1.7.0", + "tsconfig-paths": "4.1.0", + "typescript": "4.7.4" }, "resolutions": { - "sqlite3/node-gyp": "^8.4.1" + "sqlite3/node-gyp": "8.4.1", + "@types/react": "18.0.17", + "@types/react-dom": "18.0.6" }, "config": { "commitizen": { @@ -165,10 +188,6 @@ "@commitlint/config-conventional" ] }, - "prettier": { - "singleQuote": true, - "trailingComma": "es5" - }, "release": { "plugins": [ "@semantic-release/commit-analyzer", diff --git a/renovate.json b/renovate.json new file mode 100644 index 000000000..bc68da3a1 --- /dev/null +++ b/renovate.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:js-app", + "group:allNonMajor", + "docker:disableMajor", + "helpers:disableTypesNodeMajor" + ], + "packageRules": [ + { + "matchManagers": ["github-actions"], + "groupName": "GitHub Actions", + "groupSlug": "github-actions" + }, + { + "matchPackageNames": ["node"], + "groupName": "Node.js", + "groupSlug": "node" + } + ] +} diff --git a/server/api/animelist.ts b/server/api/animelist.ts index 20eb2f60e..740f67250 100644 --- a/server/api/animelist.ts +++ b/server/api/animelist.ts @@ -1,8 +1,8 @@ +import logger from '@server/logger'; import axios from 'axios'; -import xml2js from 'xml2js'; import fs, { promises as fsp } from 'fs'; import path from 'path'; -import logger from '../logger'; +import xml2js from 'xml2js'; const UPDATE_INTERVAL_MSEC = 24 * 3600 * 1000; // how often to download new mapping in milliseconds // originally at https://raw.githubusercontent.com/ScudLee/anime-lists/master/anime-list.xml @@ -14,7 +14,7 @@ const LOCAL_PATH = process.env.CONFIG_DIRECTORY const mappingRegexp = new RegExp(/;[0-9]+-([0-9]+)/g); -// Anime-List xml files are community maintained mappings that Hama agent uses to map AniDB IDs to TVDB/TMDb IDs +// Anime-List xml files are community maintained mappings that Hama agent uses to map AniDB IDs to TVDB/TMDB IDs // https://github.com/Anime-Lists/anime-lists/ interface AnimeMapping { diff --git a/server/api/externalapi.ts b/server/api/externalapi.ts index 2a1d94950..cc1e429ff 100644 --- a/server/api/externalapi.ts +++ b/server/api/externalapi.ts @@ -1,5 +1,7 @@ -import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'; -import NodeCache from 'node-cache'; +import type { AxiosInstance, AxiosRequestConfig } from 'axios'; +import axios from 'axios'; +import rateLimit from 'axios-rate-limit'; +import type NodeCache from 'node-cache'; // 5 minute default TTL (in seconds) const DEFAULT_TTL = 300; @@ -10,6 +12,10 @@ const DEFAULT_ROLLING_BUFFER = 10000; interface ExternalAPIOptions { nodeCache?: NodeCache; headers?: Record; + rateLimit?: { + maxRPS: number; + maxRequests: number; + }; } class ExternalAPI { @@ -31,6 +37,14 @@ class ExternalAPI { ...options.headers, }, }); + + if (options.rateLimit) { + this.axios = rateLimit(this.axios, { + maxRequests: options.rateLimit.maxRequests, + maxRPS: options.rateLimit.maxRPS, + }); + } + this.baseUrl = baseUrl; this.cache = options.nodeCache; } diff --git a/server/api/github.ts b/server/api/github.ts index a2a71b41f..86539903b 100644 --- a/server/api/github.ts +++ b/server/api/github.ts @@ -1,5 +1,5 @@ -import cacheManager from '../lib/cache'; -import logger from '../logger'; +import cacheManager from '@server/lib/cache'; +import logger from '@server/logger'; import ExternalAPI from './externalapi'; interface GitHubRelease { diff --git a/server/api/jellyfin.ts b/server/api/jellyfin.ts index 5dd258c4a..79b0778a9 100644 --- a/server/api/jellyfin.ts +++ b/server/api/jellyfin.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import axios, { AxiosInstance } from 'axios'; -import logger from '../logger'; +import logger from '@server/logger'; +import type { AxiosInstance } from 'axios'; +import axios from 'axios'; export interface JellyfinUserResponse { Name: string; @@ -16,7 +17,7 @@ export interface JellyfinLoginResponse { } export interface JellyfinUserListResponse { - users: Array; + users: JellyfinUserResponse[]; } export interface JellyfinLibrary { diff --git a/server/api/plexapi.ts b/server/api/plexapi.ts index 73278387a..90aae4852 100644 --- a/server/api/plexapi.ts +++ b/server/api/plexapi.ts @@ -1,6 +1,7 @@ +import type { Library, PlexSettings } from '@server/lib/settings'; +import { getSettings } from '@server/lib/settings'; +import logger from '@server/logger'; import NodePlexAPI from 'plex-api'; -import { getSettings, Library, PlexSettings } from '../lib/settings'; -import logger from '../logger'; export interface PlexLibraryItem { ratingKey: string; @@ -130,7 +131,6 @@ class PlexAPI { }); } - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types public async getStatus() { return await this.plexClient.query('/'); } diff --git a/server/api/plextv.ts b/server/api/plextv.ts index 1733a85a6..76ee66188 100644 --- a/server/api/plextv.ts +++ b/server/api/plextv.ts @@ -1,8 +1,9 @@ -import axios, { AxiosInstance } from 'axios'; +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 xml2js from 'xml2js'; -import { PlexDevice } from '../interfaces/api/plexInterfaces'; -import { getSettings } from '../lib/settings'; -import logger from '../logger'; +import ExternalAPI from './externalapi'; interface PlexAccountResponse { user: PlexUser; @@ -111,20 +112,54 @@ interface UsersResponse { }; } -class PlexTvAPI { +interface WatchlistResponse { + MediaContainer: { + totalSize: number; + Metadata?: { + ratingKey: string; + }[]; + }; +} + +interface MetadataResponse { + MediaContainer: { + Metadata: { + ratingKey: string; + type: 'movie' | 'show'; + title: string; + Guid: { + id: `imdb://tt${number}` | `tmdb://${number}` | `tvdb://${number}`; + }[]; + }[]; + }; +} + +export interface PlexWatchlistItem { + ratingKey: string; + tmdbId: number; + tvdbId?: number; + type: 'movie' | 'show'; + title: string; +} + +class PlexTvAPI extends ExternalAPI { private authToken: string; - private axios: AxiosInstance; constructor(authToken: string) { + super( + 'https://plex.tv', + {}, + { + headers: { + 'X-Plex-Token': authToken, + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + nodeCache: cacheManager.getCache('plextv').data, + } + ); + this.authToken = authToken; - this.axios = axios.create({ - baseURL: 'https://plex.tv', - headers: { - 'X-Plex-Token': this.authToken, - 'Content-Type': 'application/json', - Accept: 'application/json', - }, - }); } public async getDevices(): Promise { @@ -252,6 +287,83 @@ class PlexTvAPI { )) as UsersResponse; return parsedXml; } + + public async getWatchlist({ + offset = 0, + size = 20, + }: { offset?: number; size?: number } = {}): Promise<{ + offset: number; + size: number; + totalSize: number; + items: PlexWatchlistItem[]; + }> { + try { + const response = await this.axios.get( + '/library/sections/watchlist/all', + { + params: { + 'X-Plex-Container-Start': offset, + 'X-Plex-Container-Size': size, + }, + baseURL: 'https://metadata.provider.plex.tv', + } + ); + + const watchlistDetails = await Promise.all( + (response.data.MediaContainer.Metadata ?? []).map( + async (watchlistItem) => { + const detailedResponse = await this.getRolling( + `/library/metadata/${watchlistItem.ratingKey}`, + { + baseURL: 'https://metadata.provider.plex.tv', + } + ); + + 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') + ); + + 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); + + return { + offset, + size, + totalSize: response.data.MediaContainer.totalSize, + items: filteredList, + }; + } catch (e) { + logger.error('Failed to retrieve watchlist items', { + label: 'Plex.TV Metadata API', + errorMessage: e.message, + }); + return { + offset, + size, + totalSize: 0, + items: [], + }; + } + } } export default PlexTvAPI; diff --git a/server/api/rottentomatoes.ts b/server/api/rottentomatoes.ts index b9b00e108..e190b7b97 100644 --- a/server/api/rottentomatoes.ts +++ b/server/api/rottentomatoes.ts @@ -1,4 +1,4 @@ -import cacheManager from '../lib/cache'; +import cacheManager from '@server/lib/cache'; import ExternalAPI from './externalapi'; interface RTSearchResult { diff --git a/server/api/servarr/base.ts b/server/api/servarr/base.ts index 9e4559339..2b8ec4cb8 100644 --- a/server/api/servarr/base.ts +++ b/server/api/servarr/base.ts @@ -1,6 +1,7 @@ -import cacheManager, { AvailableCacheIds } from '../../lib/cache'; -import { DVRSettings } from '../../lib/settings'; -import ExternalAPI from '../externalapi'; +import ExternalAPI from '@server/api/externalapi'; +import type { AvailableCacheIds } from '@server/lib/cache'; +import cacheManager from '@server/lib/cache'; +import type { DVRSettings } from '@server/lib/settings'; export interface SystemStatus { version: string; diff --git a/server/api/servarr/radarr.ts b/server/api/servarr/radarr.ts index 6064fa30b..809c71ef9 100644 --- a/server/api/servarr/radarr.ts +++ b/server/api/servarr/radarr.ts @@ -1,4 +1,4 @@ -import logger from '../../logger'; +import logger from '@server/logger'; import ServarrBase from './base'; export interface RadarrMovieOptions { @@ -69,7 +69,7 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> { return response.data[0]; } catch (e) { - logger.error('Error retrieving movie by TMDb ID', { + logger.error('Error retrieving movie by TMDB ID', { label: 'Radarr API', errorMessage: e.message, tmdbId: id, diff --git a/server/api/servarr/sonarr.ts b/server/api/servarr/sonarr.ts index fdc00aadb..891641011 100644 --- a/server/api/servarr/sonarr.ts +++ b/server/api/servarr/sonarr.ts @@ -1,4 +1,4 @@ -import logger from '../../logger'; +import logger from '@server/logger'; import ServarrBase from './base'; interface SonarrSeason { diff --git a/server/api/tautulli.ts b/server/api/tautulli.ts index bb7f37235..0e5e07071 100644 --- a/server/api/tautulli.ts +++ b/server/api/tautulli.ts @@ -1,8 +1,9 @@ -import axios, { AxiosInstance } from 'axios'; +import type { User } from '@server/entity/User'; +import type { TautulliSettings } from '@server/lib/settings'; +import logger from '@server/logger'; +import type { AxiosInstance } from 'axios'; +import axios from 'axios'; import { uniqWith } from 'lodash'; -import { User } from '../entity/User'; -import { TautulliSettings } from '../lib/settings'; -import logger from '../logger'; export interface TautulliHistoryRecord { date: number; diff --git a/server/api/themoviedb/index.ts b/server/api/themoviedb/index.ts index b5060c030..ea05b8ab9 100644 --- a/server/api/themoviedb/index.ts +++ b/server/api/themoviedb/index.ts @@ -1,7 +1,7 @@ +import ExternalAPI from '@server/api/externalapi'; +import cacheManager from '@server/lib/cache'; import { sortBy } from 'lodash'; -import cacheManager from '../../lib/cache'; -import ExternalAPI from '../externalapi'; -import { +import type { TmdbCollection, TmdbExternalIdResponse, TmdbGenre, @@ -92,6 +92,10 @@ class TheMovieDb extends ExternalAPI { }, { nodeCache: cacheManager.getCache('tmdb').data, + rateLimit: { + maxRequests: 20, + maxRPS: 50, + }, } ); this.region = region; @@ -192,7 +196,7 @@ class TheMovieDb extends ExternalAPI { return data; } catch (e) { - throw new Error(`[TMDb] Failed to fetch person details: ${e.message}`); + throw new Error(`[TMDB] Failed to fetch person details: ${e.message}`); } }; @@ -214,7 +218,7 @@ class TheMovieDb extends ExternalAPI { return data; } catch (e) { throw new Error( - `[TMDb] Failed to fetch person combined credits: ${e.message}` + `[TMDB] Failed to fetch person combined credits: ${e.message}` ); } }; @@ -241,7 +245,7 @@ class TheMovieDb extends ExternalAPI { return data; } catch (e) { - throw new Error(`[TMDb] Failed to fetch movie details: ${e.message}`); + throw new Error(`[TMDB] Failed to fetch movie details: ${e.message}`); } }; @@ -267,7 +271,7 @@ class TheMovieDb extends ExternalAPI { return data; } catch (e) { - throw new Error(`[TMDb] Failed to fetch TV show details: ${e.message}`); + throw new Error(`[TMDB] Failed to fetch TV show details: ${e.message}`); } }; @@ -293,7 +297,7 @@ class TheMovieDb extends ExternalAPI { return data; } catch (e) { - throw new Error(`[TMDb] Failed to fetch TV show details: ${e.message}`); + throw new Error(`[TMDB] Failed to fetch TV show details: ${e.message}`); } }; @@ -319,7 +323,7 @@ class TheMovieDb extends ExternalAPI { return data; } catch (e) { - throw new Error(`[TMDb] Failed to fetch discover movies: ${e.message}`); + throw new Error(`[TMDB] Failed to fetch discover movies: ${e.message}`); } } @@ -345,7 +349,7 @@ class TheMovieDb extends ExternalAPI { return data; } catch (e) { - throw new Error(`[TMDb] Failed to fetch discover movies: ${e.message}`); + throw new Error(`[TMDB] Failed to fetch discover movies: ${e.message}`); } } @@ -371,7 +375,7 @@ class TheMovieDb extends ExternalAPI { return data; } catch (e) { - throw new Error(`[TMDb] Failed to fetch movies by keyword: ${e.message}`); + throw new Error(`[TMDB] Failed to fetch movies by keyword: ${e.message}`); } } @@ -398,7 +402,7 @@ class TheMovieDb extends ExternalAPI { return data; } catch (e) { throw new Error( - `[TMDb] Failed to fetch TV recommendations: ${e.message}` + `[TMDB] Failed to fetch TV recommendations: ${e.message}` ); } } @@ -422,7 +426,7 @@ class TheMovieDb extends ExternalAPI { return data; } catch (e) { - throw new Error(`[TMDb] Failed to fetch TV similar: ${e.message}`); + throw new Error(`[TMDB] Failed to fetch TV similar: ${e.message}`); } } @@ -455,7 +459,7 @@ class TheMovieDb extends ExternalAPI { return data; } catch (e) { - throw new Error(`[TMDb] Failed to fetch discover movies: ${e.message}`); + throw new Error(`[TMDB] Failed to fetch discover movies: ${e.message}`); } }; @@ -488,7 +492,7 @@ class TheMovieDb extends ExternalAPI { return data; } catch (e) { - throw new Error(`[TMDb] Failed to fetch discover TV: ${e.message}`); + throw new Error(`[TMDB] Failed to fetch discover TV: ${e.message}`); } }; @@ -514,7 +518,7 @@ class TheMovieDb extends ExternalAPI { return data; } catch (e) { - throw new Error(`[TMDb] Failed to fetch upcoming movies: ${e.message}`); + throw new Error(`[TMDB] Failed to fetch upcoming movies: ${e.message}`); } }; @@ -541,7 +545,7 @@ class TheMovieDb extends ExternalAPI { return data; } catch (e) { - throw new Error(`[TMDb] Failed to fetch all trending: ${e.message}`); + throw new Error(`[TMDB] Failed to fetch all trending: ${e.message}`); } }; @@ -564,7 +568,7 @@ class TheMovieDb extends ExternalAPI { return data; } catch (e) { - throw new Error(`[TMDb] Failed to fetch all trending: ${e.message}`); + throw new Error(`[TMDB] Failed to fetch all trending: ${e.message}`); } }; @@ -587,7 +591,7 @@ class TheMovieDb extends ExternalAPI { return data; } catch (e) { - throw new Error(`[TMDb] Failed to fetch all trending: ${e.message}`); + throw new Error(`[TMDB] Failed to fetch all trending: ${e.message}`); } }; @@ -619,7 +623,7 @@ class TheMovieDb extends ExternalAPI { return data; } catch (e) { - throw new Error(`[TMDb] Failed to find by external ID: ${e.message}`); + throw new Error(`[TMDB] Failed to find by external ID: ${e.message}`); } } @@ -657,7 +661,7 @@ class TheMovieDb extends ExternalAPI { throw new Error(`No movie or show returned from API for ID ${imdbId}`); } catch (e) { throw new Error( - `[TMDb] Failed to find media using external IMDb ID: ${e.message}` + `[TMDB] Failed to find media using external IMDb ID: ${e.message}` ); } } @@ -687,7 +691,7 @@ class TheMovieDb extends ExternalAPI { throw new Error(`No show returned from API for ID ${tvdbId}`); } catch (e) { throw new Error( - `[TMDb] Failed to get TV show using the external TVDB ID: ${e.message}` + `[TMDB] Failed to get TV show using the external TVDB ID: ${e.message}` ); } } @@ -711,7 +715,7 @@ class TheMovieDb extends ExternalAPI { return data; } catch (e) { - throw new Error(`[TMDb] Failed to fetch collection: ${e.message}`); + throw new Error(`[TMDB] Failed to fetch collection: ${e.message}`); } } @@ -727,7 +731,7 @@ class TheMovieDb extends ExternalAPI { return regions; } catch (e) { - throw new Error(`[TMDb] Failed to fetch countries: ${e.message}`); + throw new Error(`[TMDB] Failed to fetch countries: ${e.message}`); } } @@ -743,7 +747,7 @@ class TheMovieDb extends ExternalAPI { return languages; } catch (e) { - throw new Error(`[TMDb] Failed to fetch langauges: ${e.message}`); + throw new Error(`[TMDB] Failed to fetch langauges: ${e.message}`); } } @@ -755,7 +759,7 @@ class TheMovieDb extends ExternalAPI { return data; } catch (e) { - throw new Error(`[TMDb] Failed to fetch movie studio: ${e.message}`); + throw new Error(`[TMDB] Failed to fetch movie studio: ${e.message}`); } } @@ -765,7 +769,7 @@ class TheMovieDb extends ExternalAPI { return data; } catch (e) { - throw new Error(`[TMDb] Failed to fetch TV network: ${e.message}`); + throw new Error(`[TMDB] Failed to fetch TV network: ${e.message}`); } } @@ -816,7 +820,7 @@ class TheMovieDb extends ExternalAPI { return movieGenres; } catch (e) { - throw new Error(`[TMDb] Failed to fetch movie genres: ${e.message}`); + throw new Error(`[TMDB] Failed to fetch movie genres: ${e.message}`); } } @@ -867,7 +871,7 @@ class TheMovieDb extends ExternalAPI { return tvGenres; } catch (e) { - throw new Error(`[TMDb] Failed to fetch TV genres: ${e.message}`); + throw new Error(`[TMDB] Failed to fetch TV genres: ${e.message}`); } } } diff --git a/server/constants/media.ts b/server/constants/media.ts index d9ef9e022..de2bf834d 100644 --- a/server/constants/media.ts +++ b/server/constants/media.ts @@ -2,6 +2,7 @@ export enum MediaRequestStatus { PENDING = 1, APPROVED, DECLINED, + FAILED, } export enum MediaType { diff --git a/ormconfig.js b/server/datasource.ts similarity index 64% rename from ormconfig.js rename to server/datasource.ts index 4122f079e..a68392989 100644 --- a/ormconfig.js +++ b/server/datasource.ts @@ -1,4 +1,8 @@ -const devConfig = { +import 'reflect-metadata'; +import type { DataSourceOptions, EntityTarget, Repository } from 'typeorm'; +import { DataSource } from 'typeorm'; + +const devConfig: DataSourceOptions = { type: 'sqlite', database: process.env.CONFIG_DIRECTORY ? `${process.env.CONFIG_DIRECTORY}/db/db.sqlite3` @@ -10,31 +14,30 @@ const devConfig = { entities: ['server/entity/**/*.ts'], migrations: ['server/migration/**/*.ts'], subscribers: ['server/subscriber/**/*.ts'], - cli: { - entitiesDir: 'server/entity', - migrationsDir: 'server/migration', - }, }; -const prodConfig = { +const prodConfig: DataSourceOptions = { type: 'sqlite', database: process.env.CONFIG_DIRECTORY ? `${process.env.CONFIG_DIRECTORY}/db/db.sqlite3` : 'config/db/db.sqlite3', synchronize: false, + migrationsRun: false, logging: false, enableWAL: true, entities: ['dist/entity/**/*.js'], migrations: ['dist/migration/**/*.js'], - migrationsRun: false, subscribers: ['dist/subscriber/**/*.js'], - cli: { - entitiesDir: 'dist/entity', - migrationsDir: 'dist/migration', - }, }; -const finalConfig = - process.env.NODE_ENV !== 'production' ? devConfig : prodConfig; +const dataSource = new DataSource( + process.env.NODE_ENV !== 'production' ? devConfig : prodConfig +); -module.exports = finalConfig; +export const getRepository = ( + target: EntityTarget +): Repository => { + return dataSource.getRepository(target); +}; + +export default dataSource; diff --git a/server/entity/Issue.ts b/server/entity/Issue.ts index d8e05c565..fae96967d 100644 --- a/server/entity/Issue.ts +++ b/server/entity/Issue.ts @@ -1,3 +1,5 @@ +import type { IssueType } from '@server/constants/issue'; +import { IssueStatus } from '@server/constants/issue'; import { Column, CreateDateColumn, @@ -7,7 +9,6 @@ import { PrimaryGeneratedColumn, UpdateDateColumn, } from 'typeorm'; -import { IssueStatus, IssueType } from '../constants/issue'; import IssueComment from './IssueComment'; import Media from './Media'; import { User } from './User'; diff --git a/server/entity/Media.ts b/server/entity/Media.ts index e0cadeef4..ed8e588bf 100644 --- a/server/entity/Media.ts +++ b/server/entity/Media.ts @@ -1,22 +1,23 @@ +import RadarrAPI from '@server/api/servarr/radarr'; +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 type { DownloadingItem } from '@server/lib/downloadtracker'; +import downloadTracker from '@server/lib/downloadtracker'; +import { getSettings } from '@server/lib/settings'; +import logger from '@server/logger'; import { AfterLoad, Column, CreateDateColumn, Entity, - getRepository, In, Index, OneToMany, PrimaryGeneratedColumn, UpdateDateColumn, } from 'typeorm'; -import RadarrAPI from '../api/servarr/radarr'; -import SonarrAPI from '../api/servarr/sonarr'; -import { MediaStatus, MediaType } from '../constants/media'; -import { MediaServerType } from '../constants/server'; -import downloadTracker, { DownloadingItem } from '../lib/downloadtracker'; -import { getSettings } from '../lib/settings'; -import logger from '../logger'; import Issue from './Issue'; import { MediaRequest } from './MediaRequest'; import Season from './Season'; @@ -37,7 +38,7 @@ class Media { } const media = await mediaRepository.find({ - tmdbId: In(finalIds), + where: { tmdbId: In(finalIds) }, }); return media; @@ -56,10 +57,10 @@ class Media { try { const media = await mediaRepository.findOne({ where: { tmdbId: id, mediaType }, - relations: ['requests', 'issues'], + relations: { requests: true, issues: true }, }); - return media; + return media ?? undefined; } catch (e) { logger.error(e.message); return undefined; @@ -152,6 +153,9 @@ class Media { public mediaUrl?: string; public mediaUrl4k?: string; + public iOSPlexUrl?: string; + public iOSPlexUrl4k?: string; + public tautulliUrl?: string; public tautulliUrl4k?: string; @@ -172,20 +176,24 @@ class Media { this.ratingKey }`; + this.iOSPlexUrl = `plex://preplay/?metadataKey=%2Flibrary%2Fmetadata%2F${this.ratingKey}&server=${machineId}`; + if (tautulliUrl) { this.tautulliUrl = `${tautulliUrl}/info?rating_key=${this.ratingKey}`; } - } - if (this.ratingKey4k) { - this.mediaUrl4k = `${ - webAppUrl ? webAppUrl : 'https://app.plex.tv/desktop' - }#!/server/${machineId}/details?key=%2Flibrary%2Fmetadata%2F${ - this.ratingKey4k - }`; + if (this.ratingKey4k) { + this.mediaUrl4k = `${ + webAppUrl ? webAppUrl : 'https://app.plex.tv/desktop' + }#!/server/${machineId}/details?key=%2Flibrary%2Fmetadata%2F${ + this.ratingKey4k + }`; - if (tautulliUrl) { - this.tautulliUrl4k = `${tautulliUrl}/info?rating_key=${this.ratingKey4k}`; + this.iOSPlexUrl4k = `plex://preplay/?metadataKey=%2Flibrary%2Fmetadata%2F${this.ratingKey4k}&server=${machineId}`; + + if (tautulliUrl) { + this.tautulliUrl4k = `${tautulliUrl}/info?rating_key=${this.ratingKey4k}`; + } } } } else { diff --git a/server/entity/MediaRequest.ts b/server/entity/MediaRequest.ts index f7f821156..eefbc11f3 100644 --- a/server/entity/MediaRequest.ts +++ b/server/entity/MediaRequest.ts @@ -1,3 +1,23 @@ +import type { RadarrMovieOptions } from '@server/api/servarr/radarr'; +import RadarrAPI from '@server/api/servarr/radarr'; +import type { + AddSeriesOptions, + SonarrSeries, +} from '@server/api/servarr/sonarr'; +import SonarrAPI from '@server/api/servarr/sonarr'; +import TheMovieDb from '@server/api/themoviedb'; +import { ANIME_KEYWORD_ID } from '@server/api/themoviedb/constants'; +import { + MediaRequestStatus, + MediaStatus, + MediaType, +} from '@server/constants/media'; +import { getRepository } from '@server/datasource'; +import type { MediaRequestBody } from '@server/interfaces/api/requestInterfaces'; +import notificationManager, { Notification } from '@server/lib/notifications'; +import { Permission } from '@server/lib/permissions'; +import { getSettings } from '@server/lib/settings'; +import logger from '@server/logger'; import { isEqual, truncate } from 'lodash'; import { AfterInsert, @@ -6,30 +26,347 @@ import { Column, CreateDateColumn, Entity, - getRepository, ManyToOne, OneToMany, PrimaryGeneratedColumn, RelationCount, UpdateDateColumn, } from 'typeorm'; -import RadarrAPI, { RadarrMovieOptions } from '../api/servarr/radarr'; -import SonarrAPI, { - AddSeriesOptions, - SonarrSeries, -} from '../api/servarr/sonarr'; -import TheMovieDb from '../api/themoviedb'; -import { ANIME_KEYWORD_ID } from '../api/themoviedb/constants'; -import { MediaRequestStatus, MediaStatus, MediaType } from '../constants/media'; -import notificationManager, { Notification } from '../lib/notifications'; -import { getSettings } from '../lib/settings'; -import logger from '../logger'; import Media from './Media'; import SeasonRequest from './SeasonRequest'; import { User } from './User'; +export class RequestPermissionError extends Error {} +export class QuotaRestrictedError extends Error {} +export class DuplicateMediaRequestError extends Error {} +export class NoSeasonsAvailableError extends Error {} + +type MediaRequestOptions = { + isAutoRequest?: boolean; +}; + @Entity() export class MediaRequest { + public static async request( + requestBody: MediaRequestBody, + user: User, + options: MediaRequestOptions = {} + ): Promise { + const tmdb = new TheMovieDb(); + const mediaRepository = getRepository(Media); + const requestRepository = getRepository(MediaRequest); + const userRepository = getRepository(User); + + let requestUser = user; + + if ( + requestBody.userId && + !requestUser.hasPermission([ + Permission.MANAGE_USERS, + Permission.MANAGE_REQUESTS, + ]) + ) { + throw new RequestPermissionError( + 'You do not have permission to modify the request user.' + ); + } else if (requestBody.userId) { + requestUser = await userRepository.findOneOrFail({ + where: { id: requestBody.userId }, + }); + } + + if (!requestUser) { + throw new Error('User missing from request context.'); + } + + if ( + requestBody.mediaType === MediaType.MOVIE && + !requestUser.hasPermission( + requestBody.is4k + ? [Permission.REQUEST_4K, Permission.REQUEST_4K_MOVIE] + : [Permission.REQUEST, Permission.REQUEST_MOVIE], + { + type: 'or', + } + ) + ) { + throw new RequestPermissionError( + `You do not have permission to make ${ + requestBody.is4k ? '4K ' : '' + }movie requests.` + ); + } else if ( + requestBody.mediaType === MediaType.TV && + !requestUser.hasPermission( + requestBody.is4k + ? [Permission.REQUEST_4K, Permission.REQUEST_4K_TV] + : [Permission.REQUEST, Permission.REQUEST_TV], + { + type: 'or', + } + ) + ) { + throw new RequestPermissionError( + `You do not have permission to make ${ + requestBody.is4k ? '4K ' : '' + }series requests.` + ); + } + + const quotas = await requestUser.getQuota(); + + if (requestBody.mediaType === MediaType.MOVIE && quotas.movie.restricted) { + throw new QuotaRestrictedError('Movie Quota exceeded.'); + } else if (requestBody.mediaType === MediaType.TV && quotas.tv.restricted) { + throw new QuotaRestrictedError('Series Quota exceeded.'); + } + + const tmdbMedia = + requestBody.mediaType === MediaType.MOVIE + ? await tmdb.getMovie({ movieId: requestBody.mediaId }) + : await tmdb.getTvShow({ tvId: requestBody.mediaId }); + + let media = await mediaRepository.findOne({ + where: { + tmdbId: requestBody.mediaId, + mediaType: requestBody.mediaType, + }, + relations: ['requests'], + }); + + if (!media) { + media = new Media({ + tmdbId: tmdbMedia.id, + tvdbId: requestBody.tvdbId ?? tmdbMedia.external_ids.tvdb_id, + status: !requestBody.is4k ? MediaStatus.PENDING : MediaStatus.UNKNOWN, + status4k: requestBody.is4k ? MediaStatus.PENDING : MediaStatus.UNKNOWN, + mediaType: requestBody.mediaType, + }); + } else { + if (media.status === MediaStatus.UNKNOWN && !requestBody.is4k) { + media.status = MediaStatus.PENDING; + } + + if (media.status4k === MediaStatus.UNKNOWN && requestBody.is4k) { + media.status4k = MediaStatus.PENDING; + } + } + + const existing = await requestRepository + .createQueryBuilder('request') + .leftJoin('request.media', 'media') + .leftJoinAndSelect('request.requestedBy', 'user') + .where('request.is4k = :is4k', { is4k: requestBody.is4k }) + .andWhere('media.tmdbId = :tmdbId', { tmdbId: tmdbMedia.id }) + .andWhere('media.mediaType = :mediaType', { + mediaType: requestBody.mediaType, + }) + .getMany(); + + if (existing && existing.length > 0) { + // If there is an existing movie request that isn't declined, don't allow a new one. + if ( + requestBody.mediaType === MediaType.MOVIE && + existing[0].status !== MediaRequestStatus.DECLINED + ) { + logger.warn('Duplicate request for media blocked', { + tmdbId: tmdbMedia.id, + mediaType: requestBody.mediaType, + is4k: requestBody.is4k, + label: 'Media Request', + }); + + throw new DuplicateMediaRequestError( + 'Request for this media already exists.' + ); + } + + // If an existing auto-request for this media exists from the same user, + // don't allow a new one. + if ( + existing.find( + (r) => r.requestedBy.id === requestUser.id && r.isAutoRequest + ) + ) { + throw new DuplicateMediaRequestError( + 'Auto-request for this media and user already exists.' + ); + } + } + + if (requestBody.mediaType === MediaType.MOVIE) { + await mediaRepository.save(media); + + const request = new MediaRequest({ + type: MediaType.MOVIE, + media, + requestedBy: requestUser, + // If the user is an admin or has the "auto approve" permission, automatically approve the request + status: user.hasPermission( + [ + requestBody.is4k + ? Permission.AUTO_APPROVE_4K + : Permission.AUTO_APPROVE, + requestBody.is4k + ? Permission.AUTO_APPROVE_4K_MOVIE + : Permission.AUTO_APPROVE_MOVIE, + Permission.MANAGE_REQUESTS, + ], + { type: 'or' } + ) + ? MediaRequestStatus.APPROVED + : MediaRequestStatus.PENDING, + modifiedBy: user.hasPermission( + [ + requestBody.is4k + ? Permission.AUTO_APPROVE_4K + : Permission.AUTO_APPROVE, + requestBody.is4k + ? Permission.AUTO_APPROVE_4K_MOVIE + : Permission.AUTO_APPROVE_MOVIE, + Permission.MANAGE_REQUESTS, + ], + { type: 'or' } + ) + ? user + : undefined, + is4k: requestBody.is4k, + serverId: requestBody.serverId, + profileId: requestBody.profileId, + rootFolder: requestBody.rootFolder, + tags: requestBody.tags, + isAutoRequest: options.isAutoRequest ?? false, + }); + + await requestRepository.save(request); + return request; + } else { + const tmdbMediaShow = tmdbMedia as Awaited< + ReturnType + >; + const requestedSeasons = + requestBody.seasons === 'all' + ? tmdbMediaShow.seasons + .map((season) => season.season_number) + .filter((sn) => sn > 0) + : (requestBody.seasons as number[]); + let existingSeasons: number[] = []; + + // We need to check existing requests on this title to make sure we don't double up on seasons that were + // already requested. In the case they were, we just throw out any duplicates but still approve the request. + // (Unless there are no seasons, in which case we abort) + if (media.requests) { + existingSeasons = media.requests + .filter( + (request) => + request.is4k === requestBody.is4k && + request.status !== MediaRequestStatus.DECLINED + ) + .reduce((seasons, request) => { + const combinedSeasons = request.seasons.map( + (season) => season.seasonNumber + ); + + return [...seasons, ...combinedSeasons]; + }, [] as number[]); + } + + // We should also check seasons that are available/partially available but don't have existing requests + if (media.seasons) { + existingSeasons = [ + ...existingSeasons, + ...media.seasons + .filter( + (season) => + season[requestBody.is4k ? 'status4k' : 'status'] !== + MediaStatus.UNKNOWN + ) + .map((season) => season.seasonNumber), + ]; + } + + const finalSeasons = requestedSeasons.filter( + (rs) => !existingSeasons.includes(rs) + ); + + if (finalSeasons.length === 0) { + throw new NoSeasonsAvailableError('No seasons available to request'); + } else if ( + quotas.tv.limit && + finalSeasons.length > (quotas.tv.remaining ?? 0) + ) { + throw new QuotaRestrictedError('Series Quota exceeded.'); + } + + await mediaRepository.save(media); + + const request = new MediaRequest({ + type: MediaType.TV, + media, + requestedBy: requestUser, + // If the user is an admin or has the "auto approve" permission, automatically approve the request + status: user.hasPermission( + [ + requestBody.is4k + ? Permission.AUTO_APPROVE_4K + : Permission.AUTO_APPROVE, + requestBody.is4k + ? Permission.AUTO_APPROVE_4K_TV + : Permission.AUTO_APPROVE_TV, + Permission.MANAGE_REQUESTS, + ], + { type: 'or' } + ) + ? MediaRequestStatus.APPROVED + : MediaRequestStatus.PENDING, + modifiedBy: user.hasPermission( + [ + requestBody.is4k + ? Permission.AUTO_APPROVE_4K + : Permission.AUTO_APPROVE, + requestBody.is4k + ? Permission.AUTO_APPROVE_4K_TV + : Permission.AUTO_APPROVE_TV, + Permission.MANAGE_REQUESTS, + ], + { type: 'or' } + ) + ? user + : undefined, + is4k: requestBody.is4k, + serverId: requestBody.serverId, + profileId: requestBody.profileId, + rootFolder: requestBody.rootFolder, + languageProfileId: requestBody.languageProfileId, + tags: requestBody.tags, + seasons: finalSeasons.map( + (sn) => + new SeasonRequest({ + seasonNumber: sn, + status: user.hasPermission( + [ + requestBody.is4k + ? Permission.AUTO_APPROVE_4K + : Permission.AUTO_APPROVE, + requestBody.is4k + ? Permission.AUTO_APPROVE_4K_TV + : Permission.AUTO_APPROVE_TV, + Permission.MANAGE_REQUESTS, + ], + { type: 'or' } + ) + ? MediaRequestStatus.APPROVED + : MediaRequestStatus.PENDING, + }) + ), + isAutoRequest: options.isAutoRequest ?? false, + }); + + await requestRepository.save(request); + return request; + } + } + @PrimaryGeneratedColumn() public id: number; @@ -120,6 +457,9 @@ export class MediaRequest { }) public tags?: number[]; + @Column({ default: false }) + public isAutoRequest: boolean; + constructor(init?: Partial) { Object.assign(this, init); } @@ -147,6 +487,10 @@ export class MediaRequest { } this.sendNotification(media, Notification.MEDIA_PENDING); + + if (this.isAutoRequest) { + this.sendNotification(media, Notification.MEDIA_AUTO_REQUESTED); + } } } @@ -191,6 +535,14 @@ export class MediaRequest { : Notification.MEDIA_APPROVED : Notification.MEDIA_DECLINED ); + + if ( + this.status === MediaRequestStatus.APPROVED && + autoApproved && + this.isAutoRequest + ) { + this.sendNotification(media, Notification.MEDIA_AUTO_REQUESTED); + } } } @@ -207,7 +559,7 @@ export class MediaRequest { const mediaRepository = getRepository(Media); const media = await mediaRepository.findOne({ where: { id: this.media.id }, - relations: ['requests'], + relations: { requests: true }, }); if (!media) { logger.error('Media data not found', { @@ -272,7 +624,7 @@ export class MediaRequest { const mediaRepository = getRepository(Media); const fullMedia = await mediaRepository.findOneOrFail({ where: { id: this.media.id }, - relations: ['requests'], + relations: { requests: true }, }); if ( @@ -452,10 +804,13 @@ export class MediaRequest { await mediaRepository.save(media); }) .catch(async () => { - media[this.is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN; - await mediaRepository.save(media); + const requestRepository = getRepository(MediaRequest); + + this.status = MediaRequestStatus.FAILED; + requestRepository.save(this); + logger.warn( - 'Something went wrong sending movie request to Radarr, marking status as UNKNOWN', + 'Something went wrong sending movie request to Radarr, marking status as FAILED', { label: 'Media Request', requestId: this.id, @@ -543,7 +898,7 @@ export class MediaRequest { const media = await mediaRepository.findOne({ where: { id: this.media.id }, - relations: ['requests'], + relations: { requests: true }, }); if (!media) { @@ -670,7 +1025,7 @@ export class MediaRequest { // We grab media again here to make sure we have the latest version of it const media = await mediaRepository.findOne({ where: { id: this.media.id }, - relations: ['requests'], + relations: { requests: true }, }); if (!media) { @@ -685,10 +1040,13 @@ export class MediaRequest { await mediaRepository.save(media); }) .catch(async () => { - media[this.is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN; - await mediaRepository.save(media); + const requestRepository = getRepository(MediaRequest); + + this.status = MediaRequestStatus.FAILED; + requestRepository.save(this); + logger.warn( - 'Something went wrong sending series request to Sonarr, marking status as UNKNOWN', + 'Something went wrong sending series request to Sonarr, marking status as FAILED', { label: 'Media Request', requestId: this.id, @@ -723,6 +1081,7 @@ export class MediaRequest { const mediaType = this.type === MediaType.MOVIE ? 'Movie' : 'Series'; let event: string | undefined; let notifyAdmin = true; + let notifySystem = true; switch (type) { case Notification.MEDIA_APPROVED: @@ -736,6 +1095,13 @@ export class MediaRequest { case Notification.MEDIA_PENDING: event = `New ${this.is4k ? '4K ' : ''}${mediaType} Request`; break; + case Notification.MEDIA_AUTO_REQUESTED: + event = `${ + this.is4k ? '4K ' : '' + }${mediaType} Request Automatically Submitted`; + notifyAdmin = false; + notifySystem = false; + break; case Notification.MEDIA_AUTO_APPROVED: event = `${ this.is4k ? '4K ' : '' @@ -752,6 +1118,7 @@ export class MediaRequest { media, request: this, notifyAdmin, + notifySystem, notifyUser: notifyAdmin ? undefined : this.requestedBy, event, subject: `${movie.title}${ @@ -770,6 +1137,7 @@ export class MediaRequest { media, request: this, notifyAdmin, + notifySystem, notifyUser: notifyAdmin ? undefined : this.requestedBy, event, subject: `${tv.name}${ diff --git a/server/entity/Season.ts b/server/entity/Season.ts index 77f9c7607..44a83d976 100644 --- a/server/entity/Season.ts +++ b/server/entity/Season.ts @@ -1,12 +1,12 @@ +import { MediaStatus } from '@server/constants/media'; import { - Entity, - PrimaryGeneratedColumn, Column, - ManyToOne, CreateDateColumn, + Entity, + ManyToOne, + PrimaryGeneratedColumn, UpdateDateColumn, } from 'typeorm'; -import { MediaStatus } from '../constants/media'; import Media from './Media'; @Entity() diff --git a/server/entity/SeasonRequest.ts b/server/entity/SeasonRequest.ts index f499406c5..f9eeef501 100644 --- a/server/entity/SeasonRequest.ts +++ b/server/entity/SeasonRequest.ts @@ -1,12 +1,12 @@ +import { MediaRequestStatus } from '@server/constants/media'; import { - Entity, - PrimaryGeneratedColumn, Column, CreateDateColumn, - UpdateDateColumn, + Entity, ManyToOne, + PrimaryGeneratedColumn, + UpdateDateColumn, } from 'typeorm'; -import { MediaRequestStatus } from '../constants/media'; import { MediaRequest } from './MediaRequest'; @Entity() diff --git a/server/entity/Session.ts b/server/entity/Session.ts index e7462c195..ddf851a6e 100644 --- a/server/entity/Session.ts +++ b/server/entity/Session.ts @@ -1,5 +1,5 @@ -import { ISession } from 'connect-typeorm'; -import { Index, Column, PrimaryColumn, Entity } from 'typeorm'; +import type { ISession } from 'connect-typeorm'; +import { Column, Entity, Index, PrimaryColumn } from 'typeorm'; @Entity() export class Session implements ISession { diff --git a/server/entity/User.ts b/server/entity/User.ts index 7fa6dc67d..b5f781109 100644 --- a/server/entity/User.ts +++ b/server/entity/User.ts @@ -1,3 +1,13 @@ +import { MediaRequestStatus, MediaType } from '@server/constants/media'; +import { UserType } from '@server/constants/user'; +import { getRepository } from '@server/datasource'; +import type { QuotaResponse } from '@server/interfaces/api/userInterfaces'; +import PreparedEmail from '@server/lib/email'; +import type { PermissionCheckOptions } from '@server/lib/permissions'; +import { hasPermission, Permission } from '@server/lib/permissions'; +import { getSettings } from '@server/lib/settings'; +import logger from '@server/logger'; +import { AfterDate } from '@server/utils/dateHelpers'; import bcrypt from 'bcrypt'; import { randomUUID } from 'crypto'; import path from 'path'; @@ -7,8 +17,6 @@ import { Column, CreateDateColumn, Entity, - getRepository, - MoreThan, Not, OneToMany, OneToOne, @@ -16,17 +24,6 @@ import { RelationCount, UpdateDateColumn, } from 'typeorm'; -import { MediaRequestStatus, MediaType } from '../constants/media'; -import { UserType } from '../constants/user'; -import { QuotaResponse } from '../interfaces/api/userInterfaces'; -import PreparedEmail from '../lib/email'; -import { - hasPermission, - Permission, - PermissionCheckOptions, -} from '../lib/permissions'; -import { getSettings } from '../lib/settings'; -import logger from '../logger'; import Issue from './Issue'; import { MediaRequest } from './MediaRequest'; import SeasonRequest from './SeasonRequest'; @@ -270,13 +267,14 @@ export class User { if (movieQuotaDays) { movieDate.setDate(movieDate.getDate() - movieQuotaDays); } - const movieQuotaStartDate = movieDate.toJSON(); const movieQuotaUsed = movieQuotaLimit ? await requestRepository.count({ where: { - requestedBy: this, - createdAt: MoreThan(movieQuotaStartDate), + requestedBy: { + id: this.id, + }, + createdAt: AfterDate(movieDate), type: MediaType.MOVIE, status: Not(MediaRequestStatus.DECLINED), }, diff --git a/server/entity/UserSettings.ts b/server/entity/UserSettings.ts index 08397b12f..771c382d1 100644 --- a/server/entity/UserSettings.ts +++ b/server/entity/UserSettings.ts @@ -1,3 +1,6 @@ +import type { NotificationAgentTypes } from '@server/interfaces/api/userSettingsInterfaces'; +import { hasNotificationType, Notification } from '@server/lib/notifications'; +import { NotificationAgentKey } from '@server/lib/settings'; import { Column, Entity, @@ -5,9 +8,6 @@ import { OneToOne, PrimaryGeneratedColumn, } from 'typeorm'; -import { NotificationAgentTypes } from '../interfaces/api/userSettingsInterfaces'; -import { hasNotificationType, Notification } from '../lib/notifications'; -import { NotificationAgentKey } from '../lib/settings'; import { User } from './User'; export const ALL_NOTIFICATIONS = Object.values(Notification) @@ -57,6 +57,12 @@ export class UserSettings { @Column({ nullable: true }) public telegramSendSilently?: boolean; + @Column({ nullable: true }) + public watchlistSyncMovies?: boolean; + + @Column({ nullable: true }) + public watchlistSyncTv?: boolean; + @Column({ type: 'text', nullable: true, diff --git a/server/index.ts b/server/index.ts index 3f4e55035..615e789bf 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1,34 +1,37 @@ +import PlexAPI from '@server/api/plexapi'; +import dataSource, { getRepository } from '@server/datasource'; +import { Session } from '@server/entity/Session'; +import { User } from '@server/entity/User'; +import { startJobs } from '@server/job/schedule'; +import notificationManager from '@server/lib/notifications'; +import DiscordAgent from '@server/lib/notifications/agents/discord'; +import EmailAgent from '@server/lib/notifications/agents/email'; +import GotifyAgent from '@server/lib/notifications/agents/gotify'; +import LunaSeaAgent from '@server/lib/notifications/agents/lunasea'; +import PushbulletAgent from '@server/lib/notifications/agents/pushbullet'; +import PushoverAgent from '@server/lib/notifications/agents/pushover'; +import SlackAgent from '@server/lib/notifications/agents/slack'; +import TelegramAgent from '@server/lib/notifications/agents/telegram'; +import WebhookAgent from '@server/lib/notifications/agents/webhook'; +import WebPushAgent from '@server/lib/notifications/agents/webpush'; +import { getSettings } from '@server/lib/settings'; +import logger from '@server/logger'; +import routes from '@server/routes'; +import { getAppVersion } from '@server/utils/appVersion'; +import restartFlag from '@server/utils/restartFlag'; import { getClientIp } from '@supercharge/request-ip'; import { TypeormStore } from 'connect-typeorm/out'; import cookieParser from 'cookie-parser'; import csurf from 'csurf'; -import express, { NextFunction, Request, Response } from 'express'; +import type { NextFunction, Request, Response } from 'express'; +import express from 'express'; import * as OpenApiValidator from 'express-openapi-validator'; -import session, { Store } from 'express-session'; +import type { Store } from 'express-session'; +import session from 'express-session'; import next from 'next'; import path from 'path'; import swaggerUi from 'swagger-ui-express'; -import { createConnection, getRepository } from 'typeorm'; import YAML from 'yamljs'; -import PlexAPI from './api/plexapi'; -import { Session } from './entity/Session'; -import { User } from './entity/User'; -import { startJobs } from './job/schedule'; -import notificationManager from './lib/notifications'; -import DiscordAgent from './lib/notifications/agents/discord'; -import EmailAgent from './lib/notifications/agents/email'; -import GotifyAgent from './lib/notifications/agents/gotify'; -import LunaSeaAgent from './lib/notifications/agents/lunasea'; -import PushbulletAgent from './lib/notifications/agents/pushbullet'; -import PushoverAgent from './lib/notifications/agents/pushover'; -import SlackAgent from './lib/notifications/agents/slack'; -import TelegramAgent from './lib/notifications/agents/telegram'; -import WebhookAgent from './lib/notifications/agents/webhook'; -import WebPushAgent from './lib/notifications/agents/webpush'; -import { getSettings } from './lib/settings'; -import logger from './logger'; -import routes from './routes'; -import { getAppVersion } from './utils/appVersion'; const API_SPEC_PATH = path.join(__dirname, '../overseerr-api.yml'); @@ -40,7 +43,7 @@ const handle = app.getRequestHandler(); app .prepare() .then(async () => { - const dbConnection = await createConnection(); + const dbConnection = await dataSource.initialize(); // Run migrations in production if (process.env.NODE_ENV === 'production') { @@ -51,6 +54,7 @@ app // Load Settings const settings = getSettings().load(); + restartFlag.initializeSettings(settings.main); // Migrate library types if ( @@ -59,8 +63,8 @@ app ) { const userRepository = getRepository(User); const admin = await userRepository.findOne({ - select: ['id', 'plexToken'], - order: { id: 'ASC' }, + select: { id: true, plexToken: true }, + where: { id: 1 }, }); if (admin) { diff --git a/server/interfaces/api/discoverInterfaces.ts b/server/interfaces/api/discoverInterfaces.ts index db90e55d2..89cb7426f 100644 --- a/server/interfaces/api/discoverInterfaces.ts +++ b/server/interfaces/api/discoverInterfaces.ts @@ -3,3 +3,17 @@ export interface GenreSliderItem { name: string; backdrops: string[]; } + +export interface WatchlistItem { + ratingKey: string; + tmdbId: number; + mediaType: 'movie' | 'tv'; + title: string; +} + +export interface WatchlistResponse { + page: number; + totalPages: number; + totalResults: number; + results: WatchlistItem[]; +} diff --git a/server/interfaces/api/issueInterfaces.ts b/server/interfaces/api/issueInterfaces.ts index bd17f1958..e5b3643cd 100644 --- a/server/interfaces/api/issueInterfaces.ts +++ b/server/interfaces/api/issueInterfaces.ts @@ -1,5 +1,5 @@ -import Issue from '../../entity/Issue'; -import { PaginatedResponse } from './common'; +import type Issue from '@server/entity/Issue'; +import type { PaginatedResponse } from './common'; export interface IssueResultsResponse extends PaginatedResponse { results: Issue[]; diff --git a/server/interfaces/api/mediaInterfaces.ts b/server/interfaces/api/mediaInterfaces.ts index d17716d20..263d859ad 100644 --- a/server/interfaces/api/mediaInterfaces.ts +++ b/server/interfaces/api/mediaInterfaces.ts @@ -1,6 +1,6 @@ -import type Media from '../../entity/Media'; -import { User } from '../../entity/User'; -import { PaginatedResponse } from './common'; +import type Media from '@server/entity/Media'; +import type { User } from '@server/entity/User'; +import type { PaginatedResponse } from './common'; export interface MediaResultsResponse extends PaginatedResponse { results: Media[]; diff --git a/server/interfaces/api/personInterfaces.ts b/server/interfaces/api/personInterfaces.ts index 19d3468ce..c52ad0c6a 100644 --- a/server/interfaces/api/personInterfaces.ts +++ b/server/interfaces/api/personInterfaces.ts @@ -1,4 +1,4 @@ -import { PersonCreditCast, PersonCreditCrew } from '../../models/Person'; +import type { PersonCreditCast, PersonCreditCrew } from '@server/models/Person'; export interface PersonCombinedCreditsResponse { id: number; diff --git a/server/interfaces/api/plexInterfaces.ts b/server/interfaces/api/plexInterfaces.ts index 5373cb58a..32be891e9 100644 --- a/server/interfaces/api/plexInterfaces.ts +++ b/server/interfaces/api/plexInterfaces.ts @@ -1,4 +1,4 @@ -import { PlexSettings } from '../../lib/settings'; +import type { PlexSettings } from '@server/lib/settings'; export interface PlexStatus { settings: PlexSettings; diff --git a/server/interfaces/api/requestInterfaces.ts b/server/interfaces/api/requestInterfaces.ts index ca39515bd..89863cb04 100644 --- a/server/interfaces/api/requestInterfaces.ts +++ b/server/interfaces/api/requestInterfaces.ts @@ -1,6 +1,21 @@ +import type { MediaType } from '@server/constants/media'; +import type { MediaRequest } from '@server/entity/MediaRequest'; import type { PaginatedResponse } from './common'; -import type { MediaRequest } from '../../entity/MediaRequest'; export interface RequestResultsResponse extends PaginatedResponse { results: MediaRequest[]; } + +export type MediaRequestBody = { + mediaType: MediaType; + mediaId: number; + tvdbId?: number; + seasons?: number[] | 'all'; + is4k?: boolean; + serverId?: number; + profileId?: number; + rootFolder?: string; + languageProfileId?: number; + userId?: number; + tags?: number[]; +}; diff --git a/server/interfaces/api/serviceInterfaces.ts b/server/interfaces/api/serviceInterfaces.ts index 1188f24c0..3b430b0b5 100644 --- a/server/interfaces/api/serviceInterfaces.ts +++ b/server/interfaces/api/serviceInterfaces.ts @@ -1,5 +1,5 @@ -import { QualityProfile, RootFolder, Tag } from '../../api/servarr/base'; -import { LanguageProfile } from '../../api/servarr/sonarr'; +import type { QualityProfile, RootFolder, Tag } from '@server/api/servarr/base'; +import type { LanguageProfile } from '@server/api/servarr/sonarr'; export interface ServiceCommonServer { id: number; diff --git a/server/interfaces/api/settingsInterfaces.ts b/server/interfaces/api/settingsInterfaces.ts index c486a1b46..bafd15b1f 100644 --- a/server/interfaces/api/settingsInterfaces.ts +++ b/server/interfaces/api/settingsInterfaces.ts @@ -59,4 +59,5 @@ export interface StatusResponse { commitTag: string; updateAvailable: boolean; commitsBehind: number; + restartRequired: boolean; } diff --git a/server/interfaces/api/userInterfaces.ts b/server/interfaces/api/userInterfaces.ts index e5f564826..2ac75c5e1 100644 --- a/server/interfaces/api/userInterfaces.ts +++ b/server/interfaces/api/userInterfaces.ts @@ -1,7 +1,7 @@ -import Media from '../../entity/Media'; -import { MediaRequest } from '../../entity/MediaRequest'; -import type { User } from '../../entity/User'; -import { PaginatedResponse } from './common'; +import type Media from '@server/entity/Media'; +import type { MediaRequest } from '@server/entity/MediaRequest'; +import type { User } from '@server/entity/User'; +import type { PaginatedResponse } from './common'; export interface UserResultsResponse extends PaginatedResponse { results: User[]; @@ -23,6 +23,7 @@ export interface QuotaResponse { movie: QuotaStatus; tv: QuotaStatus; } + export interface UserWatchDataResponse { recentlyWatched: Media[]; playCount: number; diff --git a/server/interfaces/api/userSettingsInterfaces.ts b/server/interfaces/api/userSettingsInterfaces.ts index d0a0ff9f8..e54f0070b 100644 --- a/server/interfaces/api/userSettingsInterfaces.ts +++ b/server/interfaces/api/userSettingsInterfaces.ts @@ -1,4 +1,4 @@ -import { NotificationAgentKey } from '../../lib/settings'; +import type { NotificationAgentKey } from '@server/lib/settings'; export interface UserSettingsGeneralResponse { username?: string; @@ -15,6 +15,8 @@ export interface UserSettingsGeneralResponse { globalMovieQuotaLimit?: number; globalTvQuotaLimit?: number; globalTvQuotaDays?: number; + watchlistSyncMovies?: boolean; + watchlistSyncTv?: boolean; } export type NotificationAgentTypes = Record; diff --git a/server/job/jellyfinsync/index.ts b/server/job/jellyfinsync/index.ts index 23843d924..85c8dcc58 100644 --- a/server/job/jellyfinsync/index.ts +++ b/server/job/jellyfinsync/index.ts @@ -1,17 +1,19 @@ +import type { JellyfinLibraryItem } from '@server/api/jellyfin'; +import JellyfinAPI from '@server/api/jellyfin'; +import TheMovieDb from '@server/api/themoviedb'; +import type { TmdbTvDetails } from '@server/api/themoviedb/interfaces'; +import { MediaStatus, MediaType } from '@server/constants/media'; +import { MediaServerType } from '@server/constants/server'; +import { getRepository } from '@server/datasource'; +import Media from '@server/entity/Media'; +import Season from '@server/entity/Season'; +import { User } from '@server/entity/User'; +import type { Library } from '@server/lib/settings'; +import { getSettings } from '@server/lib/settings'; +import logger from '@server/logger'; +import AsyncLock from '@server/utils/asyncLock'; import { randomUUID as uuid } from 'crypto'; import { uniqWith } from 'lodash'; -import { getRepository } from 'typeorm'; -import JellyfinAPI, { JellyfinLibraryItem } from '../../api/jellyfin'; -import TheMovieDb from '../../api/themoviedb'; -import { TmdbTvDetails } from '../../api/themoviedb/interfaces'; -import { MediaStatus, MediaType } from '../../constants/media'; -import { MediaServerType } from '../../constants/server'; -import Media from '../../entity/Media'; -import Season from '../../entity/Season'; -import { User } from '../../entity/User'; -import { getSettings, Library } from '../../lib/settings'; -import logger from '../../logger'; -import AsyncLock from '../../utils/asyncLock'; const BUNDLE_SIZE = 20; const UPDATE_RATE = 4 * 1000; @@ -552,6 +554,7 @@ class JobJellyfinSync { this.running = true; const userRepository = getRepository(User); const admin = await userRepository.findOne({ + where: { id: 1 }, select: [ 'id', 'jellyfinAuthToken', diff --git a/server/job/schedule.ts b/server/job/schedule.ts index 181d540d3..e21908052 100644 --- a/server/job/schedule.ts +++ b/server/job/schedule.ts @@ -1,11 +1,13 @@ +import { MediaServerType } from '@server/constants/server'; +import downloadTracker from '@server/lib/downloadtracker'; +import { plexFullScanner, plexRecentScanner } from '@server/lib/scanners/plex'; +import { radarrScanner } from '@server/lib/scanners/radarr'; +import { sonarrScanner } from '@server/lib/scanners/sonarr'; +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 schedule from 'node-schedule'; -import { MediaServerType } from '../constants/server'; -import downloadTracker from '../lib/downloadtracker'; -import { plexFullScanner, plexRecentScanner } from '../lib/scanners/plex'; -import { radarrScanner } from '../lib/scanners/radarr'; -import { sonarrScanner } from '../lib/scanners/sonarr'; -import { getSettings, JobId } from '../lib/settings'; -import logger from '../logger'; import { jobJellyfinFullSync, jobJellyfinRecentSync } from './jellyfinsync'; interface ScheduledJob { @@ -14,6 +16,7 @@ interface ScheduledJob { name: string; type: 'process' | 'command'; interval: 'short' | 'long' | 'fixed'; + cronSchedule: string; running?: () => boolean; cancelFn?: () => void; } @@ -31,6 +34,7 @@ export const startJobs = (): void => { name: 'Plex Recently Added Scan', type: 'process', interval: 'short', + cronSchedule: jobs['plex-recently-added-scan'].schedule, job: schedule.scheduleJob( jobs['plex-recently-added-scan'].schedule, () => { @@ -50,6 +54,7 @@ export const startJobs = (): void => { name: 'Plex Full Library Scan', type: 'process', interval: 'long', + cronSchedule: jobs['plex-full-scan'].schedule, job: schedule.scheduleJob(jobs['plex-full-scan'].schedule, () => { logger.info('Starting scheduled job: Plex Full Library Scan', { label: 'Jobs', @@ -69,6 +74,7 @@ export const startJobs = (): void => { name: 'Jellyfin Recently Added Sync', type: 'process', interval: 'long', + cronSchedule: jobs['jellyfin-recently-added-sync'].schedule, job: schedule.scheduleJob( jobs['jellyfin-recently-added-sync'].schedule, () => { @@ -88,6 +94,7 @@ export const startJobs = (): void => { name: 'Jellyfin Full Library Sync', type: 'process', interval: 'long', + cronSchedule: jobs['jellyfin-full-sync'].schedule, job: schedule.scheduleJob(jobs['jellyfin-full-sync'].schedule, () => { logger.info('Starting scheduled job: Jellyfin Full Sync', { label: 'Jobs', @@ -99,12 +106,28 @@ export const startJobs = (): void => { }); } + // Run watchlist sync every 5 minutes + scheduledJobs.push({ + id: 'plex-watchlist-sync', + name: 'Plex Watchlist Sync', + type: 'process', + interval: 'long', + 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(); + }), + }); + // Run full radarr scan every 24 hours scheduledJobs.push({ id: 'radarr-scan', name: 'Radarr Scan', type: 'process', interval: 'long', + cronSchedule: jobs['radarr-scan'].schedule, job: schedule.scheduleJob(jobs['radarr-scan'].schedule, () => { logger.info('Starting scheduled job: Radarr Scan', { label: 'Jobs' }); radarrScanner.run(); @@ -119,6 +142,7 @@ export const startJobs = (): void => { name: 'Sonarr Scan', type: 'process', interval: 'long', + cronSchedule: jobs['sonarr-scan'].schedule, job: schedule.scheduleJob(jobs['sonarr-scan'].schedule, () => { logger.info('Starting scheduled job: Sonarr Scan', { label: 'Jobs' }); sonarrScanner.run(); @@ -133,6 +157,7 @@ export const startJobs = (): void => { name: 'Download Sync', type: 'command', interval: 'fixed', + cronSchedule: jobs['download-sync'].schedule, job: schedule.scheduleJob(jobs['download-sync'].schedule, () => { logger.debug('Starting scheduled job: Download Sync', { label: 'Jobs', @@ -147,6 +172,7 @@ export const startJobs = (): void => { name: 'Download Sync Reset', type: 'command', interval: 'long', + cronSchedule: jobs['download-sync-reset'].schedule, job: schedule.scheduleJob(jobs['download-sync-reset'].schedule, () => { logger.info('Starting scheduled job: Download Sync Reset', { label: 'Jobs', diff --git a/server/lib/cache.ts b/server/lib/cache.ts index 7782a05a8..e81466629 100644 --- a/server/lib/cache.ts +++ b/server/lib/cache.ts @@ -6,7 +6,8 @@ export type AvailableCacheIds = | 'sonarr' | 'rt' | 'github' - | 'plexguid'; + | 'plexguid' + | 'plextv'; const DEFAULT_TTL = 300; const DEFAULT_CHECK_PERIOD = 120; @@ -58,6 +59,10 @@ class CacheManager { stdTtl: 86400 * 7, // 1 week cache checkPeriod: 60 * 30, }), + plextv: new Cache('plextv', 'Plex TV', { + stdTtl: 86400 * 7, // 1 week cache + checkPeriod: 60, + }), }; public getCache(id: AvailableCacheIds): Cache { diff --git a/server/lib/downloadtracker.ts b/server/lib/downloadtracker.ts index c62e189d8..4aef968f1 100644 --- a/server/lib/downloadtracker.ts +++ b/server/lib/downloadtracker.ts @@ -1,9 +1,9 @@ +import RadarrAPI from '@server/api/servarr/radarr'; +import SonarrAPI from '@server/api/servarr/sonarr'; +import { MediaType } from '@server/constants/media'; +import { getSettings } from '@server/lib/settings'; +import logger from '@server/logger'; import { uniqWith } from 'lodash'; -import RadarrAPI from '../api/servarr/radarr'; -import SonarrAPI from '../api/servarr/sonarr'; -import { MediaType } from '../constants/media'; -import logger from '../logger'; -import { getSettings } from './settings'; export interface DownloadingItem { mediaType: MediaType; diff --git a/server/lib/email/index.ts b/server/lib/email/index.ts index 1274d6a8b..c38892ae2 100644 --- a/server/lib/email/index.ts +++ b/server/lib/email/index.ts @@ -1,7 +1,8 @@ +import type { NotificationAgentEmail } from '@server/lib/settings'; +import { getSettings } from '@server/lib/settings'; import Email from 'email-templates'; import nodemailer from 'nodemailer'; import { URL } from 'url'; -import { getSettings, NotificationAgentEmail } from '../settings'; import { openpgpEncrypt } from './openpgpEncrypt'; class PreparedEmail extends Email { diff --git a/server/lib/email/openpgpEncrypt.ts b/server/lib/email/openpgpEncrypt.ts index c067a7d58..dd320ea38 100644 --- a/server/lib/email/openpgpEncrypt.ts +++ b/server/lib/email/openpgpEncrypt.ts @@ -1,7 +1,8 @@ +import logger from '@server/logger'; import { randomBytes } from 'crypto'; import * as openpgp from 'openpgp'; -import { Transform, TransformCallback } from 'stream'; -import logger from '../../logger'; +import type { TransformCallback } from 'stream'; +import { Transform } from 'stream'; interface EncryptorOptions { signingKey?: string; @@ -26,7 +27,7 @@ class PGPEncryptor extends Transform { // just save the whole message _transform = ( - chunk: any, + chunk: Uint8Array, _encoding: BufferEncoding, callback: TransformCallback ): void => { @@ -184,6 +185,9 @@ class PGPEncryptor extends Transform { } export const openpgpEncrypt = (options: EncryptorOptions) => { + // Disabling this line because I don't want to fix it but I am tired + // of seeing the lint warning + // eslint-disable-next-line @typescript-eslint/no-explicit-any return function (mail: any, callback: () => unknown): void { if (!options.encryptionKeys.length) { setImmediate(callback); diff --git a/server/lib/notifications/agents/agent.ts b/server/lib/notifications/agents/agent.ts index edfa1262d..d2b0b1656 100644 --- a/server/lib/notifications/agents/agent.ts +++ b/server/lib/notifications/agents/agent.ts @@ -1,14 +1,15 @@ -import { Notification } from '..'; -import type Issue from '../../../entity/Issue'; -import IssueComment from '../../../entity/IssueComment'; -import Media from '../../../entity/Media'; -import { MediaRequest } from '../../../entity/MediaRequest'; -import { User } from '../../../entity/User'; -import { NotificationAgentConfig } from '../../settings'; +import type Issue from '@server/entity/Issue'; +import type IssueComment from '@server/entity/IssueComment'; +import type Media from '@server/entity/Media'; +import type { MediaRequest } from '@server/entity/MediaRequest'; +import type { User } from '@server/entity/User'; +import type { NotificationAgentConfig } from '@server/lib/settings'; +import type { Notification } from '..'; export interface NotificationPayload { event?: string; subject: string; + notifySystem: boolean; notifyAdmin: boolean; notifyUser?: User; media?: Media; diff --git a/server/lib/notifications/agents/discord.ts b/server/lib/notifications/agents/discord.ts index 321200350..67a278bfb 100644 --- a/server/lib/notifications/agents/discord.ts +++ b/server/lib/notifications/agents/discord.ts @@ -1,19 +1,17 @@ +import { IssueStatus, IssueTypeName } from '@server/constants/issue'; +import { getRepository } from '@server/datasource'; +import { User } from '@server/entity/User'; +import type { NotificationAgentDiscord } from '@server/lib/settings'; +import { getSettings, NotificationAgentKey } from '@server/lib/settings'; +import logger from '@server/logger'; import axios from 'axios'; -import { getRepository } from 'typeorm'; import { hasNotificationType, Notification, shouldSendAdminNotification, } from '..'; -import { IssueStatus, IssueTypeName } from '../../../constants/issue'; -import { User } from '../../../entity/User'; -import logger from '../../../logger'; -import { - getSettings, - NotificationAgentDiscord, - NotificationAgentKey, -} from '../../settings'; -import { BaseAgent, NotificationAgent, NotificationPayload } from './agent'; +import type { NotificationAgent, NotificationPayload } from './agent'; +import { BaseAgent } from './agent'; enum EmbedColors { DEFAULT = 0, @@ -245,7 +243,10 @@ class DiscordAgent ): Promise { const settings = this.getSettings(); - if (!hasNotificationType(type, settings.types ?? 0)) { + if ( + !payload.notifySystem || + !hasNotificationType(type, settings.types ?? 0) + ) { return true; } diff --git a/server/lib/notifications/agents/email.ts b/server/lib/notifications/agents/email.ts index cbed472fa..59c5b4aa7 100644 --- a/server/lib/notifications/agents/email.ts +++ b/server/lib/notifications/agents/email.ts @@ -1,19 +1,17 @@ -import { EmailOptions } from 'email-templates'; -import path from 'path'; -import { getRepository } from 'typeorm'; -import { Notification, shouldSendAdminNotification } from '..'; -import { IssueType, IssueTypeName } from '../../../constants/issue'; -import { MediaType } from '../../../constants/media'; -import { User } from '../../../entity/User'; -import logger from '../../../logger'; -import PreparedEmail from '../../email'; -import { - getSettings, - NotificationAgentEmail, - NotificationAgentKey, -} from '../../settings'; -import { BaseAgent, NotificationAgent, NotificationPayload } from './agent'; +import { IssueType, IssueTypeName } from '@server/constants/issue'; +import { MediaType } from '@server/constants/media'; +import { getRepository } from '@server/datasource'; +import { User } from '@server/entity/User'; +import PreparedEmail from '@server/lib/email'; +import type { NotificationAgentEmail } from '@server/lib/settings'; +import { getSettings, NotificationAgentKey } from '@server/lib/settings'; +import logger from '@server/logger'; +import type { EmailOptions } from 'email-templates'; import * as EmailValidator from 'email-validator'; +import path from 'path'; +import { Notification, shouldSendAdminNotification } from '..'; +import type { NotificationAgent, NotificationPayload } from './agent'; +import { BaseAgent } from './agent'; class EmailAgent extends BaseAgent @@ -84,6 +82,11 @@ class EmailAgent is4k ? 'in 4K ' : '' }is pending approval:`; break; + case Notification.MEDIA_AUTO_REQUESTED: + body = `A new request for the following ${mediaType} ${ + is4k ? 'in 4K ' : '' + }was automatically submitted:`; + break; case Notification.MEDIA_APPROVED: body = `Your request for the following ${mediaType} ${ is4k ? 'in 4K ' : '' diff --git a/server/lib/notifications/agents/gotify.ts b/server/lib/notifications/agents/gotify.ts index ecd54ce75..d07caac41 100644 --- a/server/lib/notifications/agents/gotify.ts +++ b/server/lib/notifications/agents/gotify.ts @@ -1,15 +1,17 @@ +import { IssueStatus, IssueTypeName } from '@server/constants/issue'; +import type { NotificationAgentGotify } from '@server/lib/settings'; +import { getSettings } from '@server/lib/settings'; +import logger from '@server/logger'; import axios from 'axios'; import { hasNotificationType, Notification } from '..'; -import { IssueStatus, IssueTypeName } from '../../../constants/issue'; -import logger from '../../../logger'; -import { getSettings, NotificationAgentGotify } from '../../settings'; -import { BaseAgent, NotificationAgent, NotificationPayload } from './agent'; +import type { NotificationAgent, NotificationPayload } from './agent'; +import { BaseAgent } from './agent'; interface GotifyPayload { title: string; message: string; priority: number; - extras: any; + extras: Record; } class GotifyAgent @@ -115,7 +117,10 @@ class GotifyAgent ): Promise { const settings = this.getSettings(); - if (!hasNotificationType(type, settings.types ?? 0)) { + if ( + !payload.notifySystem || + !hasNotificationType(type, settings.types ?? 0) + ) { return true; } diff --git a/server/lib/notifications/agents/lunasea.ts b/server/lib/notifications/agents/lunasea.ts index 0269e2600..885b038ca 100644 --- a/server/lib/notifications/agents/lunasea.ts +++ b/server/lib/notifications/agents/lunasea.ts @@ -1,10 +1,12 @@ +import { IssueStatus, IssueType } from '@server/constants/issue'; +import { MediaStatus } from '@server/constants/media'; +import type { NotificationAgentLunaSea } from '@server/lib/settings'; +import { getSettings } from '@server/lib/settings'; +import logger from '@server/logger'; import axios from 'axios'; import { hasNotificationType, Notification } from '..'; -import { IssueStatus, IssueType } from '../../../constants/issue'; -import { MediaStatus } from '../../../constants/media'; -import logger from '../../../logger'; -import { getSettings, NotificationAgentLunaSea } from '../../settings'; -import { BaseAgent, NotificationAgent, NotificationPayload } from './agent'; +import type { NotificationAgent, NotificationPayload } from './agent'; +import { BaseAgent } from './agent'; class LunaSeaAgent extends BaseAgent @@ -85,7 +87,10 @@ class LunaSeaAgent ): Promise { const settings = this.getSettings(); - if (!hasNotificationType(type, settings.types ?? 0)) { + if ( + !payload.notifySystem || + !hasNotificationType(type, settings.types ?? 0) + ) { return true; } diff --git a/server/lib/notifications/agents/pushbullet.ts b/server/lib/notifications/agents/pushbullet.ts index b7bc1919f..eed4fda91 100644 --- a/server/lib/notifications/agents/pushbullet.ts +++ b/server/lib/notifications/agents/pushbullet.ts @@ -1,19 +1,18 @@ +import { IssueStatus, IssueTypeName } from '@server/constants/issue'; +import { MediaStatus } from '@server/constants/media'; +import { getRepository } from '@server/datasource'; +import { User } from '@server/entity/User'; +import type { NotificationAgentPushbullet } from '@server/lib/settings'; +import { getSettings, NotificationAgentKey } from '@server/lib/settings'; +import logger from '@server/logger'; import axios from 'axios'; -import { getRepository } from 'typeorm'; import { hasNotificationType, Notification, shouldSendAdminNotification, } from '..'; -import { IssueStatus, IssueTypeName } from '../../../constants/issue'; -import { User } from '../../../entity/User'; -import logger from '../../../logger'; -import { - getSettings, - NotificationAgentKey, - NotificationAgentPushbullet, -} from '../../settings'; -import { BaseAgent, NotificationAgent, NotificationPayload } from './agent'; +import type { NotificationAgent, NotificationPayload } from './agent'; +import { BaseAgent } from './agent'; interface PushbulletPayload { type: string; @@ -54,6 +53,12 @@ class PushbulletAgent let status = ''; switch (type) { + case Notification.MEDIA_AUTO_REQUESTED: + status = + payload.media?.status === MediaStatus.PENDING + ? 'Pending Approval' + : 'Processing'; + break; case Notification.MEDIA_PENDING: status = 'Pending Approval'; break; @@ -106,6 +111,7 @@ class PushbulletAgent // Send system notification if ( + payload.notifySystem && hasNotificationType(type, settings.types ?? 0) && settings.enabled && settings.options.accessToken diff --git a/server/lib/notifications/agents/pushover.ts b/server/lib/notifications/agents/pushover.ts index f8364c3f2..d8deb1bdc 100644 --- a/server/lib/notifications/agents/pushover.ts +++ b/server/lib/notifications/agents/pushover.ts @@ -1,19 +1,18 @@ +import { IssueStatus, IssueTypeName } from '@server/constants/issue'; +import { MediaStatus } from '@server/constants/media'; +import { getRepository } from '@server/datasource'; +import { User } from '@server/entity/User'; +import type { NotificationAgentPushover } from '@server/lib/settings'; +import { getSettings, NotificationAgentKey } from '@server/lib/settings'; +import logger from '@server/logger'; import axios from 'axios'; -import { getRepository } from 'typeorm'; import { hasNotificationType, Notification, shouldSendAdminNotification, } from '..'; -import { IssueStatus, IssueTypeName } from '../../../constants/issue'; -import { User } from '../../../entity/User'; -import logger from '../../../logger'; -import { - getSettings, - NotificationAgentKey, - NotificationAgentPushover, -} from '../../settings'; -import { BaseAgent, NotificationAgent, NotificationPayload } from './agent'; +import type { NotificationAgent, NotificationPayload } from './agent'; +import { BaseAgent } from './agent'; interface PushoverPayload { token: string; @@ -63,6 +62,12 @@ class PushoverAgent let status = ''; switch (type) { + case Notification.MEDIA_AUTO_REQUESTED: + status = + payload.media?.status === MediaStatus.PENDING + ? 'Pending Approval' + : 'Processing'; + break; case Notification.MEDIA_PENDING: status = 'Pending Approval'; break; @@ -137,6 +142,7 @@ class PushoverAgent // Send system notification if ( + payload.notifySystem && hasNotificationType(type, settings.types ?? 0) && settings.enabled && settings.options.accessToken && diff --git a/server/lib/notifications/agents/slack.ts b/server/lib/notifications/agents/slack.ts index ca10c269c..9447cda35 100644 --- a/server/lib/notifications/agents/slack.ts +++ b/server/lib/notifications/agents/slack.ts @@ -1,9 +1,11 @@ +import { IssueStatus, IssueTypeName } from '@server/constants/issue'; +import type { NotificationAgentSlack } from '@server/lib/settings'; +import { getSettings } from '@server/lib/settings'; +import logger from '@server/logger'; import axios from 'axios'; import { hasNotificationType, Notification } from '..'; -import { IssueStatus, IssueTypeName } from '../../../constants/issue'; -import logger from '../../../logger'; -import { getSettings, NotificationAgentSlack } from '../../settings'; -import { BaseAgent, NotificationAgent, NotificationPayload } from './agent'; +import type { NotificationAgent, NotificationPayload } from './agent'; +import { BaseAgent } from './agent'; interface EmbedField { type: 'plain_text' | 'mrkdwn'; @@ -223,7 +225,10 @@ class SlackAgent ): Promise { const settings = this.getSettings(); - if (!hasNotificationType(type, settings.types ?? 0)) { + if ( + !payload.notifySystem || + !hasNotificationType(type, settings.types ?? 0) + ) { return true; } diff --git a/server/lib/notifications/agents/telegram.ts b/server/lib/notifications/agents/telegram.ts index 3450a3c2a..7d7062122 100644 --- a/server/lib/notifications/agents/telegram.ts +++ b/server/lib/notifications/agents/telegram.ts @@ -1,19 +1,18 @@ +import { IssueStatus, IssueTypeName } from '@server/constants/issue'; +import { MediaStatus } from '@server/constants/media'; +import { getRepository } from '@server/datasource'; +import { User } from '@server/entity/User'; +import type { NotificationAgentTelegram } from '@server/lib/settings'; +import { getSettings, NotificationAgentKey } from '@server/lib/settings'; +import logger from '@server/logger'; import axios from 'axios'; -import { getRepository } from 'typeorm'; import { hasNotificationType, Notification, shouldSendAdminNotification, } from '..'; -import { IssueStatus, IssueTypeName } from '../../../constants/issue'; -import { User } from '../../../entity/User'; -import logger from '../../../logger'; -import { - getSettings, - NotificationAgentKey, - NotificationAgentTelegram, -} from '../../settings'; -import { BaseAgent, NotificationAgent, NotificationPayload } from './agent'; +import type { NotificationAgent, NotificationPayload } from './agent'; +import { BaseAgent } from './agent'; interface TelegramMessagePayload { text: string; @@ -81,6 +80,12 @@ class TelegramAgent let status = ''; switch (type) { + case Notification.MEDIA_AUTO_REQUESTED: + status = + payload.media?.status === MediaStatus.PENDING + ? 'Pending Approval' + : 'Processing'; + break; case Notification.MEDIA_PENDING: status = 'Pending Approval'; break; @@ -159,6 +164,7 @@ class TelegramAgent // Send system notification if ( + payload.notifySystem && hasNotificationType(type, settings.types ?? 0) && settings.options.chatId ) { diff --git a/server/lib/notifications/agents/webhook.ts b/server/lib/notifications/agents/webhook.ts index ba2bf5e59..461cd37fd 100644 --- a/server/lib/notifications/agents/webhook.ts +++ b/server/lib/notifications/agents/webhook.ts @@ -1,11 +1,13 @@ +import { IssueStatus, IssueType } from '@server/constants/issue'; +import { MediaStatus } from '@server/constants/media'; +import type { NotificationAgentWebhook } from '@server/lib/settings'; +import { getSettings } from '@server/lib/settings'; +import logger from '@server/logger'; import axios from 'axios'; import { get } from 'lodash'; import { hasNotificationType, Notification } from '..'; -import { IssueStatus, IssueType } from '../../../constants/issue'; -import { MediaStatus } from '../../../constants/media'; -import logger from '../../../logger'; -import { getSettings, NotificationAgentWebhook } from '../../settings'; -import { BaseAgent, NotificationAgent, NotificationPayload } from './agent'; +import type { NotificationAgent, NotificationPayload } from './agent'; +import { BaseAgent } from './agent'; type KeyMapFunction = ( payload: NotificationPayload, @@ -162,7 +164,10 @@ class WebhookAgent ): Promise { const settings = this.getSettings(); - if (!hasNotificationType(type, settings.types ?? 0)) { + if ( + !payload.notifySystem || + !hasNotificationType(type, settings.types ?? 0) + ) { return true; } diff --git a/server/lib/notifications/agents/webpush.ts b/server/lib/notifications/agents/webpush.ts index c87d9496c..275a77e8e 100644 --- a/server/lib/notifications/agents/webpush.ts +++ b/server/lib/notifications/agents/webpush.ts @@ -1,17 +1,15 @@ -import { getRepository } from 'typeorm'; +import { IssueType, IssueTypeName } from '@server/constants/issue'; +import { MediaType } from '@server/constants/media'; +import { getRepository } from '@server/datasource'; +import { User } from '@server/entity/User'; +import { UserPushSubscription } from '@server/entity/UserPushSubscription'; +import type { NotificationAgentConfig } from '@server/lib/settings'; +import { getSettings, NotificationAgentKey } from '@server/lib/settings'; +import logger from '@server/logger'; import webpush from 'web-push'; import { Notification, shouldSendAdminNotification } from '..'; -import { IssueType, IssueTypeName } from '../../../constants/issue'; -import { MediaType } from '../../../constants/media'; -import { User } from '../../../entity/User'; -import { UserPushSubscription } from '../../../entity/UserPushSubscription'; -import logger from '../../../logger'; -import { - getSettings, - NotificationAgentConfig, - NotificationAgentKey, -} from '../../settings'; -import { BaseAgent, NotificationAgent, NotificationPayload } from './agent'; +import type { NotificationAgent, NotificationPayload } from './agent'; +import { BaseAgent } from './agent'; interface PushNotificationPayload { notificationType: string; @@ -59,6 +57,11 @@ class WebPushAgent case Notification.TEST_NOTIFICATION: message = payload.message; break; + case Notification.MEDIA_AUTO_REQUESTED: + message = `Automatically submitted a new ${ + is4k ? '4K ' : '' + }${mediaType} request.`; + break; case Notification.MEDIA_APPROVED: message = `Your ${ is4k ? '4K ' : '' @@ -160,7 +163,7 @@ class WebPushAgent true) ) { const notifySubs = await userPushSubRepository.find({ - where: { user: payload.notifyUser.id }, + where: { user: { id: payload.notifyUser.id } }, }); pushSubs.push(...notifySubs); diff --git a/server/lib/notifications/index.ts b/server/lib/notifications/index.ts index b8111d02f..71aea8fe9 100644 --- a/server/lib/notifications/index.ts +++ b/server/lib/notifications/index.ts @@ -1,6 +1,6 @@ -import { User } from '../../entity/User'; -import logger from '../../logger'; -import { Permission } from '../permissions'; +import type { User } from '@server/entity/User'; +import { Permission } from '@server/lib/permissions'; +import logger from '@server/logger'; import type { NotificationAgent, NotificationPayload } from './agents/agent'; export enum Notification { @@ -16,6 +16,7 @@ export enum Notification { ISSUE_COMMENT = 512, ISSUE_RESOLVED = 1024, ISSUE_REOPENED = 2048, + MEDIA_AUTO_REQUESTED = 4096, } export const hasNotificationType = ( diff --git a/server/lib/permissions.ts b/server/lib/permissions.ts index 95160d380..4a4a90d84 100644 --- a/server/lib/permissions.ts +++ b/server/lib/permissions.ts @@ -22,6 +22,11 @@ export enum Permission { MANAGE_ISSUES = 1048576, VIEW_ISSUES = 2097152, CREATE_ISSUES = 4194304, + AUTO_REQUEST = 8388608, + AUTO_REQUEST_MOVIE = 16777216, + AUTO_REQUEST_TV = 33554432, + RECENT_VIEW = 67108864, + WATCHLIST_VIEW = 134217728, } export interface PermissionCheckOptions { diff --git a/server/lib/scanners/baseScanner.ts b/server/lib/scanners/baseScanner.ts index f76ea92b0..f0f3db7e6 100644 --- a/server/lib/scanners/baseScanner.ts +++ b/server/lib/scanners/baseScanner.ts @@ -1,12 +1,12 @@ +import TheMovieDb from '@server/api/themoviedb'; +import { MediaStatus, MediaType } from '@server/constants/media'; +import { getRepository } from '@server/datasource'; +import Media from '@server/entity/Media'; +import Season from '@server/entity/Season'; +import { getSettings } from '@server/lib/settings'; +import logger from '@server/logger'; +import AsyncLock from '@server/utils/asyncLock'; import { randomUUID } from 'crypto'; -import { getRepository } from 'typeorm'; -import TheMovieDb from '../../api/themoviedb'; -import { MediaStatus, MediaType } from '../../constants/media'; -import Media from '../../entity/Media'; -import Season from '../../entity/Season'; -import logger from '../../logger'; -import AsyncLock from '../../utils/asyncLock'; -import { getSettings } from '../settings'; // Default scan rates (can be overidden) const BUNDLE_SIZE = 20; @@ -210,7 +210,7 @@ class BaseScanner { } /** - * processShow takes a TMDb ID and an array of ProcessableSeasons, which + * processShow takes a TMDB ID and an array of ProcessableSeasons, which * should include the total episodes a sesaon has + the total available * episodes that each season currently has. Unlike processMovie, this method * does not take an `is4k` option. We handle both the 4k _and_ non 4k status diff --git a/server/lib/scanners/plex/index.ts b/server/lib/scanners/plex/index.ts index cd8dbd76a..73e4d9b26 100644 --- a/server/lib/scanners/plex/index.ts +++ b/server/lib/scanners/plex/index.ts @@ -1,17 +1,20 @@ -import { uniqWith } from 'lodash'; -import { getRepository } from 'typeorm'; -import animeList from '../../../api/animelist'; -import PlexAPI, { PlexLibraryItem, PlexMetadata } from '../../../api/plexapi'; -import { TmdbTvDetails } from '../../../api/themoviedb/interfaces'; -import { User } from '../../../entity/User'; -import cacheManager from '../../cache'; -import { getSettings, Library } from '../../settings'; -import BaseScanner, { +import animeList from '@server/api/animelist'; +import type { PlexLibraryItem, PlexMetadata } from '@server/api/plexapi'; +import PlexAPI from '@server/api/plexapi'; +import type { TmdbTvDetails } from '@server/api/themoviedb/interfaces'; +import { getRepository } from '@server/datasource'; +import { User } from '@server/entity/User'; +import cacheManager from '@server/lib/cache'; +import type { MediaIds, ProcessableSeason, RunnableScanner, StatusBase, -} from '../baseScanner'; +} from '@server/lib/scanners/baseScanner'; +import BaseScanner from '@server/lib/scanners/baseScanner'; +import type { Library } from '@server/lib/settings'; +import { getSettings } from '@server/lib/settings'; +import { uniqWith } from 'lodash'; const imdbRegex = new RegExp(/imdb:\/\/(tt[0-9]+)/); const tmdbRegex = new RegExp(/tmdb:\/\/([0-9]+)/); @@ -59,8 +62,8 @@ class PlexScanner try { const userRepository = getRepository(User); const admin = await userRepository.findOne({ - select: ['id', 'plexToken'], - order: { id: 'ASC' }, + select: { id: true, plexToken: true }, + where: { id: 1 }, }); if (!admin) { @@ -141,7 +144,9 @@ class PlexScanner 'info' ); } catch (e) { - this.log('Scan interrupted', 'error', { errorMessage: e.message }); + this.log('Scan interrupted', 'error', { + errorMessage: e.message, + }); } finally { this.endRun(sessionId); } @@ -369,7 +374,7 @@ class PlexScanner } }); - // If we got an IMDb ID, but no TMDb ID, lookup the TMDb ID with the IMDb ID + // If we got an IMDb ID, but no TMDB ID, lookup the TMDB ID with the IMDb ID if (mediaIds.imdbId && !mediaIds.tmdbId) { const tmdbMedia = await this.tmdb.getMediaByImdbId({ imdbId: mediaIds.imdbId, @@ -390,7 +395,7 @@ class PlexScanner }); mediaIds.tmdbId = tmdbMedia.id; } - // Check if the agent is TMDb + // Check if the agent is TMDB } else if (plexitem.guid.match(tmdbRegex)) { const tmdbMatch = plexitem.guid.match(tmdbRegex); if (tmdbMatch) { @@ -409,7 +414,7 @@ class PlexScanner mediaIds.tvdbId = Number(matchedtvdb[1]); mediaIds.tmdbId = show.id; } - // Check if the agent (for shows) is TMDb + // Check if the agent (for shows) is TMDB } else if (plexitem.guid.match(tmdbShowRegex)) { const matchedtmdb = plexitem.guid.match(tmdbShowRegex); if (matchedtmdb) { @@ -484,10 +489,10 @@ class PlexScanner } if (!mediaIds.tmdbId) { - throw new Error('Unable to find TMDb ID'); + throw new Error('Unable to find TMDB ID'); } - // We check above if we have the TMDb ID, so we can safely assert the type below + // We check above if we have the TMDB ID, so we can safely assert the type below return mediaIds as MediaIds; } diff --git a/server/lib/scanners/radarr/index.ts b/server/lib/scanners/radarr/index.ts index 5f47b9d97..bc299d7b1 100644 --- a/server/lib/scanners/radarr/index.ts +++ b/server/lib/scanners/radarr/index.ts @@ -1,7 +1,13 @@ +import type { RadarrMovie } from '@server/api/servarr/radarr'; +import RadarrAPI from '@server/api/servarr/radarr'; +import type { + RunnableScanner, + StatusBase, +} from '@server/lib/scanners/baseScanner'; +import BaseScanner from '@server/lib/scanners/baseScanner'; +import type { RadarrSettings } from '@server/lib/settings'; +import { getSettings } from '@server/lib/settings'; import { uniqWith } from 'lodash'; -import RadarrAPI, { RadarrMovie } from '../../../api/servarr/radarr'; -import { getSettings, RadarrSettings } from '../../settings'; -import BaseScanner, { RunnableScanner, StatusBase } from '../baseScanner'; type SyncStatus = StatusBase & { currentServer: RadarrSettings; diff --git a/server/lib/scanners/sonarr/index.ts b/server/lib/scanners/sonarr/index.ts index 044f74ec7..3256c9482 100644 --- a/server/lib/scanners/sonarr/index.ts +++ b/server/lib/scanners/sonarr/index.ts @@ -1,14 +1,17 @@ -import { uniqWith } from 'lodash'; -import { getRepository } from 'typeorm'; -import SonarrAPI, { SonarrSeries } from '../../../api/servarr/sonarr'; -import { TmdbTvDetails } from '../../../api/themoviedb/interfaces'; -import Media from '../../../entity/Media'; -import { getSettings, SonarrSettings } from '../../settings'; -import BaseScanner, { +import type { SonarrSeries } from '@server/api/servarr/sonarr'; +import SonarrAPI from '@server/api/servarr/sonarr'; +import type { TmdbTvDetails } from '@server/api/themoviedb/interfaces'; +import { getRepository } from '@server/datasource'; +import Media from '@server/entity/Media'; +import type { ProcessableSeason, RunnableScanner, StatusBase, -} from '../baseScanner'; +} from '@server/lib/scanners/baseScanner'; +import BaseScanner from '@server/lib/scanners/baseScanner'; +import type { SonarrSettings } from '@server/lib/settings'; +import { getSettings } from '@server/lib/settings'; +import { uniqWith } from 'lodash'; type SyncStatus = StatusBase & { currentServer: SonarrSettings; diff --git a/server/lib/search.ts b/server/lib/search.ts index c625f512d..be9ee3ae8 100644 --- a/server/lib/search.ts +++ b/server/lib/search.ts @@ -1,5 +1,5 @@ -import TheMovieDb from '../api/themoviedb'; -import { +import TheMovieDb from '@server/api/themoviedb'; +import type { TmdbMovieDetails, TmdbMovieResult, TmdbPersonDetails, @@ -9,13 +9,17 @@ import { TmdbSearchTvResponse, TmdbTvDetails, TmdbTvResult, -} from '../api/themoviedb/interfaces'; +} from '@server/api/themoviedb/interfaces'; import { mapMovieDetailsToResult, mapPersonDetailsToResult, mapTvDetailsToResult, -} from '../models/Search'; -import { isMovie, isMovieDetails, isTvDetails } from '../utils/typeHelpers'; +} from '@server/models/Search'; +import { + isMovie, + isMovieDetails, + isTvDetails, +} from '@server/utils/typeHelpers'; interface SearchProvider { pattern: RegExp; diff --git a/server/lib/settings.ts b/server/lib/settings.ts index 53fe864c1..29e2fcf13 100644 --- a/server/lib/settings.ts +++ b/server/lib/settings.ts @@ -1,9 +1,9 @@ +import { MediaServerType } from '@server/constants/server'; import { randomUUID } from 'crypto'; import fs from 'fs'; import { merge } from 'lodash'; import path from 'path'; import webpush from 'web-push'; -import { MediaServerType } from '../constants/server'; import { Permission } from './permissions'; export interface Library { @@ -257,6 +257,7 @@ interface JobSettings { export type JobId = | 'plex-recently-added-scan' | 'plex-full-scan' + | 'plex-watchlist-sync' | 'radarr-scan' | 'sonarr-scan' | 'download-sync' @@ -424,6 +425,9 @@ class Settings { 'plex-full-scan': { schedule: '0 0 3 * * *', }, + 'plex-watchlist-sync': { + schedule: '0 */10 * * * *', + }, 'radarr-scan': { schedule: '0 0 4 * * *', }, diff --git a/server/lib/watchlistsync.ts b/server/lib/watchlistsync.ts new file mode 100644 index 000000000..46147f3fc --- /dev/null +++ b/server/lib/watchlistsync.ts @@ -0,0 +1,163 @@ +import PlexTvAPI from '@server/api/plextv'; +import { MediaStatus, MediaType } from '@server/constants/media'; +import { getRepository } from '@server/datasource'; +import Media from '@server/entity/Media'; +import { + DuplicateMediaRequestError, + MediaRequest, + NoSeasonsAvailableError, + QuotaRestrictedError, + RequestPermissionError, +} from '@server/entity/MediaRequest'; +import { User } from '@server/entity/User'; +import logger from '@server/logger'; +import { Permission } from './permissions'; + +class WatchlistSync { + public async syncWatchlist() { + const userRepository = getRepository(User); + + // Get users who actually have plex tokens + const users = await userRepository + .createQueryBuilder('user') + .addSelect('user.plexToken') + .leftJoinAndSelect('user.settings', 'settings') + .where("user.plexToken != ''") + .getMany(); + + for (const user of users) { + await this.syncUserWatchlist(user); + } + } + + private async syncUserWatchlist(user: User) { + if (!user.plexToken) { + logger.warn('Skipping user watchlist sync for user without plex token', { + label: 'Plex Watchlist Sync', + user: user.displayName, + }); + return; + } + + if ( + !user.hasPermission( + [ + Permission.AUTO_REQUEST, + Permission.AUTO_REQUEST_MOVIE, + Permission.AUTO_APPROVE_TV, + ], + { type: 'or' } + ) + ) { + return; + } + + if ( + !user.settings?.watchlistSyncMovies && + !user.settings?.watchlistSyncTv + ) { + // Skip sync if user settings have it disabled + return; + } + + const plexTvApi = new PlexTvAPI(user.plexToken); + + const response = await plexTvApi.getWatchlist({ size: 200 }); + + const mediaItems = await Media.getRelatedMedia( + response.items.map((i) => i.tmdbId) + ); + + const unavailableItems = response.items.filter( + // If we can find watchlist items in our database that are also available, we should exclude them + (i) => + !mediaItems.find( + (m) => + m.tmdbId === i.tmdbId && + ((m.status !== MediaStatus.UNKNOWN && m.mediaType === 'movie') || + (m.mediaType === 'tv' && m.status === MediaStatus.AVAILABLE)) + ) + ); + + await Promise.all( + unavailableItems.map(async (mediaItem) => { + try { + logger.info("Creating media request from user's Plex Watchlist", { + label: 'Watchlist Sync', + userId: user.id, + mediaTitle: mediaItem.title, + }); + + if (mediaItem.type === 'show' && !mediaItem.tvdbId) { + throw new Error('Missing TVDB ID from Plex Metadata'); + } + + // Check if they have auto-request permissons and watchlist sync + // enabled for the media type + if ( + ((!user.hasPermission( + [Permission.AUTO_REQUEST, Permission.AUTO_REQUEST_MOVIE], + { type: 'or' } + ) || + !user.settings?.watchlistSyncMovies) && + mediaItem.type === 'movie') || + ((!user.hasPermission( + [Permission.AUTO_REQUEST, Permission.AUTO_REQUEST_TV], + { type: 'or' } + ) || + !user.settings?.watchlistSyncTv) && + mediaItem.type === 'show') + ) { + return; + } + + await MediaRequest.request( + { + mediaId: mediaItem.tmdbId, + mediaType: + mediaItem.type === 'show' ? MediaType.TV : MediaType.MOVIE, + seasons: mediaItem.type === 'show' ? 'all' : undefined, + tvdbId: mediaItem.tvdbId, + is4k: false, + }, + user, + { isAutoRequest: true } + ); + } catch (e) { + if (!(e instanceof Error)) { + return; + } + + switch (e.constructor) { + // During watchlist sync, these errors aren't necessarily + // a problem with Overseerr. Since we are auto syncing these constantly, it's + // possible they are unexpectedly at their quota limit, for example. So we'll + // instead log these as debug messages. + case RequestPermissionError: + case DuplicateMediaRequestError: + case QuotaRestrictedError: + case NoSeasonsAvailableError: + logger.debug('Failed to create media request from watchlist', { + label: 'Watchlist Sync', + userId: user.id, + mediaTitle: mediaItem.title, + errorMessage: e.message, + }); + break; + default: + logger.error('Failed to create media request from watchlist', { + label: 'Watchlist Sync', + userId: user.id, + mediaTitle: mediaItem.title, + errorMessage: e.message, + }); + } + } + }) + ); + } +} + +const watchlistSync = new WatchlistSync(); + +export default watchlistSync; diff --git a/server/logger.ts b/server/logger.ts index 4f736e4ab..d5809a0ed 100644 --- a/server/logger.ts +++ b/server/logger.ts @@ -26,7 +26,7 @@ const hformat = winston.format.printf( ); const logger = winston.createLogger({ - level: process.env.LOG_LEVEL || 'debug', + level: process.env.LOG_LEVEL?.toLowerCase() || 'debug', format: winston.format.combine( winston.format.splat(), winston.format.timestamp(), diff --git a/server/middleware/auth.ts b/server/middleware/auth.ts index 68869222f..326d460d8 100644 --- a/server/middleware/auth.ts +++ b/server/middleware/auth.ts @@ -1,11 +1,14 @@ -import { getRepository } from 'typeorm'; -import { User } from '../entity/User'; -import { Permission, PermissionCheckOptions } from '../lib/permissions'; -import { getSettings } from '../lib/settings'; +import { getRepository } from '@server/datasource'; +import { User } from '@server/entity/User'; +import type { + Permission, + PermissionCheckOptions, +} from '@server/lib/permissions'; +import { getSettings } from '@server/lib/settings'; export const checkUser: Middleware = async (req, _res, next) => { const settings = getSettings(); - let user: User | undefined; + let user: User | undefined | null; if (req.header('X-API-Key') === settings.main.apiKey) { const userRepository = getRepository(User); diff --git a/server/migration/1603944374840-InitialMigration.ts b/server/migration/1603944374840-InitialMigration.ts index 73640565c..db71471ae 100644 --- a/server/migration/1603944374840-InitialMigration.ts +++ b/server/migration/1603944374840-InitialMigration.ts @@ -1,4 +1,4 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; +import type { MigrationInterface, QueryRunner } from 'typeorm'; export class InitialMigration1603944374840 implements MigrationInterface { name = 'InitialMigration1603944374840'; diff --git a/server/migration/1605085519544-SeasonStatus.ts b/server/migration/1605085519544-SeasonStatus.ts index bcff6f609..059c6bf51 100644 --- a/server/migration/1605085519544-SeasonStatus.ts +++ b/server/migration/1605085519544-SeasonStatus.ts @@ -1,4 +1,4 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; +import type { MigrationInterface, QueryRunner } from 'typeorm'; export class SeasonStatus1605085519544 implements MigrationInterface { name = 'SeasonStatus1605085519544'; diff --git a/server/migration/1606730060700-CascadeMigration.ts b/server/migration/1606730060700-CascadeMigration.ts index 341bc00b3..3b1ae0702 100644 --- a/server/migration/1606730060700-CascadeMigration.ts +++ b/server/migration/1606730060700-CascadeMigration.ts @@ -1,4 +1,4 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; +import type { MigrationInterface, QueryRunner } from 'typeorm'; export class CascadeMigration1606730060700 implements MigrationInterface { name = 'CascadeMigration1606730060700'; diff --git a/server/migration/1607928251245-DropImdbIdConstraint.ts b/server/migration/1607928251245-DropImdbIdConstraint.ts index 97baa861a..f602ea7fa 100644 --- a/server/migration/1607928251245-DropImdbIdConstraint.ts +++ b/server/migration/1607928251245-DropImdbIdConstraint.ts @@ -1,4 +1,5 @@ -import { MigrationInterface, QueryRunner, TableUnique } from 'typeorm'; +import type { MigrationInterface, QueryRunner } from 'typeorm'; +import { TableUnique } from 'typeorm'; export class DropImdbIdConstraint1607928251245 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { diff --git a/server/migration/1608217312474-AddUserRequestDeleteCascades.ts b/server/migration/1608217312474-AddUserRequestDeleteCascades.ts index e2aa88653..622a2c90e 100644 --- a/server/migration/1608217312474-AddUserRequestDeleteCascades.ts +++ b/server/migration/1608217312474-AddUserRequestDeleteCascades.ts @@ -1,4 +1,4 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; +import type { MigrationInterface, QueryRunner } from 'typeorm'; export class AddUserRequestDeleteCascades1608219049304 implements MigrationInterface diff --git a/server/migration/1608477467935-AddLastSeasonChangeMedia.ts b/server/migration/1608477467935-AddLastSeasonChangeMedia.ts index fba7af7f3..e5ab02506 100644 --- a/server/migration/1608477467935-AddLastSeasonChangeMedia.ts +++ b/server/migration/1608477467935-AddLastSeasonChangeMedia.ts @@ -1,4 +1,4 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; +import type { MigrationInterface, QueryRunner } from 'typeorm'; export class AddLastSeasonChangeMedia1608477467935 implements MigrationInterface diff --git a/server/migration/1608477467936-ForceDropImdbUniqueConstraint.ts b/server/migration/1608477467936-ForceDropImdbUniqueConstraint.ts index 6a109e4d1..d54c450e4 100644 --- a/server/migration/1608477467936-ForceDropImdbUniqueConstraint.ts +++ b/server/migration/1608477467936-ForceDropImdbUniqueConstraint.ts @@ -1,4 +1,4 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; +import type { MigrationInterface, QueryRunner } from 'typeorm'; export class ForceDropImdbUniqueConstraint1608477467935 implements MigrationInterface diff --git a/server/migration/1609236552057-RemoveTmdbIdUniqueConstraint.ts b/server/migration/1609236552057-RemoveTmdbIdUniqueConstraint.ts index 2cd5415e7..500568927 100644 --- a/server/migration/1609236552057-RemoveTmdbIdUniqueConstraint.ts +++ b/server/migration/1609236552057-RemoveTmdbIdUniqueConstraint.ts @@ -1,4 +1,4 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; +import type { MigrationInterface, QueryRunner } from 'typeorm'; export class RemoveTmdbIdUniqueConstraint1609236552057 implements MigrationInterface diff --git a/server/migration/1610070934506-LocalUsers.ts b/server/migration/1610070934506-LocalUsers.ts index 0ece00f4d..88b0ae607 100644 --- a/server/migration/1610070934506-LocalUsers.ts +++ b/server/migration/1610070934506-LocalUsers.ts @@ -1,4 +1,4 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; +import type { MigrationInterface, QueryRunner } from 'typeorm'; export class LocalUsers1610070934506 implements MigrationInterface { name = 'LocalUsers1610070934506'; diff --git a/server/migration/1610370640747-Add4kStatusFields.ts b/server/migration/1610370640747-Add4kStatusFields.ts index a313bf135..5502b9c0f 100644 --- a/server/migration/1610370640747-Add4kStatusFields.ts +++ b/server/migration/1610370640747-Add4kStatusFields.ts @@ -1,4 +1,4 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; +import type { MigrationInterface, QueryRunner } from 'typeorm'; export class Add4kStatusFields1610370640747 implements MigrationInterface { name = 'Add4kStatusFields1610370640747'; diff --git a/server/migration/1610522845513-AddMediaAddedFieldToMedia.ts b/server/migration/1610522845513-AddMediaAddedFieldToMedia.ts index 25e42a74e..d6574d396 100644 --- a/server/migration/1610522845513-AddMediaAddedFieldToMedia.ts +++ b/server/migration/1610522845513-AddMediaAddedFieldToMedia.ts @@ -1,4 +1,4 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; +import type { MigrationInterface, QueryRunner } from 'typeorm'; export class AddMediaAddedFieldToMedia1610522845513 implements MigrationInterface diff --git a/server/migration/1611508672722-AddDisplayNameToUser.ts b/server/migration/1611508672722-AddDisplayNameToUser.ts index cacea0597..6a36f29a9 100644 --- a/server/migration/1611508672722-AddDisplayNameToUser.ts +++ b/server/migration/1611508672722-AddDisplayNameToUser.ts @@ -1,4 +1,4 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; +import type { MigrationInterface, QueryRunner } from 'typeorm'; export class AddDisplayNameToUser1611508672722 implements MigrationInterface { name = 'AddDisplayNameToUser1611508672722'; diff --git a/server/migration/1611757511674-SonarrRadarrSyncServiceFields.ts b/server/migration/1611757511674-SonarrRadarrSyncServiceFields.ts index 355384a05..5a5b65533 100644 --- a/server/migration/1611757511674-SonarrRadarrSyncServiceFields.ts +++ b/server/migration/1611757511674-SonarrRadarrSyncServiceFields.ts @@ -1,4 +1,4 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; +import type { MigrationInterface, QueryRunner } from 'typeorm'; export class SonarrRadarrSyncServiceFields1611757511674 implements MigrationInterface diff --git a/server/migration/1611801511397-AddRatingKeysToMedia.ts b/server/migration/1611801511397-AddRatingKeysToMedia.ts index f9865c8f5..92ab4d4b4 100644 --- a/server/migration/1611801511397-AddRatingKeysToMedia.ts +++ b/server/migration/1611801511397-AddRatingKeysToMedia.ts @@ -1,4 +1,4 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; +import type { MigrationInterface, QueryRunner } from 'typeorm'; export class AddRatingKeysToMedia1611801511397 implements MigrationInterface { name = 'AddRatingKeysToMedia1611801511397'; diff --git a/server/migration/1612482778137-AddResetPasswordGuidAndExpiryDate.ts b/server/migration/1612482778137-AddResetPasswordGuidAndExpiryDate.ts index 7d191d106..55a20a390 100644 --- a/server/migration/1612482778137-AddResetPasswordGuidAndExpiryDate.ts +++ b/server/migration/1612482778137-AddResetPasswordGuidAndExpiryDate.ts @@ -1,4 +1,4 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; +import type { MigrationInterface, QueryRunner } from 'typeorm'; export class AddResetPasswordGuidAndExpiryDate1612482778137 implements MigrationInterface diff --git a/server/migration/1612571545781-AddLanguageProfileId.ts b/server/migration/1612571545781-AddLanguageProfileId.ts index fa89d81b7..7694f4e4f 100644 --- a/server/migration/1612571545781-AddLanguageProfileId.ts +++ b/server/migration/1612571545781-AddLanguageProfileId.ts @@ -1,4 +1,4 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; +import type { MigrationInterface, QueryRunner } from 'typeorm'; export class AddLanguageProfileId1612571545781 implements MigrationInterface { name = 'AddLanguageProfileId1612571545781'; diff --git a/server/migration/1613379909641-AddJellyfinUserParams.ts b/server/migration/1613379909641-AddJellyfinUserParams.ts index 46ef3319d..b56c873ac 100644 --- a/server/migration/1613379909641-AddJellyfinUserParams.ts +++ b/server/migration/1613379909641-AddJellyfinUserParams.ts @@ -1,4 +1,4 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; +import type { MigrationInterface, QueryRunner } from 'typeorm'; export class AddJellyfinUserParams1613379909641 implements MigrationInterface { name = 'AddJellyfinUserParams1613379909641'; diff --git a/server/migration/1613412948344-ServerTypeEnum.ts b/server/migration/1613412948344-ServerTypeEnum.ts index b8f950538..0fb18f239 100644 --- a/server/migration/1613412948344-ServerTypeEnum.ts +++ b/server/migration/1613412948344-ServerTypeEnum.ts @@ -1,4 +1,4 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; +import type { MigrationInterface, QueryRunner } from 'typeorm'; export class ServerTypeEnum1613412948344 implements MigrationInterface { name = 'ServerTypeEnum1613412948344'; diff --git a/server/migration/1613615266968-CreateUserSettings.ts b/server/migration/1613615266968-CreateUserSettings.ts index 4d4a973e9..fbe85339c 100644 --- a/server/migration/1613615266968-CreateUserSettings.ts +++ b/server/migration/1613615266968-CreateUserSettings.ts @@ -1,4 +1,4 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; +import type { MigrationInterface, QueryRunner } from 'typeorm'; export class CreateUserSettings1613615266968 implements MigrationInterface { name = 'CreateUserSettings1613615266968'; diff --git a/server/migration/1613670041760-AddJellyfinDeviceId.ts b/server/migration/1613670041760-AddJellyfinDeviceId.ts index 104b4146b..c800f1009 100644 --- a/server/migration/1613670041760-AddJellyfinDeviceId.ts +++ b/server/migration/1613670041760-AddJellyfinDeviceId.ts @@ -1,4 +1,4 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; +import type { MigrationInterface, QueryRunner } from 'typeorm'; export class AddJellyfinDeviceId1613670041760 implements MigrationInterface { name = 'AddJellyfinDeviceId1613670041760'; diff --git a/server/migration/1613955393450-UpdateUserSettingsRegions.ts b/server/migration/1613955393450-UpdateUserSettingsRegions.ts index d33df4eef..69060a0cb 100644 --- a/server/migration/1613955393450-UpdateUserSettingsRegions.ts +++ b/server/migration/1613955393450-UpdateUserSettingsRegions.ts @@ -1,4 +1,4 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; +import type { MigrationInterface, QueryRunner } from 'typeorm'; export class UpdateUserSettingsRegions1613955393450 implements MigrationInterface diff --git a/server/migration/1614334195680-AddTelegramSettingsToUserSettings.ts b/server/migration/1614334195680-AddTelegramSettingsToUserSettings.ts index 5e480d481..6e2598ab4 100644 --- a/server/migration/1614334195680-AddTelegramSettingsToUserSettings.ts +++ b/server/migration/1614334195680-AddTelegramSettingsToUserSettings.ts @@ -1,4 +1,4 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; +import type { MigrationInterface, QueryRunner } from 'typeorm'; export class AddTelegramSettingsToUserSettings1614334195680 implements MigrationInterface diff --git a/server/migration/1615333940450-AddPGPToUserSettings.ts b/server/migration/1615333940450-AddPGPToUserSettings.ts index b88e0dcaa..6940d4adc 100644 --- a/server/migration/1615333940450-AddPGPToUserSettings.ts +++ b/server/migration/1615333940450-AddPGPToUserSettings.ts @@ -1,4 +1,4 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; +import type { MigrationInterface, QueryRunner } from 'typeorm'; export class AddPGPToUserSettings1615333940450 implements MigrationInterface { name = 'AddPGPToUserSettings1615333940450'; diff --git a/server/migration/1616576677254-AddUserQuotaFields.ts b/server/migration/1616576677254-AddUserQuotaFields.ts index 632926900..62b39d65a 100644 --- a/server/migration/1616576677254-AddUserQuotaFields.ts +++ b/server/migration/1616576677254-AddUserQuotaFields.ts @@ -1,4 +1,4 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; +import type { MigrationInterface, QueryRunner } from 'typeorm'; export class AddUserQuotaFields1616576677254 implements MigrationInterface { name = 'AddUserQuotaFields1616576677254'; diff --git a/server/migration/1617624225464-CreateTagsFieldonMediaRequest.ts b/server/migration/1617624225464-CreateTagsFieldonMediaRequest.ts index d498a8b17..9e6761825 100644 --- a/server/migration/1617624225464-CreateTagsFieldonMediaRequest.ts +++ b/server/migration/1617624225464-CreateTagsFieldonMediaRequest.ts @@ -1,4 +1,4 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; +import type { MigrationInterface, QueryRunner } from 'typeorm'; export class CreateTagsFieldonMediaRequest1617624225464 implements MigrationInterface diff --git a/server/migration/1617730837489-AddUserSettingsNotificationAgentsField.ts b/server/migration/1617730837489-AddUserSettingsNotificationAgentsField.ts index 79cd061b8..9dd9288e6 100644 --- a/server/migration/1617730837489-AddUserSettingsNotificationAgentsField.ts +++ b/server/migration/1617730837489-AddUserSettingsNotificationAgentsField.ts @@ -1,4 +1,4 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; +import type { MigrationInterface, QueryRunner } from 'typeorm'; export class AddUserSettingsNotificationAgentsField1617730837489 implements MigrationInterface diff --git a/server/migration/1618912653565-CreateUserPushSubscriptions.ts b/server/migration/1618912653565-CreateUserPushSubscriptions.ts index 539221d17..970705990 100644 --- a/server/migration/1618912653565-CreateUserPushSubscriptions.ts +++ b/server/migration/1618912653565-CreateUserPushSubscriptions.ts @@ -1,4 +1,4 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; +import type { MigrationInterface, QueryRunner } from 'typeorm'; export class CreateUserPushSubscriptions1618912653565 implements MigrationInterface diff --git a/server/migration/1619239659754-AddUserSettingsLocale.ts b/server/migration/1619239659754-AddUserSettingsLocale.ts index 9842bca71..ba182b03a 100644 --- a/server/migration/1619239659754-AddUserSettingsLocale.ts +++ b/server/migration/1619239659754-AddUserSettingsLocale.ts @@ -1,4 +1,4 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; +import type { MigrationInterface, QueryRunner } from 'typeorm'; export class AddUserSettingsLocale1619239659754 implements MigrationInterface { name = 'AddUserSettingsLocale1619239659754'; diff --git a/server/migration/1619339817343-AddUserSettingsNotificationTypes.ts b/server/migration/1619339817343-AddUserSettingsNotificationTypes.ts index cccdae2fa..50de959b2 100644 --- a/server/migration/1619339817343-AddUserSettingsNotificationTypes.ts +++ b/server/migration/1619339817343-AddUserSettingsNotificationTypes.ts @@ -1,4 +1,4 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; +import type { MigrationInterface, QueryRunner } from 'typeorm'; export class AddUserSettingsNotificationTypes1619339817343 implements MigrationInterface diff --git a/server/migration/1634904083966-AddIssues.ts b/server/migration/1634904083966-AddIssues.ts index 0c6116f9d..ebcf8d89d 100644 --- a/server/migration/1634904083966-AddIssues.ts +++ b/server/migration/1634904083966-AddIssues.ts @@ -1,4 +1,4 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; +import type { MigrationInterface, QueryRunner } from 'typeorm'; export class AddIssues1634904083966 implements MigrationInterface { name = 'AddIssues1634904083966'; diff --git a/server/migration/1635079863457-AddPushbulletPushoverUserSettings.ts b/server/migration/1635079863457-AddPushbulletPushoverUserSettings.ts index 8934866fa..c29cef6d0 100644 --- a/server/migration/1635079863457-AddPushbulletPushoverUserSettings.ts +++ b/server/migration/1635079863457-AddPushbulletPushoverUserSettings.ts @@ -1,4 +1,4 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; +import type { MigrationInterface, QueryRunner } from 'typeorm'; export class AddPushbulletPushoverUserSettings1635079863457 implements MigrationInterface diff --git a/server/migration/1660632269368-AddWatchlistSyncUserSetting.ts b/server/migration/1660632269368-AddWatchlistSyncUserSetting.ts new file mode 100644 index 000000000..c0d0e947f --- /dev/null +++ b/server/migration/1660632269368-AddWatchlistSyncUserSetting.ts @@ -0,0 +1,33 @@ +import type { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddWatchlistSyncUserSetting1660632269368 + implements MigrationInterface +{ + name = 'AddWatchlistSyncUserSetting1660632269368'; + + 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, "region" varchar, "originalLanguage" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "pgpKey" varchar, "locale" varchar NOT NULL DEFAULT (''), "pushbulletAccessToken" varchar, "pushoverApplicationToken" varchar, "pushoverUserKey" varchar, "watchlistSyncMovies" boolean, "watchlistSyncTv" boolean, 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", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey") SELECT "id", "notificationTypes", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey" 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, "region" varchar, "originalLanguage" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "pgpKey" varchar, "locale" varchar NOT NULL DEFAULT (''), "pushbulletAccessToken" varchar, "pushoverApplicationToken" varchar, "pushoverUserKey" 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", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey") SELECT "id", "notificationTypes", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey" FROM "temporary_user_settings"` + ); + await queryRunner.query(`DROP TABLE "temporary_user_settings"`); + } +} diff --git a/server/migration/1660714479373-AddMediaRequestIsAutoRequestedField.ts b/server/migration/1660714479373-AddMediaRequestIsAutoRequestedField.ts new file mode 100644 index 000000000..8580bb4ed --- /dev/null +++ b/server/migration/1660714479373-AddMediaRequestIsAutoRequestedField.ts @@ -0,0 +1,33 @@ +import type { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddMediaRequestIsAutoRequestedField1660714479373 + implements MigrationInterface +{ + name = 'AddMediaRequestIsAutoRequestedField1660714479373'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "temporary_media_request" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "status" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "type" varchar NOT NULL, "mediaId" integer, "requestedById" integer, "modifiedById" integer, "is4k" boolean NOT NULL DEFAULT (0), "serverId" integer, "profileId" integer, "rootFolder" varchar, "languageProfileId" integer, "tags" text, "isAutoRequest" boolean NOT NULL DEFAULT (0), CONSTRAINT "FK_a1aa713f41c99e9d10c48da75a0" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_6997bee94720f1ecb7f31137095" FOREIGN KEY ("requestedById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_f4fc4efa14c3ba2b29c4525fa15" FOREIGN KEY ("modifiedById") REFERENCES "user" ("id") ON DELETE SET NULL ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "temporary_media_request"("id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById", "is4k", "serverId", "profileId", "rootFolder", "languageProfileId", "tags") SELECT "id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById", "is4k", "serverId", "profileId", "rootFolder", "languageProfileId", "tags" FROM "media_request"` + ); + await queryRunner.query(`DROP TABLE "media_request"`); + await queryRunner.query( + `ALTER TABLE "temporary_media_request" RENAME TO "media_request"` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "media_request" RENAME TO "temporary_media_request"` + ); + await queryRunner.query( + `CREATE TABLE "media_request" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "status" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "type" varchar NOT NULL, "mediaId" integer, "requestedById" integer, "modifiedById" integer, "is4k" boolean NOT NULL DEFAULT (0), "serverId" integer, "profileId" integer, "rootFolder" varchar, "languageProfileId" integer, "tags" text, CONSTRAINT "FK_a1aa713f41c99e9d10c48da75a0" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_6997bee94720f1ecb7f31137095" FOREIGN KEY ("requestedById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_f4fc4efa14c3ba2b29c4525fa15" FOREIGN KEY ("modifiedById") REFERENCES "user" ("id") ON DELETE SET NULL ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "media_request"("id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById", "is4k", "serverId", "profileId", "rootFolder", "languageProfileId", "tags") SELECT "id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById", "is4k", "serverId", "profileId", "rootFolder", "languageProfileId", "tags" FROM "temporary_media_request"` + ); + await queryRunner.query(`DROP TABLE "temporary_media_request"`); + } +} diff --git a/server/models/Collection.ts b/server/models/Collection.ts index 9cc4f3788..20a3c7158 100644 --- a/server/models/Collection.ts +++ b/server/models/Collection.ts @@ -1,8 +1,9 @@ +import type { TmdbCollection } from '@server/api/themoviedb/interfaces'; +import { MediaType } from '@server/constants/media'; +import type Media from '@server/entity/Media'; import { sortBy } from 'lodash'; -import type { TmdbCollection } from '../api/themoviedb/interfaces'; -import { MediaType } from '../constants/media'; -import Media from '../entity/Media'; -import { mapMovieResult, MovieResult } from './Search'; +import type { MovieResult } from './Search'; +import { mapMovieResult } from './Search'; export interface Collection { id: number; diff --git a/server/models/Movie.ts b/server/models/Movie.ts index ac19ce7e0..a216b7437 100644 --- a/server/models/Movie.ts +++ b/server/models/Movie.ts @@ -2,20 +2,22 @@ import type { TmdbMovieDetails, TmdbMovieReleaseResult, TmdbProductionCompany, -} from '../api/themoviedb/interfaces'; -import Media from '../entity/Media'; -import { +} from '@server/api/themoviedb/interfaces'; +import type Media from '@server/entity/Media'; +import type { Cast, Crew, ExternalIds, Genre, + ProductionCompany, + WatchProviders, +} from './common'; +import { mapCast, mapCrew, mapExternalIds, mapVideos, mapWatchProviders, - ProductionCompany, - WatchProviders, } from './common'; export interface Video { diff --git a/server/models/Person.ts b/server/models/Person.ts index 087ab1c7b..998585ee8 100644 --- a/server/models/Person.ts +++ b/server/models/Person.ts @@ -2,8 +2,8 @@ import type { TmdbPersonCreditCast, TmdbPersonCreditCrew, TmdbPersonDetails, -} from '../api/themoviedb/interfaces'; -import Media from '../entity/Media'; +} from '@server/api/themoviedb/interfaces'; +import type Media from '@server/entity/Media'; export interface PersonDetails { id: number; diff --git a/server/models/Search.ts b/server/models/Search.ts index 73427a378..6ab696fe3 100644 --- a/server/models/Search.ts +++ b/server/models/Search.ts @@ -5,9 +5,9 @@ import type { TmdbPersonResult, TmdbTvDetails, TmdbTvResult, -} from '../api/themoviedb/interfaces'; -import { MediaType as MainMediaType } from '../constants/media'; -import Media from '../entity/Media'; +} from '@server/api/themoviedb/interfaces'; +import { MediaType as MainMediaType } from '@server/constants/media'; +import type Media from '@server/entity/Media'; export type MediaType = 'tv' | 'movie' | 'person'; diff --git a/server/models/Tv.ts b/server/models/Tv.ts index b596b1d2b..7f809cbf4 100644 --- a/server/models/Tv.ts +++ b/server/models/Tv.ts @@ -5,24 +5,26 @@ import type { TmdbTvEpisodeResult, TmdbTvRatingResult, TmdbTvSeasonResult, -} from '../api/themoviedb/interfaces'; -import type Media from '../entity/Media'; -import { +} from '@server/api/themoviedb/interfaces'; +import type Media from '@server/entity/Media'; +import type { Cast, Crew, ExternalIds, Genre, Keyword, + ProductionCompany, + TvNetwork, + WatchProviders, +} from './common'; +import { mapAggregateCast, mapCrew, mapExternalIds, mapVideos, mapWatchProviders, - ProductionCompany, - TvNetwork, - WatchProviders, } from './common'; -import { Video } from './Movie'; +import type { Video } from './Movie'; interface Episode { id: number; diff --git a/server/models/common.ts b/server/models/common.ts index 49e2305cb..30b40d98c 100644 --- a/server/models/common.ts +++ b/server/models/common.ts @@ -7,8 +7,8 @@ import type { TmdbVideoResult, TmdbWatchProviderDetails, TmdbWatchProviders, -} from '../api/themoviedb/interfaces'; -import { Video } from '../models/Movie'; +} from '@server/api/themoviedb/interfaces'; +import type { Video } from '@server/models/Movie'; export interface ProductionCompany { id: number; diff --git a/server/routes/auth.ts b/server/routes/auth.ts index 81ba519fc..4b22943e1 100644 --- a/server/routes/auth.ts +++ b/server/routes/auth.ts @@ -1,16 +1,16 @@ -import { Router } from 'express'; -import { getRepository } from 'typeorm'; -import JellyfinAPI from '../api/jellyfin'; -import PlexTvAPI from '../api/plextv'; -import { MediaServerType } from '../constants/server'; -import { UserType } from '../constants/user'; -import { User } from '../entity/User'; -import { startJobs } from '../job/schedule'; -import { Permission } from '../lib/permissions'; -import { getSettings } from '../lib/settings'; -import logger from '../logger'; -import { isAuthenticated } from '../middleware/auth'; +import JellyfinAPI from '@server/api/jellyfin'; +import PlexTvAPI from '@server/api/plextv'; +import { MediaServerType } from '@server/constants/server'; +import { UserType } from '@server/constants/user'; +import { getRepository } from '@server/datasource'; +import { User } from '@server/entity/User'; +import { startJobs } from '@server/job/schedule'; +import { Permission } from '@server/lib/permissions'; +import { getSettings } from '@server/lib/settings'; +import logger from '@server/logger'; +import { isAuthenticated } from '@server/middleware/auth'; import * as EmailValidator from 'email-validator'; +import { Router } from 'express'; const authRoutes = Router(); @@ -89,8 +89,8 @@ authRoutes.post('/plex', async (req, res, next) => { await userRepository.save(user); } else { const mainUser = await userRepository.findOneOrFail({ - select: ['id', 'plexToken', 'plexId'], - order: { id: 'ASC' }, + select: { id: true, plexToken: true, plexId: true }, + where: { id: 1 }, }); const mainPlexTv = new PlexTvAPI(mainUser.plexToken ?? ''); @@ -424,8 +424,8 @@ authRoutes.post('/local', async (req, res, next) => { } const mainUser = await userRepository.findOneOrFail({ - select: ['id', 'plexToken', 'plexId'], - order: { id: 'ASC' }, + select: { id: true, plexToken: true, plexId: true }, + where: { id: 1 }, }); const mainPlexTv = new PlexTvAPI(mainUser.plexToken ?? ''); diff --git a/server/routes/collection.ts b/server/routes/collection.ts index aa8948736..d58b0357d 100644 --- a/server/routes/collection.ts +++ b/server/routes/collection.ts @@ -1,8 +1,8 @@ +import TheMovieDb from '@server/api/themoviedb'; +import Media from '@server/entity/Media'; +import logger from '@server/logger'; +import { mapCollection } from '@server/models/Collection'; import { Router } from 'express'; -import TheMovieDb from '../api/themoviedb'; -import Media from '../entity/Media'; -import logger from '../logger'; -import { mapCollection } from '../models/Collection'; const collectionRoutes = Router(); diff --git a/server/routes/discover.ts b/server/routes/discover.ts index ea78bf03d..b39a83325 100644 --- a/server/routes/discover.ts +++ b/server/routes/discover.ts @@ -1,16 +1,25 @@ +import PlexTvAPI from '@server/api/plextv'; +import TheMovieDb from '@server/api/themoviedb'; +import { MediaType } from '@server/constants/media'; +import { getRepository } from '@server/datasource'; +import Media from '@server/entity/Media'; +import { User } from '@server/entity/User'; +import type { + GenreSliderItem, + WatchlistResponse, +} from '@server/interfaces/api/discoverInterfaces'; +import { getSettings } from '@server/lib/settings'; +import logger from '@server/logger'; +import { mapProductionCompany } from '@server/models/Movie'; +import { + mapMovieResult, + mapPersonResult, + mapTvResult, +} from '@server/models/Search'; +import { mapNetwork } from '@server/models/Tv'; +import { isMovie, isPerson } from '@server/utils/typeHelpers'; import { Router } from 'express'; import { sortBy } from 'lodash'; -import TheMovieDb from '../api/themoviedb'; -import { MediaType } from '../constants/media'; -import Media from '../entity/Media'; -import { User } from '../entity/User'; -import { GenreSliderItem } from '../interfaces/api/discoverInterfaces'; -import { getSettings } from '../lib/settings'; -import logger from '../logger'; -import { mapProductionCompany } from '../models/Movie'; -import { mapMovieResult, mapPersonResult, mapTvResult } from '../models/Search'; -import { mapNetwork } from '../models/Tv'; -import { isMovie, isPerson } from '../utils/typeHelpers'; export const createTmdbWithRegionLanguage = (user?: User): TheMovieDb => { const settings = getSettings(); @@ -704,4 +713,45 @@ discoverRoutes.get<{ language: string }, GenreSliderItem[]>( } ); +discoverRoutes.get<{ page?: number }, WatchlistResponse>( + '/watchlist', + async (req, res) => { + const userRepository = getRepository(User); + const itemsPerPage = 20; + const page = req.params.page ?? 1; + const offset = (page - 1) * itemsPerPage; + + const activeUser = await userRepository.findOne({ + where: { id: req.user?.id }, + select: ['id', 'plexToken'], + }); + + if (!activeUser?.plexToken) { + // We will just return an empty array if the user has no Plex token + return res.json({ + page: 1, + totalPages: 1, + totalResults: 0, + results: [], + }); + } + + const plexTV = new PlexTvAPI(activeUser.plexToken); + + const watchlist = await plexTV.getWatchlist({ offset }); + + return res.json({ + page, + totalPages: Math.ceil(watchlist.size / itemsPerPage), + totalResults: watchlist.size, + results: watchlist.items.map((item) => ({ + ratingKey: item.ratingKey, + title: item.title, + mediaType: item.type === 'show' ? 'tv' : 'movie', + tmdbId: item.tmdbId, + })), + }); + } +); + export default discoverRoutes; diff --git a/server/routes/index.ts b/server/routes/index.ts index e28666385..9561e171b 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -1,17 +1,22 @@ +import GithubAPI from '@server/api/github'; +import TheMovieDb from '@server/api/themoviedb'; +import type { + TmdbMovieResult, + TmdbTvResult, +} from '@server/api/themoviedb/interfaces'; +import type { StatusResponse } from '@server/interfaces/api/settingsInterfaces'; +import { Permission } from '@server/lib/permissions'; +import { getSettings } from '@server/lib/settings'; +import logger from '@server/logger'; +import { checkUser, isAuthenticated } from '@server/middleware/auth'; +import { mapProductionCompany } from '@server/models/Movie'; +import { mapNetwork } from '@server/models/Tv'; +import settingsRoutes from '@server/routes/settings'; +import { appDataPath, 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 GithubAPI from '../api/github'; -import TheMovieDb from '../api/themoviedb'; -import { TmdbMovieResult, TmdbTvResult } from '../api/themoviedb/interfaces'; -import { StatusResponse } from '../interfaces/api/settingsInterfaces'; -import { Permission } from '../lib/permissions'; -import { getSettings } from '../lib/settings'; -import logger from '../logger'; -import { checkUser, isAuthenticated } from '../middleware/auth'; -import { mapProductionCompany } from '../models/Movie'; -import { mapNetwork } from '../models/Tv'; -import { appDataPath, appDataStatus } from '../utils/appDataVolume'; -import { getAppVersion, getCommitTag } from '../utils/appVersion'; -import { isPerson } from '../utils/typeHelpers'; import authRoutes from './auth'; import collectionRoutes from './collection'; import discoverRoutes, { createTmdbWithRegionLanguage } from './discover'; @@ -23,7 +28,6 @@ import personRoutes from './person'; import requestRoutes from './request'; import searchRoutes from './search'; import serviceRoutes from './service'; -import settingsRoutes from './settings'; import tvRoutes from './tv'; import user from './user'; @@ -75,6 +79,7 @@ router.get('/status', async (req, res) => { commitTag: getCommitTag(), updateAvailable, commitsBehind, + restartRequired: restartFlag.isSet(), }); }); @@ -97,11 +102,7 @@ router.get('/settings/public', async (req, res) => { return res.status(200).json(settings.fullPublicSettings); } }); -router.use( - '/settings', - isAuthenticated(Permission.MANAGE_SETTINGS), - settingsRoutes -); +router.use('/settings', isAuthenticated(Permission.ADMIN), settingsRoutes); router.use('/search', isAuthenticated(), searchRoutes); router.use('/discover', isAuthenticated(), discoverRoutes); router.use('/request', isAuthenticated(), requestRoutes); diff --git a/server/routes/issue.ts b/server/routes/issue.ts index 07cf3277d..6349bb74a 100644 --- a/server/routes/issue.ts +++ b/server/routes/issue.ts @@ -1,13 +1,13 @@ +import { IssueStatus, IssueType } from '@server/constants/issue'; +import { getRepository } from '@server/datasource'; +import Issue from '@server/entity/Issue'; +import IssueComment from '@server/entity/IssueComment'; +import Media from '@server/entity/Media'; +import type { IssueResultsResponse } from '@server/interfaces/api/issueInterfaces'; +import { Permission } from '@server/lib/permissions'; +import logger from '@server/logger'; +import { isAuthenticated } from '@server/middleware/auth'; import { Router } from 'express'; -import { getRepository } from 'typeorm'; -import { IssueStatus, IssueType } from '../constants/issue'; -import Issue from '../entity/Issue'; -import IssueComment from '../entity/IssueComment'; -import Media from '../entity/Media'; -import { IssueResultsResponse } from '../interfaces/api/issueInterfaces'; -import { Permission } from '../lib/permissions'; -import logger from '../logger'; -import { isAuthenticated } from '../middleware/auth'; const issueRoutes = Router(); @@ -365,7 +365,7 @@ issueRoutes.delete( try { const issue = await issueRepository.findOneOrFail({ where: { id: Number(req.params.issueId) }, - relations: ['createdBy'], + relations: { createdBy: true }, }); if ( diff --git a/server/routes/issueComment.ts b/server/routes/issueComment.ts index c54bce5b6..85e41aaaf 100644 --- a/server/routes/issueComment.ts +++ b/server/routes/issueComment.ts @@ -1,9 +1,9 @@ +import { getRepository } from '@server/datasource'; +import IssueComment from '@server/entity/IssueComment'; +import { Permission } from '@server/lib/permissions'; +import logger from '@server/logger'; +import { isAuthenticated } from '@server/middleware/auth'; import { Router } from 'express'; -import { getRepository } from 'typeorm'; -import IssueComment from '../entity/IssueComment'; -import { Permission } from '../lib/permissions'; -import logger from '../logger'; -import { isAuthenticated } from '../middleware/auth'; const issueCommentRoutes = Router(); diff --git a/server/routes/media.ts b/server/routes/media.ts index 73f08cabe..7ca1c3adb 100644 --- a/server/routes/media.ts +++ b/server/routes/media.ts @@ -1,20 +1,22 @@ -import { Router } from 'express'; import { FindOneOptions, FindOperator, getRepository, In } from 'typeorm'; -import RadarrAPI from '../api/servarr/radarr'; -import SonarrAPI from '../api/servarr/sonarr'; -import TautulliAPI from '../api/tautulli'; -import TheMovieDb from '../api/themoviedb'; -import { MediaStatus, MediaType } from '../constants/media'; -import Media from '../entity/Media'; -import { User } from '../entity/User'; -import { +import RadarrAPI from '@server/api/servarr/radarr'; +import SonarrAPI from '@server/api/servarr/sonarr'; +import TheMovieDb from '@server/api/themoviedb'; +import TautulliAPI from '@server/api/tautulli'; +import { MediaStatus, MediaType } from '@server/constants/media'; +import { getRepository } from '@server/datasource'; +import Media from '@server/entity/Media'; +import { User } from '@server/entity/User'; +import type { MediaResultsResponse, MediaWatchDataResponse, -} from '../interfaces/api/mediaInterfaces'; -import { Permission } from '../lib/permissions'; -import { getSettings } from '../lib/settings'; -import logger from '../logger'; -import { isAuthenticated } from '../middleware/auth'; +} from '@server/interfaces/api/mediaInterfaces'; +import { Permission } from '@server/lib/permissions'; +import { getSettings } from '@server/lib/settings'; +import logger from '@server/logger'; +import { isAuthenticated } from '@server/middleware/auth'; +import { Router } from 'express'; + const mediaRoutes = Router(); @@ -24,8 +26,7 @@ mediaRoutes.get('/', async (req, res, next) => { const pageSize = req.query.take ? Number(req.query.take) : 20; const skip = req.query.skip ? Number(req.query.skip) : 0; - let statusFilter: MediaStatus | FindOperator | undefined = - undefined; + let statusFilter = undefined; switch (req.query.filter) { case 'available': @@ -69,7 +70,7 @@ mediaRoutes.get('/', async (req, res, next) => { try { const [media, mediaCount] = await mediaRepository.findAndCount({ order: sortFilter, - where: { + where: statusFilter && { status: statusFilter, }, take: pageSize, @@ -154,7 +155,7 @@ mediaRoutes.delete( const mediaRepository = getRepository(Media); const media = await mediaRepository.findOneOrFail({ - where: { id: req.params.id }, + where: { id: Number(req.params.id) }, }); await mediaRepository.remove(media); diff --git a/server/routes/movie.ts b/server/routes/movie.ts index 98474c78e..f11cead8c 100644 --- a/server/routes/movie.ts +++ b/server/routes/movie.ts @@ -1,11 +1,11 @@ +import RottenTomatoes from '@server/api/rottentomatoes'; +import TheMovieDb from '@server/api/themoviedb'; +import { MediaType } from '@server/constants/media'; +import Media from '@server/entity/Media'; +import logger from '@server/logger'; +import { mapMovieDetails } from '@server/models/Movie'; +import { mapMovieResult } from '@server/models/Search'; import { Router } from 'express'; -import RottenTomatoes from '../api/rottentomatoes'; -import TheMovieDb from '../api/themoviedb'; -import { MediaType } from '../constants/media'; -import Media from '../entity/Media'; -import logger from '../logger'; -import { mapMovieDetails } from '../models/Movie'; -import { mapMovieResult } from '../models/Search'; const movieRoutes = Router(); diff --git a/server/routes/person.ts b/server/routes/person.ts index 5093ae46c..7f5d62236 100644 --- a/server/routes/person.ts +++ b/server/routes/person.ts @@ -1,12 +1,12 @@ -import { Router } from 'express'; -import TheMovieDb from '../api/themoviedb'; -import Media from '../entity/Media'; -import logger from '../logger'; +import TheMovieDb from '@server/api/themoviedb'; +import Media from '@server/entity/Media'; +import logger from '@server/logger'; import { mapCastCredits, mapCrewCredits, mapPersonDetails, -} from '../models/Person'; +} from '@server/models/Person'; +import { Router } from 'express'; const personRoutes = Router(); diff --git a/server/routes/request.ts b/server/routes/request.ts index cd269f4ef..9c9d96a82 100644 --- a/server/routes/request.ts +++ b/server/routes/request.ts @@ -1,15 +1,27 @@ +import { + MediaRequestStatus, + MediaStatus, + MediaType, +} from '@server/constants/media'; +import { getRepository } from '@server/datasource'; +import Media from '@server/entity/Media'; +import { + DuplicateMediaRequestError, + MediaRequest, + NoSeasonsAvailableError, + QuotaRestrictedError, + RequestPermissionError, +} from '@server/entity/MediaRequest'; +import SeasonRequest from '@server/entity/SeasonRequest'; +import { User } from '@server/entity/User'; +import type { + MediaRequestBody, + RequestResultsResponse, +} from '@server/interfaces/api/requestInterfaces'; +import { Permission } from '@server/lib/permissions'; +import logger from '@server/logger'; +import { isAuthenticated } from '@server/middleware/auth'; import { Router } from 'express'; -import { getRepository } from 'typeorm'; -import TheMovieDb from '../api/themoviedb'; -import { MediaRequestStatus, MediaStatus, MediaType } from '../constants/media'; -import Media from '../entity/Media'; -import { MediaRequest } from '../entity/MediaRequest'; -import SeasonRequest from '../entity/SeasonRequest'; -import { User } from '../entity/User'; -import { RequestResultsResponse } from '../interfaces/api/requestInterfaces'; -import { Permission } from '../lib/permissions'; -import logger from '../logger'; -import { isAuthenticated } from '../middleware/auth'; const requestRoutes = Router(); @@ -40,11 +52,15 @@ requestRoutes.get, RequestResultsResponse>( MediaRequestStatus.APPROVED, ]; break; + case 'failed': + statusFilter = [MediaRequestStatus.FAILED]; + break; default: statusFilter = [ MediaRequestStatus.PENDING, MediaRequestStatus.APPROVED, MediaRequestStatus.DECLINED, + MediaRequestStatus.FAILED, ]; } @@ -142,302 +158,38 @@ requestRoutes.get, RequestResultsResponse>( } ); -requestRoutes.post('/', async (req, res, next) => { - const tmdb = new TheMovieDb(); - const mediaRepository = getRepository(Media); - const requestRepository = getRepository(MediaRequest); - const userRepository = getRepository(User); - - try { - let requestUser = req.user; - - if ( - req.body.userId && - !req.user?.hasPermission([ - Permission.MANAGE_USERS, - Permission.MANAGE_REQUESTS, - ]) - ) { - return next({ - status: 403, - message: 'You do not have permission to modify the request user.', - }); - } else if (req.body.userId) { - requestUser = await userRepository.findOneOrFail({ - where: { id: req.body.userId }, - }); - } - - if (!requestUser) { - return next({ - status: 500, - message: 'User missing from request context.', - }); - } - - if ( - req.body.mediaType === MediaType.MOVIE && - !req.user?.hasPermission( - req.body.is4k - ? [Permission.REQUEST_4K, Permission.REQUEST_4K_MOVIE] - : [Permission.REQUEST, Permission.REQUEST_MOVIE], - { - type: 'or', - } - ) - ) { - return next({ - status: 403, - message: `You do not have permission to make ${ - req.body.is4k ? '4K ' : '' - }movie requests.`, - }); - } else if ( - req.body.mediaType === MediaType.TV && - !req.user?.hasPermission( - req.body.is4k - ? [Permission.REQUEST_4K, Permission.REQUEST_4K_TV] - : [Permission.REQUEST, Permission.REQUEST_TV], - { - type: 'or', - } - ) - ) { - return next({ - status: 403, - message: `You do not have permission to make ${ - req.body.is4k ? '4K ' : '' - }series requests.`, - }); - } - - const quotas = await requestUser.getQuota(); - - if (req.body.mediaType === MediaType.MOVIE && quotas.movie.restricted) { - return next({ - status: 403, - message: 'Movie Quota Exceeded', - }); - } else if (req.body.mediaType === MediaType.TV && quotas.tv.restricted) { - return next({ - status: 403, - message: 'Series Quota Exceeded', - }); - } - - const tmdbMedia = - req.body.mediaType === MediaType.MOVIE - ? await tmdb.getMovie({ movieId: req.body.mediaId }) - : await tmdb.getTvShow({ tvId: req.body.mediaId }); - - let media = await mediaRepository.findOne({ - where: { tmdbId: req.body.mediaId, mediaType: req.body.mediaType }, - relations: ['requests'], - }); - - if (!media) { - media = new Media({ - tmdbId: tmdbMedia.id, - tvdbId: req.body.tvdbId ?? tmdbMedia.external_ids.tvdb_id, - status: !req.body.is4k ? MediaStatus.PENDING : MediaStatus.UNKNOWN, - status4k: req.body.is4k ? MediaStatus.PENDING : MediaStatus.UNKNOWN, - mediaType: req.body.mediaType, - }); - } else { - if (media.status === MediaStatus.UNKNOWN && !req.body.is4k) { - media.status = MediaStatus.PENDING; - } - - if (media.status4k === MediaStatus.UNKNOWN && req.body.is4k) { - media.status4k = MediaStatus.PENDING; - } - } - - if (req.body.mediaType === MediaType.MOVIE) { - const existing = await requestRepository - .createQueryBuilder('request') - .leftJoin('request.media', 'media') - .where('request.is4k = :is4k', { is4k: req.body.is4k }) - .andWhere('media.tmdbId = :tmdbId', { tmdbId: tmdbMedia.id }) - .andWhere('media.mediaType = :mediaType', { - mediaType: MediaType.MOVIE, - }) - .andWhere('request.status != :requestStatus', { - requestStatus: MediaRequestStatus.DECLINED, - }) - .getOne(); - - if (existing) { - logger.warn('Duplicate request for media blocked', { - tmdbId: tmdbMedia.id, - mediaType: req.body.mediaType, - is4k: req.body.is4k, - label: 'Media Request', - }); +requestRoutes.post( + '/', + async (req, res, next) => { + try { + if (!req.user) { return next({ - status: 409, - message: 'Request for this media already exists.', + status: 401, + message: 'You must be logged in to request media.', }); } + const request = await MediaRequest.request(req.body, req.user); - await mediaRepository.save(media); - - const request = new MediaRequest({ - type: MediaType.MOVIE, - media, - requestedBy: requestUser, - // If the user is an admin or has the "auto approve" permission, automatically approve the request - status: req.user?.hasPermission( - [ - req.body.is4k - ? Permission.AUTO_APPROVE_4K - : Permission.AUTO_APPROVE, - req.body.is4k - ? Permission.AUTO_APPROVE_4K_MOVIE - : Permission.AUTO_APPROVE_MOVIE, - Permission.MANAGE_REQUESTS, - ], - { type: 'or' } - ) - ? MediaRequestStatus.APPROVED - : MediaRequestStatus.PENDING, - modifiedBy: req.user?.hasPermission( - [ - req.body.is4k - ? Permission.AUTO_APPROVE_4K - : Permission.AUTO_APPROVE, - req.body.is4k - ? Permission.AUTO_APPROVE_4K_MOVIE - : Permission.AUTO_APPROVE_MOVIE, - Permission.MANAGE_REQUESTS, - ], - { type: 'or' } - ) - ? req.user - : undefined, - is4k: req.body.is4k, - serverId: req.body.serverId, - profileId: req.body.profileId, - rootFolder: req.body.rootFolder, - tags: req.body.tags, - }); - - await requestRepository.save(request); return res.status(201).json(request); - } else if (req.body.mediaType === MediaType.TV) { - const requestedSeasons = req.body.seasons as number[]; - let existingSeasons: number[] = []; - - // We need to check existing requests on this title to make sure we don't double up on seasons that were - // already requested. In the case they were, we just throw out any duplicates but still approve the request. - // (Unless there are no seasons, in which case we abort) - if (media.requests) { - existingSeasons = media.requests - .filter( - (request) => - request.is4k === req.body.is4k && - request.status !== MediaRequestStatus.DECLINED - ) - .reduce((seasons, request) => { - const combinedSeasons = request.seasons.map( - (season) => season.seasonNumber - ); - - return [...seasons, ...combinedSeasons]; - }, [] as number[]); + } catch (error) { + if (!(error instanceof Error)) { + return; } - const finalSeasons = requestedSeasons.filter( - (rs) => !existingSeasons.includes(rs) - ); - - if (finalSeasons.length === 0) { - return next({ - status: 202, - message: 'No seasons available to request', - }); - } else if ( - quotas.tv.limit && - finalSeasons.length > (quotas.tv.remaining ?? 0) - ) { - return next({ - status: 403, - message: 'Series Quota Exceeded', - }); + switch (error.constructor) { + case RequestPermissionError: + case QuotaRestrictedError: + return next({ status: 403, message: error.message }); + case DuplicateMediaRequestError: + return next({ status: 409, message: error.message }); + case NoSeasonsAvailableError: + return next({ status: 202, message: error.message }); + default: + return next({ status: 500, message: error.message }); } - - await mediaRepository.save(media); - - const request = new MediaRequest({ - type: MediaType.TV, - media, - requestedBy: requestUser, - // If the user is an admin or has the "auto approve" permission, automatically approve the request - status: req.user?.hasPermission( - [ - req.body.is4k - ? Permission.AUTO_APPROVE_4K - : Permission.AUTO_APPROVE, - req.body.is4k - ? Permission.AUTO_APPROVE_4K_TV - : Permission.AUTO_APPROVE_TV, - Permission.MANAGE_REQUESTS, - ], - { type: 'or' } - ) - ? MediaRequestStatus.APPROVED - : MediaRequestStatus.PENDING, - modifiedBy: req.user?.hasPermission( - [ - req.body.is4k - ? Permission.AUTO_APPROVE_4K - : Permission.AUTO_APPROVE, - req.body.is4k - ? Permission.AUTO_APPROVE_4K_TV - : Permission.AUTO_APPROVE_TV, - Permission.MANAGE_REQUESTS, - ], - { type: 'or' } - ) - ? req.user - : undefined, - is4k: req.body.is4k, - serverId: req.body.serverId, - profileId: req.body.profileId, - rootFolder: req.body.rootFolder, - languageProfileId: req.body.languageProfileId, - tags: req.body.tags, - seasons: finalSeasons.map( - (sn) => - new SeasonRequest({ - seasonNumber: sn, - status: req.user?.hasPermission( - [ - req.body.is4k - ? Permission.AUTO_APPROVE_4K - : Permission.AUTO_APPROVE, - req.body.is4k - ? Permission.AUTO_APPROVE_4K_TV - : Permission.AUTO_APPROVE_TV, - Permission.MANAGE_REQUESTS, - ], - { type: 'or' } - ) - ? MediaRequestStatus.APPROVED - : MediaRequestStatus.PENDING, - }) - ), - }); - - await requestRepository.save(request); - return res.status(201).json(request); } - - next({ status: 500, message: 'Invalid media type' }); - } catch (e) { - next({ status: 500, message: e.message }); } -}); +); requestRoutes.get('/count', async (_req, res, next) => { const requestRepository = getRepository(MediaRequest); @@ -528,7 +280,7 @@ requestRoutes.get('/:requestId', async (req, res, next) => { try { const request = await requestRepository.findOneOrFail({ where: { id: Number(req.params.requestId) }, - relations: ['requestedBy', 'modifiedBy'], + relations: { requestedBy: true, modifiedBy: true }, }); if ( @@ -560,9 +312,11 @@ requestRoutes.put<{ requestId: string }>( const requestRepository = getRepository(MediaRequest); const userRepository = getRepository(User); try { - const request = await requestRepository.findOne( - Number(req.params.requestId) - ); + const request = await requestRepository.findOne({ + where: { + id: Number(req.params.requestId), + }, + }); if (!request) { return next({ status: 404, message: 'Request not found.' }); @@ -628,7 +382,7 @@ requestRoutes.put<{ requestId: string }>( // Get existing media so we can work with all the requests const media = await mediaRepository.findOneOrFail({ where: { tmdbId: request.media.tmdbId, mediaType: MediaType.TV }, - relations: ['requests'], + relations: { requests: true }, }); // Get all requested seasons that are not part of this request we are editing @@ -698,7 +452,7 @@ requestRoutes.delete('/:requestId', async (req, res, next) => { try { const request = await requestRepository.findOneOrFail({ where: { id: Number(req.params.requestId) }, - relations: ['requestedBy', 'modifiedBy'], + relations: { requestedBy: true, modifiedBy: true }, }); if ( @@ -735,7 +489,7 @@ requestRoutes.post<{ try { const request = await requestRepository.findOneOrFail({ where: { id: Number(req.params.requestId) }, - relations: ['requestedBy', 'modifiedBy'], + relations: { requestedBy: true, modifiedBy: true }, }); await request.updateParentStatus(); @@ -763,7 +517,7 @@ requestRoutes.post<{ try { const request = await requestRepository.findOneOrFail({ where: { id: Number(req.params.requestId) }, - relations: ['requestedBy', 'modifiedBy'], + relations: { requestedBy: true, modifiedBy: true }, }); let newStatus: MediaRequestStatus; diff --git a/server/routes/search.ts b/server/routes/search.ts index 3f26a3939..1152bce31 100644 --- a/server/routes/search.ts +++ b/server/routes/search.ts @@ -1,10 +1,10 @@ +import TheMovieDb from '@server/api/themoviedb'; +import type { TmdbSearchMultiResponse } from '@server/api/themoviedb/interfaces'; +import Media from '@server/entity/Media'; +import { findSearchProvider } from '@server/lib/search'; +import logger from '@server/logger'; +import { mapSearchResults } from '@server/models/Search'; import { Router } from 'express'; -import TheMovieDb from '../api/themoviedb'; -import { TmdbSearchMultiResponse } from '../api/themoviedb/interfaces'; -import Media from '../entity/Media'; -import { findSearchProvider } from '../lib/search'; -import logger from '../logger'; -import { mapSearchResults } from '../models/Search'; const searchRoutes = Router(); diff --git a/server/routes/service.ts b/server/routes/service.ts index 862ab3748..b77d58c9d 100644 --- a/server/routes/service.ts +++ b/server/routes/service.ts @@ -1,13 +1,13 @@ -import { Router } from 'express'; -import RadarrAPI from '../api/servarr/radarr'; -import SonarrAPI from '../api/servarr/sonarr'; -import TheMovieDb from '../api/themoviedb'; -import { +import RadarrAPI from '@server/api/servarr/radarr'; +import SonarrAPI from '@server/api/servarr/sonarr'; +import TheMovieDb from '@server/api/themoviedb'; +import type { ServiceCommonServer, ServiceCommonServerWithDetails, -} from '../interfaces/api/serviceInterfaces'; -import { getSettings } from '../lib/settings'; -import logger from '../logger'; +} from '@server/interfaces/api/serviceInterfaces'; +import { getSettings } from '@server/lib/settings'; +import logger from '@server/logger'; +import { Router } from 'express'; const serviceRoutes = Router(); @@ -191,7 +191,7 @@ serviceRoutes.get<{ tmdbId: string }>( try { const tv = await tmdb.getTvShow({ tvId: Number(req.params.tmdbId), - language: req.locale ?? (req.query.language as string), + language: 'en', }); const response = await sonarr.getSeriesByTitle(tv.name); diff --git a/server/routes/settings/index.ts b/server/routes/settings/index.ts index 7ebff7605..3a7cb6dfc 100644 --- a/server/routes/settings/index.ts +++ b/server/routes/settings/index.ts @@ -1,35 +1,37 @@ -import { Router } from 'express'; -import rateLimit from 'express-rate-limit'; -import fs from 'fs'; -import { merge, omit, set, sortBy } from 'lodash'; -import { rescheduleJob } from 'node-schedule'; -import path from 'path'; -import semver from 'semver'; -import { getRepository } from 'typeorm'; -import { URL } from 'url'; -import JellyfinAPI from '../../api/jellyfin'; -import PlexAPI from '../../api/plexapi'; -import PlexTvAPI from '../../api/plextv'; -import TautulliAPI from '../../api/tautulli'; -import Media from '../../entity/Media'; -import { MediaRequest } from '../../entity/MediaRequest'; -import { User } from '../../entity/User'; -import { PlexConnection } from '../../interfaces/api/plexInterfaces'; -import { +import JellyfinAPI from '@server/api/jellyfin'; +import PlexAPI from '@server/api/plexapi'; +import PlexTvAPI from '@server/api/plextv'; +import TautulliAPI from '@server/api/tautulli'; +import { getRepository } from '@server/datasource'; +import Media from '@server/entity/Media'; +import { MediaRequest } from '@server/entity/MediaRequest'; +import { User } from '@server/entity/User'; +import type { PlexConnection } from '@server/interfaces/api/plexInterfaces'; +import type { LogMessage, LogsResultsResponse, SettingsAboutResponse, -} from '../../interfaces/api/settingsInterfaces'; -import { jobJellyfinFullSync } from '../../job/jellyfinsync'; -import { scheduledJobs } from '../../job/schedule'; -import cacheManager, { AvailableCacheIds } from '../../lib/cache'; -import { Permission } from '../../lib/permissions'; -import { plexFullScanner } from '../../lib/scanners/plex'; -import { getSettings, Library, MainSettings } from '../../lib/settings'; -import logger from '../../logger'; -import { isAuthenticated } from '../../middleware/auth'; -import { appDataPath } from '../../utils/appDataVolume'; -import { getAppVersion } from '../../utils/appVersion'; +} from '@server/interfaces/api/settingsInterfaces'; +import { jobJellyfinFullSync } from '@server/job/jellyfinsync'; +import { scheduledJobs } from '@server/job/schedule'; +import type { AvailableCacheIds } from '@server/lib/cache'; +import cacheManager from '@server/lib/cache'; +import { Permission } from '@server/lib/permissions'; +import { plexFullScanner } from '@server/lib/scanners/plex'; +import type { Library, MainSettings } from '@server/lib/settings'; +import { getSettings } from '@server/lib/settings'; +import logger from '@server/logger'; +import { isAuthenticated } from '@server/middleware/auth'; +import { appDataPath } from '@server/utils/appDataVolume'; +import { getAppVersion } from '@server/utils/appVersion'; +import { Router } from 'express'; +import rateLimit from 'express-rate-limit'; +import fs from 'fs'; +import { escapeRegExp, merge, omit, set, sortBy } from 'lodash'; +import { rescheduleJob } from 'node-schedule'; +import path from 'path'; +import semver from 'semver'; +import { URL } from 'url'; import notificationRoutes from './notifications'; import radarrRoutes from './radarr'; import sonarrRoutes from './sonarr'; @@ -93,8 +95,8 @@ settingsRoutes.post('/plex', async (req, res, next) => { const settings = getSettings(); try { const admin = await userRepository.findOneOrFail({ - select: ['id', 'plexToken'], - order: { id: 'ASC' }, + select: { id: true, plexToken: true }, + where: { id: 1 }, }); Object.assign(settings.plex, req.body); @@ -129,8 +131,8 @@ settingsRoutes.get('/plex/devices/servers', async (req, res, next) => { const userRepository = getRepository(User); try { const admin = await userRepository.findOneOrFail({ - select: ['id', 'plexToken'], - order: { id: 'ASC' }, + select: { id: true, plexToken: true }, + where: { id: 1 }, }); const plexTvClient = admin.plexToken ? new PlexTvAPI(admin.plexToken) @@ -208,8 +210,8 @@ settingsRoutes.get('/plex/library', async (req, res) => { if (req.query.sync) { const userRepository = getRepository(User); const admin = await userRepository.findOneOrFail({ - select: ['id', 'plexToken'], - order: { id: 'ASC' }, + select: { id: true, plexToken: true }, + where: { id: 1 }, }); const plexapi = new PlexAPI({ plexToken: admin.plexToken }); @@ -262,6 +264,7 @@ settingsRoutes.get('/jellyfin/library', async (req, res) => { const userRepository = getRepository(User); const admin = await userRepository.findOneOrFail({ select: ['id', 'jellyfinAuthToken', 'jellyfinDeviceId', 'jellyfinUserId'], + where: { id: 1 }, order: { id: 'ASC' }, }); const jellyfinClient = new JellyfinAPI( @@ -312,6 +315,7 @@ settingsRoutes.get('/jellyfin/users', async (req, res) => { const userRepository = getRepository(User); const admin = await userRepository.findOneOrFail({ select: ['id', 'jellyfinAuthToken', 'jellyfinDeviceId', 'jellyfinUserId'], + where: { id: 1 }, order: { id: 'ASC' }, }); const jellyfinClient = new JellyfinAPI( @@ -390,8 +394,8 @@ settingsRoutes.get( try { const admin = await userRepository.findOneOrFail({ - select: ['id', 'plexToken'], - order: { id: 'ASC' }, + select: { id: true, plexToken: true }, + where: { id: 1 }, }); const plexApi = new PlexTvAPI(admin.plexToken ?? ''); const plexUsers = (await plexApi.getUsers()).MediaContainer.User.map( @@ -450,6 +454,8 @@ settingsRoutes.get( (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) ?? ''; + const searchRegexp = new RegExp(escapeRegExp(search), 'i'); let filter: string[] = []; switch (req.query.filter) { @@ -481,6 +487,22 @@ settingsRoutes.get( 'data', ]; + const deepValueStrings = (obj: Record): string[] => { + const values = []; + + for (const val of Object.values(obj)) { + if (typeof val === 'string') { + values.push(val); + } else if (typeof val === 'number') { + values.push(val.toString()); + } else if (val !== null && typeof val === 'object') { + values.push(...deepValueStrings(val as Record)); + } + } + + return values; + }; + try { fs.readFileSync(logFile, 'utf-8') .split('\n') @@ -505,6 +527,19 @@ settingsRoutes.get( }); } + if (req.query.search) { + if ( + // label and data are sometimes undefined + !searchRegexp.test(logMessage.label ?? '') && + !searchRegexp.test(logMessage.message) && + !deepValueStrings(logMessage.data ?? {}).some((val) => + searchRegexp.test(val) + ) + ) { + return; + } + } + logs.push(logMessage); }); @@ -539,6 +574,7 @@ settingsRoutes.get('/jobs', (_req, res) => { name: job.name, type: job.type, interval: job.interval, + cronSchedule: job.cronSchedule, nextExecutionTime: job.job.nextInvocation(), running: job.running ? job.running() : false, })) @@ -559,6 +595,7 @@ settingsRoutes.post<{ jobId: string }>('/jobs/:jobId/run', (req, res, next) => { name: scheduledJob.name, type: scheduledJob.type, interval: scheduledJob.interval, + cronSchedule: scheduledJob.cronSchedule, nextExecutionTime: scheduledJob.job.nextInvocation(), running: scheduledJob.running ? scheduledJob.running() : false, }); @@ -584,6 +621,7 @@ settingsRoutes.post<{ jobId: string }>( name: scheduledJob.name, type: scheduledJob.type, interval: scheduledJob.interval, + cronSchedule: scheduledJob.cronSchedule, nextExecutionTime: scheduledJob.job.nextInvocation(), running: scheduledJob.running ? scheduledJob.running() : false, }); @@ -608,11 +646,14 @@ settingsRoutes.post<{ jobId: string }>( settings.jobs[scheduledJob.id].schedule = req.body.schedule; settings.save(); + scheduledJob.cronSchedule = req.body.schedule; + return res.status(200).json({ id: scheduledJob.id, name: scheduledJob.name, type: scheduledJob.type, interval: scheduledJob.interval, + cronSchedule: scheduledJob.cronSchedule, nextExecutionTime: scheduledJob.job.nextInvocation(), running: scheduledJob.running ? scheduledJob.running() : false, }); diff --git a/server/routes/settings/notifications.ts b/server/routes/settings/notifications.ts index 5a337237d..5a38555ca 100644 --- a/server/routes/settings/notifications.ts +++ b/server/routes/settings/notifications.ts @@ -1,23 +1,24 @@ +import type { User } from '@server/entity/User'; +import { Notification } from '@server/lib/notifications'; +import type { NotificationAgent } from '@server/lib/notifications/agents/agent'; +import DiscordAgent from '@server/lib/notifications/agents/discord'; +import EmailAgent from '@server/lib/notifications/agents/email'; +import GotifyAgent from '@server/lib/notifications/agents/gotify'; +import LunaSeaAgent from '@server/lib/notifications/agents/lunasea'; +import PushbulletAgent from '@server/lib/notifications/agents/pushbullet'; +import PushoverAgent from '@server/lib/notifications/agents/pushover'; +import SlackAgent from '@server/lib/notifications/agents/slack'; +import TelegramAgent from '@server/lib/notifications/agents/telegram'; +import WebhookAgent from '@server/lib/notifications/agents/webhook'; +import WebPushAgent from '@server/lib/notifications/agents/webpush'; +import { getSettings } from '@server/lib/settings'; import { Router } from 'express'; -import { User } from '../../entity/User'; -import { Notification } from '../../lib/notifications'; -import { NotificationAgent } from '../../lib/notifications/agents/agent'; -import DiscordAgent from '../../lib/notifications/agents/discord'; -import EmailAgent from '../../lib/notifications/agents/email'; -import GotifyAgent from '../../lib/notifications/agents/gotify'; -import LunaSeaAgent from '../../lib/notifications/agents/lunasea'; -import PushbulletAgent from '../../lib/notifications/agents/pushbullet'; -import PushoverAgent from '../../lib/notifications/agents/pushover'; -import SlackAgent from '../../lib/notifications/agents/slack'; -import TelegramAgent from '../../lib/notifications/agents/telegram'; -import WebhookAgent from '../../lib/notifications/agents/webhook'; -import WebPushAgent from '../../lib/notifications/agents/webpush'; -import { getSettings } from '../../lib/settings'; const notificationRoutes = Router(); const sendTestNotification = async (agent: NotificationAgent, user: User) => await agent.send(Notification.TEST_NOTIFICATION, { + notifySystem: true, notifyAdmin: false, notifyUser: user, subject: 'Test Notification', @@ -247,7 +248,7 @@ notificationRoutes.post('/webpush/test', async (req, res, next) => { if (!req.user) { return next({ status: 500, - message: 'User information missing from request', + message: 'User information is missing from the request.', }); } @@ -363,7 +364,7 @@ notificationRoutes.post('/lunasea/test', async (req, res, next) => { if (!req.user) { return next({ status: 500, - message: 'User information missing from request', + message: 'User information is missing from the request.', }); } @@ -384,34 +385,26 @@ notificationRoutes.get('/gotify', (_req, res) => { res.status(200).json(settings.notifications.agents.gotify); }); -notificationRoutes.post('/gotify', (req, rest) => { +notificationRoutes.post('/gotify', (req, res) => { const settings = getSettings(); settings.notifications.agents.gotify = req.body; settings.save(); - rest.status(200).json(settings.notifications.agents.gotify); + res.status(200).json(settings.notifications.agents.gotify); }); -notificationRoutes.post('/gotify/test', async (req, rest, next) => { +notificationRoutes.post('/gotify/test', async (req, res, next) => { if (!req.user) { return next({ status: 500, - message: 'User information is missing from request', + message: 'User information is missing from the request.', }); } const gotifyAgent = new GotifyAgent(req.body); - if ( - await gotifyAgent.send(Notification.TEST_NOTIFICATION, { - notifyAdmin: false, - notifyUser: req.user, - subject: 'Test Notification', - message: - 'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?', - }) - ) { - return rest.status(204).send(); + if (await sendTestNotification(gotifyAgent, req.user)) { + return res.status(204).send(); } else { return next({ status: 500, diff --git a/server/routes/settings/radarr.ts b/server/routes/settings/radarr.ts index a33bfcdba..c2b0a6f52 100644 --- a/server/routes/settings/radarr.ts +++ b/server/routes/settings/radarr.ts @@ -1,7 +1,8 @@ +import RadarrAPI from '@server/api/servarr/radarr'; +import type { RadarrSettings } from '@server/lib/settings'; +import { getSettings } from '@server/lib/settings'; +import logger from '@server/logger'; import { Router } from 'express'; -import RadarrAPI from '../../api/servarr/radarr'; -import { getSettings, RadarrSettings } from '../../lib/settings'; -import logger from '../../logger'; const radarrRoutes = Router(); diff --git a/server/routes/settings/sonarr.ts b/server/routes/settings/sonarr.ts index da5a5bb3f..358d07002 100644 --- a/server/routes/settings/sonarr.ts +++ b/server/routes/settings/sonarr.ts @@ -1,7 +1,8 @@ +import SonarrAPI from '@server/api/servarr/sonarr'; +import type { SonarrSettings } from '@server/lib/settings'; +import { getSettings } from '@server/lib/settings'; +import logger from '@server/logger'; import { Router } from 'express'; -import SonarrAPI from '../../api/servarr/sonarr'; -import { getSettings, SonarrSettings } from '../../lib/settings'; -import logger from '../../logger'; const sonarrRoutes = Router(); diff --git a/server/routes/tv.ts b/server/routes/tv.ts index 201e7afe3..d45e40620 100644 --- a/server/routes/tv.ts +++ b/server/routes/tv.ts @@ -1,11 +1,11 @@ +import RottenTomatoes from '@server/api/rottentomatoes'; +import TheMovieDb from '@server/api/themoviedb'; +import { MediaType } from '@server/constants/media'; +import Media from '@server/entity/Media'; +import logger from '@server/logger'; +import { mapTvResult } from '@server/models/Search'; +import { mapSeasonWithEpisodes, mapTvDetails } from '@server/models/Tv'; import { Router } from 'express'; -import RottenTomatoes from '../api/rottentomatoes'; -import TheMovieDb from '../api/themoviedb'; -import { MediaType } from '../constants/media'; -import Media from '../entity/Media'; -import logger from '../logger'; -import { mapTvResult } from '../models/Search'; -import { mapSeasonWithEpisodes, mapTvDetails } from '../models/Tv'; const tvRoutes = Router(); diff --git a/server/routes/user/index.ts b/server/routes/user/index.ts index 5811fc05f..a875ca1fc 100644 --- a/server/routes/user/index.ts +++ b/server/routes/user/index.ts @@ -1,26 +1,28 @@ -import { Router } from 'express'; -import gravatarUrl from 'gravatar-url'; -import { findIndex, sortBy } from 'lodash'; -import { getRepository, In, Not } from 'typeorm'; -import JellyfinAPI from '../../api/jellyfin'; -import PlexTvAPI from '../../api/plextv'; -import TautulliAPI from '../../api/tautulli'; -import { MediaType } from '../../constants/media'; -import { UserType } from '../../constants/user'; -import Media from '../../entity/Media'; -import { MediaRequest } from '../../entity/MediaRequest'; -import { User } from '../../entity/User'; -import { UserPushSubscription } from '../../entity/UserPushSubscription'; -import { +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 { UserType } from '@server/constants/user'; +import { getRepository } from '@server/datasource'; +import Media from '@server/entity/Media'; +import { MediaRequest } from '@server/entity/MediaRequest'; +import { User } from '@server/entity/User'; +import { UserPushSubscription } from '@server/entity/UserPushSubscription'; +import type { WatchlistResponse } from '@server/interfaces/api/discoverInterfaces'; +import type { QuotaResponse, UserRequestsResponse, UserResultsResponse, UserWatchDataResponse, -} from '../../interfaces/api/userInterfaces'; -import { hasPermission, Permission } from '../../lib/permissions'; -import { getSettings } from '../../lib/settings'; -import logger from '../../logger'; -import { isAuthenticated } from '../../middleware/auth'; +} from '@server/interfaces/api/userInterfaces'; +import { hasPermission, Permission } from '@server/lib/permissions'; +import { getSettings } from '@server/lib/settings'; +import logger from '@server/logger'; +import { isAuthenticated } from '@server/middleware/auth'; +import { Router } from 'express'; +import gravatarUrl from 'gravatar-url'; +import { findIndex, sortBy } from 'lodash'; +import { In } from 'typeorm'; import userSettingsRoutes from './usersettings'; const router = Router(); @@ -259,12 +261,7 @@ export const canMakePermissionsChange = ( user?: User ): boolean => // Only let the owner grant admin privileges - !(hasPermission(Permission.ADMIN, permissions) && user?.id !== 1) || - // Only let users with the manage settings permission, grant the same permission - !( - hasPermission(Permission.MANAGE_SETTINGS, permissions) && - !hasPermission(Permission.MANAGE_SETTINGS, user?.permissions ?? 0) - ); + !(hasPermission(Permission.ADMIN, permissions) && user?.id !== 1); router.put< Record, @@ -283,8 +280,12 @@ router.put< const userRepository = getRepository(User); - const users = await userRepository.findByIds(req.body.ids, { - ...(!isOwner ? { id: Not(1) } : {}), + const users: User[] = await userRepository.find({ + where: { + id: In( + isOwner ? req.body.ids : req.body.ids.filter((id) => Number(id) !== 1) + ), + }, }); const updatedUsers = await Promise.all( @@ -351,7 +352,7 @@ router.delete<{ id: string }>( const user = await userRepository.findOne({ where: { id: Number(req.params.id) }, - relations: ['requests'], + relations: { requests: true }, }); if (!user) { @@ -410,8 +411,8 @@ router.post( // taken from auth.ts const mainUser = await userRepository.findOneOrFail({ - select: ['id', 'plexToken'], - order: { id: 'ASC' }, + select: { id: true, plexToken: true }, + where: { id: 1 }, }); const mainPlexTv = new PlexTvAPI(mainUser.plexToken ?? ''); @@ -477,6 +478,7 @@ router.post( // taken from auth.ts const admin = await userRepository.findOneOrFail({ + where: { id: 1 }, select: [ 'id', 'jellyfinAuthToken', @@ -598,7 +600,7 @@ router.get<{ id: string }, UserWatchDataResponse>( try { const user = await getRepository(User).findOneOrFail({ where: { id: Number(req.params.id) }, - select: ['id', 'plexId'], + select: { id: true, plexId: true }, }); const tautulli = new TautulliAPI(settings); @@ -680,4 +682,60 @@ router.get<{ id: string }, UserWatchDataResponse>( } ); +router.get<{ id: string; page?: number }, WatchlistResponse>( + '/:id/watchlist', + async (req, res, next) => { + if ( + Number(req.params.id) !== req.user?.id && + !req.user?.hasPermission( + [Permission.MANAGE_REQUESTS, Permission.WATCHLIST_VIEW], + { + type: 'or', + } + ) + ) { + return next({ + status: 403, + message: + "You do not have permission to view this user's Plex Watchlist.", + }); + } + + const itemsPerPage = 20; + const page = req.params.page ?? 1; + const offset = (page - 1) * itemsPerPage; + + const user = await getRepository(User).findOneOrFail({ + where: { id: Number(req.params.id) }, + select: { id: true, plexToken: true }, + }); + + if (!user?.plexToken) { + // We will just return an empty array if the user has no Plex token + return res.json({ + page: 1, + totalPages: 1, + totalResults: 0, + results: [], + }); + } + + const plexTV = new PlexTvAPI(user.plexToken); + + const watchlist = await plexTV.getWatchlist({ offset }); + + return res.json({ + page, + totalPages: Math.ceil(watchlist.size / itemsPerPage), + totalResults: watchlist.size, + results: watchlist.items.map((item) => ({ + ratingKey: item.ratingKey, + title: item.title, + mediaType: item.type === 'show' ? 'tv' : 'movie', + tmdbId: item.tmdbId, + })), + }); + } +); + export default router; diff --git a/server/routes/user/usersettings.ts b/server/routes/user/usersettings.ts index a05311a22..9b9a11ece 100644 --- a/server/routes/user/usersettings.ts +++ b/server/routes/user/usersettings.ts @@ -1,16 +1,16 @@ -import { Router } from 'express'; -import { getRepository } from 'typeorm'; -import { canMakePermissionsChange } from '.'; -import { User } from '../../entity/User'; -import { UserSettings } from '../../entity/UserSettings'; -import { +import { getRepository } from '@server/datasource'; +import { User } from '@server/entity/User'; +import { UserSettings } from '@server/entity/UserSettings'; +import type { UserSettingsGeneralResponse, UserSettingsNotificationsResponse, -} from '../../interfaces/api/userSettingsInterfaces'; -import { Permission } from '../../lib/permissions'; -import { getSettings } from '../../lib/settings'; -import logger from '../../logger'; -import { isAuthenticated } from '../../middleware/auth'; +} from '@server/interfaces/api/userSettingsInterfaces'; +import { Permission } from '@server/lib/permissions'; +import { getSettings } from '@server/lib/settings'; +import logger from '@server/logger'; +import { isAuthenticated } from '@server/middleware/auth'; +import { Router } from 'express'; +import { canMakePermissionsChange } from '.'; const isOwnProfileOrAdmin = (): Middleware => { const authMiddleware: Middleware = (req, res, next) => { @@ -64,6 +64,8 @@ userSettingsRoutes.get<{ id: string }, UserSettingsGeneralResponse>( globalMovieQuotaLimit: defaultQuotas.movie.quotaLimit, globalTvQuotaDays: defaultQuotas.tv.quotaDays, globalTvQuotaLimit: defaultQuotas.tv.quotaLimit, + watchlistSyncMovies: user.settings?.watchlistSyncMovies, + watchlistSyncTv: user.settings?.watchlistSyncTv, }); } catch (e) { next({ status: 500, message: e.message }); @@ -115,12 +117,16 @@ userSettingsRoutes.post< locale: req.body.locale, region: req.body.region, originalLanguage: req.body.originalLanguage, + watchlistSyncMovies: req.body.watchlistSyncMovies, + watchlistSyncTv: req.body.watchlistSyncTv, }); } else { user.settings.discordId = req.body.discordId; user.settings.locale = req.body.locale; user.settings.region = req.body.region; user.settings.originalLanguage = req.body.originalLanguage; + user.settings.watchlistSyncMovies = req.body.watchlistSyncMovies; + user.settings.watchlistSyncTv = req.body.watchlistSyncTv; user.email = req.body.email ?? user.email; } @@ -132,6 +138,8 @@ userSettingsRoutes.post< locale: user.settings.locale, region: user.settings.region, originalLanguage: user.settings.originalLanguage, + watchlistSyncMovies: user.settings.watchlistSyncMovies, + watchlistSyncTv: user.settings.watchlistSyncTv, email: user.email, }); } catch (e) { diff --git a/server/scripts/prepareTestDb.ts b/server/scripts/prepareTestDb.ts new file mode 100644 index 000000000..7caede41f --- /dev/null +++ b/server/scripts/prepareTestDb.ts @@ -0,0 +1,72 @@ +import { UserType } from '@server/constants/user'; +import dataSource, { getRepository } from '@server/datasource'; +import { User } from '@server/entity/User'; +import { copyFileSync } from 'fs'; +import gravatarUrl from 'gravatar-url'; +import path from 'path'; + +const prepareDb = async () => { + // Copy over test settings.json + copyFileSync( + path.join(__dirname, '../../cypress/config/settings.cypress.json'), + path.join(__dirname, '../../config/settings.json') + ); + + // Connect to DB and seed test data + const dbConnection = await dataSource.initialize(); + + if (process.env.PRESERVE_DB !== 'true') { + await dbConnection.dropDatabase(); + } + + // Run migrations in production + if (process.env.WITH_MIGRATIONS === 'true') { + await dbConnection.runMigrations(); + } else { + await dbConnection.synchronize(); + } + + const userRepository = getRepository(User); + + const admin = await userRepository.findOne({ + select: { id: true, plexId: true }, + where: { id: 1 }, + }); + + // Create the admin user + const user = + (await userRepository.findOne({ + where: { email: 'admin@seerr.dev' }, + })) ?? new User(); + user.plexId = admin?.plexId ?? 1; + user.plexToken = '1234'; + user.plexUsername = 'admin'; + user.username = 'admin'; + user.email = 'admin@seerr.dev'; + user.userType = UserType.PLEX; + await user.setPassword('test1234'); + user.permissions = 2; + user.avatar = gravatarUrl('admin@seerr.dev', { default: 'mm', size: 200 }); + await userRepository.save(user); + + // Create the other user + const otherUser = + (await userRepository.findOne({ + where: { email: 'friend@seerr.dev' }, + })) ?? new User(); + otherUser.plexId = admin?.plexId ?? 1; + otherUser.plexToken = '1234'; + otherUser.plexUsername = 'friend'; + otherUser.username = 'friend'; + otherUser.email = 'friend@seerr.dev'; + otherUser.userType = UserType.PLEX; + await otherUser.setPassword('test1234'); + otherUser.permissions = 32; + otherUser.avatar = gravatarUrl('friend@seerr.dev', { + default: 'mm', + size: 200, + }); + await userRepository.save(otherUser); +}; + +prepareDb(); diff --git a/server/subscriber/IssueCommentSubscriber.ts b/server/subscriber/IssueCommentSubscriber.ts index 1b1b7b55c..cb95ba008 100644 --- a/server/subscriber/IssueCommentSubscriber.ts +++ b/server/subscriber/IssueCommentSubscriber.ts @@ -1,18 +1,15 @@ +import TheMovieDb from '@server/api/themoviedb'; +import { IssueType, IssueTypeName } from '@server/constants/issue'; +import { MediaType } from '@server/constants/media'; +import { getRepository } from '@server/datasource'; +import IssueComment from '@server/entity/IssueComment'; +import Media from '@server/entity/Media'; +import notificationManager, { Notification } from '@server/lib/notifications'; +import { Permission } from '@server/lib/permissions'; +import logger from '@server/logger'; import { sortBy } from 'lodash'; -import { - EntitySubscriberInterface, - EventSubscriber, - getRepository, - InsertEvent, -} from 'typeorm'; -import TheMovieDb from '../api/themoviedb'; -import { IssueType, IssueTypeName } from '../constants/issue'; -import { MediaType } from '../constants/media'; -import IssueComment from '../entity/IssueComment'; -import Media from '../entity/Media'; -import notificationManager, { Notification } from '../lib/notifications'; -import { Permission } from '../lib/permissions'; -import logger from '../logger'; +import type { EntitySubscriberInterface, InsertEvent } from 'typeorm'; +import { EventSubscriber } from 'typeorm'; @EventSubscriber() export class IssueCommentSubscriber @@ -31,7 +28,7 @@ export class IssueCommentSubscriber const issue = ( await getRepository(IssueComment).findOneOrFail({ where: { id: entity.id }, - relations: ['issue'], + relations: { issue: true }, }) ).issue; @@ -72,6 +69,7 @@ export class IssueCommentSubscriber media, image, notifyAdmin: true, + notifySystem: true, notifyUser: !issue.createdBy.hasPermission(Permission.MANAGE_ISSUES) && issue.createdBy.id !== entity.user.id diff --git a/server/subscriber/IssueSubscriber.ts b/server/subscriber/IssueSubscriber.ts index b593095cd..eb4020415 100644 --- a/server/subscriber/IssueSubscriber.ts +++ b/server/subscriber/IssueSubscriber.ts @@ -1,17 +1,17 @@ +import TheMovieDb from '@server/api/themoviedb'; +import { IssueStatus, IssueType, IssueTypeName } from '@server/constants/issue'; +import { MediaType } from '@server/constants/media'; +import Issue from '@server/entity/Issue'; +import notificationManager, { Notification } from '@server/lib/notifications'; +import { Permission } from '@server/lib/permissions'; +import logger from '@server/logger'; import { sortBy } from 'lodash'; -import { +import type { EntitySubscriberInterface, - EventSubscriber, InsertEvent, UpdateEvent, } from 'typeorm'; -import TheMovieDb from '../api/themoviedb'; -import { IssueStatus, IssueType, IssueTypeName } from '../constants/issue'; -import { MediaType } from '../constants/media'; -import Issue from '../entity/Issue'; -import notificationManager, { Notification } from '../lib/notifications'; -import { Permission } from '../lib/permissions'; -import logger from '../logger'; +import { EventSubscriber } from 'typeorm'; @EventSubscriber() export class IssueSubscriber implements EntitySubscriberInterface { @@ -84,6 +84,7 @@ export class IssueSubscriber implements EntitySubscriberInterface { image, extra, notifyAdmin: true, + notifySystem: true, notifyUser: !entity.createdBy.hasPermission(Permission.MANAGE_ISSUES) && (type === Notification.ISSUE_RESOLVED || diff --git a/server/subscriber/MediaSubscriber.ts b/server/subscriber/MediaSubscriber.ts index 01752b0d1..eecfe6f3d 100644 --- a/server/subscriber/MediaSubscriber.ts +++ b/server/subscriber/MediaSubscriber.ts @@ -1,18 +1,18 @@ -import { truncate } from 'lodash'; +import TheMovieDb from '@server/api/themoviedb'; import { - EntitySubscriberInterface, - EventSubscriber, - getRepository, - Not, - UpdateEvent, -} from 'typeorm'; -import TheMovieDb from '../api/themoviedb'; -import { MediaRequestStatus, MediaStatus, MediaType } from '../constants/media'; -import Media from '../entity/Media'; -import { MediaRequest } from '../entity/MediaRequest'; -import Season from '../entity/Season'; -import notificationManager, { Notification } from '../lib/notifications'; -import logger from '../logger'; + MediaRequestStatus, + MediaStatus, + MediaType, +} from '@server/constants/media'; +import { getRepository } from '@server/datasource'; +import Media from '@server/entity/Media'; +import { MediaRequest } from '@server/entity/MediaRequest'; +import Season from '@server/entity/Season'; +import notificationManager, { Notification } from '@server/lib/notifications'; +import logger from '@server/logger'; +import { truncate } from 'lodash'; +import type { EntitySubscriberInterface, UpdateEvent } from 'typeorm'; +import { EventSubscriber, In, Not } from 'typeorm'; @EventSubscriber() export class MediaSubscriber implements EntitySubscriberInterface { @@ -29,7 +29,9 @@ export class MediaSubscriber implements EntitySubscriberInterface { const requestRepository = getRepository(MediaRequest); const relatedRequests = await requestRepository.find({ where: { - media: entity, + media: { + id: entity.id, + }, is4k, status: Not(MediaRequestStatus.DECLINED), }, @@ -47,6 +49,7 @@ export class MediaSubscriber implements EntitySubscriberInterface { { event: `${is4k ? '4K ' : ''}Movie Request Now Available`, notifyAdmin: false, + notifySystem: true, notifyUser: request.requestedBy, subject: `${movie.title}${ movie.release_date @@ -89,7 +92,7 @@ export class MediaSubscriber implements EntitySubscriberInterface { ) .map((season) => season.seasonNumber); const oldSeasonIds = dbEntity.seasons.map((season) => season.id); - const oldSeasons = await seasonRepository.findByIds(oldSeasonIds); + const oldSeasons = await seasonRepository.findBy({ id: In(oldSeasonIds) }); const oldAvailableSeasons = oldSeasons .filter( (season) => @@ -109,7 +112,9 @@ export class MediaSubscriber implements EntitySubscriberInterface { for (const changedSeasonNumber of changedSeasons) { const requests = await requestRepository.find({ where: { - media: entity, + media: { + id: entity.id, + }, is4k, status: Not(MediaRequestStatus.DECLINED), }, @@ -143,6 +148,7 @@ export class MediaSubscriber implements EntitySubscriberInterface { omission: '…', }), notifyAdmin: false, + notifySystem: true, notifyUser: request.requestedBy, image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`, media: entity, @@ -172,7 +178,7 @@ export class MediaSubscriber implements EntitySubscriberInterface { const requestRepository = getRepository(MediaRequest); const requests = await requestRepository.find({ - where: { media: event.id }, + where: { media: { id: event.id } }, }); for (const request of requests) { diff --git a/server/tsconfig.json b/server/tsconfig.json index d245100d9..ec4b9004d 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -4,7 +4,11 @@ "target": "ES2020", "module": "commonjs", "outDir": "../dist", - "noEmit": false + "noEmit": false, + "baseUrl": ".", + "paths": { + "@server/*": ["*"] + } }, "include": ["**/*.ts"] } diff --git a/server/types/express.d.ts b/server/types/express.d.ts index ee7fd9724..7b82477ad 100644 --- a/server/types/express.d.ts +++ b/server/types/express.d.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ +import type { User } from '@server/entity/User'; import type { NextFunction, Request, Response } from 'express'; -import type { User } from '../entity/User'; declare global { namespace Express { diff --git a/server/utils/appVersion.ts b/server/utils/appVersion.ts index 923d47089..d01a08a97 100644 --- a/server/utils/appVersion.ts +++ b/server/utils/appVersion.ts @@ -1,6 +1,6 @@ +import logger from '@server/logger'; import { existsSync } from 'fs'; import path from 'path'; -import logger from '../logger'; const COMMIT_TAG_PATH = path.join(__dirname, '../../committag.json'); let commitTag = 'local'; diff --git a/server/utils/dateHelpers.ts b/server/utils/dateHelpers.ts new file mode 100644 index 000000000..4684d7835 --- /dev/null +++ b/server/utils/dateHelpers.ts @@ -0,0 +1,4 @@ +import { addYears } from 'date-fns'; +import { Between } from 'typeorm'; + +export const AfterDate = (date: Date) => Between(date, addYears(date, 100)); diff --git a/server/utils/restartFlag.ts b/server/utils/restartFlag.ts new file mode 100644 index 000000000..387ec5ce4 --- /dev/null +++ b/server/utils/restartFlag.ts @@ -0,0 +1,23 @@ +import type { MainSettings } from '@server/lib/settings'; +import { getSettings } from '@server/lib/settings'; + +class RestartFlag { + private settings: MainSettings; + + public initializeSettings(settings: MainSettings): void { + this.settings = { ...settings }; + } + + public isSet(): boolean { + const settings = getSettings().main; + + return ( + this.settings.csrfProtection !== settings.csrfProtection || + this.settings.trustProxy !== settings.trustProxy + ); + } +} + +const restartFlag = new RestartFlag(); + +export default restartFlag; diff --git a/server/utils/typeHelpers.ts b/server/utils/typeHelpers.ts index 04070244b..507ece8cd 100644 --- a/server/utils/typeHelpers.ts +++ b/server/utils/typeHelpers.ts @@ -5,7 +5,7 @@ import type { TmdbPersonResult, TmdbTvDetails, TmdbTvResult, -} from '../api/themoviedb/interfaces'; +} from '@server/api/themoviedb/interfaces'; export const isMovie = ( movie: TmdbMovieResult | TmdbTvResult | TmdbPersonResult diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 0a7099ccf..3b693643a 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -8,10 +8,15 @@ description: > base: core18 confinement: strict +architectures: + - build-on: amd64 + - build-on: arm64 + - build-on: armhf + parts: overseerr: plugin: nodejs - nodejs-version: '16.14.0' + nodejs-version: '16.17.0' nodejs-package-manager: 'yarn' nodejs-yarn-version: v1.22.17 build-packages: @@ -31,13 +36,16 @@ parts: override-pull: | snapcraftctl pull # Get information to determine snap grade and version + git config --global --add safe.directory /data/parts/overseerr/src + #setup yarn.rc + echo "--install.frozen-lockfile\n--install.network-timeout 1000000" > .yarnrc BRANCH=$(git rev-parse --abbrev-ref HEAD) COMMIT=$(git rev-parse HEAD) COMMIT_SHORT=$(git rev-parse --short HEAD) VERSION='v'$(cat package.json | grep 'version' | head -1 | sed 's/.*"\(.*\)"\,/\1/') if [ "$VERSION" = "v0.1.0" ]; then SNAP_VERSION=$COMMIT_SHORT - GRADE=devel + GRADE=stable else SNAP_VERSION=$VERSION GRADE=stable @@ -57,6 +65,7 @@ parts: snapcraftctl set-grade "$GRADE" build-environment: - PATH: '$SNAPCRAFT_PART_BUILD/node_modules/.bin:$SNAPCRAFT_PART_BUILD/../npm/bin:$PATH' + - CYPRESS_INSTALL_BINARY: '0' override-build: | set -e # Set COMMIT_TAG before the build begins @@ -77,7 +86,7 @@ parts: prime: [.next, ./*] apps: - deamon: + daemon: command: /bin/sh -c "cd $SNAP && node dist/index.js" daemon: simple restart-condition: on-failure diff --git a/src/assets/infinity.svg b/src/assets/infinity.svg new file mode 100644 index 000000000..054149f8e --- /dev/null +++ b/src/assets/infinity.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/AirDateBadge/index.tsx b/src/components/AirDateBadge/index.tsx new file mode 100644 index 000000000..fb9268f6c --- /dev/null +++ b/src/components/AirDateBadge/index.tsx @@ -0,0 +1,62 @@ +import Badge from '@app/components/Common/Badge'; +import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl'; + +const messages = defineMessages({ + airedrelative: 'Aired {relativeTime}', + airsrelative: 'Airing {relativeTime}', +}); + +type AirDateBadgeProps = { + airDate: string; +}; + +const AirDateBadge = ({ airDate }: AirDateBadgeProps) => { + const WEEK = 1000 * 60 * 60 * 24 * 8; + const intl = useIntl(); + const dAirDate = new Date(airDate); + const nowDate = new Date(); + const alreadyAired = dAirDate.getTime() < nowDate.getTime(); + + const compareWeek = new Date( + alreadyAired ? Date.now() - WEEK : Date.now() + WEEK + ); + + let showRelative = false; + + if ( + (alreadyAired && dAirDate.getTime() > compareWeek.getTime()) || + (!alreadyAired && dAirDate.getTime() < compareWeek.getTime()) + ) { + showRelative = true; + } + + return ( +
+ + {intl.formatDate(dAirDate, { + year: 'numeric', + month: 'long', + day: 'numeric', + })} + + {showRelative && ( + + {intl.formatMessage( + alreadyAired ? messages.airedrelative : messages.airsrelative, + { + relativeTime: ( + + ), + } + )} + + )} +
+ ); +}; + +export default AirDateBadge; diff --git a/src/components/AppDataWarning/index.tsx b/src/components/AppDataWarning/index.tsx index fce97bd53..21c3dbaef 100644 --- a/src/components/AppDataWarning/index.tsx +++ b/src/components/AppDataWarning/index.tsx @@ -1,14 +1,13 @@ -import React from 'react'; +import Alert from '@app/components/Common/Alert'; import { defineMessages, useIntl } from 'react-intl'; import useSWR from 'swr'; -import Alert from '../Common/Alert'; const messages = defineMessages({ dockerVolumeMissingDescription: 'The {appDataPath} volume mount was not configured properly. All data will be cleared when the container is stopped or restarted.', }); -const AppDataWarning: React.FC = () => { +const AppDataWarning = () => { const intl = useIntl(); const { data, error } = useSWR<{ appData: boolean; appDataPath: string }>( '/api/v1/status/appdata' @@ -27,9 +26,9 @@ const AppDataWarning: React.FC = () => { {!data.appData && ( {msg}; - }, + code: (msg: React.ReactNode) => ( + {msg} + ), appDataPath: data.appDataPath, })} /> diff --git a/src/components/CollectionDetails/index.tsx b/src/components/CollectionDetails/index.tsx index 839f019ad..52bd8a269 100644 --- a/src/components/CollectionDetails/index.tsx +++ b/src/components/CollectionDetails/index.tsx @@ -1,24 +1,24 @@ +import ButtonWithDropdown from '@app/components/Common/ButtonWithDropdown'; +import CachedImage from '@app/components/Common/CachedImage'; +import LoadingSpinner from '@app/components/Common/LoadingSpinner'; +import PageTitle from '@app/components/Common/PageTitle'; +import RequestModal from '@app/components/RequestModal'; +import Slider from '@app/components/Slider'; +import StatusBadge from '@app/components/StatusBadge'; +import TitleCard from '@app/components/TitleCard'; +import useSettings from '@app/hooks/useSettings'; +import { Permission, useUser } from '@app/hooks/useUser'; +import globalMessages from '@app/i18n/globalMessages'; +import Error from '@app/pages/_error'; import { DownloadIcon } from '@heroicons/react/outline'; +import { MediaStatus } from '@server/constants/media'; +import type { Collection } from '@server/models/Collection'; import { uniq } from 'lodash'; import Link from 'next/link'; import { useRouter } from 'next/router'; -import React, { useState } from 'react'; +import { useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import useSWR from 'swr'; -import { MediaStatus } from '../../../server/constants/media'; -import type { Collection } from '../../../server/models/Collection'; -import useSettings from '../../hooks/useSettings'; -import { Permission, useUser } from '../../hooks/useUser'; -import globalMessages from '../../i18n/globalMessages'; -import Error from '../../pages/_error'; -import ButtonWithDropdown from '../Common/ButtonWithDropdown'; -import CachedImage from '../Common/CachedImage'; -import LoadingSpinner from '../Common/LoadingSpinner'; -import PageTitle from '../Common/PageTitle'; -import RequestModal from '../RequestModal'; -import Slider from '../Slider'; -import StatusBadge from '../StatusBadge'; -import TitleCard from '../TitleCard'; const messages = defineMessages({ overview: 'Overview', @@ -31,9 +31,7 @@ interface CollectionDetailsProps { collection?: Collection; } -const CollectionDetails: React.FC = ({ - collection, -}) => { +const CollectionDetails = ({ collection }: CollectionDetailsProps) => { const intl = useIntl(); const router = useRouter(); const settings = useSettings(); diff --git a/src/components/Common/Accordion/index.tsx b/src/components/Common/Accordion/index.tsx index 67e883fe0..49187bd03 100644 --- a/src/components/Common/Accordion/index.tsx +++ b/src/components/Common/Accordion/index.tsx @@ -1,9 +1,9 @@ -import * as React from 'react'; +import type * as React from 'react'; import { useState } from 'react'; import AnimateHeight from 'react-animate-height'; export interface AccordionProps { - children: (args: AccordionChildProps) => React.ReactElement | null; + children: (args: AccordionChildProps) => JSX.Element; /** If true, only one accordion item can be open at any time */ single?: boolean; /** If true, at least one accordion item will always be open */ @@ -13,22 +13,27 @@ export interface AccordionProps { export interface AccordionChildProps { openIndexes: number[]; handleClick(index: number): void; - AccordionContent: any; + AccordionContent: typeof AccordionContent; } -export const AccordionContent: React.FC<{ isOpen: boolean }> = ({ +type AccordionContentProps = { + isOpen: boolean; + children: React.ReactNode; +}; + +export const AccordionContent = ({ isOpen, children, -}) => { +}: AccordionContentProps) => { return {children}; }; -const Accordion: React.FC = ({ +const Accordion = ({ single, atLeastOne, initialOpenIndexes, children, -}) => { +}: AccordionProps) => { const initialState = initialOpenIndexes || (atLeastOne && [0]) || []; const [openIndexes, setOpenIndexes] = useState(initialState); diff --git a/src/components/Common/Alert/index.tsx b/src/components/Common/Alert/index.tsx index e9789c706..8ffb4a255 100644 --- a/src/components/Common/Alert/index.tsx +++ b/src/components/Common/Alert/index.tsx @@ -3,16 +3,17 @@ import { InformationCircleIcon, XCircleIcon, } from '@heroicons/react/solid'; -import React from 'react'; interface AlertProps { title?: React.ReactNode; type?: 'warning' | 'info' | 'error'; + children?: React.ReactNode; } -const Alert: React.FC = ({ title, children, type }) => { +const Alert = ({ title, children, type }: AlertProps) => { let design = { - bgColor: 'bg-yellow-600', + bgColor: + 'border border-yellow-500 backdrop-blur bg-yellow-400 bg-opacity-20', titleColor: 'text-yellow-100', textColor: 'text-yellow-300', svg: , @@ -21,9 +22,10 @@ const Alert: React.FC = ({ title, children, type }) => { switch (type) { case 'info': design = { - bgColor: 'bg-indigo-600', - titleColor: 'text-indigo-100', - textColor: 'text-indigo-300', + bgColor: + 'border border-indigo-500 backdrop-blur bg-indigo-400 bg-opacity-20', + titleColor: 'text-gray-100', + textColor: 'text-gray-300', svg: , }; break; diff --git a/src/components/Common/Badge/index.tsx b/src/components/Common/Badge/index.tsx index 33e55ab72..47ce6586c 100644 --- a/src/components/Common/Badge/index.tsx +++ b/src/components/Common/Badge/index.tsx @@ -2,17 +2,23 @@ import Link from 'next/link'; import React from 'react'; interface BadgeProps { - badgeType?: 'default' | 'primary' | 'danger' | 'warning' | 'success'; + badgeType?: + | 'default' + | 'primary' + | 'danger' + | 'warning' + | 'success' + | 'dark' + | 'light'; className?: string; href?: string; + children: React.ReactNode; } -const Badge: React.FC = ({ - badgeType = 'default', - className, - href, - children, -}) => { +const Badge = ( + { badgeType = 'default', className, href, children }: BadgeProps, + ref?: React.Ref +) => { const badgeStyle = [ 'px-2 inline-flex text-xs leading-5 font-semibold rounded-full whitespace-nowrap', ]; @@ -25,27 +31,47 @@ const Badge: React.FC = ({ switch (badgeType) { case 'danger': - badgeStyle.push('bg-red-600 !text-red-100'); + badgeStyle.push( + 'bg-red-600 bg-opacity-80 border-red-500 border !text-red-100' + ); if (href) { - badgeStyle.push('hover:bg-red-500'); + badgeStyle.push('hover:bg-red-500 bg-opacity-100'); } break; case 'warning': - badgeStyle.push('bg-yellow-500 !text-yellow-100'); + badgeStyle.push( + 'bg-yellow-500 bg-opacity-80 border-yellow-500 border !text-yellow-100' + ); if (href) { - badgeStyle.push('hover:bg-yellow-400'); + badgeStyle.push('hover:bg-yellow-500 hover:bg-opacity-100'); } break; case 'success': - badgeStyle.push('bg-green-500 !text-green-100'); + badgeStyle.push( + 'bg-green-500 bg-opacity-80 border border-green-500 !text-green-100' + ); if (href) { - badgeStyle.push('hover:bg-green-400'); + badgeStyle.push('hover:bg-green-500 hover:bg-opacity-100'); + } + break; + case 'dark': + badgeStyle.push('bg-gray-900 !text-gray-400'); + if (href) { + badgeStyle.push('hover:bg-gray-800'); + } + break; + case 'light': + badgeStyle.push('bg-gray-700 !text-gray-300'); + if (href) { + badgeStyle.push('hover:bg-gray-600'); } break; default: - badgeStyle.push('bg-indigo-500 !text-indigo-100'); + badgeStyle.push( + 'bg-indigo-500 bg-opacity-80 border border-indigo-500 !text-indigo-100' + ); if (href) { - badgeStyle.push('hover:bg-indigo-400'); + badgeStyle.push('hover:bg-indigo-500 bg-opacity-100'); } } @@ -60,6 +86,7 @@ const Badge: React.FC = ({ target="_blank" rel="noopener noreferrer" className={badgeStyle.join(' ')} + ref={ref as React.Ref} > {children} @@ -67,12 +94,24 @@ const Badge: React.FC = ({ } else if (href) { return ( - {children} + } + > + {children} + ); } else { - return {children}; + return ( + } + > + {children} + + ); } }; -export default Badge; +export default React.forwardRef(Badge) as typeof Badge; diff --git a/src/components/Common/Button/index.tsx b/src/components/Common/Button/index.tsx index f1083e5b2..d3f96ae98 100644 --- a/src/components/Common/Button/index.tsx +++ b/src/components/Common/Button/index.tsx @@ -1,4 +1,5 @@ -import React, { ForwardedRef } from 'react'; +import type { ForwardedRef } from 'react'; +import React from 'react'; export type ButtonType = | 'default' @@ -50,22 +51,22 @@ function Button

( switch (buttonType) { case 'primary': buttonStyle.push( - 'text-white bg-indigo-600 border-indigo-600 hover:bg-indigo-500 hover:border-indigo-500 focus:border-indigo-700 focus:ring-indigo active:bg-indigo-700 active:border-indigo-700' + 'text-white border border-indigo-500 bg-indigo-600 bg-opacity-80 hover:bg-opacity-100 hover:border-indigo-500 focus:border-indigo-700 focus:ring-indigo active:bg-opacity-100 active:border-indigo-700' ); break; case 'danger': buttonStyle.push( - 'text-white bg-red-600 border-red-600 hover:bg-red-500 hover:border-red-500 focus:border-red-700 focus:ring-red active:bg-red-700 active:border-red-700' + 'text-white bg-red-600 bg-opacity-80 border-red-500 hover:bg-opacity-100 hover:border-red-500 focus:border-red-700 focus:ring-red active:bg-red-700 active:border-red-700' ); break; case 'warning': buttonStyle.push( - 'text-white bg-yellow-500 border-yellow-500 hover:bg-yellow-400 hover:border-yellow-400 focus:border-yellow-700 focus:ring-yellow active:bg-yellow-700 active:border-yellow-700' + 'text-white border border-yellow-500 backdrop-blur bg-yellow-500 bg-opacity-80 hover:bg-opacity-100 hover:border-yellow-400 focus:border-yellow-700 focus:ring-yellow active:bg-opacity-100 active:border-yellow-700' ); break; case 'success': buttonStyle.push( - 'text-white bg-green-500 border-green-500 hover:bg-green-400 hover:border-green-400 focus:border-green-700 focus:ring-green active:bg-green-700 active:border-green-700' + 'text-white bg-green-500 bg-opacity-80 border-green-500 hover:bg-opacity-100 hover:border-green-400 focus:border-green-700 focus:ring-green active:bg-opacity-100 active:border-green-700' ); break; case 'ghost': @@ -75,7 +76,7 @@ function Button

( break; default: buttonStyle.push( - 'text-gray-200 bg-gray-600 border-gray-600 hover:text-white hover:bg-gray-500 hover:border-gray-500 group-hover:text-white group-hover:bg-gray-500 group-hover:border-gray-500 focus:border-blue-300 focus:ring-blue active:text-gray-200 active:bg-gray-500 active:border-gray-500' + 'text-gray-200 bg-gray-800 bg-opacity-80 border-gray-600 hover:text-white hover:bg-gray-700 hover:border-gray-600 group-hover:text-white group-hover:bg-gray-700 group-hover:border-gray-600 focus:border-blue-300 focus:ring-blue active:text-gray-200 active:bg-gray-700 active:border-gray-600' ); } diff --git a/src/components/Common/ButtonWithDropdown/index.tsx b/src/components/Common/ButtonWithDropdown/index.tsx index 6edb4a11f..be6815b94 100644 --- a/src/components/Common/ButtonWithDropdown/index.tsx +++ b/src/components/Common/ButtonWithDropdown/index.tsx @@ -1,34 +1,29 @@ +import useClickOutside from '@app/hooks/useClickOutside'; +import { withProperties } from '@app/utils/typeHelpers'; +import { Transition } from '@headlessui/react'; import { ChevronDownIcon } from '@heroicons/react/solid'; -import React, { - AnchorHTMLAttributes, - ButtonHTMLAttributes, - ReactNode, - useRef, - useState, -} from 'react'; -import useClickOutside from '../../../hooks/useClickOutside'; -import { withProperties } from '../../../utils/typeHelpers'; -import Transition from '../../Transition'; +import type { AnchorHTMLAttributes, ButtonHTMLAttributes } from 'react'; +import { Fragment, useRef, useState } from 'react'; interface DropdownItemProps extends AnchorHTMLAttributes { buttonType?: 'primary' | 'ghost'; } -const DropdownItem: React.FC = ({ +const DropdownItem = ({ children, buttonType = 'primary', ...props -}) => { +}: DropdownItemProps) => { let styleClass = 'button-md text-white'; switch (buttonType) { case 'ghost': styleClass += - ' bg-gray-700 hover:bg-gray-600 focus:border-gray-500 focus:text-white'; + ' bg-transparent rounded hover:bg-gradient-to-br from-indigo-600 to-purple-600 text-white focus:border-gray-500 focus:text-white'; break; default: styleClass += - ' bg-indigo-600 hover:bg-indigo-500 focus:border-indigo-700 focus:text-white'; + ' bg-indigo-600 rounded hover:bg-indigo-500 focus:border-indigo-700 focus:text-white'; } return ( = ({ interface ButtonWithDropdownProps extends ButtonHTMLAttributes { - text: ReactNode; - dropdownIcon?: ReactNode; + text: React.ReactNode; + dropdownIcon?: React.ReactNode; buttonType?: 'primary' | 'ghost'; } -const ButtonWithDropdown: React.FC = ({ +const ButtonWithDropdown = ({ text, children, dropdownIcon, className, buttonType = 'primary', ...props -}) => { +}: ButtonWithDropdownProps) => { const [isOpen, setIsOpen] = useState(false); const buttonRef = useRef(null); useClickOutside(buttonRef, () => setIsOpen(false)); @@ -70,14 +65,15 @@ const ButtonWithDropdown: React.FC = ({ styleClasses.mainButtonClasses += ' bg-transparent border-gray-600 hover:border-gray-200 focus:border-gray-100 active:border-gray-100'; styleClasses.dropdownSideButtonClasses = styleClasses.mainButtonClasses; - styleClasses.dropdownClasses += ' bg-gray-700'; + styleClasses.dropdownClasses += + ' bg-gray-800 border border-gray-700 bg-opacity-80 p-1 backdrop-blur'; break; default: styleClasses.mainButtonClasses += - ' bg-indigo-600 border-indigo-600 hover:bg-indigo-500 hover:border-indigo-500 active:bg-indigo-700 active:border-indigo-700 focus:ring-blue'; + ' bg-indigo-600 border-indigo-500 bg-opacity-80 hover:bg-opacity-100 hover:border-indigo-500 active:bg-indigo-700 active:border-indigo-700 focus:ring-blue'; styleClasses.dropdownSideButtonClasses += - ' bg-indigo-700 border-indigo-600 hover:bg-indigo-500 active:bg-indigo-700 focus:ring-blue'; - styleClasses.dropdownClasses += ' bg-indigo-600'; + ' bg-indigo-600 bg-opacity-80 border-indigo-500 hover:bg-opacity-100 active:bg-opacity-100 focus:ring-blue'; + styleClasses.dropdownClasses += ' bg-indigo-600 p-1'; } return ( @@ -103,6 +99,7 @@ const ButtonWithDropdown: React.FC = ({ {dropdownIcon ? dropdownIcon : } = (props) => { +const CachedImage = (props: ImageProps) => { const { currentSettings } = useSettings(); return ; diff --git a/src/components/Common/ConfirmButton/index.tsx b/src/components/Common/ConfirmButton/index.tsx index df3c6572a..1f5756cb9 100644 --- a/src/components/Common/ConfirmButton/index.tsx +++ b/src/components/Common/ConfirmButton/index.tsx @@ -1,19 +1,20 @@ -import React, { useRef, useState } from 'react'; -import useClickOutside from '../../../hooks/useClickOutside'; -import Button from '../Button'; +import Button from '@app/components/Common/Button'; +import useClickOutside from '@app/hooks/useClickOutside'; +import { useRef, useState } from 'react'; interface ConfirmButtonProps { onClick: () => void; confirmText: React.ReactNode; className?: string; + children: React.ReactNode; } -const ConfirmButton: React.FC = ({ +const ConfirmButton = ({ onClick, children, confirmText, className, -}) => { +}: ConfirmButtonProps) => { const ref = useRef(null); useClickOutside(ref, () => setIsClicked(false)); const [isClicked, setIsClicked] = useState(false); diff --git a/src/components/Common/Header/index.tsx b/src/components/Common/Header/index.tsx index b7c88ddd9..1653a457d 100644 --- a/src/components/Common/Header/index.tsx +++ b/src/components/Common/Header/index.tsx @@ -1,22 +1,18 @@ -import React from 'react'; - interface HeaderProps { extraMargin?: number; subtext?: React.ReactNode; + children: React.ReactNode; } -const Header: React.FC = ({ - children, - extraMargin = 0, - subtext, -}) => { +const Header = ({ children, extraMargin = 0, subtext }: HeaderProps) => { return (

-

- - {children} - +

+ {children}

{subtext &&
{subtext}
}
diff --git a/src/components/Common/ImageFader/index.tsx b/src/components/Common/ImageFader/index.tsx index 5f68376c0..a57172414 100644 --- a/src/components/Common/ImageFader/index.tsx +++ b/src/components/Common/ImageFader/index.tsx @@ -1,10 +1,6 @@ -import React, { - ForwardRefRenderFunction, - HTMLAttributes, - useEffect, - useState, -} from 'react'; -import CachedImage from '../CachedImage'; +import CachedImage from '@app/components/Common/CachedImage'; +import type { ForwardRefRenderFunction, HTMLAttributes } from 'react'; +import React, { useEffect, useState } from 'react'; interface ImageFaderProps extends HTMLAttributes { backgroundImages: string[]; diff --git a/src/components/Common/List/index.tsx b/src/components/Common/List/index.tsx index a4b917235..32057ed1a 100644 --- a/src/components/Common/List/index.tsx +++ b/src/components/Common/List/index.tsx @@ -1,12 +1,12 @@ -import React from 'react'; -import { withProperties } from '../../../utils/typeHelpers'; +import { withProperties } from '@app/utils/typeHelpers'; interface ListItemProps { title: string; className?: string; + children: React.ReactNode; } -const ListItem: React.FC = ({ title, className, children }) => { +const ListItem = ({ title, className, children }: ListItemProps) => { return (
@@ -22,9 +22,10 @@ const ListItem: React.FC = ({ title, className, children }) => { interface ListProps { title: string; subTitle?: string; + children: React.ReactNode; } -const List: React.FC = ({ title, subTitle, children }) => { +const List = ({ title, subTitle, children }: ListProps) => { return ( <>
diff --git a/src/components/Common/ListView/index.tsx b/src/components/Common/ListView/index.tsx index 0c2a0e4ed..6f09f768b 100644 --- a/src/components/Common/ListView/index.tsx +++ b/src/components/Common/ListView/index.tsx @@ -1,30 +1,33 @@ -import React from 'react'; -import { useIntl } from 'react-intl'; -import { +import PersonCard from '@app/components/PersonCard'; +import TitleCard from '@app/components/TitleCard'; +import TmdbTitleCard from '@app/components/TitleCard/TmdbTitleCard'; +import useVerticalScroll from '@app/hooks/useVerticalScroll'; +import globalMessages from '@app/i18n/globalMessages'; +import type { WatchlistItem } from '@server/interfaces/api/discoverInterfaces'; +import type { MovieResult, PersonResult, TvResult, -} from '../../../../server/models/Search'; -import useVerticalScroll from '../../../hooks/useVerticalScroll'; -import globalMessages from '../../../i18n/globalMessages'; -import PersonCard from '../../PersonCard'; -import TitleCard from '../../TitleCard'; +} from '@server/models/Search'; +import { useIntl } from 'react-intl'; -interface ListViewProps { +type ListViewProps = { items?: (TvResult | MovieResult | PersonResult)[]; + plexItems?: WatchlistItem[]; isEmpty?: boolean; isLoading?: boolean; isReachingEnd?: boolean; onScrollBottom: () => void; -} +}; -const ListView: React.FC = ({ +const ListView = ({ items, isEmpty, isLoading, onScrollBottom, isReachingEnd, -}) => { + plexItems, +}: ListViewProps) => { const intl = useIntl(); useVerticalScroll(onScrollBottom, !isLoading && !isEmpty && !isReachingEnd); return ( @@ -35,6 +38,18 @@ const ListView: React.FC = ({
)}
    + {plexItems?.map((title, index) => { + return ( +
  • + +
  • + ); + })} {items?.map((title, index) => { let titleCard: React.ReactNode; diff --git a/src/components/Common/LoadingSpinner/index.tsx b/src/components/Common/LoadingSpinner/index.tsx index be65f0094..8f922ef38 100644 --- a/src/components/Common/LoadingSpinner/index.tsx +++ b/src/components/Common/LoadingSpinner/index.tsx @@ -1,6 +1,4 @@ -import React from 'react'; - -export const SmallLoadingSpinner: React.FC = () => { +export const SmallLoadingSpinner = () => { return (
    { ); }; -const LoadingSpinner: React.FC = () => { +const LoadingSpinner = () => { return (
    ) => void; onOk?: (e?: MouseEvent) => void; onSecondary?: (e?: MouseEvent) => void; @@ -28,87 +31,94 @@ interface ModalProps { tertiaryButtonType?: ButtonType; disableScrollLock?: boolean; backgroundClickable?: boolean; - iconSvg?: ReactNode; loading?: boolean; backdrop?: string; + children?: React.ReactNode; } -const Modal: React.FC = ({ - title, - onCancel, - onOk, - cancelText, - okText, - okDisabled = false, - cancelButtonType = 'default', - okButtonType = 'primary', - children, - disableScrollLock, - backgroundClickable = true, - iconSvg, - loading = false, - secondaryButtonType = 'default', - secondaryDisabled = false, - onSecondary, - secondaryText, - tertiaryButtonType = 'default', - tertiaryDisabled = false, - tertiaryText, - onTertiary, - backdrop, -}) => { - const intl = useIntl(); - const modalRef = useRef(null); - useClickOutside(modalRef, () => { - typeof onCancel === 'function' && backgroundClickable - ? onCancel() - : undefined; - }); - useLockBodyScroll(true, disableScrollLock); +const Modal = React.forwardRef( + ( + { + title, + subTitle, + onCancel, + onOk, + cancelText, + okText, + okDisabled = false, + cancelButtonType = 'default', + okButtonType = 'primary', + children, + disableScrollLock, + backgroundClickable = true, + secondaryButtonType = 'default', + secondaryDisabled = false, + onSecondary, + secondaryText, + tertiaryButtonType = 'default', + tertiaryDisabled = false, + tertiaryText, + loading = false, + onTertiary, + backdrop, + }, + parentRef + ) => { + const intl = useIntl(); + const modalRef = useRef(null); + useClickOutside(modalRef, () => { + if (onCancel && backgroundClickable) { + onCancel(); + } + }); + useLockBodyScroll(true, disableScrollLock); - return ReactDOM.createPortal( - // eslint-disable-next-line jsx-a11y/no-static-element-interactions -
    { - if (e.key === 'Escape') { - typeof onCancel === 'function' && backgroundClickable - ? onCancel() - : undefined; - } - }} - > - -
    - -
    -
    - -
    +
    + +
    + + {backdrop && (
    @@ -123,30 +133,45 @@ const Modal: React.FC = ({ className="absolute inset-0" style={{ backgroundImage: - 'linear-gradient(180deg, rgba(55, 65, 81, 0.85) 0%, rgba(55, 65, 81, 1) 100%)', + 'linear-gradient(180deg, rgba(31, 41, 55, 0.75) 0%, rgba(31, 41, 55, 1) 100%)', }} />
    )} -
    - {iconSvg &&
    {iconSvg}
    } +
    - {title && ( - - {title} - + {(title || subTitle) && ( +
    + {title && ( + + {title} + + )} + {subTitle && ( + + {subTitle} + + )} +
    )}
    {children && ( -
    +
    {children}
    )} @@ -158,6 +183,7 @@ const Modal: React.FC = ({ onClick={onOk} className="ml-3" disabled={okDisabled} + data-testid="modal-ok-button" > {okText ? okText : 'Ok'} @@ -168,6 +194,7 @@ const Modal: React.FC = ({ onClick={onSecondary} className="ml-3" disabled={secondaryDisabled} + data-testid="modal-secondary-button" > {secondaryText} @@ -187,6 +214,7 @@ const Modal: React.FC = ({ buttonType={cancelButtonType} onClick={onCancel} className="ml-3 sm:ml-0" + data-testid="modal-cancel-button" > {cancelText ? cancelText @@ -195,11 +223,13 @@ const Modal: React.FC = ({ )}
    )} -
    -
    -
    , - document.body - ); -}; +
    + , + document.body + ); + } +); + +Modal.displayName = 'Modal'; export default Modal; diff --git a/src/components/Common/PageTitle/index.tsx b/src/components/Common/PageTitle/index.tsx index a7224b226..288a0b374 100644 --- a/src/components/Common/PageTitle/index.tsx +++ b/src/components/Common/PageTitle/index.tsx @@ -1,20 +1,20 @@ -import React from 'react'; -import useSettings from '../../../hooks/useSettings'; +import useSettings from '@app/hooks/useSettings'; import Head from 'next/head'; interface PageTitleProps { title: string | (string | undefined)[]; } -const PageTitle: React.FC = ({ title }) => { +const PageTitle = ({ title }: PageTitleProps) => { const settings = useSettings(); + const titleText = `${ + Array.isArray(title) ? title.filter(Boolean).join(' - ') : title + } - ${settings.currentSettings.applicationTitle}`; + return ( - - {Array.isArray(title) ? title.filter(Boolean).join(' - ') : title} -{' '} - {settings.currentSettings.applicationTitle} - + {titleText} ); }; diff --git a/src/components/Common/PlayButton/index.tsx b/src/components/Common/PlayButton/index.tsx index c41935aee..01d3a0121 100644 --- a/src/components/Common/PlayButton/index.tsx +++ b/src/components/Common/PlayButton/index.tsx @@ -1,5 +1,4 @@ -import React, { ReactNode } from 'react'; -import ButtonWithDropdown from '../ButtonWithDropdown'; +import ButtonWithDropdown from '@app/components/Common/ButtonWithDropdown'; interface PlayButtonProps { links: PlayButtonLink[]; @@ -8,10 +7,10 @@ interface PlayButtonProps { export interface PlayButtonLink { text: string; url: string; - svg: ReactNode; + svg: React.ReactNode; } -const PlayButton: React.FC = ({ links }) => { +const PlayButton = ({ links }: PlayButtonProps) => { if (!links || !links.length) { return null; } diff --git a/src/components/Common/ProgressCircle/index.tsx b/src/components/Common/ProgressCircle/index.tsx index 64ca49c17..7df2b041e 100644 --- a/src/components/Common/ProgressCircle/index.tsx +++ b/src/components/Common/ProgressCircle/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef } from 'react'; +import { useEffect, useRef } from 'react'; interface ProgressCircleProps { className?: string; @@ -6,11 +6,11 @@ interface ProgressCircleProps { useHeatLevel?: boolean; } -const ProgressCircle: React.FC = ({ +const ProgressCircle = ({ className, progress = 0, useHeatLevel, -}) => { +}: ProgressCircleProps) => { const ref = useRef(null); let color = ''; diff --git a/src/components/Common/SensitiveInput/index.tsx b/src/components/Common/SensitiveInput/index.tsx index 886fd7217..6652f5519 100644 --- a/src/components/Common/SensitiveInput/index.tsx +++ b/src/components/Common/SensitiveInput/index.tsx @@ -1,6 +1,6 @@ import { EyeIcon, EyeOffIcon } from '@heroicons/react/solid'; import { Field } from 'formik'; -import React, { useState } from 'react'; +import { useState } from 'react'; interface CustomInputProps extends React.ComponentProps<'input'> { as?: 'input'; @@ -12,10 +12,7 @@ interface CustomFieldProps extends React.ComponentProps { type SensitiveInputProps = CustomInputProps | CustomFieldProps; -const SensitiveInput: React.FC = ({ - as = 'input', - ...props -}) => { +const SensitiveInput = ({ as = 'input', ...props }: SensitiveInputProps) => { const [isHidden, setHidden] = useState(true); const Component = as === 'input' ? 'input' : Field; const componentProps = diff --git a/src/components/Common/SettingsTabs/index.tsx b/src/components/Common/SettingsTabs/index.tsx index 751587050..8ee46fea3 100644 --- a/src/components/Common/SettingsTabs/index.tsx +++ b/src/components/Common/SettingsTabs/index.tsx @@ -1,8 +1,8 @@ +import { useUser } from '@app/hooks/useUser'; +import type { Permission } from '@server/lib/permissions'; +import { hasPermission } from '@server/lib/permissions'; import Link from 'next/link'; import { useRouter } from 'next/router'; -import React from 'react'; -import { hasPermission, Permission } from '../../../../server/lib/permissions'; -import { useUser } from '../../../hooks/useUser'; export interface SettingsRoute { text: string; @@ -14,14 +14,17 @@ export interface SettingsRoute { hidden?: boolean; } -const SettingsLink: React.FC<{ +type SettingsLinkProps = { tabType: 'default' | 'button'; currentPath: string; route: string; regex: RegExp; hidden?: boolean; isMobile?: boolean; -}> = ({ + children: React.ReactNode; +}; + +const SettingsLink = ({ children, tabType, currentPath, @@ -29,7 +32,7 @@ const SettingsLink: React.FC<{ regex, hidden = false, isMobile = false, -}) => { +}: SettingsLinkProps) => { if (hidden) { return null; } @@ -65,10 +68,13 @@ const SettingsLink: React.FC<{ ); }; -const SettingsTabs: React.FC<{ +const SettingsTabs = ({ + tabType = 'default', + settingsRoutes, +}: { tabType?: 'default' | 'button'; settingsRoutes: SettingsRoute[]; -}> = ({ tabType = 'default', settingsRoutes }) => { +}) => { const router = useRouter(); const { user: currentUser } = useUser(); @@ -137,7 +143,7 @@ const SettingsTabs: React.FC<{
    ) : (
    -
    diff --git a/src/components/Common/Table/index.tsx b/src/components/Common/Table/index.tsx index 9e0cb0ca5..a286de69d 100644 --- a/src/components/Common/Table/index.tsx +++ b/src/components/Common/Table/index.tsx @@ -1,17 +1,20 @@ -import React, { AllHTMLAttributes } from 'react'; -import { withProperties } from '../../../utils/typeHelpers'; +import { withProperties } from '@app/utils/typeHelpers'; -const TBody: React.FC = ({ children }) => { +type TBodyProps = { + children: React.ReactNode; +}; + +const TBody = ({ children }: TBodyProps) => { return ( {children} ); }; -const TH: React.FC> = ({ +const TH = ({ children, className, ...props -}) => { +}: React.ComponentPropsWithoutRef<'th'>) => { const style = [ 'px-4 py-3 bg-gray-500 text-left text-xs leading-4 font-medium text-gray-200 uppercase tracking-wider truncate', ]; @@ -27,18 +30,18 @@ const TH: React.FC> = ({ ); }; -interface TDProps extends AllHTMLAttributes { +type TDProps = { alignText?: 'left' | 'center' | 'right'; noPadding?: boolean; -} +}; -const TD: React.FC = ({ +const TD = ({ children, alignText = 'left', noPadding, className, ...props -}) => { +}: TDProps & React.ComponentPropsWithoutRef<'td'>) => { const style = ['text-sm leading-5 text-white']; switch (alignText) { @@ -68,7 +71,11 @@ const TD: React.FC = ({ ); }; -const Table: React.FC = ({ children }) => { +type TableProps = { + children: React.ReactNode; +}; + +const Table = ({ children }: TableProps) => { return (
    diff --git a/src/components/Common/Tooltip/index.tsx b/src/components/Common/Tooltip/index.tsx new file mode 100644 index 000000000..b0c4fb2eb --- /dev/null +++ b/src/components/Common/Tooltip/index.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import type { Config } from 'react-popper-tooltip'; +import { usePopperTooltip } from 'react-popper-tooltip'; + +type TooltipProps = { + content: React.ReactNode; + children: React.ReactElement; + tooltipConfig?: Partial; +}; + +const Tooltip = ({ children, content, tooltipConfig }: TooltipProps) => { + const { getTooltipProps, setTooltipRef, setTriggerRef, visible } = + usePopperTooltip({ + followCursor: true, + offset: [-28, 6], + placement: 'auto-end', + ...tooltipConfig, + }); + + return ( + <> + {React.cloneElement(children, { ref: setTriggerRef })} + {visible && ( +
    + {content} +
    + )} + + ); +}; + +export default Tooltip; diff --git a/src/components/CompanyCard/index.tsx b/src/components/CompanyCard/index.tsx index b6383a77a..762d1a08b 100644 --- a/src/components/CompanyCard/index.tsx +++ b/src/components/CompanyCard/index.tsx @@ -1,5 +1,5 @@ import Link from 'next/link'; -import React, { useState } from 'react'; +import { useState } from 'react'; interface CompanyCardProps { name: string; @@ -7,7 +7,7 @@ interface CompanyCardProps { url: string; } -const CompanyCard: React.FC = ({ image, url, name }) => { +const CompanyCard = ({ image, url, name }: CompanyCardProps) => { const [isHovered, setHovered] = useState(false); return ( diff --git a/src/components/Discover/DiscoverMovieGenre/index.tsx b/src/components/Discover/DiscoverMovieGenre/index.tsx index e340f4eb9..d31921da6 100644 --- a/src/components/Discover/DiscoverMovieGenre/index.tsx +++ b/src/components/Discover/DiscoverMovieGenre/index.tsx @@ -1,19 +1,18 @@ -import React from 'react'; -import type { MovieResult } from '../../../../server/models/Search'; -import ListView from '../../Common/ListView'; -import { defineMessages, useIntl } from 'react-intl'; -import Header from '../../Common/Header'; -import PageTitle from '../../Common/PageTitle'; +import Header from '@app/components/Common/Header'; +import ListView from '@app/components/Common/ListView'; +import PageTitle from '@app/components/Common/PageTitle'; +import useDiscover from '@app/hooks/useDiscover'; +import globalMessages from '@app/i18n/globalMessages'; +import Error from '@app/pages/_error'; +import type { MovieResult } from '@server/models/Search'; import { useRouter } from 'next/router'; -import globalMessages from '../../../i18n/globalMessages'; -import useDiscover from '../../../hooks/useDiscover'; -import Error from '../../../pages/_error'; +import { defineMessages, useIntl } from 'react-intl'; const messages = defineMessages({ genreMovies: '{genre} Movies', }); -const DiscoverMovieGenre: React.FC = () => { +const DiscoverMovieGenre = () => { const router = useRouter(); const intl = useIntl(); diff --git a/src/components/Discover/DiscoverMovieLanguage/index.tsx b/src/components/Discover/DiscoverMovieLanguage/index.tsx index b1e19d055..e9a274fa8 100644 --- a/src/components/Discover/DiscoverMovieLanguage/index.tsx +++ b/src/components/Discover/DiscoverMovieLanguage/index.tsx @@ -1,19 +1,18 @@ -import React from 'react'; -import type { MovieResult } from '../../../../server/models/Search'; -import ListView from '../../Common/ListView'; -import { defineMessages, useIntl } from 'react-intl'; -import Header from '../../Common/Header'; -import PageTitle from '../../Common/PageTitle'; +import Header from '@app/components/Common/Header'; +import ListView from '@app/components/Common/ListView'; +import PageTitle from '@app/components/Common/PageTitle'; +import useDiscover from '@app/hooks/useDiscover'; +import globalMessages from '@app/i18n/globalMessages'; +import Error from '@app/pages/_error'; +import type { MovieResult } from '@server/models/Search'; import { useRouter } from 'next/router'; -import globalMessages from '../../../i18n/globalMessages'; -import useDiscover from '../../../hooks/useDiscover'; -import Error from '../../../pages/_error'; +import { defineMessages, useIntl } from 'react-intl'; const messages = defineMessages({ languageMovies: '{language} Movies', }); -const DiscoverMovieLanguage: React.FC = () => { +const DiscoverMovieLanguage = () => { const router = useRouter(); const intl = useIntl(); diff --git a/src/components/Discover/DiscoverMovies.tsx b/src/components/Discover/DiscoverMovies.tsx index cef4c6230..b9ec8dea8 100644 --- a/src/components/Discover/DiscoverMovies.tsx +++ b/src/components/Discover/DiscoverMovies.tsx @@ -1,17 +1,16 @@ -import React from 'react'; -import type { MovieResult } from '../../../server/models/Search'; -import ListView from '../Common/ListView'; +import Header from '@app/components/Common/Header'; +import ListView from '@app/components/Common/ListView'; +import PageTitle from '@app/components/Common/PageTitle'; +import useDiscover from '@app/hooks/useDiscover'; +import Error from '@app/pages/_error'; +import type { MovieResult } from '@server/models/Search'; import { defineMessages, useIntl } from 'react-intl'; -import Header from '../Common/Header'; -import PageTitle from '../Common/PageTitle'; -import useDiscover from '../../hooks/useDiscover'; -import Error from '../../pages/_error'; const messages = defineMessages({ discovermovies: 'Popular Movies', }); -const DiscoverMovies: React.FC = () => { +const DiscoverMovies = () => { const intl = useIntl(); const { diff --git a/src/components/Discover/DiscoverNetwork/index.tsx b/src/components/Discover/DiscoverNetwork/index.tsx index 247c5ece7..f09fef378 100644 --- a/src/components/Discover/DiscoverNetwork/index.tsx +++ b/src/components/Discover/DiscoverNetwork/index.tsx @@ -1,20 +1,19 @@ -import React from 'react'; -import type { TvResult } from '../../../../server/models/Search'; -import ListView from '../../Common/ListView'; -import { defineMessages, useIntl } from 'react-intl'; -import Header from '../../Common/Header'; -import PageTitle from '../../Common/PageTitle'; +import Header from '@app/components/Common/Header'; +import ListView from '@app/components/Common/ListView'; +import PageTitle from '@app/components/Common/PageTitle'; +import useDiscover from '@app/hooks/useDiscover'; +import globalMessages from '@app/i18n/globalMessages'; +import Error from '@app/pages/_error'; +import type { TvNetwork } from '@server/models/common'; +import type { TvResult } from '@server/models/Search'; import { useRouter } from 'next/router'; -import globalMessages from '../../../i18n/globalMessages'; -import useDiscover from '../../../hooks/useDiscover'; -import Error from '../../../pages/_error'; -import { TvNetwork } from '../../../../server/models/common'; +import { defineMessages, useIntl } from 'react-intl'; const messages = defineMessages({ networkSeries: '{network} Series', }); -const DiscoverTvNetwork: React.FC = () => { +const DiscoverTvNetwork = () => { const router = useRouter(); const intl = useIntl(); diff --git a/src/components/Discover/DiscoverStudio/index.tsx b/src/components/Discover/DiscoverStudio/index.tsx index b1f3b0662..1d78748ba 100644 --- a/src/components/Discover/DiscoverStudio/index.tsx +++ b/src/components/Discover/DiscoverStudio/index.tsx @@ -1,20 +1,19 @@ -import React from 'react'; -import type { MovieResult } from '../../../../server/models/Search'; -import ListView from '../../Common/ListView'; -import { defineMessages, useIntl } from 'react-intl'; -import Header from '../../Common/Header'; -import PageTitle from '../../Common/PageTitle'; +import Header from '@app/components/Common/Header'; +import ListView from '@app/components/Common/ListView'; +import PageTitle from '@app/components/Common/PageTitle'; +import useDiscover from '@app/hooks/useDiscover'; +import globalMessages from '@app/i18n/globalMessages'; +import Error from '@app/pages/_error'; +import type { ProductionCompany } from '@server/models/common'; +import type { MovieResult } from '@server/models/Search'; import { useRouter } from 'next/router'; -import globalMessages from '../../../i18n/globalMessages'; -import useDiscover from '../../../hooks/useDiscover'; -import Error from '../../../pages/_error'; -import { ProductionCompany } from '../../../../server/models/common'; +import { defineMessages, useIntl } from 'react-intl'; const messages = defineMessages({ studioMovies: '{studio} Movies', }); -const DiscoverMovieStudio: React.FC = () => { +const DiscoverMovieStudio = () => { const router = useRouter(); const intl = useIntl(); diff --git a/src/components/Discover/DiscoverTv.tsx b/src/components/Discover/DiscoverTv.tsx index 60c292258..404b1aa5b 100644 --- a/src/components/Discover/DiscoverTv.tsx +++ b/src/components/Discover/DiscoverTv.tsx @@ -1,17 +1,16 @@ -import React from 'react'; -import type { TvResult } from '../../../server/models/Search'; -import ListView from '../Common/ListView'; +import Header from '@app/components/Common/Header'; +import ListView from '@app/components/Common/ListView'; +import PageTitle from '@app/components/Common/PageTitle'; +import useDiscover from '@app/hooks/useDiscover'; +import Error from '@app/pages/_error'; +import type { TvResult } from '@server/models/Search'; import { defineMessages, useIntl } from 'react-intl'; -import Header from '../Common/Header'; -import PageTitle from '../Common/PageTitle'; -import useDiscover from '../../hooks/useDiscover'; -import Error from '../../pages/_error'; const messages = defineMessages({ discovertv: 'Popular Series', }); -const DiscoverTv: React.FC = () => { +const DiscoverTv = () => { const intl = useIntl(); const { diff --git a/src/components/Discover/DiscoverTvGenre/index.tsx b/src/components/Discover/DiscoverTvGenre/index.tsx index d4b672a5d..9602fbb8a 100644 --- a/src/components/Discover/DiscoverTvGenre/index.tsx +++ b/src/components/Discover/DiscoverTvGenre/index.tsx @@ -1,19 +1,18 @@ -import React from 'react'; -import type { TvResult } from '../../../../server/models/Search'; -import ListView from '../../Common/ListView'; -import { defineMessages, useIntl } from 'react-intl'; -import Header from '../../Common/Header'; -import PageTitle from '../../Common/PageTitle'; +import Header from '@app/components/Common/Header'; +import ListView from '@app/components/Common/ListView'; +import PageTitle from '@app/components/Common/PageTitle'; +import useDiscover from '@app/hooks/useDiscover'; +import globalMessages from '@app/i18n/globalMessages'; +import Error from '@app/pages/_error'; +import type { TvResult } from '@server/models/Search'; import { useRouter } from 'next/router'; -import globalMessages from '../../../i18n/globalMessages'; -import useDiscover from '../../../hooks/useDiscover'; -import Error from '../../../pages/_error'; +import { defineMessages, useIntl } from 'react-intl'; const messages = defineMessages({ genreSeries: '{genre} Series', }); -const DiscoverTvGenre: React.FC = () => { +const DiscoverTvGenre = () => { const router = useRouter(); const intl = useIntl(); diff --git a/src/components/Discover/DiscoverTvLanguage/index.tsx b/src/components/Discover/DiscoverTvLanguage/index.tsx index ed0873f90..b6c710e90 100644 --- a/src/components/Discover/DiscoverTvLanguage/index.tsx +++ b/src/components/Discover/DiscoverTvLanguage/index.tsx @@ -1,19 +1,18 @@ -import React from 'react'; -import type { TvResult } from '../../../../server/models/Search'; -import ListView from '../../Common/ListView'; -import { defineMessages, useIntl } from 'react-intl'; -import Header from '../../Common/Header'; -import PageTitle from '../../Common/PageTitle'; +import Header from '@app/components/Common/Header'; +import ListView from '@app/components/Common/ListView'; +import PageTitle from '@app/components/Common/PageTitle'; +import useDiscover from '@app/hooks/useDiscover'; +import globalMessages from '@app/i18n/globalMessages'; +import Error from '@app/pages/_error'; +import type { TvResult } from '@server/models/Search'; import { useRouter } from 'next/router'; -import globalMessages from '../../../i18n/globalMessages'; -import useDiscover from '../../../hooks/useDiscover'; -import Error from '../../../pages/_error'; +import { defineMessages, useIntl } from 'react-intl'; const messages = defineMessages({ languageSeries: '{language} Series', }); -const DiscoverTvLanguage: React.FC = () => { +const DiscoverTvLanguage = () => { const router = useRouter(); const intl = useIntl(); diff --git a/src/components/Discover/DiscoverTvUpcoming.tsx b/src/components/Discover/DiscoverTvUpcoming.tsx index 5b59f26a2..2a6939648 100644 --- a/src/components/Discover/DiscoverTvUpcoming.tsx +++ b/src/components/Discover/DiscoverTvUpcoming.tsx @@ -1,17 +1,16 @@ -import React from 'react'; -import type { TvResult } from '../../../server/models/Search'; -import ListView from '../Common/ListView'; +import Header from '@app/components/Common/Header'; +import ListView from '@app/components/Common/ListView'; +import PageTitle from '@app/components/Common/PageTitle'; +import useDiscover from '@app/hooks/useDiscover'; +import Error from '@app/pages/_error'; +import type { TvResult } from '@server/models/Search'; import { defineMessages, useIntl } from 'react-intl'; -import Header from '../Common/Header'; -import PageTitle from '../Common/PageTitle'; -import useDiscover from '../../hooks/useDiscover'; -import Error from '../../pages/_error'; const messages = defineMessages({ upcomingtv: 'Upcoming Series', }); -const DiscoverTvUpcoming: React.FC = () => { +const DiscoverTvUpcoming = () => { const intl = useIntl(); const { diff --git a/src/components/Discover/DiscoverWatchlist/index.tsx b/src/components/Discover/DiscoverWatchlist/index.tsx new file mode 100644 index 000000000..fbbdff014 --- /dev/null +++ b/src/components/Discover/DiscoverWatchlist/index.tsx @@ -0,0 +1,84 @@ +import Header from '@app/components/Common/Header'; +import ListView from '@app/components/Common/ListView'; +import PageTitle from '@app/components/Common/PageTitle'; +import useDiscover from '@app/hooks/useDiscover'; +import { useUser } from '@app/hooks/useUser'; +import Error from '@app/pages/_error'; +import type { WatchlistItem } from '@server/interfaces/api/discoverInterfaces'; +import Link from 'next/link'; +import { useRouter } from 'next/router'; +import { defineMessages, useIntl } from 'react-intl'; + +const messages = defineMessages({ + discoverwatchlist: 'Your Plex Watchlist', + watchlist: 'Plex Watchlist', +}); + +const DiscoverWatchlist = () => { + const intl = useIntl(); + const router = useRouter(); + const { user } = useUser({ + id: Number(router.query.userId), + }); + const { user: currentUser } = useUser(); + + const { + isLoadingInitialData, + isEmpty, + isLoadingMore, + isReachingEnd, + titles, + fetchMore, + error, + } = useDiscover( + `/api/v1/${ + router.pathname.startsWith('/profile') + ? `user/${currentUser?.id}` + : router.query.userId + ? `user/${router.query.userId}` + : 'discover' + }/watchlist` + ); + + if (error) { + return ; + } + + const title = intl.formatMessage( + router.query.userId ? messages.watchlist : messages.discoverwatchlist + ); + + return ( + <> + +
    + 0) + } + isReachingEnd={isReachingEnd} + onScrollBottom={fetchMore} + /> + + ); +}; + +export default DiscoverWatchlist; diff --git a/src/components/Discover/MovieGenreList/index.tsx b/src/components/Discover/MovieGenreList/index.tsx index bc85adad4..f19f5770f 100644 --- a/src/components/Discover/MovieGenreList/index.tsx +++ b/src/components/Discover/MovieGenreList/index.tsx @@ -1,19 +1,18 @@ -import React from 'react'; +import Header from '@app/components/Common/Header'; +import LoadingSpinner from '@app/components/Common/LoadingSpinner'; +import PageTitle from '@app/components/Common/PageTitle'; +import { genreColorMap } from '@app/components/Discover/constants'; +import GenreCard from '@app/components/GenreCard'; +import Error from '@app/pages/_error'; +import type { GenreSliderItem } from '@server/interfaces/api/discoverInterfaces'; import { defineMessages, useIntl } from 'react-intl'; import useSWR from 'swr'; -import { GenreSliderItem } from '../../../../server/interfaces/api/discoverInterfaces'; -import Error from '../../../pages/_error'; -import Header from '../../Common/Header'; -import LoadingSpinner from '../../Common/LoadingSpinner'; -import PageTitle from '../../Common/PageTitle'; -import GenreCard from '../../GenreCard'; -import { genreColorMap } from '../constants'; const messages = defineMessages({ moviegenres: 'Movie Genres', }); -const MovieGenreList: React.FC = () => { +const MovieGenreList = () => { const intl = useIntl(); const { data, error } = useSWR( `/api/v1/discover/genreslider/movie` diff --git a/src/components/Discover/MovieGenreSlider/index.tsx b/src/components/Discover/MovieGenreSlider/index.tsx index cf1b8ce1f..4899d3496 100644 --- a/src/components/Discover/MovieGenreSlider/index.tsx +++ b/src/components/Discover/MovieGenreSlider/index.tsx @@ -1,18 +1,18 @@ +import { genreColorMap } from '@app/components/Discover/constants'; +import GenreCard from '@app/components/GenreCard'; +import Slider from '@app/components/Slider'; import { ArrowCircleRightIcon } from '@heroicons/react/outline'; +import type { GenreSliderItem } from '@server/interfaces/api/discoverInterfaces'; import Link from 'next/link'; import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; import useSWR from 'swr'; -import { GenreSliderItem } from '../../../../server/interfaces/api/discoverInterfaces'; -import GenreCard from '../../GenreCard'; -import Slider from '../../Slider'; -import { genreColorMap } from '../constants'; const messages = defineMessages({ moviegenres: 'Movie Genres', }); -const MovieGenreSlider: React.FC = () => { +const MovieGenreSlider = () => { const intl = useIntl(); const { data, error } = useSWR( `/api/v1/discover/genreslider/movie`, diff --git a/src/components/Discover/NetworkSlider/index.tsx b/src/components/Discover/NetworkSlider/index.tsx index 61468a6fb..8973cbd1e 100644 --- a/src/components/Discover/NetworkSlider/index.tsx +++ b/src/components/Discover/NetworkSlider/index.tsx @@ -1,7 +1,6 @@ -import React from 'react'; +import CompanyCard from '@app/components/CompanyCard'; +import Slider from '@app/components/Slider'; import { defineMessages, useIntl } from 'react-intl'; -import CompanyCard from '../../CompanyCard'; -import Slider from '../../Slider'; const messages = defineMessages({ networks: 'Networks', @@ -142,7 +141,7 @@ const networks: Network[] = [ }, ]; -const NetworkSlider: React.FC = () => { +const NetworkSlider = () => { const intl = useIntl(); return ( diff --git a/src/components/Discover/StudioSlider/index.tsx b/src/components/Discover/StudioSlider/index.tsx index 59f0e8c07..3f1361427 100644 --- a/src/components/Discover/StudioSlider/index.tsx +++ b/src/components/Discover/StudioSlider/index.tsx @@ -1,7 +1,6 @@ -import React from 'react'; +import CompanyCard from '@app/components/CompanyCard'; +import Slider from '@app/components/Slider'; import { defineMessages, useIntl } from 'react-intl'; -import CompanyCard from '../../CompanyCard'; -import Slider from '../../Slider'; const messages = defineMessages({ studios: 'Studios', @@ -21,10 +20,10 @@ const studios: Studio[] = [ url: '/discover/movies/studio/2', }, { - name: '20th Century Fox', + name: '20th Century Studios', image: - 'https://image.tmdb.org/t/p/w780_filter(duotone,ffffff,bababa)/qZCc1lty5FzX30aOCVRBLzaVmcp.png', - url: '/discover/movies/studio/25', + 'https://image.tmdb.org/t/p/w780_filter(duotone,ffffff,bababa)/h0rjX5vjW5r8yEnUBStFarjcLT4.png', + url: '/discover/movies/studio/127928', }, { name: 'Sony Pictures', @@ -76,7 +75,7 @@ const studios: Studio[] = [ }, ]; -const StudioSlider: React.FC = () => { +const StudioSlider = () => { const intl = useIntl(); return ( diff --git a/src/components/Discover/Trending.tsx b/src/components/Discover/Trending.tsx index c0f2e222d..5210e7d3c 100644 --- a/src/components/Discover/Trending.tsx +++ b/src/components/Discover/Trending.tsx @@ -1,21 +1,20 @@ -import React from 'react'; +import Header from '@app/components/Common/Header'; +import ListView from '@app/components/Common/ListView'; +import PageTitle from '@app/components/Common/PageTitle'; +import useDiscover from '@app/hooks/useDiscover'; +import Error from '@app/pages/_error'; import type { MovieResult, - TvResult, PersonResult, -} from '../../../server/models/Search'; -import ListView from '../Common/ListView'; + TvResult, +} from '@server/models/Search'; import { defineMessages, useIntl } from 'react-intl'; -import Header from '../Common/Header'; -import PageTitle from '../Common/PageTitle'; -import useDiscover from '../../hooks/useDiscover'; -import Error from '../../pages/_error'; const messages = defineMessages({ trending: 'Trending', }); -const Trending: React.FC = () => { +const Trending = () => { const intl = useIntl(); const { isLoadingInitialData, diff --git a/src/components/Discover/TvGenreList/index.tsx b/src/components/Discover/TvGenreList/index.tsx index 15fe9a017..391c51f2d 100644 --- a/src/components/Discover/TvGenreList/index.tsx +++ b/src/components/Discover/TvGenreList/index.tsx @@ -1,19 +1,18 @@ -import React from 'react'; +import Header from '@app/components/Common/Header'; +import LoadingSpinner from '@app/components/Common/LoadingSpinner'; +import PageTitle from '@app/components/Common/PageTitle'; +import { genreColorMap } from '@app/components/Discover/constants'; +import GenreCard from '@app/components/GenreCard'; +import Error from '@app/pages/_error'; +import type { GenreSliderItem } from '@server/interfaces/api/discoverInterfaces'; import { defineMessages, useIntl } from 'react-intl'; import useSWR from 'swr'; -import { GenreSliderItem } from '../../../../server/interfaces/api/discoverInterfaces'; -import Error from '../../../pages/_error'; -import Header from '../../Common/Header'; -import LoadingSpinner from '../../Common/LoadingSpinner'; -import PageTitle from '../../Common/PageTitle'; -import GenreCard from '../../GenreCard'; -import { genreColorMap } from '../constants'; const messages = defineMessages({ seriesgenres: 'Series Genres', }); -const TvGenreList: React.FC = () => { +const TvGenreList = () => { const intl = useIntl(); const { data, error } = useSWR( `/api/v1/discover/genreslider/tv` diff --git a/src/components/Discover/TvGenreSlider/index.tsx b/src/components/Discover/TvGenreSlider/index.tsx index 54f8daa34..820012c32 100644 --- a/src/components/Discover/TvGenreSlider/index.tsx +++ b/src/components/Discover/TvGenreSlider/index.tsx @@ -1,18 +1,18 @@ +import { genreColorMap } from '@app/components/Discover/constants'; +import GenreCard from '@app/components/GenreCard'; +import Slider from '@app/components/Slider'; import { ArrowCircleRightIcon } from '@heroicons/react/outline'; +import type { GenreSliderItem } from '@server/interfaces/api/discoverInterfaces'; import Link from 'next/link'; import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; import useSWR from 'swr'; -import { GenreSliderItem } from '../../../../server/interfaces/api/discoverInterfaces'; -import GenreCard from '../../GenreCard'; -import Slider from '../../Slider'; -import { genreColorMap } from '../constants'; const messages = defineMessages({ tvgenres: 'Series Genres', }); -const TvGenreSlider: React.FC = () => { +const TvGenreSlider = () => { const intl = useIntl(); const { data, error } = useSWR( `/api/v1/discover/genreslider/tv`, diff --git a/src/components/Discover/Upcoming.tsx b/src/components/Discover/Upcoming.tsx index 1e14f73dc..b556e6f9d 100644 --- a/src/components/Discover/Upcoming.tsx +++ b/src/components/Discover/Upcoming.tsx @@ -1,17 +1,16 @@ -import React from 'react'; -import type { MovieResult } from '../../../server/models/Search'; -import ListView from '../Common/ListView'; +import Header from '@app/components/Common/Header'; +import ListView from '@app/components/Common/ListView'; +import PageTitle from '@app/components/Common/PageTitle'; +import useDiscover from '@app/hooks/useDiscover'; +import Error from '@app/pages/_error'; +import type { MovieResult } from '@server/models/Search'; import { defineMessages, useIntl } from 'react-intl'; -import Header from '../Common/Header'; -import PageTitle from '../Common/PageTitle'; -import useDiscover from '../../hooks/useDiscover'; -import Error from '../../pages/_error'; const messages = defineMessages({ upcomingmovies: 'Upcoming Movies', }); -const UpcomingMovies: React.FC = () => { +const UpcomingMovies = () => { const intl = useIntl(); const { diff --git a/src/components/Discover/index.tsx b/src/components/Discover/index.tsx index 3ebd6226c..24dc6fea5 100644 --- a/src/components/Discover/index.tsx +++ b/src/components/Discover/index.tsx @@ -1,19 +1,20 @@ +import PageTitle from '@app/components/Common/PageTitle'; +import MovieGenreSlider from '@app/components/Discover/MovieGenreSlider'; +import NetworkSlider from '@app/components/Discover/NetworkSlider'; +import StudioSlider from '@app/components/Discover/StudioSlider'; +import TvGenreSlider from '@app/components/Discover/TvGenreSlider'; +import MediaSlider from '@app/components/MediaSlider'; +import RequestCard from '@app/components/RequestCard'; +import Slider from '@app/components/Slider'; +import TmdbTitleCard from '@app/components/TitleCard/TmdbTitleCard'; +import { Permission, UserType, useUser } from '@app/hooks/useUser'; import { ArrowCircleRightIcon } from '@heroicons/react/outline'; +import type { WatchlistItem } from '@server/interfaces/api/discoverInterfaces'; +import type { MediaResultsResponse } from '@server/interfaces/api/mediaInterfaces'; +import type { RequestResultsResponse } from '@server/interfaces/api/requestInterfaces'; import Link from 'next/link'; -import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; import useSWR from 'swr'; -import type { MediaResultsResponse } from '../../../server/interfaces/api/mediaInterfaces'; -import type { RequestResultsResponse } from '../../../server/interfaces/api/requestInterfaces'; -import PageTitle from '../Common/PageTitle'; -import MediaSlider from '../MediaSlider'; -import RequestCard from '../RequestCard'; -import Slider from '../Slider'; -import TmdbTitleCard from '../TitleCard/TmdbTitleCard'; -import MovieGenreSlider from './MovieGenreSlider'; -import NetworkSlider from './NetworkSlider'; -import StudioSlider from './StudioSlider'; -import TvGenreSlider from './TvGenreSlider'; const messages = defineMessages({ discover: 'Discover', @@ -22,13 +23,16 @@ const messages = defineMessages({ populartv: 'Popular Series', upcomingtv: 'Upcoming Series', recentlyAdded: 'Recently Added', - noRequests: 'No requests.', upcoming: 'Upcoming Movies', trending: 'Trending', + plexwatchlist: 'Your Plex Watchlist', + emptywatchlist: + 'Media added to your Plex Watchlist will appear here.', }); -const Discover: React.FC = () => { +const Discover = () => { const intl = useIntl(); + const { user, hasPermission } = useUser(); const { data: media, error: mediaError } = useSWR( '/api/v1/media?filter=allavailable&take=20&sort=mediaAdded', @@ -38,50 +42,114 @@ const Discover: React.FC = () => { const { data: requests, error: requestError } = useSWR( '/api/v1/request?filter=all&take=10&sort=modified&skip=0', - { revalidateOnMount: true } + { + revalidateOnMount: true, + } ); + const { data: watchlistItems, error: watchlistError } = useSWR<{ + page: number; + totalPages: number; + totalResults: number; + results: WatchlistItem[]; + }>(user?.userType === UserType.PLEX ? '/api/v1/discover/watchlist' : null, { + revalidateOnMount: true, + }); + return ( <> -
    -
    - {intl.formatMessage(messages.recentlyAdded)} -
    -
    - ( - +
    +
    + {intl.formatMessage(messages.recentlyAdded)} +
    +
    + ( + + ))} + /> + + )} + {(!requests || !!requests.results.length) && !requestError && ( + <> +
    + ( + + ))} + placeholder={} /> - ))} - /> - - ( - - ))} - placeholder={} - emptyMessage={intl.formatMessage(messages.noRequests)} - /> + + )} + {user?.userType === UserType.PLEX && + (!watchlistItems || + !!watchlistItems.results.length || + user.settings?.watchlistSyncMovies || + user.settings?.watchlistSyncTv) && + !watchlistError && ( + <> + + ( + + {msg} + + ), + })} + items={watchlistItems?.results.map((item) => ( + + ))} + /> + + )} = ({ - downloadItem, - is4k = false, -}) => { +const DownloadBlock = ({ downloadItem, is4k = false }: DownloadBlockProps) => { const intl = useIntl(); return ( diff --git a/src/components/ExternalLinkBlock/index.tsx b/src/components/ExternalLinkBlock/index.tsx index af49fda99..d7c4b4602 100644 --- a/src/components/ExternalLinkBlock/index.tsx +++ b/src/components/ExternalLinkBlock/index.tsx @@ -1,15 +1,14 @@ -import React from 'react'; -import { MediaType } from '../../../server/constants/media'; -import { MediaServerType } from '../../../server/constants/server'; -import ImdbLogo from '../../assets/services/imdb.svg'; -import JellyfinLogo from '../../assets/services/jellyfin.svg'; -import PlexLogo from '../../assets/services/plex.svg'; -import RTLogo from '../../assets/services/rt.svg'; -import TmdbLogo from '../../assets/services/tmdb.svg'; -import TraktLogo from '../../assets/services/trakt.svg'; -import TvdbLogo from '../../assets/services/tvdb.svg'; -import useLocale from '../../hooks/useLocale'; -import useSettings from '../../hooks/useSettings'; +import ImdbLogo from '@app/assets/services/imdb.svg'; +import JellyfinLogo from '@app/assets/services/jellyfin.svg'; +import PlexLogo from '@app/assets/services/plex.svg'; +import RTLogo from '@app/assets/services/rt.svg'; +import TmdbLogo from '@app/assets/services/tmdb.svg'; +import TraktLogo from '@app/assets/services/trakt.svg'; +import TvdbLogo from '@app/assets/services/tvdb.svg'; +import useLocale from '@app/hooks/useLocale'; +import useSettings from '@app/hooks/useSettings'; +import { MediaType } from '@server/constants/media'; +import { MediaServerType } from '@server/constants/server'; interface ExternalLinkBlockProps { mediaType: 'movie' | 'tv'; @@ -20,14 +19,14 @@ interface ExternalLinkBlockProps { mediaUrl?: string; } -const ExternalLinkBlock: React.FC = ({ +const ExternalLinkBlock = ({ mediaType, tmdbId, tvdbId, imdbId, rtUrl, mediaUrl, -}) => { +}: ExternalLinkBlockProps) => { const settings = useSettings(); const { locale } = useLocale(); @@ -79,7 +78,7 @@ const ExternalLinkBlock: React.FC = ({ )} {rtUrl && ( = ({ - image, - url, - name, - canExpand = false, -}) => { +const GenreCard = ({ image, url, name, canExpand = false }: GenreCardProps) => { const [isHovered, setHovered] = useState(false); return ( @@ -54,7 +49,7 @@ const GenreCard: React.FC = ({ ); }; -const GenreCardPlaceholder: React.FC = () => { +const GenreCardPlaceholder = () => { return (
    = ({ issue }) => { +const IssueBlock = ({ issue }: IssueBlockProps) => { const { user } = useUser(); const intl = useIntl(); const issueOption = issueOptions.find( diff --git a/src/components/IssueDetails/IssueComment/index.tsx b/src/components/IssueDetails/IssueComment/index.tsx index 11623f53d..b941c9f31 100644 --- a/src/components/IssueDetails/IssueComment/index.tsx +++ b/src/components/IssueDetails/IssueComment/index.tsx @@ -1,18 +1,16 @@ -import { Menu } from '@headlessui/react'; -import { ExclamationIcon } from '@heroicons/react/outline'; +import Button from '@app/components/Common/Button'; +import Modal from '@app/components/Common/Modal'; +import { Permission, useUser } from '@app/hooks/useUser'; +import { Menu, Transition } from '@headlessui/react'; import { DotsVerticalIcon } from '@heroicons/react/solid'; +import type { default as IssueCommentType } from '@server/entity/IssueComment'; import axios from 'axios'; import { Field, Form, Formik } from 'formik'; import Link from 'next/link'; -import React, { useState } from 'react'; +import { Fragment, useState } from 'react'; import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl'; import ReactMarkdown from 'react-markdown'; import * as Yup from 'yup'; -import type { default as IssueCommentType } from '../../../../server/entity/IssueComment'; -import { Permission, useUser } from '../../../hooks/useUser'; -import Button from '../../Common/Button'; -import Modal from '../../Common/Modal'; -import Transition from '../../Transition'; const messages = defineMessages({ postedby: 'Posted {relativeTime} by {username}', @@ -30,12 +28,12 @@ interface IssueCommentProps { onUpdate?: () => void; } -const IssueComment: React.FC = ({ +const IssueComment = ({ comment, isReversed = false, isActiveUser = false, onUpdate, -}) => { +}: IssueCommentProps) => { const intl = useIntl(); const [showDeleteModal, setShowDeleteModal] = useState(false); const [isEditing, setIsEditing] = useState(false); @@ -66,6 +64,7 @@ const IssueComment: React.FC = ({ } mt-4 space-x-4`} > = ({ onOk={() => deleteComment()} okText={intl.formatMessage(messages.delete)} okButtonType="danger" - iconSvg={} > {intl.formatMessage(messages.areyousuredelete)} @@ -114,6 +112,7 @@ const IssueComment: React.FC = ({
    = ({ name="newMessage" className="h-24" /> - {errors.newMessage && touched.newMessage && ( -
    {errors.newMessage}
    - )} + {errors.newMessage && + touched.newMessage && + typeof errors.newMessage === 'string' && ( +
    {errors.newMessage}
    + )}
    diff --git a/src/components/IssueModal/CreateIssueModal/index.tsx b/src/components/IssueModal/CreateIssueModal/index.tsx index 5dbc41802..0ed4162f8 100644 --- a/src/components/IssueModal/CreateIssueModal/index.tsx +++ b/src/components/IssueModal/CreateIssueModal/index.tsx @@ -1,28 +1,25 @@ +import Button from '@app/components/Common/Button'; +import Modal from '@app/components/Common/Modal'; +import { issueOptions } from '@app/components/IssueModal/constants'; +import useSettings from '@app/hooks/useSettings'; +import { Permission, useUser } from '@app/hooks/useUser'; +import globalMessages from '@app/i18n/globalMessages'; import { RadioGroup } from '@headlessui/react'; -import { ExclamationIcon } from '@heroicons/react/outline'; import { ArrowCircleRightIcon } from '@heroicons/react/solid'; +import { MediaStatus } from '@server/constants/media'; +import type Issue from '@server/entity/Issue'; +import type { MovieDetails } from '@server/models/Movie'; +import type { TvDetails } from '@server/models/Tv'; import axios from 'axios'; import { Field, Formik } from 'formik'; import Link from 'next/link'; -import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; import { useToasts } from 'react-toast-notifications'; import useSWR from 'swr'; import * as Yup from 'yup'; -import { MediaStatus } from '../../../../server/constants/media'; -import type Issue from '../../../../server/entity/Issue'; -import { MovieDetails } from '../../../../server/models/Movie'; -import { TvDetails } from '../../../../server/models/Tv'; -import useSettings from '../../../hooks/useSettings'; -import { Permission, useUser } from '../../../hooks/useUser'; -import globalMessages from '../../../i18n/globalMessages'; -import Button from '../../Common/Button'; -import Modal from '../../Common/Modal'; -import { issueOptions } from '../constants'; const messages = defineMessages({ validationMessageRequired: 'You must provide a description', - issomethingwrong: 'Is there a problem with {title}?', whatswrong: "What's wrong?", providedetail: 'Please provide a detailed explanation of the issue you encountered.', @@ -55,11 +52,11 @@ interface CreateIssueModalProps { onCancel?: () => void; } -const CreateIssueModal: React.FC = ({ +const CreateIssueModal = ({ onCancel, mediaType, tmdbId, -}) => { +}: CreateIssueModalProps) => { const intl = useIntl(); const settings = useSettings(); const { hasPermission } = useUser(); @@ -118,9 +115,7 @@ const CreateIssueModal: React.FC = ({
    {intl.formatMessage(messages.toastSuccessCreate, { title: isMovie(data) ? data.title : data.name, - strong: function strong(msg) { - return {msg}; - }, + strong: (msg: React.ReactNode) => {msg}, })}
    @@ -153,23 +148,14 @@ const CreateIssueModal: React.FC = ({ } title={intl.formatMessage(messages.reportissue)} + subTitle={data && isMovie(data) ? data?.title : data?.name} cancelText={intl.formatMessage(globalMessages.close)} onOk={() => handleSubmit()} okText={intl.formatMessage(messages.submitissue)} loading={!data && !error} backdrop={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${data?.backdropPath}`} > - {data && ( -
    - - {intl.formatMessage(messages.issomethingwrong, { - title: isMovie(data) ? data.title : data.name, - })} - -
    - )} {mediaType === 'tv' && data && !isMovie(data) && ( <>
    @@ -267,7 +253,7 @@ const CreateIssueModal: React.FC = ({ ? 'rounded-bl-md rounded-br-md' : '', checked - ? 'z-10 border-indigo-500 bg-indigo-600' + ? 'z-10 border border-indigo-500 bg-indigo-400 bg-opacity-20' : 'border-gray-500', 'relative flex cursor-pointer border p-4 focus:outline-none' ) @@ -278,7 +264,7 @@ const CreateIssueModal: React.FC = ({ = ({ className="h-28" placeholder={intl.formatMessage(messages.providedetail)} /> - {errors.message && touched.message && ( -
    {errors.message}
    - )} + {errors.message && + touched.message && + typeof errors.message === 'string' && ( +
    {errors.message}
    + )}
    ); diff --git a/src/components/IssueModal/constants.ts b/src/components/IssueModal/constants.ts index 92cf6bc77..7552c6333 100644 --- a/src/components/IssueModal/constants.ts +++ b/src/components/IssueModal/constants.ts @@ -1,5 +1,6 @@ -import { defineMessages, MessageDescriptor } from 'react-intl'; -import { IssueType } from '../../../server/constants/issue'; +import { IssueType } from '@server/constants/issue'; +import type { MessageDescriptor } from 'react-intl'; +import { defineMessages } from 'react-intl'; const messages = defineMessages({ issueAudio: 'Audio', diff --git a/src/components/IssueModal/index.tsx b/src/components/IssueModal/index.tsx index f3f226de9..6ec67c245 100644 --- a/src/components/IssueModal/index.tsx +++ b/src/components/IssueModal/index.tsx @@ -1,6 +1,5 @@ -import React from 'react'; -import Transition from '../Transition'; -import CreateIssueModal from './CreateIssueModal'; +import CreateIssueModal from '@app/components/IssueModal/CreateIssueModal'; +import { Transition } from '@headlessui/react'; interface IssueModalProps { show?: boolean; @@ -10,13 +9,9 @@ interface IssueModalProps { issueId?: never; } -const IssueModal: React.FC = ({ - show, - mediaType, - onCancel, - tmdbId, -}) => ( +const IssueModal = ({ show, mediaType, onCancel, tmdbId }: IssueModalProps) => ( { name: string; value: string; onUpdate: (value: string) => void; } -const JSONEditor: React.FC = ({ - name, - value, - onUpdate, - onBlur, -}) => { +const JSONEditor = ({ name, value, onUpdate, onBlur }: JSONEditorProps) => { return (
    = ({ +const LanguageSelector = ({ value, setFieldValue, serverValue, isUserSettings = false, -}) => { +}: LanguageSelectorProps) => { const intl = useIntl(); const { data: languages } = useSWR('/api/v1/languages'); diff --git a/src/components/Layout/LanguagePicker/index.tsx b/src/components/Layout/LanguagePicker/index.tsx index 1d610604f..0eec6b7d3 100644 --- a/src/components/Layout/LanguagePicker/index.tsx +++ b/src/components/Layout/LanguagePicker/index.tsx @@ -1,19 +1,17 @@ +import type { AvailableLocale } from '@app/context/LanguageContext'; +import { availableLanguages } from '@app/context/LanguageContext'; +import useClickOutside from '@app/hooks/useClickOutside'; +import useLocale from '@app/hooks/useLocale'; +import { Transition } from '@headlessui/react'; import { TranslateIcon } from '@heroicons/react/solid'; -import React, { useRef, useState } from 'react'; +import { useRef, useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { - availableLanguages, - AvailableLocale, -} from '../../../context/LanguageContext'; -import useClickOutside from '../../../hooks/useClickOutside'; -import useLocale from '../../../hooks/useLocale'; -import Transition from '../../Transition'; const messages = defineMessages({ displaylanguage: 'Display Language', }); -const LanguagePicker: React.FC = () => { +const LanguagePicker = () => { const intl = useIntl(); const dropdownRef = useRef(null); const { locale, setLocale } = useLocale(); @@ -34,6 +32,7 @@ const LanguagePicker: React.FC = () => {
    { +const Notifications = () => { return (
    @@ -168,6 +173,7 @@ const Sidebar: React.FC = ({ open, setClosed }) => { : 'hover:bg-gray-700 focus:bg-gray-700' } `} + data-testid={`${sidebarLink.dataTestId}-mobile`} > {sidebarLink.svgIcon} {intl.formatMessage( @@ -193,7 +199,7 @@ const Sidebar: React.FC = ({ open, setClosed }) => { {/* */}
    - +
    @@ -233,6 +239,7 @@ const Sidebar: React.FC = ({ open, setClosed }) => { : 'hover:bg-gray-700 focus:bg-gray-700' } `} + data-testid={sidebarLink.dataTestId} > {sidebarLink.svgIcon} {intl.formatMessage(messages[sidebarLink.messagesKey])} diff --git a/src/components/Layout/UserDropdown/MiniQuotaDisplay/index.tsx b/src/components/Layout/UserDropdown/MiniQuotaDisplay/index.tsx new file mode 100644 index 000000000..abc08dd18 --- /dev/null +++ b/src/components/Layout/UserDropdown/MiniQuotaDisplay/index.tsx @@ -0,0 +1,93 @@ +import Infinity from '@app/assets/infinity.svg'; +import { SmallLoadingSpinner } from '@app/components/Common/LoadingSpinner'; +import ProgressCircle from '@app/components/Common/ProgressCircle'; +import type { QuotaResponse } from '@server/interfaces/api/userInterfaces'; +import { defineMessages, useIntl } from 'react-intl'; +import useSWR from 'swr'; + +const messages = defineMessages({ + movierequests: 'Movie Requests', + seriesrequests: 'Series Requests', +}); + +type MiniQuotaDisplayProps = { + userId: number; +}; + +const MiniQuotaDisplay = ({ userId }: MiniQuotaDisplayProps) => { + const intl = useIntl(); + const { data, error } = useSWR(`/api/v1/user/${userId}/quota`); + + if (error) { + return null; + } + + if (!data && !error) { + return ; + } + + return ( + <> + {((data?.movie.limit ?? 0) !== 0 || (data?.tv.limit ?? 0) !== 0) && ( +
    +
    +
    + {intl.formatMessage(messages.movierequests)} +
    +
    + {data?.movie.limit ?? 0 > 0 ? ( + <> + + + {data?.movie.remaining} / {data?.movie.limit} + + + ) : ( + <> + + Unlimited + + )} +
    +
    +
    +
    + {intl.formatMessage(messages.seriesrequests)} +
    +
    + {data?.tv.limit ?? 0 > 0 ? ( + <> + + + {data?.tv.remaining} / {data?.tv.limit} + + + ) : ( + <> + + Unlimited + + )} +
    +
    +
    + )} + + ); +}; + +export default MiniQuotaDisplay; diff --git a/src/components/Layout/UserDropdown/index.tsx b/src/components/Layout/UserDropdown/index.tsx index e51fcabf5..57481b719 100644 --- a/src/components/Layout/UserDropdown/index.tsx +++ b/src/components/Layout/UserDropdown/index.tsx @@ -1,25 +1,39 @@ -import { LogoutIcon } from '@heroicons/react/outline'; +import MiniQuotaDisplay from '@app/components/Layout/UserDropdown/MiniQuotaDisplay'; +import { useUser } from '@app/hooks/useUser'; +import { Menu, Transition } from '@headlessui/react'; +import { ClockIcon, LogoutIcon } from '@heroicons/react/outline'; import { CogIcon, UserIcon } from '@heroicons/react/solid'; import axios from 'axios'; +import type { LinkProps } from 'next/link'; import Link from 'next/link'; -import React, { useRef, useState } from 'react'; +import { forwardRef, Fragment } from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import useClickOutside from '../../../hooks/useClickOutside'; -import { useUser } from '../../../hooks/useUser'; -import Transition from '../../Transition'; const messages = defineMessages({ myprofile: 'Profile', settings: 'Settings', + requests: 'Requests', signout: 'Sign Out', }); -const UserDropdown: React.FC = () => { +const ForwardedLink = forwardRef< + HTMLAnchorElement, + LinkProps & React.ComponentPropsWithoutRef<'a'> +>(({ href, children, ...rest }, ref) => { + return ( + +
    + {children} + + + ); +}); + +ForwardedLink.displayName = 'ForwardedLink'; + +const UserDropdown = () => { const intl = useIntl(); - const dropdownRef = useRef(null); const { user, revalidate } = useUser(); - const [isDropdownOpen, setDropdownOpen] = useState(false); - useClickOutside(dropdownRef, () => setDropdownOpen(false)); const logout = async () => { const response = await axios.post('/api/v1/auth/logout'); @@ -30,86 +44,119 @@ const UserDropdown: React.FC = () => { }; return ( -
    +
    - +
    -
    -
    - - { - if (e.key === 'Enter') { - setDropdownOpen(false); - } - }} - onClick={() => setDropdownOpen(false)} - > - - {intl.formatMessage(messages.myprofile)} - - - - { - if (e.key === 'Enter') { - setDropdownOpen(false); - } - }} - onClick={() => setDropdownOpen(false)} - > - - {intl.formatMessage(messages.settings)} - - - logout()} - > - - {intl.formatMessage(messages.signout)} - + +
    +
    +
    + +
    + + {user?.displayName} + + + {user?.email} + +
    +
    + {user && } +
    +
    + + {({ active }) => ( + + + {intl.formatMessage(messages.myprofile)} + + )} + + + {({ active }) => ( + + + {intl.formatMessage(messages.requests)} + + )} + + + {({ active }) => ( + + + {intl.formatMessage(messages.settings)} + + )} + + + {({ active }) => ( + logout()} + > + + {intl.formatMessage(messages.signout)} + + )} + +
    -
    + -
    +
    ); }; diff --git a/src/components/Layout/UserWarnings/index.tsx b/src/components/Layout/UserWarnings/index.tsx index fe621d2a9..ec32ecc6b 100644 --- a/src/components/Layout/UserWarnings/index.tsx +++ b/src/components/Layout/UserWarnings/index.tsx @@ -1,8 +1,8 @@ -import React from 'react'; -import Link from 'next/link'; +import { useUser } from '@app/hooks/useUser'; import { ExclamationIcon } from '@heroicons/react/outline'; +import Link from 'next/link'; +import type React from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { useUser } from '../../../hooks/useUser'; const messages = defineMessages({ emailRequired: 'An email address is required.', diff --git a/src/components/Layout/VersionStatus/index.tsx b/src/components/Layout/VersionStatus/index.tsx index d682df4ca..515ff20e9 100644 --- a/src/components/Layout/VersionStatus/index.tsx +++ b/src/components/Layout/VersionStatus/index.tsx @@ -4,11 +4,10 @@ import { CodeIcon, ServerIcon, } from '@heroicons/react/outline'; +import type { StatusResponse } from '@server/interfaces/api/settingsInterfaces'; import Link from 'next/link'; -import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; import useSWR from 'swr'; -import { StatusResponse } from '../../../../server/interfaces/api/settingsInterfaces'; const messages = defineMessages({ streamdevelop: 'Overseerr Develop', @@ -22,7 +21,7 @@ interface VersionStatusProps { onClick?: () => void; } -const VersionStatus: React.FC = ({ onClick }) => { +const VersionStatus = ({ onClick }: VersionStatusProps) => { const intl = useIntl(); const { data } = useSWR('/api/v1/status', { refreshInterval: 60 * 1000, diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx index b560c66e8..2e63441ef 100644 --- a/src/components/Layout/index.tsx +++ b/src/components/Layout/index.tsx @@ -1,16 +1,21 @@ +import SearchInput from '@app/components/Layout/SearchInput'; +import Sidebar from '@app/components/Layout/Sidebar'; +import UserDropdown from '@app/components/Layout/UserDropdown'; +import PullToRefresh from '@app/components/PullToRefresh'; +import type { AvailableLocale } from '@app/context/LanguageContext'; +import useLocale from '@app/hooks/useLocale'; +import useSettings from '@app/hooks/useSettings'; +import { useUser } from '@app/hooks/useUser'; import { MenuAlt2Icon } from '@heroicons/react/outline'; import { ArrowLeftIcon } from '@heroicons/react/solid'; import { useRouter } from 'next/router'; -import React, { useEffect, useState } from 'react'; -import { AvailableLocale } from '../../context/LanguageContext'; -import useLocale from '../../hooks/useLocale'; -import useSettings from '../../hooks/useSettings'; -import { useUser } from '../../hooks/useUser'; -import SearchInput from './SearchInput'; -import Sidebar from './Sidebar'; -import UserDropdown from './UserDropdown'; +import { useEffect, useState } from 'react'; -const Layout: React.FC = ({ children }) => { +type LayoutProps = { + children: React.ReactNode; +}; + +const Layout = ({ children }: LayoutProps) => { const [isSidebarOpen, setSidebarOpen] = useState(false); const [isScrolled, setIsScrolled] = useState(false); const { user } = useUser(); @@ -54,6 +59,7 @@ const Layout: React.FC = ({ children }) => { setSidebarOpen(false)} />
    +
    { } transition duration-300 focus:outline-none lg:hidden`} aria-label="Open sidebar" onClick={() => setSidebarOpen(true)} + data-testid="sidebar-toggle" > diff --git a/src/components/LoadingBar/index.tsx b/src/components/LoadingBar/index.tsx index 712ba4db0..1e488c671 100644 --- a/src/components/LoadingBar/index.tsx +++ b/src/components/LoadingBar/index.tsx @@ -1,7 +1,7 @@ import { NProgress } from '@tanem/react-nprogress'; +import { useRouter } from 'next/router'; import React, { useEffect, useState } from 'react'; import ReactDOM from 'react-dom'; -import { useRouter } from 'next/router'; interface BarProps { progress: number; diff --git a/src/components/Login/AddEmailModal.tsx b/src/components/Login/AddEmailModal.tsx index 4cd271b93..01bc81234 100644 --- a/src/components/Login/AddEmailModal.tsx +++ b/src/components/Login/AddEmailModal.tsx @@ -1,11 +1,11 @@ -import React from 'react'; -import Transition from '../Transition'; -import Modal from '../Common/Modal'; -import { Formik, Field } from 'formik'; -import * as Yup from 'yup'; +import Modal from '@app/components/Common/Modal'; +import useSettings from '@app/hooks/useSettings'; +import { Transition } from '@headlessui/react'; import axios from 'axios'; +import { Field, Formik } from 'formik'; +import type React from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import useSettings from '../../hooks/useSettings'; +import * as Yup from 'yup'; const messages = defineMessages({ title: 'Add Email', diff --git a/src/components/Login/JellyfinLogin.tsx b/src/components/Login/JellyfinLogin.tsx index e76a16502..5bd38301e 100644 --- a/src/components/Login/JellyfinLogin.tsx +++ b/src/components/Login/JellyfinLogin.tsx @@ -1,12 +1,12 @@ +import Button from '@app/components/Common/Button'; +import useSettings from '@app/hooks/useSettings'; import axios from 'axios'; import { Field, Form, Formik } from 'formik'; -import React from 'react'; +import getConfig from 'next/config'; +import type React from 'react'; import { defineMessages, useIntl } from 'react-intl'; import { useToasts } from 'react-toast-notifications'; import * as Yup from 'yup'; -import useSettings from '../../hooks/useSettings'; -import Button from '../Common/Button'; -import getConfig from 'next/config'; const messages = defineMessages({ username: 'Username', diff --git a/src/components/Login/LocalLogin.tsx b/src/components/Login/LocalLogin.tsx index b97ac6bd5..fae793d13 100644 --- a/src/components/Login/LocalLogin.tsx +++ b/src/components/Login/LocalLogin.tsx @@ -1,13 +1,13 @@ +import Button from '@app/components/Common/Button'; +import SensitiveInput from '@app/components/Common/SensitiveInput'; +import useSettings from '@app/hooks/useSettings'; import { LoginIcon, SupportIcon } from '@heroicons/react/outline'; import axios from 'axios'; import { Field, Form, Formik } from 'formik'; import Link from 'next/link'; -import React, { useState } from 'react'; +import { useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import * as Yup from 'yup'; -import useSettings from '../../hooks/useSettings'; -import Button from '../Common/Button'; -import SensitiveInput from '../Common/SensitiveInput'; const messages = defineMessages({ username: 'Username', @@ -25,7 +25,7 @@ interface LocalLoginProps { revalidate: () => void; } -const LocalLogin: React.FC = ({ revalidate }) => { +const LocalLogin = ({ revalidate }: LocalLoginProps) => { const intl = useIntl(); const settings = useSettings(); const [loginError, setLoginError] = useState(null); @@ -80,11 +80,14 @@ const LocalLogin: React.FC = ({ revalidate }) => { name="email" type="text" inputMode="email" + data-testid="email" />
    - {errors.email && touched.email && ( -
    {errors.email}
    - )} + {errors.email && + touched.email && + typeof errors.email === 'string' && ( +
    {errors.email}
    + )}
    - {errors.password && touched.password && ( -
    {errors.password}
    - )} + {errors.password && + touched.password && + typeof errors.password === 'string' && ( +
    {errors.password}
    + )}
    {loginError && (
    @@ -116,6 +122,7 @@ const LocalLogin: React.FC = ({ revalidate }) => { buttonType="primary" type="submit" disabled={isSubmitting || !isValid} + data-testid="local-signin-button" > diff --git a/src/components/Login/index.tsx b/src/components/Login/index.tsx index eb8f368bd..3c16bdcf6 100644 --- a/src/components/Login/index.tsx +++ b/src/components/Login/index.tsx @@ -1,21 +1,21 @@ +import Accordion from '@app/components/Common/Accordion'; +import ImageFader from '@app/components/Common/ImageFader'; +import PageTitle from '@app/components/Common/PageTitle'; +import LanguagePicker from '@app/components/Layout/LanguagePicker'; +import LocalLogin from '@app/components/Login/LocalLogin'; +import PlexLoginButton from '@app/components/PlexLoginButton'; +import useSettings from '@app/hooks/useSettings'; +import { useUser } from '@app/hooks/useUser'; +import { Transition } from '@headlessui/react'; import { XCircleIcon } from '@heroicons/react/solid'; +import { MediaServerType } from '@server/constants/server'; import axios from 'axios'; +import getConfig from 'next/config'; import { useRouter } from 'next/dist/client/router'; -import React, { useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import useSWR from 'swr'; -import { MediaServerType } from '../../../server/constants/server'; -import useSettings from '../../hooks/useSettings'; -import { useUser } from '../../hooks/useUser'; -import Accordion from '../Common/Accordion'; -import ImageFader from '../Common/ImageFader'; -import PageTitle from '../Common/PageTitle'; -import LanguagePicker from '../Layout/LanguagePicker'; -import PlexLoginButton from '../PlexLoginButton'; -import Transition from '../Transition'; import JellyfinLogin from './JellyfinLogin'; -import LocalLogin from './LocalLogin'; -import getConfig from 'next/config'; const messages = defineMessages({ signin: 'Sign In', @@ -25,7 +25,7 @@ const messages = defineMessages({ signinwithoverseerr: 'Use your {applicationTitle} account', }); -const Login: React.FC = () => { +const Login = () => { const intl = useIntl(); const [error, setError] = useState(''); const [isProcessing, setProcessing] = useState(false); @@ -78,7 +78,7 @@ const Login: React.FC = () => { `https://www.themoviedb.org/t/p/original${backdrop}` + (backdrop) => `https://image.tmdb.org/t/p/original${backdrop}` ) ?? [] } /> @@ -98,6 +98,7 @@ const Login: React.FC = () => { > <> = ({ show, mediaType, onClose, data, revalidate }) => { +const ManageSlideOver = ({ + show, + mediaType, + onClose, + data, + revalidate, +}: ManageSlideOverMovieProps | ManageSlideOverTvProps) => { const { user: currentUser, hasPermission } = useUser(); const intl = useIntl(); const settings = useSettings(); const { data: watchData } = useSWR( - data.mediaInfo && hasPermission(Permission.ADMIN) + settings.currentSettings.mediaServerType === MediaServerType.PLEX && + data.mediaInfo && + hasPermission(Permission.ADMIN) ? `/api/v1/media/${data.mediaInfo.id}/watch_data` : null ); @@ -159,9 +162,9 @@ const ManageSlideOver: React.FC< <> {intl.formatMessage(messages.plays, { playCount, - strong: function strong(msg) { - return {msg}; - }, + strong: (msg: React.ReactNode) => ( + {msg} + ), })} ); @@ -185,7 +188,7 @@ const ManageSlideOver: React.FC<

    {intl.formatMessage(messages.downloadstatus)}

    -
    +
      {data.mediaInfo?.downloadStatus?.map((status, index) => (
    • 0 && ( - <> +

      {intl.formatMessage(messages.manageModalIssues)}

      -
      +
        {openIssues.map((issue) => (
      - +
      )} {requests.length > 0 && (

      {intl.formatMessage(messages.manageModalRequests)}

      -
      +
        {requests.map((request) => (
      • {intl.formatMessage(messages.manageModalMedia)}

        - {!!watchData?.data && ( + {(watchData?.data || data.mediaInfo?.tautulliUrl) && (
        -
        -
        -
        -
        - {intl.formatMessage(messages.pastdays, { days: 7 })} + {!!watchData?.data && ( +
        +
        +
        +
        + {intl.formatMessage(messages.pastdays, { + days: 7, + })} +
        +
        + {styledPlayCount(watchData.data.playCount7Days)} +
        -
        - {styledPlayCount(watchData.data.playCount7Days)} +
        +
        + {intl.formatMessage(messages.pastdays, { + days: 30, + })} +
        +
        + {styledPlayCount(watchData.data.playCount30Days)} +
        +
        +
        +
        + {intl.formatMessage(messages.alltime)} +
        +
        + {styledPlayCount(watchData.data.playCount)} +
        -
        -
        - {intl.formatMessage(messages.pastdays, { - days: 30, - })} + {!!watchData.data.users.length && ( +
        + + {intl.formatMessage(messages.playedby)} + + + {watchData.data.users.map((user) => ( + + + {user.displayName} + + + ))} +
        -
        - {styledPlayCount(watchData.data.playCount30Days)} -
        -
        -
        -
        - {intl.formatMessage(messages.alltime)} -
        -
        - {styledPlayCount(watchData.data.playCount)} -
        -
        + )}
        - {!!watchData.data.users.length && ( -
        - - {intl.formatMessage(messages.playedby)} - - - {watchData.data.users.map((user) => ( - - - {user.displayName} - - - ))} - -
        - )} -
        + )} {data.mediaInfo?.tautulliUrl && ( @@ -346,7 +353,7 @@ const ManageSlideOver: React.FC< )}
        )} - {data?.mediaInfo?.serviceUrl && ( + {data.mediaInfo?.serviceUrl && (

        {intl.formatMessage(messages.manageModalMedia4k)}

        - {!!watchData?.data4k && ( + {(watchData?.data4k || data.mediaInfo?.tautulliUrl4k) && (
        -
        -
        -
        -
        - {intl.formatMessage(messages.pastdays, { days: 7 })} + {watchData?.data4k && ( +
        +
        +
        +
        + {intl.formatMessage(messages.pastdays, { + days: 7, + })} +
        +
        + {styledPlayCount(watchData.data4k.playCount7Days)} +
        -
        - {styledPlayCount(watchData.data4k.playCount7Days)} +
        +
        + {intl.formatMessage(messages.pastdays, { + days: 30, + })} +
        +
        + {styledPlayCount( + watchData.data4k.playCount30Days + )} +
        +
        +
        +
        + {intl.formatMessage(messages.alltime)} +
        +
        + {styledPlayCount(watchData.data4k.playCount)} +
        -
        -
        - {intl.formatMessage(messages.pastdays, { - days: 30, - })} + {!!watchData.data4k.users.length && ( +
        + + {intl.formatMessage(messages.playedby)} + + + {watchData.data4k.users.map((user) => ( + + + {user.displayName} + + + ))} +
        -
        - {styledPlayCount(watchData.data4k.playCount30Days)} -
        -
        -
        -
        - {intl.formatMessage(messages.alltime)} -
        -
        - {styledPlayCount(watchData.data4k.playCount)} -
        -
        + )}
        - {!!watchData.data4k.users.length && ( -
        - )} -
        + )} {data.mediaInfo?.tautulliUrl4k && ( @@ -598,7 +611,7 @@ const ManageSlideOver: React.FC< {intl.formatMessage(messages.manageModalClearMedia)} -
        +
        {intl.formatMessage(messages.manageModalClearMediaWarning, { mediaType: intl.formatMessage( mediaType === 'movie' ? messages.movie : messages.tvshow diff --git a/src/components/MediaSlider/ShowMoreCard/index.tsx b/src/components/MediaSlider/ShowMoreCard/index.tsx index f6bc2ccb4..99900ac9a 100644 --- a/src/components/MediaSlider/ShowMoreCard/index.tsx +++ b/src/components/MediaSlider/ShowMoreCard/index.tsx @@ -1,6 +1,6 @@ import { ArrowCircleRightIcon } from '@heroicons/react/solid'; import Link from 'next/link'; -import React, { useState } from 'react'; +import { useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; const messages = defineMessages({ @@ -12,7 +12,7 @@ interface ShowMoreCardProps { posters: (string | undefined)[]; } -const ShowMoreCard: React.FC = ({ url, posters }) => { +const ShowMoreCard = ({ url, posters }: ShowMoreCardProps) => { const intl = useIntl(); const [isHovered, setHovered] = useState(false); return ( diff --git a/src/components/MediaSlider/index.tsx b/src/components/MediaSlider/index.tsx index 84c72822f..9a9bc054c 100644 --- a/src/components/MediaSlider/index.tsx +++ b/src/components/MediaSlider/index.tsx @@ -1,18 +1,18 @@ +import ShowMoreCard from '@app/components/MediaSlider/ShowMoreCard'; +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 { ArrowCircleRightIcon } from '@heroicons/react/outline'; -import Link from 'next/link'; -import React, { useEffect } from 'react'; -import useSWRInfinite from 'swr/infinite'; -import { MediaStatus } from '../../../server/constants/media'; +import { MediaStatus } from '@server/constants/media'; import type { MovieResult, PersonResult, TvResult, -} from '../../../server/models/Search'; -import useSettings from '../../hooks/useSettings'; -import PersonCard from '../PersonCard'; -import Slider from '../Slider'; -import TitleCard from '../TitleCard'; -import ShowMoreCard from './ShowMoreCard'; +} from '@server/models/Search'; +import Link from 'next/link'; +import { useEffect } from 'react'; +import useSWRInfinite from 'swr/infinite'; interface MixedResult { page: number; @@ -29,13 +29,13 @@ interface MediaSliderProps { hideWhenEmpty?: boolean; } -const MediaSlider: React.FC = ({ +const MediaSlider = ({ title, url, linkUrl, sliderKey, hideWhenEmpty = false, -}) => { +}: MediaSliderProps) => { const settings = useSettings(); const { data, error, setSize, size } = useSWRInfinite( (pageIndex: number, previousPageData: MixedResult | null) => { diff --git a/src/components/MovieDetails/MovieCast/index.tsx b/src/components/MovieDetails/MovieCast/index.tsx index 0cc9c2e03..2006e9dfb 100644 --- a/src/components/MovieDetails/MovieCast/index.tsx +++ b/src/components/MovieDetails/MovieCast/index.tsx @@ -1,20 +1,19 @@ +import Header from '@app/components/Common/Header'; +import LoadingSpinner from '@app/components/Common/LoadingSpinner'; +import PageTitle from '@app/components/Common/PageTitle'; +import PersonCard from '@app/components/PersonCard'; +import Error from '@app/pages/_error'; +import type { MovieDetails } from '@server/models/Movie'; import Link from 'next/link'; import { useRouter } from 'next/router'; -import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; import useSWR from 'swr'; -import { MovieDetails } from '../../../../server/models/Movie'; -import Error from '../../../pages/_error'; -import Header from '../../Common/Header'; -import LoadingSpinner from '../../Common/LoadingSpinner'; -import PageTitle from '../../Common/PageTitle'; -import PersonCard from '../../PersonCard'; const messages = defineMessages({ fullcast: 'Full Cast', }); -const MovieCast: React.FC = () => { +const MovieCast = () => { const router = useRouter(); const intl = useIntl(); const { data, error } = useSWR( diff --git a/src/components/MovieDetails/MovieCrew/index.tsx b/src/components/MovieDetails/MovieCrew/index.tsx index 14268e425..1cc43b05a 100644 --- a/src/components/MovieDetails/MovieCrew/index.tsx +++ b/src/components/MovieDetails/MovieCrew/index.tsx @@ -1,20 +1,19 @@ +import Header from '@app/components/Common/Header'; +import LoadingSpinner from '@app/components/Common/LoadingSpinner'; +import PageTitle from '@app/components/Common/PageTitle'; +import PersonCard from '@app/components/PersonCard'; +import Error from '@app/pages/_error'; +import type { MovieDetails } from '@server/models/Movie'; import Link from 'next/link'; import { useRouter } from 'next/router'; -import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; import useSWR from 'swr'; -import { MovieDetails } from '../../../../server/models/Movie'; -import Error from '../../../pages/_error'; -import Header from '../../Common/Header'; -import LoadingSpinner from '../../Common/LoadingSpinner'; -import PageTitle from '../../Common/PageTitle'; -import PersonCard from '../../PersonCard'; const messages = defineMessages({ fullcrew: 'Full Crew', }); -const MovieCrew: React.FC = () => { +const MovieCrew = () => { const router = useRouter(); const intl = useIntl(); const { data, error } = useSWR( diff --git a/src/components/MovieDetails/MovieRecommendations.tsx b/src/components/MovieDetails/MovieRecommendations.tsx index fc9c2bf2c..a7635a259 100644 --- a/src/components/MovieDetails/MovieRecommendations.tsx +++ b/src/components/MovieDetails/MovieRecommendations.tsx @@ -1,21 +1,20 @@ +import Header from '@app/components/Common/Header'; +import ListView from '@app/components/Common/ListView'; +import PageTitle from '@app/components/Common/PageTitle'; +import useDiscover from '@app/hooks/useDiscover'; +import Error from '@app/pages/_error'; +import type { MovieDetails } from '@server/models/Movie'; +import type { MovieResult } from '@server/models/Search'; import Link from 'next/link'; import { useRouter } from 'next/router'; -import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; import useSWR from 'swr'; -import type { MovieDetails } from '../../../server/models/Movie'; -import type { MovieResult } from '../../../server/models/Search'; -import useDiscover from '../../hooks/useDiscover'; -import Error from '../../pages/_error'; -import Header from '../Common/Header'; -import ListView from '../Common/ListView'; -import PageTitle from '../Common/PageTitle'; const messages = defineMessages({ recommendations: 'Recommendations', }); -const MovieRecommendations: React.FC = () => { +const MovieRecommendations = () => { const intl = useIntl(); const router = useRouter(); const { data: movieData } = useSWR( diff --git a/src/components/MovieDetails/MovieSimilar.tsx b/src/components/MovieDetails/MovieSimilar.tsx index 8103f966e..5ce5ef1a1 100644 --- a/src/components/MovieDetails/MovieSimilar.tsx +++ b/src/components/MovieDetails/MovieSimilar.tsx @@ -1,21 +1,20 @@ +import Header from '@app/components/Common/Header'; +import ListView from '@app/components/Common/ListView'; +import PageTitle from '@app/components/Common/PageTitle'; +import useDiscover from '@app/hooks/useDiscover'; +import Error from '@app/pages/_error'; +import type { MovieDetails } from '@server/models/Movie'; +import type { MovieResult } from '@server/models/Search'; import Link from 'next/link'; import { useRouter } from 'next/router'; -import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; import useSWR from 'swr'; -import type { MovieDetails } from '../../../server/models/Movie'; -import type { MovieResult } from '../../../server/models/Search'; -import useDiscover from '../../hooks/useDiscover'; -import Error from '../../pages/_error'; -import Header from '../Common/Header'; -import ListView from '../Common/ListView'; -import PageTitle from '../Common/PageTitle'; const messages = defineMessages({ similar: 'Similar Titles', }); -const MovieSimilar: React.FC = () => { +const MovieSimilar = () => { const router = useRouter(); const intl = useIntl(); const { data: movieData } = useSWR( diff --git a/src/components/MovieDetails/index.tsx b/src/components/MovieDetails/index.tsx index 3e33c4b8a..d963585bc 100644 --- a/src/components/MovieDetails/index.tsx +++ b/src/components/MovieDetails/index.tsx @@ -1,3 +1,29 @@ +import RTAudFresh from '@app/assets/rt_aud_fresh.svg'; +import RTAudRotten from '@app/assets/rt_aud_rotten.svg'; +import RTFresh from '@app/assets/rt_fresh.svg'; +import RTRotten from '@app/assets/rt_rotten.svg'; +import TmdbLogo from '@app/assets/tmdb_logo.svg'; +import Button from '@app/components/Common/Button'; +import CachedImage from '@app/components/Common/CachedImage'; +import LoadingSpinner from '@app/components/Common/LoadingSpinner'; +import PageTitle from '@app/components/Common/PageTitle'; +import type { PlayButtonLink } from '@app/components/Common/PlayButton'; +import PlayButton from '@app/components/Common/PlayButton'; +import Tooltip from '@app/components/Common/Tooltip'; +import ExternalLinkBlock from '@app/components/ExternalLinkBlock'; +import IssueModal from '@app/components/IssueModal'; +import ManageSlideOver from '@app/components/ManageSlideOver'; +import MediaSlider from '@app/components/MediaSlider'; +import PersonCard from '@app/components/PersonCard'; +import RequestButton from '@app/components/RequestButton'; +import Slider from '@app/components/Slider'; +import StatusBadge from '@app/components/StatusBadge'; +import useLocale from '@app/hooks/useLocale'; +import useSettings from '@app/hooks/useSettings'; +import { Permission, useUser } from '@app/hooks/useUser'; +import globalMessages from '@app/i18n/globalMessages'; +import Error from '@app/pages/_error'; +import { sortCrewPriority } from '@app/utils/creditHelpers'; import { ArrowCircleRightIcon, CloudIcon, @@ -11,44 +37,20 @@ import { ChevronDoubleDownIcon, ChevronDoubleUpIcon, } from '@heroicons/react/solid'; +import type { RTRating } from '@server/api/rottentomatoes'; +import { IssueStatus } from '@server/constants/issue'; +import { MediaStatus } from '@server/constants/media'; +import { MediaServerType } from '@server/constants/server'; +import type { MovieDetails as MovieDetailsType } from '@server/models/Movie'; import { hasFlag } from 'country-flag-icons'; import 'country-flag-icons/3x2/flags.css'; import { uniqBy } from 'lodash'; +import getConfig from 'next/config'; import Link from 'next/link'; import { useRouter } from 'next/router'; -import React, { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import useSWR from 'swr'; -import type { RTRating } from '../../../server/api/rottentomatoes'; -import { IssueStatus } from '../../../server/constants/issue'; -import { MediaStatus } from '../../../server/constants/media'; -import { MediaServerType } from '../../../server/constants/server'; -import type { MovieDetails as MovieDetailsType } from '../../../server/models/Movie'; -import RTAudFresh from '../../assets/rt_aud_fresh.svg'; -import RTAudRotten from '../../assets/rt_aud_rotten.svg'; -import RTFresh from '../../assets/rt_fresh.svg'; -import RTRotten from '../../assets/rt_rotten.svg'; -import TmdbLogo from '../../assets/tmdb_logo.svg'; -import useLocale from '../../hooks/useLocale'; -import useSettings from '../../hooks/useSettings'; -import { Permission, useUser } from '../../hooks/useUser'; -import globalMessages from '../../i18n/globalMessages'; -import Error from '../../pages/_error'; -import { sortCrewPriority } from '../../utils/creditHelpers'; -import Button from '../Common/Button'; -import CachedImage from '../Common/CachedImage'; -import LoadingSpinner from '../Common/LoadingSpinner'; -import PageTitle from '../Common/PageTitle'; -import PlayButton, { PlayButtonLink } from '../Common/PlayButton'; -import ExternalLinkBlock from '../ExternalLinkBlock'; -import IssueModal from '../IssueModal'; -import ManageSlideOver from '../ManageSlideOver'; -import MediaSlider from '../MediaSlider'; -import PersonCard from '../PersonCard'; -import RequestButton from '../RequestButton'; -import Slider from '../Slider'; -import StatusBadge from '../StatusBadge'; -import getConfig from 'next/config'; const messages = defineMessages({ originaltitle: 'Original Title', @@ -78,13 +80,21 @@ const messages = defineMessages({ streamingproviders: 'Currently Streaming On', productioncountries: 'Production {countryCount, plural, one {Country} other {Countries}}', + theatricalrelease: 'Theatrical Release', + digitalrelease: 'Digital Release', + physicalrelease: 'Physical Release', + reportissue: 'Report an Issue', + managemovie: 'Manage Movie', + rtcriticsscore: 'Rotten Tomatoes Tomatometer', + rtaudiencescore: 'Rotten Tomatoes Audience Score', + tmdbuserscore: 'TMDB User Score', }); interface MovieDetailsProps { movie?: MovieDetailsType; } -const MovieDetails: React.FC = ({ movie }) => { +const MovieDetails = ({ movie }: MovieDetailsProps) => { const settings = useSettings(); const { user, hasPermission } = useUser(); const router = useRouter(); @@ -119,6 +129,32 @@ const MovieDetails: React.FC = ({ movie }) => { setShowManager(router.query.manage == '1' ? true : false); }, [router.query.manage]); + const [plexUrl, setPlexUrl] = useState(data?.mediaInfo?.mediaUrl); + const [plexUrl4k, setPlexUrl4k] = useState(data?.mediaInfo?.mediaUrl4k); + + useEffect(() => { + if (data) { + if ( + settings.currentSettings.mediaServerType === MediaServerType.PLEX && + (/iPad|iPhone|iPod/.test(navigator.userAgent) || + (navigator.userAgent === 'MacIntel' && navigator.maxTouchPoints > 1)) + ) { + setPlexUrl(data.mediaInfo?.iOSPlexUrl); + setPlexUrl4k(data.mediaInfo?.iOSPlexUrl4k); + } else { + setPlexUrl(data.mediaInfo?.mediaUrl); + setPlexUrl4k(data.mediaInfo?.mediaUrl4k); + } + } + }, [ + data, + data?.mediaInfo?.iOSPlexUrl, + data?.mediaInfo?.iOSPlexUrl4k, + data?.mediaInfo?.mediaUrl, + data?.mediaInfo?.mediaUrl4k, + settings.currentSettings.mediaServerType, + ]); + if (!data && !error) { return ; } @@ -130,27 +166,32 @@ const MovieDetails: React.FC = ({ movie }) => { const showAllStudios = data.productionCompanies.length <= minStudios + 1; const mediaLinks: PlayButtonLink[] = []; - if (data.mediaInfo?.mediaUrl) { + if ( + plexUrl && + hasPermission([Permission.REQUEST, Permission.REQUEST_MOVIE], { + type: 'or', + }) + ) { mediaLinks.push({ text: getAvalaibleMediaServerName(), - url: data.mediaInfo?.mediaUrl, + url: plexUrl, svg: , }); } if ( - data.mediaInfo?.mediaUrl4k && - hasPermission([Permission.REQUEST_4K, Permission.REQUEST_4K_TV], { + settings.currentSettings.movie4kEnabled && + plexUrl4k && + hasPermission([Permission.REQUEST_4K, Permission.REQUEST_4K_MOVIE], { type: 'or', }) ) { mediaLinks.push({ text: getAvalaible4kMediaServerName(), - url: data.mediaInfo?.mediaUrl4k, + url: plexUrl4k, svg: , }); } - const trailerUrl = data.relatedVideos ?.filter((r) => r.type === 'Trailer') .sort((a, b) => a.size - b.size) @@ -315,7 +356,8 @@ const MovieDetails: React.FC = ({ movie }) => { inProgress={(data.mediaInfo?.downloadStatus ?? []).length > 0} tmdbId={data.mediaInfo?.tmdbId} mediaType="movie" - plexUrl={data.mediaInfo?.mediaUrl} + plexUrl={plexUrl} + serviceUrl={data.mediaInfo?.serviceUrl} /> {settings.currentSettings.movie4kEnabled && hasPermission( @@ -336,11 +378,12 @@ const MovieDetails: React.FC = ({ movie }) => { } tmdbId={data.mediaInfo?.tmdbId} mediaType="movie" - plexUrl={data.mediaInfo?.mediaUrl4k} + plexUrl={plexUrl} + serviceUrl={data.mediaInfo?.serviceUrl4k} /> )}
        -

        +

        {data.title}{' '} {data.releaseDate && ( @@ -384,38 +427,42 @@ const MovieDetails: React.FC = ({ movie }) => { type: 'or', } ) && ( - + + + )} {hasPermission(Permission.MANAGE_REQUESTS) && data.mediaInfo && ( - + + + )}

        @@ -489,36 +536,55 @@ const MovieDetails: React.FC = ({ movie }) => { (ratingData?.audienceRating && !!ratingData?.audienceScore)) && ( )} @@ -548,22 +614,36 @@ const MovieDetails: React.FC = ({ movie }) => { > {r.type === 3 ? ( // Theatrical - + + + ) : r.type === 4 ? ( // Digital - + + + ) : ( // Physical - - - + + + + )} {intl.formatDate(r.release_date, { diff --git a/src/components/NotificationTypeSelector/NotificationType/index.tsx b/src/components/NotificationTypeSelector/NotificationType/index.tsx index 9662ebd36..f0e6cb059 100644 --- a/src/components/NotificationTypeSelector/NotificationType/index.tsx +++ b/src/components/NotificationTypeSelector/NotificationType/index.tsx @@ -1,5 +1,5 @@ -import React from 'react'; -import { hasNotificationType, NotificationItem } from '..'; +import type { NotificationItem } from '@app/components/NotificationTypeSelector'; +import { hasNotificationType } from '@app/components/NotificationTypeSelector'; interface NotificationTypeProps { option: NotificationItem; @@ -8,12 +8,12 @@ interface NotificationTypeProps { onUpdate: (newTypes: number) => void; } -const NotificationType: React.FC = ({ +const NotificationType = ({ option, currentTypes, onUpdate, parent, -}) => { +}: NotificationTypeProps) => { return ( <>
        = ({ +const NotificationTypeSelector = ({ user, enabledTypes = ALL_NOTIFICATIONS, currentTypes, onUpdate, error, -}) => { +}: NotificationTypeSelectorProps) => { const intl = useIntl(); const settings = useSettings(); const { hasPermission } = useUser({ id: user?.id }); @@ -190,6 +195,25 @@ const NotificationTypeSelector: React.FC = ({ )))); const types: NotificationItem[] = [ + { + id: 'media-auto-requested', + name: intl.formatMessage(messages.mediaautorequested), + description: intl.formatMessage(messages.mediaautorequestedDescription), + value: Notification.MEDIA_AUTO_REQUESTED, + hidden: + !user || + (!user.settings?.watchlistSyncMovies && + !user.settings?.watchlistSyncTv) || + !hasPermission( + [ + Permission.AUTO_REQUEST, + Permission.AUTO_REQUEST_MOVIE, + Permission.AUTO_REQUEST_TV, + ], + { type: 'or' } + ), + hasNotifyUser: true, + }, { id: 'media-requested', name: intl.formatMessage(messages.mediarequested), diff --git a/src/components/PWAHeader/index.tsx b/src/components/PWAHeader/index.tsx index 1c53abfb1..0dde7e421 100644 --- a/src/components/PWAHeader/index.tsx +++ b/src/components/PWAHeader/index.tsx @@ -1,12 +1,8 @@ -import React from 'react'; - interface PWAHeaderProps { applicationTitle?: string; } -const PWAHeader: React.FC = ({ - applicationTitle = 'Overseerr', -}) => { +const PWAHeader = ({ applicationTitle = 'Overseerr' }: PWAHeaderProps) => { return ( <> void; } -export const PermissionEdit: React.FC = ({ +export const PermissionEdit = ({ actingUser, currentUser, currentPermission, onUpdate, -}) => { +}: PermissionEditProps) => { const intl = useIntl(); const permissionList: PermissionItem[] = [ @@ -86,12 +99,6 @@ export const PermissionEdit: React.FC = ({ description: intl.formatMessage(messages.adminDescription), permission: Permission.ADMIN, }, - { - id: 'settings', - name: intl.formatMessage(messages.settings), - description: intl.formatMessage(messages.settingsDescription), - permission: Permission.MANAGE_SETTINGS, - }, { id: 'users', name: intl.formatMessage(messages.users), @@ -116,6 +123,18 @@ export const PermissionEdit: React.FC = ({ description: intl.formatMessage(messages.viewrequestsDescription), permission: Permission.REQUEST_VIEW, }, + { + id: 'viewrecent', + name: intl.formatMessage(messages.viewrecent), + description: intl.formatMessage(messages.viewrecentDescription), + permission: Permission.RECENT_VIEW, + }, + { + id: 'viewwatchlists', + name: intl.formatMessage(messages.viewwatchlists), + description: intl.formatMessage(messages.viewwatchlistsDescription), + permission: Permission.WATCHLIST_VIEW, + }, ], }, { @@ -175,6 +194,43 @@ export const PermissionEdit: React.FC = ({ }, ], }, + { + id: 'autorequest', + name: intl.formatMessage(messages.autorequest), + description: intl.formatMessage(messages.autorequestDescription), + permission: Permission.AUTO_REQUEST, + requires: [{ permissions: [Permission.REQUEST] }], + children: [ + { + id: 'autorequestmovies', + name: intl.formatMessage(messages.autorequestMovies), + description: intl.formatMessage( + messages.autorequestMoviesDescription + ), + permission: Permission.AUTO_REQUEST_MOVIE, + requires: [ + { + permissions: [Permission.REQUEST, Permission.REQUEST_MOVIE], + type: 'or', + }, + ], + }, + { + id: 'autorequesttv', + name: intl.formatMessage(messages.autorequestSeries), + description: intl.formatMessage( + messages.autorequestSeriesDescription + ), + permission: Permission.AUTO_REQUEST_TV, + requires: [ + { + permissions: [Permission.REQUEST, Permission.REQUEST_TV], + type: 'or', + }, + ], + }, + ], + }, { id: 'request4k', name: intl.formatMessage(messages.request4k), diff --git a/src/components/PermissionOption/index.tsx b/src/components/PermissionOption/index.tsx index 739234759..43d5128da 100644 --- a/src/components/PermissionOption/index.tsx +++ b/src/components/PermissionOption/index.tsx @@ -1,7 +1,7 @@ -import React from 'react'; -import { hasPermission } from '../../../server/lib/permissions'; -import useSettings from '../../hooks/useSettings'; -import { Permission, User } from '../../hooks/useUser'; +import useSettings from '@app/hooks/useSettings'; +import type { User } from '@app/hooks/useUser'; +import { Permission } from '@app/hooks/useUser'; +import { hasPermission } from '@server/lib/permissions'; export interface PermissionItem { id: string; @@ -26,14 +26,14 @@ interface PermissionOptionProps { onUpdate: (newPermissions: number) => void; } -const PermissionOption: React.FC = ({ +const PermissionOption = ({ option, actingUser, currentUser, currentPermission, onUpdate, parent, -}) => { +}: PermissionOptionProps) => { const settings = useSettings(); const autoApprovePermissions = [ @@ -66,14 +66,9 @@ const PermissionOption: React.FC = ({ } if ( - // Non-Admin users cannot modify the Admin permission - (actingUser && - !hasPermission(Permission.ADMIN, actingUser.permissions) && - option.permission === Permission.ADMIN) || - // Users without the Manage Settings permission cannot modify/grant that permission - (actingUser && - !hasPermission(Permission.MANAGE_SETTINGS, actingUser.permissions) && - option.permission === Permission.MANAGE_SETTINGS) + // Only the owner can modify the Admin permission + actingUser?.id !== 1 && + option.permission === Permission.ADMIN ) { disabled = true; } diff --git a/src/components/PersonCard/index.tsx b/src/components/PersonCard/index.tsx index 47fe56efc..c2b7b6422 100644 --- a/src/components/PersonCard/index.tsx +++ b/src/components/PersonCard/index.tsx @@ -1,7 +1,7 @@ +import CachedImage from '@app/components/Common/CachedImage'; import { UserCircleIcon } from '@heroicons/react/solid'; import Link from 'next/link'; -import React, { useState } from 'react'; -import CachedImage from '../Common/CachedImage'; +import { useState } from 'react'; interface PersonCardProps { personId: number; @@ -11,13 +11,13 @@ interface PersonCardProps { canExpand?: boolean; } -const PersonCard: React.FC = ({ +const PersonCard = ({ personId, name, subName, profilePath, canExpand = false, -}) => { +}: PersonCardProps) => { const [isHovered, setHovered] = useState(false); return ( diff --git a/src/components/PersonDetails/index.tsx b/src/components/PersonDetails/index.tsx index 173fc5dab..9c8173adc 100644 --- a/src/components/PersonDetails/index.tsx +++ b/src/components/PersonDetails/index.tsx @@ -1,19 +1,19 @@ +import Ellipsis from '@app/assets/ellipsis.svg'; +import CachedImage from '@app/components/Common/CachedImage'; +import ImageFader from '@app/components/Common/ImageFader'; +import LoadingSpinner from '@app/components/Common/LoadingSpinner'; +import PageTitle from '@app/components/Common/PageTitle'; +import TitleCard from '@app/components/TitleCard'; +import globalMessages from '@app/i18n/globalMessages'; +import Error from '@app/pages/_error'; +import type { PersonCombinedCreditsResponse } from '@server/interfaces/api/personInterfaces'; +import type { PersonDetails as PersonDetailsType } from '@server/models/Person'; import { groupBy } from 'lodash'; import { useRouter } from 'next/router'; -import React, { useMemo, useState } from 'react'; +import { useMemo, useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import TruncateMarkup from 'react-truncate-markup'; import useSWR from 'swr'; -import type { PersonCombinedCreditsResponse } from '../../../server/interfaces/api/personInterfaces'; -import type { PersonDetails as PersonDetailsType } from '../../../server/models/Person'; -import Ellipsis from '../../assets/ellipsis.svg'; -import globalMessages from '../../i18n/globalMessages'; -import Error from '../../pages/_error'; -import CachedImage from '../Common/CachedImage'; -import ImageFader from '../Common/ImageFader'; -import LoadingSpinner from '../Common/LoadingSpinner'; -import PageTitle from '../Common/PageTitle'; -import TitleCard from '../TitleCard'; const messages = defineMessages({ birthdate: 'Born {birthdate}', @@ -24,7 +24,7 @@ const messages = defineMessages({ ascharacter: 'as {character}', }); -const PersonDetails: React.FC = () => { +const PersonDetails = () => { const intl = useIntl(); const router = useRouter(); const { data, error } = useSWR( diff --git a/src/components/PlexLoginButton/index.tsx b/src/components/PlexLoginButton/index.tsx index 550938716..c89f10213 100644 --- a/src/components/PlexLoginButton/index.tsx +++ b/src/components/PlexLoginButton/index.tsx @@ -1,8 +1,8 @@ +import globalMessages from '@app/i18n/globalMessages'; +import PlexOAuth from '@app/utils/plex'; import { LoginIcon } from '@heroicons/react/outline'; -import React, { useState } from 'react'; +import { useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import globalMessages from '../../i18n/globalMessages'; -import PlexOAuth from '../../utils/plex'; const messages = defineMessages({ signinwithplex: 'Sign In', @@ -17,11 +17,11 @@ interface PlexLoginButtonProps { onError?: (message: string) => void; } -const PlexLoginButton: React.FC = ({ +const PlexLoginButton = ({ onAuthToken, onError, isProcessing, -}) => { +}: PlexLoginButtonProps) => { const intl = useIntl(); const [loading, setLoading] = useState(false); diff --git a/src/components/PullToRefresh/index.tsx b/src/components/PullToRefresh/index.tsx new file mode 100644 index 000000000..14bdfbda4 --- /dev/null +++ b/src/components/PullToRefresh/index.tsx @@ -0,0 +1,36 @@ +import { RefreshIcon } from '@heroicons/react/outline'; +import Router from 'next/router'; +import PR from 'pulltorefreshjs'; +import { useEffect } from 'react'; +import ReactDOMServer from 'react-dom/server'; + +const PullToRefresh: React.FC = () => { + useEffect(() => { + PR.init({ + mainElement: '#pull-to-refresh', + onRefresh() { + Router.reload(); + }, + iconArrow: ReactDOMServer.renderToString( + + ), + iconRefreshing: ReactDOMServer.renderToString( + + ), + instructionsPullToRefresh: ReactDOMServer.renderToString(
        ), + instructionsReleaseToRefresh: ReactDOMServer.renderToString(
        ), + instructionsRefreshing: ReactDOMServer.renderToString(
        ), + distReload: 55, + }); + return () => { + PR.destroyAll(); + }; + }, []); + + return
        ; +}; + +export default PullToRefresh; diff --git a/src/components/QuotaSelector/index.tsx b/src/components/QuotaSelector/index.tsx index 9ad39e221..7240dbc28 100644 --- a/src/components/QuotaSelector/index.tsx +++ b/src/components/QuotaSelector/index.tsx @@ -24,7 +24,7 @@ interface QuotaSelectorProps { onChange: (fieldName: string, value: number) => void; } -const QuotaSelector: React.FC = ({ +const QuotaSelector = ({ mediaType, dayFieldName, limitFieldName, @@ -34,7 +34,7 @@ const QuotaSelector: React.FC = ({ limitOverride, isDisabled = false, onChange, -}) => { +}: QuotaSelectorProps) => { const initialDays = defaultDays ?? 7; const initialLimit = defaultLimit ?? 0; const [quotaDays, setQuotaDays] = useState(initialDays); diff --git a/src/components/RegionSelector/index.tsx b/src/components/RegionSelector/index.tsx index 0c4bb2c6c..5a714c742 100644 --- a/src/components/RegionSelector/index.tsx +++ b/src/components/RegionSelector/index.tsx @@ -1,13 +1,13 @@ +import useSettings from '@app/hooks/useSettings'; import { Listbox, Transition } from '@headlessui/react'; import { CheckIcon, ChevronDownIcon } from '@heroicons/react/solid'; +import type { Region } from '@server/lib/settings'; import { hasFlag } from 'country-flag-icons'; import 'country-flag-icons/3x2/flags.css'; import { sortBy } from 'lodash'; -import React, { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import useSWR from 'swr'; -import type { Region } from '../../../server/lib/settings'; -import useSettings from '../../hooks/useSettings'; const messages = defineMessages({ regionDefault: 'All Regions', @@ -21,12 +21,12 @@ interface RegionSelectorProps { onChange?: (fieldName: string, region: string) => void; } -const RegionSelector: React.FC = ({ +const RegionSelector = ({ name, value, isUserSetting = false, onChange, -}) => { +}: RegionSelectorProps) => { const { currentSettings } = useSettings(); const intl = useIntl(); const { data: regions } = useSWR('/api/v1/regions'); diff --git a/src/components/RequestBlock/index.tsx b/src/components/RequestBlock/index.tsx index d1d4ae8a6..e6a0c02bb 100644 --- a/src/components/RequestBlock/index.tsx +++ b/src/components/RequestBlock/index.tsx @@ -1,3 +1,10 @@ +import Badge from '@app/components/Common/Badge'; +import Button from '@app/components/Common/Button'; +import Tooltip from '@app/components/Common/Tooltip'; +import RequestModal from '@app/components/RequestModal'; +import useRequestOverride from '@app/hooks/useRequestOverride'; +import { useUser } from '@app/hooks/useUser'; +import globalMessages from '@app/i18n/globalMessages'; import { CalendarIcon, CheckIcon, @@ -7,18 +14,12 @@ import { UserIcon, XIcon, } from '@heroicons/react/solid'; +import { MediaRequestStatus } from '@server/constants/media'; +import type { MediaRequest } from '@server/entity/MediaRequest'; import axios from 'axios'; import Link from 'next/link'; -import React, { useState } from 'react'; +import { useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { MediaRequestStatus } from '../../../server/constants/media'; -import type { MediaRequest } from '../../../server/entity/MediaRequest'; -import useRequestOverride from '../../hooks/useRequestOverride'; -import { useUser } from '../../hooks/useUser'; -import globalMessages from '../../i18n/globalMessages'; -import Badge from '../Common/Badge'; -import Button from '../Common/Button'; -import RequestModal from '../RequestModal'; const messages = defineMessages({ seasons: '{seasonCount, plural, one {Season} other {Seasons}}', @@ -27,6 +28,13 @@ const messages = defineMessages({ profilechanged: 'Quality Profile', rootfolder: 'Root Folder', languageprofile: 'Language Profile', + requestdate: 'Request Date', + requestedby: 'Requested By', + lastmodifiedby: 'Last Modified By', + approve: 'Approve Request', + decline: 'Decline Request', + edit: 'Edit Request', + delete: 'Delete Request', }); interface RequestBlockProps { @@ -34,7 +42,7 @@ interface RequestBlockProps { onUpdate?: () => void; } -const RequestBlock: React.FC = ({ request, onUpdate }) => { +const RequestBlock = ({ request, onUpdate }: RequestBlockProps) => { const { user } = useUser(); const intl = useIntl(); const [isUpdating, setIsUpdating] = useState(false); @@ -83,7 +91,9 @@ const RequestBlock: React.FC = ({ request, onUpdate }) => {
        - + + + = ({ request, onUpdate }) => {
        {request.modifiedBy && (
        - + + + = ({ request, onUpdate }) => {
        {request.status === MediaRequestStatus.PENDING && ( <> - - - + + + + + + + + + )} {request.status !== MediaRequestStatus.PENDING && ( - + + + )}
        @@ -179,10 +199,17 @@ const RequestBlock: React.FC = ({ request, onUpdate }) => { {intl.formatMessage(globalMessages.pending)} )} + {request.status === MediaRequestStatus.FAILED && ( + + {intl.formatMessage(globalMessages.failed)} + + )}
        - + + + {intl.formatDate(request.createdAt, { year: 'numeric', diff --git a/src/components/RequestButton/index.tsx b/src/components/RequestButton/index.tsx index 5ba5bf5d5..f71589448 100644 --- a/src/components/RequestButton/index.tsx +++ b/src/components/RequestButton/index.tsx @@ -1,23 +1,20 @@ +import ButtonWithDropdown from '@app/components/Common/ButtonWithDropdown'; +import RequestModal from '@app/components/RequestModal'; +import useSettings from '@app/hooks/useSettings'; +import { Permission, useUser } from '@app/hooks/useUser'; +import globalMessages from '@app/i18n/globalMessages'; import { DownloadIcon } from '@heroicons/react/outline'; import { CheckIcon, InformationCircleIcon, XIcon, } from '@heroicons/react/solid'; +import { MediaRequestStatus, MediaStatus } from '@server/constants/media'; +import type Media from '@server/entity/Media'; +import type { MediaRequest } from '@server/entity/MediaRequest'; import axios from 'axios'; -import React, { useMemo, useState } from 'react'; +import { useMemo, useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { - MediaRequestStatus, - MediaStatus, -} from '../../../server/constants/media'; -import Media from '../../../server/entity/Media'; -import { MediaRequest } from '../../../server/entity/MediaRequest'; -import useSettings from '../../hooks/useSettings'; -import { Permission, useUser } from '../../hooks/useUser'; -import globalMessages from '../../i18n/globalMessages'; -import ButtonWithDropdown from '../Common/ButtonWithDropdown'; -import RequestModal from '../RequestModal'; const messages = defineMessages({ viewrequest: 'View Request', @@ -54,14 +51,14 @@ interface RequestButtonProps { is4kShowComplete?: boolean; } -const RequestButton: React.FC = ({ +const RequestButton = ({ tmdbId, onUpdate, media, mediaType, isShowComplete = false, is4kShowComplete = false, -}) => { +}: RequestButtonProps) => { const intl = useIntl(); const settings = useSettings(); const { user, hasPermission } = useUser(); @@ -77,13 +74,13 @@ const RequestButton: React.FC = ({ (request) => request.status === MediaRequestStatus.PENDING && request.is4k ); + // Current user's pending request, or the first pending request const activeRequest = useMemo(() => { return activeRequests && activeRequests.length > 0 ? activeRequests.find((request) => request.requestedBy.id === user?.id) ?? activeRequests[0] : undefined; }, [activeRequests, user]); - const active4kRequest = useMemo(() => { return active4kRequests && active4kRequests.length > 0 ? active4kRequests.find( @@ -121,6 +118,151 @@ const RequestButton: React.FC = ({ }; const buttons: ButtonOption[] = []; + + // If there are pending requests, show request management options first + if (activeRequest || active4kRequest) { + if ( + activeRequest && + (activeRequest.requestedBy.id === user?.id || + (activeRequests?.length === 1 && + hasPermission(Permission.MANAGE_REQUESTS))) + ) { + buttons.push({ + id: 'active-request', + text: intl.formatMessage(messages.viewrequest), + action: () => { + setEditRequest(true); + setShowRequestModal(true); + }, + svg: , + }); + } + + if ( + activeRequest && + hasPermission(Permission.MANAGE_REQUESTS) && + mediaType === 'movie' + ) { + buttons.push( + { + id: 'approve-request', + text: intl.formatMessage(messages.approverequest), + action: () => { + modifyRequest(activeRequest, 'approve'); + }, + svg: , + }, + { + id: 'decline-request', + text: intl.formatMessage(messages.declinerequest), + action: () => { + modifyRequest(activeRequest, 'decline'); + }, + svg: , + } + ); + } else if ( + activeRequests && + activeRequests.length > 0 && + hasPermission(Permission.MANAGE_REQUESTS) && + mediaType === 'tv' + ) { + buttons.push( + { + id: 'approve-request-batch', + text: intl.formatMessage(messages.approverequests, { + requestCount: activeRequests.length, + }), + action: () => { + modifyRequests(activeRequests, 'approve'); + }, + svg: , + }, + { + id: 'decline-request-batch', + text: intl.formatMessage(messages.declinerequests, { + requestCount: activeRequests.length, + }), + action: () => { + modifyRequests(activeRequests, 'decline'); + }, + svg: , + } + ); + } + + if ( + active4kRequest && + (active4kRequest.requestedBy.id === user?.id || + (active4kRequests?.length === 1 && + hasPermission(Permission.MANAGE_REQUESTS))) + ) { + buttons.push({ + id: 'active-4k-request', + text: intl.formatMessage(messages.viewrequest4k), + action: () => { + setEditRequest(true); + setShowRequest4kModal(true); + }, + svg: , + }); + } + + if ( + active4kRequest && + hasPermission(Permission.MANAGE_REQUESTS) && + mediaType === 'movie' + ) { + buttons.push( + { + id: 'approve-4k-request', + text: intl.formatMessage(messages.approverequest4k), + action: () => { + modifyRequest(active4kRequest, 'approve'); + }, + svg: , + }, + { + id: 'decline-4k-request', + text: intl.formatMessage(messages.declinerequest4k), + action: () => { + modifyRequest(active4kRequest, 'decline'); + }, + svg: , + } + ); + } else if ( + active4kRequests && + active4kRequests.length > 0 && + hasPermission(Permission.MANAGE_REQUESTS) && + mediaType === 'tv' + ) { + buttons.push( + { + id: 'approve-4k-request-batch', + text: intl.formatMessage(messages.approve4krequests, { + requestCount: active4kRequests.length, + }), + action: () => { + modifyRequests(active4kRequests, 'approve'); + }, + svg: , + }, + { + id: 'decline-4k-request-batch', + text: intl.formatMessage(messages.decline4krequests, { + requestCount: active4kRequests.length, + }), + action: () => { + modifyRequests(active4kRequests, 'decline'); + }, + svg: , + } + ); + } + } + + // Standard request button if ( (!media || media.status === MediaStatus.UNKNOWN) && hasPermission( @@ -142,8 +284,28 @@ const RequestButton: React.FC = ({ }, svg: , }); + } else if ( + mediaType === 'tv' && + (!activeRequest || activeRequest.requestedBy.id !== user?.id) && + hasPermission([Permission.REQUEST, Permission.REQUEST_TV], { + type: 'or', + }) && + media && + media.status !== MediaStatus.AVAILABLE && + !isShowComplete + ) { + buttons.push({ + id: 'request-more', + text: intl.formatMessage(messages.requestmore), + action: () => { + setEditRequest(false); + setShowRequestModal(true); + }, + svg: , + }); } + // 4K request button if ( (!media || media.status4k === MediaStatus.UNKNOWN) && hasPermission( @@ -167,175 +329,7 @@ const RequestButton: React.FC = ({ }, svg: , }); - } - - if ( - activeRequest && - (activeRequest.requestedBy.id === user?.id || - (activeRequests?.length === 1 && - hasPermission(Permission.MANAGE_REQUESTS))) - ) { - buttons.push({ - id: 'active-request', - text: intl.formatMessage(messages.viewrequest), - action: () => { - setEditRequest(true); - setShowRequestModal(true); - }, - svg: , - }); - } - - if ( - active4kRequest && - (active4kRequest.requestedBy.id === user?.id || - (active4kRequests?.length === 1 && - hasPermission(Permission.MANAGE_REQUESTS))) - ) { - buttons.push({ - id: 'active-4k-request', - text: intl.formatMessage(messages.viewrequest4k), - action: () => { - setEditRequest(true); - setShowRequest4kModal(true); - }, - svg: , - }); - } - - if ( - activeRequest && - hasPermission(Permission.MANAGE_REQUESTS) && - mediaType === 'movie' - ) { - buttons.push( - { - id: 'approve-request', - text: intl.formatMessage(messages.approverequest), - action: () => { - modifyRequest(activeRequest, 'approve'); - }, - svg: , - }, - { - id: 'decline-request', - text: intl.formatMessage(messages.declinerequest), - action: () => { - modifyRequest(activeRequest, 'decline'); - }, - svg: , - } - ); - } - - if ( - activeRequests && - activeRequests.length > 0 && - hasPermission(Permission.MANAGE_REQUESTS) && - mediaType === 'tv' - ) { - buttons.push( - { - id: 'approve-request-batch', - text: intl.formatMessage(messages.approverequests, { - requestCount: activeRequests.length, - }), - action: () => { - modifyRequests(activeRequests, 'approve'); - }, - svg: , - }, - { - id: 'decline-request-batch', - text: intl.formatMessage(messages.declinerequests, { - requestCount: activeRequests.length, - }), - action: () => { - modifyRequests(activeRequests, 'decline'); - }, - svg: , - } - ); - } - - if ( - active4kRequest && - hasPermission(Permission.MANAGE_REQUESTS) && - mediaType === 'movie' - ) { - buttons.push( - { - id: 'approve-4k-request', - text: intl.formatMessage(messages.approverequest4k), - action: () => { - modifyRequest(active4kRequest, 'approve'); - }, - svg: , - }, - { - id: 'decline-4k-request', - text: intl.formatMessage(messages.declinerequest4k), - action: () => { - modifyRequest(active4kRequest, 'decline'); - }, - svg: , - } - ); - } - - if ( - active4kRequests && - active4kRequests.length > 0 && - hasPermission(Permission.MANAGE_REQUESTS) && - mediaType === 'tv' - ) { - buttons.push( - { - id: 'approve-4k-request-batch', - text: intl.formatMessage(messages.approve4krequests, { - requestCount: active4kRequests.length, - }), - action: () => { - modifyRequests(active4kRequests, 'approve'); - }, - svg: , - }, - { - id: 'decline-4k-request-batch', - text: intl.formatMessage(messages.decline4krequests, { - requestCount: active4kRequests.length, - }), - action: () => { - modifyRequests(active4kRequests, 'decline'); - }, - svg: , - } - ); - } - - if ( - mediaType === 'tv' && - (!activeRequest || activeRequest.requestedBy.id !== user?.id) && - hasPermission([Permission.REQUEST, Permission.REQUEST_TV], { - type: 'or', - }) && - media && - media.status !== MediaStatus.AVAILABLE && - media.status !== MediaStatus.UNKNOWN && - !isShowComplete - ) { - buttons.push({ - id: 'request-more', - text: intl.formatMessage(messages.requestmore), - action: () => { - setEditRequest(false); - setShowRequestModal(true); - }, - svg: , - }); - } - - if ( + } else if ( mediaType === 'tv' && (!active4kRequest || active4kRequest.requestedBy.id !== user?.id) && hasPermission([Permission.REQUEST_4K, Permission.REQUEST_4K_TV], { @@ -343,7 +337,6 @@ const RequestButton: React.FC = ({ }) && media && media.status4k !== MediaStatus.AVAILABLE && - media.status4k !== MediaStatus.UNKNOWN && !is4kShowComplete && settings.currentSettings.series4kEnabled ) { diff --git a/src/components/RequestCard/index.tsx b/src/components/RequestCard/index.tsx index 4ac1bfe9b..e59f164f3 100644 --- a/src/components/RequestCard/index.tsx +++ b/src/components/RequestCard/index.tsx @@ -1,3 +1,12 @@ +import Badge from '@app/components/Common/Badge'; +import Button from '@app/components/Common/Button'; +import CachedImage from '@app/components/Common/CachedImage'; +import Tooltip from '@app/components/Common/Tooltip'; +import RequestModal from '@app/components/RequestModal'; +import StatusBadge from '@app/components/StatusBadge'; +import { Permission, useUser } from '@app/hooks/useUser'; +import globalMessages from '@app/i18n/globalMessages'; +import { withProperties } from '@app/utils/typeHelpers'; import { CheckIcon, PencilIcon, @@ -5,33 +14,28 @@ import { TrashIcon, XIcon, } from '@heroicons/react/solid'; +import { MediaRequestStatus } from '@server/constants/media'; +import type { MediaRequest } from '@server/entity/MediaRequest'; +import type { MovieDetails } from '@server/models/Movie'; +import type { TvDetails } from '@server/models/Tv'; import axios from 'axios'; import Link from 'next/link'; -import React, { useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { useInView } from 'react-intersection-observer'; import { defineMessages, useIntl } from 'react-intl'; import { useToasts } from 'react-toast-notifications'; import useSWR, { mutate } from 'swr'; -import { - MediaRequestStatus, - MediaStatus, -} from '../../../server/constants/media'; -import type { MediaRequest } from '../../../server/entity/MediaRequest'; -import type { MovieDetails } from '../../../server/models/Movie'; -import type { TvDetails } from '../../../server/models/Tv'; -import { Permission, useUser } from '../../hooks/useUser'; -import globalMessages from '../../i18n/globalMessages'; -import { withProperties } from '../../utils/typeHelpers'; -import Badge from '../Common/Badge'; -import Button from '../Common/Button'; -import CachedImage from '../Common/CachedImage'; -import RequestModal from '../RequestModal'; -import StatusBadge from '../StatusBadge'; const messages = defineMessages({ seasons: '{seasonCount, plural, one {Season} other {Seasons}}', failedretry: 'Something went wrong while retrying the request.', - mediaerror: 'The associated title for this request is no longer available.', + mediaerror: '{mediaType} Not Found', + tmdbid: 'TMDB ID', + tvdbid: 'TheTVDB ID', + approverequest: 'Approve Request', + declinerequest: 'Decline Request', + editrequest: 'Edit Request', + cancelrequest: 'Cancel Request', deleterequest: 'Delete Request', }); @@ -39,7 +43,7 @@ const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => { return (movie as MovieDetails).title !== undefined; }; -const RequestCardPlaceholder: React.FC = () => { +const RequestCardPlaceholder = () => { return (
        @@ -50,37 +54,133 @@ const RequestCardPlaceholder: React.FC = () => { }; interface RequestCardErrorProps { - mediaId?: number; + requestData?: MediaRequest; } -const RequestCardError: React.FC = ({ mediaId }) => { +const RequestCardError = ({ requestData }: RequestCardErrorProps) => { const { hasPermission } = useUser(); const intl = useIntl(); const deleteRequest = async () => { - await axios.delete(`/api/v1/media/${mediaId}`); + await axios.delete(`/api/v1/media/${requestData?.media.id}`); + mutate('/api/v1/media?filter=allavailable&take=20&sort=mediaAdded'); mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0'); }; return ( -
        +
        -
        -
        - {intl.formatMessage(messages.mediaerror)} +
        +
        + {intl.formatMessage(messages.mediaerror, { + mediaType: intl.formatMessage( + requestData?.type + ? requestData?.type === 'movie' + ? globalMessages.movie + : globalMessages.tvshow + : globalMessages.request + ), + })}
        - {hasPermission(Permission.MANAGE_REQUESTS) && mediaId && ( - + {requestData && ( + <> + {hasPermission( + [Permission.MANAGE_REQUESTS, Permission.REQUEST_VIEW], + { type: 'or' } + ) && ( + + )} +
        + + {intl.formatMessage(globalMessages.status)} + + {requestData.status === MediaRequestStatus.DECLINED || + requestData.status === MediaRequestStatus.FAILED ? ( + + {requestData.status === MediaRequestStatus.DECLINED + ? intl.formatMessage(globalMessages.declined) + : intl.formatMessage(globalMessages.failed)} + + ) : ( + 0 + } + is4k={requestData.is4k} + plexUrl={ + requestData.is4k + ? requestData.media.mediaUrl4k + : requestData.media.mediaUrl + } + serviceUrl={ + requestData.is4k + ? requestData.media.serviceUrl4k + : requestData.media.serviceUrl + } + /> + )} +
        + )} +
        + {hasPermission(Permission.MANAGE_REQUESTS) && + requestData?.media.id && ( + <> + + + + + + )} +
        @@ -93,7 +193,7 @@ interface RequestCardProps { onTitleData?: (requestId: number, title: MovieDetails | TvDetails) => void; } -const RequestCard: React.FC = ({ request, onTitleData }) => { +const RequestCard = ({ request, onTitleData }: RequestCardProps) => { const { ref, inView } = useInView({ triggerOnce: true, }); @@ -168,7 +268,7 @@ const RequestCard: React.FC = ({ request, onTitleData }) => { } if (!title || !requestData) { - return ; + return ; } return ( @@ -185,7 +285,10 @@ const RequestCard: React.FC = ({ request, onTitleData }) => { setShowEditModal(false); }} /> -
        +
        {title.backdropPath && (
        = ({ request, onTitleData }) => { />
        )} -
        +
        {(isMovie(title) ? title.releaseDate : title.firstAirDate)?.slice( 0, @@ -275,8 +381,7 @@ const RequestCard: React.FC = ({ request, onTitleData }) => { {intl.formatMessage(globalMessages.declined)} - ) : requestData.media[requestData.is4k ? 'status4k' : 'status'] === - MediaStatus.UNKNOWN ? ( + ) : requestData.status === MediaRequestStatus.FAILED ? ( = ({ request, onTitleData }) => { tmdbId={requestData.media.tmdbId} mediaType={requestData.type} plexUrl={ - requestData.media[ - requestData.is4k ? 'mediaUrl4k' : 'mediaUrl' - ] + requestData.is4k + ? requestData.media.mediaUrl4k + : requestData.media.mediaUrl + } + serviceUrl={ + requestData.is4k + ? requestData.media.serviceUrl4k + : requestData.media.serviceUrl } /> )}
        - {requestData.media[requestData.is4k ? 'status4k' : 'status'] === - MediaStatus.UNKNOWN && - requestData.status !== MediaRequestStatus.DECLINED && + {requestData.status === MediaRequestStatus.FAILED && hasPermission(Permission.MANAGE_REQUESTS) && ( - +
        + + + + +
        +
        + + + + +
        )} {requestData.status === MediaRequestStatus.PENDING && @@ -356,33 +490,54 @@ const RequestCard: React.FC = ({ request, onTitleData }) => { requestData.requestedBy.id === user?.id && (requestData.type === 'tv' || hasPermission(Permission.REQUEST_ADVANCED)) && ( - +
        + {!hasPermission(Permission.MANAGE_REQUESTS) && ( + + )} + + + +
        )} {requestData.status === MediaRequestStatus.PENDING && !hasPermission(Permission.MANAGE_REQUESTS) && requestData.requestedBy.id === user?.id && ( - +
        + + + + +
        )}
        diff --git a/src/components/RequestList/RequestItem/index.tsx b/src/components/RequestList/RequestItem/index.tsx index 6c98281ec..877d85396 100644 --- a/src/components/RequestList/RequestItem/index.tsx +++ b/src/components/RequestList/RequestItem/index.tsx @@ -1,3 +1,11 @@ +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 RequestModal from '@app/components/RequestModal'; +import StatusBadge from '@app/components/StatusBadge'; +import { Permission, useUser } from '@app/hooks/useUser'; +import globalMessages from '@app/i18n/globalMessages'; import { CheckIcon, PencilIcon, @@ -5,28 +13,17 @@ import { TrashIcon, XIcon, } from '@heroicons/react/solid'; +import { MediaRequestStatus } from '@server/constants/media'; +import type { MediaRequest } from '@server/entity/MediaRequest'; +import type { MovieDetails } from '@server/models/Movie'; +import type { TvDetails } from '@server/models/Tv'; import axios from 'axios'; import Link from 'next/link'; -import React, { useState } from 'react'; +import { useState } from 'react'; import { useInView } from 'react-intersection-observer'; import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl'; import { useToasts } from 'react-toast-notifications'; import useSWR from 'swr'; -import { - MediaRequestStatus, - MediaStatus, -} from '../../../../server/constants/media'; -import type { MediaRequest } from '../../../../server/entity/MediaRequest'; -import type { MovieDetails } from '../../../../server/models/Movie'; -import type { TvDetails } from '../../../../server/models/Tv'; -import { Permission, useUser } from '../../../hooks/useUser'; -import globalMessages from '../../../i18n/globalMessages'; -import Badge from '../../Common/Badge'; -import Button from '../../Common/Button'; -import CachedImage from '../../Common/CachedImage'; -import ConfirmButton from '../../Common/ConfirmButton'; -import RequestModal from '../../RequestModal'; -import StatusBadge from '../../StatusBadge'; const messages = defineMessages({ seasons: '{seasonCount, plural, one {Season} other {Seasons}}', @@ -35,50 +32,227 @@ const messages = defineMessages({ requesteddate: 'Requested', modified: 'Modified', modifieduserdate: '{date} by {user}', - mediaerror: 'The associated title for this request is no longer available.', + mediaerror: '{mediaType} Not Found', editrequest: 'Edit Request', deleterequest: 'Delete Request', cancelRequest: 'Cancel Request', + tmdbid: 'TMDB ID', + tvdbid: 'TheTVDB ID', }); const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => { return (movie as MovieDetails).title !== undefined; }; -interface RequestItemErroProps { - mediaId?: number; +interface RequestItemErrorProps { + requestData?: MediaRequest; revalidateList: () => void; } -const RequestItemError: React.FC = ({ - mediaId, +const RequestItemError = ({ + requestData, revalidateList, -}) => { +}: RequestItemErrorProps) => { const intl = useIntl(); const { hasPermission } = useUser(); const deleteRequest = async () => { - await axios.delete(`/api/v1/media/${mediaId}`); + await axios.delete(`/api/v1/media/${requestData?.media.id}`); revalidateList(); }; return ( -
        - - {intl.formatMessage(messages.mediaerror)} - - {hasPermission(Permission.MANAGE_REQUESTS) && mediaId && ( -
        +
        +
        +
        +
        + {intl.formatMessage(messages.mediaerror, { + mediaType: intl.formatMessage( + requestData?.type + ? requestData?.type === 'movie' + ? globalMessages.movie + : globalMessages.tvshow + : globalMessages.request + ), + })} +
        + {requestData && hasPermission(Permission.MANAGE_REQUESTS) && ( + <> +
        + + {intl.formatMessage(messages.tmdbid)} + + + {requestData.media.tmdbId} + +
        + {requestData.media.tvdbId && ( +
        + + {intl.formatMessage(messages.tvdbid)} + + + {requestData?.media.tvdbId} + +
        + )} + + )} +
        +
        + {requestData && ( + <> +
        + + {intl.formatMessage(globalMessages.status)} + + {requestData.status === MediaRequestStatus.DECLINED || + requestData.status === MediaRequestStatus.FAILED ? ( + + {requestData.status === MediaRequestStatus.DECLINED + ? intl.formatMessage(globalMessages.declined) + : intl.formatMessage(globalMessages.failed)} + + ) : ( + 0 + } + is4k={requestData.is4k} + plexUrl={ + requestData.is4k + ? requestData.media.mediaUrl4k + : requestData.media.mediaUrl + } + serviceUrl={ + requestData.is4k + ? requestData.media.serviceUrl4k + : requestData.media.serviceUrl + } + /> + )} +
        +
        + {hasPermission( + [Permission.MANAGE_REQUESTS, Permission.REQUEST_VIEW], + { type: 'or' } + ) ? ( + <> + + {intl.formatMessage(messages.requested)} + + + {intl.formatMessage(messages.modifieduserdate, { + date: ( + + ), + user: ( + + + + + {requestData.requestedBy.displayName} + + + + ), + })} + + + ) : ( + <> + + {intl.formatMessage(messages.requesteddate)} + + + + + + )} +
        + {requestData.modifiedBy && ( +
        + + {intl.formatMessage(messages.modified)} + + + {intl.formatMessage(messages.modifieduserdate, { + date: ( + + ), + user: ( + + + + + {requestData.modifiedBy.displayName} + + + + ), + })} + +
        + )} + + )} +
        +
        +
        + {hasPermission(Permission.MANAGE_REQUESTS) && requestData?.media.id && ( -
        - )} + )} +
        ); }; @@ -88,10 +262,7 @@ interface RequestItemProps { revalidateList: () => void; } -const RequestItem: React.FC = ({ - request, - revalidateList, -}) => { +const RequestItem = ({ request, revalidateList }: RequestItemProps) => { const { ref, inView } = useInView({ triggerOnce: true, }); @@ -157,7 +328,7 @@ const RequestItem: React.FC = ({ if (!title || !requestData) { return ( ); @@ -276,9 +447,7 @@ const RequestItem: React.FC = ({ {intl.formatMessage(globalMessages.declined)} - ) : requestData.media[ - requestData.is4k ? 'status4k' : 'status' - ] === MediaStatus.UNKNOWN ? ( + ) : requestData.status === MediaRequestStatus.FAILED ? ( = ({ tmdbId={requestData.media.tmdbId} mediaType={requestData.type} plexUrl={ - requestData.media[ - requestData.is4k ? 'mediaUrl4k' : 'mediaUrl' - ] + requestData.is4k + ? requestData.media.mediaUrl4k + : requestData.media.mediaUrl + } + serviceUrl={ + requestData.is4k + ? requestData.media.serviceUrl4k + : requestData.media.serviceUrl } /> )} @@ -405,9 +579,7 @@ const RequestItem: React.FC = ({
        - {requestData.media[requestData.is4k ? 'status4k' : 'status'] === - MediaStatus.UNKNOWN && - requestData.status !== MediaRequestStatus.DECLINED && + {requestData.status === MediaRequestStatus.FAILED && hasPermission(Permission.MANAGE_REQUESTS) && (