Compare commits
39 Commits
prisma-(de
...
v1.0.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee14ff5a51 | ||
|
|
6b62d4b862 | ||
|
|
706fea0e97 | ||
|
|
80956d1a83 | ||
|
|
6d530d9028 | ||
|
|
f12237565f | ||
|
|
11f5594ed4 | ||
|
|
e4e58bee05 | ||
|
|
13ee3a836c | ||
|
|
3f16a353f5 | ||
|
|
9c43ba95e6 | ||
|
|
13fb6fd1a7 | ||
|
|
16e8e3a38e | ||
|
|
6fecdf094d | ||
|
|
69b271b018 | ||
|
|
d6ebd9a9b9 | ||
|
|
70dad332fc | ||
|
|
a65e430c60 | ||
|
|
18f4b67b72 | ||
|
|
506c31562a | ||
|
|
7a9d7a4834 | ||
|
|
902a033b8a | ||
|
|
00eb20aa5e | ||
|
|
a2c27cfa95 | ||
|
|
7122b4d08b | ||
|
|
b03b9b1dbb | ||
|
|
73672e29f8 | ||
|
|
cc5192209f | ||
|
|
278dcf4b44 | ||
|
|
36e092f225 | ||
|
|
46d5c737a2 | ||
|
|
cba4878db3 | ||
|
|
57cc48a699 | ||
|
|
84f488be06 | ||
|
|
f885f2a0f3 | ||
|
|
eef3e5ea4c | ||
|
|
8db821c1c1 | ||
|
|
a39b882f09 | ||
|
|
754dccc4bf |
@@ -540,11 +540,22 @@
|
||||
"code"
|
||||
]
|
||||
}
|
||||
{
|
||||
"login": "Fallenbagel",
|
||||
"name": "Mohamed Nuvaas",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/98979876?s=96&v=4",
|
||||
"profile": "https://github.com/nicospz",
|
||||
"contributions": [
|
||||
"code",
|
||||
"logo",
|
||||
"design"
|
||||
]
|
||||
}
|
||||
],
|
||||
"badgeTemplate": "<a href=\"#contributors-\"><img alt=\"All Contributors\" src=\"https://img.shields.io/badge/all_contributors-<%= contributors.length %>-orange.svg\"/></a>",
|
||||
"contributorsPerLine": 7,
|
||||
"projectName": "overseerr",
|
||||
"projectOwner": "sct",
|
||||
"projectName": "jellyseerr",
|
||||
"projectOwner": "Fallenbagel",
|
||||
"repoType": "github",
|
||||
"repoHost": "https://github.com",
|
||||
"skipCi": true
|
||||
|
||||
7
.env
@@ -1,7 +0,0 @@
|
||||
# Environment variables declared in this file are automatically made available to Prisma.
|
||||
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
|
||||
|
||||
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB (Preview) and CockroachDB (Preview).
|
||||
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
|
||||
|
||||
DATABASE_URL="postgresql://jumail:1DontGive1@localhost:5432/jellyseerr?schema=public"
|
||||
@@ -7,7 +7,6 @@ module.exports = {
|
||||
'plugin:jsx-a11y/recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
'prettier',
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 6,
|
||||
@@ -26,7 +25,6 @@ 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'],
|
||||
@@ -40,7 +38,7 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
],
|
||||
plugins: ['jsx-a11y', 'prettier', 'react-hooks', 'formatjs'],
|
||||
plugins: ['jsx-a11y', 'react-hooks', 'formatjs'],
|
||||
settings: {
|
||||
react: {
|
||||
pragma: 'React',
|
||||
|
||||
11
.github/CODEOWNERS
vendored
@@ -1,12 +1,7 @@
|
||||
# Global code ownership
|
||||
* @sct
|
||||
|
||||
# Documentation
|
||||
docs/ @TheCatLady @samwiseg0
|
||||
|
||||
# Snap-related files
|
||||
.github/workflows/snap.yaml @samwiseg0
|
||||
snap/ @samwiseg0
|
||||
- @Fallenbagel
|
||||
|
||||
# i18n locale files
|
||||
src/i18n/locale/ @sct @TheCatLady
|
||||
|
||||
src/i18n/locale/ @Fallenbagel
|
||||
|
||||
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -4,10 +4,4 @@
|
||||
|
||||
#### To-Dos
|
||||
|
||||
- [ ] Successful build `yarn build`
|
||||
- [ ] Translation keys `yarn i18n:extract`
|
||||
- [ ] Database migration (if required)
|
||||
|
||||
#### Issues Fixed or Closed
|
||||
|
||||
- Fixes #XXXX
|
||||
|
||||
35
.github/workflows/ci.yml
vendored
@@ -1,9 +1,9 @@
|
||||
name: Overseerr CI
|
||||
name: Jellyseerr CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
- "*"
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
@@ -11,11 +11,12 @@ on:
|
||||
jobs:
|
||||
test:
|
||||
name: Lint & Test Build
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-20.04
|
||||
container: node:14.17-alpine
|
||||
container: node:16.14-alpine
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2.3.4
|
||||
uses: actions/checkout@v3
|
||||
- name: Install dependencies
|
||||
env:
|
||||
HUSKY_SKIP_INSTALL: 1
|
||||
@@ -27,36 +28,29 @@ jobs:
|
||||
|
||||
build_and_push:
|
||||
name: Build & Publish Docker Images
|
||||
needs: test
|
||||
if: github.ref == 'refs/heads/develop' && !contains(github.event.head_commit.message, '[skip ci]')
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2.3.4
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1.2.0
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1.3.0
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v2.1.6
|
||||
uses: actions/cache@v2
|
||||
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.9.0
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v1.9.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v2.5.0
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
@@ -65,10 +59,7 @@ jobs:
|
||||
build-args: |
|
||||
COMMIT_TAG=${{ github.sha }}
|
||||
tags: |
|
||||
sctx/overseerr:develop
|
||||
sctx/overseerr:${{ github.sha }}
|
||||
ghcr.io/sct/overseerr:develop
|
||||
ghcr.io/sct/overseerr:${{ github.sha }}
|
||||
fallenbagel/jellyseerr:develop
|
||||
cache-from: type=local,src=/tmp/.buildx-cache
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
|
||||
- # Temporary fix
|
||||
@@ -86,7 +77,7 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Get Build Job Status
|
||||
uses: technote-space/workflow-conclusion-action@v2.1.6
|
||||
uses: technote-space/workflow-conclusion-action@v2
|
||||
- name: Combine Job Status
|
||||
id: status
|
||||
run: |
|
||||
|
||||
23
.github/workflows/deploy_docs.yml
vendored
@@ -1,23 +0,0 @@
|
||||
name: Deploy API Docs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Generate Swagger UI
|
||||
uses: Legion2/swagger-ui-action@v1.1.2
|
||||
with:
|
||||
output: swagger-ui
|
||||
spec-file: overseerr-api.yml
|
||||
- name: Deploy to GitHub Pages
|
||||
uses: peaceiris/actions-gh-pages@v3.8.0
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: swagger-ui
|
||||
cname: api-docs.overseerr.dev
|
||||
19
.github/workflows/invalid_template.yml
vendored
@@ -1,19 +0,0 @@
|
||||
name: 'Invalid Template'
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled, unlabeled, reopened]
|
||||
|
||||
jobs:
|
||||
support:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: dessant/support-requests@v2.0.1
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
support-label: 'invalid:template-incomplete'
|
||||
issue-comment: >
|
||||
:wave: @{issue-author}, please follow the template provided.
|
||||
close-issue: true
|
||||
lock-issue: true
|
||||
issue-lock-reason: 'resolved'
|
||||
23
.github/workflows/preview.yml
vendored
@@ -1,9 +1,9 @@
|
||||
name: Overseerr Preview
|
||||
name: Jellyseerr Preview
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'preview-*'
|
||||
- "preview-*"
|
||||
|
||||
jobs:
|
||||
build_and_push:
|
||||
@@ -11,27 +11,21 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2.3.4
|
||||
uses: actions/checkout@v3
|
||||
- name: Get the version
|
||||
id: get_version
|
||||
run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/}
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1.2.0
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1.3.0
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v1.9.0
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v1.9.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v2.5.0
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
@@ -40,5 +34,4 @@ jobs:
|
||||
build-args: |
|
||||
COMMIT_TAG=${{ github.sha }}
|
||||
tags: |
|
||||
sctx/overseerr:${{ steps.get_version.outputs.VERSION }}
|
||||
ghcr.io/sct/overseerr:${{ steps.get_version.outputs.VERSION }}
|
||||
fallenbagel/jellyseerr:${{ steps.get_version.outputs.VERSION }}
|
||||
|
||||
102
.github/workflows/release.yml
vendored
@@ -1,118 +1,40 @@
|
||||
name: Overseerr Release
|
||||
name: Jellyseer Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Lint & Test Build
|
||||
runs-on: ubuntu-20.04
|
||||
container: node:14.17-alpine
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2.3.4
|
||||
- name: Install dependencies
|
||||
env:
|
||||
HUSKY_SKIP_INSTALL: 1
|
||||
run: yarn
|
||||
- name: Lint
|
||||
run: yarn lint
|
||||
- name: Build
|
||||
run: yarn build
|
||||
|
||||
semantic-release:
|
||||
name: Tag and release latest version
|
||||
needs: test
|
||||
runs-on: ubuntu-20.04
|
||||
env:
|
||||
HUSKY: 0
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2.3.4
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14
|
||||
node-version: 16
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1.2.0
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1.3.0
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v1.9.0
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v1.9.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
- name: Release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
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@v2.3.4
|
||||
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.2.0
|
||||
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.3.0
|
||||
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
|
||||
@@ -120,7 +42,7 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Get Build Job Status
|
||||
uses: technote-space/workflow-conclusion-action@v2.1.6
|
||||
uses: technote-space/workflow-conclusion-action@v2
|
||||
- name: Combine Job Status
|
||||
id: status
|
||||
run: |
|
||||
|
||||
107
.github/workflows/snap.yaml
vendored
@@ -1,107 +0,0 @@
|
||||
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.9.0
|
||||
with:
|
||||
access_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
test:
|
||||
name: Lint & Test Build
|
||||
needs: jobs
|
||||
runs-on: ubuntu-20.04
|
||||
container: node:14.17-alpine
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2.3.4
|
||||
- name: Install dependencies
|
||||
env:
|
||||
HUSKY_SKIP_INSTALL: 1
|
||||
run: yarn
|
||||
- name: Lint
|
||||
run: yarn lint
|
||||
- name: Build
|
||||
run: yarn build
|
||||
|
||||
build-snap:
|
||||
name: Build Snap Package (${{ matrix.architecture }})
|
||||
needs: test
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
architecture:
|
||||
- amd64
|
||||
- arm64
|
||||
- armhf
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2.3.4
|
||||
- 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@v1.2.0
|
||||
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.3.0
|
||||
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@v2.1.6
|
||||
- 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
|
||||
7
.github/workflows/support.yml
vendored
@@ -8,7 +8,7 @@ jobs:
|
||||
support:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: dessant/support-requests@v2.0.1
|
||||
- uses: dessant/support-requests@v2
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
support-label: 'support'
|
||||
@@ -16,11 +16,10 @@ jobs:
|
||||
:wave: @{issue-author}, we use the issue tracker exclusively
|
||||
for bug reports and feature requests. However, this issue appears
|
||||
to be a support request. Please use our support channels
|
||||
to get help with Overseerr.
|
||||
to get help with Jellyseerr.
|
||||
|
||||
- [Discord](https://discord.gg/overseerr)
|
||||
- [Discord](https://discord.gg/ckbvBtDJgC)
|
||||
|
||||
- [GitHub Discussions](https://github.com/sct/overseerr/discussions)
|
||||
close-issue: true
|
||||
lock-issue: true
|
||||
issue-lock-reason: 'off-topic'
|
||||
|
||||
4
.husky/commit-msg
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
[[ -n $HUSKY_BYPASS ]] || commitlint -E HUSKY_GIT_PARAMS
|
||||
4
.husky/pre-commit
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npm test
|
||||
4
.husky/prepare-commit-msg
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
exec < /dev/tty && git cz --hook || true
|
||||
7
CHANGELOG.md
Normal file
@@ -0,0 +1,7 @@
|
||||
## [1.0.2](https://github.com/Fallenbagel/jellyseerr/compare/v1.0.1...v1.0.2) (2022-04-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix usertype from local user to mediaServerType ([6d530d9](https://github.com/Fallenbagel/jellyseerr/commit/6d530d90280c929c7e8b185eb379e98c2d0a8ef9))
|
||||
* relax jellyfin url validation to allow local domains ([9c43ba9](https://github.com/Fallenbagel/jellyseerr/commit/9c43ba95e6c368b87acbfa8bac16c385605e7502)), closes [#123](https://github.com/Fallenbagel/jellyseerr/issues/123)
|
||||
18
README.md
@@ -1,8 +1,8 @@
|
||||
<p align="center">
|
||||
<img src="./public/logo_full.svg" alt="Overseerr" style="margin: 20px 0;">
|
||||
<img src="https://raw.githubusercontent.com/Fallenbagel/jellyseerr/stable/public/logo.png" alt="Overseerr" style="margin: 20px 0;">
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://discord.gg/BHak4GCk"><img src="https://img.shields.io/badge/Discord-Chat-lightgrey" alt="Discord"></a>
|
||||
<a href="https://discord.gg/ckbvBtDJgC"><img src="https://img.shields.io/badge/Discord-Chat-lightgrey" alt="Discord"></a>
|
||||
</p>
|
||||
|
||||
**Jellyseerr** is a free and open source fork of Overseerr for managing requests for your media library. It integrates with your existing services, such as **[Sonarr](https://sonarr.tv/)**, **[Radarr](https://radarr.video/)**, and **[Jellyfin](https://jellyfin.org/)**!
|
||||
@@ -22,13 +22,14 @@ Check out our [issue tracker](https://github.com/Fallenbagel/jellyseerr/issues).
|
||||
|
||||
## Supported Architectures
|
||||
|
||||
Jellyseerr image support multiple architectures such as x86-64, arm64 and armv7.
|
||||
Jellyseerr image support multiple architectures such as x86-64, arm64 and armv7.
|
||||
**NOTE: `:arm` and `:armv7` tag has been deprecated and replaced with `:latest`.**
|
||||
|
||||
| **Architecture** | **Tag** |
|
||||
|------------------|---------|
|
||||
| x86-64 | latest |
|
||||
| ARM64 | arm |
|
||||
| ARMv7 | arm |
|
||||
| ---------------- | ------- |
|
||||
| x86-64 | latest |
|
||||
| ARM64 | latest |
|
||||
| ARMv7 | latest |
|
||||
|
||||
## Getting Started
|
||||
|
||||
@@ -37,13 +38,14 @@ https://hub.docker.com/r/fallenbagel/jellyseerr
|
||||
|
||||
## Support
|
||||
|
||||
- You can get support on [Discord](https://discord.gg/VpVnZ92yQK).
|
||||
- You can get support on [Discord](https://discord.gg/ckbvBtDJgC).
|
||||
- Bug reports and feature requests can be submitted via [GitHub Issues](https://github.com/sct/overseerr/issues).
|
||||
|
||||
<!-- markdownlint-restore -->
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
## Buy me a Coffee!
|
||||
|
||||
If you like jellyseerr and want to help maintain it, please buy me a coffee as it would help me out a lot!
|
||||
|
||||
[](https://www.buymeacoffee.com/fallen.bagel)
|
||||
|
||||
11
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "overseerr",
|
||||
"version": "0.1.0",
|
||||
"name": "jellyseerr",
|
||||
"version": "1.0.2",
|
||||
"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",
|
||||
@@ -19,7 +19,6 @@
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.4.1",
|
||||
"@heroicons/react": "^1.0.4",
|
||||
"@prisma/client": "^3.11.1",
|
||||
"@supercharge/request-ip": "^1.1.2",
|
||||
"@svgr/webpack": "^5.5.0",
|
||||
"@tanem/react-nprogress": "^3.0.79",
|
||||
@@ -47,7 +46,6 @@
|
||||
"nodemailer": "^6.6.3",
|
||||
"openpgp": "^5.0.0-3",
|
||||
"plex-api": "^5.3.1",
|
||||
"prisma": "^3.11.1",
|
||||
"pug": "^3.0.2",
|
||||
"react": "17.0.2",
|
||||
"react-ace": "^9.3.0",
|
||||
@@ -203,7 +201,7 @@
|
||||
]
|
||||
],
|
||||
"branches": [
|
||||
"master"
|
||||
"main"
|
||||
],
|
||||
"npmPublish": false,
|
||||
"publish": [
|
||||
@@ -213,8 +211,7 @@
|
||||
"COMMIT_TAG": "$GITHUB_SHA"
|
||||
},
|
||||
"imageNames": [
|
||||
"sctx/overseerr",
|
||||
"ghcr.io/sct/overseerr"
|
||||
"fallenbagel/jellyseerr"
|
||||
],
|
||||
"platforms": [
|
||||
"linux/amd64",
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "media" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"mediaType" TEXT NOT NULL,
|
||||
"tmdbId" INTEGER NOT NULL,
|
||||
"tvdbId" INTEGER,
|
||||
"imdbId" TEXT,
|
||||
"status" INTEGER NOT NULL DEFAULT 1,
|
||||
"status4k" INTEGER NOT NULL DEFAULT 1,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"lastSeasonChange" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"mediaAddedAt" TIMESTAMP(3),
|
||||
"serviceId" INTEGER,
|
||||
"serviceId4k" INTEGER,
|
||||
"externalServiceId" INTEGER,
|
||||
"externalServiceId4k" INTEGER,
|
||||
"externalServiceSlug" TEXT,
|
||||
"externalServiceSlug4k" TEXT,
|
||||
"ratingKey" TEXT,
|
||||
"ratingKey4k" TEXT,
|
||||
"jellyfinMediaId" TEXT,
|
||||
"jellyfinMediaId4k" TEXT,
|
||||
|
||||
CONSTRAINT "media_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "media_request" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"status" INTEGER NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"type" TEXT NOT NULL,
|
||||
"mediaId" INTEGER,
|
||||
"requestedById" INTEGER,
|
||||
"modifiedById" INTEGER,
|
||||
"is4k" BOOLEAN NOT NULL DEFAULT false,
|
||||
"serverId" INTEGER,
|
||||
"profileId" INTEGER,
|
||||
"rootFolder" TEXT,
|
||||
"languageProfileId" INTEGER,
|
||||
|
||||
CONSTRAINT "media_request_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "migrations" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"timestamp" BIGINT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "migrations_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "season" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"seasonNumber" INTEGER NOT NULL,
|
||||
"status" INTEGER NOT NULL DEFAULT 1,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"mediaId" INTEGER,
|
||||
"status4k" INTEGER NOT NULL DEFAULT 1,
|
||||
|
||||
CONSTRAINT "season_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "season_request" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"seasonNumber" INTEGER NOT NULL,
|
||||
"status" INTEGER NOT NULL DEFAULT 1,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"requestId" INTEGER,
|
||||
|
||||
CONSTRAINT "season_request_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "session" (
|
||||
"expiredAt" BIGINT NOT NULL,
|
||||
"id" TEXT NOT NULL,
|
||||
"json" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "session_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "user" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"username" TEXT,
|
||||
"plexId" INTEGER,
|
||||
"plexToken" TEXT,
|
||||
"permissions" INTEGER NOT NULL DEFAULT 0,
|
||||
"avatar" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"password" TEXT,
|
||||
"userType" INTEGER NOT NULL DEFAULT 1,
|
||||
"plexUsername" TEXT,
|
||||
"resetPasswordGuid" TEXT,
|
||||
"recoveryLinkExpirationDate" TIMESTAMP(3),
|
||||
"jellyfinUsername" TEXT,
|
||||
"jellyfinAuthToken" TEXT,
|
||||
"jellyfinUserId" TEXT,
|
||||
"jellyfinDeviceId" TEXT,
|
||||
|
||||
CONSTRAINT "user_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "user_settings" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"enableNotifications" BOOLEAN NOT NULL DEFAULT true,
|
||||
"discordId" TEXT,
|
||||
"userId" INTEGER,
|
||||
"region" TEXT,
|
||||
"originalLanguage" TEXT,
|
||||
|
||||
CONSTRAINT "user_settings_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "sqlite_autoindex_media_1" ON "media"("tvdbId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "IDX_7ff2d11f6a83cb52386eaebe74" ON "media"("imdbId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "IDX_41a289eb1fa489c1bc6f38d9c3" ON "media"("tvdbId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5" ON "media"("tmdbId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "IDX_28c5d1d16da7908c97c9bc2f74" ON "session"("expiredAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "sqlite_autoindex_user_1" ON "user"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "sqlite_autoindex_user_settings_1" ON "user_settings"("userId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "media_request" ADD CONSTRAINT "media_request_mediaId_fkey" FOREIGN KEY ("mediaId") REFERENCES "media"("id") ON DELETE CASCADE ON UPDATE NO ACTION;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "media_request" ADD CONSTRAINT "media_request_modifiedById_fkey" FOREIGN KEY ("modifiedById") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "media_request" ADD CONSTRAINT "media_request_requestedById_fkey" FOREIGN KEY ("requestedById") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "season" ADD CONSTRAINT "season_mediaId_fkey" FOREIGN KEY ("mediaId") REFERENCES "media"("id") ON DELETE CASCADE ON UPDATE NO ACTION;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "season_request" ADD CONSTRAINT "season_request_requestId_fkey" FOREIGN KEY ("requestId") REFERENCES "media_request"("id") ON DELETE CASCADE ON UPDATE NO ACTION;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "user_settings" ADD CONSTRAINT "user_settings_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION;
|
||||
@@ -1,3 +0,0 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "postgresql"
|
||||
@@ -1,127 +0,0 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgres"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model media {
|
||||
id Int @id @default(autoincrement())
|
||||
mediaType String
|
||||
tmdbId Int
|
||||
tvdbId Int? @unique(map: "sqlite_autoindex_media_1")
|
||||
imdbId String?
|
||||
status Int @default(1)
|
||||
status4k Int @default(1)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now())
|
||||
lastSeasonChange DateTime @default(now())
|
||||
mediaAddedAt DateTime?
|
||||
serviceId Int?
|
||||
serviceId4k Int?
|
||||
externalServiceId Int?
|
||||
externalServiceId4k Int?
|
||||
externalServiceSlug String?
|
||||
externalServiceSlug4k String?
|
||||
ratingKey String?
|
||||
ratingKey4k String?
|
||||
jellyfinMediaId String?
|
||||
jellyfinMediaId4k String?
|
||||
media_request media_request[]
|
||||
season season[]
|
||||
|
||||
@@index([imdbId], map: "IDX_7ff2d11f6a83cb52386eaebe74")
|
||||
@@index([tvdbId], map: "IDX_41a289eb1fa489c1bc6f38d9c3")
|
||||
@@index([tmdbId], map: "IDX_7157aad07c73f6a6ae3bbd5ef5")
|
||||
}
|
||||
|
||||
model media_request {
|
||||
id Int @id @default(autoincrement())
|
||||
status Int
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now())
|
||||
type String
|
||||
mediaId Int?
|
||||
requestedById Int?
|
||||
modifiedById Int?
|
||||
is4k Boolean @default(false)
|
||||
serverId Int?
|
||||
profileId Int?
|
||||
rootFolder String?
|
||||
languageProfileId Int?
|
||||
media media? @relation(fields: [mediaId], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
user_media_request_modifiedByIdTouser user? @relation("media_request_modifiedByIdTouser", fields: [modifiedById], references: [id], onUpdate: NoAction)
|
||||
user_media_request_requestedByIdTouser user? @relation("media_request_requestedByIdTouser", fields: [requestedById], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
season_request season_request[]
|
||||
}
|
||||
|
||||
model migrations {
|
||||
id Int @id @default(autoincrement())
|
||||
timestamp BigInt
|
||||
name String
|
||||
}
|
||||
|
||||
model season {
|
||||
id Int @id @default(autoincrement())
|
||||
seasonNumber Int
|
||||
status Int @default(1)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now())
|
||||
mediaId Int?
|
||||
status4k Int @default(1)
|
||||
media media? @relation(fields: [mediaId], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
}
|
||||
|
||||
model season_request {
|
||||
id Int @id @default(autoincrement())
|
||||
seasonNumber Int
|
||||
status Int @default(1)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now())
|
||||
requestId Int?
|
||||
media_request media_request? @relation(fields: [requestId], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
}
|
||||
|
||||
model session {
|
||||
expiredAt BigInt
|
||||
id String @id
|
||||
json String
|
||||
|
||||
@@index([expiredAt], map: "IDX_28c5d1d16da7908c97c9bc2f74")
|
||||
}
|
||||
|
||||
model user {
|
||||
id Int @id @default(autoincrement())
|
||||
email String @unique(map: "sqlite_autoindex_user_1")
|
||||
username String?
|
||||
plexId Int?
|
||||
plexToken String?
|
||||
permissions Int @default(0)
|
||||
avatar String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now())
|
||||
password String?
|
||||
userType Int @default(1)
|
||||
plexUsername String?
|
||||
resetPasswordGuid String?
|
||||
recoveryLinkExpirationDate DateTime?
|
||||
jellyfinUsername String?
|
||||
jellyfinAuthToken String?
|
||||
jellyfinUserId String?
|
||||
jellyfinDeviceId String?
|
||||
media_request_media_request_modifiedByIdTouser media_request[] @relation("media_request_modifiedByIdTouser")
|
||||
media_request_media_request_requestedByIdTouser media_request[] @relation("media_request_requestedByIdTouser")
|
||||
user_settings user_settings?
|
||||
}
|
||||
|
||||
model user_settings {
|
||||
id Int @id @default(autoincrement())
|
||||
enableNotifications Boolean @default(true)
|
||||
discordId String?
|
||||
userId Int? @unique(map: "sqlite_autoindex_user_settings_1")
|
||||
region String?
|
||||
originalLanguage String?
|
||||
user user? @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
}
|
||||
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 161 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 164 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 170 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 186 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 175 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 774 B After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 5.5 KiB |
@@ -1 +1,45 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" fill="none" viewBox="0 0 96 96"><path fill="url(#paint0_linear)" fill-rule="evenodd" d="M48 96C74.5097 96 96 74.5097 96 48C96 21.4903 74.5097 0 48 0C21.4903 0 0 21.4903 0 48C0 74.5097 21.4903 96 48 96ZM80.0001 52C80.0001 67.464 67.4641 80 52.0001 80C36.5361 80 24.0001 67.464 24.0001 52C24.0001 49.1303 24.4318 46.3615 25.2338 43.7548C27.4288 48.6165 32.3194 52 38.0001 52C45.7321 52 52.0001 45.732 52.0001 38C52.0001 32.3192 48.6166 27.4287 43.755 25.2337C46.3616 24.4317 49.1304 24 52.0001 24C67.4641 24 80.0001 36.536 80.0001 52Z" clip-rule="evenodd"/><path fill="#131928" fill-rule="evenodd" d="M80.0002 52C80.0002 67.464 67.4642 80 52.0002 80C36.864 80 24.5329 67.9897 24.017 52.9791C24.0057 53.318 24 53.6583 24 54C24 70.5685 37.4315 84 54 84C70.5685 84 84 70.5685 84 54C84 37.4315 70.5685 24 54 24C53.6597 24 53.3207 24.0057 52.9831 24.0169C67.9919 24.5347 80.0002 36.865 80.0002 52Z" clip-rule="evenodd" opacity=".2"/><path fill="url(#paint1_linear)" fill-rule="evenodd" d="M48 12C28.1177 12 12 28.1177 12 48C12 50.2091 10.2091 52 8 52C5.79086 52 4 50.2091 4 48C4 23.6995 23.6995 4 48 4C50.2091 4 52 5.79086 52 8C52 10.2091 50.2091 12 48 12Z" clip-rule="evenodd"/><defs><linearGradient id="paint0_linear" x1="48" x2="117.5" y1="0" y2="69.5" gradientUnits="userSpaceOnUse"><stop stop-color="#C395FC"/><stop offset="1" stop-color="#4F65F5"/></linearGradient><linearGradient id="paint1_linear" x1="28" x2="28" y1="8" y2="48" gradientUnits="userSpaceOnUse"><stop stop-color="#fff" stop-opacity=".4"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></linearGradient></defs></svg>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 26.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 96 96" style="enable-background:new 0 0 96 96;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{opacity:0.2;fill-rule:evenodd;clip-rule:evenodd;fill:#131928;enable-background:new ;}
|
||||
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_1_);}
|
||||
.st2{fill:#000B25;}
|
||||
.st3{fill:#FFFFFF;}
|
||||
.st4{fill:url(#SVGID_00000095336865710271825490000009683653333454385338_);}
|
||||
.st5{fill-rule:evenodd;clip-rule:evenodd;fill:#1D1D1B;}
|
||||
</style>
|
||||
<path class="st0" d="M80,52c0,15.5-12.5,28-28,28c-15.1,0-27.5-12-28-27c0,0.3,0,0.7,0,1c0,16.6,13.4,30,30,30s30-13.4,30-30
|
||||
S70.6,24,54,24c-0.3,0-0.7,0-1,0C68,24.5,80,36.9,80,52z"/>
|
||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="28" y1="90" x2="28" y2="50" gradientTransform="matrix(1 0 0 -1 0 98)">
|
||||
<stop offset="0" style="stop-color:#FFFFFF;stop-opacity:0.4"/>
|
||||
<stop offset="1" style="stop-color:#FFFFFF;stop-opacity:0"/>
|
||||
</linearGradient>
|
||||
<path class="st1" d="M48,12c-19.9,0-36,16.1-36,36c0,2.2-1.8,4-4,4c-2.2,0-4-1.8-4-4C4,23.7,23.7,4,48,4c2.2,0,4,1.8,4,4
|
||||
C52,10.2,50.2,12,48,12z"/>
|
||||
<ellipse class="st2" cx="48" cy="48" rx="48" ry="46.1"/>
|
||||
<rect x="34.8" y="21.7" class="st3" width="12.9" height="5.3"/>
|
||||
<linearGradient id="SVGID_00000124123951135562400370000011621398912477202562_" gradientUnits="userSpaceOnUse" x1="-178.0748" y1="163.4019" x2="-111.501" y2="124.9661" gradientTransform="matrix(1 0 0 -1 194 202)">
|
||||
<stop offset="0" style="stop-color:#FFFFFF"/>
|
||||
<stop offset="0" style="stop-color:#A85DC3"/>
|
||||
<stop offset="0.15" style="stop-color:#9863C5"/>
|
||||
<stop offset="0.43" style="stop-color:#6F74CB"/>
|
||||
<stop offset="0.83" style="stop-color:#2D90D5"/>
|
||||
<stop offset="1" style="stop-color:#0F9DDA"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#SVGID_00000124123951135562400370000011621398912477202562_);" d="M82.4,51.4C82,33.8,65.4,17.2,48.2,17
|
||||
C30.8,16.8,13.9,33,13,50.5c-0.4,8.2,0.2,9,8.5,10.4c0.9,9.2-0.5,14.4-6.2,20.8c0.8,0.7,1.5,1.3,2.4,2c0.2-0.2,0.4-0.4,0.5-0.7
|
||||
c5.1-7,7.6-9.9,7-18c-0.4-5.4,1.1-7.9,6.9-7.6c0.9,10.8-1.4,16.1-5.9,26.2c-0.7,1.5-1.4,2.9-2.3,4.2c1.1,0.6,2.2,1.2,3.3,1.7
|
||||
c1.2-2,2.1-4.2,2.8-6.6c2.6-9.2,4.8-13.8,5.7-23.4c0.3-3.7,1.4-5.1,5.5-4.2c1.4,11.7-1.2,17.9-4.8,29.2c-0.8,2.6-1.8,5-3,7.3
|
||||
c1.1,0.3,2.2,0.6,3.3,0.9c0.7-1.4,1.4-2.9,2-4.4c4.4-11.3,7.1-17.9,6.3-29.9c-0.3-4.4,1.4-5.2,4.8-3.9c0.5,8.6-1.1,11.6-0.1,19.9
|
||||
c0.8,7.2,2.8,13.5,5.7,19c1.1-0.2,2.1-0.4,3.1-0.6c-5.1-10.1-4.9-22.8-4.7-37.5c4.4-0.9,5.6,0.3,5.8,4.1c0.5,8.5-0.7,11.7,1,20
|
||||
c0.9,4.2,2.5,8,4.9,11.3c1.1-0.4,2.3-0.9,3.4-1.4c-7.3-8.5-6-18.8-5.9-32.2c7.1,1.4,6.5,1.5,7.1,7.5c0.8,7.6,1.3,8.6,3.3,16
|
||||
c0.3,0.9,1.3,2.7,2.5,4.6c1-0.7,1.9-1.4,2.8-2.1c-5.2-10.7-6.4-15-4.5-22.7C81.9,59.8,82.6,59,82.4,51.4L82.4,51.4z M19.6,51.1
|
||||
C17.6,42.4,25.1,29,32,28C28,35.4,23.8,43.3,19.6,51.1L19.6,51.1z M35.4,27.2c-0.2-0.9-0.5-1.8-0.7-2.7c4.1-1,8.2-2,12.3-3l0.7,2.7
|
||||
C43.5,25.2,39.4,26.2,35.4,27.2z"/>
|
||||
<path class="st3" d="M19.6,51.1C17.6,42.4,25.1,29,32,28C28,35.4,23.8,43.3,19.6,51.1L19.6,51.1z"/>
|
||||
<path class="st5" d="M48,30.2c-1.6,0-3.1,0.5-4.3,1.4c0.2,0,0.4,0,0.5,0c2.1,0,3.8,1.7,3.8,3.8S46.3,39,44.2,39
|
||||
c-1.7,0-3.2-1.1-3.6-2.7c-0.1,0.4-0.1,0.9-0.1,1.4c0,4.1,3.4,7.5,7.5,7.5s7.5-3.4,7.5-7.5S52.1,30.2,48,30.2L48,30.2z"/>
|
||||
<circle class="st3" cx="44.3" cy="35.1" r="3.7"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 13 KiB |
BIN
public/os_logo_square.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 504 KiB After Width: | Height: | Size: 1.7 MiB |
3
public/preview.jpg:Zone.Identifier
Normal file
@@ -0,0 +1,3 @@
|
||||
[ZoneTransfer]
|
||||
LastWriterPackageFamilyName=Microsoft.ScreenSketch_8wekyb3d8bbwe
|
||||
ZoneId=3
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Overseerr",
|
||||
"short_name": "Overseerr",
|
||||
"name": "Jellyseerr",
|
||||
"short_name": "Jellyseerr",
|
||||
"start_url": "./",
|
||||
"icons": [
|
||||
{
|
||||
|
||||
@@ -83,7 +83,7 @@ class GithubAPI extends ExternalAPI {
|
||||
} = {}): Promise<GitHubRelease[]> {
|
||||
try {
|
||||
const data = await this.get<GitHubRelease[]>(
|
||||
'/repos/sct/overseerr/releases',
|
||||
'/repos/Fallenbagel/jellyseerr/releases',
|
||||
{
|
||||
params: {
|
||||
per_page: take,
|
||||
@@ -110,7 +110,7 @@ class GithubAPI extends ExternalAPI {
|
||||
} = {}): Promise<GithubCommit[]> {
|
||||
try {
|
||||
const data = await this.get<GithubCommit[]>(
|
||||
'/repos/sct/overseerr/commits',
|
||||
'/repos/Fallenbagel/jellyseerr/commits',
|
||||
{
|
||||
params: {
|
||||
per_page: take,
|
||||
@@ -122,7 +122,7 @@ class GithubAPI extends ExternalAPI {
|
||||
return data;
|
||||
} catch (e) {
|
||||
logger.warn(
|
||||
"Failed to retrieve GitHub commits. This may be an issue on GitHub's end. Overseerr can't check if it's on the latest version.",
|
||||
"Failed to retrieve GitHub commits. This may be an issue on GitHub's end. Jellyseerr can't check if it's on the latest version.",
|
||||
{ label: 'GitHub API', errorMessage: e.message }
|
||||
);
|
||||
return [];
|
||||
|
||||
@@ -81,9 +81,9 @@ class JellyfinAPI {
|
||||
|
||||
let authHeaderVal = '';
|
||||
if (this.authToken) {
|
||||
authHeaderVal = `MediaBrowser Client="Overseerr", Device="Axios", DeviceId="${deviceId}", Version="10.8.0", Token="${authToken}"`;
|
||||
authHeaderVal = `MediaBrowser Client="Jellyseerr", Device="Axios", DeviceId="${deviceId}", Version="10.8.0", Token="${authToken}"`;
|
||||
} else {
|
||||
authHeaderVal = `MediaBrowser Client="Overseerr", Device="Axios", DeviceId="${deviceId}", Version="10.8.0"`;
|
||||
authHeaderVal = `MediaBrowser Client="Jellyseerr", Device="Axios", DeviceId="${deviceId}", Version="10.8.0"`;
|
||||
}
|
||||
|
||||
this.axios = axios.create({
|
||||
|
||||
@@ -122,9 +122,9 @@ class PlexAPI {
|
||||
// },
|
||||
options: {
|
||||
identifier: settings.clientId,
|
||||
product: 'Overseerr',
|
||||
deviceName: 'Overseerr',
|
||||
platform: 'Overseerr',
|
||||
product: 'Jellyseerr',
|
||||
deviceName: 'Jellyseerr',
|
||||
platform: 'Jellyseerr',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -163,11 +163,12 @@ class Media {
|
||||
this.mediaUrl4k = `https://app.plex.tv/desktop#!/server/${settings.plex.machineId}/details?key=%2Flibrary%2Fmetadata%2F${this.ratingKey4k}`;
|
||||
}
|
||||
} else {
|
||||
const pageName = process.env.JELLYFIN_TYPE === 'emby' ? 'item' : 'details';
|
||||
if (this.jellyfinMediaId) {
|
||||
this.mediaUrl = `${settings.jellyfin.hostname}/web/index.html#!/details?id=${this.jellyfinMediaId}&context=home&serverId=${settings.jellyfin.serverId}`;
|
||||
this.mediaUrl = `${settings.jellyfin.hostname}/web/index.html#!/${pageName}?id=${this.jellyfinMediaId}&context=home&serverId=${settings.jellyfin.serverId}`;
|
||||
}
|
||||
if (this.jellyfinMediaId4k) {
|
||||
this.mediaUrl4k = `${settings.jellyfin.hostname}/web/index.html#!/details?id=${this.jellyfinMediaId4k}&context=home&serverId=${settings.jellyfin.serverId}`;
|
||||
this.mediaUrl4k = `${settings.jellyfin.hostname}/web/index.html#!/${pageName}?id=${this.jellyfinMediaId4k}&context=home&serverId=${settings.jellyfin.serverId}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
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 * as OpenApiValidator from 'express-openapi-validator';
|
||||
import session, { Store } 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';
|
||||
@@ -27,21 +31,22 @@ import { getAppVersion } from './utils/appVersion';
|
||||
|
||||
const API_SPEC_PATH = path.join(__dirname, '../overseerr-api.yml');
|
||||
|
||||
logger.info(`Starting Overseerr version ${getAppVersion()}`);
|
||||
logger.info(`Starting Jellyseerr version ${getAppVersion()}`);
|
||||
const dev = process.env.NODE_ENV !== 'production';
|
||||
const app = next({ dev });
|
||||
const handle = app.getRequestHandler();
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
app
|
||||
.prepare()
|
||||
.then(async () => {
|
||||
// // Run migrations in production
|
||||
// if (process.env.NODE_ENV === 'production') {
|
||||
// await dbConnection.query('PRAGMA foreign_keys=OFF');
|
||||
// await dbConnection.runMigrations();
|
||||
// await dbConnection.query('PRAGMA foreign_keys=ON');
|
||||
// }
|
||||
const dbConnection = await createConnection();
|
||||
|
||||
// Run migrations in production
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
await dbConnection.query('PRAGMA foreign_keys=OFF');
|
||||
await dbConnection.runMigrations();
|
||||
await dbConnection.query('PRAGMA foreign_keys=ON');
|
||||
}
|
||||
|
||||
// Load Settings
|
||||
const settings = getSettings().load();
|
||||
@@ -51,23 +56,14 @@ app
|
||||
settings.plex.libraries.length > 1 &&
|
||||
!settings.plex.libraries[0].type
|
||||
) {
|
||||
const admin = await prisma.user.findFirst({
|
||||
select: {
|
||||
id: true,
|
||||
plexToken: true,
|
||||
},
|
||||
orderBy: {
|
||||
id: 'asc',
|
||||
},
|
||||
const userRepository = getRepository(User);
|
||||
const admin = await userRepository.findOne({
|
||||
select: ['id', 'plexToken'],
|
||||
order: { id: 'ASC' },
|
||||
});
|
||||
// const userRepository = getRepository(User);
|
||||
// const admin = await userRepository.findOne({
|
||||
// select: ['id', 'plexToken'],
|
||||
// order: { id: 'ASC' },
|
||||
// });
|
||||
|
||||
if (admin) {
|
||||
const plexapi = new PlexAPI({ plexToken: admin.plexToken! });
|
||||
const plexapi = new PlexAPI({ plexToken: admin.plexToken });
|
||||
await plexapi.syncLibraries();
|
||||
logger.info('Migrating libraries to include media type', {
|
||||
label: 'Settings',
|
||||
@@ -133,24 +129,22 @@ app
|
||||
}
|
||||
|
||||
// Set up sessions
|
||||
// const sessionRespository = getRepository(Session);
|
||||
// const sessionRespository = await prisma.session.findMany();
|
||||
|
||||
// server.use(
|
||||
// '/api',
|
||||
// session({
|
||||
// secret: settings.clientId,
|
||||
// resave: false,
|
||||
// saveUninitialized: false,
|
||||
// cookie: {
|
||||
// maxAge: 1000 * 60 * 60 * 24 * 30,
|
||||
// },
|
||||
// store: new TypeormStore({
|
||||
// cleanupLimit: 2,
|
||||
// ttl: 1000 * 60 * 60 * 24 * 30,
|
||||
// }).connect(sessionRespository) as Store,
|
||||
// })
|
||||
// );
|
||||
const sessionRespository = getRepository(Session);
|
||||
server.use(
|
||||
'/api',
|
||||
session({
|
||||
secret: settings.clientId,
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
maxAge: 1000 * 60 * 60 * 24 * 30,
|
||||
},
|
||||
store: new TypeormStore({
|
||||
cleanupLimit: 2,
|
||||
ttl: 1000 * 60 * 60 * 24 * 30,
|
||||
}).connect(sessionRespository) as Store,
|
||||
})
|
||||
);
|
||||
const apiDocs = YAML.load(API_SPEC_PATH);
|
||||
server.use('/api-docs', swaggerUi.serve, swaggerUi.setup(apiDocs));
|
||||
server.use(
|
||||
|
||||
@@ -43,7 +43,8 @@ interface SlackBlockEmbed {
|
||||
|
||||
class SlackAgent
|
||||
extends BaseAgent<NotificationAgentSlack>
|
||||
implements NotificationAgent {
|
||||
implements NotificationAgent
|
||||
{
|
||||
protected getSettings(): NotificationAgentSlack {
|
||||
if (this.settings) {
|
||||
return this.settings;
|
||||
@@ -202,7 +203,7 @@ class SlackAgent
|
||||
action_id: 'button-action',
|
||||
type: 'button',
|
||||
url: actionUrl,
|
||||
value: 'open_overseerr',
|
||||
value: 'open_jellyseerr',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: `Open in ${settings.main.applicationTitle}`,
|
||||
|
||||
@@ -254,7 +254,7 @@ class Settings {
|
||||
vapidPublic: '',
|
||||
main: {
|
||||
apiKey: '',
|
||||
applicationTitle: 'Overseerr',
|
||||
applicationTitle: 'Jellyseerr',
|
||||
applicationUrl: '',
|
||||
csrfProtection: false,
|
||||
cacheImages: false,
|
||||
@@ -303,7 +303,7 @@ class Settings {
|
||||
ignoreTls: false,
|
||||
requireTls: false,
|
||||
allowSelfSigned: false,
|
||||
senderName: 'Overseerr',
|
||||
senderName: 'Jellyseerr',
|
||||
},
|
||||
},
|
||||
discord: {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import * as winston from 'winston';
|
||||
import 'winston-daily-rotate-file';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
// Migrate away from old log
|
||||
const OLD_LOG_FILE = path.join(__dirname, '../config/logs/overseerr.log');
|
||||
const OLD_LOG_FILE = path.join(__dirname, '../config/logs/Jellyseerr.log');
|
||||
if (fs.existsSync(OLD_LOG_FILE)) {
|
||||
const file = fs.lstatSync(OLD_LOG_FILE);
|
||||
|
||||
@@ -43,14 +43,14 @@ const logger = winston.createLogger({
|
||||
}),
|
||||
new winston.transports.DailyRotateFile({
|
||||
filename: process.env.CONFIG_DIRECTORY
|
||||
? `${process.env.CONFIG_DIRECTORY}/logs/overseerr-%DATE%.log`
|
||||
: path.join(__dirname, '../config/logs/overseerr-%DATE%.log'),
|
||||
? `${process.env.CONFIG_DIRECTORY}/logs/Jellyseerr-%DATE%.log`
|
||||
: path.join(__dirname, '../config/logs/Jellyseerr-%DATE%.log'),
|
||||
datePattern: 'YYYY-MM-DD',
|
||||
zippedArchive: true,
|
||||
maxSize: '20m',
|
||||
maxFiles: '7d',
|
||||
createSymlink: true,
|
||||
symlinkName: 'overseerr.log',
|
||||
symlinkName: 'Jellyseerr.log',
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -5,10 +5,10 @@ export class AddUserQuotaFields1616576677254 implements MigrationInterface {
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "temporary_user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "email" varchar NOT NULL, "username" varchar, "plexId" integer, "plexToken" varchar, "permissions" integer NOT NULL DEFAULT (0), "avatar" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "password" varchar, "userType" integer NOT NULL DEFAULT (1), "plexUsername" varchar, "resetPasswordGuid" varchar, "recoveryLinkExpirationDate" date, "movieQuotaLimit" integer, "movieQuotaDays" integer, "tvQuotaLimit" integer, "tvQuotaDays" integer, CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"))`
|
||||
`CREATE TABLE "temporary_user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "email" varchar NOT NULL, "username" varchar, "plexId" integer, "plexToken" varchar, "permissions" integer NOT NULL DEFAULT (0), "avatar" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "password" varchar, "userType" integer NOT NULL DEFAULT (1), "plexUsername" varchar, "resetPasswordGuid" varchar, "recoveryLinkExpirationDate" date, "movieQuotaLimit" integer, "movieQuotaDays" integer, "tvQuotaLimit" integer, "tvQuotaDays" integer, "jellyfinUsername" varchar, "jellyfinAuthToken" varchar, "jellyfinUserId" varchar, "jellyfinDeviceId" varchar, CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"))`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "temporary_user"("id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate") SELECT "id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate" FROM "user"`
|
||||
`INSERT INTO "temporary_user"("id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate", "jellyfinUsername", "jellyfinAuthToken", "jellyfinUserId", "jellyfinDeviceId") SELECT "id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate", "jellyfinUsername", "jellyfinAuthToken", "jellyfinUserId", "jellyfinDeviceId" FROM "user"`
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "user"`);
|
||||
await queryRunner.query(`ALTER TABLE "temporary_user" RENAME TO "user"`);
|
||||
@@ -17,10 +17,10 @@ export class AddUserQuotaFields1616576677254 implements MigrationInterface {
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "user" RENAME TO "temporary_user"`);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "email" varchar NOT NULL, "username" varchar, "plexId" integer, "plexToken" varchar, "permissions" integer NOT NULL DEFAULT (0), "avatar" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "password" varchar, "userType" integer NOT NULL DEFAULT (1), "plexUsername" varchar, "resetPasswordGuid" varchar, "recoveryLinkExpirationDate" date, CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"))`
|
||||
`CREATE TABLE "user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "email" varchar NOT NULL, "username" varchar, "plexId" integer, "plexToken" varchar, "permissions" integer NOT NULL DEFAULT (0), "avatar" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "password" varchar, "userType" integer NOT NULL DEFAULT (1), "plexUsername" varchar, "resetPasswordGuid" varchar, "recoveryLinkExpirationDate" date, "jellyfinUsername" varchar, "jellyfinAuthToken" varchar, "jellyfinUserId" varchar, "jellyfinDeviceId" varchar, CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"))`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "user"("id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate") SELECT "id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate" FROM "temporary_user"`
|
||||
`INSERT INTO "user"("id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate", "jellyfinUsername", "jellyfinAuthToken", "jellyfinUserId", "jellyfinDeviceId") SELECT "id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate", "jellyfinUsername", "jellyfinAuthToken", "jellyfinUserId", "jellyfinDeviceId" FROM "temporary_user"`
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "temporary_user"`);
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ authRoutes.post('/plex', async (req, res, next) => {
|
||||
if (!user) {
|
||||
if (!settings.main.newPlexLogin) {
|
||||
logger.info(
|
||||
'Failed sign-in attempt from user who has not been imported to Overseerr.',
|
||||
'Failed sign-in attempt from user who has not been imported to Jellyseerr.',
|
||||
{
|
||||
label: 'Auth',
|
||||
account: {
|
||||
@@ -214,7 +214,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
if (user) {
|
||||
deviceId = user.jellyfinDeviceId ?? '';
|
||||
} else {
|
||||
deviceId = Buffer.from(`BOT_overseerr_${body.username ?? ''}`).toString(
|
||||
deviceId = Buffer.from(`BOT_jellyseerr_${body.username ?? ''}`).toString(
|
||||
'base64'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -162,7 +162,7 @@ router.get('/genres/tv', isAuthenticated(), async (req, res) => {
|
||||
|
||||
router.get('/', (_req, res) => {
|
||||
return res.status(200).json({
|
||||
api: 'Overseerr API',
|
||||
api: 'Jellyseerr API',
|
||||
version: '1.0',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -325,8 +325,8 @@ settingsRoutes.get(
|
||||
}
|
||||
|
||||
const logFile = process.env.CONFIG_DIRECTORY
|
||||
? `${process.env.CONFIG_DIRECTORY}/logs/overseerr.log`
|
||||
: path.join(__dirname, '../../../config/logs/overseerr.log');
|
||||
? `${process.env.CONFIG_DIRECTORY}/logs/jellyseerr.log`
|
||||
: path.join(__dirname, '../../../config/logs/jellyseerr.log');
|
||||
const logs: LogMessage[] = [];
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
name: overseerr
|
||||
adopt-info: overseerr
|
||||
name: jellyseerr
|
||||
adopt-info: jellyseerr
|
||||
license: MIT
|
||||
summary: Request management and media discovery tool for the Plex ecosystem.
|
||||
description: >
|
||||
Overseerr is a free and open source software application for managing requests for your media library.
|
||||
Jellyseerr is a free and open source software application for managing requests for your media library.
|
||||
It integrates with your existing services such as Sonarr, Radarr and Plex!
|
||||
base: core18
|
||||
confinement: strict
|
||||
|
||||
parts:
|
||||
overseerr:
|
||||
jellyseerr:
|
||||
plugin: nodejs
|
||||
nodejs-version: '14.17.0'
|
||||
nodejs-package-manager: 'yarn'
|
||||
@@ -88,7 +88,7 @@ apps:
|
||||
- network-bind
|
||||
environment:
|
||||
PATH: '$SNAP/usr/sbin:$SNAP/usr/bin:$SNAP/sbin:$SNAP/bin:$PATH'
|
||||
OVERSEERR_SNAP: 'True'
|
||||
JELLYSEERR_SNAP: 'True'
|
||||
CONFIG_DIRECTORY: $SNAP_USER_COMMON
|
||||
LOG_LEVEL: 'debug'
|
||||
NODE_ENV: 'production'
|
||||
|
||||
@@ -11,8 +11,8 @@ import useSWR from 'swr';
|
||||
import { StatusResponse } from '../../../../server/interfaces/api/settingsInterfaces';
|
||||
|
||||
const messages = defineMessages({
|
||||
streamdevelop: 'Overseerr Develop',
|
||||
streamstable: 'Overseerr Stable',
|
||||
streamdevelop: 'Jellyseerr Develop',
|
||||
streamstable: 'Jellyseerr Stable',
|
||||
outofdate: 'Out of Date',
|
||||
commitsbehind:
|
||||
'{commitsBehind} {commitsBehind, plural, one {commit} other {commits}} behind',
|
||||
|
||||
@@ -48,7 +48,10 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
|
||||
if (initial) {
|
||||
const LoginSchema = Yup.object().shape({
|
||||
host: Yup.string()
|
||||
.url(intl.formatMessage(messages.validationhostformat))
|
||||
.matches(
|
||||
/^(?:(?:(?:https?):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*\.?)(?::\d{2,5})?(?:[/?#]\S*)?$/,
|
||||
intl.formatMessage(messages.validationhostformat)
|
||||
)
|
||||
.required(intl.formatMessage(messages.validationhostrequired)),
|
||||
email: Yup.string()
|
||||
.email(intl.formatMessage(messages.validationemailformat))
|
||||
|
||||
@@ -164,10 +164,13 @@ const PWAHeader: React.FC<PWAHeaderProps> = ({ applicationTitle }) => {
|
||||
href="/site.webmanifest"
|
||||
crossOrigin="use-credentials"
|
||||
/>
|
||||
<meta name="application-name" content={applicationTitle ?? 'Overseerr'} />
|
||||
<meta
|
||||
name="application-name"
|
||||
content={applicationTitle ?? 'Jellyseerr'}
|
||||
/>
|
||||
<meta
|
||||
name="apple-mobile-web-app-title"
|
||||
content={applicationTitle ?? 'Overseerr'}
|
||||
content={applicationTitle ?? 'Jellyseerr'}
|
||||
/>
|
||||
<meta
|
||||
name="description"
|
||||
|
||||
@@ -9,13 +9,13 @@ export const messages = defineMessages({
|
||||
'Full administrator access. Bypasses all other permission checks.',
|
||||
users: 'Manage Users',
|
||||
usersDescription:
|
||||
'Grant permission to manage Overseerr users. Users with this permission cannot modify users with or grant the Admin privilege.',
|
||||
'Grant permission to manage Jellyseerr users. Users with this permission cannot modify users with or grant the Admin privilege.',
|
||||
settings: 'Manage Settings',
|
||||
settingsDescription:
|
||||
'Grant permission to modify Overseerr settings. A user must have this permission to grant it to others.',
|
||||
'Grant permission to modify Jellyseerr settings. A user must have this permission to grant it to others.',
|
||||
managerequests: 'Manage Requests',
|
||||
managerequestsDescription:
|
||||
'Grant permission to manage Overseerr requests. All requests made by a user with this permission will be automatically approved.',
|
||||
'Grant permission to manage Jellyseerr requests. All requests made by a user with this permission will be automatically approved.',
|
||||
request: 'Request',
|
||||
requestDescription: 'Grant permission to request non-4K media.',
|
||||
requestMovies: 'Request Movies',
|
||||
|
||||
@@ -1,24 +1,38 @@
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
import type { MediaRequest } from '../../../server/entity/MediaRequest';
|
||||
import type { TvDetails } from '../../../server/models/Tv';
|
||||
import type { MovieDetails } from '../../../server/models/Movie';
|
||||
import useSWR from 'swr';
|
||||
import { LanguageContext } from '../../context/LanguageContext';
|
||||
import { MediaRequestStatus } from '../../../server/constants/media';
|
||||
import Badge from '../Common/Badge';
|
||||
import { useUser, Permission } from '../../hooks/useUser';
|
||||
import {
|
||||
CheckIcon,
|
||||
PencilIcon,
|
||||
RefreshIcon,
|
||||
TrashIcon,
|
||||
XIcon,
|
||||
} from '@heroicons/react/solid';
|
||||
import axios from 'axios';
|
||||
import Button from '../Common/Button';
|
||||
import { withProperties } from '../../utils/typeHelpers';
|
||||
import Link from 'next/link';
|
||||
import React, { 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: 'Seasons',
|
||||
all: 'All',
|
||||
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.',
|
||||
deleterequest: 'Delete Request',
|
||||
});
|
||||
|
||||
const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => {
|
||||
@@ -27,7 +41,7 @@ const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => {
|
||||
|
||||
const RequestCardPlaceholder: React.FC = () => {
|
||||
return (
|
||||
<div className="relative p-4 bg-gray-700 rounded-lg w-72 sm:w-96 animate-pulse">
|
||||
<div className="relative w-72 animate-pulse rounded-xl bg-gray-700 p-4 sm:w-96">
|
||||
<div className="w-20 sm:w-28">
|
||||
<div className="w-full" style={{ paddingBottom: '150%' }} />
|
||||
</div>
|
||||
@@ -35,6 +49,45 @@ const RequestCardPlaceholder: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
interface RequestCardErrorProps {
|
||||
mediaId?: number;
|
||||
}
|
||||
|
||||
const RequestCardError: React.FC<RequestCardErrorProps> = ({ mediaId }) => {
|
||||
const { hasPermission } = useUser();
|
||||
const intl = useIntl();
|
||||
|
||||
const deleteRequest = async () => {
|
||||
await axios.delete(`/api/v1/media/${mediaId}`);
|
||||
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative w-72 rounded-xl bg-gray-800 p-4 ring-1 ring-red-500 sm:w-96">
|
||||
<div className="w-20 sm:w-28">
|
||||
<div className="w-full" style={{ paddingBottom: '150%' }}>
|
||||
<div className="absolute inset-0 flex h-full w-full flex-col items-center justify-center px-10">
|
||||
<div className="w-full whitespace-normal text-center text-xs text-gray-300 sm:text-sm">
|
||||
{intl.formatMessage(messages.mediaerror)}
|
||||
</div>
|
||||
{hasPermission(Permission.MANAGE_REQUESTS) && mediaId && (
|
||||
<Button
|
||||
buttonType="danger"
|
||||
buttonSize="sm"
|
||||
className="mt-4"
|
||||
onClick={() => deleteRequest()}
|
||||
>
|
||||
<TrashIcon />
|
||||
<span>{intl.formatMessage(messages.deleterequest)}</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface RequestCardProps {
|
||||
request: MediaRequest;
|
||||
onTitleData?: (requestId: number, title: MovieDetails | TvDetails) => void;
|
||||
@@ -45,19 +98,21 @@ const RequestCard: React.FC<RequestCardProps> = ({ request, onTitleData }) => {
|
||||
triggerOnce: true,
|
||||
});
|
||||
const intl = useIntl();
|
||||
const { hasPermission } = useUser();
|
||||
const { locale } = useContext(LanguageContext);
|
||||
const { user, hasPermission } = useUser();
|
||||
const { addToast } = useToasts();
|
||||
const [isRetrying, setRetrying] = useState(false);
|
||||
const [showEditModal, setShowEditModal] = useState(false);
|
||||
const url =
|
||||
request.type === 'movie'
|
||||
? `/api/v1/movie/${request.media.tmdbId}`
|
||||
: `/api/v1/tv/${request.media.tmdbId}`;
|
||||
const { data: title, error } = useSWR<MovieDetails | TvDetails>(
|
||||
inView ? `${url}?language=${locale}` : null
|
||||
inView ? `${url}` : null
|
||||
);
|
||||
const {
|
||||
data: requestData,
|
||||
error: requestError,
|
||||
revalidate,
|
||||
mutate: revalidate,
|
||||
} = useSWR<MediaRequest>(`/api/v1/request/${request.id}`, {
|
||||
initialData: request,
|
||||
});
|
||||
@@ -70,6 +125,30 @@ const RequestCard: React.FC<RequestCardProps> = ({ request, onTitleData }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const deleteRequest = async () => {
|
||||
await axios.delete(`/api/v1/request/${request.id}`);
|
||||
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
|
||||
};
|
||||
|
||||
const retryRequest = async () => {
|
||||
setRetrying(true);
|
||||
|
||||
try {
|
||||
const response = await axios.post(`/api/v1/request/${request.id}/retry`);
|
||||
|
||||
if (response) {
|
||||
revalidate();
|
||||
}
|
||||
} catch (e) {
|
||||
addToast(intl.formatMessage(messages.failedretry), {
|
||||
autoDismiss: true,
|
||||
appearance: 'error',
|
||||
});
|
||||
} finally {
|
||||
setRetrying(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (title && onTitleData) {
|
||||
onTitleData(request.id, title);
|
||||
@@ -85,157 +164,251 @@ const RequestCard: React.FC<RequestCardProps> = ({ request, onTitleData }) => {
|
||||
}
|
||||
|
||||
if (!requestData && !requestError) {
|
||||
return <RequestCardPlaceholder />;
|
||||
return <RequestCardError />;
|
||||
}
|
||||
|
||||
if (!title || !requestData) {
|
||||
return <RequestCardPlaceholder />;
|
||||
return <RequestCardError mediaId={requestData?.media.id} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative flex p-4 text-gray-400 bg-gray-800 bg-center bg-cover rounded-md w-72 sm:w-96"
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(180deg, rgba(17, 24, 39, 0.47) 0%, rgba(17, 24, 39, 1) 100%), url(//image.tmdb.org/t/p/w1920_and_h800_multi_faces/${title.backdropPath})`,
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col flex-1 min-w-0 pr-4">
|
||||
<h2 className="overflow-hidden text-base text-white cursor-pointer sm:text-lg overflow-ellipsis whitespace-nowrap hover:underline">
|
||||
<Link
|
||||
href={request.type === 'movie' ? '/movie/[movieId]' : '/tv/[tvId]'}
|
||||
as={
|
||||
request.type === 'movie'
|
||||
? `/movie/${request.media.tmdbId}`
|
||||
: `/tv/${request.media.tmdbId}`
|
||||
}
|
||||
>
|
||||
{isMovie(title) ? title.title : title.name}
|
||||
</Link>
|
||||
</h2>
|
||||
<Link href={`/users/${requestData.requestedBy.id}`}>
|
||||
<a className="flex items-center group">
|
||||
<img
|
||||
src={requestData.requestedBy.avatar}
|
||||
<>
|
||||
<RequestModal
|
||||
show={showEditModal}
|
||||
tmdbId={request.media.tmdbId}
|
||||
type={request.type}
|
||||
is4k={request.is4k}
|
||||
editRequest={request}
|
||||
onCancel={() => setShowEditModal(false)}
|
||||
onComplete={() => {
|
||||
revalidate();
|
||||
setShowEditModal(false);
|
||||
}}
|
||||
/>
|
||||
<div className="relative flex w-72 overflow-hidden rounded-xl bg-gray-800 bg-cover bg-center p-4 text-gray-400 shadow ring-1 ring-gray-700 sm:w-96">
|
||||
{title.backdropPath && (
|
||||
<div className="absolute inset-0 z-0">
|
||||
<CachedImage
|
||||
alt=""
|
||||
className="w-4 mr-1 rounded-full sm:mr-2 sm:w-5"
|
||||
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${title.backdropPath}`}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
/>
|
||||
<span className="text-xs truncate sm:text-sm group-hover:underline">
|
||||
{requestData.requestedBy.displayName}
|
||||
</span>
|
||||
</a>
|
||||
</Link>
|
||||
{requestData.media.status && (
|
||||
<div className="mt-1 sm:mt-2">
|
||||
<StatusBadge
|
||||
status={
|
||||
requestData.is4k
|
||||
? requestData.media.status4k
|
||||
: requestData.media.status
|
||||
}
|
||||
is4k={requestData.is4k}
|
||||
inProgress={
|
||||
(
|
||||
requestData.media[
|
||||
requestData.is4k ? 'downloadStatus4k' : 'downloadStatus'
|
||||
] ?? []
|
||||
).length > 0
|
||||
}
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
backgroundImage:
|
||||
'linear-gradient(135deg, rgba(17, 24, 39, 0.47) 0%, rgba(17, 24, 39, 1) 75%)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{request.seasons.length > 0 && (
|
||||
<div className="items-center hidden mt-2 text-sm sm:flex">
|
||||
<span className="mr-2">{intl.formatMessage(messages.seasons)}</span>
|
||||
{!isMovie(title) &&
|
||||
title.seasons.filter((season) => season.seasonNumber !== 0)
|
||||
.length === request.seasons.length ? (
|
||||
<span className="mr-2 uppercase">
|
||||
<Badge>{intl.formatMessage(messages.all)}</Badge>
|
||||
</span>
|
||||
) : (
|
||||
<div className="overflow-x-scroll hide-scrollbar">
|
||||
{request.seasons.map((season) => (
|
||||
<span key={`season-${season.id}`} className="mr-2">
|
||||
<Badge>{season.seasonNumber}</Badge>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="relative z-10 flex min-w-0 flex-1 flex-col pr-4">
|
||||
<div className="hidden text-xs font-medium text-white sm:flex">
|
||||
{(isMovie(title) ? title.releaseDate : title.firstAirDate)?.slice(
|
||||
0,
|
||||
4
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{requestData.status === MediaRequestStatus.PENDING &&
|
||||
hasPermission(Permission.MANAGE_REQUESTS) && (
|
||||
<div className="flex items-end flex-1">
|
||||
<span className="mr-2">
|
||||
<Link
|
||||
href={
|
||||
request.type === 'movie'
|
||||
? `/movie/${requestData.media.tmdbId}`
|
||||
: `/tv/${requestData.media.tmdbId}`
|
||||
}
|
||||
>
|
||||
<a className="overflow-hidden overflow-ellipsis whitespace-nowrap text-base font-bold text-white hover:underline sm:text-lg">
|
||||
{isMovie(title) ? title.title : title.name}
|
||||
</a>
|
||||
</Link>
|
||||
{hasPermission(
|
||||
[Permission.MANAGE_REQUESTS, Permission.REQUEST_VIEW],
|
||||
{ type: 'or' }
|
||||
) && (
|
||||
<div className="card-field">
|
||||
<Link href={`/users/${requestData.requestedBy.id}`}>
|
||||
<a className="group flex items-center">
|
||||
<img
|
||||
src={requestData.requestedBy.avatar}
|
||||
alt=""
|
||||
className="avatar-sm"
|
||||
/>
|
||||
<span className="truncate font-semibold group-hover:text-white group-hover:underline">
|
||||
{requestData.requestedBy.displayName}
|
||||
</span>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
{!isMovie(title) && request.seasons.length > 0 && (
|
||||
<div className="my-0.5 hidden items-center text-sm sm:my-1 sm:flex">
|
||||
<span className="mr-2 font-bold ">
|
||||
{intl.formatMessage(messages.seasons, {
|
||||
seasonCount:
|
||||
title.seasons.filter((season) => season.seasonNumber !== 0)
|
||||
.length === request.seasons.length
|
||||
? 0
|
||||
: request.seasons.length,
|
||||
})}
|
||||
</span>
|
||||
{title.seasons.filter((season) => season.seasonNumber !== 0)
|
||||
.length === request.seasons.length ? (
|
||||
<span className="mr-2 uppercase">
|
||||
<Badge>{intl.formatMessage(globalMessages.all)}</Badge>
|
||||
</span>
|
||||
) : (
|
||||
<div className="hide-scrollbar overflow-x-scroll">
|
||||
{request.seasons.map((season) => (
|
||||
<span key={`season-${season.id}`} className="mr-2">
|
||||
<Badge>{season.seasonNumber}</Badge>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-2 flex items-center text-sm sm:mt-1">
|
||||
<span className="mr-2 hidden font-bold sm:block">
|
||||
{intl.formatMessage(globalMessages.status)}
|
||||
</span>
|
||||
{requestData.status === MediaRequestStatus.DECLINED ? (
|
||||
<Badge badgeType="danger">
|
||||
{intl.formatMessage(globalMessages.declined)}
|
||||
</Badge>
|
||||
) : requestData.media[requestData.is4k ? 'status4k' : 'status'] ===
|
||||
MediaStatus.UNKNOWN ? (
|
||||
<Badge
|
||||
badgeType="danger"
|
||||
//href={`/${requestData.type}/${requestData.media.tmdbId}?manage=1`}
|
||||
>
|
||||
{intl.formatMessage(globalMessages.failed)}
|
||||
</Badge>
|
||||
) : (
|
||||
<StatusBadge
|
||||
status={
|
||||
requestData.media[requestData.is4k ? 'status4k' : 'status']
|
||||
}
|
||||
inProgress={
|
||||
(
|
||||
requestData.media[
|
||||
requestData.is4k ? 'downloadStatus4k' : 'downloadStatus'
|
||||
] ?? []
|
||||
).length > 0
|
||||
}
|
||||
is4k={requestData.is4k}
|
||||
tmdbId={requestData.media.tmdbId}
|
||||
mediaType={requestData.type}
|
||||
plexUrl={
|
||||
requestData.media[
|
||||
requestData.is4k ? 'mediaUrl4k' : 'mediaUrl'
|
||||
]
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-1 items-end space-x-2">
|
||||
{requestData.media[requestData.is4k ? 'status4k' : 'status'] ===
|
||||
MediaStatus.UNKNOWN &&
|
||||
requestData.status !== MediaRequestStatus.DECLINED &&
|
||||
hasPermission(Permission.MANAGE_REQUESTS) && (
|
||||
<Button
|
||||
buttonType="success"
|
||||
buttonType="primary"
|
||||
buttonSize="sm"
|
||||
onClick={() => modifyRequest('approve')}
|
||||
disabled={isRetrying}
|
||||
onClick={() => retryRequest()}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 mr-0 sm:mr-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<span className="hidden sm:block">
|
||||
{intl.formatMessage(globalMessages.approve)}
|
||||
<RefreshIcon
|
||||
className={isRetrying ? 'animate-spin' : ''}
|
||||
style={{ marginRight: '0', animationDirection: 'reverse' }}
|
||||
/>
|
||||
<span className="ml-1.5 hidden sm:block">
|
||||
{intl.formatMessage(globalMessages.retry)}
|
||||
</span>
|
||||
</Button>
|
||||
</span>
|
||||
<span>
|
||||
)}
|
||||
{requestData.status === MediaRequestStatus.PENDING &&
|
||||
hasPermission(Permission.MANAGE_REQUESTS) && (
|
||||
<>
|
||||
<Button
|
||||
buttonType="success"
|
||||
buttonSize="sm"
|
||||
onClick={() => modifyRequest('approve')}
|
||||
>
|
||||
<CheckIcon style={{ marginRight: '0' }} />
|
||||
<span className="ml-1.5 hidden sm:block">
|
||||
{intl.formatMessage(globalMessages.approve)}
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
buttonType="danger"
|
||||
buttonSize="sm"
|
||||
onClick={() => modifyRequest('decline')}
|
||||
>
|
||||
<XIcon style={{ marginRight: '0' }} />
|
||||
<span className="ml-1.5 hidden sm:block">
|
||||
{intl.formatMessage(globalMessages.decline)}
|
||||
</span>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{requestData.status === MediaRequestStatus.PENDING &&
|
||||
!hasPermission(Permission.MANAGE_REQUESTS) &&
|
||||
requestData.requestedBy.id === user?.id &&
|
||||
(requestData.type === 'tv' ||
|
||||
hasPermission(Permission.REQUEST_ADVANCED)) && (
|
||||
<Button
|
||||
buttonType="primary"
|
||||
buttonSize="sm"
|
||||
onClick={() => setShowEditModal(true)}
|
||||
className={`${
|
||||
hasPermission(Permission.MANAGE_REQUESTS) ? 'sm:hidden' : ''
|
||||
}`}
|
||||
>
|
||||
<PencilIcon style={{ marginRight: '0' }} />
|
||||
<span className="ml-1.5 hidden sm:block">
|
||||
{intl.formatMessage(globalMessages.edit)}
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
{requestData.status === MediaRequestStatus.PENDING &&
|
||||
!hasPermission(Permission.MANAGE_REQUESTS) &&
|
||||
requestData.requestedBy.id === user?.id && (
|
||||
<Button
|
||||
buttonType="danger"
|
||||
buttonSize="sm"
|
||||
onClick={() => modifyRequest('decline')}
|
||||
onClick={() => deleteRequest()}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 mr-0 sm:mr-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<span className="hidden sm:block">
|
||||
{intl.formatMessage(globalMessages.decline)}
|
||||
<XIcon style={{ marginRight: '0' }} />
|
||||
<span className="ml-1.5 hidden sm:block">
|
||||
{intl.formatMessage(globalMessages.cancel)}
|
||||
</span>
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-shrink-0 w-20 sm:w-28">
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Link
|
||||
href={request.type === 'movie' ? '/movie/[movieId]' : '/tv/[tvId]'}
|
||||
as={
|
||||
href={
|
||||
request.type === 'movie'
|
||||
? `/movie/${request.media.tmdbId}`
|
||||
: `/tv/${request.media.tmdbId}`
|
||||
? `/movie/${requestData.media.tmdbId}`
|
||||
: `/tv/${requestData.media.tmdbId}`
|
||||
}
|
||||
>
|
||||
<img
|
||||
src={
|
||||
title.posterPath
|
||||
? `//image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
|
||||
: '/images/overseerr_poster_not_found.png'
|
||||
}
|
||||
alt=""
|
||||
className="w-20 transition duration-300 scale-100 rounded-md shadow-sm cursor-pointer sm:w-28 transform-gpu hover:scale-105 hover:shadow-md"
|
||||
/>
|
||||
<a className="w-20 flex-shrink-0 scale-100 transform-gpu cursor-pointer overflow-hidden rounded-md shadow-sm transition duration-300 hover:scale-105 hover:shadow-md sm:w-28">
|
||||
<CachedImage
|
||||
src={
|
||||
title.posterPath
|
||||
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
|
||||
: '/images/overseerr_poster_not_found.png'
|
||||
}
|
||||
alt=""
|
||||
layout="responsive"
|
||||
width={600}
|
||||
height={900}
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import {
|
||||
CheckIcon,
|
||||
PencilIcon,
|
||||
RefreshIcon,
|
||||
TrashIcon,
|
||||
XIcon,
|
||||
} from '@heroicons/react/solid';
|
||||
import axios from 'axios';
|
||||
import Link from 'next/link';
|
||||
import React, { useContext, useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
import {
|
||||
defineMessages,
|
||||
FormattedDate,
|
||||
FormattedRelativeTime,
|
||||
useIntl,
|
||||
} from 'react-intl';
|
||||
import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import useSWR from 'swr';
|
||||
import {
|
||||
@@ -17,25 +19,70 @@ import {
|
||||
import type { MediaRequest } from '../../../../server/entity/MediaRequest';
|
||||
import type { MovieDetails } from '../../../../server/models/Movie';
|
||||
import type { TvDetails } from '../../../../server/models/Tv';
|
||||
import { LanguageContext } from '../../../context/LanguageContext';
|
||||
import { Permission, useUser } from '../../../hooks/useUser';
|
||||
import globalMessages from '../../../i18n/globalMessages';
|
||||
import Badge from '../../Common/Badge';
|
||||
import Button from '../../Common/Button';
|
||||
import Table from '../../Common/Table';
|
||||
import CachedImage from '../../Common/CachedImage';
|
||||
import ConfirmButton from '../../Common/ConfirmButton';
|
||||
import RequestModal from '../../RequestModal';
|
||||
import StatusBadge from '../../StatusBadge';
|
||||
|
||||
const messages = defineMessages({
|
||||
seasons: 'Seasons',
|
||||
notavailable: 'N/A',
|
||||
seasons: '{seasonCount, plural, one {Season} other {Seasons}}',
|
||||
failedretry: 'Something went wrong while retrying the request.',
|
||||
requested: 'Requested',
|
||||
requesteddate: 'Requested',
|
||||
modified: 'Modified',
|
||||
modifieduserdate: '{date} by {user}',
|
||||
mediaerror: 'The associated title for this request is no longer available.',
|
||||
editrequest: 'Edit Request',
|
||||
deleterequest: 'Delete Request',
|
||||
cancelRequest: 'Cancel Request',
|
||||
});
|
||||
|
||||
const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => {
|
||||
return (movie as MovieDetails).title !== undefined;
|
||||
};
|
||||
|
||||
interface RequestItemErroProps {
|
||||
mediaId?: number;
|
||||
revalidateList: () => void;
|
||||
}
|
||||
|
||||
const RequestItemError: React.FC<RequestItemErroProps> = ({
|
||||
mediaId,
|
||||
revalidateList,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const { hasPermission } = useUser();
|
||||
|
||||
const deleteRequest = async () => {
|
||||
await axios.delete(`/api/v1/media/${mediaId}`);
|
||||
revalidateList();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-64 w-full flex-col items-center justify-center rounded-xl bg-gray-800 px-10 ring-1 ring-red-500 lg:flex-row xl:h-28">
|
||||
<span className="text-center text-sm text-gray-300 lg:text-left">
|
||||
{intl.formatMessage(messages.mediaerror)}
|
||||
</span>
|
||||
{hasPermission(Permission.MANAGE_REQUESTS) && mediaId && (
|
||||
<div className="mt-4 lg:ml-4 lg:mt-0">
|
||||
<Button
|
||||
buttonType="danger"
|
||||
buttonSize="sm"
|
||||
onClick={() => deleteRequest()}
|
||||
>
|
||||
<TrashIcon />
|
||||
<span>{intl.formatMessage(messages.deleterequest)}</span>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface RequestItemProps {
|
||||
request: MediaRequest;
|
||||
revalidateList: () => void;
|
||||
@@ -50,23 +97,21 @@ const RequestItem: React.FC<RequestItemProps> = ({
|
||||
});
|
||||
const { addToast } = useToasts();
|
||||
const intl = useIntl();
|
||||
const { hasPermission } = useUser();
|
||||
const { user, hasPermission } = useUser();
|
||||
const [showEditModal, setShowEditModal] = useState(false);
|
||||
const { locale } = useContext(LanguageContext);
|
||||
const url =
|
||||
request.type === 'movie'
|
||||
? `/api/v1/movie/${request.media.tmdbId}`
|
||||
: `/api/v1/tv/${request.media.tmdbId}`;
|
||||
const { data: title, error } = useSWR<MovieDetails | TvDetails>(
|
||||
inView ? `${url}?language=${locale}` : null
|
||||
inView ? url : null
|
||||
);
|
||||
const { data: requestData, mutate: revalidate } = useSWR<MediaRequest>(
|
||||
`/api/v1/request/${request.id}`,
|
||||
{
|
||||
initialData: request,
|
||||
}
|
||||
);
|
||||
const {
|
||||
data: requestData,
|
||||
revalidate,
|
||||
mutate,
|
||||
} = useSWR<MediaRequest>(`/api/v1/request/${request.id}`, {
|
||||
initialData: request,
|
||||
});
|
||||
|
||||
const [isRetrying, setRetrying] = useState(false);
|
||||
|
||||
@@ -89,7 +134,7 @@ const RequestItem: React.FC<RequestItemProps> = ({
|
||||
|
||||
try {
|
||||
const result = await axios.post(`/api/v1/request/${request.id}/retry`);
|
||||
mutate(result.data);
|
||||
revalidate(result.data);
|
||||
} catch (e) {
|
||||
addToast(intl.formatMessage(messages.failedretry), {
|
||||
autoDismiss: true,
|
||||
@@ -102,22 +147,24 @@ const RequestItem: React.FC<RequestItemProps> = ({
|
||||
|
||||
if (!title && !error) {
|
||||
return (
|
||||
<tr className="w-full h-24 animate-pulse" ref={ref}>
|
||||
<td colSpan={6}></td>
|
||||
</tr>
|
||||
<div
|
||||
className="h-64 w-full animate-pulse rounded-xl bg-gray-800 xl:h-28"
|
||||
ref={ref}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!title || !requestData) {
|
||||
return (
|
||||
<tr className="w-full h-24 animate-pulse">
|
||||
<td colSpan={6}></td>
|
||||
</tr>
|
||||
<RequestItemError
|
||||
mediaId={requestData?.media.id}
|
||||
revalidateList={revalidateList}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<tr className="relative w-full h-24 p-2">
|
||||
<>
|
||||
<RequestModal
|
||||
show={showEditModal}
|
||||
tmdbId={request.media.tmdbId}
|
||||
@@ -130,28 +177,26 @@ const RequestItem: React.FC<RequestItemProps> = ({
|
||||
setShowEditModal(false);
|
||||
}}
|
||||
/>
|
||||
<Table.TD>
|
||||
<div className="flex items-center">
|
||||
<Link
|
||||
href={
|
||||
request.type === 'movie'
|
||||
? `/movie/${request.media.tmdbId}`
|
||||
: `/tv/${request.media.tmdbId}`
|
||||
}
|
||||
>
|
||||
<a className="flex-shrink-0 hidden mr-4 sm:block">
|
||||
<img
|
||||
src={
|
||||
title.posterPath
|
||||
? `//image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
|
||||
: '/images/overseerr_poster_not_found.png'
|
||||
}
|
||||
alt=""
|
||||
className="w-12 transition duration-300 scale-100 rounded-md shadow-sm cursor-pointer transform-gpu hover:scale-105 hover:shadow-md"
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
<div className="flex-shrink overflow-hidden">
|
||||
<div className="relative flex w-full flex-col justify-between overflow-hidden rounded-xl bg-gray-800 py-4 text-gray-400 shadow-md ring-1 ring-gray-700 xl:h-28 xl:flex-row">
|
||||
{title.backdropPath && (
|
||||
<div className="absolute inset-0 z-0 w-full bg-cover bg-center xl:w-2/3">
|
||||
<CachedImage
|
||||
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${title.backdropPath}`}
|
||||
alt=""
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
backgroundImage:
|
||||
'linear-gradient(90deg, rgba(31, 41, 55, 0.47) 0%, rgba(31, 41, 55, 1) 100%)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="relative flex w-full flex-col justify-between overflow-hidden sm:flex-row">
|
||||
<div className="relative z-10 flex w-full items-center overflow-hidden pl-4 pr-4 sm:pr-0 xl:w-7/12 2xl:w-2/3">
|
||||
<Link
|
||||
href={
|
||||
requestData.type === 'movie'
|
||||
@@ -159,219 +204,295 @@ const RequestItem: React.FC<RequestItemProps> = ({
|
||||
: `/tv/${requestData.media.tmdbId}`
|
||||
}
|
||||
>
|
||||
<a className="min-w-0 mr-2 text-xl text-white truncate hover:underline">
|
||||
{isMovie(title) ? title.title : title.name}
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/users/${requestData.requestedBy.id}`}>
|
||||
<a className="flex items-center mt-1">
|
||||
<img
|
||||
src={requestData.requestedBy.avatar}
|
||||
<a className="relative h-auto w-12 flex-shrink-0 scale-100 transform-gpu overflow-hidden rounded-md transition duration-300 hover:scale-105">
|
||||
<CachedImage
|
||||
src={
|
||||
title.posterPath
|
||||
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
|
||||
: '/images/overseerr_poster_not_found.png'
|
||||
}
|
||||
alt=""
|
||||
className="w-5 mr-2 rounded-full"
|
||||
layout="responsive"
|
||||
width={600}
|
||||
height={900}
|
||||
objectFit="cover"
|
||||
/>
|
||||
<span className="text-sm hover:underline">
|
||||
{requestData.requestedBy.displayName}
|
||||
</span>
|
||||
</a>
|
||||
</Link>
|
||||
{requestData.seasons.length > 0 && (
|
||||
<div className="items-center hidden mt-2 text-sm sm:flex">
|
||||
<span className="mr-2">
|
||||
{intl.formatMessage(messages.seasons)}
|
||||
</span>
|
||||
{requestData.seasons.map((season) => (
|
||||
<span key={`season-${season.id}`} className="mr-2">
|
||||
<Badge>{season.seasonNumber}</Badge>
|
||||
<div className="flex flex-col justify-center overflow-hidden pl-2 xl:pl-4">
|
||||
<div className="pt-0.5 text-xs font-medium text-white sm:pt-1">
|
||||
{(isMovie(title)
|
||||
? title.releaseDate
|
||||
: title.firstAirDate
|
||||
)?.slice(0, 4)}
|
||||
</div>
|
||||
<Link
|
||||
href={
|
||||
requestData.type === 'movie'
|
||||
? `/movie/${requestData.media.tmdbId}`
|
||||
: `/tv/${requestData.media.tmdbId}`
|
||||
}
|
||||
>
|
||||
<a className="mr-2 min-w-0 truncate text-lg font-bold text-white hover:underline xl:text-xl">
|
||||
{isMovie(title) ? title.title : title.name}
|
||||
</a>
|
||||
</Link>
|
||||
{!isMovie(title) && request.seasons.length > 0 && (
|
||||
<div className="card-field">
|
||||
<span className="card-field-name">
|
||||
{intl.formatMessage(messages.seasons, {
|
||||
seasonCount:
|
||||
title.seasons.filter(
|
||||
(season) => season.seasonNumber !== 0
|
||||
).length === request.seasons.length
|
||||
? 0
|
||||
: request.seasons.length,
|
||||
})}
|
||||
</span>
|
||||
))}
|
||||
{title.seasons.filter((season) => season.seasonNumber !== 0)
|
||||
.length === request.seasons.length ? (
|
||||
<span className="mr-2 uppercase">
|
||||
<Badge>{intl.formatMessage(globalMessages.all)}</Badge>
|
||||
</span>
|
||||
) : (
|
||||
<div className="hide-scrollbar flex flex-nowrap overflow-x-scroll">
|
||||
{request.seasons.map((season) => (
|
||||
<span key={`season-${season.id}`} className="mr-2">
|
||||
<Badge>{season.seasonNumber}</Badge>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="z-10 mt-4 ml-4 flex w-full flex-col justify-center overflow-hidden pr-4 text-sm sm:ml-2 sm:mt-0 xl:flex-1 xl:pr-0">
|
||||
<div className="card-field">
|
||||
<span className="card-field-name">
|
||||
{intl.formatMessage(globalMessages.status)}
|
||||
</span>
|
||||
{requestData.status === MediaRequestStatus.DECLINED ? (
|
||||
<Badge badgeType="danger">
|
||||
{intl.formatMessage(globalMessages.declined)}
|
||||
</Badge>
|
||||
) : requestData.media[
|
||||
requestData.is4k ? 'status4k' : 'status'
|
||||
] === MediaStatus.UNKNOWN ? (
|
||||
<Badge
|
||||
badgeType="danger"
|
||||
//href={`/${requestData.type}/${requestData.media.tmdbId}?manage=1`}
|
||||
>
|
||||
{intl.formatMessage(globalMessages.failed)}
|
||||
</Badge>
|
||||
) : (
|
||||
<StatusBadge
|
||||
status={
|
||||
requestData.media[requestData.is4k ? 'status4k' : 'status']
|
||||
}
|
||||
inProgress={
|
||||
(
|
||||
requestData.media[
|
||||
requestData.is4k ? 'downloadStatus4k' : 'downloadStatus'
|
||||
] ?? []
|
||||
).length > 0
|
||||
}
|
||||
is4k={requestData.is4k}
|
||||
tmdbId={requestData.media.tmdbId}
|
||||
mediaType={requestData.type}
|
||||
plexUrl={
|
||||
requestData.media[
|
||||
requestData.is4k ? 'mediaUrl4k' : 'mediaUrl'
|
||||
]
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="card-field">
|
||||
{hasPermission(
|
||||
[Permission.MANAGE_REQUESTS, Permission.REQUEST_VIEW],
|
||||
{ type: 'or' }
|
||||
) ? (
|
||||
<>
|
||||
<span className="card-field-name">
|
||||
{intl.formatMessage(messages.requested)}
|
||||
</span>
|
||||
<span className="flex truncate text-sm text-gray-300">
|
||||
{intl.formatMessage(messages.modifieduserdate, {
|
||||
date: (
|
||||
<FormattedRelativeTime
|
||||
value={Math.floor(
|
||||
(new Date(requestData.createdAt).getTime() -
|
||||
Date.now()) /
|
||||
1000
|
||||
)}
|
||||
updateIntervalInSeconds={1}
|
||||
numeric="auto"
|
||||
/>
|
||||
),
|
||||
user: (
|
||||
<Link href={`/users/${requestData.requestedBy.id}`}>
|
||||
<a className="group flex items-center truncate">
|
||||
<img
|
||||
src={requestData.requestedBy.avatar}
|
||||
alt=""
|
||||
className="avatar-sm ml-1.5"
|
||||
/>
|
||||
<span className="truncate text-sm font-semibold group-hover:text-white group-hover:underline">
|
||||
{requestData.requestedBy.displayName}
|
||||
</span>
|
||||
</a>
|
||||
</Link>
|
||||
),
|
||||
})}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="card-field-name">
|
||||
{intl.formatMessage(messages.requesteddate)}
|
||||
</span>
|
||||
<span className="flex truncate text-sm text-gray-300">
|
||||
<FormattedRelativeTime
|
||||
value={Math.floor(
|
||||
(new Date(requestData.createdAt).getTime() -
|
||||
Date.now()) /
|
||||
1000
|
||||
)}
|
||||
updateIntervalInSeconds={1}
|
||||
numeric="auto"
|
||||
/>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{requestData.modifiedBy && (
|
||||
<div className="card-field">
|
||||
<span className="card-field-name">
|
||||
{intl.formatMessage(messages.modified)}
|
||||
</span>
|
||||
<span className="flex truncate text-sm text-gray-300">
|
||||
{intl.formatMessage(messages.modifieduserdate, {
|
||||
date: (
|
||||
<FormattedRelativeTime
|
||||
value={Math.floor(
|
||||
(new Date(requestData.updatedAt).getTime() -
|
||||
Date.now()) /
|
||||
1000
|
||||
)}
|
||||
updateIntervalInSeconds={1}
|
||||
numeric="auto"
|
||||
/>
|
||||
),
|
||||
user: (
|
||||
<Link href={`/users/${requestData.modifiedBy.id}`}>
|
||||
<a className="group flex items-center truncate">
|
||||
<img
|
||||
src={requestData.modifiedBy.avatar}
|
||||
alt=""
|
||||
className="avatar-sm ml-1.5"
|
||||
/>
|
||||
<span className="truncate text-sm font-semibold group-hover:text-white group-hover:underline">
|
||||
{requestData.modifiedBy.displayName}
|
||||
</span>
|
||||
</a>
|
||||
</Link>
|
||||
),
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Table.TD>
|
||||
<Table.TD>
|
||||
{requestData.media[requestData.is4k ? 'status4k' : 'status'] ===
|
||||
MediaStatus.UNKNOWN ||
|
||||
requestData.status === MediaRequestStatus.DECLINED ? (
|
||||
<Badge badgeType="danger">
|
||||
{requestData.status === MediaRequestStatus.DECLINED
|
||||
? intl.formatMessage(globalMessages.declined)
|
||||
: intl.formatMessage(globalMessages.failed)}
|
||||
</Badge>
|
||||
) : (
|
||||
<StatusBadge
|
||||
status={requestData.media[requestData.is4k ? 'status4k' : 'status']}
|
||||
inProgress={
|
||||
(
|
||||
requestData.media[
|
||||
requestData.is4k ? 'downloadStatus4k' : 'downloadStatus'
|
||||
] ?? []
|
||||
).length > 0
|
||||
}
|
||||
is4k={requestData.is4k}
|
||||
/>
|
||||
)}
|
||||
</Table.TD>
|
||||
<Table.TD>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm text-gray-300">
|
||||
<FormattedDate value={requestData.createdAt} />
|
||||
</span>
|
||||
</div>
|
||||
</Table.TD>
|
||||
<Table.TD>
|
||||
<div className="flex flex-col">
|
||||
{requestData.modifiedBy ? (
|
||||
<span className="text-sm text-gray-300">
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
src={requestData.modifiedBy.avatar}
|
||||
alt=""
|
||||
className="w-5 mr-2 rounded-full"
|
||||
<div className="z-10 mt-4 flex w-full flex-col justify-center space-y-2 pl-4 pr-4 xl:mt-0 xl:w-96 xl:items-end xl:pl-0">
|
||||
{requestData.media[requestData.is4k ? 'status4k' : 'status'] ===
|
||||
MediaStatus.UNKNOWN &&
|
||||
requestData.status !== MediaRequestStatus.DECLINED &&
|
||||
hasPermission(Permission.MANAGE_REQUESTS) && (
|
||||
<Button
|
||||
className="w-full"
|
||||
buttonType="primary"
|
||||
disabled={isRetrying}
|
||||
onClick={() => retryRequest()}
|
||||
>
|
||||
<RefreshIcon
|
||||
className={isRetrying ? 'animate-spin' : ''}
|
||||
style={{ animationDirection: 'reverse' }}
|
||||
/>
|
||||
<span className="text-sm">
|
||||
{requestData.modifiedBy.displayName} (
|
||||
<FormattedRelativeTime
|
||||
value={Math.floor(
|
||||
(new Date(requestData.updatedAt).getTime() - Date.now()) /
|
||||
1000
|
||||
)}
|
||||
updateIntervalInSeconds={1}
|
||||
/>
|
||||
)
|
||||
<span>
|
||||
{intl.formatMessage(
|
||||
isRetrying ? globalMessages.retrying : globalMessages.retry
|
||||
)}
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
{requestData.status !== MediaRequestStatus.PENDING &&
|
||||
hasPermission(Permission.MANAGE_REQUESTS) && (
|
||||
<ConfirmButton
|
||||
onClick={() => deleteRequest()}
|
||||
confirmText={intl.formatMessage(globalMessages.areyousure)}
|
||||
className="w-full"
|
||||
>
|
||||
<TrashIcon />
|
||||
<span>{intl.formatMessage(messages.deleterequest)}</span>
|
||||
</ConfirmButton>
|
||||
)}
|
||||
{requestData.status === MediaRequestStatus.PENDING &&
|
||||
hasPermission(Permission.MANAGE_REQUESTS) && (
|
||||
<div className="flex w-full flex-row space-x-2">
|
||||
<span className="w-full">
|
||||
<Button
|
||||
className="w-full"
|
||||
buttonType="success"
|
||||
onClick={() => modifyRequest('approve')}
|
||||
>
|
||||
<CheckIcon />
|
||||
<span>{intl.formatMessage(globalMessages.approve)}</span>
|
||||
</Button>
|
||||
</span>
|
||||
<span className="w-full">
|
||||
<Button
|
||||
className="w-full"
|
||||
buttonType="danger"
|
||||
onClick={() => modifyRequest('decline')}
|
||||
>
|
||||
<XIcon />
|
||||
<span>{intl.formatMessage(globalMessages.decline)}</span>
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-sm text-gray-300">N/A</span>
|
||||
)}
|
||||
</div>
|
||||
</Table.TD>
|
||||
<Table.TD alignText="right">
|
||||
{requestData.media[requestData.is4k ? 'status4k' : 'status'] ===
|
||||
MediaStatus.UNKNOWN &&
|
||||
requestData.status !== MediaRequestStatus.DECLINED &&
|
||||
hasPermission(Permission.MANAGE_REQUESTS) && (
|
||||
<Button
|
||||
className="mr-2"
|
||||
buttonType="primary"
|
||||
buttonSize="sm"
|
||||
disabled={isRetrying}
|
||||
onClick={() => retryRequest()}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 mr-0 sm:mr-1"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="18px"
|
||||
height="18px"
|
||||
>
|
||||
<path d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M7 7h10v3l4-4-4-4v3H5v6h2V7zm10 10H7v-3l-4 4 4 4v-3h12v-6h-2v4z" />
|
||||
</svg>
|
||||
<span className="hidden sm:block">
|
||||
{intl.formatMessage(globalMessages.retry)}
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
{requestData.status !== MediaRequestStatus.PENDING &&
|
||||
hasPermission(Permission.MANAGE_REQUESTS) && (
|
||||
<Button
|
||||
buttonType="danger"
|
||||
buttonSize="sm"
|
||||
onClick={() => deleteRequest()}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 mr-0 sm:mr-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<span className="hidden sm:block">
|
||||
{intl.formatMessage(globalMessages.delete)}
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
{requestData.status === MediaRequestStatus.PENDING &&
|
||||
hasPermission(Permission.MANAGE_REQUESTS) && (
|
||||
<>
|
||||
<span className="mr-2">
|
||||
<Button
|
||||
buttonType="success"
|
||||
buttonSize="sm"
|
||||
onClick={() => modifyRequest('approve')}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 mr-0 sm:mr-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<span className="hidden sm:block">
|
||||
{intl.formatMessage(globalMessages.approve)}
|
||||
</span>
|
||||
</Button>
|
||||
</span>
|
||||
<span className="mr-2">
|
||||
<Button
|
||||
buttonType="danger"
|
||||
buttonSize="sm"
|
||||
onClick={() => modifyRequest('decline')}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 mr-0 sm:mr-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<span className="hidden sm:block">
|
||||
{intl.formatMessage(globalMessages.decline)}
|
||||
</span>
|
||||
</Button>
|
||||
</span>
|
||||
<span>
|
||||
)}
|
||||
{requestData.status === MediaRequestStatus.PENDING &&
|
||||
(hasPermission(Permission.MANAGE_REQUESTS) ||
|
||||
(requestData.requestedBy.id === user?.id &&
|
||||
(requestData.type === 'tv' ||
|
||||
hasPermission(Permission.REQUEST_ADVANCED)))) && (
|
||||
<span className="w-full">
|
||||
<Button
|
||||
className="w-full"
|
||||
buttonType="primary"
|
||||
buttonSize="sm"
|
||||
onClick={() => setShowEditModal(true)}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 mr-0 sm:mr-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
|
||||
</svg>
|
||||
<span className="hidden sm:block">
|
||||
{intl.formatMessage(globalMessages.edit)}
|
||||
</span>
|
||||
<PencilIcon />
|
||||
<span>{intl.formatMessage(messages.editrequest)}</span>
|
||||
</Button>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</Table.TD>
|
||||
</tr>
|
||||
)}
|
||||
{requestData.status === MediaRequestStatus.PENDING &&
|
||||
!hasPermission(Permission.MANAGE_REQUESTS) &&
|
||||
requestData.requestedBy.id === user?.id && (
|
||||
<ConfirmButton
|
||||
onClick={() => deleteRequest()}
|
||||
confirmText={intl.formatMessage(globalMessages.areyousure)}
|
||||
className="w-full"
|
||||
>
|
||||
<XIcon />
|
||||
<span>{intl.formatMessage(messages.cancelRequest)}</span>
|
||||
</ConfirmButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,51 +1,98 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
FilterIcon,
|
||||
SortDescendingIcon,
|
||||
} from '@heroicons/react/solid';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import useSWR from 'swr';
|
||||
import type { RequestResultsResponse } from '../../../server/interfaces/api/requestInterfaces';
|
||||
import LoadingSpinner from '../Common/LoadingSpinner';
|
||||
import RequestItem from './RequestItem';
|
||||
import Header from '../Common/Header';
|
||||
import Table from '../Common/Table';
|
||||
import { useUpdateQueryParams } from '../../hooks/useUpdateQueryParams';
|
||||
import { useUser } from '../../hooks/useUser';
|
||||
import globalMessages from '../../i18n/globalMessages';
|
||||
import Button from '../Common/Button';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import Header from '../Common/Header';
|
||||
import LoadingSpinner from '../Common/LoadingSpinner';
|
||||
import PageTitle from '../Common/PageTitle';
|
||||
import RequestItem from './RequestItem';
|
||||
|
||||
const messages = defineMessages({
|
||||
requests: 'Requests',
|
||||
mediaInfo: 'Media Info',
|
||||
status: 'Status',
|
||||
requestedAt: 'Requested At',
|
||||
modifiedBy: 'Last Modified By',
|
||||
showingresults:
|
||||
'Showing <strong>{from}</strong> to <strong>{to}</strong> of <strong>{total}</strong> results',
|
||||
resultsperpage: 'Display {pageSize} results per page',
|
||||
next: 'Next',
|
||||
previous: 'Previous',
|
||||
filterAll: 'All',
|
||||
filterPending: 'Pending',
|
||||
filterApproved: 'Approved',
|
||||
filterAvailable: 'Available',
|
||||
filterProcessing: 'Processing',
|
||||
noresults: 'No results.',
|
||||
showallrequests: 'Show All Requests',
|
||||
sortAdded: 'Request Date',
|
||||
sortAdded: 'Most Recent',
|
||||
sortModified: 'Last Modified',
|
||||
});
|
||||
|
||||
type Filter = 'all' | 'pending' | 'approved' | 'processing' | 'available';
|
||||
enum Filter {
|
||||
ALL = 'all',
|
||||
PENDING = 'pending',
|
||||
APPROVED = 'approved',
|
||||
PROCESSING = 'processing',
|
||||
AVAILABLE = 'available',
|
||||
UNAVAILABLE = 'unavailable',
|
||||
}
|
||||
|
||||
type Sort = 'added' | 'modified';
|
||||
|
||||
const RequestList: React.FC = () => {
|
||||
const router = useRouter();
|
||||
const intl = useIntl();
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [currentFilter, setCurrentFilter] = useState<Filter>('pending');
|
||||
const { user } = useUser({
|
||||
id: Number(router.query.userId),
|
||||
});
|
||||
const [currentFilter, setCurrentFilter] = useState<Filter>(Filter.PENDING);
|
||||
const [currentSort, setCurrentSort] = useState<Sort>('added');
|
||||
const [currentPageSize, setCurrentPageSize] = useState<number>(10);
|
||||
|
||||
const { data, error, revalidate } = useSWR<RequestResultsResponse>(
|
||||
const page = router.query.page ? Number(router.query.page) : 1;
|
||||
const pageIndex = page - 1;
|
||||
const updateQueryParams = useUpdateQueryParams({ page: page.toString() });
|
||||
|
||||
const {
|
||||
data,
|
||||
error,
|
||||
mutate: revalidate,
|
||||
} = useSWR<RequestResultsResponse>(
|
||||
`/api/v1/request?take=${currentPageSize}&skip=${
|
||||
pageIndex * currentPageSize
|
||||
}&filter=${currentFilter}&sort=${currentSort}`
|
||||
}&filter=${currentFilter}&sort=${currentSort}${
|
||||
router.query.userId ? `&requestedBy=${router.query.userId}` : ''
|
||||
}`
|
||||
);
|
||||
|
||||
// Restore last set filter values on component mount
|
||||
useEffect(() => {
|
||||
const filterString = window.localStorage.getItem('rl-filter-settings');
|
||||
|
||||
if (filterString) {
|
||||
const filterSettings = JSON.parse(filterString);
|
||||
|
||||
setCurrentFilter(filterSettings.currentFilter);
|
||||
setCurrentSort(filterSettings.currentSort);
|
||||
setCurrentPageSize(filterSettings.currentPageSize);
|
||||
}
|
||||
|
||||
// If filter value is provided in query, use that instead
|
||||
if (Object.values(Filter).includes(router.query.filter as Filter)) {
|
||||
setCurrentFilter(router.query.filter as Filter);
|
||||
}
|
||||
}, [router.query.filter]);
|
||||
|
||||
// Set filter values to local storage any time they are changed
|
||||
useEffect(() => {
|
||||
window.localStorage.setItem(
|
||||
'rl-filter-settings',
|
||||
JSON.stringify({
|
||||
currentFilter,
|
||||
currentSort,
|
||||
currentPageSize,
|
||||
})
|
||||
);
|
||||
}, [currentFilter, currentSort, currentPageSize]);
|
||||
|
||||
if (!data && !error) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
@@ -59,73 +106,81 @@ const RequestList: React.FC = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageTitle title={intl.formatMessage(messages.requests)} />
|
||||
<div className="flex flex-col justify-between lg:items-end lg:flex-row">
|
||||
<Header>{intl.formatMessage(messages.requests)}</Header>
|
||||
<div className="flex flex-col flex-grow mt-2 sm:flex-row lg:flex-grow-0">
|
||||
<div className="flex flex-grow mb-2 sm:mb-0 sm:mr-2 lg:flex-grow-0">
|
||||
<span className="inline-flex items-center px-3 text-sm text-gray-100 bg-gray-800 border border-r-0 border-gray-500 cursor-default rounded-l-md">
|
||||
<svg
|
||||
className="w-6 h-6"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<PageTitle
|
||||
title={[
|
||||
intl.formatMessage(messages.requests),
|
||||
router.query.userId ? user?.displayName : '',
|
||||
]}
|
||||
/>
|
||||
<div className="mb-4 flex flex-col justify-between lg:flex-row lg:items-end">
|
||||
<Header
|
||||
subtext={
|
||||
router.query.userId ? (
|
||||
<Link href={`/users/${user?.id}`}>
|
||||
<a className="hover:underline">{user?.displayName}</a>
|
||||
</Link>
|
||||
) : (
|
||||
''
|
||||
)
|
||||
}
|
||||
>
|
||||
{intl.formatMessage(messages.requests)}
|
||||
</Header>
|
||||
<div className="mt-2 flex flex-grow flex-col sm:flex-row lg:flex-grow-0">
|
||||
<div className="mb-2 flex flex-grow sm:mb-0 sm:mr-2 lg:flex-grow-0">
|
||||
<span className="inline-flex cursor-default items-center rounded-l-md border border-r-0 border-gray-500 bg-gray-800 px-3 text-sm text-gray-100">
|
||||
<FilterIcon className="h-6 w-6" />
|
||||
</span>
|
||||
<select
|
||||
id="filter"
|
||||
name="filter"
|
||||
onChange={(e) => {
|
||||
setPageIndex(0);
|
||||
setCurrentFilter(e.target.value as Filter);
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: router.query.userId
|
||||
? { userId: router.query.userId }
|
||||
: {},
|
||||
});
|
||||
}}
|
||||
value={currentFilter}
|
||||
className="rounded-r-only"
|
||||
>
|
||||
<option value="all">
|
||||
{intl.formatMessage(messages.filterAll)}
|
||||
{intl.formatMessage(globalMessages.all)}
|
||||
</option>
|
||||
<option value="pending">
|
||||
{intl.formatMessage(messages.filterPending)}
|
||||
{intl.formatMessage(globalMessages.pending)}
|
||||
</option>
|
||||
<option value="approved">
|
||||
{intl.formatMessage(messages.filterApproved)}
|
||||
{intl.formatMessage(globalMessages.approved)}
|
||||
</option>
|
||||
<option value="processing">
|
||||
{intl.formatMessage(messages.filterProcessing)}
|
||||
{intl.formatMessage(globalMessages.processing)}
|
||||
</option>
|
||||
<option value="available">
|
||||
{intl.formatMessage(messages.filterAvailable)}
|
||||
{intl.formatMessage(globalMessages.available)}
|
||||
</option>
|
||||
<option value="unavailable">
|
||||
{intl.formatMessage(globalMessages.unavailable)}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex flex-grow mb-2 sm:mb-0 lg:flex-grow-0">
|
||||
<span className="inline-flex items-center px-3 text-gray-100 bg-gray-800 border border-r-0 border-gray-500 cursor-default sm:text-sm rounded-l-md">
|
||||
<svg
|
||||
className="w-6 h-6"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M3 3a1 1 0 000 2h11a1 1 0 100-2H3zM3 7a1 1 0 000 2h7a1 1 0 100-2H3zM3 11a1 1 0 100 2h4a1 1 0 100-2H3zM15 8a1 1 0 10-2 0v5.586l-1.293-1.293a1 1 0 00-1.414 1.414l3 3a1 1 0 001.414 0l3-3a1 1 0 00-1.414-1.414L15 13.586V8z" />
|
||||
</svg>
|
||||
<div className="mb-2 flex flex-grow sm:mb-0 lg:flex-grow-0">
|
||||
<span className="inline-flex cursor-default items-center rounded-l-md border border-r-0 border-gray-500 bg-gray-800 px-3 text-gray-100 sm:text-sm">
|
||||
<SortDescendingIcon className="h-6 w-6" />
|
||||
</span>
|
||||
<select
|
||||
id="sort"
|
||||
name="sort"
|
||||
onChange={(e) => {
|
||||
setPageIndex(0);
|
||||
setCurrentSort(e.target.value as Sort);
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
setPageIndex(0);
|
||||
setCurrentSort(e.target.value as Sort);
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: router.query.userId
|
||||
? { userId: router.query.userId }
|
||||
: {},
|
||||
});
|
||||
}}
|
||||
value={currentSort}
|
||||
className="rounded-r-only"
|
||||
@@ -140,114 +195,104 @@ const RequestList: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<Table.TH>{intl.formatMessage(messages.mediaInfo)}</Table.TH>
|
||||
<Table.TH>{intl.formatMessage(messages.status)}</Table.TH>
|
||||
<Table.TH>{intl.formatMessage(messages.requestedAt)}</Table.TH>
|
||||
<Table.TH>{intl.formatMessage(messages.modifiedBy)}</Table.TH>
|
||||
<Table.TH></Table.TH>
|
||||
</tr>
|
||||
</thead>
|
||||
<Table.TBody>
|
||||
{data.results.map((request) => {
|
||||
return (
|
||||
<RequestItem
|
||||
request={request}
|
||||
key={`request-list-${request.id}`}
|
||||
revalidateList={() => revalidate()}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{data.results.map((request) => {
|
||||
return (
|
||||
<div className="py-2" key={`request-list-${request.id}`}>
|
||||
<RequestItem
|
||||
request={request}
|
||||
revalidateList={() => revalidate()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{data.results.length === 0 && (
|
||||
<tr className="relative h-24 p-2 text-white">
|
||||
<Table.TD colSpan={6} noPadding>
|
||||
<div className="flex flex-col items-center justify-center w-screen p-6 lg:w-full">
|
||||
<span className="text-base">
|
||||
{intl.formatMessage(messages.noresults)}
|
||||
</span>
|
||||
{currentFilter !== 'all' && (
|
||||
<div className="mt-4">
|
||||
<Button
|
||||
buttonSize="sm"
|
||||
buttonType="primary"
|
||||
onClick={() => setCurrentFilter('all')}
|
||||
>
|
||||
{intl.formatMessage(messages.showallrequests)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Table.TD>
|
||||
</tr>
|
||||
)}
|
||||
<tr className="bg-gray-700">
|
||||
<Table.TD colSpan={6} noPadding>
|
||||
<nav
|
||||
className="flex flex-col items-center w-screen px-6 py-3 space-x-4 space-y-3 sm:space-y-0 sm:flex-row lg:w-full"
|
||||
aria-label="Pagination"
|
||||
{data.results.length === 0 && (
|
||||
<div className="flex w-full flex-col items-center justify-center py-24 text-white">
|
||||
<span className="text-2xl text-gray-400">
|
||||
{intl.formatMessage(globalMessages.noresults)}
|
||||
</span>
|
||||
{currentFilter !== Filter.ALL && (
|
||||
<div className="mt-4">
|
||||
<Button
|
||||
buttonType="primary"
|
||||
onClick={() => setCurrentFilter(Filter.ALL)}
|
||||
>
|
||||
<div className="hidden lg:flex lg:flex-1">
|
||||
<p className="text-sm">
|
||||
{data.results.length > 0 &&
|
||||
intl.formatMessage(messages.showingresults, {
|
||||
from: pageIndex * currentPageSize + 1,
|
||||
to:
|
||||
data.results.length < currentPageSize
|
||||
? pageIndex * currentPageSize + data.results.length
|
||||
: (pageIndex + 1) * currentPageSize,
|
||||
total: data.pageInfo.results,
|
||||
strong: function strong(msg) {
|
||||
return <span className="font-medium">{msg}</span>;
|
||||
},
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-center sm:flex-1 sm:justify-start lg:justify-center">
|
||||
<span className="items-center -mt-3 text-sm sm:-ml-4 lg:ml-0 sm:mt-0">
|
||||
{intl.formatMessage(messages.resultsperpage, {
|
||||
pageSize: (
|
||||
<select
|
||||
id="pageSize"
|
||||
name="pageSize"
|
||||
onChange={(e) => {
|
||||
setPageIndex(0);
|
||||
setCurrentPageSize(Number(e.target.value));
|
||||
}}
|
||||
value={currentPageSize}
|
||||
className="inline short"
|
||||
>
|
||||
<option value="5">5</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
),
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-center flex-auto space-x-2 sm:justify-end sm:flex-1">
|
||||
<Button
|
||||
disabled={!hasPrevPage}
|
||||
onClick={() => setPageIndex((current) => current - 1)}
|
||||
{intl.formatMessage(messages.showallrequests)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="actions">
|
||||
<nav
|
||||
className="mb-3 flex flex-col items-center space-y-3 sm:flex-row sm:space-y-0"
|
||||
aria-label="Pagination"
|
||||
>
|
||||
<div className="hidden lg:flex lg:flex-1">
|
||||
<p className="text-sm">
|
||||
{data.results.length > 0 &&
|
||||
intl.formatMessage(globalMessages.showingresults, {
|
||||
from: pageIndex * currentPageSize + 1,
|
||||
to:
|
||||
data.results.length < currentPageSize
|
||||
? pageIndex * currentPageSize + data.results.length
|
||||
: (pageIndex + 1) * currentPageSize,
|
||||
total: data.pageInfo.results,
|
||||
strong: function strong(msg) {
|
||||
return <span className="font-medium">{msg}</span>;
|
||||
},
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-center sm:flex-1 sm:justify-start lg:justify-center">
|
||||
<span className="-mt-3 items-center truncate text-sm sm:mt-0">
|
||||
{intl.formatMessage(globalMessages.resultsperpage, {
|
||||
pageSize: (
|
||||
<select
|
||||
id="pageSize"
|
||||
name="pageSize"
|
||||
onChange={(e) => {
|
||||
setCurrentPageSize(Number(e.target.value));
|
||||
router
|
||||
.push({
|
||||
pathname: router.pathname,
|
||||
query: router.query.userId
|
||||
? { userId: router.query.userId }
|
||||
: {},
|
||||
})
|
||||
.then(() => window.scrollTo(0, 0));
|
||||
}}
|
||||
value={currentPageSize}
|
||||
className="short inline"
|
||||
>
|
||||
{intl.formatMessage(messages.previous)}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!hasNextPage}
|
||||
onClick={() => setPageIndex((current) => current + 1)}
|
||||
>
|
||||
{intl.formatMessage(messages.next)}
|
||||
</Button>
|
||||
</div>
|
||||
</nav>
|
||||
</Table.TD>
|
||||
</tr>
|
||||
</Table.TBody>
|
||||
</Table>
|
||||
<option value="5">5</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
),
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-auto justify-center space-x-2 sm:flex-1 sm:justify-end">
|
||||
<Button
|
||||
disabled={!hasPrevPage}
|
||||
onClick={() => updateQueryParams('page', (page - 1).toString())}
|
||||
>
|
||||
<ChevronLeftIcon />
|
||||
<span>{intl.formatMessage(globalMessages.previous)}</span>
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!hasNextPage}
|
||||
onClick={() => updateQueryParams('page', (page + 1).toString())}
|
||||
>
|
||||
<span>{intl.formatMessage(globalMessages.next)}</span>
|
||||
<ChevronRightIcon />
|
||||
</Button>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -23,12 +23,14 @@ const messages = defineMessages({
|
||||
requesttitle: 'Request {title}',
|
||||
request4ktitle: 'Request {title} in 4K',
|
||||
edit: 'Edit Request',
|
||||
approve: 'Approve Request',
|
||||
cancel: 'Cancel Request',
|
||||
pendingrequest: 'Pending Request for {title}',
|
||||
pending4krequest: 'Pending 4K Request for {title}',
|
||||
requestfrom: "{username}'s request is pending approval.",
|
||||
errorediting: 'Something went wrong while editing the request.',
|
||||
requestedited: 'Request for <strong>{title}</strong> edited successfully!',
|
||||
requestApproved: 'Request for <strong>{title}</strong> approved!',
|
||||
requesterror: 'Something went wrong while submitting the request.',
|
||||
pendingapproval: 'Your request is pending approval.',
|
||||
});
|
||||
@@ -60,7 +62,10 @@ const MovieRequestModal: React.FC<RequestModalProps> = ({
|
||||
const intl = useIntl();
|
||||
const { user, hasPermission } = useUser();
|
||||
const { data: quota } = useSWR<QuotaResponse>(
|
||||
user ? `/api/v1/user/${requestOverrides?.user?.id ?? user.id}/quota` : null
|
||||
user &&
|
||||
(!requestOverrides?.user?.id || hasPermission(Permission.MANAGE_USERS))
|
||||
? `/api/v1/user/${requestOverrides?.user?.id ?? user.id}/quota`
|
||||
: null
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -156,7 +161,7 @@ const MovieRequestModal: React.FC<RequestModalProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const updateRequest = async () => {
|
||||
const updateRequest = async (alsoApproveRequest = false) => {
|
||||
setIsUpdating(true);
|
||||
|
||||
try {
|
||||
@@ -169,14 +174,23 @@ const MovieRequestModal: React.FC<RequestModalProps> = ({
|
||||
tags: requestOverrides?.tags,
|
||||
});
|
||||
|
||||
if (alsoApproveRequest) {
|
||||
await axios.post(`/api/v1/request/${editRequest?.id}/approve`);
|
||||
}
|
||||
|
||||
addToast(
|
||||
<span>
|
||||
{intl.formatMessage(messages.requestedited, {
|
||||
title: data?.title,
|
||||
strong: function strong(msg) {
|
||||
return <strong>{msg}</strong>;
|
||||
},
|
||||
})}
|
||||
{intl.formatMessage(
|
||||
alsoApproveRequest
|
||||
? messages.requestApproved
|
||||
: messages.requestedited,
|
||||
{
|
||||
title: data?.title,
|
||||
strong: function strong(msg) {
|
||||
return <strong>{msg}</strong>;
|
||||
},
|
||||
}
|
||||
)}
|
||||
</span>,
|
||||
{
|
||||
appearance: 'success',
|
||||
@@ -199,12 +213,6 @@ const MovieRequestModal: React.FC<RequestModalProps> = ({
|
||||
|
||||
if (editRequest) {
|
||||
const isOwner = editRequest.requestedBy.id === user?.id;
|
||||
const showEditButton = hasPermission(
|
||||
[Permission.MANAGE_REQUESTS, Permission.REQUEST_ADVANCED],
|
||||
{
|
||||
type: 'or',
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -215,20 +223,44 @@ const MovieRequestModal: React.FC<RequestModalProps> = ({
|
||||
is4k ? messages.pending4krequest : messages.pendingrequest,
|
||||
{ title: data?.title }
|
||||
)}
|
||||
onOk={() => (showEditButton ? updateRequest() : cancelRequest())}
|
||||
onOk={() =>
|
||||
hasPermission(Permission.MANAGE_REQUESTS)
|
||||
? updateRequest(true)
|
||||
: hasPermission(Permission.REQUEST_ADVANCED)
|
||||
? updateRequest()
|
||||
: cancelRequest()
|
||||
}
|
||||
okDisabled={isUpdating}
|
||||
okText={
|
||||
showEditButton
|
||||
hasPermission(Permission.MANAGE_REQUESTS)
|
||||
? intl.formatMessage(messages.approve)
|
||||
: hasPermission(Permission.REQUEST_ADVANCED)
|
||||
? intl.formatMessage(messages.edit)
|
||||
: intl.formatMessage(messages.cancel)
|
||||
}
|
||||
okButtonType={showEditButton ? 'primary' : 'danger'}
|
||||
okButtonType={
|
||||
hasPermission(Permission.MANAGE_REQUESTS)
|
||||
? 'success'
|
||||
: hasPermission(Permission.REQUEST_ADVANCED)
|
||||
? 'primary'
|
||||
: 'danger'
|
||||
}
|
||||
onSecondary={
|
||||
isOwner && showEditButton ? () => cancelRequest() : undefined
|
||||
isOwner &&
|
||||
hasPermission(
|
||||
[Permission.REQUEST_ADVANCED, Permission.MANAGE_REQUESTS],
|
||||
{ type: 'or' }
|
||||
)
|
||||
? () => cancelRequest()
|
||||
: undefined
|
||||
}
|
||||
secondaryDisabled={isUpdating}
|
||||
secondaryText={
|
||||
isOwner && showEditButton
|
||||
isOwner &&
|
||||
hasPermission(
|
||||
[Permission.REQUEST_ADVANCED, Permission.MANAGE_REQUESTS],
|
||||
{ type: 'or' }
|
||||
)
|
||||
? intl.formatMessage(messages.cancel)
|
||||
: undefined
|
||||
}
|
||||
@@ -244,22 +276,20 @@ const MovieRequestModal: React.FC<RequestModalProps> = ({
|
||||
})}
|
||||
{(hasPermission(Permission.REQUEST_ADVANCED) ||
|
||||
hasPermission(Permission.MANAGE_REQUESTS)) && (
|
||||
<div className="mt-4">
|
||||
<AdvancedRequester
|
||||
type="movie"
|
||||
is4k={is4k}
|
||||
requestUser={editRequest.requestedBy}
|
||||
defaultOverrides={{
|
||||
folder: editRequest.rootFolder,
|
||||
profile: editRequest.profileId,
|
||||
server: editRequest.serverId,
|
||||
tags: editRequest.tags,
|
||||
}}
|
||||
onChange={(overrides) => {
|
||||
setRequestOverrides(overrides);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<AdvancedRequester
|
||||
type="movie"
|
||||
is4k={is4k}
|
||||
requestUser={editRequest.requestedBy}
|
||||
defaultOverrides={{
|
||||
folder: editRequest.rootFolder,
|
||||
profile: editRequest.profileId,
|
||||
server: editRequest.serverId,
|
||||
tags: editRequest.tags,
|
||||
}}
|
||||
onChange={(overrides) => {
|
||||
setRequestOverrides(overrides);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -30,13 +30,15 @@ const messages = defineMessages({
|
||||
requesttitle: 'Request {title}',
|
||||
request4ktitle: 'Request {title} in 4K',
|
||||
edit: 'Edit Request',
|
||||
approve: 'Approve Request',
|
||||
cancel: 'Cancel Request',
|
||||
pendingrequest: 'Pending Request for {title}',
|
||||
pending4krequest: 'Pending 4K Request for {title}',
|
||||
requestfrom: "{username}'s request is pending approval.",
|
||||
requestseasons:
|
||||
'Request {seasonCount} {seasonCount, plural, one {Season} other {Seasons}}',
|
||||
requestall: 'Request All Seasons',
|
||||
requestseasons4k:
|
||||
'Request {seasonCount} {seasonCount, plural, one {Season} other {Seasons}} in 4K',
|
||||
alreadyrequested: 'Already Requested',
|
||||
selectseason: 'Select Season(s)',
|
||||
season: 'Season',
|
||||
@@ -45,6 +47,7 @@ const messages = defineMessages({
|
||||
extras: 'Extras',
|
||||
errorediting: 'Something went wrong while editing the request.',
|
||||
requestedited: 'Request for <strong>{title}</strong> edited successfully!',
|
||||
requestApproved: 'Request for <strong>{title}</strong> approved!',
|
||||
requestcancelled: 'Request for <strong>{title}</strong> canceled.',
|
||||
autoapproval: 'Automatic Approval',
|
||||
requesterror: 'Something went wrong while submitting the request.',
|
||||
@@ -88,7 +91,10 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
|
||||
});
|
||||
const [tvdbId, setTvdbId] = useState<number | undefined>(undefined);
|
||||
const { data: quota } = useSWR<QuotaResponse>(
|
||||
user ? `/api/v1/user/${requestOverrides?.user?.id ?? user.id}/quota` : null
|
||||
user &&
|
||||
(!requestOverrides?.user?.id || hasPermission(Permission.MANAGE_USERS))
|
||||
? `/api/v1/user/${requestOverrides?.user?.id ?? user.id}/quota`
|
||||
: null
|
||||
);
|
||||
|
||||
const currentlyRemaining =
|
||||
@@ -96,7 +102,7 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
|
||||
selectedSeasons.length +
|
||||
(editRequest?.seasons ?? []).length;
|
||||
|
||||
const updateRequest = async () => {
|
||||
const updateRequest = async (alsoApproveRequest = false) => {
|
||||
if (!editRequest) {
|
||||
return;
|
||||
}
|
||||
@@ -117,6 +123,10 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
|
||||
tags: requestOverrides?.tags,
|
||||
seasons: selectedSeasons,
|
||||
});
|
||||
|
||||
if (alsoApproveRequest) {
|
||||
await axios.post(`/api/v1/request/${editRequest.id}/approve`);
|
||||
}
|
||||
} else {
|
||||
await axios.delete(`/api/v1/request/${editRequest.id}`);
|
||||
}
|
||||
@@ -124,12 +134,17 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
|
||||
addToast(
|
||||
<span>
|
||||
{selectedSeasons.length > 0
|
||||
? intl.formatMessage(messages.requestedited, {
|
||||
title: data?.name,
|
||||
strong: function strong(msg) {
|
||||
return <strong>{msg}</strong>;
|
||||
},
|
||||
})
|
||||
? intl.formatMessage(
|
||||
alsoApproveRequest
|
||||
? messages.requestApproved
|
||||
: messages.requestedited,
|
||||
{
|
||||
title: data?.name,
|
||||
strong: function strong(msg) {
|
||||
return <strong>{msg}</strong>;
|
||||
},
|
||||
}
|
||||
)
|
||||
: intl.formatMessage(messages.requestcancelled, {
|
||||
title: data?.name,
|
||||
strong: function strong(msg) {
|
||||
@@ -368,7 +383,13 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
|
||||
loading={!data && !error}
|
||||
backgroundClickable
|
||||
onCancel={tvdbId ? () => setSearchModal({ show: true }) : onCancel}
|
||||
onOk={() => (editRequest ? updateRequest() : sendRequest())}
|
||||
onOk={() =>
|
||||
editRequest
|
||||
? hasPermission(Permission.MANAGE_REQUESTS)
|
||||
? updateRequest(true)
|
||||
: updateRequest()
|
||||
: sendRequest()
|
||||
}
|
||||
title={intl.formatMessage(
|
||||
editRequest
|
||||
? is4k
|
||||
@@ -383,16 +404,23 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
|
||||
editRequest
|
||||
? selectedSeasons.length === 0
|
||||
? intl.formatMessage(messages.cancel)
|
||||
: hasPermission(Permission.MANAGE_REQUESTS)
|
||||
? intl.formatMessage(messages.approve)
|
||||
: intl.formatMessage(messages.edit)
|
||||
: getAllRequestedSeasons().length >= getAllSeasons().length
|
||||
? intl.formatMessage(messages.alreadyrequested)
|
||||
: !settings.currentSettings.partialRequestsEnabled
|
||||
? intl.formatMessage(messages.requestall)
|
||||
? intl.formatMessage(
|
||||
is4k ? globalMessages.request4k : globalMessages.request
|
||||
)
|
||||
: selectedSeasons.length === 0
|
||||
? intl.formatMessage(messages.selectseason)
|
||||
: intl.formatMessage(messages.requestseasons, {
|
||||
seasonCount: selectedSeasons.length,
|
||||
})
|
||||
: intl.formatMessage(
|
||||
is4k ? messages.requestseasons4k : messages.requestseasons,
|
||||
{
|
||||
seasonCount: selectedSeasons.length,
|
||||
}
|
||||
)
|
||||
}
|
||||
okDisabled={
|
||||
editRequest
|
||||
@@ -406,11 +434,14 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
|
||||
selectedSeasons.length === 0)
|
||||
}
|
||||
okButtonType={
|
||||
editRequest &&
|
||||
settings.currentSettings.partialRequestsEnabled &&
|
||||
selectedSeasons.length === 0
|
||||
? 'danger'
|
||||
: `primary`
|
||||
editRequest
|
||||
? settings.currentSettings.partialRequestsEnabled &&
|
||||
selectedSeasons.length === 0
|
||||
? 'danger'
|
||||
: hasPermission(Permission.MANAGE_REQUESTS)
|
||||
? 'success'
|
||||
: 'primary'
|
||||
: 'primary'
|
||||
}
|
||||
cancelText={
|
||||
editRequest
|
||||
@@ -440,7 +471,7 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
|
||||
!(
|
||||
quota?.tv.limit &&
|
||||
!settings.currentSettings.partialRequestsEnabled &&
|
||||
unrequestedSeasons.length > (quota?.tv.limit ?? 0)
|
||||
unrequestedSeasons.length > (quota?.tv.remaining ?? 0)
|
||||
) &&
|
||||
getAllRequestedSeasons().length < getAllSeasons().length &&
|
||||
!editRequest && (
|
||||
@@ -457,7 +488,7 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
|
||||
quota={quota?.tv}
|
||||
remaining={
|
||||
!settings.currentSettings.partialRequestsEnabled &&
|
||||
unrequestedSeasons.length > (quota?.tv.limit ?? 0)
|
||||
unrequestedSeasons.length > (quota?.tv.remaining ?? 0)
|
||||
? 0
|
||||
: currentlyRemaining
|
||||
}
|
||||
@@ -468,7 +499,7 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
|
||||
}
|
||||
overLimit={
|
||||
!settings.currentSettings.partialRequestsEnabled &&
|
||||
unrequestedSeasons.length > (quota?.tv.limit ?? 0)
|
||||
unrequestedSeasons.length > (quota?.tv.remaining ?? 0)
|
||||
? unrequestedSeasons.length
|
||||
: undefined
|
||||
}
|
||||
@@ -482,7 +513,7 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
className={`w-16 px-4 py-3 bg-gray-500 ${
|
||||
className={`w-16 bg-gray-500 px-4 py-3 ${
|
||||
!settings.currentSettings.partialRequestsEnabled &&
|
||||
'hidden'
|
||||
}`}
|
||||
@@ -497,7 +528,7 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
|
||||
toggleAllSeasons();
|
||||
}
|
||||
}}
|
||||
className={`relative inline-flex items-center justify-center flex-shrink-0 w-10 h-5 pt-2 cursor-pointer focus:outline-none ${
|
||||
className={`relative inline-flex h-5 w-10 flex-shrink-0 cursor-pointer items-center justify-center pt-2 focus:outline-none ${
|
||||
quota?.tv.remaining &&
|
||||
quota.tv.limit &&
|
||||
quota.tv.remaining < unrequestedSeasons.length
|
||||
@@ -509,28 +540,28 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
|
||||
aria-hidden="true"
|
||||
className={`${
|
||||
isAllSeasons() ? 'bg-indigo-500' : 'bg-gray-800'
|
||||
} absolute h-4 w-9 mx-auto rounded-full transition-colors ease-in-out duration-200`}
|
||||
} absolute mx-auto h-4 w-9 rounded-full transition-colors duration-200 ease-in-out`}
|
||||
></span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`${
|
||||
isAllSeasons() ? 'translate-x-5' : 'translate-x-0'
|
||||
} absolute left-0 inline-block h-5 w-5 border border-gray-200 rounded-full bg-white shadow transform group-focus:ring group-focus:border-blue-300 transition-transform ease-in-out duration-200`}
|
||||
} absolute left-0 inline-block h-5 w-5 transform rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
|
||||
></span>
|
||||
</span>
|
||||
</th>
|
||||
<th className="px-1 py-3 text-xs font-medium leading-4 tracking-wider text-left text-gray-200 uppercase bg-gray-500 md:px-6">
|
||||
<th className="bg-gray-500 px-1 py-3 text-left text-xs font-medium uppercase leading-4 tracking-wider text-gray-200 md:px-6">
|
||||
{intl.formatMessage(messages.season)}
|
||||
</th>
|
||||
<th className="px-5 py-3 text-xs font-medium leading-4 tracking-wider text-left text-gray-200 uppercase bg-gray-500 md:px-6">
|
||||
<th className="bg-gray-500 px-5 py-3 text-left text-xs font-medium uppercase leading-4 tracking-wider text-gray-200 md:px-6">
|
||||
{intl.formatMessage(messages.numberofepisodes)}
|
||||
</th>
|
||||
<th className="px-2 py-3 text-xs font-medium leading-4 tracking-wider text-left text-gray-200 uppercase bg-gray-500 md:px-6">
|
||||
<th className="bg-gray-500 px-2 py-3 text-left text-xs font-medium uppercase leading-4 tracking-wider text-gray-200 md:px-6">
|
||||
{intl.formatMessage(globalMessages.status)}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-gray-600 divide-y divide-gray-700">
|
||||
<tbody className="divide-y divide-gray-700 bg-gray-600">
|
||||
{data?.seasons
|
||||
.filter((season) => season.seasonNumber !== 0)
|
||||
.map((season) => {
|
||||
@@ -546,7 +577,7 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
|
||||
return (
|
||||
<tr key={`season-${season.id}`}>
|
||||
<td
|
||||
className={`px-4 py-4 text-sm font-medium leading-5 text-gray-100 whitespace-nowrap ${
|
||||
className={`whitespace-nowrap px-4 py-4 text-sm font-medium leading-5 text-gray-100 ${
|
||||
!settings.currentSettings
|
||||
.partialRequestsEnabled && 'hidden'
|
||||
}`}
|
||||
@@ -568,7 +599,7 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
|
||||
toggleSeason(season.seasonNumber);
|
||||
}
|
||||
}}
|
||||
className={`pt-2 relative inline-flex items-center justify-center flex-shrink-0 h-5 w-10 cursor-pointer focus:outline-none ${
|
||||
className={`relative inline-flex h-5 w-10 flex-shrink-0 cursor-pointer items-center justify-center pt-2 focus:outline-none ${
|
||||
mediaSeason ||
|
||||
(quota?.tv.limit &&
|
||||
currentlyRemaining <= 0 &&
|
||||
@@ -590,7 +621,7 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
|
||||
isSelectedSeason(season.seasonNumber)
|
||||
? 'bg-indigo-500'
|
||||
: 'bg-gray-800'
|
||||
} absolute h-4 w-9 mx-auto rounded-full transition-colors ease-in-out duration-200`}
|
||||
} absolute mx-auto h-4 w-9 rounded-full transition-colors duration-200 ease-in-out`}
|
||||
></span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
@@ -603,21 +634,21 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
|
||||
isSelectedSeason(season.seasonNumber)
|
||||
? 'translate-x-5'
|
||||
: 'translate-x-0'
|
||||
} absolute left-0 inline-block h-5 w-5 border border-gray-200 rounded-full bg-white shadow transform group-focus:ring group-focus:border-blue-300 transition-transform ease-in-out duration-200`}
|
||||
} absolute left-0 inline-block h-5 w-5 transform rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
|
||||
></span>
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-1 py-4 text-sm font-medium leading-5 text-gray-100 md:px-6 whitespace-nowrap">
|
||||
<td className="whitespace-nowrap px-1 py-4 text-sm font-medium leading-5 text-gray-100 md:px-6">
|
||||
{season.seasonNumber === 0
|
||||
? intl.formatMessage(messages.extras)
|
||||
: intl.formatMessage(messages.seasonnumber, {
|
||||
number: season.seasonNumber,
|
||||
})}
|
||||
</td>
|
||||
<td className="px-5 py-4 text-sm leading-5 text-gray-200 md:px-6 whitespace-nowrap">
|
||||
<td className="whitespace-nowrap px-5 py-4 text-sm leading-5 text-gray-200 md:px-6">
|
||||
{season.episodeCount}
|
||||
</td>
|
||||
<td className="py-4 pr-2 text-sm leading-5 text-gray-200 md:px-6 whitespace-nowrap">
|
||||
<td className="whitespace-nowrap py-4 pr-2 text-sm leading-5 text-gray-200 md:px-6">
|
||||
{!seasonRequest && !mediaSeason && (
|
||||
<Badge>
|
||||
{intl.formatMessage(
|
||||
@@ -667,28 +698,26 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
|
||||
</div>
|
||||
{(hasPermission(Permission.REQUEST_ADVANCED) ||
|
||||
hasPermission(Permission.MANAGE_REQUESTS)) && (
|
||||
<div className="mt-4">
|
||||
<AdvancedRequester
|
||||
type="tv"
|
||||
is4k={is4k}
|
||||
isAnime={data?.keywords.some(
|
||||
(keyword) => keyword.id === ANIME_KEYWORD_ID
|
||||
)}
|
||||
onChange={(overrides) => setRequestOverrides(overrides)}
|
||||
requestUser={editRequest?.requestedBy}
|
||||
defaultOverrides={
|
||||
editRequest
|
||||
? {
|
||||
folder: editRequest.rootFolder,
|
||||
profile: editRequest.profileId,
|
||||
server: editRequest.serverId,
|
||||
language: editRequest.languageProfileId,
|
||||
tags: editRequest.tags,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<AdvancedRequester
|
||||
type="tv"
|
||||
is4k={is4k}
|
||||
isAnime={data?.keywords.some(
|
||||
(keyword) => keyword.id === ANIME_KEYWORD_ID
|
||||
)}
|
||||
onChange={(overrides) => setRequestOverrides(overrides)}
|
||||
requestUser={editRequest?.requestedBy}
|
||||
defaultOverrides={
|
||||
editRequest
|
||||
? {
|
||||
folder: editRequest.rootFolder,
|
||||
profile: editRequest.profileId,
|
||||
server: editRequest.serverId,
|
||||
language: editRequest.languageProfileId,
|
||||
tags: editRequest.tags,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import MovieRequestModal from './MovieRequestModal';
|
||||
import type { MediaStatus } from '../../../server/constants/media';
|
||||
import TvRequestModal from './TvRequestModal';
|
||||
import Transition from '../Transition';
|
||||
import { MediaRequest } from '../../../server/entity/MediaRequest';
|
||||
import Transition from '../Transition';
|
||||
import MovieRequestModal from './MovieRequestModal';
|
||||
import TvRequestModal from './TvRequestModal';
|
||||
|
||||
interface RequestModalProps {
|
||||
show: boolean;
|
||||
@@ -26,29 +26,6 @@ const RequestModal: React.FC<RequestModalProps> = ({
|
||||
onUpdating,
|
||||
onCancel,
|
||||
}) => {
|
||||
if (type === 'tv') {
|
||||
return (
|
||||
<Transition
|
||||
enter="transition opacity-0 duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition opacity-100 duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
show={show}
|
||||
>
|
||||
<TvRequestModal
|
||||
onComplete={onComplete}
|
||||
onCancel={onCancel}
|
||||
tmdbId={tmdbId}
|
||||
onUpdating={onUpdating}
|
||||
is4k={is4k}
|
||||
editRequest={editRequest}
|
||||
/>
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Transition
|
||||
enter="transition opacity-0 duration-300"
|
||||
@@ -59,14 +36,25 @@ const RequestModal: React.FC<RequestModalProps> = ({
|
||||
leaveTo="opacity-0"
|
||||
show={show}
|
||||
>
|
||||
<MovieRequestModal
|
||||
onComplete={onComplete}
|
||||
onCancel={onCancel}
|
||||
tmdbId={tmdbId}
|
||||
onUpdating={onUpdating}
|
||||
is4k={is4k}
|
||||
editRequest={editRequest}
|
||||
/>
|
||||
{type === 'movie' ? (
|
||||
<MovieRequestModal
|
||||
onComplete={onComplete}
|
||||
onCancel={onCancel}
|
||||
tmdbId={tmdbId}
|
||||
onUpdating={onUpdating}
|
||||
is4k={is4k}
|
||||
editRequest={editRequest}
|
||||
/>
|
||||
) : (
|
||||
<TvRequestModal
|
||||
onComplete={onComplete}
|
||||
onCancel={onCancel}
|
||||
tmdbId={tmdbId}
|
||||
onUpdating={onUpdating}
|
||||
is4k={is4k}
|
||||
editRequest={editRequest}
|
||||
/>
|
||||
)}
|
||||
</Transition>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ const messages = defineMessages({
|
||||
agentenabled: 'Enable Agent',
|
||||
accessToken: 'Application API Token',
|
||||
accessTokenTip:
|
||||
'<ApplicationRegistrationLink>Register an application</ApplicationRegistrationLink> for use with Overseerr',
|
||||
'<ApplicationRegistrationLink>Register an application</ApplicationRegistrationLink> for use with Jellyseerr',
|
||||
userToken: 'User or Group Key',
|
||||
userTokenTip:
|
||||
'Your 30-character <UsersGroupsLink>user or group identifier</UsersGroupsLink>',
|
||||
|
||||
@@ -19,7 +19,7 @@ const messages = defineMessages({
|
||||
'Allow users to also start a chat with your bot and configure their own notifications',
|
||||
botAPI: 'Bot Authorization Token',
|
||||
botApiTip:
|
||||
'<CreateBotLink>Create a bot</CreateBotLink> for use with Overseerr',
|
||||
'<CreateBotLink>Create a bot</CreateBotLink> for use with Jellyseerr',
|
||||
chatId: 'Chat ID',
|
||||
chatIdTip:
|
||||
'Start a chat with your bot, add <GetIdBotLink>@get_id_bot</GetIdBotLink>, and issue the <code>/my_id</code> command',
|
||||
|
||||
@@ -18,7 +18,7 @@ const messages = defineMessages({
|
||||
toastWebPushTestSuccess: 'Web push test notification sent!',
|
||||
toastWebPushTestFailed: 'Web push test notification failed to send.',
|
||||
httpsRequirement:
|
||||
'In order to receive web push notifications, Overseerr must be served over HTTPS.',
|
||||
'In order to receive web push notifications, Jellyseerr must be served over HTTPS.',
|
||||
});
|
||||
|
||||
const NotificationsWebPush: React.FC = () => {
|
||||
|
||||
@@ -20,11 +20,11 @@ const messages = defineMessages({
|
||||
currentversion: 'Current Version',
|
||||
viewchangelog: 'View Changelog',
|
||||
runningDevelopMessage:
|
||||
'The latest changes to the <code>develop</code> branch of Overseerr are not shown below. Please see the commit history for this branch on <GithubLink>GitHub</GithubLink> for details.',
|
||||
'The latest changes to the <code>develop</code> branch of Jellyseerr are not shown below. Please see the commit history for this branch on <GithubLink>GitHub</GithubLink> for details.',
|
||||
});
|
||||
|
||||
const REPO_RELEASE_API =
|
||||
'https://api.github.com/repos/sct/overseerr/releases?per_page=20';
|
||||
'https://api.github.com/repos/Fallenbagel/jellyseerr/releases?per_page=20';
|
||||
|
||||
interface GitHubRelease {
|
||||
url: string;
|
||||
@@ -153,7 +153,7 @@ const Releases: React.FC<ReleasesProps> = ({ currentVersion }) => {
|
||||
GithubLink: function GithubLink(msg) {
|
||||
return (
|
||||
<a
|
||||
href="https://github.com/sct/overseerr"
|
||||
href="https://github.com/Fallenbagel/jellyseerr"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-yellow-100 underline transition duration-300 hover:text-white"
|
||||
|
||||
@@ -16,14 +16,14 @@ import Releases from './Releases';
|
||||
|
||||
const messages = defineMessages({
|
||||
about: 'About',
|
||||
overseerrinformation: 'Overseerr Information',
|
||||
overseerrinformation: 'Jellyseerr Information',
|
||||
version: 'Version',
|
||||
totalmedia: 'Total Media',
|
||||
totalrequests: 'Total Requests',
|
||||
gettingsupport: 'Getting Support',
|
||||
githubdiscussions: 'GitHub Discussions',
|
||||
timezone: 'Time Zone',
|
||||
supportoverseerr: 'Support Overseerr',
|
||||
supportoverseerr: 'Support Jellyseerr',
|
||||
helppaycoffee: 'Help Pay for Coffee',
|
||||
documentation: 'Documentation',
|
||||
preferredmethod: 'Preferred',
|
||||
@@ -68,7 +68,7 @@ const SettingsAbout: React.FC = () => {
|
||||
</p>
|
||||
<p className="mt-3 text-sm leading-5 md:mt-0 md:ml-6">
|
||||
<a
|
||||
href="http://github.com/sct/overseerr"
|
||||
href="https://github.com/Fallenbagel/jellyseerr"
|
||||
className="font-medium text-indigo-100 transition duration-150 ease-in-out whitespace-nowrap hover:text-white"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
@@ -115,32 +115,32 @@ const SettingsAbout: React.FC = () => {
|
||||
<List title={intl.formatMessage(messages.gettingsupport)}>
|
||||
<List.Item title={intl.formatMessage(messages.documentation)}>
|
||||
<a
|
||||
href="https://docs.overseerr.dev"
|
||||
href="https://github.com/Fallenbagel/jellyseerr#readme"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-indigo-500 hover:underline"
|
||||
>
|
||||
https://docs.overseerr.dev
|
||||
https://github.com/Fallenbagel/jellyseerr#readme
|
||||
</a>
|
||||
</List.Item>
|
||||
<List.Item title={intl.formatMessage(messages.githubdiscussions)}>
|
||||
<a
|
||||
href="https://github.com/sct/overseerr/discussions"
|
||||
href="https://github.com/Fallenbagel/jellyseerr/discussions"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-indigo-500 hover:underline"
|
||||
>
|
||||
https://github.com/sct/overseerr/discussions
|
||||
https://github.com/Fallenbagel/jellyseerr/discussions
|
||||
</a>
|
||||
</List.Item>
|
||||
<List.Item title="Discord">
|
||||
<a
|
||||
href="https://discord.gg/overseerr"
|
||||
href="https://discord.gg/XDyAd3AuUV"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-indigo-500 hover:underline"
|
||||
>
|
||||
https://discord.gg/overseerr
|
||||
https://discord.gg/XDyAd3AuUV
|
||||
</a>
|
||||
</List.Item>
|
||||
</List>
|
||||
@@ -151,27 +151,17 @@ const SettingsAbout: React.FC = () => {
|
||||
title={`${intl.formatMessage(messages.helppaycoffee)} ☕️`}
|
||||
>
|
||||
<a
|
||||
href="https://github.com/sponsors/sct"
|
||||
href="https://www.buymeacoffee.com/fallen.bagel"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-indigo-500 hover:underline"
|
||||
>
|
||||
https://github.com/sponsors/sct
|
||||
https://www.buymeacoffee.com/fallen.bagel
|
||||
</a>
|
||||
<Badge className="ml-2">
|
||||
{intl.formatMessage(messages.preferredmethod)}
|
||||
</Badge>
|
||||
</List.Item>
|
||||
<List.Item title="">
|
||||
<a
|
||||
href="https://patreon.com/overseerr"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-indigo-500 hover:underline"
|
||||
>
|
||||
https://patreon.com/overseerr
|
||||
</a>
|
||||
</List.Item>
|
||||
</List>
|
||||
</div>
|
||||
<div className="section">
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import React, { useState } from 'react';
|
||||
import LoadingSpinner from '../Common/LoadingSpinner';
|
||||
import type { JellyfinSettings } from '../../../server/lib/settings';
|
||||
import useSWR from 'swr';
|
||||
import Button from '../Common/Button';
|
||||
import axios from 'axios';
|
||||
import LibraryItem from './LibraryItem';
|
||||
import Badge from '../Common/Badge';
|
||||
import React, { useState } from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
import useSWR from 'swr';
|
||||
import type { JellyfinSettings } from '../../../server/lib/settings';
|
||||
import Badge from '../Common/Badge';
|
||||
import Button from '../Common/Button';
|
||||
import LoadingSpinner from '../Common/LoadingSpinner';
|
||||
import LibraryItem from './LibraryItem';
|
||||
|
||||
const messages = defineMessages({
|
||||
jellyfinsettings: 'Jellyfin Settings',
|
||||
jellyfinsettingsDescription:
|
||||
'Configure the settings for your Jellyfin server. Overseerr scans your Jellyfin libraries to see what content is available.',
|
||||
'Configure the settings for your Jellyfin server. Jellyseerr scans your Jellyfin libraries to see what content is available.',
|
||||
timeout: 'Timeout',
|
||||
save: 'Save Changes',
|
||||
saving: 'Saving…',
|
||||
jellyfinlibraries: 'Jellyfin Libraries',
|
||||
jellyfinlibrariesDescription:
|
||||
'The libraries Overseerr scans for titles. Click the button below if no libraries are listed.',
|
||||
'The libraries Jellyseerr scans for titles. Click the button below if no libraries are listed.',
|
||||
syncing: 'Syncing',
|
||||
syncJellyfin: 'Sync Libraries',
|
||||
manualscanJellyfin: 'Manual Library Scan',
|
||||
manualscanDescriptionJellyfin:
|
||||
"Normally, this will only be run once every 24 hours. Overseerr will check your Jellyfin server's recently added more aggressively. If this is your first time configuring Jellyfin, a one-time full manual library scan is recommended!",
|
||||
"Normally, this will only be run once every 24 hours. Jellyseerr will check your Jellyfin server's recently added more aggressively. If this is your first time configuring Jellyfin, a one-time full manual library scan is recommended!",
|
||||
notrunning: 'Not Running',
|
||||
currentlibrary: 'Current Library: {name}',
|
||||
librariesRemaining: 'Libraries Remaining: {count}',
|
||||
|
||||
@@ -23,7 +23,7 @@ const messages: { [messageName: string]: MessageDescriptor } = defineMessages({
|
||||
jobsandcache: 'Jobs & Cache',
|
||||
jobs: 'Jobs',
|
||||
jobsDescription:
|
||||
'Overseerr performs certain maintenance tasks as regularly-scheduled jobs, but they can also be manually triggered below. Manually running a job will not alter its schedule.',
|
||||
'Jellyseerr performs certain maintenance tasks as regularly-scheduled jobs, but they can also be manually triggered below. Manually running a job will not alter its schedule.',
|
||||
jobname: 'Job Name',
|
||||
jobtype: 'Type',
|
||||
nextexecution: 'Next Execution',
|
||||
@@ -35,7 +35,7 @@ const messages: { [messageName: string]: MessageDescriptor } = defineMessages({
|
||||
command: 'Command',
|
||||
cache: 'Cache',
|
||||
cacheDescription:
|
||||
'Overseerr caches requests to external API endpoints to optimize performance and avoid making unnecessary API calls.',
|
||||
'Jellyseerr caches requests to external API endpoints to optimize performance and avoid making unnecessary API calls.',
|
||||
cacheflushed: '{cachename} cache flushed.',
|
||||
cachename: 'Cache Name',
|
||||
cachehits: 'Hits',
|
||||
@@ -47,6 +47,8 @@ const messages: { [messageName: string]: MessageDescriptor } = defineMessages({
|
||||
unknownJob: 'Unknown Job',
|
||||
'plex-recently-added-scan': 'Plex Recently Added Scan',
|
||||
'plex-full-scan': 'Plex Full Library Scan',
|
||||
'jellyfin-recently-added-sync': 'Jellyfin Recently Added Scan',
|
||||
'jellyfin-full-sync': 'Jellyfin Full Library Scan',
|
||||
'radarr-scan': 'Radarr Scan',
|
||||
'sonarr-scan': 'Sonarr Scan',
|
||||
'download-sync': 'Download Sync',
|
||||
|
||||
@@ -31,7 +31,7 @@ import Transition from '../../Transition';
|
||||
const messages = defineMessages({
|
||||
logs: 'Logs',
|
||||
logsDescription:
|
||||
'You can also view these logs directly via <code>stdout</code>, or in <code>{configDir}/logs/overseerr.log</code>.',
|
||||
'You can also view these logs directly via <code>stdout</code>, or in <code>{configDir}/logs/jellyseerr.log</code>.',
|
||||
time: 'Timestamp',
|
||||
level: 'Severity',
|
||||
label: 'Label',
|
||||
|
||||