mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2025-12-24 10:49:30 -05:00
Compare commits
1 Commits
test-disab
...
rename-err
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d2bf3a065 |
@@ -18,7 +18,7 @@ config/logs/*
|
||||
config/*.json
|
||||
dist
|
||||
Dockerfile*
|
||||
compose.yaml
|
||||
docker-compose.yml
|
||||
docs
|
||||
LICENSE
|
||||
node_modules
|
||||
|
||||
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -40,7 +40,7 @@ docs export-ignore
|
||||
.all-contributorsrc export-ignore
|
||||
.editorconfig export-ignore
|
||||
Dockerfile.local export-ignore
|
||||
compose.yaml export-ignore
|
||||
docker-compose.yml export-ignore
|
||||
stylelint.config.js export-ignore
|
||||
|
||||
public/os_logo_filled.png export-ignore
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -34,7 +34,6 @@ yarn-error.log*
|
||||
# database
|
||||
config/db/*.sqlite3*
|
||||
config/settings.json
|
||||
config/settings.old.json
|
||||
|
||||
# logs
|
||||
config/logs/*.log*
|
||||
|
||||
@@ -52,7 +52,7 @@ All help is welcome and greatly appreciated! If you would like to contribute to
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
- Alternatively, you can use [Docker](https://www.docker.com/) with `docker compose up -d`. This method does not require installing NodeJS or Yarn on your machine directly.
|
||||
- Alternatively, you can use [Docker](https://www.docker.com/) with `docker-compose up -d`. This method does not require installing NodeJS or Yarn on your machine directly.
|
||||
|
||||
5. Create your patch and test your changes.
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
version: '3'
|
||||
services:
|
||||
jellyseerr:
|
||||
build:
|
||||
@@ -190,7 +190,7 @@ Caddy will automatically obtain and renew SSL certificates for your domain.
|
||||
|
||||
## Traefik (v2)
|
||||
|
||||
Add the following labels to the Jellyseerr service in your `compose.yaml` file:
|
||||
Add the following labels to the Jellyseerr service in your `docker-compose.yml` file:
|
||||
|
||||
```yaml
|
||||
labels:
|
||||
|
||||
@@ -6,10 +6,6 @@ sidebar_position: 4
|
||||
|
||||
# AUR (Arch User Repository)
|
||||
|
||||
:::note Disclaimer
|
||||
This AUR package is not maintained by us but by a third party. Please refer to the maintainer for any issues.
|
||||
:::
|
||||
|
||||
:::info
|
||||
This method is not recommended for most users. It is intended for advanced users who are using Arch Linux or an Arch-based distribution.
|
||||
:::
|
||||
|
||||
@@ -71,7 +71,7 @@ You could also use [diun](https://github.com/crazy-max/diun) to receive notifica
|
||||
For details on how to use Docker Compose, please [review the official Compose documentation](https://docs.docker.com/compose/reference/).
|
||||
|
||||
#### Installation:
|
||||
Define the `jellyseerr` service in your `compose.yaml` as follows:
|
||||
Define the `jellyseerr` service in your `docker-compose.yml` as follows:
|
||||
```yaml
|
||||
---
|
||||
services:
|
||||
@@ -94,17 +94,17 @@ If you are using emby, make sure to set the `JELLYFIN_TYPE` environment variable
|
||||
|
||||
Then, start all services defined in the Compose file:
|
||||
```bash
|
||||
docker compose up -d
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
#### Updating:
|
||||
Pull the latest image:
|
||||
```bash
|
||||
docker compose pull jellyseerr
|
||||
docker-compose pull jellyseerr
|
||||
```
|
||||
Then, restart all services defined in the Compose file:
|
||||
```bash
|
||||
docker compose up -d
|
||||
docker-compose up -d
|
||||
```
|
||||
:::tip
|
||||
You may alternatively use a third-party mechanism like [dockge](https://github.com/louislam/dockge) to manage your docker compose files.
|
||||
|
||||
2
next-env.d.ts
vendored
2
next-env.d.ts
vendored
@@ -2,4 +2,4 @@
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
|
||||
@@ -1988,9 +1988,6 @@ paths:
|
||||
appDataPath:
|
||||
type: string
|
||||
example: /app/config
|
||||
appDataPermissions:
|
||||
type: boolean
|
||||
example: true
|
||||
/settings/main:
|
||||
get:
|
||||
summary: Get main settings
|
||||
|
||||
@@ -93,8 +93,7 @@
|
||||
"sqlite3": "5.1.4",
|
||||
"swagger-ui-express": "4.6.2",
|
||||
"swr": "2.2.5",
|
||||
"typeorm": "0.3.11",
|
||||
"undici": "^6.20.1",
|
||||
"typeorm": "0.3.12",
|
||||
"web-push": "3.5.0",
|
||||
"winston": "3.8.2",
|
||||
"winston-daily-rotate-file": "4.7.1",
|
||||
|
||||
65
pnpm-lock.yaml
generated
65
pnpm-lock.yaml
generated
@@ -49,7 +49,7 @@ importers:
|
||||
version: 2.11.0
|
||||
connect-typeorm:
|
||||
specifier: 1.1.4
|
||||
version: 1.1.4(typeorm@0.3.11(sqlite3@5.1.4(encoding@0.1.13))(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5)))
|
||||
version: 1.1.4(typeorm@0.3.12(sqlite3@5.1.4(encoding@0.1.13))(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5)))
|
||||
cookie-parser:
|
||||
specifier: 1.4.6
|
||||
version: 1.4.6
|
||||
@@ -192,11 +192,8 @@ importers:
|
||||
specifier: 2.2.5
|
||||
version: 2.2.5(react@18.3.1)
|
||||
typeorm:
|
||||
specifier: 0.3.11
|
||||
version: 0.3.11(sqlite3@5.1.4(encoding@0.1.13))(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5))
|
||||
undici:
|
||||
specifier: ^6.20.1
|
||||
version: 6.20.1
|
||||
specifier: 0.3.12
|
||||
version: 0.3.12(sqlite3@5.1.4(encoding@0.1.13))(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5))
|
||||
web-push:
|
||||
specifier: 3.5.0
|
||||
version: 3.5.0
|
||||
@@ -4267,6 +4264,10 @@ packages:
|
||||
resolution: {integrity: sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==}
|
||||
engines: {node: '>=0.11'}
|
||||
|
||||
date-fns@2.30.0:
|
||||
resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==}
|
||||
engines: {node: '>=0.11'}
|
||||
|
||||
dateformat@3.0.3:
|
||||
resolution: {integrity: sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==}
|
||||
|
||||
@@ -5388,8 +5389,8 @@ packages:
|
||||
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
https-proxy-agent@7.0.5:
|
||||
resolution: {integrity: sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==}
|
||||
https-proxy-agent@7.0.4:
|
||||
resolution: {integrity: sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
human-signals@1.1.1:
|
||||
@@ -6553,6 +6554,11 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
mkdirp@2.1.6:
|
||||
resolution: {integrity: sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
modify-values@1.0.1:
|
||||
resolution: {integrity: sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -7724,6 +7730,9 @@ packages:
|
||||
reflect-metadata@0.1.13:
|
||||
resolution: {integrity: sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==}
|
||||
|
||||
reflect-metadata@0.1.14:
|
||||
resolution: {integrity: sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==}
|
||||
|
||||
reflect.getprototypeof@1.0.6:
|
||||
resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -8661,8 +8670,8 @@ packages:
|
||||
typedarray@0.0.6:
|
||||
resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
|
||||
|
||||
typeorm@0.3.11:
|
||||
resolution: {integrity: sha512-pzdOyWbVuz/z8Ww6gqvBW4nylsM0KLdUCDExr2gR20/x1khGSVxQkjNV/3YqliG90jrWzrknYbYscpk8yxFJVg==}
|
||||
typeorm@0.3.12:
|
||||
resolution: {integrity: sha512-sYSxBmCf1nJLLTcYtwqZ+lQIRtLPyUoO93rHTOKk9vJCyT4UfRtU7oRsJvfvKP3nnZTD1hzz2SEy2zwPEN6OyA==}
|
||||
engines: {node: '>= 12.9.0'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
@@ -8673,7 +8682,7 @@ packages:
|
||||
ioredis: ^5.0.4
|
||||
mongodb: ^3.6.0
|
||||
mssql: ^7.3.0
|
||||
mysql2: ^2.2.5
|
||||
mysql2: ^2.2.5 || ^3.0.1
|
||||
oracledb: ^5.1.0
|
||||
pg: ^8.5.1
|
||||
pg-native: ^3.0.0
|
||||
@@ -8759,10 +8768,6 @@ packages:
|
||||
undici-types@5.26.5:
|
||||
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
|
||||
|
||||
undici@6.20.1:
|
||||
resolution: {integrity: sha512-AjQF1QsmqfJys+LXfGTNum+qw4S88CojRInG/6t31W/1fk6G59s92bnAvGz5Cmur+kQv2SURXEvvudLmbrE8QA==}
|
||||
engines: {node: '>=18.17'}
|
||||
|
||||
unicode-canonical-property-names-ecmascript@2.0.0:
|
||||
resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -12305,7 +12310,7 @@ snapshots:
|
||||
fs-extra: 11.2.0
|
||||
globby: 11.1.0
|
||||
http-proxy-agent: 7.0.2
|
||||
https-proxy-agent: 7.0.5
|
||||
https-proxy-agent: 7.0.4
|
||||
issue-parser: 6.0.0
|
||||
lodash: 4.17.21
|
||||
mime: 3.0.0
|
||||
@@ -13819,13 +13824,13 @@ snapshots:
|
||||
ini: 1.3.8
|
||||
proto-list: 1.2.4
|
||||
|
||||
connect-typeorm@1.1.4(typeorm@0.3.11(sqlite3@5.1.4(encoding@0.1.13))(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5))):
|
||||
connect-typeorm@1.1.4(typeorm@0.3.12(sqlite3@5.1.4(encoding@0.1.13))(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5))):
|
||||
dependencies:
|
||||
'@types/debug': 0.0.31
|
||||
'@types/express-session': 1.17.6
|
||||
debug: 4.3.5(supports-color@8.1.1)
|
||||
express-session: 1.18.0
|
||||
typeorm: 0.3.11(sqlite3@5.1.4(encoding@0.1.13))(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5))
|
||||
typeorm: 0.3.12(sqlite3@5.1.4(encoding@0.1.13))(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -14176,6 +14181,10 @@ snapshots:
|
||||
|
||||
date-fns@2.29.3: {}
|
||||
|
||||
date-fns@2.30.0:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.24.7
|
||||
|
||||
dateformat@3.0.3: {}
|
||||
|
||||
dayjs@1.11.11: {}
|
||||
@@ -15730,7 +15739,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
https-proxy-agent@7.0.5:
|
||||
https-proxy-agent@7.0.4:
|
||||
dependencies:
|
||||
agent-base: 7.1.1
|
||||
debug: 4.3.5(supports-color@8.1.1)
|
||||
@@ -17140,6 +17149,8 @@ snapshots:
|
||||
|
||||
mkdirp@1.0.4: {}
|
||||
|
||||
mkdirp@2.1.6: {}
|
||||
|
||||
modify-values@1.0.1: {}
|
||||
|
||||
moment@2.30.1: {}
|
||||
@@ -18361,6 +18372,8 @@ snapshots:
|
||||
|
||||
reflect-metadata@0.1.13: {}
|
||||
|
||||
reflect-metadata@0.1.14: {}
|
||||
|
||||
reflect.getprototypeof@1.0.6:
|
||||
dependencies:
|
||||
call-bind: 1.0.7
|
||||
@@ -19418,23 +19431,23 @@ snapshots:
|
||||
|
||||
typedarray@0.0.6: {}
|
||||
|
||||
typeorm@0.3.11(sqlite3@5.1.4(encoding@0.1.13))(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5)):
|
||||
typeorm@0.3.12(sqlite3@5.1.4(encoding@0.1.13))(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5)):
|
||||
dependencies:
|
||||
'@sqltools/formatter': 1.2.5
|
||||
app-root-path: 3.1.0
|
||||
buffer: 6.0.3
|
||||
chalk: 4.1.2
|
||||
cli-highlight: 2.1.11
|
||||
date-fns: 2.29.3
|
||||
date-fns: 2.30.0
|
||||
debug: 4.3.5(supports-color@8.1.1)
|
||||
dotenv: 16.4.5
|
||||
glob: 7.2.3
|
||||
glob: 8.1.0
|
||||
js-yaml: 4.1.0
|
||||
mkdirp: 1.0.4
|
||||
reflect-metadata: 0.1.13
|
||||
mkdirp: 2.1.6
|
||||
reflect-metadata: 0.1.14
|
||||
sha.js: 2.4.11
|
||||
tslib: 2.6.3
|
||||
uuid: 8.3.2
|
||||
uuid: 9.0.1
|
||||
xml2js: 0.4.23
|
||||
yargs: 17.7.2
|
||||
optionalDependencies:
|
||||
@@ -19473,8 +19486,6 @@ snapshots:
|
||||
|
||||
undici-types@5.26.5: {}
|
||||
|
||||
undici@6.20.1: {}
|
||||
|
||||
unicode-canonical-property-names-ecmascript@2.0.0: {}
|
||||
|
||||
unicode-emoji-utils@1.2.0:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { RateLimitOptions } from '@server/utils/rateLimit';
|
||||
// import rateLimit from '@server/utils/rateLimit';
|
||||
import rateLimit from '@server/utils/rateLimit';
|
||||
import type NodeCache from 'node-cache';
|
||||
|
||||
// 5 minute default TTL (in seconds)
|
||||
@@ -26,34 +26,19 @@ class ExternalAPI {
|
||||
params: Record<string, string> = {},
|
||||
options: ExternalAPIOptions = {}
|
||||
) {
|
||||
// if (options.rateLimit) {
|
||||
// this.fetch = rateLimit(fetch, options.rateLimit);
|
||||
// } else {
|
||||
// this.fetch = fetch;
|
||||
// }
|
||||
this.fetch = fetch;
|
||||
|
||||
const url = new URL(baseUrl);
|
||||
|
||||
this.defaultHeaders = {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
...((url.username || url.password) && {
|
||||
Authorization: `Basic ${Buffer.from(
|
||||
`${url.username}:${url.password}`
|
||||
).toString('base64')}`,
|
||||
}),
|
||||
...options.headers,
|
||||
};
|
||||
|
||||
if (url.username || url.password) {
|
||||
url.username = '';
|
||||
url.password = '';
|
||||
baseUrl = url.toString();
|
||||
if (options.rateLimit) {
|
||||
this.fetch = rateLimit(fetch, options.rateLimit);
|
||||
} else {
|
||||
this.fetch = fetch;
|
||||
}
|
||||
|
||||
this.baseUrl = baseUrl;
|
||||
this.params = params;
|
||||
this.defaultHeaders = {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
...options.headers,
|
||||
};
|
||||
this.cache = options.nodeCache;
|
||||
}
|
||||
|
||||
|
||||
@@ -410,7 +410,7 @@ class JellyfinAPI extends ExternalAPI {
|
||||
).AccessToken;
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`Something went wrong while creating an API key from the Jellyfin server: ${e.message}`,
|
||||
`Something went wrong while creating an API key the Jellyfin server: ${e.message}`,
|
||||
{ label: 'Jellyfin API' }
|
||||
);
|
||||
|
||||
|
||||
@@ -180,7 +180,7 @@ class PlexAPI {
|
||||
settings.plex.libraries = [];
|
||||
}
|
||||
|
||||
await settings.save();
|
||||
settings.save();
|
||||
}
|
||||
|
||||
public async getLibraryContents(
|
||||
|
||||
@@ -21,9 +21,7 @@ import clearCookies from '@server/middleware/clearcookies';
|
||||
import routes from '@server/routes';
|
||||
import avatarproxy from '@server/routes/avatarproxy';
|
||||
import imageproxy from '@server/routes/imageproxy';
|
||||
import { appDataPermissions } from '@server/utils/appDataVolume';
|
||||
import { getAppVersion } from '@server/utils/appVersion';
|
||||
import createCustomProxyAgent from '@server/utils/customProxyAgent';
|
||||
import restartFlag from '@server/utils/restartFlag';
|
||||
import { getClientIp } from '@supercharge/request-ip';
|
||||
import { TypeormStore } from 'connect-typeorm/out';
|
||||
@@ -53,12 +51,6 @@ const dev = process.env.NODE_ENV !== 'production';
|
||||
const app = next({ dev });
|
||||
const handle = app.getRequestHandler();
|
||||
|
||||
if (!appDataPermissions()) {
|
||||
logger.error(
|
||||
'Something went wrong while checking config folder! Please ensure the config folder is set up properly.\nhttps://docs.jellyseerr.dev/getting-started'
|
||||
);
|
||||
}
|
||||
|
||||
app
|
||||
.prepare()
|
||||
.then(async () => {
|
||||
@@ -75,11 +67,6 @@ app
|
||||
const settings = await getSettings().load();
|
||||
restartFlag.initializeSettings(settings.main);
|
||||
|
||||
// Register HTTP proxy
|
||||
if (settings.main.proxy.enabled) {
|
||||
await createCustomProxyAgent(settings.main.proxy);
|
||||
}
|
||||
|
||||
// Migrate library types
|
||||
if (
|
||||
settings.plex.libraries.length > 1 &&
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import logger from '@server/logger';
|
||||
import type { RateLimitOptions } from '@server/utils/rateLimit';
|
||||
// import rateLimit from '@server/utils/rateLimit';
|
||||
import rateLimit from '@server/utils/rateLimit';
|
||||
import { createHash } from 'crypto';
|
||||
import { promises } from 'fs';
|
||||
import mime from 'mime/lite';
|
||||
@@ -135,7 +135,6 @@ class ImageProxy {
|
||||
private cacheVersion;
|
||||
private key;
|
||||
private baseUrl;
|
||||
private headers: HeadersInit | null = null;
|
||||
|
||||
constructor(
|
||||
key: string,
|
||||
@@ -143,28 +142,22 @@ class ImageProxy {
|
||||
options: {
|
||||
cacheVersion?: number;
|
||||
rateLimitOptions?: RateLimitOptions;
|
||||
headers?: HeadersInit;
|
||||
} = {}
|
||||
) {
|
||||
this.cacheVersion = options.cacheVersion ?? 1;
|
||||
this.baseUrl = baseUrl;
|
||||
this.key = key;
|
||||
|
||||
// if (options.rateLimitOptions) {
|
||||
// this.fetch = rateLimit(fetch, {
|
||||
// ...options.rateLimitOptions,
|
||||
// });
|
||||
// } else {
|
||||
// this.fetch = fetch;
|
||||
// }
|
||||
this.fetch = fetch;
|
||||
this.headers = options.headers || null;
|
||||
if (options.rateLimitOptions) {
|
||||
this.fetch = rateLimit(fetch, {
|
||||
...options.rateLimitOptions,
|
||||
});
|
||||
} else {
|
||||
this.fetch = fetch;
|
||||
}
|
||||
}
|
||||
|
||||
public async getImage(
|
||||
path: string,
|
||||
fallbackPath?: string
|
||||
): Promise<ImageResponse> {
|
||||
public async getImage(path: string): Promise<ImageResponse> {
|
||||
const cacheKey = this.getCacheKey(path);
|
||||
|
||||
const imageResponse = await this.get(cacheKey);
|
||||
@@ -173,11 +166,7 @@ class ImageProxy {
|
||||
const newImage = await this.set(path, cacheKey);
|
||||
|
||||
if (!newImage) {
|
||||
if (fallbackPath) {
|
||||
return await this.getImage(fallbackPath);
|
||||
} else {
|
||||
throw new Error('Failed to load image');
|
||||
}
|
||||
throw new Error('Failed to load image');
|
||||
}
|
||||
|
||||
return newImage;
|
||||
@@ -258,12 +247,7 @@ class ImageProxy {
|
||||
: '/'
|
||||
: '') +
|
||||
(path.startsWith('/') ? path.slice(1) : path);
|
||||
const response = await this.fetch(href, {
|
||||
headers: this.headers || undefined,
|
||||
});
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
const response = await this.fetch(href);
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ class PlexScanner
|
||||
});
|
||||
|
||||
settings.plex.libraries = newLibraries;
|
||||
await settings.save();
|
||||
settings.save();
|
||||
}
|
||||
} else {
|
||||
for (const library of this.libraries) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { MediaServerType } from '@server/constants/server';
|
||||
import { Permission } from '@server/lib/permissions';
|
||||
import { runMigrations } from '@server/lib/settings/migrator';
|
||||
import { randomUUID } from 'crypto';
|
||||
import fs from 'fs/promises';
|
||||
import fs from 'fs';
|
||||
import { merge } from 'lodash';
|
||||
import path from 'path';
|
||||
import webpush from 'web-push';
|
||||
@@ -99,17 +99,6 @@ interface Quota {
|
||||
quotaDays?: number;
|
||||
}
|
||||
|
||||
export interface ProxySettings {
|
||||
enabled: boolean;
|
||||
hostname: string;
|
||||
port: number;
|
||||
useSsl: boolean;
|
||||
user: string;
|
||||
password: string;
|
||||
bypassFilter: string;
|
||||
bypassLocalAddresses: boolean;
|
||||
}
|
||||
|
||||
export interface MainSettings {
|
||||
apiKey: string;
|
||||
applicationTitle: string;
|
||||
@@ -130,7 +119,6 @@ export interface MainSettings {
|
||||
mediaServerType: number;
|
||||
partialRequestsEnabled: boolean;
|
||||
locale: string;
|
||||
proxy: ProxySettings;
|
||||
}
|
||||
|
||||
interface PublicSettings {
|
||||
@@ -337,16 +325,6 @@ class Settings {
|
||||
mediaServerType: MediaServerType.NOT_CONFIGURED,
|
||||
partialRequestsEnabled: true,
|
||||
locale: 'en',
|
||||
proxy: {
|
||||
enabled: false,
|
||||
hostname: '',
|
||||
port: 8080,
|
||||
useSsl: false,
|
||||
user: '',
|
||||
password: '',
|
||||
bypassFilter: '',
|
||||
bypassLocalAddresses: true,
|
||||
},
|
||||
},
|
||||
plex: {
|
||||
name: '',
|
||||
@@ -501,6 +479,10 @@ class Settings {
|
||||
}
|
||||
|
||||
get main(): MainSettings {
|
||||
if (!this.data.main.apiKey) {
|
||||
this.data.main.apiKey = this.generateApiKey();
|
||||
this.save();
|
||||
}
|
||||
return this.data.main;
|
||||
}
|
||||
|
||||
@@ -602,20 +584,29 @@ class Settings {
|
||||
}
|
||||
|
||||
get clientId(): string {
|
||||
if (!this.data.clientId) {
|
||||
this.data.clientId = randomUUID();
|
||||
this.save();
|
||||
}
|
||||
|
||||
return this.data.clientId;
|
||||
}
|
||||
|
||||
get vapidPublic(): string {
|
||||
this.generateVapidKeys();
|
||||
|
||||
return this.data.vapidPublic;
|
||||
}
|
||||
|
||||
get vapidPrivate(): string {
|
||||
this.generateVapidKeys();
|
||||
|
||||
return this.data.vapidPrivate;
|
||||
}
|
||||
|
||||
public async regenerateApiKey(): Promise<MainSettings> {
|
||||
public regenerateApiKey(): MainSettings {
|
||||
this.main.apiKey = this.generateApiKey();
|
||||
await this.save();
|
||||
this.save();
|
||||
return this.main;
|
||||
}
|
||||
|
||||
@@ -627,6 +618,15 @@ class Settings {
|
||||
}
|
||||
}
|
||||
|
||||
private generateVapidKeys(force = false): void {
|
||||
if (!this.data.vapidPublic || !this.data.vapidPrivate || force) {
|
||||
const vapidKeys = webpush.generateVAPIDKeys();
|
||||
this.data.vapidPrivate = vapidKeys.privateKey;
|
||||
this.data.vapidPublic = vapidKeys.publicKey;
|
||||
this.save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings Load
|
||||
*
|
||||
@@ -641,51 +641,30 @@ class Settings {
|
||||
return this;
|
||||
}
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = await fs.readFile(SETTINGS_PATH, 'utf-8');
|
||||
} catch {
|
||||
await this.save();
|
||||
if (!fs.existsSync(SETTINGS_PATH)) {
|
||||
this.save();
|
||||
}
|
||||
const data = fs.readFileSync(SETTINGS_PATH, 'utf-8');
|
||||
|
||||
if (data) {
|
||||
const parsedJson = JSON.parse(data);
|
||||
const migratedData = await runMigrations(parsedJson, SETTINGS_PATH);
|
||||
this.data = merge(this.data, migratedData);
|
||||
}
|
||||
this.data = await runMigrations(parsedJson, SETTINGS_PATH);
|
||||
|
||||
// generate keys and ids if it's missing
|
||||
let change = false;
|
||||
if (!this.data.main.apiKey) {
|
||||
this.data.main.apiKey = this.generateApiKey();
|
||||
change = true;
|
||||
} else if (process.env.API_KEY) {
|
||||
if (this.main.apiKey != process.env.API_KEY) {
|
||||
this.main.apiKey = process.env.API_KEY;
|
||||
this.data = merge(this.data, parsedJson);
|
||||
|
||||
if (process.env.API_KEY) {
|
||||
if (this.main.apiKey != process.env.API_KEY) {
|
||||
this.main.apiKey = process.env.API_KEY;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!this.data.clientId) {
|
||||
this.data.clientId = randomUUID();
|
||||
change = true;
|
||||
}
|
||||
if (!this.data.vapidPublic || !this.data.vapidPrivate) {
|
||||
const vapidKeys = webpush.generateVAPIDKeys();
|
||||
this.data.vapidPrivate = vapidKeys.privateKey;
|
||||
this.data.vapidPublic = vapidKeys.publicKey;
|
||||
change = true;
|
||||
}
|
||||
if (change) {
|
||||
await this.save();
|
||||
}
|
||||
|
||||
this.save();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public async save(): Promise<void> {
|
||||
await fs.writeFile(
|
||||
SETTINGS_PATH,
|
||||
JSON.stringify(this.data, undefined, ' ')
|
||||
);
|
||||
public save(): void {
|
||||
fs.writeFileSync(SETTINGS_PATH, JSON.stringify(this.data, undefined, ' '));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import type { AllSettings } from '@server/lib/settings';
|
||||
|
||||
const migrateHostname = (settings: any): AllSettings => {
|
||||
if (settings.jellyfin?.hostname) {
|
||||
const { hostname } = settings.jellyfin;
|
||||
const oldJellyfinSettings = settings.jellyfin;
|
||||
if (oldJellyfinSettings && oldJellyfinSettings.hostname) {
|
||||
const { hostname } = oldJellyfinSettings;
|
||||
const protocolMatch = hostname.match(/^(https?):\/\//i);
|
||||
const useSsl = protocolMatch && protocolMatch[1].toLowerCase() === 'https';
|
||||
const remainingUrl = hostname.replace(/^(https?):\/\//i, '');
|
||||
const urlMatch = remainingUrl.match(/^([^:]+)(:([0-9]+))?(\/.*)?$/);
|
||||
|
||||
delete settings.jellyfin.hostname;
|
||||
delete oldJellyfinSettings.hostname;
|
||||
if (urlMatch) {
|
||||
const [, ip, , port, urlBase] = urlMatch;
|
||||
settings.jellyfin = {
|
||||
@@ -20,7 +21,9 @@ const migrateHostname = (settings: any): AllSettings => {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.jellyfin && settings.jellyfin.hostname) {
|
||||
delete settings.jellyfin.hostname;
|
||||
}
|
||||
return settings;
|
||||
};
|
||||
|
||||
|
||||
@@ -27,14 +27,8 @@ const migrateApiTokens = async (settings: any): Promise<AllSettings> => {
|
||||
admin.jellyfinDeviceId
|
||||
);
|
||||
jellyfinClient.setUserId(admin.jellyfinUserId ?? '');
|
||||
try {
|
||||
const apiKey = await jellyfinClient.createApiToken('Jellyseerr');
|
||||
settings.jellyfin.apiKey = apiKey;
|
||||
} catch {
|
||||
throw new Error(
|
||||
"Failed to create Jellyfin API token from admin account. Please check your network configuration or edit your settings.json by adding an 'apiKey' field inside of the 'jellyfin' section to fix this issue."
|
||||
);
|
||||
}
|
||||
const apiKey = await jellyfinClient.createApiToken('Jellyseerr');
|
||||
settings.jellyfin.apiKey = apiKey;
|
||||
}
|
||||
return settings;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable no-console */
|
||||
import type { AllSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import fs from 'fs/promises';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const migrationsDir = path.join(__dirname, 'migrations');
|
||||
@@ -9,56 +10,19 @@ export const runMigrations = async (
|
||||
settings: AllSettings,
|
||||
SETTINGS_PATH: string
|
||||
): Promise<AllSettings> => {
|
||||
const migrations = fs
|
||||
.readdirSync(migrationsDir)
|
||||
.filter((file) => file.endsWith('.js') || file.endsWith('.ts'))
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
.map((file) => require(path.join(migrationsDir, file)).default);
|
||||
|
||||
let migrated = settings;
|
||||
|
||||
try {
|
||||
// we read old backup and create a backup of currents settings
|
||||
const BACKUP_PATH = SETTINGS_PATH.replace('.json', '.old.json');
|
||||
let oldBackup: string | null = null;
|
||||
try {
|
||||
oldBackup = await fs.readFile(BACKUP_PATH, 'utf-8');
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
await fs.writeFile(BACKUP_PATH, JSON.stringify(settings, undefined, ' '));
|
||||
|
||||
const migrations = (await fs.readdir(migrationsDir)).filter(
|
||||
(file) => file.endsWith('.js') || file.endsWith('.ts')
|
||||
);
|
||||
|
||||
const settingsBefore = JSON.stringify(migrated);
|
||||
|
||||
for (const migration of migrations) {
|
||||
try {
|
||||
logger.debug(`Checking migration '${migration}'...`, {
|
||||
label: 'Settings Migrator',
|
||||
});
|
||||
const { default: migrationFn } = await import(
|
||||
path.join(migrationsDir, migration)
|
||||
);
|
||||
const newSettings = await migrationFn(structuredClone(migrated));
|
||||
if (JSON.stringify(migrated) !== JSON.stringify(newSettings)) {
|
||||
logger.debug(`Migration '${migration}' has been applied.`, {
|
||||
label: 'Settings Migrator',
|
||||
});
|
||||
}
|
||||
migrated = newSettings;
|
||||
} catch (e) {
|
||||
// we stop jellyseerr if the migration failed
|
||||
logger.error(
|
||||
`Error while running migration '${migration}': ${e.message}`,
|
||||
{
|
||||
label: 'Settings Migrator',
|
||||
}
|
||||
);
|
||||
logger.error(
|
||||
'A common cause for this error is a permission issue with your configuration folder, a network issue or a corrupted database.',
|
||||
{
|
||||
label: 'Settings Migrator',
|
||||
}
|
||||
);
|
||||
process.exit();
|
||||
}
|
||||
migrated = await migration(migrated);
|
||||
}
|
||||
|
||||
const settingsAfter = JSON.stringify(migrated);
|
||||
@@ -66,33 +30,30 @@ export const runMigrations = async (
|
||||
if (settingsBefore !== settingsAfter) {
|
||||
// a migration occured
|
||||
// we check that the new config will be saved
|
||||
await fs.writeFile(
|
||||
SETTINGS_PATH,
|
||||
JSON.stringify(migrated, undefined, ' ')
|
||||
);
|
||||
const fileSaved = JSON.parse(await fs.readFile(SETTINGS_PATH, 'utf-8'));
|
||||
fs.writeFileSync(SETTINGS_PATH, JSON.stringify(migrated, undefined, ' '));
|
||||
const fileSaved = JSON.parse(fs.readFileSync(SETTINGS_PATH, 'utf-8'));
|
||||
if (JSON.stringify(fileSaved) !== settingsAfter) {
|
||||
// something went wrong while saving file
|
||||
throw new Error('Unable to save settings after migration.');
|
||||
}
|
||||
} else if (oldBackup) {
|
||||
// no migration occured
|
||||
// we save the old backup (to avoid settings.json and settings.old.json being the same)
|
||||
await fs.writeFile(BACKUP_PATH, oldBackup.toString());
|
||||
}
|
||||
} catch (e) {
|
||||
// we stop jellyseerr if the migration failed
|
||||
logger.error(
|
||||
`Something went wrong while running settings migrations: ${e.message}`,
|
||||
{
|
||||
label: 'Settings Migrator',
|
||||
}
|
||||
{ label: 'Settings Migrator' }
|
||||
);
|
||||
logger.error(
|
||||
'A common cause for this issue is a permission error of your configuration folder.',
|
||||
{
|
||||
label: 'Settings Migrator',
|
||||
}
|
||||
// we stop jellyseerr if the migration failed
|
||||
console.log(
|
||||
'===================================================================='
|
||||
);
|
||||
console.log(
|
||||
' SOMETHING WENT WRONG WHILE RUNNING SETTINGS MIGRATIONS '
|
||||
);
|
||||
console.log(
|
||||
' Please check that your configuration folder is properly set up '
|
||||
);
|
||||
console.log(
|
||||
'===================================================================='
|
||||
);
|
||||
process.exit();
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { UserType } from '@server/constants/user';
|
||||
import { getRepository } from '@server/datasource';
|
||||
import { User } from '@server/entity/User';
|
||||
import { startJobs } from '@server/job/schedule';
|
||||
import ImageProxy from '@server/lib/imageproxy';
|
||||
import { Permission } from '@server/lib/permissions';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
@@ -14,6 +15,7 @@ import { ApiError } from '@server/types/error';
|
||||
import { getHostname } from '@server/utils/getHostname';
|
||||
import * as EmailValidator from 'email-validator';
|
||||
import { Router } from 'express';
|
||||
import gravatarUrl from 'gravatar-url';
|
||||
import net from 'net';
|
||||
|
||||
const authRoutes = Router();
|
||||
@@ -87,7 +89,7 @@ authRoutes.post('/plex', async (req, res, next) => {
|
||||
});
|
||||
|
||||
settings.main.mediaServerType = MediaServerType.PLEX;
|
||||
await settings.save();
|
||||
settings.save();
|
||||
startJobs();
|
||||
|
||||
await userRepository.save(user);
|
||||
@@ -299,84 +301,64 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
where: { jellyfinUserId: account.User.Id },
|
||||
});
|
||||
|
||||
const missingAdminUser = !user && !(await userRepository.count());
|
||||
if (
|
||||
missingAdminUser ||
|
||||
settings.main.mediaServerType === MediaServerType.NOT_CONFIGURED
|
||||
) {
|
||||
if (!user && !(await userRepository.count())) {
|
||||
// Check if user is admin on jellyfin
|
||||
if (account.User.Policy.IsAdministrator === false) {
|
||||
throw new ApiError(403, ApiErrorCode.NotAdmin);
|
||||
}
|
||||
|
||||
if (
|
||||
body.serverType !== MediaServerType.JELLYFIN &&
|
||||
body.serverType !== MediaServerType.EMBY
|
||||
) {
|
||||
throw new Error('select_server_type');
|
||||
}
|
||||
settings.main.mediaServerType = body.serverType;
|
||||
|
||||
if (missingAdminUser) {
|
||||
logger.info(
|
||||
'Sign-in attempt from Jellyfin user with access to the media server; creating initial admin user for Jellyseerr',
|
||||
{
|
||||
label: 'API',
|
||||
ip: req.ip,
|
||||
jellyfinUsername: account.User.Name,
|
||||
}
|
||||
);
|
||||
|
||||
// User doesn't exist, and there are no users in the database, we'll create the user
|
||||
// with admin permissions
|
||||
|
||||
user = new User({
|
||||
id: 1,
|
||||
email: body.email || account.User.Name,
|
||||
logger.info(
|
||||
'Sign-in attempt from Jellyfin user with access to the media server; creating initial admin user for Overseerr',
|
||||
{
|
||||
label: 'API',
|
||||
ip: req.ip,
|
||||
jellyfinUsername: account.User.Name,
|
||||
jellyfinUserId: account.User.Id,
|
||||
jellyfinDeviceId: deviceId,
|
||||
jellyfinAuthToken: account.AccessToken,
|
||||
permissions: Permission.ADMIN,
|
||||
avatar: `/avatarproxy/${account.User.Id}`,
|
||||
userType:
|
||||
body.serverType === MediaServerType.JELLYFIN
|
||||
? UserType.JELLYFIN
|
||||
: UserType.EMBY,
|
||||
});
|
||||
|
||||
await userRepository.save(user);
|
||||
} else {
|
||||
logger.info(
|
||||
'Sign-in attempt from Jellyfin user with access to the media server; editing admin user for Jellyseerr',
|
||||
{
|
||||
label: 'API',
|
||||
ip: req.ip,
|
||||
jellyfinUsername: account.User.Name,
|
||||
}
|
||||
);
|
||||
|
||||
// User alread exist but settings.json is not configured, we'll edit the admin user
|
||||
|
||||
user = await userRepository.findOne({
|
||||
where: { id: 1 },
|
||||
});
|
||||
if (!user) {
|
||||
throw new Error('Unable to find admin user to edit');
|
||||
}
|
||||
user.email = body.email || account.User.Name;
|
||||
user.jellyfinUsername = account.User.Name;
|
||||
user.jellyfinUserId = account.User.Id;
|
||||
user.jellyfinDeviceId = deviceId;
|
||||
user.jellyfinAuthToken = account.AccessToken;
|
||||
user.permissions = Permission.ADMIN;
|
||||
user.avatar = `/avatarproxy/${account.User.Id}`;
|
||||
user.userType =
|
||||
body.serverType === MediaServerType.JELLYFIN
|
||||
? UserType.JELLYFIN
|
||||
: UserType.EMBY;
|
||||
);
|
||||
|
||||
await userRepository.save(user);
|
||||
// User doesn't exist, and there are no users in the database, we'll create the user
|
||||
// with admin permissions
|
||||
switch (body.serverType) {
|
||||
case MediaServerType.EMBY:
|
||||
settings.main.mediaServerType = MediaServerType.EMBY;
|
||||
user = new User({
|
||||
email: body.email || account.User.Name,
|
||||
jellyfinUsername: account.User.Name,
|
||||
jellyfinUserId: account.User.Id,
|
||||
jellyfinDeviceId: deviceId,
|
||||
jellyfinAuthToken: account.AccessToken,
|
||||
permissions: Permission.ADMIN,
|
||||
avatar: account.User.PrimaryImageTag
|
||||
? `/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
|
||||
: gravatarUrl(body.email || account.User.Name, {
|
||||
default: 'mm',
|
||||
size: 200,
|
||||
}),
|
||||
userType: UserType.EMBY,
|
||||
});
|
||||
|
||||
break;
|
||||
case MediaServerType.JELLYFIN:
|
||||
settings.main.mediaServerType = MediaServerType.JELLYFIN;
|
||||
user = new User({
|
||||
email: body.email || account.User.Name,
|
||||
jellyfinUsername: account.User.Name,
|
||||
jellyfinUserId: account.User.Id,
|
||||
jellyfinDeviceId: deviceId,
|
||||
jellyfinAuthToken: account.AccessToken,
|
||||
permissions: Permission.ADMIN,
|
||||
avatar: account.User.PrimaryImageTag
|
||||
? `/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
|
||||
: gravatarUrl(body.email || account.User.Name, {
|
||||
default: 'mm',
|
||||
size: 200,
|
||||
}),
|
||||
userType: UserType.JELLYFIN,
|
||||
});
|
||||
|
||||
break;
|
||||
default:
|
||||
throw new Error('select_server_type');
|
||||
}
|
||||
|
||||
// Create an API key on Jellyfin from this admin user
|
||||
@@ -396,8 +378,10 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
settings.jellyfin.urlBase = body.urlBase ?? '';
|
||||
settings.jellyfin.useSsl = body.useSsl ?? false;
|
||||
settings.jellyfin.apiKey = apiKey;
|
||||
await settings.save();
|
||||
settings.save();
|
||||
startJobs();
|
||||
|
||||
await userRepository.save(user);
|
||||
}
|
||||
// User already exists, let's update their information
|
||||
else if (account.User.Id === user?.jellyfinUserId) {
|
||||
@@ -417,7 +401,27 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
jellyfinUsername: account.User.Name,
|
||||
}
|
||||
);
|
||||
user.avatar = `/avatarproxy/${account.User.Id}`;
|
||||
// Update the users avatar with their jellyfin profile pic (incase it changed)
|
||||
if (account.User.PrimaryImageTag) {
|
||||
const avatar = `/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`;
|
||||
if (avatar !== user.avatar) {
|
||||
const avatarProxy = new ImageProxy('avatar', '');
|
||||
avatarProxy.clearCachedImage(user.avatar);
|
||||
}
|
||||
user.avatar = avatar;
|
||||
} else {
|
||||
const avatar = gravatarUrl(user.email || account.User.Name, {
|
||||
default: 'mm',
|
||||
size: 200,
|
||||
});
|
||||
|
||||
if (avatar !== user.avatar) {
|
||||
const avatarProxy = new ImageProxy('avatar', '');
|
||||
avatarProxy.clearCachedImage(user.avatar);
|
||||
}
|
||||
|
||||
user.avatar = avatar;
|
||||
}
|
||||
user.jellyfinUsername = account.User.Name;
|
||||
|
||||
if (user.username === account.User.Name) {
|
||||
@@ -455,7 +459,12 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
jellyfinUserId: account.User.Id,
|
||||
jellyfinDeviceId: deviceId,
|
||||
permissions: settings.main.defaultPermissions,
|
||||
avatar: `/avatarproxy/${account.User.Id}`,
|
||||
avatar: account.User.PrimaryImageTag
|
||||
? `/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
|
||||
: gravatarUrl(body.email || account.User.Name, {
|
||||
default: 'mm',
|
||||
size: 200,
|
||||
}),
|
||||
userType:
|
||||
settings.main.mediaServerType === MediaServerType.JELLYFIN
|
||||
? UserType.JELLYFIN
|
||||
|
||||
@@ -1,39 +1,21 @@
|
||||
import { MediaServerType } from '@server/constants/server';
|
||||
import { getRepository } from '@server/datasource';
|
||||
import { User } from '@server/entity/User';
|
||||
import ImageProxy from '@server/lib/imageproxy';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import { getAppVersion } from '@server/utils/appVersion';
|
||||
import { getHostname } from '@server/utils/getHostname';
|
||||
import { Router } from 'express';
|
||||
import gravatarUrl from 'gravatar-url';
|
||||
|
||||
const router = Router();
|
||||
|
||||
let _avatarImageProxy: ImageProxy | null = null;
|
||||
async function initAvatarImageProxy() {
|
||||
if (!_avatarImageProxy) {
|
||||
const userRepository = getRepository(User);
|
||||
const admin = await userRepository.findOne({
|
||||
where: { id: 1 },
|
||||
select: ['id', 'jellyfinUserId', 'jellyfinDeviceId'],
|
||||
order: { id: 'ASC' },
|
||||
});
|
||||
const deviceId = admin?.jellyfinDeviceId;
|
||||
const authToken = getSettings().jellyfin.apiKey;
|
||||
_avatarImageProxy = new ImageProxy('avatar', '', {
|
||||
headers: {
|
||||
'X-Emby-Authorization': `MediaBrowser Client="Jellyseerr", Device="Jellyseerr", DeviceId="${deviceId}", Version="${getAppVersion()}", Token="${authToken}"`,
|
||||
},
|
||||
});
|
||||
}
|
||||
return _avatarImageProxy;
|
||||
}
|
||||
|
||||
router.get('/:jellyfinUserId', async (req, res) => {
|
||||
const avatarImageProxy = new ImageProxy('avatar', '');
|
||||
// Proxy avatar images
|
||||
router.get('/*', async (req, res) => {
|
||||
let imagePath = '';
|
||||
try {
|
||||
if (!req.params.jellyfinUserId.match(/^[a-f0-9]{32}$/)) {
|
||||
const jellyfinAvatar = req.url.match(
|
||||
/(\/Users\/\w+\/Images\/Primary\/?\?tag=\w+&quality=90)$/
|
||||
)?.[1];
|
||||
if (!jellyfinAvatar) {
|
||||
const mediaServerType = getSettings().main.mediaServerType;
|
||||
throw new Error(
|
||||
`Provided URL is not ${
|
||||
@@ -44,28 +26,10 @@ router.get('/:jellyfinUserId', async (req, res) => {
|
||||
);
|
||||
}
|
||||
|
||||
const avatarImageCache = await initAvatarImageProxy();
|
||||
const imageUrl = new URL(jellyfinAvatar, getHostname());
|
||||
imagePath = imageUrl.toString();
|
||||
|
||||
const user = await getRepository(User).findOne({
|
||||
where: { jellyfinUserId: req.params.jellyfinUserId },
|
||||
});
|
||||
|
||||
const fallbackUrl = gravatarUrl(user?.email || 'none', {
|
||||
default: 'mm',
|
||||
size: 200,
|
||||
});
|
||||
const jellyfinAvatarUrl = `${getHostname()}/UserImage?UserId=${
|
||||
req.params.jellyfinUserId
|
||||
}`;
|
||||
let imageData = await avatarImageCache.getImage(
|
||||
jellyfinAvatarUrl,
|
||||
fallbackUrl
|
||||
);
|
||||
|
||||
if (imageData.meta.extension === 'json') {
|
||||
// this is a 404
|
||||
imageData = await avatarImageCache.getImage(fallbackUrl);
|
||||
}
|
||||
const imageData = await avatarImageProxy.getImage(imagePath);
|
||||
|
||||
res.writeHead(200, {
|
||||
'Content-Type': `image/${imageData.meta.extension}`,
|
||||
@@ -78,6 +42,7 @@ router.get('/:jellyfinUserId', async (req, res) => {
|
||||
res.end(imageData.imageBuffer);
|
||||
} catch (e) {
|
||||
logger.error('Failed to proxy avatar image', {
|
||||
imagePath,
|
||||
errorMessage: e.message,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -17,11 +17,7 @@ import { mapProductionCompany } from '@server/models/Movie';
|
||||
import { mapNetwork } from '@server/models/Tv';
|
||||
import settingsRoutes from '@server/routes/settings';
|
||||
import watchlistRoutes from '@server/routes/watchlist';
|
||||
import {
|
||||
appDataPath,
|
||||
appDataPermissions,
|
||||
appDataStatus,
|
||||
} from '@server/utils/appDataVolume';
|
||||
import { appDataPath, appDataStatus } from '@server/utils/appDataVolume';
|
||||
import { getAppVersion, getCommitTag } from '@server/utils/appVersion';
|
||||
import restartFlag from '@server/utils/restartFlag';
|
||||
import { isPerson } from '@server/utils/typeHelpers';
|
||||
@@ -97,7 +93,6 @@ router.get('/status/appdata', (_req, res) => {
|
||||
return res.status(200).json({
|
||||
appData: appDataStatus(),
|
||||
appDataPath: appDataPath(),
|
||||
appDataPermissions: appDataPermissions(),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -123,13 +123,9 @@ serviceRoutes.get<{ sonarrId: string }>(
|
||||
});
|
||||
|
||||
try {
|
||||
const systemStatus = await sonarr.getSystemStatus();
|
||||
const sonarrMajorVersion = Number(systemStatus.version.split('.')[0]);
|
||||
|
||||
const profiles = await sonarr.getProfiles();
|
||||
const rootFolders = await sonarr.getRootFolders();
|
||||
const languageProfiles =
|
||||
sonarrMajorVersion <= 3 ? await sonarr.getLanguageProfiles() : null;
|
||||
const languageProfiles = await sonarr.getLanguageProfiles();
|
||||
const tags = await sonarr.getTags();
|
||||
|
||||
return res.status(200).json({
|
||||
|
||||
@@ -32,6 +32,7 @@ import { getHostname } from '@server/utils/getHostname';
|
||||
import { Router } from 'express';
|
||||
import rateLimit from 'express-rate-limit';
|
||||
import fs from 'fs';
|
||||
import gravatarUrl from 'gravatar-url';
|
||||
import { escapeRegExp, merge, omit, set, sortBy } from 'lodash';
|
||||
import { rescheduleJob } from 'node-schedule';
|
||||
import path from 'path';
|
||||
@@ -69,19 +70,19 @@ settingsRoutes.get('/main', (req, res, next) => {
|
||||
res.status(200).json(filteredMainSettings(req.user, settings.main));
|
||||
});
|
||||
|
||||
settingsRoutes.post('/main', async (req, res) => {
|
||||
settingsRoutes.post('/main', (req, res) => {
|
||||
const settings = getSettings();
|
||||
|
||||
settings.main = merge(settings.main, req.body);
|
||||
await settings.save();
|
||||
settings.save();
|
||||
|
||||
return res.status(200).json(settings.main);
|
||||
});
|
||||
|
||||
settingsRoutes.post('/main/regenerate', async (req, res, next) => {
|
||||
settingsRoutes.post('/main/regenerate', (req, res, next) => {
|
||||
const settings = getSettings();
|
||||
|
||||
const main = await settings.regenerateApiKey();
|
||||
const main = settings.regenerateApiKey();
|
||||
|
||||
if (!req.user) {
|
||||
return next({ status: 500, message: 'User missing from request.' });
|
||||
@@ -118,7 +119,7 @@ settingsRoutes.post('/plex', async (req, res, next) => {
|
||||
settings.plex.machineId = result.MediaContainer.machineIdentifier;
|
||||
settings.plex.name = result.MediaContainer.friendlyName;
|
||||
|
||||
await settings.save();
|
||||
settings.save();
|
||||
} catch (e) {
|
||||
logger.error('Something went wrong testing Plex connection', {
|
||||
label: 'API',
|
||||
@@ -231,7 +232,7 @@ settingsRoutes.get('/plex/library', async (req, res) => {
|
||||
...library,
|
||||
enabled: enabledLibraries.includes(library.id),
|
||||
}));
|
||||
await settings.save();
|
||||
settings.save();
|
||||
return res.status(200).json(settings.plex.libraries);
|
||||
});
|
||||
|
||||
@@ -282,7 +283,7 @@ settingsRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
Object.assign(settings.jellyfin, req.body);
|
||||
settings.jellyfin.serverId = result.Id;
|
||||
settings.jellyfin.name = result.ServerName;
|
||||
await settings.save();
|
||||
settings.save();
|
||||
} catch (e) {
|
||||
if (e instanceof ApiError) {
|
||||
logger.error('Something went wrong testing Jellyfin connection', {
|
||||
@@ -370,7 +371,7 @@ settingsRoutes.get('/jellyfin/library', async (req, res, next) => {
|
||||
...library,
|
||||
enabled: enabledLibraries.includes(library.id),
|
||||
}));
|
||||
await settings.save();
|
||||
settings.save();
|
||||
return res.status(200).json(settings.jellyfin.libraries);
|
||||
});
|
||||
|
||||
@@ -394,7 +395,9 @@ settingsRoutes.get('/jellyfin/users', async (req, res) => {
|
||||
const users = resp.users.map((user) => ({
|
||||
username: user.Name,
|
||||
id: user.Id,
|
||||
thumb: `/avatarproxy/${user.Id}`,
|
||||
thumb: user.PrimaryImageTag
|
||||
? `/Users/${user.Id}/Images/Primary/?tag=${user.PrimaryImageTag}&quality=90`
|
||||
: gravatarUrl(user.Name, { default: 'mm', size: 200 }),
|
||||
email: user.Name,
|
||||
}));
|
||||
|
||||
@@ -434,7 +437,7 @@ settingsRoutes.post('/tautulli', async (req, res, next) => {
|
||||
throw new Error('Tautulli version not supported');
|
||||
}
|
||||
|
||||
await settings.save();
|
||||
settings.save();
|
||||
} catch (e) {
|
||||
logger.error('Something went wrong testing Tautulli connection', {
|
||||
label: 'API',
|
||||
@@ -695,7 +698,7 @@ settingsRoutes.post<{ jobId: JobId }>(
|
||||
|
||||
settingsRoutes.post<{ jobId: JobId }>(
|
||||
'/jobs/:jobId/schedule',
|
||||
async (req, res, next) => {
|
||||
(req, res, next) => {
|
||||
const scheduledJob = scheduledJobs.find(
|
||||
(job) => job.id === req.params.jobId
|
||||
);
|
||||
@@ -709,7 +712,7 @@ settingsRoutes.post<{ jobId: JobId }>(
|
||||
|
||||
if (result) {
|
||||
settings.jobs[scheduledJob.id].schedule = req.body.schedule;
|
||||
await settings.save();
|
||||
settings.save();
|
||||
|
||||
scheduledJob.cronSchedule = req.body.schedule;
|
||||
|
||||
@@ -766,11 +769,11 @@ settingsRoutes.post<{ cacheId: AvailableCacheIds }>(
|
||||
settingsRoutes.post(
|
||||
'/initialize',
|
||||
isAuthenticated(Permission.ADMIN),
|
||||
async (_req, res) => {
|
||||
(_req, res) => {
|
||||
const settings = getSettings();
|
||||
|
||||
settings.public.initialized = true;
|
||||
await settings.save();
|
||||
settings.save();
|
||||
|
||||
return res.status(200).json(settings.public);
|
||||
}
|
||||
|
||||
@@ -31,11 +31,11 @@ notificationRoutes.get('/discord', (_req, res) => {
|
||||
res.status(200).json(settings.notifications.agents.discord);
|
||||
});
|
||||
|
||||
notificationRoutes.post('/discord', async (req, res) => {
|
||||
notificationRoutes.post('/discord', (req, res) => {
|
||||
const settings = getSettings();
|
||||
|
||||
settings.notifications.agents.discord = req.body;
|
||||
await settings.save();
|
||||
settings.save();
|
||||
|
||||
res.status(200).json(settings.notifications.agents.discord);
|
||||
});
|
||||
@@ -65,11 +65,11 @@ notificationRoutes.get('/slack', (_req, res) => {
|
||||
res.status(200).json(settings.notifications.agents.slack);
|
||||
});
|
||||
|
||||
notificationRoutes.post('/slack', async (req, res) => {
|
||||
notificationRoutes.post('/slack', (req, res) => {
|
||||
const settings = getSettings();
|
||||
|
||||
settings.notifications.agents.slack = req.body;
|
||||
await settings.save();
|
||||
settings.save();
|
||||
|
||||
res.status(200).json(settings.notifications.agents.slack);
|
||||
});
|
||||
@@ -99,11 +99,11 @@ notificationRoutes.get('/telegram', (_req, res) => {
|
||||
res.status(200).json(settings.notifications.agents.telegram);
|
||||
});
|
||||
|
||||
notificationRoutes.post('/telegram', async (req, res) => {
|
||||
notificationRoutes.post('/telegram', (req, res) => {
|
||||
const settings = getSettings();
|
||||
|
||||
settings.notifications.agents.telegram = req.body;
|
||||
await settings.save();
|
||||
settings.save();
|
||||
|
||||
res.status(200).json(settings.notifications.agents.telegram);
|
||||
});
|
||||
@@ -133,11 +133,11 @@ notificationRoutes.get('/pushbullet', (_req, res) => {
|
||||
res.status(200).json(settings.notifications.agents.pushbullet);
|
||||
});
|
||||
|
||||
notificationRoutes.post('/pushbullet', async (req, res) => {
|
||||
notificationRoutes.post('/pushbullet', (req, res) => {
|
||||
const settings = getSettings();
|
||||
|
||||
settings.notifications.agents.pushbullet = req.body;
|
||||
await settings.save();
|
||||
settings.save();
|
||||
|
||||
res.status(200).json(settings.notifications.agents.pushbullet);
|
||||
});
|
||||
@@ -167,11 +167,11 @@ notificationRoutes.get('/pushover', (_req, res) => {
|
||||
res.status(200).json(settings.notifications.agents.pushover);
|
||||
});
|
||||
|
||||
notificationRoutes.post('/pushover', async (req, res) => {
|
||||
notificationRoutes.post('/pushover', (req, res) => {
|
||||
const settings = getSettings();
|
||||
|
||||
settings.notifications.agents.pushover = req.body;
|
||||
await settings.save();
|
||||
settings.save();
|
||||
|
||||
res.status(200).json(settings.notifications.agents.pushover);
|
||||
});
|
||||
@@ -201,11 +201,11 @@ notificationRoutes.get('/email', (_req, res) => {
|
||||
res.status(200).json(settings.notifications.agents.email);
|
||||
});
|
||||
|
||||
notificationRoutes.post('/email', async (req, res) => {
|
||||
notificationRoutes.post('/email', (req, res) => {
|
||||
const settings = getSettings();
|
||||
|
||||
settings.notifications.agents.email = req.body;
|
||||
await settings.save();
|
||||
settings.save();
|
||||
|
||||
res.status(200).json(settings.notifications.agents.email);
|
||||
});
|
||||
@@ -235,11 +235,11 @@ notificationRoutes.get('/webpush', (_req, res) => {
|
||||
res.status(200).json(settings.notifications.agents.webpush);
|
||||
});
|
||||
|
||||
notificationRoutes.post('/webpush', async (req, res) => {
|
||||
notificationRoutes.post('/webpush', (req, res) => {
|
||||
const settings = getSettings();
|
||||
|
||||
settings.notifications.agents.webpush = req.body;
|
||||
await settings.save();
|
||||
settings.save();
|
||||
|
||||
res.status(200).json(settings.notifications.agents.webpush);
|
||||
});
|
||||
@@ -284,7 +284,7 @@ notificationRoutes.get('/webhook', (_req, res) => {
|
||||
res.status(200).json(response);
|
||||
});
|
||||
|
||||
notificationRoutes.post('/webhook', async (req, res, next) => {
|
||||
notificationRoutes.post('/webhook', (req, res, next) => {
|
||||
const settings = getSettings();
|
||||
try {
|
||||
JSON.parse(req.body.options.jsonPayload);
|
||||
@@ -300,7 +300,7 @@ notificationRoutes.post('/webhook', async (req, res, next) => {
|
||||
authHeader: req.body.options.authHeader,
|
||||
},
|
||||
};
|
||||
await settings.save();
|
||||
settings.save();
|
||||
|
||||
res.status(200).json(settings.notifications.agents.webhook);
|
||||
} catch (e) {
|
||||
@@ -351,11 +351,11 @@ notificationRoutes.get('/lunasea', (_req, res) => {
|
||||
res.status(200).json(settings.notifications.agents.lunasea);
|
||||
});
|
||||
|
||||
notificationRoutes.post('/lunasea', async (req, res) => {
|
||||
notificationRoutes.post('/lunasea', (req, res) => {
|
||||
const settings = getSettings();
|
||||
|
||||
settings.notifications.agents.lunasea = req.body;
|
||||
await settings.save();
|
||||
settings.save();
|
||||
|
||||
res.status(200).json(settings.notifications.agents.lunasea);
|
||||
});
|
||||
@@ -385,11 +385,11 @@ notificationRoutes.get('/gotify', (_req, res) => {
|
||||
res.status(200).json(settings.notifications.agents.gotify);
|
||||
});
|
||||
|
||||
notificationRoutes.post('/gotify', async (req, res) => {
|
||||
notificationRoutes.post('/gotify', (req, res) => {
|
||||
const settings = getSettings();
|
||||
|
||||
settings.notifications.agents.gotify = req.body;
|
||||
await settings.save();
|
||||
settings.save();
|
||||
|
||||
res.status(200).json(settings.notifications.agents.gotify);
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ radarrRoutes.get('/', (_req, res) => {
|
||||
res.status(200).json(settings.radarr);
|
||||
});
|
||||
|
||||
radarrRoutes.post('/', async (req, res) => {
|
||||
radarrRoutes.post('/', (req, res) => {
|
||||
const settings = getSettings();
|
||||
|
||||
const newRadarr = req.body as RadarrSettings;
|
||||
@@ -31,7 +31,7 @@ radarrRoutes.post('/', async (req, res) => {
|
||||
}
|
||||
|
||||
settings.radarr = [...settings.radarr, newRadarr];
|
||||
await settings.save();
|
||||
settings.save();
|
||||
|
||||
return res.status(201).json(newRadarr);
|
||||
});
|
||||
@@ -76,7 +76,7 @@ radarrRoutes.post<
|
||||
|
||||
radarrRoutes.put<{ id: string }, RadarrSettings, RadarrSettings>(
|
||||
'/:id',
|
||||
async (req, res, next) => {
|
||||
(req, res, next) => {
|
||||
const settings = getSettings();
|
||||
|
||||
const radarrIndex = settings.radarr.findIndex(
|
||||
@@ -102,7 +102,7 @@ radarrRoutes.put<{ id: string }, RadarrSettings, RadarrSettings>(
|
||||
...req.body,
|
||||
id: Number(req.params.id),
|
||||
} as RadarrSettings;
|
||||
await settings.save();
|
||||
settings.save();
|
||||
|
||||
return res.status(200).json(settings.radarr[radarrIndex]);
|
||||
}
|
||||
@@ -134,7 +134,7 @@ radarrRoutes.get<{ id: string }>('/:id/profiles', async (req, res, next) => {
|
||||
);
|
||||
});
|
||||
|
||||
radarrRoutes.delete<{ id: string }>('/:id', async (req, res, next) => {
|
||||
radarrRoutes.delete<{ id: string }>('/:id', (req, res, next) => {
|
||||
const settings = getSettings();
|
||||
|
||||
const radarrIndex = settings.radarr.findIndex(
|
||||
@@ -146,7 +146,7 @@ radarrRoutes.delete<{ id: string }>('/:id', async (req, res, next) => {
|
||||
}
|
||||
|
||||
const removed = settings.radarr.splice(radarrIndex, 1);
|
||||
await settings.save();
|
||||
settings.save();
|
||||
|
||||
return res.status(200).json(removed[0]);
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ sonarrRoutes.get('/', (_req, res) => {
|
||||
res.status(200).json(settings.sonarr);
|
||||
});
|
||||
|
||||
sonarrRoutes.post('/', async (req, res) => {
|
||||
sonarrRoutes.post('/', (req, res) => {
|
||||
const settings = getSettings();
|
||||
|
||||
const newSonarr = req.body as SonarrSettings;
|
||||
@@ -31,7 +31,7 @@ sonarrRoutes.post('/', async (req, res) => {
|
||||
}
|
||||
|
||||
settings.sonarr = [...settings.sonarr, newSonarr];
|
||||
await settings.save();
|
||||
settings.save();
|
||||
|
||||
return res.status(201).json(newSonarr);
|
||||
});
|
||||
@@ -43,14 +43,13 @@ sonarrRoutes.post('/test', async (req, res, next) => {
|
||||
url: SonarrAPI.buildUrl(req.body, '/api/v3'),
|
||||
});
|
||||
|
||||
const systemStatus = await sonarr.getSystemStatus();
|
||||
const sonarrMajorVersion = Number(systemStatus.version.split('.')[0]);
|
||||
|
||||
const urlBase = systemStatus.urlBase;
|
||||
const urlBase = await sonarr
|
||||
.getSystemStatus()
|
||||
.then((value) => value.urlBase)
|
||||
.catch(() => req.body.baseUrl);
|
||||
const profiles = await sonarr.getProfiles();
|
||||
const folders = await sonarr.getRootFolders();
|
||||
const languageProfiles =
|
||||
sonarrMajorVersion <= 3 ? await sonarr.getLanguageProfiles() : null;
|
||||
const languageProfiles = await sonarr.getLanguageProfiles();
|
||||
const tags = await sonarr.getTags();
|
||||
|
||||
return res.status(200).json({
|
||||
@@ -73,7 +72,7 @@ sonarrRoutes.post('/test', async (req, res, next) => {
|
||||
}
|
||||
});
|
||||
|
||||
sonarrRoutes.put<{ id: string }>('/:id', async (req, res) => {
|
||||
sonarrRoutes.put<{ id: string }>('/:id', (req, res) => {
|
||||
const settings = getSettings();
|
||||
|
||||
const sonarrIndex = settings.sonarr.findIndex(
|
||||
@@ -101,12 +100,12 @@ sonarrRoutes.put<{ id: string }>('/:id', async (req, res) => {
|
||||
...req.body,
|
||||
id: Number(req.params.id),
|
||||
} as SonarrSettings;
|
||||
await settings.save();
|
||||
settings.save();
|
||||
|
||||
return res.status(200).json(settings.sonarr[sonarrIndex]);
|
||||
});
|
||||
|
||||
sonarrRoutes.delete<{ id: string }>('/:id', async (req, res) => {
|
||||
sonarrRoutes.delete<{ id: string }>('/:id', (req, res) => {
|
||||
const settings = getSettings();
|
||||
|
||||
const sonarrIndex = settings.sonarr.findIndex(
|
||||
@@ -120,7 +119,7 @@ sonarrRoutes.delete<{ id: string }>('/:id', async (req, res) => {
|
||||
}
|
||||
|
||||
const removed = settings.sonarr.splice(sonarrIndex, 1);
|
||||
await settings.save();
|
||||
settings.save();
|
||||
|
||||
return res.status(200).json(removed[0]);
|
||||
});
|
||||
|
||||
@@ -539,7 +539,12 @@ router.post(
|
||||
).toString('base64'),
|
||||
email: jellyfinUser?.Name,
|
||||
permissions: settings.main.defaultPermissions,
|
||||
avatar: `/avatarproxy/${jellyfinUser?.Id}`,
|
||||
avatar: jellyfinUser?.PrimaryImageTag
|
||||
? `/Users/${jellyfinUser.Id}/Images/Primary/?tag=${jellyfinUser.PrimaryImageTag}&quality=90`
|
||||
: gravatarUrl(jellyfinUser?.Name ?? '', {
|
||||
default: 'mm',
|
||||
size: 200,
|
||||
}),
|
||||
userType:
|
||||
settings.main.mediaServerType === MediaServerType.JELLYFIN
|
||||
? UserType.JELLYFIN
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { accessSync, existsSync } from 'fs';
|
||||
import { existsSync } from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const CONFIG_PATH = process.env.CONFIG_DIRECTORY
|
||||
@@ -14,12 +14,3 @@ export const appDataStatus = (): boolean => {
|
||||
export const appDataPath = (): string => {
|
||||
return CONFIG_PATH;
|
||||
};
|
||||
|
||||
export const appDataPermissions = (): boolean => {
|
||||
try {
|
||||
accessSync(CONFIG_PATH);
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
import type { ProxySettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import type { Dispatcher } from 'undici';
|
||||
import { Agent, ProxyAgent, setGlobalDispatcher } from 'undici';
|
||||
|
||||
export default async function createCustomProxyAgent(
|
||||
proxySettings: ProxySettings
|
||||
) {
|
||||
const defaultAgent = new Agent();
|
||||
|
||||
const skipUrl = (url: string) => {
|
||||
const hostname = new URL(url).hostname;
|
||||
|
||||
if (proxySettings.bypassLocalAddresses && isLocalAddress(hostname)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const address of proxySettings.bypassFilter.split(',')) {
|
||||
const trimmedAddress = address.trim();
|
||||
if (!trimmedAddress) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (trimmedAddress.startsWith('*')) {
|
||||
const domain = trimmedAddress.slice(1);
|
||||
if (hostname.endsWith(domain)) {
|
||||
return true;
|
||||
}
|
||||
} else if (hostname === trimmedAddress) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const noProxyInterceptor = (
|
||||
dispatch: Dispatcher['dispatch']
|
||||
): Dispatcher['dispatch'] => {
|
||||
return (opts, handler) => {
|
||||
const url = opts.origin?.toString();
|
||||
return url && skipUrl(url)
|
||||
? defaultAgent.dispatch(opts, handler)
|
||||
: dispatch(opts, handler);
|
||||
};
|
||||
};
|
||||
|
||||
const token =
|
||||
proxySettings.user && proxySettings.password
|
||||
? `Basic ${Buffer.from(
|
||||
`${proxySettings.user}:${proxySettings.password}`
|
||||
).toString('base64')}`
|
||||
: undefined;
|
||||
|
||||
try {
|
||||
const proxyAgent = new ProxyAgent({
|
||||
uri:
|
||||
(proxySettings.useSsl ? 'https://' : 'http://') +
|
||||
proxySettings.hostname +
|
||||
':' +
|
||||
proxySettings.port,
|
||||
token,
|
||||
interceptors: {
|
||||
Client: [noProxyInterceptor],
|
||||
},
|
||||
});
|
||||
|
||||
setGlobalDispatcher(proxyAgent);
|
||||
} catch (e) {
|
||||
logger.error('Failed to connect to the proxy: ' + e.message, {
|
||||
label: 'Proxy',
|
||||
});
|
||||
setGlobalDispatcher(defaultAgent);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch('https://www.google.com', { method: 'HEAD' });
|
||||
if (res.ok) {
|
||||
logger.debug('HTTP(S) proxy connected successfully', { label: 'Proxy' });
|
||||
} else {
|
||||
logger.error('Proxy responded, but with a non-OK status: ' + res.status, {
|
||||
label: 'Proxy',
|
||||
});
|
||||
setGlobalDispatcher(defaultAgent);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
'Failed to connect to the proxy: ' + e.message + ': ' + e.cause,
|
||||
{ label: 'Proxy' }
|
||||
);
|
||||
setGlobalDispatcher(defaultAgent);
|
||||
}
|
||||
}
|
||||
|
||||
function isLocalAddress(hostname: string) {
|
||||
if (hostname === 'localhost' || hostname === '127.0.0.1') {
|
||||
return true;
|
||||
}
|
||||
|
||||
const privateIpRanges = [
|
||||
/^10\./, // 10.x.x.x
|
||||
/^172\.(1[6-9]|2[0-9]|3[0-1])\./, // 172.16.x.x - 172.31.x.x
|
||||
/^192\.168\./, // 192.168.x.x
|
||||
];
|
||||
if (privateIpRanges.some((regex) => regex.test(hostname))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -13,8 +13,7 @@ class RestartFlag {
|
||||
|
||||
return (
|
||||
this.settings.csrfProtection !== settings.csrfProtection ||
|
||||
this.settings.trustProxy !== settings.trustProxy ||
|
||||
this.settings.proxy.enabled !== settings.proxy.enabled
|
||||
this.settings.trustProxy !== settings.trustProxy
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import useDebouncedState from '@app/hooks/useDebouncedState';
|
||||
import { useUpdateQueryParams } from '@app/hooks/useUpdateQueryParams';
|
||||
import { Permission, useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import Error from '@app/pages/_error';
|
||||
import ErrorPage from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
@@ -75,7 +75,7 @@ const Blacklist = () => {
|
||||
// check if there's no data and no errors in the table
|
||||
// so as to show a spinner inside the table and not refresh the whole component
|
||||
if (!data && error) {
|
||||
return <Error statusCode={500} />;
|
||||
return <ErrorPage statusCode={500} />;
|
||||
}
|
||||
|
||||
const searchItem = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
|
||||
@@ -9,7 +9,7 @@ import TitleCard from '@app/components/TitleCard';
|
||||
import useSettings from '@app/hooks/useSettings';
|
||||
import { Permission, useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import Error from '@app/pages/_error';
|
||||
import ErrorPage from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { refreshIntervalHelper } from '@app/utils/refreshIntervalHelper';
|
||||
import { ArrowDownTrayIcon } from '@heroicons/react/24/outline';
|
||||
@@ -91,7 +91,7 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => {
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return <Error statusCode={404} />;
|
||||
return <ErrorPage statusCode={404} />;
|
||||
}
|
||||
|
||||
let collectionStatus = MediaStatus.UNKNOWN;
|
||||
|
||||
@@ -25,8 +25,11 @@ const CachedImage = ({ src, type, ...props }: CachedImageProps) => {
|
||||
? src.replace(/^https:\/\/image\.tmdb\.org\//, '/imageproxy/')
|
||||
: src;
|
||||
} else if (type === 'avatar') {
|
||||
// jellyfin avatar (if any)
|
||||
imageUrl = src;
|
||||
// jellyfin avatar (in any)
|
||||
const jellyfinAvatar = src.match(
|
||||
/(\/Users\/\w+\/Images\/Primary\/?\?tag=\w+&quality=90)$/
|
||||
)?.[1];
|
||||
imageUrl = jellyfinAvatar ? `/avatarproxy` + jellyfinAvatar : src;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import ListView from '@app/components/Common/ListView';
|
||||
import PageTitle from '@app/components/Common/PageTitle';
|
||||
import useDiscover from '@app/hooks/useDiscover';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import Error from '@app/pages/_error';
|
||||
import ErrorPage from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type { MovieResult } from '@server/models/Search';
|
||||
import { useRouter } from 'next/router';
|
||||
@@ -31,7 +31,7 @@ const DiscoverMovieGenre = () => {
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return <Error statusCode={500} />;
|
||||
return <ErrorPage statusCode={500} />;
|
||||
}
|
||||
|
||||
const title = isLoadingInitialData
|
||||
|
||||
@@ -3,7 +3,7 @@ import ListView from '@app/components/Common/ListView';
|
||||
import PageTitle from '@app/components/Common/PageTitle';
|
||||
import useDiscover, { encodeURIExtraParams } from '@app/hooks/useDiscover';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import Error from '@app/pages/_error';
|
||||
import ErrorPage from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type { TmdbKeyword } from '@server/api/themoviedb/interfaces';
|
||||
import type { MovieResult } from '@server/models/Search';
|
||||
@@ -35,7 +35,7 @@ const DiscoverMovieKeyword = () => {
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return <Error statusCode={500} />;
|
||||
return <ErrorPage statusCode={500} />;
|
||||
}
|
||||
|
||||
const title = isLoadingInitialData
|
||||
|
||||
@@ -3,7 +3,7 @@ import ListView from '@app/components/Common/ListView';
|
||||
import PageTitle from '@app/components/Common/PageTitle';
|
||||
import useDiscover from '@app/hooks/useDiscover';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import Error from '@app/pages/_error';
|
||||
import ErrorPage from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type { MovieResult } from '@server/models/Search';
|
||||
import { useRouter } from 'next/router';
|
||||
@@ -37,7 +37,7 @@ const DiscoverMovieLanguage = () => {
|
||||
>(`/api/v1/discover/movies/language/${router.query.language}`);
|
||||
|
||||
if (error) {
|
||||
return <Error statusCode={500} />;
|
||||
return <ErrorPage statusCode={500} />;
|
||||
}
|
||||
|
||||
const title = isLoadingInitialData
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import FilterSlideover from '@app/components/Discover/FilterSlideover';
|
||||
import useDiscover from '@app/hooks/useDiscover';
|
||||
import { useUpdateQueryParams } from '@app/hooks/useUpdateQueryParams';
|
||||
import Error from '@app/pages/_error';
|
||||
import ErrorPage from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { BarsArrowDownIcon, FunnelIcon } from '@heroicons/react/24/solid';
|
||||
import type { SortOptions as TMDBSortOptions } from '@server/api/themoviedb';
|
||||
@@ -66,7 +66,7 @@ const DiscoverMovies = () => {
|
||||
const [showFilters, setShowFilters] = useState(false);
|
||||
|
||||
if (error) {
|
||||
return <Error statusCode={500} />;
|
||||
return <ErrorPage statusCode={500} />;
|
||||
}
|
||||
|
||||
const title = intl.formatMessage(messages.discovermovies);
|
||||
|
||||
@@ -3,7 +3,7 @@ import ListView from '@app/components/Common/ListView';
|
||||
import PageTitle from '@app/components/Common/PageTitle';
|
||||
import useDiscover from '@app/hooks/useDiscover';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import Error from '@app/pages/_error';
|
||||
import ErrorPage from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type { TvNetwork } from '@server/models/common';
|
||||
import type { TvResult } from '@server/models/Search';
|
||||
@@ -33,7 +33,7 @@ const DiscoverTvNetwork = () => {
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return <Error statusCode={500} />;
|
||||
return <ErrorPage statusCode={500} />;
|
||||
}
|
||||
|
||||
const title = isLoadingInitialData
|
||||
|
||||
@@ -3,7 +3,7 @@ import ListView from '@app/components/Common/ListView';
|
||||
import PageTitle from '@app/components/Common/PageTitle';
|
||||
import useDiscover from '@app/hooks/useDiscover';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import Error from '@app/pages/_error';
|
||||
import ErrorPage from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type { ProductionCompany } from '@server/models/common';
|
||||
import type { MovieResult } from '@server/models/Search';
|
||||
@@ -33,7 +33,7 @@ const DiscoverMovieStudio = () => {
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return <Error statusCode={500} />;
|
||||
return <ErrorPage statusCode={500} />;
|
||||
}
|
||||
|
||||
const title = isLoadingInitialData
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import FilterSlideover from '@app/components/Discover/FilterSlideover';
|
||||
import useDiscover from '@app/hooks/useDiscover';
|
||||
import { useUpdateQueryParams } from '@app/hooks/useUpdateQueryParams';
|
||||
import Error from '@app/pages/_error';
|
||||
import ErrorPage from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { BarsArrowDownIcon, FunnelIcon } from '@heroicons/react/24/solid';
|
||||
import type { SortOptions as TMDBSortOptions } from '@server/api/themoviedb';
|
||||
@@ -64,7 +64,7 @@ const DiscoverTv = () => {
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return <Error statusCode={500} />;
|
||||
return <ErrorPage statusCode={500} />;
|
||||
}
|
||||
|
||||
const title = intl.formatMessage(messages.discovertv);
|
||||
|
||||
@@ -3,7 +3,7 @@ import ListView from '@app/components/Common/ListView';
|
||||
import PageTitle from '@app/components/Common/PageTitle';
|
||||
import useDiscover from '@app/hooks/useDiscover';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import Error from '@app/pages/_error';
|
||||
import ErrorPage from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type { TvResult } from '@server/models/Search';
|
||||
import { useRouter } from 'next/router';
|
||||
@@ -31,7 +31,7 @@ const DiscoverTvGenre = () => {
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return <Error statusCode={500} />;
|
||||
return <ErrorPage statusCode={500} />;
|
||||
}
|
||||
|
||||
const title = isLoadingInitialData
|
||||
|
||||
@@ -3,7 +3,7 @@ import ListView from '@app/components/Common/ListView';
|
||||
import PageTitle from '@app/components/Common/PageTitle';
|
||||
import useDiscover, { encodeURIExtraParams } from '@app/hooks/useDiscover';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import Error from '@app/pages/_error';
|
||||
import ErrorPage from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type { TmdbKeyword } from '@server/api/themoviedb/interfaces';
|
||||
import type { TvResult } from '@server/models/Search';
|
||||
@@ -35,7 +35,7 @@ const DiscoverTvKeyword = () => {
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return <Error statusCode={500} />;
|
||||
return <ErrorPage statusCode={500} />;
|
||||
}
|
||||
|
||||
const title = isLoadingInitialData
|
||||
|
||||
@@ -3,7 +3,7 @@ import ListView from '@app/components/Common/ListView';
|
||||
import PageTitle from '@app/components/Common/PageTitle';
|
||||
import useDiscover from '@app/hooks/useDiscover';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import Error from '@app/pages/_error';
|
||||
import ErrorPage from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type { TvResult } from '@server/models/Search';
|
||||
import { useRouter } from 'next/router';
|
||||
@@ -37,7 +37,7 @@ const DiscoverTvLanguage = () => {
|
||||
>(`/api/v1/discover/tv/language/${router.query.language}`);
|
||||
|
||||
if (error) {
|
||||
return <Error statusCode={500} />;
|
||||
return <ErrorPage statusCode={500} />;
|
||||
}
|
||||
|
||||
const title = isLoadingInitialData
|
||||
|
||||
@@ -2,7 +2,7 @@ import Header from '@app/components/Common/Header';
|
||||
import ListView from '@app/components/Common/ListView';
|
||||
import PageTitle from '@app/components/Common/PageTitle';
|
||||
import useDiscover from '@app/hooks/useDiscover';
|
||||
import Error from '@app/pages/_error';
|
||||
import ErrorPage from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type { TvResult } from '@server/models/Search';
|
||||
import { useIntl } from 'react-intl';
|
||||
@@ -23,7 +23,7 @@ const DiscoverTvUpcoming = () => {
|
||||
} = useDiscover<TvResult>('/api/v1/discover/tv/upcoming');
|
||||
|
||||
if (error) {
|
||||
return <Error statusCode={500} />;
|
||||
return <ErrorPage statusCode={500} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,7 +3,7 @@ import ListView from '@app/components/Common/ListView';
|
||||
import PageTitle from '@app/components/Common/PageTitle';
|
||||
import useDiscover from '@app/hooks/useDiscover';
|
||||
import { useUser } from '@app/hooks/useUser';
|
||||
import Error from '@app/pages/_error';
|
||||
import ErrorPage from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type { WatchlistItem } from '@server/interfaces/api/discoverInterfaces';
|
||||
import Link from 'next/link';
|
||||
@@ -43,7 +43,7 @@ const DiscoverWatchlist = () => {
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return <Error statusCode={500} />;
|
||||
return <ErrorPage statusCode={500} />;
|
||||
}
|
||||
|
||||
const title = intl.formatMessage(
|
||||
|
||||
@@ -3,7 +3,7 @@ import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
||||
import PageTitle from '@app/components/Common/PageTitle';
|
||||
import { genreColorMap } from '@app/components/Discover/constants';
|
||||
import GenreCard from '@app/components/GenreCard';
|
||||
import Error from '@app/pages/_error';
|
||||
import ErrorPage from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type { GenreSliderItem } from '@server/interfaces/api/discoverInterfaces';
|
||||
import { useIntl } from 'react-intl';
|
||||
@@ -24,7 +24,7 @@ const MovieGenreList = () => {
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return <Error statusCode={404} />;
|
||||
return <ErrorPage statusCode={404} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,7 +2,7 @@ import Header from '@app/components/Common/Header';
|
||||
import ListView from '@app/components/Common/ListView';
|
||||
import PageTitle from '@app/components/Common/PageTitle';
|
||||
import useDiscover from '@app/hooks/useDiscover';
|
||||
import Error from '@app/pages/_error';
|
||||
import ErrorPage from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type {
|
||||
MovieResult,
|
||||
@@ -30,7 +30,7 @@ const Trending = () => {
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return <Error statusCode={500} />;
|
||||
return <ErrorPage statusCode={500} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,7 +3,7 @@ import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
||||
import PageTitle from '@app/components/Common/PageTitle';
|
||||
import { genreColorMap } from '@app/components/Discover/constants';
|
||||
import GenreCard from '@app/components/GenreCard';
|
||||
import Error from '@app/pages/_error';
|
||||
import ErrorPage from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type { GenreSliderItem } from '@server/interfaces/api/discoverInterfaces';
|
||||
import { useIntl } from 'react-intl';
|
||||
@@ -24,7 +24,7 @@ const TvGenreList = () => {
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return <Error statusCode={404} />;
|
||||
return <ErrorPage statusCode={404} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,7 +2,7 @@ import Header from '@app/components/Common/Header';
|
||||
import ListView from '@app/components/Common/ListView';
|
||||
import PageTitle from '@app/components/Common/PageTitle';
|
||||
import useDiscover from '@app/hooks/useDiscover';
|
||||
import Error from '@app/pages/_error';
|
||||
import ErrorPage from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type { MovieResult } from '@server/models/Search';
|
||||
import { useIntl } from 'react-intl';
|
||||
@@ -25,7 +25,7 @@ const UpcomingMovies = () => {
|
||||
} = useDiscover<MovieResult>('/api/v1/discover/movies/upcoming');
|
||||
|
||||
if (error) {
|
||||
return <Error statusCode={500} />;
|
||||
return <ErrorPage statusCode={500} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,7 +2,7 @@ import Header from '@app/components/Common/Header';
|
||||
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
||||
import PageTitle from '@app/components/Common/PageTitle';
|
||||
import PersonCard from '@app/components/PersonCard';
|
||||
import Error from '@app/pages/_error';
|
||||
import ErrorPage from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type { MovieDetails } from '@server/models/Movie';
|
||||
import Link from 'next/link';
|
||||
@@ -26,7 +26,7 @@ const MovieCast = () => {
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return <Error statusCode={404} />;
|
||||
return <ErrorPage statusCode={404} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,7 +2,7 @@ import Header from '@app/components/Common/Header';
|
||||
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
||||
import PageTitle from '@app/components/Common/PageTitle';
|
||||
import PersonCard from '@app/components/PersonCard';
|
||||
import Error from '@app/pages/_error';
|
||||
import ErrorPage from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type { MovieDetails } from '@server/models/Movie';
|
||||
import Link from 'next/link';
|
||||
@@ -26,7 +26,7 @@ const MovieCrew = () => {
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return <Error statusCode={404} />;
|
||||
return <ErrorPage statusCode={404} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,7 +2,7 @@ import Header from '@app/components/Common/Header';
|
||||
import ListView from '@app/components/Common/ListView';
|
||||
import PageTitle from '@app/components/Common/PageTitle';
|
||||
import useDiscover from '@app/hooks/useDiscover';
|
||||
import Error from '@app/pages/_error';
|
||||
import ErrorPage from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type { MovieDetails } from '@server/models/Movie';
|
||||
import type { MovieResult } from '@server/models/Search';
|
||||
@@ -34,7 +34,7 @@ const MovieRecommendations = () => {
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return <Error statusCode={500} />;
|
||||
return <ErrorPage statusCode={500} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,7 +2,7 @@ import Header from '@app/components/Common/Header';
|
||||
import ListView from '@app/components/Common/ListView';
|
||||
import PageTitle from '@app/components/Common/PageTitle';
|
||||
import useDiscover from '@app/hooks/useDiscover';
|
||||
import Error from '@app/pages/_error';
|
||||
import ErrorPage from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type { MovieDetails } from '@server/models/Movie';
|
||||
import type { MovieResult } from '@server/models/Search';
|
||||
@@ -32,7 +32,7 @@ const MovieSimilar = () => {
|
||||
} = useDiscover<MovieResult>(`/api/v1/movie/${router.query.movieId}/similar`);
|
||||
|
||||
if (error) {
|
||||
return <Error statusCode={500} />;
|
||||
return <ErrorPage statusCode={500} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -5,7 +5,7 @@ import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
||||
import PageTitle from '@app/components/Common/PageTitle';
|
||||
import TitleCard from '@app/components/TitleCard';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import Error from '@app/pages/_error';
|
||||
import ErrorPage from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type { PersonCombinedCreditsResponse } from '@server/interfaces/api/personInterfaces';
|
||||
import type { PersonDetails as PersonDetailsType } from '@server/models/Person';
|
||||
@@ -79,7 +79,7 @@ const PersonDetails = () => {
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return <Error statusCode={404} />;
|
||||
return <ErrorPage statusCode={404} />;
|
||||
}
|
||||
|
||||
const personAttributes: string[] = [];
|
||||
|
||||
@@ -2,7 +2,7 @@ import Header from '@app/components/Common/Header';
|
||||
import ListView from '@app/components/Common/ListView';
|
||||
import PageTitle from '@app/components/Common/PageTitle';
|
||||
import useDiscover from '@app/hooks/useDiscover';
|
||||
import Error from '@app/pages/_error';
|
||||
import ErrorPage from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type {
|
||||
MovieResult,
|
||||
@@ -38,7 +38,7 @@ const Search = () => {
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return <Error statusCode={500} />;
|
||||
return <ErrorPage statusCode={500} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -5,7 +5,7 @@ import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
||||
import PageTitle from '@app/components/Common/PageTitle';
|
||||
import Releases from '@app/components/Settings/SettingsAbout/Releases';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import Error from '@app/pages/_error';
|
||||
import ErrorPage from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { InformationCircleIcon } from '@heroicons/react/24/solid';
|
||||
import type {
|
||||
@@ -51,7 +51,7 @@ const SettingsAbout = () => {
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return <Error statusCode={500} />;
|
||||
return <ErrorPage statusCode={500} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -8,7 +8,7 @@ import Tooltip from '@app/components/Common/Tooltip';
|
||||
import useDebouncedState from '@app/hooks/useDebouncedState';
|
||||
import { useUpdateQueryParams } from '@app/hooks/useUpdateQueryParams';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import Error from '@app/pages/_error';
|
||||
import ErrorPage from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { Transition } from '@headlessui/react';
|
||||
import {
|
||||
@@ -128,7 +128,7 @@ const SettingsLogs = () => {
|
||||
// check if there's no data and no errors in the table
|
||||
// so as to show a spinner inside the table and not refresh the whole component
|
||||
if (!data && error) {
|
||||
return <Error statusCode={500} />;
|
||||
return <ErrorPage statusCode={500} />;
|
||||
}
|
||||
|
||||
const hasNextPage = data?.pageInfo.pages ?? 0 > pageIndex + 1;
|
||||
|
||||
@@ -55,17 +55,6 @@ const messages = defineMessages('components.Settings.SettingsMain', {
|
||||
validationApplicationUrlTrailingSlash: 'URL must not end in a trailing slash',
|
||||
partialRequestsEnabled: 'Allow Partial Series Requests',
|
||||
locale: 'Display Language',
|
||||
proxyEnabled: 'HTTP(S) Proxy',
|
||||
proxyHostname: 'Proxy Hostname',
|
||||
proxyPort: 'Proxy Port',
|
||||
proxySsl: 'Use SSL For Proxy',
|
||||
proxyUser: 'Proxy Username',
|
||||
proxyPassword: 'Proxy Password',
|
||||
proxyBypassFilter: 'Proxy Ignored Addresses',
|
||||
proxyBypassFilterTip:
|
||||
"Use ',' as a separator, and '*.' as a wildcard for subdomains",
|
||||
proxyBypassLocalAddresses: 'Bypass Proxy for Local Addresses',
|
||||
validationProxyPort: 'You must provide a valid port',
|
||||
});
|
||||
|
||||
const SettingsMain = () => {
|
||||
@@ -93,12 +82,6 @@ const SettingsMain = () => {
|
||||
intl.formatMessage(messages.validationApplicationUrlTrailingSlash),
|
||||
(value) => !value || !value.endsWith('/')
|
||||
),
|
||||
proxyPort: Yup.number().when('proxyEnabled', {
|
||||
is: (proxyEnabled: boolean) => proxyEnabled,
|
||||
then: Yup.number().required(
|
||||
intl.formatMessage(messages.validationProxyPort)
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
const regenerate = async () => {
|
||||
@@ -154,14 +137,6 @@ const SettingsMain = () => {
|
||||
partialRequestsEnabled: data?.partialRequestsEnabled,
|
||||
trustProxy: data?.trustProxy,
|
||||
cacheImages: data?.cacheImages,
|
||||
proxyEnabled: data?.proxy?.enabled,
|
||||
proxyHostname: data?.proxy?.hostname,
|
||||
proxyPort: data?.proxy?.port,
|
||||
proxySsl: data?.proxy?.useSsl,
|
||||
proxyUser: data?.proxy?.user,
|
||||
proxyPassword: data?.proxy?.password,
|
||||
proxyBypassFilter: data?.proxy?.bypassFilter,
|
||||
proxyBypassLocalAddresses: data?.proxy?.bypassLocalAddresses,
|
||||
}}
|
||||
enableReinitialize
|
||||
validationSchema={MainSettingsSchema}
|
||||
@@ -183,16 +158,6 @@ const SettingsMain = () => {
|
||||
partialRequestsEnabled: values.partialRequestsEnabled,
|
||||
trustProxy: values.trustProxy,
|
||||
cacheImages: values.cacheImages,
|
||||
proxy: {
|
||||
enabled: values.proxyEnabled,
|
||||
hostname: values.proxyHostname,
|
||||
port: values.proxyPort,
|
||||
useSsl: values.proxySsl,
|
||||
user: values.proxyUser,
|
||||
password: values.proxyPassword,
|
||||
bypassFilter: values.proxyBypassFilter,
|
||||
bypassLocalAddresses: values.proxyBypassLocalAddresses,
|
||||
},
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
@@ -472,176 +437,6 @@ const SettingsMain = () => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="proxyEnabled" className="checkbox-label">
|
||||
<span className="mr-2">
|
||||
{intl.formatMessage(messages.proxyEnabled)}
|
||||
</span>
|
||||
<SettingsBadge badgeType="advanced" className="mr-2" />
|
||||
<SettingsBadge badgeType="restartRequired" />
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<Field
|
||||
type="checkbox"
|
||||
id="proxyEnabled"
|
||||
name="proxyEnabled"
|
||||
onChange={() => {
|
||||
setFieldValue('proxyEnabled', !values.proxyEnabled);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{values.proxyEnabled && (
|
||||
<>
|
||||
<div className="form-row">
|
||||
<label htmlFor="proxyHostname" className="checkbox-label">
|
||||
<span className="mr-2 ml-4">
|
||||
{intl.formatMessage(messages.proxyHostname)}
|
||||
</span>
|
||||
</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">
|
||||
<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)}
|
||||
</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>
|
||||
<div className="form-row">
|
||||
<label htmlFor="proxyPassword" className="checkbox-label">
|
||||
<span className="mr-2 ml-4">
|
||||
{intl.formatMessage(messages.proxyPassword)}
|
||||
</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>
|
||||
<div className="form-row">
|
||||
<label
|
||||
htmlFor="proxyBypassFilter"
|
||||
className="checkbox-label"
|
||||
>
|
||||
<span className="mr-2 ml-4">
|
||||
{intl.formatMessage(messages.proxyBypassFilter)}
|
||||
</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>
|
||||
<div className="form-row">
|
||||
<label
|
||||
htmlFor="proxyBypassLocalAddresses"
|
||||
className="checkbox-label"
|
||||
>
|
||||
<span className="mr-2 ml-4">
|
||||
{intl.formatMessage(
|
||||
messages.proxyBypassLocalAddresses
|
||||
)}
|
||||
</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<Field
|
||||
type="checkbox"
|
||||
id="proxyBypassLocalAddresses"
|
||||
name="proxyBypassLocalAddresses"
|
||||
onChange={() => {
|
||||
setFieldValue(
|
||||
'proxyBypassLocalAddresses',
|
||||
!values.proxyBypassLocalAddresses
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="actions">
|
||||
<div className="flex justify-end">
|
||||
<span className="ml-3 inline-flex rounded-md shadow-sm">
|
||||
|
||||
@@ -86,12 +86,10 @@ interface TestResponse {
|
||||
id: number;
|
||||
path: string;
|
||||
}[];
|
||||
languageProfiles:
|
||||
| {
|
||||
id: number;
|
||||
name: string;
|
||||
}[]
|
||||
| null;
|
||||
languageProfiles: {
|
||||
id: number;
|
||||
name: string;
|
||||
}[];
|
||||
tags: {
|
||||
id: number;
|
||||
label: string;
|
||||
@@ -114,7 +112,7 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
|
||||
const [testResponse, setTestResponse] = useState<TestResponse>({
|
||||
profiles: [],
|
||||
rootFolders: [],
|
||||
languageProfiles: null,
|
||||
languageProfiles: [],
|
||||
tags: [],
|
||||
});
|
||||
const SonarrSettingsSchema = Yup.object().shape({
|
||||
@@ -139,11 +137,9 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
|
||||
activeProfileId: Yup.string().required(
|
||||
intl.formatMessage(messages.validationProfileRequired)
|
||||
),
|
||||
activeLanguageProfileId: testResponse.languageProfiles
|
||||
? Yup.number().required(
|
||||
intl.formatMessage(messages.validationLanguageProfileRequired)
|
||||
)
|
||||
: Yup.number(),
|
||||
activeLanguageProfileId: Yup.number().required(
|
||||
intl.formatMessage(messages.validationLanguageProfileRequired)
|
||||
),
|
||||
externalUrl: Yup.string()
|
||||
.url(intl.formatMessage(messages.validationApplicationUrl))
|
||||
.test(
|
||||
@@ -662,56 +658,54 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{testResponse.languageProfiles && (
|
||||
<div className="form-row">
|
||||
<label
|
||||
htmlFor="activeLanguageProfileId"
|
||||
className="text-label"
|
||||
>
|
||||
{intl.formatMessage(messages.languageprofile)}
|
||||
<span className="label-required">*</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<Field
|
||||
as="select"
|
||||
id="activeLanguageProfileId"
|
||||
name="activeLanguageProfileId"
|
||||
disabled={!isValidated || isTesting}
|
||||
>
|
||||
<option value="">
|
||||
{isTesting
|
||||
? intl.formatMessage(
|
||||
messages.loadinglanguageprofiles
|
||||
)
|
||||
: !isValidated
|
||||
? intl.formatMessage(
|
||||
messages.testFirstLanguageProfiles
|
||||
)
|
||||
: intl.formatMessage(
|
||||
messages.selectLanguageProfile
|
||||
)}
|
||||
</option>
|
||||
{testResponse.languageProfiles.length > 0 &&
|
||||
testResponse.languageProfiles.map((language) => (
|
||||
<option
|
||||
key={`loaded-profile-${language.id}`}
|
||||
value={language.id}
|
||||
>
|
||||
{language.name}
|
||||
</option>
|
||||
))}
|
||||
</Field>
|
||||
</div>
|
||||
{errors.activeLanguageProfileId &&
|
||||
touched.activeLanguageProfileId && (
|
||||
<div className="error">
|
||||
{errors.activeLanguageProfileId}
|
||||
</div>
|
||||
)}
|
||||
<div className="form-row">
|
||||
<label
|
||||
htmlFor="activeLanguageProfileId"
|
||||
className="text-label"
|
||||
>
|
||||
{intl.formatMessage(messages.languageprofile)}
|
||||
<span className="label-required">*</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<Field
|
||||
as="select"
|
||||
id="activeLanguageProfileId"
|
||||
name="activeLanguageProfileId"
|
||||
disabled={!isValidated || isTesting}
|
||||
>
|
||||
<option value="">
|
||||
{isTesting
|
||||
? intl.formatMessage(
|
||||
messages.loadinglanguageprofiles
|
||||
)
|
||||
: !isValidated
|
||||
? intl.formatMessage(
|
||||
messages.testFirstLanguageProfiles
|
||||
)
|
||||
: intl.formatMessage(
|
||||
messages.selectLanguageProfile
|
||||
)}
|
||||
</option>
|
||||
{testResponse.languageProfiles.length > 0 &&
|
||||
testResponse.languageProfiles.map((language) => (
|
||||
<option
|
||||
key={`loaded-profile-${language.id}`}
|
||||
value={language.id}
|
||||
>
|
||||
{language.name}
|
||||
</option>
|
||||
))}
|
||||
</Field>
|
||||
</div>
|
||||
{errors.activeLanguageProfileId &&
|
||||
touched.activeLanguageProfileId && (
|
||||
<div className="error">
|
||||
{errors.activeLanguageProfileId}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="tags" className="text-label">
|
||||
{intl.formatMessage(messages.tags)}
|
||||
@@ -869,55 +863,53 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{testResponse.languageProfiles && (
|
||||
<div className="form-row">
|
||||
<label
|
||||
htmlFor="activeAnimeLanguageProfileId"
|
||||
className="text-label"
|
||||
>
|
||||
{intl.formatMessage(messages.animelanguageprofile)}
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<Field
|
||||
as="select"
|
||||
id="activeAnimeLanguageProfileId"
|
||||
name="activeAnimeLanguageProfileId"
|
||||
disabled={!isValidated || isTesting}
|
||||
>
|
||||
<option value="">
|
||||
{isTesting
|
||||
? intl.formatMessage(
|
||||
messages.loadinglanguageprofiles
|
||||
)
|
||||
: !isValidated
|
||||
? intl.formatMessage(
|
||||
messages.testFirstLanguageProfiles
|
||||
)
|
||||
: intl.formatMessage(
|
||||
messages.selectLanguageProfile
|
||||
)}
|
||||
</option>
|
||||
{testResponse.languageProfiles.length > 0 &&
|
||||
testResponse.languageProfiles.map((language) => (
|
||||
<option
|
||||
key={`loaded-profile-${language.id}`}
|
||||
value={language.id}
|
||||
>
|
||||
{language.name}
|
||||
</option>
|
||||
))}
|
||||
</Field>
|
||||
</div>
|
||||
{errors.activeAnimeLanguageProfileId &&
|
||||
touched.activeAnimeLanguageProfileId && (
|
||||
<div className="error">
|
||||
{errors.activeAnimeLanguageProfileId}
|
||||
</div>
|
||||
)}
|
||||
<div className="form-row">
|
||||
<label
|
||||
htmlFor="activeAnimeLanguageProfileId"
|
||||
className="text-label"
|
||||
>
|
||||
{intl.formatMessage(messages.animelanguageprofile)}
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<Field
|
||||
as="select"
|
||||
id="activeAnimeLanguageProfileId"
|
||||
name="activeAnimeLanguageProfileId"
|
||||
disabled={!isValidated || isTesting}
|
||||
>
|
||||
<option value="">
|
||||
{isTesting
|
||||
? intl.formatMessage(
|
||||
messages.loadinglanguageprofiles
|
||||
)
|
||||
: !isValidated
|
||||
? intl.formatMessage(
|
||||
messages.testFirstLanguageProfiles
|
||||
)
|
||||
: intl.formatMessage(
|
||||
messages.selectLanguageProfile
|
||||
)}
|
||||
</option>
|
||||
{testResponse.languageProfiles.length > 0 &&
|
||||
testResponse.languageProfiles.map((language) => (
|
||||
<option
|
||||
key={`loaded-profile-${language.id}`}
|
||||
value={language.id}
|
||||
>
|
||||
{language.name}
|
||||
</option>
|
||||
))}
|
||||
</Field>
|
||||
</div>
|
||||
{errors.activeAnimeLanguageProfileId &&
|
||||
touched.activeAnimeLanguageProfileId && (
|
||||
<div className="error">
|
||||
{errors.activeAnimeLanguageProfileId}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="tags" className="text-label">
|
||||
{intl.formatMessage(messages.animeTags)}
|
||||
|
||||
@@ -2,7 +2,7 @@ import Header from '@app/components/Common/Header';
|
||||
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
||||
import PageTitle from '@app/components/Common/PageTitle';
|
||||
import PersonCard from '@app/components/PersonCard';
|
||||
import Error from '@app/pages/_error';
|
||||
import ErrorPage from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type { TvDetails } from '@server/models/Tv';
|
||||
import Link from 'next/link';
|
||||
@@ -24,7 +24,7 @@ const TvCast = () => {
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return <Error statusCode={404} />;
|
||||
return <ErrorPage statusCode={404} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,7 +2,7 @@ import Header from '@app/components/Common/Header';
|
||||
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
||||
import PageTitle from '@app/components/Common/PageTitle';
|
||||
import PersonCard from '@app/components/PersonCard';
|
||||
import Error from '@app/pages/_error';
|
||||
import ErrorPage from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type { TvDetails } from '@server/models/Tv';
|
||||
import Link from 'next/link';
|
||||
@@ -24,7 +24,7 @@ const TvCrew = () => {
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return <Error statusCode={404} />;
|
||||
return <ErrorPage statusCode={404} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,7 +2,7 @@ import Header from '@app/components/Common/Header';
|
||||
import ListView from '@app/components/Common/ListView';
|
||||
import PageTitle from '@app/components/Common/PageTitle';
|
||||
import useDiscover from '@app/hooks/useDiscover';
|
||||
import Error from '@app/pages/_error';
|
||||
import ErrorPage from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type { TvResult } from '@server/models/Search';
|
||||
import type { TvDetails } from '@server/models/Tv';
|
||||
@@ -30,7 +30,7 @@ const TvRecommendations = () => {
|
||||
} = useDiscover<TvResult>(`/api/v1/tv/${router.query.tvId}/recommendations`);
|
||||
|
||||
if (error) {
|
||||
return <Error statusCode={500} />;
|
||||
return <ErrorPage statusCode={500} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,7 +2,7 @@ import Header from '@app/components/Common/Header';
|
||||
import ListView from '@app/components/Common/ListView';
|
||||
import PageTitle from '@app/components/Common/PageTitle';
|
||||
import useDiscover from '@app/hooks/useDiscover';
|
||||
import Error from '@app/pages/_error';
|
||||
import ErrorPage from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type { TvResult } from '@server/models/Search';
|
||||
import type { TvDetails } from '@server/models/Tv';
|
||||
@@ -30,7 +30,7 @@ const TvSimilar = () => {
|
||||
} = useDiscover<TvResult>(`/api/v1/tv/${router.query.tvId}/similar`);
|
||||
|
||||
if (error) {
|
||||
return <Error statusCode={500} />;
|
||||
return <ErrorPage statusCode={500} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -30,7 +30,7 @@ import useLocale from '@app/hooks/useLocale';
|
||||
import useSettings from '@app/hooks/useSettings';
|
||||
import { Permission, useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import Error from '@app/pages/_error';
|
||||
import ErrorPage from '@app/pages/_error';
|
||||
import { sortCrewPriority } from '@app/utils/creditHelpers';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { refreshIntervalHelper } from '@app/utils/refreshIntervalHelper';
|
||||
@@ -177,7 +177,7 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return <Error statusCode={404} />;
|
||||
return <ErrorPage statusCode={404} />;
|
||||
}
|
||||
|
||||
const mediaLinks: PlayButtonLink[] = [];
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { SettingsRoute } from '@app/components/Common/SettingsTabs';
|
||||
import SettingsTabs from '@app/components/Common/SettingsTabs';
|
||||
import { useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import Error from '@app/pages/_error';
|
||||
import ErrorPage from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { CloudIcon, EnvelopeIcon } from '@heroicons/react/24/solid';
|
||||
import type { UserSettingsNotificationsResponse } from '@server/interfaces/api/userSettingsInterfaces';
|
||||
@@ -124,7 +124,7 @@ const UserNotificationSettings = ({
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return <Error statusCode={500} />;
|
||||
return <ErrorPage statusCode={500} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -7,7 +7,7 @@ import ProfileHeader from '@app/components/UserProfile/ProfileHeader';
|
||||
import useSettings from '@app/hooks/useSettings';
|
||||
import { useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import Error from '@app/pages/_error';
|
||||
import ErrorPage from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type { UserSettingsNotificationsResponse } from '@server/interfaces/api/userSettingsInterfaces';
|
||||
import { hasPermission, Permission } from '@server/lib/permissions';
|
||||
@@ -43,7 +43,7 @@ const UserSettings = ({ children }: UserSettingsProps) => {
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return <Error statusCode={500} />;
|
||||
return <ErrorPage statusCode={500} />;
|
||||
}
|
||||
|
||||
const settingsRoutes: SettingsRoute[] = [
|
||||
|
||||
@@ -7,7 +7,7 @@ import Slider from '@app/components/Slider';
|
||||
import TmdbTitleCard from '@app/components/TitleCard/TmdbTitleCard';
|
||||
import ProfileHeader from '@app/components/UserProfile/ProfileHeader';
|
||||
import { Permission, UserType, useUser } from '@app/hooks/useUser';
|
||||
import Error from '@app/pages/_error';
|
||||
import ErrorPage from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { ArrowRightCircleIcon } from '@heroicons/react/24/outline';
|
||||
import type { WatchlistResponse } from '@server/interfaces/api/discoverInterfaces';
|
||||
@@ -116,7 +116,7 @@ const UserProfile = () => {
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return <Error statusCode={404} />;
|
||||
return <ErrorPage statusCode={404} />;
|
||||
}
|
||||
|
||||
const watchlistSliderTitle = intl.formatMessage(
|
||||
|
||||
Reference in New Issue
Block a user