mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2025-12-26 03:43:09 -05:00
Compare commits
71 Commits
ci-arm64-s
...
preview-po
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00e18a272a | ||
|
|
6957b7606e | ||
|
|
0154e6b538 | ||
|
|
3d0166aaef | ||
|
|
2fc8996606 | ||
|
|
830f431e01 | ||
|
|
94efdf7a18 | ||
|
|
ab5cdf5464 | ||
|
|
d3805d99e8 | ||
|
|
7a5ee18e2c | ||
|
|
96591a9ddd | ||
|
|
46c3af115a | ||
|
|
4e63cee12b | ||
|
|
5ee29823c6 | ||
|
|
2add7af5ec | ||
|
|
3b12c98242 | ||
|
|
bd7339a105 | ||
|
|
66e308ba1d | ||
|
|
357b927ab3 | ||
|
|
f8926fa86c | ||
|
|
e2992fea9c | ||
|
|
2b0d497370 | ||
|
|
028012185c | ||
|
|
94a9806089 | ||
|
|
caaed7c8b8 | ||
|
|
29452648e6 | ||
|
|
f6b5a6fe9f | ||
|
|
cf4e3fd579 | ||
|
|
326d2cb4ca | ||
|
|
ff7bb884ae | ||
|
|
ef78fdd534 | ||
|
|
637e7dbd8e | ||
|
|
3ebf47fe61 | ||
|
|
88b67686ff | ||
|
|
86444b80b9 | ||
|
|
5f7679982a | ||
|
|
610c372498 | ||
|
|
7aca0be41c | ||
|
|
0bab6887b0 | ||
|
|
0d6a1f12fb | ||
|
|
87c8444ec6 | ||
|
|
f08c537cba | ||
|
|
b1b4dd9cfc | ||
|
|
eb111ac1db | ||
|
|
106cd195d4 | ||
|
|
42ad4e0ae3 | ||
|
|
501859207a | ||
|
|
8c7004c50d | ||
|
|
b594dec992 | ||
|
|
e93ab06504 | ||
|
|
0581d7b6ad | ||
|
|
00c811d10d | ||
|
|
14d3ec22b0 | ||
|
|
ed57911c7c | ||
|
|
e6cc2c55a2 | ||
|
|
4d85f29843 | ||
|
|
44aaca0fb2 | ||
|
|
abd80c1fa8 | ||
|
|
b6592bf9f7 | ||
|
|
573b64f901 | ||
|
|
b39a5a7d82 | ||
|
|
325e2ed6d3 | ||
|
|
e7c11da52b | ||
|
|
5712e19804 | ||
|
|
4b549763e5 | ||
|
|
24151d27f7 | ||
|
|
f3cc8cba0a | ||
|
|
57e7d68092 | ||
|
|
d3622f7bb3 | ||
|
|
20c821e2eb | ||
|
|
7b82ced5e6 |
@@ -511,87 +511,6 @@
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Zariel",
|
||||
"name": "Chris Bannister",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/2213?v=4",
|
||||
"profile": "https://github.com/Zariel",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "C4J3",
|
||||
"name": "Joe",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/13005453?v=4",
|
||||
"profile": "https://github.com/C4J3",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "guillaumearnx",
|
||||
"name": "Guillaume ARNOUX",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/37373941?v=4",
|
||||
"profile": "https://me.garnx.fr",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "dr-carrot",
|
||||
"name": "dr-carrot",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/17272571?v=4",
|
||||
"profile": "https://github.com/dr-carrot",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "gageorsburn",
|
||||
"name": "Gage Orsburn",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4692734?v=4",
|
||||
"profile": "https://github.com/gageorsburn",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "GkhnGRBZ",
|
||||
"name": "GkhnGRBZ",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/127258824?v=4",
|
||||
"profile": "https://github.com/GkhnGRBZ",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "benhaney",
|
||||
"name": "Ben Haney",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/31331498?v=4",
|
||||
"profile": "http://benhaney.com",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Wunderharke",
|
||||
"name": "Wunderharke",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/5105672?v=4",
|
||||
"profile": "https://github.com/Wunderharke",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "methbkts",
|
||||
"name": "Metin Bektas",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/30674934?v=4",
|
||||
"profile": "https://github.com/methbkts",
|
||||
"contributions": [
|
||||
"infra"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/bug.yml
vendored
2
.github/ISSUE_TEMPLATE/bug.yml
vendored
@@ -63,8 +63,6 @@ body:
|
||||
- PostgreSQL
|
||||
label: Database
|
||||
description: Which database backend are you using?
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: device
|
||||
attributes:
|
||||
|
||||
19
.github/workflows/ci.yml
vendored
19
.github/workflows/ci.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
name: Lint & Test Build
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-22.04
|
||||
container: node:22-alpine
|
||||
container: node:20-alpine
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -46,17 +46,12 @@ jobs:
|
||||
build_and_push:
|
||||
name: Build & Publish Docker Images
|
||||
if: github.ref == 'refs/heads/develop' && !contains(github.event.head_commit.message, '[skip ci]')
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- runner: ubuntu-22.04
|
||||
platform: linux/amd64
|
||||
- runner: ubuntu-22.04-arm64
|
||||
platform: linux/arm64
|
||||
runs-on: ${{ matrix.runner }}
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Log in to Docker Hub
|
||||
@@ -80,17 +75,13 @@ jobs:
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
# platforms: linux/amd64,linux/arm64
|
||||
platforms: ${{ matrix.platform }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
build-args: |
|
||||
COMMIT_TAG=${{ github.sha }}
|
||||
tags: |
|
||||
fallenbagel/jellyseerr:develop
|
||||
ghcr.io/${{ env.OWNER_LC }}/jellyseerr:develop
|
||||
cache-from: type=gha,scope=${{ matrix.platform }}
|
||||
cache-to: type=gha,mode=max,scope=${{ matrix.platform }}
|
||||
provenance: false
|
||||
|
||||
discord:
|
||||
name: Send Discord Notification
|
||||
|
||||
2
.github/workflows/cypress.yml
vendored
2
.github/workflows/cypress.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 20
|
||||
- name: Pnpm Setup
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
|
||||
135
.github/workflows/helm.yml
vendored
135
.github/workflows/helm.yml
vendored
@@ -1,135 +0,0 @@
|
||||
name: Release Charts
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
|
||||
jobs:
|
||||
package-helm-chart:
|
||||
name: Package helm chart
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
outputs:
|
||||
has_artifacts: ${{ steps.check-artifacts.outputs.has_artifacts }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install helm
|
||||
uses: azure/setup-helm@v4
|
||||
|
||||
- name: Install Oras
|
||||
uses: oras-project/setup-oras@v1
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Package helm charts
|
||||
run: |
|
||||
mkdir -p ./.cr-release-packages
|
||||
for chart_path in ./charts/*; do
|
||||
if [ -d "$chart_path" ] && [ -f "$chart_path/Chart.yaml" ]; then
|
||||
chart_name=$(grep '^name:' "$chart_path/Chart.yaml" | awk '{print $2}')
|
||||
# get current version
|
||||
current_version=$(grep '^version:' "$chart_path/Chart.yaml" | awk '{print $2}')
|
||||
# try to get current release version
|
||||
set +e
|
||||
oras discover ghcr.io/${GITHUB_REPOSITORY@L}/${chart_name}:${current_version}
|
||||
oras_exit_code=$?
|
||||
set -e
|
||||
|
||||
if [ $oras_exit_code -ne 0 ]; then
|
||||
helm dependency build "$chart_path"
|
||||
helm package "$chart_path" --destination ./.cr-release-packages
|
||||
else
|
||||
echo "No version change for $chart_name. Skipping."
|
||||
fi
|
||||
else
|
||||
echo "Skipping $chart_name: Not a valid Helm chart"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Check if artifacts exist
|
||||
id: check-artifacts
|
||||
run: |
|
||||
if ls .cr-release-packages/* >/dev/null 2>&1; then
|
||||
echo "has_artifacts=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "has_artifacts=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
if: steps.check-artifacts.outputs.has_artifacts == 'true'
|
||||
with:
|
||||
name: artifacts
|
||||
include-hidden-files: true
|
||||
path: .cr-release-packages/
|
||||
|
||||
publish:
|
||||
name: Publish to ghcr.io
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write # needed for pushing to github registry
|
||||
id-token: write # needed for signing the images with GitHub OIDC Token
|
||||
needs: [package-helm-chart]
|
||||
if: needs.package-helm-chart.outputs.has_artifacts == 'true'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install helm
|
||||
uses: azure/setup-helm@v4
|
||||
|
||||
- name: Install Oras
|
||||
uses: oras-project/setup-oras@v1
|
||||
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@v3
|
||||
|
||||
- name: Downloads artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: artifacts
|
||||
path: .cr-release-packages/
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Push charts to GHCR
|
||||
env:
|
||||
COSIGN_YES: true
|
||||
run: |
|
||||
for chart_path in `find .cr-release-packages -name '*.tgz' -print`; do
|
||||
# push chart to OCI
|
||||
chart_release_file=$(basename "$chart_path")
|
||||
chart_name=${chart_release_file%-*}
|
||||
helm push ${chart_path} oci://ghcr.io/${GITHUB_REPOSITORY@L} |& tee helm-push-output.log
|
||||
chart_digest=$(awk -F "[, ]+" '/Digest/{print $NF}' < helm-push-output.log)
|
||||
# sign chart
|
||||
cosign sign "ghcr.io/${GITHUB_REPOSITORY@L}/${chart_name}@${chart_digest}"
|
||||
# push artifacthub-repo.yml to OCI
|
||||
oras push \
|
||||
ghcr.io/${GITHUB_REPOSITORY@L}/${chart_name}:artifacthub.io \
|
||||
--config /dev/null:application/vnd.cncf.artifacthub.config.v1+yaml \
|
||||
charts/$chart_name/artifacthub-repo.yml:application/vnd.cncf.artifacthub.repository-metadata.layer.v1.yaml \
|
||||
|& tee oras-push-output.log
|
||||
artifacthub_digest=$(grep "Digest:" oras-push-output.log | awk '{print $2}')
|
||||
# sign artifacthub-repo.yml
|
||||
cosign sign "ghcr.io/${GITHUB_REPOSITORY@L}/${chart_name}:artifacthub.io@${artifacthub_digest}"
|
||||
done
|
||||
33
.github/workflows/lint-helm-charts.yml
vendored
33
.github/workflows/lint-helm-charts.yml
vendored
@@ -1,33 +0,0 @@
|
||||
name: Lint and Test Charts
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
paths:
|
||||
- '.github/workflows/lint-helm-charts.yml'
|
||||
- 'charts/**'
|
||||
jobs:
|
||||
lint-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Helm
|
||||
uses: azure/setup-helm@v4.2.0
|
||||
- name: Ensure documentation is updated
|
||||
uses: docker://jnorwood/helm-docs:v1.14.2
|
||||
- name: Set up chart-testing
|
||||
uses: helm/chart-testing-action@v2.6.1
|
||||
- name: Run chart-testing (list-changed)
|
||||
id: list-changed
|
||||
run: |
|
||||
changed=$(ct list-changed --target-branch ${{ github.event.repository.default_branch }})
|
||||
if [[ -n "$changed" ]]; then
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
- name: Run chart-testing
|
||||
if: steps.list-changed.outputs.changed == 'true'
|
||||
run: ct lint --target-branch ${{ github.event.repository.default_branch }} --validate-maintainers=false
|
||||
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 20
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
@@ -26,12 +26,6 @@ jobs:
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GH_TOKEN }}
|
||||
- name: Pnpm Setup
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
|
||||
2
.github/workflows/test-docs-deploy.yml
vendored
2
.github/workflows/test-docs-deploy.yml
vendored
@@ -4,7 +4,7 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
paths:
|
||||
path:
|
||||
- 'docs/**'
|
||||
- 'gen-docs/**'
|
||||
|
||||
|
||||
@@ -9,6 +9,3 @@ pnpm-lock.yaml
|
||||
src/assets/
|
||||
public/
|
||||
docs/
|
||||
|
||||
# helm charts
|
||||
**/charts
|
||||
|
||||
@@ -15,11 +15,5 @@ module.exports = {
|
||||
rangeEnd: 0, // default: Infinity
|
||||
},
|
||||
},
|
||||
{
|
||||
files: 'charts/**',
|
||||
options: {
|
||||
rangeEnd: 0, // default: Infinity
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ All help is welcome and greatly appreciated! If you would like to contribute to
|
||||
|
||||
- HTML/Typescript/Javascript editor
|
||||
- [VSCode](https://code.visualstudio.com/) is recommended. Upon opening the project, a few extensions will be automatically recommended for install.
|
||||
- [NodeJS](https://nodejs.org/en/download/) (Node 22.x)
|
||||
- [NodeJS](https://nodejs.org/en/download/) (Node 20.x)
|
||||
- [Pnpm](https://pnpm.io/cli/install)
|
||||
- [Git](https://git-scm.com/downloads)
|
||||
|
||||
@@ -101,46 +101,6 @@ We use [Weblate](https://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-f
|
||||
|
||||
<a href="https://jellyseerr.borgcube.de/engage/jellysseerr/"><img src="https://jellyseerr.borgcube.de/widget/jellyseerr/multi-auto.svg" alt="Translation status" /></a>
|
||||
|
||||
## Migrations
|
||||
|
||||
If you are adding a new feature that requires a database migration, you will need to create 2 migrations: one for SQLite and one for PostgreSQL. Here is how you could do it:
|
||||
|
||||
1. Create a PostgreSQL database or use an existing one:
|
||||
|
||||
```bash
|
||||
sudo docker run --name postgres-jellyseerr -e POSTGRES_PASSWORD=postgres -d -p 127.0.0.1:5432:5432/tcp postgres:latest
|
||||
```
|
||||
|
||||
2. Reset the SQLite database and the PostgreSQL database:
|
||||
|
||||
```bash
|
||||
rm config/db/db.*
|
||||
rm config/settings.*
|
||||
PGPASSWORD=postgres sudo docker exec -it postgres-jellyseerr /usr/bin/psql -h 127.0.0.1 -U postgres -c "DROP DATABASE IF EXISTS jellyseerr;"
|
||||
PGPASSWORD=postgres sudo docker exec -it postgres-jellyseerr /usr/bin/psql -h 127.0.0.1 -U postgres -c "CREATE DATABASE jellyseerr;"
|
||||
```
|
||||
|
||||
3. Checkout the `develop` branch and create the original database for SQLite and PostgreSQL so that TypeORM can automatically generate the migrations:
|
||||
|
||||
```bash
|
||||
git checkout develop
|
||||
pnpm i
|
||||
rm -r .next dist; pnpm build
|
||||
pnpm start
|
||||
DB_TYPE="postgres" DB_USER=postgres DB_PASS=postgres pnpm start
|
||||
```
|
||||
|
||||
(You can shutdown the server once the message "Server ready on 5055" appears)
|
||||
|
||||
4. Let TypeORM generate the migrations:
|
||||
|
||||
```bash
|
||||
git checkout -b your-feature-branch
|
||||
pnpm i
|
||||
pnpm migration:generate server/migration/sqlite/YourMigrationName
|
||||
DB_TYPE="postgres" DB_USER=postgres DB_PASS=postgres pnpm migration:generate server/migration/postgres/YourMigrationName
|
||||
```
|
||||
|
||||
## Attribution
|
||||
|
||||
This contribution guide was inspired by the [Next.js](https://github.com/vercel/next.js), [Radarr](https://github.com/Radarr/Radarr), and [Overseerr](https://github.com/sct/Overseerr) contribution guides.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:22-alpine AS BUILD_IMAGE
|
||||
FROM node:20-alpine AS BUILD_IMAGE
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -36,7 +36,7 @@ RUN touch config/DOCKER
|
||||
RUN echo "{\"commitTag\": \"${COMMIT_TAG}\"}" > committag.json
|
||||
|
||||
|
||||
FROM node:22-alpine
|
||||
FROM node:20-alpine
|
||||
|
||||
# Metadata for Github Package Registry
|
||||
LABEL org.opencontainers.image.source="https://github.com/Fallenbagel/jellyseerr"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:22-alpine
|
||||
FROM node:20-alpine
|
||||
|
||||
COPY . /app
|
||||
WORKDIR /app
|
||||
|
||||
37
README.md
37
README.md
@@ -11,17 +11,17 @@
|
||||
<a href="http://translate.jellyseerr.dev/engage/jellyseerr/"><img src="http://translate.jellyseerr.dev/widget/jellyseerr/jellyseerr-frontend/svg-badge.svg" alt="Translation status" /></a>
|
||||
<a href="https://github.com/fallenbagel/jellyseerr/blob/develop/LICENSE"><img alt="GitHub" src="https://img.shields.io/github/license/fallenbagel/jellyseerr"></a>
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
<a href="#contributors-"><img alt="All Contributors" src="https://img.shields.io/badge/all_contributors-64-orange.svg"/></a>
|
||||
<a href="#contributors-"><img alt="All Contributors" src="https://img.shields.io/badge/all_contributors-48-orange.svg"/></a>
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||
|
||||
**Jellyseerr** is a free and open source software application for managing requests for your media library. It integrates with the media server of your choice: [Jellyfin](https://jellyfin.org), [Plex](https://plex.tv), and [Emby](https://emby.media/). In addition, it integrates with your existing services, such as **[Sonarr](https://sonarr.tv/)**, **[Radarr](https://radarr.video/)**.
|
||||
**Jellyseerr** is a free and open source software application for managing requests for your media library.
|
||||
It is a fork of [Overseerr](https://github.com/sct/overseerr) built to bring support for [Jellyfin](https://github.com/jellyfin/jellyfin) & [Emby](https://github.com/MediaBrowser/Emby) media servers!
|
||||
|
||||
## Current Features
|
||||
|
||||
- Full Jellyfin/Emby/Plex integration including authentication with user import & management.
|
||||
- Support for **PostgreSQL** and **SQLite** databases.
|
||||
- Supports Movies, Shows and Mixed Libraries.
|
||||
- Ability to change email addresses for SMTP purposes.
|
||||
- Full Jellyfin/Emby/Plex integration including authentication with user import & management
|
||||
- Supports Movies, Shows and Mixed Libraries
|
||||
- Ability to change email addresses for smtp purposes
|
||||
- Easy integration with your existing services. Currently, Jellyseerr supports Sonarr and Radarr. More to come!
|
||||
- Jellyfin/Emby/Plex library scan, to keep track of the titles which are already available.
|
||||
- Customizable request system, which allows users to request individual seasons or movies in a friendly, easy-to-use interface.
|
||||
@@ -29,7 +29,8 @@
|
||||
- Granular permission system.
|
||||
- Support for various notification agents.
|
||||
- Mobile-friendly design, for when you need to approve requests on the go!
|
||||
- Support for watchlisting & blacklisting media.
|
||||
|
||||
(Upcoming Features include: Multiple Server Instances, and much more!)
|
||||
|
||||
With more features on the way! Check out our [issue tracker](https://github.com/fallenbagel/jellyseerr/issues) to see the features which have already been requested.
|
||||
|
||||
@@ -146,28 +147,6 @@ Thanks goes to these wonderful people from Overseerr ([emoji key](https://allcon
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/franciscofsales"><img src="https://avatars.githubusercontent.com/u/7977645?v=4?s=100" width="100px;" alt="Francisco Sales"/><br /><sub><b>Francisco Sales</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=franciscofsales" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/myselfolli"><img src="https://avatars.githubusercontent.com/u/37535998?v=4?s=100" width="100px;" alt="Oliver Laing"/><br /><sub><b>Oliver Laing</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=myselfolli" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/M0NsTeRRR"><img src="https://avatars.githubusercontent.com/u/37785089?v=4?s=100" width="100px;" alt="Ludovic Ortega"/><br /><sub><b>Ludovic Ortega</b></sub></a><br /><a href="#security-M0NsTeRRR" title="Security">🛡️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://josephrisk.com"><img src="https://avatars.githubusercontent.com/u/18372584?v=4?s=100" width="100px;" alt="Joseph Risk"/><br /><sub><b>Joseph Risk</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=j0srisk" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Loetwiek"><img src="https://avatars.githubusercontent.com/u/79059734?v=4?s=100" width="100px;" alt="Loetwiek"/><br /><sub><b>Loetwiek</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=Loetwiek" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Fuochi"><img src="https://avatars.githubusercontent.com/u/4720478?v=4?s=100" width="100px;" alt="Fuochi"/><br /><sub><b>Fuochi</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=Fuochi" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/demrich"><img src="https://avatars.githubusercontent.com/u/30092389?v=4?s=100" width="100px;" alt="David Emrich"/><br /><sub><b>David Emrich</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=demrich" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://maxtrier.dk"><img src="https://avatars.githubusercontent.com/u/5898152?v=4?s=100" width="100px;" alt="Max T. Kristiansen"/><br /><sub><b>Max T. Kristiansen</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=maxnatamo" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://damsdev.me"><img src="https://avatars.githubusercontent.com/u/60252259?v=4?s=100" width="100px;" alt="Damien Fajole"/><br /><sub><b>Damien Fajole</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=DamsDev1" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/AhmedNSidd"><img src="https://avatars.githubusercontent.com/u/36286128?v=4?s=100" width="100px;" alt="Ahmed Siddiqui"/><br /><sub><b>Ahmed Siddiqui</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=AhmedNSidd" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Zariel"><img src="https://avatars.githubusercontent.com/u/2213?v=4?s=100" width="100px;" alt="Chris Bannister"/><br /><sub><b>Chris Bannister</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=Zariel" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/C4J3"><img src="https://avatars.githubusercontent.com/u/13005453?v=4?s=100" width="100px;" alt="Joe"/><br /><sub><b>Joe</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=C4J3" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://me.garnx.fr"><img src="https://avatars.githubusercontent.com/u/37373941?v=4?s=100" width="100px;" alt="Guillaume ARNOUX"/><br /><sub><b>Guillaume ARNOUX</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=guillaumearnx" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dr-carrot"><img src="https://avatars.githubusercontent.com/u/17272571?v=4?s=100" width="100px;" alt="dr-carrot"/><br /><sub><b>dr-carrot</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=dr-carrot" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gageorsburn"><img src="https://avatars.githubusercontent.com/u/4692734?v=4?s=100" width="100px;" alt="Gage Orsburn"/><br /><sub><b>Gage Orsburn</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=gageorsburn" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/GkhnGRBZ"><img src="https://avatars.githubusercontent.com/u/127258824?v=4?s=100" width="100px;" alt="GkhnGRBZ"/><br /><sub><b>GkhnGRBZ</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=GkhnGRBZ" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://benhaney.com"><img src="https://avatars.githubusercontent.com/u/31331498?v=4?s=100" width="100px;" alt="Ben Haney"/><br /><sub><b>Ben Haney</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=benhaney" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Wunderharke"><img src="https://avatars.githubusercontent.com/u/5105672?v=4?s=100" width="100px;" alt="Wunderharke"/><br /><sub><b>Wunderharke</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=Wunderharke" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/methbkts"><img src="https://avatars.githubusercontent.com/u/30674934?v=4?s=100" width="100px;" alt="Metin Bektas"/><br /><sub><b>Metin Bektas</b></sub></a><br /><a href="#infra-methbkts" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*.orig
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
# go template
|
||||
*.gotmpl
|
||||
@@ -1,13 +0,0 @@
|
||||
apiVersion: v2
|
||||
kubeVersion: ">=1.23.0-0"
|
||||
name: jellyseerr-chart
|
||||
description: Jellyseerr helm chart for Kubernetes
|
||||
type: application
|
||||
version: 2.0.0
|
||||
appVersion: "2.3.0"
|
||||
maintainers:
|
||||
- name: Jellyseerr
|
||||
url: https://github.com/Fallenbagel/jellyseerr
|
||||
sources:
|
||||
- https://github.com/Fallenbagel/jellyseerr/tree/main/charts/jellyseerr
|
||||
home: https://github.com/Fallenbagel/jellyseerr
|
||||
@@ -1,65 +0,0 @@
|
||||
# jellyseerr-chart
|
||||
|
||||
  
|
||||
|
||||
Jellyseerr helm chart for Kubernetes
|
||||
|
||||
**Homepage:** <https://github.com/Fallenbagel/jellyseerr>
|
||||
|
||||
## Maintainers
|
||||
|
||||
| Name | Email | Url |
|
||||
| ---- | ------ | --- |
|
||||
| Jellyseerr | | <https://github.com/Fallenbagel/jellyseerr> |
|
||||
|
||||
## Source Code
|
||||
|
||||
* <https://github.com/Fallenbagel/jellyseerr/tree/main/charts/jellyseerr>
|
||||
|
||||
## Requirements
|
||||
|
||||
Kubernetes: `>=1.23.0-0`
|
||||
|
||||
## Values
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| affinity | object | `{}` | |
|
||||
| config | object | `{"persistence":{"accessModes":["ReadWriteOnce"],"annotations":{},"name":"","size":"5Gi","volumeName":""}}` | Creating PVC to store configuration |
|
||||
| config.persistence.accessModes | list | `["ReadWriteOnce"]` | Access modes of persistent disk |
|
||||
| config.persistence.annotations | object | `{}` | Annotations for PVCs |
|
||||
| config.persistence.name | string | `""` | Config name |
|
||||
| config.persistence.size | string | `"5Gi"` | Size of persistent disk |
|
||||
| config.persistence.volumeName | string | `""` | Name of the permanent volume to reference in the claim. Can be used to bind to existing volumes. |
|
||||
| extraEnv | list | `[]` | Environment variables to add to the jellyseerr pods |
|
||||
| extraEnvFrom | list | `[]` | Environment variables from secrets or configmaps to add to the jellyseerr pods |
|
||||
| fullnameOverride | string | `""` | |
|
||||
| image.pullPolicy | string | `"IfNotPresent"` | |
|
||||
| image.registry | string | `"ghcr.io"` | |
|
||||
| image.repository | string | `"fallenbagel/jellyseerr"` | |
|
||||
| image.sha | string | `""` | |
|
||||
| image.tag | string | `""` | Overrides the image tag whose default is the chart appVersion. |
|
||||
| imagePullSecrets | list | `[]` | |
|
||||
| ingress.annotations | object | `{}` | |
|
||||
| ingress.enabled | bool | `false` | |
|
||||
| ingress.hosts[0].host | string | `"chart-example.local"` | |
|
||||
| ingress.hosts[0].paths[0].path | string | `"/"` | |
|
||||
| ingress.hosts[0].paths[0].pathType | string | `"ImplementationSpecific"` | |
|
||||
| ingress.ingressClassName | string | `""` | |
|
||||
| ingress.tls | list | `[]` | |
|
||||
| nameOverride | string | `""` | |
|
||||
| nodeSelector | object | `{}` | |
|
||||
| podAnnotations | object | `{}` | |
|
||||
| podLabels | object | `{}` | |
|
||||
| podSecurityContext | object | `{}` | |
|
||||
| replicaCount | int | `1` | |
|
||||
| resources | object | `{}` | |
|
||||
| securityContext | object | `{}` | |
|
||||
| service.port | int | `80` | |
|
||||
| service.type | string | `"ClusterIP"` | |
|
||||
| serviceAccount.annotations | object | `{}` | Annotations to add to the service account |
|
||||
| serviceAccount.automount | bool | `true` | Automatically mount a ServiceAccount's API credentials? |
|
||||
| serviceAccount.create | bool | `true` | Specifies whether a service account should be created |
|
||||
| serviceAccount.name | string | `""` | If not set and create is true, a name is generated using the fullname template |
|
||||
| strategy | object | `{"type":"Recreate"}` | Deployment strategy |
|
||||
| tolerations | list | `[]` | |
|
||||
@@ -1,17 +0,0 @@
|
||||
{{ template "chart.header" . }}
|
||||
|
||||
{{ template "chart.deprecationWarning" . }}
|
||||
|
||||
{{ template "chart.badgesSection" . }}
|
||||
|
||||
{{ template "chart.description" . }}
|
||||
|
||||
{{ template "chart.homepageLine" . }}
|
||||
|
||||
{{ template "chart.maintainersSection" . }}
|
||||
|
||||
{{ template "chart.sourcesSection" . }}
|
||||
|
||||
{{ template "chart.requirementsSection" . }}
|
||||
|
||||
{{ template "chart.valuesSection" . }}
|
||||
@@ -1 +0,0 @@
|
||||
repositoryID: c6b3f2dc-444c-4e37-b397-6a5ff563ee8b
|
||||
@@ -1,5 +0,0 @@
|
||||
***********************************************************************
|
||||
Welcome to {{ .Chart.Name }}
|
||||
Chart version: {{ .Chart.Version }}
|
||||
App version: {{ .Chart.AppVersion }}
|
||||
***********************************************************************
|
||||
@@ -1,70 +0,0 @@
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "jellyseerr.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "jellyseerr.fullname" -}}
|
||||
{{- if .Values.fullnameOverride }}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||
{{- if contains $name .Release.Name }}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "jellyseerr.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "jellyseerr.labels" -}}
|
||||
helm.sh/chart: {{ include "jellyseerr.chart" . }}
|
||||
{{ include "jellyseerr.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/part-of: {{ .Chart.Name }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
{{- define "jellyseerr.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "jellyseerr.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create the name of the service account to use
|
||||
*/}}
|
||||
{{- define "jellyseerr.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
{{- default (include "jellyseerr.fullname" .) .Values.serviceAccount.name }}
|
||||
{{- else }}
|
||||
{{- default "default" .Values.serviceAccount.name }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create the name of the pvc config to use
|
||||
*/}}
|
||||
{{- define "jellyseerr.configPersistenceName" -}}
|
||||
{{- default (printf "%s-config" (include "jellyseerr.fullname" .)) .Values.config.persistence.name }}
|
||||
{{- end }}
|
||||
@@ -1,83 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "jellyseerr.fullname" . }}
|
||||
labels:
|
||||
{{- include "jellyseerr.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
strategy:
|
||||
type: {{ .Values.strategy.type }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "jellyseerr.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
{{- with .Values.podAnnotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "jellyseerr.labels" . | nindent 8 }}
|
||||
{{- with .Values.podLabels }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
serviceAccountName: {{ include "jellyseerr.serviceAccountName" . }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
{{- if .Values.image.sha }}
|
||||
image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}@sha256:{{ .Values.image.sha }}"
|
||||
{{- else }}
|
||||
image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
{{- end }}
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 5055
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
{{- with .Values.extraEnv }}
|
||||
env:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.extraEnvFrom }}
|
||||
envFrom:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /app/config
|
||||
volumes:
|
||||
- name: config
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ include "jellyseerr.configPersistenceName" . }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
@@ -1,41 +0,0 @@
|
||||
{{- if .Values.ingress.enabled -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ include "jellyseerr.fullname" . }}
|
||||
labels:
|
||||
{{- include "jellyseerr.labels" . | nindent 4 }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if .Values.ingress.ingressClassName }}
|
||||
ingressClassName: {{ .Values.ingress.ingressClassName }}
|
||||
{{- end }}
|
||||
{{- if .Values.ingress.tls }}
|
||||
tls:
|
||||
{{- range .Values.ingress.tls }}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
secretName: {{ .secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- range .Values.ingress.hosts }}
|
||||
- host: {{ .host | quote }}
|
||||
http:
|
||||
paths:
|
||||
{{- range .paths }}
|
||||
- path: {{ .path }}
|
||||
pathType: {{ .pathType }}
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "jellyseerr.fullname" $ }}
|
||||
port:
|
||||
number: {{ $.Values.service.port }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -1,20 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: {{ include "jellyseerr.configPersistenceName" . }}
|
||||
labels:
|
||||
{{- include "jellyseerr.labels" . | nindent 4 }}
|
||||
spec:
|
||||
{{- with .Values.config.persistence.accessModes }}
|
||||
accessModes:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- if .Values.config.persistence.volumeName }}
|
||||
volumeName: {{ .Values.config.persistence.volumeName }}
|
||||
{{- end }}
|
||||
{{- with .Values.config.persistence.storageClass }}
|
||||
storageClassName: {{ if (eq "-" .) }}""{{ else }}{{ . }}{{ end }}
|
||||
{{- end }}
|
||||
resources:
|
||||
requests:
|
||||
storage: "{{ .Values.config.persistence.size }}"
|
||||
@@ -1,16 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "jellyseerr.fullname" . }}
|
||||
labels:
|
||||
{{- include "jellyseerr.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
{{- include "jellyseerr.selectorLabels" . | nindent 4 }}
|
||||
ipFamilyPolicy: PreferDualStack
|
||||
@@ -1,13 +0,0 @@
|
||||
{{- if .Values.serviceAccount.create -}}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "jellyseerr.serviceAccountName" . }}
|
||||
labels:
|
||||
{{- include "jellyseerr.labels" . | nindent 4 }}
|
||||
{{- with .Values.serviceAccount.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
automountServiceAccountToken: {{ .Values.serviceAccount.automount }}
|
||||
{{- end }}
|
||||
@@ -1,15 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: "{{ include "jellyseerr.fullname" . }}-test-connection"
|
||||
labels:
|
||||
{{- include "jellyseerr.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
"helm.sh/hook": test
|
||||
spec:
|
||||
containers:
|
||||
- name: wget
|
||||
image: busybox
|
||||
command: ['wget']
|
||||
args: ['{{ include "jellyseerr.fullname" . }}:{{ .Values.service.port }}']
|
||||
restartPolicy: Never
|
||||
@@ -1,101 +0,0 @@
|
||||
replicaCount: 1
|
||||
|
||||
image:
|
||||
registry: ghcr.io
|
||||
repository: fallenbagel/jellyseerr
|
||||
pullPolicy: IfNotPresent
|
||||
# -- Overrides the image tag whose default is the chart appVersion.
|
||||
tag: ""
|
||||
sha: ""
|
||||
|
||||
imagePullSecrets: []
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
|
||||
# -- Deployment strategy
|
||||
strategy:
|
||||
type: Recreate
|
||||
|
||||
# -- Environment variables to add to the jellyseerr pods
|
||||
extraEnv: []
|
||||
# -- Environment variables from secrets or configmaps to add to the jellyseerr pods
|
||||
extraEnvFrom: []
|
||||
|
||||
serviceAccount:
|
||||
# -- Specifies whether a service account should be created
|
||||
create: true
|
||||
# -- Automatically mount a ServiceAccount's API credentials?
|
||||
automount: true
|
||||
# -- Annotations to add to the service account
|
||||
annotations: {}
|
||||
# -- The name of the service account to use.
|
||||
# -- If not set and create is true, a name is generated using the fullname template
|
||||
name: ""
|
||||
|
||||
podAnnotations: {}
|
||||
podLabels: {}
|
||||
|
||||
podSecurityContext: {}
|
||||
# fsGroup: 2000
|
||||
|
||||
securityContext: {}
|
||||
# capabilities:
|
||||
# drop:
|
||||
# - ALL
|
||||
# readOnlyRootFilesystem: true
|
||||
# runAsNonRoot: true
|
||||
# runAsUser: 1000
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 80
|
||||
|
||||
# -- Creating PVC to store configuration
|
||||
config:
|
||||
persistence:
|
||||
# -- Size of persistent disk
|
||||
size: 5Gi
|
||||
# -- Annotations for PVCs
|
||||
annotations: {}
|
||||
# -- Access modes of persistent disk
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
# -- Config name
|
||||
name: ""
|
||||
# -- Name of the permanent volume to reference in the claim.
|
||||
# Can be used to bind to existing volumes.
|
||||
volumeName: ""
|
||||
|
||||
ingress:
|
||||
enabled: false
|
||||
ingressClassName: ""
|
||||
annotations: {}
|
||||
# kubernetes.io/ingress.class: nginx
|
||||
# kubernetes.io/tls-acme: "true"
|
||||
hosts:
|
||||
- host: chart-example.local
|
||||
paths:
|
||||
- path: /
|
||||
pathType: ImplementationSpecific
|
||||
tls: []
|
||||
# - secretName: chart-example-tls
|
||||
# hosts:
|
||||
# - chart-example.local
|
||||
|
||||
resources: {}
|
||||
# We usually recommend not to specify default resources and to leave this as a conscious
|
||||
# choice for the user. This also increases chances charts run on environments with little
|
||||
# resources, such as Minikube. If you do want to specify resources, uncomment the following
|
||||
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
|
||||
# limits:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
# requests:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
|
||||
nodeSelector: {}
|
||||
|
||||
tolerations: []
|
||||
|
||||
affinity: {}
|
||||
@@ -16,15 +16,11 @@
|
||||
"hideAvailable": false,
|
||||
"localLogin": true,
|
||||
"newPlexLogin": true,
|
||||
"discoverRegion": "",
|
||||
"streamingRegion": "",
|
||||
"region": "",
|
||||
"originalLanguage": "",
|
||||
"trustProxy": false,
|
||||
"mediaServerType": 1,
|
||||
"partialRequestsEnabled": true,
|
||||
"enableSpecialEpisodes": false,
|
||||
"forceIpv4First": false,
|
||||
"dnsServers": "",
|
||||
"locale": "en"
|
||||
},
|
||||
"plex": {
|
||||
@@ -103,7 +99,6 @@
|
||||
"options": {
|
||||
"botAPI": "",
|
||||
"chatId": "",
|
||||
"messageThreadId": "",
|
||||
"sendSilently": false
|
||||
}
|
||||
},
|
||||
|
||||
@@ -7,34 +7,31 @@ sidebar_position: 1
|
||||
|
||||
Welcome to the Jellyseerr Documentation.
|
||||
|
||||
**Jellyseerr** is a free and open source software application for managing requests for your media library. It integrates with the media server of your choice: [Jellyfin](https://jellyfin.org), [Plex](https://plex.tv), and [Emby](https://emby.media/). In addition, it integrates with your existing services, such as **[Sonarr](https://sonarr.tv/)**, **[Radarr](https://radarr.video/)**.
|
||||
|
||||
## Features
|
||||
|
||||
- **Full Jellyfin/Emby/Plex integration**. Login and manage user access with Jellyfin/Emby/Plex.
|
||||
- **Syncs to your Jellyfin/Emby/Plex library** to show what titles you already have.
|
||||
- Supports Movies, Shows and Mixed Libraries.
|
||||
- **Integrates with Sonarr and Radarr**. With more services to come in the future.
|
||||
- Optionally set **Override rules** for requests to match with your defined conditions.
|
||||
- **Easy to use request system** allowing users to request individual seasons or movies in a friendly, clean UI.
|
||||
- **Simple request management UI**. Don't dig through the app to approve recent requests.
|
||||
- **Mobile-friendly design**, for when you need to approve requests on the go.
|
||||
- Granular permission system.
|
||||
- Localization into other languages.
|
||||
- Support for **PostgreSQL** and **SQLite** databases.
|
||||
- Support for various notification agents.
|
||||
- Easily **Watchlist** or **Blacklist** media.
|
||||
- Support for PostgreSQL and SQLite databases.
|
||||
- More features to come!
|
||||
|
||||
## Motivation
|
||||
|
||||
The primary motivation for starting Jellyseerr was to bring Jellyfin and Emby support to Overseerr. However, over time, **Jellyseerr** has evolved into its own distinct application with unique features. Designed as a one-stop shop for media requests, it offers a simple, easy-to-use experience for managing requests on Jellyfin, Emby, and Plex servers.
|
||||
The primary motivation for starting this project was to add support for Jellyfin and Emby to Overseerr. As Overseerr is an incredibly performant and easy-to-use application, we wanted to bring that same experience to Jellyfin and Emby users. Thus, **Jellyseerr** was born.
|
||||
|
||||
This application is designed to be a **one-stop-shop** for all your media requests. It is designed to be a **simple, easy-to-use** application that allows users to request media to be added to your Jellyfin/Emby/Plex server.
|
||||
|
||||
## We need your help!
|
||||
|
||||
[Jellyseerr](https://github.com/Fallenbagel/jellyseerr) is an ambitious project where developers/contributors poured a lot of work into, and that builds on top of [Overseerr](https://github.com/sct/overseerr). And we have a lot more to do as well.
|
||||
[Jellyseerr](https://github.com/Fallenbagel/jellyseerr) is a fork of Overseerr, with a heavy focus on Jellyfin and Emby integration.
|
||||
[Overseerr](https://github.com/sct/overseerr) is an ambitious project where the original developers/contributors have already poured a lot of work into, and we wanted to build on top of that.
|
||||
|
||||
We value your feedback and support in identifying and fixing bugs to make Jellyseerr even better. As an open-source project, we welcome contributions from everyone. While Jellyseerr has diverged from Overseerr and evolved into its own unique application, we still encourage contributions to Overseerr, as it played a crucial role in inspiring what Jellyseerr has become today.
|
||||
We also have poured a lot of work into this project, and we have a lot more to do as well. We need your valuable feedback and help to find and fix bugs. Also, with Jellyseerr being an open-source project, anyone is welcome to contribute. We also encourage you to contribute to Overseerr as well.
|
||||
|
||||
Contribution includes building new features, patching bugs, translating the application, or even just writing documentation.
|
||||
|
||||
|
||||
@@ -9,8 +9,6 @@ Jellyseerr supports SQLite and PostgreSQL. The database connection can be config
|
||||
|
||||
## SQLite Options
|
||||
|
||||
If you want to use SQLite, you can simply set the `DB_TYPE` environment variable to `sqlite`. This is the default configuration so even if you don't set any other options, SQLite will be used.
|
||||
|
||||
```dotenv
|
||||
DB_TYPE="sqlite" # Which DB engine to use, either "sqlite" or "postgres". The default is "sqlite".
|
||||
CONFIG_DIRECTORY="config" # (optional) The path to the config directory where the db file is stored. The default is "config".
|
||||
@@ -19,35 +17,17 @@ DB_LOG_QUERIES="false" # (optional) Whether to log the DB queries for debugging.
|
||||
|
||||
## PostgreSQL Options
|
||||
|
||||
### TCP Connection
|
||||
|
||||
If your PostgreSQL server is configured to accept TCP connections, you can specify the host and port using the `DB_HOST` and `DB_PORT` environment variables. This is useful for remote connections where the server uses a network host and port.
|
||||
|
||||
```dotenv
|
||||
DB_TYPE="postgres" # Which DB engine to use, either "sqlite" or "postgres". The default is "sqlite".
|
||||
DB_HOST="localhost" # (optional) The host (URL) of the database. The default is "localhost".
|
||||
DB_TYPE="postgres" # Which DB engine to use, either "sqlite" or "postgres". The default is "sqlite". To use postgres, this needs to be set to "postgres"
|
||||
DB_HOST="localhost" # (optional) The host (url) of the database. The default is "localhost".
|
||||
DB_PORT="5432" # (optional) The port to connect to. The default is "5432".
|
||||
DB_USER= # (required) Username used to connect to the database.
|
||||
DB_PASS= # (required) Password of the user used to connect to the database.
|
||||
DB_NAME="jellyseerr" # (optional) The name of the database to connect to. The default is "jellyseerr".
|
||||
DB_LOG_QUERIES="false" # (optional) Whether to log the DB queries for debugging. The default is "false".
|
||||
```
|
||||
|
||||
### Unix Socket Connection
|
||||
|
||||
If your PostgreSQL server is configured to accept Unix socket connections, you can specify the path to the socket directory using the `DB_SOCKET_PATH` environment variable. This is useful for local connections where the server uses a Unix socket.
|
||||
|
||||
```dotenv
|
||||
DB_TYPE="postgres" # Which DB engine to use, either "sqlite" or "postgres". The default is "sqlite".
|
||||
DB_SOCKET_PATH="/var/run/postgresql" # (required) The path to the PostgreSQL Unix socket directory.
|
||||
DB_USER= # (required) Username used to connect to the database.
|
||||
DB_PASS= # (optional) Password of the user used to connect to the database, depending on the server's authentication configuration.
|
||||
DB_USER= # (required) Username used to connect to the database
|
||||
DB_PASS= # (required) Password of the user used to connect to the database
|
||||
DB_NAME="jellyseerr" # (optional) The name of the database to connect to. The default is "jellyseerr".
|
||||
DB_LOG_QUERIES="false" # (optional) Whether to log the DB queries for debugging. The default is "false".
|
||||
```
|
||||
|
||||
### SSL configuration
|
||||
|
||||
The following options can be used to further configure ssl. Certificates can be provided as a string or a file path, with the string version taking precedence.
|
||||
|
||||
```dotenv
|
||||
@@ -60,7 +40,6 @@ DB_SSL_KEY_FILE= # (optinal) Path to the private key for the connection in PEM f
|
||||
DB_SSL_CERT= # (optional) Certificate chain in pem format for the private key, provided as a string. The default is "".
|
||||
DB_SSL_CERT_FILE= # (optional) Path to certificate chain in pem format for the private key. The default is "".
|
||||
```
|
||||
---
|
||||
|
||||
### Migrating from SQLite to PostgreSQL
|
||||
|
||||
@@ -68,15 +47,10 @@ DB_SSL_CERT_FILE= # (optional) Path to certificate chain in pem format for the p
|
||||
2. Run Jellyseerr to create the tables in the PostgreSQL database
|
||||
3. Stop Jellyseerr
|
||||
4. Run the following command to export the data from the SQLite database and import it into the PostgreSQL database:
|
||||
:::info
|
||||
Edit the postgres connection string to match your setup.
|
||||
|
||||
If you don't have or don't want to use docker, you can build the working pgloader version [in this PR](https://github.com/dimitri/pgloader/pull/1531) from source and use the same options as below.
|
||||
:::
|
||||
:::caution
|
||||
The most recent release of pgloader has an issue quoting the table columns. Use the version in the docker container to avoid this issue.
|
||||
:::
|
||||
- Edit the postgres connection string to match your setup
|
||||
- WARNING: The most recent release of pgloader has an issue quoting the table columns. Use the version in the docker container to avoid this issue.
|
||||
- "I don't have or don't want to use docker" - You can build the working pgloader version [in this PR](https://github.com/dimitri/pgloader/pull/1531) from source and use the same options as below.
|
||||
```bash
|
||||
docker run --rm -v config/db.sqlite3:/db.sqlite3:ro ghcr.io/ralgar/pgloader:pr-1531 pgloader --with "quote identifiers" --with "data only" /db.sqlite3 postgresql://{{DB_USER}}:{{DB_PASS}}@{{DB_HOST}}:{{DB_PORT}}/{{DB_NAME}}
|
||||
docker run --rm -v config/db.sqlite3:/db.sqlite3:ro -v pgloader/pgloader.load:/pgloader.load ghcr.io/ralgar/pgloader:pr-1531 pgloader --with "quote identifiers" --with "data only" /db.sqlite3 postgresql://{{DB_USER}}:{{DB_PASS}}@{{DB_HOST}}:{{DB_PORT}}/{{DB_NAME}}
|
||||
```
|
||||
5. Start Jellyseerr
|
||||
|
||||
@@ -6,15 +6,13 @@ sidebar_position: 2
|
||||
# Build from Source (Advanced)
|
||||
:::warning
|
||||
This method is not recommended for most users. It is intended for advanced users who are familiar with managing their own server infrastructure.
|
||||
|
||||
Refer to [Configuring Databases](/extending-jellyseerr/database-config#postgresql-options) for details on how to configure your database.
|
||||
:::
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
### Prerequisites
|
||||
- [Node.js 22.x](https://nodejs.org/en/download/)
|
||||
- [Node.js 20.x](https://nodejs.org/en/download/)
|
||||
- [Pnpm 9.x](https://pnpm.io/installation)
|
||||
- [Git](https://git-scm.com/downloads)
|
||||
|
||||
@@ -28,7 +26,7 @@ sudo mkdir -p /opt/jellyseerr && cd /opt/jellyseerr
|
||||
```bash
|
||||
git clone https://github.com/Fallenbagel/jellyseerr.git
|
||||
cd jellyseerr
|
||||
git checkout main
|
||||
git checkout develop # by default, you are on the develop branch so this step is not necessary
|
||||
```
|
||||
3. Install the dependencies:
|
||||
```bash
|
||||
@@ -60,6 +58,9 @@ PORT=5055
|
||||
## specify on which interface to listen, by default jellyseerr listens on all interfaces
|
||||
#HOST=127.0.0.1
|
||||
|
||||
## Uncomment if your media server is emby instead of jellyfin.
|
||||
# JELLYFIN_TYPE=emby
|
||||
|
||||
## Uncomment if you want to force Node.js to resolve IPv4 before IPv6 (advanced users only)
|
||||
# FORCE_IPV4_FIRST=true
|
||||
```
|
||||
@@ -202,7 +203,7 @@ cd C:\jellyseerr
|
||||
2. Clone the Jellyseerr repository and checkout the develop branch:
|
||||
```powershell
|
||||
git clone https://github.com/Fallenbagel/jellyseerr.git .
|
||||
git checkout main
|
||||
git checkout develop # by default, you are on the develop branch so this step is not necessary
|
||||
```
|
||||
3. Install the dependencies:
|
||||
```powershell
|
||||
|
||||
@@ -7,8 +7,6 @@ sidebar_position: 1
|
||||
:::info
|
||||
This is the recommended method for most users.
|
||||
Details on how to install Docker can be found on the [official Docker website](https://docs.docker.com/get-docker/).
|
||||
|
||||
Refer to [Configuring Databases](/extending-jellyseerr/database-config#postgresql-options) for details on how to configure your database.
|
||||
:::
|
||||
|
||||
## Unix (Linux, macOS)
|
||||
@@ -147,16 +145,6 @@ Then, create and start the Jellyseerr container:
|
||||
<TabItem value="docker-cli" label="Docker CLI">
|
||||
```bash
|
||||
docker run -d --name jellyseerr -e LOG_LEVEL=debug -e TZ=Asia/Tashkent -p 5055:5055 -v "jellyseerr-data:/app/config" --restart unless-stopped fallenbagel/jellyseerr:latest
|
||||
```
|
||||
|
||||
#### Updating:
|
||||
Pull the latest image:
|
||||
```bash
|
||||
docker compose pull jellyseerr
|
||||
```
|
||||
Then, restart all services defined in the Compose file:
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
</TabItem>
|
||||
|
||||
@@ -179,16 +167,6 @@ services:
|
||||
volumes:
|
||||
jellyseerr-data:
|
||||
external: true
|
||||
```
|
||||
|
||||
#### Updating:
|
||||
Pull the latest image:
|
||||
```bash
|
||||
docker compose pull jellyseerr
|
||||
```
|
||||
Then, restart all services defined in the Compose file:
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
@@ -207,6 +185,3 @@ Docker on Windows works differently than it does on Linux; it runs Docker inside
|
||||
**If you must run Docker on Windows, you should put the `/app/config` directory mount inside the VM and not on the Windows host.** (This also applies to other containers with SQLite databases.)
|
||||
|
||||
Named volumes, like in the example commands above, are automatically mounted inside the VM. Therefore the warning on the setup about the `/app/config` folder being incorrectly mounted page should be ignored.
|
||||
:::
|
||||
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
---
|
||||
title: Kubernetes
|
||||
description: Install Jellyseerr in Kubernetes
|
||||
sidebar_position: 5
|
||||
---
|
||||
# Kubernetes
|
||||
:::info
|
||||
This method is not recommended for most users. It is intended for advanced users who are using Kubernetes.
|
||||
:::
|
||||
|
||||
## Installation
|
||||
```console
|
||||
helm install jellyseerr oci://ghcr.io/fallenbagel/jellyseerr/jellyseerr-chart
|
||||
```
|
||||
Helm values can be found in the Jellyseerr repository under [charts/jellyseerr-chart/README.md](https://github.com/Fallenbagel/jellyseerr/tree/develop/charts/jellyseerr-chart).
|
||||
|
||||
Verify the signature with [cosign](https://docs.sigstore.dev/cosign/system_config/installation/) (replace [tag], with the TAG you want to verify) :
|
||||
```console
|
||||
cosign verify ghcr.io/fallenbagel/jellyseerr/jellyseerr-chart:[tag] --certificate-identity=https://github.com/Fallenbagel/jellyseerr/.github/workflows/helm.yml@refs/heads/main --certificate-oidc-issuer=https://token.ac
|
||||
tions.githubusercontent.com
|
||||
```
|
||||
@@ -6,8 +6,6 @@ sidebar_position: 3
|
||||
|
||||
import { JellyseerrVersion, NixpkgVersion } from '@site/src/components/JellyseerrVersion';
|
||||
import Admonition from '@theme/Admonition';
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
# Nix Package Manager (Advanced)
|
||||
:::info
|
||||
@@ -15,55 +13,22 @@ This method is not recommended for most users. It is intended for advanced users
|
||||
:::
|
||||
|
||||
export const VersionMismatchWarning = () => {
|
||||
let jellyseerrVersion = null;
|
||||
let nixpkgVersions = null;
|
||||
try {
|
||||
jellyseerrVersion = JellyseerrVersion();
|
||||
nixpkgVersions = NixpkgVersion();
|
||||
} catch (err) {
|
||||
return (
|
||||
<Admonition type="error">
|
||||
Failed to load version information. Error: {err.message || JSON.stringify(err)}
|
||||
</Admonition>
|
||||
);
|
||||
}
|
||||
const jellyseerrVersion = JellyseerrVersion();
|
||||
const nixpkgVersion = NixpkgVersion();
|
||||
|
||||
if (!nixpkgVersions || nixpkgVersions.error) {
|
||||
return (
|
||||
<Admonition type="error">
|
||||
Failed to fetch Nixpkg versions: {nixpkgVersions?.error || 'Unknown error'}
|
||||
</Admonition>
|
||||
);
|
||||
}
|
||||
|
||||
const isUnstableUpToDate = jellyseerrVersion === nixpkgVersions.unstable;
|
||||
const isStableUpToDate = jellyseerrVersion === nixpkgVersions.stable;
|
||||
const isUpToDate = jellyseerrVersion === nixpkgVersion;
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isStableUpToDate ? (
|
||||
<Admonition type="warning">
|
||||
The{' '}
|
||||
<a href="https://github.com/NixOS/nixpkgs/blob/nixos-24.11/pkgs/servers/jellyseerr/default.nix#L14">
|
||||
upstream Jellyseerr Nix Package (v{nixpkgVersions.stable})
|
||||
</a>{' '}
|
||||
is not <b>up-to-date</b>. If you want to use <b>Jellyseerr v{jellyseerrVersion}</b>,{' '}
|
||||
{isUnstableUpToDate ? (
|
||||
<>
|
||||
consider using the{' '}
|
||||
<a href="https://github.com/NixOS/nixpkgs/blob/nixos-unstable/pkgs/by-name/je/jellyseerr/package.nix">
|
||||
unstable package
|
||||
</a>{' '}
|
||||
instead.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
you will need to{' '}
|
||||
<a href="#overriding-the-package-derivation">override the package derivation</a>.
|
||||
</>
|
||||
)}
|
||||
</Admonition>
|
||||
) : null}
|
||||
<>
|
||||
{!isUpToDate ? (
|
||||
<Admonition type="warning">
|
||||
The <a href="https://github.com/NixOS/nixpkgs/blob/nixos-unstable/pkgs/servers/jellyseerr/default.nix#L14">upstream Jellyseerr Nix Package (v{nixpkgVersion})</a> is not <b>up-to-date</b>. If you want to use <b>Jellyseerr v{jellyseerrVersion}</b>, you will need to <a href="#overriding-the-package-derivation">override the package derivation</a>.
|
||||
</Admonition>
|
||||
) : (
|
||||
<Admonition type="success">
|
||||
The <a href="https://github.com/NixOS/nixpkgs/blob/nixos-unstable/pkgs/servers/jellyseerr/default.nix#L14">upstream Jellyseerr Nix Package (v{nixpkgVersion})</a> is <b>up-to-date</b> with <b>Jellyseerr v{jellyseerrVersion}</b>.
|
||||
</Admonition>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -83,8 +48,6 @@ To get up and running with jellyseerr using Nix, you can add the following to yo
|
||||
|
||||
If you want more advanced configuration options, you can use the following:
|
||||
|
||||
<Tabs groupId="nixpkg-methods" queryString>
|
||||
<TabItem value="default" label="Default Configurations">
|
||||
```nix
|
||||
{ config, pkgs, ... }:
|
||||
|
||||
@@ -93,20 +56,53 @@ If you want more advanced configuration options, you can use the following:
|
||||
enable = true;
|
||||
port = 5055;
|
||||
openFirewall = true;
|
||||
package = pkgs.jellyseerr; # Use the unstable package if stable is not up-to-date
|
||||
};
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="custom" label="Database Configurations">
|
||||
In order to use postgres, you will need to add override the default module of jellyseerr with the following as the current default module is not compatible with postgres:
|
||||
```nix
|
||||
|
||||
After adding the configuration to your `configuration.nix`, you can run the following command to install jellyseerr:
|
||||
|
||||
```bash
|
||||
nixos-rebuild switch
|
||||
```
|
||||
After rebuild is complete jellyseerr should be running, verify that it is with the following command.
|
||||
```bash
|
||||
systemctl status jellyseerr
|
||||
```
|
||||
|
||||
:::info
|
||||
You can now access Jellyseerr by visiting `http://localhost:5055` in your web browser.
|
||||
:::
|
||||
|
||||
|
||||
|
||||
import CodeBlock from '@theme/CodeBlock';
|
||||
|
||||
## Overriding the package derivation
|
||||
export const VersionMatch = () => {
|
||||
const jellyseerrVersion = JellyseerrVersion();
|
||||
const nixpkgVersion = NixpkgVersion();
|
||||
|
||||
const code = `{ config, pkgs, ... }:
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
nixpkgs.config.packageOverrides = pkgs: {
|
||||
jellyseerr = pkgs.jellyseerr.overrideAttrs (oldAttrs: rec {
|
||||
version = "${jellyseerrVersion}";
|
||||
|
||||
src = pkgs.fetchFromGitHub {
|
||||
rev = "v\${version}";
|
||||
sha256 = pkgs.lib.fakeSha256;
|
||||
};
|
||||
|
||||
offlineCache = pkgs.fetchYarnDeps {
|
||||
sha256 = pkgs.lib.fakeSha256;
|
||||
};
|
||||
});
|
||||
};
|
||||
}`;
|
||||
|
||||
const module = `{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
let
|
||||
cfg = config.services.jellyseerr;
|
||||
@@ -117,65 +113,28 @@ in
|
||||
disabledModules = [ "services/misc/jellyseerr.nix" ];
|
||||
|
||||
options.services.jellyseerr = {
|
||||
enable = mkEnableOption ''Jellyseerr, a requests manager for Jellyfin'';
|
||||
enable = mkEnableOption (mdDoc ''Jellyseerr, a requests manager for Jellyfin'');
|
||||
|
||||
openFirewall = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = ''Open port in the firewall for the Jellyseerr web interface.'';
|
||||
description = mdDoc ''Open port in the firewall for the Jellyseerr web interface.'';
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 5055;
|
||||
description = ''The port which the Jellyseerr web UI should listen to.'';
|
||||
description = mdDoc ''The port which the Jellyseerr web UI should listen to.'';
|
||||
};
|
||||
|
||||
package = mkOption {
|
||||
type = types.package;
|
||||
default = pkgs.jellyseerr;
|
||||
defaultText = literalExpression "pkgs.jellyseerr";
|
||||
description = ''
|
||||
Jellyseerr package to use.
|
||||
'';
|
||||
};
|
||||
|
||||
databaseConfig = mkOption {
|
||||
type = types.attrsOf types.str;
|
||||
default = {
|
||||
type = "sqlite";
|
||||
configDirectory = "config";
|
||||
logQueries = "false";
|
||||
type = types.package;
|
||||
default = pkgs.jellyseerr;
|
||||
defaultText = literalExpression "pkgs.jellyseerr";
|
||||
description = lib.mdDoc ''
|
||||
Jellyseerr package to use.
|
||||
'';
|
||||
};
|
||||
description = ''
|
||||
Database configuration. For "sqlite", only "type", "configDirectory", and "logQueries" are relevant.
|
||||
For "postgres", include host, port, user, pass, name, and optionally socket.
|
||||
Example:
|
||||
{
|
||||
type = "postgres";
|
||||
socket = "/run/postgresql";
|
||||
user = "jellyseerr";
|
||||
name = "jellyseerr";
|
||||
logQueries = "false";
|
||||
}
|
||||
or
|
||||
{
|
||||
type = "postgres";
|
||||
host = "localhost";
|
||||
port = "5432";
|
||||
user = "dbuser";
|
||||
pass = "password";
|
||||
name = "jellyseerr";
|
||||
logQueries = "false";
|
||||
}
|
||||
or
|
||||
{
|
||||
type = "sqlite";
|
||||
configDirectory = "config";
|
||||
logQueries = "false";
|
||||
}
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
@@ -183,29 +142,14 @@ in
|
||||
description = "Jellyseerr, a requests manager for Jellyfin";
|
||||
after = [ "network.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
environment =
|
||||
let
|
||||
dbConfig = cfg.databaseConfig;
|
||||
in
|
||||
{
|
||||
PORT = toString cfg.port;
|
||||
DB_TYPE = toString dbConfig.type;
|
||||
CONFIG_DIRECTORY = toString dbConfig.configDirectory or "";
|
||||
DB_LOG_QUERIES = toString dbConfig.logQueries;
|
||||
DB_HOST = if dbConfig.type == "postgres" && !(hasAttr "socket" dbConfig) then toString dbConfig.host or "" else "";
|
||||
DB_PORT = if dbConfig.type == "postgres" && !(hasAttr "socket" dbConfig) then toString dbConfig.port or "" else "";
|
||||
DB_SOCKET_PATH = if dbConfig.type == "postgres" && hasAttr "socket" dbConfig then toString dbConfig.socket or "" else "";
|
||||
DB_USER = if dbConfig.type == "postgres" then toString dbConfig.user or "" else "";
|
||||
DB_PASS = if dbConfig.type == "postgres" then toString dbConfig.pass or "" else "";
|
||||
DB_NAME = if dbConfig.type == "postgres" then toString dbConfig.name or "" else "";
|
||||
};
|
||||
environment.PORT = toString cfg.port;
|
||||
serviceConfig = {
|
||||
Type = "exec";
|
||||
StateDirectory = "jellyseerr";
|
||||
WorkingDirectory = "${cfg.package}/libexec/jellyseerr";
|
||||
WorkingDirectory = "\${cfg.package}/libexec/jellyseerr/deps/jellyseerr";
|
||||
DynamicUser = true;
|
||||
ExecStart = "${cfg.package}/bin/jellyseerr";
|
||||
BindPaths = [ "/var/lib/jellyseerr/:${cfg.package}/libexec/jellyseerr/config/" ];
|
||||
ExecStart = "\${cfg.package}/bin/jellyseerr";
|
||||
BindPaths = [ "/var/lib/jellyseerr/:\${cfg.package}/libexec/jellyseerr/deps/jellyseerr/config/" ];
|
||||
Restart = "on-failure";
|
||||
ProtectHome = true;
|
||||
ProtectSystem = "strict";
|
||||
@@ -225,47 +169,57 @@ in
|
||||
};
|
||||
};
|
||||
|
||||
networking.firewall = mkIf cfg.openFirewall { allowedTCPPorts = [ cfg.port ]; };
|
||||
};
|
||||
}
|
||||
```
|
||||
Then, import the module into your `configuration.nix`:
|
||||
```nix
|
||||
{ config, pkgs, ... }:
|
||||
{
|
||||
imports = [ ./modules/jellyseerr.nix ];
|
||||
|
||||
services.jellyseerr = {
|
||||
enable = true;
|
||||
port = 5055;
|
||||
openFirewall = true;
|
||||
package = pkgs.unstable.jellyseerr; # use the unstable package if stable is not up-to-date
|
||||
databaseConfig = {
|
||||
type = "postgres";
|
||||
host = "localhost"; # or socket: "/run/postgresql"
|
||||
port = "5432"; # if using socket, this is not needed
|
||||
user = "jellyseerr";
|
||||
pass = "jellyseerr";
|
||||
name = "jellyseerr";
|
||||
logQueries = "false";
|
||||
networking.firewall = mkIf cfg.openFirewall {
|
||||
allowedTCPPorts = [ cfg.port ];
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
};
|
||||
}`;
|
||||
|
||||
After adding the configuration to your `configuration.nix`, you can run the following command to install jellyseerr:
|
||||
const configuration = `{ config, pkgs, ... }:
|
||||
{
|
||||
imports = [ ./jellyseerr-module.nix ]
|
||||
|
||||
```bash
|
||||
nixos-rebuild switch
|
||||
```
|
||||
After rebuild is complete jellyseerr should be running, verify that it is with the following command.
|
||||
```bash
|
||||
systemctl status jellyseerr
|
||||
```
|
||||
services.jellyseerr = {
|
||||
enable = true;
|
||||
port = 5055;
|
||||
openFirewall = true;
|
||||
package = (pkgs.callPackage (import ../../../pkgs/jellyseerr) { });
|
||||
};
|
||||
}`;
|
||||
|
||||
:::info
|
||||
You can now access Jellyseerr by visiting `http://localhost:5055` in your web browser.
|
||||
:::
|
||||
const isUpToDate = jellyseerrVersion === nixpkgVersion;
|
||||
|
||||
return (
|
||||
<>
|
||||
{isUpToDate ? (
|
||||
<>
|
||||
<p>The latest version of Jellyseerr <strong>({jellyseerrVersion})</strong> and the Jellyseerr nixpkg package version <strong>({nixpkgVersion})</strong> is <strong>up-to-date</strong>.</p>
|
||||
<p>There is no need to override the package derivation.</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p>The latest version of Jellyseerr <strong>({jellyseerrVersion})</strong> and the Jellyseerr nixpkg version <strong>(v{nixpkgVersion})</strong> is <strong>out-of-date</strong>.
|
||||
If you want to use <b>Jellyseerr v{jellyseerrVersion}</b>, you will need to override the package derivation.</p>
|
||||
<p>In order to override the package derivation:</p>
|
||||
<ol>
|
||||
<li style={{ marginBottom: '1rem' }}>Grab the <a href="https://raw.githubusercontent.com/NixOS/nixpkgs/nixos-unstable/pkgs/servers/jellyseerr/default.nix">latest nixpkg derivation for Jellyseerr</a></li>
|
||||
<li style={{ marginBottom: '1rem' }}>Grab the latest <a href="https://raw.githubusercontent.com/Fallenbagel/jellyseerr/main/package.json">package.json</a> for Jellyseerr</li>
|
||||
<li style={{ marginBottom: '1rem' }}>Add it to the same directory as the nixpkg derivation</li>
|
||||
<li style={{ marginBottom: '1rem' }}>Update the `src` and `offlineCache` attributes in the nixpkg derivation:</li>
|
||||
<CodeBlock className="language-nix" style={{ marginBottom: '1rem' }}>{code}</CodeBlock>
|
||||
<Admonition type="tip" style={{ marginBottom: '1rem' }}>You can replace the <b>sha256</b> with the actual hash that <b>nixos-rebuild</b> outputs when you run the command.</Admonition>
|
||||
<li style={{ marginBottom: '1rem' }}>Grab this module and import it in your `configuration.nix`</li>
|
||||
<CodeBlock className="language-nix" style={{ marginBottom: '1rem' }}>{module}</CodeBlock>
|
||||
<Admonition type="tip" style={{ marginBottom: '1rem' }}>We are using a custom module because the upstream module does not have a package option.</Admonition>
|
||||
<li style={{ marginBottom: '1rem' }}>Call the new package in your `configuration.nix`</li>
|
||||
<CodeBlock className="language-nix" style={{ marginBottom: '1rem' }}>{configuration}</CodeBlock>
|
||||
</ol>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
<VersionMatch />
|
||||
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
---
|
||||
title: Troubleshooting
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
## [TMDB] failed to retrieve/fetch XXX
|
||||
|
||||
### Option 1: Change your DNS servers
|
||||
|
||||
This error often comes from your Internet Service Provider (ISP) blocking TMDB API. The ISP may block the DNS resolution to the TMDB API hostname.
|
||||
|
||||
To fix this, you can change your DNS servers to a public DNS service like Google's DNS or Cloudflare's DNS:
|
||||
|
||||
<Tabs groupId="methods" queryString>
|
||||
<TabItem value="docker-cli" label="Docker CLI">
|
||||
|
||||
Add the following to your `docker run` command to use Google's DNS:
|
||||
```bash
|
||||
--dns=8.8.8.8
|
||||
```
|
||||
or for Cloudflare's DNS:
|
||||
```bash
|
||||
--dns=1.1.1.1
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="docker-compose" label="Docker Compose">
|
||||
|
||||
Add the following to your `compose.yaml` to use Google's DNS:
|
||||
```yaml
|
||||
---
|
||||
services:
|
||||
jellyseerr:
|
||||
dns:
|
||||
- 8.8.8.8
|
||||
```
|
||||
or for Cloudflare's DNS:
|
||||
```yaml
|
||||
---
|
||||
services:
|
||||
jellyseerr:
|
||||
dns:
|
||||
- 1.1.1.1
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="windows" label="Windows">
|
||||
|
||||
1. Open the Control Panel.
|
||||
2. Click on Network and Internet.
|
||||
3. Click on Network and Sharing Center.
|
||||
4. Click on Change adapter settings.
|
||||
5. Right-click the network interface connected to the internet and select Properties.
|
||||
6. Select Internet Protocol Version 4 (TCP/IPv4) and click Properties.
|
||||
7. Select Use the following DNS server addresses and enter `8.8.8.8` for Google's DNS or `1.1.1.1` for Cloudflare's DNS.
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="linux" label="Linux">
|
||||
|
||||
1. Open a terminal.
|
||||
2. Edit the `/etc/resolv.conf` file with your favorite text editor.
|
||||
3. Add the following line to use Google's DNS:
|
||||
```bash
|
||||
nameserver 8.8.8.8
|
||||
```
|
||||
or for Cloudflare's DNS:
|
||||
|
||||
```bash
|
||||
nameserver 1.1.1.1
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Option 2: Force IPV4 resolution first
|
||||
|
||||
Sometimes there are configuration issues with IPV6 that prevent the hostname resolution from working correctly.
|
||||
|
||||
You can try to force the resolution to use IPV4 first by setting the `FORCE_IPV4_FIRST` environment variable to `true`:
|
||||
|
||||
<Tabs groupId="methods" queryString>
|
||||
<TabItem value="docker-cli" label="Docker CLI">
|
||||
|
||||
Add the following to your `docker run` command:
|
||||
```bash
|
||||
-e "FORCE_IPV4_FIRST=true"
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="docker-compose" label="Docker Compose">
|
||||
|
||||
Add the following to your `compose.yaml`:
|
||||
```yaml
|
||||
---
|
||||
services:
|
||||
jellyseerr:
|
||||
environment:
|
||||
- FORCE_IPV4_FIRST=true
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Option 3: Use Jellyseerr through a proxy
|
||||
|
||||
If you can't change your DNS servers or force IPV4 resolution, you can use Jellyseerr through a proxy.
|
||||
|
||||
In some places (like China), the ISP blocks not only the DNS resolution but also the connection to the TMDB API.
|
||||
|
||||
You can configure Jellyseerr to use a proxy with the [HTTP(S) Proxy](/using-jellyseerr/settings/general#https-proxy) setting.
|
||||
|
||||
### Option 4: Check that your server can reach TMDB API
|
||||
|
||||
Make sure that your server can reach the TMDB API by running the following command:
|
||||
|
||||
<Tabs groupId="methods" queryString>
|
||||
<TabItem value="docker-cli" label="Docker CLI">
|
||||
|
||||
```bash
|
||||
docker exec -it jellyseerr sh -c "apk update && apk add curl && curl -L https://api.themoviedb.org"
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="docker-compose" label="Docker Compose">
|
||||
|
||||
```bash
|
||||
docker compose exec jellyseerr sh -c "apk update && apk add curl && curl -L https://api.themoviedb.org"
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="linux" label="Linux">
|
||||
|
||||
In a terminal:
|
||||
```bash
|
||||
curl -L https://api.themoviedb.org
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="windows" label="Windows">
|
||||
|
||||
In a PowerShell window:
|
||||
```powershell
|
||||
(Invoke-WebRequest -Uri "https://api.themoviedb.org" -Method Get).Content
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
|
||||
If you can't get a response, then your server can't reach the TMDB API.
|
||||
This is usually due to a network configuration issue or a firewall blocking the connection.
|
||||
@@ -1,93 +0,0 @@
|
||||
---
|
||||
title: Backups
|
||||
description: Understand which data you should back up.
|
||||
sidebar_position: 4
|
||||
---
|
||||
|
||||
# Which data does Jellyseerr save and where?
|
||||
|
||||
## Settings
|
||||
|
||||
All configurations from the **Settings** panel in the Jellyseerr web UI are saved, including integrations with Radarr, Sonarr, Jellyfin, Plex, and notification settings.
|
||||
These settings are stored in the `settings.json` file located in the Jellyseerr data folder.
|
||||
|
||||
## User Data
|
||||
|
||||
Apart from the settings, all other data—including user accounts, media requests, blacklist etc. are stored in the database (either SQLite or PostgreSQL).
|
||||
|
||||
# Backup
|
||||
|
||||
### SQLite
|
||||
|
||||
If your backup system uses filesystem snapshots (such as Kubernetes with Volsync), you can directly back up the Jellyseerr data folder.
|
||||
Otherwise, you need to stop the Jellyseerr application and back up the `config` folder.
|
||||
|
||||
For advanced users, it's possible to back up the database without stopping the application by using the [SQLite CLI](https://www.sqlite.org/download.html). Run the following command to create a backup:
|
||||
|
||||
```bash
|
||||
sqlite3 db/db.sqlite3 ".backup '/tmp/jellyseerr_db.sqlite3.bak'"
|
||||
```
|
||||
|
||||
Then, copy the `/tmp/jellyseerr_dump.sqlite3.bak` file to your desired backup location.
|
||||
|
||||
### PostgreSQL
|
||||
|
||||
You can back up the `config` folder and dump the PostgreSQL database without stopping the Jellyseerr application.
|
||||
|
||||
Install [postgresql-client](https://www.postgresql.org/download/) and run the following command to create a backup (just replace the placeholders):
|
||||
|
||||
:::info
|
||||
Depending on how your PostgreSQL instance is configured, you may need to add these options to the command below.
|
||||
|
||||
-h, --host=HOSTNAME database server host or socket directory
|
||||
|
||||
-p, --port=PORT database server port number
|
||||
:::
|
||||
|
||||
```bash
|
||||
pg_dump -U <database_user> -d <database_name> -f /tmp/jellyseerr_db.sql
|
||||
```
|
||||
|
||||
# Restore
|
||||
|
||||
### SQLite
|
||||
|
||||
After restoring your `db/db.sqlite3` file and, optionally, the `settings.json` file, the `config` folder structure should look like this:
|
||||
|
||||
```
|
||||
.
|
||||
├── cache <-- Optional
|
||||
├── db
|
||||
│ └── db.sqlite3
|
||||
├── logs <-- Optional
|
||||
└── settings.json <-- Optional (required if you want to avoid reconfiguring Jellyseerr)
|
||||
```
|
||||
|
||||
Once the files are restored, start the Jellyseerr application.
|
||||
|
||||
### PostgreSQL
|
||||
|
||||
Install the [PostgreSQL client](https://www.postgresql.org/download/) and restore the PostgreSQL database using the following command (replace the placeholders accordingly):
|
||||
|
||||
:::info
|
||||
Depending on how your PostgreSQL instance is configured, you may need to add these options to the command below.
|
||||
|
||||
-h, --host=HOSTNAME database server host or socket directory
|
||||
|
||||
-p, --port=PORT database server port number
|
||||
:::
|
||||
|
||||
```bash
|
||||
pg_restore -U <database_user> -d <database_name> /tmp/jellyseerr_db.sql
|
||||
```
|
||||
|
||||
Optionally, restore the `settings.json` file. The `config` folder structure should look like this:
|
||||
|
||||
```
|
||||
.
|
||||
├── cache <-- Optional
|
||||
├── logs <-- Optional
|
||||
└── settings.json <-- Optional (required if you want to avoid reconfiguring Jellyseerr)
|
||||
```
|
||||
|
||||
Once the database and files are restored, start the Jellyseerr application.
|
||||
@@ -58,9 +58,9 @@ You should enable this if you are having issues with loading images directly fro
|
||||
|
||||
Set the default display language for Jellyseerr. Users can override this setting in their user settings.
|
||||
|
||||
## Discover Region, Discover Language & Streaming Region
|
||||
## Discover Region & Discover Language
|
||||
|
||||
These settings filter content shown on the "Discover" home page based on regional availability and original language, respectively. The Streaming Region filters the available streaming providers on the media page. Users can override these global settings by configuring these same options in their user settings.
|
||||
These settings filter content shown on the "Discover" home page based on regional availability and original language, respectively. Users can override these global settings by configuring these same options in their user settings.
|
||||
|
||||
## Hide Available Media
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ Users can override the [global display language](/using-jellyseerr/settings/gene
|
||||
|
||||
### Discover Region & Discover Language
|
||||
|
||||
Users can override the [global filter settings](/using-jellyseerr/settings/general#discover-region-discover-language--streaming-region) to suit their own preferences.
|
||||
Users can override the [global filter settings](/using-jellyseerr/settings/general#discover-region--discover-language) to suit their own preferences.
|
||||
|
||||
### Movie Request Limit & Series Request Limit
|
||||
|
||||
|
||||
@@ -47,6 +47,6 @@
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0"
|
||||
"node": ">=18.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,37 +26,25 @@ export const JellyseerrVersion = () => {
|
||||
};
|
||||
|
||||
export const NixpkgVersion = () => {
|
||||
const [versions, setVersions] = useState(null);
|
||||
const [version, setVersion] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchVersion = async () => {
|
||||
try {
|
||||
const unstableUrl =
|
||||
'https://raw.githubusercontent.com/NixOS/nixpkgs/refs/heads/nixos-unstable/pkgs/by-name/je/jellyseerr/package.nix';
|
||||
const stableUrl =
|
||||
'https://raw.githubusercontent.com/NixOS/nixpkgs/refs/heads/nixos-24.11/pkgs/servers/jellyseerr/default.nix';
|
||||
|
||||
const [unstableResponse, stableResponse] = await Promise.all([
|
||||
fetch(unstableUrl),
|
||||
fetch(stableUrl),
|
||||
]);
|
||||
|
||||
const unstableData = await unstableResponse.text();
|
||||
const stableData = await stableResponse.text();
|
||||
const url =
|
||||
'https://raw.githubusercontent.com/NixOS/nixpkgs/nixos-unstable/pkgs/servers/jellyseerr/default.nix';
|
||||
const response = await fetch(url);
|
||||
const data = await response.text();
|
||||
|
||||
const versionRegex = /version\s*=\s*"([^"]+)"/;
|
||||
|
||||
const unstableMatch = unstableData.match(versionRegex);
|
||||
const stableMatch = stableData.match(versionRegex);
|
||||
|
||||
const unstableVersion =
|
||||
unstableMatch && unstableMatch[1] ? unstableMatch[1] : '0.0.0';
|
||||
const stableVersion =
|
||||
stableMatch && stableMatch[1] ? stableMatch[1] : '0.0.0';
|
||||
|
||||
setVersions({ unstable: unstableVersion, stable: stableVersion });
|
||||
const match = data.match(versionRegex);
|
||||
if (match && match[1]) {
|
||||
setVersion(match[1]);
|
||||
} else {
|
||||
setError('0.0.0');
|
||||
}
|
||||
setLoading(false);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
@@ -75,5 +63,5 @@ export const NixpkgVersion = () => {
|
||||
return { error };
|
||||
}
|
||||
|
||||
return versions;
|
||||
return version;
|
||||
};
|
||||
|
||||
@@ -143,12 +143,10 @@ components:
|
||||
properties:
|
||||
locale:
|
||||
type: string
|
||||
discoverRegion:
|
||||
region:
|
||||
type: string
|
||||
originalLanguage:
|
||||
type: string
|
||||
streamingRegion:
|
||||
type: string
|
||||
MainSettings:
|
||||
type: object
|
||||
properties:
|
||||
@@ -188,15 +186,6 @@ components:
|
||||
defaultPermissions:
|
||||
type: number
|
||||
example: 32
|
||||
enableSpecialEpisodes:
|
||||
type: boolean
|
||||
example: false
|
||||
forceIpv4First:
|
||||
type: boolean
|
||||
example: false
|
||||
dnsServers:
|
||||
type: string
|
||||
example: '1.1.1.1'
|
||||
PlexLibrary:
|
||||
type: object
|
||||
properties:
|
||||
@@ -1347,8 +1336,6 @@ components:
|
||||
type: string
|
||||
chatId:
|
||||
type: string
|
||||
messageThreadId:
|
||||
type: string
|
||||
sendSilently:
|
||||
type: boolean
|
||||
PushbulletSettings:
|
||||
@@ -1832,9 +1819,6 @@ components:
|
||||
telegramChatId:
|
||||
type: string
|
||||
nullable: true
|
||||
telegramMessageThreadId:
|
||||
type: string
|
||||
nullable: true
|
||||
telegramSendSilently:
|
||||
type: boolean
|
||||
nullable: true
|
||||
@@ -1948,11 +1932,6 @@ components:
|
||||
type: string
|
||||
native_name:
|
||||
type: string
|
||||
OverrideRule:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
securitySchemes:
|
||||
cookieAuth:
|
||||
type: apiKey
|
||||
@@ -3776,11 +3755,6 @@ paths:
|
||||
type: string
|
||||
enum: [created, updated, requests, displayname]
|
||||
default: created
|
||||
- in: query
|
||||
name: q
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: A JSON array of all users
|
||||
@@ -3897,7 +3871,7 @@ paths:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
jellyfinUserIds:
|
||||
jellyfinIds:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
@@ -5462,13 +5436,6 @@ paths:
|
||||
type: string
|
||||
enum: [added, modified]
|
||||
default: added
|
||||
- in: query
|
||||
name: sortDirection
|
||||
schema:
|
||||
type: string
|
||||
enum: [asc, desc]
|
||||
nullable: true
|
||||
default: desc
|
||||
- in: query
|
||||
name: requestedBy
|
||||
schema:
|
||||
@@ -6989,74 +6956,6 @@ paths:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/WatchProviderDetails'
|
||||
/overrideRule:
|
||||
get:
|
||||
summary: Get override rules
|
||||
description: Returns a list of all override rules with their conditions and settings
|
||||
tags:
|
||||
- overriderule
|
||||
responses:
|
||||
'200':
|
||||
description: Override rules returned
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/OverrideRule'
|
||||
post:
|
||||
summary: Create override rule
|
||||
description: Creates a new Override Rule from the request body.
|
||||
tags:
|
||||
- overriderule
|
||||
responses:
|
||||
'200':
|
||||
description: 'Values were successfully created'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/OverrideRule'
|
||||
/overrideRule/{ruleId}:
|
||||
put:
|
||||
summary: Update override rule
|
||||
description: Updates an Override Rule from the request body.
|
||||
tags:
|
||||
- overriderule
|
||||
parameters:
|
||||
- in: path
|
||||
name: ruleId
|
||||
required: true
|
||||
schema:
|
||||
type: number
|
||||
responses:
|
||||
'200':
|
||||
description: 'Values were successfully updated'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/OverrideRule'
|
||||
delete:
|
||||
summary: Delete override rule by ID
|
||||
description: Deletes the override rule with the provided ruleId.
|
||||
tags:
|
||||
- overriderule
|
||||
parameters:
|
||||
- in: path
|
||||
name: ruleId
|
||||
required: true
|
||||
schema:
|
||||
type: number
|
||||
responses:
|
||||
'200':
|
||||
description: Override rule successfully deleted
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/OverrideRule'
|
||||
security:
|
||||
- cookieAuth: []
|
||||
- apiKey: []
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
"@types/express-session": "1.17.6",
|
||||
"@types/lodash": "4.14.191",
|
||||
"@types/mime": "3",
|
||||
"@types/node": "22.10.5",
|
||||
"@types/node": "20.14.8",
|
||||
"@types/node-schedule": "2.1.0",
|
||||
"@types/nodemailer": "6.4.7",
|
||||
"@types/react": "^18.3.3",
|
||||
@@ -169,7 +169,7 @@
|
||||
"typescript": "4.9.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^22.0.0",
|
||||
"node": "^20.0.0",
|
||||
"pnpm": "^9.0.0"
|
||||
},
|
||||
"overrides": {
|
||||
@@ -235,8 +235,7 @@
|
||||
"COMMIT_TAG": "$GIT_SHA"
|
||||
},
|
||||
"imageNames": [
|
||||
"fallenbagel/jellyseerr",
|
||||
"ghcr.io/fallenbagel/jellyseerr"
|
||||
"fallenbagel/jellyseerr"
|
||||
],
|
||||
"platforms": [
|
||||
"linux/amd64",
|
||||
|
||||
1813
pnpm-lock.yaml
generated
1813
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,3 @@
|
||||
import { MediaServerType } from '@server/constants/server';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import type { RateLimitOptions } from '@server/utils/rateLimit';
|
||||
import rateLimit from '@server/utils/rateLimit';
|
||||
import type NodeCache from 'node-cache';
|
||||
@@ -36,8 +34,6 @@ class ExternalAPI {
|
||||
|
||||
const url = new URL(baseUrl);
|
||||
|
||||
const settings = getSettings();
|
||||
|
||||
this.defaultHeaders = {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
@@ -46,9 +42,6 @@ class ExternalAPI {
|
||||
`${url.username}:${url.password}`
|
||||
).toString('base64')}`,
|
||||
}),
|
||||
...(settings.main.mediaServerType === MediaServerType.EMBY && {
|
||||
'Accept-Encoding': 'gzip',
|
||||
}),
|
||||
...options.headers,
|
||||
};
|
||||
|
||||
@@ -69,13 +62,10 @@ class ExternalAPI {
|
||||
ttl?: number,
|
||||
config?: RequestInit
|
||||
): Promise<T> {
|
||||
const headers = { ...this.defaultHeaders, ...config?.headers };
|
||||
const cacheKey = this.serializeCacheKey(endpoint, {
|
||||
...this.params,
|
||||
...params,
|
||||
headers,
|
||||
});
|
||||
|
||||
const cachedItem = this.cache?.get<T>(cacheKey);
|
||||
if (cachedItem) {
|
||||
return cachedItem;
|
||||
@@ -84,7 +74,10 @@ class ExternalAPI {
|
||||
const url = this.formatUrl(endpoint, params);
|
||||
const response = await this.fetch(url, {
|
||||
...config,
|
||||
headers,
|
||||
headers: {
|
||||
...this.defaultHeaders,
|
||||
...config?.headers,
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
@@ -111,13 +104,10 @@ class ExternalAPI {
|
||||
ttl?: number,
|
||||
config?: RequestInit
|
||||
): Promise<T> {
|
||||
const headers = { ...this.defaultHeaders, ...config?.headers };
|
||||
const cacheKey = this.serializeCacheKey(endpoint, {
|
||||
config: { ...this.params, ...params },
|
||||
headers,
|
||||
data,
|
||||
});
|
||||
|
||||
const cachedItem = this.cache?.get<T>(cacheKey);
|
||||
if (cachedItem) {
|
||||
return cachedItem;
|
||||
@@ -127,7 +117,10 @@ class ExternalAPI {
|
||||
const response = await this.fetch(url, {
|
||||
method: 'POST',
|
||||
...config,
|
||||
headers,
|
||||
headers: {
|
||||
...this.defaultHeaders,
|
||||
...config?.headers,
|
||||
},
|
||||
body: data ? JSON.stringify(data) : undefined,
|
||||
});
|
||||
if (!response.ok) {
|
||||
@@ -155,13 +148,10 @@ class ExternalAPI {
|
||||
ttl?: number,
|
||||
config?: RequestInit
|
||||
): Promise<T> {
|
||||
const headers = { ...this.defaultHeaders, ...config?.headers };
|
||||
const cacheKey = this.serializeCacheKey(endpoint, {
|
||||
config: { ...this.params, ...params },
|
||||
data,
|
||||
headers,
|
||||
});
|
||||
|
||||
const cachedItem = this.cache?.get<T>(cacheKey);
|
||||
if (cachedItem) {
|
||||
return cachedItem;
|
||||
@@ -171,7 +161,10 @@ class ExternalAPI {
|
||||
const response = await this.fetch(url, {
|
||||
method: 'PUT',
|
||||
...config,
|
||||
headers,
|
||||
headers: {
|
||||
...this.defaultHeaders,
|
||||
...config?.headers,
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (!response.ok) {
|
||||
@@ -227,11 +220,9 @@ class ExternalAPI {
|
||||
config?: RequestInit,
|
||||
overwriteBaseUrl?: string
|
||||
): Promise<T> {
|
||||
const headers = { ...this.defaultHeaders, ...config?.headers };
|
||||
const cacheKey = this.serializeCacheKey(endpoint, {
|
||||
...this.params,
|
||||
...params,
|
||||
headers,
|
||||
});
|
||||
const cachedItem = this.cache?.get<T>(cacheKey);
|
||||
|
||||
@@ -246,7 +237,10 @@ class ExternalAPI {
|
||||
const url = this.formatUrl(endpoint, params, overwriteBaseUrl);
|
||||
this.fetch(url, {
|
||||
...config,
|
||||
headers,
|
||||
headers: {
|
||||
...this.defaultHeaders,
|
||||
...config?.headers,
|
||||
},
|
||||
}).then(async (response) => {
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
@@ -269,7 +263,10 @@ class ExternalAPI {
|
||||
const url = this.formatUrl(endpoint, params, overwriteBaseUrl);
|
||||
const response = await this.fetch(url, {
|
||||
...config,
|
||||
headers,
|
||||
headers: {
|
||||
...this.defaultHeaders,
|
||||
...config?.headers,
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
@@ -289,14 +286,6 @@ class ExternalAPI {
|
||||
return data;
|
||||
}
|
||||
|
||||
protected removeCache(endpoint: string, options?: Record<string, string>) {
|
||||
const cacheKey = this.serializeCacheKey(endpoint, {
|
||||
...this.params,
|
||||
...options,
|
||||
});
|
||||
this.cache?.del(cacheKey);
|
||||
}
|
||||
|
||||
private formatUrl(
|
||||
endpoint: string,
|
||||
params?: Record<string, string>,
|
||||
@@ -321,13 +310,13 @@ class ExternalAPI {
|
||||
|
||||
private serializeCacheKey(
|
||||
endpoint: string,
|
||||
options?: Record<string, unknown>
|
||||
params?: Record<string, unknown>
|
||||
) {
|
||||
if (!options) {
|
||||
if (!params) {
|
||||
return `${this.baseUrl}${endpoint}`;
|
||||
}
|
||||
|
||||
return `${this.baseUrl}${endpoint}${JSON.stringify(options)}`;
|
||||
return `${this.baseUrl}${endpoint}${JSON.stringify(params)}`;
|
||||
}
|
||||
|
||||
private async getDataFromResponse(response: Response) {
|
||||
|
||||
@@ -128,7 +128,7 @@ class RottenTomatoes extends ExternalAPI {
|
||||
movie = contentResults.hits.find((movie) => movie.title === name);
|
||||
}
|
||||
|
||||
if (!movie?.rottenTomatoes) {
|
||||
if (!movie) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -230,23 +230,6 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
|
||||
throw new Error(`[Radarr] Failed to remove movie: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
public clearCache = ({
|
||||
tmdbId,
|
||||
externalId,
|
||||
}: {
|
||||
tmdbId?: number | null;
|
||||
externalId?: number | null;
|
||||
}) => {
|
||||
if (tmdbId) {
|
||||
this.removeCache('/movie/lookup', {
|
||||
term: `tmdb:${tmdbId}`,
|
||||
});
|
||||
}
|
||||
if (externalId) {
|
||||
this.removeCache(`/movie/${externalId}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default RadarrAPI;
|
||||
|
||||
@@ -353,30 +353,6 @@ class SonarrAPI extends ServarrBase<{
|
||||
throw new Error(`[Radarr] Failed to remove serie: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
public clearCache = ({
|
||||
tvdbId,
|
||||
externalId,
|
||||
title,
|
||||
}: {
|
||||
tvdbId?: number | null;
|
||||
externalId?: number | null;
|
||||
title?: string | null;
|
||||
}) => {
|
||||
if (tvdbId) {
|
||||
this.removeCache('/series/lookup', {
|
||||
term: `tvdb:${tvdbId}`,
|
||||
});
|
||||
}
|
||||
if (externalId) {
|
||||
this.removeCache(`/series/${externalId}`);
|
||||
}
|
||||
if (title) {
|
||||
this.removeCache('/series/lookup', {
|
||||
term: title,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default SonarrAPI;
|
||||
|
||||
@@ -99,16 +99,16 @@ interface DiscoverTvOptions {
|
||||
}
|
||||
|
||||
class TheMovieDb extends ExternalAPI {
|
||||
private discoverRegion?: string;
|
||||
private region?: string;
|
||||
private originalLanguage?: string;
|
||||
constructor({
|
||||
discoverRegion,
|
||||
region,
|
||||
originalLanguage,
|
||||
}: { discoverRegion?: string; originalLanguage?: string } = {}) {
|
||||
}: { region?: string; originalLanguage?: string } = {}) {
|
||||
super(
|
||||
'https://api.themoviedb.org/3',
|
||||
{
|
||||
api_key: '431a8708161bcd1f1fbe7536137e61ed',
|
||||
api_key: 'db55323b8d3e4154498498a75642b381',
|
||||
},
|
||||
{
|
||||
nodeCache: cacheManager.getCache('tmdb').data,
|
||||
@@ -118,7 +118,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
},
|
||||
}
|
||||
);
|
||||
this.discoverRegion = discoverRegion;
|
||||
this.region = region;
|
||||
this.originalLanguage = originalLanguage;
|
||||
}
|
||||
|
||||
@@ -469,7 +469,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
page: page.toString(),
|
||||
include_adult: includeAdult ? 'true' : 'false',
|
||||
language,
|
||||
region: this.discoverRegion || '',
|
||||
region: this.region || '',
|
||||
with_original_language:
|
||||
originalLanguage && originalLanguage !== 'all'
|
||||
? originalLanguage
|
||||
@@ -541,7 +541,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
sort_by: sortBy,
|
||||
page: page.toString(),
|
||||
language,
|
||||
region: this.discoverRegion || '',
|
||||
region: this.region || '',
|
||||
// Set our release date values, but check if one is set and not the other,
|
||||
// so we can force a past date or a future date. TMDB Requires both values if one is set!
|
||||
'first_air_date.gte':
|
||||
@@ -594,7 +594,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
{
|
||||
page: page.toString(),
|
||||
language,
|
||||
region: this.discoverRegion || '',
|
||||
region: this.region || '',
|
||||
originalLanguage: this.originalLanguage || '',
|
||||
}
|
||||
);
|
||||
@@ -620,7 +620,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
{
|
||||
page: page.toString(),
|
||||
language,
|
||||
region: this.discoverRegion || '',
|
||||
region: this.region || '',
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ export enum ApiErrorCode {
|
||||
InvalidAuthToken = 'INVALID_AUTH_TOKEN',
|
||||
InvalidEmail = 'INVALID_EMAIL',
|
||||
NotAdmin = 'NOT_ADMIN',
|
||||
NoAdminUser = 'NO_ADMIN_USER',
|
||||
SyncErrorGroupedFolders = 'SYNC_ERROR_GROUPED_FOLDERS',
|
||||
SyncErrorNoLibraries = 'SYNC_ERROR_NO_LIBRARIES',
|
||||
Unknown = 'UNKNOWN',
|
||||
|
||||
@@ -68,10 +68,8 @@ const prodConfig: DataSourceOptions = {
|
||||
|
||||
const postgresDevConfig: DataSourceOptions = {
|
||||
type: 'postgres',
|
||||
host: process.env.DB_SOCKET_PATH || process.env.DB_HOST,
|
||||
port: process.env.DB_SOCKET_PATH
|
||||
? undefined
|
||||
: parseInt(process.env.DB_PORT ?? '5432'),
|
||||
host: process.env.DB_HOST,
|
||||
port: parseInt(process.env.DB_PORT ?? '5432'),
|
||||
username: process.env.DB_USER,
|
||||
password: process.env.DB_PASS,
|
||||
database: process.env.DB_NAME ?? 'jellyseerr',
|
||||
@@ -86,10 +84,8 @@ const postgresDevConfig: DataSourceOptions = {
|
||||
|
||||
const postgresProdConfig: DataSourceOptions = {
|
||||
type: 'postgres',
|
||||
host: process.env.DB_SOCKET_PATH || process.env.DB_HOST,
|
||||
port: process.env.DB_SOCKET_PATH
|
||||
? undefined
|
||||
: parseInt(process.env.DB_PORT ?? '5432'),
|
||||
host: process.env.DB_HOST,
|
||||
port: parseInt(process.env.DB_PORT ?? '5432'),
|
||||
username: process.env.DB_USER,
|
||||
password: process.env.DB_PASS,
|
||||
database: process.env.DB_NAME ?? 'jellyseerr',
|
||||
|
||||
@@ -7,14 +7,12 @@ import type {
|
||||
import SonarrAPI from '@server/api/servarr/sonarr';
|
||||
import TheMovieDb from '@server/api/themoviedb';
|
||||
import { ANIME_KEYWORD_ID } from '@server/api/themoviedb/constants';
|
||||
import type { TmdbKeyword } from '@server/api/themoviedb/interfaces';
|
||||
import {
|
||||
MediaRequestStatus,
|
||||
MediaStatus,
|
||||
MediaType,
|
||||
} from '@server/constants/media';
|
||||
import { getRepository } from '@server/datasource';
|
||||
import OverrideRule from '@server/entity/OverrideRule';
|
||||
import type { MediaRequestBody } from '@server/interfaces/api/requestInterfaces';
|
||||
import notificationManager, { Notification } from '@server/lib/notifications';
|
||||
import { Permission } from '@server/lib/permissions';
|
||||
@@ -59,7 +57,6 @@ export class MediaRequest {
|
||||
const mediaRepository = getRepository(Media);
|
||||
const requestRepository = getRepository(MediaRequest);
|
||||
const userRepository = getRepository(User);
|
||||
const settings = getSettings();
|
||||
|
||||
let requestUser = user;
|
||||
|
||||
@@ -208,134 +205,6 @@ export class MediaRequest {
|
||||
}
|
||||
}
|
||||
|
||||
// Apply overrides if the user is not an admin or has the "advanced request" permission
|
||||
const useOverrides = !user.hasPermission([Permission.MANAGE_REQUESTS], {
|
||||
type: 'or',
|
||||
});
|
||||
|
||||
let rootFolder = requestBody.rootFolder;
|
||||
let profileId = requestBody.profileId;
|
||||
let tags = requestBody.tags;
|
||||
|
||||
if (useOverrides) {
|
||||
const defaultRadarrId = requestBody.is4k
|
||||
? settings.radarr.findIndex((r) => r.is4k && r.isDefault)
|
||||
: settings.radarr.findIndex((r) => !r.is4k && r.isDefault);
|
||||
const defaultSonarrId = requestBody.is4k
|
||||
? settings.sonarr.findIndex((s) => s.is4k && s.isDefault)
|
||||
: settings.sonarr.findIndex((s) => !s.is4k && s.isDefault);
|
||||
|
||||
const overrideRuleRepository = getRepository(OverrideRule);
|
||||
const overrideRules = await overrideRuleRepository.find({
|
||||
where:
|
||||
requestBody.mediaType === MediaType.MOVIE
|
||||
? { radarrServiceId: defaultRadarrId }
|
||||
: { sonarrServiceId: defaultSonarrId },
|
||||
});
|
||||
|
||||
const appliedOverrideRules = overrideRules.filter((rule) => {
|
||||
const hasAnimeKeyword =
|
||||
'results' in tmdbMedia.keywords &&
|
||||
tmdbMedia.keywords.results.some(
|
||||
(keyword: TmdbKeyword) => keyword.id === ANIME_KEYWORD_ID
|
||||
);
|
||||
|
||||
// Skip override rules if the media is an anime TV show as anime TV
|
||||
// is handled by default and override rules do not explicitly include
|
||||
// the anime keyword
|
||||
if (
|
||||
requestBody.mediaType === MediaType.TV &&
|
||||
hasAnimeKeyword &&
|
||||
(!rule.keywords ||
|
||||
!rule.keywords.split(',').map(Number).includes(ANIME_KEYWORD_ID))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
rule.users &&
|
||||
!rule.users
|
||||
.split(',')
|
||||
.some((userId) => Number(userId) === requestUser.id)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
rule.genre &&
|
||||
!rule.genre
|
||||
.split(',')
|
||||
.some((genreId) =>
|
||||
tmdbMedia.genres
|
||||
.map((genre) => genre.id)
|
||||
.includes(Number(genreId))
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
rule.language &&
|
||||
!rule.language
|
||||
.split('|')
|
||||
.some((languageId) => languageId === tmdbMedia.original_language)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
rule.keywords &&
|
||||
!rule.keywords.split(',').some((keywordId) => {
|
||||
let keywordList: TmdbKeyword[] = [];
|
||||
|
||||
if ('keywords' in tmdbMedia.keywords) {
|
||||
keywordList = tmdbMedia.keywords.keywords;
|
||||
} else if ('results' in tmdbMedia.keywords) {
|
||||
keywordList = tmdbMedia.keywords.results;
|
||||
}
|
||||
|
||||
return keywordList
|
||||
.map((keyword: TmdbKeyword) => keyword.id)
|
||||
.includes(Number(keywordId));
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// hacky way to prioritize rules
|
||||
// TODO: make this better
|
||||
const prioritizedRule = appliedOverrideRules.sort((a, b) => {
|
||||
const keys: (keyof OverrideRule)[] = ['genre', 'language', 'keywords'];
|
||||
|
||||
const aSpecificity = keys.filter((key) => a[key] !== null).length;
|
||||
const bSpecificity = keys.filter((key) => b[key] !== null).length;
|
||||
|
||||
// Take the rule with the most specific condition first
|
||||
return bSpecificity - aSpecificity;
|
||||
})[0];
|
||||
|
||||
if (prioritizedRule) {
|
||||
if (prioritizedRule.rootFolder) {
|
||||
rootFolder = prioritizedRule.rootFolder;
|
||||
}
|
||||
if (prioritizedRule.profileId) {
|
||||
profileId = prioritizedRule.profileId;
|
||||
}
|
||||
if (prioritizedRule.tags) {
|
||||
tags = [
|
||||
...new Set([
|
||||
...(tags || []),
|
||||
...prioritizedRule.tags.split(',').map((tag) => Number(tag)),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
logger.debug('Override rule applied.', {
|
||||
label: 'Media Request',
|
||||
overrides: prioritizedRule,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (requestBody.mediaType === MediaType.MOVIE) {
|
||||
await mediaRepository.save(media);
|
||||
|
||||
@@ -374,9 +243,9 @@ export class MediaRequest {
|
||||
: undefined,
|
||||
is4k: requestBody.is4k,
|
||||
serverId: requestBody.serverId,
|
||||
profileId: profileId,
|
||||
rootFolder: rootFolder,
|
||||
tags: tags,
|
||||
profileId: requestBody.profileId,
|
||||
rootFolder: requestBody.rootFolder,
|
||||
tags: requestBody.tags,
|
||||
isAutoRequest: options.isAutoRequest ?? false,
|
||||
});
|
||||
|
||||
@@ -386,14 +255,10 @@ export class MediaRequest {
|
||||
const tmdbMediaShow = tmdbMedia as Awaited<
|
||||
ReturnType<typeof tmdb.getTvShow>
|
||||
>;
|
||||
let requestedSeasons =
|
||||
const requestedSeasons =
|
||||
requestBody.seasons === 'all'
|
||||
? tmdbMediaShow.seasons.map((season) => season.season_number)
|
||||
: (requestBody.seasons as number[]);
|
||||
if (!settings.main.enableSpecialEpisodes) {
|
||||
requestedSeasons = requestedSeasons.filter((sn) => sn > 0);
|
||||
}
|
||||
|
||||
let existingSeasons: number[] = [];
|
||||
|
||||
// We need to check existing requests on this title to make sure we don't double up on seasons that were
|
||||
@@ -479,10 +344,10 @@ export class MediaRequest {
|
||||
: undefined,
|
||||
is4k: requestBody.is4k,
|
||||
serverId: requestBody.serverId,
|
||||
profileId: profileId,
|
||||
rootFolder: rootFolder,
|
||||
profileId: requestBody.profileId,
|
||||
rootFolder: requestBody.rootFolder,
|
||||
languageProfileId: requestBody.languageProfileId,
|
||||
tags: tags,
|
||||
tags: requestBody.tags,
|
||||
seasons: finalSeasons.map(
|
||||
(sn) =>
|
||||
new SeasonRequest({
|
||||
@@ -520,6 +385,7 @@ export class MediaRequest {
|
||||
@ManyToOne(() => Media, (media) => media.requests, {
|
||||
eager: true,
|
||||
onDelete: 'CASCADE',
|
||||
nullable: false,
|
||||
})
|
||||
public media: Media;
|
||||
|
||||
@@ -719,15 +585,10 @@ export class MediaRequest {
|
||||
// Do not update the status if the item is already partially available or available
|
||||
media[this.is4k ? 'status4k' : 'status'] !== MediaStatus.AVAILABLE &&
|
||||
media[this.is4k ? 'status4k' : 'status'] !==
|
||||
MediaStatus.PARTIALLY_AVAILABLE &&
|
||||
media[this.is4k ? 'status4k' : 'status'] !== MediaStatus.PROCESSING
|
||||
MediaStatus.PARTIALLY_AVAILABLE
|
||||
) {
|
||||
const statusField = this.is4k ? 'status4k' : 'status';
|
||||
|
||||
await mediaRepository.update(
|
||||
{ id: this.media.id },
|
||||
{ [statusField]: MediaStatus.PROCESSING }
|
||||
);
|
||||
media[this.is4k ? 'status4k' : 'status'] = MediaStatus.PROCESSING;
|
||||
mediaRepository.save(media);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -1010,14 +871,6 @@ export class MediaRequest {
|
||||
);
|
||||
|
||||
this.sendNotification(media, Notification.MEDIA_FAILED);
|
||||
})
|
||||
.finally(() => {
|
||||
radarr.clearCache({
|
||||
tmdbId: movie.id,
|
||||
externalId: this.is4k
|
||||
? media.externalServiceId4k
|
||||
: media.externalServiceId,
|
||||
});
|
||||
});
|
||||
logger.info('Sent request to Radarr', {
|
||||
label: 'Media Request',
|
||||
@@ -1275,23 +1128,19 @@ export class MediaRequest {
|
||||
throw new Error('Media data not found');
|
||||
}
|
||||
|
||||
const updateFields = {
|
||||
[this.is4k ? 'externalServiceId4k' : 'externalServiceId']:
|
||||
sonarrSeries.id,
|
||||
[this.is4k ? 'externalServiceSlug4k' : 'externalServiceSlug']:
|
||||
sonarrSeries.titleSlug,
|
||||
[this.is4k ? 'serviceId4k' : 'serviceId']: sonarrSettings?.id,
|
||||
};
|
||||
media[this.is4k ? 'externalServiceId4k' : 'externalServiceId'] =
|
||||
sonarrSeries.id;
|
||||
media[this.is4k ? 'externalServiceSlug4k' : 'externalServiceSlug'] =
|
||||
sonarrSeries.titleSlug;
|
||||
media[this.is4k ? 'serviceId4k' : 'serviceId'] = sonarrSettings?.id;
|
||||
|
||||
await mediaRepository.update({ id: this.media.id }, updateFields);
|
||||
await mediaRepository.save(media);
|
||||
})
|
||||
.catch(async () => {
|
||||
const requestRepository = getRepository(MediaRequest);
|
||||
|
||||
await requestRepository.update(
|
||||
{ id: this.id },
|
||||
{ status: MediaRequestStatus.FAILED }
|
||||
);
|
||||
this.status = MediaRequestStatus.FAILED;
|
||||
await requestRepository.save(this);
|
||||
|
||||
logger.warn(
|
||||
'Something went wrong sending series request to Sonarr, marking status as FAILED',
|
||||
@@ -1304,15 +1153,6 @@ export class MediaRequest {
|
||||
);
|
||||
|
||||
this.sendNotification(media, Notification.MEDIA_FAILED);
|
||||
})
|
||||
.finally(() => {
|
||||
sonarr.clearCache({
|
||||
tvdbId,
|
||||
externalId: this.is4k
|
||||
? media.externalServiceId4k
|
||||
: media.externalServiceId,
|
||||
title: series.name,
|
||||
});
|
||||
});
|
||||
logger.info('Sent request to Sonarr', {
|
||||
label: 'Media Request',
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity()
|
||||
class OverrideRule {
|
||||
@PrimaryGeneratedColumn()
|
||||
public id: number;
|
||||
|
||||
@Column({ type: 'int', nullable: true })
|
||||
public radarrServiceId?: number;
|
||||
|
||||
@Column({ type: 'int', nullable: true })
|
||||
public sonarrServiceId?: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public users?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public genre?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public language?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public keywords?: string;
|
||||
|
||||
@Column({ type: 'int', nullable: true })
|
||||
public profileId?: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public rootFolder?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public tags?: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
public createdAt: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
public updatedAt: Date;
|
||||
|
||||
constructor(init?: Partial<OverrideRule>) {
|
||||
Object.assign(this, init);
|
||||
}
|
||||
}
|
||||
|
||||
export default OverrideRule;
|
||||
@@ -25,6 +25,7 @@ class Season {
|
||||
|
||||
@ManyToOne(() => Media, (media) => media.seasons, {
|
||||
onDelete: 'CASCADE',
|
||||
nullable: false,
|
||||
})
|
||||
public media: Promise<Media>;
|
||||
|
||||
|
||||
@@ -31,10 +31,7 @@ export class UserSettings {
|
||||
public locale?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public discoverRegion?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public streamingRegion?: string;
|
||||
public region?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public originalLanguage?: string;
|
||||
@@ -60,9 +57,6 @@ export class UserSettings {
|
||||
@Column({ nullable: true })
|
||||
public telegramChatId?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public telegramMessageThreadId?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public telegramSendSilently?: boolean;
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ export class Watchlist implements WatchlistItem {
|
||||
@ManyToOne(() => Media, (media) => media.watchlists, {
|
||||
eager: true,
|
||||
onDelete: 'CASCADE',
|
||||
nullable: false,
|
||||
})
|
||||
public media: Media;
|
||||
|
||||
|
||||
@@ -41,6 +41,11 @@ import path from 'path';
|
||||
import swaggerUi from 'swagger-ui-express';
|
||||
import YAML from 'yamljs';
|
||||
|
||||
if (process.env.forceIpv4First === 'true') {
|
||||
dns.setDefaultResultOrder('ipv4first');
|
||||
net.setDefaultAutoSelectFamily(false);
|
||||
}
|
||||
|
||||
const API_SPEC_PATH = path.join(__dirname, '../overseerr-api.yml');
|
||||
|
||||
logger.info(`Starting Overseerr version ${getAppVersion()}`);
|
||||
@@ -74,18 +79,6 @@ app
|
||||
const settings = await getSettings().load();
|
||||
restartFlag.initializeSettings(settings.main);
|
||||
|
||||
// Check if we force IPv4 first
|
||||
if (process.env.forceIpv4First === 'true' || settings.main.forceIpv4First) {
|
||||
dns.setDefaultResultOrder('ipv4first');
|
||||
net.setDefaultAutoSelectFamily(false);
|
||||
}
|
||||
|
||||
if (settings.main.dnsServers.trim() !== '') {
|
||||
dns.setServers(
|
||||
settings.main.dnsServers.split(',').map((server) => server.trim())
|
||||
);
|
||||
}
|
||||
|
||||
// Register HTTP proxy
|
||||
if (settings.main.proxy.enabled) {
|
||||
await createCustomProxyAgent(settings.main.proxy);
|
||||
|
||||
@@ -5,7 +5,6 @@ export interface GenreSliderItem {
|
||||
}
|
||||
|
||||
export interface WatchlistItem {
|
||||
id: number;
|
||||
ratingKey: string;
|
||||
tmdbId: number;
|
||||
mediaType: 'movie' | 'tv';
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import type OverrideRule from '@server/entity/OverrideRule';
|
||||
|
||||
export type OverrideRuleResultsResponse = OverrideRule[];
|
||||
@@ -32,12 +32,10 @@ export interface PublicSettingsResponse {
|
||||
localLogin: boolean;
|
||||
movie4kEnabled: boolean;
|
||||
series4kEnabled: boolean;
|
||||
discoverRegion: string;
|
||||
streamingRegion: string;
|
||||
region: string;
|
||||
originalLanguage: string;
|
||||
mediaServerType: number;
|
||||
partialRequestsEnabled: boolean;
|
||||
enableSpecialEpisodes: boolean;
|
||||
cacheImages: boolean;
|
||||
vapidPublic: string;
|
||||
enablePushRegistration: boolean;
|
||||
|
||||
@@ -5,8 +5,7 @@ export interface UserSettingsGeneralResponse {
|
||||
email?: string;
|
||||
discordId?: string;
|
||||
locale?: string;
|
||||
discoverRegion?: string;
|
||||
streamingRegion?: string;
|
||||
region?: string;
|
||||
originalLanguage?: string;
|
||||
movieQuotaLimit?: number;
|
||||
movieQuotaDays?: number;
|
||||
@@ -34,7 +33,6 @@ export interface UserSettingsNotificationsResponse {
|
||||
telegramEnabled?: boolean;
|
||||
telegramBotUsername?: string;
|
||||
telegramChatId?: string;
|
||||
telegramMessageThreadId?: string;
|
||||
telegramSendSilently?: boolean;
|
||||
webPushEnabled?: boolean;
|
||||
notificationTypes: Partial<NotificationAgentTypes>;
|
||||
|
||||
@@ -17,7 +17,6 @@ interface TelegramMessagePayload {
|
||||
text: string;
|
||||
parse_mode: string;
|
||||
chat_id: string;
|
||||
message_thread_id: string;
|
||||
disable_notification: boolean;
|
||||
}
|
||||
|
||||
@@ -26,7 +25,6 @@ interface TelegramPhotoPayload {
|
||||
caption: string;
|
||||
parse_mode: string;
|
||||
chat_id: string;
|
||||
message_thread_id: string;
|
||||
disable_notification: boolean;
|
||||
}
|
||||
|
||||
@@ -184,7 +182,6 @@ class TelegramAgent
|
||||
body: JSON.stringify({
|
||||
...notificationPayload,
|
||||
chat_id: settings.options.chatId,
|
||||
message_thread_id: settings.options.messageThreadId,
|
||||
disable_notification: !!settings.options.sendSilently,
|
||||
} as TelegramMessagePayload | TelegramPhotoPayload),
|
||||
});
|
||||
@@ -236,8 +233,6 @@ class TelegramAgent
|
||||
body: JSON.stringify({
|
||||
...notificationPayload,
|
||||
chat_id: payload.notifyUser.settings.telegramChatId,
|
||||
message_thread_id:
|
||||
payload.notifyUser.settings.telegramMessageThreadId,
|
||||
disable_notification:
|
||||
!!payload.notifyUser.settings.telegramSendSilently,
|
||||
} as TelegramMessagePayload | TelegramPhotoPayload),
|
||||
@@ -301,7 +296,6 @@ class TelegramAgent
|
||||
body: JSON.stringify({
|
||||
...notificationPayload,
|
||||
chat_id: user.settings.telegramChatId,
|
||||
message_thread_id: user.settings.telegramMessageThreadId,
|
||||
disable_notification: !!user.settings?.telegramSendSilently,
|
||||
} as TelegramMessagePayload | TelegramPhotoPayload),
|
||||
});
|
||||
|
||||
@@ -210,27 +210,14 @@ class JellyfinScanner {
|
||||
return;
|
||||
}
|
||||
|
||||
if (metadata.ProviderIds.Tmdb) {
|
||||
try {
|
||||
tvShow = await this.tmdb.getTvShow({
|
||||
tvId: Number(metadata.ProviderIds.Tmdb),
|
||||
});
|
||||
} catch {
|
||||
this.log('Unable to find TMDb ID for this title.', 'debug', {
|
||||
jellyfinitem,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!tvShow && metadata.ProviderIds.Tvdb) {
|
||||
try {
|
||||
tvShow = await this.tmdb.getShowByTvdbId({
|
||||
tvdbId: Number(metadata.ProviderIds.Tvdb),
|
||||
});
|
||||
} catch {
|
||||
this.log('Unable to find TVDb ID for this title.', 'debug', {
|
||||
jellyfinitem,
|
||||
});
|
||||
}
|
||||
if (metadata.ProviderIds.Tvdb) {
|
||||
tvShow = await this.tmdb.getShowByTvdbId({
|
||||
tvdbId: Number(metadata.ProviderIds.Tvdb),
|
||||
});
|
||||
} else if (metadata.ProviderIds.Tmdb) {
|
||||
tvShow = await this.tmdb.getTvShow({
|
||||
tvId: Number(metadata.ProviderIds.Tmdb),
|
||||
});
|
||||
}
|
||||
|
||||
if (tvShow) {
|
||||
@@ -504,13 +491,7 @@ class JellyfinScanner {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.log(
|
||||
`No information found for the show: ${metadata.Name}`,
|
||||
'debug',
|
||||
{
|
||||
jellyfinitem,
|
||||
}
|
||||
);
|
||||
this.log(`failed show: ${metadata.Name}`);
|
||||
}
|
||||
} catch (e) {
|
||||
this.log(
|
||||
|
||||
@@ -277,13 +277,8 @@ class PlexScanner
|
||||
|
||||
const seasons = tvShow.seasons;
|
||||
const processableSeasons: ProcessableSeason[] = [];
|
||||
const settings = getSettings();
|
||||
|
||||
const filteredSeasons = settings.main.enableSpecialEpisodes
|
||||
? seasons
|
||||
: seasons.filter((sn) => sn.season_number !== 0);
|
||||
|
||||
for (const season of filteredSeasons) {
|
||||
for (const season of seasons) {
|
||||
const matchedPlexSeason = metadata.Children?.Metadata.find(
|
||||
(md) => Number(md.index) === season.season_number
|
||||
);
|
||||
|
||||
@@ -102,12 +102,9 @@ class SonarrScanner
|
||||
}
|
||||
|
||||
const tmdbId = tvShow.id;
|
||||
const settings = getSettings();
|
||||
|
||||
const filteredSeasons = sonarrSeries.seasons.filter(
|
||||
(sn) =>
|
||||
tvShow.seasons.find((s) => s.season_number === sn.seasonNumber) &&
|
||||
(!settings.main.enableSpecialEpisodes ? sn.seasonNumber !== 0 : true)
|
||||
const filteredSeasons = sonarrSeries.seasons.filter((sn) =>
|
||||
tvShow.seasons.find((s) => s.season_number === sn.seasonNumber)
|
||||
);
|
||||
|
||||
for (const season of filteredSeasons) {
|
||||
|
||||
@@ -76,7 +76,6 @@ export interface DVRSettings {
|
||||
syncEnabled: boolean;
|
||||
preventSearch: boolean;
|
||||
tagRequests: boolean;
|
||||
overrideRule: number[];
|
||||
}
|
||||
|
||||
export interface RadarrSettings extends DVRSettings {
|
||||
@@ -125,15 +124,11 @@ export interface MainSettings {
|
||||
hideAvailable: boolean;
|
||||
localLogin: boolean;
|
||||
newPlexLogin: boolean;
|
||||
discoverRegion: string;
|
||||
streamingRegion: string;
|
||||
region: string;
|
||||
originalLanguage: string;
|
||||
trustProxy: boolean;
|
||||
mediaServerType: number;
|
||||
partialRequestsEnabled: boolean;
|
||||
enableSpecialEpisodes: boolean;
|
||||
forceIpv4First: boolean;
|
||||
dnsServers: string;
|
||||
locale: string;
|
||||
proxy: ProxySettings;
|
||||
}
|
||||
@@ -149,15 +144,13 @@ interface FullPublicSettings extends PublicSettings {
|
||||
localLogin: boolean;
|
||||
movie4kEnabled: boolean;
|
||||
series4kEnabled: boolean;
|
||||
discoverRegion: string;
|
||||
streamingRegion: string;
|
||||
region: string;
|
||||
originalLanguage: string;
|
||||
mediaServerType: number;
|
||||
jellyfinExternalHost?: string;
|
||||
jellyfinForgotPasswordUrl?: string;
|
||||
jellyfinServerName?: string;
|
||||
partialRequestsEnabled: boolean;
|
||||
enableSpecialEpisodes: boolean;
|
||||
cacheImages: boolean;
|
||||
vapidPublic: string;
|
||||
enablePushRegistration: boolean;
|
||||
@@ -218,7 +211,6 @@ export interface NotificationAgentTelegram extends NotificationAgentConfig {
|
||||
botUsername?: string;
|
||||
botAPI: string;
|
||||
chatId: string;
|
||||
messageThreadId: string;
|
||||
sendSilently: boolean;
|
||||
};
|
||||
}
|
||||
@@ -341,15 +333,11 @@ class Settings {
|
||||
hideAvailable: false,
|
||||
localLogin: true,
|
||||
newPlexLogin: true,
|
||||
discoverRegion: '',
|
||||
streamingRegion: '',
|
||||
region: '',
|
||||
originalLanguage: '',
|
||||
trustProxy: false,
|
||||
mediaServerType: MediaServerType.NOT_CONFIGURED,
|
||||
partialRequestsEnabled: true,
|
||||
enableSpecialEpisodes: false,
|
||||
forceIpv4First: false,
|
||||
dnsServers: '',
|
||||
locale: 'en',
|
||||
proxy: {
|
||||
enabled: false,
|
||||
@@ -432,7 +420,6 @@ class Settings {
|
||||
options: {
|
||||
botAPI: '',
|
||||
chatId: '',
|
||||
messageThreadId: '',
|
||||
sendSilently: false,
|
||||
},
|
||||
},
|
||||
@@ -589,12 +576,10 @@ class Settings {
|
||||
series4kEnabled: this.data.sonarr.some(
|
||||
(sonarr) => sonarr.is4k && sonarr.isDefault
|
||||
),
|
||||
discoverRegion: this.data.main.discoverRegion,
|
||||
streamingRegion: this.data.main.streamingRegion,
|
||||
region: this.data.main.region,
|
||||
originalLanguage: this.data.main.originalLanguage,
|
||||
mediaServerType: this.main.mediaServerType,
|
||||
partialRequestsEnabled: this.data.main.partialRequestsEnabled,
|
||||
enableSpecialEpisodes: this.data.main.enableSpecialEpisodes,
|
||||
cacheImages: this.data.main.cacheImages,
|
||||
vapidPublic: this.vapidPublic,
|
||||
enablePushRegistration: this.data.notifications.agents.webpush.enabled,
|
||||
@@ -703,9 +688,10 @@ class Settings {
|
||||
}
|
||||
|
||||
public async save(): Promise<void> {
|
||||
const tmp = SETTINGS_PATH + '.tmp';
|
||||
await fs.writeFile(tmp, JSON.stringify(this.data, undefined, ' '));
|
||||
await fs.rename(tmp, SETTINGS_PATH);
|
||||
await fs.writeFile(
|
||||
SETTINGS_PATH,
|
||||
JSON.stringify(this.data, undefined, ' ')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import type { AllSettings } from '@server/lib/settings';
|
||||
|
||||
const migrateRegionSetting = (settings: any): AllSettings => {
|
||||
if (
|
||||
settings.main.discoverRegion !== undefined &&
|
||||
settings.main.streamingRegion !== undefined
|
||||
) {
|
||||
return settings;
|
||||
}
|
||||
|
||||
const oldRegion = settings.main.region;
|
||||
if (oldRegion) {
|
||||
settings.main.discoverRegion = oldRegion;
|
||||
settings.main.streamingRegion = oldRegion;
|
||||
} else {
|
||||
settings.main.discoverRegion = '';
|
||||
settings.main.streamingRegion = 'US';
|
||||
}
|
||||
delete settings.main.region;
|
||||
|
||||
return settings;
|
||||
};
|
||||
|
||||
export default migrateRegionSetting;
|
||||
304
server/migration/postgres/1705599190375-InitialMigration.ts
Normal file
304
server/migration/postgres/1705599190375-InitialMigration.ts
Normal file
@@ -0,0 +1,304 @@
|
||||
import type { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class InitialMigration1705599190375 implements MigrationInterface {
|
||||
name = 'InitialMigration1705599190375';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`create table if not exists session
|
||||
(
|
||||
"expiredAt" bigint,
|
||||
id text,
|
||||
json text
|
||||
);`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`create index if not exists "idx_194703_IDX_28c5d1d16da7908c97c9bc2f74"
|
||||
on session ("expiredAt");`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`create unique index if not exists idx_194703_sqlite_autoindex_session_1
|
||||
on session (id);`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`create table if not exists media
|
||||
(
|
||||
id serial,
|
||||
"mediaType" text,
|
||||
"tmdbId" int,
|
||||
"tvdbId" int,
|
||||
"imdbId" text,
|
||||
status int default '1'::int,
|
||||
status4k int default '1'::int,
|
||||
"createdAt" timestamp with time zone default CURRENT_TIMESTAMP,
|
||||
"updatedAt" timestamp with time zone default CURRENT_TIMESTAMP,
|
||||
"lastSeasonChange" timestamp with time zone default CURRENT_TIMESTAMP,
|
||||
"mediaAddedAt" timestamp with time zone,
|
||||
"serviceId" int,
|
||||
"serviceId4k" int,
|
||||
"externalServiceId" int,
|
||||
"externalServiceId4k" int,
|
||||
"externalServiceSlug" text,
|
||||
"externalServiceSlug4k" text,
|
||||
"ratingKey" text,
|
||||
"ratingKey4k" text,
|
||||
"jellyfinMediaId" text,
|
||||
"jellyfinMediaId4k" text,
|
||||
constraint idx_194722_media_pkey
|
||||
primary key (id)
|
||||
);`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`create table if not exists season
|
||||
(
|
||||
id serial,
|
||||
"seasonNumber" int,
|
||||
status int default '1'::int,
|
||||
"createdAt" timestamp with time zone default CURRENT_TIMESTAMP,
|
||||
"updatedAt" timestamp with time zone default CURRENT_TIMESTAMP,
|
||||
"mediaId" int not null,
|
||||
status4k int default '1'::int,
|
||||
constraint idx_194715_season_pkey
|
||||
primary key (id),
|
||||
foreign key ("mediaId") references media
|
||||
on delete cascade
|
||||
);`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`create index if not exists "idx_194722_IDX_7ff2d11f6a83cb52386eaebe74"
|
||||
on media ("imdbId");`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`create index if not exists "idx_194722_IDX_41a289eb1fa489c1bc6f38d9c3"
|
||||
on media ("tvdbId");`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`create index if not exists "idx_194722_IDX_7157aad07c73f6a6ae3bbd5ef5"
|
||||
on media ("tmdbId");`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`create unique index if not exists idx_194722_sqlite_autoindex_media_1
|
||||
on media ("tvdbId");`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`create table if not exists "user"
|
||||
(
|
||||
id serial,
|
||||
email text,
|
||||
username text,
|
||||
"plexId" int,
|
||||
"plexToken" text,
|
||||
permissions int default '0'::int,
|
||||
avatar text,
|
||||
"createdAt" timestamp with time zone default CURRENT_TIMESTAMP,
|
||||
"updatedAt" timestamp with time zone default CURRENT_TIMESTAMP,
|
||||
password text,
|
||||
"userType" int default '1'::int,
|
||||
"plexUsername" text,
|
||||
"resetPasswordGuid" text,
|
||||
"recoveryLinkExpirationDate" date,
|
||||
"movieQuotaLimit" int,
|
||||
"movieQuotaDays" int,
|
||||
"tvQuotaLimit" int,
|
||||
"tvQuotaDays" int,
|
||||
"jellyfinUsername" text,
|
||||
"jellyfinAuthToken" text,
|
||||
"jellyfinUserId" text,
|
||||
"jellyfinDeviceId" text,
|
||||
constraint idx_194731_user_pkey
|
||||
primary key (id)
|
||||
);`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`create unique index if not exists idx_194731_sqlite_autoindex_user_1
|
||||
on "user" (email);`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`create table if not exists user_push_subscription
|
||||
(
|
||||
id serial,
|
||||
endpoint text,
|
||||
p256dh text,
|
||||
auth text,
|
||||
"userId" int,
|
||||
constraint idx_194740_user_push_subscription_pkey
|
||||
primary key (id),
|
||||
foreign key ("userId") references "user"
|
||||
on delete cascade
|
||||
);`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`create unique index if not exists idx_194740_sqlite_autoindex_user_push_subscription_1
|
||||
on user_push_subscription (auth);`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`create table if not exists issue
|
||||
(
|
||||
id serial,
|
||||
"issueType" int,
|
||||
status int default '1'::int,
|
||||
"problemSeason" int default '0'::int,
|
||||
"problemEpisode" int default '0'::int,
|
||||
"createdAt" timestamp with time zone default CURRENT_TIMESTAMP,
|
||||
"updatedAt" timestamp with time zone default CURRENT_TIMESTAMP,
|
||||
"mediaId" int not null,
|
||||
"createdById" int,
|
||||
"modifiedById" int,
|
||||
constraint idx_194747_issue_pkey
|
||||
primary key (id),
|
||||
foreign key ("modifiedById") references "user"
|
||||
on delete cascade,
|
||||
foreign key ("createdById") references "user"
|
||||
on delete cascade,
|
||||
foreign key ("mediaId") references media
|
||||
on delete cascade
|
||||
);`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`create table if not exists issue_comment
|
||||
(
|
||||
id serial,
|
||||
message text,
|
||||
"createdAt" timestamp with time zone default CURRENT_TIMESTAMP,
|
||||
"updatedAt" timestamp with time zone default CURRENT_TIMESTAMP,
|
||||
"userId" int,
|
||||
"issueId" int,
|
||||
constraint idx_194755_issue_comment_pkey
|
||||
primary key (id),
|
||||
foreign key ("issueId") references issue
|
||||
on delete cascade,
|
||||
foreign key ("userId") references "user"
|
||||
on delete cascade
|
||||
);`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`create table if not exists user_settings
|
||||
(
|
||||
id serial,
|
||||
"notificationTypes" text,
|
||||
"discordId" text,
|
||||
"userId" int,
|
||||
region text,
|
||||
"originalLanguage" text,
|
||||
"telegramChatId" text,
|
||||
"telegramSendSilently" boolean,
|
||||
"pgpKey" text,
|
||||
locale text default ''::text,
|
||||
"pushbulletAccessToken" text,
|
||||
"pushoverApplicationToken" text,
|
||||
"pushoverUserKey" text,
|
||||
"watchlistSyncMovies" boolean,
|
||||
"watchlistSyncTv" boolean,
|
||||
"pushoverSound" varchar,
|
||||
constraint idx_194762_user_settings_pkey
|
||||
primary key (id),
|
||||
foreign key ("userId") references "user"
|
||||
on delete cascade
|
||||
);`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`create unique index if not exists idx_194762_sqlite_autoindex_user_settings_1
|
||||
on user_settings ("userId");`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`create table if not exists media_request
|
||||
(
|
||||
id serial,
|
||||
status int,
|
||||
"createdAt" timestamp with time zone default CURRENT_TIMESTAMP,
|
||||
"updatedAt" timestamp with time zone default CURRENT_TIMESTAMP,
|
||||
type text,
|
||||
"mediaId" int not null,
|
||||
"requestedById" int,
|
||||
"modifiedById" int,
|
||||
is4k boolean default false,
|
||||
"serverId" int,
|
||||
"profileId" int,
|
||||
"rootFolder" text,
|
||||
"languageProfileId" int,
|
||||
tags text,
|
||||
"isAutoRequest" boolean default false,
|
||||
constraint idx_194770_media_request_pkey
|
||||
primary key (id),
|
||||
foreign key ("modifiedById") references "user"
|
||||
on delete set null,
|
||||
foreign key ("requestedById") references "user"
|
||||
on delete cascade,
|
||||
foreign key ("mediaId") references media
|
||||
on delete cascade
|
||||
);`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`create table if not exists season_request
|
||||
(
|
||||
id serial NOT NULL,
|
||||
"seasonNumber" int,
|
||||
status int default '1'::int,
|
||||
"createdAt" timestamp with time zone default now(),
|
||||
"updatedAt" timestamp with time zone default now(),
|
||||
"requestId" int,
|
||||
constraint idx_194709_season_request_pkey
|
||||
primary key (id),
|
||||
foreign key ("requestId") references media_request
|
||||
on delete cascade
|
||||
);`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`create table if not exists discover_slider
|
||||
(
|
||||
id serial,
|
||||
type integer,
|
||||
"order" integer,
|
||||
"isBuiltIn" boolean default false,
|
||||
enabled boolean default true,
|
||||
title text,
|
||||
data text,
|
||||
"createdAt" timestamp with time zone default CURRENT_TIMESTAMP,
|
||||
"updatedAt" timestamp with time zone default CURRENT_TIMESTAMP,
|
||||
constraint idx_194779_discover_slider_pkey
|
||||
primary key (id)
|
||||
);`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`create table if not exists watchlist
|
||||
(
|
||||
id serial,
|
||||
"ratingKey" text,
|
||||
"mediaType" text,
|
||||
title text,
|
||||
"tmdbId" int,
|
||||
"createdAt" timestamp with time zone default CURRENT_TIMESTAMP,
|
||||
"updatedAt" timestamp with time zone default CURRENT_TIMESTAMP,
|
||||
"requestedById" int,
|
||||
"mediaId" int not null,
|
||||
constraint idx_194788_watchlist_pkey
|
||||
primary key (id)
|
||||
);`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`create index if not exists "idx_194788_IDX_939f205946256cc0d2a1ac51a8"
|
||||
on watchlist ("tmdbId");`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`create unique index if not exists idx_194788_sqlite_autoindex_watchlist_1
|
||||
on watchlist ("tmdbId", "requestedById");`
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`drop table if exists session cascade`);
|
||||
await queryRunner.query(`drop table if exists season_request cascade`);
|
||||
await queryRunner.query(`drop table if exists season cascade`);
|
||||
await queryRunner.query(
|
||||
`drop table if exists user_push_subscription cascade`
|
||||
);
|
||||
await queryRunner.query(`drop table if exists issue_comment cascade`);
|
||||
await queryRunner.query(`drop table if exists issue cascade`);
|
||||
await queryRunner.query(`drop table if exists user_settings cascade`);
|
||||
await queryRunner.query(`drop table if exists media_request cascade`);
|
||||
await queryRunner.query(`drop table if exists media cascade`);
|
||||
await queryRunner.query(`drop table if exists "user" cascade`);
|
||||
await queryRunner.query(`drop table if exists discover_slider cascade`);
|
||||
await queryRunner.query(`drop table if exists watchlist cascade`);
|
||||
}
|
||||
}
|
||||
32
server/migration/postgres/1730770837441-AddBlacklist.ts
Normal file
32
server/migration/postgres/1730770837441-AddBlacklist.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddBlacklist1730770837441 implements MigrationInterface {
|
||||
name = 'AddBlacklist1730770837441';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "blacklist"
|
||||
(
|
||||
"id" SERIAL PRIMARY KEY,
|
||||
"mediaType" VARCHAR NOT NULL,
|
||||
"title" VARCHAR,
|
||||
"tmdbId" INTEGER NOT NULL,
|
||||
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
"userId" INTEGER,
|
||||
"mediaId" INTEGER,
|
||||
CONSTRAINT "UQ_6bbafa28411e6046421991ea21c" UNIQUE ("tmdbId", "userId")
|
||||
)`
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_6bbafa28411e6046421991ea21" ON "blacklist" ("tmdbId")`
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`DROP INDEX IF EXISTS "IDX_6bbafa28411e6046421991ea21"`
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS "blacklist"`);
|
||||
}
|
||||
}
|
||||
@@ -1,195 +0,0 @@
|
||||
import type { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class InitialMigration1734786061496 implements MigrationInterface {
|
||||
name = 'InitialMigration1734786061496';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "blacklist" ("id" SERIAL NOT NULL, "mediaType" character varying NOT NULL, "title" character varying, "tmdbId" integer NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "userId" integer, "mediaId" integer, CONSTRAINT "UQ_6bbafa28411e6046421991ea21c" UNIQUE ("tmdbId"), CONSTRAINT "REL_62b7ade94540f9f8d8bede54b9" UNIQUE ("mediaId"), CONSTRAINT "PK_04dc42a96bf0914cda31b579702" PRIMARY KEY ("id"))`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_6bbafa28411e6046421991ea21" ON "blacklist" ("tmdbId") `
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "season_request" ("id" SERIAL NOT NULL, "seasonNumber" integer NOT NULL, "status" integer NOT NULL DEFAULT '1', "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "requestId" integer, CONSTRAINT "PK_4811e502081543bf620f1fa4328" PRIMARY KEY ("id"))`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "media_request" ("id" SERIAL NOT NULL, "status" integer NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "type" character varying NOT NULL, "is4k" boolean NOT NULL DEFAULT false, "serverId" integer, "profileId" integer, "rootFolder" character varying, "languageProfileId" integer, "tags" text, "isAutoRequest" boolean NOT NULL DEFAULT false, "mediaId" integer NOT NULL, "requestedById" integer, "modifiedById" integer, CONSTRAINT "PK_f8334500e8e12db87536558c66c" PRIMARY KEY ("id"))`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "season" ("id" SERIAL NOT NULL, "seasonNumber" integer NOT NULL, "status" integer NOT NULL DEFAULT '1', "status4k" integer NOT NULL DEFAULT '1', "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "mediaId" integer NOT NULL, CONSTRAINT "PK_8ac0d081dbdb7ab02d166bcda9f" PRIMARY KEY ("id"))`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "media" ("id" SERIAL NOT NULL, "mediaType" character varying NOT NULL, "tmdbId" integer NOT NULL, "tvdbId" integer, "imdbId" character varying, "status" integer NOT NULL DEFAULT '1', "status4k" integer NOT NULL DEFAULT '1', "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "lastSeasonChange" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "mediaAddedAt" TIMESTAMP WITH TIME ZONE DEFAULT now(), "serviceId" integer, "serviceId4k" integer, "externalServiceId" integer, "externalServiceId4k" integer, "externalServiceSlug" character varying, "externalServiceSlug4k" character varying, "ratingKey" character varying, "ratingKey4k" character varying, "jellyfinMediaId" character varying, "jellyfinMediaId4k" character varying, CONSTRAINT "UQ_41a289eb1fa489c1bc6f38d9c3c" UNIQUE ("tvdbId"), CONSTRAINT "PK_f4e0fcac36e050de337b670d8bd" PRIMARY KEY ("id"))`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5" ON "media" ("tmdbId") `
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_41a289eb1fa489c1bc6f38d9c3" ON "media" ("tvdbId") `
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_7ff2d11f6a83cb52386eaebe74" ON "media" ("imdbId") `
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "watchlist" ("id" SERIAL NOT NULL, "ratingKey" character varying NOT NULL, "mediaType" character varying NOT NULL, "title" character varying NOT NULL, "tmdbId" integer NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "requestedById" integer, "mediaId" integer NOT NULL, CONSTRAINT "UNIQUE_USER_DB" UNIQUE ("tmdbId", "requestedById"), CONSTRAINT "PK_0c8c0dbcc8d379117138e71ad5b" PRIMARY KEY ("id"))`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_939f205946256cc0d2a1ac51a8" ON "watchlist" ("tmdbId") `
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "user_push_subscription" ("id" SERIAL NOT NULL, "endpoint" character varying NOT NULL, "p256dh" character varying NOT NULL, "auth" character varying NOT NULL, "userId" integer, CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "PK_397020e7be9a4086cc798e0bb63" PRIMARY KEY ("id"))`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "user_settings" ("id" SERIAL NOT NULL, "locale" character varying NOT NULL DEFAULT '', "discoverRegion" character varying, "streamingRegion" character varying, "originalLanguage" character varying, "pgpKey" character varying, "discordId" character varying, "pushbulletAccessToken" character varying, "pushoverApplicationToken" character varying, "pushoverUserKey" character varying, "pushoverSound" character varying, "telegramChatId" character varying, "telegramSendSilently" boolean, "watchlistSyncMovies" boolean, "watchlistSyncTv" boolean, "notificationTypes" text, "userId" integer, CONSTRAINT "REL_986a2b6d3c05eb4091bb8066f7" UNIQUE ("userId"), CONSTRAINT "PK_00f004f5922a0744d174530d639" PRIMARY KEY ("id"))`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "user" ("id" SERIAL NOT NULL, "email" character varying NOT NULL, "plexUsername" character varying, "jellyfinUsername" character varying, "username" character varying, "password" character varying, "resetPasswordGuid" character varying, "recoveryLinkExpirationDate" date, "userType" integer NOT NULL DEFAULT '1', "plexId" integer, "jellyfinUserId" character varying, "jellyfinDeviceId" character varying, "jellyfinAuthToken" character varying, "plexToken" character varying, "permissions" integer NOT NULL DEFAULT '0', "avatar" character varying NOT NULL, "movieQuotaLimit" integer, "movieQuotaDays" integer, "tvQuotaLimit" integer, "tvQuotaDays" integer, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"), CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id"))`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "issue_comment" ("id" SERIAL NOT NULL, "message" text NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "userId" integer, "issueId" integer, CONSTRAINT "PK_2ad05784e2ae661fa409e5e0248" PRIMARY KEY ("id"))`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "issue" ("id" SERIAL NOT NULL, "issueType" integer NOT NULL, "status" integer NOT NULL DEFAULT '1', "problemSeason" integer NOT NULL DEFAULT '0', "problemEpisode" integer NOT NULL DEFAULT '0', "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "mediaId" integer, "createdById" integer, "modifiedById" integer, CONSTRAINT "PK_f80e086c249b9f3f3ff2fd321b7" PRIMARY KEY ("id"))`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "discover_slider" ("id" SERIAL NOT NULL, "type" integer NOT NULL, "order" integer NOT NULL, "isBuiltIn" boolean NOT NULL DEFAULT false, "enabled" boolean NOT NULL DEFAULT true, "title" character varying, "data" character varying, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_20a71a098d04bae448e4d51db23" PRIMARY KEY ("id"))`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "session" ("expiredAt" bigint NOT NULL, "id" character varying(255) NOT NULL, "json" text NOT NULL, CONSTRAINT "PK_f55da76ac1c3ac420f444d2ff11" PRIMARY KEY ("id"))`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_28c5d1d16da7908c97c9bc2f74" ON "session" ("expiredAt") `
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "blacklist" ADD CONSTRAINT "FK_53c1ab62c3e5875bc3ac474823e" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "blacklist" ADD CONSTRAINT "FK_62b7ade94540f9f8d8bede54b99" FOREIGN KEY ("mediaId") REFERENCES "media"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "season_request" ADD CONSTRAINT "FK_6f14737e346d6b27d8e50d2157a" FOREIGN KEY ("requestId") REFERENCES "media_request"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "media_request" ADD CONSTRAINT "FK_a1aa713f41c99e9d10c48da75a0" FOREIGN KEY ("mediaId") REFERENCES "media"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "media_request" ADD CONSTRAINT "FK_6997bee94720f1ecb7f31137095" FOREIGN KEY ("requestedById") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "media_request" ADD CONSTRAINT "FK_f4fc4efa14c3ba2b29c4525fa15" FOREIGN KEY ("modifiedById") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "season" ADD CONSTRAINT "FK_087099b39600be695591da9a49c" FOREIGN KEY ("mediaId") REFERENCES "media"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "watchlist" ADD CONSTRAINT "FK_ae34e6b153a90672eb9dc4857d7" FOREIGN KEY ("requestedById") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "watchlist" ADD CONSTRAINT "FK_6641da8d831b93dfcb429f8b8bc" FOREIGN KEY ("mediaId") REFERENCES "media"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "user_push_subscription" ADD CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "user_settings" ADD CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "issue_comment" ADD CONSTRAINT "FK_707b033c2d0653f75213614789d" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "issue_comment" ADD CONSTRAINT "FK_180710fead1c94ca499c57a7d42" FOREIGN KEY ("issueId") REFERENCES "issue"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "issue" ADD CONSTRAINT "FK_276e20d053f3cff1645803c95d8" FOREIGN KEY ("mediaId") REFERENCES "media"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "issue" ADD CONSTRAINT "FK_10b17b49d1ee77e7184216001e0" FOREIGN KEY ("createdById") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "issue" ADD CONSTRAINT "FK_da88a1019c850d1a7b143ca02e5" FOREIGN KEY ("modifiedById") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "issue" DROP CONSTRAINT "FK_da88a1019c850d1a7b143ca02e5"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "issue" DROP CONSTRAINT "FK_10b17b49d1ee77e7184216001e0"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "issue" DROP CONSTRAINT "FK_276e20d053f3cff1645803c95d8"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "issue_comment" DROP CONSTRAINT "FK_180710fead1c94ca499c57a7d42"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "issue_comment" DROP CONSTRAINT "FK_707b033c2d0653f75213614789d"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "user_settings" DROP CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "user_push_subscription" DROP CONSTRAINT "FK_03f7958328e311761b0de675fbe"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "watchlist" DROP CONSTRAINT "FK_6641da8d831b93dfcb429f8b8bc"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "watchlist" DROP CONSTRAINT "FK_ae34e6b153a90672eb9dc4857d7"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "season" DROP CONSTRAINT "FK_087099b39600be695591da9a49c"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "media_request" DROP CONSTRAINT "FK_f4fc4efa14c3ba2b29c4525fa15"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "media_request" DROP CONSTRAINT "FK_6997bee94720f1ecb7f31137095"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "media_request" DROP CONSTRAINT "FK_a1aa713f41c99e9d10c48da75a0"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "season_request" DROP CONSTRAINT "FK_6f14737e346d6b27d8e50d2157a"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "blacklist" DROP CONSTRAINT "FK_62b7ade94540f9f8d8bede54b99"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "blacklist" DROP CONSTRAINT "FK_53c1ab62c3e5875bc3ac474823e"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_28c5d1d16da7908c97c9bc2f74"`
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "session"`);
|
||||
await queryRunner.query(`DROP TABLE "discover_slider"`);
|
||||
await queryRunner.query(`DROP TABLE "issue"`);
|
||||
await queryRunner.query(`DROP TABLE "issue_comment"`);
|
||||
await queryRunner.query(`DROP TABLE "user"`);
|
||||
await queryRunner.query(`DROP TABLE "user_settings"`);
|
||||
await queryRunner.query(`DROP TABLE "user_push_subscription"`);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_939f205946256cc0d2a1ac51a8"`
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "watchlist"`);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_7ff2d11f6a83cb52386eaebe74"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_41a289eb1fa489c1bc6f38d9c3"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_7157aad07c73f6a6ae3bbd5ef5"`
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "media"`);
|
||||
await queryRunner.query(`DROP TABLE "season"`);
|
||||
await queryRunner.query(`DROP TABLE "media_request"`);
|
||||
await queryRunner.query(`DROP TABLE "season_request"`);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_6bbafa28411e6046421991ea21"`
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "blacklist"`);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import type { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddTelegramMessageThreadId1734786596045
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'AddTelegramMessageThreadId1734786596045';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "user_settings" ADD "telegramMessageThreadId" character varying`
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "user_settings" DROP COLUMN "telegramMessageThreadId"`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import type { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddOverrideRules1734805738349 implements MigrationInterface {
|
||||
name = 'AddOverrideRules1734805738349';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "override_rule" ("id" SERIAL NOT NULL, "radarrServiceId" integer, "sonarrServiceId" integer, "users" character varying, "genre" character varying, "language" character varying, "keywords" character varying, "profileId" integer, "rootFolder" character varying, "tags" character varying, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_657f810c7b20a4fce45aee8f182" PRIMARY KEY ("id"))`
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP TABLE "override_rule"`);
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import type { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class FixNullFields1734809898562 implements MigrationInterface {
|
||||
name = 'FixNullFields1734809898562';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "watchlist" DROP CONSTRAINT "FK_6641da8d831b93dfcb429f8b8bc"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "watchlist" ALTER COLUMN "mediaId" DROP NOT NULL`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "media_request" DROP CONSTRAINT "FK_a1aa713f41c99e9d10c48da75a0"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "media_request" ALTER COLUMN "mediaId" DROP NOT NULL`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "season" DROP CONSTRAINT "FK_087099b39600be695591da9a49c"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "season" ALTER COLUMN "mediaId" DROP NOT NULL`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "watchlist" ADD CONSTRAINT "FK_6641da8d831b93dfcb429f8b8bc" FOREIGN KEY ("mediaId") REFERENCES "media"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "media_request" ADD CONSTRAINT "FK_a1aa713f41c99e9d10c48da75a0" FOREIGN KEY ("mediaId") REFERENCES "media"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "season" ADD CONSTRAINT "FK_087099b39600be695591da9a49c" FOREIGN KEY ("mediaId") REFERENCES "media"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "season" DROP CONSTRAINT "FK_087099b39600be695591da9a49c"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "media_request" DROP CONSTRAINT "FK_a1aa713f41c99e9d10c48da75a0"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "watchlist" DROP CONSTRAINT "FK_6641da8d831b93dfcb429f8b8bc"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "season" ALTER COLUMN "mediaId" SET NOT NULL`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "season" ADD CONSTRAINT "FK_087099b39600be695591da9a49c" FOREIGN KEY ("mediaId") REFERENCES "media"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "media_request" ALTER COLUMN "mediaId" SET NOT NULL`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "media_request" ADD CONSTRAINT "FK_a1aa713f41c99e9d10c48da75a0" FOREIGN KEY ("mediaId") REFERENCES "media"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "watchlist" ALTER COLUMN "mediaId" SET NOT NULL`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "watchlist" ADD CONSTRAINT "FK_6641da8d831b93dfcb429f8b8bc" FOREIGN KEY ("mediaId") REFERENCES "media"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import type { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddUserSettingsStreamingRegion1727907530757
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'AddUserSettingsStreamingRegion1727907530757';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "temporary_user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "notificationTypes" text, "discordId" varchar, "userId" integer, "originalLanguage" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "pgpKey" varchar, "locale" varchar NOT NULL DEFAULT (''), "pushbulletAccessToken" varchar, "pushoverApplicationToken" varchar, "pushoverUserKey" varchar, "watchlistSyncMovies" boolean, "watchlistSyncTv" boolean, "pushoverSound" varchar, CONSTRAINT "UQ_986a2b6d3c05eb4091bb8066f78" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "temporary_user_settings"("id", "notificationTypes", "discordId", "userId", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "watchlistSyncMovies", "watchlistSyncTv", "pushoverSound") SELECT "id", "notificationTypes", "discordId", "userId", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "watchlistSyncMovies", "watchlistSyncTv", "pushoverSound" FROM "user_settings"`
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "user_settings"`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "temporary_user_settings" RENAME TO "user_settings"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "temporary_user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "notificationTypes" text, "discordId" varchar, "userId" integer, "originalLanguage" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "pgpKey" varchar, "locale" varchar NOT NULL DEFAULT (''), "pushbulletAccessToken" varchar, "pushoverApplicationToken" varchar, "pushoverUserKey" varchar, "watchlistSyncMovies" boolean, "watchlistSyncTv" boolean, "pushoverSound" varchar, "discoverRegion" varchar, "streamingRegion" varchar, CONSTRAINT "UQ_986a2b6d3c05eb4091bb8066f78" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "temporary_user_settings"("id", "notificationTypes", "discordId", "userId", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "watchlistSyncMovies", "watchlistSyncTv", "pushoverSound") SELECT "id", "notificationTypes", "discordId", "userId", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "watchlistSyncMovies", "watchlistSyncTv", "pushoverSound" FROM "user_settings"`
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "user_settings"`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "temporary_user_settings" RENAME TO "user_settings"`
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "user_settings" RENAME TO "temporary_user_settings"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "notificationTypes" text, "discordId" varchar, "userId" integer, "originalLanguage" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "pgpKey" varchar, "locale" varchar NOT NULL DEFAULT (''), "pushbulletAccessToken" varchar, "pushoverApplicationToken" varchar, "pushoverUserKey" varchar, "watchlistSyncMovies" boolean, "watchlistSyncTv" boolean, "pushoverSound" varchar, CONSTRAINT "UQ_986a2b6d3c05eb4091bb8066f78" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "user_settings"("id", "notificationTypes", "discordId", "userId", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "watchlistSyncMovies", "watchlistSyncTv", "pushoverSound") SELECT "id", "notificationTypes", "discordId", "userId", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "watchlistSyncMovies", "watchlistSyncTv", "pushoverSound" FROM "temporary_user_settings"`
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "temporary_user_settings"`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "user_settings" RENAME TO "temporary_user_settings"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "notificationTypes" text, "discordId" varchar, "userId" integer, "originalLanguage" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "pgpKey" varchar, "locale" varchar NOT NULL DEFAULT (''), "pushbulletAccessToken" varchar, "pushoverApplicationToken" varchar, "pushoverUserKey" varchar, "watchlistSyncMovies" boolean, "watchlistSyncTv" boolean, "pushoverSound" varchar, "region" varchar, CONSTRAINT "UQ_986a2b6d3c05eb4091bb8066f78" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "user_settings"("id", "notificationTypes", "discordId", "userId", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "watchlistSyncMovies", "watchlistSyncTv", "pushoverSound") SELECT "id", "notificationTypes", "discordId", "userId", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "watchlistSyncMovies", "watchlistSyncTv", "pushoverSound" FROM "temporary_user_settings"`
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "temporary_user_settings"`);
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import type { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddTelegramMessageThreadId1734287582736
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'AddTelegramMessageThreadId1734287582736';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "temporary_user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "locale" varchar NOT NULL DEFAULT (''), "discoverRegion" varchar, "streamingRegion" varchar, "originalLanguage" varchar, "pgpKey" varchar, "discordId" varchar, "pushbulletAccessToken" varchar, "pushoverApplicationToken" varchar, "pushoverUserKey" varchar, "pushoverSound" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "watchlistSyncMovies" boolean, "watchlistSyncTv" boolean, "notificationTypes" text, "userId" integer, "telegramMessageThreadId" varchar, CONSTRAINT "REL_986a2b6d3c05eb4091bb8066f7" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "temporary_user_settings"("id", "locale", "discoverRegion", "streamingRegion", "originalLanguage", "pgpKey", "discordId", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "pushoverSound", "telegramChatId", "telegramSendSilently", "watchlistSyncMovies", "watchlistSyncTv", "notificationTypes", "userId") SELECT "id", "locale", "discoverRegion", "streamingRegion", "originalLanguage", "pgpKey", "discordId", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "pushoverSound", "telegramChatId", "telegramSendSilently", "watchlistSyncMovies", "watchlistSyncTv", "notificationTypes", "userId" FROM "user_settings"`
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "user_settings"`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "temporary_user_settings" RENAME TO "user_settings"`
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "user_settings" RENAME TO "temporary_user_settings"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "locale" varchar NOT NULL DEFAULT (''), "discoverRegion" varchar, "streamingRegion" varchar, "originalLanguage" varchar, "pgpKey" varchar, "discordId" varchar, "pushbulletAccessToken" varchar, "pushoverApplicationToken" varchar, "pushoverUserKey" varchar, "pushoverSound" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "watchlistSyncMovies" boolean, "watchlistSyncTv" boolean, "notificationTypes" text, "userId" integer, CONSTRAINT "REL_986a2b6d3c05eb4091bb8066f7" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "user_settings"("id", "locale", "discoverRegion", "streamingRegion", "originalLanguage", "pgpKey", "discordId", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "pushoverSound", "telegramChatId", "telegramSendSilently", "watchlistSyncMovies", "watchlistSyncTv", "notificationTypes", "userId") SELECT "id", "locale", "discoverRegion", "streamingRegion", "originalLanguage", "pgpKey", "discordId", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "pushoverSound", "telegramChatId", "telegramSendSilently", "watchlistSyncMovies", "watchlistSyncTv", "notificationTypes", "userId" FROM "temporary_user_settings"`
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "temporary_user_settings"`);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import type { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddOverrideRules1734805733535 implements MigrationInterface {
|
||||
name = 'AddOverrideRules1734805733535';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "override_rule" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "radarrServiceId" integer, "sonarrServiceId" integer, "users" varchar, "genre" varchar, "language" varchar, "keywords" varchar, "profileId" integer, "rootFolder" varchar, "tags" varchar, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')))`
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP TABLE "override_rule"`);
|
||||
}
|
||||
}
|
||||
@@ -313,7 +313,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
body.serverType !== MediaServerType.JELLYFIN &&
|
||||
body.serverType !== MediaServerType.EMBY
|
||||
) {
|
||||
throw new ApiError(500, ApiErrorCode.NoAdminUser);
|
||||
throw new Error('select_server_type');
|
||||
}
|
||||
settings.main.mediaServerType = body.serverType;
|
||||
|
||||
@@ -533,22 +533,6 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
message: e.errorCode,
|
||||
});
|
||||
|
||||
case ApiErrorCode.NoAdminUser:
|
||||
logger.warn(
|
||||
'Failed login attempt from user without admin permissions and no admin user exists',
|
||||
{
|
||||
label: 'Auth',
|
||||
account: {
|
||||
ip: req.ip,
|
||||
email: body.username,
|
||||
},
|
||||
}
|
||||
);
|
||||
return next({
|
||||
status: e.statusCode,
|
||||
message: e.errorCode,
|
||||
});
|
||||
|
||||
default:
|
||||
logger.error(e.message, { label: 'Auth' });
|
||||
return next({
|
||||
|
||||
@@ -54,15 +54,9 @@ router.get('/:jellyfinUserId', async (req, res) => {
|
||||
default: 'mm',
|
||||
size: 200,
|
||||
});
|
||||
|
||||
const setttings = getSettings();
|
||||
const jellyfinAvatarUrl =
|
||||
setttings.main.mediaServerType === MediaServerType.JELLYFIN
|
||||
? `${getHostname()}/UserImage?UserId=${req.params.jellyfinUserId}`
|
||||
: `${getHostname()}/Users/${
|
||||
req.params.jellyfinUserId
|
||||
}/Images/Primary?quality=90`;
|
||||
|
||||
const jellyfinAvatarUrl = `${getHostname()}/UserImage?UserId=${
|
||||
req.params.jellyfinUserId
|
||||
}`;
|
||||
let imageData = await avatarImageCache.getImage(
|
||||
jellyfinAvatarUrl,
|
||||
fallbackUrl
|
||||
|
||||
@@ -29,12 +29,12 @@ import { z } from 'zod';
|
||||
export const createTmdbWithRegionLanguage = (user?: User): TheMovieDb => {
|
||||
const settings = getSettings();
|
||||
|
||||
const discoverRegion =
|
||||
user?.settings?.streamingRegion === 'all'
|
||||
const region =
|
||||
user?.settings?.region === 'all'
|
||||
? ''
|
||||
: user?.settings?.streamingRegion
|
||||
? user?.settings?.streamingRegion
|
||||
: settings.main.discoverRegion;
|
||||
: user?.settings?.region
|
||||
? user?.settings?.region
|
||||
: settings.main.region;
|
||||
|
||||
const originalLanguage =
|
||||
user?.settings?.originalLanguage === 'all'
|
||||
@@ -44,7 +44,7 @@ export const createTmdbWithRegionLanguage = (user?: User): TheMovieDb => {
|
||||
: settings.main.originalLanguage;
|
||||
|
||||
return new TheMovieDb({
|
||||
discoverRegion,
|
||||
region,
|
||||
originalLanguage,
|
||||
});
|
||||
};
|
||||
@@ -875,7 +875,6 @@ discoverRoutes.get<Record<string, unknown>, WatchlistResponse>(
|
||||
totalPages: Math.ceil(watchlist.totalSize / itemsPerPage),
|
||||
totalResults: watchlist.totalSize,
|
||||
results: watchlist.items.map((item) => ({
|
||||
id: item.tmdbId,
|
||||
ratingKey: item.ratingKey,
|
||||
title: item.title,
|
||||
mediaType: item.type === 'show' ? 'tv' : 'movie',
|
||||
|
||||
@@ -15,7 +15,6 @@ import { checkUser, isAuthenticated } from '@server/middleware/auth';
|
||||
import { mapWatchProviderDetails } from '@server/models/common';
|
||||
import { mapProductionCompany } from '@server/models/Movie';
|
||||
import { mapNetwork } from '@server/models/Tv';
|
||||
import overrideRuleRoutes from '@server/routes/overrideRule';
|
||||
import settingsRoutes from '@server/routes/settings';
|
||||
import watchlistRoutes from '@server/routes/watchlist';
|
||||
import {
|
||||
@@ -161,11 +160,6 @@ router.use('/service', isAuthenticated(), serviceRoutes);
|
||||
router.use('/issue', isAuthenticated(), issueRoutes);
|
||||
router.use('/issueComment', isAuthenticated(), issueCommentRoutes);
|
||||
router.use('/auth', authRoutes);
|
||||
router.use(
|
||||
'/overrideRule',
|
||||
isAuthenticated(Permission.ADMIN),
|
||||
overrideRuleRoutes
|
||||
);
|
||||
|
||||
router.get('/regions', isAuthenticated(), async (req, res, next) => {
|
||||
const tmdb = new TheMovieDb();
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
import { getRepository } from '@server/datasource';
|
||||
import OverrideRule from '@server/entity/OverrideRule';
|
||||
import type { OverrideRuleResultsResponse } from '@server/interfaces/api/overrideRuleInterfaces';
|
||||
import { Permission } from '@server/lib/permissions';
|
||||
import { isAuthenticated } from '@server/middleware/auth';
|
||||
import { Router } from 'express';
|
||||
|
||||
const overrideRuleRoutes = Router();
|
||||
|
||||
overrideRuleRoutes.get(
|
||||
'/',
|
||||
isAuthenticated(Permission.ADMIN),
|
||||
async (req, res, next) => {
|
||||
const overrideRuleRepository = getRepository(OverrideRule);
|
||||
|
||||
try {
|
||||
const rules = await overrideRuleRepository.find({});
|
||||
|
||||
return res.status(200).json(rules as OverrideRuleResultsResponse);
|
||||
} catch (e) {
|
||||
next({ status: 404, message: e.message });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
overrideRuleRoutes.post<
|
||||
Record<string, string>,
|
||||
OverrideRule,
|
||||
{
|
||||
users?: string;
|
||||
genre?: string;
|
||||
language?: string;
|
||||
keywords?: string;
|
||||
profileId?: number;
|
||||
rootFolder?: string;
|
||||
tags?: string;
|
||||
radarrServiceId?: number;
|
||||
sonarrServiceId?: number;
|
||||
}
|
||||
>('/', isAuthenticated(Permission.ADMIN), async (req, res, next) => {
|
||||
const overrideRuleRepository = getRepository(OverrideRule);
|
||||
|
||||
try {
|
||||
const rule = new OverrideRule({
|
||||
users: req.body.users,
|
||||
genre: req.body.genre,
|
||||
language: req.body.language,
|
||||
keywords: req.body.keywords,
|
||||
profileId: req.body.profileId,
|
||||
rootFolder: req.body.rootFolder,
|
||||
tags: req.body.tags,
|
||||
radarrServiceId: req.body.radarrServiceId,
|
||||
sonarrServiceId: req.body.sonarrServiceId,
|
||||
});
|
||||
|
||||
const newRule = await overrideRuleRepository.save(rule);
|
||||
|
||||
return res.status(200).json(newRule);
|
||||
} catch (e) {
|
||||
next({ status: 404, message: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
overrideRuleRoutes.put<
|
||||
{ ruleId: string },
|
||||
OverrideRule,
|
||||
{
|
||||
users?: string;
|
||||
genre?: string;
|
||||
language?: string;
|
||||
keywords?: string;
|
||||
profileId?: number;
|
||||
rootFolder?: string;
|
||||
tags?: string;
|
||||
radarrServiceId?: number;
|
||||
sonarrServiceId?: number;
|
||||
}
|
||||
>('/:ruleId', isAuthenticated(Permission.ADMIN), async (req, res, next) => {
|
||||
const overrideRuleRepository = getRepository(OverrideRule);
|
||||
|
||||
try {
|
||||
const rule = await overrideRuleRepository.findOne({
|
||||
where: {
|
||||
id: Number(req.params.ruleId),
|
||||
},
|
||||
});
|
||||
|
||||
if (!rule) {
|
||||
return next({ status: 404, message: 'Override Rule not found.' });
|
||||
}
|
||||
|
||||
rule.users = req.body.users;
|
||||
rule.genre = req.body.genre;
|
||||
rule.language = req.body.language;
|
||||
rule.keywords = req.body.keywords;
|
||||
rule.profileId = req.body.profileId;
|
||||
rule.rootFolder = req.body.rootFolder;
|
||||
rule.tags = req.body.tags;
|
||||
rule.radarrServiceId = req.body.radarrServiceId;
|
||||
rule.sonarrServiceId = req.body.sonarrServiceId;
|
||||
|
||||
const newRule = await overrideRuleRepository.save(rule);
|
||||
|
||||
return res.status(200).json(newRule);
|
||||
} catch (e) {
|
||||
next({ status: 404, message: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
overrideRuleRoutes.delete<{ ruleId: string }, OverrideRule>(
|
||||
'/:ruleId',
|
||||
isAuthenticated(Permission.ADMIN),
|
||||
async (req, res, next) => {
|
||||
const overrideRuleRepository = getRepository(OverrideRule);
|
||||
|
||||
try {
|
||||
const rule = await overrideRuleRepository.findOne({
|
||||
where: {
|
||||
id: Number(req.params.ruleId),
|
||||
},
|
||||
});
|
||||
|
||||
if (!rule) {
|
||||
return next({ status: 404, message: 'Override Rule not found.' });
|
||||
}
|
||||
|
||||
await overrideRuleRepository.remove(rule);
|
||||
|
||||
return res.status(200).json(rule);
|
||||
} catch (e) {
|
||||
next({ status: 404, message: e.message });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default overrideRuleRoutes;
|
||||
@@ -94,7 +94,6 @@ requestRoutes.get<Record<string, unknown>, RequestResultsResponse>(
|
||||
}
|
||||
|
||||
let sortFilter: string;
|
||||
let sortDirection: 'ASC' | 'DESC';
|
||||
|
||||
switch (req.query.sort) {
|
||||
case 'modified':
|
||||
@@ -104,14 +103,6 @@ requestRoutes.get<Record<string, unknown>, RequestResultsResponse>(
|
||||
sortFilter = 'request.id';
|
||||
}
|
||||
|
||||
switch (req.query.sortDirection) {
|
||||
case 'asc':
|
||||
sortDirection = 'ASC';
|
||||
break;
|
||||
default:
|
||||
sortDirection = 'DESC';
|
||||
}
|
||||
|
||||
let query = getRepository(MediaRequest)
|
||||
.createQueryBuilder('request')
|
||||
.leftJoinAndSelect('request.media', 'media')
|
||||
@@ -151,7 +142,7 @@ requestRoutes.get<Record<string, unknown>, RequestResultsResponse>(
|
||||
}
|
||||
|
||||
const [requests, requestCount] = await query
|
||||
.orderBy(sortFilter, sortDirection)
|
||||
.orderBy(sortFilter, 'DESC')
|
||||
.take(pageSize)
|
||||
.skip(skip)
|
||||
.getManyAndCount();
|
||||
@@ -168,7 +159,7 @@ requestRoutes.get<Record<string, unknown>, RequestResultsResponse>(
|
||||
|
||||
return {
|
||||
id: sonarrSetting.id,
|
||||
profiles: await sonarr.getProfiles().catch(() => undefined),
|
||||
profiles: await sonarr.getProfiles(),
|
||||
};
|
||||
})
|
||||
);
|
||||
@@ -183,7 +174,7 @@ requestRoutes.get<Record<string, unknown>, RequestResultsResponse>(
|
||||
|
||||
return {
|
||||
id: radarrSetting.id,
|
||||
profiles: await radarr.getProfiles().catch(() => undefined),
|
||||
profiles: await radarr.getProfiles(),
|
||||
};
|
||||
})
|
||||
);
|
||||
@@ -194,7 +185,7 @@ requestRoutes.get<Record<string, unknown>, RequestResultsResponse>(
|
||||
case MediaType.MOVIE: {
|
||||
const profileName = radarrServers
|
||||
.find((serverr) => serverr.id === r.serverId)
|
||||
?.profiles?.find((profile) => profile.id === r.profileId)?.name;
|
||||
?.profiles.find((profile) => profile.id === r.profileId)?.name;
|
||||
|
||||
return {
|
||||
...r,
|
||||
@@ -206,7 +197,7 @@ requestRoutes.get<Record<string, unknown>, RequestResultsResponse>(
|
||||
...r,
|
||||
profileName: sonarrServers
|
||||
.find((serverr) => serverr.id === r.serverId)
|
||||
?.profiles?.find((profile) => profile.id === r.profileId)?.name,
|
||||
?.profiles.find((profile) => profile.id === r.profileId)?.name,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,16 +34,8 @@ router.get('/', async (req, res, next) => {
|
||||
try {
|
||||
const pageSize = req.query.take ? Number(req.query.take) : 10;
|
||||
const skip = req.query.skip ? Number(req.query.skip) : 0;
|
||||
const q = req.query.q ? req.query.q.toString().toLowerCase() : '';
|
||||
let query = getRepository(User).createQueryBuilder('user');
|
||||
|
||||
if (q) {
|
||||
query = query.where(
|
||||
'LOWER(user.username) LIKE :q OR LOWER(user.email) LIKE :q OR LOWER(user.plexUsername) LIKE :q OR LOWER(user.jellyfinUsername) LIKE :q',
|
||||
{ q: `%${q}%` }
|
||||
);
|
||||
}
|
||||
|
||||
switch (req.query.sort) {
|
||||
case 'updated':
|
||||
query = query.orderBy('user.updatedAt', 'DESC');
|
||||
@@ -70,11 +62,11 @@ router.get('/', async (req, res, next) => {
|
||||
query = query
|
||||
.addSelect((subQuery) => {
|
||||
return subQuery
|
||||
.select('COUNT(request.id)', 'request_count')
|
||||
.select('COUNT(request.id)', 'requestCount')
|
||||
.from(MediaRequest, 'request')
|
||||
.where('request.requestedBy.id = user.id');
|
||||
}, 'request_count')
|
||||
.orderBy('request_count', 'DESC');
|
||||
}, 'requestCount')
|
||||
.orderBy('requestCount', 'DESC');
|
||||
break;
|
||||
default:
|
||||
query = query.orderBy('user.id', 'ASC');
|
||||
@@ -772,7 +764,6 @@ router.get<{ id: string }, WatchlistResponse>(
|
||||
totalPages: Math.ceil(watchlist.totalSize / itemsPerPage),
|
||||
totalResults: watchlist.totalSize,
|
||||
results: watchlist.items.map((item) => ({
|
||||
id: item.tmdbId,
|
||||
ratingKey: item.ratingKey,
|
||||
title: item.title,
|
||||
mediaType: item.type === 'show' ? 'tv' : 'movie',
|
||||
|
||||
@@ -57,8 +57,7 @@ userSettingsRoutes.get<{ id: string }, UserSettingsGeneralResponse>(
|
||||
email: user.email,
|
||||
discordId: user.settings?.discordId,
|
||||
locale: user.settings?.locale,
|
||||
discoverRegion: user.settings?.discoverRegion,
|
||||
streamingRegion: user.settings?.streamingRegion,
|
||||
region: user.settings?.region,
|
||||
originalLanguage: user.settings?.originalLanguage,
|
||||
movieQuotaLimit: user.movieQuotaLimit,
|
||||
movieQuotaDays: user.movieQuotaDays,
|
||||
@@ -148,8 +147,7 @@ userSettingsRoutes.post<
|
||||
user: req.user,
|
||||
discordId: req.body.discordId,
|
||||
locale: req.body.locale,
|
||||
discoverRegion: req.body.discoverRegion,
|
||||
streamingRegion: req.body.streamingRegion,
|
||||
region: req.body.region,
|
||||
originalLanguage: req.body.originalLanguage,
|
||||
watchlistSyncMovies: req.body.watchlistSyncMovies,
|
||||
watchlistSyncTv: req.body.watchlistSyncTv,
|
||||
@@ -157,8 +155,7 @@ userSettingsRoutes.post<
|
||||
} else {
|
||||
user.settings.discordId = req.body.discordId;
|
||||
user.settings.locale = req.body.locale;
|
||||
user.settings.discoverRegion = req.body.discoverRegion;
|
||||
user.settings.streamingRegion = req.body.streamingRegion;
|
||||
user.settings.region = req.body.region;
|
||||
user.settings.originalLanguage = req.body.originalLanguage;
|
||||
user.settings.watchlistSyncMovies = req.body.watchlistSyncMovies;
|
||||
user.settings.watchlistSyncTv = req.body.watchlistSyncTv;
|
||||
@@ -170,8 +167,7 @@ userSettingsRoutes.post<
|
||||
username: savedUser.username,
|
||||
discordId: savedUser.settings?.discordId,
|
||||
locale: savedUser.settings?.locale,
|
||||
discoverRegion: savedUser.settings?.discoverRegion,
|
||||
streamingRegion: savedUser.settings?.streamingRegion,
|
||||
region: savedUser.settings?.region,
|
||||
originalLanguage: savedUser.settings?.originalLanguage,
|
||||
watchlistSyncMovies: savedUser.settings?.watchlistSyncMovies,
|
||||
watchlistSyncTv: savedUser.settings?.watchlistSyncTv,
|
||||
@@ -323,7 +319,6 @@ userSettingsRoutes.get<{ id: string }, UserSettingsNotificationsResponse>(
|
||||
telegramEnabled: settings.telegram.enabled,
|
||||
telegramBotUsername: settings.telegram.options.botUsername,
|
||||
telegramChatId: user.settings?.telegramChatId,
|
||||
telegramMessageThreadId: user.settings?.telegramMessageThreadId,
|
||||
telegramSendSilently: user.settings?.telegramSendSilently,
|
||||
webPushEnabled: settings.webpush.enabled,
|
||||
notificationTypes: user.settings?.notificationTypes ?? {},
|
||||
@@ -366,7 +361,6 @@ userSettingsRoutes.post<{ id: string }, UserSettingsNotificationsResponse>(
|
||||
pushoverApplicationToken: req.body.pushoverApplicationToken,
|
||||
pushoverUserKey: req.body.pushoverUserKey,
|
||||
telegramChatId: req.body.telegramChatId,
|
||||
telegramMessageThreadId: req.body.telegramMessageThreadId,
|
||||
telegramSendSilently: req.body.telegramSendSilently,
|
||||
notificationTypes: req.body.notificationTypes,
|
||||
});
|
||||
@@ -379,8 +373,6 @@ userSettingsRoutes.post<{ id: string }, UserSettingsNotificationsResponse>(
|
||||
user.settings.pushoverUserKey = req.body.pushoverUserKey;
|
||||
user.settings.pushoverSound = req.body.pushoverSound;
|
||||
user.settings.telegramChatId = req.body.telegramChatId;
|
||||
user.settings.telegramMessageThreadId =
|
||||
req.body.telegramMessageThreadId;
|
||||
user.settings.telegramSendSilently = req.body.telegramSendSilently;
|
||||
user.settings.notificationTypes = Object.assign(
|
||||
{},
|
||||
@@ -399,7 +391,6 @@ userSettingsRoutes.post<{ id: string }, UserSettingsNotificationsResponse>(
|
||||
pushoverUserKey: user.settings.pushoverUserKey,
|
||||
pushoverSound: user.settings.pushoverSound,
|
||||
telegramChatId: user.settings.telegramChatId,
|
||||
telegramMessageThreadId: user.settings.telegramMessageThreadId,
|
||||
telegramSendSilently: user.settings.telegramSendSilently,
|
||||
notificationTypes: user.settings.notificationTypes,
|
||||
});
|
||||
|
||||
@@ -14,9 +14,7 @@ class RestartFlag {
|
||||
return (
|
||||
this.settings.csrfProtection !== settings.csrfProtection ||
|
||||
this.settings.trustProxy !== settings.trustProxy ||
|
||||
this.settings.proxy.enabled !== settings.proxy.enabled ||
|
||||
this.settings.forceIpv4First !== settings.forceIpv4First ||
|
||||
this.settings.dnsServers !== settings.dnsServers
|
||||
this.settings.proxy.enabled !== settings.proxy.enabled
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import defineMessages from '@app/utils/defineMessages';
|
||||
import { Transition } from '@headlessui/react';
|
||||
import type { MovieDetails } from '@server/models/Movie';
|
||||
import type { TvDetails } from '@server/models/Tv';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import useSWR from 'swr';
|
||||
|
||||
interface BlacklistModalProps {
|
||||
tmdbId: number;
|
||||
@@ -21,7 +21,7 @@ const messages = defineMessages('component.BlacklistModal', {
|
||||
});
|
||||
|
||||
const isMovie = (
|
||||
movie: MovieDetails | TvDetails | null
|
||||
movie: MovieDetails | TvDetails | undefined
|
||||
): movie is MovieDetails => {
|
||||
if (!movie) return false;
|
||||
return (movie as MovieDetails).title !== undefined;
|
||||
@@ -36,25 +36,10 @@ const BlacklistModal = ({
|
||||
isUpdating,
|
||||
}: BlacklistModalProps) => {
|
||||
const intl = useIntl();
|
||||
const [data, setData] = useState<TvDetails | MovieDetails | null>(null);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (!show) return;
|
||||
try {
|
||||
setError(null);
|
||||
const response = await fetch(`/api/v1/${type}/${tmdbId}`);
|
||||
if (!response.ok) {
|
||||
throw new Error();
|
||||
}
|
||||
const result = await response.json();
|
||||
setData(result);
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
}
|
||||
})();
|
||||
}, [show, tmdbId, type]);
|
||||
const { data, error } = useSWR<TvDetails | MovieDetails>(
|
||||
show ? `/api/v1/${type}/${tmdbId}` : null
|
||||
);
|
||||
|
||||
return (
|
||||
<Transition
|
||||
|
||||
@@ -2,11 +2,7 @@ import useClickOutside from '@app/hooks/useClickOutside';
|
||||
import { withProperties } from '@app/utils/typeHelpers';
|
||||
import { Transition } from '@headlessui/react';
|
||||
import { ChevronDownIcon } from '@heroicons/react/24/solid';
|
||||
import type {
|
||||
AnchorHTMLAttributes,
|
||||
ButtonHTMLAttributes,
|
||||
RefObject,
|
||||
} from 'react';
|
||||
import type { AnchorHTMLAttributes, ButtonHTMLAttributes } from 'react';
|
||||
import { Fragment, useRef, useState } from 'react';
|
||||
|
||||
interface DropdownItemProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
|
||||
@@ -39,33 +35,23 @@ const DropdownItem = ({
|
||||
);
|
||||
};
|
||||
|
||||
interface ButtonWithDropdownProps {
|
||||
interface ButtonWithDropdownProps
|
||||
extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
text: React.ReactNode;
|
||||
dropdownIcon?: React.ReactNode;
|
||||
buttonType?: 'primary' | 'ghost';
|
||||
}
|
||||
interface ButtonProps
|
||||
extends ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
ButtonWithDropdownProps {
|
||||
as?: 'button';
|
||||
}
|
||||
interface AnchorProps
|
||||
extends AnchorHTMLAttributes<HTMLAnchorElement>,
|
||||
ButtonWithDropdownProps {
|
||||
as: 'a';
|
||||
}
|
||||
|
||||
const ButtonWithDropdown = ({
|
||||
as,
|
||||
text,
|
||||
children,
|
||||
dropdownIcon,
|
||||
className,
|
||||
buttonType = 'primary',
|
||||
...props
|
||||
}: ButtonProps | AnchorProps) => {
|
||||
}: ButtonWithDropdownProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const buttonRef = useRef<HTMLButtonElement | HTMLAnchorElement>(null);
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
useClickOutside(buttonRef, () => setIsOpen(false));
|
||||
|
||||
const styleClasses = {
|
||||
@@ -92,28 +78,16 @@ const ButtonWithDropdown = ({
|
||||
|
||||
return (
|
||||
<span className="relative inline-flex h-full rounded-md shadow-sm">
|
||||
{as === 'a' ? (
|
||||
<a
|
||||
className={`relative z-10 inline-flex h-full items-center px-4 py-2 text-sm font-medium leading-5 transition duration-150 ease-in-out hover:z-20 focus:z-20 focus:outline-none ${
|
||||
styleClasses.mainButtonClasses
|
||||
} ${children ? 'rounded-l-md' : 'rounded-md'} ${className}`}
|
||||
ref={buttonRef as RefObject<HTMLAnchorElement>}
|
||||
{...(props as AnchorHTMLAttributes<HTMLAnchorElement>)}
|
||||
>
|
||||
{text}
|
||||
</a>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className={`relative z-10 inline-flex h-full items-center px-4 py-2 text-sm font-medium leading-5 transition duration-150 ease-in-out hover:z-20 focus:z-20 focus:outline-none ${
|
||||
styleClasses.mainButtonClasses
|
||||
} ${children ? 'rounded-l-md' : 'rounded-md'} ${className}`}
|
||||
ref={buttonRef as RefObject<HTMLButtonElement>}
|
||||
{...(props as ButtonHTMLAttributes<HTMLButtonElement>)}
|
||||
>
|
||||
{text}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className={`relative z-10 inline-flex h-full items-center px-4 py-2 text-sm font-medium leading-5 transition duration-150 ease-in-out hover:z-20 focus:z-20 focus:outline-none ${
|
||||
styleClasses.mainButtonClasses
|
||||
} ${children ? 'rounded-l-md' : 'rounded-md'} ${className}`}
|
||||
ref={buttonRef}
|
||||
{...props}
|
||||
>
|
||||
{text}
|
||||
</button>
|
||||
{children && (
|
||||
<span className="relative -ml-px block">
|
||||
<button
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useLockBodyScroll } from '@app/hooks/useLockBodyScroll';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import { Transition } from '@headlessui/react';
|
||||
import type { MouseEvent } from 'react';
|
||||
import React, { Fragment, useEffect, useRef } from 'react';
|
||||
import React, { Fragment, useRef } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
@@ -66,12 +66,8 @@ const Modal = React.forwardRef<HTMLDivElement, ModalProps>(
|
||||
) => {
|
||||
const intl = useIntl();
|
||||
const modalRef = useRef<HTMLDivElement>(null);
|
||||
const backgroundClickableRef = useRef(backgroundClickable); // This ref is used to detect state change inside the useClickOutside hook
|
||||
useEffect(() => {
|
||||
backgroundClickableRef.current = backgroundClickable;
|
||||
}, [backgroundClickable]);
|
||||
useClickOutside(modalRef, () => {
|
||||
if (onCancel && backgroundClickableRef.current) {
|
||||
if (onCancel && backgroundClickable) {
|
||||
onCancel();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -17,7 +17,6 @@ const PlayButton = ({ links }: PlayButtonProps) => {
|
||||
|
||||
return (
|
||||
<ButtonWithDropdown
|
||||
as="a"
|
||||
buttonType="ghost"
|
||||
text={
|
||||
<>
|
||||
@@ -25,17 +24,19 @@ const PlayButton = ({ links }: PlayButtonProps) => {
|
||||
<span>{links[0].text}</span>
|
||||
</>
|
||||
}
|
||||
href={links[0].url}
|
||||
target="_blank"
|
||||
onClick={() => {
|
||||
window.open(links[0].url, '_blank');
|
||||
}}
|
||||
>
|
||||
{links.length > 1 &&
|
||||
links.slice(1).map((link, i) => {
|
||||
return (
|
||||
<ButtonWithDropdown.Item
|
||||
key={`play-button-dropdown-item-${i}`}
|
||||
onClick={() => {
|
||||
window.open(link.url, '_blank');
|
||||
}}
|
||||
buttonType="ghost"
|
||||
href={link.url}
|
||||
target="_blank"
|
||||
>
|
||||
{link.svg}
|
||||
<span>{link.text}</span>
|
||||
|
||||
@@ -7,9 +7,7 @@ import defineMessages from '@app/utils/defineMessages';
|
||||
import type { TvResult } from '@server/models/Search';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
const messages = defineMessages('components.DiscoverTvUpcoming', {
|
||||
upcomingtv: 'Upcoming Series',
|
||||
});
|
||||
const messages = defineMessages('components.DiscoverTvUpcoming', {});
|
||||
|
||||
const DiscoverTvUpcoming = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
@@ -34,7 +34,6 @@ const messages = defineMessages('components.Login', {
|
||||
validationUrlBaseTrailingSlash: 'URL base must not end in a trailing slash',
|
||||
loginerror: 'Something went wrong while trying to sign in.',
|
||||
adminerror: 'You must use an admin account to sign in.',
|
||||
noadminerror: 'No admin user found on the server.',
|
||||
credentialerror: 'The username or password is incorrect.',
|
||||
invalidurlerror: 'Unable to connect to {mediaServerName} server.',
|
||||
signingin: 'Signing in…',
|
||||
@@ -158,9 +157,6 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
|
||||
case ApiErrorCode.NotAdmin:
|
||||
errorMessage = messages.adminerror;
|
||||
break;
|
||||
case ApiErrorCode.NoAdminUser:
|
||||
errorMessage = messages.noadminerror;
|
||||
break;
|
||||
default:
|
||||
errorMessage = messages.loginerror;
|
||||
break;
|
||||
@@ -392,35 +388,14 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
|
||||
email: values.username,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error(res.statusText, { cause: res });
|
||||
if (!res.ok) throw new Error();
|
||||
} catch (e) {
|
||||
let errorData;
|
||||
try {
|
||||
errorData = await e.cause?.text();
|
||||
errorData = JSON.parse(errorData);
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
let errorMessage = null;
|
||||
switch (errorData?.message) {
|
||||
case ApiErrorCode.InvalidUrl:
|
||||
errorMessage = messages.invalidurlerror;
|
||||
break;
|
||||
case ApiErrorCode.InvalidCredentials:
|
||||
errorMessage = messages.credentialerror;
|
||||
break;
|
||||
case ApiErrorCode.NotAdmin:
|
||||
errorMessage = messages.adminerror;
|
||||
break;
|
||||
case ApiErrorCode.NoAdminUser:
|
||||
errorMessage = messages.noadminerror;
|
||||
break;
|
||||
default:
|
||||
errorMessage = messages.loginerror;
|
||||
break;
|
||||
}
|
||||
toasts.addToast(
|
||||
intl.formatMessage(errorMessage, mediaServerFormatValues),
|
||||
intl.formatMessage(
|
||||
e.message == 'Request failed with status code 401'
|
||||
? messages.credentialerror
|
||||
: messages.loginerror
|
||||
),
|
||||
{
|
||||
autoDismiss: true,
|
||||
appearance: 'error',
|
||||
|
||||
@@ -222,14 +222,14 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
|
||||
});
|
||||
}
|
||||
|
||||
const discoverRegion = user?.settings?.discoverRegion
|
||||
? user.settings.discoverRegion
|
||||
: settings.currentSettings.discoverRegion
|
||||
? settings.currentSettings.discoverRegion
|
||||
const region = user?.settings?.region
|
||||
? user.settings.region
|
||||
: settings.currentSettings.region
|
||||
? settings.currentSettings.region
|
||||
: 'US';
|
||||
|
||||
const releases = data.releases.results.find(
|
||||
(r) => r.iso_3166_1 === discoverRegion
|
||||
(r) => r.iso_3166_1 === region
|
||||
)?.release_dates;
|
||||
|
||||
// Release date types:
|
||||
@@ -282,15 +282,9 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
|
||||
);
|
||||
}
|
||||
|
||||
const streamingRegion = user?.settings?.streamingRegion
|
||||
? user.settings.streamingRegion
|
||||
: settings.currentSettings.streamingRegion
|
||||
? settings.currentSettings.streamingRegion
|
||||
: 'US';
|
||||
const streamingProviders =
|
||||
data?.watchProviders?.find(
|
||||
(provider) => provider.iso_3166_1 === streamingRegion
|
||||
)?.flatrate ?? [];
|
||||
data?.watchProviders?.find((provider) => provider.iso_3166_1 === region)
|
||||
?.flatrate ?? [];
|
||||
|
||||
function getAvalaibleMediaServerName() {
|
||||
if (settings.currentSettings.mediaServerType === MediaServerType.EMBY) {
|
||||
@@ -780,13 +774,13 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
|
||||
<div className="media-facts">
|
||||
{(!!data.voteCount ||
|
||||
(ratingData?.rt?.criticsRating &&
|
||||
typeof ratingData?.rt?.criticsScore === 'number') ||
|
||||
!!ratingData?.rt?.criticsScore) ||
|
||||
(ratingData?.rt?.audienceRating &&
|
||||
!!ratingData?.rt?.audienceScore) ||
|
||||
ratingData?.imdb?.criticsScore) && (
|
||||
<div className="media-ratings">
|
||||
{ratingData?.rt?.criticsRating &&
|
||||
typeof ratingData?.rt?.criticsScore === 'number' && (
|
||||
!!ratingData?.rt?.criticsScore && (
|
||||
<Tooltip
|
||||
content={intl.formatMessage(messages.rtcriticsscore)}
|
||||
>
|
||||
@@ -1063,26 +1057,14 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
|
||||
</div>
|
||||
)}
|
||||
{!!streamingProviders.length && (
|
||||
<div className="media-fact flex-col gap-1">
|
||||
<div className="media-fact">
|
||||
<span>{intl.formatMessage(messages.streamingproviders)}</span>
|
||||
<span className="media-fact-value flex flex-row flex-wrap gap-5">
|
||||
<span className="media-fact-value">
|
||||
{streamingProviders.map((p) => {
|
||||
return (
|
||||
<Tooltip content={p.name}>
|
||||
<span
|
||||
className="opacity-50 transition duration-300 hover:opacity-100"
|
||||
key={`provider-${p.id}`}
|
||||
>
|
||||
<CachedImage
|
||||
type="tmdb"
|
||||
src={'https://image.tmdb.org/t/p/w45/' + p.logoPath}
|
||||
alt={p.name}
|
||||
width={32}
|
||||
height={32}
|
||||
className="rounded-md"
|
||||
/>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<span className="block" key={`provider-${p.id}`}>
|
||||
{p.name}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</span>
|
||||
|
||||
@@ -21,7 +21,6 @@ interface RegionSelectorProps {
|
||||
isUserSetting?: boolean;
|
||||
disableAll?: boolean;
|
||||
watchProviders?: boolean;
|
||||
regionType?: 'discover' | 'streaming';
|
||||
onChange?: (fieldName: string, region: string) => void;
|
||||
}
|
||||
|
||||
@@ -31,7 +30,6 @@ const RegionSelector = ({
|
||||
isUserSetting = false,
|
||||
disableAll = false,
|
||||
watchProviders = false,
|
||||
regionType = 'discover',
|
||||
onChange,
|
||||
}: RegionSelectorProps) => {
|
||||
const { currentSettings } = useSettings();
|
||||
@@ -65,11 +63,6 @@ const RegionSelector = ({
|
||||
sortedRegions?.find((region) => region.iso_3166_1 === regionCode)?.name ??
|
||||
regionCode;
|
||||
|
||||
const regionValue =
|
||||
regionType === 'discover'
|
||||
? currentSettings.discoverRegion
|
||||
: currentSettings.streamingRegion;
|
||||
|
||||
useEffect(() => {
|
||||
if (regions && value) {
|
||||
if (value === 'all') {
|
||||
@@ -104,12 +97,14 @@ const RegionSelector = ({
|
||||
countries.includes(selectedRegion?.iso_3166_1)) ||
|
||||
(isUserSetting &&
|
||||
!selectedRegion &&
|
||||
regionValue &&
|
||||
countries.includes(regionValue))) && (
|
||||
currentSettings.region &&
|
||||
countries.includes(currentSettings.region))) && (
|
||||
<span className="mr-2 h-4 overflow-hidden text-base leading-4">
|
||||
<span
|
||||
className={`flag:${
|
||||
selectedRegion ? selectedRegion.iso_3166_1 : regionValue
|
||||
selectedRegion
|
||||
? selectedRegion.iso_3166_1
|
||||
: currentSettings.region
|
||||
}`}
|
||||
/>
|
||||
</span>
|
||||
@@ -119,8 +114,8 @@ const RegionSelector = ({
|
||||
? regionName(selectedRegion.iso_3166_1)
|
||||
: isUserSetting && selectedRegion?.iso_3166_1 !== 'all'
|
||||
? intl.formatMessage(messages.regionServerDefault, {
|
||||
region: regionValue
|
||||
? regionName(regionValue)
|
||||
region: currentSettings.region
|
||||
? regionName(currentSettings.region)
|
||||
: intl.formatMessage(messages.regionDefault),
|
||||
})
|
||||
: intl.formatMessage(messages.regionDefault)}
|
||||
@@ -153,8 +148,8 @@ const RegionSelector = ({
|
||||
<span className="mr-2 text-base">
|
||||
<span
|
||||
className={
|
||||
countries.includes(regionValue)
|
||||
? `flag:${regionValue}`
|
||||
countries.includes(currentSettings.region)
|
||||
? `flag:${currentSettings.region}`
|
||||
: 'pr-6'
|
||||
}
|
||||
/>
|
||||
@@ -165,8 +160,8 @@ const RegionSelector = ({
|
||||
} block truncate`}
|
||||
>
|
||||
{intl.formatMessage(messages.regionServerDefault, {
|
||||
region: regionValue
|
||||
? regionName(regionValue)
|
||||
region: currentSettings.region
|
||||
? regionName(currentSettings.region)
|
||||
: intl.formatMessage(messages.regionDefault),
|
||||
})}
|
||||
</span>
|
||||
|
||||
@@ -5,7 +5,6 @@ import Tooltip from '@app/components/Common/Tooltip';
|
||||
import RequestModal from '@app/components/RequestModal';
|
||||
import StatusBadge from '@app/components/StatusBadge';
|
||||
import useDeepLinks from '@app/hooks/useDeepLinks';
|
||||
import useSettings from '@app/hooks/useSettings';
|
||||
import { Permission, useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
@@ -220,7 +219,6 @@ interface RequestCardProps {
|
||||
}
|
||||
|
||||
const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
|
||||
const settings = useSettings();
|
||||
const { ref, inView } = useInView({
|
||||
triggerOnce: true,
|
||||
});
|
||||
@@ -413,11 +411,7 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
|
||||
<span className="mr-2 font-bold ">
|
||||
{intl.formatMessage(messages.seasons, {
|
||||
seasonCount:
|
||||
(settings.currentSettings.enableSpecialEpisodes
|
||||
? title.seasons.length
|
||||
: title.seasons.filter(
|
||||
(season) => season.seasonNumber !== 0
|
||||
).length) === request.seasons.length
|
||||
title.seasons.length === request.seasons.length
|
||||
? 0
|
||||
: request.seasons.length,
|
||||
})}
|
||||
|
||||
@@ -5,7 +5,6 @@ import ConfirmButton from '@app/components/Common/ConfirmButton';
|
||||
import RequestModal from '@app/components/RequestModal';
|
||||
import StatusBadge from '@app/components/StatusBadge';
|
||||
import useDeepLinks from '@app/hooks/useDeepLinks';
|
||||
import useSettings from '@app/hooks/useSettings';
|
||||
import { Permission, useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
@@ -295,7 +294,6 @@ interface RequestItemProps {
|
||||
}
|
||||
|
||||
const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
||||
const settings = useSettings();
|
||||
const { ref, inView } = useInView({
|
||||
triggerOnce: true,
|
||||
});
|
||||
@@ -483,11 +481,7 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
||||
<span className="card-field-name">
|
||||
{intl.formatMessage(messages.seasons, {
|
||||
seasonCount:
|
||||
(settings.currentSettings.enableSpecialEpisodes
|
||||
? title.seasons.length
|
||||
: title.seasons.filter(
|
||||
(season) => season.seasonNumber !== 0
|
||||
).length) === request.seasons.length
|
||||
title.seasons.length === request.seasons.length
|
||||
? 0
|
||||
: request.seasons.length,
|
||||
})}
|
||||
|
||||
@@ -2,16 +2,13 @@ import Button from '@app/components/Common/Button';
|
||||
import Header from '@app/components/Common/Header';
|
||||
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
||||
import PageTitle from '@app/components/Common/PageTitle';
|
||||
import Tooltip from '@app/components/Common/Tooltip';
|
||||
import RequestItem from '@app/components/RequestList/RequestItem';
|
||||
import { useUpdateQueryParams } from '@app/hooks/useUpdateQueryParams';
|
||||
import { useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import {
|
||||
ArrowDownIcon,
|
||||
ArrowUpIcon,
|
||||
Bars3BottomLeftIcon,
|
||||
BarsArrowDownIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
FunnelIcon,
|
||||
@@ -28,7 +25,6 @@ const messages = defineMessages('components.RequestList', {
|
||||
showallrequests: 'Show All Requests',
|
||||
sortAdded: 'Most Recent',
|
||||
sortModified: 'Last Modified',
|
||||
sortDirection: 'Toggle Sort Direction',
|
||||
});
|
||||
|
||||
enum Filter {
|
||||
@@ -43,8 +39,6 @@ enum Filter {
|
||||
|
||||
type Sort = 'added' | 'modified';
|
||||
|
||||
type SortDirection = 'asc' | 'desc';
|
||||
|
||||
const RequestList = () => {
|
||||
const router = useRouter();
|
||||
const intl = useIntl();
|
||||
@@ -54,8 +48,6 @@ const RequestList = () => {
|
||||
const { user: currentUser } = useUser();
|
||||
const [currentFilter, setCurrentFilter] = useState<Filter>(Filter.PENDING);
|
||||
const [currentSort, setCurrentSort] = useState<Sort>('added');
|
||||
const [currentSortDirection, setCurrentSortDirection] =
|
||||
useState<SortDirection>('desc');
|
||||
const [currentPageSize, setCurrentPageSize] = useState<number>(10);
|
||||
|
||||
const page = router.query.page ? Number(router.query.page) : 1;
|
||||
@@ -69,7 +61,7 @@ const RequestList = () => {
|
||||
} = useSWR<RequestResultsResponse>(
|
||||
`/api/v1/request?take=${currentPageSize}&skip=${
|
||||
pageIndex * currentPageSize
|
||||
}&filter=${currentFilter}&sort=${currentSort}&sortDirection=${currentSortDirection}${
|
||||
}&filter=${currentFilter}&sort=${currentSort}${
|
||||
router.pathname.startsWith('/profile')
|
||||
? `&requestedBy=${currentUser?.id}`
|
||||
: router.query.userId
|
||||
@@ -88,9 +80,6 @@ const RequestList = () => {
|
||||
setCurrentFilter(filterSettings.currentFilter);
|
||||
setCurrentSort(filterSettings.currentSort);
|
||||
setCurrentPageSize(filterSettings.currentPageSize);
|
||||
if (['asc', 'desc'].includes(filterSettings.currentSortDirection)) {
|
||||
setCurrentSortDirection(filterSettings.currentSortDirection);
|
||||
}
|
||||
}
|
||||
|
||||
// If filter value is provided in query, use that instead
|
||||
@@ -106,11 +95,10 @@ const RequestList = () => {
|
||||
JSON.stringify({
|
||||
currentFilter,
|
||||
currentSort,
|
||||
currentSortDirection,
|
||||
currentPageSize,
|
||||
})
|
||||
);
|
||||
}, [currentFilter, currentSort, currentSortDirection, currentPageSize]);
|
||||
}, [currentFilter, currentSort, currentPageSize]);
|
||||
|
||||
if (!data && !error) {
|
||||
return <LoadingSpinner />;
|
||||
@@ -194,7 +182,7 @@ const RequestList = () => {
|
||||
</div>
|
||||
<div className="mb-2 flex flex-grow sm:mb-0 lg:flex-grow-0">
|
||||
<span className="inline-flex cursor-default items-center rounded-l-md border border-r-0 border-gray-500 bg-gray-800 px-3 text-gray-100 sm:text-sm">
|
||||
<Bars3BottomLeftIcon className="h-6 w-6" />
|
||||
<BarsArrowDownIcon className="h-6 w-6" />
|
||||
</span>
|
||||
<select
|
||||
id="sort"
|
||||
@@ -209,7 +197,7 @@ const RequestList = () => {
|
||||
});
|
||||
}}
|
||||
value={currentSort}
|
||||
className="rounded-none border-r-0"
|
||||
className="rounded-r-only"
|
||||
>
|
||||
<option value="added">
|
||||
{intl.formatMessage(messages.sortAdded)}
|
||||
@@ -218,24 +206,6 @@ const RequestList = () => {
|
||||
{intl.formatMessage(messages.sortModified)}
|
||||
</option>
|
||||
</select>
|
||||
<Tooltip content={intl.formatMessage(messages.sortDirection)}>
|
||||
<Button
|
||||
buttonType="default"
|
||||
className="z-40 mr-2 rounded-l-none border !border-gray-500 !bg-gray-800 !px-3 !text-gray-500 hover:!bg-gray-400 hover:!text-white"
|
||||
buttonSize="md"
|
||||
onClick={() =>
|
||||
setCurrentSortDirection(
|
||||
currentSortDirection === 'asc' ? 'desc' : 'asc'
|
||||
)
|
||||
}
|
||||
>
|
||||
{currentSortDirection === 'asc' ? (
|
||||
<ArrowUpIcon className="h-6 w-6" />
|
||||
) : (
|
||||
<ArrowDownIcon className="h-6 w-6" />
|
||||
)}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -253,13 +253,9 @@ const TvRequestModal = ({
|
||||
};
|
||||
|
||||
const getAllSeasons = (): number[] => {
|
||||
let allSeasons = (data?.seasons ?? []).filter(
|
||||
(season) => season.episodeCount !== 0
|
||||
);
|
||||
if (!settings.currentSettings.enableSpecialEpisodes) {
|
||||
allSeasons = allSeasons.filter((season) => season.seasonNumber > 0);
|
||||
}
|
||||
return allSeasons.map((season) => season.seasonNumber);
|
||||
return (data?.seasons ?? [])
|
||||
.filter((season) => season.episodeCount !== 0)
|
||||
.map((season) => season.seasonNumber);
|
||||
};
|
||||
|
||||
const getAllRequestedSeasons = (): number[] => {
|
||||
@@ -581,12 +577,7 @@ const TvRequestModal = ({
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-700">
|
||||
{data?.seasons
|
||||
.filter(
|
||||
(season) =>
|
||||
(!settings.currentSettings.enableSpecialEpisodes
|
||||
? season.seasonNumber !== 0
|
||||
: true) && season.episodeCount !== 0
|
||||
)
|
||||
.filter((season) => season.episodeCount !== 0)
|
||||
.map((season) => {
|
||||
const seasonRequest = getSeasonRequest(
|
||||
season.seasonNumber
|
||||
|
||||
@@ -13,7 +13,6 @@ import type {
|
||||
TmdbKeywordSearchResponse,
|
||||
} from '@server/api/themoviedb/interfaces';
|
||||
import type { GenreSliderItem } from '@server/interfaces/api/discoverInterfaces';
|
||||
import type { UserResultsResponse } from '@server/interfaces/api/userInterfaces';
|
||||
import type {
|
||||
Keyword,
|
||||
ProductionCompany,
|
||||
@@ -30,7 +29,6 @@ const messages = defineMessages('components.Selector', {
|
||||
searchKeywords: 'Search keywords…',
|
||||
searchGenres: 'Select genres…',
|
||||
searchStudios: 'Search studios…',
|
||||
searchUsers: 'Select users…',
|
||||
starttyping: 'Starting typing to search.',
|
||||
nooptions: 'No results.',
|
||||
showmore: 'Show More',
|
||||
@@ -376,11 +374,7 @@ export const WatchProviderSelector = ({
|
||||
const { currentSettings } = useSettings();
|
||||
const [showMore, setShowMore] = useState(false);
|
||||
const [watchRegion, setWatchRegion] = useState(
|
||||
region
|
||||
? region
|
||||
: currentSettings.discoverRegion
|
||||
? currentSettings.discoverRegion
|
||||
: 'US'
|
||||
region ? region : currentSettings.region ? currentSettings.region : 'US'
|
||||
);
|
||||
const [activeProvider, setActiveProvider] = useState<number[]>(
|
||||
activeProviders ?? []
|
||||
@@ -548,77 +542,3 @@ export const WatchProviderSelector = ({
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const UserSelector = ({
|
||||
isMulti,
|
||||
defaultValue,
|
||||
onChange,
|
||||
}: BaseSelectorMultiProps | BaseSelectorSingleProps) => {
|
||||
const intl = useIntl();
|
||||
const [defaultDataValue, setDefaultDataValue] = useState<
|
||||
{ label: string; value: number }[] | null
|
||||
>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const loadUsers = async (): Promise<void> => {
|
||||
if (!defaultValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
const users = defaultValue.split(',');
|
||||
|
||||
const res = await fetch(`/api/v1/user`);
|
||||
if (!res.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
const response: UserResultsResponse = await res.json();
|
||||
|
||||
const genreData = users
|
||||
.filter((u) => response.results.find((user) => user.id === Number(u)))
|
||||
.map((u) => response.results.find((user) => user.id === Number(u)))
|
||||
.map((u) => ({
|
||||
label: u?.displayName ?? '',
|
||||
value: u?.id ?? 0,
|
||||
}));
|
||||
|
||||
setDefaultDataValue(genreData);
|
||||
};
|
||||
|
||||
loadUsers();
|
||||
}, [defaultValue]);
|
||||
|
||||
const loadUserOptions = async (inputValue: string) => {
|
||||
const res = await fetch(
|
||||
`/api/v1/user${inputValue ? `?q=${encodeURIComponent(inputValue)}` : ''}`
|
||||
);
|
||||
if (!res.ok) throw new Error();
|
||||
const results: UserResultsResponse = await res.json();
|
||||
|
||||
return results.results
|
||||
.map((result) => ({
|
||||
label: result.displayName,
|
||||
value: result.id,
|
||||
}))
|
||||
.filter(({ label }) =>
|
||||
label.toLowerCase().includes(inputValue.toLowerCase())
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<AsyncSelect
|
||||
key={`user-select-${defaultDataValue}`}
|
||||
className="react-select-container"
|
||||
classNamePrefix="react-select"
|
||||
defaultValue={isMulti ? defaultDataValue : defaultDataValue?.[0]}
|
||||
defaultOptions
|
||||
cacheOptions
|
||||
isMulti={isMulti}
|
||||
loadOptions={loadUserOptions}
|
||||
placeholder={intl.formatMessage(messages.searchUsers)}
|
||||
onChange={(value) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
onChange(value as any);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -23,13 +23,8 @@ const messages = defineMessages('components.Settings.Notifications', {
|
||||
chatId: 'Chat ID',
|
||||
chatIdTip:
|
||||
'Start a chat with your bot, add <GetIdBotLink>@get_id_bot</GetIdBotLink>, and issue the <code>/my_id</code> command',
|
||||
messageThreadId: 'Thread/Topic ID',
|
||||
messageThreadIdTip:
|
||||
"If your group-chat has topics enabled, you can specify a thread/topic's ID here",
|
||||
validationBotAPIRequired: 'You must provide a bot authorization token',
|
||||
validationChatIdRequired: 'You must provide a valid chat ID',
|
||||
validationMessageThreadId:
|
||||
'The thread/topic ID must be a positive whole number',
|
||||
telegramsettingssaved: 'Telegram notification settings saved successfully!',
|
||||
telegramsettingsfailed: 'Telegram notification settings failed to save.',
|
||||
toastTelegramTestSending: 'Sending Telegram test notification…',
|
||||
@@ -69,15 +64,6 @@ const NotificationsTelegram = () => {
|
||||
/^-?\d+$/,
|
||||
intl.formatMessage(messages.validationChatIdRequired)
|
||||
),
|
||||
messageThreadId: Yup.string()
|
||||
.when(['types'], {
|
||||
is: (enabled: boolean, types: number) => enabled && !!types,
|
||||
then: Yup.string()
|
||||
.nullable()
|
||||
.required(intl.formatMessage(messages.validationMessageThreadId)),
|
||||
otherwise: Yup.string().nullable(),
|
||||
})
|
||||
.matches(/^\d+$/, intl.formatMessage(messages.validationMessageThreadId)),
|
||||
});
|
||||
|
||||
if (!data && !error) {
|
||||
@@ -92,7 +78,6 @@ const NotificationsTelegram = () => {
|
||||
botUsername: data?.options.botUsername,
|
||||
botAPI: data?.options.botAPI,
|
||||
chatId: data?.options.chatId,
|
||||
messageThreadId: data?.options.messageThreadId,
|
||||
sendSilently: data?.options.sendSilently,
|
||||
}}
|
||||
validationSchema={NotificationsTelegramSchema}
|
||||
@@ -109,7 +94,6 @@ const NotificationsTelegram = () => {
|
||||
options: {
|
||||
botAPI: values.botAPI,
|
||||
chatId: values.chatId,
|
||||
messageThreadId: values.messageThreadId,
|
||||
sendSilently: values.sendSilently,
|
||||
botUsername: values.botUsername,
|
||||
},
|
||||
@@ -167,7 +151,6 @@ const NotificationsTelegram = () => {
|
||||
options: {
|
||||
botAPI: values.botAPI,
|
||||
chatId: values.chatId,
|
||||
messageThreadId: values.messageThreadId,
|
||||
sendSilently: values.sendSilently,
|
||||
botUsername: values.botUsername,
|
||||
},
|
||||
@@ -303,28 +286,6 @@ const NotificationsTelegram = () => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="messageThreadId" className="text-label">
|
||||
{intl.formatMessage(messages.messageThreadId)}
|
||||
<span className="label-tip">
|
||||
{intl.formatMessage(messages.messageThreadIdTip)}
|
||||
</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<Field
|
||||
id="messageThreadId"
|
||||
name="messageThreadId"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
{errors.messageThreadId &&
|
||||
touched.messageThreadId &&
|
||||
typeof errors.messageThreadId === 'string' && (
|
||||
<div className="error">{errors.messageThreadId}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="sendSilently" className="checkbox-label">
|
||||
<span>{intl.formatMessage(messages.sendSilently)}</span>
|
||||
|
||||
@@ -1,391 +0,0 @@
|
||||
import Modal from '@app/components/Common/Modal';
|
||||
import LanguageSelector from '@app/components/LanguageSelector';
|
||||
import {
|
||||
GenreSelector,
|
||||
KeywordSelector,
|
||||
UserSelector,
|
||||
} from '@app/components/Selector';
|
||||
import type { DVRTestResponse } from '@app/components/Settings/SettingsServices';
|
||||
import useSettings from '@app/hooks/useSettings';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { Transition } from '@headlessui/react';
|
||||
import type OverrideRule from '@server/entity/OverrideRule';
|
||||
import { Field, Formik } from 'formik';
|
||||
import { useIntl } from 'react-intl';
|
||||
import Select from 'react-select';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
|
||||
const messages = defineMessages('components.Settings.OverrideRuleModal', {
|
||||
createrule: 'New Override Rule',
|
||||
editrule: 'Edit Override Rule',
|
||||
create: 'Create rule',
|
||||
conditions: 'Conditions',
|
||||
conditionsDescription:
|
||||
'Specifies conditions before applying parameter changes. Each field must be validated for the rules to be applied (AND operation). A field is considered verified if any of its properties match (OR operation).',
|
||||
settings: 'Settings',
|
||||
settingsDescription:
|
||||
'Specifies which settings will be changed when the above conditions are met.',
|
||||
users: 'Users',
|
||||
genres: 'Genres',
|
||||
languages: 'Languages',
|
||||
keywords: 'Keywords',
|
||||
rootfolder: 'Root Folder',
|
||||
selectRootFolder: 'Select root folder',
|
||||
qualityprofile: 'Quality Profile',
|
||||
selectQualityProfile: 'Select quality profile',
|
||||
tags: 'Tags',
|
||||
notagoptions: 'No tags.',
|
||||
selecttags: 'Select tags',
|
||||
ruleCreated: 'Override rule created successfully!',
|
||||
ruleUpdated: 'Override rule updated successfully!',
|
||||
});
|
||||
|
||||
type OptionType = {
|
||||
value: number;
|
||||
label: string;
|
||||
};
|
||||
|
||||
interface OverrideRuleModalProps {
|
||||
rule: OverrideRule | null;
|
||||
onClose: () => void;
|
||||
testResponse: DVRTestResponse;
|
||||
radarrId?: number;
|
||||
sonarrId?: number;
|
||||
}
|
||||
|
||||
const OverrideRuleModal = ({
|
||||
onClose,
|
||||
rule,
|
||||
testResponse,
|
||||
radarrId,
|
||||
sonarrId,
|
||||
}: OverrideRuleModalProps) => {
|
||||
const intl = useIntl();
|
||||
const { addToast } = useToasts();
|
||||
const { currentSettings } = useSettings();
|
||||
|
||||
return (
|
||||
<Transition
|
||||
as="div"
|
||||
appear
|
||||
show
|
||||
enter="transition-opacity ease-in-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity ease-in-out duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Formik
|
||||
initialValues={{
|
||||
users: rule?.users,
|
||||
genre: rule?.genre,
|
||||
language: rule?.language,
|
||||
keywords: rule?.keywords,
|
||||
profileId: rule?.profileId,
|
||||
rootFolder: rule?.rootFolder,
|
||||
tags: rule?.tags,
|
||||
}}
|
||||
onSubmit={async (values) => {
|
||||
try {
|
||||
const submission = {
|
||||
users: values.users || null,
|
||||
genre: values.genre || null,
|
||||
language: values.language || null,
|
||||
keywords: values.keywords || null,
|
||||
profileId: Number(values.profileId) || null,
|
||||
rootFolder: values.rootFolder || null,
|
||||
tags: values.tags || null,
|
||||
radarrServiceId: radarrId,
|
||||
sonarrServiceId: sonarrId,
|
||||
};
|
||||
if (!rule) {
|
||||
const res = await fetch('/api/v1/overrideRule', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(submission),
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
addToast(intl.formatMessage(messages.ruleCreated), {
|
||||
appearance: 'success',
|
||||
autoDismiss: true,
|
||||
});
|
||||
} else {
|
||||
const res = await fetch(`/api/v1/overrideRule/${rule.id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(submission),
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
addToast(intl.formatMessage(messages.ruleUpdated), {
|
||||
appearance: 'success',
|
||||
autoDismiss: true,
|
||||
});
|
||||
}
|
||||
onClose();
|
||||
} catch (e) {
|
||||
// set error here
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({
|
||||
errors,
|
||||
touched,
|
||||
values,
|
||||
handleSubmit,
|
||||
setFieldValue,
|
||||
isSubmitting,
|
||||
isValid,
|
||||
}) => {
|
||||
return (
|
||||
<Modal
|
||||
onCancel={onClose}
|
||||
okButtonType="primary"
|
||||
okText={
|
||||
isSubmitting
|
||||
? intl.formatMessage(globalMessages.saving)
|
||||
: rule
|
||||
? intl.formatMessage(globalMessages.save)
|
||||
: intl.formatMessage(messages.create)
|
||||
}
|
||||
okDisabled={
|
||||
isSubmitting ||
|
||||
!isValid ||
|
||||
(!values.users &&
|
||||
!values.genre &&
|
||||
!values.language &&
|
||||
!values.keywords) ||
|
||||
(!values.rootFolder && !values.profileId && !values.tags)
|
||||
}
|
||||
onOk={() => handleSubmit()}
|
||||
title={
|
||||
!rule
|
||||
? intl.formatMessage(messages.createrule)
|
||||
: intl.formatMessage(messages.editrule)
|
||||
}
|
||||
>
|
||||
<div className="mb-6">
|
||||
<h3 className="text-lg font-bold leading-8 text-gray-100">
|
||||
{intl.formatMessage(messages.conditions)}
|
||||
</h3>
|
||||
<p className="description">
|
||||
{intl.formatMessage(messages.conditionsDescription)}
|
||||
</p>
|
||||
<div className="form-row">
|
||||
<label htmlFor="users" className="text-label">
|
||||
{intl.formatMessage(messages.users)}
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<UserSelector
|
||||
defaultValue={values.users}
|
||||
isMulti
|
||||
onChange={(users) => {
|
||||
setFieldValue(
|
||||
'users',
|
||||
users?.map((v) => v.value).join(',')
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{errors.users &&
|
||||
touched.users &&
|
||||
typeof errors.users === 'string' && (
|
||||
<div className="error">{errors.users}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="genre" className="text-label">
|
||||
{intl.formatMessage(messages.genres)}
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<GenreSelector
|
||||
type={radarrId ? 'movie' : 'tv'}
|
||||
defaultValue={values.genre}
|
||||
isMulti
|
||||
onChange={(genres) => {
|
||||
setFieldValue(
|
||||
'genre',
|
||||
genres?.map((v) => v.value).join(',')
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{errors.genre &&
|
||||
touched.genre &&
|
||||
typeof errors.genre === 'string' && (
|
||||
<div className="error">{errors.genre}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="language" className="text-label">
|
||||
{intl.formatMessage(messages.languages)}
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<LanguageSelector
|
||||
value={values.language}
|
||||
serverValue={currentSettings.originalLanguage}
|
||||
setFieldValue={(_key, value) => {
|
||||
setFieldValue('language', value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{errors.language &&
|
||||
touched.language &&
|
||||
typeof errors.language === 'string' && (
|
||||
<div className="error">{errors.language}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="keywords" className="text-label">
|
||||
{intl.formatMessage(messages.keywords)}
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<KeywordSelector
|
||||
defaultValue={values.keywords}
|
||||
isMulti
|
||||
onChange={(value) => {
|
||||
setFieldValue(
|
||||
'keywords',
|
||||
value?.map((v) => v.value).join(',')
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{errors.keywords &&
|
||||
touched.keywords &&
|
||||
typeof errors.keywords === 'string' && (
|
||||
<div className="error">{errors.keywords}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="mt-4 text-lg font-bold leading-8 text-gray-100">
|
||||
{intl.formatMessage(messages.settings)}
|
||||
</h3>
|
||||
<p className="description">
|
||||
{intl.formatMessage(messages.settingsDescription)}
|
||||
</p>
|
||||
<div className="form-row">
|
||||
<label htmlFor="rootFolderRule" className="text-label">
|
||||
{intl.formatMessage(messages.rootfolder)}
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<Field as="select" id="rootFolderRule" name="rootFolder">
|
||||
<option value="">
|
||||
{intl.formatMessage(messages.selectRootFolder)}
|
||||
</option>
|
||||
{testResponse.rootFolders.length > 0 &&
|
||||
testResponse.rootFolders.map((folder) => (
|
||||
<option
|
||||
key={`loaded-profile-${folder.id}`}
|
||||
value={folder.path}
|
||||
>
|
||||
{folder.path}
|
||||
</option>
|
||||
))}
|
||||
</Field>
|
||||
</div>
|
||||
{errors.rootFolder &&
|
||||
touched.rootFolder &&
|
||||
typeof errors.rootFolder === 'string' && (
|
||||
<div className="error">{errors.rootFolder}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="profileIdRule" className="text-label">
|
||||
{intl.formatMessage(messages.qualityprofile)}
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<Field as="select" id="profileIdRule" name="profileId">
|
||||
<option value="">
|
||||
{intl.formatMessage(messages.selectQualityProfile)}
|
||||
</option>
|
||||
{testResponse.profiles.length > 0 &&
|
||||
testResponse.profiles.map((profile) => (
|
||||
<option
|
||||
key={`loaded-profile-${profile.id}`}
|
||||
value={profile.id}
|
||||
>
|
||||
{profile.name}
|
||||
</option>
|
||||
))}
|
||||
</Field>
|
||||
</div>
|
||||
{errors.profileId &&
|
||||
touched.profileId &&
|
||||
typeof errors.profileId === 'string' && (
|
||||
<div className="error">{errors.profileId}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="tags" className="text-label">
|
||||
{intl.formatMessage(messages.tags)}
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<Select<OptionType, true>
|
||||
options={testResponse.tags.map((tag) => ({
|
||||
label: tag.label,
|
||||
value: tag.id,
|
||||
}))}
|
||||
isMulti
|
||||
placeholder={intl.formatMessage(messages.selecttags)}
|
||||
className="react-select-container"
|
||||
classNamePrefix="react-select"
|
||||
value={
|
||||
(values?.tags
|
||||
?.split(',')
|
||||
.map((tagId) => {
|
||||
const foundTag = testResponse.tags.find(
|
||||
(tag) => tag.id === Number(tagId)
|
||||
);
|
||||
|
||||
if (!foundTag) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
value: foundTag.id,
|
||||
label: foundTag.label,
|
||||
};
|
||||
})
|
||||
.filter(
|
||||
(option) => option !== undefined
|
||||
) as OptionType[]) || []
|
||||
}
|
||||
onChange={(value) => {
|
||||
setFieldValue(
|
||||
'tags',
|
||||
value.map((option) => option.value).join(',')
|
||||
);
|
||||
}}
|
||||
noOptionsMessage={() =>
|
||||
intl.formatMessage(messages.notagoptions)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}}
|
||||
</Formik>
|
||||
</Transition>
|
||||
);
|
||||
};
|
||||
|
||||
export default OverrideRuleModal;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user