Compare commits

..

16 Commits

Author SHA1 Message Date
JoaquinOlivero
c195a6f720 style: formatting 2024-09-18 03:02:41 +00:00
JoaquinOlivero
f7a4efdde1 fix: avatar images not showing on issues page 2024-09-18 02:45:55 +00:00
JoaquinOlivero
6a475b5e41 fix: remove log and correctly set the if statement for the cached image component 2024-09-18 02:45:43 +00:00
JoaquinOlivero
ae5542b6d3 refactor: only cache avatar with http url protocol 2024-09-18 02:45:41 +00:00
JoaquinOlivero
cbf3bbb17d fix: fix incomplete URL substring sanitization 2024-09-18 02:45:40 +00:00
JoaquinOlivero
14fdd4e293 fix: fix vulnerability 2024-09-18 02:45:39 +00:00
JoaquinOlivero
d2f651081a refactor: checks if the default avatar is cached to avoid creating duplicates for different users 2024-09-18 02:45:35 +00:00
JoaquinOlivero
822ca690da style: grammar 2024-09-18 02:44:04 +00:00
JoaquinOlivero
b2291f5125 refactor: use 'mime' package to detmerine file extension 2024-09-18 02:44:02 +00:00
JoaquinOlivero
84f7a9795e fix: requested changes 2024-09-18 02:44:01 +00:00
JoaquinOlivero
9c5f4fe2f0 fix: remove unexpired unused image when a user changes their avatar 2024-09-18 02:44:00 +00:00
JoaquinOlivero
3af39dd5f0 fix(s): set correct src URL for cached image 2024-09-18 02:43:58 +00:00
JoaquinOlivero
7ad44830d1 fix: show the correct avatar in the list of available users in advanced request 2024-09-18 02:43:57 +00:00
JoaquinOlivero
bd8deedfbe fix: set avatar image URL 2024-09-18 02:43:56 +00:00
JoaquinOlivero
548aaede26 fix: extract keys 2024-09-18 02:43:55 +00:00
JoaquinOlivero
8c7a08b4e9 refactor: proxy and cache user avatar images 2024-09-18 02:43:50 +00:00
65 changed files with 443 additions and 1014 deletions

View File

@@ -439,15 +439,6 @@
"contributions": [
"code"
]
},
{
"login": "M0NsTeRRR",
"name": "Ludovic Ortega",
"avatar_url": "https://avatars.githubusercontent.com/u/37785089?v=4",
"profile": "https://github.com/M0NsTeRRR",
"contributions": [
"security"
]
}
]
}

1
.gitignore vendored
View File

@@ -34,7 +34,6 @@ yarn-error.log*
# database
config/db/*.sqlite3*
config/settings.json
config/settings.old.json
# logs
config/logs/*.log*

View File

@@ -11,7 +11,7 @@
<a href="http://translate.jellyseerr.dev/engage/jellyseerr/"><img src="http://translate.jellyseerr.dev/widget/jellyseerr/jellyseerr-frontend/svg-badge.svg" alt="Translation status" /></a>
<a href="https://github.com/fallenbagel/jellyseerr/blob/develop/LICENSE"><img alt="GitHub" src="https://img.shields.io/github/license/fallenbagel/jellyseerr"></a>
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
<a href="#contributors-"><img alt="All Contributors" src="https://img.shields.io/badge/all_contributors-48-orange.svg"/></a>
<a href="#contributors-"><img alt="All Contributors" src="https://img.shields.io/badge/all_contributors-47-orange.svg"/></a>
<!-- ALL-CONTRIBUTORS-BADGE:END -->
**Jellyseerr** is a free and open source software application for managing requests for your media library.
@@ -146,7 +146,6 @@ Thanks goes to these wonderful people from Overseerr ([emoji key](https://allcon
<td align="center" valign="top" width="14.28%"><a href="https://github.com/XDark187"><img src="https://avatars.githubusercontent.com/u/39034192?v=4?s=100" width="100px;" alt="Baraa"/><br /><sub><b>Baraa</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=XDark187" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/franciscofsales"><img src="https://avatars.githubusercontent.com/u/7977645?v=4?s=100" width="100px;" alt="Francisco Sales"/><br /><sub><b>Francisco Sales</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=franciscofsales" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/myselfolli"><img src="https://avatars.githubusercontent.com/u/37535998?v=4?s=100" width="100px;" alt="Oliver Laing"/><br /><sub><b>Oliver Laing</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=myselfolli" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/M0NsTeRRR"><img src="https://avatars.githubusercontent.com/u/37785089?v=4?s=100" width="100px;" alt="Ludovic Ortega"/><br /><sub><b>Ludovic Ortega</b></sub></a><br /><a href="#security-M0NsTeRRR" title="Security">🛡️</a></td>
</tr>
</tbody>
</table>

View File

@@ -6,10 +6,6 @@ sidebar_position: 4
# AUR (Arch User Repository)
:::note Disclaimer
This AUR package is not maintained by us but by a third party. Please refer to the maintainer for any issues.
:::
:::info
This method is not recommended for most users. It is intended for advanced users who are using Arch Linux or an Arch-based distribution.
:::

View File

@@ -12,12 +12,49 @@ import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
### Prerequisites
<Tabs groupId="versions" queryString>
<TabItem value="latest" label="Latest">
- [Node.js 18.x](https://nodejs.org/en/download/)
- [Yarn 1.x](https://classic.yarnpkg.com/lang/en/docs/install)
- [Git](https://git-scm.com/downloads)
</TabItem>
<TabItem value="develop" label="Develop">
- [Node.js 20.x](https://nodejs.org/en/download/)
- [Pnpm 9.x](https://pnpm.io/installation)
- [Git](https://git-scm.com/downloads)
</TabItem>
</Tabs>
## Unix (Linux, macOS)
### Installation
<Tabs groupId="versions" queryString>
<TabItem value="latest" label="latest">
1. Assuming you want the working directory to be `/opt/jellyseerr`, create the directory and navigate to it:
```bash
sudo mkdir -p /opt/jellyseerr && cd /opt/jellyseerr
```
2. Clone the Jellyseerr repository and checkout the latest release:
```bash
git clone https://github.com/Fallenbagel/jellyseerr.git
cd jellyseerr
git checkout main
```
3. Install the dependencies:
```bash
CYPRESS_INSTALL_BINARY=0 yarn install --frozen-lockfile --network-timeout 1000000
```
4. Build the project:
```bash
yarn build
```
5. Start Jellyseerr:
```bash
yarn start
```
</TabItem>
<TabItem value="develop" label="develop">
1. Assuming you want the working directory to be `/opt/jellyseerr`, create the directory and navigate to it:
```bash
sudo mkdir -p /opt/jellyseerr && cd /opt/jellyseerr
@@ -40,6 +77,8 @@ pnpm build
```bash
pnpm start
```
</TabItem>
</Tabs>
:::info
You can now access Jellyseerr by visiting `http://localhost:5055` in your web browser.
@@ -195,6 +234,33 @@ pm2 status jellyseerr
## Windows
### Installation
<Tabs groupId="versions" queryString>
<TabItem value="latest" label="latest">
1. Assuming you want the working directory to be `C:\jellyseerr`, create the directory and navigate to it:
```powershell
mkdir C:\jellyseerr
cd C:\jellyseerr
```
2. Clone the Jellyseerr repository and checkout the latest release:
```powershell
git clone https://github.com/Fallenbagel/jellyseerr.git .
git checkout main
```
3. Install the dependencies:
```powershell
npm install -g win-node-env
set CYPRESS_INSTALL_BINARY=0 && yarn install --frozen-lockfile --network-timeout 1000000
```
4. Build the project:
```powershell
yarn build
```
5. Start Jellyseerr:
```powershell
yarn start
```
</TabItem>
<TabItem value="develop" label="develop">
1. Assuming you want the working directory to be `C:\jellyseerr`, create the directory and navigate to it:
```powershell
mkdir C:\jellyseerr
@@ -218,6 +284,8 @@ pnpm build
```powershell
pnpm start
```
</TabItem>
</Tabs>
:::tip
You can add the environment variables to a `.env` file in the Jellyseerr directory.
@@ -245,7 +313,6 @@ node dist/index.js
- Set the trigger to "When the computer starts"
- Set the action to "Start a program"
- Set the program/script to the path of the `start-jellyseerr.bat` file
- Set the "Start in" to the jellyseerr directory.
- Click "Finish"
Now, Jellyseerr will start when the computer boots up in the background.

View File

@@ -1988,9 +1988,6 @@ paths:
appDataPath:
type: string
example: /app/config
appDataPermissions:
type: boolean
example: true
/settings/main:
get:
summary: Get main settings

View File

@@ -93,8 +93,7 @@
"sqlite3": "5.1.4",
"swagger-ui-express": "4.6.2",
"swr": "2.2.5",
"typeorm": "0.3.11",
"undici": "^6.20.1",
"typeorm": "0.3.12",
"web-push": "3.5.0",
"winston": "3.8.2",
"winston-daily-rotate-file": "4.7.1",

65
pnpm-lock.yaml generated
View File

@@ -49,7 +49,7 @@ importers:
version: 2.11.0
connect-typeorm:
specifier: 1.1.4
version: 1.1.4(typeorm@0.3.11(sqlite3@5.1.4(encoding@0.1.13))(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5)))
version: 1.1.4(typeorm@0.3.12(sqlite3@5.1.4(encoding@0.1.13))(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5)))
cookie-parser:
specifier: 1.4.6
version: 1.4.6
@@ -192,11 +192,8 @@ importers:
specifier: 2.2.5
version: 2.2.5(react@18.3.1)
typeorm:
specifier: 0.3.11
version: 0.3.11(sqlite3@5.1.4(encoding@0.1.13))(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5))
undici:
specifier: ^6.20.1
version: 6.20.1
specifier: 0.3.12
version: 0.3.12(sqlite3@5.1.4(encoding@0.1.13))(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5))
web-push:
specifier: 3.5.0
version: 3.5.0
@@ -4267,6 +4264,10 @@ packages:
resolution: {integrity: sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==}
engines: {node: '>=0.11'}
date-fns@2.30.0:
resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==}
engines: {node: '>=0.11'}
dateformat@3.0.3:
resolution: {integrity: sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==}
@@ -5388,8 +5389,8 @@ packages:
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
engines: {node: '>= 6'}
https-proxy-agent@7.0.5:
resolution: {integrity: sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==}
https-proxy-agent@7.0.4:
resolution: {integrity: sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==}
engines: {node: '>= 14'}
human-signals@1.1.1:
@@ -6553,6 +6554,11 @@ packages:
engines: {node: '>=10'}
hasBin: true
mkdirp@2.1.6:
resolution: {integrity: sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==}
engines: {node: '>=10'}
hasBin: true
modify-values@1.0.1:
resolution: {integrity: sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==}
engines: {node: '>=0.10.0'}
@@ -7724,6 +7730,9 @@ packages:
reflect-metadata@0.1.13:
resolution: {integrity: sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==}
reflect-metadata@0.1.14:
resolution: {integrity: sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==}
reflect.getprototypeof@1.0.6:
resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==}
engines: {node: '>= 0.4'}
@@ -8661,8 +8670,8 @@ packages:
typedarray@0.0.6:
resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
typeorm@0.3.11:
resolution: {integrity: sha512-pzdOyWbVuz/z8Ww6gqvBW4nylsM0KLdUCDExr2gR20/x1khGSVxQkjNV/3YqliG90jrWzrknYbYscpk8yxFJVg==}
typeorm@0.3.12:
resolution: {integrity: sha512-sYSxBmCf1nJLLTcYtwqZ+lQIRtLPyUoO93rHTOKk9vJCyT4UfRtU7oRsJvfvKP3nnZTD1hzz2SEy2zwPEN6OyA==}
engines: {node: '>= 12.9.0'}
hasBin: true
peerDependencies:
@@ -8673,7 +8682,7 @@ packages:
ioredis: ^5.0.4
mongodb: ^3.6.0
mssql: ^7.3.0
mysql2: ^2.2.5
mysql2: ^2.2.5 || ^3.0.1
oracledb: ^5.1.0
pg: ^8.5.1
pg-native: ^3.0.0
@@ -8759,10 +8768,6 @@ packages:
undici-types@5.26.5:
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
undici@6.20.1:
resolution: {integrity: sha512-AjQF1QsmqfJys+LXfGTNum+qw4S88CojRInG/6t31W/1fk6G59s92bnAvGz5Cmur+kQv2SURXEvvudLmbrE8QA==}
engines: {node: '>=18.17'}
unicode-canonical-property-names-ecmascript@2.0.0:
resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==}
engines: {node: '>=4'}
@@ -12305,7 +12310,7 @@ snapshots:
fs-extra: 11.2.0
globby: 11.1.0
http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.5
https-proxy-agent: 7.0.4
issue-parser: 6.0.0
lodash: 4.17.21
mime: 3.0.0
@@ -13819,13 +13824,13 @@ snapshots:
ini: 1.3.8
proto-list: 1.2.4
connect-typeorm@1.1.4(typeorm@0.3.11(sqlite3@5.1.4(encoding@0.1.13))(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5))):
connect-typeorm@1.1.4(typeorm@0.3.12(sqlite3@5.1.4(encoding@0.1.13))(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5))):
dependencies:
'@types/debug': 0.0.31
'@types/express-session': 1.17.6
debug: 4.3.5(supports-color@8.1.1)
express-session: 1.18.0
typeorm: 0.3.11(sqlite3@5.1.4(encoding@0.1.13))(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5))
typeorm: 0.3.12(sqlite3@5.1.4(encoding@0.1.13))(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5))
transitivePeerDependencies:
- supports-color
@@ -14176,6 +14181,10 @@ snapshots:
date-fns@2.29.3: {}
date-fns@2.30.0:
dependencies:
'@babel/runtime': 7.24.7
dateformat@3.0.3: {}
dayjs@1.11.11: {}
@@ -15730,7 +15739,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
https-proxy-agent@7.0.5:
https-proxy-agent@7.0.4:
dependencies:
agent-base: 7.1.1
debug: 4.3.5(supports-color@8.1.1)
@@ -17140,6 +17149,8 @@ snapshots:
mkdirp@1.0.4: {}
mkdirp@2.1.6: {}
modify-values@1.0.1: {}
moment@2.30.1: {}
@@ -18361,6 +18372,8 @@ snapshots:
reflect-metadata@0.1.13: {}
reflect-metadata@0.1.14: {}
reflect.getprototypeof@1.0.6:
dependencies:
call-bind: 1.0.7
@@ -19418,23 +19431,23 @@ snapshots:
typedarray@0.0.6: {}
typeorm@0.3.11(sqlite3@5.1.4(encoding@0.1.13))(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5)):
typeorm@0.3.12(sqlite3@5.1.4(encoding@0.1.13))(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5)):
dependencies:
'@sqltools/formatter': 1.2.5
app-root-path: 3.1.0
buffer: 6.0.3
chalk: 4.1.2
cli-highlight: 2.1.11
date-fns: 2.29.3
date-fns: 2.30.0
debug: 4.3.5(supports-color@8.1.1)
dotenv: 16.4.5
glob: 7.2.3
glob: 8.1.0
js-yaml: 4.1.0
mkdirp: 1.0.4
reflect-metadata: 0.1.13
mkdirp: 2.1.6
reflect-metadata: 0.1.14
sha.js: 2.4.11
tslib: 2.6.3
uuid: 8.3.2
uuid: 9.0.1
xml2js: 0.4.23
yargs: 17.7.2
optionalDependencies:
@@ -19473,8 +19486,6 @@ snapshots:
undici-types@5.26.5: {}
undici@6.20.1: {}
unicode-canonical-property-names-ecmascript@2.0.0: {}
unicode-emoji-utils@1.2.0:

View File

@@ -32,27 +32,13 @@ class ExternalAPI {
this.fetch = fetch;
}
const url = new URL(baseUrl);
this.baseUrl = baseUrl;
this.params = params;
this.defaultHeaders = {
'Content-Type': 'application/json',
Accept: 'application/json',
...((url.username || url.password) && {
Authorization: `Basic ${Buffer.from(
`${url.username}:${url.password}`
).toString('base64')}`,
}),
...options.headers,
};
if (url.username || url.password) {
url.username = '';
url.password = '';
baseUrl = url.toString();
}
this.baseUrl = baseUrl;
this.params = params;
this.cache = options.nodeCache;
}
@@ -90,7 +76,7 @@ class ExternalAPI {
}
const data = await this.getDataFromResponse(response);
if (this.cache && ttl !== 0) {
if (this.cache) {
this.cache.set(cacheKey, data, ttl ?? DEFAULT_TTL);
}
@@ -134,7 +120,7 @@ class ExternalAPI {
}
const resData = await this.getDataFromResponse(response);
if (this.cache && ttl !== 0) {
if (this.cache) {
this.cache.set(cacheKey, resData, ttl ?? DEFAULT_TTL);
}
@@ -178,7 +164,7 @@ class ExternalAPI {
}
const resData = await this.getDataFromResponse(response);
if (this.cache && ttl !== 0) {
if (this.cache) {
this.cache.set(cacheKey, resData, ttl ?? DEFAULT_TTL);
}

View File

@@ -410,7 +410,7 @@ class JellyfinAPI extends ExternalAPI {
).AccessToken;
} catch (e) {
logger.error(
`Something went wrong while creating an API key from the Jellyfin server: ${e.message}`,
`Something went wrong while creating an API key the Jellyfin server: ${e.message}`,
{ label: 'Jellyfin API' }
);

View File

@@ -180,7 +180,7 @@ class PlexAPI {
settings.plex.libraries = [];
}
await settings.save();
settings.save();
}
public async getLibraryContents(

View File

@@ -182,7 +182,7 @@ class RottenTomatoes extends ExternalAPI {
);
}
if (!tvshow || !tvshow.rottenTomatoes) {
if (!tvshow) {
return null;
}

View File

@@ -157,13 +157,9 @@ class ServarrBase<QueueItemAppendT> extends ExternalAPI {
public getQueue = async (): Promise<(QueueItem & QueueItemAppendT)[]> => {
try {
const data = await this.get<QueueResponse<QueueItemAppendT>>(
`/queue`,
{
includeEpisode: 'true',
},
0
);
const data = await this.get<QueueResponse<QueueItemAppendT>>(`/queue`, {
includeEpisode: 'true',
});
return data.records;
} catch (e) {
@@ -197,24 +193,15 @@ class ServarrBase<QueueItemAppendT> extends ExternalAPI {
}
};
async refreshMonitoredDownloads(): Promise<void> {
await this.runCommand('RefreshMonitoredDownloads', {});
}
protected async runCommand(
commandName: string,
options: Record<string, unknown>
): Promise<void> {
try {
await this.post(
`/command`,
{
name: commandName,
...options,
},
{},
0
);
await this.post(`/command`, {
name: commandName,
...options,
});
} catch (e) {
throw new Error(`[${this.apiName}] Failed to run command: ${e.message}`);
}

View File

@@ -231,7 +231,7 @@ class Media {
this.mediaUrl = `${jellyfinHost}/web/index.html#!/${pageName}?id=${this.jellyfinMediaId}&context=home&serverId=${serverId}`;
}
if (this.jellyfinMediaId4k) {
this.mediaUrl4k = `${jellyfinHost}/web/index.html#!/${pageName}?id=${this.jellyfinMediaId4k}&context=home&serverId=${serverId}`;
this.mediaUrl4k = `${jellyfinHost}/web/index.html#!/${pageName}?id=${this.jellyfinMediaId}&context=home&serverId=${serverId}`;
}
}
}

View File

@@ -21,9 +21,7 @@ import clearCookies from '@server/middleware/clearcookies';
import routes from '@server/routes';
import avatarproxy from '@server/routes/avatarproxy';
import imageproxy from '@server/routes/imageproxy';
import { appDataPermissions } from '@server/utils/appDataVolume';
import { getAppVersion } from '@server/utils/appVersion';
import createCustomProxyAgent from '@server/utils/customProxyAgent';
import restartFlag from '@server/utils/restartFlag';
import { getClientIp } from '@supercharge/request-ip';
import { TypeormStore } from 'connect-typeorm/out';
@@ -53,12 +51,6 @@ const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();
if (!appDataPermissions()) {
logger.error(
'Something went wrong while checking config folder! Please ensure the config folder is set up properly.\nhttps://docs.jellyseerr.dev/getting-started'
);
}
app
.prepare()
.then(async () => {
@@ -75,11 +67,6 @@ app
const settings = await getSettings().load();
restartFlag.initializeSettings(settings.main);
// Register HTTP proxy
if (settings.main.proxy.enabled) {
await createCustomProxyAgent(settings.main.proxy);
}
// Migrate library types
if (
settings.plex.libraries.length > 1 &&
@@ -188,7 +175,7 @@ app
},
store: new TypeormStore({
cleanupLimit: 2,
ttl: 60 * 60 * 24 * 30,
ttl: 1000 * 60 * 60 * 24 * 30,
}).connect(sessionRespository) as Store,
})
);

View File

@@ -85,7 +85,6 @@ class DownloadTracker {
});
try {
await radarr.refreshMonitoredDownloads();
const queueItems = await radarr.getQueue();
this.radarrServers[server.id] = queueItems.map((item) => ({
@@ -163,7 +162,6 @@ class DownloadTracker {
});
try {
await sonarr.refreshMonitoredDownloads();
const queueItems = await sonarr.getQueue();
this.sonarrServers[server.id] = queueItems.map((item) => ({

View File

@@ -135,7 +135,6 @@ class ImageProxy {
private cacheVersion;
private key;
private baseUrl;
private headers: HeadersInit | null = null;
constructor(
key: string,
@@ -143,7 +142,6 @@ class ImageProxy {
options: {
cacheVersion?: number;
rateLimitOptions?: RateLimitOptions;
headers?: HeadersInit;
} = {}
) {
this.cacheVersion = options.cacheVersion ?? 1;
@@ -157,13 +155,9 @@ class ImageProxy {
} else {
this.fetch = fetch;
}
this.headers = options.headers || null;
}
public async getImage(
path: string,
fallbackPath?: string
): Promise<ImageResponse> {
public async getImage(path: string): Promise<ImageResponse> {
const cacheKey = this.getCacheKey(path);
const imageResponse = await this.get(cacheKey);
@@ -172,11 +166,7 @@ class ImageProxy {
const newImage = await this.set(path, cacheKey);
if (!newImage) {
if (fallbackPath) {
return await this.getImage(fallbackPath);
} else {
throw new Error('Failed to load image');
}
throw new Error('Failed to load image');
}
return newImage;
@@ -257,12 +247,7 @@ class ImageProxy {
: '/'
: '') +
(path.startsWith('/') ? path.slice(1) : path);
const response = await this.fetch(href, {
headers: this.headers || undefined,
});
if (!response.ok) {
return null;
}
const response = await this.fetch(href);
const arrayBuffer = await response.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);

View File

@@ -129,7 +129,7 @@ class PlexScanner
});
settings.plex.libraries = newLibraries;
await settings.save();
settings.save();
}
} else {
for (const library of this.libraries) {

View File

@@ -2,7 +2,7 @@ import { MediaServerType } from '@server/constants/server';
import { Permission } from '@server/lib/permissions';
import { runMigrations } from '@server/lib/settings/migrator';
import { randomUUID } from 'crypto';
import fs from 'fs/promises';
import fs from 'fs';
import { merge } from 'lodash';
import path from 'path';
import webpush from 'web-push';
@@ -99,17 +99,6 @@ interface Quota {
quotaDays?: number;
}
export interface ProxySettings {
enabled: boolean;
hostname: string;
port: number;
useSsl: boolean;
user: string;
password: string;
bypassFilter: string;
bypassLocalAddresses: boolean;
}
export interface MainSettings {
apiKey: string;
applicationTitle: string;
@@ -130,7 +119,6 @@ export interface MainSettings {
mediaServerType: number;
partialRequestsEnabled: boolean;
locale: string;
proxy: ProxySettings;
}
interface PublicSettings {
@@ -337,16 +325,6 @@ class Settings {
mediaServerType: MediaServerType.NOT_CONFIGURED,
partialRequestsEnabled: true,
locale: 'en',
proxy: {
enabled: false,
hostname: '',
port: 8080,
useSsl: false,
user: '',
password: '',
bypassFilter: '',
bypassLocalAddresses: true,
},
},
plex: {
name: '',
@@ -501,6 +479,10 @@ class Settings {
}
get main(): MainSettings {
if (!this.data.main.apiKey) {
this.data.main.apiKey = this.generateApiKey();
this.save();
}
return this.data.main;
}
@@ -602,20 +584,29 @@ class Settings {
}
get clientId(): string {
if (!this.data.clientId) {
this.data.clientId = randomUUID();
this.save();
}
return this.data.clientId;
}
get vapidPublic(): string {
this.generateVapidKeys();
return this.data.vapidPublic;
}
get vapidPrivate(): string {
this.generateVapidKeys();
return this.data.vapidPrivate;
}
public async regenerateApiKey(): Promise<MainSettings> {
public regenerateApiKey(): MainSettings {
this.main.apiKey = this.generateApiKey();
await this.save();
this.save();
return this.main;
}
@@ -627,6 +618,15 @@ class Settings {
}
}
private generateVapidKeys(force = false): void {
if (!this.data.vapidPublic || !this.data.vapidPrivate || force) {
const vapidKeys = webpush.generateVAPIDKeys();
this.data.vapidPrivate = vapidKeys.privateKey;
this.data.vapidPublic = vapidKeys.publicKey;
this.save();
}
}
/**
* Settings Load
*
@@ -641,51 +641,30 @@ class Settings {
return this;
}
let data;
try {
data = await fs.readFile(SETTINGS_PATH, 'utf-8');
} catch {
await this.save();
if (!fs.existsSync(SETTINGS_PATH)) {
this.save();
}
const data = fs.readFileSync(SETTINGS_PATH, 'utf-8');
if (data) {
const parsedJson = JSON.parse(data);
const migratedData = await runMigrations(parsedJson, SETTINGS_PATH);
this.data = merge(this.data, migratedData);
}
this.data = await runMigrations(parsedJson);
// generate keys and ids if it's missing
let change = false;
if (!this.data.main.apiKey) {
this.data.main.apiKey = this.generateApiKey();
change = true;
} else if (process.env.API_KEY) {
if (this.main.apiKey != process.env.API_KEY) {
this.main.apiKey = process.env.API_KEY;
this.data = merge(this.data, parsedJson);
if (process.env.API_KEY) {
if (this.main.apiKey != process.env.API_KEY) {
this.main.apiKey = process.env.API_KEY;
}
}
}
if (!this.data.clientId) {
this.data.clientId = randomUUID();
change = true;
}
if (!this.data.vapidPublic || !this.data.vapidPrivate) {
const vapidKeys = webpush.generateVAPIDKeys();
this.data.vapidPrivate = vapidKeys.privateKey;
this.data.vapidPublic = vapidKeys.publicKey;
change = true;
}
if (change) {
await this.save();
}
this.save();
}
return this;
}
public async save(): Promise<void> {
await fs.writeFile(
SETTINGS_PATH,
JSON.stringify(this.data, undefined, ' ')
);
public save(): void {
fs.writeFileSync(SETTINGS_PATH, JSON.stringify(this.data, undefined, ' '));
}
}

View File

@@ -1,14 +1,15 @@
import type { AllSettings } from '@server/lib/settings';
const migrateHostname = (settings: any): AllSettings => {
if (settings.jellyfin?.hostname) {
const { hostname } = settings.jellyfin;
const oldJellyfinSettings = settings.jellyfin;
if (oldJellyfinSettings && oldJellyfinSettings.hostname) {
const { hostname } = oldJellyfinSettings;
const protocolMatch = hostname.match(/^(https?):\/\//i);
const useSsl = protocolMatch && protocolMatch[1].toLowerCase() === 'https';
const remainingUrl = hostname.replace(/^(https?):\/\//i, '');
const urlMatch = remainingUrl.match(/^([^:]+)(:([0-9]+))?(\/.*)?$/);
delete settings.jellyfin.hostname;
delete oldJellyfinSettings.hostname;
if (urlMatch) {
const [, ip, , port, urlBase] = urlMatch;
settings.jellyfin = {
@@ -20,7 +21,9 @@ const migrateHostname = (settings: any): AllSettings => {
};
}
}
if (settings.jellyfin && settings.jellyfin.hostname) {
delete settings.jellyfin.hostname;
}
return settings;
};

View File

@@ -27,14 +27,8 @@ const migrateApiTokens = async (settings: any): Promise<AllSettings> => {
admin.jellyfinDeviceId
);
jellyfinClient.setUserId(admin.jellyfinUserId ?? '');
try {
const apiKey = await jellyfinClient.createApiToken('Jellyseerr');
settings.jellyfin.apiKey = apiKey;
} catch {
throw new Error(
"Failed to create Jellyfin API token from admin account. Please check your network configuration or edit your settings.json by adding an 'apiKey' field inside of the 'jellyfin' section to fix this issue."
);
}
const apiKey = await jellyfinClient.createApiToken('Jellyseerr');
settings.jellyfin.apiKey = apiKey;
}
return settings;
};

View File

@@ -1,100 +1,30 @@
import type { AllSettings } from '@server/lib/settings';
import logger from '@server/logger';
import fs from 'fs/promises';
import fs from 'fs';
import path from 'path';
const migrationsDir = path.join(__dirname, 'migrations');
export const runMigrations = async (
settings: AllSettings,
SETTINGS_PATH: string
settings: 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;
try {
// we read old backup and create a backup of currents settings
const BACKUP_PATH = SETTINGS_PATH.replace('.json', '.old.json');
let oldBackup: string | null = null;
try {
oldBackup = await fs.readFile(BACKUP_PATH, 'utf-8');
} catch {
/* empty */
}
await fs.writeFile(BACKUP_PATH, JSON.stringify(settings, undefined, ' '));
const migrations = (await fs.readdir(migrationsDir)).filter(
(file) => file.endsWith('.js') || file.endsWith('.ts')
);
const settingsBefore = JSON.stringify(migrated);
for (const migration of migrations) {
try {
logger.debug(`Checking migration '${migration}'...`, {
label: 'Settings Migrator',
});
const { default: migrationFn } = await import(
path.join(migrationsDir, migration)
);
const newSettings = await migrationFn(structuredClone(migrated));
if (JSON.stringify(migrated) !== JSON.stringify(newSettings)) {
logger.debug(`Migration '${migration}' has been applied.`, {
label: 'Settings Migrator',
});
}
migrated = newSettings;
} catch (e) {
// we stop jellyseerr if the migration failed
logger.error(
`Error while running migration '${migration}': ${e.message}`,
{
label: 'Settings Migrator',
}
);
logger.error(
'A common cause for this error is a permission issue with your configuration folder, a network issue or a corrupted database.',
{
label: 'Settings Migrator',
}
);
process.exit();
}
}
const settingsAfter = JSON.stringify(migrated);
if (settingsBefore !== settingsAfter) {
// a migration occured
// we check that the new config will be saved
await fs.writeFile(
SETTINGS_PATH,
JSON.stringify(migrated, undefined, ' ')
);
const fileSaved = JSON.parse(await fs.readFile(SETTINGS_PATH, 'utf-8'));
if (JSON.stringify(fileSaved) !== settingsAfter) {
// something went wrong while saving file
throw new Error('Unable to save settings after migration.');
}
} else if (oldBackup) {
// no migration occured
// we save the old backup (to avoid settings.json and settings.old.json being the same)
await fs.writeFile(BACKUP_PATH, oldBackup.toString());
migrated = await migration(migrated);
}
} catch (e) {
// we stop jellyseerr if the migration failed
logger.error(
`Something went wrong while running settings migrations: ${e.message}`,
{
label: 'Settings Migrator',
}
{ label: 'Settings Migrator' }
);
logger.error(
'A common cause for this issue is a permission error of your configuration folder.',
{
label: 'Settings Migrator',
}
);
process.exit();
}
return migrated;

View File

@@ -6,6 +6,7 @@ import { UserType } from '@server/constants/user';
import { getRepository } from '@server/datasource';
import { User } from '@server/entity/User';
import { startJobs } from '@server/job/schedule';
import ImageProxy from '@server/lib/imageproxy';
import { Permission } from '@server/lib/permissions';
import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
@@ -14,6 +15,7 @@ import { ApiError } from '@server/types/error';
import { getHostname } from '@server/utils/getHostname';
import * as EmailValidator from 'email-validator';
import { Router } from 'express';
import gravatarUrl from 'gravatar-url';
import net from 'net';
const authRoutes = Router();
@@ -87,7 +89,7 @@ authRoutes.post('/plex', async (req, res, next) => {
});
settings.main.mediaServerType = MediaServerType.PLEX;
await settings.save();
settings.save();
startJobs();
await userRepository.save(user);
@@ -260,6 +262,8 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
urlBase: body.urlBase,
});
const { externalHostname } = getSettings().jellyfin;
// Try to find deviceId that corresponds to jellyfin user, else generate a new one
let user = await userRepository.findOne({
where: { jellyfinUsername: body.username },
@@ -277,6 +281,11 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
// First we need to attempt to log the user in to jellyfin
const jellyfinserver = new JellyfinAPI(hostname ?? '', undefined, deviceId);
const jellyfinHost =
externalHostname && externalHostname.length > 0
? externalHostname
: hostname;
const ip = req.ip;
let clientIp;
@@ -326,7 +335,12 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
jellyfinDeviceId: deviceId,
jellyfinAuthToken: account.AccessToken,
permissions: Permission.ADMIN,
avatar: `/avatarproxy/${account.User.Id}`,
avatar: account.User.PrimaryImageTag
? `${jellyfinHost}/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,
});
@@ -340,7 +354,12 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
jellyfinDeviceId: deviceId,
jellyfinAuthToken: account.AccessToken,
permissions: Permission.ADMIN,
avatar: `/avatarproxy/${account.User.Id}`,
avatar: account.User.PrimaryImageTag
? `${jellyfinHost}/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,
});
@@ -366,7 +385,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
settings.jellyfin.urlBase = body.urlBase ?? '';
settings.jellyfin.useSsl = body.useSsl ?? false;
settings.jellyfin.apiKey = apiKey;
await settings.save();
settings.save();
startJobs();
await userRepository.save(user);
@@ -389,7 +408,27 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
jellyfinUsername: account.User.Name,
}
);
user.avatar = `/avatarproxy/${account.User.Id}`;
// Update the users avatar with their jellyfin profile pic (incase it changed)
if (account.User.PrimaryImageTag) {
const avatar = `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`;
if (avatar !== user.avatar) {
const avatarProxy = new ImageProxy('avatar', '');
avatarProxy.clearCachedImage(user.avatar);
}
user.avatar = avatar;
} else {
const avatar = gravatarUrl(user.email || account.User.Name, {
default: 'mm',
size: 200,
});
if (avatar !== user.avatar) {
const avatarProxy = new ImageProxy('avatar', '');
avatarProxy.clearCachedImage(user.avatar);
}
user.avatar = avatar;
}
user.jellyfinUsername = account.User.Name;
if (user.username === account.User.Name) {
@@ -427,7 +466,12 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
jellyfinUserId: account.User.Id,
jellyfinDeviceId: deviceId,
permissions: settings.main.defaultPermissions,
avatar: `/avatarproxy/${account.User.Id}`,
avatar: account.User.PrimaryImageTag
? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
: gravatarUrl(body.email || account.User.Name, {
default: 'mm',
size: 200,
}),
userType:
settings.main.mediaServerType === MediaServerType.JELLYFIN
? UserType.JELLYFIN

View File

@@ -1,71 +1,16 @@
import { MediaServerType } from '@server/constants/server';
import { getRepository } from '@server/datasource';
import { User } from '@server/entity/User';
import ImageProxy from '@server/lib/imageproxy';
import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
import { getAppVersion } from '@server/utils/appVersion';
import { getHostname } from '@server/utils/getHostname';
import { Router } from 'express';
import gravatarUrl from 'gravatar-url';
const router = Router();
let _avatarImageProxy: ImageProxy | null = null;
async function initAvatarImageProxy() {
if (!_avatarImageProxy) {
const userRepository = getRepository(User);
const admin = await userRepository.findOne({
where: { id: 1 },
select: ['id', 'jellyfinUserId', 'jellyfinDeviceId'],
order: { id: 'ASC' },
});
const deviceId = admin?.jellyfinDeviceId;
const authToken = getSettings().jellyfin.apiKey;
_avatarImageProxy = new ImageProxy('avatar', '', {
headers: {
'X-Emby-Authorization': `MediaBrowser Client="Jellyseerr", Device="Jellyseerr", DeviceId="${deviceId}", Version="${getAppVersion()}", Token="${authToken}"`,
},
});
}
return _avatarImageProxy;
}
const avatarImageProxy = new ImageProxy('avatar', '');
// Proxy avatar images
router.get('/*', async (req, res) => {
const imagePath = req.url.startsWith('/') ? req.url.slice(1) : req.url;
router.get('/:jellyfinUserId', async (req, res) => {
try {
if (!req.params.jellyfinUserId.match(/^[a-f0-9]{32}$/)) {
const mediaServerType = getSettings().main.mediaServerType;
throw new Error(
`Provided URL is not ${
mediaServerType === MediaServerType.JELLYFIN
? 'a Jellyfin'
: 'an Emby'
} avatar.`
);
}
const avatarImageCache = await initAvatarImageProxy();
const user = await getRepository(User).findOne({
where: { jellyfinUserId: req.params.jellyfinUserId },
});
const fallbackUrl = gravatarUrl(user?.email || 'none', {
default: 'mm',
size: 200,
});
const jellyfinAvatarUrl = `${getHostname()}/UserImage?UserId=${
req.params.jellyfinUserId
}`;
let imageData = await avatarImageCache.getImage(
jellyfinAvatarUrl,
fallbackUrl
);
if (imageData.meta.extension === 'json') {
// this is a 404
imageData = await avatarImageCache.getImage(fallbackUrl);
}
const imageData = await avatarImageProxy.getImage(imagePath);
res.writeHead(200, {
'Content-Type': `image/${imageData.meta.extension}`,
@@ -78,6 +23,7 @@ router.get('/:jellyfinUserId', async (req, res) => {
res.end(imageData.imageBuffer);
} catch (e) {
logger.error('Failed to proxy avatar image', {
imagePath,
errorMessage: e.message,
});
}

View File

@@ -17,11 +17,7 @@ import { mapProductionCompany } from '@server/models/Movie';
import { mapNetwork } from '@server/models/Tv';
import settingsRoutes from '@server/routes/settings';
import watchlistRoutes from '@server/routes/watchlist';
import {
appDataPath,
appDataPermissions,
appDataStatus,
} from '@server/utils/appDataVolume';
import { appDataPath, appDataStatus } from '@server/utils/appDataVolume';
import { getAppVersion, getCommitTag } from '@server/utils/appVersion';
import restartFlag from '@server/utils/restartFlag';
import { isPerson } from '@server/utils/typeHelpers';
@@ -97,7 +93,6 @@ router.get('/status/appdata', (_req, res) => {
return res.status(200).json({
appData: appDataStatus(),
appDataPath: appDataPath(),
appDataPermissions: appDataPermissions(),
});
});

View File

@@ -123,13 +123,9 @@ serviceRoutes.get<{ sonarrId: string }>(
});
try {
const systemStatus = await sonarr.getSystemStatus();
const sonarrMajorVersion = Number(systemStatus.version.split('.')[0]);
const profiles = await sonarr.getProfiles();
const rootFolders = await sonarr.getRootFolders();
const languageProfiles =
sonarrMajorVersion <= 3 ? await sonarr.getLanguageProfiles() : null;
const languageProfiles = await sonarr.getLanguageProfiles();
const tags = await sonarr.getTags();
return res.status(200).json({

View File

@@ -32,6 +32,7 @@ import { getHostname } from '@server/utils/getHostname';
import { Router } from 'express';
import rateLimit from 'express-rate-limit';
import fs from 'fs';
import gravatarUrl from 'gravatar-url';
import { escapeRegExp, merge, omit, set, sortBy } from 'lodash';
import { rescheduleJob } from 'node-schedule';
import path from 'path';
@@ -69,19 +70,19 @@ settingsRoutes.get('/main', (req, res, next) => {
res.status(200).json(filteredMainSettings(req.user, settings.main));
});
settingsRoutes.post('/main', async (req, res) => {
settingsRoutes.post('/main', (req, res) => {
const settings = getSettings();
settings.main = merge(settings.main, req.body);
await settings.save();
settings.save();
return res.status(200).json(settings.main);
});
settingsRoutes.post('/main/regenerate', async (req, res, next) => {
settingsRoutes.post('/main/regenerate', (req, res, next) => {
const settings = getSettings();
const main = await settings.regenerateApiKey();
const main = settings.regenerateApiKey();
if (!req.user) {
return next({ status: 500, message: 'User missing from request.' });
@@ -118,7 +119,7 @@ settingsRoutes.post('/plex', async (req, res, next) => {
settings.plex.machineId = result.MediaContainer.machineIdentifier;
settings.plex.name = result.MediaContainer.friendlyName;
await settings.save();
settings.save();
} catch (e) {
logger.error('Something went wrong testing Plex connection', {
label: 'API',
@@ -231,7 +232,7 @@ settingsRoutes.get('/plex/library', async (req, res) => {
...library,
enabled: enabledLibraries.includes(library.id),
}));
await settings.save();
settings.save();
return res.status(200).json(settings.plex.libraries);
});
@@ -282,7 +283,7 @@ settingsRoutes.post('/jellyfin', async (req, res, next) => {
Object.assign(settings.jellyfin, req.body);
settings.jellyfin.serverId = result.Id;
settings.jellyfin.name = result.ServerName;
await settings.save();
settings.save();
} catch (e) {
if (e instanceof ApiError) {
logger.error('Something went wrong testing Jellyfin connection', {
@@ -370,12 +371,17 @@ settingsRoutes.get('/jellyfin/library', async (req, res, next) => {
...library,
enabled: enabledLibraries.includes(library.id),
}));
await settings.save();
settings.save();
return res.status(200).json(settings.jellyfin.libraries);
});
settingsRoutes.get('/jellyfin/users', async (req, res) => {
const settings = getSettings();
const { externalHostname } = settings.jellyfin;
const jellyfinHost =
externalHostname && externalHostname.length > 0
? externalHostname
: getHostname();
const userRepository = getRepository(User);
const admin = await userRepository.findOneOrFail({
@@ -394,7 +400,9 @@ settingsRoutes.get('/jellyfin/users', async (req, res) => {
const users = resp.users.map((user) => ({
username: user.Name,
id: user.Id,
thumb: `/avatarproxy/${user.Id}`,
thumb: user.PrimaryImageTag
? `${jellyfinHost}/Users/${user.Id}/Images/Primary/?tag=${user.PrimaryImageTag}&quality=90`
: gravatarUrl(user.Name, { default: 'mm', size: 200 }),
email: user.Name,
}));
@@ -434,7 +442,7 @@ settingsRoutes.post('/tautulli', async (req, res, next) => {
throw new Error('Tautulli version not supported');
}
await settings.save();
settings.save();
} catch (e) {
logger.error('Something went wrong testing Tautulli connection', {
label: 'API',
@@ -695,7 +703,7 @@ settingsRoutes.post<{ jobId: JobId }>(
settingsRoutes.post<{ jobId: JobId }>(
'/jobs/:jobId/schedule',
async (req, res, next) => {
(req, res, next) => {
const scheduledJob = scheduledJobs.find(
(job) => job.id === req.params.jobId
);
@@ -709,7 +717,7 @@ settingsRoutes.post<{ jobId: JobId }>(
if (result) {
settings.jobs[scheduledJob.id].schedule = req.body.schedule;
await settings.save();
settings.save();
scheduledJob.cronSchedule = req.body.schedule;
@@ -766,11 +774,11 @@ settingsRoutes.post<{ cacheId: AvailableCacheIds }>(
settingsRoutes.post(
'/initialize',
isAuthenticated(Permission.ADMIN),
async (_req, res) => {
(_req, res) => {
const settings = getSettings();
settings.public.initialized = true;
await settings.save();
settings.save();
return res.status(200).json(settings.public);
}

View File

@@ -31,11 +31,11 @@ notificationRoutes.get('/discord', (_req, res) => {
res.status(200).json(settings.notifications.agents.discord);
});
notificationRoutes.post('/discord', async (req, res) => {
notificationRoutes.post('/discord', (req, res) => {
const settings = getSettings();
settings.notifications.agents.discord = req.body;
await settings.save();
settings.save();
res.status(200).json(settings.notifications.agents.discord);
});
@@ -65,11 +65,11 @@ notificationRoutes.get('/slack', (_req, res) => {
res.status(200).json(settings.notifications.agents.slack);
});
notificationRoutes.post('/slack', async (req, res) => {
notificationRoutes.post('/slack', (req, res) => {
const settings = getSettings();
settings.notifications.agents.slack = req.body;
await settings.save();
settings.save();
res.status(200).json(settings.notifications.agents.slack);
});
@@ -99,11 +99,11 @@ notificationRoutes.get('/telegram', (_req, res) => {
res.status(200).json(settings.notifications.agents.telegram);
});
notificationRoutes.post('/telegram', async (req, res) => {
notificationRoutes.post('/telegram', (req, res) => {
const settings = getSettings();
settings.notifications.agents.telegram = req.body;
await settings.save();
settings.save();
res.status(200).json(settings.notifications.agents.telegram);
});
@@ -133,11 +133,11 @@ notificationRoutes.get('/pushbullet', (_req, res) => {
res.status(200).json(settings.notifications.agents.pushbullet);
});
notificationRoutes.post('/pushbullet', async (req, res) => {
notificationRoutes.post('/pushbullet', (req, res) => {
const settings = getSettings();
settings.notifications.agents.pushbullet = req.body;
await settings.save();
settings.save();
res.status(200).json(settings.notifications.agents.pushbullet);
});
@@ -167,11 +167,11 @@ notificationRoutes.get('/pushover', (_req, res) => {
res.status(200).json(settings.notifications.agents.pushover);
});
notificationRoutes.post('/pushover', async (req, res) => {
notificationRoutes.post('/pushover', (req, res) => {
const settings = getSettings();
settings.notifications.agents.pushover = req.body;
await settings.save();
settings.save();
res.status(200).json(settings.notifications.agents.pushover);
});
@@ -201,11 +201,11 @@ notificationRoutes.get('/email', (_req, res) => {
res.status(200).json(settings.notifications.agents.email);
});
notificationRoutes.post('/email', async (req, res) => {
notificationRoutes.post('/email', (req, res) => {
const settings = getSettings();
settings.notifications.agents.email = req.body;
await settings.save();
settings.save();
res.status(200).json(settings.notifications.agents.email);
});
@@ -235,11 +235,11 @@ notificationRoutes.get('/webpush', (_req, res) => {
res.status(200).json(settings.notifications.agents.webpush);
});
notificationRoutes.post('/webpush', async (req, res) => {
notificationRoutes.post('/webpush', (req, res) => {
const settings = getSettings();
settings.notifications.agents.webpush = req.body;
await settings.save();
settings.save();
res.status(200).json(settings.notifications.agents.webpush);
});
@@ -284,7 +284,7 @@ notificationRoutes.get('/webhook', (_req, res) => {
res.status(200).json(response);
});
notificationRoutes.post('/webhook', async (req, res, next) => {
notificationRoutes.post('/webhook', (req, res, next) => {
const settings = getSettings();
try {
JSON.parse(req.body.options.jsonPayload);
@@ -300,7 +300,7 @@ notificationRoutes.post('/webhook', async (req, res, next) => {
authHeader: req.body.options.authHeader,
},
};
await settings.save();
settings.save();
res.status(200).json(settings.notifications.agents.webhook);
} catch (e) {
@@ -351,11 +351,11 @@ notificationRoutes.get('/lunasea', (_req, res) => {
res.status(200).json(settings.notifications.agents.lunasea);
});
notificationRoutes.post('/lunasea', async (req, res) => {
notificationRoutes.post('/lunasea', (req, res) => {
const settings = getSettings();
settings.notifications.agents.lunasea = req.body;
await settings.save();
settings.save();
res.status(200).json(settings.notifications.agents.lunasea);
});
@@ -385,11 +385,11 @@ notificationRoutes.get('/gotify', (_req, res) => {
res.status(200).json(settings.notifications.agents.gotify);
});
notificationRoutes.post('/gotify', async (req, res) => {
notificationRoutes.post('/gotify', (req, res) => {
const settings = getSettings();
settings.notifications.agents.gotify = req.body;
await settings.save();
settings.save();
res.status(200).json(settings.notifications.agents.gotify);
});

View File

@@ -12,7 +12,7 @@ radarrRoutes.get('/', (_req, res) => {
res.status(200).json(settings.radarr);
});
radarrRoutes.post('/', async (req, res) => {
radarrRoutes.post('/', (req, res) => {
const settings = getSettings();
const newRadarr = req.body as RadarrSettings;
@@ -31,7 +31,7 @@ radarrRoutes.post('/', async (req, res) => {
}
settings.radarr = [...settings.radarr, newRadarr];
await settings.save();
settings.save();
return res.status(201).json(newRadarr);
});
@@ -76,7 +76,7 @@ radarrRoutes.post<
radarrRoutes.put<{ id: string }, RadarrSettings, RadarrSettings>(
'/:id',
async (req, res, next) => {
(req, res, next) => {
const settings = getSettings();
const radarrIndex = settings.radarr.findIndex(
@@ -102,7 +102,7 @@ radarrRoutes.put<{ id: string }, RadarrSettings, RadarrSettings>(
...req.body,
id: Number(req.params.id),
} as RadarrSettings;
await settings.save();
settings.save();
return res.status(200).json(settings.radarr[radarrIndex]);
}
@@ -134,7 +134,7 @@ radarrRoutes.get<{ id: string }>('/:id/profiles', async (req, res, next) => {
);
});
radarrRoutes.delete<{ id: string }>('/:id', async (req, res, next) => {
radarrRoutes.delete<{ id: string }>('/:id', (req, res, next) => {
const settings = getSettings();
const radarrIndex = settings.radarr.findIndex(
@@ -146,7 +146,7 @@ radarrRoutes.delete<{ id: string }>('/:id', async (req, res, next) => {
}
const removed = settings.radarr.splice(radarrIndex, 1);
await settings.save();
settings.save();
return res.status(200).json(removed[0]);
});

View File

@@ -12,7 +12,7 @@ sonarrRoutes.get('/', (_req, res) => {
res.status(200).json(settings.sonarr);
});
sonarrRoutes.post('/', async (req, res) => {
sonarrRoutes.post('/', (req, res) => {
const settings = getSettings();
const newSonarr = req.body as SonarrSettings;
@@ -31,7 +31,7 @@ sonarrRoutes.post('/', async (req, res) => {
}
settings.sonarr = [...settings.sonarr, newSonarr];
await settings.save();
settings.save();
return res.status(201).json(newSonarr);
});
@@ -43,14 +43,13 @@ sonarrRoutes.post('/test', async (req, res, next) => {
url: SonarrAPI.buildUrl(req.body, '/api/v3'),
});
const systemStatus = await sonarr.getSystemStatus();
const sonarrMajorVersion = Number(systemStatus.version.split('.')[0]);
const urlBase = systemStatus.urlBase;
const urlBase = await sonarr
.getSystemStatus()
.then((value) => value.urlBase)
.catch(() => req.body.baseUrl);
const profiles = await sonarr.getProfiles();
const folders = await sonarr.getRootFolders();
const languageProfiles =
sonarrMajorVersion <= 3 ? await sonarr.getLanguageProfiles() : null;
const languageProfiles = await sonarr.getLanguageProfiles();
const tags = await sonarr.getTags();
return res.status(200).json({
@@ -73,7 +72,7 @@ sonarrRoutes.post('/test', async (req, res, next) => {
}
});
sonarrRoutes.put<{ id: string }>('/:id', async (req, res) => {
sonarrRoutes.put<{ id: string }>('/:id', (req, res) => {
const settings = getSettings();
const sonarrIndex = settings.sonarr.findIndex(
@@ -101,12 +100,12 @@ sonarrRoutes.put<{ id: string }>('/:id', async (req, res) => {
...req.body,
id: Number(req.params.id),
} as SonarrSettings;
await settings.save();
settings.save();
return res.status(200).json(settings.sonarr[sonarrIndex]);
});
sonarrRoutes.delete<{ id: string }>('/:id', async (req, res) => {
sonarrRoutes.delete<{ id: string }>('/:id', (req, res) => {
const settings = getSettings();
const sonarrIndex = settings.sonarr.findIndex(
@@ -120,7 +119,7 @@ sonarrRoutes.delete<{ id: string }>('/:id', async (req, res) => {
}
const removed = settings.sonarr.splice(sonarrIndex, 1);
await settings.save();
settings.save();
return res.status(200).json(removed[0]);
});

View File

@@ -516,6 +516,12 @@ router.post(
//const jellyfinUsersResponse = await jellyfinClient.getUsers();
const createdUsers: User[] = [];
const { externalHostname } = getSettings().jellyfin;
const jellyfinHost =
externalHostname && externalHostname.length > 0
? externalHostname
: hostname;
jellyfinClient.setUserId(admin.jellyfinUserId ?? '');
const jellyfinUsers = await jellyfinClient.getUsers();
@@ -539,7 +545,12 @@ router.post(
).toString('base64'),
email: jellyfinUser?.Name,
permissions: settings.main.defaultPermissions,
avatar: `/avatarproxy/${jellyfinUser?.Id}`,
avatar: jellyfinUser?.PrimaryImageTag
? `${jellyfinHost}/Users/${jellyfinUser.Id}/Images/Primary/?tag=${jellyfinUser.PrimaryImageTag}&quality=90`
: gravatarUrl(jellyfinUser?.Name ?? '', {
default: 'mm',
size: 200,
}),
userType:
settings.main.mediaServerType === MediaServerType.JELLYFIN
? UserType.JELLYFIN

View File

@@ -1,4 +1,4 @@
import { accessSync, existsSync } from 'fs';
import { existsSync } from 'fs';
import path from 'path';
const CONFIG_PATH = process.env.CONFIG_DIRECTORY
@@ -14,12 +14,3 @@ export const appDataStatus = (): boolean => {
export const appDataPath = (): string => {
return CONFIG_PATH;
};
export const appDataPermissions = (): boolean => {
try {
accessSync(CONFIG_PATH);
return true;
} catch (err) {
return false;
}
};

View File

@@ -1,111 +0,0 @@
import type { ProxySettings } from '@server/lib/settings';
import logger from '@server/logger';
import type { Dispatcher } from 'undici';
import { Agent, ProxyAgent, setGlobalDispatcher } from 'undici';
export default async function createCustomProxyAgent(
proxySettings: ProxySettings
) {
const defaultAgent = new Agent();
const skipUrl = (url: string) => {
const hostname = new URL(url).hostname;
if (proxySettings.bypassLocalAddresses && isLocalAddress(hostname)) {
return true;
}
for (const address of proxySettings.bypassFilter.split(',')) {
const trimmedAddress = address.trim();
if (!trimmedAddress) {
continue;
}
if (trimmedAddress.startsWith('*')) {
const domain = trimmedAddress.slice(1);
if (hostname.endsWith(domain)) {
return true;
}
} else if (hostname === trimmedAddress) {
return true;
}
}
return false;
};
const noProxyInterceptor = (
dispatch: Dispatcher['dispatch']
): Dispatcher['dispatch'] => {
return (opts, handler) => {
const url = opts.origin?.toString();
return url && skipUrl(url)
? defaultAgent.dispatch(opts, handler)
: dispatch(opts, handler);
};
};
const token =
proxySettings.user && proxySettings.password
? `Basic ${Buffer.from(
`${proxySettings.user}:${proxySettings.password}`
).toString('base64')}`
: undefined;
try {
const proxyAgent = new ProxyAgent({
uri:
(proxySettings.useSsl ? 'https://' : 'http://') +
proxySettings.hostname +
':' +
proxySettings.port,
token,
interceptors: {
Client: [noProxyInterceptor],
},
});
setGlobalDispatcher(proxyAgent);
} catch (e) {
logger.error('Failed to connect to the proxy: ' + e.message, {
label: 'Proxy',
});
setGlobalDispatcher(defaultAgent);
return;
}
try {
const res = await fetch('https://www.google.com', { method: 'HEAD' });
if (res.ok) {
logger.debug('HTTP(S) proxy connected successfully', { label: 'Proxy' });
} else {
logger.error('Proxy responded, but with a non-OK status: ' + res.status, {
label: 'Proxy',
});
setGlobalDispatcher(defaultAgent);
}
} catch (e) {
logger.error(
'Failed to connect to the proxy: ' + e.message + ': ' + e.cause,
{ label: 'Proxy' }
);
setGlobalDispatcher(defaultAgent);
}
}
function isLocalAddress(hostname: string) {
if (hostname === 'localhost' || hostname === '127.0.0.1') {
return true;
}
const privateIpRanges = [
/^10\./, // 10.x.x.x
/^172\.(1[6-9]|2[0-9]|3[0-1])\./, // 172.16.x.x - 172.31.x.x
/^192\.168\./, // 192.168.x.x
];
if (privateIpRanges.some((regex) => regex.test(hostname))) {
return true;
}
return false;
}

View File

@@ -13,8 +13,7 @@ class RestartFlag {
return (
this.settings.csrfProtection !== settings.csrfProtection ||
this.settings.trustProxy !== settings.trustProxy ||
this.settings.proxy.enabled !== settings.proxy.enabled
this.settings.trustProxy !== settings.trustProxy
);
}
}

View File

@@ -268,7 +268,6 @@ const BlacklistedItem = ({ item, revalidateList }: BlacklistedItemProps) => {
{title && title.backdropPath && (
<div className="absolute inset-0 z-0 w-full bg-cover bg-center xl:w-2/3">
<CachedImage
type="tmdb"
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${title.backdropPath}`}
alt=""
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
@@ -294,7 +293,6 @@ const BlacklistedItem = ({ item, revalidateList }: BlacklistedItemProps) => {
className="relative h-auto w-12 flex-shrink-0 scale-100 transform-gpu overflow-hidden rounded-md transition duration-300 hover:scale-105"
>
<CachedImage
type="tmdb"
src={
title?.posterPath
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
@@ -357,7 +355,6 @@ const BlacklistedItem = ({ item, revalidateList }: BlacklistedItemProps) => {
<Link href={`/users/${item.user.id}`}>
<span className="group flex items-center truncate">
<CachedImage
type="avatar"
src={item.user.avatar}
alt=""
className="avatar-sm ml-1.5"

View File

@@ -198,7 +198,6 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => {
{data.backdropPath && (
<div className="media-page-bg-image">
<CachedImage
type="tmdb"
alt=""
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${data.backdropPath}`}
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
@@ -229,7 +228,6 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => {
<div className="media-header">
<div className="media-poster">
<CachedImage
type="tmdb"
src={
data.posterPath
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${data.posterPath}`

View File

@@ -4,31 +4,24 @@ import Image from 'next/image';
const imageLoader: ImageLoader = ({ src }) => src;
export type CachedImageProps = ImageProps & {
src: string;
type: 'tmdb' | 'avatar';
};
/**
* The CachedImage component should be used wherever
* we want to offer the option to locally cache images.
**/
const CachedImage = ({ src, type, ...props }: CachedImageProps) => {
const CachedImage = ({ src, ...props }: ImageProps) => {
const { currentSettings } = useSettings();
let imageUrl: string;
let imageUrl = src;
if (type === 'tmdb') {
// tmdb stuff
imageUrl =
currentSettings.cacheImages && !src.startsWith('/')
? src.replace(/^https:\/\/image\.tmdb\.org\//, '/imageproxy/')
: src;
} else if (type === 'avatar') {
// jellyfin avatar (if any)
imageUrl = src;
} else {
return null;
if (typeof imageUrl === 'string' && imageUrl.startsWith('http')) {
const parsedUrl = new URL(imageUrl);
if (parsedUrl.host === 'image.tmdb.org') {
if (currentSettings.cacheImages)
imageUrl = imageUrl.replace('https://image.tmdb.org', '/imageproxy');
} else if (parsedUrl.host !== 'gravatar.com') {
imageUrl = '/avatarproxy/' + imageUrl;
}
}
return <Image unoptimized loader={imageLoader} src={imageUrl} {...props} />;

View File

@@ -61,7 +61,6 @@ const ImageFader: ForwardRefRenderFunction<HTMLDivElement, ImageFaderProps> = (
{...props}
>
<CachedImage
type="tmdb"
className="absolute inset-0 h-full w-full"
alt=""
src={imageUrl}

View File

@@ -123,7 +123,6 @@ const Modal = React.forwardRef<HTMLDivElement, ModalProps>(
{backdrop && (
<div className="absolute top-0 left-0 right-0 z-0 h-64 max-h-full w-full">
<CachedImage
type="tmdb"
alt=""
src={backdrop}
style={{ width: '100%', height: '100%', objectFit: 'cover' }}

View File

@@ -33,7 +33,6 @@ const CompanyCard = ({ image, url, name }: CompanyCardProps) => {
>
<div className="relative h-full w-full">
<CachedImage
type="tmdb"
src={image}
alt={name}
className="relative z-40 h-full w-full"

View File

@@ -36,7 +36,6 @@ const GenreCard = ({ image, url, name, canExpand = false }: GenreCardProps) => {
tabIndex={0}
>
<CachedImage
type="tmdb"
src={image}
alt=""
style={{ width: '100%', height: '100%', objectFit: 'cover' }}

View File

@@ -89,8 +89,7 @@ const IssueComment = ({
</Transition>
<Link href={isActiveUser ? '/profile' : `/users/${comment.user.id}`}>
<CachedImage
type="avatar"
src={comment.user.avatar}
src={`${comment.user.avatar}`}
alt=""
className="h-10 w-10 scale-100 transform-gpu rounded-full object-cover ring-1 ring-gray-500 transition duration-300 hover:scale-105"
width={40}

View File

@@ -217,7 +217,6 @@ const IssueDetails = () => {
{data.backdropPath && (
<div className="media-page-bg-image">
<CachedImage
type="tmdb"
alt=""
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${data.backdropPath}`}
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
@@ -236,7 +235,6 @@ const IssueDetails = () => {
<div className="media-header">
<div className="media-poster">
<CachedImage
type="tmdb"
src={
data.posterPath
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${data.posterPath}`
@@ -289,8 +287,7 @@ const IssueDetails = () => {
className="group ml-1 inline-flex h-full items-center xl:ml-1.5"
>
<CachedImage
type="avatar"
src={issueData.createdBy.avatar}
src={`${issueData.createdBy.avatar}`}
alt=""
className="mr-0.5 h-5 w-5 scale-100 transform-gpu rounded-full object-cover transition duration-300 group-hover:scale-105 xl:mr-1 xl:h-6 xl:w-6"
width={20}

View File

@@ -112,7 +112,6 @@ const IssueItem = ({ issue }: IssueItemProps) => {
{title.backdropPath && (
<div className="absolute inset-0 z-0 w-full bg-cover bg-center xl:w-2/3">
<CachedImage
type="tmdb"
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${title.backdropPath}`}
alt=""
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
@@ -138,7 +137,6 @@ const IssueItem = ({ issue }: IssueItemProps) => {
className="relative h-auto w-12 flex-shrink-0 scale-100 transform-gpu overflow-hidden rounded-md transition duration-300 hover:scale-105"
>
<CachedImage
type="tmdb"
src={
title.posterPath
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
@@ -228,8 +226,7 @@ const IssueItem = ({ issue }: IssueItemProps) => {
className="group flex items-center truncate"
>
<CachedImage
type="avatar"
src={issue.createdBy.avatar}
src={'/avatarproxy/' + issue.createdBy.avatar}
alt=""
className="avatar-sm ml-1.5 object-cover"
width={20}

View File

@@ -7,7 +7,6 @@ import {
CogIcon,
EllipsisHorizontalIcon,
ExclamationTriangleIcon,
EyeSlashIcon,
FilmIcon,
SparklesIcon,
TvIcon,
@@ -17,7 +16,6 @@ import {
ClockIcon as FilledClockIcon,
CogIcon as FilledCogIcon,
ExclamationTriangleIcon as FilledExclamationTriangleIcon,
EyeSlashIcon as FilledEyeSlashIcon,
FilmIcon as FilledFilmIcon,
SparklesIcon as FilledSparklesIcon,
TvIcon as FilledTvIcon,
@@ -86,18 +84,6 @@ const MobileMenu = () => {
svgIconSelected: <FilledClockIcon className="h-6 w-6" />,
activeRegExp: /^\/requests/,
},
{
href: '/blacklist',
content: intl.formatMessage(menuMessages.blacklist),
svgIcon: <EyeSlashIcon className="h-6 w-6" />,
svgIconSelected: <FilledEyeSlashIcon className="h-6 w-6" />,
activeRegExp: /^\/blacklist/,
requiredPermission: [
Permission.MANAGE_BLACKLIST,
Permission.VIEW_BLACKLIST,
],
permissionType: 'or',
},
{
href: '/issues',
content: intl.formatMessage(menuMessages.issues),

View File

@@ -57,7 +57,6 @@ const UserDropdown = () => {
data-testid="user-menu"
>
<CachedImage
type="avatar"
className="h-8 w-8 rounded-full object-cover sm:h-10 sm:w-10"
src={user ? user.avatar : ''}
alt=""
@@ -81,7 +80,6 @@ const UserDropdown = () => {
<div className="flex flex-col space-y-4 px-4 py-4">
<div className="flex items-center space-x-2">
<CachedImage
type="avatar"
className="h-8 w-8 rounded-full object-cover sm:h-10 sm:w-10"
src={user ? user.avatar : ''}
alt=""

View File

@@ -369,7 +369,6 @@ const ManageSlideOver = ({
content={user.displayName}
>
<CachedImage
type="avatar"
src={user.avatar}
alt={user.displayName}
className="h-8 w-8 scale-100 transform-gpu rounded-full object-cover ring-1 ring-gray-500 transition duration-300 hover:scale-105"
@@ -531,7 +530,6 @@ const ManageSlideOver = ({
content={user.displayName}
>
<CachedImage
type="avatar"
src={user.avatar}
alt={user.displayName}
className="h-8 w-8 scale-100 transform-gpu rounded-full object-cover ring-1 ring-gray-500 transition duration-300 hover:scale-105"

View File

@@ -448,7 +448,6 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
{data.backdropPath && (
<div className="media-page-bg-image">
<CachedImage
type="tmdb"
alt=""
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${data.backdropPath}`}
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
@@ -495,7 +494,6 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
<div className="media-header">
<div className="media-poster">
<CachedImage
type="tmdb"
src={
data.posterPath
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${data.posterPath}`
@@ -743,7 +741,6 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
<div className="group relative z-0 scale-100 transform-gpu cursor-pointer overflow-hidden rounded-lg bg-gray-800 bg-cover bg-center shadow-md ring-1 ring-gray-700 transition duration-300 hover:scale-105 hover:ring-gray-500">
<div className="absolute inset-0 z-0">
<CachedImage
type="tmdb"
src={`https://image.tmdb.org/t/p/w1440_and_h320_multi_faces/${data.collection.backdropPath}`}
alt=""
style={{

View File

@@ -51,7 +51,6 @@ const PersonCard = ({
{profilePath ? (
<div className="relative h-full w-3/4 overflow-hidden rounded-full ring-1 ring-gray-700">
<CachedImage
type="tmdb"
src={`https://image.tmdb.org/t/p/w600_and_h900_bestv2${profilePath}`}
alt=""
style={{

View File

@@ -227,7 +227,6 @@ const PersonDetails = () => {
{data.profilePath && (
<div className="relative mb-6 mr-0 h-36 w-36 flex-shrink-0 overflow-hidden rounded-full ring-1 ring-gray-700 lg:mb-0 lg:mr-6 lg:h-44 lg:w-44">
<CachedImage
type="tmdb"
src={`https://image.tmdb.org/t/p/w600_and_h900_bestv2${data.profilePath}`}
alt=""
style={{ width: '100%', height: '100%', objectFit: 'cover' }}

View File

@@ -116,7 +116,6 @@ const RequestCardError = ({ requestData }: RequestCardErrorProps) => {
>
<span className="avatar-sm">
<CachedImage
type="avatar"
src={requestData.requestedBy.avatar}
alt=""
className="avatar-sm object-cover"
@@ -346,7 +345,6 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
{title.backdropPath && (
<div className="absolute inset-0 z-0">
<CachedImage
type="tmdb"
alt=""
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${title.backdropPath}`}
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
@@ -392,7 +390,6 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
>
<span className="avatar-sm">
<CachedImage
type="avatar"
src={requestData.requestedBy.avatar}
alt=""
className="avatar-sm object-cover"
@@ -605,7 +602,6 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
className="w-20 flex-shrink-0 scale-100 transform-gpu cursor-pointer overflow-hidden rounded-md shadow-sm transition duration-300 hover:scale-105 hover:shadow-md sm:w-28"
>
<CachedImage
type="tmdb"
src={
title.posterPath
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`

View File

@@ -42,7 +42,6 @@ const messages = defineMessages('components.RequestList.RequestItem', {
tmdbid: 'TMDB ID',
tvdbid: 'TheTVDB ID',
unknowntitle: 'Unknown Title',
removearr: 'Remove from {arr}',
profileName: 'Profile',
});
@@ -191,7 +190,6 @@ const RequestItemError = ({
>
<span className="avatar-sm ml-1.5">
<CachedImage
type="avatar"
src={requestData.requestedBy.avatar}
alt=""
className="avatar-sm object-cover"
@@ -251,7 +249,6 @@ const RequestItemError = ({
>
<span className="avatar-sm ml-1.5">
<CachedImage
type="avatar"
src={requestData.modifiedBy.avatar}
alt=""
className="avatar-sm object-cover"
@@ -344,18 +341,6 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
revalidateList();
};
const deleteMediaFile = async () => {
if (request.media) {
await fetch(`/api/v1/media/${request.media.id}/file`, {
method: 'DELETE',
});
await fetch(`/api/v1/media/${request.media.id}`, {
method: 'DELETE',
});
revalidateList();
}
};
const retryRequest = async () => {
setRetrying(true);
@@ -420,7 +405,6 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
{title.backdropPath && (
<div className="absolute inset-0 z-0 w-full bg-cover bg-center xl:w-2/3">
<CachedImage
type="tmdb"
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${title.backdropPath}`}
alt=""
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
@@ -446,7 +430,6 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
className="relative h-auto w-12 flex-shrink-0 scale-100 transform-gpu overflow-hidden rounded-md transition duration-300 hover:scale-105"
>
<CachedImage
type="tmdb"
src={
title.posterPath
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
@@ -574,7 +557,6 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
>
<span className="avatar-sm ml-1.5">
<CachedImage
type="avatar"
src={requestData.requestedBy.avatar}
alt=""
className="avatar-sm object-cover"
@@ -634,8 +616,7 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
>
<span className="avatar-sm ml-1.5">
<CachedImage
type="avatar"
src={requestData.modifiedBy.avatar}
src={requestData.requestedBy.avatar}
alt=""
className="avatar-sm object-cover"
width={20}
@@ -685,28 +666,14 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
)}
{requestData.status !== MediaRequestStatus.PENDING &&
hasPermission(Permission.MANAGE_REQUESTS) && (
<>
<ConfirmButton
onClick={() => deleteRequest()}
confirmText={intl.formatMessage(globalMessages.areyousure)}
className="w-full"
>
<TrashIcon />
<span>{intl.formatMessage(messages.deleterequest)}</span>
</ConfirmButton>
<ConfirmButton
onClick={() => deleteMediaFile()}
confirmText={intl.formatMessage(globalMessages.areyousure)}
className="w-full"
>
<TrashIcon />
<span>
{intl.formatMessage(messages.removearr, {
arr: request.type === 'movie' ? 'Radarr' : 'Sonarr',
})}
</span>
</ConfirmButton>
</>
<ConfirmButton
onClick={() => deleteRequest()}
confirmText={intl.formatMessage(globalMessages.areyousure)}
className="w-full"
>
<TrashIcon />
<span>{intl.formatMessage(messages.deleterequest)}</span>
</ConfirmButton>
)}
{requestData.status === MediaRequestStatus.PENDING &&
hasPermission(Permission.MANAGE_REQUESTS) && (

View File

@@ -562,7 +562,6 @@ const AdvancedRequester = ({
<Listbox.Button className="focus:shadow-outline-blue relative w-full cursor-default rounded-md border border-gray-700 bg-gray-800 py-2 pl-3 pr-10 text-left text-white transition duration-150 ease-in-out focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5">
<span className="flex items-center">
<CachedImage
type="avatar"
src={selectedUser.avatar}
alt=""
className="h-6 w-6 flex-shrink-0 rounded-full object-cover"
@@ -615,7 +614,6 @@ const AdvancedRequester = ({
} flex items-center`}
>
<CachedImage
type="avatar"
src={user.avatar}
alt=""
className="h-6 w-6 flex-shrink-0 rounded-full object-cover"

View File

@@ -437,7 +437,6 @@ const CollectionRequestModal = ({
>
<div className="relative h-auto w-10 flex-shrink-0 overflow-hidden rounded-md">
<CachedImage
type="tmdb"
src={
part.posterPath
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${part.posterPath}`

View File

@@ -452,7 +452,6 @@ export const WatchProviderSelector = ({
tabIndex={0}
>
<CachedImage
type="tmdb"
src={`https://image.tmdb.org/t/p/original${provider.logoPath}`}
alt=""
style={{
@@ -498,7 +497,6 @@ export const WatchProviderSelector = ({
tabIndex={0}
>
<CachedImage
type="tmdb"
src={`https://image.tmdb.org/t/p/original${provider.logoPath}`}
alt=""
style={{

View File

@@ -55,17 +55,6 @@ const messages = defineMessages('components.Settings.SettingsMain', {
validationApplicationUrlTrailingSlash: 'URL must not end in a trailing slash',
partialRequestsEnabled: 'Allow Partial Series Requests',
locale: 'Display Language',
proxyEnabled: 'HTTP(S) Proxy',
proxyHostname: 'Proxy Hostname',
proxyPort: 'Proxy Port',
proxySsl: 'Use SSL For Proxy',
proxyUser: 'Proxy Username',
proxyPassword: 'Proxy Password',
proxyBypassFilter: 'Proxy Ignored Addresses',
proxyBypassFilterTip:
"Use ',' as a separator, and '*.' as a wildcard for subdomains",
proxyBypassLocalAddresses: 'Bypass Proxy for Local Addresses',
validationProxyPort: 'You must provide a valid port',
});
const SettingsMain = () => {
@@ -93,12 +82,6 @@ const SettingsMain = () => {
intl.formatMessage(messages.validationApplicationUrlTrailingSlash),
(value) => !value || !value.endsWith('/')
),
proxyPort: Yup.number().when('proxyEnabled', {
is: (proxyEnabled: boolean) => proxyEnabled,
then: Yup.number().required(
intl.formatMessage(messages.validationProxyPort)
),
}),
});
const regenerate = async () => {
@@ -154,14 +137,6 @@ const SettingsMain = () => {
partialRequestsEnabled: data?.partialRequestsEnabled,
trustProxy: data?.trustProxy,
cacheImages: data?.cacheImages,
proxyEnabled: data?.proxy?.enabled,
proxyHostname: data?.proxy?.hostname,
proxyPort: data?.proxy?.port,
proxySsl: data?.proxy?.useSsl,
proxyUser: data?.proxy?.user,
proxyPassword: data?.proxy?.password,
proxyBypassFilter: data?.proxy?.bypassFilter,
proxyBypassLocalAddresses: data?.proxy?.bypassLocalAddresses,
}}
enableReinitialize
validationSchema={MainSettingsSchema}
@@ -183,16 +158,6 @@ const SettingsMain = () => {
partialRequestsEnabled: values.partialRequestsEnabled,
trustProxy: values.trustProxy,
cacheImages: values.cacheImages,
proxy: {
enabled: values.proxyEnabled,
hostname: values.proxyHostname,
port: values.proxyPort,
useSsl: values.proxySsl,
user: values.proxyUser,
password: values.proxyPassword,
bypassFilter: values.proxyBypassFilter,
bypassLocalAddresses: values.proxyBypassLocalAddresses,
},
}),
});
if (!res.ok) throw new Error();
@@ -472,176 +437,6 @@ const SettingsMain = () => {
/>
</div>
</div>
<div className="form-row">
<label htmlFor="proxyEnabled" className="checkbox-label">
<span className="mr-2">
{intl.formatMessage(messages.proxyEnabled)}
</span>
<SettingsBadge badgeType="advanced" className="mr-2" />
<SettingsBadge badgeType="restartRequired" />
</label>
<div className="form-input-area">
<Field
type="checkbox"
id="proxyEnabled"
name="proxyEnabled"
onChange={() => {
setFieldValue('proxyEnabled', !values.proxyEnabled);
}}
/>
</div>
</div>
{values.proxyEnabled && (
<>
<div className="form-row">
<label htmlFor="proxyHostname" className="checkbox-label">
<span className="mr-2 ml-4">
{intl.formatMessage(messages.proxyHostname)}
</span>
</label>
<div className="form-input-area">
<div className="form-input-field">
<Field
id="proxyHostname"
name="proxyHostname"
type="text"
/>
</div>
{errors.proxyHostname &&
touched.proxyHostname &&
typeof errors.proxyHostname === 'string' && (
<div className="error">{errors.proxyHostname}</div>
)}
</div>
</div>
<div className="form-row">
<label htmlFor="proxyPort" className="checkbox-label">
<span className="mr-2 ml-4">
{intl.formatMessage(messages.proxyPort)}
</span>
</label>
<div className="form-input-area">
<div className="form-input-field">
<Field id="proxyPort" name="proxyPort" type="text" />
</div>
{errors.proxyPort &&
touched.proxyPort &&
typeof errors.proxyPort === 'string' && (
<div className="error">{errors.proxyPort}</div>
)}
</div>
</div>
<div className="form-row">
<label htmlFor="proxySsl" className="checkbox-label">
<span className="mr-2 ml-4">
{intl.formatMessage(messages.proxySsl)}
</span>
</label>
<div className="form-input-area">
<Field
type="checkbox"
id="proxySsl"
name="proxySsl"
onChange={() => {
setFieldValue('proxySsl', !values.proxySsl);
}}
/>
</div>
</div>
<div className="form-row">
<label htmlFor="proxyUser" className="checkbox-label">
<span className="mr-2 ml-4">
{intl.formatMessage(messages.proxyUser)}
</span>
</label>
<div className="form-input-area">
<div className="form-input-field">
<Field id="proxyUser" name="proxyUser" type="text" />
</div>
{errors.proxyUser &&
touched.proxyUser &&
typeof errors.proxyUser === 'string' && (
<div className="error">{errors.proxyUser}</div>
)}
</div>
</div>
<div className="form-row">
<label htmlFor="proxyPassword" className="checkbox-label">
<span className="mr-2 ml-4">
{intl.formatMessage(messages.proxyPassword)}
</span>
</label>
<div className="form-input-area">
<div className="form-input-field">
<Field
id="proxyPassword"
name="proxyPassword"
type="password"
/>
</div>
{errors.proxyPassword &&
touched.proxyPassword &&
typeof errors.proxyPassword === 'string' && (
<div className="error">{errors.proxyPassword}</div>
)}
</div>
</div>
<div className="form-row">
<label
htmlFor="proxyBypassFilter"
className="checkbox-label"
>
<span className="mr-2 ml-4">
{intl.formatMessage(messages.proxyBypassFilter)}
</span>
<span className="label-tip ml-4">
{intl.formatMessage(messages.proxyBypassFilterTip)}
</span>
</label>
<div className="form-input-area">
<div className="form-input-field">
<Field
id="proxyBypassFilter"
name="proxyBypassFilter"
type="text"
/>
</div>
{errors.proxyBypassFilter &&
touched.proxyBypassFilter &&
typeof errors.proxyBypassFilter === 'string' && (
<div className="error">
{errors.proxyBypassFilter}
</div>
)}
</div>
</div>
<div className="form-row">
<label
htmlFor="proxyBypassLocalAddresses"
className="checkbox-label"
>
<span className="mr-2 ml-4">
{intl.formatMessage(
messages.proxyBypassLocalAddresses
)}
</span>
</label>
<div className="form-input-area">
<Field
type="checkbox"
id="proxyBypassLocalAddresses"
name="proxyBypassLocalAddresses"
onChange={() => {
setFieldValue(
'proxyBypassLocalAddresses',
!values.proxyBypassLocalAddresses
);
}}
/>
</div>
</div>
</>
)}
<div className="actions">
<div className="flex justify-end">
<span className="ml-3 inline-flex rounded-md shadow-sm">

View File

@@ -86,12 +86,10 @@ interface TestResponse {
id: number;
path: string;
}[];
languageProfiles:
| {
id: number;
name: string;
}[]
| null;
languageProfiles: {
id: number;
name: string;
}[];
tags: {
id: number;
label: string;
@@ -114,7 +112,7 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
const [testResponse, setTestResponse] = useState<TestResponse>({
profiles: [],
rootFolders: [],
languageProfiles: null,
languageProfiles: [],
tags: [],
});
const SonarrSettingsSchema = Yup.object().shape({
@@ -139,11 +137,9 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
activeProfileId: Yup.string().required(
intl.formatMessage(messages.validationProfileRequired)
),
activeLanguageProfileId: testResponse.languageProfiles
? Yup.number().required(
intl.formatMessage(messages.validationLanguageProfileRequired)
)
: Yup.number(),
activeLanguageProfileId: Yup.number().required(
intl.formatMessage(messages.validationLanguageProfileRequired)
),
externalUrl: Yup.string()
.url(intl.formatMessage(messages.validationApplicationUrl))
.test(
@@ -662,56 +658,54 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
)}
</div>
</div>
{testResponse.languageProfiles && (
<div className="form-row">
<label
htmlFor="activeLanguageProfileId"
className="text-label"
>
{intl.formatMessage(messages.languageprofile)}
<span className="label-required">*</span>
</label>
<div className="form-input-area">
<div className="form-input-field">
<Field
as="select"
id="activeLanguageProfileId"
name="activeLanguageProfileId"
disabled={!isValidated || isTesting}
>
<option value="">
{isTesting
? intl.formatMessage(
messages.loadinglanguageprofiles
)
: !isValidated
? intl.formatMessage(
messages.testFirstLanguageProfiles
)
: intl.formatMessage(
messages.selectLanguageProfile
)}
</option>
{testResponse.languageProfiles.length > 0 &&
testResponse.languageProfiles.map((language) => (
<option
key={`loaded-profile-${language.id}`}
value={language.id}
>
{language.name}
</option>
))}
</Field>
</div>
{errors.activeLanguageProfileId &&
touched.activeLanguageProfileId && (
<div className="error">
{errors.activeLanguageProfileId}
</div>
)}
<div className="form-row">
<label
htmlFor="activeLanguageProfileId"
className="text-label"
>
{intl.formatMessage(messages.languageprofile)}
<span className="label-required">*</span>
</label>
<div className="form-input-area">
<div className="form-input-field">
<Field
as="select"
id="activeLanguageProfileId"
name="activeLanguageProfileId"
disabled={!isValidated || isTesting}
>
<option value="">
{isTesting
? intl.formatMessage(
messages.loadinglanguageprofiles
)
: !isValidated
? intl.formatMessage(
messages.testFirstLanguageProfiles
)
: intl.formatMessage(
messages.selectLanguageProfile
)}
</option>
{testResponse.languageProfiles.length > 0 &&
testResponse.languageProfiles.map((language) => (
<option
key={`loaded-profile-${language.id}`}
value={language.id}
>
{language.name}
</option>
))}
</Field>
</div>
{errors.activeLanguageProfileId &&
touched.activeLanguageProfileId && (
<div className="error">
{errors.activeLanguageProfileId}
</div>
)}
</div>
)}
</div>
<div className="form-row">
<label htmlFor="tags" className="text-label">
{intl.formatMessage(messages.tags)}
@@ -869,55 +863,53 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
)}
</div>
</div>
{testResponse.languageProfiles && (
<div className="form-row">
<label
htmlFor="activeAnimeLanguageProfileId"
className="text-label"
>
{intl.formatMessage(messages.animelanguageprofile)}
</label>
<div className="form-input-area">
<div className="form-input-field">
<Field
as="select"
id="activeAnimeLanguageProfileId"
name="activeAnimeLanguageProfileId"
disabled={!isValidated || isTesting}
>
<option value="">
{isTesting
? intl.formatMessage(
messages.loadinglanguageprofiles
)
: !isValidated
? intl.formatMessage(
messages.testFirstLanguageProfiles
)
: intl.formatMessage(
messages.selectLanguageProfile
)}
</option>
{testResponse.languageProfiles.length > 0 &&
testResponse.languageProfiles.map((language) => (
<option
key={`loaded-profile-${language.id}`}
value={language.id}
>
{language.name}
</option>
))}
</Field>
</div>
{errors.activeAnimeLanguageProfileId &&
touched.activeAnimeLanguageProfileId && (
<div className="error">
{errors.activeAnimeLanguageProfileId}
</div>
)}
<div className="form-row">
<label
htmlFor="activeAnimeLanguageProfileId"
className="text-label"
>
{intl.formatMessage(messages.animelanguageprofile)}
</label>
<div className="form-input-area">
<div className="form-input-field">
<Field
as="select"
id="activeAnimeLanguageProfileId"
name="activeAnimeLanguageProfileId"
disabled={!isValidated || isTesting}
>
<option value="">
{isTesting
? intl.formatMessage(
messages.loadinglanguageprofiles
)
: !isValidated
? intl.formatMessage(
messages.testFirstLanguageProfiles
)
: intl.formatMessage(
messages.selectLanguageProfile
)}
</option>
{testResponse.languageProfiles.length > 0 &&
testResponse.languageProfiles.map((language) => (
<option
key={`loaded-profile-${language.id}`}
value={language.id}
>
{language.name}
</option>
))}
</Field>
</div>
{errors.activeAnimeLanguageProfileId &&
touched.activeAnimeLanguageProfileId && (
<div className="error">
{errors.activeAnimeLanguageProfileId}
</div>
)}
</div>
)}
</div>
<div className="form-row">
<label htmlFor="tags" className="text-label">
{intl.formatMessage(messages.animeTags)}

View File

@@ -346,7 +346,6 @@ const TitleCard = ({
>
<div className="absolute inset-0 h-full w-full overflow-hidden">
<CachedImage
type="tmdb"
className="absolute inset-0 h-full w-full"
alt=""
src={

View File

@@ -471,7 +471,6 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
{data.backdropPath && (
<div className="media-page-bg-image">
<CachedImage
type="tmdb"
alt=""
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${data.backdropPath}`}
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
@@ -528,7 +527,6 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
<div className="media-header">
<div className="media-poster">
<CachedImage
type="tmdb"
src={
data.posterPath
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${data.posterPath}`

View File

@@ -250,7 +250,6 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
<td className="whitespace-nowrap px-1 py-4 text-sm font-medium leading-5 text-gray-100 md:px-6">
<div className="flex items-center">
<CachedImage
type="avatar"
className="h-10 w-10 flex-shrink-0 rounded-full"
src={user.thumb}
alt=""

View File

@@ -634,7 +634,6 @@ const UserList = () => {
className="h-10 w-10 flex-shrink-0"
>
<CachedImage
type="avatar"
className="h-10 w-10 rounded-full object-cover"
src={user.avatar}
alt=""

View File

@@ -43,7 +43,6 @@ const ProfileHeader = ({ user, isSettingsPage }: ProfileHeaderProps) => {
<div className="flex-shrink-0">
<div className="relative">
<CachedImage
type="avatar"
className="h-24 w-24 rounded-full bg-gray-600 object-cover ring-1 ring-gray-700"
src={user.avatar}
alt=""

View File

@@ -298,7 +298,6 @@
"components.ManageSlideOver.plays": "<strong>{playCount, number}</strong> {playCount, plural, one {play} other {plays}}",
"components.ManageSlideOver.removearr": "Remove from {arr}",
"components.ManageSlideOver.removearr4k": "Remove from 4K {arr}",
"components.RequestList.RequestItem.removearr": "Remove from {arr}",
"components.ManageSlideOver.tvshow": "series",
"components.MediaSlider.ShowMoreCard.seemore": "See More",
"components.MovieDetails.MovieCast.fullcast": "Full Cast",
@@ -1031,7 +1030,6 @@
"components.Settings.save": "Save Changes",
"components.Settings.saving": "Saving…",
"components.Settings.scan": "Sync Libraries",
"components.Settings.scanbackground": "Scanning will run in the background. You can continue the setup process in the meantime.",
"components.Settings.scanning": "Syncing…",
"components.Settings.serverLocal": "local",
"components.Settings.serverRemote": "remote",
@@ -1052,7 +1050,6 @@
"components.Settings.tautulliSettings": "Tautulli Settings",
"components.Settings.tautulliSettingsDescription": "Optionally configure the settings for your Tautulli server. Jellyseerr fetches watch history data for your Plex media from Tautulli.",
"components.Settings.timeout": "Timeout",
"components.Settings.tip": "Tip",
"components.Settings.toastPlexConnecting": "Attempting to connect to Plex…",
"components.Settings.toastPlexConnectingFailure": "Failed to connect to Plex.",
"components.Settings.toastPlexConnectingSuccess": "Plex connection established successfully!",
@@ -1082,14 +1079,16 @@
"components.Setup.continue": "Continue",
"components.Setup.finish": "Finish Setup",
"components.Setup.finishing": "Finishing…",
"components.Setup.scanbackground": "Scanning will run in the background. You can continue the setup process in the meantime.",
"components.Setup.servertype": "Choose Server Type",
"components.Setup.setup": "Setup",
"components.Setup.signin": "Sign in to your account",
"components.Setup.signin": "Sign In",
"components.Setup.signinMessage": "Get started by signing in",
"components.Setup.signinWithEmby": "Enter your Emby details",
"components.Setup.signinWithJellyfin": "Enter your Jellyfin details",
"components.Setup.signinWithPlex": "Enter your Plex details",
"components.Setup.subtitle": "Get started by choosing your media server",
"components.Setup.tip": "Tip",
"components.Setup.welcome": "Welcome to Jellyseerr",
"components.StatusBadge.managemedia": "Manage {mediaType}",
"components.StatusBadge.openinarr": "Open in {arr}",
@@ -1234,7 +1233,6 @@
"components.UserProfile.UserSettings.UserGeneralSettings.saving": "Saving…",
"components.UserProfile.UserSettings.UserGeneralSettings.seriesrequestlimit": "Series Request Limit",
"components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailure": "Something went wrong while saving settings.",
"components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmail": "This email is already taken!",
"components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsSuccess": "Settings saved successfully!",
"components.UserProfile.UserSettings.UserGeneralSettings.user": "User",
"components.UserProfile.UserSettings.UserGeneralSettings.validationDiscordId": "You must provide a valid Discord user ID",
@@ -1314,7 +1312,6 @@
"components.UserProfile.seriesrequest": "Series Requests",
"components.UserProfile.totalrequests": "Total Requests",
"components.UserProfile.unlimited": "Unlimited",
"i18n.addToBlacklist": "Add to Blacklist",
"i18n.advanced": "Advanced",
"i18n.all": "All",
"i18n.approve": "Approve",

View File

@@ -12,7 +12,6 @@ import { SettingsProvider } from '@app/context/SettingsContext';
import { UserContext } from '@app/context/UserContext';
import type { User } from '@app/hooks/useUser';
import '@app/styles/globals.css';
import '@app/utils/fetchOverride';
import { polyfillIntl } from '@app/utils/polyfillIntl';
import { MediaServerType } from '@server/constants/server';
import type { PublicSettingsResponse } from '@server/interfaces/api/settingsInterfaces';

View File

@@ -1,46 +0,0 @@
const getCsrfToken = (): string | null => {
if (typeof window !== 'undefined') {
const match = document.cookie.match(/XSRF-TOKEN=([^;]+)/);
return match ? decodeURIComponent(match[1]) : null;
}
return null;
};
const isSameOrigin = (url: RequestInfo | URL): boolean => {
const parsedUrl = new URL(
url instanceof Request ? url.url : url.toString(),
window.location.origin
);
return parsedUrl.origin === window.location.origin;
};
// We are using a custom fetch implementation to add the X-XSRF-TOKEN heade
// to all requests. This is required when CSRF protection is enabled.
if (typeof window !== 'undefined') {
const originalFetch: typeof fetch = window.fetch;
(window as typeof globalThis).fetch = async (
input: RequestInfo | URL,
init?: RequestInit
): Promise<Response> => {
if (!isSameOrigin(input)) {
return originalFetch(input, init);
}
const csrfToken = getCsrfToken();
const headers = {
...(init?.headers || {}),
...(csrfToken ? { 'XSRF-TOKEN': csrfToken } : {}),
};
const newInit: RequestInit = {
...init,
headers,
};
return originalFetch(input, newInit);
};
}
export {};