mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2025-12-24 02:39:18 -05:00
Compare commits
74 Commits
preview-pr
...
v1.1.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8feb20ff52 | ||
|
|
f2c659c6f3 | ||
|
|
99f1a4e4f3 | ||
|
|
fea9457dad | ||
|
|
23c9595933 | ||
|
|
eceedbbaad | ||
|
|
29f06a965c | ||
|
|
9ec05d3ba4 | ||
|
|
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 | ||
|
|
f97ee11430 | ||
|
|
54868fd486 | ||
|
|
eea389879f | ||
|
|
5c917f95b4 | ||
|
|
dd4d42fd31 | ||
|
|
e5c6b9cd74 | ||
|
|
508fccae4e | ||
|
|
f77573c838 | ||
|
|
7dfe38001e | ||
|
|
48f55da43e | ||
|
|
1e97503802 | ||
|
|
42ff34bb3d | ||
|
|
107b766c44 | ||
|
|
fb51ce5570 | ||
|
|
3357343d98 | ||
|
|
9d61092f37 | ||
|
|
29274614c3 | ||
|
|
19b51592ea | ||
|
|
757c0fc29e | ||
|
|
3eb48abc14 | ||
|
|
01cd9d3872 | ||
|
|
9582196e1f | ||
|
|
3743edab8d | ||
|
|
d81e7cdbab | ||
|
|
6e1d7f7075 | ||
|
|
91cf2de33a | ||
|
|
a6ec2d5220 |
@@ -665,78 +665,6 @@
|
||||
"contributions": [
|
||||
"translation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "sambartik",
|
||||
"name": "Samuel Bartík",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/63553146?v=4",
|
||||
"profile": "https://github.com/sambartik",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "frank-cywong",
|
||||
"name": "Chun Yeung Wong",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/90653148?v=4",
|
||||
"profile": "https://github.com/frank-cywong",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "TheMeanCanEHdian",
|
||||
"name": "TheMeanCanEHdian",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/16025103?v=4",
|
||||
"profile": "https://github.com/TheMeanCanEHdian",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Gylesie",
|
||||
"name": "Gylesie",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/86306812?v=4",
|
||||
"profile": "https://github.com/Gylesie",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Fhd-pro",
|
||||
"name": "Fhd-pro",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/82862079?v=4",
|
||||
"profile": "https://github.com/Fhd-pro",
|
||||
"contributions": [
|
||||
"translation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "PovilasID",
|
||||
"name": "PovilasID",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/396243?v=4",
|
||||
"profile": "https://github.com/PovilasID",
|
||||
"contributions": [
|
||||
"translation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "byakurau",
|
||||
"name": "byakurau",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1811683?v=4",
|
||||
"profile": "https://github.com/byakurau",
|
||||
"contributions": [
|
||||
"translation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "miknii",
|
||||
"name": "miknii",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/109232569?v=4",
|
||||
"profile": "https://github.com/miknii",
|
||||
"contributions": [
|
||||
"translation"
|
||||
]
|
||||
}
|
||||
],
|
||||
"badgeTemplate": "<a href=\"#contributors-\"><img alt=\"All Contributors\" src=\"https://img.shields.io/badge/all_contributors-<%= contributors.length %>-orange.svg\"/></a>",
|
||||
@@ -745,5 +673,5 @@
|
||||
"projectOwner": "sct",
|
||||
"repoType": "github",
|
||||
"repoHost": "https://github.com",
|
||||
"skipCi": false
|
||||
"skipCi": true
|
||||
}
|
||||
|
||||
@@ -26,4 +26,3 @@ public/os_logo_filled.png
|
||||
public/preview.jpg
|
||||
snap
|
||||
stylelint.config.js
|
||||
cypress
|
||||
|
||||
15
.eslintrc.js
15
.eslintrc.js
@@ -7,7 +7,6 @@ module.exports = {
|
||||
'plugin:jsx-a11y/recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
'plugin:react/jsx-runtime',
|
||||
'prettier',
|
||||
],
|
||||
parserOptions: {
|
||||
@@ -27,21 +26,11 @@ module.exports = {
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
'react-hooks/exhaustive-deps': 'warn',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'prettier/prettier': ['error', { endOfLine: 'auto' }],
|
||||
'formatjs/no-offset': 'error',
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': ['error'],
|
||||
'@typescript-eslint/array-type': ['error', { default: 'array' }],
|
||||
'jsx-a11y/no-onchange': 'off',
|
||||
'@typescript-eslint/consistent-type-imports': [
|
||||
'error',
|
||||
{
|
||||
prefer: 'type-imports',
|
||||
},
|
||||
],
|
||||
'no-relative-import-paths/no-relative-import-paths': [
|
||||
'error',
|
||||
{ allowSameFolder: true },
|
||||
],
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
@@ -51,7 +40,7 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
],
|
||||
plugins: ['jsx-a11y', 'react-hooks', 'formatjs', 'no-relative-import-paths'],
|
||||
plugins: ['jsx-a11y', 'prettier', 'react-hooks', 'formatjs'],
|
||||
settings: {
|
||||
react: {
|
||||
pragma: 'React',
|
||||
|
||||
7
.github/CODEOWNERS
vendored
Normal file
7
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
# Global code ownership
|
||||
|
||||
- @Fallenbagel
|
||||
|
||||
# i18n locale files
|
||||
|
||||
src/i18n/locale/ @Fallenbagel
|
||||
26
.github/workflows/ci.yml
vendored
26
.github/workflows/ci.yml
vendored
@@ -3,7 +3,7 @@ name: Jellyseerr CI
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
- "*"
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
@@ -13,18 +13,16 @@ jobs:
|
||||
name: Lint & Test Build
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-20.04
|
||||
container: node:16.17-alpine
|
||||
container: node:16.14-alpine
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Install dependencies
|
||||
env:
|
||||
HUSKY: 0
|
||||
HUSKY_SKIP_INSTALL: 1
|
||||
run: yarn
|
||||
- name: Lint
|
||||
run: yarn lint
|
||||
- name: Formatting
|
||||
run: yarn format:check
|
||||
- name: Build
|
||||
run: yarn build
|
||||
|
||||
@@ -36,29 +34,23 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v3
|
||||
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@v2
|
||||
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@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v3
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
@@ -85,7 +77,7 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Get Build Job Status
|
||||
uses: technote-space/workflow-conclusion-action@v3
|
||||
uses: technote-space/workflow-conclusion-action@v2
|
||||
- name: Combine Job Status
|
||||
id: status
|
||||
run: |
|
||||
|
||||
30
.github/workflows/cypress.yml
vendored
30
.github/workflows/cypress.yml
vendored
@@ -1,30 +0,0 @@
|
||||
name: Cypress Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
|
||||
jobs:
|
||||
cypress-run:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Cypress run
|
||||
uses: cypress-io/github-action@v4
|
||||
with:
|
||||
build: yarn cypress:build
|
||||
start: yarn start
|
||||
wait-on: 'http://localhost:5055'
|
||||
record: true
|
||||
env:
|
||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
WITH_MIGRATIONS: true
|
||||
# Fix test titles in cypress dashboard
|
||||
COMMIT_INFO_MESSAGE: ${{github.event.pull_request.title}}
|
||||
COMMIT_INFO_SHA: ${{github.event.pull_request.head.sha}}
|
||||
10
.github/workflows/preview.yml
vendored
10
.github/workflows/preview.yml
vendored
@@ -3,7 +3,7 @@ name: Jellyseerr Preview
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'preview-*'
|
||||
- "preview-*"
|
||||
|
||||
jobs:
|
||||
build_and_push:
|
||||
@@ -16,16 +16,16 @@ jobs:
|
||||
id: get_version
|
||||
run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/}
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v3
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
|
||||
39
.github/workflows/private_registery_push.yml
vendored
Normal file
39
.github/workflows/private_registery_push.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
name: 'create docker image on pull request and push to private registery'
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-image:
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
-
|
||||
name: Login to private registery
|
||||
uses: docker/login-action@v2.0.0
|
||||
with:
|
||||
registry: ${{ secrets.REGISTRY_URL }}
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: ./
|
||||
file: ./Dockerfile
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
push: true
|
||||
tags: '${{ secrets.REGISTRY_URL }}/fallenbagel/jellyseerr:${{ github.sha }}'
|
||||
cache-from: 'type=registry,ref=${{ secrets.REGISTRY_URL }}/fallenbagel/jellyseerr:buildcache'
|
||||
cache-to: 'type=registry,ref=${{ secrets.REGISTRY_URL }}/fallenbagel/jellyseerr:buildcache,mode=max'
|
||||
66
.github/workflows/release.yml
vendored
66
.github/workflows/release.yml
vendored
@@ -5,7 +5,7 @@ on: workflow_dispatch
|
||||
jobs:
|
||||
semantic-release:
|
||||
name: Tag and release latest version
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: self-hosted
|
||||
env:
|
||||
HUSKY: 0
|
||||
steps:
|
||||
@@ -18,14 +18,16 @@ jobs:
|
||||
with:
|
||||
node-version: 16
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
- name: Install Yarn
|
||||
run: npm install -g yarn
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
- name: Release
|
||||
@@ -35,60 +37,6 @@ jobs:
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
run: npx semantic-release
|
||||
|
||||
build-snap:
|
||||
name: Build Snap Package (${{ matrix.architecture }})
|
||||
needs: semantic-release
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
architecture:
|
||||
- amd64
|
||||
- arm64
|
||||
- armhf
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Switch to master branch
|
||||
run: git checkout master
|
||||
- name: Pull latest changes
|
||||
run: git pull
|
||||
- name: Prepare
|
||||
id: prepare
|
||||
run: |
|
||||
git fetch --prune --tags
|
||||
if [[ $GITHUB_REF == refs/tags/* || $GITHUB_REF == refs/heads/master ]]; then
|
||||
echo ::set-output name=RELEASE::stable
|
||||
else
|
||||
echo ::set-output name=RELEASE::edge
|
||||
fi
|
||||
- name: Set Up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
with:
|
||||
image: tonistiigi/binfmt@sha256:df15403e06a03c2f461c1f7938b171fda34a5849eb63a70e2a2109ed5a778bde
|
||||
- name: Build Snap Package
|
||||
uses: diddlesnaps/snapcraft-multiarch-action@v1
|
||||
id: build
|
||||
with:
|
||||
architecture: ${{ matrix.architecture }}
|
||||
- name: Upload Snap Package
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: overseerr-snap-package-${{ matrix.architecture }}
|
||||
path: ${{ steps.build.outputs.snap }}
|
||||
- name: Review Snap Package
|
||||
uses: diddlesnaps/snapcraft-review-tools-action@v1
|
||||
with:
|
||||
snap: ${{ steps.build.outputs.snap }}
|
||||
- name: Publish Snap Package
|
||||
uses: snapcore/action-publish@v1
|
||||
with:
|
||||
store_login: ${{ secrets.SNAP_LOGIN }}
|
||||
snap: ${{ steps.build.outputs.snap }}
|
||||
release: ${{ steps.prepare.outputs.RELEASE }}
|
||||
|
||||
discord:
|
||||
name: Send Discord Notification
|
||||
needs: semantic-release
|
||||
@@ -96,7 +44,7 @@ jobs:
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: Get Build Job Status
|
||||
uses: technote-space/workflow-conclusion-action@v3
|
||||
uses: technote-space/workflow-conclusion-action@v2
|
||||
- name: Combine Job Status
|
||||
id: status
|
||||
run: |
|
||||
|
||||
88
.github/workflows/snap.yaml
vendored
88
.github/workflows/snap.yaml
vendored
@@ -1,88 +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.10.0
|
||||
with:
|
||||
access_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
build-snap:
|
||||
name: Build Snap Package (${{ matrix.architecture }})
|
||||
needs: jobs
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
architecture:
|
||||
- amd64
|
||||
- arm64
|
||||
- armhf
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
- name: Prepare
|
||||
id: prepare
|
||||
run: |
|
||||
git fetch --prune --unshallow --tags
|
||||
if [[ $GITHUB_REF == refs/tags/* || $GITHUB_REF == refs/heads/master ]]; then
|
||||
echo ::set-output name=RELEASE::stable
|
||||
else
|
||||
echo ::set-output name=RELEASE::edge
|
||||
fi
|
||||
- name: Set Up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Build Snap Package
|
||||
uses: diddlesnaps/snapcraft-multiarch-action@v1
|
||||
id: build
|
||||
with:
|
||||
architecture: ${{ matrix.architecture }}
|
||||
- name: Upload Snap Package
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: overseerr-snap-package-${{ matrix.architecture }}
|
||||
path: ${{ steps.build.outputs.snap }}
|
||||
- name: Review Snap Package
|
||||
uses: diddlesnaps/snapcraft-review-tools-action@v1
|
||||
with:
|
||||
snap: ${{ steps.build.outputs.snap }}
|
||||
- name: Publish Snap Package
|
||||
uses: snapcore/action-publish@v1
|
||||
with:
|
||||
store_login: ${{ secrets.SNAP_LOGIN }}
|
||||
snap: ${{ steps.build.outputs.snap }}
|
||||
release: ${{ steps.prepare.outputs.RELEASE }}
|
||||
|
||||
discord:
|
||||
name: Send Discord Notification
|
||||
needs: build-snap
|
||||
if: always() && !contains(github.event.head_commit.message, '[skip ci]')
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Get Build Job Status
|
||||
uses: technote-space/workflow-conclusion-action@v3
|
||||
- name: Combine Job Status
|
||||
id: status
|
||||
run: |
|
||||
failures=(neutral, skipped, timed_out, action_required)
|
||||
if [[ ${array[@]} =~ $WORKFLOW_CONCLUSION ]]; then
|
||||
echo ::set-output name=status::failure
|
||||
else
|
||||
echo ::set-output name=status::$WORKFLOW_CONCLUSION
|
||||
fi
|
||||
- name: Post Status to Discord
|
||||
uses: sarisia/actions-status-discord@v1
|
||||
with:
|
||||
webhook: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
status: ${{ steps.status.outputs.status }}
|
||||
title: ${{ github.workflow }}
|
||||
nofail: true
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -54,16 +54,5 @@ config/db/db.sqlite3-journal
|
||||
# VS Code
|
||||
.vscode/launch.json
|
||||
|
||||
# Cypress
|
||||
cypress.env.json
|
||||
cypress/videos
|
||||
cypress/screenshots
|
||||
|
||||
# ESLint
|
||||
.eslintcache
|
||||
|
||||
# TS Build Info
|
||||
tsconfig.tsbuildinfo
|
||||
|
||||
# Webstorm
|
||||
.idea
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: [require('./merged-prettier-plugin.js')],
|
||||
singleQuote: true,
|
||||
trailingComma: 'es5',
|
||||
};
|
||||
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
@@ -11,6 +11,9 @@
|
||||
// https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode
|
||||
"esbenp.prettier-vscode",
|
||||
|
||||
// https://marketplace.visualstudio.com/items?itemName=eg2.vscode-npm-script
|
||||
"eg2.vscode-npm-script",
|
||||
|
||||
// https://marketplace.visualstudio.com/items?itemName=Orta.vscode-jest
|
||||
"Orta.vscode-jest",
|
||||
|
||||
|
||||
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
@@ -15,6 +15,8 @@
|
||||
"database": "./config/db/db.sqlite3"
|
||||
}
|
||||
],
|
||||
"editor.formatOnSave": true,
|
||||
"typescript.preferences.importModuleSpecifier": "non-relative"
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": true
|
||||
},
|
||||
"editor.formatOnSave": true
|
||||
}
|
||||
|
||||
205
CHANGELOG.md
205
CHANGELOG.md
@@ -1,3 +1,208 @@
|
||||
# [1.1.1](https://github.com/fallenbagel/jellyseerr/compare/v1.1.0...v1.1.1) (2022-06-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* conditional media server name for 4k url to add emby to tvdetails ([ddd773c](https://github.com/fallenbagel/jellyseerr/commit/ddd773c03ff61654490644dec21f406d03374b3d))
|
||||
* don't show 0 playcount in slideover ([dec4062](https://github.com/fallenbagel/jellyseerr/commit/dec4062cdcecbe297f72364ede6a000b863117f4))
|
||||
* fix mediaServerType not set for Plex which leads to Plex users seeing Jellyfin settings ([94ade93](https://github.com/fallenbagel/jellyseerr/commit/94ade93e16f02b372dafd2765bea475117431975))
|
||||
* fixes jellyfin forgot password and adds emby support to the forgot password link ([0259975](https://github.com/fallenbagel/jellyseerr/commit/02599754026e6a66662f753bb6b6117dfabb5f9a)), closes [#99](https://github.com/fallenbagel/jellyseerr/issues/99)
|
||||
* hide plex guid cache settings from ui when running in jellyfin/emby mode ([7450138](https://github.com/fallenbagel/jellyseerr/commit/7450138ac12640797952c1a2d5e1e111d17a11e1))
|
||||
* **import all:** fis for import all ([29478fc](https://github.com/fallenbagel/jellyseerr/commit/29478fc19534589db37499f1cdcc21ea4d389a74))
|
||||
* **jellyfin:** ignore additional items with virtual location type ([c811548](https://github.com/fallenbagel/jellyseerr/commit/c81154800fd7dc48fe890f4dd57ff33cbab973bb))
|
||||
* **jellyfinimportmodal:** fix for importing all jellyfin users ([a483ca9](https://github.com/fallenbagel/jellyseerr/commit/a483ca9837e12e2385d0e2407e52d6c64ae435e2))
|
||||
* **jellyfin:** sync errors ([d1dbd6e](https://github.com/fallenbagel/jellyseerr/commit/d1dbd6e3b9b1134e06150fc5eb21f729f64c0955))
|
||||
* manual browser refresh would redirect to home on search page ([9ded45f](https://github.com/fallenbagel/jellyseerr/commit/9ded45fef80b4a7e0be237fbe0301629f862fff9))
|
||||
* manual browser refresh would redirect to home on search page ([#2692](https://github.com/fallenbagel/jellyseerr/issues/2692)) ([b287839](https://github.com/fallenbagel/jellyseerr/commit/b2878390b486e338151f26a2354711147012f88e)), closes [#2683](https://github.com/fallenbagel/jellyseerr/issues/2683)
|
||||
* only show mediaserver settings for current active mediaserver ([739f5f9](https://github.com/fallenbagel/jellyseerr/commit/739f5f9c9ade8a1680bcb374f6c9e919a9e1426c))
|
||||
* **recommendations:** fixed recommendations page causing infinite network requests to tmdb api ([4f972be](https://github.com/fallenbagel/jellyseerr/commit/4f972be8584e48f544268aef9d1d05769ba2e38e))
|
||||
* **recommendations:** only load more titles if there can be more than 40 ([#2749](https://github.com/fallenbagel/jellyseerr/issues/2749)) ([14519ef](https://github.com/fallenbagel/jellyseerr/commit/14519ef5559038b0d9d037a2bdc5d98e63c9db6f)), closes [#2710](https://github.com/fallenbagel/jellyseerr/issues/2710)
|
||||
* remove internal Overseerr sponsor link, this is remaining on the main github page instead ([4b7bdd3](https://github.com/fallenbagel/jellyseerr/commit/4b7bdd3d7d608fe0bf52f494766fd7c40bede859))
|
||||
* **scan:** ignore virtual seasons ([6574e18](https://github.com/fallenbagel/jellyseerr/commit/6574e18516201bc11b5f0c422bf6b432c722e067)), closes [#119](https://github.com/fallenbagel/jellyseerr/issues/119)
|
||||
* **search:** use correct param to filter movies by year ([b07f703](https://github.com/fallenbagel/jellyseerr/commit/b07f7032ad89ccb359f3a6a4f4508de6b59ec393))
|
||||
* **search:** use correct param to filter movies by year ([#2727](https://github.com/fallenbagel/jellyseerr/issues/2727)) ([1054b4e](https://github.com/fallenbagel/jellyseerr/commit/1054b4e2d7262d841fa83cde624f1138ad7bd23a))
|
||||
* **setup&login:** fix a description error in the manual scan in setup and add emby to login page ([8810c20](https://github.com/fallenbagel/jellyseerr/commit/8810c20fc18a55c2f6768ddc40830a8494946072))
|
||||
* **ui:** don't show 0 playcount in slideover ([#2714](https://github.com/fallenbagel/jellyseerr/issues/2714)) ([29be659](https://github.com/fallenbagel/jellyseerr/commit/29be6595125017700eccb34d33a0e852f23c97ba))
|
||||
* **ui:** fix Avatar being broken when setup using internal ip ([01e81a7](https://github.com/fallenbagel/jellyseerr/commit/01e81a73a3ae3c4692d0b9b68dc27fe1a54b1a1d)), closes [#110](https://github.com/fallenbagel/jellyseerr/issues/110)
|
||||
* **ui:** fix translation errors for all locales in the import plex user button ([0fb5803](https://github.com/fallenbagel/jellyseerr/commit/0fb5803eb9a7589141a63e13df9a8aa8ea4cebf2))
|
||||
* **ui:** fix ui elements not reflecting the env variable ([722dda5](https://github.com/fallenbagel/jellyseerr/commit/722dda585631be365a2fb400b62dbc201f2b80de))
|
||||
* **ui:** fixed translation issue where it showed as import {mediaServerName} user ([819190c](https://github.com/fallenbagel/jellyseerr/commit/819190ce98720d8d66a07c98a4f12e3c8cdcac94))
|
||||
* **ui:** rectangular avatars getting stretched ([#2782](https://github.com/fallenbagel/jellyseerr/issues/2782)) ([db05172](https://github.com/fallenbagel/jellyseerr/commit/db05172d8b924a591ece4fae72d076eb59ee5f82))
|
||||
* **ui:** replaced {mediaServerName} in the plex variable in NL locale ([d417fca](https://github.com/fallenbagel/jellyseerr/commit/d417fcafa1e38c6d56ed8360ae451e8b8ff82a8d))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add Paramount+ to network slider ([d22bc09](https://github.com/fallenbagel/jellyseerr/commit/d22bc09652e5d4e703fca6838d06e4908432fe06))
|
||||
* **api:** add issue counts endpoint ([af23a25](https://github.com/fallenbagel/jellyseerr/commit/af23a257d5795b5c3930cd3884a84a2e2eeeb1dc))
|
||||
* **api:** add issue counts endpoint ([#2713](https://github.com/fallenbagel/jellyseerr/issues/2713)) ([e4039d0](https://github.com/fallenbagel/jellyseerr/commit/e4039d09c0380d80f03c7a00b51a150f88c02cca))
|
||||
* conditional media server name ([2bfdf02](https://github.com/fallenbagel/jellyseerr/commit/2bfdf02c7942762bd9f5201459b1a9ad6003b9a6))
|
||||
* conditional media server name to add emby to tvdetails ([e75b71b](https://github.com/fallenbagel/jellyseerr/commit/e75b71b8168b4a661971b809c88f9910c4206545))
|
||||
* conditional media server name to add emby to tvdetails ([ff3e3ce](https://github.com/fallenbagel/jellyseerr/commit/ff3e3ce841f0676713242d0c8e3a977ef65530d8))
|
||||
* **discover:** add Paramount+ to network slider ([#2608](https://github.com/fallenbagel/jellyseerr/issues/2608)) ([1d00229](https://github.com/fallenbagel/jellyseerr/commit/1d00229a485bb2b376e9f63b52c70c7719f5f023))
|
||||
* email ([a8bc0c0](https://github.com/fallenbagel/jellyseerr/commit/a8bc0c068b305710a224fa56a3725cc7e0758eb7)), closes [#122](https://github.com/fallenbagel/jellyseerr/issues/122)
|
||||
* **email validation:** email requirement and validation + better importer ([d835336](https://github.com/fallenbagel/jellyseerr/commit/d835336d330abfef5b15bc9febcb748a8154c7df))
|
||||
* **manage slideover:** show more request override details ([#2772](https://github.com/fallenbagel/jellyseerr/issues/2772)) ([90095bb](https://github.com/fallenbagel/jellyseerr/commit/90095bb18548dfd663a78df1908c40dbf2f99faf))
|
||||
* **uesrprofile:** email requirement and validation ([543859e](https://github.com/fallenbagel/jellyseerr/commit/543859e6f3b3a8cd4c61499a74bda610d3217626))
|
||||
* **ui:** add emby as a mediaServerType to the import user button ([6a6bfe0](https://github.com/fallenbagel/jellyseerr/commit/6a6bfe0c6875a1d8ccb1a6fdc409f595202ef38e))
|
||||
* **ui:** add emby user badge to the user list and fix local user badge ([410b536](https://github.com/fallenbagel/jellyseerr/commit/410b536c9474806ab9f7f5f097cedfafde1fbf67))
|
||||
* **ui:** add emby user badge to the userProfile ([b9546e6](https://github.com/fallenbagel/jellyseerr/commit/b9546e6daa8583c60fac7961447a13715bbc7f6b))
|
||||
* **ui:** conditional media server name to add emby to issuedetails play on button ([377a4fd](https://github.com/fallenbagel/jellyseerr/commit/377a4fd85b7194afb48a8ba9bfa4ce4ccf996be8))
|
||||
* **ui:** conditional media server name to add emby to moviedetails ([14d2937](https://github.com/fallenbagel/jellyseerr/commit/14d293799bb37f45449c201ab03638af257623be))
|
||||
* **user email setting:** added field to save user email ([30c48f1](https://github.com/fallenbagel/jellyseerr/commit/30c48f16ca0a74e7551b533bd75bc43304f946b1)), closes [#122](https://github.com/fallenbagel/jellyseerr/issues/122)
|
||||
* **user settings:** added email field to user profiel settings ([b22f20b](https://github.com/fallenbagel/jellyseerr/commit/b22f20b6fa5f68398850ccbf9b6e1cc233b3c8f4)), closes [#122](https://github.com/fallenbagel/jellyseerr/issues/122)
|
||||
|
||||
# [1.1.0](https://github.com/fallenbagel/jellyseerr/compare/v1.0.2...v1.1.0) (2022-05-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add Discord ID setting to general user settings page ([#2406](https://github.com/fallenbagel/jellyseerr/issues/2406)) ([eff665e](https://github.com/fallenbagel/jellyseerr/commit/eff665ef4b688aac881408790304b77bd9a31ddb))
|
||||
* add missing route guards to issues pages ([#2235](https://github.com/fallenbagel/jellyseerr/issues/2235)) ([c79dc9f](https://github.com/fallenbagel/jellyseerr/commit/c79dc9f70f512dbec0e3460ee78dbc9feccfbbb1))
|
||||
* address unhandled promise rejections & bump node to v16.13 ([#2398](https://github.com/fallenbagel/jellyseerr/issues/2398)) ([8cba486](https://github.com/fallenbagel/jellyseerr/commit/8cba486249fed88232e93a688c8bfe0f6179c589))
|
||||
* allow basic HTTP auth in hostname validation ([#2307](https://github.com/fallenbagel/jellyseerr/issues/2307)) ([d48a7ba](https://github.com/fallenbagel/jellyseerr/commit/d48a7ba518f9c79d70e499037cb730eb3efe2c08))
|
||||
* **api:** return queried user's requests instead of own requests ([#2174](https://github.com/fallenbagel/jellyseerr/issues/2174)) ([0edb1f4](https://github.com/fallenbagel/jellyseerr/commit/0edb1f452b6ff4a49ae2bde15f7273769788cf4f))
|
||||
* **api:** use query builder for user requests endpoint ([#2119](https://github.com/fallenbagel/jellyseerr/issues/2119)) ([a20f395](https://github.com/fallenbagel/jellyseerr/commit/a20f395c94c97dd7ddbc25590f15def2c9bf13c9))
|
||||
* apply request overrides iff override & selected servers match ([#2164](https://github.com/fallenbagel/jellyseerr/issues/2164)) ([50ce198](https://github.com/fallenbagel/jellyseerr/commit/50ce198471b1a3777a183d68904bbfb39ebd4523))
|
||||
* **auth:** resolve local/password authentication issues ([#2677](https://github.com/fallenbagel/jellyseerr/issues/2677)) ([b75fc7b](https://github.com/fallenbagel/jellyseerr/commit/b75fc7b2384ce760432620faaa92277dcd42b8e1))
|
||||
* **css:** rename form-input to form-input-area ([#2613](https://github.com/fallenbagel/jellyseerr/issues/2613)) ([086f0b6](https://github.com/fallenbagel/jellyseerr/commit/086f0b6ce23f607d20c2cec3c73b2e4d1ce9b426))
|
||||
* disable user-import from mediaserver for non-plex mediaservers until implemented ([4db8e54](https://github.com/fallenbagel/jellyseerr/commit/4db8e5464d6ce3450e2687a0cbee126961d847d2))
|
||||
* **docker:** explicitly install python3 ([#2273](https://github.com/fallenbagel/jellyseerr/issues/2273)) [skip ci] ([f1cd087](https://github.com/fallenbagel/jellyseerr/commit/f1cd0878a5c74bddc864f5f8ce9e2f041bdde5ec))
|
||||
* don't allow login for unimported Jellyfin users if not set in settings ([72ca694](https://github.com/fallenbagel/jellyseerr/commit/72ca694f212ab616ca7b7fe02e428ff61f79c67c))
|
||||
* **email:** do not attempt to display logo if app URL not configured ([#2125](https://github.com/fallenbagel/jellyseerr/issues/2125)) ([b3b421a](https://github.com/fallenbagel/jellyseerr/commit/b3b421a67408a4a48d23c15341fcdf7aaf19b25a))
|
||||
* **email:** enclose PGP encryption logic in try/catch ([#2519](https://github.com/fallenbagel/jellyseerr/issues/2519)) ([a76b608](https://github.com/fallenbagel/jellyseerr/commit/a76b608ab796944c0c660e3296a7aca6615d69f3))
|
||||
* **email:** use decrypted private key ([#2232](https://github.com/fallenbagel/jellyseerr/issues/2232)) ([8d29685](https://github.com/fallenbagel/jellyseerr/commit/8d2968572a569ed77a4d7c14ae1dc69935fa847e))
|
||||
* fix usertype from local user to mediaServerType ([25bee8b](https://github.com/fallenbagel/jellyseerr/commit/25bee8b9f70d7948191ba9cf07d16427da81d425))
|
||||
* **frontend:** disable autocomplete on search field ([#2592](https://github.com/fallenbagel/jellyseerr/issues/2592)) ([82d1617](https://github.com/fallenbagel/jellyseerr/commit/82d16177bf763fe8097b4aae326793e3e21e847d))
|
||||
* **frontend:** more issues-related fixes ([#2234](https://github.com/fallenbagel/jellyseerr/issues/2234)) ([3ec4a9c](https://github.com/fallenbagel/jellyseerr/commit/3ec4a9c76e1f31bee5c8801b389721bf8e5884e0))
|
||||
* **frontend:** notification type validation ([#2207](https://github.com/fallenbagel/jellyseerr/issues/2207)) ([2f204b9](https://github.com/fallenbagel/jellyseerr/commit/2f204b995269a53ae36f2a8733f27ae6ab70da5a))
|
||||
* **frontend:** setup page backdrops ([#2251](https://github.com/fallenbagel/jellyseerr/issues/2251)) ([78a8091](https://github.com/fallenbagel/jellyseerr/commit/78a8091bcd29a7cf50cc7c493c28710389817adf))
|
||||
* **frontend:** theme-color meta tag ([#2420](https://github.com/fallenbagel/jellyseerr/issues/2420)) ([ff28c9b](https://github.com/fallenbagel/jellyseerr/commit/ff28c9bfebf4a930e2542ee3b3c35f8af4e1b97e))
|
||||
* **frontend:** use consistent formatting & strings ([#2231](https://github.com/fallenbagel/jellyseerr/issues/2231)) ([2164471](https://github.com/fallenbagel/jellyseerr/commit/216447121b686b6d01a31b95ec0c8eb005f6b103))
|
||||
* **frontend:** various fixes ([#2524](https://github.com/fallenbagel/jellyseerr/issues/2524)) ([c3dbd0d](https://github.com/fallenbagel/jellyseerr/commit/c3dbd0d6913946e0e1b5308edfbb5ca744740223))
|
||||
* handle Plex library settings migration failure gracefully ([#2254](https://github.com/fallenbagel/jellyseerr/issues/2254)) ([ed53810](https://github.com/fallenbagel/jellyseerr/commit/ed53810fb33f70722361c67d176ff4edf531ba45))
|
||||
* **holiday:** remove special holiday slider ([22f2037](https://github.com/fallenbagel/jellyseerr/commit/22f2037ea6c5a0ba2ffa4d69f2b7cf42bdcf8575))
|
||||
* **issues:** only allow edit of own comments & do not allow non-admin delete of issues with comments ([#2248](https://github.com/fallenbagel/jellyseerr/issues/2248)) ([bba09d6](https://github.com/fallenbagel/jellyseerr/commit/bba09d69c1bc55c2f35db5a7986e7c935cc9619c))
|
||||
* jellyfin user signin after manual user import ([36c3c9d](https://github.com/fallenbagel/jellyseerr/commit/36c3c9d7c60176a5c4090b86313743b3ce433406))
|
||||
* **lang:** add missing string ([#2370](https://github.com/fallenbagel/jellyseerr/issues/2370)) ([d36c1d2](https://github.com/fallenbagel/jellyseerr/commit/d36c1d29295020efb76bac21a443b6f9049802f3))
|
||||
* **lang:** rename 'Media' notification types for clarity ([#2400](https://github.com/fallenbagel/jellyseerr/issues/2400)) ([399b037](https://github.com/fallenbagel/jellyseerr/commit/399b0379186ed34dcc436bd95330fd1a05fef4b3))
|
||||
* **lang:** string edits ([#2229](https://github.com/fallenbagel/jellyseerr/issues/2229)) ([ab20c21](https://github.com/fallenbagel/jellyseerr/commit/ab20c21184639e1c7725f7cae96249c6fa157351))
|
||||
* **lang:** translations update from Hosted Weblate ([#2625](https://github.com/fallenbagel/jellyseerr/issues/2625)) ([19cdedd](https://github.com/fallenbagel/jellyseerr/commit/19cdedd2a6656b1a852e1cc653bbdb140e978b51))
|
||||
* **lang:** translations update from Hosted Weblate ([#2639](https://github.com/fallenbagel/jellyseerr/issues/2639)) ([418a533](https://github.com/fallenbagel/jellyseerr/commit/418a533588bbbdbbbb4caee1ef91d57c1ca35717))
|
||||
* **lang:** translations update from Weblate ([#2212](https://github.com/fallenbagel/jellyseerr/issues/2212)) ([85aec4f](https://github.com/fallenbagel/jellyseerr/commit/85aec4f8925746ebae9bcc99d8480b78ccfd851e))
|
||||
* **logs:** handle log message nested extra properties ([#2459](https://github.com/fallenbagel/jellyseerr/issues/2459)) ([d777940](https://github.com/fallenbagel/jellyseerr/commit/d7779408d162949b2eafcacefc8eabe53fae229f))
|
||||
* **logs:** handle unexpected log messages ([#2303](https://github.com/fallenbagel/jellyseerr/issues/2303)) ([f284e4a](https://github.com/fallenbagel/jellyseerr/commit/f284e4ab978e502d2cc08e76226a8ebac91bb48f))
|
||||
* **logs:** lazily parse log message label ([#2359](https://github.com/fallenbagel/jellyseerr/issues/2359)) ([5af06bd](https://github.com/fallenbagel/jellyseerr/commit/5af06bd87226fbc6176b0c5e362824793165a34e))
|
||||
* **notif:** correct issue notif action URLs ([#2333](https://github.com/fallenbagel/jellyseerr/issues/2333)) ([dc7f959](https://github.com/fallenbagel/jellyseerr/commit/dc7f959cb422a8d89bcebc78377f1513412e542c))
|
||||
* **notif:** duplicate notification check logic ([#2424](https://github.com/fallenbagel/jellyseerr/issues/2424)) ([10651ba](https://github.com/fallenbagel/jellyseerr/commit/10651baa675993f7109989bbac67f54661c8693f))
|
||||
* **notif:** only send MEDIA_AVAILABLE notifications for non-declined requests ([#2343](https://github.com/fallenbagel/jellyseerr/issues/2343)) ([fcb0dcf](https://github.com/fallenbagel/jellyseerr/commit/fcb0dcf5be64bf9ca814bfe119586908922099c5))
|
||||
* **notif:** show event in pop up notification for slack ([#2413](https://github.com/fallenbagel/jellyseerr/issues/2413)) ([d4438c8](https://github.com/fallenbagel/jellyseerr/commit/d4438c82e3753c9b29b6269ad406d263b3fcef4c)), closes [#2408](https://github.com/fallenbagel/jellyseerr/issues/2408)
|
||||
* only run scheduled mediaserver jobs that apply to the current mediaserver ([791106a](https://github.com/fallenbagel/jellyseerr/commit/791106a7f5b8356b67119300bad245f587f6dc5f))
|
||||
* play on Jellyfin for TV shows ([d0c5481](https://github.com/fallenbagel/jellyseerr/commit/d0c5481d22ddceee0b5c3d7d82029f44c46dbbd0))
|
||||
* plex Login ([9d54776](https://github.com/fallenbagel/jellyseerr/commit/9d54776a2c4c23a61d5e619ca952b9e5d947a79b))
|
||||
* **plex:** correctly generate uuid for safari ([#2614](https://github.com/fallenbagel/jellyseerr/issues/2614)) ([d06f2cd](https://github.com/fallenbagel/jellyseerr/commit/d06f2cdb08bfa6f05cf7cec2c408a258fa926b09))
|
||||
* **plex:** find TV series in addition to movies from IMDb IDs ([#1830](https://github.com/fallenbagel/jellyseerr/issues/1830)) ([30644f6](https://github.com/fallenbagel/jellyseerr/commit/30644f65ea2e8437676422ae0b083c642a836887))
|
||||
* **plex:** include 'Overseerr' in X-Plex-Device-Name header ([#2635](https://github.com/fallenbagel/jellyseerr/issues/2635)) ([d4f9650](https://github.com/fallenbagel/jellyseerr/commit/d4f9650cd07704a97f8b591b7de7351c1e85b825))
|
||||
* **plex:** use unique client identifier ([#2602](https://github.com/fallenbagel/jellyseerr/issues/2602)) ([648b346](https://github.com/fallenbagel/jellyseerr/commit/648b346cbe5a941c7e1ec4ddfb276fb0e27ed502))
|
||||
* **plex:** user import ([#2442](https://github.com/fallenbagel/jellyseerr/issues/2442)) ([86dff12](https://github.com/fallenbagel/jellyseerr/commit/86dff12cdeef6dca92527dd31757a3a4c7f921bf))
|
||||
* **radarr:** correctly check for existing movies ([#2490](https://github.com/fallenbagel/jellyseerr/issues/2490)) ([5d4b06b](https://github.com/fallenbagel/jellyseerr/commit/5d4b06bbcc6cf6d328f6b4a86c4c0f9b0f3aff3e))
|
||||
* **radarr:** remove PreDB minimum availability option ([#2386](https://github.com/fallenbagel/jellyseerr/issues/2386)) ([3e5eb4e](https://github.com/fallenbagel/jellyseerr/commit/3e5eb4e148a9f88b871abc4ee1784b870f691534))
|
||||
* relax jellyfin url validation to allow local domains ([3a010f8](https://github.com/fallenbagel/jellyseerr/commit/3a010f821189414efd334b4cad2a300501f40a18))
|
||||
* replaced unknown job with jellyfin in jobsandcache and added translations for it ([f09b86a](https://github.com/fallenbagel/jellyseerr/commit/f09b86aa87d84af1ddee07390a04dd8543cff8a6))
|
||||
* **requests:** check for existing media of same type when requesting ([#2445](https://github.com/fallenbagel/jellyseerr/issues/2445)) ([eb9ca2e](https://github.com/fallenbagel/jellyseerr/commit/eb9ca2e86f3be3f4ff8ee2e7c4aecdf337d8976d))
|
||||
* **requests:** do not fail request edits if acting user lacks Manage Users permission ([#2338](https://github.com/fallenbagel/jellyseerr/issues/2338)) ([91bfff7](https://github.com/fallenbagel/jellyseerr/commit/91bfff71b7c05c9b9aad2c95282533eefbb6b2e7))
|
||||
* **scripts:** update migration scripts ([#2208](https://github.com/fallenbagel/jellyseerr/issues/2208)) [skip ci] ([d0ac74e](https://github.com/fallenbagel/jellyseerr/commit/d0ac74ea4bbfcf3d25d30cbd422d9df1c1259a18))
|
||||
* secure session cookie ([#2308](https://github.com/fallenbagel/jellyseerr/issues/2308)) ([7f330af](https://github.com/fallenbagel/jellyseerr/commit/7f330aff2e1d3546e8dd1a3e4b037b9beb1cc7f0))
|
||||
* **servarr:** handle baseurl error when testing connection ([#2294](https://github.com/fallenbagel/jellyseerr/issues/2294)) ([93b5ea2](https://github.com/fallenbagel/jellyseerr/commit/93b5ea20ca590996f6dc90713a76800180d0621c))
|
||||
* **servarr:** handle servaarr server being unavailable when scanning downloads ([#2358](https://github.com/fallenbagel/jellyseerr/issues/2358)) ([488874f](https://github.com/fallenbagel/jellyseerr/commit/488874fc17e4e4719e90d383b83b1e1a5217213b))
|
||||
* **sonarr:** monitor existing series upon request approval ([#2553](https://github.com/fallenbagel/jellyseerr/issues/2553)) ([aa062d9](https://github.com/fallenbagel/jellyseerr/commit/aa062d921c425d4b64bfdb28a5f102b0c92f7d87))
|
||||
* **sonarr:** only scan seasons that exist in TMDb ([#2523](https://github.com/fallenbagel/jellyseerr/issues/2523)) ([6168185](https://github.com/fallenbagel/jellyseerr/commit/61681857b123802aaeff02a8f61b1ba046c5d333))
|
||||
* sort collection parts by release date ([#2368](https://github.com/fallenbagel/jellyseerr/issues/2368)) ([1b3797c](https://github.com/fallenbagel/jellyseerr/commit/1b3797cf6e6ef6b3d8c81e644382f6e3f68cfaaa))
|
||||
* **tautulli:** fetch additional user history as necessary to return 20 unique media ([#2446](https://github.com/fallenbagel/jellyseerr/issues/2446)) ([7d19de6](https://github.com/fallenbagel/jellyseerr/commit/7d19de6a4af6297be18140ca59402b40f7bbb30b))
|
||||
* **ui:** Fix webhook URL validation regex ([#864](https://github.com/fallenbagel/jellyseerr/issues/864)) ([726f62b](https://github.com/fallenbagel/jellyseerr/commit/726f62b9b69b5078e718f129e26abdf358f5cb06))
|
||||
* **ui:** refinements for 'About' page ([#2173](https://github.com/fallenbagel/jellyseerr/issues/2173)) ([084a842](https://github.com/fallenbagel/jellyseerr/commit/084a842a4f9b6caaed22edbe77bc9e414bc1f387))
|
||||
* **ui:** request badge styling in request list ([#2302](https://github.com/fallenbagel/jellyseerr/issues/2302)) ([f2375c9](https://github.com/fallenbagel/jellyseerr/commit/f2375c902b79dcb1f349500862775ae57ea7d406))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **about:** show config directory ([#2600](https://github.com/fallenbagel/jellyseerr/issues/2600)) ([0c7373c](https://github.com/fallenbagel/jellyseerr/commit/0c7373c7e89a4ff717efaa7d6a5854f7ccd6a8d3))
|
||||
* add emby detail url support ([88c2c5e](https://github.com/fallenbagel/jellyseerr/commit/88c2c5ebcddd1eb8aea4a4e72c68a91197dec065))
|
||||
* add production countries to movie/TV detail pages ([#2170](https://github.com/fallenbagel/jellyseerr/issues/2170)) ([30b20df](https://github.com/fallenbagel/jellyseerr/commit/30b20df37a9604ba1c066f89e54a5482a09575ea))
|
||||
* add quotas, advanced options, and toggles to collection request modal ([#1742](https://github.com/fallenbagel/jellyseerr/issues/1742)) ([af40212](https://github.com/fallenbagel/jellyseerr/commit/af40212a738f8d6d9a5bf26dc20c0c87780d6020))
|
||||
* allow Jellyfin to set a playback URL different to the Jellyfin host specified during setup ([9fbc407](https://github.com/fallenbagel/jellyseerr/commit/9fbc4074e491bbeba7880fd54c99d4e3c95c7d01))
|
||||
* **api:** add additional request counts ([#2426](https://github.com/fallenbagel/jellyseerr/issues/2426)) ([2535edc](https://github.com/fallenbagel/jellyseerr/commit/2535edcc7fd6ec66fd45ad754c03929f1fe94871))
|
||||
* **discord:** add 'Enable Mentions' setting ([#1779](https://github.com/fallenbagel/jellyseerr/issues/1779)) ([5f7538a](https://github.com/fallenbagel/jellyseerr/commit/5f7538ae2bf9c6e2feea385cc299bd08df071218))
|
||||
* display release dates for theatrical, digital, and physical release types ([#1492](https://github.com/fallenbagel/jellyseerr/issues/1492)) ([a4dca23](https://github.com/fallenbagel/jellyseerr/commit/a4dca2356b7605026f7bc45b691496e765c3328c))
|
||||
* dynamically fetch login screen backdrop images ([#2206](https://github.com/fallenbagel/jellyseerr/issues/2206)) ([3486d0b](https://github.com/fallenbagel/jellyseerr/commit/3486d0bf5520cbdff60bd8fd023caed76c452973))
|
||||
* **frontend:** add Discovery+ to network slider ([#2345](https://github.com/fallenbagel/jellyseerr/issues/2345)) ([2ded8f5](https://github.com/fallenbagel/jellyseerr/commit/2ded8f5484168bd7b8f45124d9ebdd296a5708d5))
|
||||
* **frontend:** add Hulu to network slider ([#2204](https://github.com/fallenbagel/jellyseerr/issues/2204)) ([1e402f7](https://github.com/fallenbagel/jellyseerr/commit/1e402f710b53c11855aab0abdb4b12c51c30b022))
|
||||
* **frontend:** open media management slideover on status badge click ([#2407](https://github.com/fallenbagel/jellyseerr/issues/2407)) ([1f5785d](https://github.com/fallenbagel/jellyseerr/commit/1f5785d6c53b2ca2da67a8ccee72165c052c61a1))
|
||||
* implement import users from Jellyfin button ([9e2f3f0](https://github.com/fallenbagel/jellyseerr/commit/9e2f3f06393e71ba5d1c0ba3c9512b64a3ce3ad7))
|
||||
* initialize Jellyfin/Emby users with local login ([103350f](https://github.com/fallenbagel/jellyseerr/commit/103350fe146fbf212b12a3348bcfb40399e1a0fc))
|
||||
* issues ([#2180](https://github.com/fallenbagel/jellyseerr/issues/2180)) ([e402c42](https://github.com/fallenbagel/jellyseerr/commit/e402c42aaa7d795cd724856a2e23615bb1a3695d))
|
||||
* **jobs:** allow modifying job schedules ([#1440](https://github.com/fallenbagel/jellyseerr/issues/1440)) ([82614ca](https://github.com/fallenbagel/jellyseerr/commit/82614ca4410782a12d65b4c0a6526ff064be1241))
|
||||
* **lang:** add Albanian display language ([#2605](https://github.com/fallenbagel/jellyseerr/issues/2605)) ([3d32462](https://github.com/fallenbagel/jellyseerr/commit/3d32462f50b4ced0d9205b79003c35d6d1c948a3))
|
||||
* **lang:** add Czech and Danish display languages ([#2176](https://github.com/fallenbagel/jellyseerr/issues/2176)) ([8d8db6c](https://github.com/fallenbagel/jellyseerr/commit/8d8db6cf5d98d4e498a31db339d02f8a98057c8d))
|
||||
* **lang:** add Polish display language ([#2261](https://github.com/fallenbagel/jellyseerr/issues/2261)) ([c760cea](https://github.com/fallenbagel/jellyseerr/commit/c760ceaa5f36c77fa3ce320fae1b4597d2d8b976))
|
||||
* **lang:** translated using Weblate (Chinese (Traditional)) ([#2272](https://github.com/fallenbagel/jellyseerr/issues/2272)) ([d401e33](https://github.com/fallenbagel/jellyseerr/commit/d401e33249cbbca6e707479e5f0207e298ef3248))
|
||||
* **lang:** translations update from Hosted Weblate ([#2277](https://github.com/fallenbagel/jellyseerr/issues/2277)) ([92732fc](https://github.com/fallenbagel/jellyseerr/commit/92732fcb42c56242d16daab00e2d38740b92dea0))
|
||||
* **lang:** translations update from Hosted Weblate ([#2315](https://github.com/fallenbagel/jellyseerr/issues/2315)) ([6245be1](https://github.com/fallenbagel/jellyseerr/commit/6245be1e10dda67c869b59522c1290e7c100145f))
|
||||
* **lang:** translations update from Hosted Weblate ([#2320](https://github.com/fallenbagel/jellyseerr/issues/2320)) ([68112fa](https://github.com/fallenbagel/jellyseerr/commit/68112faefbd64d5c71d3eff21620767f88ccfc34))
|
||||
* **lang:** translations update from Hosted Weblate ([#2325](https://github.com/fallenbagel/jellyseerr/issues/2325)) ([febf067](https://github.com/fallenbagel/jellyseerr/commit/febf0677b880d2fed2822ce510db7cbb0826a920))
|
||||
* **lang:** translations update from Hosted Weblate ([#2336](https://github.com/fallenbagel/jellyseerr/issues/2336)) ([3f7ef7a](https://github.com/fallenbagel/jellyseerr/commit/3f7ef7af97a807ef38041f4f2642b565aa33d066))
|
||||
* **lang:** translations update from Hosted Weblate ([#2341](https://github.com/fallenbagel/jellyseerr/issues/2341)) ([33fe0bd](https://github.com/fallenbagel/jellyseerr/commit/33fe0bdd1e00da40e85b4e4b4780134b31a105d2))
|
||||
* **lang:** translations update from Hosted Weblate ([#2346](https://github.com/fallenbagel/jellyseerr/issues/2346)) ([50dc934](https://github.com/fallenbagel/jellyseerr/commit/50dc9341dd98cb2d8ef3ef6471882a5a9b060afa))
|
||||
* **lang:** translations update from Hosted Weblate ([#2364](https://github.com/fallenbagel/jellyseerr/issues/2364)) ([d437cc2](https://github.com/fallenbagel/jellyseerr/commit/d437cc25392e9c0881888371ffabc82892a1b15c))
|
||||
* **lang:** translations update from Hosted Weblate ([#2366](https://github.com/fallenbagel/jellyseerr/issues/2366)) ([cc2b2bc](https://github.com/fallenbagel/jellyseerr/commit/cc2b2bc7a8ecd89e1feb38a907596b16df9bf0fc))
|
||||
* **lang:** translations update from Hosted Weblate ([#2374](https://github.com/fallenbagel/jellyseerr/issues/2374)) ([b9bedac](https://github.com/fallenbagel/jellyseerr/commit/b9bedac7d7ba85223ecf1d9b93b96e2a490d571a))
|
||||
* **lang:** translations update from Hosted Weblate ([#2379](https://github.com/fallenbagel/jellyseerr/issues/2379)) ([bd93168](https://github.com/fallenbagel/jellyseerr/commit/bd93168ba1ed650baf4024569bb6a76811a99820))
|
||||
* **lang:** translations update from Hosted Weblate ([#2389](https://github.com/fallenbagel/jellyseerr/issues/2389)) ([d2241a4](https://github.com/fallenbagel/jellyseerr/commit/d2241a41877d126a802fc53c925d258af31f34fd))
|
||||
* **lang:** translations update from Hosted Weblate ([#2404](https://github.com/fallenbagel/jellyseerr/issues/2404)) ([1b29b15](https://github.com/fallenbagel/jellyseerr/commit/1b29b15d7c9a7ec918cb59116d60e1ae2e797dc4))
|
||||
* **lang:** translations update from Hosted Weblate ([#2405](https://github.com/fallenbagel/jellyseerr/issues/2405)) ([879df20](https://github.com/fallenbagel/jellyseerr/commit/879df20022c8c5d9b32858ac5499d3e4369fc064))
|
||||
* **lang:** translations update from Hosted Weblate ([#2414](https://github.com/fallenbagel/jellyseerr/issues/2414)) ([88536b1](https://github.com/fallenbagel/jellyseerr/commit/88536b1f9d6e8c1a11e1adf91b85bab4f34b751c))
|
||||
* **lang:** translations update from Hosted Weblate ([#2425](https://github.com/fallenbagel/jellyseerr/issues/2425)) ([e9d4b63](https://github.com/fallenbagel/jellyseerr/commit/e9d4b6327b50a005ee6c2c3292b6f107e90fc50c))
|
||||
* **lang:** translations update from Hosted Weblate ([#2428](https://github.com/fallenbagel/jellyseerr/issues/2428)) ([f8b1bcc](https://github.com/fallenbagel/jellyseerr/commit/f8b1bccda44371bb6f3f8f4ceeab900b1df3de31))
|
||||
* **lang:** translations update from Hosted Weblate ([#2436](https://github.com/fallenbagel/jellyseerr/issues/2436)) ([99c0407](https://github.com/fallenbagel/jellyseerr/commit/99c04072e9f7be8191f25cbcfd5103017b8796eb))
|
||||
* **lang:** translations update from Hosted Weblate ([#2452](https://github.com/fallenbagel/jellyseerr/issues/2452)) ([b5bd6ee](https://github.com/fallenbagel/jellyseerr/commit/b5bd6ee78f3d4aa14f0c440d1f2a8323dccfa399))
|
||||
* **lang:** translations update from Hosted Weblate ([#2457](https://github.com/fallenbagel/jellyseerr/issues/2457)) ([92b2d32](https://github.com/fallenbagel/jellyseerr/commit/92b2d32d2e1e1d319410a9e357e1304065a77598))
|
||||
* **lang:** translations update from Hosted Weblate ([#2489](https://github.com/fallenbagel/jellyseerr/issues/2489)) ([ec08fa6](https://github.com/fallenbagel/jellyseerr/commit/ec08fa67934715ff4a4d618d5b9ff97853913b78))
|
||||
* **lang:** translations update from Hosted Weblate ([#2508](https://github.com/fallenbagel/jellyseerr/issues/2508)) ([9f4ae34](https://github.com/fallenbagel/jellyseerr/commit/9f4ae34da76707a40e2c89a50c722ffa1c0327c0))
|
||||
* **lang:** translations update from Hosted Weblate ([#2531](https://github.com/fallenbagel/jellyseerr/issues/2531)) ([54b32eb](https://github.com/fallenbagel/jellyseerr/commit/54b32ebfd6b2eb6aeeea98c25939166eda8cc17f))
|
||||
* **lang:** translations update from Hosted Weblate ([#2541](https://github.com/fallenbagel/jellyseerr/issues/2541)) ([4549ed3](https://github.com/fallenbagel/jellyseerr/commit/4549ed389e4f25c0946dc01526387e5ac000c3cf))
|
||||
* **lang:** translations update from Hosted Weblate ([#2611](https://github.com/fallenbagel/jellyseerr/issues/2611)) ([81c75c8](https://github.com/fallenbagel/jellyseerr/commit/81c75c800edf6d36a1082a291ef7e308f338d005))
|
||||
* **lang:** translations update from Hosted Weblate ([#2629](https://github.com/fallenbagel/jellyseerr/issues/2629)) ([1d0cbd2](https://github.com/fallenbagel/jellyseerr/commit/1d0cbd2e761072be0b4b3de461397ad9f9f681f3))
|
||||
* **lang:** translations update from Hosted Weblate ([#2645](https://github.com/fallenbagel/jellyseerr/issues/2645)) ([341e3b8](https://github.com/fallenbagel/jellyseerr/commit/341e3b8f0657e09f53ad0b813b051290947343c0))
|
||||
* **lang:** translations update from Weblate ([#2101](https://github.com/fallenbagel/jellyseerr/issues/2101)) ([c73cf7b](https://github.com/fallenbagel/jellyseerr/commit/c73cf7b19cbc19e97a777c0facb9264fb0113093))
|
||||
* **lang:** translations update from Weblate ([#2179](https://github.com/fallenbagel/jellyseerr/issues/2179)) ([e3312ce](https://github.com/fallenbagel/jellyseerr/commit/e3312cef33821c8cb76a4a63bd565c78d67b3e0b))
|
||||
* **lang:** translations update from Weblate ([#2185](https://github.com/fallenbagel/jellyseerr/issues/2185)) ([dce10f7](https://github.com/fallenbagel/jellyseerr/commit/dce10f743f52cb04036e2cdaee280e26a81b253b))
|
||||
* **lang:** translations update from Weblate ([#2202](https://github.com/fallenbagel/jellyseerr/issues/2202)) ([492d8e3](https://github.com/fallenbagel/jellyseerr/commit/492d8e3daa5fb99aa9df2a18978085d5ddd581e7))
|
||||
* **lang:** translations update from Weblate ([#2210](https://github.com/fallenbagel/jellyseerr/issues/2210)) ([0a6ef6c](https://github.com/fallenbagel/jellyseerr/commit/0a6ef6cc81376f7a02f1483109be7ae4ab851c48))
|
||||
* **lang:** translations update from Weblate ([#2226](https://github.com/fallenbagel/jellyseerr/issues/2226)) ([62b3dc5](https://github.com/fallenbagel/jellyseerr/commit/62b3dc5471c28f4d0e4399cb3bc8bfab94cff5ea))
|
||||
* **lang:** translations update from Weblate ([#2241](https://github.com/fallenbagel/jellyseerr/issues/2241)) ([2b0b8e0](https://github.com/fallenbagel/jellyseerr/commit/2b0b8e05d9c95ff9218cea858a920a2815871186))
|
||||
* **lang:** translations update from Weblate ([#2244](https://github.com/fallenbagel/jellyseerr/issues/2244)) ([0828b00](https://github.com/fallenbagel/jellyseerr/commit/0828b008badc8b512316799a6787bb7c403658d5))
|
||||
* **lang:** translations update from Weblate ([#2247](https://github.com/fallenbagel/jellyseerr/issues/2247)) ([8c49309](https://github.com/fallenbagel/jellyseerr/commit/8c49309c35c31f7bcd0b84b0a307febc16842f68))
|
||||
* **lang:** translations update from Weblate ([#2252](https://github.com/fallenbagel/jellyseerr/issues/2252)) ([99d5000](https://github.com/fallenbagel/jellyseerr/commit/99d50004e58f6b4594df0a171f6bc668635ec50c))
|
||||
* **lang:** translations update from Weblate ([#2265](https://github.com/fallenbagel/jellyseerr/issues/2265)) ([b1b367a](https://github.com/fallenbagel/jellyseerr/commit/b1b367aac625ed3eb865832c94c2352e5a5c40f5))
|
||||
* **logs:** use separate json file to parse logs for log viewer ([#2399](https://github.com/fallenbagel/jellyseerr/issues/2399)) ([ce31bef](https://github.com/fallenbagel/jellyseerr/commit/ce31bef8a125c5492f2a1cfef0dcf3d8a4e9ee11))
|
||||
* **notif:** 4K media notifications ([#2324](https://github.com/fallenbagel/jellyseerr/issues/2324)) ([88a8c1a](https://github.com/fallenbagel/jellyseerr/commit/88a8c1aa596e1113d6da52e5e8cbe443abc6384f))
|
||||
* **notif:** add Gotify agent ([#2196](https://github.com/fallenbagel/jellyseerr/issues/2196)) ([e0b6abe](https://github.com/fallenbagel/jellyseerr/commit/e0b6abe4796f5a324c0ff78cff317fcaead671f1)), closes [#2183](https://github.com/fallenbagel/jellyseerr/issues/2183) [#2183](https://github.com/fallenbagel/jellyseerr/issues/2183) [#2077](https://github.com/fallenbagel/jellyseerr/issues/2077) [#2183](https://github.com/fallenbagel/jellyseerr/issues/2183) [#2183](https://github.com/fallenbagel/jellyseerr/issues/2183) [#2183](https://github.com/fallenbagel/jellyseerr/issues/2183) [#2077](https://github.com/fallenbagel/jellyseerr/issues/2077) [#2183](https://github.com/fallenbagel/jellyseerr/issues/2183) [#2183](https://github.com/fallenbagel/jellyseerr/issues/2183) [#2183](https://github.com/fallenbagel/jellyseerr/issues/2183)
|
||||
* **notif:** add Pushbullet and Pushover agents to user notification settings ([#1740](https://github.com/fallenbagel/jellyseerr/issues/1740)) ([aeb7a48](https://github.com/fallenbagel/jellyseerr/commit/aeb7a48d72cec3fa2b857030aad3eaa0a457a896))
|
||||
* **notif:** add Pushbullet channel tag ([#2198](https://github.com/fallenbagel/jellyseerr/issues/2198)) ([f9200b7](https://github.com/fallenbagel/jellyseerr/commit/f9200b7977208f9b8267ce3a74bd8a86d6f28f7b))
|
||||
* **notif:** issue notifications ([#2242](https://github.com/fallenbagel/jellyseerr/issues/2242)) ([c9ffac3](https://github.com/fallenbagel/jellyseerr/commit/c9ffac33f7c04d926f8c45295703689d42fe87af))
|
||||
* **plex:** selective user import ([#2188](https://github.com/fallenbagel/jellyseerr/issues/2188)) ([9cb97db](https://github.com/fallenbagel/jellyseerr/commit/9cb97db13ced5df2dc595cd9033470b1a0750093))
|
||||
* remove email requirement for jellyfin/emby non-admin users ([3e1e11d](https://github.com/fallenbagel/jellyseerr/commit/3e1e11d9d93e5d055c92989361a3ced3b77b1d39))
|
||||
* **search:** close search bar when hitting return ([#2260](https://github.com/fallenbagel/jellyseerr/issues/2260)) ([b423dc1](https://github.com/fallenbagel/jellyseerr/commit/b423dc167d12f0ba49f902876bceb2e876e35f58))
|
||||
* **search:** filter search results by year ([#2460](https://github.com/fallenbagel/jellyseerr/issues/2460)) ([72c825d](https://github.com/fallenbagel/jellyseerr/commit/72c825d2a5109688bcc1991a30249284bf281500))
|
||||
* **search:** search by id ([#2082](https://github.com/fallenbagel/jellyseerr/issues/2082)) ([b31cdbf](https://github.com/fallenbagel/jellyseerr/commit/b31cdbf074d5dbecbbf6da135a9b686aea9e3c0e))
|
||||
* **servarr:** auto fill base url when testing service if missing ([#1995](https://github.com/fallenbagel/jellyseerr/issues/1995)) ([739f667](https://github.com/fallenbagel/jellyseerr/commit/739f667b54d8dec258b74d0cd8fd8b3b88dcf8d5))
|
||||
* Tautulli integration ([#2230](https://github.com/fallenbagel/jellyseerr/issues/2230)) ([0842c23](https://github.com/fallenbagel/jellyseerr/commit/0842c233d0fc56d44824cad18749492cd52cbed5))
|
||||
* **tautulli:** validate upon saving settings ([#2511](https://github.com/fallenbagel/jellyseerr/issues/2511)) ([1dc900d](https://github.com/fallenbagel/jellyseerr/commit/1dc900d5ce9689d179c9d2f554abc74ca50bd9cb))
|
||||
* **ui:** add trakt external link ([#2367](https://github.com/fallenbagel/jellyseerr/issues/2367)) ([4e56bae](https://github.com/fallenbagel/jellyseerr/commit/4e56bae98508c1a60aeb3a08560ba1c00acce7e7))
|
||||
* **ui:** allow admins to edit & approve request from advanced request modal ([#2067](https://github.com/fallenbagel/jellyseerr/issues/2067)) ([340f1a2](https://github.com/fallenbagel/jellyseerr/commit/340f1a211952bd2e8f40f0ea4622b52dbe934e85))
|
||||
* **ui:** link processing/requested status badges to service URL ([#1761](https://github.com/fallenbagel/jellyseerr/issues/1761)) ([032c14a](https://github.com/fallenbagel/jellyseerr/commit/032c14a22680f62f8106943297b081b68645ce61))
|
||||
* verify Plex server access during auth for existing users with Plex IDs ([#2458](https://github.com/fallenbagel/jellyseerr/issues/2458)) ([85bb30e](https://github.com/fallenbagel/jellyseerr/commit/85bb30e252c27047ae367491f0e5bb92a7d52605))
|
||||
|
||||
## [1.29.1](https://github.com/sct/overseerr/compare/v1.29.0...v1.29.1) (2022-04-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -86,7 +86,7 @@ When adding new UI text, please try to adhere to the following guidelines:
|
||||
1. Be concise and clear, and use as few words as possible to make your point.
|
||||
2. Use the Oxford comma where appropriate.
|
||||
3. Use the appropriate Unicode characters for ellipses, arrows, and other special characters/symbols.
|
||||
4. Capitalize proper nouns, such as Plex, Radarr, Sonarr, Telegram, Slack, Pushover, etc. Be sure to also use the official capitalization for any abbreviations; e.g., IMDb has a lowercase 'b', whereas TMDB and TheTVDB have a capital 'B'.
|
||||
4. Capitalize proper nouns, such as Plex, Radarr, Sonarr, Telegram, Slack, Pushover, etc. Be sure to also use the official capitalization for any abbreviations; e.g., TMDb and IMDb have a lowercase 'b', whereas TheTVDB has a capital 'B'.
|
||||
5. Title case headings, button text, and form labels. Note that verbs such as "is" should be capitalized, whereas prepositions like "from" should be lowercase (unless as the first or last word of the string, in which case they are also capitalized).
|
||||
6. Capitalize the first word in validation error messages, dropdowns, and form "tips." These strings should not end in punctuation.
|
||||
7. Ensure that toast notification strings are complete sentences ending in punctuation.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:16.17-alpine AS BUILD_IMAGE
|
||||
FROM node:16.14-alpine AS BUILD_IMAGE
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -14,7 +14,7 @@ RUN \
|
||||
esac
|
||||
|
||||
COPY package.json yarn.lock ./
|
||||
RUN CYPRESS_INSTALL_BINARY=0 yarn install --frozen-lockfile --network-timeout 1000000
|
||||
RUN yarn install --frozen-lockfile --network-timeout 1000000
|
||||
|
||||
COPY . ./
|
||||
|
||||
@@ -33,7 +33,7 @@ RUN touch config/DOCKER
|
||||
RUN echo "{\"commitTag\": \"${COMMIT_TAG}\"}" > committag.json
|
||||
|
||||
|
||||
FROM node:16.17-alpine
|
||||
FROM node:16.14-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:16.17-alpine
|
||||
FROM node:16.14-alpine
|
||||
|
||||
COPY . /app
|
||||
WORKDIR /app
|
||||
|
||||
@@ -9,15 +9,11 @@
|
||||
|
||||
**Jellyseerr** is a free and open source software application for managing requests for your media library. It is a a fork of Overseerr built to bring support for Jellyfin & Emby media servers!
|
||||
|
||||
_The original Overseerr team have been busy and Jellyfin/Emby support aren't on their roadmap, so we started this project as we wanted to bring the Overseerr experience to the Jellyfin/Emby Community!_
|
||||
|
||||
## Current Features
|
||||
|
||||
- Jellyfin Support
|
||||
- Emby Support
|
||||
|
||||
(Upcoming Features include: Multiple Server Instances, Music Support, Ability to change email address and much more!)
|
||||
|
||||
Along with all the existing Overseerr features:
|
||||
|
||||
- Full Plex integration. Authenticate and manage user access with Plex!
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
projectId: 'onnqy3',
|
||||
e2e: {
|
||||
baseUrl: 'http://localhost:5055',
|
||||
experimentalSessionAndOrigin: true,
|
||||
},
|
||||
env: {
|
||||
ADMIN_EMAIL: 'admin@seerr.dev',
|
||||
ADMIN_PASSWORD: 'test1234',
|
||||
USER_EMAIL: 'friend@seerr.dev',
|
||||
USER_PASSWORD: 'test1234',
|
||||
},
|
||||
retries: {
|
||||
runMode: 2,
|
||||
openMode: 0,
|
||||
},
|
||||
});
|
||||
@@ -1,149 +0,0 @@
|
||||
{
|
||||
"clientId": "6919275e-142a-48d8-be6b-93594cbd4626",
|
||||
"vapidPrivate": "tmnslaO8ZWN6bNbSEv_rolPeBTlNxOwCCAHrM9oZz3M",
|
||||
"vapidPublic": "BK_EpP8NDm9waor2zn6_S28o3ZYv4kCkJOfYpO3pt3W6jnPmxrgTLANUBNbbyaNatPnSQ12De9CeqSYQrqWzHTs",
|
||||
"main": {
|
||||
"apiKey": "testkey",
|
||||
"applicationTitle": "Overseerr",
|
||||
"applicationUrl": "",
|
||||
"csrfProtection": false,
|
||||
"cacheImages": false,
|
||||
"defaultPermissions": 32,
|
||||
"defaultQuotas": {
|
||||
"movie": {},
|
||||
"tv": {}
|
||||
},
|
||||
"hideAvailable": false,
|
||||
"localLogin": true,
|
||||
"newPlexLogin": true,
|
||||
"region": "",
|
||||
"originalLanguage": "",
|
||||
"trustProxy": false,
|
||||
"partialRequestsEnabled": true,
|
||||
"locale": "en"
|
||||
},
|
||||
"plex": {
|
||||
"name": "Seerr",
|
||||
"ip": "192.168.1.1",
|
||||
"port": 32400,
|
||||
"useSsl": false,
|
||||
"libraries": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "Movies",
|
||||
"enabled": true,
|
||||
"type": "movie"
|
||||
}
|
||||
],
|
||||
"machineId": "test"
|
||||
},
|
||||
"tautulli": {},
|
||||
"radarr": [],
|
||||
"sonarr": [],
|
||||
"public": {
|
||||
"initialized": true
|
||||
},
|
||||
"notifications": {
|
||||
"agents": {
|
||||
"email": {
|
||||
"enabled": false,
|
||||
"options": {
|
||||
"emailFrom": "",
|
||||
"smtpHost": "",
|
||||
"smtpPort": 587,
|
||||
"secure": false,
|
||||
"ignoreTls": false,
|
||||
"requireTls": false,
|
||||
"allowSelfSigned": false,
|
||||
"senderName": "Overseerr"
|
||||
}
|
||||
},
|
||||
"discord": {
|
||||
"enabled": false,
|
||||
"types": 0,
|
||||
"options": {
|
||||
"webhookUrl": "",
|
||||
"enableMentions": true
|
||||
}
|
||||
},
|
||||
"lunasea": {
|
||||
"enabled": false,
|
||||
"types": 0,
|
||||
"options": {
|
||||
"webhookUrl": ""
|
||||
}
|
||||
},
|
||||
"slack": {
|
||||
"enabled": false,
|
||||
"types": 0,
|
||||
"options": {
|
||||
"webhookUrl": ""
|
||||
}
|
||||
},
|
||||
"telegram": {
|
||||
"enabled": false,
|
||||
"types": 0,
|
||||
"options": {
|
||||
"botAPI": "",
|
||||
"chatId": "",
|
||||
"sendSilently": false
|
||||
}
|
||||
},
|
||||
"pushbullet": {
|
||||
"enabled": false,
|
||||
"types": 0,
|
||||
"options": {
|
||||
"accessToken": ""
|
||||
}
|
||||
},
|
||||
"pushover": {
|
||||
"enabled": false,
|
||||
"types": 0,
|
||||
"options": {
|
||||
"accessToken": "",
|
||||
"userToken": ""
|
||||
}
|
||||
},
|
||||
"webhook": {
|
||||
"enabled": false,
|
||||
"types": 0,
|
||||
"options": {
|
||||
"webhookUrl": "",
|
||||
"jsonPayload": "IntcbiAgICBcIm5vdGlmaWNhdGlvbl90eXBlXCI6IFwie3tub3RpZmljYXRpb25fdHlwZX19XCIsXG4gICAgXCJldmVudFwiOiBcInt7ZXZlbnR9fVwiLFxuICAgIFwic3ViamVjdFwiOiBcInt7c3ViamVjdH19XCIsXG4gICAgXCJtZXNzYWdlXCI6IFwie3ttZXNzYWdlfX1cIixcbiAgICBcImltYWdlXCI6IFwie3tpbWFnZX19XCIsXG4gICAgXCJ7e21lZGlhfX1cIjoge1xuICAgICAgICBcIm1lZGlhX3R5cGVcIjogXCJ7e21lZGlhX3R5cGV9fVwiLFxuICAgICAgICBcInRtZGJJZFwiOiBcInt7bWVkaWFfdG1kYmlkfX1cIixcbiAgICAgICAgXCJ0dmRiSWRcIjogXCJ7e21lZGlhX3R2ZGJpZH19XCIsXG4gICAgICAgIFwic3RhdHVzXCI6IFwie3ttZWRpYV9zdGF0dXN9fVwiLFxuICAgICAgICBcInN0YXR1czRrXCI6IFwie3ttZWRpYV9zdGF0dXM0a319XCJcbiAgICB9LFxuICAgIFwie3tyZXF1ZXN0fX1cIjoge1xuICAgICAgICBcInJlcXVlc3RfaWRcIjogXCJ7e3JlcXVlc3RfaWR9fVwiLFxuICAgICAgICBcInJlcXVlc3RlZEJ5X2VtYWlsXCI6IFwie3tyZXF1ZXN0ZWRCeV9lbWFpbH19XCIsXG4gICAgICAgIFwicmVxdWVzdGVkQnlfdXNlcm5hbWVcIjogXCJ7e3JlcXVlc3RlZEJ5X3VzZXJuYW1lfX1cIixcbiAgICAgICAgXCJyZXF1ZXN0ZWRCeV9hdmF0YXJcIjogXCJ7e3JlcXVlc3RlZEJ5X2F2YXRhcn19XCJcbiAgICB9LFxuICAgIFwie3tpc3N1ZX19XCI6IHtcbiAgICAgICAgXCJpc3N1ZV9pZFwiOiBcInt7aXNzdWVfaWR9fVwiLFxuICAgICAgICBcImlzc3VlX3R5cGVcIjogXCJ7e2lzc3VlX3R5cGV9fVwiLFxuICAgICAgICBcImlzc3VlX3N0YXR1c1wiOiBcInt7aXNzdWVfc3RhdHVzfX1cIixcbiAgICAgICAgXCJyZXBvcnRlZEJ5X2VtYWlsXCI6IFwie3tyZXBvcnRlZEJ5X2VtYWlsfX1cIixcbiAgICAgICAgXCJyZXBvcnRlZEJ5X3VzZXJuYW1lXCI6IFwie3tyZXBvcnRlZEJ5X3VzZXJuYW1lfX1cIixcbiAgICAgICAgXCJyZXBvcnRlZEJ5X2F2YXRhclwiOiBcInt7cmVwb3J0ZWRCeV9hdmF0YXJ9fVwiXG4gICAgfSxcbiAgICBcInt7Y29tbWVudH19XCI6IHtcbiAgICAgICAgXCJjb21tZW50X21lc3NhZ2VcIjogXCJ7e2NvbW1lbnRfbWVzc2FnZX19XCIsXG4gICAgICAgIFwiY29tbWVudGVkQnlfZW1haWxcIjogXCJ7e2NvbW1lbnRlZEJ5X2VtYWlsfX1cIixcbiAgICAgICAgXCJjb21tZW50ZWRCeV91c2VybmFtZVwiOiBcInt7Y29tbWVudGVkQnlfdXNlcm5hbWV9fVwiLFxuICAgICAgICBcImNvbW1lbnRlZEJ5X2F2YXRhclwiOiBcInt7Y29tbWVudGVkQnlfYXZhdGFyfX1cIlxuICAgIH0sXG4gICAgXCJ7e2V4dHJhfX1cIjogW11cbn0i"
|
||||
}
|
||||
},
|
||||
"webpush": {
|
||||
"enabled": false,
|
||||
"options": {}
|
||||
},
|
||||
"gotify": {
|
||||
"enabled": false,
|
||||
"types": 0,
|
||||
"options": {
|
||||
"url": "",
|
||||
"token": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"jobs": {
|
||||
"plex-recently-added-scan": {
|
||||
"schedule": "0 */5 * * * *"
|
||||
},
|
||||
"plex-full-scan": {
|
||||
"schedule": "0 0 3 * * *"
|
||||
},
|
||||
"radarr-scan": {
|
||||
"schedule": "0 0 4 * * *"
|
||||
},
|
||||
"sonarr-scan": {
|
||||
"schedule": "0 30 4 * * *"
|
||||
},
|
||||
"download-sync": {
|
||||
"schedule": "0 * * * * *"
|
||||
},
|
||||
"download-sync-reset": {
|
||||
"schedule": "0 0 1 * * *"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,210 +0,0 @@
|
||||
const clickFirstTitleCardInSlider = (sliderTitle: string): void => {
|
||||
cy.contains('.slider-header', sliderTitle)
|
||||
.next('[data-testid=media-slider]')
|
||||
.find('[data-testid=title-card]')
|
||||
.first()
|
||||
.trigger('mouseover')
|
||||
.find('[data-testid=title-card-title]')
|
||||
.invoke('text')
|
||||
.then((text) => {
|
||||
cy.contains('.slider-header', sliderTitle)
|
||||
.next('[data-testid=media-slider]')
|
||||
.find('[data-testid=title-card]')
|
||||
.first()
|
||||
.click();
|
||||
cy.get('[data-testid=media-title]').should('contain', text);
|
||||
});
|
||||
};
|
||||
|
||||
describe('Discover', () => {
|
||||
beforeEach(() => {
|
||||
cy.loginAsAdmin();
|
||||
});
|
||||
|
||||
it('loads a trending item', () => {
|
||||
cy.intercept('/api/v1/discover/trending*').as('getTrending');
|
||||
cy.visit('/');
|
||||
cy.wait('@getTrending');
|
||||
clickFirstTitleCardInSlider('Trending');
|
||||
});
|
||||
|
||||
it('loads popular movies', () => {
|
||||
cy.intercept('/api/v1/discover/movies*').as('getPopularMovies');
|
||||
cy.visit('/');
|
||||
cy.wait('@getPopularMovies');
|
||||
clickFirstTitleCardInSlider('Popular Movies');
|
||||
});
|
||||
|
||||
it('loads upcoming movies', () => {
|
||||
cy.intercept('/api/v1/discover/movies/upcoming*').as('getUpcomingMovies');
|
||||
cy.visit('/');
|
||||
cy.wait('@getUpcomingMovies');
|
||||
clickFirstTitleCardInSlider('Upcoming Movies');
|
||||
});
|
||||
|
||||
it('loads popular series', () => {
|
||||
cy.intercept('/api/v1/discover/tv*').as('getPopularTv');
|
||||
cy.visit('/');
|
||||
cy.wait('@getPopularTv');
|
||||
clickFirstTitleCardInSlider('Popular Series');
|
||||
});
|
||||
|
||||
it('loads upcoming series', () => {
|
||||
cy.intercept('/api/v1/discover/tv/upcoming*').as('getUpcomingSeries');
|
||||
cy.visit('/');
|
||||
cy.wait('@getUpcomingSeries');
|
||||
clickFirstTitleCardInSlider('Upcoming Series');
|
||||
});
|
||||
|
||||
it('displays error for media with invalid TMDB ID', () => {
|
||||
cy.intercept('GET', '/api/v1/media?*', {
|
||||
pageInfo: { pages: 1, pageSize: 20, results: 1, page: 1 },
|
||||
results: [
|
||||
{
|
||||
downloadStatus: [],
|
||||
downloadStatus4k: [],
|
||||
id: 1922,
|
||||
mediaType: 'movie',
|
||||
tmdbId: 998814,
|
||||
tvdbId: null,
|
||||
imdbId: null,
|
||||
status: 5,
|
||||
status4k: 1,
|
||||
createdAt: '2022-08-18T18:11:13.000Z',
|
||||
updatedAt: '2022-08-18T19:56:41.000Z',
|
||||
lastSeasonChange: '2022-08-18T19:56:41.000Z',
|
||||
mediaAddedAt: '2022-08-18T19:56:41.000Z',
|
||||
serviceId: null,
|
||||
serviceId4k: null,
|
||||
externalServiceId: null,
|
||||
externalServiceId4k: null,
|
||||
externalServiceSlug: null,
|
||||
externalServiceSlug4k: null,
|
||||
ratingKey: null,
|
||||
ratingKey4k: null,
|
||||
seasons: [],
|
||||
},
|
||||
],
|
||||
}).as('getMedia');
|
||||
|
||||
cy.visit('/');
|
||||
cy.wait('@getMedia');
|
||||
cy.contains('.slider-header', 'Recently Added')
|
||||
.next('[data-testid=media-slider]')
|
||||
.find('[data-testid=title-card]')
|
||||
.first()
|
||||
.find('[data-testid=title-card-title]')
|
||||
.contains('Movie Not Found');
|
||||
});
|
||||
|
||||
it('displays error for request with invalid TMDB ID', () => {
|
||||
cy.intercept('GET', '/api/v1/request?*', {
|
||||
pageInfo: { pages: 1, pageSize: 10, results: 1, page: 1 },
|
||||
results: [
|
||||
{
|
||||
id: 582,
|
||||
status: 1,
|
||||
createdAt: '2022-08-18T18:11:13.000Z',
|
||||
updatedAt: '2022-08-18T18:11:13.000Z',
|
||||
type: 'movie',
|
||||
is4k: false,
|
||||
serverId: null,
|
||||
profileId: null,
|
||||
rootFolder: null,
|
||||
languageProfileId: null,
|
||||
tags: null,
|
||||
media: {
|
||||
downloadStatus: [],
|
||||
downloadStatus4k: [],
|
||||
id: 1922,
|
||||
mediaType: 'movie',
|
||||
tmdbId: 998814,
|
||||
tvdbId: null,
|
||||
imdbId: null,
|
||||
status: 2,
|
||||
status4k: 1,
|
||||
createdAt: '2022-08-18T18:11:13.000Z',
|
||||
updatedAt: '2022-08-18T18:11:13.000Z',
|
||||
lastSeasonChange: '2022-08-18T18:11:13.000Z',
|
||||
mediaAddedAt: null,
|
||||
serviceId: null,
|
||||
serviceId4k: null,
|
||||
externalServiceId: null,
|
||||
externalServiceId4k: null,
|
||||
externalServiceSlug: null,
|
||||
externalServiceSlug4k: null,
|
||||
ratingKey: null,
|
||||
ratingKey4k: null,
|
||||
},
|
||||
seasons: [],
|
||||
modifiedBy: null,
|
||||
requestedBy: {
|
||||
permissions: 4194336,
|
||||
id: 18,
|
||||
email: 'friend@seerr.dev',
|
||||
plexUsername: null,
|
||||
username: '',
|
||||
recoveryLinkExpirationDate: null,
|
||||
userType: 2,
|
||||
avatar:
|
||||
'https://gravatar.com/avatar/c77fdc27cab83732b8623d2ea873d330?default=mm&size=200',
|
||||
movieQuotaLimit: null,
|
||||
movieQuotaDays: null,
|
||||
tvQuotaLimit: null,
|
||||
tvQuotaDays: null,
|
||||
createdAt: '2022-08-17T04:55:28.000Z',
|
||||
updatedAt: '2022-08-17T04:55:28.000Z',
|
||||
requestCount: 1,
|
||||
displayName: 'friend@seerr.dev',
|
||||
},
|
||||
seasonCount: 0,
|
||||
},
|
||||
],
|
||||
}).as('getRequests');
|
||||
|
||||
cy.visit('/');
|
||||
cy.wait('@getRequests');
|
||||
cy.contains('.slider-header', 'Recent Requests')
|
||||
.next('[data-testid=media-slider]')
|
||||
.find('[data-testid=request-card]')
|
||||
.first()
|
||||
.find('[data-testid=request-card-title]')
|
||||
.contains('Movie Not Found');
|
||||
});
|
||||
|
||||
it('loads plex watchlist', () => {
|
||||
cy.intercept('/api/v1/discover/watchlist', {
|
||||
fixture: 'watchlist.json',
|
||||
}).as('getWatchlist');
|
||||
// Wait for one of the watchlist movies to resolve
|
||||
cy.intercept('/api/v1/movie/361743').as('getTmdbMovie');
|
||||
|
||||
cy.visit('/');
|
||||
|
||||
cy.wait('@getWatchlist');
|
||||
|
||||
const sliderHeader = cy.contains('.slider-header', 'Your Plex Watchlist');
|
||||
|
||||
sliderHeader.scrollIntoView();
|
||||
|
||||
cy.wait('@getTmdbMovie');
|
||||
// Wait a little longer to make sure the movie component reloaded
|
||||
cy.wait(500);
|
||||
|
||||
sliderHeader
|
||||
.next('[data-testid=media-slider]')
|
||||
.find('[data-testid=title-card]')
|
||||
.first()
|
||||
.trigger('mouseover')
|
||||
.find('[data-testid=title-card-title]')
|
||||
.invoke('text')
|
||||
.then((text) => {
|
||||
cy.contains('.slider-header', 'Plex Watchlist')
|
||||
.next('[data-testid=media-slider]')
|
||||
.find('[data-testid=title-card]')
|
||||
.first()
|
||||
.click();
|
||||
cy.get('[data-testid=media-title]').should('contain', text);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,13 +0,0 @@
|
||||
describe('Login Page', () => {
|
||||
it('succesfully logs in as an admin', () => {
|
||||
cy.loginAsAdmin();
|
||||
cy.visit('/');
|
||||
cy.contains('Trending');
|
||||
});
|
||||
|
||||
it('succesfully logs in as a local user', () => {
|
||||
cy.loginAsUser();
|
||||
cy.visit('/');
|
||||
cy.contains('Trending');
|
||||
});
|
||||
});
|
||||
@@ -1,12 +0,0 @@
|
||||
describe('Movie Details', () => {
|
||||
it('loads a movie page', () => {
|
||||
cy.loginAsAdmin();
|
||||
// Try to load minions: rise of gru
|
||||
cy.visit('/movie/438148');
|
||||
|
||||
cy.get('[data-testid=media-title]').should(
|
||||
'contain',
|
||||
'Minions: The Rise of Gru (2022)'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,25 +0,0 @@
|
||||
describe('Pull To Refresh', () => {
|
||||
beforeEach(() => {
|
||||
cy.login(Cypress.env('ADMIN_EMAIL'), Cypress.env('ADMIN_PASSWORD'));
|
||||
cy.viewport(390, 844);
|
||||
cy.visitMobile('/');
|
||||
});
|
||||
|
||||
it('reloads the current page', () => {
|
||||
cy.wait(500);
|
||||
|
||||
cy.intercept({
|
||||
method: 'GET',
|
||||
url: '/api/v1/*',
|
||||
}).as('apiCall');
|
||||
|
||||
cy.get('.searchbar').swipe('bottom', [190, 400]);
|
||||
|
||||
cy.wait('@apiCall').then((interception) => {
|
||||
assert.isNotNull(
|
||||
interception.response.body,
|
||||
'API was called and received data'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,32 +0,0 @@
|
||||
describe('General Settings', () => {
|
||||
beforeEach(() => {
|
||||
cy.loginAsAdmin();
|
||||
});
|
||||
|
||||
it('opens the settings page from the home page', () => {
|
||||
cy.visit('/');
|
||||
|
||||
cy.get('[data-testid=sidebar-toggle]').click();
|
||||
cy.get('[data-testid=sidebar-menu-settings-mobile]').click();
|
||||
|
||||
cy.get('.heading').should('contain', 'General Settings');
|
||||
});
|
||||
|
||||
it('modifies setting that requires restart', () => {
|
||||
cy.visit('/settings');
|
||||
|
||||
cy.get('#trustProxy').click();
|
||||
cy.get('form').submit();
|
||||
cy.get('[data-testid=modal-title]').should(
|
||||
'contain',
|
||||
'Server Restart Required'
|
||||
);
|
||||
|
||||
cy.get('[data-testid=modal-ok-button]').click();
|
||||
cy.get('[data-testid=modal-title]').should('not.exist');
|
||||
|
||||
cy.get('[type=checkbox]#trustProxy').click();
|
||||
cy.get('form').submit();
|
||||
cy.get('[data-testid=modal-title]').should('not.exist');
|
||||
});
|
||||
});
|
||||
@@ -1,28 +0,0 @@
|
||||
describe('TV Details', () => {
|
||||
it('loads a tv details page', () => {
|
||||
cy.loginAsAdmin();
|
||||
// Try to load stranger things
|
||||
cy.visit('/tv/66732');
|
||||
|
||||
cy.get('[data-testid=media-title]').should(
|
||||
'contain',
|
||||
'Stranger Things (2016)'
|
||||
);
|
||||
});
|
||||
|
||||
it('shows seasons and expands episodes', () => {
|
||||
cy.loginAsAdmin();
|
||||
|
||||
// Try to load stranger things
|
||||
cy.visit('/tv/66732');
|
||||
|
||||
// intercept request for season info
|
||||
cy.intercept('/api/v1/tv/66732/season/4').as('season4');
|
||||
|
||||
cy.contains('Season 4').should('be.visible').scrollIntoView().click();
|
||||
|
||||
cy.wait('@season4');
|
||||
|
||||
cy.contains('Chapter Nine').should('be.visible');
|
||||
});
|
||||
});
|
||||
@@ -1,74 +0,0 @@
|
||||
const visitUserEditPage = (email: string): void => {
|
||||
cy.visit('/users');
|
||||
|
||||
cy.contains('[data-testid=user-list-row]', email).contains('Edit').click();
|
||||
};
|
||||
|
||||
describe('Auto Request Settings', () => {
|
||||
beforeEach(() => {
|
||||
cy.loginAsAdmin();
|
||||
});
|
||||
|
||||
it('should not see watchlist sync settings on an account without permissions', () => {
|
||||
visitUserEditPage(Cypress.env('USER_EMAIL'));
|
||||
|
||||
cy.contains('Auto-Request Movies').should('not.exist');
|
||||
cy.contains('Auto-Request Series').should('not.exist');
|
||||
});
|
||||
|
||||
it('should see watchlist sync settings on an admin account', () => {
|
||||
visitUserEditPage(Cypress.env('ADMIN_EMAIL'));
|
||||
|
||||
cy.contains('Auto-Request Movies').should('exist');
|
||||
cy.contains('Auto-Request Series').should('exist');
|
||||
});
|
||||
|
||||
it('should see auto-request settings after being given permission', () => {
|
||||
visitUserEditPage(Cypress.env('USER_EMAIL'));
|
||||
|
||||
cy.get('[data-testid=settings-nav-desktop').contains('Permissions').click();
|
||||
|
||||
cy.get('#autorequest').should('not.be.checked').click();
|
||||
|
||||
cy.intercept('/api/v1/user/*/settings/permissions').as('userPermissions');
|
||||
|
||||
cy.contains('Save Changes').click();
|
||||
|
||||
cy.wait('@userPermissions');
|
||||
|
||||
cy.reload();
|
||||
|
||||
cy.get('#autorequest').should('be.checked');
|
||||
cy.get('#autorequestmovies').should('be.checked');
|
||||
cy.get('#autorequesttv').should('be.checked');
|
||||
|
||||
cy.get('[data-testid=settings-nav-desktop').contains('General').click();
|
||||
|
||||
cy.contains('Auto-Request Movies').should('exist');
|
||||
cy.contains('Auto-Request Series').should('exist');
|
||||
|
||||
cy.get('#watchlistSyncMovies').should('not.be.checked').click();
|
||||
cy.get('#watchlistSyncTv').should('not.be.checked').click();
|
||||
|
||||
cy.intercept('/api/v1/user/*/settings/main').as('userMain');
|
||||
|
||||
cy.contains('Save Changes').click();
|
||||
|
||||
cy.wait('@userMain');
|
||||
|
||||
cy.reload();
|
||||
|
||||
cy.get('#watchlistSyncMovies').should('be.checked').click();
|
||||
cy.get('#watchlistSyncTv').should('be.checked').click();
|
||||
|
||||
cy.contains('Save Changes').click();
|
||||
|
||||
cy.wait('@userMain');
|
||||
|
||||
cy.get('[data-testid=settings-nav-desktop').contains('Permissions').click();
|
||||
|
||||
cy.get('#autorequest').should('be.checked').click();
|
||||
|
||||
cy.contains('Save Changes').click();
|
||||
});
|
||||
});
|
||||
@@ -1,50 +0,0 @@
|
||||
describe('User Profile', () => {
|
||||
beforeEach(() => {
|
||||
cy.loginAsAdmin();
|
||||
});
|
||||
|
||||
it('opens user profile page from the home page', () => {
|
||||
cy.visit('/');
|
||||
|
||||
cy.get('[data-testid=user-menu]').click();
|
||||
cy.get('[data-testid=user-menu-profile]').click();
|
||||
|
||||
cy.get('h1').should('contain', Cypress.env('ADMIN_EMAIL'));
|
||||
});
|
||||
|
||||
it('loads plex watchlist', () => {
|
||||
cy.intercept('/api/v1/user/[0-9]*/watchlist', {
|
||||
fixture: 'watchlist.json',
|
||||
}).as('getWatchlist');
|
||||
// Wait for one of the watchlist movies to resolve
|
||||
cy.intercept('/api/v1/movie/361743').as('getTmdbMovie');
|
||||
|
||||
cy.visit('/profile');
|
||||
|
||||
cy.wait('@getWatchlist');
|
||||
|
||||
const sliderHeader = cy.contains('.slider-header', 'Plex Watchlist');
|
||||
|
||||
sliderHeader.scrollIntoView();
|
||||
|
||||
cy.wait('@getTmdbMovie');
|
||||
// Wait a little longer to make sure the movie component reloaded
|
||||
cy.wait(500);
|
||||
|
||||
sliderHeader
|
||||
.next('[data-testid=media-slider]')
|
||||
.find('[data-testid=title-card]')
|
||||
.first()
|
||||
.trigger('mouseover')
|
||||
.find('[data-testid=title-card-title]')
|
||||
.invoke('text')
|
||||
.then((text) => {
|
||||
cy.contains('.slider-header', 'Plex Watchlist')
|
||||
.next('[data-testid=media-slider]')
|
||||
.find('[data-testid=title-card]')
|
||||
.first()
|
||||
.click();
|
||||
cy.get('[data-testid=media-title]').should('contain', text);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,70 +0,0 @@
|
||||
const testUser = {
|
||||
displayName: 'Test User',
|
||||
emailAddress: 'test@seeerr.dev',
|
||||
password: 'test1234',
|
||||
};
|
||||
|
||||
describe('User List', () => {
|
||||
beforeEach(() => {
|
||||
cy.loginAsAdmin();
|
||||
});
|
||||
|
||||
it('opens the user list from the home page', () => {
|
||||
cy.visit('/');
|
||||
|
||||
cy.get('[data-testid=sidebar-toggle]').click();
|
||||
cy.get('[data-testid=sidebar-menu-users-mobile]').click();
|
||||
|
||||
cy.get('[data-testid=page-header]').should('contain', 'User List');
|
||||
});
|
||||
|
||||
it('can find the admin user and friend user in the user list', () => {
|
||||
cy.visit('/users');
|
||||
|
||||
cy.get('[data-testid=user-list-row]').contains(Cypress.env('ADMIN_EMAIL'));
|
||||
cy.get('[data-testid=user-list-row]').contains(Cypress.env('USER_EMAIL'));
|
||||
});
|
||||
|
||||
it('can create a local user', () => {
|
||||
cy.visit('/users');
|
||||
|
||||
cy.contains('Create Local User').click();
|
||||
|
||||
cy.get('[data-testid=modal-title]').should('contain', 'Create Local User');
|
||||
|
||||
cy.get('#displayName').type(testUser.displayName);
|
||||
cy.get('#email').type(testUser.emailAddress);
|
||||
cy.get('#password').type(testUser.password);
|
||||
|
||||
cy.intercept('/api/v1/user?take=10&skip=0&sort=displayname').as('user');
|
||||
|
||||
cy.get('[data-testid=modal-ok-button]').click();
|
||||
|
||||
cy.wait('@user');
|
||||
// Wait a little longer for the user list to fully re-render
|
||||
cy.wait(1000);
|
||||
|
||||
cy.get('[data-testid=user-list-row]').contains(testUser.emailAddress);
|
||||
});
|
||||
|
||||
it('can delete the created local test user', () => {
|
||||
cy.visit('/users');
|
||||
|
||||
cy.contains('[data-testid=user-list-row]', testUser.emailAddress)
|
||||
.contains('Delete')
|
||||
.click();
|
||||
|
||||
cy.get('[data-testid=modal-title]').should('contain', `Delete User`);
|
||||
|
||||
cy.intercept('/api/v1/user?take=10&skip=0&sort=displayname').as('user');
|
||||
|
||||
cy.get('[data-testid=modal-ok-button]').should('contain', 'Delete').click();
|
||||
|
||||
cy.wait('@user');
|
||||
cy.wait(1000);
|
||||
|
||||
cy.get('[data-testid=user-list-row]')
|
||||
.contains(testUser.emailAddress)
|
||||
.should('not.exist');
|
||||
});
|
||||
});
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"page": 1,
|
||||
"totalPages": 1,
|
||||
"totalResults": 3,
|
||||
"results": [
|
||||
{
|
||||
"ratingKey": "5d776be17a53e9001e732ab9",
|
||||
"title": "Top Gun: Maverick",
|
||||
"mediaType": "movie",
|
||||
"tmdbId": 361743
|
||||
},
|
||||
{
|
||||
"ratingKey": "5e16338fbc1372003ea68ab3",
|
||||
"title": "Nope",
|
||||
"mediaType": "movie",
|
||||
"tmdbId": 762504
|
||||
},
|
||||
{
|
||||
"ratingKey": "5f409b8452f200004161e126",
|
||||
"title": "Hocus Pocus 2",
|
||||
"mediaType": "movie",
|
||||
"tmdbId": 642885
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
import 'cy-mobile-commands';
|
||||
|
||||
Cypress.Commands.add('login', (email, password) => {
|
||||
cy.session(
|
||||
[email, password],
|
||||
() => {
|
||||
cy.visit('/login');
|
||||
cy.contains('Use your Overseerr account').click();
|
||||
|
||||
cy.get('[data-testid=email]').type(email);
|
||||
cy.get('[data-testid=password]').type(password);
|
||||
|
||||
cy.intercept('/api/v1/auth/local').as('localLogin');
|
||||
cy.get('[data-testid=local-signin-button]').click();
|
||||
|
||||
cy.wait('@localLogin');
|
||||
|
||||
cy.url().should('contain', '/');
|
||||
},
|
||||
{
|
||||
validate() {
|
||||
cy.request('/api/v1/auth/me').its('status').should('eq', 200);
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('loginAsAdmin', () => {
|
||||
cy.login(Cypress.env('ADMIN_EMAIL'), Cypress.env('ADMIN_PASSWORD'));
|
||||
});
|
||||
|
||||
Cypress.Commands.add('loginAsUser', () => {
|
||||
cy.login(Cypress.env('USER_EMAIL'), Cypress.env('USER_PASSWORD'));
|
||||
});
|
||||
@@ -1,7 +0,0 @@
|
||||
import './commands';
|
||||
|
||||
before(() => {
|
||||
if (Cypress.env('SEED_DATABASE')) {
|
||||
cy.exec('yarn cypress:prepare');
|
||||
}
|
||||
});
|
||||
@@ -1,14 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-namespace */
|
||||
/// <reference types="cypress" />
|
||||
|
||||
declare global {
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
login(email?: string, password?: string): Chainable<Element>;
|
||||
loginAsAdmin(): Chainable<Element>;
|
||||
loginAsUser(): Chainable<Element>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["es5", "dom"],
|
||||
"types": ["cypress", "node"],
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"include": ["**/*.ts"]
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
- [LunaSea](https://docs.lunasea.app/modules/overseerr), a self-hosted controller for mobile and macOS
|
||||
- [Requestrr](https://github.com/darkalfx/requestrr/wiki/Configuring-Overseerr), a Discord chatbot
|
||||
- [Doplarr](https://github.com/kiranshila/Doplarr), a Discord request bot
|
||||
- [Overseerr Assistant](https://github.com/RemiRigal/Overseerr-Assistant), a browser extension for requesting directly from TMDB and IMDb
|
||||
- [Overseerr Assistant](https://github.com/RemiRigal/Overseerr-Assistant), a browser extension for requesting directly from TMDb and IMDb
|
||||
- [ha-overseerr](https://github.com/vaparr/ha-overseerr), a custom Home Assistant component
|
||||
- [OverCLIrr](https://github.com/WillFantom/OverCLIrr), a command-line tool
|
||||
- [Overseerr Exporter](https://github.com/WillFantom/overseerr-exporter), a Prometheus exporter
|
||||
|
||||
@@ -45,7 +45,7 @@ Overseerr currently supports the following agents:
|
||||
- New Plex TV
|
||||
- Legacy Plex TV
|
||||
- TheTVDB
|
||||
- TMDB
|
||||
- TMDb
|
||||
- [HAMA](https://github.com/ZeroQI/Hama.bundle)
|
||||
|
||||
Please verify that your library is using one of the agents previously listed.
|
||||
@@ -67,7 +67,7 @@ You can also perform the following to verify the media item has a GUID Overseerr
|
||||
1. Go to the media item in Plex and **"Get info"** and click on **"View XML"**.
|
||||
2. Verify that the media item's GUID follows one of the below formats:
|
||||
|
||||
1. TMDB agent `guid="com.plexapp.agents.themoviedb://1705"`
|
||||
1. TMDb agent `guid="com.plexapp.agents.themoviedb://1705"`
|
||||
2. New Plex Movie agent `<Guid id="tmdb://464052"/>`
|
||||
3. TheTVDB agent `guid="com.plexapp.agents.thetvdb://78874/1/1"`
|
||||
4. Legacy Plex Movie agent `guid="com.plexapp.agents.imdb://tt0765446"`
|
||||
|
||||
@@ -81,7 +81,7 @@ These following special variables are only included in media-related notificatio
|
||||
| Variable | Value |
|
||||
| -------------------- | -------------------------------------------------------------------------------------------------------------- |
|
||||
| `{{media_type}}` | The media type (`movie` or `tv`) |
|
||||
| `{{media_tmdbid}}` | The media's TMDB ID |
|
||||
| `{{media_tmdbid}}` | The media's TMDb ID |
|
||||
| `{{media_tvdbid}}` | The media's TheTVDB ID |
|
||||
| `{{media_status}}` | The media's availability status (`UNKNOWN`, `PENDING`, `PROCESSING`, `PARTIALLY_AVAILABLE`, or `AVAILABLE`) |
|
||||
| `{{media_status4k}}` | The media's 4K availability status (`UNKNOWN`, `PENDING`, `PROCESSING`, `PARTIALLY_AVAILABLE`, or `AVAILABLE`) |
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
/* eslint-disable */
|
||||
const tailwind = require('prettier-plugin-tailwindcss');
|
||||
const organizeImports = require('prettier-plugin-organize-imports');
|
||||
|
||||
const combinedFormatter = {
|
||||
...tailwind,
|
||||
parsers: {
|
||||
...tailwind.parsers,
|
||||
...Object.keys(organizeImports.parsers).reduce((acc, key) => {
|
||||
acc[key] = {
|
||||
...tailwind.parsers[key],
|
||||
preprocess(code, options) {
|
||||
return organizeImports.parsers[key].preprocess(code, options);
|
||||
},
|
||||
};
|
||||
return acc;
|
||||
}, {}),
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = combinedFormatter;
|
||||
@@ -1,6 +1,3 @@
|
||||
/**
|
||||
* @type {import('next').NextConfig}
|
||||
*/
|
||||
module.exports = {
|
||||
env: {
|
||||
commitTag: process.env.COMMIT_TAG || 'local',
|
||||
@@ -21,7 +18,4 @@ module.exports = {
|
||||
|
||||
return config;
|
||||
},
|
||||
experimental: {
|
||||
scrollRestoration: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import 'reflect-metadata';
|
||||
import type { DataSourceOptions, EntityTarget, Repository } from 'typeorm';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
const devConfig: DataSourceOptions = {
|
||||
const devConfig = {
|
||||
type: 'sqlite',
|
||||
database: process.env.CONFIG_DIRECTORY
|
||||
? `${process.env.CONFIG_DIRECTORY}/db/db.sqlite3`
|
||||
@@ -14,30 +10,31 @@ const devConfig: DataSourceOptions = {
|
||||
entities: ['server/entity/**/*.ts'],
|
||||
migrations: ['server/migration/**/*.ts'],
|
||||
subscribers: ['server/subscriber/**/*.ts'],
|
||||
cli: {
|
||||
entitiesDir: 'server/entity',
|
||||
migrationsDir: 'server/migration',
|
||||
},
|
||||
};
|
||||
|
||||
const prodConfig: DataSourceOptions = {
|
||||
const prodConfig = {
|
||||
type: 'sqlite',
|
||||
database: process.env.CONFIG_DIRECTORY
|
||||
? `${process.env.CONFIG_DIRECTORY}/db/db.sqlite3`
|
||||
: 'config/db/db.sqlite3',
|
||||
synchronize: false,
|
||||
migrationsRun: false,
|
||||
logging: false,
|
||||
enableWAL: true,
|
||||
entities: ['dist/entity/**/*.js'],
|
||||
migrations: ['dist/migration/**/*.js'],
|
||||
migrationsRun: false,
|
||||
subscribers: ['dist/subscriber/**/*.js'],
|
||||
cli: {
|
||||
entitiesDir: 'dist/entity',
|
||||
migrationsDir: 'dist/migration',
|
||||
},
|
||||
};
|
||||
|
||||
const dataSource = new DataSource(
|
||||
process.env.NODE_ENV !== 'production' ? devConfig : prodConfig
|
||||
);
|
||||
const finalConfig =
|
||||
process.env.NODE_ENV !== 'production' ? devConfig : prodConfig;
|
||||
|
||||
export const getRepository = <Entity>(
|
||||
target: EntityTarget<Entity>
|
||||
): Repository<Entity> => {
|
||||
return dataSource.getRepository(target);
|
||||
};
|
||||
|
||||
export default dataSource;
|
||||
module.exports = finalConfig;
|
||||
@@ -1841,14 +1841,14 @@ components:
|
||||
paths:
|
||||
/status:
|
||||
get:
|
||||
summary: Get Overseerr status
|
||||
description: Returns the current Overseerr status in a JSON object.
|
||||
summary: Get Overseerr version
|
||||
description: Returns the current Overseerr version in a JSON object.
|
||||
security: []
|
||||
tags:
|
||||
- public
|
||||
responses:
|
||||
'200':
|
||||
description: Returned status
|
||||
description: Returned version
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@@ -1859,12 +1859,6 @@ paths:
|
||||
example: 1.0.0
|
||||
commitTag:
|
||||
type: string
|
||||
updateAvailable:
|
||||
type: boolean
|
||||
commitsBehind:
|
||||
type: number
|
||||
restartRequired:
|
||||
type: boolean
|
||||
/status/appdata:
|
||||
get:
|
||||
summary: Get application data volume status
|
||||
@@ -2731,12 +2725,6 @@ paths:
|
||||
nullable: true
|
||||
enum: [debug, info, warn, error]
|
||||
default: debug
|
||||
- in: query
|
||||
name: search
|
||||
schema:
|
||||
type: string
|
||||
nullable: true
|
||||
example: plex
|
||||
responses:
|
||||
'200':
|
||||
description: Server log returned
|
||||
@@ -3406,8 +3394,8 @@ paths:
|
||||
name: guid
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
example: '9afef5a7-ec89-4d5f-9397-261e96970b50'
|
||||
type: number
|
||||
example: 1
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
@@ -3771,53 +3759,6 @@ paths:
|
||||
restricted:
|
||||
type: boolean
|
||||
example: false
|
||||
/user/{userId}/watchlist:
|
||||
get:
|
||||
summary: Get user by ID
|
||||
description: |
|
||||
Retrieves a user's Plex Watchlist in a JSON object.
|
||||
tags:
|
||||
- users
|
||||
parameters:
|
||||
- in: path
|
||||
name: userId
|
||||
required: true
|
||||
schema:
|
||||
type: number
|
||||
- in: query
|
||||
name: page
|
||||
schema:
|
||||
type: number
|
||||
example: 1
|
||||
default: 1
|
||||
responses:
|
||||
'200':
|
||||
description: Watchlist data returned
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
page:
|
||||
type: number
|
||||
totalPages:
|
||||
type: number
|
||||
totalResults:
|
||||
type: number
|
||||
results:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
tmdbId:
|
||||
type: number
|
||||
example: 1
|
||||
ratingKey:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
title:
|
||||
type: string
|
||||
/user/{userId}/settings/main:
|
||||
get:
|
||||
summary: Get general settings for a user
|
||||
@@ -4709,46 +4650,6 @@ paths:
|
||||
name:
|
||||
type: string
|
||||
example: Genre Name
|
||||
/discover/watchlist:
|
||||
get:
|
||||
summary: Get the Plex watchlist.
|
||||
tags:
|
||||
- search
|
||||
parameters:
|
||||
- in: query
|
||||
name: page
|
||||
schema:
|
||||
type: number
|
||||
example: 1
|
||||
default: 1
|
||||
responses:
|
||||
'200':
|
||||
description: Watchlist data returned
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
page:
|
||||
type: number
|
||||
totalPages:
|
||||
type: number
|
||||
totalResults:
|
||||
type: number
|
||||
results:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
tmdbId:
|
||||
type: number
|
||||
example: 1
|
||||
ratingKey:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
title:
|
||||
type: string
|
||||
/request:
|
||||
get:
|
||||
summary: Get all requests
|
||||
@@ -4776,16 +4677,7 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
nullable: true
|
||||
enum:
|
||||
[
|
||||
all,
|
||||
approved,
|
||||
available,
|
||||
pending,
|
||||
processing,
|
||||
unavailable,
|
||||
failed,
|
||||
]
|
||||
enum: [all, approved, available, pending, processing, unavailable]
|
||||
- in: query
|
||||
name: sort
|
||||
schema:
|
||||
@@ -5470,23 +5362,6 @@ paths:
|
||||
responses:
|
||||
'204':
|
||||
description: Succesfully removed media item
|
||||
/media/{mediaId}/file:
|
||||
delete:
|
||||
summary: Delete media file
|
||||
description: Removes a media file from radarr/sonarr. The `ADMIN` permission is required to perform this action.
|
||||
tags:
|
||||
- media
|
||||
parameters:
|
||||
- in: path
|
||||
name: mediaId
|
||||
description: Media ID
|
||||
required: true
|
||||
example: '1'
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'204':
|
||||
description: Succesfully removed media item
|
||||
/media/{mediaId}/{status}:
|
||||
post:
|
||||
summary: Update media status
|
||||
@@ -5705,7 +5580,7 @@ paths:
|
||||
$ref: '#/components/schemas/SonarrSeries'
|
||||
/regions:
|
||||
get:
|
||||
summary: Regions supported by TMDB
|
||||
summary: Regions supported by TMDb
|
||||
description: Returns a list of regions in a JSON object.
|
||||
tags:
|
||||
- tmdb
|
||||
@@ -5727,7 +5602,7 @@ paths:
|
||||
example: United States of America
|
||||
/languages:
|
||||
get:
|
||||
summary: Languages supported by TMDB
|
||||
summary: Languages supported by TMDb
|
||||
description: Returns a list of languages in a JSON object.
|
||||
tags:
|
||||
- tmdb
|
||||
@@ -5792,7 +5667,7 @@ paths:
|
||||
$ref: '#/components/schemas/ProductionCompany'
|
||||
/genres/movie:
|
||||
get:
|
||||
summary: Get list of official TMDB movie genres
|
||||
summary: Get list of official TMDb movie genres
|
||||
description: Returns a list of genres in a JSON array.
|
||||
tags:
|
||||
- tmdb
|
||||
@@ -5820,7 +5695,7 @@ paths:
|
||||
example: Family
|
||||
/genres/tv:
|
||||
get:
|
||||
summary: Get list of official TMDB movie genres
|
||||
summary: Get list of official TMDb movie genres
|
||||
description: Returns a list of genres in a JSON array.
|
||||
tags:
|
||||
- tmdb
|
||||
|
||||
283
package.json
283
package.json
@@ -1,27 +1,20 @@
|
||||
{
|
||||
"name": "jellyseerr",
|
||||
"version": "0.1.0",
|
||||
"version": "1.1.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "nodemon -e ts --watch server --watch overseerr-api.yml -e .json,.ts,.yml -x ts-node -r tsconfig-paths/register --files --project server/tsconfig.json server/index.ts",
|
||||
"build:server": "tsc --project server/tsconfig.json && copyfiles -u 2 server/templates/**/*.{html,pug} dist/templates && tsc-alias -p server/tsconfig.json",
|
||||
"dev": "nodemon -e ts --watch server --watch overseerr-api.yml -e .json,.ts,.yml -x ts-node --files --project server/tsconfig.json server/index.ts",
|
||||
"build:server": "tsc --project server/tsconfig.json && copyfiles -u 2 server/templates/**/*.{html,pug} dist/templates",
|
||||
"build:next": "next build",
|
||||
"build": "yarn build:next && yarn build:server",
|
||||
"lint": "eslint \"./server/**/*.{ts,tsx}\" \"./src/**/*.{ts,tsx}\" --cache",
|
||||
"lint": "eslint \"./server/**/*.{ts,tsx}\" \"./src/**/*.{ts,tsx}\"",
|
||||
"start": "NODE_ENV=production node dist/index.js",
|
||||
"i18n:extract": "extract-messages -l=en -o src/i18n/locale -d en --flat true --overwriteDefault true \"./src/**/!(*.test).{ts,tsx}\"",
|
||||
"migration:generate": "ts-node -r tsconfig-paths/register --project server/tsconfig.json ./node_modules/typeorm/cli.js migration:generate -d server/datasource.ts",
|
||||
"migration:create": "ts-node -r tsconfig-paths/register --project server/tsconfig.json ./node_modules/typeorm/cli.js migration:create -d server/datasource.ts",
|
||||
"migration:run": "ts-node -r tsconfig-paths/register --project server/tsconfig.json ./node_modules/typeorm/cli.js migration:run -d server/datasource.ts",
|
||||
"format": "prettier --loglevel warn --write --cache .",
|
||||
"format:check": "prettier --check --cache .",
|
||||
"typecheck": "yarn typecheck:server && yarn typecheck:client",
|
||||
"typecheck:server": "tsc --project server/tsconfig.json --noEmit",
|
||||
"typecheck:client": "tsc --noEmit",
|
||||
"prepare": "husky install",
|
||||
"cypress:open": "cypress open",
|
||||
"cypress:prepare": "ts-node -r tsconfig-paths/register --files --project server/tsconfig.json server/scripts/prepareTestDb.ts",
|
||||
"cypress:build": "yarn build && yarn cypress:prepare"
|
||||
"migration:generate": "ts-node --project server/tsconfig.json ./node_modules/typeorm/cli.js migration:generate",
|
||||
"migration:create": "ts-node --project server/tsconfig.json ./node_modules/typeorm/cli.js migration:create",
|
||||
"migration:run": "ts-node --project server/tsconfig.json ./node_modules/typeorm/cli.js migration:run",
|
||||
"format": "prettier --write .",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -29,145 +22,129 @@
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@formatjs/intl-displaynames": "6.0.3",
|
||||
"@formatjs/intl-locale": "3.0.3",
|
||||
"@formatjs/intl-pluralrules": "5.0.3",
|
||||
"@formatjs/intl-utils": "3.8.4",
|
||||
"@headlessui/react": "0.0.0-insiders.b301f04",
|
||||
"@heroicons/react": "1.0.6",
|
||||
"@supercharge/request-ip": "1.2.0",
|
||||
"@svgr/webpack": "6.3.1",
|
||||
"@tanem/react-nprogress": "5.0.11",
|
||||
"ace-builds": "1.9.6",
|
||||
"axios": "0.27.2",
|
||||
"axios-rate-limit": "1.3.0",
|
||||
"bcrypt": "5.0.1",
|
||||
"bowser": "2.11.0",
|
||||
"connect-typeorm": "1.1.4",
|
||||
"cookie-parser": "1.4.6",
|
||||
"copy-to-clipboard": "3.3.2",
|
||||
"country-flag-icons": "1.5.5",
|
||||
"cronstrue": "2.11.0",
|
||||
"csurf": "1.11.0",
|
||||
"date-fns": "2.29.1",
|
||||
"email-templates": "9.0.0",
|
||||
"email-validator": "2.0.4",
|
||||
"express": "4.18.1",
|
||||
"express-openapi-validator": "4.13.8",
|
||||
"express-rate-limit": "6.5.1",
|
||||
"express-session": "1.17.3",
|
||||
"formik": "2.2.9",
|
||||
"gravatar-url": "3.1.0",
|
||||
"intl": "1.2.5",
|
||||
"lodash": "4.17.21",
|
||||
"next": "12.2.5",
|
||||
"node-cache": "5.1.2",
|
||||
"node-gyp": "9.1.0",
|
||||
"node-schedule": "2.1.0",
|
||||
"nodemailer": "6.7.8",
|
||||
"openpgp": "5.4.0",
|
||||
"plex-api": "5.3.2",
|
||||
"pug": "3.0.2",
|
||||
"pulltorefreshjs": "0.1.22",
|
||||
"react": "18.2.0",
|
||||
"react-ace": "10.1.0",
|
||||
"react-animate-height": "2.1.2",
|
||||
"react-dom": "18.2.0",
|
||||
"react-intersection-observer": "9.4.0",
|
||||
"react-intl": "6.0.5",
|
||||
"react-markdown": "8.0.3",
|
||||
"react-popper-tooltip": "4.4.2",
|
||||
"react-select": "5.4.0",
|
||||
"react-spring": "9.5.2",
|
||||
"react-toast-notifications": "2.5.1",
|
||||
"react-truncate-markup": "5.1.2",
|
||||
"react-use-clipboard": "1.0.8",
|
||||
"reflect-metadata": "0.1.13",
|
||||
"secure-random-password": "0.2.3",
|
||||
"semver": "7.3.7",
|
||||
"sqlite3": "5.0.11",
|
||||
"swagger-ui-express": "4.5.0",
|
||||
"swr": "1.3.0",
|
||||
"typeorm": "0.3.7",
|
||||
"web-push": "3.5.0",
|
||||
"winston": "3.8.1",
|
||||
"winston-daily-rotate-file": "4.7.1",
|
||||
"xml2js": "0.4.23",
|
||||
"yamljs": "0.3.0",
|
||||
"yup": "0.32.11"
|
||||
"@headlessui/react": "^1.5.0",
|
||||
"@heroicons/react": "^1.0.6",
|
||||
"@supercharge/request-ip": "^1.2.0",
|
||||
"@svgr/webpack": "^6.2.1",
|
||||
"@tanem/react-nprogress": "^4.0.10",
|
||||
"ace-builds": "^1.4.14",
|
||||
"axios": "^0.26.1",
|
||||
"bcrypt": "^5.0.1",
|
||||
"bowser": "^2.11.0",
|
||||
"connect-typeorm": "^1.1.4",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"copy-to-clipboard": "^3.3.1",
|
||||
"country-flag-icons": "^1.4.21",
|
||||
"csurf": "^1.11.0",
|
||||
"email-templates": "^8.0.10",
|
||||
"email-validator": "^2.0.4",
|
||||
"express": "^4.17.3",
|
||||
"express-openapi-validator": "^4.13.6",
|
||||
"express-rate-limit": "^6.3.0",
|
||||
"express-session": "^1.17.2",
|
||||
"formik": "^2.2.9",
|
||||
"gravatar-url": "^3.1.0",
|
||||
"intl": "^1.2.5",
|
||||
"lodash": "^4.17.21",
|
||||
"next": "12.1.0",
|
||||
"node-cache": "^5.1.2",
|
||||
"node-gyp": "^9.0.0",
|
||||
"node-schedule": "^2.1.0",
|
||||
"nodemailer": "^6.7.2",
|
||||
"openpgp": "^5.2.0",
|
||||
"plex-api": "^5.3.2",
|
||||
"pug": "^3.0.2",
|
||||
"react": "17.0.2",
|
||||
"react-ace": "^9.5.0",
|
||||
"react-animate-height": "^2.0.23",
|
||||
"react-dom": "17.0.2",
|
||||
"react-intersection-observer": "^8.33.1",
|
||||
"react-intl": "5.24.7",
|
||||
"react-markdown": "^8.0.0",
|
||||
"react-select": "^5.2.2",
|
||||
"react-spring": "^9.4.4",
|
||||
"react-toast-notifications": "^2.5.1",
|
||||
"react-transition-group": "^4.4.2",
|
||||
"react-truncate-markup": "^5.1.0",
|
||||
"react-use-clipboard": "1.0.7",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"secure-random-password": "^0.2.3",
|
||||
"semver": "^7.3.5",
|
||||
"sqlite3": "^5.0.2",
|
||||
"swagger-ui-express": "^4.3.0",
|
||||
"swr": "^1.2.2",
|
||||
"typeorm": "0.2.45",
|
||||
"web-push": "^3.4.5",
|
||||
"winston": "^3.6.0",
|
||||
"winston-daily-rotate-file": "^4.6.1",
|
||||
"xml2js": "^0.4.23",
|
||||
"yamljs": "^0.3.0",
|
||||
"yup": "^0.32.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.18.10",
|
||||
"@commitlint/cli": "17.0.3",
|
||||
"@commitlint/config-conventional": "17.0.3",
|
||||
"@semantic-release/changelog": "6.0.1",
|
||||
"@semantic-release/commit-analyzer": "9.0.2",
|
||||
"@semantic-release/exec": "6.0.3",
|
||||
"@semantic-release/git": "10.0.1",
|
||||
"@tailwindcss/aspect-ratio": "0.4.0",
|
||||
"@tailwindcss/forms": "0.5.2",
|
||||
"@tailwindcss/typography": "0.5.4",
|
||||
"@types/bcrypt": "5.0.0",
|
||||
"@types/cookie-parser": "1.4.3",
|
||||
"@types/country-flag-icons": "1.2.0",
|
||||
"@types/csurf": "1.11.2",
|
||||
"@types/email-templates": "8.0.4",
|
||||
"@types/express": "4.17.13",
|
||||
"@types/express-session": "1.17.4",
|
||||
"@types/lodash": "4.14.183",
|
||||
"@types/node": "17.0.36",
|
||||
"@types/node-schedule": "2.1.0",
|
||||
"@types/nodemailer": "6.4.5",
|
||||
"@types/pulltorefreshjs": "0.1.5",
|
||||
"@types/react": "18.0.17",
|
||||
"@types/react-dom": "18.0.6",
|
||||
"@types/react-transition-group": "4.4.5",
|
||||
"@types/secure-random-password": "0.2.1",
|
||||
"@types/semver": "7.3.12",
|
||||
"@types/swagger-ui-express": "4.1.3",
|
||||
"@types/web-push": "3.3.2",
|
||||
"@types/xml2js": "0.4.11",
|
||||
"@types/yamljs": "0.2.31",
|
||||
"@types/yup": "0.29.14",
|
||||
"@typescript-eslint/eslint-plugin": "5.33.1",
|
||||
"@typescript-eslint/parser": "5.33.1",
|
||||
"autoprefixer": "10.4.8",
|
||||
"babel-plugin-react-intl": "8.2.25",
|
||||
"babel-plugin-react-intl-auto": "3.3.0",
|
||||
"commitizen": "4.2.5",
|
||||
"copyfiles": "2.4.1",
|
||||
"cy-mobile-commands": "0.3.0",
|
||||
"cypress": "10.6.0",
|
||||
"cz-conventional-changelog": "3.3.0",
|
||||
"eslint": "8.22.0",
|
||||
"eslint-config-next": "12.2.5",
|
||||
"eslint-config-prettier": "8.5.0",
|
||||
"eslint-plugin-formatjs": "4.1.0",
|
||||
"eslint-plugin-jsx-a11y": "6.6.1",
|
||||
"eslint-plugin-no-relative-import-paths": "1.4.0",
|
||||
"eslint-plugin-prettier": "4.2.1",
|
||||
"eslint-plugin-react": "7.30.1",
|
||||
"eslint-plugin-react-hooks": "4.6.0",
|
||||
"extract-react-intl-messages": "4.1.1",
|
||||
"husky": "8.0.1",
|
||||
"lint-staged": "12.4.3",
|
||||
"nodemon": "2.0.19",
|
||||
"postcss": "8.4.16",
|
||||
"prettier": "2.7.1",
|
||||
"prettier-plugin-organize-imports": "3.1.0",
|
||||
"prettier-plugin-tailwindcss": "0.1.13",
|
||||
"semantic-release": "19.0.3",
|
||||
"semantic-release-docker-buildx": "1.0.1",
|
||||
"tailwindcss": "3.1.8",
|
||||
"ts-node": "10.9.1",
|
||||
"tsc-alias": "1.7.0",
|
||||
"tsconfig-paths": "4.1.0",
|
||||
"typescript": "4.7.4"
|
||||
"@babel/cli": "^7.17.6",
|
||||
"@commitlint/cli": "^16.2.1",
|
||||
"@commitlint/config-conventional": "^16.2.1",
|
||||
"@next/eslint-plugin-next": "^12.1.6",
|
||||
"@semantic-release/changelog": "^6.0.1",
|
||||
"@semantic-release/commit-analyzer": "^9.0.2",
|
||||
"@semantic-release/exec": "^6.0.3",
|
||||
"@semantic-release/git": "^10.0.1",
|
||||
"@tailwindcss/aspect-ratio": "^0.4.0",
|
||||
"@tailwindcss/forms": "^0.5.0",
|
||||
"@tailwindcss/typography": "^0.5.2",
|
||||
"@types/bcrypt": "^5.0.0",
|
||||
"@types/cookie-parser": "^1.4.2",
|
||||
"@types/country-flag-icons": "^1.2.0",
|
||||
"@types/csurf": "^1.11.2",
|
||||
"@types/email-templates": "^8.0.4",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/express-session": "^1.17.4",
|
||||
"@types/lodash": "^4.14.179",
|
||||
"@types/node": "^17.0.21",
|
||||
"@types/node-schedule": "^1.3.2",
|
||||
"@types/nodemailer": "^6.4.4",
|
||||
"@types/react": "^17.0.40",
|
||||
"@types/react-dom": "^17.0.13",
|
||||
"@types/react-transition-group": "^4.4.4",
|
||||
"@types/secure-random-password": "^0.2.1",
|
||||
"@types/semver": "^7.3.9",
|
||||
"@types/swagger-ui-express": "^4.1.3",
|
||||
"@types/web-push": "^3.3.2",
|
||||
"@types/xml2js": "^0.4.9",
|
||||
"@types/yamljs": "^0.2.31",
|
||||
"@types/yup": "^0.29.13",
|
||||
"@typescript-eslint/eslint-plugin": "^5.14.0",
|
||||
"@typescript-eslint/parser": "^5.14.0",
|
||||
"autoprefixer": "^10.4.2",
|
||||
"babel-plugin-react-intl": "^8.2.25",
|
||||
"babel-plugin-react-intl-auto": "^3.3.0",
|
||||
"commitizen": "^4.2.4",
|
||||
"copyfiles": "^2.4.1",
|
||||
"cz-conventional-changelog": "^3.3.0",
|
||||
"eslint": "^8.11.0",
|
||||
"eslint-config-next": "^12.1.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-formatjs": "^3.0.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-react": "^7.29.3",
|
||||
"eslint-plugin-react-hooks": "^4.3.0",
|
||||
"extract-react-intl-messages": "^4.1.1",
|
||||
"husky": "^7.0.4",
|
||||
"lint-staged": "^12.3.5",
|
||||
"nodemon": "^2.0.15",
|
||||
"postcss": "^8.4.8",
|
||||
"prettier": "^2.5.1",
|
||||
"prettier-plugin-tailwindcss": "^0.1.8",
|
||||
"semantic-release": "^19.0.2",
|
||||
"semantic-release-docker-buildx": "^1.0.1",
|
||||
"tailwindcss": "^3.0.23",
|
||||
"ts-node": "^10.7.0",
|
||||
"typescript": "^4.6.2"
|
||||
},
|
||||
"resolutions": {
|
||||
"sqlite3/node-gyp": "8.4.1",
|
||||
"@types/react": "18.0.17",
|
||||
"@types/react-dom": "18.0.6"
|
||||
"sqlite3/node-gyp": "^8.4.1"
|
||||
},
|
||||
"config": {
|
||||
"commitizen": {
|
||||
@@ -188,6 +165,10 @@
|
||||
"@commitlint/config-conventional"
|
||||
]
|
||||
},
|
||||
"prettier": {
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5"
|
||||
},
|
||||
"release": {
|
||||
"plugins": [
|
||||
"@semantic-release/commit-analyzer",
|
||||
|
||||
3
public/preview.jpg:Zone.Identifier
Normal file
3
public/preview.jpg:Zone.Identifier
Normal file
@@ -0,0 +1,3 @@
|
||||
[ZoneTransfer]
|
||||
LastWriterPackageFamilyName=Microsoft.ScreenSketch_8wekyb3d8bbwe
|
||||
ZoneId=3
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:js-app",
|
||||
"group:allNonMajor",
|
||||
"docker:disableMajor",
|
||||
"helpers:disableTypesNodeMajor"
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"matchManagers": ["github-actions"],
|
||||
"groupName": "GitHub Actions",
|
||||
"groupSlug": "github-actions"
|
||||
},
|
||||
{
|
||||
"matchPackageNames": ["node"],
|
||||
"groupName": "Node.js",
|
||||
"groupSlug": "node"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import logger from '@server/logger';
|
||||
import axios from 'axios';
|
||||
import xml2js from 'xml2js';
|
||||
import fs, { promises as fsp } from 'fs';
|
||||
import path from 'path';
|
||||
import xml2js from 'xml2js';
|
||||
import logger from '../logger';
|
||||
|
||||
const UPDATE_INTERVAL_MSEC = 24 * 3600 * 1000; // how often to download new mapping in milliseconds
|
||||
// originally at https://raw.githubusercontent.com/ScudLee/anime-lists/master/anime-list.xml
|
||||
@@ -14,7 +14,7 @@ const LOCAL_PATH = process.env.CONFIG_DIRECTORY
|
||||
|
||||
const mappingRegexp = new RegExp(/;[0-9]+-([0-9]+)/g);
|
||||
|
||||
// Anime-List xml files are community maintained mappings that Hama agent uses to map AniDB IDs to TVDB/TMDB IDs
|
||||
// Anime-List xml files are community maintained mappings that Hama agent uses to map AniDB IDs to TVDB/TMDb IDs
|
||||
// https://github.com/Anime-Lists/anime-lists/
|
||||
|
||||
interface AnimeMapping {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import type { AxiosInstance, AxiosRequestConfig } from 'axios';
|
||||
import axios from 'axios';
|
||||
import rateLimit from 'axios-rate-limit';
|
||||
import type NodeCache from 'node-cache';
|
||||
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
|
||||
import NodeCache from 'node-cache';
|
||||
|
||||
// 5 minute default TTL (in seconds)
|
||||
const DEFAULT_TTL = 300;
|
||||
@@ -12,10 +10,6 @@ const DEFAULT_ROLLING_BUFFER = 10000;
|
||||
interface ExternalAPIOptions {
|
||||
nodeCache?: NodeCache;
|
||||
headers?: Record<string, unknown>;
|
||||
rateLimit?: {
|
||||
maxRPS: number;
|
||||
maxRequests: number;
|
||||
};
|
||||
}
|
||||
|
||||
class ExternalAPI {
|
||||
@@ -37,14 +31,6 @@ class ExternalAPI {
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (options.rateLimit) {
|
||||
this.axios = rateLimit(this.axios, {
|
||||
maxRequests: options.rateLimit.maxRequests,
|
||||
maxRPS: options.rateLimit.maxRPS,
|
||||
});
|
||||
}
|
||||
|
||||
this.baseUrl = baseUrl;
|
||||
this.cache = options.nodeCache;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import cacheManager from '@server/lib/cache';
|
||||
import logger from '@server/logger';
|
||||
import cacheManager from '../lib/cache';
|
||||
import logger from '../logger';
|
||||
import ExternalAPI from './externalapi';
|
||||
|
||||
interface GitHubRelease {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import logger from '@server/logger';
|
||||
import type { AxiosInstance } from 'axios';
|
||||
import axios from 'axios';
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import logger from '../logger';
|
||||
|
||||
export interface JellyfinUserResponse {
|
||||
Name: string;
|
||||
@@ -17,7 +16,7 @@ export interface JellyfinLoginResponse {
|
||||
}
|
||||
|
||||
export interface JellyfinUserListResponse {
|
||||
users: JellyfinUserResponse[];
|
||||
users: Array<JellyfinUserResponse>;
|
||||
}
|
||||
|
||||
export interface JellyfinLibrary {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { Library, PlexSettings } from '@server/lib/settings';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import NodePlexAPI from 'plex-api';
|
||||
import { getSettings, Library, PlexSettings } from '../lib/settings';
|
||||
import logger from '../logger';
|
||||
|
||||
export interface PlexLibraryItem {
|
||||
ratingKey: string;
|
||||
@@ -131,6 +130,7 @@ class PlexAPI {
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
public async getStatus() {
|
||||
return await this.plexClient.query('/');
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import type { PlexDevice } from '@server/interfaces/api/plexInterfaces';
|
||||
import cacheManager from '@server/lib/cache';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import xml2js from 'xml2js';
|
||||
import ExternalAPI from './externalapi';
|
||||
import { PlexDevice } from '../interfaces/api/plexInterfaces';
|
||||
import { getSettings } from '../lib/settings';
|
||||
import logger from '../logger';
|
||||
|
||||
interface PlexAccountResponse {
|
||||
user: PlexUser;
|
||||
@@ -112,54 +111,20 @@ interface UsersResponse {
|
||||
};
|
||||
}
|
||||
|
||||
interface WatchlistResponse {
|
||||
MediaContainer: {
|
||||
totalSize: number;
|
||||
Metadata?: {
|
||||
ratingKey: string;
|
||||
}[];
|
||||
};
|
||||
}
|
||||
|
||||
interface MetadataResponse {
|
||||
MediaContainer: {
|
||||
Metadata: {
|
||||
ratingKey: string;
|
||||
type: 'movie' | 'show';
|
||||
title: string;
|
||||
Guid: {
|
||||
id: `imdb://tt${number}` | `tmdb://${number}` | `tvdb://${number}`;
|
||||
}[];
|
||||
}[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface PlexWatchlistItem {
|
||||
ratingKey: string;
|
||||
tmdbId: number;
|
||||
tvdbId?: number;
|
||||
type: 'movie' | 'show';
|
||||
title: string;
|
||||
}
|
||||
|
||||
class PlexTvAPI extends ExternalAPI {
|
||||
class PlexTvAPI {
|
||||
private authToken: string;
|
||||
private axios: AxiosInstance;
|
||||
|
||||
constructor(authToken: string) {
|
||||
super(
|
||||
'https://plex.tv',
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
'X-Plex-Token': authToken,
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
nodeCache: cacheManager.getCache('plextv').data,
|
||||
}
|
||||
);
|
||||
|
||||
this.authToken = authToken;
|
||||
this.axios = axios.create({
|
||||
baseURL: 'https://plex.tv',
|
||||
headers: {
|
||||
'X-Plex-Token': this.authToken,
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public async getDevices(): Promise<PlexDevice[]> {
|
||||
@@ -287,83 +252,6 @@ class PlexTvAPI extends ExternalAPI {
|
||||
)) as UsersResponse;
|
||||
return parsedXml;
|
||||
}
|
||||
|
||||
public async getWatchlist({
|
||||
offset = 0,
|
||||
size = 20,
|
||||
}: { offset?: number; size?: number } = {}): Promise<{
|
||||
offset: number;
|
||||
size: number;
|
||||
totalSize: number;
|
||||
items: PlexWatchlistItem[];
|
||||
}> {
|
||||
try {
|
||||
const response = await this.axios.get<WatchlistResponse>(
|
||||
'/library/sections/watchlist/all',
|
||||
{
|
||||
params: {
|
||||
'X-Plex-Container-Start': offset,
|
||||
'X-Plex-Container-Size': size,
|
||||
},
|
||||
baseURL: 'https://metadata.provider.plex.tv',
|
||||
}
|
||||
);
|
||||
|
||||
const watchlistDetails = await Promise.all(
|
||||
(response.data.MediaContainer.Metadata ?? []).map(
|
||||
async (watchlistItem) => {
|
||||
const detailedResponse = await this.getRolling<MetadataResponse>(
|
||||
`/library/metadata/${watchlistItem.ratingKey}`,
|
||||
{
|
||||
baseURL: 'https://metadata.provider.plex.tv',
|
||||
}
|
||||
);
|
||||
|
||||
const metadata = detailedResponse.MediaContainer.Metadata[0];
|
||||
|
||||
const tmdbString = metadata.Guid.find((guid) =>
|
||||
guid.id.startsWith('tmdb')
|
||||
);
|
||||
const tvdbString = metadata.Guid.find((guid) =>
|
||||
guid.id.startsWith('tvdb')
|
||||
);
|
||||
|
||||
return {
|
||||
ratingKey: metadata.ratingKey,
|
||||
// This should always be set? But I guess it also cannot be?
|
||||
// We will filter out the 0's afterwards
|
||||
tmdbId: tmdbString ? Number(tmdbString.id.split('//')[1]) : 0,
|
||||
tvdbId: tvdbString
|
||||
? Number(tvdbString.id.split('//')[1])
|
||||
: undefined,
|
||||
title: metadata.title,
|
||||
type: metadata.type,
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const filteredList = watchlistDetails.filter((detail) => detail.tmdbId);
|
||||
|
||||
return {
|
||||
offset,
|
||||
size,
|
||||
totalSize: response.data.MediaContainer.totalSize,
|
||||
items: filteredList,
|
||||
};
|
||||
} catch (e) {
|
||||
logger.error('Failed to retrieve watchlist items', {
|
||||
label: 'Plex.TV Metadata API',
|
||||
errorMessage: e.message,
|
||||
});
|
||||
return {
|
||||
offset,
|
||||
size,
|
||||
totalSize: 0,
|
||||
items: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default PlexTvAPI;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import cacheManager from '@server/lib/cache';
|
||||
import cacheManager from '../lib/cache';
|
||||
import ExternalAPI from './externalapi';
|
||||
|
||||
interface RTSearchResult {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import ExternalAPI from '@server/api/externalapi';
|
||||
import type { AvailableCacheIds } from '@server/lib/cache';
|
||||
import cacheManager from '@server/lib/cache';
|
||||
import type { DVRSettings } from '@server/lib/settings';
|
||||
import cacheManager, { AvailableCacheIds } from '../../lib/cache';
|
||||
import { DVRSettings } from '../../lib/settings';
|
||||
import ExternalAPI from '../externalapi';
|
||||
|
||||
export interface SystemStatus {
|
||||
version: string;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import logger from '@server/logger';
|
||||
import logger from '../../logger';
|
||||
import ServarrBase from './base';
|
||||
|
||||
export interface RadarrMovieOptions {
|
||||
@@ -69,7 +69,7 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
|
||||
|
||||
return response.data[0];
|
||||
} catch (e) {
|
||||
logger.error('Error retrieving movie by TMDB ID', {
|
||||
logger.error('Error retrieving movie by TMDb ID', {
|
||||
label: 'Radarr API',
|
||||
errorMessage: e.message,
|
||||
tmdbId: id,
|
||||
@@ -213,20 +213,6 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
|
||||
);
|
||||
}
|
||||
}
|
||||
public removeMovie = async (movieId: number): Promise<void> => {
|
||||
try {
|
||||
const { id, title } = await this.getMovieByTmdbId(movieId);
|
||||
await this.axios.delete(`/movie/${id}`, {
|
||||
params: {
|
||||
deleteFiles: true,
|
||||
addImportExclusion: false,
|
||||
},
|
||||
});
|
||||
logger.info(`[Radarr] Removed movie ${title}`);
|
||||
} catch (e) {
|
||||
throw new Error(`[Radarr] Failed to remove movie: ${e.message}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default RadarrAPI;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import logger from '@server/logger';
|
||||
import logger from '../../logger';
|
||||
import ServarrBase from './base';
|
||||
|
||||
interface SonarrSeason {
|
||||
@@ -302,20 +302,6 @@ class SonarrAPI extends ServarrBase<{ seriesId: number; episodeId: number }> {
|
||||
|
||||
return newSeasons;
|
||||
}
|
||||
public removeSerie = async (serieId: number): Promise<void> => {
|
||||
try {
|
||||
const { id, title } = await this.getSeriesByTvdbId(serieId);
|
||||
await this.axios.delete(`/series/${id}`, {
|
||||
params: {
|
||||
deleteFiles: true,
|
||||
addImportExclusion: false,
|
||||
},
|
||||
});
|
||||
logger.info(`[Radarr] Removed serie ${title}`);
|
||||
} catch (e) {
|
||||
throw new Error(`[Radarr] Failed to remove serie: ${e.message}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default SonarrAPI;
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import type { User } from '@server/entity/User';
|
||||
import type { TautulliSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import type { AxiosInstance } from 'axios';
|
||||
import axios from 'axios';
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import { uniqWith } from 'lodash';
|
||||
import { User } from '../entity/User';
|
||||
import { TautulliSettings } from '../lib/settings';
|
||||
import logger from '../logger';
|
||||
|
||||
export interface TautulliHistoryRecord {
|
||||
date: number;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import ExternalAPI from '@server/api/externalapi';
|
||||
import cacheManager from '@server/lib/cache';
|
||||
import { sortBy } from 'lodash';
|
||||
import type {
|
||||
import cacheManager from '../../lib/cache';
|
||||
import ExternalAPI from '../externalapi';
|
||||
import {
|
||||
TmdbCollection,
|
||||
TmdbExternalIdResponse,
|
||||
TmdbGenre,
|
||||
@@ -92,10 +92,6 @@ class TheMovieDb extends ExternalAPI {
|
||||
},
|
||||
{
|
||||
nodeCache: cacheManager.getCache('tmdb').data,
|
||||
rateLimit: {
|
||||
maxRequests: 20,
|
||||
maxRPS: 50,
|
||||
},
|
||||
}
|
||||
);
|
||||
this.region = region;
|
||||
@@ -196,7 +192,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch person details: ${e.message}`);
|
||||
throw new Error(`[TMDb] Failed to fetch person details: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -218,7 +214,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`[TMDB] Failed to fetch person combined credits: ${e.message}`
|
||||
`[TMDb] Failed to fetch person combined credits: ${e.message}`
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -245,7 +241,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch movie details: ${e.message}`);
|
||||
throw new Error(`[TMDb] Failed to fetch movie details: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -271,7 +267,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch TV show details: ${e.message}`);
|
||||
throw new Error(`[TMDb] Failed to fetch TV show details: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -297,7 +293,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch TV show details: ${e.message}`);
|
||||
throw new Error(`[TMDb] Failed to fetch TV show details: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -323,7 +319,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch discover movies: ${e.message}`);
|
||||
throw new Error(`[TMDb] Failed to fetch discover movies: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,7 +345,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch discover movies: ${e.message}`);
|
||||
throw new Error(`[TMDb] Failed to fetch discover movies: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,7 +371,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch movies by keyword: ${e.message}`);
|
||||
throw new Error(`[TMDb] Failed to fetch movies by keyword: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -402,7 +398,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`[TMDB] Failed to fetch TV recommendations: ${e.message}`
|
||||
`[TMDb] Failed to fetch TV recommendations: ${e.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -426,7 +422,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch TV similar: ${e.message}`);
|
||||
throw new Error(`[TMDb] Failed to fetch TV similar: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -459,7 +455,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch discover movies: ${e.message}`);
|
||||
throw new Error(`[TMDb] Failed to fetch discover movies: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -492,7 +488,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch discover TV: ${e.message}`);
|
||||
throw new Error(`[TMDb] Failed to fetch discover TV: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -518,7 +514,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch upcoming movies: ${e.message}`);
|
||||
throw new Error(`[TMDb] Failed to fetch upcoming movies: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -545,7 +541,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch all trending: ${e.message}`);
|
||||
throw new Error(`[TMDb] Failed to fetch all trending: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -568,7 +564,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch all trending: ${e.message}`);
|
||||
throw new Error(`[TMDb] Failed to fetch all trending: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -591,7 +587,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch all trending: ${e.message}`);
|
||||
throw new Error(`[TMDb] Failed to fetch all trending: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -623,7 +619,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to find by external ID: ${e.message}`);
|
||||
throw new Error(`[TMDb] Failed to find by external ID: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -661,7 +657,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
throw new Error(`No movie or show returned from API for ID ${imdbId}`);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`[TMDB] Failed to find media using external IMDb ID: ${e.message}`
|
||||
`[TMDb] Failed to find media using external IMDb ID: ${e.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -691,7 +687,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
throw new Error(`No show returned from API for ID ${tvdbId}`);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`[TMDB] Failed to get TV show using the external TVDB ID: ${e.message}`
|
||||
`[TMDb] Failed to get TV show using the external TVDB ID: ${e.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -715,7 +711,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch collection: ${e.message}`);
|
||||
throw new Error(`[TMDb] Failed to fetch collection: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -731,7 +727,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
|
||||
return regions;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch countries: ${e.message}`);
|
||||
throw new Error(`[TMDb] Failed to fetch countries: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -747,7 +743,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
|
||||
return languages;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch langauges: ${e.message}`);
|
||||
throw new Error(`[TMDb] Failed to fetch langauges: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -759,7 +755,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch movie studio: ${e.message}`);
|
||||
throw new Error(`[TMDb] Failed to fetch movie studio: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -769,7 +765,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch TV network: ${e.message}`);
|
||||
throw new Error(`[TMDb] Failed to fetch TV network: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -820,7 +816,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
|
||||
return movieGenres;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch movie genres: ${e.message}`);
|
||||
throw new Error(`[TMDb] Failed to fetch movie genres: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -871,7 +867,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
|
||||
return tvGenres;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch TV genres: ${e.message}`);
|
||||
throw new Error(`[TMDb] Failed to fetch TV genres: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ export enum MediaRequestStatus {
|
||||
PENDING = 1,
|
||||
APPROVED,
|
||||
DECLINED,
|
||||
FAILED,
|
||||
}
|
||||
|
||||
export enum MediaType {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import type { IssueType } from '@server/constants/issue';
|
||||
import { IssueStatus } from '@server/constants/issue';
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
@@ -9,6 +7,7 @@ import {
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { IssueStatus, IssueType } from '../constants/issue';
|
||||
import IssueComment from './IssueComment';
|
||||
import Media from './Media';
|
||||
import { User } from './User';
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
import RadarrAPI from '@server/api/servarr/radarr';
|
||||
import SonarrAPI from '@server/api/servarr/sonarr';
|
||||
import { MediaStatus, MediaType } from '@server/constants/media';
|
||||
import { MediaServerType } from '@server/constants/server';
|
||||
import { getRepository } from '@server/datasource';
|
||||
import type { DownloadingItem } from '@server/lib/downloadtracker';
|
||||
import downloadTracker from '@server/lib/downloadtracker';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import {
|
||||
AfterLoad,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
getRepository,
|
||||
In,
|
||||
Index,
|
||||
OneToMany,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import RadarrAPI from '../api/servarr/radarr';
|
||||
import SonarrAPI from '../api/servarr/sonarr';
|
||||
import { MediaStatus, MediaType } from '../constants/media';
|
||||
import { MediaServerType } from '../constants/server';
|
||||
import downloadTracker, { DownloadingItem } from '../lib/downloadtracker';
|
||||
import { getSettings } from '../lib/settings';
|
||||
import logger from '../logger';
|
||||
import Issue from './Issue';
|
||||
import { MediaRequest } from './MediaRequest';
|
||||
import Season from './Season';
|
||||
@@ -38,7 +37,7 @@ class Media {
|
||||
}
|
||||
|
||||
const media = await mediaRepository.find({
|
||||
where: { tmdbId: In(finalIds) },
|
||||
tmdbId: In(finalIds),
|
||||
});
|
||||
|
||||
return media;
|
||||
@@ -57,10 +56,10 @@ class Media {
|
||||
try {
|
||||
const media = await mediaRepository.findOne({
|
||||
where: { tmdbId: id, mediaType },
|
||||
relations: { requests: true, issues: true },
|
||||
relations: ['requests', 'issues'],
|
||||
});
|
||||
|
||||
return media ?? undefined;
|
||||
return media;
|
||||
} catch (e) {
|
||||
logger.error(e.message);
|
||||
return undefined;
|
||||
@@ -153,9 +152,6 @@ class Media {
|
||||
public mediaUrl?: string;
|
||||
public mediaUrl4k?: string;
|
||||
|
||||
public iOSPlexUrl?: string;
|
||||
public iOSPlexUrl4k?: string;
|
||||
|
||||
public tautulliUrl?: string;
|
||||
public tautulliUrl4k?: string;
|
||||
|
||||
@@ -176,24 +172,20 @@ class Media {
|
||||
this.ratingKey
|
||||
}`;
|
||||
|
||||
this.iOSPlexUrl = `plex://preplay/?metadataKey=%2Flibrary%2Fmetadata%2F${this.ratingKey}&server=${machineId}`;
|
||||
|
||||
if (tautulliUrl) {
|
||||
this.tautulliUrl = `${tautulliUrl}/info?rating_key=${this.ratingKey}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.ratingKey4k) {
|
||||
this.mediaUrl4k = `${
|
||||
webAppUrl ? webAppUrl : 'https://app.plex.tv/desktop'
|
||||
}#!/server/${machineId}/details?key=%2Flibrary%2Fmetadata%2F${
|
||||
this.ratingKey4k
|
||||
}`;
|
||||
if (this.ratingKey4k) {
|
||||
this.mediaUrl4k = `${
|
||||
webAppUrl ? webAppUrl : 'https://app.plex.tv/desktop'
|
||||
}#!/server/${machineId}/details?key=%2Flibrary%2Fmetadata%2F${
|
||||
this.ratingKey4k
|
||||
}`;
|
||||
|
||||
this.iOSPlexUrl4k = `plex://preplay/?metadataKey=%2Flibrary%2Fmetadata%2F${this.ratingKey4k}&server=${machineId}`;
|
||||
|
||||
if (tautulliUrl) {
|
||||
this.tautulliUrl4k = `${tautulliUrl}/info?rating_key=${this.ratingKey4k}`;
|
||||
}
|
||||
if (tautulliUrl) {
|
||||
this.tautulliUrl4k = `${tautulliUrl}/info?rating_key=${this.ratingKey4k}`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -1,23 +1,3 @@
|
||||
import type { RadarrMovieOptions } from '@server/api/servarr/radarr';
|
||||
import RadarrAPI from '@server/api/servarr/radarr';
|
||||
import type {
|
||||
AddSeriesOptions,
|
||||
SonarrSeries,
|
||||
} from '@server/api/servarr/sonarr';
|
||||
import SonarrAPI from '@server/api/servarr/sonarr';
|
||||
import TheMovieDb from '@server/api/themoviedb';
|
||||
import { ANIME_KEYWORD_ID } from '@server/api/themoviedb/constants';
|
||||
import {
|
||||
MediaRequestStatus,
|
||||
MediaStatus,
|
||||
MediaType,
|
||||
} from '@server/constants/media';
|
||||
import { getRepository } from '@server/datasource';
|
||||
import type { MediaRequestBody } from '@server/interfaces/api/requestInterfaces';
|
||||
import notificationManager, { Notification } from '@server/lib/notifications';
|
||||
import { Permission } from '@server/lib/permissions';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import { isEqual, truncate } from 'lodash';
|
||||
import {
|
||||
AfterInsert,
|
||||
@@ -26,347 +6,30 @@ import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
getRepository,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
PrimaryGeneratedColumn,
|
||||
RelationCount,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import RadarrAPI, { RadarrMovieOptions } from '../api/servarr/radarr';
|
||||
import SonarrAPI, {
|
||||
AddSeriesOptions,
|
||||
SonarrSeries,
|
||||
} from '../api/servarr/sonarr';
|
||||
import TheMovieDb from '../api/themoviedb';
|
||||
import { ANIME_KEYWORD_ID } from '../api/themoviedb/constants';
|
||||
import { MediaRequestStatus, MediaStatus, MediaType } from '../constants/media';
|
||||
import notificationManager, { Notification } from '../lib/notifications';
|
||||
import { getSettings } from '../lib/settings';
|
||||
import logger from '../logger';
|
||||
import Media from './Media';
|
||||
import SeasonRequest from './SeasonRequest';
|
||||
import { User } from './User';
|
||||
|
||||
export class RequestPermissionError extends Error {}
|
||||
export class QuotaRestrictedError extends Error {}
|
||||
export class DuplicateMediaRequestError extends Error {}
|
||||
export class NoSeasonsAvailableError extends Error {}
|
||||
|
||||
type MediaRequestOptions = {
|
||||
isAutoRequest?: boolean;
|
||||
};
|
||||
|
||||
@Entity()
|
||||
export class MediaRequest {
|
||||
public static async request(
|
||||
requestBody: MediaRequestBody,
|
||||
user: User,
|
||||
options: MediaRequestOptions = {}
|
||||
): Promise<MediaRequest> {
|
||||
const tmdb = new TheMovieDb();
|
||||
const mediaRepository = getRepository(Media);
|
||||
const requestRepository = getRepository(MediaRequest);
|
||||
const userRepository = getRepository(User);
|
||||
|
||||
let requestUser = user;
|
||||
|
||||
if (
|
||||
requestBody.userId &&
|
||||
!requestUser.hasPermission([
|
||||
Permission.MANAGE_USERS,
|
||||
Permission.MANAGE_REQUESTS,
|
||||
])
|
||||
) {
|
||||
throw new RequestPermissionError(
|
||||
'You do not have permission to modify the request user.'
|
||||
);
|
||||
} else if (requestBody.userId) {
|
||||
requestUser = await userRepository.findOneOrFail({
|
||||
where: { id: requestBody.userId },
|
||||
});
|
||||
}
|
||||
|
||||
if (!requestUser) {
|
||||
throw new Error('User missing from request context.');
|
||||
}
|
||||
|
||||
if (
|
||||
requestBody.mediaType === MediaType.MOVIE &&
|
||||
!requestUser.hasPermission(
|
||||
requestBody.is4k
|
||||
? [Permission.REQUEST_4K, Permission.REQUEST_4K_MOVIE]
|
||||
: [Permission.REQUEST, Permission.REQUEST_MOVIE],
|
||||
{
|
||||
type: 'or',
|
||||
}
|
||||
)
|
||||
) {
|
||||
throw new RequestPermissionError(
|
||||
`You do not have permission to make ${
|
||||
requestBody.is4k ? '4K ' : ''
|
||||
}movie requests.`
|
||||
);
|
||||
} else if (
|
||||
requestBody.mediaType === MediaType.TV &&
|
||||
!requestUser.hasPermission(
|
||||
requestBody.is4k
|
||||
? [Permission.REQUEST_4K, Permission.REQUEST_4K_TV]
|
||||
: [Permission.REQUEST, Permission.REQUEST_TV],
|
||||
{
|
||||
type: 'or',
|
||||
}
|
||||
)
|
||||
) {
|
||||
throw new RequestPermissionError(
|
||||
`You do not have permission to make ${
|
||||
requestBody.is4k ? '4K ' : ''
|
||||
}series requests.`
|
||||
);
|
||||
}
|
||||
|
||||
const quotas = await requestUser.getQuota();
|
||||
|
||||
if (requestBody.mediaType === MediaType.MOVIE && quotas.movie.restricted) {
|
||||
throw new QuotaRestrictedError('Movie Quota exceeded.');
|
||||
} else if (requestBody.mediaType === MediaType.TV && quotas.tv.restricted) {
|
||||
throw new QuotaRestrictedError('Series Quota exceeded.');
|
||||
}
|
||||
|
||||
const tmdbMedia =
|
||||
requestBody.mediaType === MediaType.MOVIE
|
||||
? await tmdb.getMovie({ movieId: requestBody.mediaId })
|
||||
: await tmdb.getTvShow({ tvId: requestBody.mediaId });
|
||||
|
||||
let media = await mediaRepository.findOne({
|
||||
where: {
|
||||
tmdbId: requestBody.mediaId,
|
||||
mediaType: requestBody.mediaType,
|
||||
},
|
||||
relations: ['requests'],
|
||||
});
|
||||
|
||||
if (!media) {
|
||||
media = new Media({
|
||||
tmdbId: tmdbMedia.id,
|
||||
tvdbId: requestBody.tvdbId ?? tmdbMedia.external_ids.tvdb_id,
|
||||
status: !requestBody.is4k ? MediaStatus.PENDING : MediaStatus.UNKNOWN,
|
||||
status4k: requestBody.is4k ? MediaStatus.PENDING : MediaStatus.UNKNOWN,
|
||||
mediaType: requestBody.mediaType,
|
||||
});
|
||||
} else {
|
||||
if (media.status === MediaStatus.UNKNOWN && !requestBody.is4k) {
|
||||
media.status = MediaStatus.PENDING;
|
||||
}
|
||||
|
||||
if (media.status4k === MediaStatus.UNKNOWN && requestBody.is4k) {
|
||||
media.status4k = MediaStatus.PENDING;
|
||||
}
|
||||
}
|
||||
|
||||
const existing = await requestRepository
|
||||
.createQueryBuilder('request')
|
||||
.leftJoin('request.media', 'media')
|
||||
.leftJoinAndSelect('request.requestedBy', 'user')
|
||||
.where('request.is4k = :is4k', { is4k: requestBody.is4k })
|
||||
.andWhere('media.tmdbId = :tmdbId', { tmdbId: tmdbMedia.id })
|
||||
.andWhere('media.mediaType = :mediaType', {
|
||||
mediaType: requestBody.mediaType,
|
||||
})
|
||||
.getMany();
|
||||
|
||||
if (existing && existing.length > 0) {
|
||||
// If there is an existing movie request that isn't declined, don't allow a new one.
|
||||
if (
|
||||
requestBody.mediaType === MediaType.MOVIE &&
|
||||
existing[0].status !== MediaRequestStatus.DECLINED
|
||||
) {
|
||||
logger.warn('Duplicate request for media blocked', {
|
||||
tmdbId: tmdbMedia.id,
|
||||
mediaType: requestBody.mediaType,
|
||||
is4k: requestBody.is4k,
|
||||
label: 'Media Request',
|
||||
});
|
||||
|
||||
throw new DuplicateMediaRequestError(
|
||||
'Request for this media already exists.'
|
||||
);
|
||||
}
|
||||
|
||||
// If an existing auto-request for this media exists from the same user,
|
||||
// don't allow a new one.
|
||||
if (
|
||||
existing.find(
|
||||
(r) => r.requestedBy.id === requestUser.id && r.isAutoRequest
|
||||
)
|
||||
) {
|
||||
throw new DuplicateMediaRequestError(
|
||||
'Auto-request for this media and user already exists.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (requestBody.mediaType === MediaType.MOVIE) {
|
||||
await mediaRepository.save(media);
|
||||
|
||||
const request = new MediaRequest({
|
||||
type: MediaType.MOVIE,
|
||||
media,
|
||||
requestedBy: requestUser,
|
||||
// If the user is an admin or has the "auto approve" permission, automatically approve the request
|
||||
status: user.hasPermission(
|
||||
[
|
||||
requestBody.is4k
|
||||
? Permission.AUTO_APPROVE_4K
|
||||
: Permission.AUTO_APPROVE,
|
||||
requestBody.is4k
|
||||
? Permission.AUTO_APPROVE_4K_MOVIE
|
||||
: Permission.AUTO_APPROVE_MOVIE,
|
||||
Permission.MANAGE_REQUESTS,
|
||||
],
|
||||
{ type: 'or' }
|
||||
)
|
||||
? MediaRequestStatus.APPROVED
|
||||
: MediaRequestStatus.PENDING,
|
||||
modifiedBy: user.hasPermission(
|
||||
[
|
||||
requestBody.is4k
|
||||
? Permission.AUTO_APPROVE_4K
|
||||
: Permission.AUTO_APPROVE,
|
||||
requestBody.is4k
|
||||
? Permission.AUTO_APPROVE_4K_MOVIE
|
||||
: Permission.AUTO_APPROVE_MOVIE,
|
||||
Permission.MANAGE_REQUESTS,
|
||||
],
|
||||
{ type: 'or' }
|
||||
)
|
||||
? user
|
||||
: undefined,
|
||||
is4k: requestBody.is4k,
|
||||
serverId: requestBody.serverId,
|
||||
profileId: requestBody.profileId,
|
||||
rootFolder: requestBody.rootFolder,
|
||||
tags: requestBody.tags,
|
||||
isAutoRequest: options.isAutoRequest ?? false,
|
||||
});
|
||||
|
||||
await requestRepository.save(request);
|
||||
return request;
|
||||
} else {
|
||||
const tmdbMediaShow = tmdbMedia as Awaited<
|
||||
ReturnType<typeof tmdb.getTvShow>
|
||||
>;
|
||||
const requestedSeasons =
|
||||
requestBody.seasons === 'all'
|
||||
? tmdbMediaShow.seasons
|
||||
.map((season) => season.season_number)
|
||||
.filter((sn) => sn > 0)
|
||||
: (requestBody.seasons as number[]);
|
||||
let existingSeasons: number[] = [];
|
||||
|
||||
// We need to check existing requests on this title to make sure we don't double up on seasons that were
|
||||
// already requested. In the case they were, we just throw out any duplicates but still approve the request.
|
||||
// (Unless there are no seasons, in which case we abort)
|
||||
if (media.requests) {
|
||||
existingSeasons = media.requests
|
||||
.filter(
|
||||
(request) =>
|
||||
request.is4k === requestBody.is4k &&
|
||||
request.status !== MediaRequestStatus.DECLINED
|
||||
)
|
||||
.reduce((seasons, request) => {
|
||||
const combinedSeasons = request.seasons.map(
|
||||
(season) => season.seasonNumber
|
||||
);
|
||||
|
||||
return [...seasons, ...combinedSeasons];
|
||||
}, [] as number[]);
|
||||
}
|
||||
|
||||
// We should also check seasons that are available/partially available but don't have existing requests
|
||||
if (media.seasons) {
|
||||
existingSeasons = [
|
||||
...existingSeasons,
|
||||
...media.seasons
|
||||
.filter(
|
||||
(season) =>
|
||||
season[requestBody.is4k ? 'status4k' : 'status'] !==
|
||||
MediaStatus.UNKNOWN
|
||||
)
|
||||
.map((season) => season.seasonNumber),
|
||||
];
|
||||
}
|
||||
|
||||
const finalSeasons = requestedSeasons.filter(
|
||||
(rs) => !existingSeasons.includes(rs)
|
||||
);
|
||||
|
||||
if (finalSeasons.length === 0) {
|
||||
throw new NoSeasonsAvailableError('No seasons available to request');
|
||||
} else if (
|
||||
quotas.tv.limit &&
|
||||
finalSeasons.length > (quotas.tv.remaining ?? 0)
|
||||
) {
|
||||
throw new QuotaRestrictedError('Series Quota exceeded.');
|
||||
}
|
||||
|
||||
await mediaRepository.save(media);
|
||||
|
||||
const request = new MediaRequest({
|
||||
type: MediaType.TV,
|
||||
media,
|
||||
requestedBy: requestUser,
|
||||
// If the user is an admin or has the "auto approve" permission, automatically approve the request
|
||||
status: user.hasPermission(
|
||||
[
|
||||
requestBody.is4k
|
||||
? Permission.AUTO_APPROVE_4K
|
||||
: Permission.AUTO_APPROVE,
|
||||
requestBody.is4k
|
||||
? Permission.AUTO_APPROVE_4K_TV
|
||||
: Permission.AUTO_APPROVE_TV,
|
||||
Permission.MANAGE_REQUESTS,
|
||||
],
|
||||
{ type: 'or' }
|
||||
)
|
||||
? MediaRequestStatus.APPROVED
|
||||
: MediaRequestStatus.PENDING,
|
||||
modifiedBy: user.hasPermission(
|
||||
[
|
||||
requestBody.is4k
|
||||
? Permission.AUTO_APPROVE_4K
|
||||
: Permission.AUTO_APPROVE,
|
||||
requestBody.is4k
|
||||
? Permission.AUTO_APPROVE_4K_TV
|
||||
: Permission.AUTO_APPROVE_TV,
|
||||
Permission.MANAGE_REQUESTS,
|
||||
],
|
||||
{ type: 'or' }
|
||||
)
|
||||
? user
|
||||
: undefined,
|
||||
is4k: requestBody.is4k,
|
||||
serverId: requestBody.serverId,
|
||||
profileId: requestBody.profileId,
|
||||
rootFolder: requestBody.rootFolder,
|
||||
languageProfileId: requestBody.languageProfileId,
|
||||
tags: requestBody.tags,
|
||||
seasons: finalSeasons.map(
|
||||
(sn) =>
|
||||
new SeasonRequest({
|
||||
seasonNumber: sn,
|
||||
status: user.hasPermission(
|
||||
[
|
||||
requestBody.is4k
|
||||
? Permission.AUTO_APPROVE_4K
|
||||
: Permission.AUTO_APPROVE,
|
||||
requestBody.is4k
|
||||
? Permission.AUTO_APPROVE_4K_TV
|
||||
: Permission.AUTO_APPROVE_TV,
|
||||
Permission.MANAGE_REQUESTS,
|
||||
],
|
||||
{ type: 'or' }
|
||||
)
|
||||
? MediaRequestStatus.APPROVED
|
||||
: MediaRequestStatus.PENDING,
|
||||
})
|
||||
),
|
||||
isAutoRequest: options.isAutoRequest ?? false,
|
||||
});
|
||||
|
||||
await requestRepository.save(request);
|
||||
return request;
|
||||
}
|
||||
}
|
||||
|
||||
@PrimaryGeneratedColumn()
|
||||
public id: number;
|
||||
|
||||
@@ -457,9 +120,6 @@ export class MediaRequest {
|
||||
})
|
||||
public tags?: number[];
|
||||
|
||||
@Column({ default: false })
|
||||
public isAutoRequest: boolean;
|
||||
|
||||
constructor(init?: Partial<MediaRequest>) {
|
||||
Object.assign(this, init);
|
||||
}
|
||||
@@ -487,10 +147,6 @@ export class MediaRequest {
|
||||
}
|
||||
|
||||
this.sendNotification(media, Notification.MEDIA_PENDING);
|
||||
|
||||
if (this.isAutoRequest) {
|
||||
this.sendNotification(media, Notification.MEDIA_AUTO_REQUESTED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -535,14 +191,6 @@ export class MediaRequest {
|
||||
: Notification.MEDIA_APPROVED
|
||||
: Notification.MEDIA_DECLINED
|
||||
);
|
||||
|
||||
if (
|
||||
this.status === MediaRequestStatus.APPROVED &&
|
||||
autoApproved &&
|
||||
this.isAutoRequest
|
||||
) {
|
||||
this.sendNotification(media, Notification.MEDIA_AUTO_REQUESTED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -559,7 +207,7 @@ export class MediaRequest {
|
||||
const mediaRepository = getRepository(Media);
|
||||
const media = await mediaRepository.findOne({
|
||||
where: { id: this.media.id },
|
||||
relations: { requests: true },
|
||||
relations: ['requests'],
|
||||
});
|
||||
if (!media) {
|
||||
logger.error('Media data not found', {
|
||||
@@ -624,7 +272,7 @@ export class MediaRequest {
|
||||
const mediaRepository = getRepository(Media);
|
||||
const fullMedia = await mediaRepository.findOneOrFail({
|
||||
where: { id: this.media.id },
|
||||
relations: { requests: true },
|
||||
relations: ['requests'],
|
||||
});
|
||||
|
||||
if (
|
||||
@@ -804,13 +452,10 @@ export class MediaRequest {
|
||||
await mediaRepository.save(media);
|
||||
})
|
||||
.catch(async () => {
|
||||
const requestRepository = getRepository(MediaRequest);
|
||||
|
||||
this.status = MediaRequestStatus.FAILED;
|
||||
requestRepository.save(this);
|
||||
|
||||
media[this.is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN;
|
||||
await mediaRepository.save(media);
|
||||
logger.warn(
|
||||
'Something went wrong sending movie request to Radarr, marking status as FAILED',
|
||||
'Something went wrong sending movie request to Radarr, marking status as UNKNOWN',
|
||||
{
|
||||
label: 'Media Request',
|
||||
requestId: this.id,
|
||||
@@ -898,7 +543,7 @@ export class MediaRequest {
|
||||
|
||||
const media = await mediaRepository.findOne({
|
||||
where: { id: this.media.id },
|
||||
relations: { requests: true },
|
||||
relations: ['requests'],
|
||||
});
|
||||
|
||||
if (!media) {
|
||||
@@ -1025,7 +670,7 @@ export class MediaRequest {
|
||||
// We grab media again here to make sure we have the latest version of it
|
||||
const media = await mediaRepository.findOne({
|
||||
where: { id: this.media.id },
|
||||
relations: { requests: true },
|
||||
relations: ['requests'],
|
||||
});
|
||||
|
||||
if (!media) {
|
||||
@@ -1040,13 +685,10 @@ export class MediaRequest {
|
||||
await mediaRepository.save(media);
|
||||
})
|
||||
.catch(async () => {
|
||||
const requestRepository = getRepository(MediaRequest);
|
||||
|
||||
this.status = MediaRequestStatus.FAILED;
|
||||
requestRepository.save(this);
|
||||
|
||||
media[this.is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN;
|
||||
await mediaRepository.save(media);
|
||||
logger.warn(
|
||||
'Something went wrong sending series request to Sonarr, marking status as FAILED',
|
||||
'Something went wrong sending series request to Sonarr, marking status as UNKNOWN',
|
||||
{
|
||||
label: 'Media Request',
|
||||
requestId: this.id,
|
||||
@@ -1081,7 +723,6 @@ export class MediaRequest {
|
||||
const mediaType = this.type === MediaType.MOVIE ? 'Movie' : 'Series';
|
||||
let event: string | undefined;
|
||||
let notifyAdmin = true;
|
||||
let notifySystem = true;
|
||||
|
||||
switch (type) {
|
||||
case Notification.MEDIA_APPROVED:
|
||||
@@ -1095,13 +736,6 @@ export class MediaRequest {
|
||||
case Notification.MEDIA_PENDING:
|
||||
event = `New ${this.is4k ? '4K ' : ''}${mediaType} Request`;
|
||||
break;
|
||||
case Notification.MEDIA_AUTO_REQUESTED:
|
||||
event = `${
|
||||
this.is4k ? '4K ' : ''
|
||||
}${mediaType} Request Automatically Submitted`;
|
||||
notifyAdmin = false;
|
||||
notifySystem = false;
|
||||
break;
|
||||
case Notification.MEDIA_AUTO_APPROVED:
|
||||
event = `${
|
||||
this.is4k ? '4K ' : ''
|
||||
@@ -1118,7 +752,6 @@ export class MediaRequest {
|
||||
media,
|
||||
request: this,
|
||||
notifyAdmin,
|
||||
notifySystem,
|
||||
notifyUser: notifyAdmin ? undefined : this.requestedBy,
|
||||
event,
|
||||
subject: `${movie.title}${
|
||||
@@ -1137,7 +770,6 @@ export class MediaRequest {
|
||||
media,
|
||||
request: this,
|
||||
notifyAdmin,
|
||||
notifySystem,
|
||||
notifyUser: notifyAdmin ? undefined : this.requestedBy,
|
||||
event,
|
||||
subject: `${tv.name}${
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { MediaStatus } from '@server/constants/media';
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
ManyToOne,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { MediaStatus } from '../constants/media';
|
||||
import Media from './Media';
|
||||
|
||||
@Entity()
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { MediaRequestStatus } from '@server/constants/media';
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
ManyToOne,
|
||||
} from 'typeorm';
|
||||
import { MediaRequestStatus } from '../constants/media';
|
||||
import { MediaRequest } from './MediaRequest';
|
||||
|
||||
@Entity()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ISession } from 'connect-typeorm';
|
||||
import { Column, Entity, Index, PrimaryColumn } from 'typeorm';
|
||||
import { ISession } from 'connect-typeorm';
|
||||
import { Index, Column, PrimaryColumn, Entity } from 'typeorm';
|
||||
|
||||
@Entity()
|
||||
export class Session implements ISession {
|
||||
|
||||
@@ -1,13 +1,3 @@
|
||||
import { MediaRequestStatus, MediaType } from '@server/constants/media';
|
||||
import { UserType } from '@server/constants/user';
|
||||
import { getRepository } from '@server/datasource';
|
||||
import type { QuotaResponse } from '@server/interfaces/api/userInterfaces';
|
||||
import PreparedEmail from '@server/lib/email';
|
||||
import type { PermissionCheckOptions } from '@server/lib/permissions';
|
||||
import { hasPermission, Permission } from '@server/lib/permissions';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import { AfterDate } from '@server/utils/dateHelpers';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { randomUUID } from 'crypto';
|
||||
import path from 'path';
|
||||
@@ -17,6 +7,8 @@ import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
getRepository,
|
||||
MoreThan,
|
||||
Not,
|
||||
OneToMany,
|
||||
OneToOne,
|
||||
@@ -24,6 +16,17 @@ import {
|
||||
RelationCount,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { MediaRequestStatus, MediaType } from '../constants/media';
|
||||
import { UserType } from '../constants/user';
|
||||
import { QuotaResponse } from '../interfaces/api/userInterfaces';
|
||||
import PreparedEmail from '../lib/email';
|
||||
import {
|
||||
hasPermission,
|
||||
Permission,
|
||||
PermissionCheckOptions,
|
||||
} from '../lib/permissions';
|
||||
import { getSettings } from '../lib/settings';
|
||||
import logger from '../logger';
|
||||
import Issue from './Issue';
|
||||
import { MediaRequest } from './MediaRequest';
|
||||
import SeasonRequest from './SeasonRequest';
|
||||
@@ -267,14 +270,13 @@ export class User {
|
||||
if (movieQuotaDays) {
|
||||
movieDate.setDate(movieDate.getDate() - movieQuotaDays);
|
||||
}
|
||||
const movieQuotaStartDate = movieDate.toJSON();
|
||||
|
||||
const movieQuotaUsed = movieQuotaLimit
|
||||
? await requestRepository.count({
|
||||
where: {
|
||||
requestedBy: {
|
||||
id: this.id,
|
||||
},
|
||||
createdAt: AfterDate(movieDate),
|
||||
requestedBy: this,
|
||||
createdAt: MoreThan(movieQuotaStartDate),
|
||||
type: MediaType.MOVIE,
|
||||
status: Not(MediaRequestStatus.DECLINED),
|
||||
},
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import type { NotificationAgentTypes } from '@server/interfaces/api/userSettingsInterfaces';
|
||||
import { hasNotificationType, Notification } from '@server/lib/notifications';
|
||||
import { NotificationAgentKey } from '@server/lib/settings';
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
@@ -8,6 +5,9 @@ import {
|
||||
OneToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
} from 'typeorm';
|
||||
import { NotificationAgentTypes } from '../interfaces/api/userSettingsInterfaces';
|
||||
import { hasNotificationType, Notification } from '../lib/notifications';
|
||||
import { NotificationAgentKey } from '../lib/settings';
|
||||
import { User } from './User';
|
||||
|
||||
export const ALL_NOTIFICATIONS = Object.values(Notification)
|
||||
@@ -57,12 +57,6 @@ export class UserSettings {
|
||||
@Column({ nullable: true })
|
||||
public telegramSendSilently?: boolean;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public watchlistSyncMovies?: boolean;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public watchlistSyncTv?: boolean;
|
||||
|
||||
@Column({
|
||||
type: 'text',
|
||||
nullable: true,
|
||||
|
||||
@@ -1,37 +1,34 @@
|
||||
import PlexAPI from '@server/api/plexapi';
|
||||
import dataSource, { getRepository } from '@server/datasource';
|
||||
import { Session } from '@server/entity/Session';
|
||||
import { User } from '@server/entity/User';
|
||||
import { startJobs } from '@server/job/schedule';
|
||||
import notificationManager from '@server/lib/notifications';
|
||||
import DiscordAgent from '@server/lib/notifications/agents/discord';
|
||||
import EmailAgent from '@server/lib/notifications/agents/email';
|
||||
import GotifyAgent from '@server/lib/notifications/agents/gotify';
|
||||
import LunaSeaAgent from '@server/lib/notifications/agents/lunasea';
|
||||
import PushbulletAgent from '@server/lib/notifications/agents/pushbullet';
|
||||
import PushoverAgent from '@server/lib/notifications/agents/pushover';
|
||||
import SlackAgent from '@server/lib/notifications/agents/slack';
|
||||
import TelegramAgent from '@server/lib/notifications/agents/telegram';
|
||||
import WebhookAgent from '@server/lib/notifications/agents/webhook';
|
||||
import WebPushAgent from '@server/lib/notifications/agents/webpush';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import routes from '@server/routes';
|
||||
import { getAppVersion } from '@server/utils/appVersion';
|
||||
import restartFlag from '@server/utils/restartFlag';
|
||||
import { getClientIp } from '@supercharge/request-ip';
|
||||
import { TypeormStore } from 'connect-typeorm/out';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import csurf from 'csurf';
|
||||
import type { NextFunction, Request, Response } from 'express';
|
||||
import express from 'express';
|
||||
import express, { NextFunction, Request, Response } from 'express';
|
||||
import * as OpenApiValidator from 'express-openapi-validator';
|
||||
import type { Store } from 'express-session';
|
||||
import session from 'express-session';
|
||||
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';
|
||||
import EmailAgent from './lib/notifications/agents/email';
|
||||
import GotifyAgent from './lib/notifications/agents/gotify';
|
||||
import LunaSeaAgent from './lib/notifications/agents/lunasea';
|
||||
import PushbulletAgent from './lib/notifications/agents/pushbullet';
|
||||
import PushoverAgent from './lib/notifications/agents/pushover';
|
||||
import SlackAgent from './lib/notifications/agents/slack';
|
||||
import TelegramAgent from './lib/notifications/agents/telegram';
|
||||
import WebhookAgent from './lib/notifications/agents/webhook';
|
||||
import WebPushAgent from './lib/notifications/agents/webpush';
|
||||
import { getSettings } from './lib/settings';
|
||||
import logger from './logger';
|
||||
import routes from './routes';
|
||||
import { getAppVersion } from './utils/appVersion';
|
||||
|
||||
const API_SPEC_PATH = path.join(__dirname, '../overseerr-api.yml');
|
||||
|
||||
@@ -43,7 +40,7 @@ const handle = app.getRequestHandler();
|
||||
app
|
||||
.prepare()
|
||||
.then(async () => {
|
||||
const dbConnection = await dataSource.initialize();
|
||||
const dbConnection = await createConnection();
|
||||
|
||||
// Run migrations in production
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
@@ -54,7 +51,6 @@ app
|
||||
|
||||
// Load Settings
|
||||
const settings = getSettings().load();
|
||||
restartFlag.initializeSettings(settings.main);
|
||||
|
||||
// Migrate library types
|
||||
if (
|
||||
@@ -63,8 +59,8 @@ app
|
||||
) {
|
||||
const userRepository = getRepository(User);
|
||||
const admin = await userRepository.findOne({
|
||||
select: { id: true, plexToken: true },
|
||||
where: { id: 1 },
|
||||
select: ['id', 'plexToken'],
|
||||
order: { id: 'ASC' },
|
||||
});
|
||||
|
||||
if (admin) {
|
||||
@@ -91,18 +87,8 @@ app
|
||||
new WebPushAgent(),
|
||||
]);
|
||||
|
||||
const userRepository = getRepository(User);
|
||||
const totalUsers = await userRepository.count();
|
||||
if (totalUsers > 0) {
|
||||
startJobs();
|
||||
} else {
|
||||
logger.info(
|
||||
`Skipping starting the scheduled jobs as we have no Plex/Jellyfin/Emby servers setup yet`,
|
||||
{
|
||||
label: 'Server',
|
||||
}
|
||||
);
|
||||
}
|
||||
// Start Jobs
|
||||
startJobs();
|
||||
|
||||
const server = express();
|
||||
if (settings.main.trustProxy) {
|
||||
|
||||
@@ -3,17 +3,3 @@ export interface GenreSliderItem {
|
||||
name: string;
|
||||
backdrops: string[];
|
||||
}
|
||||
|
||||
export interface WatchlistItem {
|
||||
ratingKey: string;
|
||||
tmdbId: number;
|
||||
mediaType: 'movie' | 'tv';
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface WatchlistResponse {
|
||||
page: number;
|
||||
totalPages: number;
|
||||
totalResults: number;
|
||||
results: WatchlistItem[];
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type Issue from '@server/entity/Issue';
|
||||
import type { PaginatedResponse } from './common';
|
||||
import Issue from '../../entity/Issue';
|
||||
import { PaginatedResponse } from './common';
|
||||
|
||||
export interface IssueResultsResponse extends PaginatedResponse {
|
||||
results: Issue[];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type Media from '@server/entity/Media';
|
||||
import type { User } from '@server/entity/User';
|
||||
import type { PaginatedResponse } from './common';
|
||||
import type Media from '../../entity/Media';
|
||||
import { User } from '../../entity/User';
|
||||
import { PaginatedResponse } from './common';
|
||||
|
||||
export interface MediaResultsResponse extends PaginatedResponse {
|
||||
results: Media[];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { PersonCreditCast, PersonCreditCrew } from '@server/models/Person';
|
||||
import { PersonCreditCast, PersonCreditCrew } from '../../models/Person';
|
||||
|
||||
export interface PersonCombinedCreditsResponse {
|
||||
id: number;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { PlexSettings } from '@server/lib/settings';
|
||||
import { PlexSettings } from '../../lib/settings';
|
||||
|
||||
export interface PlexStatus {
|
||||
settings: PlexSettings;
|
||||
|
||||
@@ -1,21 +1,6 @@
|
||||
import type { MediaType } from '@server/constants/media';
|
||||
import type { MediaRequest } from '@server/entity/MediaRequest';
|
||||
import type { PaginatedResponse } from './common';
|
||||
import type { MediaRequest } from '../../entity/MediaRequest';
|
||||
|
||||
export interface RequestResultsResponse extends PaginatedResponse {
|
||||
results: MediaRequest[];
|
||||
}
|
||||
|
||||
export type MediaRequestBody = {
|
||||
mediaType: MediaType;
|
||||
mediaId: number;
|
||||
tvdbId?: number;
|
||||
seasons?: number[] | 'all';
|
||||
is4k?: boolean;
|
||||
serverId?: number;
|
||||
profileId?: number;
|
||||
rootFolder?: string;
|
||||
languageProfileId?: number;
|
||||
userId?: number;
|
||||
tags?: number[];
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { QualityProfile, RootFolder, Tag } from '@server/api/servarr/base';
|
||||
import type { LanguageProfile } from '@server/api/servarr/sonarr';
|
||||
import { QualityProfile, RootFolder, Tag } from '../../api/servarr/base';
|
||||
import { LanguageProfile } from '../../api/servarr/sonarr';
|
||||
|
||||
export interface ServiceCommonServer {
|
||||
id: number;
|
||||
|
||||
@@ -59,5 +59,4 @@ export interface StatusResponse {
|
||||
commitTag: string;
|
||||
updateAvailable: boolean;
|
||||
commitsBehind: number;
|
||||
restartRequired: boolean;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type Media from '@server/entity/Media';
|
||||
import type { MediaRequest } from '@server/entity/MediaRequest';
|
||||
import type { User } from '@server/entity/User';
|
||||
import type { PaginatedResponse } from './common';
|
||||
import Media from '../../entity/Media';
|
||||
import { MediaRequest } from '../../entity/MediaRequest';
|
||||
import type { User } from '../../entity/User';
|
||||
import { PaginatedResponse } from './common';
|
||||
|
||||
export interface UserResultsResponse extends PaginatedResponse {
|
||||
results: User[];
|
||||
@@ -23,7 +23,6 @@ export interface QuotaResponse {
|
||||
movie: QuotaStatus;
|
||||
tv: QuotaStatus;
|
||||
}
|
||||
|
||||
export interface UserWatchDataResponse {
|
||||
recentlyWatched: Media[];
|
||||
playCount: number;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { NotificationAgentKey } from '@server/lib/settings';
|
||||
import { NotificationAgentKey } from '../../lib/settings';
|
||||
|
||||
export interface UserSettingsGeneralResponse {
|
||||
username?: string;
|
||||
@@ -15,8 +15,6 @@ export interface UserSettingsGeneralResponse {
|
||||
globalMovieQuotaLimit?: number;
|
||||
globalTvQuotaLimit?: number;
|
||||
globalTvQuotaDays?: number;
|
||||
watchlistSyncMovies?: boolean;
|
||||
watchlistSyncTv?: boolean;
|
||||
}
|
||||
|
||||
export type NotificationAgentTypes = Record<NotificationAgentKey, number>;
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
import type { JellyfinLibraryItem } from '@server/api/jellyfin';
|
||||
import JellyfinAPI from '@server/api/jellyfin';
|
||||
import TheMovieDb from '@server/api/themoviedb';
|
||||
import type { TmdbTvDetails } from '@server/api/themoviedb/interfaces';
|
||||
import { MediaStatus, MediaType } from '@server/constants/media';
|
||||
import { MediaServerType } from '@server/constants/server';
|
||||
import { getRepository } from '@server/datasource';
|
||||
import Media from '@server/entity/Media';
|
||||
import Season from '@server/entity/Season';
|
||||
import { User } from '@server/entity/User';
|
||||
import type { Library } from '@server/lib/settings';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import AsyncLock from '@server/utils/asyncLock';
|
||||
import { randomUUID as uuid } from 'crypto';
|
||||
import { uniqWith } from 'lodash';
|
||||
import { getRepository } from 'typeorm';
|
||||
import JellyfinAPI, { JellyfinLibraryItem } from '../../api/jellyfin';
|
||||
import TheMovieDb from '../../api/themoviedb';
|
||||
import { TmdbTvDetails } from '../../api/themoviedb/interfaces';
|
||||
import { MediaStatus, MediaType } from '../../constants/media';
|
||||
import { MediaServerType } from '../../constants/server';
|
||||
import Media from '../../entity/Media';
|
||||
import Season from '../../entity/Season';
|
||||
import { User } from '../../entity/User';
|
||||
import { getSettings, Library } from '../../lib/settings';
|
||||
import logger from '../../logger';
|
||||
import AsyncLock from '../../utils/asyncLock';
|
||||
|
||||
const BUNDLE_SIZE = 20;
|
||||
const UPDATE_RATE = 4 * 1000;
|
||||
@@ -554,7 +552,6 @@ class JobJellyfinSync {
|
||||
this.running = true;
|
||||
const userRepository = getRepository(User);
|
||||
const admin = await userRepository.findOne({
|
||||
where: { id: 1 },
|
||||
select: [
|
||||
'id',
|
||||
'jellyfinAuthToken',
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { MediaServerType } from '@server/constants/server';
|
||||
import downloadTracker from '@server/lib/downloadtracker';
|
||||
import { plexFullScanner, plexRecentScanner } from '@server/lib/scanners/plex';
|
||||
import { radarrScanner } from '@server/lib/scanners/radarr';
|
||||
import { sonarrScanner } from '@server/lib/scanners/sonarr';
|
||||
import type { JobId } from '@server/lib/settings';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import watchlistSync from '@server/lib/watchlistsync';
|
||||
import logger from '@server/logger';
|
||||
import schedule from 'node-schedule';
|
||||
import { MediaServerType } from '../constants/server';
|
||||
import downloadTracker from '../lib/downloadtracker';
|
||||
import { plexFullScanner, plexRecentScanner } from '../lib/scanners/plex';
|
||||
import { radarrScanner } from '../lib/scanners/radarr';
|
||||
import { sonarrScanner } from '../lib/scanners/sonarr';
|
||||
import { getSettings, JobId } from '../lib/settings';
|
||||
import logger from '../logger';
|
||||
import { jobJellyfinFullSync, jobJellyfinRecentSync } from './jellyfinsync';
|
||||
|
||||
interface ScheduledJob {
|
||||
@@ -16,7 +14,6 @@ interface ScheduledJob {
|
||||
name: string;
|
||||
type: 'process' | 'command';
|
||||
interval: 'short' | 'long' | 'fixed';
|
||||
cronSchedule: string;
|
||||
running?: () => boolean;
|
||||
cancelFn?: () => void;
|
||||
}
|
||||
@@ -34,7 +31,6 @@ export const startJobs = (): void => {
|
||||
name: 'Plex Recently Added Scan',
|
||||
type: 'process',
|
||||
interval: 'short',
|
||||
cronSchedule: jobs['plex-recently-added-scan'].schedule,
|
||||
job: schedule.scheduleJob(
|
||||
jobs['plex-recently-added-scan'].schedule,
|
||||
() => {
|
||||
@@ -54,7 +50,6 @@ export const startJobs = (): void => {
|
||||
name: 'Plex Full Library Scan',
|
||||
type: 'process',
|
||||
interval: 'long',
|
||||
cronSchedule: jobs['plex-full-scan'].schedule,
|
||||
job: schedule.scheduleJob(jobs['plex-full-scan'].schedule, () => {
|
||||
logger.info('Starting scheduled job: Plex Full Library Scan', {
|
||||
label: 'Jobs',
|
||||
@@ -74,7 +69,6 @@ export const startJobs = (): void => {
|
||||
name: 'Jellyfin Recently Added Sync',
|
||||
type: 'process',
|
||||
interval: 'long',
|
||||
cronSchedule: jobs['jellyfin-recently-added-sync'].schedule,
|
||||
job: schedule.scheduleJob(
|
||||
jobs['jellyfin-recently-added-sync'].schedule,
|
||||
() => {
|
||||
@@ -94,7 +88,6 @@ export const startJobs = (): void => {
|
||||
name: 'Jellyfin Full Library Sync',
|
||||
type: 'process',
|
||||
interval: 'long',
|
||||
cronSchedule: jobs['jellyfin-full-sync'].schedule,
|
||||
job: schedule.scheduleJob(jobs['jellyfin-full-sync'].schedule, () => {
|
||||
logger.info('Starting scheduled job: Jellyfin Full Sync', {
|
||||
label: 'Jobs',
|
||||
@@ -106,28 +99,12 @@ export const startJobs = (): void => {
|
||||
});
|
||||
}
|
||||
|
||||
// Run watchlist sync every 5 minutes
|
||||
scheduledJobs.push({
|
||||
id: 'plex-watchlist-sync',
|
||||
name: 'Plex Watchlist Sync',
|
||||
type: 'process',
|
||||
interval: 'long',
|
||||
cronSchedule: jobs['plex-watchlist-sync'].schedule,
|
||||
job: schedule.scheduleJob(jobs['plex-watchlist-sync'].schedule, () => {
|
||||
logger.info('Starting scheduled job: Plex Watchlist Sync', {
|
||||
label: 'Jobs',
|
||||
});
|
||||
watchlistSync.syncWatchlist();
|
||||
}),
|
||||
});
|
||||
|
||||
// Run full radarr scan every 24 hours
|
||||
scheduledJobs.push({
|
||||
id: 'radarr-scan',
|
||||
name: 'Radarr Scan',
|
||||
type: 'process',
|
||||
interval: 'long',
|
||||
cronSchedule: jobs['radarr-scan'].schedule,
|
||||
job: schedule.scheduleJob(jobs['radarr-scan'].schedule, () => {
|
||||
logger.info('Starting scheduled job: Radarr Scan', { label: 'Jobs' });
|
||||
radarrScanner.run();
|
||||
@@ -142,7 +119,6 @@ export const startJobs = (): void => {
|
||||
name: 'Sonarr Scan',
|
||||
type: 'process',
|
||||
interval: 'long',
|
||||
cronSchedule: jobs['sonarr-scan'].schedule,
|
||||
job: schedule.scheduleJob(jobs['sonarr-scan'].schedule, () => {
|
||||
logger.info('Starting scheduled job: Sonarr Scan', { label: 'Jobs' });
|
||||
sonarrScanner.run();
|
||||
@@ -157,7 +133,6 @@ export const startJobs = (): void => {
|
||||
name: 'Download Sync',
|
||||
type: 'command',
|
||||
interval: 'fixed',
|
||||
cronSchedule: jobs['download-sync'].schedule,
|
||||
job: schedule.scheduleJob(jobs['download-sync'].schedule, () => {
|
||||
logger.debug('Starting scheduled job: Download Sync', {
|
||||
label: 'Jobs',
|
||||
@@ -172,7 +147,6 @@ export const startJobs = (): void => {
|
||||
name: 'Download Sync Reset',
|
||||
type: 'command',
|
||||
interval: 'long',
|
||||
cronSchedule: jobs['download-sync-reset'].schedule,
|
||||
job: schedule.scheduleJob(jobs['download-sync-reset'].schedule, () => {
|
||||
logger.info('Starting scheduled job: Download Sync Reset', {
|
||||
label: 'Jobs',
|
||||
|
||||
@@ -6,8 +6,7 @@ export type AvailableCacheIds =
|
||||
| 'sonarr'
|
||||
| 'rt'
|
||||
| 'github'
|
||||
| 'plexguid'
|
||||
| 'plextv';
|
||||
| 'plexguid';
|
||||
|
||||
const DEFAULT_TTL = 300;
|
||||
const DEFAULT_CHECK_PERIOD = 120;
|
||||
@@ -59,10 +58,6 @@ class CacheManager {
|
||||
stdTtl: 86400 * 7, // 1 week cache
|
||||
checkPeriod: 60 * 30,
|
||||
}),
|
||||
plextv: new Cache('plextv', 'Plex TV', {
|
||||
stdTtl: 86400 * 7, // 1 week cache
|
||||
checkPeriod: 60,
|
||||
}),
|
||||
};
|
||||
|
||||
public getCache(id: AvailableCacheIds): Cache {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import RadarrAPI from '@server/api/servarr/radarr';
|
||||
import SonarrAPI from '@server/api/servarr/sonarr';
|
||||
import { MediaType } from '@server/constants/media';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import { uniqWith } from 'lodash';
|
||||
import RadarrAPI from '../api/servarr/radarr';
|
||||
import SonarrAPI from '../api/servarr/sonarr';
|
||||
import { MediaType } from '../constants/media';
|
||||
import logger from '../logger';
|
||||
import { getSettings } from './settings';
|
||||
|
||||
export interface DownloadingItem {
|
||||
mediaType: MediaType;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { NotificationAgentEmail } from '@server/lib/settings';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import Email from 'email-templates';
|
||||
import nodemailer from 'nodemailer';
|
||||
import { URL } from 'url';
|
||||
import { getSettings, NotificationAgentEmail } from '../settings';
|
||||
import { openpgpEncrypt } from './openpgpEncrypt';
|
||||
|
||||
class PreparedEmail extends Email {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import logger from '@server/logger';
|
||||
import { randomBytes } from 'crypto';
|
||||
import * as openpgp from 'openpgp';
|
||||
import type { TransformCallback } from 'stream';
|
||||
import { Transform } from 'stream';
|
||||
import { Transform, TransformCallback } from 'stream';
|
||||
import logger from '../../logger';
|
||||
|
||||
interface EncryptorOptions {
|
||||
signingKey?: string;
|
||||
@@ -27,7 +26,7 @@ class PGPEncryptor extends Transform {
|
||||
|
||||
// just save the whole message
|
||||
_transform = (
|
||||
chunk: Uint8Array,
|
||||
chunk: any,
|
||||
_encoding: BufferEncoding,
|
||||
callback: TransformCallback
|
||||
): void => {
|
||||
@@ -185,9 +184,6 @@ class PGPEncryptor extends Transform {
|
||||
}
|
||||
|
||||
export const openpgpEncrypt = (options: EncryptorOptions) => {
|
||||
// Disabling this line because I don't want to fix it but I am tired
|
||||
// of seeing the lint warning
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return function (mail: any, callback: () => unknown): void {
|
||||
if (!options.encryptionKeys.length) {
|
||||
setImmediate(callback);
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import type Issue from '@server/entity/Issue';
|
||||
import type IssueComment from '@server/entity/IssueComment';
|
||||
import type Media from '@server/entity/Media';
|
||||
import type { MediaRequest } from '@server/entity/MediaRequest';
|
||||
import type { User } from '@server/entity/User';
|
||||
import type { NotificationAgentConfig } from '@server/lib/settings';
|
||||
import type { Notification } from '..';
|
||||
import { Notification } from '..';
|
||||
import type Issue from '../../../entity/Issue';
|
||||
import IssueComment from '../../../entity/IssueComment';
|
||||
import Media from '../../../entity/Media';
|
||||
import { MediaRequest } from '../../../entity/MediaRequest';
|
||||
import { User } from '../../../entity/User';
|
||||
import { NotificationAgentConfig } from '../../settings';
|
||||
|
||||
export interface NotificationPayload {
|
||||
event?: string;
|
||||
subject: string;
|
||||
notifySystem: boolean;
|
||||
notifyAdmin: boolean;
|
||||
notifyUser?: User;
|
||||
media?: Media;
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import { IssueStatus, IssueTypeName } from '@server/constants/issue';
|
||||
import { getRepository } from '@server/datasource';
|
||||
import { User } from '@server/entity/User';
|
||||
import type { NotificationAgentDiscord } from '@server/lib/settings';
|
||||
import { getSettings, NotificationAgentKey } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import axios from 'axios';
|
||||
import { getRepository } from 'typeorm';
|
||||
import {
|
||||
hasNotificationType,
|
||||
Notification,
|
||||
shouldSendAdminNotification,
|
||||
} from '..';
|
||||
import type { NotificationAgent, NotificationPayload } from './agent';
|
||||
import { BaseAgent } from './agent';
|
||||
import { IssueStatus, IssueTypeName } from '../../../constants/issue';
|
||||
import { User } from '../../../entity/User';
|
||||
import logger from '../../../logger';
|
||||
import {
|
||||
getSettings,
|
||||
NotificationAgentDiscord,
|
||||
NotificationAgentKey,
|
||||
} from '../../settings';
|
||||
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
|
||||
|
||||
enum EmbedColors {
|
||||
DEFAULT = 0,
|
||||
@@ -243,10 +245,7 @@ class DiscordAgent
|
||||
): Promise<boolean> {
|
||||
const settings = this.getSettings();
|
||||
|
||||
if (
|
||||
!payload.notifySystem ||
|
||||
!hasNotificationType(type, settings.types ?? 0)
|
||||
) {
|
||||
if (!hasNotificationType(type, settings.types ?? 0)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import { IssueType, IssueTypeName } from '@server/constants/issue';
|
||||
import { MediaType } from '@server/constants/media';
|
||||
import { getRepository } from '@server/datasource';
|
||||
import { User } from '@server/entity/User';
|
||||
import PreparedEmail from '@server/lib/email';
|
||||
import type { NotificationAgentEmail } from '@server/lib/settings';
|
||||
import { getSettings, NotificationAgentKey } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import type { EmailOptions } from 'email-templates';
|
||||
import * as EmailValidator from 'email-validator';
|
||||
import { EmailOptions } from 'email-templates';
|
||||
import path from 'path';
|
||||
import { getRepository } from 'typeorm';
|
||||
import { Notification, shouldSendAdminNotification } from '..';
|
||||
import type { NotificationAgent, NotificationPayload } from './agent';
|
||||
import { BaseAgent } from './agent';
|
||||
import { IssueType, IssueTypeName } from '../../../constants/issue';
|
||||
import { MediaType } from '../../../constants/media';
|
||||
import { User } from '../../../entity/User';
|
||||
import logger from '../../../logger';
|
||||
import PreparedEmail from '../../email';
|
||||
import {
|
||||
getSettings,
|
||||
NotificationAgentEmail,
|
||||
NotificationAgentKey,
|
||||
} from '../../settings';
|
||||
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
|
||||
import * as EmailValidator from 'email-validator';
|
||||
|
||||
class EmailAgent
|
||||
extends BaseAgent<NotificationAgentEmail>
|
||||
@@ -82,11 +84,6 @@ class EmailAgent
|
||||
is4k ? 'in 4K ' : ''
|
||||
}is pending approval:`;
|
||||
break;
|
||||
case Notification.MEDIA_AUTO_REQUESTED:
|
||||
body = `A new request for the following ${mediaType} ${
|
||||
is4k ? 'in 4K ' : ''
|
||||
}was automatically submitted:`;
|
||||
break;
|
||||
case Notification.MEDIA_APPROVED:
|
||||
body = `Your request for the following ${mediaType} ${
|
||||
is4k ? 'in 4K ' : ''
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import { IssueStatus, IssueTypeName } from '@server/constants/issue';
|
||||
import type { NotificationAgentGotify } from '@server/lib/settings';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import axios from 'axios';
|
||||
import { hasNotificationType, Notification } from '..';
|
||||
import type { NotificationAgent, NotificationPayload } from './agent';
|
||||
import { BaseAgent } from './agent';
|
||||
import { IssueStatus, IssueTypeName } from '../../../constants/issue';
|
||||
import logger from '../../../logger';
|
||||
import { getSettings, NotificationAgentGotify } from '../../settings';
|
||||
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
|
||||
|
||||
interface GotifyPayload {
|
||||
title: string;
|
||||
message: string;
|
||||
priority: number;
|
||||
extras: Record<string, unknown>;
|
||||
extras: any;
|
||||
}
|
||||
|
||||
class GotifyAgent
|
||||
@@ -117,10 +115,7 @@ class GotifyAgent
|
||||
): Promise<boolean> {
|
||||
const settings = this.getSettings();
|
||||
|
||||
if (
|
||||
!payload.notifySystem ||
|
||||
!hasNotificationType(type, settings.types ?? 0)
|
||||
) {
|
||||
if (!hasNotificationType(type, settings.types ?? 0)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { IssueStatus, IssueType } from '@server/constants/issue';
|
||||
import { MediaStatus } from '@server/constants/media';
|
||||
import type { NotificationAgentLunaSea } from '@server/lib/settings';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import axios from 'axios';
|
||||
import { hasNotificationType, Notification } from '..';
|
||||
import type { NotificationAgent, NotificationPayload } from './agent';
|
||||
import { BaseAgent } from './agent';
|
||||
import { IssueStatus, IssueType } from '../../../constants/issue';
|
||||
import { MediaStatus } from '../../../constants/media';
|
||||
import logger from '../../../logger';
|
||||
import { getSettings, NotificationAgentLunaSea } from '../../settings';
|
||||
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
|
||||
|
||||
class LunaSeaAgent
|
||||
extends BaseAgent<NotificationAgentLunaSea>
|
||||
@@ -87,10 +85,7 @@ class LunaSeaAgent
|
||||
): Promise<boolean> {
|
||||
const settings = this.getSettings();
|
||||
|
||||
if (
|
||||
!payload.notifySystem ||
|
||||
!hasNotificationType(type, settings.types ?? 0)
|
||||
) {
|
||||
if (!hasNotificationType(type, settings.types ?? 0)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import { IssueStatus, IssueTypeName } from '@server/constants/issue';
|
||||
import { MediaStatus } from '@server/constants/media';
|
||||
import { getRepository } from '@server/datasource';
|
||||
import { User } from '@server/entity/User';
|
||||
import type { NotificationAgentPushbullet } from '@server/lib/settings';
|
||||
import { getSettings, NotificationAgentKey } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import axios from 'axios';
|
||||
import { getRepository } from 'typeorm';
|
||||
import {
|
||||
hasNotificationType,
|
||||
Notification,
|
||||
shouldSendAdminNotification,
|
||||
} from '..';
|
||||
import type { NotificationAgent, NotificationPayload } from './agent';
|
||||
import { BaseAgent } from './agent';
|
||||
import { IssueStatus, IssueTypeName } from '../../../constants/issue';
|
||||
import { User } from '../../../entity/User';
|
||||
import logger from '../../../logger';
|
||||
import {
|
||||
getSettings,
|
||||
NotificationAgentKey,
|
||||
NotificationAgentPushbullet,
|
||||
} from '../../settings';
|
||||
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
|
||||
|
||||
interface PushbulletPayload {
|
||||
type: string;
|
||||
@@ -53,12 +54,6 @@ class PushbulletAgent
|
||||
|
||||
let status = '';
|
||||
switch (type) {
|
||||
case Notification.MEDIA_AUTO_REQUESTED:
|
||||
status =
|
||||
payload.media?.status === MediaStatus.PENDING
|
||||
? 'Pending Approval'
|
||||
: 'Processing';
|
||||
break;
|
||||
case Notification.MEDIA_PENDING:
|
||||
status = 'Pending Approval';
|
||||
break;
|
||||
@@ -111,7 +106,6 @@ class PushbulletAgent
|
||||
|
||||
// Send system notification
|
||||
if (
|
||||
payload.notifySystem &&
|
||||
hasNotificationType(type, settings.types ?? 0) &&
|
||||
settings.enabled &&
|
||||
settings.options.accessToken
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import { IssueStatus, IssueTypeName } from '@server/constants/issue';
|
||||
import { MediaStatus } from '@server/constants/media';
|
||||
import { getRepository } from '@server/datasource';
|
||||
import { User } from '@server/entity/User';
|
||||
import type { NotificationAgentPushover } from '@server/lib/settings';
|
||||
import { getSettings, NotificationAgentKey } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import axios from 'axios';
|
||||
import { getRepository } from 'typeorm';
|
||||
import {
|
||||
hasNotificationType,
|
||||
Notification,
|
||||
shouldSendAdminNotification,
|
||||
} from '..';
|
||||
import type { NotificationAgent, NotificationPayload } from './agent';
|
||||
import { BaseAgent } from './agent';
|
||||
import { IssueStatus, IssueTypeName } from '../../../constants/issue';
|
||||
import { User } from '../../../entity/User';
|
||||
import logger from '../../../logger';
|
||||
import {
|
||||
getSettings,
|
||||
NotificationAgentKey,
|
||||
NotificationAgentPushover,
|
||||
} from '../../settings';
|
||||
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
|
||||
|
||||
interface PushoverPayload {
|
||||
token: string;
|
||||
@@ -62,12 +63,6 @@ class PushoverAgent
|
||||
|
||||
let status = '';
|
||||
switch (type) {
|
||||
case Notification.MEDIA_AUTO_REQUESTED:
|
||||
status =
|
||||
payload.media?.status === MediaStatus.PENDING
|
||||
? 'Pending Approval'
|
||||
: 'Processing';
|
||||
break;
|
||||
case Notification.MEDIA_PENDING:
|
||||
status = 'Pending Approval';
|
||||
break;
|
||||
@@ -142,7 +137,6 @@ class PushoverAgent
|
||||
|
||||
// Send system notification
|
||||
if (
|
||||
payload.notifySystem &&
|
||||
hasNotificationType(type, settings.types ?? 0) &&
|
||||
settings.enabled &&
|
||||
settings.options.accessToken &&
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { IssueStatus, IssueTypeName } from '@server/constants/issue';
|
||||
import type { NotificationAgentSlack } from '@server/lib/settings';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import axios from 'axios';
|
||||
import { hasNotificationType, Notification } from '..';
|
||||
import type { NotificationAgent, NotificationPayload } from './agent';
|
||||
import { BaseAgent } from './agent';
|
||||
import { IssueStatus, IssueTypeName } from '../../../constants/issue';
|
||||
import logger from '../../../logger';
|
||||
import { getSettings, NotificationAgentSlack } from '../../settings';
|
||||
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
|
||||
|
||||
interface EmbedField {
|
||||
type: 'plain_text' | 'mrkdwn';
|
||||
@@ -225,10 +223,7 @@ class SlackAgent
|
||||
): Promise<boolean> {
|
||||
const settings = this.getSettings();
|
||||
|
||||
if (
|
||||
!payload.notifySystem ||
|
||||
!hasNotificationType(type, settings.types ?? 0)
|
||||
) {
|
||||
if (!hasNotificationType(type, settings.types ?? 0)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import { IssueStatus, IssueTypeName } from '@server/constants/issue';
|
||||
import { MediaStatus } from '@server/constants/media';
|
||||
import { getRepository } from '@server/datasource';
|
||||
import { User } from '@server/entity/User';
|
||||
import type { NotificationAgentTelegram } from '@server/lib/settings';
|
||||
import { getSettings, NotificationAgentKey } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import axios from 'axios';
|
||||
import { getRepository } from 'typeorm';
|
||||
import {
|
||||
hasNotificationType,
|
||||
Notification,
|
||||
shouldSendAdminNotification,
|
||||
} from '..';
|
||||
import type { NotificationAgent, NotificationPayload } from './agent';
|
||||
import { BaseAgent } from './agent';
|
||||
import { IssueStatus, IssueTypeName } from '../../../constants/issue';
|
||||
import { User } from '../../../entity/User';
|
||||
import logger from '../../../logger';
|
||||
import {
|
||||
getSettings,
|
||||
NotificationAgentKey,
|
||||
NotificationAgentTelegram,
|
||||
} from '../../settings';
|
||||
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
|
||||
|
||||
interface TelegramMessagePayload {
|
||||
text: string;
|
||||
@@ -80,12 +81,6 @@ class TelegramAgent
|
||||
|
||||
let status = '';
|
||||
switch (type) {
|
||||
case Notification.MEDIA_AUTO_REQUESTED:
|
||||
status =
|
||||
payload.media?.status === MediaStatus.PENDING
|
||||
? 'Pending Approval'
|
||||
: 'Processing';
|
||||
break;
|
||||
case Notification.MEDIA_PENDING:
|
||||
status = 'Pending Approval';
|
||||
break;
|
||||
@@ -164,7 +159,6 @@ class TelegramAgent
|
||||
|
||||
// Send system notification
|
||||
if (
|
||||
payload.notifySystem &&
|
||||
hasNotificationType(type, settings.types ?? 0) &&
|
||||
settings.options.chatId
|
||||
) {
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { IssueStatus, IssueType } from '@server/constants/issue';
|
||||
import { MediaStatus } from '@server/constants/media';
|
||||
import type { NotificationAgentWebhook } from '@server/lib/settings';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import axios from 'axios';
|
||||
import { get } from 'lodash';
|
||||
import { hasNotificationType, Notification } from '..';
|
||||
import type { NotificationAgent, NotificationPayload } from './agent';
|
||||
import { BaseAgent } from './agent';
|
||||
import { IssueStatus, IssueType } from '../../../constants/issue';
|
||||
import { MediaStatus } from '../../../constants/media';
|
||||
import logger from '../../../logger';
|
||||
import { getSettings, NotificationAgentWebhook } from '../../settings';
|
||||
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
|
||||
|
||||
type KeyMapFunction = (
|
||||
payload: NotificationPayload,
|
||||
@@ -164,10 +162,7 @@ class WebhookAgent
|
||||
): Promise<boolean> {
|
||||
const settings = this.getSettings();
|
||||
|
||||
if (
|
||||
!payload.notifySystem ||
|
||||
!hasNotificationType(type, settings.types ?? 0)
|
||||
) {
|
||||
if (!hasNotificationType(type, settings.types ?? 0)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { IssueType, IssueTypeName } from '@server/constants/issue';
|
||||
import { MediaType } from '@server/constants/media';
|
||||
import { getRepository } from '@server/datasource';
|
||||
import { User } from '@server/entity/User';
|
||||
import { UserPushSubscription } from '@server/entity/UserPushSubscription';
|
||||
import type { NotificationAgentConfig } from '@server/lib/settings';
|
||||
import { getSettings, NotificationAgentKey } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import { getRepository } from 'typeorm';
|
||||
import webpush from 'web-push';
|
||||
import { Notification, shouldSendAdminNotification } from '..';
|
||||
import type { NotificationAgent, NotificationPayload } from './agent';
|
||||
import { BaseAgent } from './agent';
|
||||
import { IssueType, IssueTypeName } from '../../../constants/issue';
|
||||
import { MediaType } from '../../../constants/media';
|
||||
import { User } from '../../../entity/User';
|
||||
import { UserPushSubscription } from '../../../entity/UserPushSubscription';
|
||||
import logger from '../../../logger';
|
||||
import {
|
||||
getSettings,
|
||||
NotificationAgentConfig,
|
||||
NotificationAgentKey,
|
||||
} from '../../settings';
|
||||
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
|
||||
|
||||
interface PushNotificationPayload {
|
||||
notificationType: string;
|
||||
@@ -57,11 +59,6 @@ class WebPushAgent
|
||||
case Notification.TEST_NOTIFICATION:
|
||||
message = payload.message;
|
||||
break;
|
||||
case Notification.MEDIA_AUTO_REQUESTED:
|
||||
message = `Automatically submitted a new ${
|
||||
is4k ? '4K ' : ''
|
||||
}${mediaType} request.`;
|
||||
break;
|
||||
case Notification.MEDIA_APPROVED:
|
||||
message = `Your ${
|
||||
is4k ? '4K ' : ''
|
||||
@@ -163,7 +160,7 @@ class WebPushAgent
|
||||
true)
|
||||
) {
|
||||
const notifySubs = await userPushSubRepository.find({
|
||||
where: { user: { id: payload.notifyUser.id } },
|
||||
where: { user: payload.notifyUser.id },
|
||||
});
|
||||
|
||||
pushSubs.push(...notifySubs);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { User } from '@server/entity/User';
|
||||
import { Permission } from '@server/lib/permissions';
|
||||
import logger from '@server/logger';
|
||||
import { User } from '../../entity/User';
|
||||
import logger from '../../logger';
|
||||
import { Permission } from '../permissions';
|
||||
import type { NotificationAgent, NotificationPayload } from './agents/agent';
|
||||
|
||||
export enum Notification {
|
||||
@@ -16,7 +16,6 @@ export enum Notification {
|
||||
ISSUE_COMMENT = 512,
|
||||
ISSUE_RESOLVED = 1024,
|
||||
ISSUE_REOPENED = 2048,
|
||||
MEDIA_AUTO_REQUESTED = 4096,
|
||||
}
|
||||
|
||||
export const hasNotificationType = (
|
||||
|
||||
@@ -22,11 +22,6 @@ export enum Permission {
|
||||
MANAGE_ISSUES = 1048576,
|
||||
VIEW_ISSUES = 2097152,
|
||||
CREATE_ISSUES = 4194304,
|
||||
AUTO_REQUEST = 8388608,
|
||||
AUTO_REQUEST_MOVIE = 16777216,
|
||||
AUTO_REQUEST_TV = 33554432,
|
||||
RECENT_VIEW = 67108864,
|
||||
WATCHLIST_VIEW = 134217728,
|
||||
}
|
||||
|
||||
export interface PermissionCheckOptions {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import TheMovieDb from '@server/api/themoviedb';
|
||||
import { MediaStatus, MediaType } from '@server/constants/media';
|
||||
import { getRepository } from '@server/datasource';
|
||||
import Media from '@server/entity/Media';
|
||||
import Season from '@server/entity/Season';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import AsyncLock from '@server/utils/asyncLock';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { getRepository } from 'typeorm';
|
||||
import TheMovieDb from '../../api/themoviedb';
|
||||
import { MediaStatus, MediaType } from '../../constants/media';
|
||||
import Media from '../../entity/Media';
|
||||
import Season from '../../entity/Season';
|
||||
import logger from '../../logger';
|
||||
import AsyncLock from '../../utils/asyncLock';
|
||||
import { getSettings } from '../settings';
|
||||
|
||||
// Default scan rates (can be overidden)
|
||||
const BUNDLE_SIZE = 20;
|
||||
@@ -210,7 +210,7 @@ class BaseScanner<T> {
|
||||
}
|
||||
|
||||
/**
|
||||
* processShow takes a TMDB ID and an array of ProcessableSeasons, which
|
||||
* processShow takes a TMDb ID and an array of ProcessableSeasons, which
|
||||
* should include the total episodes a sesaon has + the total available
|
||||
* episodes that each season currently has. Unlike processMovie, this method
|
||||
* does not take an `is4k` option. We handle both the 4k _and_ non 4k status
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
import animeList from '@server/api/animelist';
|
||||
import type { PlexLibraryItem, PlexMetadata } from '@server/api/plexapi';
|
||||
import PlexAPI from '@server/api/plexapi';
|
||||
import type { TmdbTvDetails } from '@server/api/themoviedb/interfaces';
|
||||
import { getRepository } from '@server/datasource';
|
||||
import { User } from '@server/entity/User';
|
||||
import cacheManager from '@server/lib/cache';
|
||||
import type {
|
||||
import { uniqWith } from 'lodash';
|
||||
import { getRepository } from 'typeorm';
|
||||
import animeList from '../../../api/animelist';
|
||||
import PlexAPI, { PlexLibraryItem, PlexMetadata } from '../../../api/plexapi';
|
||||
import { TmdbTvDetails } from '../../../api/themoviedb/interfaces';
|
||||
import { User } from '../../../entity/User';
|
||||
import cacheManager from '../../cache';
|
||||
import { getSettings, Library } from '../../settings';
|
||||
import BaseScanner, {
|
||||
MediaIds,
|
||||
ProcessableSeason,
|
||||
RunnableScanner,
|
||||
StatusBase,
|
||||
} from '@server/lib/scanners/baseScanner';
|
||||
import BaseScanner from '@server/lib/scanners/baseScanner';
|
||||
import type { Library } from '@server/lib/settings';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import { uniqWith } from 'lodash';
|
||||
} from '../baseScanner';
|
||||
|
||||
const imdbRegex = new RegExp(/imdb:\/\/(tt[0-9]+)/);
|
||||
const tmdbRegex = new RegExp(/tmdb:\/\/([0-9]+)/);
|
||||
@@ -62,8 +59,8 @@ class PlexScanner
|
||||
try {
|
||||
const userRepository = getRepository(User);
|
||||
const admin = await userRepository.findOne({
|
||||
select: { id: true, plexToken: true },
|
||||
where: { id: 1 },
|
||||
select: ['id', 'plexToken'],
|
||||
order: { id: 'ASC' },
|
||||
});
|
||||
|
||||
if (!admin) {
|
||||
@@ -144,9 +141,7 @@ class PlexScanner
|
||||
'info'
|
||||
);
|
||||
} catch (e) {
|
||||
this.log('Scan interrupted', 'error', {
|
||||
errorMessage: e.message,
|
||||
});
|
||||
this.log('Scan interrupted', 'error', { errorMessage: e.message });
|
||||
} finally {
|
||||
this.endRun(sessionId);
|
||||
}
|
||||
@@ -374,7 +369,7 @@ class PlexScanner
|
||||
}
|
||||
});
|
||||
|
||||
// If we got an IMDb ID, but no TMDB ID, lookup the TMDB ID with the IMDb ID
|
||||
// If we got an IMDb ID, but no TMDb ID, lookup the TMDb ID with the IMDb ID
|
||||
if (mediaIds.imdbId && !mediaIds.tmdbId) {
|
||||
const tmdbMedia = await this.tmdb.getMediaByImdbId({
|
||||
imdbId: mediaIds.imdbId,
|
||||
@@ -395,7 +390,7 @@ class PlexScanner
|
||||
});
|
||||
mediaIds.tmdbId = tmdbMedia.id;
|
||||
}
|
||||
// Check if the agent is TMDB
|
||||
// Check if the agent is TMDb
|
||||
} else if (plexitem.guid.match(tmdbRegex)) {
|
||||
const tmdbMatch = plexitem.guid.match(tmdbRegex);
|
||||
if (tmdbMatch) {
|
||||
@@ -414,7 +409,7 @@ class PlexScanner
|
||||
mediaIds.tvdbId = Number(matchedtvdb[1]);
|
||||
mediaIds.tmdbId = show.id;
|
||||
}
|
||||
// Check if the agent (for shows) is TMDB
|
||||
// Check if the agent (for shows) is TMDb
|
||||
} else if (plexitem.guid.match(tmdbShowRegex)) {
|
||||
const matchedtmdb = plexitem.guid.match(tmdbShowRegex);
|
||||
if (matchedtmdb) {
|
||||
@@ -489,10 +484,10 @@ class PlexScanner
|
||||
}
|
||||
|
||||
if (!mediaIds.tmdbId) {
|
||||
throw new Error('Unable to find TMDB ID');
|
||||
throw new Error('Unable to find TMDb ID');
|
||||
}
|
||||
|
||||
// We check above if we have the TMDB ID, so we can safely assert the type below
|
||||
// We check above if we have the TMDb ID, so we can safely assert the type below
|
||||
return mediaIds as MediaIds;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import type { RadarrMovie } from '@server/api/servarr/radarr';
|
||||
import RadarrAPI from '@server/api/servarr/radarr';
|
||||
import type {
|
||||
RunnableScanner,
|
||||
StatusBase,
|
||||
} from '@server/lib/scanners/baseScanner';
|
||||
import BaseScanner from '@server/lib/scanners/baseScanner';
|
||||
import type { RadarrSettings } from '@server/lib/settings';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import { uniqWith } from 'lodash';
|
||||
import RadarrAPI, { RadarrMovie } from '../../../api/servarr/radarr';
|
||||
import { getSettings, RadarrSettings } from '../../settings';
|
||||
import BaseScanner, { RunnableScanner, StatusBase } from '../baseScanner';
|
||||
|
||||
type SyncStatus = StatusBase & {
|
||||
currentServer: RadarrSettings;
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import type { SonarrSeries } from '@server/api/servarr/sonarr';
|
||||
import SonarrAPI from '@server/api/servarr/sonarr';
|
||||
import type { TmdbTvDetails } from '@server/api/themoviedb/interfaces';
|
||||
import { getRepository } from '@server/datasource';
|
||||
import Media from '@server/entity/Media';
|
||||
import type {
|
||||
import { uniqWith } from 'lodash';
|
||||
import { getRepository } from 'typeorm';
|
||||
import SonarrAPI, { SonarrSeries } from '../../../api/servarr/sonarr';
|
||||
import { TmdbTvDetails } from '../../../api/themoviedb/interfaces';
|
||||
import Media from '../../../entity/Media';
|
||||
import { getSettings, SonarrSettings } from '../../settings';
|
||||
import BaseScanner, {
|
||||
ProcessableSeason,
|
||||
RunnableScanner,
|
||||
StatusBase,
|
||||
} from '@server/lib/scanners/baseScanner';
|
||||
import BaseScanner from '@server/lib/scanners/baseScanner';
|
||||
import type { SonarrSettings } from '@server/lib/settings';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import { uniqWith } from 'lodash';
|
||||
} from '../baseScanner';
|
||||
|
||||
type SyncStatus = StatusBase & {
|
||||
currentServer: SonarrSettings;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user