mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2025-12-24 02:39:18 -05:00
Compare commits
71 Commits
preview-fi
...
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,51 +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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
2
.github/workflows/ci.yml
vendored
2
.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
|
||||
|
||||
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:
|
||||
|
||||
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
|
||||
2
.github/workflows/release.yml
vendored
2
.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
|
||||
|
||||
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
|
||||
|
||||
20
README.md
20
README.md
@@ -11,11 +11,11 @@
|
||||
<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-60-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 is a fork of [Overseerr](https://github.com/sct/overseerr) built to bring additional support for [Jellyfin](https://github.com/jellyfin/jellyfin) & [Emby](https://github.com/MediaBrowser/Emby) media servers!
|
||||
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
|
||||
|
||||
@@ -147,22 +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>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -1,23 +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/
|
||||
@@ -1,13 +0,0 @@
|
||||
apiVersion: v2
|
||||
kubeVersion: ">=1.23.0-0"
|
||||
name: Jellyseerr
|
||||
description: Jellyseerr helm chart for Kubernetes
|
||||
type: application
|
||||
version: 1.1.0
|
||||
appVersion: "2.1.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,69 +0,0 @@
|
||||
# Jellyseerr
|
||||
|
||||
  
|
||||
|
||||
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 | `{}` | |
|
||||
| autoscaling.enabled | bool | `false` | |
|
||||
| autoscaling.maxReplicas | int | `100` | |
|
||||
| autoscaling.minReplicas | int | `1` | |
|
||||
| autoscaling.targetCPUUtilizationPercentage | int | `80` | |
|
||||
| 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 | `"docker.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,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,85 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "jellyseerr.fullname" . }}
|
||||
labels:
|
||||
{{- include "jellyseerr.labels" . | nindent 4 }}
|
||||
spec:
|
||||
{{- if not .Values.autoscaling.enabled }}
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
{{- end }}
|
||||
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,32 +0,0 @@
|
||||
{{- if .Values.autoscaling.enabled }}
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ include "jellyseerr.fullname" . }}
|
||||
labels:
|
||||
{{- include "jellyseerr.labels" . | nindent 4 }}
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: {{ include "jellyseerr.fullname" . }}
|
||||
minReplicas: {{ .Values.autoscaling.minReplicas }}
|
||||
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
|
||||
metrics:
|
||||
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: memory
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- 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,108 +0,0 @@
|
||||
replicaCount: 1
|
||||
|
||||
image:
|
||||
registry: docker.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
|
||||
|
||||
autoscaling:
|
||||
enabled: false
|
||||
minReplicas: 1
|
||||
maxReplicas: 100
|
||||
targetCPUUtilizationPercentage: 80
|
||||
# targetMemoryUtilizationPercentage: 80
|
||||
|
||||
nodeSelector: {}
|
||||
|
||||
tolerations: []
|
||||
|
||||
affinity: {}
|
||||
@@ -16,13 +16,11 @@
|
||||
"hideAvailable": false,
|
||||
"localLogin": true,
|
||||
"newPlexLogin": true,
|
||||
"discoverRegion": "",
|
||||
"streamingRegion": "",
|
||||
"region": "",
|
||||
"originalLanguage": "",
|
||||
"trustProxy": false,
|
||||
"mediaServerType": 1,
|
||||
"partialRequestsEnabled": true,
|
||||
"enableSpecialEpisodes": false,
|
||||
"locale": "en"
|
||||
},
|
||||
"plex": {
|
||||
@@ -101,7 +99,6 @@
|
||||
"options": {
|
||||
"botAPI": "",
|
||||
"chatId": "",
|
||||
"messageThreadId": "",
|
||||
"sendSilently": false
|
||||
}
|
||||
},
|
||||
|
||||
@@ -47,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
|
||||
|
||||
@@ -12,7 +12,7 @@ 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)
|
||||
|
||||
@@ -26,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
|
||||
@@ -58,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
|
||||
```
|
||||
@@ -200,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
|
||||
|
||||
@@ -145,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>
|
||||
|
||||
@@ -177,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>
|
||||
@@ -205,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,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.
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,12 +143,10 @@ components:
|
||||
properties:
|
||||
locale:
|
||||
type: string
|
||||
discoverRegion:
|
||||
region:
|
||||
type: string
|
||||
originalLanguage:
|
||||
type: string
|
||||
streamingRegion:
|
||||
type: string
|
||||
MainSettings:
|
||||
type: object
|
||||
properties:
|
||||
@@ -188,9 +186,6 @@ components:
|
||||
defaultPermissions:
|
||||
type: number
|
||||
example: 32
|
||||
enableSpecialEpisodes:
|
||||
type: boolean
|
||||
example: false
|
||||
PlexLibrary:
|
||||
type: object
|
||||
properties:
|
||||
@@ -1341,8 +1336,6 @@ components:
|
||||
type: string
|
||||
chatId:
|
||||
type: string
|
||||
messageThreadId:
|
||||
type: string
|
||||
sendSilently:
|
||||
type: boolean
|
||||
PushbulletSettings:
|
||||
@@ -1826,9 +1819,6 @@ components:
|
||||
telegramChatId:
|
||||
type: string
|
||||
nullable: true
|
||||
telegramMessageThreadId:
|
||||
type: string
|
||||
nullable: true
|
||||
telegramSendSilently:
|
||||
type: boolean
|
||||
nullable: true
|
||||
@@ -1942,11 +1932,6 @@ components:
|
||||
type: string
|
||||
native_name:
|
||||
type: string
|
||||
OverrideRule:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
securitySchemes:
|
||||
cookieAuth:
|
||||
type: apiKey
|
||||
@@ -3770,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
|
||||
@@ -3891,7 +3871,7 @@ paths:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
jellyfinUserIds:
|
||||
jellyfinIds:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
@@ -5456,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:
|
||||
@@ -6983,68 +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
|
||||
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": {
|
||||
|
||||
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';
|
||||
@@ -16,10 +14,6 @@ interface ExternalAPIOptions {
|
||||
rateLimit?: RateLimitOptions;
|
||||
}
|
||||
|
||||
interface CustomRequestConfig extends RequestInit {
|
||||
params?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
class ExternalAPI {
|
||||
protected fetch: typeof fetch;
|
||||
protected params: Record<string, string>;
|
||||
@@ -40,8 +34,6 @@ class ExternalAPI {
|
||||
|
||||
const url = new URL(baseUrl);
|
||||
|
||||
const settings = getSettings();
|
||||
|
||||
this.defaultHeaders = {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
@@ -50,9 +42,6 @@ class ExternalAPI {
|
||||
`${url.username}:${url.password}`
|
||||
).toString('base64')}`,
|
||||
}),
|
||||
...(settings.main.mediaServerType === MediaServerType.EMBY && {
|
||||
'Accept-Encoding': 'gzip',
|
||||
}),
|
||||
...options.headers,
|
||||
};
|
||||
|
||||
@@ -71,11 +60,12 @@ class ExternalAPI {
|
||||
endpoint: string,
|
||||
params?: Record<string, string>,
|
||||
ttl?: number,
|
||||
config?: CustomRequestConfig
|
||||
config?: RequestInit
|
||||
): Promise<T> {
|
||||
const headers = { ...this.defaultHeaders, ...config?.headers };
|
||||
const cacheKey = this.serializeCacheKey(endpoint, config?.params, headers);
|
||||
|
||||
const cacheKey = this.serializeCacheKey(endpoint, {
|
||||
...this.params,
|
||||
...params,
|
||||
});
|
||||
const cachedItem = this.cache?.get<T>(cacheKey);
|
||||
if (cachedItem) {
|
||||
return cachedItem;
|
||||
@@ -296,14 +286,6 @@ class ExternalAPI {
|
||||
return data;
|
||||
}
|
||||
|
||||
protected removeCache(endpoint: string, params?: Record<string, string>) {
|
||||
const cacheKey = this.serializeCacheKey(endpoint, {
|
||||
...this.params,
|
||||
...params,
|
||||
});
|
||||
this.cache?.del(cacheKey);
|
||||
}
|
||||
|
||||
private formatUrl(
|
||||
endpoint: string,
|
||||
params?: Record<string, string>,
|
||||
@@ -328,15 +310,13 @@ class ExternalAPI {
|
||||
|
||||
private serializeCacheKey(
|
||||
endpoint: string,
|
||||
params?: Record<string, unknown>,
|
||||
headers?: Record<string, unknown>
|
||||
params?: Record<string, unknown>
|
||||
) {
|
||||
const key = `${this.baseUrl}${endpoint}`;
|
||||
if (!params && !headers) {
|
||||
return key;
|
||||
if (!params) {
|
||||
return `${this.baseUrl}${endpoint}`;
|
||||
}
|
||||
|
||||
return `${key}${JSON.stringify({ params, headers })}`;
|
||||
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,12 +99,12 @@ 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',
|
||||
{
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,13 +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;
|
||||
locale: string;
|
||||
proxy: ProxySettings;
|
||||
}
|
||||
@@ -147,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;
|
||||
@@ -216,7 +211,6 @@ export interface NotificationAgentTelegram extends NotificationAgentConfig {
|
||||
botUsername?: string;
|
||||
botAPI: string;
|
||||
chatId: string;
|
||||
messageThreadId: string;
|
||||
sendSilently: boolean;
|
||||
};
|
||||
}
|
||||
@@ -339,13 +333,11 @@ class Settings {
|
||||
hideAvailable: false,
|
||||
localLogin: true,
|
||||
newPlexLogin: true,
|
||||
discoverRegion: '',
|
||||
streamingRegion: '',
|
||||
region: '',
|
||||
originalLanguage: '',
|
||||
trustProxy: false,
|
||||
mediaServerType: MediaServerType.NOT_CONFIGURED,
|
||||
partialRequestsEnabled: true,
|
||||
enableSpecialEpisodes: false,
|
||||
locale: 'en',
|
||||
proxy: {
|
||||
enabled: false,
|
||||
@@ -428,7 +420,6 @@ class Settings {
|
||||
options: {
|
||||
botAPI: '',
|
||||
chatId: '',
|
||||
messageThreadId: '',
|
||||
sendSilently: false,
|
||||
},
|
||||
},
|
||||
@@ -585,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,
|
||||
@@ -699,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,17 +0,0 @@
|
||||
import type { AllSettings } from '@server/lib/settings';
|
||||
|
||||
const migrateRegionSetting = (settings: any): AllSettings => {
|
||||
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,
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
@@ -1,267 +0,0 @@
|
||||
import type { DVRTestResponse } from '@app/components/Settings/SettingsServices';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { PencilIcon, TrashIcon } from '@heroicons/react/24/solid';
|
||||
import type { TmdbGenre } from '@server/api/themoviedb/interfaces';
|
||||
import type OverrideRule from '@server/entity/OverrideRule';
|
||||
import type { User } from '@server/entity/User';
|
||||
import type {
|
||||
Language,
|
||||
RadarrSettings,
|
||||
SonarrSettings,
|
||||
} from '@server/lib/settings';
|
||||
import type { Keyword } from '@server/models/common';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import useSWR from 'swr';
|
||||
|
||||
const messages = defineMessages('components.Settings.OverrideRuleTile', {
|
||||
qualityprofile: 'Quality Profile',
|
||||
rootfolder: 'Root Folder',
|
||||
tags: 'Tags',
|
||||
users: 'Users',
|
||||
genre: 'Genre',
|
||||
language: 'Language',
|
||||
keywords: 'Keywords',
|
||||
conditions: 'Conditions',
|
||||
settings: 'Settings',
|
||||
});
|
||||
|
||||
interface OverrideRuleTileProps {
|
||||
rules: OverrideRule[];
|
||||
setOverrideRuleModal: ({
|
||||
open,
|
||||
rule,
|
||||
testResponse,
|
||||
}: {
|
||||
open: boolean;
|
||||
rule: OverrideRule | null;
|
||||
testResponse: DVRTestResponse;
|
||||
}) => void;
|
||||
testResponse: DVRTestResponse;
|
||||
radarr?: RadarrSettings | null;
|
||||
sonarr?: SonarrSettings | null;
|
||||
revalidate: () => void;
|
||||
}
|
||||
|
||||
const OverrideRuleTile = ({
|
||||
rules,
|
||||
setOverrideRuleModal,
|
||||
testResponse,
|
||||
radarr,
|
||||
sonarr,
|
||||
revalidate,
|
||||
}: OverrideRuleTileProps) => {
|
||||
const intl = useIntl();
|
||||
const [users, setUsers] = useState<User[] | null>(null);
|
||||
const [keywords, setKeywords] = useState<Keyword[] | null>(null);
|
||||
const { data: languages } = useSWR<Language[]>('/api/v1/languages');
|
||||
const { data: genres } = useSWR<TmdbGenre[]>('/api/v1/genres/movie');
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const keywords = await Promise.all(
|
||||
rules
|
||||
.map((rule) => rule.keywords?.split(','))
|
||||
.flat()
|
||||
.filter((keywordId) => keywordId)
|
||||
.map(async (keywordId) => {
|
||||
const res = await fetch(`/api/v1/keyword/${keywordId}`);
|
||||
if (!res.ok) throw new Error();
|
||||
const keyword: Keyword = await res.json();
|
||||
return keyword;
|
||||
})
|
||||
);
|
||||
setKeywords(keywords);
|
||||
const users = await Promise.all(
|
||||
rules
|
||||
.map((rule) => rule.users?.split(','))
|
||||
.flat()
|
||||
.filter((userId) => userId)
|
||||
.map(async (userId) => {
|
||||
const res = await fetch(`/api/v1/user/${userId}`);
|
||||
if (!res.ok) throw new Error();
|
||||
const user: User = await res.json();
|
||||
return user;
|
||||
})
|
||||
);
|
||||
setUsers(users);
|
||||
})();
|
||||
}, [rules]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{rules
|
||||
.filter(
|
||||
(rule) =>
|
||||
(rule.radarrServiceId !== null &&
|
||||
rule.radarrServiceId === radarr?.id) ||
|
||||
(rule.sonarrServiceId !== null &&
|
||||
rule.sonarrServiceId === sonarr?.id)
|
||||
)
|
||||
.map((rule) => (
|
||||
<li className="flex h-full flex-col rounded-lg bg-gray-800 text-left shadow ring-1 ring-gray-500">
|
||||
<div className="flex w-full flex-1 items-center justify-between space-x-6 p-6">
|
||||
<div className="flex-1 truncate">
|
||||
<span className="text-lg">
|
||||
{intl.formatMessage(messages.conditions)}
|
||||
</span>
|
||||
{rule.users && (
|
||||
<p className="truncate text-sm leading-5 text-gray-300">
|
||||
<span className="mr-2 font-bold">
|
||||
{intl.formatMessage(messages.users)}
|
||||
</span>
|
||||
<div className="inline-flex gap-2">
|
||||
{rule.users.split(',').map((userId) => {
|
||||
return (
|
||||
<span>
|
||||
{
|
||||
users?.find((user) => user.id === Number(userId))
|
||||
?.displayName
|
||||
}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</p>
|
||||
)}
|
||||
{rule.genre && (
|
||||
<p className="truncate text-sm leading-5 text-gray-300">
|
||||
<span className="mr-2 font-bold">
|
||||
{intl.formatMessage(messages.genre)}
|
||||
</span>
|
||||
<div className="inline-flex gap-2">
|
||||
{rule.genre.split(',').map((genreId) => (
|
||||
<span>
|
||||
{genres?.find((g) => g.id === Number(genreId))?.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</p>
|
||||
)}
|
||||
{rule.language && (
|
||||
<p className="truncate text-sm leading-5 text-gray-300">
|
||||
<span className="mr-2 font-bold">
|
||||
{intl.formatMessage(messages.language)}
|
||||
</span>
|
||||
<div className="inline-flex gap-2">
|
||||
{rule.language
|
||||
.split('|')
|
||||
.filter((languageId) => languageId !== 'server')
|
||||
.map((languageId) => {
|
||||
const language = languages?.find(
|
||||
(language) => language.iso_639_1 === languageId
|
||||
);
|
||||
if (!language) return null;
|
||||
const languageName =
|
||||
intl.formatDisplayName(language.iso_639_1, {
|
||||
type: 'language',
|
||||
fallback: 'none',
|
||||
}) ?? language.english_name;
|
||||
return <span>{languageName}</span>;
|
||||
})}
|
||||
</div>
|
||||
</p>
|
||||
)}
|
||||
{rule.keywords && (
|
||||
<p className="truncate text-sm leading-5 text-gray-300">
|
||||
<span className="mr-2 font-bold">
|
||||
{intl.formatMessage(messages.keywords)}
|
||||
</span>
|
||||
<div className="inline-flex gap-2">
|
||||
{rule.keywords.split(',').map((keywordId) => {
|
||||
return (
|
||||
<span>
|
||||
{
|
||||
keywords?.find(
|
||||
(keyword) => keyword.id === Number(keywordId)
|
||||
)?.name
|
||||
}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</p>
|
||||
)}
|
||||
<span className="text-lg">
|
||||
{intl.formatMessage(messages.settings)}
|
||||
</span>
|
||||
{rule.profileId && (
|
||||
<p className="runcate text-sm leading-5 text-gray-300">
|
||||
<span className="mr-2 font-bold">
|
||||
{intl.formatMessage(messages.qualityprofile)}
|
||||
</span>
|
||||
{
|
||||
testResponse.profiles.find(
|
||||
(profile) => rule.profileId === profile.id
|
||||
)?.name
|
||||
}
|
||||
</p>
|
||||
)}
|
||||
{rule.rootFolder && (
|
||||
<p className="truncate text-sm leading-5 text-gray-300">
|
||||
<span className="mr-2 font-bold">
|
||||
{intl.formatMessage(messages.rootfolder)}
|
||||
</span>
|
||||
{rule.rootFolder}
|
||||
</p>
|
||||
)}
|
||||
{rule.tags && rule.tags.length > 0 && (
|
||||
<p className="truncate text-sm leading-5 text-gray-300">
|
||||
<span className="mr-2 font-bold">
|
||||
{intl.formatMessage(messages.tags)}
|
||||
</span>
|
||||
<div className="inline-flex gap-2">
|
||||
{rule.tags.split(',').map((tag) => (
|
||||
<span>
|
||||
{
|
||||
testResponse.tags?.find((t) => t.id === Number(tag))
|
||||
?.label
|
||||
}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t border-gray-500">
|
||||
<div className="-mt-px flex">
|
||||
<div className="flex w-0 flex-1 border-r border-gray-500">
|
||||
<button
|
||||
onClick={() =>
|
||||
setOverrideRuleModal({ open: true, rule, testResponse })
|
||||
}
|
||||
className="focus:ring-blue relative -mr-px inline-flex w-0 flex-1 items-center justify-center rounded-bl-lg border border-transparent py-4 text-sm font-medium leading-5 text-gray-200 transition duration-150 ease-in-out hover:text-white focus:z-10 focus:border-gray-500 focus:outline-none"
|
||||
>
|
||||
<PencilIcon className="mr-2 h-5 w-5" />
|
||||
<span>{intl.formatMessage(globalMessages.edit)}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="-ml-px flex w-0 flex-1">
|
||||
<button
|
||||
onClick={async () => {
|
||||
const res = await fetch(
|
||||
`/api/v1/overrideRule/${rule.id}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
}
|
||||
);
|
||||
if (!res.ok) throw new Error();
|
||||
revalidate();
|
||||
}}
|
||||
className="focus:ring-blue relative inline-flex w-0 flex-1 items-center justify-center rounded-br-lg border border-transparent py-4 text-sm font-medium leading-5 text-gray-200 transition duration-150 ease-in-out hover:text-white focus:z-10 focus:border-gray-500 focus:outline-none"
|
||||
>
|
||||
<TrashIcon className="mr-2 h-5 w-5" />
|
||||
<span>{intl.formatMessage(globalMessages.delete)}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default OverrideRuleTile;
|
||||
@@ -1,24 +1,14 @@
|
||||
import Button from '@app/components/Common/Button';
|
||||
import Modal from '@app/components/Common/Modal';
|
||||
import SensitiveInput from '@app/components/Common/SensitiveInput';
|
||||
import OverrideRuleTile from '@app/components/Settings/OverrideRule/OverrideRuleTile';
|
||||
import type {
|
||||
DVRTestResponse,
|
||||
RadarrTestResponse,
|
||||
} from '@app/components/Settings/SettingsServices';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { Transition } from '@headlessui/react';
|
||||
import { PlusIcon } from '@heroicons/react/24/solid';
|
||||
import type OverrideRule from '@server/entity/OverrideRule';
|
||||
import type { OverrideRuleResultsResponse } from '@server/interfaces/api/overrideRuleInterfaces';
|
||||
import type { RadarrSettings } from '@server/lib/settings';
|
||||
import { Field, Formik } from 'formik';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import Select from 'react-select';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import useSWR from 'swr';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
type OptionType = {
|
||||
@@ -79,46 +69,41 @@ const messages = defineMessages('components.Settings.RadarrModal', {
|
||||
announced: 'Announced',
|
||||
inCinemas: 'In Cinemas',
|
||||
released: 'Released',
|
||||
overrideRules: 'Override Rules',
|
||||
addrule: 'New Override Rule',
|
||||
});
|
||||
|
||||
interface TestResponse {
|
||||
profiles: {
|
||||
id: number;
|
||||
name: string;
|
||||
}[];
|
||||
rootFolders: {
|
||||
id: number;
|
||||
path: string;
|
||||
}[];
|
||||
tags: {
|
||||
id: number;
|
||||
label: string;
|
||||
}[];
|
||||
urlBase?: string;
|
||||
}
|
||||
|
||||
interface RadarrModalProps {
|
||||
radarr: RadarrSettings | null;
|
||||
onClose: () => void;
|
||||
onSave: () => void;
|
||||
overrideRuleModal: { open: boolean; rule: OverrideRule | null };
|
||||
setOverrideRuleModal: ({
|
||||
open,
|
||||
rule,
|
||||
testResponse,
|
||||
}: {
|
||||
open: boolean;
|
||||
rule: OverrideRule | null;
|
||||
testResponse: DVRTestResponse;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
const RadarrModal = ({
|
||||
onClose,
|
||||
radarr,
|
||||
onSave,
|
||||
overrideRuleModal,
|
||||
setOverrideRuleModal,
|
||||
}: RadarrModalProps) => {
|
||||
const RadarrModal = ({ onClose, radarr, onSave }: RadarrModalProps) => {
|
||||
const intl = useIntl();
|
||||
const { data: rules, mutate: revalidate } =
|
||||
useSWR<OverrideRuleResultsResponse>('/api/v1/overrideRule');
|
||||
const initialLoad = useRef(false);
|
||||
const { addToast } = useToasts();
|
||||
const [isValidated, setIsValidated] = useState(radarr ? true : false);
|
||||
const [isTesting, setIsTesting] = useState(false);
|
||||
const [testResponse, setTestResponse] = useState<RadarrTestResponse>({
|
||||
const [testResponse, setTestResponse] = useState<TestResponse>({
|
||||
profiles: [],
|
||||
rootFolders: [],
|
||||
tags: [],
|
||||
});
|
||||
|
||||
const RadarrSettingsSchema = Yup.object().shape({
|
||||
name: Yup.string().required(
|
||||
intl.formatMessage(messages.validationNameRequired)
|
||||
@@ -235,10 +220,6 @@ const RadarrModal = ({
|
||||
}
|
||||
}, [radarr, testConnection]);
|
||||
|
||||
useEffect(() => {
|
||||
revalidate();
|
||||
}, [overrideRuleModal, revalidate]);
|
||||
|
||||
return (
|
||||
<Transition
|
||||
as="div"
|
||||
@@ -382,7 +363,6 @@ const RadarrModal = ({
|
||||
values.is4k ? messages.edit4kradarr : messages.editradarr
|
||||
)
|
||||
}
|
||||
backgroundClickable={!overrideRuleModal.open}
|
||||
>
|
||||
<div className="mb-6">
|
||||
<div className="form-row">
|
||||
@@ -773,38 +753,6 @@ const RadarrModal = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="mb-4 text-xl font-bold leading-8 text-gray-100">
|
||||
{intl.formatMessage(messages.overrideRules)}
|
||||
</h3>
|
||||
<ul className="grid grid-cols-2 gap-6">
|
||||
{rules && (
|
||||
<OverrideRuleTile
|
||||
rules={rules}
|
||||
setOverrideRuleModal={setOverrideRuleModal}
|
||||
testResponse={testResponse}
|
||||
radarr={radarr}
|
||||
revalidate={revalidate}
|
||||
/>
|
||||
)}
|
||||
<li className="min-h-[8rem] rounded-lg border-2 border-dashed border-gray-400 shadow sm:min-h-[11rem]">
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Button
|
||||
buttonType="ghost"
|
||||
onClick={() =>
|
||||
setOverrideRuleModal({
|
||||
open: true,
|
||||
rule: null,
|
||||
testResponse,
|
||||
})
|
||||
}
|
||||
disabled={!isValidated}
|
||||
>
|
||||
<PlusIcon />
|
||||
<span>{intl.formatMessage(messages.addrule)}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</Modal>
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -31,12 +31,10 @@ const messages = defineMessages('components.Settings.SettingsMain', {
|
||||
apikey: 'API Key',
|
||||
applicationTitle: 'Application Title',
|
||||
applicationurl: 'Application URL',
|
||||
discoverRegion: 'Discover Region',
|
||||
discoverRegionTip: 'Filter content by regional availability',
|
||||
region: 'Discover Region',
|
||||
regionTip: 'Filter content by regional availability',
|
||||
originallanguage: 'Discover Language',
|
||||
originallanguageTip: 'Filter content by original language',
|
||||
streamingRegion: 'Streaming Region',
|
||||
streamingRegionTip: 'Show streaming sites by regional availability',
|
||||
toastApiKeySuccess: 'New API key generated successfully!',
|
||||
toastApiKeyFailure: 'Something went wrong while generating a new API key.',
|
||||
toastSettingsSuccess: 'Settings saved successfully!',
|
||||
@@ -56,7 +54,6 @@ const messages = defineMessages('components.Settings.SettingsMain', {
|
||||
validationApplicationUrl: 'You must provide a valid URL',
|
||||
validationApplicationUrlTrailingSlash: 'URL must not end in a trailing slash',
|
||||
partialRequestsEnabled: 'Allow Partial Series Requests',
|
||||
enableSpecialEpisodes: 'Allow Special Episodes Requests',
|
||||
locale: 'Display Language',
|
||||
proxyEnabled: 'HTTP(S) Proxy',
|
||||
proxyHostname: 'Proxy Hostname',
|
||||
@@ -155,11 +152,9 @@ const SettingsMain = () => {
|
||||
csrfProtection: data?.csrfProtection,
|
||||
hideAvailable: data?.hideAvailable,
|
||||
locale: data?.locale ?? 'en',
|
||||
discoverRegion: data?.discoverRegion,
|
||||
region: data?.region,
|
||||
originalLanguage: data?.originalLanguage,
|
||||
streamingRegion: data?.streamingRegion || 'US',
|
||||
partialRequestsEnabled: data?.partialRequestsEnabled,
|
||||
enableSpecialEpisodes: data?.enableSpecialEpisodes,
|
||||
trustProxy: data?.trustProxy,
|
||||
cacheImages: data?.cacheImages,
|
||||
proxyEnabled: data?.proxy?.enabled,
|
||||
@@ -186,11 +181,9 @@ const SettingsMain = () => {
|
||||
csrfProtection: values.csrfProtection,
|
||||
hideAvailable: values.hideAvailable,
|
||||
locale: values.locale,
|
||||
discoverRegion: values.discoverRegion,
|
||||
streamingRegion: values.streamingRegion,
|
||||
region: values.region,
|
||||
originalLanguage: values.originalLanguage,
|
||||
partialRequestsEnabled: values.partialRequestsEnabled,
|
||||
enableSpecialEpisodes: values.enableSpecialEpisodes,
|
||||
trustProxy: values.trustProxy,
|
||||
cacheImages: values.cacheImages,
|
||||
proxy: {
|
||||
@@ -409,17 +402,17 @@ const SettingsMain = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="discoverRegion" className="text-label">
|
||||
<span>{intl.formatMessage(messages.discoverRegion)}</span>
|
||||
<label htmlFor="region" className="text-label">
|
||||
<span>{intl.formatMessage(messages.region)}</span>
|
||||
<span className="label-tip">
|
||||
{intl.formatMessage(messages.discoverRegionTip)}
|
||||
{intl.formatMessage(messages.regionTip)}
|
||||
</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<RegionSelector
|
||||
value={values.discoverRegion ?? ''}
|
||||
name="discoverRegion"
|
||||
value={values.region ?? ''}
|
||||
name="region"
|
||||
onChange={setFieldValue}
|
||||
/>
|
||||
</div>
|
||||
@@ -433,7 +426,7 @@ const SettingsMain = () => {
|
||||
</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field relative z-30">
|
||||
<div className="form-input-field">
|
||||
<LanguageSelector
|
||||
setFieldValue={setFieldValue}
|
||||
value={values.originalLanguage}
|
||||
@@ -441,25 +434,6 @@ const SettingsMain = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="streamingRegion" className="text-label">
|
||||
<span>{intl.formatMessage(messages.streamingRegion)}</span>
|
||||
<span className="label-tip">
|
||||
{intl.formatMessage(messages.streamingRegionTip)}
|
||||
</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field relative z-20">
|
||||
<RegionSelector
|
||||
value={values.streamingRegion}
|
||||
name="streamingRegion"
|
||||
onChange={setFieldValue}
|
||||
regionType="streaming"
|
||||
disableAll
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="hideAvailable" className="checkbox-label">
|
||||
<span className="mr-2">
|
||||
@@ -501,29 +475,6 @@ const SettingsMain = () => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label
|
||||
htmlFor="enableSpecialEpisodes"
|
||||
className="checkbox-label"
|
||||
>
|
||||
<span className="mr-2">
|
||||
{intl.formatMessage(messages.enableSpecialEpisodes)}
|
||||
</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<Field
|
||||
type="checkbox"
|
||||
id="enableSpecialEpisodes"
|
||||
name="enableSpecialEpisodes"
|
||||
onChange={() => {
|
||||
setFieldValue(
|
||||
'enableSpecialEpisodes',
|
||||
!values.enableSpecialEpisodes
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="proxyEnabled" className="checkbox-label">
|
||||
<span className="mr-2">
|
||||
@@ -545,157 +496,151 @@ const SettingsMain = () => {
|
||||
</div>
|
||||
{values.proxyEnabled && (
|
||||
<>
|
||||
<div className="mr-2 ml-4">
|
||||
<div className="form-row">
|
||||
<label
|
||||
htmlFor="proxyHostname"
|
||||
className="checkbox-label"
|
||||
>
|
||||
<div className="form-row">
|
||||
<label htmlFor="proxyHostname" className="checkbox-label">
|
||||
<span className="mr-2 ml-4">
|
||||
{intl.formatMessage(messages.proxyHostname)}
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<Field
|
||||
id="proxyHostname"
|
||||
name="proxyHostname"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
{errors.proxyHostname &&
|
||||
touched.proxyHostname &&
|
||||
typeof errors.proxyHostname === 'string' && (
|
||||
<div className="error">
|
||||
{errors.proxyHostname}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="proxyPort" className="checkbox-label">
|
||||
{intl.formatMessage(messages.proxyPort)}
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<Field
|
||||
id="proxyPort"
|
||||
name="proxyPort"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
{errors.proxyPort &&
|
||||
touched.proxyPort &&
|
||||
typeof errors.proxyPort === 'string' && (
|
||||
<div className="error">{errors.proxyPort}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="proxySsl" className="checkbox-label">
|
||||
{intl.formatMessage(messages.proxySsl)}
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<Field
|
||||
type="checkbox"
|
||||
id="proxySsl"
|
||||
name="proxySsl"
|
||||
onChange={() => {
|
||||
setFieldValue('proxySsl', !values.proxySsl);
|
||||
}}
|
||||
id="proxyHostname"
|
||||
name="proxyHostname"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
{errors.proxyHostname &&
|
||||
touched.proxyHostname &&
|
||||
typeof errors.proxyHostname === 'string' && (
|
||||
<div className="error">{errors.proxyHostname}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="proxyUser" className="checkbox-label">
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="proxyPort" className="checkbox-label">
|
||||
<span className="mr-2 ml-4">
|
||||
{intl.formatMessage(messages.proxyPort)}
|
||||
</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<Field id="proxyPort" name="proxyPort" type="text" />
|
||||
</div>
|
||||
{errors.proxyPort &&
|
||||
touched.proxyPort &&
|
||||
typeof errors.proxyPort === 'string' && (
|
||||
<div className="error">{errors.proxyPort}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="proxySsl" className="checkbox-label">
|
||||
<span className="mr-2 ml-4">
|
||||
{intl.formatMessage(messages.proxySsl)}
|
||||
</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<Field
|
||||
type="checkbox"
|
||||
id="proxySsl"
|
||||
name="proxySsl"
|
||||
onChange={() => {
|
||||
setFieldValue('proxySsl', !values.proxySsl);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="proxyUser" className="checkbox-label">
|
||||
<span className="mr-2 ml-4">
|
||||
{intl.formatMessage(messages.proxyUser)}
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<Field
|
||||
id="proxyUser"
|
||||
name="proxyUser"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
{errors.proxyUser &&
|
||||
touched.proxyUser &&
|
||||
typeof errors.proxyUser === 'string' && (
|
||||
<div className="error">{errors.proxyUser}</div>
|
||||
)}
|
||||
</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<Field id="proxyUser" name="proxyUser" type="text" />
|
||||
</div>
|
||||
{errors.proxyUser &&
|
||||
touched.proxyUser &&
|
||||
typeof errors.proxyUser === 'string' && (
|
||||
<div className="error">{errors.proxyUser}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label
|
||||
htmlFor="proxyPassword"
|
||||
className="checkbox-label"
|
||||
>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="proxyPassword" className="checkbox-label">
|
||||
<span className="mr-2 ml-4">
|
||||
{intl.formatMessage(messages.proxyPassword)}
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<Field
|
||||
id="proxyPassword"
|
||||
name="proxyPassword"
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
{errors.proxyPassword &&
|
||||
touched.proxyPassword &&
|
||||
typeof errors.proxyPassword === 'string' && (
|
||||
<div className="error">
|
||||
{errors.proxyPassword}
|
||||
</div>
|
||||
)}
|
||||
</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<Field
|
||||
id="proxyPassword"
|
||||
name="proxyPassword"
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
{errors.proxyPassword &&
|
||||
touched.proxyPassword &&
|
||||
typeof errors.proxyPassword === 'string' && (
|
||||
<div className="error">{errors.proxyPassword}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label
|
||||
htmlFor="proxyBypassFilter"
|
||||
className="checkbox-label"
|
||||
>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label
|
||||
htmlFor="proxyBypassFilter"
|
||||
className="checkbox-label"
|
||||
>
|
||||
<span className="mr-2 ml-4">
|
||||
{intl.formatMessage(messages.proxyBypassFilter)}
|
||||
<span className="label-tip">
|
||||
{intl.formatMessage(messages.proxyBypassFilterTip)}
|
||||
</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<Field
|
||||
id="proxyBypassFilter"
|
||||
name="proxyBypassFilter"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
{errors.proxyBypassFilter &&
|
||||
touched.proxyBypassFilter &&
|
||||
typeof errors.proxyBypassFilter === 'string' && (
|
||||
<div className="error">
|
||||
{errors.proxyBypassFilter}
|
||||
</div>
|
||||
)}
|
||||
</span>
|
||||
<span className="label-tip ml-4">
|
||||
{intl.formatMessage(messages.proxyBypassFilterTip)}
|
||||
</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<Field
|
||||
id="proxyBypassFilter"
|
||||
name="proxyBypassFilter"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
{errors.proxyBypassFilter &&
|
||||
touched.proxyBypassFilter &&
|
||||
typeof errors.proxyBypassFilter === 'string' && (
|
||||
<div className="error">
|
||||
{errors.proxyBypassFilter}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label
|
||||
htmlFor="proxyBypassLocalAddresses"
|
||||
className="checkbox-label"
|
||||
>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label
|
||||
htmlFor="proxyBypassLocalAddresses"
|
||||
className="checkbox-label"
|
||||
>
|
||||
<span className="mr-2 ml-4">
|
||||
{intl.formatMessage(
|
||||
messages.proxyBypassLocalAddresses
|
||||
)}
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<Field
|
||||
type="checkbox"
|
||||
id="proxyBypassLocalAddresses"
|
||||
name="proxyBypassLocalAddresses"
|
||||
onChange={() => {
|
||||
setFieldValue(
|
||||
'proxyBypassLocalAddresses',
|
||||
!values.proxyBypassLocalAddresses
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<Field
|
||||
type="checkbox"
|
||||
id="proxyBypassLocalAddresses"
|
||||
name="proxyBypassLocalAddresses"
|
||||
onChange={() => {
|
||||
setFieldValue(
|
||||
'proxyBypassLocalAddresses',
|
||||
!values.proxyBypassLocalAddresses
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -350,10 +350,6 @@ const SettingsPlex = ({ onComplete }: SettingsPlexProps) => {
|
||||
);
|
||||
if (!res.ok) throw new Error();
|
||||
}
|
||||
|
||||
if (onComplete) {
|
||||
onComplete();
|
||||
}
|
||||
setIsSyncing(false);
|
||||
revalidate();
|
||||
};
|
||||
@@ -439,6 +435,10 @@ const SettingsPlex = ({ onComplete }: SettingsPlexProps) => {
|
||||
autoDismiss: true,
|
||||
appearance: 'success',
|
||||
});
|
||||
|
||||
if (onComplete) {
|
||||
onComplete();
|
||||
}
|
||||
} catch (e) {
|
||||
if (toastId) {
|
||||
removeToast(toastId);
|
||||
|
||||
@@ -6,14 +6,12 @@ import Button from '@app/components/Common/Button';
|
||||
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
||||
import Modal from '@app/components/Common/Modal';
|
||||
import PageTitle from '@app/components/Common/PageTitle';
|
||||
import OverrideRuleModal from '@app/components/Settings/OverrideRule/OverrideRuleModal';
|
||||
import RadarrModal from '@app/components/Settings/RadarrModal';
|
||||
import SonarrModal from '@app/components/Settings/SonarrModal';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { Transition } from '@headlessui/react';
|
||||
import { PencilIcon, PlusIcon, TrashIcon } from '@heroicons/react/24/solid';
|
||||
import type OverrideRule from '@server/entity/OverrideRule';
|
||||
import type { RadarrSettings, SonarrSettings } from '@server/lib/settings';
|
||||
import { Fragment, useState } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
@@ -59,33 +57,6 @@ interface ServerInstanceProps {
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
export interface DVRTestResponse {
|
||||
profiles: {
|
||||
id: number;
|
||||
name: string;
|
||||
}[];
|
||||
rootFolders: {
|
||||
id: number;
|
||||
path: string;
|
||||
}[];
|
||||
tags: {
|
||||
id: number;
|
||||
label: string;
|
||||
}[];
|
||||
urlBase?: string;
|
||||
}
|
||||
|
||||
export type RadarrTestResponse = DVRTestResponse;
|
||||
|
||||
export type SonarrTestResponse = DVRTestResponse & {
|
||||
languageProfiles:
|
||||
| {
|
||||
id: number;
|
||||
name: string;
|
||||
}[]
|
||||
| null;
|
||||
};
|
||||
|
||||
const ServerInstance = ({
|
||||
name,
|
||||
hostname,
|
||||
@@ -222,15 +193,6 @@ const SettingsServices = () => {
|
||||
type: 'radarr',
|
||||
serverId: null,
|
||||
});
|
||||
const [overrideRuleModal, setOverrideRuleModal] = useState<{
|
||||
open: boolean;
|
||||
rule: OverrideRule | null;
|
||||
testResponse: DVRTestResponse | null;
|
||||
}>({
|
||||
open: false,
|
||||
rule: null,
|
||||
testResponse: null,
|
||||
});
|
||||
|
||||
const deleteServer = async () => {
|
||||
const res = await fetch(
|
||||
@@ -265,51 +227,26 @@ const SettingsServices = () => {
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
{overrideRuleModal.open && overrideRuleModal.testResponse && (
|
||||
<OverrideRuleModal
|
||||
rule={overrideRuleModal.rule}
|
||||
onClose={() =>
|
||||
setOverrideRuleModal({
|
||||
open: false,
|
||||
rule: null,
|
||||
testResponse: null,
|
||||
})
|
||||
}
|
||||
testResponse={overrideRuleModal.testResponse}
|
||||
radarrId={editRadarrModal.radarr?.id}
|
||||
sonarrId={editSonarrModal.sonarr?.id}
|
||||
/>
|
||||
)}
|
||||
{editRadarrModal.open && (
|
||||
<RadarrModal
|
||||
radarr={editRadarrModal.radarr}
|
||||
onClose={() => {
|
||||
if (!overrideRuleModal.open)
|
||||
setEditRadarrModal({ open: false, radarr: null });
|
||||
}}
|
||||
onClose={() => setEditRadarrModal({ open: false, radarr: null })}
|
||||
onSave={() => {
|
||||
revalidateRadarr();
|
||||
mutate('/api/v1/settings/public');
|
||||
setEditRadarrModal({ open: false, radarr: null });
|
||||
}}
|
||||
overrideRuleModal={overrideRuleModal}
|
||||
setOverrideRuleModal={setOverrideRuleModal}
|
||||
/>
|
||||
)}
|
||||
{editSonarrModal.open && (
|
||||
<SonarrModal
|
||||
sonarr={editSonarrModal.sonarr}
|
||||
onClose={() => {
|
||||
if (!overrideRuleModal.open)
|
||||
setEditSonarrModal({ open: false, sonarr: null });
|
||||
}}
|
||||
onClose={() => setEditSonarrModal({ open: false, sonarr: null })}
|
||||
onSave={() => {
|
||||
revalidateSonarr();
|
||||
mutate('/api/v1/settings/public');
|
||||
setEditSonarrModal({ open: false, sonarr: null });
|
||||
}}
|
||||
overrideRuleModal={overrideRuleModal}
|
||||
setOverrideRuleModal={setOverrideRuleModal}
|
||||
/>
|
||||
)}
|
||||
<Transition
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
import Button from '@app/components/Common/Button';
|
||||
import Modal from '@app/components/Common/Modal';
|
||||
import SensitiveInput from '@app/components/Common/SensitiveInput';
|
||||
import OverrideRuleTile from '@app/components/Settings/OverrideRule/OverrideRuleTile';
|
||||
import type {
|
||||
DVRTestResponse,
|
||||
SonarrTestResponse,
|
||||
} from '@app/components/Settings/SettingsServices';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { Transition } from '@headlessui/react';
|
||||
import { PlusIcon } from '@heroicons/react/24/solid';
|
||||
import type OverrideRule from '@server/entity/OverrideRule';
|
||||
import type { OverrideRuleResultsResponse } from '@server/interfaces/api/overrideRuleInterfaces';
|
||||
import type { SonarrSettings } from '@server/lib/settings';
|
||||
import { Field, Formik } from 'formik';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
@@ -19,7 +10,6 @@ import { useIntl } from 'react-intl';
|
||||
import type { OnChangeValue } from 'react-select';
|
||||
import Select from 'react-select';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import useSWR from 'swr';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
type OptionType = {
|
||||
@@ -85,47 +75,48 @@ const messages = defineMessages('components.Settings.SonarrModal', {
|
||||
animeTags: 'Anime Tags',
|
||||
notagoptions: 'No tags.',
|
||||
selecttags: 'Select tags',
|
||||
overrideRules: 'Override Rules',
|
||||
addrule: 'New Override Rule',
|
||||
});
|
||||
|
||||
interface TestResponse {
|
||||
profiles: {
|
||||
id: number;
|
||||
name: string;
|
||||
}[];
|
||||
rootFolders: {
|
||||
id: number;
|
||||
path: string;
|
||||
}[];
|
||||
languageProfiles:
|
||||
| {
|
||||
id: number;
|
||||
name: string;
|
||||
}[]
|
||||
| null;
|
||||
tags: {
|
||||
id: number;
|
||||
label: string;
|
||||
}[];
|
||||
urlBase?: string;
|
||||
}
|
||||
|
||||
interface SonarrModalProps {
|
||||
sonarr: SonarrSettings | null;
|
||||
onClose: () => void;
|
||||
onSave: () => void;
|
||||
overrideRuleModal: { open: boolean; rule: OverrideRule | null };
|
||||
setOverrideRuleModal: ({
|
||||
open,
|
||||
rule,
|
||||
testResponse,
|
||||
}: {
|
||||
open: boolean;
|
||||
rule: OverrideRule | null;
|
||||
testResponse: DVRTestResponse;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
const SonarrModal = ({
|
||||
onClose,
|
||||
sonarr,
|
||||
onSave,
|
||||
overrideRuleModal,
|
||||
setOverrideRuleModal,
|
||||
}: SonarrModalProps) => {
|
||||
const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
|
||||
const intl = useIntl();
|
||||
const { data: rules, mutate: revalidate } =
|
||||
useSWR<OverrideRuleResultsResponse>('/api/v1/overrideRule');
|
||||
const initialLoad = useRef(false);
|
||||
const { addToast } = useToasts();
|
||||
const [isValidated, setIsValidated] = useState(sonarr ? true : false);
|
||||
const [isTesting, setIsTesting] = useState(false);
|
||||
const [testResponse, setTestResponse] = useState<SonarrTestResponse>({
|
||||
const [testResponse, setTestResponse] = useState<TestResponse>({
|
||||
profiles: [],
|
||||
rootFolders: [],
|
||||
languageProfiles: null,
|
||||
tags: [],
|
||||
});
|
||||
|
||||
const SonarrSettingsSchema = Yup.object().shape({
|
||||
name: Yup.string().required(
|
||||
intl.formatMessage(messages.validationNameRequired)
|
||||
@@ -206,7 +197,7 @@ const SonarrModal = ({
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
const data: SonarrTestResponse = await res.json();
|
||||
const data: TestResponse = await res.json();
|
||||
|
||||
setIsValidated(true);
|
||||
setTestResponse(data);
|
||||
@@ -244,10 +235,6 @@ const SonarrModal = ({
|
||||
}
|
||||
}, [sonarr, testConnection]);
|
||||
|
||||
useEffect(() => {
|
||||
revalidate();
|
||||
}, [overrideRuleModal, revalidate]);
|
||||
|
||||
return (
|
||||
<Transition
|
||||
as="div"
|
||||
@@ -415,7 +402,6 @@ const SonarrModal = ({
|
||||
values.is4k ? messages.edit4ksonarr : messages.editsonarr
|
||||
)
|
||||
}
|
||||
backgroundClickable={!overrideRuleModal.open}
|
||||
>
|
||||
<div className="mb-6">
|
||||
<div className="form-row">
|
||||
@@ -1070,38 +1056,6 @@ const SonarrModal = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="mb-4 text-xl font-bold leading-8 text-gray-100">
|
||||
{intl.formatMessage(messages.overrideRules)}
|
||||
</h3>
|
||||
<ul className="grid grid-cols-2 gap-6">
|
||||
{rules && (
|
||||
<OverrideRuleTile
|
||||
rules={rules}
|
||||
setOverrideRuleModal={setOverrideRuleModal}
|
||||
testResponse={testResponse}
|
||||
sonarr={sonarr}
|
||||
revalidate={revalidate}
|
||||
/>
|
||||
)}
|
||||
<li className="min-h-[8rem] rounded-lg border-2 border-dashed border-gray-400 shadow sm:min-h-[11rem]">
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Button
|
||||
buttonType="ghost"
|
||||
onClick={() =>
|
||||
setOverrideRuleModal({
|
||||
open: true,
|
||||
rule: null,
|
||||
testResponse,
|
||||
})
|
||||
}
|
||||
disabled={!isValidated}
|
||||
>
|
||||
<PlusIcon />
|
||||
<span>{intl.formatMessage(messages.addrule)}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</Modal>
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -14,12 +14,10 @@ import useLocale from '@app/hooks/useLocale';
|
||||
import useSettings from '@app/hooks/useSettings';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { MediaServerType } from '@server/constants/server';
|
||||
import type { Library } from '@server/lib/settings';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import useSWR, { mutate } from 'swr';
|
||||
import SetupLogin from './SetupLogin';
|
||||
|
||||
@@ -37,8 +35,6 @@ const messages = defineMessages('components.Setup', {
|
||||
signin: 'Sign In',
|
||||
configuremediaserver: 'Configure Media Server',
|
||||
configureservices: 'Configure Services',
|
||||
librarieserror:
|
||||
'Validation failed. Please toggle the libraries again to continue.',
|
||||
});
|
||||
|
||||
const Setup = () => {
|
||||
@@ -53,7 +49,6 @@ const Setup = () => {
|
||||
const router = useRouter();
|
||||
const { locale } = useLocale();
|
||||
const settings = useSettings();
|
||||
const toasts = useToasts();
|
||||
|
||||
const finishSetup = async () => {
|
||||
setIsUpdating(true);
|
||||
@@ -92,65 +87,19 @@ const Setup = () => {
|
||||
if (settings.currentSettings.initialized) {
|
||||
router.push('/');
|
||||
}
|
||||
|
||||
if (
|
||||
settings.currentSettings.mediaServerType !==
|
||||
MediaServerType.NOT_CONFIGURED
|
||||
) {
|
||||
setCurrentStep(3);
|
||||
setMediaServerType(settings.currentSettings.mediaServerType);
|
||||
if (currentStep < 3) {
|
||||
setCurrentStep(3);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentStep === 3) {
|
||||
validateLibraries();
|
||||
}
|
||||
}, [
|
||||
settings.currentSettings.mediaServerType,
|
||||
settings.currentSettings.initialized,
|
||||
router,
|
||||
toasts,
|
||||
intl,
|
||||
currentStep,
|
||||
mediaServerType,
|
||||
]);
|
||||
|
||||
const validateLibraries = async () => {
|
||||
try {
|
||||
const endpointMap: Record<MediaServerType, string> = {
|
||||
[MediaServerType.JELLYFIN]: '/api/v1/settings/jellyfin',
|
||||
[MediaServerType.EMBY]: '/api/v1/settings/jellyfin',
|
||||
[MediaServerType.PLEX]: '/api/v1/settings/plex',
|
||||
[MediaServerType.NOT_CONFIGURED]: '',
|
||||
};
|
||||
|
||||
const endpoint = endpointMap[mediaServerType];
|
||||
if (!endpoint) return;
|
||||
|
||||
const res = await fetch(endpoint);
|
||||
if (!res.ok) throw new Error('Fetch failed');
|
||||
const data = await res.json();
|
||||
|
||||
const hasEnabledLibraries = data?.libraries?.some(
|
||||
(library: Library) => library.enabled
|
||||
);
|
||||
|
||||
setMediaServerSettingsComplete(hasEnabledLibraries);
|
||||
} catch (e) {
|
||||
toasts.addToast(intl.formatMessage(messages.librarieserror), {
|
||||
autoDismiss: true,
|
||||
appearance: 'error',
|
||||
});
|
||||
|
||||
setMediaServerSettingsComplete(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleComplete = () => {
|
||||
validateLibraries();
|
||||
};
|
||||
|
||||
if (settings.currentSettings.initialized) return <></>;
|
||||
|
||||
return (
|
||||
@@ -276,9 +225,14 @@ const Setup = () => {
|
||||
{currentStep === 3 && (
|
||||
<div className="p-2">
|
||||
{mediaServerType === MediaServerType.PLEX ? (
|
||||
<SettingsPlex onComplete={handleComplete} />
|
||||
<SettingsPlex
|
||||
onComplete={() => setMediaServerSettingsComplete(true)}
|
||||
/>
|
||||
) : (
|
||||
<SettingsJellyfin isSetupSettings onComplete={handleComplete} />
|
||||
<SettingsJellyfin
|
||||
isSetupSettings
|
||||
onComplete={() => setMediaServerSettingsComplete(true)}
|
||||
/>
|
||||
)}
|
||||
<div className="actions">
|
||||
<div className="flex justify-end">
|
||||
|
||||
@@ -222,15 +222,15 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
||||
});
|
||||
}
|
||||
|
||||
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 seriesAttributes: React.ReactNode[] = [];
|
||||
|
||||
const contentRating = data.contentRatings.results.find(
|
||||
(r) => r.iso_3166_1 === discoverRegion
|
||||
(r) => r.iso_3166_1 === region
|
||||
)?.rating;
|
||||
if (contentRating) {
|
||||
seriesAttributes.push(
|
||||
@@ -301,9 +301,7 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
||||
};
|
||||
|
||||
const showHasSpecials = data.seasons.some(
|
||||
(season) =>
|
||||
season.seasonNumber === 0 &&
|
||||
settings.currentSettings.enableSpecialEpisodes
|
||||
(season) => season.seasonNumber === 0
|
||||
);
|
||||
|
||||
const isComplete =
|
||||
@@ -314,15 +312,9 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
||||
(showHasSpecials ? seasonCount + 1 : seasonCount) <=
|
||||
getAllRequestedSeasons(true).length;
|
||||
|
||||
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) {
|
||||
@@ -801,11 +793,6 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
||||
{data.seasons
|
||||
.slice()
|
||||
.reverse()
|
||||
.filter(
|
||||
(season) =>
|
||||
settings.currentSettings.enableSpecialEpisodes ||
|
||||
season.seasonNumber !== 0
|
||||
)
|
||||
.map((season) => {
|
||||
const show4k =
|
||||
settings.currentSettings.series4kEnabled &&
|
||||
@@ -1243,26 +1230,14 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
||||
</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>
|
||||
|
||||
@@ -48,12 +48,8 @@ const messages = defineMessages(
|
||||
'Another user already has this username. You must set an email',
|
||||
region: 'Discover Region',
|
||||
regionTip: 'Filter content by regional availability',
|
||||
discoverRegion: 'Discover Region',
|
||||
discoverRegionTip: 'Filter content by regional availability',
|
||||
originallanguage: 'Discover Language',
|
||||
originallanguageTip: 'Filter content by original language',
|
||||
streamingRegion: 'Streaming Region',
|
||||
streamingRegionTip: 'Show streaming sites by regional availability',
|
||||
movierequestlimit: 'Movie Request Limit',
|
||||
seriesrequestlimit: 'Series Request Limit',
|
||||
enableOverride: 'Override Global Limit',
|
||||
@@ -148,8 +144,7 @@ const UserGeneralSettings = () => {
|
||||
email: data?.email?.includes('@') ? data.email : '',
|
||||
discordId: data?.discordId ?? '',
|
||||
locale: data?.locale,
|
||||
discoverRegion: data?.discoverRegion,
|
||||
streamingRegion: data?.streamingRegion,
|
||||
region: data?.region,
|
||||
originalLanguage: data?.originalLanguage,
|
||||
movieQuotaLimit: data?.movieQuotaLimit,
|
||||
movieQuotaDays: data?.movieQuotaDays,
|
||||
@@ -173,8 +168,7 @@ const UserGeneralSettings = () => {
|
||||
values.email || user?.jellyfinUsername || user?.plexUsername,
|
||||
discordId: values.discordId,
|
||||
locale: values.locale,
|
||||
discoverRegion: values.discoverRegion,
|
||||
streamingRegion: values.streamingRegion,
|
||||
region: values.region,
|
||||
originalLanguage: values.originalLanguage,
|
||||
movieQuotaLimit: movieQuotaEnabled
|
||||
? values.movieQuotaLimit
|
||||
@@ -406,17 +400,17 @@ const UserGeneralSettings = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="discoverRegion" className="text-label">
|
||||
<span>{intl.formatMessage(messages.discoverRegion)}</span>
|
||||
<label htmlFor="displayName" className="text-label">
|
||||
<span>{intl.formatMessage(messages.region)}</span>
|
||||
<span className="label-tip">
|
||||
{intl.formatMessage(messages.discoverRegionTip)}
|
||||
{intl.formatMessage(messages.regionTip)}
|
||||
</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<RegionSelector
|
||||
name="discoverRegion"
|
||||
value={values.discoverRegion ?? ''}
|
||||
name="region"
|
||||
value={values.region ?? ''}
|
||||
isUserSetting
|
||||
onChange={setFieldValue}
|
||||
/>
|
||||
@@ -441,26 +435,6 @@ const UserGeneralSettings = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="streamingRegionTip" className="text-label">
|
||||
<span>{intl.formatMessage(messages.streamingRegion)}</span>
|
||||
<span className="label-tip">
|
||||
{intl.formatMessage(messages.streamingRegionTip)}
|
||||
</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<RegionSelector
|
||||
name="streamingRegion"
|
||||
value={values.streamingRegion || ''}
|
||||
isUserSetting
|
||||
onChange={setFieldValue}
|
||||
regionType="streaming"
|
||||
disableAll
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{currentHasPermission(Permission.MANAGE_USERS) &&
|
||||
!hasPermission(Permission.MANAGE_USERS) && (
|
||||
<>
|
||||
|
||||
@@ -21,14 +21,9 @@ const messages = defineMessages(
|
||||
telegramChatId: 'Chat ID',
|
||||
telegramChatIdTipLong:
|
||||
'<TelegramBotLink>Start a chat</TelegramBotLink>, add <GetIdBotLink>@get_id_bot</GetIdBotLink>, and issue the <code>/my_id</code> command',
|
||||
telegramMessageThreadId: 'Thread/Topic ID',
|
||||
telegramMessageThreadIdTip:
|
||||
"If your group-chat has topics enabled, you can specify a thread/topic's ID here",
|
||||
sendSilently: 'Send Silently',
|
||||
sendSilentlyDescription: 'Send notifications with no sound',
|
||||
validationTelegramChatId: 'You must provide a valid chat ID',
|
||||
validationTelegramMessageThreadId:
|
||||
'The thread/topic ID must be a positive whole number',
|
||||
}
|
||||
);
|
||||
|
||||
@@ -58,20 +53,6 @@ const UserTelegramSettings = () => {
|
||||
/^-?\d+$/,
|
||||
intl.formatMessage(messages.validationTelegramChatId)
|
||||
),
|
||||
telegramMessageThreadId: Yup.string()
|
||||
.when(['types'], {
|
||||
is: (enabled: boolean, types: number) => enabled && !!types,
|
||||
then: Yup.string()
|
||||
.nullable()
|
||||
.required(
|
||||
intl.formatMessage(messages.validationTelegramMessageThreadId)
|
||||
),
|
||||
otherwise: Yup.string().nullable(),
|
||||
})
|
||||
.matches(
|
||||
/^\d+$/,
|
||||
intl.formatMessage(messages.validationTelegramMessageThreadId)
|
||||
),
|
||||
});
|
||||
|
||||
if (!data && !error) {
|
||||
@@ -82,7 +63,6 @@ const UserTelegramSettings = () => {
|
||||
<Formik
|
||||
initialValues={{
|
||||
telegramChatId: data?.telegramChatId,
|
||||
telegramMessageThreadId: data?.telegramMessageThreadId,
|
||||
telegramSendSilently: data?.telegramSendSilently,
|
||||
types: data?.notificationTypes.telegram ?? 0,
|
||||
}}
|
||||
@@ -104,7 +84,6 @@ const UserTelegramSettings = () => {
|
||||
pushoverApplicationToken: data?.pushoverApplicationToken,
|
||||
pushoverUserKey: data?.pushoverUserKey,
|
||||
telegramChatId: values.telegramChatId,
|
||||
telegramMessageThreadId: values.telegramMessageThreadId,
|
||||
telegramSendSilently: values.telegramSendSilently,
|
||||
notificationTypes: {
|
||||
telegram: values.types,
|
||||
@@ -183,30 +162,6 @@ const UserTelegramSettings = () => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="telegramMessageThreadId" className="text-label">
|
||||
{intl.formatMessage(messages.telegramMessageThreadId)}
|
||||
<span className="label-tip">
|
||||
{intl.formatMessage(messages.telegramMessageThreadIdTip)}
|
||||
</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<Field
|
||||
id="telegramMessageThreadId"
|
||||
name="telegramMessageThreadId"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
{errors.telegramMessageThreadId &&
|
||||
touched.telegramMessageThreadId &&
|
||||
typeof errors.telegramMessageThreadId === 'string' && (
|
||||
<div className="error">
|
||||
{errors.telegramMessageThreadId}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="telegramSendSilently" className="checkbox-label">
|
||||
{intl.formatMessage(messages.sendSilently)}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user