Compare commits
19 Commits
v2.4.0
...
logs-plex-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd42194f61 | ||
|
|
77a36f9714 | ||
|
|
f773e0fb2a | ||
|
|
767a24164d | ||
|
|
8394eb5ad4 | ||
|
|
b8425d6388 | ||
|
|
ebb7f00305 | ||
|
|
418d51590d | ||
|
|
a6dd4a8fed | ||
|
|
4d1163c343 | ||
|
|
b085e12ff9 | ||
|
|
33e7a153aa | ||
|
|
9891a7577c | ||
|
|
077e355c77 | ||
|
|
21ab20bba9 | ||
|
|
cdfb30ea16 | ||
|
|
771ecdf781 | ||
|
|
863b675c77 | ||
|
|
5b998bef82 |
1
.github/workflows/cypress.yml
vendored
1
.github/workflows/cypress.yml
vendored
@@ -37,6 +37,7 @@ jobs:
|
|||||||
COMMIT_INFO_MESSAGE: ${{github.event.pull_request.title}}
|
COMMIT_INFO_MESSAGE: ${{github.event.pull_request.title}}
|
||||||
COMMIT_INFO_SHA: ${{github.event.pull_request.head.sha}}
|
COMMIT_INFO_SHA: ${{github.event.pull_request.head.sha}}
|
||||||
- name: Upload video files
|
- name: Upload video files
|
||||||
|
if: always()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: cypress-videos
|
name: cypress-videos
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ kubeVersion: ">=1.23.0-0"
|
|||||||
name: jellyseerr-chart
|
name: jellyseerr-chart
|
||||||
description: Jellyseerr helm chart for Kubernetes
|
description: Jellyseerr helm chart for Kubernetes
|
||||||
type: application
|
type: application
|
||||||
version: 2.1.1
|
version: 2.3.0
|
||||||
appVersion: "2.3.0"
|
appVersion: "2.5.0"
|
||||||
maintainers:
|
maintainers:
|
||||||
- name: Jellyseerr
|
- name: Jellyseerr
|
||||||
url: https://github.com/Fallenbagel/jellyseerr
|
url: https://github.com/Fallenbagel/jellyseerr
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# jellyseerr-chart
|
# jellyseerr-chart
|
||||||
|
|
||||||
  
|
  
|
||||||
|
|
||||||
Jellyseerr helm chart for Kubernetes
|
Jellyseerr helm chart for Kubernetes
|
||||||
|
|
||||||
|
|||||||
@@ -255,7 +255,8 @@ To run jellyseerr as a service:
|
|||||||
1. Download the [Non-Sucking Service Manager](https://nssm.cc/download)
|
1. Download the [Non-Sucking Service Manager](https://nssm.cc/download)
|
||||||
2. Install NSSM:
|
2. Install NSSM:
|
||||||
```powershell
|
```powershell
|
||||||
nssm install Jellyseerr "C:\Program Files\nodejs\node.exe" ["C:\jellyseerr\dist\index.js"]
|
nssm install Jellyseerr "C:\Program Files\nodejs\node.exe" "C:\jellyseerr\dist\index.js"
|
||||||
|
nssm set Jellyseerr AppDirectory "C:\jellyseerr"
|
||||||
nssm set Jellyseerr AppEnvironmentExtra NODE_ENV=production
|
nssm set Jellyseerr AppEnvironmentExtra NODE_ENV=production
|
||||||
```
|
```
|
||||||
3. Start the service:
|
3. Start the service:
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ or for Cloudflare's DNS:
|
|||||||
```bash
|
```bash
|
||||||
--dns=1.1.1.1
|
--dns=1.1.1.1
|
||||||
```
|
```
|
||||||
|
or for Quad9 DNS:
|
||||||
|
```bash
|
||||||
|
--dns=9.9.9.9
|
||||||
|
```
|
||||||
|
|
||||||
|
You can try them all and see which one works for your network.
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
||||||
@@ -45,6 +51,16 @@ services:
|
|||||||
dns:
|
dns:
|
||||||
- 1.1.1.1
|
- 1.1.1.1
|
||||||
```
|
```
|
||||||
|
or for Quad9's DNS:
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
services:
|
||||||
|
jellyseerr:
|
||||||
|
dns:
|
||||||
|
- 9.9.9.9
|
||||||
|
```
|
||||||
|
|
||||||
|
You can try them all and see which one works for your network.
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
||||||
@@ -56,7 +72,7 @@ services:
|
|||||||
4. Click on Change adapter settings.
|
4. Click on Change adapter settings.
|
||||||
5. Right-click the network interface connected to the internet and select Properties.
|
5. Right-click the network interface connected to the internet and select Properties.
|
||||||
6. Select Internet Protocol Version 4 (TCP/IPv4) and click 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.
|
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 or `9.9.9.9` for Quad9's DNS.
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
||||||
@@ -73,6 +89,10 @@ services:
|
|||||||
```bash
|
```bash
|
||||||
nameserver 1.1.1.1
|
nameserver 1.1.1.1
|
||||||
```
|
```
|
||||||
|
or for Quad9's DNS:
|
||||||
|
```bash
|
||||||
|
nameserver 9.9.9.9
|
||||||
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
@@ -81,7 +101,7 @@ services:
|
|||||||
|
|
||||||
Sometimes there are configuration issues with IPV6 that prevent the hostname resolution from working correctly.
|
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`:
|
You can try to force the resolution to use IPV4 first by going to `Settings > Networking > Advanced Networking` and enabling `Force IPv4 Resolution First` setting and restarting. You can also add the environment variable, `FORCE_IPV4_FIRST=true`:
|
||||||
|
|
||||||
<Tabs groupId="methods" queryString>
|
<Tabs groupId="methods" queryString>
|
||||||
<TabItem value="docker-cli" label="Docker CLI">
|
<TabItem value="docker-cli" label="Docker CLI">
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@dr.pogodin/csurf": "^1.14.1",
|
||||||
"@formatjs/intl-displaynames": "6.2.6",
|
"@formatjs/intl-displaynames": "6.2.6",
|
||||||
"@formatjs/intl-locale": "3.1.1",
|
"@formatjs/intl-locale": "3.1.1",
|
||||||
"@formatjs/intl-pluralrules": "5.1.10",
|
"@formatjs/intl-pluralrules": "5.1.10",
|
||||||
@@ -51,7 +52,6 @@
|
|||||||
"copy-to-clipboard": "3.3.3",
|
"copy-to-clipboard": "3.3.3",
|
||||||
"country-flag-icons": "1.5.5",
|
"country-flag-icons": "1.5.5",
|
||||||
"cronstrue": "2.23.0",
|
"cronstrue": "2.23.0",
|
||||||
"csrf-csrf": "^3.1.0",
|
|
||||||
"date-fns": "2.29.3",
|
"date-fns": "2.29.3",
|
||||||
"dayjs": "1.11.7",
|
"dayjs": "1.11.7",
|
||||||
"email-templates": "12.0.1",
|
"email-templates": "12.0.1",
|
||||||
|
|||||||
49
pnpm-lock.yaml
generated
49
pnpm-lock.yaml
generated
@@ -8,6 +8,9 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@dr.pogodin/csurf':
|
||||||
|
specifier: ^1.14.1
|
||||||
|
version: 1.14.1
|
||||||
'@formatjs/intl-displaynames':
|
'@formatjs/intl-displaynames':
|
||||||
specifier: 6.2.6
|
specifier: 6.2.6
|
||||||
version: 6.2.6
|
version: 6.2.6
|
||||||
@@ -65,9 +68,6 @@ importers:
|
|||||||
cronstrue:
|
cronstrue:
|
||||||
specifier: 2.23.0
|
specifier: 2.23.0
|
||||||
version: 2.23.0
|
version: 2.23.0
|
||||||
csrf-csrf:
|
|
||||||
specifier: ^3.1.0
|
|
||||||
version: 3.1.0
|
|
||||||
date-fns:
|
date-fns:
|
||||||
specifier: 2.29.3
|
specifier: 2.29.3
|
||||||
version: 2.29.3
|
version: 2.29.3
|
||||||
@@ -1549,6 +1549,10 @@ packages:
|
|||||||
'@dabh/diagnostics@2.0.3':
|
'@dabh/diagnostics@2.0.3':
|
||||||
resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==}
|
resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==}
|
||||||
|
|
||||||
|
'@dr.pogodin/csurf@1.14.1':
|
||||||
|
resolution: {integrity: sha512-ijqJsKSDlepDYbprkEEcqbiYero2y4DeL4X5ivnkbKonliLtH8SfHCEtdUwoRZLPTUy2WeFPHI+gveU+Z8ZxLA==}
|
||||||
|
engines: {node: '>= 18'}
|
||||||
|
|
||||||
'@emnapi/runtime@1.2.0':
|
'@emnapi/runtime@1.2.0':
|
||||||
resolution: {integrity: sha512-bV21/9LQmcQeCPEg3BDFtvwL6cwiTMksYNWQQ4KOxCZikEGalWtenoZ0wCiukJINlGCIi2KXx01g4FoH/LxpzQ==}
|
resolution: {integrity: sha512-bV21/9LQmcQeCPEg3BDFtvwL6cwiTMksYNWQQ4KOxCZikEGalWtenoZ0wCiukJINlGCIi2KXx01g4FoH/LxpzQ==}
|
||||||
|
|
||||||
@@ -4405,6 +4409,10 @@ packages:
|
|||||||
cookie-signature@1.0.6:
|
cookie-signature@1.0.6:
|
||||||
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
|
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
|
||||||
|
|
||||||
|
cookie-signature@1.2.2:
|
||||||
|
resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==}
|
||||||
|
engines: {node: '>=6.6.0'}
|
||||||
|
|
||||||
cookie@0.4.2:
|
cookie@0.4.2:
|
||||||
resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==}
|
resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@@ -4417,6 +4425,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
|
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
cookie@1.0.2:
|
||||||
|
resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
copy-to-clipboard@3.3.3:
|
copy-to-clipboard@3.3.3:
|
||||||
resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==}
|
resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==}
|
||||||
|
|
||||||
@@ -4509,9 +4521,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==}
|
resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
csrf-csrf@3.1.0:
|
|
||||||
resolution: {integrity: sha512-kZacFfFbdYFxNnFdigRHCzVAq019vJyUUtgPLjCtzh6jMXcWmf8bGUx/hsqtSEMXaNcPm8iXpjC+hW5aeOsRMg==}
|
|
||||||
|
|
||||||
css-select@4.3.0:
|
css-select@4.3.0:
|
||||||
resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==}
|
resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==}
|
||||||
|
|
||||||
@@ -8317,6 +8326,9 @@ packages:
|
|||||||
deprecated: Rimraf versions prior to v4 are no longer supported
|
deprecated: Rimraf versions prior to v4 are no longer supported
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
rndm@1.2.0:
|
||||||
|
resolution: {integrity: sha512-fJhQQI5tLrQvYIYFpOnFinzv9dwmR7hRnUz1XqP3OJ1jIweTNOd6aTO4jwQSgcBSFUB+/KHJxuGneime+FdzOw==}
|
||||||
|
|
||||||
run-applescript@3.2.0:
|
run-applescript@3.2.0:
|
||||||
resolution: {integrity: sha512-Ep0RsvAjnRcBX1p5vogbaBdAGu/8j/ewpvGqnQYunnLd9SM0vWcPJewPKNnWFggf0hF0pwIgwV5XK7qQ7UZ8Qg==}
|
resolution: {integrity: sha512-Ep0RsvAjnRcBX1p5vogbaBdAGu/8j/ewpvGqnQYunnLd9SM0vWcPJewPKNnWFggf0hF0pwIgwV5XK7qQ7UZ8Qg==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -9064,6 +9076,10 @@ packages:
|
|||||||
tslib@2.8.1:
|
tslib@2.8.1:
|
||||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||||
|
|
||||||
|
tsscmp@1.0.6:
|
||||||
|
resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==}
|
||||||
|
engines: {node: '>=0.6.x'}
|
||||||
|
|
||||||
tsutils@3.21.0:
|
tsutils@3.21.0:
|
||||||
resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
|
resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
@@ -11290,6 +11306,15 @@ snapshots:
|
|||||||
enabled: 2.0.0
|
enabled: 2.0.0
|
||||||
kuler: 2.0.0
|
kuler: 2.0.0
|
||||||
|
|
||||||
|
'@dr.pogodin/csurf@1.14.1':
|
||||||
|
dependencies:
|
||||||
|
cookie: 1.0.2
|
||||||
|
cookie-signature: 1.2.2
|
||||||
|
http-errors: 2.0.0
|
||||||
|
rndm: 1.2.0
|
||||||
|
tsscmp: 1.0.6
|
||||||
|
uid-safe: 2.1.5
|
||||||
|
|
||||||
'@emnapi/runtime@1.2.0':
|
'@emnapi/runtime@1.2.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.6.3
|
tslib: 2.6.3
|
||||||
@@ -14928,12 +14953,16 @@ snapshots:
|
|||||||
|
|
||||||
cookie-signature@1.0.6: {}
|
cookie-signature@1.0.6: {}
|
||||||
|
|
||||||
|
cookie-signature@1.2.2: {}
|
||||||
|
|
||||||
cookie@0.4.2: {}
|
cookie@0.4.2: {}
|
||||||
|
|
||||||
cookie@0.7.1: {}
|
cookie@0.7.1: {}
|
||||||
|
|
||||||
cookie@0.7.2: {}
|
cookie@0.7.2: {}
|
||||||
|
|
||||||
|
cookie@1.0.2: {}
|
||||||
|
|
||||||
copy-to-clipboard@3.3.3:
|
copy-to-clipboard@3.3.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
toggle-selection: 1.0.6
|
toggle-selection: 1.0.6
|
||||||
@@ -15044,10 +15073,6 @@ snapshots:
|
|||||||
|
|
||||||
crypto-random-string@2.0.0: {}
|
crypto-random-string@2.0.0: {}
|
||||||
|
|
||||||
csrf-csrf@3.1.0:
|
|
||||||
dependencies:
|
|
||||||
http-errors: 2.0.0
|
|
||||||
|
|
||||||
css-select@4.3.0:
|
css-select@4.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
boolbase: 1.0.0
|
boolbase: 1.0.0
|
||||||
@@ -19665,6 +19690,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
glob: 7.2.3
|
glob: 7.2.3
|
||||||
|
|
||||||
|
rndm@1.2.0: {}
|
||||||
|
|
||||||
run-applescript@3.2.0:
|
run-applescript@3.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
execa: 0.10.0
|
execa: 0.10.0
|
||||||
@@ -20534,6 +20561,8 @@ snapshots:
|
|||||||
|
|
||||||
tslib@2.8.1: {}
|
tslib@2.8.1: {}
|
||||||
|
|
||||||
|
tsscmp@1.0.6: {}
|
||||||
|
|
||||||
tsutils@3.21.0(typescript@4.9.5):
|
tsutils@3.21.0(typescript@4.9.5):
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 1.14.1
|
tslib: 1.14.1
|
||||||
|
|||||||
@@ -378,6 +378,7 @@ class PlexTvAPI extends ExternalAPI {
|
|||||||
logger.error('Failed to ping token', {
|
logger.error('Failed to ping token', {
|
||||||
label: 'Plex Refresh Token',
|
label: 'Plex Refresh Token',
|
||||||
errorMessage: e.message,
|
errorMessage: e.message,
|
||||||
|
errorCause: e.cause,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import csurf from '@dr.pogodin/csurf';
|
||||||
import PlexAPI from '@server/api/plexapi';
|
import PlexAPI from '@server/api/plexapi';
|
||||||
import dataSource, { getRepository, isPgsql } from '@server/datasource';
|
import dataSource, { getRepository, isPgsql } from '@server/datasource';
|
||||||
import DiscoverSlider from '@server/entity/DiscoverSlider';
|
import DiscoverSlider from '@server/entity/DiscoverSlider';
|
||||||
@@ -28,7 +29,6 @@ import restartFlag from '@server/utils/restartFlag';
|
|||||||
import { getClientIp } from '@supercharge/request-ip';
|
import { getClientIp } from '@supercharge/request-ip';
|
||||||
import { TypeormStore } from 'connect-typeorm/out';
|
import { TypeormStore } from 'connect-typeorm/out';
|
||||||
import cookieParser from 'cookie-parser';
|
import cookieParser from 'cookie-parser';
|
||||||
import { doubleCsrf } from 'csrf-csrf';
|
|
||||||
import type { NextFunction, Request, Response } from 'express';
|
import type { NextFunction, Request, Response } from 'express';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import * as OpenApiValidator from 'express-openapi-validator';
|
import * as OpenApiValidator from 'express-openapi-validator';
|
||||||
@@ -162,23 +162,18 @@ app
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (settings.network.csrfProtection) {
|
if (settings.network.csrfProtection) {
|
||||||
const { doubleCsrfProtection, generateToken } = doubleCsrf({
|
server.use(
|
||||||
getSecret: () => settings.clientId,
|
csurf({
|
||||||
cookieName: 'XSRF-TOKEN',
|
cookie: {
|
||||||
cookieOptions: {
|
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
sameSite: 'strict',
|
sameSite: true,
|
||||||
secure: !dev,
|
secure: !dev,
|
||||||
},
|
},
|
||||||
size: 64,
|
})
|
||||||
ignoredMethods: ['GET', 'HEAD', 'OPTIONS'],
|
);
|
||||||
});
|
|
||||||
|
|
||||||
server.use(doubleCsrfProtection);
|
|
||||||
|
|
||||||
server.use((req, res, next) => {
|
server.use((req, res, next) => {
|
||||||
res.cookie('XSRF-TOKEN', generateToken(req, res), {
|
res.cookie('XSRF-TOKEN', req.csrfToken(), {
|
||||||
sameSite: 'strict',
|
sameSite: true,
|
||||||
secure: !dev,
|
secure: !dev,
|
||||||
});
|
});
|
||||||
next();
|
next();
|
||||||
|
|||||||
@@ -404,6 +404,34 @@ class AvailabilitySync {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!showExists &&
|
||||||
|
(media.status === MediaStatus.AVAILABLE ||
|
||||||
|
media.status === MediaStatus.PARTIALLY_AVAILABLE ||
|
||||||
|
media.seasons.some(
|
||||||
|
(season) => season.status === MediaStatus.AVAILABLE
|
||||||
|
) ||
|
||||||
|
media.seasons.some(
|
||||||
|
(season) => season.status === MediaStatus.PARTIALLY_AVAILABLE
|
||||||
|
))
|
||||||
|
) {
|
||||||
|
await this.mediaUpdater(media, false, mediaServerType);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!showExists4k &&
|
||||||
|
(media.status4k === MediaStatus.AVAILABLE ||
|
||||||
|
media.status4k === MediaStatus.PARTIALLY_AVAILABLE ||
|
||||||
|
media.seasons.some(
|
||||||
|
(season) => season.status4k === MediaStatus.AVAILABLE
|
||||||
|
) ||
|
||||||
|
media.seasons.some(
|
||||||
|
(season) => season.status4k === MediaStatus.PARTIALLY_AVAILABLE
|
||||||
|
))
|
||||||
|
) {
|
||||||
|
await this.mediaUpdater(media, true, mediaServerType);
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Figure out how to run seasonUpdater for each season
|
// TODO: Figure out how to run seasonUpdater for each season
|
||||||
|
|
||||||
if ([...finalSeasons.values()].includes(false)) {
|
if ([...finalSeasons.values()].includes(false)) {
|
||||||
@@ -423,22 +451,6 @@ class AvailabilitySync {
|
|||||||
mediaServerType
|
mediaServerType
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
!showExists &&
|
|
||||||
(media.status === MediaStatus.AVAILABLE ||
|
|
||||||
media.status === MediaStatus.PARTIALLY_AVAILABLE)
|
|
||||||
) {
|
|
||||||
await this.mediaUpdater(media, false, mediaServerType);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!showExists4k &&
|
|
||||||
(media.status4k === MediaStatus.AVAILABLE ||
|
|
||||||
media.status4k === MediaStatus.PARTIALLY_AVAILABLE)
|
|
||||||
) {
|
|
||||||
await this.mediaUpdater(media, true, mediaServerType);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
@@ -466,6 +478,10 @@ class AvailabilitySync {
|
|||||||
{ status: MediaStatus.PARTIALLY_AVAILABLE },
|
{ status: MediaStatus.PARTIALLY_AVAILABLE },
|
||||||
{ status4k: MediaStatus.AVAILABLE },
|
{ status4k: MediaStatus.AVAILABLE },
|
||||||
{ status4k: MediaStatus.PARTIALLY_AVAILABLE },
|
{ status4k: MediaStatus.PARTIALLY_AVAILABLE },
|
||||||
|
{ seasons: { status: MediaStatus.AVAILABLE } },
|
||||||
|
{ seasons: { status: MediaStatus.PARTIALLY_AVAILABLE } },
|
||||||
|
{ seasons: { status4k: MediaStatus.AVAILABLE } },
|
||||||
|
{ seasons: { status4k: MediaStatus.PARTIALLY_AVAILABLE } },
|
||||||
];
|
];
|
||||||
|
|
||||||
let mediaPage: Media[];
|
let mediaPage: Media[];
|
||||||
|
|||||||
@@ -237,6 +237,19 @@ mediaRoutes.delete(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isMovie) {
|
if (isMovie) {
|
||||||
|
// check if the movie exists
|
||||||
|
try {
|
||||||
|
await (service as RadarrAPI).getMovie({
|
||||||
|
id: parseInt(
|
||||||
|
is4k
|
||||||
|
? (media.externalServiceSlug4k as string)
|
||||||
|
: (media.externalServiceSlug as string)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return res.status(204).send();
|
||||||
|
}
|
||||||
|
// remove the movie
|
||||||
await (service as RadarrAPI).removeMovie(
|
await (service as RadarrAPI).removeMovie(
|
||||||
parseInt(
|
parseInt(
|
||||||
is4k
|
is4k
|
||||||
@@ -251,6 +264,13 @@ mediaRoutes.delete(
|
|||||||
if (!tvdbId) {
|
if (!tvdbId) {
|
||||||
throw new Error('TVDB ID not found');
|
throw new Error('TVDB ID not found');
|
||||||
}
|
}
|
||||||
|
// check if the series exists
|
||||||
|
try {
|
||||||
|
await (service as SonarrAPI).getSeriesByTvdbId(tvdbId);
|
||||||
|
} catch {
|
||||||
|
return res.status(204).send();
|
||||||
|
}
|
||||||
|
// remove the series
|
||||||
await (service as SonarrAPI).removeSerie(tvdbId);
|
await (service as SonarrAPI).removeSerie(tvdbId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,8 @@ router.get('/', async (req, res, next) => {
|
|||||||
query = query.orderBy('user.updatedAt', 'DESC');
|
query = query.orderBy('user.updatedAt', 'DESC');
|
||||||
break;
|
break;
|
||||||
case 'displayname':
|
case 'displayname':
|
||||||
query = query.orderBy(
|
query = query
|
||||||
|
.addSelect(
|
||||||
`CASE WHEN (user.username IS NULL OR user.username = '') THEN (
|
`CASE WHEN (user.username IS NULL OR user.username = '') THEN (
|
||||||
CASE WHEN (user.plexUsername IS NULL OR user.plexUsername = '') THEN (
|
CASE WHEN (user.plexUsername IS NULL OR user.plexUsername = '') THEN (
|
||||||
CASE WHEN (user.jellyfinUsername IS NULL OR user.jellyfinUsername = '') THEN
|
CASE WHEN (user.jellyfinUsername IS NULL OR user.jellyfinUsername = '') THEN
|
||||||
@@ -74,8 +75,9 @@ router.get('/', async (req, res, next) => {
|
|||||||
ELSE
|
ELSE
|
||||||
LOWER(user.username)
|
LOWER(user.username)
|
||||||
END`,
|
END`,
|
||||||
'ASC'
|
'displayname_sort_key'
|
||||||
);
|
)
|
||||||
|
.orderBy('displayname_sort_key', 'ASC');
|
||||||
break;
|
break;
|
||||||
case 'requests':
|
case 'requests':
|
||||||
query = query
|
query = query
|
||||||
|
|||||||
4
server/types/custom.d.ts
vendored
Normal file
4
server/types/custom.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
declare module '@dr.pogodin/csurf' {
|
||||||
|
import csrf = require('csurf');
|
||||||
|
export = csrf;
|
||||||
|
}
|
||||||
@@ -8,8 +8,9 @@ export default async function createCustomProxyAgent(
|
|||||||
) {
|
) {
|
||||||
const defaultAgent = new Agent({ keepAliveTimeout: 5000 });
|
const defaultAgent = new Agent({ keepAliveTimeout: 5000 });
|
||||||
|
|
||||||
const skipUrl = (url: string) => {
|
const skipUrl = (url: string | URL) => {
|
||||||
const hostname = new URL(url).hostname;
|
const hostname =
|
||||||
|
typeof url === 'string' ? new URL(url).hostname : url.hostname;
|
||||||
|
|
||||||
if (proxySettings.bypassLocalAddresses && isLocalAddress(hostname)) {
|
if (proxySettings.bypassLocalAddresses && isLocalAddress(hostname)) {
|
||||||
return true;
|
return true;
|
||||||
@@ -38,8 +39,7 @@ export default async function createCustomProxyAgent(
|
|||||||
dispatch: Dispatcher['dispatch']
|
dispatch: Dispatcher['dispatch']
|
||||||
): Dispatcher['dispatch'] => {
|
): Dispatcher['dispatch'] => {
|
||||||
return (opts, handler) => {
|
return (opts, handler) => {
|
||||||
const url = opts.origin?.toString();
|
return opts.origin && skipUrl(opts.origin)
|
||||||
return url && skipUrl(url)
|
|
||||||
? defaultAgent.dispatch(opts, handler)
|
? defaultAgent.dispatch(opts, handler)
|
||||||
: dispatch(opts, handler);
|
: dispatch(opts, handler);
|
||||||
};
|
};
|
||||||
@@ -60,13 +60,10 @@ export default async function createCustomProxyAgent(
|
|||||||
':' +
|
':' +
|
||||||
proxySettings.port,
|
proxySettings.port,
|
||||||
token,
|
token,
|
||||||
interceptors: {
|
|
||||||
Client: [noProxyInterceptor],
|
|
||||||
},
|
|
||||||
keepAliveTimeout: 5000,
|
keepAliveTimeout: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
setGlobalDispatcher(proxyAgent);
|
setGlobalDispatcher(proxyAgent.compose(noProxyInterceptor));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Failed to connect to the proxy: ' + e.message, {
|
logger.error('Failed to connect to the proxy: ' + e.message, {
|
||||||
label: 'Proxy',
|
label: 'Proxy',
|
||||||
@@ -95,7 +92,11 @@ export default async function createCustomProxyAgent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isLocalAddress(hostname: string) {
|
function isLocalAddress(hostname: string) {
|
||||||
if (hostname === 'localhost' || hostname === '127.0.0.1') {
|
if (
|
||||||
|
hostname === 'localhost' ||
|
||||||
|
hostname === '127.0.0.1' ||
|
||||||
|
hostname === '::1'
|
||||||
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,17 +14,13 @@ type AirDateBadgeProps = {
|
|||||||
const AirDateBadge = ({ airDate }: AirDateBadgeProps) => {
|
const AirDateBadge = ({ airDate }: AirDateBadgeProps) => {
|
||||||
const WEEK = 1000 * 60 * 60 * 24 * 8;
|
const WEEK = 1000 * 60 * 60 * 24 * 8;
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
||||||
const dAirDate = new Date(airDate);
|
const dAirDate = new Date(airDate);
|
||||||
const nowDate = new Date();
|
const nowDate = new Date();
|
||||||
const alreadyAired = dAirDate.getTime() < nowDate.getTime();
|
const alreadyAired = dAirDate.getTime() < nowDate.getTime();
|
||||||
|
|
||||||
const compareWeek = new Date(
|
const compareWeek = new Date(
|
||||||
alreadyAired ? Date.now() - WEEK : Date.now() + WEEK
|
alreadyAired ? Date.now() - WEEK : Date.now() + WEEK
|
||||||
);
|
);
|
||||||
|
|
||||||
let showRelative = false;
|
let showRelative = false;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(alreadyAired && dAirDate.getTime() > compareWeek.getTime()) ||
|
(alreadyAired && dAirDate.getTime() > compareWeek.getTime()) ||
|
||||||
(!alreadyAired && dAirDate.getTime() < compareWeek.getTime())
|
(!alreadyAired && dAirDate.getTime() < compareWeek.getTime())
|
||||||
@@ -32,6 +28,10 @@ const AirDateBadge = ({ airDate }: AirDateBadgeProps) => {
|
|||||||
showRelative = true;
|
showRelative = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const diffInDays = Math.round(
|
||||||
|
(dAirDate.getTime() - nowDate.getTime()) / (1000 * 60 * 60 * 24)
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Badge badgeType="light">
|
<Badge badgeType="light">
|
||||||
@@ -39,7 +39,7 @@ const AirDateBadge = ({ airDate }: AirDateBadgeProps) => {
|
|||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: 'long',
|
month: 'long',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
timeZone,
|
timeZone: 'UTC',
|
||||||
})}
|
})}
|
||||||
</Badge>
|
</Badge>
|
||||||
{showRelative && (
|
{showRelative && (
|
||||||
@@ -49,9 +49,9 @@ const AirDateBadge = ({ airDate }: AirDateBadgeProps) => {
|
|||||||
{
|
{
|
||||||
relativeTime: (
|
relativeTime: (
|
||||||
<FormattedRelativeTime
|
<FormattedRelativeTime
|
||||||
value={(dAirDate.getTime() - Date.now()) / 1000}
|
value={diffInDays}
|
||||||
|
unit="day"
|
||||||
numeric="auto"
|
numeric="auto"
|
||||||
updateIntervalInSeconds={1}
|
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,7 +161,6 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
|
|||||||
data-form-type="password"
|
data-form-type="password"
|
||||||
data-1pignore="false"
|
data-1pignore="false"
|
||||||
data-lpignore="false"
|
data-lpignore="false"
|
||||||
data-bwignore="false"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
|
|||||||
@@ -118,7 +118,6 @@ const LocalLogin = ({ revalidate }: LocalLoginProps) => {
|
|||||||
className="!bg-gray-700/80 placeholder:text-gray-400"
|
className="!bg-gray-700/80 placeholder:text-gray-400"
|
||||||
data-1pignore="false"
|
data-1pignore="false"
|
||||||
data-lpignore="false"
|
data-lpignore="false"
|
||||||
data-bwignore="false"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
|
|||||||
@@ -590,7 +590,7 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
|
|||||||
buttonSize={'md'}
|
buttonSize={'md'}
|
||||||
onClick={() => setShowBlacklistModal(true)}
|
onClick={() => setShowBlacklistModal(true)}
|
||||||
>
|
>
|
||||||
<EyeSlashIcon className={'h-3'} />
|
<EyeSlashIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
@@ -608,9 +608,9 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
|
|||||||
onClick={onClickWatchlistBtn}
|
onClick={onClickWatchlistBtn}
|
||||||
>
|
>
|
||||||
{isUpdating ? (
|
{isUpdating ? (
|
||||||
<Spinner className="h-3" />
|
<Spinner />
|
||||||
) : (
|
) : (
|
||||||
<StarIcon className={'h-3 text-amber-300'} />
|
<StarIcon className={'text-amber-300'} />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -623,17 +623,15 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
|
|||||||
buttonSize={'md'}
|
buttonSize={'md'}
|
||||||
onClick={onClickDeleteWatchlistBtn}
|
onClick={onClickDeleteWatchlistBtn}
|
||||||
>
|
>
|
||||||
{isUpdating ? (
|
{isUpdating ? <Spinner /> : <MinusCircleIcon />}
|
||||||
<Spinner className="h-3" />
|
|
||||||
) : (
|
|
||||||
<MinusCircleIcon className={'h-3'} />
|
|
||||||
)}
|
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<div className="z-20">
|
||||||
<PlayButton links={mediaLinks} />
|
<PlayButton links={mediaLinks} />
|
||||||
|
</div>
|
||||||
<RequestButton
|
<RequestButton
|
||||||
mediaType="movie"
|
mediaType="movie"
|
||||||
media={data.mediaInfo}
|
media={data.mediaInfo}
|
||||||
|
|||||||
@@ -17,9 +17,10 @@ import {
|
|||||||
TrashIcon,
|
TrashIcon,
|
||||||
XMarkIcon,
|
XMarkIcon,
|
||||||
} from '@heroicons/react/24/solid';
|
} from '@heroicons/react/24/solid';
|
||||||
import { MediaRequestStatus } from '@server/constants/media';
|
import { MediaRequestStatus, MediaType } from '@server/constants/media';
|
||||||
import type { MediaRequest } from '@server/entity/MediaRequest';
|
import type { MediaRequest } from '@server/entity/MediaRequest';
|
||||||
import type { NonFunctionProperties } from '@server/interfaces/api/common';
|
import type { NonFunctionProperties } from '@server/interfaces/api/common';
|
||||||
|
import type { RadarrSettings, SonarrSettings } from '@server/lib/settings';
|
||||||
import type { MovieDetails } from '@server/models/Movie';
|
import type { MovieDetails } from '@server/models/Movie';
|
||||||
import type { TvDetails } from '@server/models/Tv';
|
import type { TvDetails } from '@server/models/Tv';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
@@ -293,9 +294,16 @@ const RequestItemError = ({
|
|||||||
interface RequestItemProps {
|
interface RequestItemProps {
|
||||||
request: NonFunctionProperties<MediaRequest> & { profileName?: string };
|
request: NonFunctionProperties<MediaRequest> & { profileName?: string };
|
||||||
revalidateList: () => void;
|
revalidateList: () => void;
|
||||||
|
radarrData?: RadarrSettings[];
|
||||||
|
sonarrData?: SonarrSettings[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
const RequestItem = ({
|
||||||
|
request,
|
||||||
|
revalidateList,
|
||||||
|
radarrData,
|
||||||
|
sonarrData,
|
||||||
|
}: RequestItemProps) => {
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
const { ref, inView } = useInView({
|
const { ref, inView } = useInView({
|
||||||
triggerOnce: true,
|
triggerOnce: true,
|
||||||
@@ -390,6 +398,23 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
|||||||
iOSPlexUrl4k: requestData?.media?.iOSPlexUrl4k,
|
iOSPlexUrl4k: requestData?.media?.iOSPlexUrl4k,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const serviceExists = () => {
|
||||||
|
if (title?.mediaInfo) {
|
||||||
|
if (title?.mediaInfo.mediaType === MediaType.MOVIE) {
|
||||||
|
return (
|
||||||
|
radarrData?.find((radarr) => radarr.id === request.serverId) !==
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
sonarrData?.find((sonarr) => sonarr.id === request.serverId) !==
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
if (!title && !error) {
|
if (!title && !error) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -697,7 +722,6 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
|||||||
)}
|
)}
|
||||||
{requestData.status !== MediaRequestStatus.PENDING &&
|
{requestData.status !== MediaRequestStatus.PENDING &&
|
||||||
hasPermission(Permission.MANAGE_REQUESTS) && (
|
hasPermission(Permission.MANAGE_REQUESTS) && (
|
||||||
<>
|
|
||||||
<ConfirmButton
|
<ConfirmButton
|
||||||
onClick={() => deleteRequest()}
|
onClick={() => deleteRequest()}
|
||||||
confirmText={intl.formatMessage(globalMessages.areyousure)}
|
confirmText={intl.formatMessage(globalMessages.areyousure)}
|
||||||
@@ -706,6 +730,10 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
|||||||
<TrashIcon />
|
<TrashIcon />
|
||||||
<span>{intl.formatMessage(messages.deleterequest)}</span>
|
<span>{intl.formatMessage(messages.deleterequest)}</span>
|
||||||
</ConfirmButton>
|
</ConfirmButton>
|
||||||
|
)}
|
||||||
|
{hasPermission(Permission.MANAGE_REQUESTS) &&
|
||||||
|
title?.mediaInfo?.serviceId &&
|
||||||
|
serviceExists() && (
|
||||||
<ConfirmButton
|
<ConfirmButton
|
||||||
onClick={() => deleteMediaFile()}
|
onClick={() => deleteMediaFile()}
|
||||||
confirmText={intl.formatMessage(globalMessages.areyousure)}
|
confirmText={intl.formatMessage(globalMessages.areyousure)}
|
||||||
@@ -718,7 +746,6 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
|||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
</ConfirmButton>
|
</ConfirmButton>
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
{requestData.status === MediaRequestStatus.PENDING &&
|
{requestData.status === MediaRequestStatus.PENDING &&
|
||||||
hasPermission(Permission.MANAGE_REQUESTS) && (
|
hasPermission(Permission.MANAGE_REQUESTS) && (
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import {
|
|||||||
FunnelIcon,
|
FunnelIcon,
|
||||||
} from '@heroicons/react/24/solid';
|
} from '@heroicons/react/24/solid';
|
||||||
import type { RequestResultsResponse } from '@server/interfaces/api/requestInterfaces';
|
import type { RequestResultsResponse } from '@server/interfaces/api/requestInterfaces';
|
||||||
|
import { Permission } from '@server/lib/permissions';
|
||||||
|
import type { RadarrSettings, SonarrSettings } from '@server/lib/settings';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
@@ -51,7 +53,7 @@ const RequestList = () => {
|
|||||||
const { user } = useUser({
|
const { user } = useUser({
|
||||||
id: Number(router.query.userId),
|
id: Number(router.query.userId),
|
||||||
});
|
});
|
||||||
const { user: currentUser } = useUser();
|
const { user: currentUser, hasPermission } = useUser();
|
||||||
const [currentFilter, setCurrentFilter] = useState<Filter>(Filter.PENDING);
|
const [currentFilter, setCurrentFilter] = useState<Filter>(Filter.PENDING);
|
||||||
const [currentSort, setCurrentSort] = useState<Sort>('added');
|
const [currentSort, setCurrentSort] = useState<Sort>('added');
|
||||||
const [currentSortDirection, setCurrentSortDirection] =
|
const [currentSortDirection, setCurrentSortDirection] =
|
||||||
@@ -62,6 +64,13 @@ const RequestList = () => {
|
|||||||
const pageIndex = page - 1;
|
const pageIndex = page - 1;
|
||||||
const updateQueryParams = useUpdateQueryParams({ page: page.toString() });
|
const updateQueryParams = useUpdateQueryParams({ page: page.toString() });
|
||||||
|
|
||||||
|
const { data: radarrData } = useSWR<RadarrSettings[]>(
|
||||||
|
hasPermission(Permission.ADMIN) ? '/api/v1/settings/radarr' : null
|
||||||
|
);
|
||||||
|
const { data: sonarrData } = useSWR<SonarrSettings[]>(
|
||||||
|
hasPermission(Permission.ADMIN) ? '/api/v1/settings/sonarr' : null
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
error,
|
error,
|
||||||
@@ -245,6 +254,8 @@ const RequestList = () => {
|
|||||||
<RequestItem
|
<RequestItem
|
||||||
request={request}
|
request={request}
|
||||||
revalidateList={() => revalidate()}
|
revalidateList={() => revalidate()}
|
||||||
|
radarrData={radarrData}
|
||||||
|
sonarrData={sonarrData}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -221,6 +221,7 @@ const NotificationsEmail = () => {
|
|||||||
requireTls: values.encryption === 'opportunistic',
|
requireTls: values.encryption === 'opportunistic',
|
||||||
authUser: values.authUser,
|
authUser: values.authUser,
|
||||||
authPass: values.authPass,
|
authPass: values.authPass,
|
||||||
|
allowSelfSigned: values.allowSelfSigned,
|
||||||
senderName: values.senderName,
|
senderName: values.senderName,
|
||||||
pgpPrivateKey: values.pgpPrivateKey,
|
pgpPrivateKey: values.pgpPrivateKey,
|
||||||
pgpPassword: values.pgpPassword,
|
pgpPassword: values.pgpPassword,
|
||||||
|
|||||||
@@ -373,11 +373,10 @@ const TitleCard = ({
|
|||||||
: intl.formatMessage(globalMessages.tvshow)}
|
: intl.formatMessage(globalMessages.tvshow)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{showDetail &&
|
{showDetail && currentStatus !== MediaStatus.BLACKLISTED && (
|
||||||
currentStatus !== MediaStatus.BLACKLISTED &&
|
|
||||||
user?.userType !== UserType.PLEX && (
|
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
{toggleWatchlist ? (
|
{user?.userType !== UserType.PLEX &&
|
||||||
|
(toggleWatchlist ? (
|
||||||
<Button
|
<Button
|
||||||
buttonType={'ghost'}
|
buttonType={'ghost'}
|
||||||
className="z-40"
|
className="z-40"
|
||||||
@@ -394,7 +393,7 @@ const TitleCard = ({
|
|||||||
>
|
>
|
||||||
<MinusCircleIcon className={'h-3'} />
|
<MinusCircleIcon className={'h-3'} />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
))}
|
||||||
{showHideButton &&
|
{showHideButton &&
|
||||||
currentStatus !== MediaStatus.PROCESSING &&
|
currentStatus !== MediaStatus.PROCESSING &&
|
||||||
currentStatus !== MediaStatus.AVAILABLE &&
|
currentStatus !== MediaStatus.AVAILABLE &&
|
||||||
|
|||||||
@@ -632,7 +632,7 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
|||||||
buttonSize={'md'}
|
buttonSize={'md'}
|
||||||
onClick={() => setShowBlacklistModal(true)}
|
onClick={() => setShowBlacklistModal(true)}
|
||||||
>
|
>
|
||||||
<EyeSlashIcon className={'h-3'} />
|
<EyeSlashIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
@@ -650,9 +650,9 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
|||||||
onClick={onClickWatchlistBtn}
|
onClick={onClickWatchlistBtn}
|
||||||
>
|
>
|
||||||
{isUpdating ? (
|
{isUpdating ? (
|
||||||
<Spinner className="h-3" />
|
<Spinner />
|
||||||
) : (
|
) : (
|
||||||
<StarIcon className={'h-3 text-amber-300'} />
|
<StarIcon className={'text-amber-300'} />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -665,17 +665,15 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
|||||||
buttonSize={'md'}
|
buttonSize={'md'}
|
||||||
onClick={onClickDeleteWatchlistBtn}
|
onClick={onClickDeleteWatchlistBtn}
|
||||||
>
|
>
|
||||||
{isUpdating ? (
|
{isUpdating ? <Spinner /> : <MinusCircleIcon />}
|
||||||
<Spinner className="h-3" />
|
|
||||||
) : (
|
|
||||||
<MinusCircleIcon className={'h-3'} />
|
|
||||||
)}
|
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<div className="z-20">
|
||||||
<PlayButton links={mediaLinks} />
|
<PlayButton links={mediaLinks} />
|
||||||
|
</div>
|
||||||
<RequestButton
|
<RequestButton
|
||||||
mediaType="tv"
|
mediaType="tv"
|
||||||
onUpdate={() => revalidate()}
|
onUpdate={() => revalidate()}
|
||||||
|
|||||||
@@ -415,7 +415,7 @@ const UserGeneralSettings = () => {
|
|||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<div className="form-input-area">
|
<div className="form-input-area">
|
||||||
<div className="form-input-field">
|
<div className="form-input-field relative z-30">
|
||||||
<RegionSelector
|
<RegionSelector
|
||||||
name="discoverRegion"
|
name="discoverRegion"
|
||||||
value={values.discoverRegion ?? ''}
|
value={values.discoverRegion ?? ''}
|
||||||
@@ -451,7 +451,7 @@ const UserGeneralSettings = () => {
|
|||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<div className="form-input-area">
|
<div className="form-input-area">
|
||||||
<div className="form-input-field">
|
<div className="form-input-field relative z-20">
|
||||||
<RegionSelector
|
<RegionSelector
|
||||||
name="streamingRegion"
|
name="streamingRegion"
|
||||||
value={values.streamingRegion || ''}
|
value={values.streamingRegion || ''}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ if (typeof window !== 'undefined') {
|
|||||||
|
|
||||||
const headers = {
|
const headers = {
|
||||||
...(init?.headers || {}),
|
...(init?.headers || {}),
|
||||||
...(csrfToken ? { 'X-CSRF-TOKEN': csrfToken } : {}),
|
...(csrfToken ? { 'XSRF-TOKEN': csrfToken } : {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const newInit: RequestInit = {
|
const newInit: RequestInit = {
|
||||||
|
|||||||
Reference in New Issue
Block a user