mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2025-12-24 10:49:30 -05:00
Compare commits
9 Commits
preview-pr
...
preview-fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6dc00c8732 | ||
|
|
ca838a00fa | ||
|
|
f2ed101e52 | ||
|
|
4b4eeb6ec7 | ||
|
|
d331798b28 | ||
|
|
f2b63156d1 | ||
|
|
326001c3ec | ||
|
|
0bbcfcbd5e | ||
|
|
32e0b129fe |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -34,6 +34,7 @@ yarn-error.log*
|
|||||||
# database
|
# database
|
||||||
config/db/*.sqlite3*
|
config/db/*.sqlite3*
|
||||||
config/settings.json
|
config/settings.json
|
||||||
|
config/settings.old.json
|
||||||
|
|
||||||
# logs
|
# logs
|
||||||
config/logs/*.log*
|
config/logs/*.log*
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ sidebar_position: 4
|
|||||||
|
|
||||||
# AUR (Arch User Repository)
|
# 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
|
:::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.
|
This method is not recommended for most users. It is intended for advanced users who are using Arch Linux or an Arch-based distribution.
|
||||||
:::
|
:::
|
||||||
|
|||||||
@@ -1988,6 +1988,9 @@ paths:
|
|||||||
appDataPath:
|
appDataPath:
|
||||||
type: string
|
type: string
|
||||||
example: /app/config
|
example: /app/config
|
||||||
|
appDataPermissions:
|
||||||
|
type: boolean
|
||||||
|
example: true
|
||||||
/settings/main:
|
/settings/main:
|
||||||
get:
|
get:
|
||||||
summary: Get main settings
|
summary: Get main settings
|
||||||
|
|||||||
@@ -93,7 +93,8 @@
|
|||||||
"sqlite3": "5.1.4",
|
"sqlite3": "5.1.4",
|
||||||
"swagger-ui-express": "4.6.2",
|
"swagger-ui-express": "4.6.2",
|
||||||
"swr": "2.2.5",
|
"swr": "2.2.5",
|
||||||
"typeorm": "0.3.12",
|
"typeorm": "0.3.11",
|
||||||
|
"undici": "^6.20.1",
|
||||||
"web-push": "3.5.0",
|
"web-push": "3.5.0",
|
||||||
"winston": "3.8.2",
|
"winston": "3.8.2",
|
||||||
"winston-daily-rotate-file": "4.7.1",
|
"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
|
version: 2.11.0
|
||||||
connect-typeorm:
|
connect-typeorm:
|
||||||
specifier: 1.1.4
|
specifier: 1.1.4
|
||||||
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)))
|
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)))
|
||||||
cookie-parser:
|
cookie-parser:
|
||||||
specifier: 1.4.6
|
specifier: 1.4.6
|
||||||
version: 1.4.6
|
version: 1.4.6
|
||||||
@@ -192,8 +192,11 @@ importers:
|
|||||||
specifier: 2.2.5
|
specifier: 2.2.5
|
||||||
version: 2.2.5(react@18.3.1)
|
version: 2.2.5(react@18.3.1)
|
||||||
typeorm:
|
typeorm:
|
||||||
specifier: 0.3.12
|
specifier: 0.3.11
|
||||||
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))
|
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
|
||||||
web-push:
|
web-push:
|
||||||
specifier: 3.5.0
|
specifier: 3.5.0
|
||||||
version: 3.5.0
|
version: 3.5.0
|
||||||
@@ -4264,10 +4267,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==}
|
resolution: {integrity: sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==}
|
||||||
engines: {node: '>=0.11'}
|
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:
|
dateformat@3.0.3:
|
||||||
resolution: {integrity: sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==}
|
resolution: {integrity: sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==}
|
||||||
|
|
||||||
@@ -5389,8 +5388,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
|
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
https-proxy-agent@7.0.4:
|
https-proxy-agent@7.0.5:
|
||||||
resolution: {integrity: sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==}
|
resolution: {integrity: sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==}
|
||||||
engines: {node: '>= 14'}
|
engines: {node: '>= 14'}
|
||||||
|
|
||||||
human-signals@1.1.1:
|
human-signals@1.1.1:
|
||||||
@@ -6554,11 +6553,6 @@ packages:
|
|||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
mkdirp@2.1.6:
|
|
||||||
resolution: {integrity: sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
modify-values@1.0.1:
|
modify-values@1.0.1:
|
||||||
resolution: {integrity: sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==}
|
resolution: {integrity: sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -7730,9 +7724,6 @@ packages:
|
|||||||
reflect-metadata@0.1.13:
|
reflect-metadata@0.1.13:
|
||||||
resolution: {integrity: sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==}
|
resolution: {integrity: sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==}
|
||||||
|
|
||||||
reflect-metadata@0.1.14:
|
|
||||||
resolution: {integrity: sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==}
|
|
||||||
|
|
||||||
reflect.getprototypeof@1.0.6:
|
reflect.getprototypeof@1.0.6:
|
||||||
resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==}
|
resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -8670,8 +8661,8 @@ packages:
|
|||||||
typedarray@0.0.6:
|
typedarray@0.0.6:
|
||||||
resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
|
resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
|
||||||
|
|
||||||
typeorm@0.3.12:
|
typeorm@0.3.11:
|
||||||
resolution: {integrity: sha512-sYSxBmCf1nJLLTcYtwqZ+lQIRtLPyUoO93rHTOKk9vJCyT4UfRtU7oRsJvfvKP3nnZTD1hzz2SEy2zwPEN6OyA==}
|
resolution: {integrity: sha512-pzdOyWbVuz/z8Ww6gqvBW4nylsM0KLdUCDExr2gR20/x1khGSVxQkjNV/3YqliG90jrWzrknYbYscpk8yxFJVg==}
|
||||||
engines: {node: '>= 12.9.0'}
|
engines: {node: '>= 12.9.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -8682,7 +8673,7 @@ packages:
|
|||||||
ioredis: ^5.0.4
|
ioredis: ^5.0.4
|
||||||
mongodb: ^3.6.0
|
mongodb: ^3.6.0
|
||||||
mssql: ^7.3.0
|
mssql: ^7.3.0
|
||||||
mysql2: ^2.2.5 || ^3.0.1
|
mysql2: ^2.2.5
|
||||||
oracledb: ^5.1.0
|
oracledb: ^5.1.0
|
||||||
pg: ^8.5.1
|
pg: ^8.5.1
|
||||||
pg-native: ^3.0.0
|
pg-native: ^3.0.0
|
||||||
@@ -8768,6 +8759,10 @@ packages:
|
|||||||
undici-types@5.26.5:
|
undici-types@5.26.5:
|
||||||
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
|
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:
|
unicode-canonical-property-names-ecmascript@2.0.0:
|
||||||
resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==}
|
resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -12310,7 +12305,7 @@ snapshots:
|
|||||||
fs-extra: 11.2.0
|
fs-extra: 11.2.0
|
||||||
globby: 11.1.0
|
globby: 11.1.0
|
||||||
http-proxy-agent: 7.0.2
|
http-proxy-agent: 7.0.2
|
||||||
https-proxy-agent: 7.0.4
|
https-proxy-agent: 7.0.5
|
||||||
issue-parser: 6.0.0
|
issue-parser: 6.0.0
|
||||||
lodash: 4.17.21
|
lodash: 4.17.21
|
||||||
mime: 3.0.0
|
mime: 3.0.0
|
||||||
@@ -13824,13 +13819,13 @@ snapshots:
|
|||||||
ini: 1.3.8
|
ini: 1.3.8
|
||||||
proto-list: 1.2.4
|
proto-list: 1.2.4
|
||||||
|
|
||||||
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))):
|
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))):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/debug': 0.0.31
|
'@types/debug': 0.0.31
|
||||||
'@types/express-session': 1.17.6
|
'@types/express-session': 1.17.6
|
||||||
debug: 4.3.5(supports-color@8.1.1)
|
debug: 4.3.5(supports-color@8.1.1)
|
||||||
express-session: 1.18.0
|
express-session: 1.18.0
|
||||||
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))
|
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))
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -14181,10 +14176,6 @@ snapshots:
|
|||||||
|
|
||||||
date-fns@2.29.3: {}
|
date-fns@2.29.3: {}
|
||||||
|
|
||||||
date-fns@2.30.0:
|
|
||||||
dependencies:
|
|
||||||
'@babel/runtime': 7.24.7
|
|
||||||
|
|
||||||
dateformat@3.0.3: {}
|
dateformat@3.0.3: {}
|
||||||
|
|
||||||
dayjs@1.11.11: {}
|
dayjs@1.11.11: {}
|
||||||
@@ -15739,7 +15730,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
https-proxy-agent@7.0.4:
|
https-proxy-agent@7.0.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
agent-base: 7.1.1
|
agent-base: 7.1.1
|
||||||
debug: 4.3.5(supports-color@8.1.1)
|
debug: 4.3.5(supports-color@8.1.1)
|
||||||
@@ -17149,8 +17140,6 @@ snapshots:
|
|||||||
|
|
||||||
mkdirp@1.0.4: {}
|
mkdirp@1.0.4: {}
|
||||||
|
|
||||||
mkdirp@2.1.6: {}
|
|
||||||
|
|
||||||
modify-values@1.0.1: {}
|
modify-values@1.0.1: {}
|
||||||
|
|
||||||
moment@2.30.1: {}
|
moment@2.30.1: {}
|
||||||
@@ -18372,8 +18361,6 @@ snapshots:
|
|||||||
|
|
||||||
reflect-metadata@0.1.13: {}
|
reflect-metadata@0.1.13: {}
|
||||||
|
|
||||||
reflect-metadata@0.1.14: {}
|
|
||||||
|
|
||||||
reflect.getprototypeof@1.0.6:
|
reflect.getprototypeof@1.0.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind: 1.0.7
|
call-bind: 1.0.7
|
||||||
@@ -19431,23 +19418,23 @@ snapshots:
|
|||||||
|
|
||||||
typedarray@0.0.6: {}
|
typedarray@0.0.6: {}
|
||||||
|
|
||||||
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)):
|
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)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@sqltools/formatter': 1.2.5
|
'@sqltools/formatter': 1.2.5
|
||||||
app-root-path: 3.1.0
|
app-root-path: 3.1.0
|
||||||
buffer: 6.0.3
|
buffer: 6.0.3
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
cli-highlight: 2.1.11
|
cli-highlight: 2.1.11
|
||||||
date-fns: 2.30.0
|
date-fns: 2.29.3
|
||||||
debug: 4.3.5(supports-color@8.1.1)
|
debug: 4.3.5(supports-color@8.1.1)
|
||||||
dotenv: 16.4.5
|
dotenv: 16.4.5
|
||||||
glob: 8.1.0
|
glob: 7.2.3
|
||||||
js-yaml: 4.1.0
|
js-yaml: 4.1.0
|
||||||
mkdirp: 2.1.6
|
mkdirp: 1.0.4
|
||||||
reflect-metadata: 0.1.14
|
reflect-metadata: 0.1.13
|
||||||
sha.js: 2.4.11
|
sha.js: 2.4.11
|
||||||
tslib: 2.6.3
|
tslib: 2.6.3
|
||||||
uuid: 9.0.1
|
uuid: 8.3.2
|
||||||
xml2js: 0.4.23
|
xml2js: 0.4.23
|
||||||
yargs: 17.7.2
|
yargs: 17.7.2
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
@@ -19486,6 +19473,8 @@ snapshots:
|
|||||||
|
|
||||||
undici-types@5.26.5: {}
|
undici-types@5.26.5: {}
|
||||||
|
|
||||||
|
undici@6.20.1: {}
|
||||||
|
|
||||||
unicode-canonical-property-names-ecmascript@2.0.0: {}
|
unicode-canonical-property-names-ecmascript@2.0.0: {}
|
||||||
|
|
||||||
unicode-emoji-utils@1.2.0:
|
unicode-emoji-utils@1.2.0:
|
||||||
|
|||||||
@@ -32,13 +32,27 @@ class ExternalAPI {
|
|||||||
this.fetch = fetch;
|
this.fetch = fetch;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.baseUrl = baseUrl;
|
const url = new URL(baseUrl);
|
||||||
this.params = params;
|
|
||||||
this.defaultHeaders = {
|
this.defaultHeaders = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
|
...((url.username || url.password) && {
|
||||||
|
Authorization: `Basic ${Buffer.from(
|
||||||
|
`${url.username}:${url.password}`
|
||||||
|
).toString('base64')}`,
|
||||||
|
}),
|
||||||
...options.headers,
|
...options.headers,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (url.username || url.password) {
|
||||||
|
url.username = '';
|
||||||
|
url.password = '';
|
||||||
|
baseUrl = url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.baseUrl = baseUrl;
|
||||||
|
this.params = params;
|
||||||
this.cache = options.nodeCache;
|
this.cache = options.nodeCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -410,7 +410,7 @@ class JellyfinAPI extends ExternalAPI {
|
|||||||
).AccessToken;
|
).AccessToken;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Something went wrong while creating an API key the Jellyfin server: ${e.message}`,
|
`Something went wrong while creating an API key from the Jellyfin server: ${e.message}`,
|
||||||
{ label: 'Jellyfin API' }
|
{ label: 'Jellyfin API' }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ class PlexAPI {
|
|||||||
settings.plex.libraries = [];
|
settings.plex.libraries = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
settings.save();
|
await settings.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getLibraryContents(
|
public async getLibraryContents(
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ import clearCookies from '@server/middleware/clearcookies';
|
|||||||
import routes from '@server/routes';
|
import routes from '@server/routes';
|
||||||
import avatarproxy from '@server/routes/avatarproxy';
|
import avatarproxy from '@server/routes/avatarproxy';
|
||||||
import imageproxy from '@server/routes/imageproxy';
|
import imageproxy from '@server/routes/imageproxy';
|
||||||
|
import { appDataPermissions } from '@server/utils/appDataVolume';
|
||||||
import { getAppVersion } from '@server/utils/appVersion';
|
import { getAppVersion } from '@server/utils/appVersion';
|
||||||
|
import createCustomProxyAgent from '@server/utils/customProxyAgent';
|
||||||
import restartFlag from '@server/utils/restartFlag';
|
import restartFlag from '@server/utils/restartFlag';
|
||||||
import { getClientIp } from '@supercharge/request-ip';
|
import { getClientIp } from '@supercharge/request-ip';
|
||||||
import { TypeormStore } from 'connect-typeorm/out';
|
import { TypeormStore } from 'connect-typeorm/out';
|
||||||
@@ -51,6 +53,12 @@ const dev = process.env.NODE_ENV !== 'production';
|
|||||||
const app = next({ dev });
|
const app = next({ dev });
|
||||||
const handle = app.getRequestHandler();
|
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
|
app
|
||||||
.prepare()
|
.prepare()
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
@@ -67,6 +75,11 @@ app
|
|||||||
const settings = await getSettings().load();
|
const settings = await getSettings().load();
|
||||||
restartFlag.initializeSettings(settings.main);
|
restartFlag.initializeSettings(settings.main);
|
||||||
|
|
||||||
|
// Register HTTP proxy
|
||||||
|
if (settings.main.proxy.enabled) {
|
||||||
|
await createCustomProxyAgent(settings.main.proxy);
|
||||||
|
}
|
||||||
|
|
||||||
// Migrate library types
|
// Migrate library types
|
||||||
if (
|
if (
|
||||||
settings.plex.libraries.length > 1 &&
|
settings.plex.libraries.length > 1 &&
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ class ImageProxy {
|
|||||||
private cacheVersion;
|
private cacheVersion;
|
||||||
private key;
|
private key;
|
||||||
private baseUrl;
|
private baseUrl;
|
||||||
|
private headers: HeadersInit | null = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
key: string,
|
key: string,
|
||||||
@@ -142,6 +143,7 @@ class ImageProxy {
|
|||||||
options: {
|
options: {
|
||||||
cacheVersion?: number;
|
cacheVersion?: number;
|
||||||
rateLimitOptions?: RateLimitOptions;
|
rateLimitOptions?: RateLimitOptions;
|
||||||
|
headers?: HeadersInit;
|
||||||
} = {}
|
} = {}
|
||||||
) {
|
) {
|
||||||
this.cacheVersion = options.cacheVersion ?? 1;
|
this.cacheVersion = options.cacheVersion ?? 1;
|
||||||
@@ -155,9 +157,13 @@ class ImageProxy {
|
|||||||
} else {
|
} else {
|
||||||
this.fetch = fetch;
|
this.fetch = fetch;
|
||||||
}
|
}
|
||||||
|
this.headers = options.headers || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getImage(path: string): Promise<ImageResponse> {
|
public async getImage(
|
||||||
|
path: string,
|
||||||
|
fallbackPath?: string
|
||||||
|
): Promise<ImageResponse> {
|
||||||
const cacheKey = this.getCacheKey(path);
|
const cacheKey = this.getCacheKey(path);
|
||||||
|
|
||||||
const imageResponse = await this.get(cacheKey);
|
const imageResponse = await this.get(cacheKey);
|
||||||
@@ -166,7 +172,11 @@ class ImageProxy {
|
|||||||
const newImage = await this.set(path, cacheKey);
|
const newImage = await this.set(path, cacheKey);
|
||||||
|
|
||||||
if (!newImage) {
|
if (!newImage) {
|
||||||
throw new Error('Failed to load image');
|
if (fallbackPath) {
|
||||||
|
return await this.getImage(fallbackPath);
|
||||||
|
} else {
|
||||||
|
throw new Error('Failed to load image');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return newImage;
|
return newImage;
|
||||||
@@ -247,7 +257,12 @@ class ImageProxy {
|
|||||||
: '/'
|
: '/'
|
||||||
: '') +
|
: '') +
|
||||||
(path.startsWith('/') ? path.slice(1) : path);
|
(path.startsWith('/') ? path.slice(1) : path);
|
||||||
const response = await this.fetch(href);
|
const response = await this.fetch(href, {
|
||||||
|
headers: this.headers || undefined,
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const arrayBuffer = await response.arrayBuffer();
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
const buffer = Buffer.from(arrayBuffer);
|
const buffer = Buffer.from(arrayBuffer);
|
||||||
|
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ class PlexScanner
|
|||||||
});
|
});
|
||||||
|
|
||||||
settings.plex.libraries = newLibraries;
|
settings.plex.libraries = newLibraries;
|
||||||
settings.save();
|
await settings.save();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (const library of this.libraries) {
|
for (const library of this.libraries) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { MediaServerType } from '@server/constants/server';
|
|||||||
import { Permission } from '@server/lib/permissions';
|
import { Permission } from '@server/lib/permissions';
|
||||||
import { runMigrations } from '@server/lib/settings/migrator';
|
import { runMigrations } from '@server/lib/settings/migrator';
|
||||||
import { randomUUID } from 'crypto';
|
import { randomUUID } from 'crypto';
|
||||||
import fs from 'fs';
|
import fs from 'fs/promises';
|
||||||
import { merge } from 'lodash';
|
import { merge } from 'lodash';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import webpush from 'web-push';
|
import webpush from 'web-push';
|
||||||
@@ -99,6 +99,17 @@ interface Quota {
|
|||||||
quotaDays?: number;
|
quotaDays?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ProxySettings {
|
||||||
|
enabled: boolean;
|
||||||
|
hostname: string;
|
||||||
|
port: number;
|
||||||
|
useSsl: boolean;
|
||||||
|
user: string;
|
||||||
|
password: string;
|
||||||
|
bypassFilter: string;
|
||||||
|
bypassLocalAddresses: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface MainSettings {
|
export interface MainSettings {
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
applicationTitle: string;
|
applicationTitle: string;
|
||||||
@@ -119,6 +130,7 @@ export interface MainSettings {
|
|||||||
mediaServerType: number;
|
mediaServerType: number;
|
||||||
partialRequestsEnabled: boolean;
|
partialRequestsEnabled: boolean;
|
||||||
locale: string;
|
locale: string;
|
||||||
|
proxy: ProxySettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PublicSettings {
|
interface PublicSettings {
|
||||||
@@ -325,6 +337,16 @@ class Settings {
|
|||||||
mediaServerType: MediaServerType.NOT_CONFIGURED,
|
mediaServerType: MediaServerType.NOT_CONFIGURED,
|
||||||
partialRequestsEnabled: true,
|
partialRequestsEnabled: true,
|
||||||
locale: 'en',
|
locale: 'en',
|
||||||
|
proxy: {
|
||||||
|
enabled: false,
|
||||||
|
hostname: '',
|
||||||
|
port: 8080,
|
||||||
|
useSsl: false,
|
||||||
|
user: '',
|
||||||
|
password: '',
|
||||||
|
bypassFilter: '',
|
||||||
|
bypassLocalAddresses: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
plex: {
|
plex: {
|
||||||
name: '',
|
name: '',
|
||||||
@@ -479,10 +501,6 @@ class Settings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get main(): MainSettings {
|
get main(): MainSettings {
|
||||||
if (!this.data.main.apiKey) {
|
|
||||||
this.data.main.apiKey = this.generateApiKey();
|
|
||||||
this.save();
|
|
||||||
}
|
|
||||||
return this.data.main;
|
return this.data.main;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -584,29 +602,20 @@ class Settings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get clientId(): string {
|
get clientId(): string {
|
||||||
if (!this.data.clientId) {
|
|
||||||
this.data.clientId = randomUUID();
|
|
||||||
this.save();
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.data.clientId;
|
return this.data.clientId;
|
||||||
}
|
}
|
||||||
|
|
||||||
get vapidPublic(): string {
|
get vapidPublic(): string {
|
||||||
this.generateVapidKeys();
|
|
||||||
|
|
||||||
return this.data.vapidPublic;
|
return this.data.vapidPublic;
|
||||||
}
|
}
|
||||||
|
|
||||||
get vapidPrivate(): string {
|
get vapidPrivate(): string {
|
||||||
this.generateVapidKeys();
|
|
||||||
|
|
||||||
return this.data.vapidPrivate;
|
return this.data.vapidPrivate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public regenerateApiKey(): MainSettings {
|
public async regenerateApiKey(): Promise<MainSettings> {
|
||||||
this.main.apiKey = this.generateApiKey();
|
this.main.apiKey = this.generateApiKey();
|
||||||
this.save();
|
await this.save();
|
||||||
return this.main;
|
return this.main;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -618,15 +627,6 @@ 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
|
* Settings Load
|
||||||
*
|
*
|
||||||
@@ -641,30 +641,51 @@ class Settings {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!fs.existsSync(SETTINGS_PATH)) {
|
let data;
|
||||||
this.save();
|
try {
|
||||||
|
data = await fs.readFile(SETTINGS_PATH, 'utf-8');
|
||||||
|
} catch {
|
||||||
|
await this.save();
|
||||||
}
|
}
|
||||||
const data = fs.readFileSync(SETTINGS_PATH, 'utf-8');
|
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
const parsedJson = JSON.parse(data);
|
const parsedJson = JSON.parse(data);
|
||||||
this.data = await runMigrations(parsedJson, SETTINGS_PATH);
|
const migratedData = await runMigrations(parsedJson, SETTINGS_PATH);
|
||||||
|
this.data = merge(this.data, migratedData);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.save();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public save(): void {
|
public async save(): Promise<void> {
|
||||||
fs.writeFileSync(SETTINGS_PATH, JSON.stringify(this.data, undefined, ' '));
|
await fs.writeFile(
|
||||||
|
SETTINGS_PATH,
|
||||||
|
JSON.stringify(this.data, undefined, ' ')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
import type { AllSettings } from '@server/lib/settings';
|
import type { AllSettings } from '@server/lib/settings';
|
||||||
|
|
||||||
const migrateHostname = (settings: any): AllSettings => {
|
const migrateHostname = (settings: any): AllSettings => {
|
||||||
const oldJellyfinSettings = settings.jellyfin;
|
if (settings.jellyfin?.hostname) {
|
||||||
if (oldJellyfinSettings && oldJellyfinSettings.hostname) {
|
const { hostname } = settings.jellyfin;
|
||||||
const { hostname } = oldJellyfinSettings;
|
|
||||||
const protocolMatch = hostname.match(/^(https?):\/\//i);
|
const protocolMatch = hostname.match(/^(https?):\/\//i);
|
||||||
const useSsl = protocolMatch && protocolMatch[1].toLowerCase() === 'https';
|
const useSsl = protocolMatch && protocolMatch[1].toLowerCase() === 'https';
|
||||||
const remainingUrl = hostname.replace(/^(https?):\/\//i, '');
|
const remainingUrl = hostname.replace(/^(https?):\/\//i, '');
|
||||||
const urlMatch = remainingUrl.match(/^([^:]+)(:([0-9]+))?(\/.*)?$/);
|
const urlMatch = remainingUrl.match(/^([^:]+)(:([0-9]+))?(\/.*)?$/);
|
||||||
|
|
||||||
delete oldJellyfinSettings.hostname;
|
delete settings.jellyfin.hostname;
|
||||||
if (urlMatch) {
|
if (urlMatch) {
|
||||||
const [, ip, , port, urlBase] = urlMatch;
|
const [, ip, , port, urlBase] = urlMatch;
|
||||||
settings.jellyfin = {
|
settings.jellyfin = {
|
||||||
@@ -21,9 +20,7 @@ const migrateHostname = (settings: any): AllSettings => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (settings.jellyfin && settings.jellyfin.hostname) {
|
|
||||||
delete settings.jellyfin.hostname;
|
|
||||||
}
|
|
||||||
return settings;
|
return settings;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -27,8 +27,14 @@ const migrateApiTokens = async (settings: any): Promise<AllSettings> => {
|
|||||||
admin.jellyfinDeviceId
|
admin.jellyfinDeviceId
|
||||||
);
|
);
|
||||||
jellyfinClient.setUserId(admin.jellyfinUserId ?? '');
|
jellyfinClient.setUserId(admin.jellyfinUserId ?? '');
|
||||||
const apiKey = await jellyfinClient.createApiToken('Jellyseerr');
|
try {
|
||||||
settings.jellyfin.apiKey = apiKey;
|
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."
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return settings;
|
return settings;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
/* eslint-disable no-console */
|
|
||||||
import type { AllSettings } from '@server/lib/settings';
|
import type { AllSettings } from '@server/lib/settings';
|
||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
import fs from 'fs';
|
import fs from 'fs/promises';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
const migrationsDir = path.join(__dirname, 'migrations');
|
const migrationsDir = path.join(__dirname, 'migrations');
|
||||||
@@ -10,19 +9,56 @@ export const runMigrations = async (
|
|||||||
settings: AllSettings,
|
settings: AllSettings,
|
||||||
SETTINGS_PATH: string
|
SETTINGS_PATH: string
|
||||||
): Promise<AllSettings> => {
|
): 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;
|
let migrated = settings;
|
||||||
|
|
||||||
try {
|
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);
|
const settingsBefore = JSON.stringify(migrated);
|
||||||
|
|
||||||
for (const migration of migrations) {
|
for (const migration of migrations) {
|
||||||
migrated = await migration(migrated);
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const settingsAfter = JSON.stringify(migrated);
|
const settingsAfter = JSON.stringify(migrated);
|
||||||
@@ -30,30 +66,33 @@ export const runMigrations = async (
|
|||||||
if (settingsBefore !== settingsAfter) {
|
if (settingsBefore !== settingsAfter) {
|
||||||
// a migration occured
|
// a migration occured
|
||||||
// we check that the new config will be saved
|
// we check that the new config will be saved
|
||||||
fs.writeFileSync(SETTINGS_PATH, JSON.stringify(migrated, undefined, ' '));
|
await fs.writeFile(
|
||||||
const fileSaved = JSON.parse(fs.readFileSync(SETTINGS_PATH, 'utf-8'));
|
SETTINGS_PATH,
|
||||||
|
JSON.stringify(migrated, undefined, ' ')
|
||||||
|
);
|
||||||
|
const fileSaved = JSON.parse(await fs.readFile(SETTINGS_PATH, 'utf-8'));
|
||||||
if (JSON.stringify(fileSaved) !== settingsAfter) {
|
if (JSON.stringify(fileSaved) !== settingsAfter) {
|
||||||
// something went wrong while saving file
|
// something went wrong while saving file
|
||||||
throw new Error('Unable to save settings after migration.');
|
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) {
|
} catch (e) {
|
||||||
|
// we stop jellyseerr if the migration failed
|
||||||
logger.error(
|
logger.error(
|
||||||
`Something went wrong while running settings migrations: ${e.message}`,
|
`Something went wrong while running settings migrations: ${e.message}`,
|
||||||
{ label: 'Settings Migrator' }
|
{
|
||||||
|
label: 'Settings Migrator',
|
||||||
|
}
|
||||||
);
|
);
|
||||||
// we stop jellyseerr if the migration failed
|
logger.error(
|
||||||
console.log(
|
'A common cause for this issue is a permission error of your configuration folder.',
|
||||||
'===================================================================='
|
{
|
||||||
);
|
label: 'Settings Migrator',
|
||||||
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();
|
process.exit();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { UserType } from '@server/constants/user';
|
|||||||
import { getRepository } from '@server/datasource';
|
import { getRepository } from '@server/datasource';
|
||||||
import { User } from '@server/entity/User';
|
import { User } from '@server/entity/User';
|
||||||
import { startJobs } from '@server/job/schedule';
|
import { startJobs } from '@server/job/schedule';
|
||||||
import ImageProxy from '@server/lib/imageproxy';
|
|
||||||
import { Permission } from '@server/lib/permissions';
|
import { Permission } from '@server/lib/permissions';
|
||||||
import { getSettings } from '@server/lib/settings';
|
import { getSettings } from '@server/lib/settings';
|
||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
@@ -15,7 +14,6 @@ import { ApiError } from '@server/types/error';
|
|||||||
import { getHostname } from '@server/utils/getHostname';
|
import { getHostname } from '@server/utils/getHostname';
|
||||||
import * as EmailValidator from 'email-validator';
|
import * as EmailValidator from 'email-validator';
|
||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import gravatarUrl from 'gravatar-url';
|
|
||||||
import net from 'net';
|
import net from 'net';
|
||||||
|
|
||||||
const authRoutes = Router();
|
const authRoutes = Router();
|
||||||
@@ -89,7 +87,7 @@ authRoutes.post('/plex', async (req, res, next) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
settings.main.mediaServerType = MediaServerType.PLEX;
|
settings.main.mediaServerType = MediaServerType.PLEX;
|
||||||
settings.save();
|
await settings.save();
|
||||||
startJobs();
|
startJobs();
|
||||||
|
|
||||||
await userRepository.save(user);
|
await userRepository.save(user);
|
||||||
@@ -328,12 +326,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
jellyfinDeviceId: deviceId,
|
jellyfinDeviceId: deviceId,
|
||||||
jellyfinAuthToken: account.AccessToken,
|
jellyfinAuthToken: account.AccessToken,
|
||||||
permissions: Permission.ADMIN,
|
permissions: Permission.ADMIN,
|
||||||
avatar: account.User.PrimaryImageTag
|
avatar: `/avatarproxy/${account.User.Id}`,
|
||||||
? `/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,
|
userType: UserType.EMBY,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -347,12 +340,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
jellyfinDeviceId: deviceId,
|
jellyfinDeviceId: deviceId,
|
||||||
jellyfinAuthToken: account.AccessToken,
|
jellyfinAuthToken: account.AccessToken,
|
||||||
permissions: Permission.ADMIN,
|
permissions: Permission.ADMIN,
|
||||||
avatar: account.User.PrimaryImageTag
|
avatar: `/avatarproxy/${account.User.Id}`,
|
||||||
? `/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,
|
userType: UserType.JELLYFIN,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -378,7 +366,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
settings.jellyfin.urlBase = body.urlBase ?? '';
|
settings.jellyfin.urlBase = body.urlBase ?? '';
|
||||||
settings.jellyfin.useSsl = body.useSsl ?? false;
|
settings.jellyfin.useSsl = body.useSsl ?? false;
|
||||||
settings.jellyfin.apiKey = apiKey;
|
settings.jellyfin.apiKey = apiKey;
|
||||||
settings.save();
|
await settings.save();
|
||||||
startJobs();
|
startJobs();
|
||||||
|
|
||||||
await userRepository.save(user);
|
await userRepository.save(user);
|
||||||
@@ -401,27 +389,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
jellyfinUsername: account.User.Name,
|
jellyfinUsername: account.User.Name,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
// Update the users avatar with their jellyfin profile pic (incase it changed)
|
user.avatar = `/avatarproxy/${account.User.Id}`;
|
||||||
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;
|
user.jellyfinUsername = account.User.Name;
|
||||||
|
|
||||||
if (user.username === account.User.Name) {
|
if (user.username === account.User.Name) {
|
||||||
@@ -459,12 +427,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
jellyfinUserId: account.User.Id,
|
jellyfinUserId: account.User.Id,
|
||||||
jellyfinDeviceId: deviceId,
|
jellyfinDeviceId: deviceId,
|
||||||
permissions: settings.main.defaultPermissions,
|
permissions: settings.main.defaultPermissions,
|
||||||
avatar: account.User.PrimaryImageTag
|
avatar: `/avatarproxy/${account.User.Id}`,
|
||||||
? `/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
|
|
||||||
: gravatarUrl(body.email || account.User.Name, {
|
|
||||||
default: 'mm',
|
|
||||||
size: 200,
|
|
||||||
}),
|
|
||||||
userType:
|
userType:
|
||||||
settings.main.mediaServerType === MediaServerType.JELLYFIN
|
settings.main.mediaServerType === MediaServerType.JELLYFIN
|
||||||
? UserType.JELLYFIN
|
? UserType.JELLYFIN
|
||||||
|
|||||||
@@ -1,21 +1,39 @@
|
|||||||
import { MediaServerType } from '@server/constants/server';
|
import { MediaServerType } from '@server/constants/server';
|
||||||
|
import { getRepository } from '@server/datasource';
|
||||||
|
import { User } from '@server/entity/User';
|
||||||
import ImageProxy from '@server/lib/imageproxy';
|
import ImageProxy from '@server/lib/imageproxy';
|
||||||
import { getSettings } from '@server/lib/settings';
|
import { getSettings } from '@server/lib/settings';
|
||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
|
import { getAppVersion } from '@server/utils/appVersion';
|
||||||
import { getHostname } from '@server/utils/getHostname';
|
import { getHostname } from '@server/utils/getHostname';
|
||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
|
import gravatarUrl from 'gravatar-url';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
const avatarImageProxy = new ImageProxy('avatar', '');
|
let _avatarImageProxy: ImageProxy | null = null;
|
||||||
// Proxy avatar images
|
async function initAvatarImageProxy() {
|
||||||
router.get('/*', async (req, res) => {
|
if (!_avatarImageProxy) {
|
||||||
let imagePath = '';
|
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) => {
|
||||||
try {
|
try {
|
||||||
const jellyfinAvatar = req.url.match(
|
if (!req.params.jellyfinUserId.match(/^[a-f0-9]{32}$/)) {
|
||||||
/(\/Users\/\w+\/Images\/Primary\/?\?tag=\w+&quality=90)$/
|
|
||||||
)?.[1];
|
|
||||||
if (!jellyfinAvatar) {
|
|
||||||
const mediaServerType = getSettings().main.mediaServerType;
|
const mediaServerType = getSettings().main.mediaServerType;
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Provided URL is not ${
|
`Provided URL is not ${
|
||||||
@@ -26,10 +44,28 @@ router.get('/*', async (req, res) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const imageUrl = new URL(jellyfinAvatar, getHostname());
|
const avatarImageCache = await initAvatarImageProxy();
|
||||||
imagePath = imageUrl.toString();
|
|
||||||
|
|
||||||
const imageData = await avatarImageProxy.getImage(imagePath);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
res.writeHead(200, {
|
res.writeHead(200, {
|
||||||
'Content-Type': `image/${imageData.meta.extension}`,
|
'Content-Type': `image/${imageData.meta.extension}`,
|
||||||
@@ -42,7 +78,6 @@ router.get('/*', async (req, res) => {
|
|||||||
res.end(imageData.imageBuffer);
|
res.end(imageData.imageBuffer);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Failed to proxy avatar image', {
|
logger.error('Failed to proxy avatar image', {
|
||||||
imagePath,
|
|
||||||
errorMessage: e.message,
|
errorMessage: e.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,11 @@ import { mapProductionCompany } from '@server/models/Movie';
|
|||||||
import { mapNetwork } from '@server/models/Tv';
|
import { mapNetwork } from '@server/models/Tv';
|
||||||
import settingsRoutes from '@server/routes/settings';
|
import settingsRoutes from '@server/routes/settings';
|
||||||
import watchlistRoutes from '@server/routes/watchlist';
|
import watchlistRoutes from '@server/routes/watchlist';
|
||||||
import { appDataPath, appDataStatus } from '@server/utils/appDataVolume';
|
import {
|
||||||
|
appDataPath,
|
||||||
|
appDataPermissions,
|
||||||
|
appDataStatus,
|
||||||
|
} from '@server/utils/appDataVolume';
|
||||||
import { getAppVersion, getCommitTag } from '@server/utils/appVersion';
|
import { getAppVersion, getCommitTag } from '@server/utils/appVersion';
|
||||||
import restartFlag from '@server/utils/restartFlag';
|
import restartFlag from '@server/utils/restartFlag';
|
||||||
import { isPerson } from '@server/utils/typeHelpers';
|
import { isPerson } from '@server/utils/typeHelpers';
|
||||||
@@ -93,6 +97,7 @@ router.get('/status/appdata', (_req, res) => {
|
|||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
appData: appDataStatus(),
|
appData: appDataStatus(),
|
||||||
appDataPath: appDataPath(),
|
appDataPath: appDataPath(),
|
||||||
|
appDataPermissions: appDataPermissions(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -123,9 +123,13 @@ serviceRoutes.get<{ sonarrId: string }>(
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const systemStatus = await sonarr.getSystemStatus();
|
||||||
|
const sonarrMajorVersion = Number(systemStatus.version.split('.')[0]);
|
||||||
|
|
||||||
const profiles = await sonarr.getProfiles();
|
const profiles = await sonarr.getProfiles();
|
||||||
const rootFolders = await sonarr.getRootFolders();
|
const rootFolders = await sonarr.getRootFolders();
|
||||||
const languageProfiles = await sonarr.getLanguageProfiles();
|
const languageProfiles =
|
||||||
|
sonarrMajorVersion <= 3 ? await sonarr.getLanguageProfiles() : null;
|
||||||
const tags = await sonarr.getTags();
|
const tags = await sonarr.getTags();
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ import { getHostname } from '@server/utils/getHostname';
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import rateLimit from 'express-rate-limit';
|
import rateLimit from 'express-rate-limit';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import gravatarUrl from 'gravatar-url';
|
|
||||||
import { escapeRegExp, merge, omit, set, sortBy } from 'lodash';
|
import { escapeRegExp, merge, omit, set, sortBy } from 'lodash';
|
||||||
import { rescheduleJob } from 'node-schedule';
|
import { rescheduleJob } from 'node-schedule';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
@@ -70,19 +69,19 @@ settingsRoutes.get('/main', (req, res, next) => {
|
|||||||
res.status(200).json(filteredMainSettings(req.user, settings.main));
|
res.status(200).json(filteredMainSettings(req.user, settings.main));
|
||||||
});
|
});
|
||||||
|
|
||||||
settingsRoutes.post('/main', (req, res) => {
|
settingsRoutes.post('/main', async (req, res) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
settings.main = merge(settings.main, req.body);
|
settings.main = merge(settings.main, req.body);
|
||||||
settings.save();
|
await settings.save();
|
||||||
|
|
||||||
return res.status(200).json(settings.main);
|
return res.status(200).json(settings.main);
|
||||||
});
|
});
|
||||||
|
|
||||||
settingsRoutes.post('/main/regenerate', (req, res, next) => {
|
settingsRoutes.post('/main/regenerate', async (req, res, next) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
const main = settings.regenerateApiKey();
|
const main = await settings.regenerateApiKey();
|
||||||
|
|
||||||
if (!req.user) {
|
if (!req.user) {
|
||||||
return next({ status: 500, message: 'User missing from request.' });
|
return next({ status: 500, message: 'User missing from request.' });
|
||||||
@@ -119,7 +118,7 @@ settingsRoutes.post('/plex', async (req, res, next) => {
|
|||||||
settings.plex.machineId = result.MediaContainer.machineIdentifier;
|
settings.plex.machineId = result.MediaContainer.machineIdentifier;
|
||||||
settings.plex.name = result.MediaContainer.friendlyName;
|
settings.plex.name = result.MediaContainer.friendlyName;
|
||||||
|
|
||||||
settings.save();
|
await settings.save();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Something went wrong testing Plex connection', {
|
logger.error('Something went wrong testing Plex connection', {
|
||||||
label: 'API',
|
label: 'API',
|
||||||
@@ -232,7 +231,7 @@ settingsRoutes.get('/plex/library', async (req, res) => {
|
|||||||
...library,
|
...library,
|
||||||
enabled: enabledLibraries.includes(library.id),
|
enabled: enabledLibraries.includes(library.id),
|
||||||
}));
|
}));
|
||||||
settings.save();
|
await settings.save();
|
||||||
return res.status(200).json(settings.plex.libraries);
|
return res.status(200).json(settings.plex.libraries);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -283,7 +282,7 @@ settingsRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
Object.assign(settings.jellyfin, req.body);
|
Object.assign(settings.jellyfin, req.body);
|
||||||
settings.jellyfin.serverId = result.Id;
|
settings.jellyfin.serverId = result.Id;
|
||||||
settings.jellyfin.name = result.ServerName;
|
settings.jellyfin.name = result.ServerName;
|
||||||
settings.save();
|
await settings.save();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof ApiError) {
|
if (e instanceof ApiError) {
|
||||||
logger.error('Something went wrong testing Jellyfin connection', {
|
logger.error('Something went wrong testing Jellyfin connection', {
|
||||||
@@ -371,7 +370,7 @@ settingsRoutes.get('/jellyfin/library', async (req, res, next) => {
|
|||||||
...library,
|
...library,
|
||||||
enabled: enabledLibraries.includes(library.id),
|
enabled: enabledLibraries.includes(library.id),
|
||||||
}));
|
}));
|
||||||
settings.save();
|
await settings.save();
|
||||||
return res.status(200).json(settings.jellyfin.libraries);
|
return res.status(200).json(settings.jellyfin.libraries);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -395,9 +394,7 @@ settingsRoutes.get('/jellyfin/users', async (req, res) => {
|
|||||||
const users = resp.users.map((user) => ({
|
const users = resp.users.map((user) => ({
|
||||||
username: user.Name,
|
username: user.Name,
|
||||||
id: user.Id,
|
id: user.Id,
|
||||||
thumb: user.PrimaryImageTag
|
thumb: `/avatarproxy/${user.Id}`,
|
||||||
? `/Users/${user.Id}/Images/Primary/?tag=${user.PrimaryImageTag}&quality=90`
|
|
||||||
: gravatarUrl(user.Name, { default: 'mm', size: 200 }),
|
|
||||||
email: user.Name,
|
email: user.Name,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -437,7 +434,7 @@ settingsRoutes.post('/tautulli', async (req, res, next) => {
|
|||||||
throw new Error('Tautulli version not supported');
|
throw new Error('Tautulli version not supported');
|
||||||
}
|
}
|
||||||
|
|
||||||
settings.save();
|
await settings.save();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Something went wrong testing Tautulli connection', {
|
logger.error('Something went wrong testing Tautulli connection', {
|
||||||
label: 'API',
|
label: 'API',
|
||||||
@@ -698,7 +695,7 @@ settingsRoutes.post<{ jobId: JobId }>(
|
|||||||
|
|
||||||
settingsRoutes.post<{ jobId: JobId }>(
|
settingsRoutes.post<{ jobId: JobId }>(
|
||||||
'/jobs/:jobId/schedule',
|
'/jobs/:jobId/schedule',
|
||||||
(req, res, next) => {
|
async (req, res, next) => {
|
||||||
const scheduledJob = scheduledJobs.find(
|
const scheduledJob = scheduledJobs.find(
|
||||||
(job) => job.id === req.params.jobId
|
(job) => job.id === req.params.jobId
|
||||||
);
|
);
|
||||||
@@ -712,7 +709,7 @@ settingsRoutes.post<{ jobId: JobId }>(
|
|||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
settings.jobs[scheduledJob.id].schedule = req.body.schedule;
|
settings.jobs[scheduledJob.id].schedule = req.body.schedule;
|
||||||
settings.save();
|
await settings.save();
|
||||||
|
|
||||||
scheduledJob.cronSchedule = req.body.schedule;
|
scheduledJob.cronSchedule = req.body.schedule;
|
||||||
|
|
||||||
@@ -769,11 +766,11 @@ settingsRoutes.post<{ cacheId: AvailableCacheIds }>(
|
|||||||
settingsRoutes.post(
|
settingsRoutes.post(
|
||||||
'/initialize',
|
'/initialize',
|
||||||
isAuthenticated(Permission.ADMIN),
|
isAuthenticated(Permission.ADMIN),
|
||||||
(_req, res) => {
|
async (_req, res) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
settings.public.initialized = true;
|
settings.public.initialized = true;
|
||||||
settings.save();
|
await settings.save();
|
||||||
|
|
||||||
return res.status(200).json(settings.public);
|
return res.status(200).json(settings.public);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,11 +31,11 @@ notificationRoutes.get('/discord', (_req, res) => {
|
|||||||
res.status(200).json(settings.notifications.agents.discord);
|
res.status(200).json(settings.notifications.agents.discord);
|
||||||
});
|
});
|
||||||
|
|
||||||
notificationRoutes.post('/discord', (req, res) => {
|
notificationRoutes.post('/discord', async (req, res) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
settings.notifications.agents.discord = req.body;
|
settings.notifications.agents.discord = req.body;
|
||||||
settings.save();
|
await settings.save();
|
||||||
|
|
||||||
res.status(200).json(settings.notifications.agents.discord);
|
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);
|
res.status(200).json(settings.notifications.agents.slack);
|
||||||
});
|
});
|
||||||
|
|
||||||
notificationRoutes.post('/slack', (req, res) => {
|
notificationRoutes.post('/slack', async (req, res) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
settings.notifications.agents.slack = req.body;
|
settings.notifications.agents.slack = req.body;
|
||||||
settings.save();
|
await settings.save();
|
||||||
|
|
||||||
res.status(200).json(settings.notifications.agents.slack);
|
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);
|
res.status(200).json(settings.notifications.agents.telegram);
|
||||||
});
|
});
|
||||||
|
|
||||||
notificationRoutes.post('/telegram', (req, res) => {
|
notificationRoutes.post('/telegram', async (req, res) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
settings.notifications.agents.telegram = req.body;
|
settings.notifications.agents.telegram = req.body;
|
||||||
settings.save();
|
await settings.save();
|
||||||
|
|
||||||
res.status(200).json(settings.notifications.agents.telegram);
|
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);
|
res.status(200).json(settings.notifications.agents.pushbullet);
|
||||||
});
|
});
|
||||||
|
|
||||||
notificationRoutes.post('/pushbullet', (req, res) => {
|
notificationRoutes.post('/pushbullet', async (req, res) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
settings.notifications.agents.pushbullet = req.body;
|
settings.notifications.agents.pushbullet = req.body;
|
||||||
settings.save();
|
await settings.save();
|
||||||
|
|
||||||
res.status(200).json(settings.notifications.agents.pushbullet);
|
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);
|
res.status(200).json(settings.notifications.agents.pushover);
|
||||||
});
|
});
|
||||||
|
|
||||||
notificationRoutes.post('/pushover', (req, res) => {
|
notificationRoutes.post('/pushover', async (req, res) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
settings.notifications.agents.pushover = req.body;
|
settings.notifications.agents.pushover = req.body;
|
||||||
settings.save();
|
await settings.save();
|
||||||
|
|
||||||
res.status(200).json(settings.notifications.agents.pushover);
|
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);
|
res.status(200).json(settings.notifications.agents.email);
|
||||||
});
|
});
|
||||||
|
|
||||||
notificationRoutes.post('/email', (req, res) => {
|
notificationRoutes.post('/email', async (req, res) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
settings.notifications.agents.email = req.body;
|
settings.notifications.agents.email = req.body;
|
||||||
settings.save();
|
await settings.save();
|
||||||
|
|
||||||
res.status(200).json(settings.notifications.agents.email);
|
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);
|
res.status(200).json(settings.notifications.agents.webpush);
|
||||||
});
|
});
|
||||||
|
|
||||||
notificationRoutes.post('/webpush', (req, res) => {
|
notificationRoutes.post('/webpush', async (req, res) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
settings.notifications.agents.webpush = req.body;
|
settings.notifications.agents.webpush = req.body;
|
||||||
settings.save();
|
await settings.save();
|
||||||
|
|
||||||
res.status(200).json(settings.notifications.agents.webpush);
|
res.status(200).json(settings.notifications.agents.webpush);
|
||||||
});
|
});
|
||||||
@@ -284,7 +284,7 @@ notificationRoutes.get('/webhook', (_req, res) => {
|
|||||||
res.status(200).json(response);
|
res.status(200).json(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
notificationRoutes.post('/webhook', (req, res, next) => {
|
notificationRoutes.post('/webhook', async (req, res, next) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
try {
|
try {
|
||||||
JSON.parse(req.body.options.jsonPayload);
|
JSON.parse(req.body.options.jsonPayload);
|
||||||
@@ -300,7 +300,7 @@ notificationRoutes.post('/webhook', (req, res, next) => {
|
|||||||
authHeader: req.body.options.authHeader,
|
authHeader: req.body.options.authHeader,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
settings.save();
|
await settings.save();
|
||||||
|
|
||||||
res.status(200).json(settings.notifications.agents.webhook);
|
res.status(200).json(settings.notifications.agents.webhook);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -351,11 +351,11 @@ notificationRoutes.get('/lunasea', (_req, res) => {
|
|||||||
res.status(200).json(settings.notifications.agents.lunasea);
|
res.status(200).json(settings.notifications.agents.lunasea);
|
||||||
});
|
});
|
||||||
|
|
||||||
notificationRoutes.post('/lunasea', (req, res) => {
|
notificationRoutes.post('/lunasea', async (req, res) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
settings.notifications.agents.lunasea = req.body;
|
settings.notifications.agents.lunasea = req.body;
|
||||||
settings.save();
|
await settings.save();
|
||||||
|
|
||||||
res.status(200).json(settings.notifications.agents.lunasea);
|
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);
|
res.status(200).json(settings.notifications.agents.gotify);
|
||||||
});
|
});
|
||||||
|
|
||||||
notificationRoutes.post('/gotify', (req, res) => {
|
notificationRoutes.post('/gotify', async (req, res) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
settings.notifications.agents.gotify = req.body;
|
settings.notifications.agents.gotify = req.body;
|
||||||
settings.save();
|
await settings.save();
|
||||||
|
|
||||||
res.status(200).json(settings.notifications.agents.gotify);
|
res.status(200).json(settings.notifications.agents.gotify);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ radarrRoutes.get('/', (_req, res) => {
|
|||||||
res.status(200).json(settings.radarr);
|
res.status(200).json(settings.radarr);
|
||||||
});
|
});
|
||||||
|
|
||||||
radarrRoutes.post('/', (req, res) => {
|
radarrRoutes.post('/', async (req, res) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
const newRadarr = req.body as RadarrSettings;
|
const newRadarr = req.body as RadarrSettings;
|
||||||
@@ -31,7 +31,7 @@ radarrRoutes.post('/', (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
settings.radarr = [...settings.radarr, newRadarr];
|
settings.radarr = [...settings.radarr, newRadarr];
|
||||||
settings.save();
|
await settings.save();
|
||||||
|
|
||||||
return res.status(201).json(newRadarr);
|
return res.status(201).json(newRadarr);
|
||||||
});
|
});
|
||||||
@@ -76,7 +76,7 @@ radarrRoutes.post<
|
|||||||
|
|
||||||
radarrRoutes.put<{ id: string }, RadarrSettings, RadarrSettings>(
|
radarrRoutes.put<{ id: string }, RadarrSettings, RadarrSettings>(
|
||||||
'/:id',
|
'/:id',
|
||||||
(req, res, next) => {
|
async (req, res, next) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
const radarrIndex = settings.radarr.findIndex(
|
const radarrIndex = settings.radarr.findIndex(
|
||||||
@@ -102,7 +102,7 @@ radarrRoutes.put<{ id: string }, RadarrSettings, RadarrSettings>(
|
|||||||
...req.body,
|
...req.body,
|
||||||
id: Number(req.params.id),
|
id: Number(req.params.id),
|
||||||
} as RadarrSettings;
|
} as RadarrSettings;
|
||||||
settings.save();
|
await settings.save();
|
||||||
|
|
||||||
return res.status(200).json(settings.radarr[radarrIndex]);
|
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', (req, res, next) => {
|
radarrRoutes.delete<{ id: string }>('/:id', async (req, res, next) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
const radarrIndex = settings.radarr.findIndex(
|
const radarrIndex = settings.radarr.findIndex(
|
||||||
@@ -146,7 +146,7 @@ radarrRoutes.delete<{ id: string }>('/:id', (req, res, next) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const removed = settings.radarr.splice(radarrIndex, 1);
|
const removed = settings.radarr.splice(radarrIndex, 1);
|
||||||
settings.save();
|
await settings.save();
|
||||||
|
|
||||||
return res.status(200).json(removed[0]);
|
return res.status(200).json(removed[0]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ sonarrRoutes.get('/', (_req, res) => {
|
|||||||
res.status(200).json(settings.sonarr);
|
res.status(200).json(settings.sonarr);
|
||||||
});
|
});
|
||||||
|
|
||||||
sonarrRoutes.post('/', (req, res) => {
|
sonarrRoutes.post('/', async (req, res) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
const newSonarr = req.body as SonarrSettings;
|
const newSonarr = req.body as SonarrSettings;
|
||||||
@@ -31,7 +31,7 @@ sonarrRoutes.post('/', (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
settings.sonarr = [...settings.sonarr, newSonarr];
|
settings.sonarr = [...settings.sonarr, newSonarr];
|
||||||
settings.save();
|
await settings.save();
|
||||||
|
|
||||||
return res.status(201).json(newSonarr);
|
return res.status(201).json(newSonarr);
|
||||||
});
|
});
|
||||||
@@ -43,13 +43,14 @@ sonarrRoutes.post('/test', async (req, res, next) => {
|
|||||||
url: SonarrAPI.buildUrl(req.body, '/api/v3'),
|
url: SonarrAPI.buildUrl(req.body, '/api/v3'),
|
||||||
});
|
});
|
||||||
|
|
||||||
const urlBase = await sonarr
|
const systemStatus = await sonarr.getSystemStatus();
|
||||||
.getSystemStatus()
|
const sonarrMajorVersion = Number(systemStatus.version.split('.')[0]);
|
||||||
.then((value) => value.urlBase)
|
|
||||||
.catch(() => req.body.baseUrl);
|
const urlBase = systemStatus.urlBase;
|
||||||
const profiles = await sonarr.getProfiles();
|
const profiles = await sonarr.getProfiles();
|
||||||
const folders = await sonarr.getRootFolders();
|
const folders = await sonarr.getRootFolders();
|
||||||
const languageProfiles = await sonarr.getLanguageProfiles();
|
const languageProfiles =
|
||||||
|
sonarrMajorVersion <= 3 ? await sonarr.getLanguageProfiles() : null;
|
||||||
const tags = await sonarr.getTags();
|
const tags = await sonarr.getTags();
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
@@ -72,7 +73,7 @@ sonarrRoutes.post('/test', async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
sonarrRoutes.put<{ id: string }>('/:id', (req, res) => {
|
sonarrRoutes.put<{ id: string }>('/:id', async (req, res) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
const sonarrIndex = settings.sonarr.findIndex(
|
const sonarrIndex = settings.sonarr.findIndex(
|
||||||
@@ -100,12 +101,12 @@ sonarrRoutes.put<{ id: string }>('/:id', (req, res) => {
|
|||||||
...req.body,
|
...req.body,
|
||||||
id: Number(req.params.id),
|
id: Number(req.params.id),
|
||||||
} as SonarrSettings;
|
} as SonarrSettings;
|
||||||
settings.save();
|
await settings.save();
|
||||||
|
|
||||||
return res.status(200).json(settings.sonarr[sonarrIndex]);
|
return res.status(200).json(settings.sonarr[sonarrIndex]);
|
||||||
});
|
});
|
||||||
|
|
||||||
sonarrRoutes.delete<{ id: string }>('/:id', (req, res) => {
|
sonarrRoutes.delete<{ id: string }>('/:id', async (req, res) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
const sonarrIndex = settings.sonarr.findIndex(
|
const sonarrIndex = settings.sonarr.findIndex(
|
||||||
@@ -119,7 +120,7 @@ sonarrRoutes.delete<{ id: string }>('/:id', (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const removed = settings.sonarr.splice(sonarrIndex, 1);
|
const removed = settings.sonarr.splice(sonarrIndex, 1);
|
||||||
settings.save();
|
await settings.save();
|
||||||
|
|
||||||
return res.status(200).json(removed[0]);
|
return res.status(200).json(removed[0]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -539,12 +539,7 @@ router.post(
|
|||||||
).toString('base64'),
|
).toString('base64'),
|
||||||
email: jellyfinUser?.Name,
|
email: jellyfinUser?.Name,
|
||||||
permissions: settings.main.defaultPermissions,
|
permissions: settings.main.defaultPermissions,
|
||||||
avatar: jellyfinUser?.PrimaryImageTag
|
avatar: `/avatarproxy/${jellyfinUser?.Id}`,
|
||||||
? `/Users/${jellyfinUser.Id}/Images/Primary/?tag=${jellyfinUser.PrimaryImageTag}&quality=90`
|
|
||||||
: gravatarUrl(jellyfinUser?.Name ?? '', {
|
|
||||||
default: 'mm',
|
|
||||||
size: 200,
|
|
||||||
}),
|
|
||||||
userType:
|
userType:
|
||||||
settings.main.mediaServerType === MediaServerType.JELLYFIN
|
settings.main.mediaServerType === MediaServerType.JELLYFIN
|
||||||
? UserType.JELLYFIN
|
? UserType.JELLYFIN
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { existsSync } from 'fs';
|
import { accessSync, existsSync } from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
const CONFIG_PATH = process.env.CONFIG_DIRECTORY
|
const CONFIG_PATH = process.env.CONFIG_DIRECTORY
|
||||||
@@ -14,3 +14,12 @@ export const appDataStatus = (): boolean => {
|
|||||||
export const appDataPath = (): string => {
|
export const appDataPath = (): string => {
|
||||||
return CONFIG_PATH;
|
return CONFIG_PATH;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const appDataPermissions = (): boolean => {
|
||||||
|
try {
|
||||||
|
accessSync(CONFIG_PATH);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
111
server/utils/customProxyAgent.ts
Normal file
111
server/utils/customProxyAgent.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
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,7 +13,8 @@ class RestartFlag {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
this.settings.csrfProtection !== settings.csrfProtection ||
|
this.settings.csrfProtection !== settings.csrfProtection ||
|
||||||
this.settings.trustProxy !== settings.trustProxy
|
this.settings.trustProxy !== settings.trustProxy ||
|
||||||
|
this.settings.proxy.enabled !== settings.proxy.enabled
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,11 +25,8 @@ const CachedImage = ({ src, type, ...props }: CachedImageProps) => {
|
|||||||
? src.replace(/^https:\/\/image\.tmdb\.org\//, '/imageproxy/')
|
? src.replace(/^https:\/\/image\.tmdb\.org\//, '/imageproxy/')
|
||||||
: src;
|
: src;
|
||||||
} else if (type === 'avatar') {
|
} else if (type === 'avatar') {
|
||||||
// jellyfin avatar (in any)
|
// jellyfin avatar (if any)
|
||||||
const jellyfinAvatar = src.match(
|
imageUrl = src;
|
||||||
/(\/Users\/\w+\/Images\/Primary\/?\?tag=\w+&quality=90)$/
|
|
||||||
)?.[1];
|
|
||||||
imageUrl = jellyfinAvatar ? `/avatarproxy` + jellyfinAvatar : src;
|
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,17 @@ const messages = defineMessages('components.Settings.SettingsMain', {
|
|||||||
validationApplicationUrlTrailingSlash: 'URL must not end in a trailing slash',
|
validationApplicationUrlTrailingSlash: 'URL must not end in a trailing slash',
|
||||||
partialRequestsEnabled: 'Allow Partial Series Requests',
|
partialRequestsEnabled: 'Allow Partial Series Requests',
|
||||||
locale: 'Display Language',
|
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 = () => {
|
const SettingsMain = () => {
|
||||||
@@ -82,6 +93,12 @@ const SettingsMain = () => {
|
|||||||
intl.formatMessage(messages.validationApplicationUrlTrailingSlash),
|
intl.formatMessage(messages.validationApplicationUrlTrailingSlash),
|
||||||
(value) => !value || !value.endsWith('/')
|
(value) => !value || !value.endsWith('/')
|
||||||
),
|
),
|
||||||
|
proxyPort: Yup.number().when('proxyEnabled', {
|
||||||
|
is: (proxyEnabled: boolean) => proxyEnabled,
|
||||||
|
then: Yup.number().required(
|
||||||
|
intl.formatMessage(messages.validationProxyPort)
|
||||||
|
),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const regenerate = async () => {
|
const regenerate = async () => {
|
||||||
@@ -137,6 +154,14 @@ const SettingsMain = () => {
|
|||||||
partialRequestsEnabled: data?.partialRequestsEnabled,
|
partialRequestsEnabled: data?.partialRequestsEnabled,
|
||||||
trustProxy: data?.trustProxy,
|
trustProxy: data?.trustProxy,
|
||||||
cacheImages: data?.cacheImages,
|
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
|
enableReinitialize
|
||||||
validationSchema={MainSettingsSchema}
|
validationSchema={MainSettingsSchema}
|
||||||
@@ -158,6 +183,16 @@ const SettingsMain = () => {
|
|||||||
partialRequestsEnabled: values.partialRequestsEnabled,
|
partialRequestsEnabled: values.partialRequestsEnabled,
|
||||||
trustProxy: values.trustProxy,
|
trustProxy: values.trustProxy,
|
||||||
cacheImages: values.cacheImages,
|
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();
|
if (!res.ok) throw new Error();
|
||||||
@@ -437,6 +472,176 @@ const SettingsMain = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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="actions">
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<span className="ml-3 inline-flex rounded-md shadow-sm">
|
<span className="ml-3 inline-flex rounded-md shadow-sm">
|
||||||
|
|||||||
@@ -86,10 +86,12 @@ interface TestResponse {
|
|||||||
id: number;
|
id: number;
|
||||||
path: string;
|
path: string;
|
||||||
}[];
|
}[];
|
||||||
languageProfiles: {
|
languageProfiles:
|
||||||
id: number;
|
| {
|
||||||
name: string;
|
id: number;
|
||||||
}[];
|
name: string;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
tags: {
|
tags: {
|
||||||
id: number;
|
id: number;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -112,7 +114,7 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
|
|||||||
const [testResponse, setTestResponse] = useState<TestResponse>({
|
const [testResponse, setTestResponse] = useState<TestResponse>({
|
||||||
profiles: [],
|
profiles: [],
|
||||||
rootFolders: [],
|
rootFolders: [],
|
||||||
languageProfiles: [],
|
languageProfiles: null,
|
||||||
tags: [],
|
tags: [],
|
||||||
});
|
});
|
||||||
const SonarrSettingsSchema = Yup.object().shape({
|
const SonarrSettingsSchema = Yup.object().shape({
|
||||||
@@ -137,9 +139,11 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
|
|||||||
activeProfileId: Yup.string().required(
|
activeProfileId: Yup.string().required(
|
||||||
intl.formatMessage(messages.validationProfileRequired)
|
intl.formatMessage(messages.validationProfileRequired)
|
||||||
),
|
),
|
||||||
activeLanguageProfileId: Yup.number().required(
|
activeLanguageProfileId: testResponse.languageProfiles
|
||||||
intl.formatMessage(messages.validationLanguageProfileRequired)
|
? Yup.number().required(
|
||||||
),
|
intl.formatMessage(messages.validationLanguageProfileRequired)
|
||||||
|
)
|
||||||
|
: Yup.number(),
|
||||||
externalUrl: Yup.string()
|
externalUrl: Yup.string()
|
||||||
.url(intl.formatMessage(messages.validationApplicationUrl))
|
.url(intl.formatMessage(messages.validationApplicationUrl))
|
||||||
.test(
|
.test(
|
||||||
@@ -658,54 +662,56 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-row">
|
{testResponse.languageProfiles && (
|
||||||
<label
|
<div className="form-row">
|
||||||
htmlFor="activeLanguageProfileId"
|
<label
|
||||||
className="text-label"
|
htmlFor="activeLanguageProfileId"
|
||||||
>
|
className="text-label"
|
||||||
{intl.formatMessage(messages.languageprofile)}
|
>
|
||||||
<span className="label-required">*</span>
|
{intl.formatMessage(messages.languageprofile)}
|
||||||
</label>
|
<span className="label-required">*</span>
|
||||||
<div className="form-input-area">
|
</label>
|
||||||
<div className="form-input-field">
|
<div className="form-input-area">
|
||||||
<Field
|
<div className="form-input-field">
|
||||||
as="select"
|
<Field
|
||||||
id="activeLanguageProfileId"
|
as="select"
|
||||||
name="activeLanguageProfileId"
|
id="activeLanguageProfileId"
|
||||||
disabled={!isValidated || isTesting}
|
name="activeLanguageProfileId"
|
||||||
>
|
disabled={!isValidated || isTesting}
|
||||||
<option value="">
|
>
|
||||||
{isTesting
|
<option value="">
|
||||||
? intl.formatMessage(
|
{isTesting
|
||||||
messages.loadinglanguageprofiles
|
? intl.formatMessage(
|
||||||
)
|
messages.loadinglanguageprofiles
|
||||||
: !isValidated
|
)
|
||||||
? intl.formatMessage(
|
: !isValidated
|
||||||
messages.testFirstLanguageProfiles
|
? intl.formatMessage(
|
||||||
)
|
messages.testFirstLanguageProfiles
|
||||||
: intl.formatMessage(
|
)
|
||||||
messages.selectLanguageProfile
|
: intl.formatMessage(
|
||||||
)}
|
messages.selectLanguageProfile
|
||||||
</option>
|
)}
|
||||||
{testResponse.languageProfiles.length > 0 &&
|
</option>
|
||||||
testResponse.languageProfiles.map((language) => (
|
{testResponse.languageProfiles.length > 0 &&
|
||||||
<option
|
testResponse.languageProfiles.map((language) => (
|
||||||
key={`loaded-profile-${language.id}`}
|
<option
|
||||||
value={language.id}
|
key={`loaded-profile-${language.id}`}
|
||||||
>
|
value={language.id}
|
||||||
{language.name}
|
>
|
||||||
</option>
|
{language.name}
|
||||||
))}
|
</option>
|
||||||
</Field>
|
))}
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
{errors.activeLanguageProfileId &&
|
||||||
|
touched.activeLanguageProfileId && (
|
||||||
|
<div className="error">
|
||||||
|
{errors.activeLanguageProfileId}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{errors.activeLanguageProfileId &&
|
|
||||||
touched.activeLanguageProfileId && (
|
|
||||||
<div className="error">
|
|
||||||
{errors.activeLanguageProfileId}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<label htmlFor="tags" className="text-label">
|
<label htmlFor="tags" className="text-label">
|
||||||
{intl.formatMessage(messages.tags)}
|
{intl.formatMessage(messages.tags)}
|
||||||
@@ -863,53 +869,55 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-row">
|
{testResponse.languageProfiles && (
|
||||||
<label
|
<div className="form-row">
|
||||||
htmlFor="activeAnimeLanguageProfileId"
|
<label
|
||||||
className="text-label"
|
htmlFor="activeAnimeLanguageProfileId"
|
||||||
>
|
className="text-label"
|
||||||
{intl.formatMessage(messages.animelanguageprofile)}
|
>
|
||||||
</label>
|
{intl.formatMessage(messages.animelanguageprofile)}
|
||||||
<div className="form-input-area">
|
</label>
|
||||||
<div className="form-input-field">
|
<div className="form-input-area">
|
||||||
<Field
|
<div className="form-input-field">
|
||||||
as="select"
|
<Field
|
||||||
id="activeAnimeLanguageProfileId"
|
as="select"
|
||||||
name="activeAnimeLanguageProfileId"
|
id="activeAnimeLanguageProfileId"
|
||||||
disabled={!isValidated || isTesting}
|
name="activeAnimeLanguageProfileId"
|
||||||
>
|
disabled={!isValidated || isTesting}
|
||||||
<option value="">
|
>
|
||||||
{isTesting
|
<option value="">
|
||||||
? intl.formatMessage(
|
{isTesting
|
||||||
messages.loadinglanguageprofiles
|
? intl.formatMessage(
|
||||||
)
|
messages.loadinglanguageprofiles
|
||||||
: !isValidated
|
)
|
||||||
? intl.formatMessage(
|
: !isValidated
|
||||||
messages.testFirstLanguageProfiles
|
? intl.formatMessage(
|
||||||
)
|
messages.testFirstLanguageProfiles
|
||||||
: intl.formatMessage(
|
)
|
||||||
messages.selectLanguageProfile
|
: intl.formatMessage(
|
||||||
)}
|
messages.selectLanguageProfile
|
||||||
</option>
|
)}
|
||||||
{testResponse.languageProfiles.length > 0 &&
|
</option>
|
||||||
testResponse.languageProfiles.map((language) => (
|
{testResponse.languageProfiles.length > 0 &&
|
||||||
<option
|
testResponse.languageProfiles.map((language) => (
|
||||||
key={`loaded-profile-${language.id}`}
|
<option
|
||||||
value={language.id}
|
key={`loaded-profile-${language.id}`}
|
||||||
>
|
value={language.id}
|
||||||
{language.name}
|
>
|
||||||
</option>
|
{language.name}
|
||||||
))}
|
</option>
|
||||||
</Field>
|
))}
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
{errors.activeAnimeLanguageProfileId &&
|
||||||
|
touched.activeAnimeLanguageProfileId && (
|
||||||
|
<div className="error">
|
||||||
|
{errors.activeAnimeLanguageProfileId}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{errors.activeAnimeLanguageProfileId &&
|
|
||||||
touched.activeAnimeLanguageProfileId && (
|
|
||||||
<div className="error">
|
|
||||||
{errors.activeAnimeLanguageProfileId}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<label htmlFor="tags" className="text-label">
|
<label htmlFor="tags" className="text-label">
|
||||||
{intl.formatMessage(messages.animeTags)}
|
{intl.formatMessage(messages.animeTags)}
|
||||||
|
|||||||
Reference in New Issue
Block a user