Compare commits
1 Commits
preview-ca
...
preview-fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb25ad8457 |
@@ -403,42 +403,6 @@
|
|||||||
"contributions": [
|
"contributions": [
|
||||||
"doc"
|
"doc"
|
||||||
]
|
]
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "mobihen",
|
|
||||||
"name": "Nir Israel Hen",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/35529491?v=4",
|
|
||||||
"profile": "https://mobihen.com",
|
|
||||||
"contributions": [
|
|
||||||
"translation"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "XDark187",
|
|
||||||
"name": "Baraa",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/39034192?v=4",
|
|
||||||
"profile": "https://github.com/XDark187",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "franciscofsales",
|
|
||||||
"name": "Francisco Sales",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/7977645?v=4",
|
|
||||||
"profile": "https://github.com/franciscofsales",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "myselfolli",
|
|
||||||
"name": "Oliver Laing",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/37535998?v=4",
|
|
||||||
"profile": "https://github.com/myselfolli",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -1,2 +1,2 @@
|
|||||||
# Global code ownership
|
# Global code ownership
|
||||||
* @Fallenbagel @gauthier-th
|
* @Fallenbagel
|
||||||
|
|||||||
@@ -8,4 +8,3 @@ pnpm-lock.yaml
|
|||||||
# assets
|
# assets
|
||||||
src/assets/
|
src/assets/
|
||||||
public/
|
public/
|
||||||
docs/
|
|
||||||
|
|||||||
@@ -3,12 +3,6 @@ module.exports = {
|
|||||||
singleQuote: true,
|
singleQuote: true,
|
||||||
trailingComma: 'es5',
|
trailingComma: 'es5',
|
||||||
overrides: [
|
overrides: [
|
||||||
{
|
|
||||||
files: 'pnpm-lock.yaml',
|
|
||||||
options: {
|
|
||||||
rangeEnd: 0, // default: Infinity
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
files: 'gen-docs/pnpm-lock.yaml',
|
files: 'gen-docs/pnpm-lock.yaml',
|
||||||
options: {
|
options: {
|
||||||
|
|||||||
11
README.md
11
README.md
@@ -11,7 +11,7 @@
|
|||||||
<a href="http://jellyseerr.borgcube.de/engage/jellyseerr/"><img src="http://jellyseerr.borgcube.de/widget/jellyseerr/jellyseerr-frontend/svg-badge.svg" alt="Translation status" /></a>
|
<a href="http://jellyseerr.borgcube.de/engage/jellyseerr/"><img src="http://jellyseerr.borgcube.de/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>
|
<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 -->
|
<!-- 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-47-orange.svg"/></a>
|
<a href="#contributors-"><img alt="All Contributors" src="https://img.shields.io/badge/all_contributors-40-orange.svg"/></a>
|
||||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||||
|
|
||||||
**Jellyseerr** is a free and open source software application for managing requests for your media library.
|
**Jellyseerr** is a free and open source software application for managing requests for your media library.
|
||||||
@@ -137,15 +137,6 @@ Thanks goes to these wonderful people from Overseerr ([emoji key](https://allcon
|
|||||||
<td align="center" valign="top" width="14.28%"><a href="https://joaquinolivero.com"><img src="https://avatars.githubusercontent.com/u/66050823?v=4?s=100" width="100px;" alt="Joaquin Olivero"/><br /><sub><b>Joaquin Olivero</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=JoaquinOlivero" title="Code">💻</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://joaquinolivero.com"><img src="https://avatars.githubusercontent.com/u/66050823?v=4?s=100" width="100px;" alt="Joaquin Olivero"/><br /><sub><b>Joaquin Olivero</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=JoaquinOlivero" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Bretterteig"><img src="https://avatars.githubusercontent.com/u/47298401?v=4?s=100" width="100px;" alt="Julian Behr"/><br /><sub><b>Julian Behr</b></sub></a><br /><a href="#translation-Bretterteig" title="Translation">🌍</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Bretterteig"><img src="https://avatars.githubusercontent.com/u/47298401?v=4?s=100" width="100px;" alt="Julian Behr"/><br /><sub><b>Julian Behr</b></sub></a><br /><a href="#translation-Bretterteig" title="Translation">🌍</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ThowZzy"><img src="https://avatars.githubusercontent.com/u/61882536?v=4?s=100" width="100px;" alt="ThowZzy"/><br /><sub><b>ThowZzy</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=ThowZzy" title="Code">💻</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ThowZzy"><img src="https://avatars.githubusercontent.com/u/61882536?v=4?s=100" width="100px;" alt="ThowZzy"/><br /><sub><b>ThowZzy</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=ThowZzy" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="http://josephrisk.com"><img src="https://avatars.githubusercontent.com/u/18372584?v=4?s=100" width="100px;" alt="Joseph Risk"/><br /><sub><b>Joseph Risk</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=j0srisk" title="Code">💻</a></td>
|
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Loetwiek"><img src="https://avatars.githubusercontent.com/u/79059734?v=4?s=100" width="100px;" alt="Loetwiek"/><br /><sub><b>Loetwiek</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=Loetwiek" title="Code">💻</a></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Fuochi"><img src="https://avatars.githubusercontent.com/u/4720478?v=4?s=100" width="100px;" alt="Fuochi"/><br /><sub><b>Fuochi</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=Fuochi" title="Documentation">📖</a></td>
|
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://mobihen.com"><img src="https://avatars.githubusercontent.com/u/35529491?v=4?s=100" width="100px;" alt="Nir Israel Hen"/><br /><sub><b>Nir Israel Hen</b></sub></a><br /><a href="#translation-mobihen" title="Translation">🌍</a></td>
|
|
||||||
<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>
|
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const testUser = {
|
const testUser = {
|
||||||
username: 'Test User',
|
displayName: 'Test User',
|
||||||
emailAddress: 'test@seeerr.dev',
|
emailAddress: 'test@seeerr.dev',
|
||||||
password: 'test1234',
|
password: 'test1234',
|
||||||
};
|
};
|
||||||
@@ -32,7 +32,7 @@ describe('User List', () => {
|
|||||||
|
|
||||||
cy.get('[data-testid=modal-title]').should('contain', 'Create Local User');
|
cy.get('[data-testid=modal-title]').should('contain', 'Create Local User');
|
||||||
|
|
||||||
cy.get('#username').type(testUser.username);
|
cy.get('#displayName').type(testUser.displayName);
|
||||||
cy.get('#email').type(testUser.emailAddress);
|
cy.get('#email').type(testUser.emailAddress);
|
||||||
cy.get('#password').type(testUser.password);
|
cy.get('#password').type(testUser.password);
|
||||||
|
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ pnpm start
|
|||||||
```
|
```
|
||||||
</TabItem>
|
</TabItem>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
:::info
|
:::info
|
||||||
You can now access Jellyseerr by visiting `http://localhost:5055` in your web browser.
|
You can now access Jellyseerr by visiting `http://localhost:5055` in your web browser.
|
||||||
:::
|
:::
|
||||||
@@ -99,9 +99,6 @@ PORT=5055
|
|||||||
|
|
||||||
## Uncomment if your media server is emby instead of jellyfin.
|
## Uncomment if your media server is emby instead of jellyfin.
|
||||||
# JELLYFIN_TYPE=emby
|
# JELLYFIN_TYPE=emby
|
||||||
|
|
||||||
## Uncomment if you want to force Node.js to resolve IPv4 before IPv6 (advanced users only)
|
|
||||||
# FORCE_IPV4_FIRST=true
|
|
||||||
```
|
```
|
||||||
2. Then run the following commands:
|
2. Then run the following commands:
|
||||||
```bash
|
```bash
|
||||||
@@ -248,7 +245,6 @@ git checkout main
|
|||||||
```
|
```
|
||||||
3. Install the dependencies:
|
3. Install the dependencies:
|
||||||
```powershell
|
```powershell
|
||||||
npm install -g win-node-env
|
|
||||||
set CYPRESS_INSTALL_BINARY=0 && yarn install --frozen-lockfile --network-timeout 1000000
|
set CYPRESS_INSTALL_BINARY=0 && yarn install --frozen-lockfile --network-timeout 1000000
|
||||||
```
|
```
|
||||||
4. Build the project:
|
4. Build the project:
|
||||||
@@ -273,7 +269,6 @@ git checkout develop # by default, you are on the develop branch so this step is
|
|||||||
```
|
```
|
||||||
3. Install the dependencies:
|
3. Install the dependencies:
|
||||||
```powershell
|
```powershell
|
||||||
npm install -g win-node-env
|
|
||||||
set CYPRESS_INSTALL_BINARY=0 && pnpm install --frozen-lockfile
|
set CYPRESS_INSTALL_BINARY=0 && pnpm install --frozen-lockfile
|
||||||
```
|
```
|
||||||
4. Build the project:
|
4. Build the project:
|
||||||
@@ -317,7 +312,7 @@ node dist/index.js
|
|||||||
|
|
||||||
Now, Jellyseerr will start when the computer boots up in the background.
|
Now, Jellyseerr will start when the computer boots up in the background.
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
||||||
<TabItem value="nssm" label="NSSM">
|
<TabItem value="nssm" label="NSSM">
|
||||||
To run jellyseerr as a service:
|
To run jellyseerr as a service:
|
||||||
1. Download the [Non-Sucking Service Manager](https://nssm.cc/download)
|
1. Download the [Non-Sucking Service Manager](https://nssm.cc/download)
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
env: {
|
env: {
|
||||||
commitTag: process.env.COMMIT_TAG || 'local',
|
commitTag: process.env.COMMIT_TAG || 'local',
|
||||||
forceIpv4First: process.env.FORCE_IPV4_FIRST === 'true' ? 'true' : 'false',
|
|
||||||
},
|
},
|
||||||
publicRuntimeConfig: {
|
publicRuntimeConfig: {
|
||||||
// Will be available on both server and client
|
// Will be available on both server and client
|
||||||
@@ -14,6 +13,7 @@ module.exports = {
|
|||||||
remotePatterns: [
|
remotePatterns: [
|
||||||
{ hostname: 'gravatar.com' },
|
{ hostname: 'gravatar.com' },
|
||||||
{ hostname: 'image.tmdb.org' },
|
{ hostname: 'image.tmdb.org' },
|
||||||
|
{ hostname: '*', protocol: 'https' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
webpack(config) {
|
webpack(config) {
|
||||||
|
|||||||
@@ -2775,15 +2775,6 @@ paths:
|
|||||||
imageCount:
|
imageCount:
|
||||||
type: number
|
type: number
|
||||||
example: 123
|
example: 123
|
||||||
avatar:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
size:
|
|
||||||
type: number
|
|
||||||
example: 123456
|
|
||||||
imageCount:
|
|
||||||
type: number
|
|
||||||
example: 123
|
|
||||||
apiCaches:
|
apiCaches:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
|
|||||||
@@ -43,6 +43,8 @@
|
|||||||
"@svgr/webpack": "6.5.1",
|
"@svgr/webpack": "6.5.1",
|
||||||
"@tanem/react-nprogress": "5.0.30",
|
"@tanem/react-nprogress": "5.0.30",
|
||||||
"ace-builds": "1.15.2",
|
"ace-builds": "1.15.2",
|
||||||
|
"axios": "1.3.4",
|
||||||
|
"axios-rate-limit": "1.3.0",
|
||||||
"bcrypt": "5.1.0",
|
"bcrypt": "5.1.0",
|
||||||
"bowser": "2.11.0",
|
"bowser": "2.11.0",
|
||||||
"connect-typeorm": "1.1.4",
|
"connect-typeorm": "1.1.4",
|
||||||
@@ -62,7 +64,6 @@
|
|||||||
"formik": "^2.4.6",
|
"formik": "^2.4.6",
|
||||||
"gravatar-url": "3.1.0",
|
"gravatar-url": "3.1.0",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"mime": "3",
|
|
||||||
"next": "^14.2.4",
|
"next": "^14.2.4",
|
||||||
"node-cache": "5.1.2",
|
"node-cache": "5.1.2",
|
||||||
"node-gyp": "9.3.1",
|
"node-gyp": "9.3.1",
|
||||||
@@ -120,8 +121,7 @@
|
|||||||
"@types/express": "4.17.17",
|
"@types/express": "4.17.17",
|
||||||
"@types/express-session": "1.17.6",
|
"@types/express-session": "1.17.6",
|
||||||
"@types/lodash": "4.14.191",
|
"@types/lodash": "4.14.191",
|
||||||
"@types/mime": "3",
|
"@types/node": "17.0.36",
|
||||||
"@types/node": "20.14.8",
|
|
||||||
"@types/node-schedule": "2.1.0",
|
"@types/node-schedule": "2.1.0",
|
||||||
"@types/nodemailer": "6.4.7",
|
"@types/nodemailer": "6.4.7",
|
||||||
"@types/react": "^18.3.3",
|
"@types/react": "^18.3.3",
|
||||||
|
|||||||
12824
pnpm-lock.yaml
generated
12824
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,7 @@
|
|||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
import fs, { promises as fsp } from 'node:fs';
|
import axios from 'axios';
|
||||||
import path from 'node:path';
|
import fs, { promises as fsp } from 'fs';
|
||||||
import { Readable } from 'node:stream';
|
import path from 'path';
|
||||||
import type { ReadableStream } from 'node:stream/web';
|
|
||||||
import xml2js from 'xml2js';
|
import xml2js from 'xml2js';
|
||||||
|
|
||||||
const UPDATE_INTERVAL_MSEC = 24 * 3600 * 1000; // how often to download new mapping in milliseconds
|
const UPDATE_INTERVAL_MSEC = 24 * 3600 * 1000; // how often to download new mapping in milliseconds
|
||||||
@@ -162,18 +161,13 @@ class AnimeListMapping {
|
|||||||
label: 'Anime-List Sync',
|
label: 'Anime-List Sync',
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
const response = await fetch(MAPPING_URL);
|
const response = await axios.get(MAPPING_URL, {
|
||||||
if (!response.ok) {
|
responseType: 'stream',
|
||||||
throw new Error(`Failed to fetch: ${response.statusText}`);
|
});
|
||||||
}
|
await new Promise<void>((resolve) => {
|
||||||
await new Promise<void>((resolve, reject) => {
|
|
||||||
const writer = fs.createWriteStream(LOCAL_PATH);
|
const writer = fs.createWriteStream(LOCAL_PATH);
|
||||||
writer.on('finish', resolve);
|
writer.on('finish', resolve);
|
||||||
writer.on('error', reject);
|
response.data.pipe(writer);
|
||||||
if (!response.body) return reject();
|
|
||||||
Readable.fromWeb(response.body as ReadableStream<Uint8Array>).pipe(
|
|
||||||
writer
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`Failed to download Anime-List mapping: ${e.message}`);
|
throw new Error(`Failed to download Anime-List mapping: ${e.message}`);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { RateLimitOptions } from '@server/utils/rateLimit';
|
import type { AxiosInstance, AxiosRequestConfig } from 'axios';
|
||||||
import rateLimit from '@server/utils/rateLimit';
|
import axios from 'axios';
|
||||||
|
import rateLimit from 'axios-rate-limit';
|
||||||
import type NodeCache from 'node-cache';
|
import type NodeCache from 'node-cache';
|
||||||
|
|
||||||
// 5 minute default TTL (in seconds)
|
// 5 minute default TTL (in seconds)
|
||||||
@@ -11,131 +12,71 @@ const DEFAULT_ROLLING_BUFFER = 10000;
|
|||||||
interface ExternalAPIOptions {
|
interface ExternalAPIOptions {
|
||||||
nodeCache?: NodeCache;
|
nodeCache?: NodeCache;
|
||||||
headers?: Record<string, unknown>;
|
headers?: Record<string, unknown>;
|
||||||
rateLimit?: RateLimitOptions;
|
rateLimit?: {
|
||||||
|
maxRPS: number;
|
||||||
|
maxRequests: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
class ExternalAPI {
|
class ExternalAPI {
|
||||||
protected fetch: typeof fetch;
|
protected axios: AxiosInstance;
|
||||||
protected params: Record<string, string>;
|
|
||||||
protected defaultHeaders: { [key: string]: string };
|
|
||||||
private baseUrl: string;
|
private baseUrl: string;
|
||||||
private cache?: NodeCache;
|
private cache?: NodeCache;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
params: Record<string, string> = {},
|
params: Record<string, unknown>,
|
||||||
options: ExternalAPIOptions = {}
|
options: ExternalAPIOptions = {}
|
||||||
) {
|
) {
|
||||||
|
this.axios = axios.create({
|
||||||
|
baseURL: baseUrl,
|
||||||
|
params,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json',
|
||||||
|
...options.headers,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (options.rateLimit) {
|
if (options.rateLimit) {
|
||||||
this.fetch = rateLimit(fetch, options.rateLimit);
|
this.axios = rateLimit(this.axios, {
|
||||||
} else {
|
maxRequests: options.rateLimit.maxRequests,
|
||||||
this.fetch = fetch;
|
maxRPS: options.rateLimit.maxRPS,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.baseUrl = baseUrl;
|
this.baseUrl = baseUrl;
|
||||||
this.params = params;
|
|
||||||
this.defaultHeaders = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Accept: 'application/json',
|
|
||||||
...options.headers,
|
|
||||||
};
|
|
||||||
this.cache = options.nodeCache;
|
this.cache = options.nodeCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async get<T>(
|
protected async get<T>(
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
params?: Record<string, string>,
|
config?: AxiosRequestConfig,
|
||||||
ttl?: number,
|
ttl?: number
|
||||||
config?: RequestInit
|
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const cacheKey = this.serializeCacheKey(endpoint, {
|
const cacheKey = this.serializeCacheKey(endpoint, config?.params);
|
||||||
...this.params,
|
|
||||||
...params,
|
|
||||||
});
|
|
||||||
const cachedItem = this.cache?.get<T>(cacheKey);
|
const cachedItem = this.cache?.get<T>(cacheKey);
|
||||||
if (cachedItem) {
|
if (cachedItem) {
|
||||||
return cachedItem;
|
return cachedItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = this.formatUrl(endpoint, params);
|
const response = await this.axios.get<T>(endpoint, config);
|
||||||
const response = await this.fetch(url, {
|
|
||||||
...config,
|
|
||||||
headers: {
|
|
||||||
...this.defaultHeaders,
|
|
||||||
...config?.headers,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
const text = await response.text();
|
|
||||||
throw new Error(
|
|
||||||
`${response.status} ${response.statusText}${text ? ': ' + text : ''}`,
|
|
||||||
{
|
|
||||||
cause: response,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const data = await this.getDataFromResponse(response);
|
|
||||||
|
|
||||||
if (this.cache) {
|
if (this.cache) {
|
||||||
this.cache.set(cacheKey, data, ttl ?? DEFAULT_TTL);
|
this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async post<T>(
|
protected async post<T>(
|
||||||
endpoint: string,
|
|
||||||
data?: Record<string, unknown>,
|
|
||||||
params?: Record<string, string>,
|
|
||||||
ttl?: number,
|
|
||||||
config?: RequestInit
|
|
||||||
): Promise<T> {
|
|
||||||
const cacheKey = this.serializeCacheKey(endpoint, {
|
|
||||||
config: { ...this.params, ...params },
|
|
||||||
data,
|
|
||||||
});
|
|
||||||
const cachedItem = this.cache?.get<T>(cacheKey);
|
|
||||||
if (cachedItem) {
|
|
||||||
return cachedItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = this.formatUrl(endpoint, params);
|
|
||||||
const response = await this.fetch(url, {
|
|
||||||
method: 'POST',
|
|
||||||
...config,
|
|
||||||
headers: {
|
|
||||||
...this.defaultHeaders,
|
|
||||||
...config?.headers,
|
|
||||||
},
|
|
||||||
body: data ? JSON.stringify(data) : undefined,
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
const text = await response.text();
|
|
||||||
throw new Error(
|
|
||||||
`${response.status} ${response.statusText}${text ? ': ' + text : ''}`,
|
|
||||||
{
|
|
||||||
cause: response,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const resData = await this.getDataFromResponse(response);
|
|
||||||
|
|
||||||
if (this.cache) {
|
|
||||||
this.cache.set(cacheKey, resData, ttl ?? DEFAULT_TTL);
|
|
||||||
}
|
|
||||||
|
|
||||||
return resData;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async put<T>(
|
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
data: Record<string, unknown>,
|
data: Record<string, unknown>,
|
||||||
params?: Record<string, string>,
|
config?: AxiosRequestConfig,
|
||||||
ttl?: number,
|
ttl?: number
|
||||||
config?: RequestInit
|
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const cacheKey = this.serializeCacheKey(endpoint, {
|
const cacheKey = this.serializeCacheKey(endpoint, {
|
||||||
config: { ...this.params, ...params },
|
config: config?.params,
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
const cachedItem = this.cache?.get<T>(cacheKey);
|
const cachedItem = this.cache?.get<T>(cacheKey);
|
||||||
@@ -143,73 +84,21 @@ class ExternalAPI {
|
|||||||
return cachedItem;
|
return cachedItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = this.formatUrl(endpoint, params);
|
const response = await this.axios.post<T>(endpoint, data, config);
|
||||||
const response = await this.fetch(url, {
|
|
||||||
method: 'PUT',
|
|
||||||
...config,
|
|
||||||
headers: {
|
|
||||||
...this.defaultHeaders,
|
|
||||||
...config?.headers,
|
|
||||||
},
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
const text = await response.text();
|
|
||||||
throw new Error(
|
|
||||||
`${response.status} ${response.statusText}${text ? ': ' + text : ''}`,
|
|
||||||
{
|
|
||||||
cause: response,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const resData = await this.getDataFromResponse(response);
|
|
||||||
|
|
||||||
if (this.cache) {
|
if (this.cache) {
|
||||||
this.cache.set(cacheKey, resData, ttl ?? DEFAULT_TTL);
|
this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
|
||||||
}
|
}
|
||||||
|
|
||||||
return resData;
|
return response.data;
|
||||||
}
|
|
||||||
|
|
||||||
protected async delete<T>(
|
|
||||||
endpoint: string,
|
|
||||||
params?: Record<string, string>,
|
|
||||||
config?: RequestInit
|
|
||||||
): Promise<T> {
|
|
||||||
const url = this.formatUrl(endpoint, params);
|
|
||||||
const response = await this.fetch(url, {
|
|
||||||
method: 'DELETE',
|
|
||||||
...config,
|
|
||||||
headers: {
|
|
||||||
...this.defaultHeaders,
|
|
||||||
...config?.headers,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
const text = await response.text();
|
|
||||||
throw new Error(
|
|
||||||
`${response.status} ${response.statusText}${text ? ': ' + text : ''}`,
|
|
||||||
{
|
|
||||||
cause: response,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const data = await this.getDataFromResponse(response);
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async getRolling<T>(
|
protected async getRolling<T>(
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
params?: Record<string, string>,
|
config?: AxiosRequestConfig,
|
||||||
ttl?: number,
|
ttl?: number
|
||||||
config?: RequestInit,
|
|
||||||
overwriteBaseUrl?: string
|
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const cacheKey = this.serializeCacheKey(endpoint, {
|
const cacheKey = this.serializeCacheKey(endpoint, config?.params);
|
||||||
...this.params,
|
|
||||||
...params,
|
|
||||||
});
|
|
||||||
const cachedItem = this.cache?.get<T>(cacheKey);
|
const cachedItem = this.cache?.get<T>(cacheKey);
|
||||||
|
|
||||||
if (cachedItem) {
|
if (cachedItem) {
|
||||||
@@ -220,78 +109,20 @@ class ExternalAPI {
|
|||||||
keyTtl - (ttl ?? DEFAULT_TTL) * 1000 <
|
keyTtl - (ttl ?? DEFAULT_TTL) * 1000 <
|
||||||
Date.now() - DEFAULT_ROLLING_BUFFER
|
Date.now() - DEFAULT_ROLLING_BUFFER
|
||||||
) {
|
) {
|
||||||
const url = this.formatUrl(endpoint, params, overwriteBaseUrl);
|
this.axios.get<T>(endpoint, config).then((response) => {
|
||||||
this.fetch(url, {
|
this.cache?.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
|
||||||
...config,
|
|
||||||
headers: {
|
|
||||||
...this.defaultHeaders,
|
|
||||||
...config?.headers,
|
|
||||||
},
|
|
||||||
}).then(async (response) => {
|
|
||||||
if (!response.ok) {
|
|
||||||
const text = await response.text();
|
|
||||||
throw new Error(
|
|
||||||
`${response.status} ${response.statusText}${
|
|
||||||
text ? ': ' + text : ''
|
|
||||||
}`,
|
|
||||||
{
|
|
||||||
cause: response,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const data = await this.getDataFromResponse(response);
|
|
||||||
this.cache?.set(cacheKey, data, ttl ?? DEFAULT_TTL);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return cachedItem;
|
return cachedItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = this.formatUrl(endpoint, params, overwriteBaseUrl);
|
const response = await this.axios.get<T>(endpoint, config);
|
||||||
const response = await this.fetch(url, {
|
|
||||||
...config,
|
|
||||||
headers: {
|
|
||||||
...this.defaultHeaders,
|
|
||||||
...config?.headers,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
const text = await response.text();
|
|
||||||
throw new Error(
|
|
||||||
`${response.status} ${response.statusText}${text ? ': ' + text : ''}`,
|
|
||||||
{
|
|
||||||
cause: response,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const data = await this.getDataFromResponse(response);
|
|
||||||
|
|
||||||
if (this.cache) {
|
if (this.cache) {
|
||||||
this.cache.set(cacheKey, data, ttl ?? DEFAULT_TTL);
|
this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
return response.data;
|
||||||
}
|
|
||||||
|
|
||||||
private formatUrl(
|
|
||||||
endpoint: string,
|
|
||||||
params?: Record<string, string>,
|
|
||||||
overwriteBaseUrl?: string
|
|
||||||
): string {
|
|
||||||
const baseUrl = overwriteBaseUrl || this.baseUrl;
|
|
||||||
const href =
|
|
||||||
baseUrl +
|
|
||||||
(baseUrl.endsWith('/') ? '' : '/') +
|
|
||||||
(endpoint.startsWith('/') ? endpoint.slice(1) : endpoint);
|
|
||||||
const searchParams = new URLSearchParams({
|
|
||||||
...this.params,
|
|
||||||
...params,
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
href +
|
|
||||||
(searchParams.toString().length
|
|
||||||
? '?' + searchParams.toString()
|
|
||||||
: searchParams.toString())
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private serializeCacheKey(
|
private serializeCacheKey(
|
||||||
@@ -304,29 +135,6 @@ class ExternalAPI {
|
|||||||
|
|
||||||
return `${this.baseUrl}${endpoint}${JSON.stringify(params)}`;
|
return `${this.baseUrl}${endpoint}${JSON.stringify(params)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getDataFromResponse(response: Response) {
|
|
||||||
const contentType = response.headers.get('Content-Type');
|
|
||||||
if (contentType?.includes('application/json')) {
|
|
||||||
return await response.json();
|
|
||||||
} else if (
|
|
||||||
contentType?.includes('application/xml') ||
|
|
||||||
contentType?.includes('text/html') ||
|
|
||||||
contentType?.includes('text/plain')
|
|
||||||
) {
|
|
||||||
return await response.text();
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
return await response.json();
|
|
||||||
} catch {
|
|
||||||
try {
|
|
||||||
return await response.blob();
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ExternalAPI;
|
export default ExternalAPI;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import ExternalAPI from '@server/api/externalapi';
|
|
||||||
import cacheManager from '@server/lib/cache';
|
import cacheManager from '@server/lib/cache';
|
||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
|
import ExternalAPI from './externalapi';
|
||||||
|
|
||||||
interface GitHubRelease {
|
interface GitHubRelease {
|
||||||
url: string;
|
url: string;
|
||||||
@@ -67,6 +67,10 @@ class GithubAPI extends ExternalAPI {
|
|||||||
'https://api.github.com',
|
'https://api.github.com',
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json',
|
||||||
|
},
|
||||||
nodeCache: cacheManager.getCache('github').data,
|
nodeCache: cacheManager.getCache('github').data,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -81,7 +85,9 @@ class GithubAPI extends ExternalAPI {
|
|||||||
const data = await this.get<GitHubRelease[]>(
|
const data = await this.get<GitHubRelease[]>(
|
||||||
'/repos/fallenbagel/jellyseerr/releases',
|
'/repos/fallenbagel/jellyseerr/releases',
|
||||||
{
|
{
|
||||||
per_page: take.toString(),
|
params: {
|
||||||
|
per_page: take,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -106,8 +112,10 @@ class GithubAPI extends ExternalAPI {
|
|||||||
const data = await this.get<GithubCommit[]>(
|
const data = await this.get<GithubCommit[]>(
|
||||||
'/repos/fallenbagel/jellyseerr/commits',
|
'/repos/fallenbagel/jellyseerr/commits',
|
||||||
{
|
{
|
||||||
per_page: take.toString(),
|
params: {
|
||||||
branch,
|
per_page: take,
|
||||||
|
branch,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -93,7 +93,9 @@ export interface JellyfinLibraryItemExtended extends JellyfinLibraryItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class JellyfinAPI extends ExternalAPI {
|
class JellyfinAPI extends ExternalAPI {
|
||||||
|
private authToken?: string;
|
||||||
private userId?: string;
|
private userId?: string;
|
||||||
|
private jellyfinHost: string;
|
||||||
|
|
||||||
constructor(jellyfinHost: string, authToken?: string, deviceId?: string) {
|
constructor(jellyfinHost: string, authToken?: string, deviceId?: string) {
|
||||||
let authHeaderVal: string;
|
let authHeaderVal: string;
|
||||||
@@ -109,9 +111,14 @@ class JellyfinAPI extends ExternalAPI {
|
|||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
'X-Emby-Authorization': authHeaderVal,
|
'X-Emby-Authorization': authHeaderVal,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.jellyfinHost = jellyfinHost;
|
||||||
|
this.authToken = authToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async login(
|
public async login(
|
||||||
@@ -120,7 +127,7 @@ class JellyfinAPI extends ExternalAPI {
|
|||||||
ClientIP?: string
|
ClientIP?: string
|
||||||
): Promise<JellyfinLoginResponse> {
|
): Promise<JellyfinLoginResponse> {
|
||||||
const authenticate = async (useHeaders: boolean) => {
|
const authenticate = async (useHeaders: boolean) => {
|
||||||
const headers: { [key: string]: string } =
|
const headers =
|
||||||
useHeaders && ClientIP ? { 'X-Forwarded-For': ClientIP } : {};
|
useHeaders && ClientIP ? { 'X-Forwarded-For': ClientIP } : {};
|
||||||
|
|
||||||
return this.post<JellyfinLoginResponse>(
|
return this.post<JellyfinLoginResponse>(
|
||||||
@@ -129,8 +136,6 @@ class JellyfinAPI extends ExternalAPI {
|
|||||||
Username,
|
Username,
|
||||||
Pw: Password,
|
Pw: Password,
|
||||||
},
|
},
|
||||||
{},
|
|
||||||
undefined,
|
|
||||||
{ headers }
|
{ headers }
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -147,7 +152,7 @@ class JellyfinAPI extends ExternalAPI {
|
|||||||
try {
|
try {
|
||||||
return await authenticate(false);
|
return await authenticate(false);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const status = e.cause?.status;
|
const status = e.response?.status;
|
||||||
|
|
||||||
const networkErrorCodes = new Set([
|
const networkErrorCodes = new Set([
|
||||||
'ECONNREFUSED',
|
'ECONNREFUSED',
|
||||||
@@ -185,7 +190,7 @@ class JellyfinAPI extends ExternalAPI {
|
|||||||
|
|
||||||
return systemInfoResponse;
|
return systemInfoResponse;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken);
|
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,7 +207,7 @@ class JellyfinAPI extends ExternalAPI {
|
|||||||
{ label: 'Jellyfin API' }
|
{ label: 'Jellyfin API' }
|
||||||
);
|
);
|
||||||
|
|
||||||
throw new ApiError(e.cause?.status, ApiErrorCode.Unknown);
|
throw new ApiError(e.response?.status, ApiErrorCode.Unknown);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,7 +222,7 @@ class JellyfinAPI extends ExternalAPI {
|
|||||||
{ label: 'Jellyfin API' }
|
{ label: 'Jellyfin API' }
|
||||||
);
|
);
|
||||||
|
|
||||||
throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken);
|
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,7 +238,7 @@ class JellyfinAPI extends ExternalAPI {
|
|||||||
{ label: 'Jellyfin API' }
|
{ label: 'Jellyfin API' }
|
||||||
);
|
);
|
||||||
|
|
||||||
throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken);
|
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,16 +296,7 @@ class JellyfinAPI extends ExternalAPI {
|
|||||||
public async getLibraryContents(id: string): Promise<JellyfinLibraryItem[]> {
|
public async getLibraryContents(id: string): Promise<JellyfinLibraryItem[]> {
|
||||||
try {
|
try {
|
||||||
const libraryItemsResponse = await this.get<any>(
|
const libraryItemsResponse = await this.get<any>(
|
||||||
`/Users/${this.userId}/Items`,
|
`/Users/${this.userId}/Items?SortBy=SortName&SortOrder=Ascending&IncludeItemTypes=Series,Movie,Others&Recursive=true&StartIndex=0&ParentId=${id}&collapseBoxSetItems=false`
|
||||||
{
|
|
||||||
SortBy: 'SortName',
|
|
||||||
SortOrder: 'Ascending',
|
|
||||||
IncludeItemTypes: 'Series,Movie,Others',
|
|
||||||
Recursive: 'true',
|
|
||||||
StartIndex: '0',
|
|
||||||
ParentId: id,
|
|
||||||
collapseBoxSetItems: 'false',
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return libraryItemsResponse.Items.filter(
|
return libraryItemsResponse.Items.filter(
|
||||||
@@ -312,18 +308,14 @@ class JellyfinAPI extends ExternalAPI {
|
|||||||
{ label: 'Jellyfin API' }
|
{ label: 'Jellyfin API' }
|
||||||
);
|
);
|
||||||
|
|
||||||
throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken);
|
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getRecentlyAdded(id: string): Promise<JellyfinLibraryItem[]> {
|
public async getRecentlyAdded(id: string): Promise<JellyfinLibraryItem[]> {
|
||||||
try {
|
try {
|
||||||
const itemResponse = await this.get<any>(
|
const itemResponse = await this.get<any>(
|
||||||
`/Users/${this.userId}/Items/Latest`,
|
`/Users/${this.userId}/Items/Latest?Limit=12&ParentId=${id}`
|
||||||
{
|
|
||||||
Limit: '12',
|
|
||||||
ParentId: id,
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return itemResponse;
|
return itemResponse;
|
||||||
@@ -333,7 +325,7 @@ class JellyfinAPI extends ExternalAPI {
|
|||||||
{ label: 'Jellyfin API' }
|
{ label: 'Jellyfin API' }
|
||||||
);
|
);
|
||||||
|
|
||||||
throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken);
|
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,7 +340,7 @@ class JellyfinAPI extends ExternalAPI {
|
|||||||
return itemResponse;
|
return itemResponse;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (availabilitySync.running) {
|
if (availabilitySync.running) {
|
||||||
if (e.cause?.status === 500) {
|
if (e.response && e.response.status === 500) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -357,7 +349,7 @@ class JellyfinAPI extends ExternalAPI {
|
|||||||
`Something went wrong while getting library content from the Jellyfin server: ${e.message}`,
|
`Something went wrong while getting library content from the Jellyfin server: ${e.message}`,
|
||||||
{ label: 'Jellyfin API' }
|
{ label: 'Jellyfin API' }
|
||||||
);
|
);
|
||||||
throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken);
|
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,7 +364,7 @@ class JellyfinAPI extends ExternalAPI {
|
|||||||
{ label: 'Jellyfin API' }
|
{ label: 'Jellyfin API' }
|
||||||
);
|
);
|
||||||
|
|
||||||
throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken);
|
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -382,10 +374,7 @@ class JellyfinAPI extends ExternalAPI {
|
|||||||
): Promise<JellyfinLibraryItem[]> {
|
): Promise<JellyfinLibraryItem[]> {
|
||||||
try {
|
try {
|
||||||
const episodeResponse = await this.get<any>(
|
const episodeResponse = await this.get<any>(
|
||||||
`/Shows/${seriesID}/Episodes`,
|
`/Shows/${seriesID}/Episodes?seasonId=${seasonID}`
|
||||||
{
|
|
||||||
seasonId: seasonID,
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return episodeResponse.Items.filter(
|
return episodeResponse.Items.filter(
|
||||||
@@ -397,23 +386,6 @@ class JellyfinAPI extends ExternalAPI {
|
|||||||
{ label: 'Jellyfin API' }
|
{ label: 'Jellyfin API' }
|
||||||
);
|
);
|
||||||
|
|
||||||
throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async createApiToken(appName: string): Promise<string> {
|
|
||||||
try {
|
|
||||||
await this.post(`/Auth/Keys?App=${appName}`);
|
|
||||||
const apiKeys = await this.get<any>(`/Auth/Keys`);
|
|
||||||
return apiKeys.Items.reverse().find(
|
|
||||||
(item: any) => item.AppName === appName
|
|
||||||
).AccessToken;
|
|
||||||
} catch (e) {
|
|
||||||
logger.error(
|
|
||||||
`Something went wrong while creating an API key the Jellyfin server: ${e.message}`,
|
|
||||||
{ label: 'Jellyfin API' }
|
|
||||||
);
|
|
||||||
|
|
||||||
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
|
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import ExternalAPI from '@server/api/externalapi';
|
|
||||||
import type { PlexDevice } from '@server/interfaces/api/plexInterfaces';
|
import type { PlexDevice } from '@server/interfaces/api/plexInterfaces';
|
||||||
import cacheManager from '@server/lib/cache';
|
import cacheManager from '@server/lib/cache';
|
||||||
import { getSettings } from '@server/lib/settings';
|
import { getSettings } from '@server/lib/settings';
|
||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
import xml2js from 'xml2js';
|
import xml2js from 'xml2js';
|
||||||
|
import ExternalAPI from './externalapi';
|
||||||
|
|
||||||
interface PlexAccountResponse {
|
interface PlexAccountResponse {
|
||||||
user: PlexUser;
|
user: PlexUser;
|
||||||
@@ -137,6 +137,8 @@ class PlexTvAPI extends ExternalAPI {
|
|||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
'X-Plex-Token': authToken,
|
'X-Plex-Token': authToken,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json',
|
||||||
},
|
},
|
||||||
nodeCache: cacheManager.getCache('plextv').data,
|
nodeCache: cacheManager.getCache('plextv').data,
|
||||||
}
|
}
|
||||||
@@ -147,11 +149,15 @@ class PlexTvAPI extends ExternalAPI {
|
|||||||
|
|
||||||
public async getDevices(): Promise<PlexDevice[]> {
|
public async getDevices(): Promise<PlexDevice[]> {
|
||||||
try {
|
try {
|
||||||
const devicesResp = await this.get('/api/resources', {
|
const devicesResp = await this.axios.get(
|
||||||
includeHttps: '1',
|
'/api/resources?includeHttps=1',
|
||||||
});
|
{
|
||||||
|
transformResponse: [],
|
||||||
|
responseType: 'text',
|
||||||
|
}
|
||||||
|
);
|
||||||
const parsedXml = await xml2js.parseStringPromise(
|
const parsedXml = await xml2js.parseStringPromise(
|
||||||
devicesResp as DeviceResponse
|
devicesResp.data as DeviceResponse
|
||||||
);
|
);
|
||||||
return parsedXml?.MediaContainer?.Device?.map((pxml: DeviceResponse) => ({
|
return parsedXml?.MediaContainer?.Device?.map((pxml: DeviceResponse) => ({
|
||||||
name: pxml.$.name,
|
name: pxml.$.name,
|
||||||
@@ -199,11 +205,11 @@ class PlexTvAPI extends ExternalAPI {
|
|||||||
|
|
||||||
public async getUser(): Promise<PlexUser> {
|
public async getUser(): Promise<PlexUser> {
|
||||||
try {
|
try {
|
||||||
const account = await this.get<PlexAccountResponse>(
|
const account = await this.axios.get<PlexAccountResponse>(
|
||||||
'/users/account.json'
|
'/users/account.json'
|
||||||
);
|
);
|
||||||
|
|
||||||
return account.user;
|
return account.data.user;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Something went wrong while getting the account from plex.tv: ${e.message}`,
|
`Something went wrong while getting the account from plex.tv: ${e.message}`,
|
||||||
@@ -243,10 +249,13 @@ class PlexTvAPI extends ExternalAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getUsers(): Promise<UsersResponse> {
|
public async getUsers(): Promise<UsersResponse> {
|
||||||
const data = await this.get('/api/users');
|
const response = await this.axios.get('/api/users', {
|
||||||
|
transformResponse: [],
|
||||||
|
responseType: 'text',
|
||||||
|
});
|
||||||
|
|
||||||
const parsedXml = (await xml2js.parseStringPromise(
|
const parsedXml = (await xml2js.parseStringPromise(
|
||||||
data as string
|
response.data
|
||||||
)) as UsersResponse;
|
)) as UsersResponse;
|
||||||
return parsedXml;
|
return parsedXml;
|
||||||
}
|
}
|
||||||
@@ -261,49 +270,49 @@ class PlexTvAPI extends ExternalAPI {
|
|||||||
items: PlexWatchlistItem[];
|
items: PlexWatchlistItem[];
|
||||||
}> {
|
}> {
|
||||||
try {
|
try {
|
||||||
const params = new URLSearchParams({
|
const response = await this.axios.get<WatchlistResponse>(
|
||||||
'X-Plex-Container-Start': offset.toString(),
|
'/library/sections/watchlist/all',
|
||||||
'X-Plex-Container-Size': size.toString(),
|
|
||||||
});
|
|
||||||
const response = await this.fetch(
|
|
||||||
`https://metadata.provider.plex.tv/library/sections/watchlist/all?${params.toString()}`,
|
|
||||||
{
|
{
|
||||||
headers: this.defaultHeaders,
|
params: {
|
||||||
|
'X-Plex-Container-Start': offset,
|
||||||
|
'X-Plex-Container-Size': size,
|
||||||
|
},
|
||||||
|
baseURL: 'https://metadata.provider.plex.tv',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const data = (await response.json()) as WatchlistResponse;
|
|
||||||
|
|
||||||
const watchlistDetails = await Promise.all(
|
const watchlistDetails = await Promise.all(
|
||||||
(data.MediaContainer.Metadata ?? []).map(async (watchlistItem) => {
|
(response.data.MediaContainer.Metadata ?? []).map(
|
||||||
const detailedResponse = await this.getRolling<MetadataResponse>(
|
async (watchlistItem) => {
|
||||||
`/library/metadata/${watchlistItem.ratingKey}`,
|
const detailedResponse = await this.getRolling<MetadataResponse>(
|
||||||
{},
|
`/library/metadata/${watchlistItem.ratingKey}`,
|
||||||
undefined,
|
{
|
||||||
{},
|
baseURL: 'https://metadata.provider.plex.tv',
|
||||||
'https://metadata.provider.plex.tv'
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const metadata = detailedResponse.MediaContainer.Metadata[0];
|
const metadata = detailedResponse.MediaContainer.Metadata[0];
|
||||||
|
|
||||||
const tmdbString = metadata.Guid.find((guid) =>
|
const tmdbString = metadata.Guid.find((guid) =>
|
||||||
guid.id.startsWith('tmdb')
|
guid.id.startsWith('tmdb')
|
||||||
);
|
);
|
||||||
const tvdbString = metadata.Guid.find((guid) =>
|
const tvdbString = metadata.Guid.find((guid) =>
|
||||||
guid.id.startsWith('tvdb')
|
guid.id.startsWith('tvdb')
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ratingKey: metadata.ratingKey,
|
ratingKey: metadata.ratingKey,
|
||||||
// This should always be set? But I guess it also cannot be?
|
// This should always be set? But I guess it also cannot be?
|
||||||
// We will filter out the 0's afterwards
|
// We will filter out the 0's afterwards
|
||||||
tmdbId: tmdbString ? Number(tmdbString.id.split('//')[1]) : 0,
|
tmdbId: tmdbString ? Number(tmdbString.id.split('//')[1]) : 0,
|
||||||
tvdbId: tvdbString
|
tvdbId: tvdbString
|
||||||
? Number(tvdbString.id.split('//')[1])
|
? Number(tvdbString.id.split('//')[1])
|
||||||
: undefined,
|
: undefined,
|
||||||
title: metadata.title,
|
title: metadata.title,
|
||||||
type: metadata.type,
|
type: metadata.type,
|
||||||
};
|
};
|
||||||
})
|
}
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const filteredList = watchlistDetails.filter((detail) => detail.tmdbId);
|
const filteredList = watchlistDetails.filter((detail) => detail.tmdbId);
|
||||||
@@ -311,7 +320,7 @@ class PlexTvAPI extends ExternalAPI {
|
|||||||
return {
|
return {
|
||||||
offset,
|
offset,
|
||||||
size,
|
size,
|
||||||
totalSize: data.MediaContainer.totalSize,
|
totalSize: response.data.MediaContainer.totalSize,
|
||||||
items: filteredList,
|
items: filteredList,
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import ExternalAPI from '@server/api/externalapi';
|
import ExternalAPI from './externalapi';
|
||||||
|
|
||||||
interface PushoverSoundsResponse {
|
interface PushoverSoundsResponse {
|
||||||
sounds: {
|
sounds: {
|
||||||
@@ -26,13 +26,24 @@ export const mapSounds = (sounds: {
|
|||||||
|
|
||||||
class PushoverAPI extends ExternalAPI {
|
class PushoverAPI extends ExternalAPI {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('https://api.pushover.net/1');
|
super(
|
||||||
|
'https://api.pushover.net/1',
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getSounds(appToken: string): Promise<PushoverSound[]> {
|
public async getSounds(appToken: string): Promise<PushoverSound[]> {
|
||||||
try {
|
try {
|
||||||
const data = await this.get<PushoverSoundsResponse>('/sounds.json', {
|
const data = await this.get<PushoverSoundsResponse>('/sounds.json', {
|
||||||
token: appToken,
|
params: {
|
||||||
|
token: appToken,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return mapSounds(data.sounds);
|
return mapSounds(data.sounds);
|
||||||
|
|||||||
@@ -155,13 +155,13 @@ export interface IMDBRating {
|
|||||||
*/
|
*/
|
||||||
class IMDBRadarrProxy extends ExternalAPI {
|
class IMDBRadarrProxy extends ExternalAPI {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(
|
super('https://api.radarr.video/v1', {
|
||||||
'https://api.radarr.video/v1',
|
headers: {
|
||||||
{},
|
'Content-Type': 'application/json',
|
||||||
{
|
Accept: 'application/json',
|
||||||
nodeCache: cacheManager.getCache('imdb').data,
|
},
|
||||||
}
|
nodeCache: cacheManager.getCache('imdb').data,
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -175,11 +175,7 @@ class IMDBRadarrProxy extends ExternalAPI {
|
|||||||
`/movie/imdb/${IMDBid}`
|
`/movie/imdb/${IMDBid}`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (!data?.length || data[0].ImdbId !== IMDBid) {
|
||||||
!data?.length ||
|
|
||||||
data[0].ImdbId !== IMDBid ||
|
|
||||||
!data[0].MovieRatings.Imdb
|
|
||||||
) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,12 +63,15 @@ class RottenTomatoes extends ExternalAPI {
|
|||||||
super(
|
super(
|
||||||
'https://79frdp12pn-dsn.algolia.net/1/indexes/*',
|
'https://79frdp12pn-dsn.algolia.net/1/indexes/*',
|
||||||
{
|
{
|
||||||
'x-algolia-agent': 'Algolia for JavaScript (4.14.3); Browser (lite)',
|
'x-algolia-agent':
|
||||||
|
'Algolia%20for%20JavaScript%20(4.14.3)%3B%20Browser%20(lite)',
|
||||||
'x-algolia-api-key': '175588f6e5f8319b27702e4cc4013561',
|
'x-algolia-api-key': '175588f6e5f8319b27702e4cc4013561',
|
||||||
'x-algolia-application-id': '79FRDP12PN',
|
'x-algolia-application-id': '79FRDP12PN',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json',
|
||||||
'x-algolia-usertoken': settings.clientId,
|
'x-algolia-usertoken': settings.clientId,
|
||||||
},
|
},
|
||||||
nodeCache: cacheManager.getCache('rt').data,
|
nodeCache: cacheManager.getCache('rt').data,
|
||||||
|
|||||||
@@ -113,9 +113,9 @@ class ServarrBase<QueueItemAppendT> extends ExternalAPI {
|
|||||||
|
|
||||||
public getSystemStatus = async (): Promise<SystemStatus> => {
|
public getSystemStatus = async (): Promise<SystemStatus> => {
|
||||||
try {
|
try {
|
||||||
const data = await this.get<SystemStatus>('/system/status');
|
const response = await this.axios.get<SystemStatus>('/system/status');
|
||||||
|
|
||||||
return data;
|
return response.data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`[${this.apiName}] Failed to retrieve system status: ${e.message}`
|
`[${this.apiName}] Failed to retrieve system status: ${e.message}`
|
||||||
@@ -157,11 +157,16 @@ class ServarrBase<QueueItemAppendT> extends ExternalAPI {
|
|||||||
|
|
||||||
public getQueue = async (): Promise<(QueueItem & QueueItemAppendT)[]> => {
|
public getQueue = async (): Promise<(QueueItem & QueueItemAppendT)[]> => {
|
||||||
try {
|
try {
|
||||||
const data = await this.get<QueueResponse<QueueItemAppendT>>(`/queue`, {
|
const response = await this.axios.get<QueueResponse<QueueItemAppendT>>(
|
||||||
includeEpisode: 'true',
|
`/queue`,
|
||||||
});
|
{
|
||||||
|
params: {
|
||||||
|
includeEpisode: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return data.records;
|
return response.data.records;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`[${this.apiName}] Failed to retrieve queue: ${e.message}`
|
`[${this.apiName}] Failed to retrieve queue: ${e.message}`
|
||||||
@@ -171,9 +176,9 @@ class ServarrBase<QueueItemAppendT> extends ExternalAPI {
|
|||||||
|
|
||||||
public getTags = async (): Promise<Tag[]> => {
|
public getTags = async (): Promise<Tag[]> => {
|
||||||
try {
|
try {
|
||||||
const data = await this.get<Tag[]>(`/tag`);
|
const response = await this.axios.get<Tag[]>(`/tag`);
|
||||||
|
|
||||||
return data;
|
return response.data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`[${this.apiName}] Failed to retrieve tags: ${e.message}`
|
`[${this.apiName}] Failed to retrieve tags: ${e.message}`
|
||||||
@@ -183,11 +188,11 @@ class ServarrBase<QueueItemAppendT> extends ExternalAPI {
|
|||||||
|
|
||||||
public createTag = async ({ label }: { label: string }): Promise<Tag> => {
|
public createTag = async ({ label }: { label: string }): Promise<Tag> => {
|
||||||
try {
|
try {
|
||||||
const data = await this.post<Tag>(`/tag`, {
|
const response = await this.axios.post<Tag>(`/tag`, {
|
||||||
label,
|
label,
|
||||||
});
|
});
|
||||||
|
|
||||||
return data;
|
return response.data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`[${this.apiName}] Failed to create tag: ${e.message}`);
|
throw new Error(`[${this.apiName}] Failed to create tag: ${e.message}`);
|
||||||
}
|
}
|
||||||
@@ -198,7 +203,7 @@ class ServarrBase<QueueItemAppendT> extends ExternalAPI {
|
|||||||
options: Record<string, unknown>
|
options: Record<string, unknown>
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await this.post(`/command`, {
|
await this.axios.post(`/command`, {
|
||||||
name: commandName,
|
name: commandName,
|
||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -37,9 +37,9 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
|
|||||||
|
|
||||||
public getMovies = async (): Promise<RadarrMovie[]> => {
|
public getMovies = async (): Promise<RadarrMovie[]> => {
|
||||||
try {
|
try {
|
||||||
const data = await this.get<RadarrMovie[]>('/movie');
|
const response = await this.axios.get<RadarrMovie[]>('/movie');
|
||||||
|
|
||||||
return data;
|
return response.data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`[Radarr] Failed to retrieve movies: ${e.message}`);
|
throw new Error(`[Radarr] Failed to retrieve movies: ${e.message}`);
|
||||||
}
|
}
|
||||||
@@ -47,9 +47,9 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
|
|||||||
|
|
||||||
public getMovie = async ({ id }: { id: number }): Promise<RadarrMovie> => {
|
public getMovie = async ({ id }: { id: number }): Promise<RadarrMovie> => {
|
||||||
try {
|
try {
|
||||||
const data = await this.get<RadarrMovie>(`/movie/${id}`);
|
const response = await this.axios.get<RadarrMovie>(`/movie/${id}`);
|
||||||
|
|
||||||
return data;
|
return response.data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`[Radarr] Failed to retrieve movie: ${e.message}`);
|
throw new Error(`[Radarr] Failed to retrieve movie: ${e.message}`);
|
||||||
}
|
}
|
||||||
@@ -57,15 +57,17 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
|
|||||||
|
|
||||||
public async getMovieByTmdbId(id: number): Promise<RadarrMovie> {
|
public async getMovieByTmdbId(id: number): Promise<RadarrMovie> {
|
||||||
try {
|
try {
|
||||||
const data = await this.get<RadarrMovie[]>('/movie/lookup', {
|
const response = await this.axios.get<RadarrMovie[]>('/movie/lookup', {
|
||||||
term: `tmdb:${id}`,
|
params: {
|
||||||
|
term: `tmdb:${id}`,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!data[0]) {
|
if (!response.data[0]) {
|
||||||
throw new Error('Movie not found');
|
throw new Error('Movie not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
return data[0];
|
return response.data[0];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Error retrieving movie by TMDB ID', {
|
logger.error('Error retrieving movie by TMDB ID', {
|
||||||
label: 'Radarr API',
|
label: 'Radarr API',
|
||||||
@@ -95,7 +97,7 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
|
|||||||
|
|
||||||
// movie exists in Radarr but is neither downloaded nor monitored
|
// movie exists in Radarr but is neither downloaded nor monitored
|
||||||
if (movie.id && !movie.monitored) {
|
if (movie.id && !movie.monitored) {
|
||||||
const data = await this.put<RadarrMovie>(`/movie`, {
|
const response = await this.axios.put<RadarrMovie>(`/movie`, {
|
||||||
...movie,
|
...movie,
|
||||||
title: options.title,
|
title: options.title,
|
||||||
qualityProfileId: options.qualityProfileId,
|
qualityProfileId: options.qualityProfileId,
|
||||||
@@ -112,25 +114,25 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data.monitored) {
|
if (response.data.monitored) {
|
||||||
logger.info(
|
logger.info(
|
||||||
'Found existing title in Radarr and set it to monitored.',
|
'Found existing title in Radarr and set it to monitored.',
|
||||||
{
|
{
|
||||||
label: 'Radarr',
|
label: 'Radarr',
|
||||||
movieId: data.id,
|
movieId: response.data.id,
|
||||||
movieTitle: data.title,
|
movieTitle: response.data.title,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
logger.debug('Radarr update details', {
|
logger.debug('Radarr update details', {
|
||||||
label: 'Radarr',
|
label: 'Radarr',
|
||||||
movie: data,
|
movie: response.data,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (options.searchNow) {
|
if (options.searchNow) {
|
||||||
this.searchMovie(data.id);
|
this.searchMovie(response.data.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
return response.data;
|
||||||
} else {
|
} else {
|
||||||
logger.error('Failed to update existing movie in Radarr.', {
|
logger.error('Failed to update existing movie in Radarr.', {
|
||||||
label: 'Radarr',
|
label: 'Radarr',
|
||||||
@@ -148,7 +150,7 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
|
|||||||
return movie;
|
return movie;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await this.post<RadarrMovie>(`/movie`, {
|
const response = await this.axios.post<RadarrMovie>(`/movie`, {
|
||||||
title: options.title,
|
title: options.title,
|
||||||
qualityProfileId: options.qualityProfileId,
|
qualityProfileId: options.qualityProfileId,
|
||||||
profileId: options.profileId,
|
profileId: options.profileId,
|
||||||
@@ -164,11 +166,11 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data.id) {
|
if (response.data.id) {
|
||||||
logger.info('Radarr accepted request', { label: 'Radarr' });
|
logger.info('Radarr accepted request', { label: 'Radarr' });
|
||||||
logger.debug('Radarr add details', {
|
logger.debug('Radarr add details', {
|
||||||
label: 'Radarr',
|
label: 'Radarr',
|
||||||
movie: data,
|
movie: response.data,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
logger.error('Failed to add movie to Radarr', {
|
logger.error('Failed to add movie to Radarr', {
|
||||||
@@ -177,22 +179,15 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
|
|||||||
});
|
});
|
||||||
throw new Error('Failed to add movie to Radarr');
|
throw new Error('Failed to add movie to Radarr');
|
||||||
}
|
}
|
||||||
return data;
|
return response.data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
let errorData;
|
|
||||||
try {
|
|
||||||
errorData = await e.cause?.text();
|
|
||||||
errorData = JSON.parse(errorData);
|
|
||||||
} catch {
|
|
||||||
/* empty */
|
|
||||||
}
|
|
||||||
logger.error(
|
logger.error(
|
||||||
'Failed to add movie to Radarr. This might happen if the movie already exists, in which case you can safely ignore this error.',
|
'Failed to add movie to Radarr. This might happen if the movie already exists, in which case you can safely ignore this error.',
|
||||||
{
|
{
|
||||||
label: 'Radarr',
|
label: 'Radarr',
|
||||||
errorMessage: e.message,
|
errorMessage: e.message,
|
||||||
options,
|
options,
|
||||||
response: errorData,
|
response: e?.response?.data,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
throw new Error('Failed to add movie to Radarr');
|
throw new Error('Failed to add movie to Radarr');
|
||||||
@@ -221,9 +216,11 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
|
|||||||
public removeMovie = async (movieId: number): Promise<void> => {
|
public removeMovie = async (movieId: number): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const { id, title } = await this.getMovieByTmdbId(movieId);
|
const { id, title } = await this.getMovieByTmdbId(movieId);
|
||||||
await this.delete(`/movie/${id}`, {
|
await this.axios.delete(`/movie/${id}`, {
|
||||||
deleteFiles: 'true',
|
params: {
|
||||||
addImportExclusion: 'false',
|
deleteFiles: true,
|
||||||
|
addImportExclusion: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
logger.info(`[Radarr] Removed movie ${title}`);
|
logger.info(`[Radarr] Removed movie ${title}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -117,9 +117,9 @@ class SonarrAPI extends ServarrBase<{
|
|||||||
|
|
||||||
public async getSeries(): Promise<SonarrSeries[]> {
|
public async getSeries(): Promise<SonarrSeries[]> {
|
||||||
try {
|
try {
|
||||||
const data = await this.get<SonarrSeries[]>('/series');
|
const response = await this.axios.get<SonarrSeries[]>('/series');
|
||||||
|
|
||||||
return data;
|
return response.data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`[Sonarr] Failed to retrieve series: ${e.message}`);
|
throw new Error(`[Sonarr] Failed to retrieve series: ${e.message}`);
|
||||||
}
|
}
|
||||||
@@ -127,9 +127,9 @@ class SonarrAPI extends ServarrBase<{
|
|||||||
|
|
||||||
public async getSeriesById(id: number): Promise<SonarrSeries> {
|
public async getSeriesById(id: number): Promise<SonarrSeries> {
|
||||||
try {
|
try {
|
||||||
const data = await this.get<SonarrSeries>(`/series/${id}`);
|
const response = await this.axios.get<SonarrSeries>(`/series/${id}`);
|
||||||
|
|
||||||
return data;
|
return response.data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`[Sonarr] Failed to retrieve series by ID: ${e.message}`);
|
throw new Error(`[Sonarr] Failed to retrieve series by ID: ${e.message}`);
|
||||||
}
|
}
|
||||||
@@ -137,15 +137,17 @@ class SonarrAPI extends ServarrBase<{
|
|||||||
|
|
||||||
public async getSeriesByTitle(title: string): Promise<SonarrSeries[]> {
|
public async getSeriesByTitle(title: string): Promise<SonarrSeries[]> {
|
||||||
try {
|
try {
|
||||||
const data = await this.get<SonarrSeries[]>('/series/lookup', {
|
const response = await this.axios.get<SonarrSeries[]>('/series/lookup', {
|
||||||
term: title,
|
params: {
|
||||||
|
term: title,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!data[0]) {
|
if (!response.data[0]) {
|
||||||
throw new Error('No series found');
|
throw new Error('No series found');
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
return response.data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Error retrieving series by series title', {
|
logger.error('Error retrieving series by series title', {
|
||||||
label: 'Sonarr API',
|
label: 'Sonarr API',
|
||||||
@@ -158,15 +160,17 @@ class SonarrAPI extends ServarrBase<{
|
|||||||
|
|
||||||
public async getSeriesByTvdbId(id: number): Promise<SonarrSeries> {
|
public async getSeriesByTvdbId(id: number): Promise<SonarrSeries> {
|
||||||
try {
|
try {
|
||||||
const data = await this.get<SonarrSeries[]>('/series/lookup', {
|
const response = await this.axios.get<SonarrSeries[]>('/series/lookup', {
|
||||||
term: `tvdb:${id}`,
|
params: {
|
||||||
|
term: `tvdb:${id}`,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!data[0]) {
|
if (!response.data[0]) {
|
||||||
throw new Error('Series not found');
|
throw new Error('Series not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
return data[0];
|
return response.data[0];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Error retrieving series by tvdb ID', {
|
logger.error('Error retrieving series by tvdb ID', {
|
||||||
label: 'Sonarr API',
|
label: 'Sonarr API',
|
||||||
@@ -187,27 +191,27 @@ class SonarrAPI extends ServarrBase<{
|
|||||||
series.tags = options.tags ?? series.tags;
|
series.tags = options.tags ?? series.tags;
|
||||||
series.seasons = this.buildSeasonList(options.seasons, series.seasons);
|
series.seasons = this.buildSeasonList(options.seasons, series.seasons);
|
||||||
|
|
||||||
const newSeriesData = await this.put<SonarrSeries>(
|
const newSeriesResponse = await this.axios.put<SonarrSeries>(
|
||||||
'/series',
|
'/series',
|
||||||
series as any
|
series
|
||||||
);
|
);
|
||||||
|
|
||||||
if (newSeriesData.id) {
|
if (newSeriesResponse.data.id) {
|
||||||
logger.info('Updated existing series in Sonarr.', {
|
logger.info('Updated existing series in Sonarr.', {
|
||||||
label: 'Sonarr',
|
label: 'Sonarr',
|
||||||
seriesId: newSeriesData.id,
|
seriesId: newSeriesResponse.data.id,
|
||||||
seriesTitle: newSeriesData.title,
|
seriesTitle: newSeriesResponse.data.title,
|
||||||
});
|
});
|
||||||
logger.debug('Sonarr update details', {
|
logger.debug('Sonarr update details', {
|
||||||
label: 'Sonarr',
|
label: 'Sonarr',
|
||||||
movie: newSeriesData,
|
movie: newSeriesResponse.data,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (options.searchNow) {
|
if (options.searchNow) {
|
||||||
this.searchSeries(newSeriesData.id);
|
this.searchSeries(newSeriesResponse.data.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return newSeriesData;
|
return newSeriesResponse.data;
|
||||||
} else {
|
} else {
|
||||||
logger.error('Failed to update series in Sonarr', {
|
logger.error('Failed to update series in Sonarr', {
|
||||||
label: 'Sonarr',
|
label: 'Sonarr',
|
||||||
@@ -217,35 +221,38 @@ class SonarrAPI extends ServarrBase<{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const createdSeriesData = await this.post<SonarrSeries>('/series', {
|
const createdSeriesResponse = await this.axios.post<SonarrSeries>(
|
||||||
tvdbId: options.tvdbid,
|
'/series',
|
||||||
title: options.title,
|
{
|
||||||
qualityProfileId: options.profileId,
|
tvdbId: options.tvdbid,
|
||||||
languageProfileId: options.languageProfileId,
|
title: options.title,
|
||||||
seasons: this.buildSeasonList(
|
qualityProfileId: options.profileId,
|
||||||
options.seasons,
|
languageProfileId: options.languageProfileId,
|
||||||
series.seasons.map((season) => ({
|
seasons: this.buildSeasonList(
|
||||||
seasonNumber: season.seasonNumber,
|
options.seasons,
|
||||||
// We force all seasons to false if its the first request
|
series.seasons.map((season) => ({
|
||||||
monitored: false,
|
seasonNumber: season.seasonNumber,
|
||||||
}))
|
// We force all seasons to false if its the first request
|
||||||
),
|
monitored: false,
|
||||||
tags: options.tags,
|
}))
|
||||||
seasonFolder: options.seasonFolder,
|
),
|
||||||
monitored: options.monitored,
|
tags: options.tags,
|
||||||
rootFolderPath: options.rootFolderPath,
|
seasonFolder: options.seasonFolder,
|
||||||
seriesType: options.seriesType,
|
monitored: options.monitored,
|
||||||
addOptions: {
|
rootFolderPath: options.rootFolderPath,
|
||||||
ignoreEpisodesWithFiles: true,
|
seriesType: options.seriesType,
|
||||||
searchForMissingEpisodes: options.searchNow,
|
addOptions: {
|
||||||
},
|
ignoreEpisodesWithFiles: true,
|
||||||
} as Partial<SonarrSeries>);
|
searchForMissingEpisodes: options.searchNow,
|
||||||
|
},
|
||||||
|
} as Partial<SonarrSeries>
|
||||||
|
);
|
||||||
|
|
||||||
if (createdSeriesData.id) {
|
if (createdSeriesResponse.data.id) {
|
||||||
logger.info('Sonarr accepted request', { label: 'Sonarr' });
|
logger.info('Sonarr accepted request', { label: 'Sonarr' });
|
||||||
logger.debug('Sonarr add details', {
|
logger.debug('Sonarr add details', {
|
||||||
label: 'Sonarr',
|
label: 'Sonarr',
|
||||||
movie: createdSeriesData,
|
movie: createdSeriesResponse.data,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
logger.error('Failed to add movie to Sonarr', {
|
logger.error('Failed to add movie to Sonarr', {
|
||||||
@@ -255,20 +262,13 @@ class SonarrAPI extends ServarrBase<{
|
|||||||
throw new Error('Failed to add series to Sonarr');
|
throw new Error('Failed to add series to Sonarr');
|
||||||
}
|
}
|
||||||
|
|
||||||
return createdSeriesData;
|
return createdSeriesResponse.data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
let errorData;
|
|
||||||
try {
|
|
||||||
errorData = await e.cause?.text();
|
|
||||||
errorData = JSON.parse(errorData);
|
|
||||||
} catch {
|
|
||||||
/* empty */
|
|
||||||
}
|
|
||||||
logger.error('Something went wrong while adding a series to Sonarr.', {
|
logger.error('Something went wrong while adding a series to Sonarr.', {
|
||||||
label: 'Sonarr API',
|
label: 'Sonarr API',
|
||||||
errorMessage: e.message,
|
errorMessage: e.message,
|
||||||
options,
|
options,
|
||||||
response: errorData,
|
response: e?.response?.data,
|
||||||
});
|
});
|
||||||
throw new Error('Failed to add series');
|
throw new Error('Failed to add series');
|
||||||
}
|
}
|
||||||
@@ -340,13 +340,14 @@ class SonarrAPI extends ServarrBase<{
|
|||||||
|
|
||||||
return newSeasons;
|
return newSeasons;
|
||||||
}
|
}
|
||||||
|
|
||||||
public removeSerie = async (serieId: number): Promise<void> => {
|
public removeSerie = async (serieId: number): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const { id, title } = await this.getSeriesByTvdbId(serieId);
|
const { id, title } = await this.getSeriesByTvdbId(serieId);
|
||||||
await this.delete(`/series/${id}`, {
|
await this.axios.delete(`/series/${id}`, {
|
||||||
deleteFiles: 'true',
|
params: {
|
||||||
addImportExclusion: 'false',
|
deleteFiles: true,
|
||||||
|
addImportExclusion: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
logger.info(`[Radarr] Removed serie ${title}`);
|
logger.info(`[Radarr] Removed serie ${title}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import ExternalAPI from '@server/api/externalapi';
|
|
||||||
import type { User } from '@server/entity/User';
|
import type { User } from '@server/entity/User';
|
||||||
import type { TautulliSettings } from '@server/lib/settings';
|
import type { TautulliSettings } from '@server/lib/settings';
|
||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
|
import type { AxiosInstance } from 'axios';
|
||||||
|
import axios from 'axios';
|
||||||
import { uniqWith } from 'lodash';
|
import { uniqWith } from 'lodash';
|
||||||
|
|
||||||
export interface TautulliHistoryRecord {
|
export interface TautulliHistoryRecord {
|
||||||
@@ -112,25 +113,25 @@ interface TautulliInfoResponse {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
class TautulliAPI extends ExternalAPI {
|
class TautulliAPI {
|
||||||
|
private axios: AxiosInstance;
|
||||||
|
|
||||||
constructor(settings: TautulliSettings) {
|
constructor(settings: TautulliSettings) {
|
||||||
super(
|
this.axios = axios.create({
|
||||||
`${settings.useSsl ? 'https' : 'http'}://${settings.hostname}:${
|
baseURL: `${settings.useSsl ? 'https' : 'http'}://${settings.hostname}:${
|
||||||
settings.port
|
settings.port
|
||||||
}${settings.urlBase ?? ''}`,
|
}${settings.urlBase ?? ''}`,
|
||||||
{
|
params: { apikey: settings.apiKey },
|
||||||
apikey: settings.apiKey || '',
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getInfo(): Promise<TautulliInfo> {
|
public async getInfo(): Promise<TautulliInfo> {
|
||||||
try {
|
try {
|
||||||
return (
|
return (
|
||||||
await this.get<TautulliInfoResponse>('/api/v2', {
|
await this.axios.get<TautulliInfoResponse>('/api/v2', {
|
||||||
cmd: 'get_tautulli_info',
|
params: { cmd: 'get_tautulli_info' },
|
||||||
})
|
})
|
||||||
).response.data;
|
).data.response.data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Something went wrong fetching Tautulli server info', {
|
logger.error('Something went wrong fetching Tautulli server info', {
|
||||||
label: 'Tautulli API',
|
label: 'Tautulli API',
|
||||||
@@ -147,12 +148,14 @@ class TautulliAPI extends ExternalAPI {
|
|||||||
): Promise<TautulliWatchStats[]> {
|
): Promise<TautulliWatchStats[]> {
|
||||||
try {
|
try {
|
||||||
return (
|
return (
|
||||||
await this.get<TautulliWatchStatsResponse>('/api/v2', {
|
await this.axios.get<TautulliWatchStatsResponse>('/api/v2', {
|
||||||
cmd: 'get_item_watch_time_stats',
|
params: {
|
||||||
rating_key: ratingKey,
|
cmd: 'get_item_watch_time_stats',
|
||||||
grouping: '1',
|
rating_key: ratingKey,
|
||||||
|
grouping: 1,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
).response.data;
|
).data.response.data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(
|
logger.error(
|
||||||
'Something went wrong fetching media watch stats from Tautulli',
|
'Something went wrong fetching media watch stats from Tautulli',
|
||||||
@@ -173,12 +176,14 @@ class TautulliAPI extends ExternalAPI {
|
|||||||
): Promise<TautulliWatchUser[]> {
|
): Promise<TautulliWatchUser[]> {
|
||||||
try {
|
try {
|
||||||
return (
|
return (
|
||||||
await this.get<TautulliWatchUsersResponse>('/api/v2', {
|
await this.axios.get<TautulliWatchUsersResponse>('/api/v2', {
|
||||||
cmd: 'get_item_user_stats',
|
params: {
|
||||||
rating_key: ratingKey,
|
cmd: 'get_item_user_stats',
|
||||||
grouping: '1',
|
rating_key: ratingKey,
|
||||||
|
grouping: 1,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
).response.data;
|
).data.response.data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(
|
logger.error(
|
||||||
'Something went wrong fetching media watch users from Tautulli',
|
'Something went wrong fetching media watch users from Tautulli',
|
||||||
@@ -201,13 +206,15 @@ class TautulliAPI extends ExternalAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
await this.get<TautulliWatchStatsResponse>('/api/v2', {
|
await this.axios.get<TautulliWatchStatsResponse>('/api/v2', {
|
||||||
cmd: 'get_user_watch_time_stats',
|
params: {
|
||||||
user_id: user.plexId.toString(),
|
cmd: 'get_user_watch_time_stats',
|
||||||
query_days: '0',
|
user_id: user.plexId,
|
||||||
grouping: '1',
|
query_days: 0,
|
||||||
|
grouping: 1,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
).response.data[0];
|
).data.response.data[0];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(
|
logger.error(
|
||||||
'Something went wrong fetching user watch stats from Tautulli',
|
'Something went wrong fetching user watch stats from Tautulli',
|
||||||
@@ -238,17 +245,19 @@ class TautulliAPI extends ExternalAPI {
|
|||||||
|
|
||||||
while (results.length < 20) {
|
while (results.length < 20) {
|
||||||
const tautulliData = (
|
const tautulliData = (
|
||||||
await this.get<TautulliHistoryResponse>('/api/v2', {
|
await this.axios.get<TautulliHistoryResponse>('/api/v2', {
|
||||||
cmd: 'get_history',
|
params: {
|
||||||
grouping: '1',
|
cmd: 'get_history',
|
||||||
order_column: 'date',
|
grouping: 1,
|
||||||
order_dir: 'desc',
|
order_column: 'date',
|
||||||
user_id: user.plexId.toString(),
|
order_dir: 'desc',
|
||||||
media_type: 'movie,episode',
|
user_id: user.plexId,
|
||||||
length: take.toString(),
|
media_type: 'movie,episode',
|
||||||
start: start.toString(),
|
length: take,
|
||||||
|
start,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
).response.data.data;
|
).data.response.data.data;
|
||||||
|
|
||||||
if (!tautulliData.length) {
|
if (!tautulliData.length) {
|
||||||
return results;
|
return results;
|
||||||
|
|||||||
@@ -112,8 +112,8 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
{
|
{
|
||||||
nodeCache: cacheManager.getCache('tmdb').data,
|
nodeCache: cacheManager.getCache('tmdb').data,
|
||||||
rateLimit: {
|
rateLimit: {
|
||||||
|
maxRequests: 20,
|
||||||
maxRPS: 50,
|
maxRPS: 50,
|
||||||
id: 'tmdb',
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -129,10 +129,7 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
}: SearchOptions): Promise<TmdbSearchMultiResponse> => {
|
}: SearchOptions): Promise<TmdbSearchMultiResponse> => {
|
||||||
try {
|
try {
|
||||||
const data = await this.get<TmdbSearchMultiResponse>('/search/multi', {
|
const data = await this.get<TmdbSearchMultiResponse>('/search/multi', {
|
||||||
query,
|
params: { query, page, include_adult: includeAdult, language },
|
||||||
page: page.toString(),
|
|
||||||
include_adult: includeAdult ? 'true' : 'false',
|
|
||||||
language,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
@@ -155,11 +152,13 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
}: SingleSearchOptions): Promise<TmdbSearchMovieResponse> => {
|
}: SingleSearchOptions): Promise<TmdbSearchMovieResponse> => {
|
||||||
try {
|
try {
|
||||||
const data = await this.get<TmdbSearchMovieResponse>('/search/movie', {
|
const data = await this.get<TmdbSearchMovieResponse>('/search/movie', {
|
||||||
query,
|
params: {
|
||||||
page: page.toString(),
|
query,
|
||||||
include_adult: includeAdult ? 'true' : 'false',
|
page,
|
||||||
language,
|
include_adult: includeAdult,
|
||||||
primary_release_year: year?.toString() || '',
|
language,
|
||||||
|
primary_release_year: year,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
@@ -182,11 +181,13 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
}: SingleSearchOptions): Promise<TmdbSearchTvResponse> => {
|
}: SingleSearchOptions): Promise<TmdbSearchTvResponse> => {
|
||||||
try {
|
try {
|
||||||
const data = await this.get<TmdbSearchTvResponse>('/search/tv', {
|
const data = await this.get<TmdbSearchTvResponse>('/search/tv', {
|
||||||
query,
|
params: {
|
||||||
page: page.toString(),
|
query,
|
||||||
include_adult: includeAdult ? 'true' : 'false',
|
page,
|
||||||
language,
|
include_adult: includeAdult,
|
||||||
first_air_date_year: year?.toString() || '',
|
language,
|
||||||
|
first_air_date_year: year,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
@@ -209,7 +210,7 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
}): Promise<TmdbPersonDetails> => {
|
}): Promise<TmdbPersonDetails> => {
|
||||||
try {
|
try {
|
||||||
const data = await this.get<TmdbPersonDetails>(`/person/${personId}`, {
|
const data = await this.get<TmdbPersonDetails>(`/person/${personId}`, {
|
||||||
language,
|
params: { language },
|
||||||
});
|
});
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
@@ -229,7 +230,7 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
const data = await this.get<TmdbPersonCombinedCredits>(
|
const data = await this.get<TmdbPersonCombinedCredits>(
|
||||||
`/person/${personId}/combined_credits`,
|
`/person/${personId}/combined_credits`,
|
||||||
{
|
{
|
||||||
language,
|
params: { language },
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -252,9 +253,11 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
const data = await this.get<TmdbMovieDetails>(
|
const data = await this.get<TmdbMovieDetails>(
|
||||||
`/movie/${movieId}`,
|
`/movie/${movieId}`,
|
||||||
{
|
{
|
||||||
language,
|
params: {
|
||||||
append_to_response:
|
language,
|
||||||
'credits,external_ids,videos,keywords,release_dates,watch/providers',
|
append_to_response:
|
||||||
|
'credits,external_ids,videos,keywords,release_dates,watch/providers',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
43200
|
43200
|
||||||
);
|
);
|
||||||
@@ -276,9 +279,11 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
const data = await this.get<TmdbTvDetails>(
|
const data = await this.get<TmdbTvDetails>(
|
||||||
`/tv/${tvId}`,
|
`/tv/${tvId}`,
|
||||||
{
|
{
|
||||||
language,
|
params: {
|
||||||
append_to_response:
|
language,
|
||||||
'aggregate_credits,credits,external_ids,keywords,videos,content_ratings,watch/providers',
|
append_to_response:
|
||||||
|
'aggregate_credits,credits,external_ids,keywords,videos,content_ratings,watch/providers',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
43200
|
43200
|
||||||
);
|
);
|
||||||
@@ -302,8 +307,10 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
const data = await this.get<TmdbSeasonWithEpisodes>(
|
const data = await this.get<TmdbSeasonWithEpisodes>(
|
||||||
`/tv/${tvId}/season/${seasonNumber}`,
|
`/tv/${tvId}/season/${seasonNumber}`,
|
||||||
{
|
{
|
||||||
language: language || '',
|
params: {
|
||||||
append_to_response: 'external_ids',
|
language,
|
||||||
|
append_to_response: 'external_ids',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -326,8 +333,10 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
const data = await this.get<TmdbSearchMovieResponse>(
|
const data = await this.get<TmdbSearchMovieResponse>(
|
||||||
`/movie/${movieId}/recommendations`,
|
`/movie/${movieId}/recommendations`,
|
||||||
{
|
{
|
||||||
page: page.toString(),
|
params: {
|
||||||
language,
|
page,
|
||||||
|
language,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -350,8 +359,10 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
const data = await this.get<TmdbSearchMovieResponse>(
|
const data = await this.get<TmdbSearchMovieResponse>(
|
||||||
`/movie/${movieId}/similar`,
|
`/movie/${movieId}/similar`,
|
||||||
{
|
{
|
||||||
page: page.toString(),
|
params: {
|
||||||
language,
|
page,
|
||||||
|
language,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -374,8 +385,10 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
const data = await this.get<TmdbSearchMovieResponse>(
|
const data = await this.get<TmdbSearchMovieResponse>(
|
||||||
`/keyword/${keywordId}/movies`,
|
`/keyword/${keywordId}/movies`,
|
||||||
{
|
{
|
||||||
page: page.toString(),
|
params: {
|
||||||
language,
|
page,
|
||||||
|
language,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -398,8 +411,10 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
const data = await this.get<TmdbSearchTvResponse>(
|
const data = await this.get<TmdbSearchTvResponse>(
|
||||||
`/tv/${tvId}/recommendations`,
|
`/tv/${tvId}/recommendations`,
|
||||||
{
|
{
|
||||||
page: page.toString(),
|
params: {
|
||||||
language,
|
page,
|
||||||
|
language,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -422,8 +437,10 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
}): Promise<TmdbSearchTvResponse> {
|
}): Promise<TmdbSearchTvResponse> {
|
||||||
try {
|
try {
|
||||||
const data = await this.get<TmdbSearchTvResponse>(`/tv/${tvId}/similar`, {
|
const data = await this.get<TmdbSearchTvResponse>(`/tv/${tvId}/similar`, {
|
||||||
page: page.toString(),
|
params: {
|
||||||
language,
|
page,
|
||||||
|
language,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
@@ -464,38 +481,40 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
.split('T')[0];
|
.split('T')[0];
|
||||||
|
|
||||||
const data = await this.get<TmdbSearchMovieResponse>('/discover/movie', {
|
const data = await this.get<TmdbSearchMovieResponse>('/discover/movie', {
|
||||||
sort_by: sortBy,
|
params: {
|
||||||
page: page.toString(),
|
sort_by: sortBy,
|
||||||
include_adult: includeAdult ? 'true' : 'false',
|
page,
|
||||||
language,
|
include_adult: includeAdult,
|
||||||
region: this.region || '',
|
language,
|
||||||
with_original_language:
|
region: this.region,
|
||||||
originalLanguage && originalLanguage !== 'all'
|
with_original_language:
|
||||||
? originalLanguage
|
originalLanguage && originalLanguage !== 'all'
|
||||||
: originalLanguage === 'all'
|
? originalLanguage
|
||||||
? ''
|
: originalLanguage === 'all'
|
||||||
: this.originalLanguage || '',
|
? undefined
|
||||||
// Set our release date values, but check if one is set and not the other,
|
: this.originalLanguage,
|
||||||
// so we can force a past date or a future date. TMDB Requires both values if one is set!
|
// Set our release date values, but check if one is set and not the other,
|
||||||
'primary_release_date.gte':
|
// so we can force a past date or a future date. TMDB Requires both values if one is set!
|
||||||
!primaryReleaseDateGte && primaryReleaseDateLte
|
'primary_release_date.gte':
|
||||||
? defaultPastDate
|
!primaryReleaseDateGte && primaryReleaseDateLte
|
||||||
: primaryReleaseDateGte || '',
|
? defaultPastDate
|
||||||
'primary_release_date.lte':
|
: primaryReleaseDateGte,
|
||||||
!primaryReleaseDateLte && primaryReleaseDateGte
|
'primary_release_date.lte':
|
||||||
? defaultFutureDate
|
!primaryReleaseDateLte && primaryReleaseDateGte
|
||||||
: primaryReleaseDateLte || '',
|
? defaultFutureDate
|
||||||
with_genres: genre || '',
|
: primaryReleaseDateLte,
|
||||||
with_companies: studio || '',
|
with_genres: genre,
|
||||||
with_keywords: keywords || '',
|
with_companies: studio,
|
||||||
'with_runtime.gte': withRuntimeGte || '',
|
with_keywords: keywords,
|
||||||
'with_runtime.lte': withRuntimeLte || '',
|
'with_runtime.gte': withRuntimeGte,
|
||||||
'vote_average.gte': voteAverageGte || '',
|
'with_runtime.lte': withRuntimeLte,
|
||||||
'vote_average.lte': voteAverageLte || '',
|
'vote_average.gte': voteAverageGte,
|
||||||
'vote_count.gte': voteCountGte || '',
|
'vote_average.lte': voteAverageLte,
|
||||||
'vote_count.lte': voteCountLte || '',
|
'vote_count.gte': voteCountGte,
|
||||||
watch_region: watchRegion || '',
|
'vote_count.lte': voteCountLte,
|
||||||
with_watch_providers: watchProviders || '',
|
watch_region: watchRegion,
|
||||||
|
with_watch_providers: watchProviders,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
@@ -536,40 +555,40 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
.split('T')[0];
|
.split('T')[0];
|
||||||
|
|
||||||
const data = await this.get<TmdbSearchTvResponse>('/discover/tv', {
|
const data = await this.get<TmdbSearchTvResponse>('/discover/tv', {
|
||||||
sort_by: sortBy,
|
params: {
|
||||||
page: page.toString(),
|
sort_by: sortBy,
|
||||||
language,
|
page,
|
||||||
region: this.region || '',
|
language,
|
||||||
// Set our release date values, but check if one is set and not the other,
|
region: this.region,
|
||||||
// so we can force a past date or a future date. TMDB Requires both values if one is set!
|
// Set our release date values, but check if one is set and not the other,
|
||||||
'first_air_date.gte':
|
// so we can force a past date or a future date. TMDB Requires both values if one is set!
|
||||||
!firstAirDateGte && firstAirDateLte
|
'first_air_date.gte':
|
||||||
? defaultPastDate
|
!firstAirDateGte && firstAirDateLte
|
||||||
: firstAirDateGte || '',
|
? defaultPastDate
|
||||||
'first_air_date.lte':
|
: firstAirDateGte,
|
||||||
!firstAirDateLte && firstAirDateGte
|
'first_air_date.lte':
|
||||||
? defaultFutureDate
|
!firstAirDateLte && firstAirDateGte
|
||||||
: firstAirDateLte || '',
|
? defaultFutureDate
|
||||||
with_original_language:
|
: firstAirDateLte,
|
||||||
originalLanguage && originalLanguage !== 'all'
|
with_original_language:
|
||||||
? originalLanguage
|
originalLanguage && originalLanguage !== 'all'
|
||||||
: originalLanguage === 'all'
|
? originalLanguage
|
||||||
? ''
|
: originalLanguage === 'all'
|
||||||
: this.originalLanguage || '',
|
? undefined
|
||||||
include_null_first_air_dates: includeEmptyReleaseDate
|
: this.originalLanguage,
|
||||||
? 'true'
|
include_null_first_air_dates: includeEmptyReleaseDate,
|
||||||
: 'false',
|
with_genres: genre,
|
||||||
with_genres: genre || '',
|
with_networks: network,
|
||||||
with_networks: network?.toString() || '',
|
with_keywords: keywords,
|
||||||
with_keywords: keywords || '',
|
'with_runtime.gte': withRuntimeGte,
|
||||||
'with_runtime.gte': withRuntimeGte || '',
|
'with_runtime.lte': withRuntimeLte,
|
||||||
'with_runtime.lte': withRuntimeLte || '',
|
'vote_average.gte': voteAverageGte,
|
||||||
'vote_average.gte': voteAverageGte || '',
|
'vote_average.lte': voteAverageLte,
|
||||||
'vote_average.lte': voteAverageLte || '',
|
'vote_count.gte': voteCountGte,
|
||||||
'vote_count.gte': voteCountGte || '',
|
'vote_count.lte': voteCountLte,
|
||||||
'vote_count.lte': voteCountLte || '',
|
with_watch_providers: watchProviders,
|
||||||
with_watch_providers: watchProviders || '',
|
watch_region: watchRegion,
|
||||||
watch_region: watchRegion || '',
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
@@ -589,10 +608,12 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
const data = await this.get<TmdbUpcomingMoviesResponse>(
|
const data = await this.get<TmdbUpcomingMoviesResponse>(
|
||||||
'/movie/upcoming',
|
'/movie/upcoming',
|
||||||
{
|
{
|
||||||
page: page.toString(),
|
params: {
|
||||||
language,
|
page,
|
||||||
region: this.region || '',
|
language,
|
||||||
originalLanguage: this.originalLanguage || '',
|
region: this.region,
|
||||||
|
originalLanguage: this.originalLanguage,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -615,9 +636,11 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
const data = await this.get<TmdbSearchMultiResponse>(
|
const data = await this.get<TmdbSearchMultiResponse>(
|
||||||
`/trending/all/${timeWindow}`,
|
`/trending/all/${timeWindow}`,
|
||||||
{
|
{
|
||||||
page: page.toString(),
|
params: {
|
||||||
language,
|
page,
|
||||||
region: this.region || '',
|
language,
|
||||||
|
region: this.region,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -638,7 +661,9 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
const data = await this.get<TmdbSearchMovieResponse>(
|
const data = await this.get<TmdbSearchMovieResponse>(
|
||||||
`/trending/movie/${timeWindow}`,
|
`/trending/movie/${timeWindow}`,
|
||||||
{
|
{
|
||||||
page: page.toString(),
|
params: {
|
||||||
|
page,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -659,7 +684,9 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
const data = await this.get<TmdbSearchTvResponse>(
|
const data = await this.get<TmdbSearchTvResponse>(
|
||||||
`/trending/tv/${timeWindow}`,
|
`/trending/tv/${timeWindow}`,
|
||||||
{
|
{
|
||||||
page: page.toString(),
|
params: {
|
||||||
|
page,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -688,8 +715,10 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
const data = await this.get<TmdbExternalIdResponse>(
|
const data = await this.get<TmdbExternalIdResponse>(
|
||||||
`/find/${externalId}`,
|
`/find/${externalId}`,
|
||||||
{
|
{
|
||||||
external_source: type === 'imdb' ? 'imdb_id' : 'tvdb_id',
|
params: {
|
||||||
language,
|
external_source: type === 'imdb' ? 'imdb_id' : 'tvdb_id',
|
||||||
|
language,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -779,7 +808,9 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
const data = await this.get<TmdbCollection>(
|
const data = await this.get<TmdbCollection>(
|
||||||
`/collection/${collectionId}`,
|
`/collection/${collectionId}`,
|
||||||
{
|
{
|
||||||
language,
|
params: {
|
||||||
|
language,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -852,7 +883,9 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
const data = await this.get<TmdbGenresResult>(
|
const data = await this.get<TmdbGenresResult>(
|
||||||
'/genre/movie/list',
|
'/genre/movie/list',
|
||||||
{
|
{
|
||||||
language,
|
params: {
|
||||||
|
language,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
86400 // 24 hours
|
86400 // 24 hours
|
||||||
);
|
);
|
||||||
@@ -864,7 +897,9 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
const englishData = await this.get<TmdbGenresResult>(
|
const englishData = await this.get<TmdbGenresResult>(
|
||||||
'/genre/movie/list',
|
'/genre/movie/list',
|
||||||
{
|
{
|
||||||
language: 'en',
|
params: {
|
||||||
|
language: 'en',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
86400 // 24 hours
|
86400 // 24 hours
|
||||||
);
|
);
|
||||||
@@ -899,7 +934,9 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
const data = await this.get<TmdbGenresResult>(
|
const data = await this.get<TmdbGenresResult>(
|
||||||
'/genre/tv/list',
|
'/genre/tv/list',
|
||||||
{
|
{
|
||||||
language,
|
params: {
|
||||||
|
language,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
86400 // 24 hours
|
86400 // 24 hours
|
||||||
);
|
);
|
||||||
@@ -911,7 +948,9 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
const englishData = await this.get<TmdbGenresResult>(
|
const englishData = await this.get<TmdbGenresResult>(
|
||||||
'/genre/tv/list',
|
'/genre/tv/list',
|
||||||
{
|
{
|
||||||
language: 'en',
|
params: {
|
||||||
|
language: 'en',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
86400 // 24 hours
|
86400 // 24 hours
|
||||||
);
|
);
|
||||||
@@ -966,8 +1005,10 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
const data = await this.get<TmdbKeywordSearchResponse>(
|
const data = await this.get<TmdbKeywordSearchResponse>(
|
||||||
'/search/keyword',
|
'/search/keyword',
|
||||||
{
|
{
|
||||||
query,
|
params: {
|
||||||
page: page.toString(),
|
query,
|
||||||
|
page,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
86400 // 24 hours
|
86400 // 24 hours
|
||||||
);
|
);
|
||||||
@@ -989,8 +1030,10 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
const data = await this.get<TmdbCompanySearchResponse>(
|
const data = await this.get<TmdbCompanySearchResponse>(
|
||||||
'/search/company',
|
'/search/company',
|
||||||
{
|
{
|
||||||
query,
|
params: {
|
||||||
page: page.toString(),
|
query,
|
||||||
|
page,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
86400 // 24 hours
|
86400 // 24 hours
|
||||||
);
|
);
|
||||||
@@ -1010,7 +1053,9 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
const data = await this.get<{ results: TmdbWatchProviderRegion[] }>(
|
const data = await this.get<{ results: TmdbWatchProviderRegion[] }>(
|
||||||
'/watch/providers/regions',
|
'/watch/providers/regions',
|
||||||
{
|
{
|
||||||
language: language ? this.originalLanguage || '' : '',
|
params: {
|
||||||
|
language: language ?? this.originalLanguage,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
86400 // 24 hours
|
86400 // 24 hours
|
||||||
);
|
);
|
||||||
@@ -1034,8 +1079,10 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
const data = await this.get<{ results: TmdbWatchProviderDetails[] }>(
|
const data = await this.get<{ results: TmdbWatchProviderDetails[] }>(
|
||||||
'/watch/providers/movie',
|
'/watch/providers/movie',
|
||||||
{
|
{
|
||||||
language: language ? this.originalLanguage || '' : '',
|
params: {
|
||||||
watch_region: watchRegion,
|
language: language ?? this.originalLanguage,
|
||||||
|
watch_region: watchRegion,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
86400 // 24 hours
|
86400 // 24 hours
|
||||||
);
|
);
|
||||||
@@ -1059,8 +1106,10 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
const data = await this.get<{ results: TmdbWatchProviderDetails[] }>(
|
const data = await this.get<{ results: TmdbWatchProviderDetails[] }>(
|
||||||
'/watch/providers/tv',
|
'/watch/providers/tv',
|
||||||
{
|
{
|
||||||
language: language ? this.originalLanguage || '' : '',
|
params: {
|
||||||
watch_region: watchRegion,
|
language: language ?? this.originalLanguage,
|
||||||
|
watch_region: watchRegion,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
86400 // 24 hours
|
86400 // 24 hours
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import { getSettings } from '@server/lib/settings';
|
|||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
import clearCookies from '@server/middleware/clearcookies';
|
import clearCookies from '@server/middleware/clearcookies';
|
||||||
import routes from '@server/routes';
|
import routes from '@server/routes';
|
||||||
import avatarproxy from '@server/routes/avatarproxy';
|
|
||||||
import imageproxy from '@server/routes/imageproxy';
|
import imageproxy from '@server/routes/imageproxy';
|
||||||
import { getAppVersion } from '@server/utils/appVersion';
|
import { getAppVersion } from '@server/utils/appVersion';
|
||||||
import restartFlag from '@server/utils/restartFlag';
|
import restartFlag from '@server/utils/restartFlag';
|
||||||
@@ -33,17 +32,10 @@ import * as OpenApiValidator from 'express-openapi-validator';
|
|||||||
import type { Store } from 'express-session';
|
import type { Store } from 'express-session';
|
||||||
import session from 'express-session';
|
import session from 'express-session';
|
||||||
import next from 'next';
|
import next from 'next';
|
||||||
import dns from 'node:dns';
|
|
||||||
import net from 'node:net';
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import swaggerUi from 'swagger-ui-express';
|
import swaggerUi from 'swagger-ui-express';
|
||||||
import YAML from 'yamljs';
|
import YAML from 'yamljs';
|
||||||
|
|
||||||
if (process.env.forceIpv4First === 'true') {
|
|
||||||
dns.setDefaultResultOrder('ipv4first');
|
|
||||||
net.setDefaultAutoSelectFamily(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
const API_SPEC_PATH = path.join(__dirname, '../overseerr-api.yml');
|
const API_SPEC_PATH = path.join(__dirname, '../overseerr-api.yml');
|
||||||
|
|
||||||
logger.info(`Starting Overseerr version ${getAppVersion()}`);
|
logger.info(`Starting Overseerr version ${getAppVersion()}`);
|
||||||
@@ -64,7 +56,7 @@ app
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load Settings
|
// Load Settings
|
||||||
const settings = await getSettings().load();
|
const settings = getSettings().load();
|
||||||
restartFlag.initializeSettings(settings.main);
|
restartFlag.initializeSettings(settings.main);
|
||||||
|
|
||||||
// Migrate library types
|
// Migrate library types
|
||||||
@@ -203,7 +195,6 @@ app
|
|||||||
|
|
||||||
// Do not set cookies so CDNs can cache them
|
// Do not set cookies so CDNs can cache them
|
||||||
server.use('/imageproxy', clearCookies, imageproxy);
|
server.use('/imageproxy', clearCookies, imageproxy);
|
||||||
server.use('/avatarproxy', clearCookies, avatarproxy);
|
|
||||||
|
|
||||||
server.get('*', (req, res) => handle(req, res));
|
server.get('*', (req, res) => handle(req, res));
|
||||||
server.use(
|
server.use(
|
||||||
|
|||||||
@@ -8,16 +8,3 @@ interface PageInfo {
|
|||||||
export interface PaginatedResponse {
|
export interface PaginatedResponse {
|
||||||
pageInfo: PageInfo;
|
pageInfo: PageInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the keys of an object that are not functions
|
|
||||||
*/
|
|
||||||
type NonFunctionPropertyNames<T> = {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
||||||
[K in keyof T]: T[K] extends Function ? never : K;
|
|
||||||
}[keyof T];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the properties of an object that are not functions
|
|
||||||
*/
|
|
||||||
export type NonFunctionProperties<T> = Pick<T, NonFunctionPropertyNames<T>>;
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { MediaType } from '@server/constants/media';
|
import type { MediaType } from '@server/constants/media';
|
||||||
import type { MediaRequest } from '@server/entity/MediaRequest';
|
import type { MediaRequest } from '@server/entity/MediaRequest';
|
||||||
import type { NonFunctionProperties, PaginatedResponse } from './common';
|
import type { PaginatedResponse } from './common';
|
||||||
|
|
||||||
export interface RequestResultsResponse extends PaginatedResponse {
|
export interface RequestResultsResponse extends PaginatedResponse {
|
||||||
results: NonFunctionProperties<MediaRequest>[];
|
results: MediaRequest[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MediaRequestBody = {
|
export type MediaRequestBody = {
|
||||||
@@ -14,7 +14,6 @@ export type MediaRequestBody = {
|
|||||||
is4k?: boolean;
|
is4k?: boolean;
|
||||||
serverId?: number;
|
serverId?: number;
|
||||||
profileId?: number;
|
profileId?: number;
|
||||||
profileName?: string;
|
|
||||||
rootFolder?: string;
|
rootFolder?: string;
|
||||||
languageProfileId?: number;
|
languageProfileId?: number;
|
||||||
userId?: number;
|
userId?: number;
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export interface CacheItem {
|
|||||||
|
|
||||||
export interface CacheResponse {
|
export interface CacheResponse {
|
||||||
apiCaches: CacheItem[];
|
apiCaches: CacheItem[];
|
||||||
imageCache: Record<'tmdb' | 'avatar', { size: number; imageCount: number }>;
|
imageCache: Record<'tmdb', { size: number; imageCount: number }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StatusResponse {
|
export interface StatusResponse {
|
||||||
|
|||||||
@@ -227,9 +227,6 @@ export const startJobs = (): void => {
|
|||||||
});
|
});
|
||||||
// Clean TMDB image cache
|
// Clean TMDB image cache
|
||||||
ImageProxy.clearCache('tmdb');
|
ImageProxy.clearCache('tmdb');
|
||||||
|
|
||||||
// Clean users avatar image cache
|
|
||||||
ImageProxy.clearCache('avatar');
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,12 @@ class AvailabilitySync {
|
|||||||
) {
|
) {
|
||||||
admin = await userRepository.findOne({
|
admin = await userRepository.findOne({
|
||||||
where: { id: 1 },
|
where: { id: 1 },
|
||||||
select: ['id', 'jellyfinUserId', 'jellyfinDeviceId'],
|
select: [
|
||||||
|
'id',
|
||||||
|
'jellyfinAuthToken',
|
||||||
|
'jellyfinUserId',
|
||||||
|
'jellyfinDeviceId',
|
||||||
|
],
|
||||||
order: { id: 'ASC' },
|
order: { id: 'ASC' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -81,7 +86,7 @@ class AvailabilitySync {
|
|||||||
if (admin) {
|
if (admin) {
|
||||||
this.jellyfinClient = new JellyfinAPI(
|
this.jellyfinClient = new JellyfinAPI(
|
||||||
getHostname(),
|
getHostname(),
|
||||||
settings.jellyfin.apiKey,
|
admin.jellyfinAuthToken,
|
||||||
admin.jellyfinDeviceId
|
admin.jellyfinDeviceId
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
import type { RateLimitOptions } from '@server/utils/rateLimit';
|
import axios from 'axios';
|
||||||
import rateLimit from '@server/utils/rateLimit';
|
import rateLimit, { type rateLimitOptions } from 'axios-rate-limit';
|
||||||
import { createHash } from 'crypto';
|
import { createHash } from 'crypto';
|
||||||
import { promises } from 'fs';
|
import { promises } from 'fs';
|
||||||
import mime from 'mime/lite';
|
|
||||||
import path, { join } from 'path';
|
import path, { join } from 'path';
|
||||||
|
|
||||||
type ImageResponse = {
|
type ImageResponse = {
|
||||||
@@ -12,7 +11,7 @@ type ImageResponse = {
|
|||||||
curRevalidate: number;
|
curRevalidate: number;
|
||||||
isStale: boolean;
|
isStale: boolean;
|
||||||
etag: string;
|
etag: string;
|
||||||
extension: string | null;
|
extension: string;
|
||||||
cacheKey: string;
|
cacheKey: string;
|
||||||
cacheMiss: boolean;
|
cacheMiss: boolean;
|
||||||
};
|
};
|
||||||
@@ -28,45 +27,29 @@ class ImageProxy {
|
|||||||
let deletedImages = 0;
|
let deletedImages = 0;
|
||||||
const cacheDirectory = path.join(baseCacheDirectory, key);
|
const cacheDirectory = path.join(baseCacheDirectory, key);
|
||||||
|
|
||||||
try {
|
const files = await promises.readdir(cacheDirectory);
|
||||||
const files = await promises.readdir(cacheDirectory);
|
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const filePath = path.join(cacheDirectory, file);
|
const filePath = path.join(cacheDirectory, file);
|
||||||
const stat = await promises.lstat(filePath);
|
const stat = await promises.lstat(filePath);
|
||||||
|
|
||||||
if (stat.isDirectory()) {
|
if (stat.isDirectory()) {
|
||||||
const imageFiles = await promises.readdir(filePath);
|
const imageFiles = await promises.readdir(filePath);
|
||||||
|
|
||||||
for (const imageFile of imageFiles) {
|
for (const imageFile of imageFiles) {
|
||||||
const [, expireAtSt] = imageFile.split('.');
|
const [, expireAtSt] = imageFile.split('.');
|
||||||
const expireAt = Number(expireAtSt);
|
const expireAt = Number(expireAtSt);
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
if (now > expireAt) {
|
if (now > expireAt) {
|
||||||
await promises.rm(path.join(filePath), {
|
await promises.rm(path.join(filePath, imageFile));
|
||||||
recursive: true,
|
deletedImages += 1;
|
||||||
});
|
|
||||||
deletedImages += 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
if (e.code === 'ENOENT') {
|
|
||||||
logger.error('Directory not found', {
|
|
||||||
label: 'Image Cache',
|
|
||||||
message: e.message,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
logger.error('Failed to read directory', {
|
|
||||||
label: 'Image Cache',
|
|
||||||
message: e.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`Cleared ${deletedImages} stale image(s) from cache '${key}'`, {
|
logger.info(`Cleared ${deletedImages} stale image(s) from cache`, {
|
||||||
label: 'Image Cache',
|
label: 'Image Cache',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -86,74 +69,55 @@ class ImageProxy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static async getDirectorySize(dir: string): Promise<number> {
|
private static async getDirectorySize(dir: string): Promise<number> {
|
||||||
try {
|
const files = await promises.readdir(dir, {
|
||||||
const files = await promises.readdir(dir, {
|
withFileTypes: true,
|
||||||
withFileTypes: true,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
const paths = files.map(async (file) => {
|
const paths = files.map(async (file) => {
|
||||||
const path = join(dir, file.name);
|
const path = join(dir, file.name);
|
||||||
|
|
||||||
if (file.isDirectory()) return await ImageProxy.getDirectorySize(path);
|
if (file.isDirectory()) return await ImageProxy.getDirectorySize(path);
|
||||||
|
|
||||||
if (file.isFile()) {
|
if (file.isFile()) {
|
||||||
const { size } = await promises.stat(path);
|
const { size } = await promises.stat(path);
|
||||||
|
|
||||||
return size;
|
return size;
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
return (await Promise.all(paths))
|
|
||||||
.flat(Infinity)
|
|
||||||
.reduce((i, size) => i + size, 0);
|
|
||||||
} catch (e) {
|
|
||||||
if (e.code === 'ENOENT') {
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (await Promise.all(paths))
|
||||||
|
.flat(Infinity)
|
||||||
|
.reduce((i, size) => i + size, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async getImageCount(dir: string) {
|
private static async getImageCount(dir: string) {
|
||||||
try {
|
const files = await promises.readdir(dir);
|
||||||
const files = await promises.readdir(dir);
|
|
||||||
|
|
||||||
return files.length;
|
return files.length;
|
||||||
} catch (e) {
|
|
||||||
if (e.code === 'ENOENT') {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fetch: typeof fetch;
|
private axios;
|
||||||
private cacheVersion;
|
private cacheVersion;
|
||||||
private key;
|
private key;
|
||||||
private baseUrl;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
key: string,
|
key: string,
|
||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
options: {
|
options: {
|
||||||
cacheVersion?: number;
|
cacheVersion?: number;
|
||||||
rateLimitOptions?: RateLimitOptions;
|
rateLimitOptions?: rateLimitOptions;
|
||||||
} = {}
|
} = {}
|
||||||
) {
|
) {
|
||||||
this.cacheVersion = options.cacheVersion ?? 1;
|
this.cacheVersion = options.cacheVersion ?? 1;
|
||||||
this.baseUrl = baseUrl;
|
|
||||||
this.key = key;
|
this.key = key;
|
||||||
|
this.axios = axios.create({
|
||||||
|
baseURL: baseUrl,
|
||||||
|
});
|
||||||
|
|
||||||
if (options.rateLimitOptions) {
|
if (options.rateLimitOptions) {
|
||||||
this.fetch = rateLimit(fetch, {
|
this.axios = rateLimit(this.axios, options.rateLimitOptions);
|
||||||
...options.rateLimitOptions,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.fetch = fetch;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,27 +144,6 @@ class ImageProxy {
|
|||||||
return imageResponse;
|
return imageResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async clearCachedImage(path: string) {
|
|
||||||
// find cacheKey
|
|
||||||
const cacheKey = this.getCacheKey(path);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const directory = join(this.getCacheDirectory(), cacheKey);
|
|
||||||
const files = await promises.readdir(directory);
|
|
||||||
|
|
||||||
await promises.rm(directory, { recursive: true });
|
|
||||||
|
|
||||||
logger.info(`Cleared ${files[0]} image from cache 'avatar'`, {
|
|
||||||
label: 'Image Cache',
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('Failed to clear cached image', {
|
|
||||||
label: 'Image Cache',
|
|
||||||
message: e.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async get(cacheKey: string): Promise<ImageResponse | null> {
|
private async get(cacheKey: string): Promise<ImageResponse | null> {
|
||||||
try {
|
try {
|
||||||
const directory = join(this.getCacheDirectory(), cacheKey);
|
const directory = join(this.getCacheDirectory(), cacheKey);
|
||||||
@@ -239,29 +182,17 @@ class ImageProxy {
|
|||||||
): Promise<ImageResponse | null> {
|
): Promise<ImageResponse | null> {
|
||||||
try {
|
try {
|
||||||
const directory = join(this.getCacheDirectory(), cacheKey);
|
const directory = join(this.getCacheDirectory(), cacheKey);
|
||||||
const href =
|
const response = await this.axios.get(path, {
|
||||||
this.baseUrl +
|
responseType: 'arraybuffer',
|
||||||
(this.baseUrl.length > 0
|
});
|
||||||
? this.baseUrl.endsWith('/')
|
|
||||||
? ''
|
|
||||||
: '/'
|
|
||||||
: '') +
|
|
||||||
(path.startsWith('/') ? path.slice(1) : path);
|
|
||||||
const response = await this.fetch(href);
|
|
||||||
const arrayBuffer = await response.arrayBuffer();
|
|
||||||
const buffer = Buffer.from(arrayBuffer);
|
|
||||||
|
|
||||||
const extension = mime.getExtension(
|
const buffer = Buffer.from(response.data, 'binary');
|
||||||
response.headers.get('content-type') ?? ''
|
const extension = path.split('.').pop() ?? '';
|
||||||
|
const maxAge = Number(
|
||||||
|
(response.headers['cache-control'] ?? '0').split('=')[1]
|
||||||
);
|
);
|
||||||
|
|
||||||
let maxAge = Number(
|
|
||||||
(response.headers.get('cache-control') ?? '0').split('=')[1]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!maxAge) maxAge = 86400;
|
|
||||||
const expireAt = Date.now() + maxAge * 1000;
|
const expireAt = Date.now() + maxAge * 1000;
|
||||||
const etag = (response.headers.get('etag') ?? '').replace(/"/g, '');
|
const etag = (response.headers.etag ?? '').replace(/"/g, '');
|
||||||
|
|
||||||
await this.writeToCacheDir(
|
await this.writeToCacheDir(
|
||||||
directory,
|
directory,
|
||||||
@@ -295,7 +226,7 @@ class ImageProxy {
|
|||||||
|
|
||||||
private async writeToCacheDir(
|
private async writeToCacheDir(
|
||||||
dir: string,
|
dir: string,
|
||||||
extension: string | null,
|
extension: string,
|
||||||
maxAge: number,
|
maxAge: number,
|
||||||
expireAt: number,
|
expireAt: number,
|
||||||
buffer: Buffer,
|
buffer: Buffer,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { User } from '@server/entity/User';
|
|||||||
import type { NotificationAgentDiscord } from '@server/lib/settings';
|
import type { NotificationAgentDiscord } from '@server/lib/settings';
|
||||||
import { getSettings, NotificationAgentKey } from '@server/lib/settings';
|
import { getSettings, NotificationAgentKey } from '@server/lib/settings';
|
||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
|
import axios from 'axios';
|
||||||
import {
|
import {
|
||||||
hasNotificationType,
|
hasNotificationType,
|
||||||
Notification,
|
Notification,
|
||||||
@@ -291,39 +292,23 @@ class DiscordAgent
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(settings.options.webhookUrl, {
|
await axios.post(settings.options.webhookUrl, {
|
||||||
method: 'POST',
|
username: settings.options.botUsername
|
||||||
headers: {
|
? settings.options.botUsername
|
||||||
'Content-Type': 'application/json',
|
: getSettings().main.applicationTitle,
|
||||||
},
|
avatar_url: settings.options.botAvatarUrl,
|
||||||
body: JSON.stringify({
|
embeds: [this.buildEmbed(type, payload)],
|
||||||
username: settings.options.botUsername
|
content: userMentions.join(' '),
|
||||||
? settings.options.botUsername
|
} as DiscordWebhookPayload);
|
||||||
: getSettings().main.applicationTitle,
|
|
||||||
avatar_url: settings.options.botAvatarUrl,
|
|
||||||
embeds: [this.buildEmbed(type, payload)],
|
|
||||||
content: userMentions.join(' '),
|
|
||||||
} as DiscordWebhookPayload),
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(response.statusText, { cause: response });
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
let errorData;
|
|
||||||
try {
|
|
||||||
errorData = await e.cause?.text();
|
|
||||||
errorData = JSON.parse(errorData);
|
|
||||||
} catch {
|
|
||||||
/* empty */
|
|
||||||
}
|
|
||||||
logger.error('Error sending Discord notification', {
|
logger.error('Error sending Discord notification', {
|
||||||
label: 'Notifications',
|
label: 'Notifications',
|
||||||
type: Notification[type],
|
type: Notification[type],
|
||||||
subject: payload.subject,
|
subject: payload.subject,
|
||||||
errorMessage: e.message,
|
errorMessage: e.message,
|
||||||
response: errorData,
|
response: e.response?.data,
|
||||||
});
|
});
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { IssueStatus, IssueTypeName } from '@server/constants/issue';
|
|||||||
import type { NotificationAgentGotify } from '@server/lib/settings';
|
import type { NotificationAgentGotify } from '@server/lib/settings';
|
||||||
import { getSettings } from '@server/lib/settings';
|
import { getSettings } from '@server/lib/settings';
|
||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
|
import axios from 'axios';
|
||||||
import { hasNotificationType, Notification } from '..';
|
import { hasNotificationType, Notification } from '..';
|
||||||
import type { NotificationAgent, NotificationPayload } from './agent';
|
import type { NotificationAgent, NotificationPayload } from './agent';
|
||||||
import { BaseAgent } from './agent';
|
import { BaseAgent } from './agent';
|
||||||
@@ -132,32 +133,16 @@ class GotifyAgent
|
|||||||
const endpoint = `${settings.options.url}/message?token=${settings.options.token}`;
|
const endpoint = `${settings.options.url}/message?token=${settings.options.token}`;
|
||||||
const notificationPayload = this.getNotificationPayload(type, payload);
|
const notificationPayload = this.getNotificationPayload(type, payload);
|
||||||
|
|
||||||
const response = await fetch(endpoint, {
|
await axios.post(endpoint, notificationPayload);
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(notificationPayload),
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(response.statusText, { cause: response });
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
let errorData;
|
|
||||||
try {
|
|
||||||
errorData = await e.cause?.text();
|
|
||||||
errorData = JSON.parse(errorData);
|
|
||||||
} catch {
|
|
||||||
/* empty */
|
|
||||||
}
|
|
||||||
logger.error('Error sending Gotify notification', {
|
logger.error('Error sending Gotify notification', {
|
||||||
label: 'Notifications',
|
label: 'Notifications',
|
||||||
type: Notification[type],
|
type: Notification[type],
|
||||||
subject: payload.subject,
|
subject: payload.subject,
|
||||||
errorMessage: e.message,
|
errorMessage: e.message,
|
||||||
response: errorData,
|
response: e.response?.data,
|
||||||
});
|
});
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { MediaStatus } from '@server/constants/media';
|
|||||||
import type { NotificationAgentLunaSea } from '@server/lib/settings';
|
import type { NotificationAgentLunaSea } from '@server/lib/settings';
|
||||||
import { getSettings } from '@server/lib/settings';
|
import { getSettings } from '@server/lib/settings';
|
||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
|
import axios from 'axios';
|
||||||
import { hasNotificationType, Notification } from '..';
|
import { hasNotificationType, Notification } from '..';
|
||||||
import type { NotificationAgent, NotificationPayload } from './agent';
|
import type { NotificationAgent, NotificationPayload } from './agent';
|
||||||
import { BaseAgent } from './agent';
|
import { BaseAgent } from './agent';
|
||||||
@@ -100,39 +101,28 @@ class LunaSeaAgent
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(settings.options.webhookUrl, {
|
await axios.post(
|
||||||
method: 'POST',
|
settings.options.webhookUrl,
|
||||||
headers: settings.options.profileName
|
this.buildPayload(type, payload),
|
||||||
|
settings.options.profileName
|
||||||
? {
|
? {
|
||||||
'Content-Type': 'application/json',
|
headers: {
|
||||||
|
Authorization: `Basic ${Buffer.from(
|
||||||
|
`${settings.options.profileName}:`
|
||||||
|
).toString('base64')}`,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
: {
|
: undefined
|
||||||
'Content-Type': 'application/json',
|
);
|
||||||
Authorization: `Basic ${Buffer.from(
|
|
||||||
`${settings.options.profileName}:`
|
|
||||||
).toString('base64')}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify(this.buildPayload(type, payload)),
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(response.statusText, { cause: response });
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
let errorData;
|
|
||||||
try {
|
|
||||||
errorData = await e.cause?.text();
|
|
||||||
errorData = JSON.parse(errorData);
|
|
||||||
} catch {
|
|
||||||
/* empty */
|
|
||||||
}
|
|
||||||
logger.error('Error sending LunaSea notification', {
|
logger.error('Error sending LunaSea notification', {
|
||||||
label: 'Notifications',
|
label: 'Notifications',
|
||||||
type: Notification[type],
|
type: Notification[type],
|
||||||
subject: payload.subject,
|
subject: payload.subject,
|
||||||
errorMessage: e.message,
|
errorMessage: e.message,
|
||||||
response: errorData,
|
response: e.response?.data,
|
||||||
});
|
});
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { User } from '@server/entity/User';
|
|||||||
import type { NotificationAgentPushbullet } from '@server/lib/settings';
|
import type { NotificationAgentPushbullet } from '@server/lib/settings';
|
||||||
import { getSettings, NotificationAgentKey } from '@server/lib/settings';
|
import { getSettings, NotificationAgentKey } from '@server/lib/settings';
|
||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
|
import axios from 'axios';
|
||||||
import {
|
import {
|
||||||
hasNotificationType,
|
hasNotificationType,
|
||||||
Notification,
|
Notification,
|
||||||
@@ -122,34 +123,22 @@ class PushbulletAgent
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(endpoint, {
|
await axios.post(
|
||||||
method: 'POST',
|
endpoint,
|
||||||
headers: {
|
{ ...notificationPayload, channel_tag: settings.options.channelTag },
|
||||||
'Content-Type': 'application/json',
|
{
|
||||||
'Access-Token': settings.options.accessToken,
|
headers: {
|
||||||
},
|
'Access-Token': settings.options.accessToken,
|
||||||
body: JSON.stringify({
|
},
|
||||||
...notificationPayload,
|
}
|
||||||
channel_tag: settings.options.channelTag,
|
);
|
||||||
}),
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(response.statusText, { cause: response });
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
let errorData;
|
|
||||||
try {
|
|
||||||
errorData = await e.cause?.text();
|
|
||||||
errorData = JSON.parse(errorData);
|
|
||||||
} catch {
|
|
||||||
/* empty */
|
|
||||||
}
|
|
||||||
logger.error('Error sending Pushbullet notification', {
|
logger.error('Error sending Pushbullet notification', {
|
||||||
label: 'Notifications',
|
label: 'Notifications',
|
||||||
type: Notification[type],
|
type: Notification[type],
|
||||||
subject: payload.subject,
|
subject: payload.subject,
|
||||||
errorMessage: e.message,
|
errorMessage: e.message,
|
||||||
response: errorData,
|
response: e.response?.data,
|
||||||
});
|
});
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -174,32 +163,19 @@ class PushbulletAgent
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(endpoint, {
|
await axios.post(endpoint, notificationPayload, {
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Access-Token': payload.notifyUser.settings.pushbulletAccessToken,
|
'Access-Token': payload.notifyUser.settings.pushbulletAccessToken,
|
||||||
},
|
},
|
||||||
body: JSON.stringify(notificationPayload),
|
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(response.statusText, { cause: response });
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
let errorData;
|
|
||||||
try {
|
|
||||||
errorData = await e.cause?.text();
|
|
||||||
errorData = JSON.parse(errorData);
|
|
||||||
} catch {
|
|
||||||
/* empty */
|
|
||||||
}
|
|
||||||
logger.error('Error sending Pushbullet notification', {
|
logger.error('Error sending Pushbullet notification', {
|
||||||
label: 'Notifications',
|
label: 'Notifications',
|
||||||
recipient: payload.notifyUser.displayName,
|
recipient: payload.notifyUser.displayName,
|
||||||
type: Notification[type],
|
type: Notification[type],
|
||||||
subject: payload.subject,
|
subject: payload.subject,
|
||||||
errorMessage: e.message,
|
errorMessage: e.message,
|
||||||
response: errorData,
|
response: e.response?.data,
|
||||||
});
|
});
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -235,32 +211,19 @@ class PushbulletAgent
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(endpoint, {
|
await axios.post(endpoint, notificationPayload, {
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Access-Token': user.settings.pushbulletAccessToken,
|
'Access-Token': user.settings.pushbulletAccessToken,
|
||||||
},
|
},
|
||||||
body: JSON.stringify(notificationPayload),
|
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(response.statusText, { cause: response });
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
let errorData;
|
|
||||||
try {
|
|
||||||
errorData = await e.cause?.text();
|
|
||||||
errorData = JSON.parse(errorData);
|
|
||||||
} catch {
|
|
||||||
/* empty */
|
|
||||||
}
|
|
||||||
logger.error('Error sending Pushbullet notification', {
|
logger.error('Error sending Pushbullet notification', {
|
||||||
label: 'Notifications',
|
label: 'Notifications',
|
||||||
recipient: user.displayName,
|
recipient: user.displayName,
|
||||||
type: Notification[type],
|
type: Notification[type],
|
||||||
subject: payload.subject,
|
subject: payload.subject,
|
||||||
errorMessage: e.message,
|
errorMessage: e.message,
|
||||||
response: errorData,
|
response: e.response?.data,
|
||||||
});
|
});
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { User } from '@server/entity/User';
|
|||||||
import type { NotificationAgentPushover } from '@server/lib/settings';
|
import type { NotificationAgentPushover } from '@server/lib/settings';
|
||||||
import { getSettings, NotificationAgentKey } from '@server/lib/settings';
|
import { getSettings, NotificationAgentKey } from '@server/lib/settings';
|
||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
|
import axios from 'axios';
|
||||||
import {
|
import {
|
||||||
hasNotificationType,
|
hasNotificationType,
|
||||||
Notification,
|
Notification,
|
||||||
@@ -51,15 +52,12 @@ class PushoverAgent
|
|||||||
imageUrl: string
|
imageUrl: string
|
||||||
): Promise<Partial<PushoverImagePayload>> {
|
): Promise<Partial<PushoverImagePayload>> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(imageUrl);
|
const response = await axios.get(imageUrl, {
|
||||||
if (!response.ok) {
|
responseType: 'arraybuffer',
|
||||||
throw new Error(response.statusText, { cause: response });
|
});
|
||||||
}
|
const base64 = Buffer.from(response.data, 'binary').toString('base64');
|
||||||
const arrayBuffer = await response.arrayBuffer();
|
|
||||||
const base64 = Buffer.from(arrayBuffer).toString('base64');
|
|
||||||
const contentType = (
|
const contentType = (
|
||||||
response.headers.get('Content-Type') ||
|
response.headers['Content-Type'] || response.headers['content-type']
|
||||||
response.headers.get('content-type')
|
|
||||||
)?.toString();
|
)?.toString();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -67,17 +65,10 @@ class PushoverAgent
|
|||||||
attachment_type: contentType,
|
attachment_type: contentType,
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
let errorData;
|
|
||||||
try {
|
|
||||||
errorData = await e.cause?.text();
|
|
||||||
errorData = JSON.parse(errorData);
|
|
||||||
} catch {
|
|
||||||
/* empty */
|
|
||||||
}
|
|
||||||
logger.error('Error getting image payload', {
|
logger.error('Error getting image payload', {
|
||||||
label: 'Notifications',
|
label: 'Notifications',
|
||||||
errorMessage: e.message,
|
errorMessage: e.message,
|
||||||
response: errorData,
|
response: e.response?.data,
|
||||||
});
|
});
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
@@ -210,35 +201,19 @@ class PushoverAgent
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(endpoint, {
|
await axios.post(endpoint, {
|
||||||
method: 'POST',
|
...notificationPayload,
|
||||||
headers: {
|
token: settings.options.accessToken,
|
||||||
'Content-Type': 'application/json',
|
user: settings.options.userToken,
|
||||||
},
|
sound: settings.options.sound,
|
||||||
body: JSON.stringify({
|
} as PushoverPayload);
|
||||||
...notificationPayload,
|
|
||||||
token: settings.options.accessToken,
|
|
||||||
user: settings.options.userToken,
|
|
||||||
sound: settings.options.sound,
|
|
||||||
} as PushoverPayload),
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(response.statusText, { cause: response });
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
let errorData;
|
|
||||||
try {
|
|
||||||
errorData = await e.cause?.text();
|
|
||||||
errorData = JSON.parse(errorData);
|
|
||||||
} catch {
|
|
||||||
/* empty */
|
|
||||||
}
|
|
||||||
logger.error('Error sending Pushover notification', {
|
logger.error('Error sending Pushover notification', {
|
||||||
label: 'Notifications',
|
label: 'Notifications',
|
||||||
type: Notification[type],
|
type: Notification[type],
|
||||||
subject: payload.subject,
|
subject: payload.subject,
|
||||||
errorMessage: e.message,
|
errorMessage: e.message,
|
||||||
response: errorData,
|
response: e.response?.data,
|
||||||
});
|
});
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -266,36 +241,20 @@ class PushoverAgent
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(endpoint, {
|
await axios.post(endpoint, {
|
||||||
method: 'POST',
|
...notificationPayload,
|
||||||
headers: {
|
token: payload.notifyUser.settings.pushoverApplicationToken,
|
||||||
'Content-Type': 'application/json',
|
user: payload.notifyUser.settings.pushoverUserKey,
|
||||||
},
|
sound: payload.notifyUser.settings.pushoverSound,
|
||||||
body: JSON.stringify({
|
} as PushoverPayload);
|
||||||
...notificationPayload,
|
|
||||||
token: payload.notifyUser.settings.pushoverApplicationToken,
|
|
||||||
user: payload.notifyUser.settings.pushoverUserKey,
|
|
||||||
sound: payload.notifyUser.settings.pushoverSound,
|
|
||||||
} as PushoverPayload),
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(response.statusText, { cause: response });
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
let errorData;
|
|
||||||
try {
|
|
||||||
errorData = await e.cause?.text();
|
|
||||||
errorData = JSON.parse(errorData);
|
|
||||||
} catch {
|
|
||||||
/* empty */
|
|
||||||
}
|
|
||||||
logger.error('Error sending Pushover notification', {
|
logger.error('Error sending Pushover notification', {
|
||||||
label: 'Notifications',
|
label: 'Notifications',
|
||||||
recipient: payload.notifyUser.displayName,
|
recipient: payload.notifyUser.displayName,
|
||||||
type: Notification[type],
|
type: Notification[type],
|
||||||
subject: payload.subject,
|
subject: payload.subject,
|
||||||
errorMessage: e.message,
|
errorMessage: e.message,
|
||||||
response: errorData,
|
response: e.response?.data,
|
||||||
});
|
});
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -332,35 +291,19 @@ class PushoverAgent
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(endpoint, {
|
await axios.post(endpoint, {
|
||||||
method: 'POST',
|
...notificationPayload,
|
||||||
headers: {
|
token: user.settings.pushoverApplicationToken,
|
||||||
'Content-Type': 'application/json',
|
user: user.settings.pushoverUserKey,
|
||||||
},
|
} as PushoverPayload);
|
||||||
body: JSON.stringify({
|
|
||||||
...notificationPayload,
|
|
||||||
token: user.settings.pushoverApplicationToken,
|
|
||||||
user: user.settings.pushoverUserKey,
|
|
||||||
} as PushoverPayload),
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(response.statusText, { cause: response });
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
let errorData;
|
|
||||||
try {
|
|
||||||
errorData = await e.cause?.text();
|
|
||||||
errorData = JSON.parse(errorData);
|
|
||||||
} catch {
|
|
||||||
/* empty */
|
|
||||||
}
|
|
||||||
logger.error('Error sending Pushover notification', {
|
logger.error('Error sending Pushover notification', {
|
||||||
label: 'Notifications',
|
label: 'Notifications',
|
||||||
recipient: user.displayName,
|
recipient: user.displayName,
|
||||||
type: Notification[type],
|
type: Notification[type],
|
||||||
subject: payload.subject,
|
subject: payload.subject,
|
||||||
errorMessage: e.message,
|
errorMessage: e.message,
|
||||||
response: errorData,
|
response: e.response?.data,
|
||||||
});
|
});
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { IssueStatus, IssueTypeName } from '@server/constants/issue';
|
|||||||
import type { NotificationAgentSlack } from '@server/lib/settings';
|
import type { NotificationAgentSlack } from '@server/lib/settings';
|
||||||
import { getSettings } from '@server/lib/settings';
|
import { getSettings } from '@server/lib/settings';
|
||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
|
import axios from 'axios';
|
||||||
import { hasNotificationType, Notification } from '..';
|
import { hasNotificationType, Notification } from '..';
|
||||||
import type { NotificationAgent, NotificationPayload } from './agent';
|
import type { NotificationAgent, NotificationPayload } from './agent';
|
||||||
import { BaseAgent } from './agent';
|
import { BaseAgent } from './agent';
|
||||||
@@ -237,32 +238,19 @@ class SlackAgent
|
|||||||
subject: payload.subject,
|
subject: payload.subject,
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
const response = await fetch(settings.options.webhookUrl, {
|
await axios.post(
|
||||||
method: 'POST',
|
settings.options.webhookUrl,
|
||||||
headers: {
|
this.buildEmbed(type, payload)
|
||||||
'Content-Type': 'application/json',
|
);
|
||||||
},
|
|
||||||
body: JSON.stringify(this.buildEmbed(type, payload)),
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(response.statusText, { cause: response });
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
let errorData;
|
|
||||||
try {
|
|
||||||
errorData = await e.cause?.text();
|
|
||||||
errorData = JSON.parse(errorData);
|
|
||||||
} catch {
|
|
||||||
/* empty */
|
|
||||||
}
|
|
||||||
logger.error('Error sending Slack notification', {
|
logger.error('Error sending Slack notification', {
|
||||||
label: 'Notifications',
|
label: 'Notifications',
|
||||||
type: Notification[type],
|
type: Notification[type],
|
||||||
subject: payload.subject,
|
subject: payload.subject,
|
||||||
errorMessage: e.message,
|
errorMessage: e.message,
|
||||||
response: errorData,
|
response: e.response?.data,
|
||||||
});
|
});
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { User } from '@server/entity/User';
|
|||||||
import type { NotificationAgentTelegram } from '@server/lib/settings';
|
import type { NotificationAgentTelegram } from '@server/lib/settings';
|
||||||
import { getSettings, NotificationAgentKey } from '@server/lib/settings';
|
import { getSettings, NotificationAgentKey } from '@server/lib/settings';
|
||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
|
import axios from 'axios';
|
||||||
import {
|
import {
|
||||||
hasNotificationType,
|
hasNotificationType,
|
||||||
Notification,
|
Notification,
|
||||||
@@ -174,34 +175,18 @@ class TelegramAgent
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(endpoint, {
|
await axios.post(endpoint, {
|
||||||
method: 'POST',
|
...notificationPayload,
|
||||||
headers: {
|
chat_id: settings.options.chatId,
|
||||||
'Content-Type': 'application/json',
|
disable_notification: !!settings.options.sendSilently,
|
||||||
},
|
} as TelegramMessagePayload | TelegramPhotoPayload);
|
||||||
body: JSON.stringify({
|
|
||||||
...notificationPayload,
|
|
||||||
chat_id: settings.options.chatId,
|
|
||||||
disable_notification: !!settings.options.sendSilently,
|
|
||||||
} as TelegramMessagePayload | TelegramPhotoPayload),
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(response.statusText, { cause: response });
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
let errorData;
|
|
||||||
try {
|
|
||||||
errorData = await e.cause?.text();
|
|
||||||
errorData = JSON.parse(errorData);
|
|
||||||
} catch {
|
|
||||||
/* empty */
|
|
||||||
}
|
|
||||||
logger.error('Error sending Telegram notification', {
|
logger.error('Error sending Telegram notification', {
|
||||||
label: 'Notifications',
|
label: 'Notifications',
|
||||||
type: Notification[type],
|
type: Notification[type],
|
||||||
subject: payload.subject,
|
subject: payload.subject,
|
||||||
errorMessage: e.message,
|
errorMessage: e.message,
|
||||||
response: errorData,
|
response: e.response?.data,
|
||||||
});
|
});
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -225,36 +210,20 @@ class TelegramAgent
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(endpoint, {
|
await axios.post(endpoint, {
|
||||||
method: 'POST',
|
...notificationPayload,
|
||||||
headers: {
|
chat_id: payload.notifyUser.settings.telegramChatId,
|
||||||
'Content-Type': 'application/json',
|
disable_notification:
|
||||||
},
|
!!payload.notifyUser.settings.telegramSendSilently,
|
||||||
body: JSON.stringify({
|
} as TelegramMessagePayload | TelegramPhotoPayload);
|
||||||
...notificationPayload,
|
|
||||||
chat_id: payload.notifyUser.settings.telegramChatId,
|
|
||||||
disable_notification:
|
|
||||||
!!payload.notifyUser.settings.telegramSendSilently,
|
|
||||||
} as TelegramMessagePayload | TelegramPhotoPayload),
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(response.statusText, { cause: response });
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
let errorData;
|
|
||||||
try {
|
|
||||||
errorData = await e.cause?.text();
|
|
||||||
errorData = JSON.parse(errorData);
|
|
||||||
} catch {
|
|
||||||
/* empty */
|
|
||||||
}
|
|
||||||
logger.error('Error sending Telegram notification', {
|
logger.error('Error sending Telegram notification', {
|
||||||
label: 'Notifications',
|
label: 'Notifications',
|
||||||
recipient: payload.notifyUser.displayName,
|
recipient: payload.notifyUser.displayName,
|
||||||
type: Notification[type],
|
type: Notification[type],
|
||||||
subject: payload.subject,
|
subject: payload.subject,
|
||||||
errorMessage: e.message,
|
errorMessage: e.message,
|
||||||
response: errorData,
|
response: e.response?.data,
|
||||||
});
|
});
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -288,35 +257,19 @@ class TelegramAgent
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(endpoint, {
|
await axios.post(endpoint, {
|
||||||
method: 'POST',
|
...notificationPayload,
|
||||||
headers: {
|
chat_id: user.settings.telegramChatId,
|
||||||
'Content-Type': 'application/json',
|
disable_notification: !!user.settings?.telegramSendSilently,
|
||||||
},
|
} as TelegramMessagePayload | TelegramPhotoPayload);
|
||||||
body: JSON.stringify({
|
|
||||||
...notificationPayload,
|
|
||||||
chat_id: user.settings.telegramChatId,
|
|
||||||
disable_notification: !!user.settings?.telegramSendSilently,
|
|
||||||
} as TelegramMessagePayload | TelegramPhotoPayload),
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(response.statusText, { cause: response });
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
let errorData;
|
|
||||||
try {
|
|
||||||
errorData = await e.cause?.text();
|
|
||||||
errorData = JSON.parse(errorData);
|
|
||||||
} catch {
|
|
||||||
/* empty */
|
|
||||||
}
|
|
||||||
logger.error('Error sending Telegram notification', {
|
logger.error('Error sending Telegram notification', {
|
||||||
label: 'Notifications',
|
label: 'Notifications',
|
||||||
recipient: user.displayName,
|
recipient: user.displayName,
|
||||||
type: Notification[type],
|
type: Notification[type],
|
||||||
subject: payload.subject,
|
subject: payload.subject,
|
||||||
errorMessage: e.message,
|
errorMessage: e.message,
|
||||||
response: errorData,
|
response: e.response?.data,
|
||||||
});
|
});
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { MediaStatus } from '@server/constants/media';
|
|||||||
import type { NotificationAgentWebhook } from '@server/lib/settings';
|
import type { NotificationAgentWebhook } from '@server/lib/settings';
|
||||||
import { getSettings } from '@server/lib/settings';
|
import { getSettings } from '@server/lib/settings';
|
||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
|
import axios from 'axios';
|
||||||
import { get } from 'lodash';
|
import { get } from 'lodash';
|
||||||
import { hasNotificationType, Notification } from '..';
|
import { hasNotificationType, Notification } from '..';
|
||||||
import type { NotificationAgent, NotificationPayload } from './agent';
|
import type { NotificationAgent, NotificationPayload } from './agent';
|
||||||
@@ -177,35 +178,26 @@ class WebhookAgent
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(settings.options.webhookUrl, {
|
await axios.post(
|
||||||
method: 'POST',
|
settings.options.webhookUrl,
|
||||||
headers: {
|
this.buildPayload(type, payload),
|
||||||
'Content-Type': 'application/json',
|
settings.options.authHeader
|
||||||
...(settings.options.authHeader
|
? {
|
||||||
? { Authorization: settings.options.authHeader }
|
headers: {
|
||||||
: {}),
|
Authorization: settings.options.authHeader,
|
||||||
},
|
},
|
||||||
body: JSON.stringify(this.buildPayload(type, payload)),
|
}
|
||||||
});
|
: undefined
|
||||||
if (!response.ok) {
|
);
|
||||||
throw new Error(response.statusText, { cause: response });
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
let errorData;
|
|
||||||
try {
|
|
||||||
errorData = await e.cause?.text();
|
|
||||||
errorData = JSON.parse(errorData);
|
|
||||||
} catch {
|
|
||||||
/* empty */
|
|
||||||
}
|
|
||||||
logger.error('Error sending webhook notification', {
|
logger.error('Error sending webhook notification', {
|
||||||
label: 'Notifications',
|
label: 'Notifications',
|
||||||
type: Notification[type],
|
type: Notification[type],
|
||||||
subject: payload.subject,
|
subject: payload.subject,
|
||||||
errorMessage: e.message,
|
errorMessage: e.message,
|
||||||
response: errorData,
|
response: e.response?.data,
|
||||||
});
|
});
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -582,7 +582,12 @@ class JellyfinScanner {
|
|||||||
const userRepository = getRepository(User);
|
const userRepository = getRepository(User);
|
||||||
const admin = await userRepository.findOne({
|
const admin = await userRepository.findOne({
|
||||||
where: { id: 1 },
|
where: { id: 1 },
|
||||||
select: ['id', 'jellyfinUserId', 'jellyfinDeviceId'],
|
select: [
|
||||||
|
'id',
|
||||||
|
'jellyfinAuthToken',
|
||||||
|
'jellyfinUserId',
|
||||||
|
'jellyfinDeviceId',
|
||||||
|
],
|
||||||
order: { id: 'ASC' },
|
order: { id: 'ASC' },
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -590,9 +595,11 @@ class JellyfinScanner {
|
|||||||
return this.log('No admin configured. Jellyfin sync skipped.', 'warn');
|
return this.log('No admin configured. Jellyfin sync skipped.', 'warn');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hostname = getHostname();
|
||||||
|
|
||||||
this.jfClient = new JellyfinAPI(
|
this.jfClient = new JellyfinAPI(
|
||||||
getHostname(),
|
hostname,
|
||||||
settings.jellyfin.apiKey,
|
admin.jellyfinAuthToken,
|
||||||
admin.jellyfinDeviceId
|
admin.jellyfinDeviceId
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ export interface JellyfinSettings {
|
|||||||
jellyfinForgotPasswordUrl?: string;
|
jellyfinForgotPasswordUrl?: string;
|
||||||
libraries: Library[];
|
libraries: Library[];
|
||||||
serverId: string;
|
serverId: string;
|
||||||
apiKey: string;
|
|
||||||
}
|
}
|
||||||
export interface TautulliSettings {
|
export interface TautulliSettings {
|
||||||
hostname?: string;
|
hostname?: string;
|
||||||
@@ -343,7 +342,6 @@ class Settings {
|
|||||||
jellyfinForgotPasswordUrl: '',
|
jellyfinForgotPasswordUrl: '',
|
||||||
libraries: [],
|
libraries: [],
|
||||||
serverId: '',
|
serverId: '',
|
||||||
apiKey: '',
|
|
||||||
},
|
},
|
||||||
tautulli: {},
|
tautulli: {},
|
||||||
radarr: [],
|
radarr: [],
|
||||||
@@ -631,7 +629,7 @@ class Settings {
|
|||||||
* @param overrideSettings If passed in, will override all existing settings with these
|
* @param overrideSettings If passed in, will override all existing settings with these
|
||||||
* values
|
* values
|
||||||
*/
|
*/
|
||||||
public async load(overrideSettings?: AllSettings): Promise<Settings> {
|
public load(overrideSettings?: AllSettings): Settings {
|
||||||
if (overrideSettings) {
|
if (overrideSettings) {
|
||||||
this.data = overrideSettings;
|
this.data = overrideSettings;
|
||||||
return this;
|
return this;
|
||||||
@@ -644,7 +642,7 @@ class Settings {
|
|||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
const parsedJson = JSON.parse(data);
|
const parsedJson = JSON.parse(data);
|
||||||
this.data = await runMigrations(parsedJson);
|
this.data = runMigrations(parsedJson);
|
||||||
|
|
||||||
this.data = merge(this.data, parsedJson);
|
this.data = merge(this.data, parsedJson);
|
||||||
|
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
import JellyfinAPI from '@server/api/jellyfin';
|
|
||||||
import { MediaServerType } from '@server/constants/server';
|
|
||||||
import { getRepository } from '@server/datasource';
|
|
||||||
import { User } from '@server/entity/User';
|
|
||||||
import type { AllSettings } from '@server/lib/settings';
|
|
||||||
import { getHostname } from '@server/utils/getHostname';
|
|
||||||
|
|
||||||
const migrateApiTokens = async (settings: any): Promise<AllSettings> => {
|
|
||||||
const mediaServerType = settings.main.mediaServerType;
|
|
||||||
if (
|
|
||||||
!settings.jellyfin.apiKey &&
|
|
||||||
(mediaServerType === MediaServerType.JELLYFIN ||
|
|
||||||
mediaServerType === MediaServerType.EMBY)
|
|
||||||
) {
|
|
||||||
const userRepository = getRepository(User);
|
|
||||||
const admin = await userRepository.findOne({
|
|
||||||
where: { id: 1 },
|
|
||||||
select: ['id', 'jellyfinAuthToken', 'jellyfinUserId', 'jellyfinDeviceId'],
|
|
||||||
order: { id: 'ASC' },
|
|
||||||
});
|
|
||||||
if (!admin) {
|
|
||||||
return settings;
|
|
||||||
}
|
|
||||||
const jellyfinClient = new JellyfinAPI(
|
|
||||||
getHostname(settings.jellyfin),
|
|
||||||
admin.jellyfinAuthToken,
|
|
||||||
admin.jellyfinDeviceId
|
|
||||||
);
|
|
||||||
jellyfinClient.setUserId(admin.jellyfinUserId ?? '');
|
|
||||||
const apiKey = await jellyfinClient.createApiToken('Jellyseerr');
|
|
||||||
settings.jellyfin.apiKey = apiKey;
|
|
||||||
}
|
|
||||||
return settings;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default migrateApiTokens;
|
|
||||||
@@ -1,13 +1,10 @@
|
|||||||
import type { AllSettings } from '@server/lib/settings';
|
import type { AllSettings } from '@server/lib/settings';
|
||||||
import logger from '@server/logger';
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
const migrationsDir = path.join(__dirname, 'migrations');
|
const migrationsDir = path.join(__dirname, 'migrations');
|
||||||
|
|
||||||
export const runMigrations = async (
|
export const runMigrations = (settings: AllSettings): AllSettings => {
|
||||||
settings: AllSettings
|
|
||||||
): Promise<AllSettings> => {
|
|
||||||
const migrations = fs
|
const migrations = fs
|
||||||
.readdirSync(migrationsDir)
|
.readdirSync(migrationsDir)
|
||||||
.filter((file) => file.endsWith('.js') || file.endsWith('.ts'))
|
.filter((file) => file.endsWith('.js') || file.endsWith('.ts'))
|
||||||
@@ -16,15 +13,8 @@ export const runMigrations = async (
|
|||||||
|
|
||||||
let migrated = settings;
|
let migrated = settings;
|
||||||
|
|
||||||
try {
|
for (const migration of migrations) {
|
||||||
for (const migration of migrations) {
|
migrated = migration(migrated);
|
||||||
migrated = await migration(migrated);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
logger.error(
|
|
||||||
`Something went wrong while running settings migrations: ${e.message}`,
|
|
||||||
{ label: 'Settings Migrator' }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return migrated;
|
return migrated;
|
||||||
|
|||||||
@@ -85,7 +85,6 @@ export interface MovieDetails {
|
|||||||
mediaUrl?: string;
|
mediaUrl?: string;
|
||||||
watchProviders?: WatchProviders[];
|
watchProviders?: WatchProviders[];
|
||||||
keywords: Keyword[];
|
keywords: Keyword[];
|
||||||
onUserWatchlist?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mapProductionCompany = (
|
export const mapProductionCompany = (
|
||||||
@@ -102,8 +101,7 @@ export const mapProductionCompany = (
|
|||||||
|
|
||||||
export const mapMovieDetails = (
|
export const mapMovieDetails = (
|
||||||
movie: TmdbMovieDetails,
|
movie: TmdbMovieDetails,
|
||||||
media?: Media,
|
media?: Media
|
||||||
userWatchlist?: boolean
|
|
||||||
): MovieDetails => ({
|
): MovieDetails => ({
|
||||||
id: movie.id,
|
id: movie.id,
|
||||||
adult: movie.adult,
|
adult: movie.adult,
|
||||||
@@ -150,5 +148,4 @@ export const mapMovieDetails = (
|
|||||||
id: keyword.id,
|
id: keyword.id,
|
||||||
name: keyword.name,
|
name: keyword.name,
|
||||||
})),
|
})),
|
||||||
onUserWatchlist: userWatchlist,
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -111,7 +111,6 @@ export interface TvDetails {
|
|||||||
keywords: Keyword[];
|
keywords: Keyword[];
|
||||||
mediaInfo?: Media;
|
mediaInfo?: Media;
|
||||||
watchProviders?: WatchProviders[];
|
watchProviders?: WatchProviders[];
|
||||||
onUserWatchlist?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapEpisodeResult = (episode: TmdbTvEpisodeResult): Episode => ({
|
const mapEpisodeResult = (episode: TmdbTvEpisodeResult): Episode => ({
|
||||||
@@ -162,8 +161,7 @@ export const mapNetwork = (network: TmdbNetwork): TvNetwork => ({
|
|||||||
|
|
||||||
export const mapTvDetails = (
|
export const mapTvDetails = (
|
||||||
show: TmdbTvDetails,
|
show: TmdbTvDetails,
|
||||||
media?: Media,
|
media?: Media
|
||||||
userWatchlist?: boolean
|
|
||||||
): TvDetails => ({
|
): TvDetails => ({
|
||||||
createdBy: show.created_by,
|
createdBy: show.created_by,
|
||||||
episodeRunTime: show.episode_run_time,
|
episodeRunTime: show.episode_run_time,
|
||||||
@@ -225,5 +223,4 @@ export const mapTvDetails = (
|
|||||||
})),
|
})),
|
||||||
mediaInfo: media,
|
mediaInfo: media,
|
||||||
watchProviders: mapWatchProviders(show['watch/providers']?.results ?? {}),
|
watchProviders: mapWatchProviders(show['watch/providers']?.results ?? {}),
|
||||||
onUserWatchlist: userWatchlist,
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -321,28 +320,18 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
// with admin permission
|
// with admin permission
|
||||||
settings.main.mediaServerType = MediaServerType.JELLYFIN;
|
settings.main.mediaServerType = MediaServerType.JELLYFIN;
|
||||||
user = new User({
|
user = new User({
|
||||||
email: body.email || account.User.Name,
|
email: body.email,
|
||||||
jellyfinUsername: account.User.Name,
|
jellyfinUsername: account.User.Name,
|
||||||
jellyfinUserId: account.User.Id,
|
jellyfinUserId: account.User.Id,
|
||||||
jellyfinDeviceId: deviceId,
|
jellyfinDeviceId: deviceId,
|
||||||
|
jellyfinAuthToken: account.AccessToken,
|
||||||
permissions: Permission.ADMIN,
|
permissions: Permission.ADMIN,
|
||||||
avatar: account.User.PrimaryImageTag
|
avatar: account.User.PrimaryImageTag
|
||||||
? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
|
? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
|
||||||
: gravatarUrl(body.email || account.User.Name, {
|
: gravatarUrl(body.email ?? '', { default: 'mm', size: 200 }),
|
||||||
default: 'mm',
|
|
||||||
size: 200,
|
|
||||||
}),
|
|
||||||
userType: UserType.JELLYFIN,
|
userType: UserType.JELLYFIN,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create an API key on Jellyfin from this admin user
|
|
||||||
const jellyfinClient = new JellyfinAPI(
|
|
||||||
hostname,
|
|
||||||
account.AccessToken,
|
|
||||||
deviceId
|
|
||||||
);
|
|
||||||
const apiKey = await jellyfinClient.createApiToken('Jellyseerr');
|
|
||||||
|
|
||||||
const serverName = await jellyfinserver.getServerName();
|
const serverName = await jellyfinserver.getServerName();
|
||||||
|
|
||||||
settings.jellyfin.name = serverName;
|
settings.jellyfin.name = serverName;
|
||||||
@@ -351,7 +340,6 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
settings.jellyfin.port = body.port ?? 8096;
|
settings.jellyfin.port = body.port ?? 8096;
|
||||||
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.save();
|
settings.save();
|
||||||
startJobs();
|
startJobs();
|
||||||
|
|
||||||
@@ -375,25 +363,18 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
jellyfinUsername: account.User.Name,
|
jellyfinUsername: account.User.Name,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
// Let's check if their authtoken is up to date
|
||||||
|
if (user.jellyfinAuthToken !== account.AccessToken) {
|
||||||
|
user.jellyfinAuthToken = account.AccessToken;
|
||||||
|
}
|
||||||
// Update the users avatar with their jellyfin profile pic (incase it changed)
|
// Update the users avatar with their jellyfin profile pic (incase it changed)
|
||||||
if (account.User.PrimaryImageTag) {
|
if (account.User.PrimaryImageTag) {
|
||||||
const avatar = `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`;
|
user.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 {
|
} else {
|
||||||
const avatar = gravatarUrl(user.email || account.User.Name, {
|
user.avatar = gravatarUrl(user.email, {
|
||||||
default: 'mm',
|
default: 'mm',
|
||||||
size: 200,
|
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;
|
||||||
|
|
||||||
@@ -432,18 +413,20 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!body.email) {
|
||||||
|
throw new Error('add_email');
|
||||||
|
}
|
||||||
|
|
||||||
user = new User({
|
user = new User({
|
||||||
email: body.email,
|
email: body.email,
|
||||||
jellyfinUsername: account.User.Name,
|
jellyfinUsername: account.User.Name,
|
||||||
jellyfinUserId: account.User.Id,
|
jellyfinUserId: account.User.Id,
|
||||||
jellyfinDeviceId: deviceId,
|
jellyfinDeviceId: deviceId,
|
||||||
|
jellyfinAuthToken: account.AccessToken,
|
||||||
permissions: settings.main.defaultPermissions,
|
permissions: settings.main.defaultPermissions,
|
||||||
avatar: account.User.PrimaryImageTag
|
avatar: account.User.PrimaryImageTag
|
||||||
? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
|
? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
|
||||||
: gravatarUrl(body.email || account.User.Name, {
|
: gravatarUrl(body.email, { default: 'mm', size: 200 }),
|
||||||
default: 'mm',
|
|
||||||
size: 200,
|
|
||||||
}),
|
|
||||||
userType: UserType.JELLYFIN,
|
userType: UserType.JELLYFIN,
|
||||||
});
|
});
|
||||||
//initialize Jellyfin/Emby users with local login
|
//initialize Jellyfin/Emby users with local login
|
||||||
@@ -747,7 +730,6 @@ authRoutes.post('/reset-password/:guid', async (req, res, next) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
user.recoveryLinkExpirationDate = null;
|
user.recoveryLinkExpirationDate = null;
|
||||||
await user.setPassword(req.body.password);
|
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
logger.info('Successfully reset password', {
|
logger.info('Successfully reset password', {
|
||||||
label: 'API',
|
label: 'API',
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
import ImageProxy from '@server/lib/imageproxy';
|
|
||||||
import logger from '@server/logger';
|
|
||||||
import { Router } from 'express';
|
|
||||||
|
|
||||||
const router = Router();
|
|
||||||
|
|
||||||
const avatarImageProxy = new ImageProxy('avatar', '');
|
|
||||||
// Proxy avatar images
|
|
||||||
router.get('/*', async (req, res) => {
|
|
||||||
const imagePath = req.url.startsWith('/') ? req.url.slice(1) : req.url;
|
|
||||||
try {
|
|
||||||
const imageData = await avatarImageProxy.getImage(imagePath);
|
|
||||||
|
|
||||||
res.writeHead(200, {
|
|
||||||
'Content-Type': `image/${imageData.meta.extension}`,
|
|
||||||
'Content-Length': imageData.imageBuffer.length,
|
|
||||||
'Cache-Control': `public, max-age=${imageData.meta.curRevalidate}`,
|
|
||||||
'OS-Cache-Key': imageData.meta.cacheKey,
|
|
||||||
'OS-Cache-Status': imageData.meta.cacheMiss ? 'MISS' : 'HIT',
|
|
||||||
});
|
|
||||||
|
|
||||||
res.end(imageData.imageBuffer);
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('Failed to proxy avatar image', {
|
|
||||||
imagePath,
|
|
||||||
errorMessage: e.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@@ -5,6 +5,7 @@ import { Router } from 'express';
|
|||||||
const router = Router();
|
const router = Router();
|
||||||
const tmdbImageProxy = new ImageProxy('tmdb', 'https://image.tmdb.org', {
|
const tmdbImageProxy = new ImageProxy('tmdb', 'https://image.tmdb.org', {
|
||||||
rateLimitOptions: {
|
rateLimitOptions: {
|
||||||
|
maxRequests: 20,
|
||||||
maxRPS: 50,
|
maxRPS: 50,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ import RottenTomatoes from '@server/api/rating/rottentomatoes';
|
|||||||
import { type RatingResponse } from '@server/api/ratings';
|
import { type RatingResponse } from '@server/api/ratings';
|
||||||
import TheMovieDb from '@server/api/themoviedb';
|
import TheMovieDb from '@server/api/themoviedb';
|
||||||
import { MediaType } from '@server/constants/media';
|
import { MediaType } from '@server/constants/media';
|
||||||
import { getRepository } from '@server/datasource';
|
|
||||||
import Media from '@server/entity/Media';
|
import Media from '@server/entity/Media';
|
||||||
import { Watchlist } from '@server/entity/Watchlist';
|
|
||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
import { mapMovieDetails } from '@server/models/Movie';
|
import { mapMovieDetails } from '@server/models/Movie';
|
||||||
import { mapMovieResult } from '@server/models/Search';
|
import { mapMovieResult } from '@server/models/Search';
|
||||||
@@ -24,24 +22,7 @@ movieRoutes.get('/:id', async (req, res, next) => {
|
|||||||
|
|
||||||
const media = await Media.getMedia(tmdbMovie.id, MediaType.MOVIE);
|
const media = await Media.getMedia(tmdbMovie.id, MediaType.MOVIE);
|
||||||
|
|
||||||
const onUserWatchlist = await getRepository(Watchlist).exist({
|
return res.status(200).json(mapMovieDetails(tmdbMovie, media));
|
||||||
where: {
|
|
||||||
tmdbId: Number(req.params.id),
|
|
||||||
requestedBy: {
|
|
||||||
id: req.user?.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = mapMovieDetails(tmdbMovie, media, onUserWatchlist);
|
|
||||||
|
|
||||||
// TMDB issue where it doesnt fallback to English when no overview is available in requested locale.
|
|
||||||
if (!data.overview) {
|
|
||||||
const tvEnglish = await tmdb.getMovie({ movieId: Number(req.params.id) });
|
|
||||||
data.overview = tvEnglish.overview;
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).json(data);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug('Something went wrong retrieving movie', {
|
logger.debug('Something went wrong retrieving movie', {
|
||||||
label: 'API',
|
label: 'API',
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import RadarrAPI from '@server/api/servarr/radarr';
|
|
||||||
import SonarrAPI from '@server/api/servarr/sonarr';
|
|
||||||
import {
|
import {
|
||||||
MediaRequestStatus,
|
MediaRequestStatus,
|
||||||
MediaStatus,
|
MediaStatus,
|
||||||
@@ -21,7 +19,6 @@ import type {
|
|||||||
RequestResultsResponse,
|
RequestResultsResponse,
|
||||||
} from '@server/interfaces/api/requestInterfaces';
|
} from '@server/interfaces/api/requestInterfaces';
|
||||||
import { Permission } from '@server/lib/permissions';
|
import { Permission } from '@server/lib/permissions';
|
||||||
import { getSettings } from '@server/lib/settings';
|
|
||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
import { isAuthenticated } from '@server/middleware/auth';
|
import { isAuthenticated } from '@server/middleware/auth';
|
||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
@@ -146,62 +143,6 @@ requestRoutes.get<Record<string, unknown>, RequestResultsResponse>(
|
|||||||
.skip(skip)
|
.skip(skip)
|
||||||
.getManyAndCount();
|
.getManyAndCount();
|
||||||
|
|
||||||
const settings = getSettings();
|
|
||||||
|
|
||||||
// get all quality profiles for every configured sonarr server
|
|
||||||
const sonarrServers = await Promise.all(
|
|
||||||
settings.sonarr.map(async (sonarrSetting) => {
|
|
||||||
const sonarr = new SonarrAPI({
|
|
||||||
apiKey: sonarrSetting.apiKey,
|
|
||||||
url: SonarrAPI.buildUrl(sonarrSetting, '/api/v3'),
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: sonarrSetting.id,
|
|
||||||
profiles: await sonarr.getProfiles(),
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// get all quality profiles for every configured radarr server
|
|
||||||
const radarrServers = await Promise.all(
|
|
||||||
settings.radarr.map(async (radarrSetting) => {
|
|
||||||
const radarr = new RadarrAPI({
|
|
||||||
apiKey: radarrSetting.apiKey,
|
|
||||||
url: RadarrAPI.buildUrl(radarrSetting, '/api/v3'),
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: radarrSetting.id,
|
|
||||||
profiles: await radarr.getProfiles(),
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// add profile names to the media requests, with undefined if not found
|
|
||||||
const requestsWithProfileNames = requests.map((r) => {
|
|
||||||
switch (r.type) {
|
|
||||||
case MediaType.MOVIE: {
|
|
||||||
const profileName = radarrServers
|
|
||||||
.find((serverr) => serverr.id === r.serverId)
|
|
||||||
?.profiles.find((profile) => profile.id === r.profileId)?.name;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...r,
|
|
||||||
profileName,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case MediaType.TV: {
|
|
||||||
return {
|
|
||||||
...r,
|
|
||||||
profileName: sonarrServers
|
|
||||||
.find((serverr) => serverr.id === r.serverId)
|
|
||||||
?.profiles.find((profile) => profile.id === r.profileId)?.name,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
pageInfo: {
|
pageInfo: {
|
||||||
pages: Math.ceil(requestCount / pageSize),
|
pages: Math.ceil(requestCount / pageSize),
|
||||||
@@ -209,7 +150,7 @@ requestRoutes.get<Record<string, unknown>, RequestResultsResponse>(
|
|||||||
results: requestCount,
|
results: requestCount,
|
||||||
page: Math.ceil(skip / pageSize) + 1,
|
page: Math.ceil(skip / pageSize) + 1,
|
||||||
},
|
},
|
||||||
results: requestsWithProfileNames,
|
results: requests,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
next({ status: 500, message: e.message });
|
next({ status: 500, message: e.message });
|
||||||
|
|||||||
@@ -262,7 +262,7 @@ settingsRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
try {
|
try {
|
||||||
const admin = await userRepository.findOneOrFail({
|
const admin = await userRepository.findOneOrFail({
|
||||||
where: { id: 1 },
|
where: { id: 1 },
|
||||||
select: ['id', 'jellyfinUserId', 'jellyfinDeviceId'],
|
select: ['id', 'jellyfinAuthToken', 'jellyfinUserId', 'jellyfinDeviceId'],
|
||||||
order: { id: 'ASC' },
|
order: { id: 'ASC' },
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -270,7 +270,7 @@ settingsRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
|
|
||||||
const jellyfinClient = new JellyfinAPI(
|
const jellyfinClient = new JellyfinAPI(
|
||||||
getHostname(tempJellyfinSettings),
|
getHostname(tempJellyfinSettings),
|
||||||
tempJellyfinSettings.apiKey,
|
admin.jellyfinAuthToken ?? '',
|
||||||
admin.jellyfinDeviceId ?? ''
|
admin.jellyfinDeviceId ?? ''
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -318,13 +318,13 @@ settingsRoutes.get('/jellyfin/library', async (req, res, next) => {
|
|||||||
if (req.query.sync) {
|
if (req.query.sync) {
|
||||||
const userRepository = getRepository(User);
|
const userRepository = getRepository(User);
|
||||||
const admin = await userRepository.findOneOrFail({
|
const admin = await userRepository.findOneOrFail({
|
||||||
select: ['id', 'jellyfinDeviceId', 'jellyfinUserId'],
|
select: ['id', 'jellyfinAuthToken', 'jellyfinDeviceId', 'jellyfinUserId'],
|
||||||
where: { id: 1 },
|
where: { id: 1 },
|
||||||
order: { id: 'ASC' },
|
order: { id: 'ASC' },
|
||||||
});
|
});
|
||||||
const jellyfinClient = new JellyfinAPI(
|
const jellyfinClient = new JellyfinAPI(
|
||||||
getHostname(),
|
getHostname(),
|
||||||
settings.jellyfin.apiKey,
|
admin.jellyfinAuthToken ?? '',
|
||||||
admin.jellyfinDeviceId ?? ''
|
admin.jellyfinDeviceId ?? ''
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -376,8 +376,7 @@ settingsRoutes.get('/jellyfin/library', async (req, res, next) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
settingsRoutes.get('/jellyfin/users', async (req, res) => {
|
settingsRoutes.get('/jellyfin/users', async (req, res) => {
|
||||||
const settings = getSettings();
|
const { externalHostname } = getSettings().jellyfin;
|
||||||
const { externalHostname } = settings.jellyfin;
|
|
||||||
const jellyfinHost =
|
const jellyfinHost =
|
||||||
externalHostname && externalHostname.length > 0
|
externalHostname && externalHostname.length > 0
|
||||||
? externalHostname
|
? externalHostname
|
||||||
@@ -385,13 +384,12 @@ settingsRoutes.get('/jellyfin/users', async (req, res) => {
|
|||||||
|
|
||||||
const userRepository = getRepository(User);
|
const userRepository = getRepository(User);
|
||||||
const admin = await userRepository.findOneOrFail({
|
const admin = await userRepository.findOneOrFail({
|
||||||
select: ['id', 'jellyfinDeviceId', 'jellyfinUserId'],
|
select: ['id', 'jellyfinAuthToken', 'jellyfinDeviceId', 'jellyfinUserId'],
|
||||||
where: { id: 1 },
|
where: { id: 1 },
|
||||||
order: { id: 'ASC' },
|
order: { id: 'ASC' },
|
||||||
});
|
});
|
||||||
const jellyfinClient = new JellyfinAPI(
|
const jellyfinClient = new JellyfinAPI(
|
||||||
getHostname(),
|
admin.jellyfinAuthToken ?? '',
|
||||||
settings.jellyfin.apiKey,
|
|
||||||
admin.jellyfinDeviceId ?? ''
|
admin.jellyfinDeviceId ?? ''
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -746,13 +744,11 @@ settingsRoutes.get('/cache', async (_req, res) => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const tmdbImageCache = await ImageProxy.getImageStats('tmdb');
|
const tmdbImageCache = await ImageProxy.getImageStats('tmdb');
|
||||||
const avatarImageCache = await ImageProxy.getImageStats('avatar');
|
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
apiCaches,
|
apiCaches,
|
||||||
imageCache: {
|
imageCache: {
|
||||||
tmdb: tmdbImageCache,
|
tmdb: tmdbImageCache,
|
||||||
avatar: avatarImageCache,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import RottenTomatoes from '@server/api/rating/rottentomatoes';
|
import RottenTomatoes from '@server/api/rating/rottentomatoes';
|
||||||
import TheMovieDb from '@server/api/themoviedb';
|
import TheMovieDb from '@server/api/themoviedb';
|
||||||
import { MediaType } from '@server/constants/media';
|
import { MediaType } from '@server/constants/media';
|
||||||
import { getRepository } from '@server/datasource';
|
|
||||||
import Media from '@server/entity/Media';
|
import Media from '@server/entity/Media';
|
||||||
import { Watchlist } from '@server/entity/Watchlist';
|
|
||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
import { mapTvResult } from '@server/models/Search';
|
import { mapTvResult } from '@server/models/Search';
|
||||||
import { mapSeasonWithEpisodes, mapTvDetails } from '@server/models/Tv';
|
import { mapSeasonWithEpisodes, mapTvDetails } from '@server/models/Tv';
|
||||||
@@ -21,24 +19,7 @@ tvRoutes.get('/:id', async (req, res, next) => {
|
|||||||
|
|
||||||
const media = await Media.getMedia(tv.id, MediaType.TV);
|
const media = await Media.getMedia(tv.id, MediaType.TV);
|
||||||
|
|
||||||
const onUserWatchlist = await getRepository(Watchlist).exist({
|
return res.status(200).json(mapTvDetails(tv, media));
|
||||||
where: {
|
|
||||||
tmdbId: Number(req.params.id),
|
|
||||||
requestedBy: {
|
|
||||||
id: req.user?.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = mapTvDetails(tv, media, onUserWatchlist);
|
|
||||||
|
|
||||||
// TMDB issue where it doesnt fallback to English when no overview is available in requested locale.
|
|
||||||
if (!data.overview) {
|
|
||||||
const tvEnglish = await tmdb.getTvShow({ tvId: Number(req.params.id) });
|
|
||||||
data.overview = tvEnglish.overview;
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).json(data);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug('Something went wrong retrieving series', {
|
logger.debug('Something went wrong retrieving series', {
|
||||||
label: 'API',
|
label: 'API',
|
||||||
|
|||||||
@@ -41,19 +41,7 @@ router.get('/', async (req, res, next) => {
|
|||||||
break;
|
break;
|
||||||
case 'displayname':
|
case 'displayname':
|
||||||
query = query.orderBy(
|
query = query.orderBy(
|
||||||
`CASE WHEN (user.username IS NULL OR user.username = '') THEN (
|
"(CASE WHEN (user.username IS NULL OR user.username = '') THEN (CASE WHEN (user.plexUsername IS NULL OR user.plexUsername = '') THEN user.email ELSE LOWER(user.plexUsername) END) ELSE LOWER(user.username) END)",
|
||||||
CASE WHEN (user.plexUsername IS NULL OR user.plexUsername = '') THEN (
|
|
||||||
CASE WHEN (user.jellyfinUsername IS NULL OR user.jellyfinUsername = '') THEN
|
|
||||||
user.email
|
|
||||||
ELSE
|
|
||||||
LOWER(user.jellyfinUsername)
|
|
||||||
END)
|
|
||||||
ELSE
|
|
||||||
LOWER(user.jellyfinUsername)
|
|
||||||
END)
|
|
||||||
ELSE
|
|
||||||
LOWER(user.username)
|
|
||||||
END`,
|
|
||||||
'ASC'
|
'ASC'
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
@@ -102,13 +90,12 @@ router.post(
|
|||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
const body = req.body;
|
const body = req.body;
|
||||||
const email = body.email || body.username;
|
|
||||||
const userRepository = getRepository(User);
|
const userRepository = getRepository(User);
|
||||||
|
|
||||||
const existingUser = await userRepository
|
const existingUser = await userRepository
|
||||||
.createQueryBuilder('user')
|
.createQueryBuilder('user')
|
||||||
.where('user.email = :email', {
|
.where('user.email = :email', {
|
||||||
email: email.toLowerCase(),
|
email: body.email.toLowerCase(),
|
||||||
})
|
})
|
||||||
.getOne();
|
.getOne();
|
||||||
|
|
||||||
@@ -121,7 +108,7 @@ router.post(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const passedExplicitPassword = body.password && body.password.length > 0;
|
const passedExplicitPassword = body.password && body.password.length > 0;
|
||||||
const avatar = gravatarUrl(email, { default: 'mm', size: 200 });
|
const avatar = gravatarUrl(body.email, { default: 'mm', size: 200 });
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!passedExplicitPassword &&
|
!passedExplicitPassword &&
|
||||||
@@ -131,9 +118,9 @@ router.post(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const user = new User({
|
const user = new User({
|
||||||
email,
|
|
||||||
avatar: body.avatar ?? avatar,
|
avatar: body.avatar ?? avatar,
|
||||||
username: body.username,
|
username: body.username,
|
||||||
|
email: body.email,
|
||||||
password: body.password,
|
password: body.password,
|
||||||
permissions: settings.main.defaultPermissions,
|
permissions: settings.main.defaultPermissions,
|
||||||
plexToken: '',
|
plexToken: '',
|
||||||
@@ -501,14 +488,16 @@ router.post(
|
|||||||
// taken from auth.ts
|
// taken from auth.ts
|
||||||
const admin = await userRepository.findOneOrFail({
|
const admin = await userRepository.findOneOrFail({
|
||||||
where: { id: 1 },
|
where: { id: 1 },
|
||||||
select: ['id', 'jellyfinDeviceId', 'jellyfinUserId'],
|
select: [
|
||||||
|
'id',
|
||||||
|
'jellyfinAuthToken',
|
||||||
|
'jellyfinDeviceId',
|
||||||
|
'jellyfinUserId',
|
||||||
|
],
|
||||||
order: { id: 'ASC' },
|
order: { id: 'ASC' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const hostname = getHostname();
|
|
||||||
const jellyfinClient = new JellyfinAPI(
|
const jellyfinClient = new JellyfinAPI(
|
||||||
hostname,
|
admin.jellyfinAuthToken ?? '',
|
||||||
settings.jellyfin.apiKey,
|
|
||||||
admin.jellyfinDeviceId ?? ''
|
admin.jellyfinDeviceId ?? ''
|
||||||
);
|
);
|
||||||
jellyfinClient.setUserId(admin.jellyfinUserId ?? '');
|
jellyfinClient.setUserId(admin.jellyfinUserId ?? '');
|
||||||
@@ -516,6 +505,7 @@ router.post(
|
|||||||
//const jellyfinUsersResponse = await jellyfinClient.getUsers();
|
//const jellyfinUsersResponse = await jellyfinClient.getUsers();
|
||||||
const createdUsers: User[] = [];
|
const createdUsers: User[] = [];
|
||||||
const { externalHostname } = getSettings().jellyfin;
|
const { externalHostname } = getSettings().jellyfin;
|
||||||
|
const hostname = getHostname();
|
||||||
|
|
||||||
const jellyfinHost =
|
const jellyfinHost =
|
||||||
externalHostname && externalHostname.length > 0
|
externalHostname && externalHostname.length > 0
|
||||||
|
|||||||
@@ -98,9 +98,7 @@ userSettingsRoutes.post<
|
|||||||
}
|
}
|
||||||
|
|
||||||
user.username = req.body.username;
|
user.username = req.body.username;
|
||||||
if (user.jellyfinUsername) {
|
user.email = req.body.email ?? user.email;
|
||||||
user.email = req.body.email || user.jellyfinUsername || user.email;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update quota values only if the user has the correct permissions
|
// Update quota values only if the user has the correct permissions
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
export type RateLimitOptions = {
|
|
||||||
maxRPS: number;
|
|
||||||
id?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type RateLimiteState<T extends (...args: Parameters<T>) => Promise<U>, U> = {
|
|
||||||
queue: {
|
|
||||||
args: Parameters<T>;
|
|
||||||
resolve: (value: U) => void;
|
|
||||||
reject: (reason?: unknown) => void;
|
|
||||||
}[];
|
|
||||||
lastTimestamps: number[];
|
|
||||||
timeout: ReturnType<typeof setTimeout>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const rateLimitById: Record<string, unknown> = {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a rate limit to a function so it doesn't exceed a maximum number of requests per second. Function calls exceeding the rate will be delayed.
|
|
||||||
* @param fn The function to rate limit
|
|
||||||
* @param options.maxRPS Maximum number of Requests Per Second
|
|
||||||
* @param options.id An ID to share between rate limits, so it uses the same request queue.
|
|
||||||
* @returns The function with a rate limit
|
|
||||||
*/
|
|
||||||
export default function rateLimit<
|
|
||||||
T extends (...args: Parameters<T>) => Promise<U>,
|
|
||||||
U
|
|
||||||
>(fn: T, options: RateLimitOptions): (...args: Parameters<T>) => Promise<U> {
|
|
||||||
const state: RateLimiteState<T, U> = (rateLimitById[
|
|
||||||
options.id || ''
|
|
||||||
] as RateLimiteState<T, U>) || { queue: [], lastTimestamps: [] };
|
|
||||||
if (options.id) {
|
|
||||||
rateLimitById[options.id] = state;
|
|
||||||
}
|
|
||||||
|
|
||||||
const processQueue = () => {
|
|
||||||
// remove old timestamps
|
|
||||||
state.lastTimestamps = state.lastTimestamps.filter(
|
|
||||||
(timestamp) => Date.now() - timestamp < 1000
|
|
||||||
);
|
|
||||||
|
|
||||||
if (state.lastTimestamps.length < options.maxRPS) {
|
|
||||||
// process requests if RPS not exceeded
|
|
||||||
const item = state.queue.shift();
|
|
||||||
if (!item) return;
|
|
||||||
state.lastTimestamps.push(Date.now());
|
|
||||||
const { args, resolve, reject } = item;
|
|
||||||
fn(...args)
|
|
||||||
.then(resolve)
|
|
||||||
.catch(reject);
|
|
||||||
processQueue();
|
|
||||||
} else {
|
|
||||||
// rerun once the oldest item in queue is older than 1s
|
|
||||||
if (state.timeout) clearTimeout(state.timeout);
|
|
||||||
state.timeout = setTimeout(
|
|
||||||
processQueue,
|
|
||||||
1000 - (Date.now() - state.lastTimestamps[0])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (...args: Parameters<T>): Promise<U> => {
|
|
||||||
return new Promise<U>((resolve, reject) => {
|
|
||||||
state.queue.push({ args, resolve, reject });
|
|
||||||
processQueue();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,85 +1 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320.03 103.61"><defs><style>.cls-1{fill:#fff}.cls-2{fill:url(#a)}.cls-3{fill:#e5a00d}</style><radialGradient id="a" cx="258.33" cy="51.76" r="42.95" gradientUnits="userSpaceOnUse"><stop offset=".17" stop-color="#f9be03"/><stop offset=".51" stop-color="#e8a50b"/><stop offset="1" stop-color="#cc7c19"/></radialGradient></defs><polygon points="320.03 -.09 289.96 -.09 259.88 51.76 289.96 103.61 320.01 103.61 289.96 51.79" class="cls-1"/><polygon points="226.7 -.09 256.78 -.09 289.96 51.76 256.78 103.61 226.7 103.61 259.88 51.76" class="cls-2"/><polygon points="226.7 -.09 256.78 -.09 289.96 51.76 256.78 103.61 226.7 103.61 259.88 51.76" class="cls-3"/><path d="M216.32,103.61H156.49V-.09h59.83v18h-37.8V40.69H213.7v18H178.52V85.45h37.8Z" class="cls-1"/><path d="M82.07,103.61V-.09h22V85.45h42.07v18.16Z" class="cls-1"/><path d="M71.66,32.25Q71.66,49,61.2,57.87T31.44,66.73H22v36.88H0V-.09H33.14Q52-.09,61.83,8T71.66,32.25ZM22,48.71h7.24q10.15,0,15.18-4c3.37-2.66,5-6.56,5-11.67s-1.41-9-4.22-11.42S38,17.93,32,17.93H22Z" class="cls-1"/></svg>
|
||||||
<!-- Generator: Adobe Illustrator 26.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
version="1.1"
|
|
||||||
id="plex-logo"
|
|
||||||
x="0px"
|
|
||||||
y="0px"
|
|
||||||
viewBox="0 0 1000 460.89727"
|
|
||||||
xml:space="preserve"
|
|
||||||
sodipodi:docname="plex-logo.svg"
|
|
||||||
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"><metadata
|
|
||||||
id="metadata25"><rdf:RDF><cc:Work
|
|
||||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
|
|
||||||
id="defs23">
|
|
||||||
</defs><sodipodi:namedview
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#111111"
|
|
||||||
borderopacity="1"
|
|
||||||
objecttolerance="10"
|
|
||||||
gridtolerance="10"
|
|
||||||
guidetolerance="10"
|
|
||||||
inkscape:pageopacity="0"
|
|
||||||
inkscape:pageshadow="2"
|
|
||||||
inkscape:window-width="1920"
|
|
||||||
inkscape:window-height="1017"
|
|
||||||
id="namedview21"
|
|
||||||
showgrid="false"
|
|
||||||
fit-margin-top="0"
|
|
||||||
fit-margin-left="0"
|
|
||||||
fit-margin-right="0"
|
|
||||||
fit-margin-bottom="0"
|
|
||||||
inkscape:zoom="0.27956081"
|
|
||||||
inkscape:cx="783.06912"
|
|
||||||
inkscape:cy="-132.85701"
|
|
||||||
inkscape:window-x="1912"
|
|
||||||
inkscape:window-y="-8"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
inkscape:current-layer="plex-logo" />
|
|
||||||
<style
|
|
||||||
type="text/css"
|
|
||||||
id="style2">
|
|
||||||
.st0{fill:#FFFFFF;}
|
|
||||||
.st1{fill:#EBAF00;}
|
|
||||||
</style>
|
|
||||||
<path
|
|
||||||
class="st0"
|
|
||||||
d="m 164.18919,82.43243 c -39.86487,0 -65.540543,11.48648 -87.162163,38.51351 V 91.21621 H 0 v 366.21621 c 0,0 1.3513514,0.67567 5.4054053,1.35135 5.4054057,1.35135 33.7837827,7.43243 54.7297287,-10.13514 18.243244,-15.54054 22.297295,-33.78378 22.297295,-54.05405 v -52.7027 c 22.297301,23.64864 47.297301,33.78378 82.432431,33.78378 75.67567,0 133.78378,-61.48648 133.78378,-143.24323 0,-88.51352 -56.08108,-150 -134.45945,-150 z m -14.86487,223.64864 c -42.56756,0 -76.351351,-35.13513 -76.351351,-77.7027 0,-41.89189 39.864871,-75.67567 76.351351,-75.67567 43.24324,0 76.35135,33.1081 76.35135,76.35135 0,43.24324 -33.78378,77.02702 -76.35135,77.02702 z"
|
|
||||||
id="path4"
|
|
||||||
inkscape:connector-curvature="0"
|
|
||||||
style="fill:#ffffff;stroke-width:6.75675678" /><path
|
|
||||||
class="st0"
|
|
||||||
d="m 408.1081,223.64864 c 0,31.75676 3.37838,70.27027 34.45946,112.16216 0.67567,0.67567 2.02702,2.7027 2.02702,2.7027 -12.83783,21.62162 -28.37837,36.48648 -49.32432,36.48648 -16.21622,0 -32.43243,-8.78378 -45.94595,-23.64864 -14.18918,-16.21622 -20.94594,-37.16216 -20.94594,-59.45946 V 0 h 79.05405 z"
|
|
||||||
id="path6"
|
|
||||||
inkscape:connector-curvature="0"
|
|
||||||
style="fill:#ffffff;stroke-width:6.75675678" /><polygon
|
|
||||||
class="st1"
|
|
||||||
points="117.9,33.9 104.1,13.5 118.3,13.5 132,33.9 118.3,54.2 104.1,54.2 "
|
|
||||||
id="polygon8"
|
|
||||||
style="fill:#ebaf00"
|
|
||||||
transform="scale(6.7567568)" /><polygon
|
|
||||||
class="st0"
|
|
||||||
points="135.7,31.6 148,13.5 133.8,13.5 128.7,21 "
|
|
||||||
id="polygon10"
|
|
||||||
style="fill:#ffffff"
|
|
||||||
transform="scale(6.7567568)" /><path
|
|
||||||
class="st0"
|
|
||||||
d="m 869.59458,316.2162 c 0,0 16.2162,22.2973 16.2162,22.2973 15.54058,24.32432 35.8108,36.48648 59.45949,36.48648 25,-0.67567 42.56752,-22.29729 49.3243,-30.4054 0,0 -12.16218,-10.81081 -27.7027,-29.05405 -20.94598,-24.32432 -48.64868,-68.91892 -49.3243,-70.94594 z"
|
|
||||||
id="path12"
|
|
||||||
inkscape:connector-curvature="0"
|
|
||||||
style="fill:#ffffff;stroke-width:6.75675678" /><path
|
|
||||||
style="fill:#ffffff;stroke-width:6.75675678"
|
|
||||||
inkscape:connector-curvature="0"
|
|
||||||
id="path16"
|
|
||||||
d="m 632.43242,287.16215 c -16.21622,14.86486 -27.02703,22.97297 -49.32432,22.97297 -39.86487,0 -62.83784,-28.37837 -66.21622,-59.45945 h 211.4865 c 1.35131,-4.05406 2.027,-9.45946 2.027,-18.24324 0,-85.81082 -62.83783,-150 -145.27026,-150 -78.37837,0 -142.56756,65.54054 -142.56756,147.29729 0,81.08108 64.18919,145.27026 144.59459,145.27026 56.08108,0 104.72973,-31.75675 131.08105,-87.83783 z M 585.8108,147.29729 c 35.13513,0 61.48648,22.97297 67.56756,53.37838 H 519.59458 c 6.75676,-31.75676 31.75676,-53.37838 66.21622,-53.37838 z"
|
|
||||||
class="st0" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -14,6 +14,7 @@ import { DiscoverSliderType } from '@server/constants/discover';
|
|||||||
import type DiscoverSlider from '@server/entity/DiscoverSlider';
|
import type DiscoverSlider from '@server/entity/DiscoverSlider';
|
||||||
import type { GenreSliderItem } from '@server/interfaces/api/discoverInterfaces';
|
import type { GenreSliderItem } from '@server/interfaces/api/discoverInterfaces';
|
||||||
import type { Keyword, ProductionCompany } from '@server/models/common';
|
import type { Keyword, ProductionCompany } from '@server/models/common';
|
||||||
|
import axios from 'axios';
|
||||||
import { Field, Form, Formik } from 'formik';
|
import { Field, Form, Formik } from 'formik';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
@@ -76,9 +77,11 @@ const CreateSlider = ({ onCreate, slider }: CreateSliderProps) => {
|
|||||||
|
|
||||||
const keywords = await Promise.all(
|
const keywords = await Promise.all(
|
||||||
slider.data.split(',').map(async (keywordId) => {
|
slider.data.split(',').map(async (keywordId) => {
|
||||||
const res = await fetch(`/api/v1/keyword/${keywordId}`);
|
const keyword = await axios.get<Keyword>(
|
||||||
const keyword: Keyword = await res.json();
|
`/api/v1/keyword/${keywordId}`
|
||||||
return keyword;
|
);
|
||||||
|
|
||||||
|
return keyword.data;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -95,13 +98,15 @@ const CreateSlider = ({ onCreate, slider }: CreateSliderProps) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch(
|
const response = await axios.get<TmdbGenre[]>(
|
||||||
`/api/v1/genres/${
|
`/api/v1/genres/${
|
||||||
slider.type === DiscoverSliderType.TMDB_MOVIE_GENRE ? 'movie' : 'tv'
|
slider.type === DiscoverSliderType.TMDB_MOVIE_GENRE ? 'movie' : 'tv'
|
||||||
}`
|
}`
|
||||||
);
|
);
|
||||||
const genres: TmdbGenre[] = await res.json();
|
|
||||||
const genre = genres.find((genre) => genre.id === Number(slider.data));
|
const genre = response.data.find(
|
||||||
|
(genre) => genre.id === Number(slider.data)
|
||||||
|
);
|
||||||
|
|
||||||
setDefaultDataValue([
|
setDefaultDataValue([
|
||||||
{
|
{
|
||||||
@@ -116,8 +121,11 @@ const CreateSlider = ({ onCreate, slider }: CreateSliderProps) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch(`/api/v1/studio/${slider.data}`);
|
const response = await axios.get<ProductionCompany>(
|
||||||
const studio: ProductionCompany = await res.json();
|
`/api/v1/studio/${slider.data}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const studio = response.data;
|
||||||
|
|
||||||
setDefaultDataValue([
|
setDefaultDataValue([
|
||||||
{
|
{
|
||||||
@@ -160,17 +168,16 @@ const CreateSlider = ({ onCreate, slider }: CreateSliderProps) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const loadKeywordOptions = async (inputValue: string) => {
|
const loadKeywordOptions = async (inputValue: string) => {
|
||||||
const res = await fetch(
|
const results = await axios.get<TmdbKeywordSearchResponse>(
|
||||||
`/api/v1/search/keyword?query=${encodeURIExtraParams(inputValue)}`,
|
'/api/v1/search/keyword',
|
||||||
{
|
{
|
||||||
headers: {
|
params: {
|
||||||
'Content-Type': 'application/json',
|
query: encodeURIExtraParams(inputValue),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const results: TmdbKeywordSearchResponse = await res.json();
|
|
||||||
|
|
||||||
return results.results.map((result) => ({
|
return results.data.results.map((result) => ({
|
||||||
label: result.name,
|
label: result.name,
|
||||||
value: result.id,
|
value: result.id,
|
||||||
}));
|
}));
|
||||||
@@ -181,37 +188,38 @@ const CreateSlider = ({ onCreate, slider }: CreateSliderProps) => {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch(
|
const results = await axios.get<TmdbCompanySearchResponse>(
|
||||||
`/api/v1/search/company?query=${encodeURIExtraParams(inputValue)}`,
|
'/api/v1/search/company',
|
||||||
{
|
{
|
||||||
headers: {
|
params: {
|
||||||
'Content-Type': 'application/json',
|
query: encodeURIExtraParams(inputValue),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const results: TmdbCompanySearchResponse = await res.json();
|
|
||||||
|
|
||||||
return results.results.map((result) => ({
|
return results.data.results.map((result) => ({
|
||||||
label: result.name,
|
label: result.name,
|
||||||
value: result.id,
|
value: result.id,
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadMovieGenreOptions = async () => {
|
const loadMovieGenreOptions = async () => {
|
||||||
const res = await fetch('/api/v1/discover/genreslider/movie');
|
const results = await axios.get<GenreSliderItem[]>(
|
||||||
const results: GenreSliderItem[] = await res.json();
|
'/api/v1/discover/genreslider/movie'
|
||||||
|
);
|
||||||
|
|
||||||
return results.map((result) => ({
|
return results.data.map((result) => ({
|
||||||
label: result.name,
|
label: result.name,
|
||||||
value: result.id,
|
value: result.id,
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadTvGenreOptions = async () => {
|
const loadTvGenreOptions = async () => {
|
||||||
const res = await fetch('/api/v1/discover/genreslider/tv');
|
const results = await axios.get<GenreSliderItem[]>(
|
||||||
const results: GenreSliderItem[] = await res.json();
|
'/api/v1/discover/genreslider/tv'
|
||||||
|
);
|
||||||
|
|
||||||
return results.map((result) => ({
|
return results.data.map((result) => ({
|
||||||
label: result.name,
|
label: result.name,
|
||||||
value: result.id,
|
value: result.id,
|
||||||
}));
|
}));
|
||||||
@@ -306,31 +314,17 @@ const CreateSlider = ({ onCreate, slider }: CreateSliderProps) => {
|
|||||||
onSubmit={async (values, { resetForm }) => {
|
onSubmit={async (values, { resetForm }) => {
|
||||||
try {
|
try {
|
||||||
if (slider) {
|
if (slider) {
|
||||||
const res = await fetch(`/api/v1/settings/discover/${slider.id}`, {
|
await axios.put(`/api/v1/settings/discover/${slider.id}`, {
|
||||||
method: 'PUT',
|
type: Number(values.sliderType),
|
||||||
headers: {
|
title: values.title,
|
||||||
'Content-Type': 'application/json',
|
data: values.data,
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
type: Number(values.sliderType),
|
|
||||||
title: values.title,
|
|
||||||
data: values.data,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
} else {
|
} else {
|
||||||
const res = await fetch('/api/v1/settings/discover/add', {
|
await axios.post('/api/v1/settings/discover/add', {
|
||||||
method: 'POST',
|
type: Number(values.sliderType),
|
||||||
headers: {
|
title: values.title,
|
||||||
'Content-Type': 'application/json',
|
data: values.data,
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
type: Number(values.sliderType),
|
|
||||||
title: values.title,
|
|
||||||
data: values.data,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addToast(
|
addToast(
|
||||||
|
|||||||
@@ -48,11 +48,11 @@ const DiscoverTvNetwork = () => {
|
|||||||
<div className="mt-1 mb-5">
|
<div className="mt-1 mb-5">
|
||||||
<Header>
|
<Header>
|
||||||
{firstResultData?.network.logoPath ? (
|
{firstResultData?.network.logoPath ? (
|
||||||
<div className="relative mb-6 flex h-24 justify-center sm:h-32">
|
<div className="mb-6 flex justify-center">
|
||||||
<Image
|
<Image
|
||||||
src={`https://image.tmdb.org/t/p/w780_filter(duotone,ffffff,bababa)${firstResultData.network.logoPath}`}
|
src={`//image.tmdb.org/t/p/w780_filter(duotone,ffffff,bababa)${firstResultData.network.logoPath}`}
|
||||||
alt={firstResultData.network.name}
|
alt={firstResultData.network.name}
|
||||||
className="object-contain"
|
className="max-h-24 sm:max-h-32"
|
||||||
fill
|
fill
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
} from '@heroicons/react/24/solid';
|
} from '@heroicons/react/24/solid';
|
||||||
import { DiscoverSliderType } from '@server/constants/discover';
|
import { DiscoverSliderType } from '@server/constants/discover';
|
||||||
import type DiscoverSlider from '@server/entity/DiscoverSlider';
|
import type DiscoverSlider from '@server/entity/DiscoverSlider';
|
||||||
|
import axios from 'axios';
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { useDrag, useDrop } from 'react-aria';
|
import { useDrag, useDrop } from 'react-aria';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
@@ -77,10 +78,7 @@ const DiscoverSliderEdit = ({
|
|||||||
|
|
||||||
const deleteSlider = async () => {
|
const deleteSlider = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/v1/settings/discover/${slider.id}`, {
|
await axios.delete(`/api/v1/settings/discover/${slider.id}`);
|
||||||
method: 'DELETE',
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
addToast(intl.formatMessage(messages.deletesuccess), {
|
addToast(intl.formatMessage(messages.deletesuccess), {
|
||||||
appearance: 'success',
|
appearance: 'success',
|
||||||
autoDismiss: true,
|
autoDismiss: true,
|
||||||
|
|||||||
@@ -48,11 +48,11 @@ const DiscoverMovieStudio = () => {
|
|||||||
<div className="mt-1 mb-5">
|
<div className="mt-1 mb-5">
|
||||||
<Header>
|
<Header>
|
||||||
{firstResultData?.studio.logoPath ? (
|
{firstResultData?.studio.logoPath ? (
|
||||||
<div className="relative mb-6 flex h-24 justify-center sm:h-32">
|
<div className="mb-6 flex justify-center">
|
||||||
<Image
|
<Image
|
||||||
src={`https://image.tmdb.org/t/p/w780_filter(duotone,ffffff,bababa)${firstResultData.studio.logoPath}`}
|
src={`//image.tmdb.org/t/p/w780_filter(duotone,ffffff,bababa)${firstResultData.studio.logoPath}`}
|
||||||
alt={firstResultData.studio.name}
|
alt={firstResultData.studio.name}
|
||||||
className="object-contain"
|
className="max-h-24 sm:max-h-32"
|
||||||
fill
|
fill
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
} from '@heroicons/react/24/solid';
|
} from '@heroicons/react/24/solid';
|
||||||
import { DiscoverSliderType } from '@server/constants/discover';
|
import { DiscoverSliderType } from '@server/constants/discover';
|
||||||
import type DiscoverSlider from '@server/entity/DiscoverSlider';
|
import type DiscoverSlider from '@server/entity/DiscoverSlider';
|
||||||
|
import axios from 'axios';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import { useToasts } from 'react-toast-notifications';
|
import { useToasts } from 'react-toast-notifications';
|
||||||
@@ -75,14 +76,7 @@ const Discover = () => {
|
|||||||
|
|
||||||
const updateSliders = async () => {
|
const updateSliders = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/v1/settings/discover', {
|
await axios.post('/api/v1/settings/discover', sliders);
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(sliders),
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
|
|
||||||
addToast(intl.formatMessage(messages.updatesuccess), {
|
addToast(intl.formatMessage(messages.updatesuccess), {
|
||||||
appearance: 'success',
|
appearance: 'success',
|
||||||
@@ -100,10 +94,7 @@ const Discover = () => {
|
|||||||
|
|
||||||
const resetSliders = async () => {
|
const resetSliders = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/v1/settings/discover/reset', {
|
await axios.get('/api/v1/settings/discover/reset');
|
||||||
method: 'GET',
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
|
|
||||||
addToast(intl.formatMessage(messages.resetsuccess), {
|
addToast(intl.formatMessage(messages.resetsuccess), {
|
||||||
appearance: 'success',
|
appearance: 'success',
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import Button from '@app/components/Common/Button';
|
import Button from '@app/components/Common/Button';
|
||||||
import CachedImage from '@app/components/Common/CachedImage';
|
|
||||||
import Modal from '@app/components/Common/Modal';
|
import Modal from '@app/components/Common/Modal';
|
||||||
import { Permission, useUser } from '@app/hooks/useUser';
|
import { Permission, useUser } from '@app/hooks/useUser';
|
||||||
import defineMessages from '@app/utils/defineMessages';
|
import defineMessages from '@app/utils/defineMessages';
|
||||||
import { Menu, Transition } from '@headlessui/react';
|
import { Menu, Transition } from '@headlessui/react';
|
||||||
import { EllipsisVerticalIcon } from '@heroicons/react/24/solid';
|
import { EllipsisVerticalIcon } from '@heroicons/react/24/solid';
|
||||||
import type { default as IssueCommentType } from '@server/entity/IssueComment';
|
import type { default as IssueCommentType } from '@server/entity/IssueComment';
|
||||||
|
import axios from 'axios';
|
||||||
import { Field, Form, Formik } from 'formik';
|
import { Field, Form, Formik } from 'formik';
|
||||||
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Fragment, useState } from 'react';
|
import { Fragment, useState } from 'react';
|
||||||
import { FormattedRelativeTime, useIntl } from 'react-intl';
|
import { FormattedRelativeTime, useIntl } from 'react-intl';
|
||||||
@@ -48,10 +49,7 @@ const IssueComment = ({
|
|||||||
|
|
||||||
const deleteComment = async () => {
|
const deleteComment = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/v1/issueComment/${comment.id}`, {
|
await axios.delete(`/api/v1/issueComment/${comment.id}`);
|
||||||
method: 'DELETE',
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// something went wrong deleting the comment
|
// something went wrong deleting the comment
|
||||||
} finally {
|
} finally {
|
||||||
@@ -88,8 +86,8 @@ const IssueComment = ({
|
|||||||
</Modal>
|
</Modal>
|
||||||
</Transition>
|
</Transition>
|
||||||
<Link href={isActiveUser ? '/profile' : `/users/${comment.user.id}`}>
|
<Link href={isActiveUser ? '/profile' : `/users/${comment.user.id}`}>
|
||||||
<CachedImage
|
<Image
|
||||||
src={`/avatarproxy/${comment.user.avatar}`}
|
src={comment.user.avatar}
|
||||||
alt=""
|
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"
|
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}
|
width={40}
|
||||||
@@ -177,17 +175,9 @@ const IssueComment = ({
|
|||||||
<Formik
|
<Formik
|
||||||
initialValues={{ newMessage: comment.message }}
|
initialValues={{ newMessage: comment.message }}
|
||||||
onSubmit={async (values) => {
|
onSubmit={async (values) => {
|
||||||
const res = await fetch(
|
await axios.put(`/api/v1/issueComment/${comment.id}`, {
|
||||||
`/api/v1/issueComment/${comment.id}`,
|
message: values.newMessage,
|
||||||
{
|
});
|
||||||
method: 'PUT',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ message: values.newMessage }),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
|
|
||||||
if (onUpdate) {
|
if (onUpdate) {
|
||||||
onUpdate();
|
onUpdate();
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import useDeepLinks from '@app/hooks/useDeepLinks';
|
|||||||
import useSettings from '@app/hooks/useSettings';
|
import useSettings from '@app/hooks/useSettings';
|
||||||
import { Permission, useUser } from '@app/hooks/useUser';
|
import { Permission, useUser } from '@app/hooks/useUser';
|
||||||
import globalMessages from '@app/i18n/globalMessages';
|
import globalMessages from '@app/i18n/globalMessages';
|
||||||
import ErrorPage from '@app/pages/_error';
|
import Error from '@app/pages/_error';
|
||||||
import defineMessages from '@app/utils/defineMessages';
|
import defineMessages from '@app/utils/defineMessages';
|
||||||
import { Transition } from '@headlessui/react';
|
import { Transition } from '@headlessui/react';
|
||||||
import {
|
import {
|
||||||
@@ -27,8 +27,10 @@ import { MediaServerType } from '@server/constants/server';
|
|||||||
import type Issue from '@server/entity/Issue';
|
import type Issue from '@server/entity/Issue';
|
||||||
import type { MovieDetails } from '@server/models/Movie';
|
import type { MovieDetails } from '@server/models/Movie';
|
||||||
import type { TvDetails } from '@server/models/Tv';
|
import type { TvDetails } from '@server/models/Tv';
|
||||||
|
import axios from 'axios';
|
||||||
import { Field, Form, Formik } from 'formik';
|
import { Field, Form, Formik } from 'formik';
|
||||||
import getConfig from 'next/config';
|
import getConfig from 'next/config';
|
||||||
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
@@ -114,7 +116,7 @@ const IssueDetails = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!data || !issueData) {
|
if (!data || !issueData) {
|
||||||
return <ErrorPage statusCode={404} />;
|
return <Error statusCode={404} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const belongsToUser = issueData.createdBy.id === currentUser?.id;
|
const belongsToUser = issueData.createdBy.id === currentUser?.id;
|
||||||
@@ -123,14 +125,9 @@ const IssueDetails = () => {
|
|||||||
|
|
||||||
const editFirstComment = async (newMessage: string) => {
|
const editFirstComment = async (newMessage: string) => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/v1/issueComment/${firstComment.id}`, {
|
await axios.put(`/api/v1/issueComment/${firstComment.id}`, {
|
||||||
method: 'PUT',
|
message: newMessage,
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ message: newMessage }),
|
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
|
|
||||||
addToast(intl.formatMessage(messages.toasteditdescriptionsuccess), {
|
addToast(intl.formatMessage(messages.toasteditdescriptionsuccess), {
|
||||||
appearance: 'success',
|
appearance: 'success',
|
||||||
@@ -147,10 +144,7 @@ const IssueDetails = () => {
|
|||||||
|
|
||||||
const updateIssueStatus = async (newStatus: 'open' | 'resolved') => {
|
const updateIssueStatus = async (newStatus: 'open' | 'resolved') => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/v1/issue/${issueData.id}/${newStatus}`, {
|
await axios.post(`/api/v1/issue/${issueData.id}/${newStatus}`);
|
||||||
method: 'POST',
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
|
|
||||||
addToast(intl.formatMessage(messages.toaststatusupdated), {
|
addToast(intl.formatMessage(messages.toaststatusupdated), {
|
||||||
appearance: 'success',
|
appearance: 'success',
|
||||||
@@ -167,10 +161,7 @@ const IssueDetails = () => {
|
|||||||
|
|
||||||
const deleteIssue = async () => {
|
const deleteIssue = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/v1/issue/${issueData.id}`, {
|
await axios.delete(`/api/v1/issue/${issueData.id}`);
|
||||||
method: 'DELETE',
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
|
|
||||||
addToast(intl.formatMessage(messages.toastissuedeleted), {
|
addToast(intl.formatMessage(messages.toastissuedeleted), {
|
||||||
appearance: 'success',
|
appearance: 'success',
|
||||||
@@ -288,10 +279,10 @@ const IssueDetails = () => {
|
|||||||
}
|
}
|
||||||
className="group ml-1 inline-flex h-full items-center xl:ml-1.5"
|
className="group ml-1 inline-flex h-full items-center xl:ml-1.5"
|
||||||
>
|
>
|
||||||
<CachedImage
|
<Image
|
||||||
src={`/avatarproxy/${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"
|
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"
|
||||||
|
src={issueData.createdBy.avatar}
|
||||||
|
alt=""
|
||||||
width={20}
|
width={20}
|
||||||
height={20}
|
height={20}
|
||||||
/>
|
/>
|
||||||
@@ -499,17 +490,9 @@ const IssueDetails = () => {
|
|||||||
}}
|
}}
|
||||||
validationSchema={CommentSchema}
|
validationSchema={CommentSchema}
|
||||||
onSubmit={async (values, { resetForm }) => {
|
onSubmit={async (values, { resetForm }) => {
|
||||||
const res = await fetch(
|
await axios.post(`/api/v1/issue/${issueData?.id}/comment`, {
|
||||||
`/api/v1/issue/${issueData?.id}/comment`,
|
message: values.message,
|
||||||
{
|
});
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ message: values.message }),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
revalidateIssue();
|
revalidateIssue();
|
||||||
resetForm();
|
resetForm();
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { MediaStatus } from '@server/constants/media';
|
|||||||
import type Issue from '@server/entity/Issue';
|
import type Issue from '@server/entity/Issue';
|
||||||
import type { MovieDetails } from '@server/models/Movie';
|
import type { MovieDetails } from '@server/models/Movie';
|
||||||
import type { TvDetails } from '@server/models/Tv';
|
import type { TvDetails } from '@server/models/Tv';
|
||||||
|
import axios from 'axios';
|
||||||
import { Field, Formik } from 'formik';
|
import { Field, Formik } from 'formik';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
@@ -100,22 +101,14 @@ const CreateIssueModal = ({
|
|||||||
validationSchema={CreateIssueModalSchema}
|
validationSchema={CreateIssueModalSchema}
|
||||||
onSubmit={async (values) => {
|
onSubmit={async (values) => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/v1/issue', {
|
const newIssue = await axios.post<Issue>('/api/v1/issue', {
|
||||||
method: 'POST',
|
issueType: values.selectedIssue.issueType,
|
||||||
headers: {
|
message: values.message,
|
||||||
'Content-Type': 'application/json',
|
mediaId: data?.mediaInfo?.id,
|
||||||
},
|
problemSeason: values.problemSeason,
|
||||||
body: JSON.stringify({
|
problemEpisode:
|
||||||
issueType: values.selectedIssue.issueType,
|
values.problemSeason > 0 ? values.problemEpisode : 0,
|
||||||
message: values.message,
|
|
||||||
mediaId: data?.mediaInfo?.id,
|
|
||||||
problemSeason: values.problemSeason,
|
|
||||||
problemEpisode:
|
|
||||||
values.problemSeason > 0 ? values.problemEpisode : 0,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
const newIssue: Issue = await res.json();
|
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
addToast(
|
addToast(
|
||||||
@@ -126,7 +119,7 @@ const CreateIssueModal = ({
|
|||||||
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
|
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<Link href={`/issues/${newIssue.id}`} legacyBehavior>
|
<Link href={`/issues/${newIssue.data.id}`} legacyBehavior>
|
||||||
<Button as="a" className="mt-4">
|
<Button as="a" className="mt-4">
|
||||||
<span>{intl.formatMessage(messages.toastviewissue)}</span>
|
<span>{intl.formatMessage(messages.toastviewissue)}</span>
|
||||||
<ArrowRightCircleIcon />
|
<ArrowRightCircleIcon />
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import CachedImage from '@app/components/Common/CachedImage';
|
|
||||||
import MiniQuotaDisplay from '@app/components/Layout/UserDropdown/MiniQuotaDisplay';
|
import MiniQuotaDisplay from '@app/components/Layout/UserDropdown/MiniQuotaDisplay';
|
||||||
import { useUser } from '@app/hooks/useUser';
|
import { useUser } from '@app/hooks/useUser';
|
||||||
import defineMessages from '@app/utils/defineMessages';
|
import defineMessages from '@app/utils/defineMessages';
|
||||||
@@ -8,6 +7,8 @@ import {
|
|||||||
ClockIcon,
|
ClockIcon,
|
||||||
} from '@heroicons/react/24/outline';
|
} from '@heroicons/react/24/outline';
|
||||||
import { CogIcon, UserIcon } from '@heroicons/react/24/solid';
|
import { CogIcon, UserIcon } from '@heroicons/react/24/solid';
|
||||||
|
import axios from 'axios';
|
||||||
|
import Image from 'next/image';
|
||||||
import type { LinkProps } from 'next/link';
|
import type { LinkProps } from 'next/link';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { forwardRef, Fragment } from 'react';
|
import { forwardRef, Fragment } from 'react';
|
||||||
@@ -38,13 +39,9 @@ const UserDropdown = () => {
|
|||||||
const { user, revalidate } = useUser();
|
const { user, revalidate } = useUser();
|
||||||
|
|
||||||
const logout = async () => {
|
const logout = async () => {
|
||||||
const res = await fetch('/api/v1/auth/logout', {
|
const response = await axios.post('/api/v1/auth/logout');
|
||||||
method: 'POST',
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
const data = await res.json();
|
|
||||||
|
|
||||||
if (data?.status === 'ok') {
|
if (response.data?.status === 'ok') {
|
||||||
revalidate();
|
revalidate();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -56,9 +53,9 @@ const UserDropdown = () => {
|
|||||||
className="flex max-w-xs items-center rounded-full text-sm ring-1 ring-gray-700 hover:ring-gray-500 focus:outline-none focus:ring-gray-500"
|
className="flex max-w-xs items-center rounded-full text-sm ring-1 ring-gray-700 hover:ring-gray-500 focus:outline-none focus:ring-gray-500"
|
||||||
data-testid="user-menu"
|
data-testid="user-menu"
|
||||||
>
|
>
|
||||||
<CachedImage
|
<Image
|
||||||
className="h-8 w-8 rounded-full object-cover sm:h-10 sm:w-10"
|
className="h-8 w-8 rounded-full object-cover sm:h-10 sm:w-10"
|
||||||
src={user && user.avatar ? `/avatarproxy/${user.avatar}` : ''}
|
src={user?.avatar || ''}
|
||||||
alt=""
|
alt=""
|
||||||
width={40}
|
width={40}
|
||||||
height={40}
|
height={40}
|
||||||
@@ -79,9 +76,9 @@ const UserDropdown = () => {
|
|||||||
<div className="divide-y divide-gray-700 rounded-md bg-gray-800 bg-opacity-80 ring-1 ring-gray-700 backdrop-blur">
|
<div className="divide-y divide-gray-700 rounded-md bg-gray-800 bg-opacity-80 ring-1 ring-gray-700 backdrop-blur">
|
||||||
<div className="flex flex-col space-y-4 px-4 py-4">
|
<div className="flex flex-col space-y-4 px-4 py-4">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<CachedImage
|
<Image
|
||||||
className="h-8 w-8 rounded-full object-cover sm:h-10 sm:w-10"
|
className="h-8 w-8 rounded-full object-cover sm:h-10 sm:w-10"
|
||||||
src={user && user.avatar ? `/avatarproxy/${user.avatar}` : ''}
|
src={user?.avatar || ''}
|
||||||
alt=""
|
alt=""
|
||||||
width={40}
|
width={40}
|
||||||
height={40}
|
height={40}
|
||||||
@@ -90,11 +87,9 @@ const UserDropdown = () => {
|
|||||||
<span className="truncate text-xl font-semibold text-gray-200">
|
<span className="truncate text-xl font-semibold text-gray-200">
|
||||||
{user?.displayName}
|
{user?.displayName}
|
||||||
</span>
|
</span>
|
||||||
{user?.displayName?.toLowerCase() !== user?.email && (
|
<span className="truncate text-sm text-gray-400">
|
||||||
<span className="truncate text-sm text-gray-400">
|
{user?.email}
|
||||||
{user?.email}
|
</span>
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{user && <MiniQuotaDisplay userId={user?.id} />}
|
{user && <MiniQuotaDisplay userId={user?.id} />}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import Modal from '@app/components/Common/Modal';
|
|||||||
import useSettings from '@app/hooks/useSettings';
|
import useSettings from '@app/hooks/useSettings';
|
||||||
import defineMessages from '@app/utils/defineMessages';
|
import defineMessages from '@app/utils/defineMessages';
|
||||||
import { Transition } from '@headlessui/react';
|
import { Transition } from '@headlessui/react';
|
||||||
|
import axios from 'axios';
|
||||||
import { Field, Formik } from 'formik';
|
import { Field, Formik } from 'formik';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
@@ -57,18 +58,11 @@ const AddEmailModal: React.FC<AddEmailModalProps> = ({
|
|||||||
validationSchema={EmailSettingsSchema}
|
validationSchema={EmailSettingsSchema}
|
||||||
onSubmit={async (values) => {
|
onSubmit={async (values) => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/v1/auth/jellyfin', {
|
await axios.post('/api/v1/auth/jellyfin', {
|
||||||
method: 'POST',
|
username: username,
|
||||||
headers: {
|
password: password,
|
||||||
'Content-Type': 'application/json',
|
email: values.email,
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
username: username,
|
|
||||||
password: password,
|
|
||||||
email: values.email,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
|
|
||||||
onSave();
|
onSave();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import useSettings from '@app/hooks/useSettings';
|
|||||||
import defineMessages from '@app/utils/defineMessages';
|
import defineMessages from '@app/utils/defineMessages';
|
||||||
import { InformationCircleIcon } from '@heroicons/react/24/solid';
|
import { InformationCircleIcon } from '@heroicons/react/24/solid';
|
||||||
import { ApiErrorCode } from '@server/constants/error';
|
import { ApiErrorCode } from '@server/constants/error';
|
||||||
|
import axios from 'axios';
|
||||||
import { Field, Form, Formik } from 'formik';
|
import { Field, Form, Formik } from 'formik';
|
||||||
import getConfig from 'next/config';
|
import getConfig from 'next/config';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
@@ -104,32 +105,18 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
|
|||||||
validationSchema={LoginSchema}
|
validationSchema={LoginSchema}
|
||||||
onSubmit={async (values) => {
|
onSubmit={async (values) => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/v1/auth/jellyfin', {
|
await axios.post('/api/v1/auth/jellyfin', {
|
||||||
method: 'POST',
|
username: values.username,
|
||||||
headers: {
|
password: values.password,
|
||||||
'Content-Type': 'application/json',
|
hostname: values.hostname,
|
||||||
},
|
port: values.port,
|
||||||
body: JSON.stringify({
|
useSsl: values.useSsl,
|
||||||
username: values.username,
|
urlBase: values.urlBase,
|
||||||
password: values.password,
|
email: values.email,
|
||||||
hostname: values.hostname,
|
|
||||||
port: values.port,
|
|
||||||
useSsl: values.useSsl,
|
|
||||||
urlBase: values.urlBase,
|
|
||||||
email: values.email,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error(res.statusText, { cause: res });
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
let errorData;
|
|
||||||
try {
|
|
||||||
errorData = await e.cause?.text();
|
|
||||||
errorData = JSON.parse(errorData);
|
|
||||||
} catch {
|
|
||||||
/* empty */
|
|
||||||
}
|
|
||||||
let errorMessage = null;
|
let errorMessage = null;
|
||||||
switch (errorData?.message) {
|
switch (e.response?.data?.message) {
|
||||||
case ApiErrorCode.InvalidUrl:
|
case ApiErrorCode.InvalidUrl:
|
||||||
errorMessage = messages.invalidurlerror;
|
errorMessage = messages.invalidurlerror;
|
||||||
break;
|
break;
|
||||||
@@ -352,18 +339,11 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
|
|||||||
validationSchema={LoginSchema}
|
validationSchema={LoginSchema}
|
||||||
onSubmit={async (values) => {
|
onSubmit={async (values) => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/v1/auth/jellyfin', {
|
await axios.post('/api/v1/auth/jellyfin', {
|
||||||
method: 'POST',
|
username: values.username,
|
||||||
headers: {
|
password: values.password,
|
||||||
'Content-Type': 'application/json',
|
email: values.username,
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
username: values.username,
|
|
||||||
password: values.password,
|
|
||||||
email: values.username,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toasts.addToast(
|
toasts.addToast(
|
||||||
intl.formatMessage(
|
intl.formatMessage(
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
ArrowLeftOnRectangleIcon,
|
ArrowLeftOnRectangleIcon,
|
||||||
LifebuoyIcon,
|
LifebuoyIcon,
|
||||||
} from '@heroicons/react/24/outline';
|
} from '@heroicons/react/24/outline';
|
||||||
|
import axios from 'axios';
|
||||||
import { Field, Form, Formik } from 'formik';
|
import { Field, Form, Formik } from 'formik';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
@@ -55,17 +56,10 @@ const LocalLogin = ({ revalidate }: LocalLoginProps) => {
|
|||||||
validationSchema={LoginSchema}
|
validationSchema={LoginSchema}
|
||||||
onSubmit={async (values) => {
|
onSubmit={async (values) => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/v1/auth/local', {
|
await axios.post('/api/v1/auth/local', {
|
||||||
method: 'POST',
|
email: values.email,
|
||||||
headers: {
|
password: values.password,
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
email: values.email,
|
|
||||||
password: values.password,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setLoginError(intl.formatMessage(messages.loginerror));
|
setLoginError(intl.formatMessage(messages.loginerror));
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import defineMessages from '@app/utils/defineMessages';
|
|||||||
import { Transition } from '@headlessui/react';
|
import { Transition } from '@headlessui/react';
|
||||||
import { XCircleIcon } from '@heroicons/react/24/solid';
|
import { XCircleIcon } from '@heroicons/react/24/solid';
|
||||||
import { MediaServerType } from '@server/constants/server';
|
import { MediaServerType } from '@server/constants/server';
|
||||||
|
import axios from 'axios';
|
||||||
import getConfig from 'next/config';
|
import getConfig from 'next/config';
|
||||||
import { useRouter } from 'next/dist/client/router';
|
import { useRouter } from 'next/dist/client/router';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
@@ -43,28 +44,13 @@ const Login = () => {
|
|||||||
const login = async () => {
|
const login = async () => {
|
||||||
setProcessing(true);
|
setProcessing(true);
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/v1/auth/plex', {
|
const response = await axios.post('/api/v1/auth/plex', { authToken });
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ authToken }),
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error(res.statusText, { cause: res });
|
|
||||||
const data = await res.json();
|
|
||||||
|
|
||||||
if (data?.id) {
|
if (response.data?.id) {
|
||||||
revalidate();
|
revalidate();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
let errorData;
|
setError(e.response.data.message);
|
||||||
try {
|
|
||||||
errorData = await e.cause?.text();
|
|
||||||
errorData = JSON.parse(errorData);
|
|
||||||
} catch {
|
|
||||||
/* empty */
|
|
||||||
}
|
|
||||||
setError(errorData?.message);
|
|
||||||
setAuthToken(undefined);
|
setAuthToken(undefined);
|
||||||
setProcessing(false);
|
setProcessing(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import Button from '@app/components/Common/Button';
|
import Button from '@app/components/Common/Button';
|
||||||
import CachedImage from '@app/components/Common/CachedImage';
|
|
||||||
import ConfirmButton from '@app/components/Common/ConfirmButton';
|
import ConfirmButton from '@app/components/Common/ConfirmButton';
|
||||||
import SlideOver from '@app/components/Common/SlideOver';
|
import SlideOver from '@app/components/Common/SlideOver';
|
||||||
import Tooltip from '@app/components/Common/Tooltip';
|
import Tooltip from '@app/components/Common/Tooltip';
|
||||||
@@ -27,7 +26,9 @@ import type { MediaWatchDataResponse } from '@server/interfaces/api/mediaInterfa
|
|||||||
import type { RadarrSettings, SonarrSettings } from '@server/lib/settings';
|
import type { RadarrSettings, SonarrSettings } from '@server/lib/settings';
|
||||||
import type { MovieDetails } from '@server/models/Movie';
|
import type { MovieDetails } from '@server/models/Movie';
|
||||||
import type { TvDetails } from '@server/models/Tv';
|
import type { TvDetails } from '@server/models/Tv';
|
||||||
|
import axios from 'axios';
|
||||||
import getConfig from 'next/config';
|
import getConfig from 'next/config';
|
||||||
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
@@ -112,29 +113,16 @@ const ManageSlideOver = ({
|
|||||||
|
|
||||||
const deleteMedia = async () => {
|
const deleteMedia = async () => {
|
||||||
if (data.mediaInfo) {
|
if (data.mediaInfo) {
|
||||||
const res = await fetch(`/api/v1/media/${data.mediaInfo.id}`, {
|
await axios.delete(`/api/v1/media/${data.mediaInfo.id}`);
|
||||||
method: 'DELETE',
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
revalidate();
|
revalidate();
|
||||||
onClose();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteMediaFile = async () => {
|
const deleteMediaFile = async () => {
|
||||||
if (data.mediaInfo) {
|
if (data.mediaInfo) {
|
||||||
const res1 = await fetch(`/api/v1/media/${data.mediaInfo.id}/file`, {
|
await axios.delete(`/api/v1/media/${data.mediaInfo.id}/file`);
|
||||||
method: 'DELETE',
|
await axios.delete(`/api/v1/media/${data.mediaInfo.id}`);
|
||||||
});
|
|
||||||
if (!res1.ok) throw new Error();
|
|
||||||
|
|
||||||
const res2 = await fetch(`/api/v1/media/${data.mediaInfo.id}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
});
|
|
||||||
if (!res2.ok) throw new Error();
|
|
||||||
|
|
||||||
revalidate();
|
revalidate();
|
||||||
onClose();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -161,16 +149,9 @@ const ManageSlideOver = ({
|
|||||||
|
|
||||||
const markAvailable = async (is4k = false) => {
|
const markAvailable = async (is4k = false) => {
|
||||||
if (data.mediaInfo) {
|
if (data.mediaInfo) {
|
||||||
const res = await fetch(`/api/v1/media/${data.mediaInfo?.id}/available`, {
|
await axios.post(`/api/v1/media/${data.mediaInfo?.id}/available`, {
|
||||||
method: 'POST',
|
is4k,
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
is4k,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
revalidate();
|
revalidate();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -355,8 +336,8 @@ const ManageSlideOver = ({
|
|||||||
key={`watch-user-${user.id}`}
|
key={`watch-user-${user.id}`}
|
||||||
content={user.displayName}
|
content={user.displayName}
|
||||||
>
|
>
|
||||||
<CachedImage
|
<Image
|
||||||
src={`/avatarproxy/${user.avatar}`}
|
src={user.avatar}
|
||||||
alt={user.displayName}
|
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"
|
className="h-8 w-8 scale-100 transform-gpu rounded-full object-cover ring-1 ring-gray-500 transition duration-300 hover:scale-105"
|
||||||
width={32}
|
width={32}
|
||||||
@@ -516,8 +497,8 @@ const ManageSlideOver = ({
|
|||||||
key={`watch-user-${user.id}`}
|
key={`watch-user-${user.id}`}
|
||||||
content={user.displayName}
|
content={user.displayName}
|
||||||
>
|
>
|
||||||
<CachedImage
|
<Image
|
||||||
src={`/avatarproxy/${user.avatar}`}
|
src={user.avatar}
|
||||||
alt={user.displayName}
|
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"
|
className="h-8 w-8 scale-100 transform-gpu rounded-full object-cover ring-1 ring-gray-500 transition duration-300 hover:scale-105"
|
||||||
width={32}
|
width={32}
|
||||||
|
|||||||
@@ -57,48 +57,44 @@ const ShowMoreCard = ({ url, posters }: ShowMoreCardProps) => {
|
|||||||
>
|
>
|
||||||
<div style={{ paddingBottom: '150%' }}>
|
<div style={{ paddingBottom: '150%' }}>
|
||||||
<div className="absolute inset-0 flex h-full w-full flex-col items-center p-2">
|
<div className="absolute inset-0 flex h-full w-full flex-col items-center p-2">
|
||||||
<div className="relative z-10 grid h-full w-full grid-cols-2 items-center justify-center gap-2 opacity-30">
|
<div className="relative z-10 flex h-full flex-wrap items-center justify-center opacity-30">
|
||||||
{posters[0] && (
|
{posters[0] && (
|
||||||
<div className="">
|
<div className="w-1/2 p-1">
|
||||||
<Image
|
<Image
|
||||||
src={`https://image.tmdb.org/t/p/w300_and_h450_face${posters[0]}`}
|
src={`//image.tmdb.org/t/p/w300_and_h450_face${posters[0]}`}
|
||||||
alt=""
|
alt=""
|
||||||
className="rounded-md"
|
className="w-full rounded-md"
|
||||||
width={300}
|
fill
|
||||||
height={450}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{posters[1] && (
|
{posters[1] && (
|
||||||
<div className="">
|
<div className="w-1/2 p-1">
|
||||||
<Image
|
<Image
|
||||||
src={`https://image.tmdb.org/t/p/w300_and_h450_face${posters[1]}`}
|
src={`//image.tmdb.org/t/p/w300_and_h450_face${posters[1]}`}
|
||||||
alt=""
|
alt=""
|
||||||
className="rounded-md"
|
className="w-full rounded-md"
|
||||||
width={300}
|
fill
|
||||||
height={450}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{posters[2] && (
|
{posters[2] && (
|
||||||
<div className="">
|
<div className="w-1/2 p-1">
|
||||||
<Image
|
<Image
|
||||||
src={`https://image.tmdb.org/t/p/w300_and_h450_face${posters[2]}`}
|
src={`//image.tmdb.org/t/p/w300_and_h450_face${posters[2]}`}
|
||||||
alt=""
|
alt=""
|
||||||
className="rounded-md"
|
className="w-full rounded-md"
|
||||||
width={300}
|
fill
|
||||||
height={450}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{posters[3] && (
|
{posters[3] && (
|
||||||
<div className="">
|
<div className="w-1/2 p-1">
|
||||||
<Image
|
<Image
|
||||||
src={`https://image.tmdb.org/t/p/w300_and_h450_face${posters[3]}`}
|
src={`//image.tmdb.org/t/p/w300_and_h450_face${posters[3]}`}
|
||||||
alt=""
|
alt=""
|
||||||
className="rounded-md"
|
className="w-full rounded-md"
|
||||||
width={300}
|
fill
|
||||||
height={450}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import RTAudRotten from '@app/assets/rt_aud_rotten.svg';
|
|||||||
import RTFresh from '@app/assets/rt_fresh.svg';
|
import RTFresh from '@app/assets/rt_fresh.svg';
|
||||||
import RTRotten from '@app/assets/rt_rotten.svg';
|
import RTRotten from '@app/assets/rt_rotten.svg';
|
||||||
import ImdbLogo from '@app/assets/services/imdb.svg';
|
import ImdbLogo from '@app/assets/services/imdb.svg';
|
||||||
import Spinner from '@app/assets/spinner.svg';
|
|
||||||
import TmdbLogo from '@app/assets/tmdb_logo.svg';
|
import TmdbLogo from '@app/assets/tmdb_logo.svg';
|
||||||
import Button from '@app/components/Common/Button';
|
import Button from '@app/components/Common/Button';
|
||||||
import CachedImage from '@app/components/Common/CachedImage';
|
import CachedImage from '@app/components/Common/CachedImage';
|
||||||
@@ -26,7 +25,7 @@ import useLocale from '@app/hooks/useLocale';
|
|||||||
import useSettings from '@app/hooks/useSettings';
|
import useSettings from '@app/hooks/useSettings';
|
||||||
import { Permission, useUser } from '@app/hooks/useUser';
|
import { Permission, useUser } from '@app/hooks/useUser';
|
||||||
import globalMessages from '@app/i18n/globalMessages';
|
import globalMessages from '@app/i18n/globalMessages';
|
||||||
import ErrorPage from '@app/pages/_error';
|
import Error from '@app/pages/_error';
|
||||||
import { sortCrewPriority } from '@app/utils/creditHelpers';
|
import { sortCrewPriority } from '@app/utils/creditHelpers';
|
||||||
import defineMessages from '@app/utils/defineMessages';
|
import defineMessages from '@app/utils/defineMessages';
|
||||||
import { refreshIntervalHelper } from '@app/utils/refreshIntervalHelper';
|
import { refreshIntervalHelper } from '@app/utils/refreshIntervalHelper';
|
||||||
@@ -42,12 +41,10 @@ import {
|
|||||||
import {
|
import {
|
||||||
ChevronDoubleDownIcon,
|
ChevronDoubleDownIcon,
|
||||||
ChevronDoubleUpIcon,
|
ChevronDoubleUpIcon,
|
||||||
MinusCircleIcon,
|
|
||||||
StarIcon,
|
|
||||||
} from '@heroicons/react/24/solid';
|
} from '@heroicons/react/24/solid';
|
||||||
import { type RatingResponse } from '@server/api/ratings';
|
import { type RatingResponse } from '@server/api/ratings';
|
||||||
import { IssueStatus } from '@server/constants/issue';
|
import { IssueStatus } from '@server/constants/issue';
|
||||||
import { MediaStatus, MediaType } from '@server/constants/media';
|
import { MediaStatus } from '@server/constants/media';
|
||||||
import { MediaServerType } from '@server/constants/server';
|
import { MediaServerType } from '@server/constants/server';
|
||||||
import type { MovieDetails as MovieDetailsType } from '@server/models/Movie';
|
import type { MovieDetails as MovieDetailsType } from '@server/models/Movie';
|
||||||
import { countries } from 'country-flag-icons';
|
import { countries } from 'country-flag-icons';
|
||||||
@@ -58,7 +55,6 @@ import Link from 'next/link';
|
|||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import { useToasts } from 'react-toast-notifications';
|
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
|
||||||
const messages = defineMessages('components.MovieDetails', {
|
const messages = defineMessages('components.MovieDetails', {
|
||||||
@@ -98,12 +94,6 @@ const messages = defineMessages('components.MovieDetails', {
|
|||||||
rtaudiencescore: 'Rotten Tomatoes Audience Score',
|
rtaudiencescore: 'Rotten Tomatoes Audience Score',
|
||||||
tmdbuserscore: 'TMDB User Score',
|
tmdbuserscore: 'TMDB User Score',
|
||||||
imdbuserscore: 'IMDB User Score',
|
imdbuserscore: 'IMDB User Score',
|
||||||
watchlistSuccess: '<strong>{title}</strong> added to watchlist successfully!',
|
|
||||||
watchlistDeleted:
|
|
||||||
'<strong>{title}</strong> Removed from watchlist successfully!',
|
|
||||||
watchlistError: 'Something went wrong try again.',
|
|
||||||
removefromwatchlist: 'Remove From Watchlist',
|
|
||||||
addtowatchlist: 'Add To Watchlist',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
interface MovieDetailsProps {
|
interface MovieDetailsProps {
|
||||||
@@ -122,12 +112,7 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
|
|||||||
const minStudios = 3;
|
const minStudios = 3;
|
||||||
const [showMoreStudios, setShowMoreStudios] = useState(false);
|
const [showMoreStudios, setShowMoreStudios] = useState(false);
|
||||||
const [showIssueModal, setShowIssueModal] = useState(false);
|
const [showIssueModal, setShowIssueModal] = useState(false);
|
||||||
const [isUpdating, setIsUpdating] = useState<boolean>(false);
|
|
||||||
const [toggleWatchlist, setToggleWatchlist] = useState<boolean>(
|
|
||||||
!movie?.onUserWatchlist
|
|
||||||
);
|
|
||||||
const { publicRuntimeConfig } = getConfig();
|
const { publicRuntimeConfig } = getConfig();
|
||||||
const { addToast } = useToasts();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
@@ -169,7 +154,7 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return <ErrorPage statusCode={404} />;
|
return <Error statusCode={404} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const showAllStudios = data.productionCompanies.length <= minStudios + 1;
|
const showAllStudios = data.productionCompanies.length <= minStudios + 1;
|
||||||
@@ -302,80 +287,6 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
|
|||||||
return intl.formatMessage(messages.play4k, { mediaServerName: 'Jellyfin' });
|
return intl.formatMessage(messages.play4k, { mediaServerName: 'Jellyfin' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const onClickWatchlistBtn = async (): Promise<void> => {
|
|
||||||
setIsUpdating(true);
|
|
||||||
|
|
||||||
const res = await fetch('/api/v1/watchlist', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
tmdbId: movie?.id,
|
|
||||||
mediaType: MediaType.MOVIE,
|
|
||||||
title: movie?.title,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
addToast(intl.formatMessage(messages.watchlistError), {
|
|
||||||
appearance: 'error',
|
|
||||||
autoDismiss: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
setIsUpdating(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await res.json();
|
|
||||||
|
|
||||||
if (data) {
|
|
||||||
addToast(
|
|
||||||
<span>
|
|
||||||
{intl.formatMessage(messages.watchlistSuccess, {
|
|
||||||
title: movie?.title,
|
|
||||||
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
|
|
||||||
})}
|
|
||||||
</span>,
|
|
||||||
{ appearance: 'success', autoDismiss: true }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsUpdating(false);
|
|
||||||
setToggleWatchlist((prevState) => !prevState);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onClickDeleteWatchlistBtn = async (): Promise<void> => {
|
|
||||||
setIsUpdating(true);
|
|
||||||
try {
|
|
||||||
const res = await fetch(`/api/v1/watchlist/${movie?.id}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
|
|
||||||
if (res.status === 204) {
|
|
||||||
addToast(
|
|
||||||
<span>
|
|
||||||
{intl.formatMessage(messages.watchlistDeleted, {
|
|
||||||
title: movie?.title,
|
|
||||||
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
|
|
||||||
})}
|
|
||||||
</span>,
|
|
||||||
{ appearance: 'info', autoDismiss: true }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
addToast(intl.formatMessage(messages.watchlistError), {
|
|
||||||
appearance: 'error',
|
|
||||||
autoDismiss: true,
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setIsUpdating(false);
|
|
||||||
setToggleWatchlist((prevState) => !prevState);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="media-page"
|
className="media-page"
|
||||||
@@ -497,40 +408,6 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="media-actions">
|
<div className="media-actions">
|
||||||
<>
|
|
||||||
{toggleWatchlist ? (
|
|
||||||
<Tooltip content={intl.formatMessage(messages.addtowatchlist)}>
|
|
||||||
<Button
|
|
||||||
buttonType={'ghost'}
|
|
||||||
className="z-40 mr-2"
|
|
||||||
buttonSize={'md'}
|
|
||||||
onClick={onClickWatchlistBtn}
|
|
||||||
>
|
|
||||||
{isUpdating ? (
|
|
||||||
<Spinner className="h-3" />
|
|
||||||
) : (
|
|
||||||
<StarIcon className={'h-3 text-amber-300'} />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
) : (
|
|
||||||
<Tooltip
|
|
||||||
content={intl.formatMessage(messages.removefromwatchlist)}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
className="z-40 mr-2"
|
|
||||||
buttonSize={'md'}
|
|
||||||
onClick={onClickDeleteWatchlistBtn}
|
|
||||||
>
|
|
||||||
{isUpdating ? (
|
|
||||||
<Spinner className="h-3" />
|
|
||||||
) : (
|
|
||||||
<MinusCircleIcon className={'h-3'} />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
<PlayButton links={mediaLinks} />
|
<PlayButton links={mediaLinks} />
|
||||||
<RequestButton
|
<RequestButton
|
||||||
mediaType="movie"
|
mediaType="movie"
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
} from '@heroicons/react/24/solid';
|
} from '@heroicons/react/24/solid';
|
||||||
import { MediaRequestStatus } from '@server/constants/media';
|
import { MediaRequestStatus } from '@server/constants/media';
|
||||||
import type { MediaRequest } from '@server/entity/MediaRequest';
|
import type { MediaRequest } from '@server/entity/MediaRequest';
|
||||||
|
import axios from 'axios';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
@@ -52,10 +53,7 @@ const RequestBlock = ({ request, onUpdate }: RequestBlockProps) => {
|
|||||||
|
|
||||||
const updateRequest = async (type: 'approve' | 'decline'): Promise<void> => {
|
const updateRequest = async (type: 'approve' | 'decline'): Promise<void> => {
|
||||||
setIsUpdating(true);
|
setIsUpdating(true);
|
||||||
const res = await fetch(`/api/v1/request/${request.id}/${type}`, {
|
await axios.post(`/api/v1/request/${request.id}/${type}`);
|
||||||
method: 'POST',
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
|
|
||||||
if (onUpdate) {
|
if (onUpdate) {
|
||||||
onUpdate();
|
onUpdate();
|
||||||
@@ -65,10 +63,7 @@ const RequestBlock = ({ request, onUpdate }: RequestBlockProps) => {
|
|||||||
|
|
||||||
const deleteRequest = async () => {
|
const deleteRequest = async () => {
|
||||||
setIsUpdating(true);
|
setIsUpdating(true);
|
||||||
const res = await fetch(`/api/v1/request/${request.id}`, {
|
await axios.delete(`/api/v1/request/${request.id}`);
|
||||||
method: 'DELETE',
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
|
|
||||||
if (onUpdate) {
|
if (onUpdate) {
|
||||||
onUpdate();
|
onUpdate();
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
import { MediaRequestStatus, MediaStatus } from '@server/constants/media';
|
import { MediaRequestStatus, MediaStatus } from '@server/constants/media';
|
||||||
import type Media from '@server/entity/Media';
|
import type Media from '@server/entity/Media';
|
||||||
import type { MediaRequest } from '@server/entity/MediaRequest';
|
import type { MediaRequest } from '@server/entity/MediaRequest';
|
||||||
|
import axios from 'axios';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
|
|
||||||
@@ -93,13 +94,9 @@ const RequestButton = ({
|
|||||||
request: MediaRequest,
|
request: MediaRequest,
|
||||||
type: 'approve' | 'decline'
|
type: 'approve' | 'decline'
|
||||||
) => {
|
) => {
|
||||||
const res = await fetch(`/api/v1/request/${request.id}/${type}`, {
|
const response = await axios.post(`/api/v1/request/${request.id}/${type}`);
|
||||||
method: 'POST',
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
const data = await res.json();
|
|
||||||
|
|
||||||
if (data) {
|
if (response) {
|
||||||
onUpdate();
|
onUpdate();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -114,11 +111,7 @@ const RequestButton = ({
|
|||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
requests.map(async (request) => {
|
requests.map(async (request) => {
|
||||||
const res = await fetch(`/api/v1/request/${request.id}/${type}`, {
|
return axios.post(`/api/v1/request/${request.id}/${type}`);
|
||||||
method: 'POST',
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
return res.json();
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -19,9 +19,10 @@ import {
|
|||||||
} from '@heroicons/react/24/solid';
|
} from '@heroicons/react/24/solid';
|
||||||
import { MediaRequestStatus } from '@server/constants/media';
|
import { MediaRequestStatus } from '@server/constants/media';
|
||||||
import type { MediaRequest } from '@server/entity/MediaRequest';
|
import type { MediaRequest } from '@server/entity/MediaRequest';
|
||||||
import type { NonFunctionProperties } from '@server/interfaces/api/common';
|
|
||||||
import type { MovieDetails } from '@server/models/Movie';
|
import type { MovieDetails } from '@server/models/Movie';
|
||||||
import type { TvDetails } from '@server/models/Tv';
|
import type { TvDetails } from '@server/models/Tv';
|
||||||
|
import axios from 'axios';
|
||||||
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useInView } from 'react-intersection-observer';
|
import { useInView } from 'react-intersection-observer';
|
||||||
@@ -58,7 +59,7 @@ const RequestCardPlaceholder = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface RequestCardErrorProps {
|
interface RequestCardErrorProps {
|
||||||
requestData?: NonFunctionProperties<MediaRequest>;
|
requestData?: MediaRequest;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RequestCardError = ({ requestData }: RequestCardErrorProps) => {
|
const RequestCardError = ({ requestData }: RequestCardErrorProps) => {
|
||||||
@@ -73,10 +74,7 @@ const RequestCardError = ({ requestData }: RequestCardErrorProps) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const deleteRequest = async () => {
|
const deleteRequest = async () => {
|
||||||
const res = await fetch(`/api/v1/media/${requestData?.media.id}`, {
|
await axios.delete(`/api/v1/media/${requestData?.media.id}`);
|
||||||
method: 'DELETE',
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
mutate('/api/v1/media?filter=allavailable&take=20&sort=mediaAdded');
|
mutate('/api/v1/media?filter=allavailable&take=20&sort=mediaAdded');
|
||||||
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
|
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
|
||||||
};
|
};
|
||||||
@@ -115,8 +113,8 @@ const RequestCardError = ({ requestData }: RequestCardErrorProps) => {
|
|||||||
className="group flex items-center"
|
className="group flex items-center"
|
||||||
>
|
>
|
||||||
<span className="avatar-sm">
|
<span className="avatar-sm">
|
||||||
<CachedImage
|
<Image
|
||||||
src={`/avatarproxy/${requestData.requestedBy.avatar}`}
|
src={requestData.requestedBy.avatar}
|
||||||
alt=""
|
alt=""
|
||||||
className="avatar-sm object-cover"
|
className="avatar-sm object-cover"
|
||||||
width={20}
|
width={20}
|
||||||
@@ -213,7 +211,7 @@ const RequestCardError = ({ requestData }: RequestCardErrorProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface RequestCardProps {
|
interface RequestCardProps {
|
||||||
request: NonFunctionProperties<MediaRequest>;
|
request: MediaRequest;
|
||||||
onTitleData?: (requestId: number, title: MovieDetails | TvDetails) => void;
|
onTitleData?: (requestId: number, title: MovieDetails | TvDetails) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,19 +236,16 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
|
|||||||
data: requestData,
|
data: requestData,
|
||||||
error: requestError,
|
error: requestError,
|
||||||
mutate: revalidate,
|
mutate: revalidate,
|
||||||
} = useSWR<NonFunctionProperties<MediaRequest>>(
|
} = useSWR<MediaRequest>(`/api/v1/request/${request.id}`, {
|
||||||
`/api/v1/request/${request.id}`,
|
fallbackData: request,
|
||||||
{
|
refreshInterval: refreshIntervalHelper(
|
||||||
fallbackData: request,
|
{
|
||||||
refreshInterval: refreshIntervalHelper(
|
downloadStatus: request.media.downloadStatus,
|
||||||
{
|
downloadStatus4k: request.media.downloadStatus4k,
|
||||||
downloadStatus: request.media.downloadStatus,
|
},
|
||||||
downloadStatus4k: request.media.downloadStatus4k,
|
15000
|
||||||
},
|
),
|
||||||
15000
|
});
|
||||||
),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const { mediaUrl: plexUrl, mediaUrl4k: plexUrl4k } = useDeepLinks({
|
const { mediaUrl: plexUrl, mediaUrl4k: plexUrl4k } = useDeepLinks({
|
||||||
mediaUrl: requestData?.media?.mediaUrl,
|
mediaUrl: requestData?.media?.mediaUrl,
|
||||||
@@ -260,22 +255,15 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const modifyRequest = async (type: 'approve' | 'decline') => {
|
const modifyRequest = async (type: 'approve' | 'decline') => {
|
||||||
const res = await fetch(`/api/v1/request/${request.id}/${type}`, {
|
const response = await axios.post(`/api/v1/request/${request.id}/${type}`);
|
||||||
method: 'POST',
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
const data = await res.json();
|
|
||||||
|
|
||||||
if (data) {
|
if (response) {
|
||||||
revalidate();
|
revalidate();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteRequest = async () => {
|
const deleteRequest = async () => {
|
||||||
const res = await fetch(`/api/v1/request/${request.id}`, {
|
await axios.delete(`/api/v1/request/${request.id}`);
|
||||||
method: 'DELETE',
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
|
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -283,13 +271,9 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
|
|||||||
setRetrying(true);
|
setRetrying(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/v1/request/${request.id}/retry`, {
|
const response = await axios.post(`/api/v1/request/${request.id}/retry`);
|
||||||
method: 'POST',
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
const data = await res.json();
|
|
||||||
|
|
||||||
if (data) {
|
if (response) {
|
||||||
revalidate();
|
revalidate();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -389,8 +373,8 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
|
|||||||
className="group flex items-center"
|
className="group flex items-center"
|
||||||
>
|
>
|
||||||
<span className="avatar-sm">
|
<span className="avatar-sm">
|
||||||
<CachedImage
|
<Image
|
||||||
src={`/avatarproxy/${requestData.requestedBy.avatar}`}
|
src={requestData.requestedBy.avatar}
|
||||||
alt=""
|
alt=""
|
||||||
className="avatar-sm object-cover"
|
className="avatar-sm object-cover"
|
||||||
width={20}
|
width={20}
|
||||||
|
|||||||
@@ -18,9 +18,10 @@ import {
|
|||||||
} from '@heroicons/react/24/solid';
|
} from '@heroicons/react/24/solid';
|
||||||
import { MediaRequestStatus } from '@server/constants/media';
|
import { MediaRequestStatus } from '@server/constants/media';
|
||||||
import type { MediaRequest } from '@server/entity/MediaRequest';
|
import type { MediaRequest } from '@server/entity/MediaRequest';
|
||||||
import type { NonFunctionProperties } from '@server/interfaces/api/common';
|
|
||||||
import type { MovieDetails } from '@server/models/Movie';
|
import type { MovieDetails } from '@server/models/Movie';
|
||||||
import type { TvDetails } from '@server/models/Tv';
|
import type { TvDetails } from '@server/models/Tv';
|
||||||
|
import axios from 'axios';
|
||||||
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useInView } from 'react-intersection-observer';
|
import { useInView } from 'react-intersection-observer';
|
||||||
@@ -42,7 +43,6 @@ const messages = defineMessages('components.RequestList.RequestItem', {
|
|||||||
tmdbid: 'TMDB ID',
|
tmdbid: 'TMDB ID',
|
||||||
tvdbid: 'TheTVDB ID',
|
tvdbid: 'TheTVDB ID',
|
||||||
unknowntitle: 'Unknown Title',
|
unknowntitle: 'Unknown Title',
|
||||||
profileName: 'Profile',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => {
|
const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => {
|
||||||
@@ -50,7 +50,7 @@ const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface RequestItemErrorProps {
|
interface RequestItemErrorProps {
|
||||||
requestData?: NonFunctionProperties<MediaRequest>;
|
requestData?: MediaRequest;
|
||||||
revalidateList: () => void;
|
revalidateList: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,10 +62,7 @@ const RequestItemError = ({
|
|||||||
const { hasPermission } = useUser();
|
const { hasPermission } = useUser();
|
||||||
|
|
||||||
const deleteRequest = async () => {
|
const deleteRequest = async () => {
|
||||||
const res = await fetch(`/api/v1/media/${requestData?.media.id}`, {
|
await axios.delete(`/api/v1/media/${requestData?.media.id}`);
|
||||||
method: 'DELETE',
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
revalidateList();
|
revalidateList();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -189,8 +186,8 @@ const RequestItemError = ({
|
|||||||
className="group flex items-center truncate"
|
className="group flex items-center truncate"
|
||||||
>
|
>
|
||||||
<span className="avatar-sm ml-1.5">
|
<span className="avatar-sm ml-1.5">
|
||||||
<CachedImage
|
<Image
|
||||||
src={`/avatarproxy/${requestData.requestedBy.avatar}`}
|
src={requestData.requestedBy.avatar}
|
||||||
alt=""
|
alt=""
|
||||||
className="avatar-sm object-cover"
|
className="avatar-sm object-cover"
|
||||||
width={20}
|
width={20}
|
||||||
@@ -248,8 +245,8 @@ const RequestItemError = ({
|
|||||||
className="group flex items-center truncate"
|
className="group flex items-center truncate"
|
||||||
>
|
>
|
||||||
<span className="avatar-sm ml-1.5">
|
<span className="avatar-sm ml-1.5">
|
||||||
<CachedImage
|
<Image
|
||||||
src={`/avatarproxy/${requestData.modifiedBy.avatar}`}
|
src={requestData.modifiedBy.avatar}
|
||||||
alt=""
|
alt=""
|
||||||
className="avatar-sm object-cover"
|
className="avatar-sm object-cover"
|
||||||
width={20}
|
width={20}
|
||||||
@@ -286,7 +283,7 @@ const RequestItemError = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface RequestItemProps {
|
interface RequestItemProps {
|
||||||
request: NonFunctionProperties<MediaRequest> & { profileName?: string };
|
request: MediaRequest;
|
||||||
revalidateList: () => void;
|
revalidateList: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,38 +302,32 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
|||||||
const { data: title, error } = useSWR<MovieDetails | TvDetails>(
|
const { data: title, error } = useSWR<MovieDetails | TvDetails>(
|
||||||
inView ? url : null
|
inView ? url : null
|
||||||
);
|
);
|
||||||
const { data: requestData, mutate: revalidate } = useSWR<
|
const { data: requestData, mutate: revalidate } = useSWR<MediaRequest>(
|
||||||
NonFunctionProperties<MediaRequest>
|
`/api/v1/request/${request.id}`,
|
||||||
>(`/api/v1/request/${request.id}`, {
|
{
|
||||||
fallbackData: request,
|
fallbackData: request,
|
||||||
refreshInterval: refreshIntervalHelper(
|
refreshInterval: refreshIntervalHelper(
|
||||||
{
|
{
|
||||||
downloadStatus: request.media.downloadStatus,
|
downloadStatus: request.media.downloadStatus,
|
||||||
downloadStatus4k: request.media.downloadStatus4k,
|
downloadStatus4k: request.media.downloadStatus4k,
|
||||||
},
|
},
|
||||||
15000
|
15000
|
||||||
),
|
),
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const [isRetrying, setRetrying] = useState(false);
|
const [isRetrying, setRetrying] = useState(false);
|
||||||
|
|
||||||
const modifyRequest = async (type: 'approve' | 'decline') => {
|
const modifyRequest = async (type: 'approve' | 'decline') => {
|
||||||
const res = await fetch(`/api/v1/request/${request.id}/${type}`, {
|
const response = await axios.post(`/api/v1/request/${request.id}/${type}`);
|
||||||
method: 'POST',
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
const data = await res.json();
|
|
||||||
|
|
||||||
if (data) {
|
if (response) {
|
||||||
revalidate();
|
revalidate();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteRequest = async () => {
|
const deleteRequest = async () => {
|
||||||
const res = await fetch(`/api/v1/request/${request.id}`, {
|
await axios.delete(`/api/v1/request/${request.id}`);
|
||||||
method: 'DELETE',
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
|
|
||||||
revalidateList();
|
revalidateList();
|
||||||
};
|
};
|
||||||
@@ -345,12 +336,7 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
|||||||
setRetrying(true);
|
setRetrying(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/v1/request/${request.id}/retry`, {
|
const result = await axios.post(`/api/v1/request/${request.id}/retry`);
|
||||||
method: 'POST',
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
const result = await res.json();
|
|
||||||
|
|
||||||
revalidate(result.data);
|
revalidate(result.data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
addToast(intl.formatMessage(messages.failedretry), {
|
addToast(intl.formatMessage(messages.failedretry), {
|
||||||
@@ -401,7 +387,7 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
|||||||
setShowEditModal(false);
|
setShowEditModal(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="relative flex w-full flex-col justify-between overflow-hidden rounded-xl bg-gray-800 py-2 text-gray-400 shadow-md ring-1 ring-gray-700 xl:h-28 xl:flex-row">
|
<div className="relative flex w-full flex-col justify-between overflow-hidden rounded-xl bg-gray-800 py-4 text-gray-400 shadow-md ring-1 ring-gray-700 xl:h-28 xl:flex-row">
|
||||||
{title.backdropPath && (
|
{title.backdropPath && (
|
||||||
<div className="absolute inset-0 z-0 w-full bg-cover bg-center xl:w-2/3">
|
<div className="absolute inset-0 z-0 w-full bg-cover bg-center xl:w-2/3">
|
||||||
<CachedImage
|
<CachedImage
|
||||||
@@ -482,7 +468,7 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="z-10 mt-4 ml-4 flex w-full flex-col justify-center gap-1 overflow-hidden pr-4 text-sm sm:ml-2 sm:mt-0 xl:flex-1 xl:pr-0">
|
<div className="z-10 mt-4 ml-4 flex w-full flex-col justify-center overflow-hidden pr-4 text-sm sm:ml-2 sm:mt-0 xl:flex-1 xl:pr-0">
|
||||||
<div className="card-field">
|
<div className="card-field">
|
||||||
<span className="card-field-name">
|
<span className="card-field-name">
|
||||||
{intl.formatMessage(globalMessages.status)}
|
{intl.formatMessage(globalMessages.status)}
|
||||||
@@ -556,8 +542,8 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
|||||||
className="group flex items-center truncate"
|
className="group flex items-center truncate"
|
||||||
>
|
>
|
||||||
<span className="avatar-sm ml-1.5">
|
<span className="avatar-sm ml-1.5">
|
||||||
<CachedImage
|
<Image
|
||||||
src={`/avatarproxy/${requestData.requestedBy.avatar}`}
|
src={requestData.requestedBy.avatar}
|
||||||
alt=""
|
alt=""
|
||||||
className="avatar-sm object-cover"
|
className="avatar-sm object-cover"
|
||||||
width={20}
|
width={20}
|
||||||
@@ -615,8 +601,8 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
|||||||
className="group flex items-center truncate"
|
className="group flex items-center truncate"
|
||||||
>
|
>
|
||||||
<span className="avatar-sm ml-1.5">
|
<span className="avatar-sm ml-1.5">
|
||||||
<CachedImage
|
<Image
|
||||||
src={`/avatarproxy/${requestData.requestedBy.avatar}`}
|
src={requestData.requestedBy.avatar}
|
||||||
alt=""
|
alt=""
|
||||||
className="avatar-sm object-cover"
|
className="avatar-sm object-cover"
|
||||||
width={20}
|
width={20}
|
||||||
@@ -632,16 +618,6 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{request.profileName && (
|
|
||||||
<div className="card-field">
|
|
||||||
<span className="card-field-name">
|
|
||||||
{intl.formatMessage(messages.profileName)}
|
|
||||||
</span>
|
|
||||||
<span className="flex truncate text-sm text-gray-300">
|
|
||||||
{request.profileName}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="z-10 mt-4 flex w-full flex-col justify-center space-y-2 pl-4 pr-4 xl:mt-0 xl:w-96 xl:items-end xl:pl-0">
|
<div className="z-10 mt-4 flex w-full flex-col justify-center space-y-2 pl-4 pr-4 xl:mt-0 xl:w-96 xl:items-end xl:pl-0">
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import CachedImage from '@app/components/Common/CachedImage';
|
|
||||||
import { SmallLoadingSpinner } from '@app/components/Common/LoadingSpinner';
|
import { SmallLoadingSpinner } from '@app/components/Common/LoadingSpinner';
|
||||||
import type { User } from '@app/hooks/useUser';
|
import type { User } from '@app/hooks/useUser';
|
||||||
import { Permission, useUser } from '@app/hooks/useUser';
|
import { Permission, useUser } from '@app/hooks/useUser';
|
||||||
@@ -15,6 +14,7 @@ import type {
|
|||||||
import type { UserResultsResponse } from '@server/interfaces/api/userInterfaces';
|
import type { UserResultsResponse } from '@server/interfaces/api/userInterfaces';
|
||||||
import { hasPermission } from '@server/lib/permissions';
|
import { hasPermission } from '@server/lib/permissions';
|
||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
|
import Image from 'next/image';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import Select from 'react-select';
|
import Select from 'react-select';
|
||||||
@@ -561,8 +561,8 @@ const AdvancedRequester = ({
|
|||||||
<span className="inline-block w-full rounded-md shadow-sm">
|
<span className="inline-block w-full rounded-md shadow-sm">
|
||||||
<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">
|
<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">
|
<span className="flex items-center">
|
||||||
<CachedImage
|
<Image
|
||||||
src={`/avatarproxy/${selectedUser.avatar}`}
|
src={selectedUser.avatar}
|
||||||
alt=""
|
alt=""
|
||||||
className="h-6 w-6 flex-shrink-0 rounded-full object-cover"
|
className="h-6 w-6 flex-shrink-0 rounded-full object-cover"
|
||||||
width={24}
|
width={24}
|
||||||
@@ -613,8 +613,8 @@ const AdvancedRequester = ({
|
|||||||
selected ? 'font-semibold' : 'font-normal'
|
selected ? 'font-semibold' : 'font-normal'
|
||||||
} flex items-center`}
|
} flex items-center`}
|
||||||
>
|
>
|
||||||
<CachedImage
|
<Image
|
||||||
src={`/avatarproxy/${user.avatar}`}
|
src={user.avatar}
|
||||||
alt=""
|
alt=""
|
||||||
className="h-6 w-6 flex-shrink-0 rounded-full object-cover"
|
className="h-6 w-6 flex-shrink-0 rounded-full object-cover"
|
||||||
width={24}
|
width={24}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import type { MediaRequest } from '@server/entity/MediaRequest';
|
|||||||
import type { QuotaResponse } from '@server/interfaces/api/userInterfaces';
|
import type { QuotaResponse } from '@server/interfaces/api/userInterfaces';
|
||||||
import { Permission } from '@server/lib/permissions';
|
import { Permission } from '@server/lib/permissions';
|
||||||
import type { Collection } from '@server/models/Collection';
|
import type { Collection } from '@server/models/Collection';
|
||||||
|
import axios from 'axios';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import { useToasts } from 'react-toast-notifications';
|
import { useToasts } from 'react-toast-notifications';
|
||||||
@@ -196,19 +197,12 @@ const CollectionRequestModal = ({
|
|||||||
(
|
(
|
||||||
data?.parts.filter((part) => selectedParts.includes(part.id)) ?? []
|
data?.parts.filter((part) => selectedParts.includes(part.id)) ?? []
|
||||||
).map(async (part) => {
|
).map(async (part) => {
|
||||||
const res = await fetch('/api/v1/request', {
|
await axios.post<MediaRequest>('/api/v1/request', {
|
||||||
method: 'POST',
|
mediaId: part.id,
|
||||||
headers: {
|
mediaType: 'movie',
|
||||||
'Content-Type': 'application/json',
|
is4k,
|
||||||
},
|
...overrideParams,
|
||||||
body: JSON.stringify({
|
|
||||||
mediaId: part.id,
|
|
||||||
mediaType: 'movie',
|
|
||||||
is4k,
|
|
||||||
...overrideParams,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ import globalMessages from '@app/i18n/globalMessages';
|
|||||||
import defineMessages from '@app/utils/defineMessages';
|
import defineMessages from '@app/utils/defineMessages';
|
||||||
import { MediaStatus } from '@server/constants/media';
|
import { MediaStatus } from '@server/constants/media';
|
||||||
import type { MediaRequest } from '@server/entity/MediaRequest';
|
import type { MediaRequest } from '@server/entity/MediaRequest';
|
||||||
import type { NonFunctionProperties } from '@server/interfaces/api/common';
|
|
||||||
import type { QuotaResponse } from '@server/interfaces/api/userInterfaces';
|
import type { QuotaResponse } from '@server/interfaces/api/userInterfaces';
|
||||||
import { Permission } from '@server/lib/permissions';
|
import { Permission } from '@server/lib/permissions';
|
||||||
import type { MovieDetails } from '@server/models/Movie';
|
import type { MovieDetails } from '@server/models/Movie';
|
||||||
|
import axios from 'axios';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import { useToasts } from 'react-toast-notifications';
|
import { useToasts } from 'react-toast-notifications';
|
||||||
@@ -39,7 +39,7 @@ const messages = defineMessages('components.RequestModal', {
|
|||||||
interface RequestModalProps extends React.HTMLAttributes<HTMLDivElement> {
|
interface RequestModalProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
tmdbId: number;
|
tmdbId: number;
|
||||||
is4k?: boolean;
|
is4k?: boolean;
|
||||||
editRequest?: NonFunctionProperties<MediaRequest>;
|
editRequest?: MediaRequest;
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
onComplete?: (newStatus: MediaStatus) => void;
|
onComplete?: (newStatus: MediaStatus) => void;
|
||||||
onUpdating?: (isUpdating: boolean) => void;
|
onUpdating?: (isUpdating: boolean) => void;
|
||||||
@@ -89,23 +89,15 @@ const MovieRequestModal = ({
|
|||||||
tags: requestOverrides.tags,
|
tags: requestOverrides.tags,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const res = await fetch('/api/v1/request', {
|
const response = await axios.post<MediaRequest>('/api/v1/request', {
|
||||||
method: 'POST',
|
mediaId: data?.id,
|
||||||
headers: {
|
mediaType: 'movie',
|
||||||
'Content-Type': 'application/json',
|
is4k,
|
||||||
},
|
...overrideParams,
|
||||||
body: JSON.stringify({
|
|
||||||
mediaId: data?.id,
|
|
||||||
mediaType: 'movie',
|
|
||||||
is4k,
|
|
||||||
...overrideParams,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
const mediaRequest: MediaRequest = await res.json();
|
|
||||||
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
|
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
|
||||||
|
|
||||||
if (mediaRequest) {
|
if (response.data) {
|
||||||
if (onComplete) {
|
if (onComplete) {
|
||||||
onComplete(
|
onComplete(
|
||||||
hasPermission(
|
hasPermission(
|
||||||
@@ -144,14 +136,12 @@ const MovieRequestModal = ({
|
|||||||
setIsUpdating(true);
|
setIsUpdating(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/v1/request/${editRequest?.id}`, {
|
const response = await axios.delete<MediaRequest>(
|
||||||
method: 'DELETE',
|
`/api/v1/request/${editRequest?.id}`
|
||||||
});
|
);
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
|
|
||||||
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
|
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
|
||||||
|
|
||||||
if (res.status === 204) {
|
if (response.status === 204) {
|
||||||
if (onComplete) {
|
if (onComplete) {
|
||||||
onComplete(MediaStatus.UNKNOWN);
|
onComplete(MediaStatus.UNKNOWN);
|
||||||
}
|
}
|
||||||
@@ -174,27 +164,17 @@ const MovieRequestModal = ({
|
|||||||
setIsUpdating(true);
|
setIsUpdating(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/v1/request/${editRequest?.id}`, {
|
await axios.put(`/api/v1/request/${editRequest?.id}`, {
|
||||||
method: 'PUT',
|
mediaType: 'movie',
|
||||||
headers: {
|
serverId: requestOverrides?.server,
|
||||||
'Content-Type': 'application/json',
|
profileId: requestOverrides?.profile,
|
||||||
},
|
rootFolder: requestOverrides?.folder,
|
||||||
body: JSON.stringify({
|
userId: requestOverrides?.user?.id,
|
||||||
mediaType: 'movie',
|
tags: requestOverrides?.tags,
|
||||||
serverId: requestOverrides?.server,
|
|
||||||
profileId: requestOverrides?.profile,
|
|
||||||
rootFolder: requestOverrides?.folder,
|
|
||||||
userId: requestOverrides?.user?.id,
|
|
||||||
tags: requestOverrides?.tags,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
|
|
||||||
if (alsoApproveRequest) {
|
if (alsoApproveRequest) {
|
||||||
const res = await fetch(`/api/v1/request/${editRequest?.id}/approve`, {
|
await axios.post(`/api/v1/request/${editRequest?.id}/approve`);
|
||||||
method: 'POST',
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
}
|
}
|
||||||
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
|
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
|
||||||
|
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ import { ANIME_KEYWORD_ID } from '@server/api/themoviedb/constants';
|
|||||||
import { MediaRequestStatus, MediaStatus } from '@server/constants/media';
|
import { MediaRequestStatus, MediaStatus } from '@server/constants/media';
|
||||||
import type { MediaRequest } from '@server/entity/MediaRequest';
|
import type { MediaRequest } from '@server/entity/MediaRequest';
|
||||||
import type SeasonRequest from '@server/entity/SeasonRequest';
|
import type SeasonRequest from '@server/entity/SeasonRequest';
|
||||||
import type { NonFunctionProperties } from '@server/interfaces/api/common';
|
|
||||||
import type { QuotaResponse } from '@server/interfaces/api/userInterfaces';
|
import type { QuotaResponse } from '@server/interfaces/api/userInterfaces';
|
||||||
import { Permission } from '@server/lib/permissions';
|
import { Permission } from '@server/lib/permissions';
|
||||||
import type { TvDetails } from '@server/models/Tv';
|
import type { TvDetails } from '@server/models/Tv';
|
||||||
|
import axios from 'axios';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import { useToasts } from 'react-toast-notifications';
|
import { useToasts } from 'react-toast-notifications';
|
||||||
@@ -58,7 +58,7 @@ interface RequestModalProps extends React.HTMLAttributes<HTMLDivElement> {
|
|||||||
onComplete?: (newStatus: MediaStatus) => void;
|
onComplete?: (newStatus: MediaStatus) => void;
|
||||||
onUpdating?: (isUpdating: boolean) => void;
|
onUpdating?: (isUpdating: boolean) => void;
|
||||||
is4k?: boolean;
|
is4k?: boolean;
|
||||||
editRequest?: NonFunctionProperties<MediaRequest>;
|
editRequest?: MediaRequest;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TvRequestModal = ({
|
const TvRequestModal = ({
|
||||||
@@ -111,35 +111,22 @@ const TvRequestModal = ({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (selectedSeasons.length > 0) {
|
if (selectedSeasons.length > 0) {
|
||||||
const res = await fetch(`/api/v1/request/${editRequest.id}`, {
|
await axios.put(`/api/v1/request/${editRequest.id}`, {
|
||||||
method: 'PUT',
|
mediaType: 'tv',
|
||||||
headers: {
|
serverId: requestOverrides?.server,
|
||||||
'Content-Type': 'application/json',
|
profileId: requestOverrides?.profile,
|
||||||
},
|
rootFolder: requestOverrides?.folder,
|
||||||
body: JSON.stringify({
|
languageProfileId: requestOverrides?.language,
|
||||||
mediaType: 'tv',
|
userId: requestOverrides?.user?.id,
|
||||||
serverId: requestOverrides?.server,
|
tags: requestOverrides?.tags,
|
||||||
profileId: requestOverrides?.profile,
|
seasons: selectedSeasons,
|
||||||
rootFolder: requestOverrides?.folder,
|
|
||||||
languageProfileId: requestOverrides?.language,
|
|
||||||
userId: requestOverrides?.user?.id,
|
|
||||||
tags: requestOverrides?.tags,
|
|
||||||
seasons: selectedSeasons,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
|
|
||||||
if (alsoApproveRequest) {
|
if (alsoApproveRequest) {
|
||||||
const res = await fetch(`/api/v1/request/${editRequest.id}/approve`, {
|
await axios.post(`/api/v1/request/${editRequest.id}/approve`);
|
||||||
method: 'POST',
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const res = await fetch(`/api/v1/request/${editRequest.id}`, {
|
await axios.delete(`/api/v1/request/${editRequest.id}`);
|
||||||
method: 'DELETE',
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
}
|
}
|
||||||
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
|
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
|
||||||
|
|
||||||
@@ -204,32 +191,23 @@ const TvRequestModal = ({
|
|||||||
tags: requestOverrides.tags,
|
tags: requestOverrides.tags,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const res = await fetch('/api/v1/request', {
|
const response = await axios.post<MediaRequest>('/api/v1/request', {
|
||||||
method: 'POST',
|
mediaId: data?.id,
|
||||||
headers: {
|
tvdbId: tvdbId ?? data?.externalIds.tvdbId,
|
||||||
'Content-Type': 'application/json',
|
mediaType: 'tv',
|
||||||
},
|
is4k,
|
||||||
body: JSON.stringify({
|
seasons: settings.currentSettings.partialRequestsEnabled
|
||||||
mediaId: data?.id,
|
? selectedSeasons
|
||||||
tvdbId: tvdbId ?? data?.externalIds.tvdbId,
|
: getAllSeasons().filter(
|
||||||
mediaType: 'tv',
|
(season) => !getAllRequestedSeasons().includes(season)
|
||||||
is4k,
|
),
|
||||||
seasons: settings.currentSettings.partialRequestsEnabled
|
...overrideParams,
|
||||||
? selectedSeasons
|
|
||||||
: getAllSeasons().filter(
|
|
||||||
(season) => !getAllRequestedSeasons().includes(season)
|
|
||||||
),
|
|
||||||
...overrideParams,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
const mediaRequest: MediaRequest = await res.json();
|
|
||||||
|
|
||||||
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
|
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
|
||||||
|
|
||||||
if (mediaRequest) {
|
if (response.data) {
|
||||||
if (onComplete) {
|
if (onComplete) {
|
||||||
onComplete(mediaRequest.media.status);
|
onComplete(response.data.media.status);
|
||||||
}
|
}
|
||||||
addToast(
|
addToast(
|
||||||
<span>
|
<span>
|
||||||
|
|||||||
@@ -4,14 +4,13 @@ import TvRequestModal from '@app/components/RequestModal/TvRequestModal';
|
|||||||
import { Transition } from '@headlessui/react';
|
import { Transition } from '@headlessui/react';
|
||||||
import type { MediaStatus } from '@server/constants/media';
|
import type { MediaStatus } from '@server/constants/media';
|
||||||
import type { MediaRequest } from '@server/entity/MediaRequest';
|
import type { MediaRequest } from '@server/entity/MediaRequest';
|
||||||
import type { NonFunctionProperties } from '@server/interfaces/api/common';
|
|
||||||
|
|
||||||
interface RequestModalProps {
|
interface RequestModalProps {
|
||||||
show: boolean;
|
show: boolean;
|
||||||
type: 'movie' | 'tv' | 'collection';
|
type: 'movie' | 'tv' | 'collection';
|
||||||
tmdbId: number;
|
tmdbId: number;
|
||||||
is4k?: boolean;
|
is4k?: boolean;
|
||||||
editRequest?: NonFunctionProperties<MediaRequest>;
|
editRequest?: MediaRequest;
|
||||||
onComplete?: (newStatus: MediaStatus) => void;
|
onComplete?: (newStatus: MediaStatus) => void;
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
onUpdating?: (isUpdating: boolean) => void;
|
onUpdating?: (isUpdating: boolean) => void;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import PageTitle from '@app/components/Common/PageTitle';
|
|||||||
import LanguagePicker from '@app/components/Layout/LanguagePicker';
|
import LanguagePicker from '@app/components/Layout/LanguagePicker';
|
||||||
import defineMessages from '@app/utils/defineMessages';
|
import defineMessages from '@app/utils/defineMessages';
|
||||||
import { ArrowLeftIcon, EnvelopeIcon } from '@heroicons/react/24/solid';
|
import { ArrowLeftIcon, EnvelopeIcon } from '@heroicons/react/24/solid';
|
||||||
|
import axios from 'axios';
|
||||||
import { Field, Form, Formik } from 'formik';
|
import { Field, Form, Formik } from 'formik';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
@@ -84,18 +85,14 @@ const ResetPassword = () => {
|
|||||||
}}
|
}}
|
||||||
validationSchema={ResetSchema}
|
validationSchema={ResetSchema}
|
||||||
onSubmit={async (values) => {
|
onSubmit={async (values) => {
|
||||||
const res = await fetch(`/api/v1/auth/reset-password`, {
|
const response = await axios.post(
|
||||||
method: 'POST',
|
`/api/v1/auth/reset-password`,
|
||||||
headers: {
|
{
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
email: values.email,
|
email: values.email,
|
||||||
}),
|
}
|
||||||
});
|
);
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
|
|
||||||
if (res.status === 200) {
|
if (response.status === 200) {
|
||||||
setSubmitted(true);
|
setSubmitted(true);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import LanguagePicker from '@app/components/Layout/LanguagePicker';
|
|||||||
import globalMessages from '@app/i18n/globalMessages';
|
import globalMessages from '@app/i18n/globalMessages';
|
||||||
import defineMessages from '@app/utils/defineMessages';
|
import defineMessages from '@app/utils/defineMessages';
|
||||||
import { LifebuoyIcon } from '@heroicons/react/24/outline';
|
import { LifebuoyIcon } from '@heroicons/react/24/outline';
|
||||||
|
import axios from 'axios';
|
||||||
import { Form, Formik } from 'formik';
|
import { Form, Formik } from 'formik';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
@@ -99,21 +100,14 @@ const ResetPassword = () => {
|
|||||||
}}
|
}}
|
||||||
validationSchema={ResetSchema}
|
validationSchema={ResetSchema}
|
||||||
onSubmit={async (values) => {
|
onSubmit={async (values) => {
|
||||||
const res = await fetch(
|
const response = await axios.post(
|
||||||
`/api/v1/auth/reset-password/${guid}`,
|
`/api/v1/auth/reset-password/${guid}`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
password: values.password,
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
password: values.password,
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
|
|
||||||
if (res.status === 200) {
|
if (response.status === 200) {
|
||||||
setSubmitted(true);
|
setSubmitted(true);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import type {
|
|||||||
ProductionCompany,
|
ProductionCompany,
|
||||||
WatchProviderDetails,
|
WatchProviderDetails,
|
||||||
} from '@server/models/common';
|
} from '@server/models/common';
|
||||||
|
import axios from 'axios';
|
||||||
import { orderBy } from 'lodash';
|
import { orderBy } from 'lodash';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
@@ -68,9 +69,11 @@ export const CompanySelector = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch(`/api/v1/studio/${defaultValue}`);
|
const response = await axios.get<ProductionCompany>(
|
||||||
if (!res.ok) throw new Error();
|
`/api/v1/studio/${defaultValue}`
|
||||||
const studio: ProductionCompany = await res.json();
|
);
|
||||||
|
|
||||||
|
const studio = response.data;
|
||||||
|
|
||||||
setDefaultDataValue([
|
setDefaultDataValue([
|
||||||
{
|
{
|
||||||
@@ -88,15 +91,16 @@ export const CompanySelector = ({
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch(
|
const results = await axios.get<TmdbCompanySearchResponse>(
|
||||||
`/api/v1/search/company?query=${encodeURIExtraParams(inputValue)}`
|
'/api/v1/search/company',
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
query: encodeURIExtraParams(inputValue),
|
||||||
|
},
|
||||||
|
}
|
||||||
);
|
);
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error('Network response was not ok');
|
|
||||||
}
|
|
||||||
const results: TmdbCompanySearchResponse = await res.json();
|
|
||||||
|
|
||||||
return results.results.map((result) => ({
|
return results.data.results.map((result) => ({
|
||||||
label: result.name,
|
label: result.name,
|
||||||
value: result.id,
|
value: result.id,
|
||||||
}));
|
}));
|
||||||
@@ -150,15 +154,11 @@ export const GenreSelector = ({
|
|||||||
|
|
||||||
const genres = defaultValue.split(',');
|
const genres = defaultValue.split(',');
|
||||||
|
|
||||||
const res = await fetch(`/api/v1/genres/${type}`);
|
const response = await axios.get<TmdbGenre[]>(`/api/v1/genres/${type}`);
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error('Network response was not ok');
|
|
||||||
}
|
|
||||||
const response: TmdbGenre[] = await res.json();
|
|
||||||
|
|
||||||
const genreData = genres
|
const genreData = genres
|
||||||
.filter((genre) => response.find((gd) => gd.id === Number(genre)))
|
.filter((genre) => response.data.find((gd) => gd.id === Number(genre)))
|
||||||
.map((g) => response.find((gd) => gd.id === Number(g)))
|
.map((g) => response.data.find((gd) => gd.id === Number(g)))
|
||||||
.map((g) => ({
|
.map((g) => ({
|
||||||
label: g?.name ?? '',
|
label: g?.name ?? '',
|
||||||
value: g?.id ?? 0,
|
value: g?.id ?? 0,
|
||||||
@@ -171,11 +171,11 @@ export const GenreSelector = ({
|
|||||||
}, [defaultValue, type]);
|
}, [defaultValue, type]);
|
||||||
|
|
||||||
const loadGenreOptions = async (inputValue: string) => {
|
const loadGenreOptions = async (inputValue: string) => {
|
||||||
const res = await fetch(`/api/v1/discover/genreslider/${type}`);
|
const results = await axios.get<GenreSliderItem[]>(
|
||||||
if (!res.ok) throw new Error();
|
`/api/v1/discover/genreslider/${type}`
|
||||||
const results: GenreSliderItem[] = await res.json();
|
);
|
||||||
|
|
||||||
return results
|
return results.data
|
||||||
.map((result) => ({
|
.map((result) => ({
|
||||||
label: result.name,
|
label: result.name,
|
||||||
value: result.id,
|
value: result.id,
|
||||||
@@ -222,13 +222,11 @@ export const KeywordSelector = ({
|
|||||||
|
|
||||||
const keywords = await Promise.all(
|
const keywords = await Promise.all(
|
||||||
defaultValue.split(',').map(async (keywordId) => {
|
defaultValue.split(',').map(async (keywordId) => {
|
||||||
const res = await fetch(`/api/v1/keyword/${keywordId}`);
|
const keyword = await axios.get<Keyword>(
|
||||||
if (!res.ok) {
|
`/api/v1/keyword/${keywordId}`
|
||||||
throw new Error('Network response was not ok');
|
);
|
||||||
}
|
|
||||||
const keyword: Keyword = await res.json();
|
|
||||||
|
|
||||||
return keyword;
|
return keyword.data;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -244,15 +242,16 @@ export const KeywordSelector = ({
|
|||||||
}, [defaultValue]);
|
}, [defaultValue]);
|
||||||
|
|
||||||
const loadKeywordOptions = async (inputValue: string) => {
|
const loadKeywordOptions = async (inputValue: string) => {
|
||||||
const res = await fetch(
|
const results = await axios.get<TmdbKeywordSearchResponse>(
|
||||||
`/api/v1/search/keyword?query=${encodeURIExtraParams(inputValue)}`
|
'/api/v1/search/keyword',
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
query: encodeURIExtraParams(inputValue),
|
||||||
|
},
|
||||||
|
}
|
||||||
);
|
);
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error('Network response was not ok');
|
|
||||||
}
|
|
||||||
const results: TmdbKeywordSearchResponse = await res.json();
|
|
||||||
|
|
||||||
return results.results.map((result) => ({
|
return results.data.results.map((result) => ({
|
||||||
label: result.name,
|
label: result.name,
|
||||||
value: result.id,
|
value: result.id,
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
import useSettings from '@app/hooks/useSettings';
|
import useSettings from '@app/hooks/useSettings';
|
||||||
import { useUser } from '@app/hooks/useUser';
|
import { useUser } from '@app/hooks/useUser';
|
||||||
|
import axios from 'axios';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
const ServiceWorkerSetup = () => {
|
const ServiceWorkerSetup = () => {
|
||||||
@@ -25,18 +26,11 @@ const ServiceWorkerSetup = () => {
|
|||||||
const parsedSub = JSON.parse(JSON.stringify(sub));
|
const parsedSub = JSON.parse(JSON.stringify(sub));
|
||||||
|
|
||||||
if (parsedSub.keys.p256dh && parsedSub.keys.auth) {
|
if (parsedSub.keys.p256dh && parsedSub.keys.auth) {
|
||||||
const res = await fetch('/api/v1/user/registerPushSubscription', {
|
await axios.post('/api/v1/user/registerPushSubscription', {
|
||||||
method: 'POST',
|
endpoint: parsedSub.endpoint,
|
||||||
headers: {
|
p256dh: parsedSub.keys.p256dh,
|
||||||
'Content-Type': 'application/json',
|
auth: parsedSub.keys.auth,
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
endpoint: parsedSub.endpoint,
|
|
||||||
p256dh: parsedSub.keys.p256dh,
|
|
||||||
auth: parsedSub.keys.auth,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import useSettings from '@app/hooks/useSettings';
|
|||||||
import globalMessages from '@app/i18n/globalMessages';
|
import globalMessages from '@app/i18n/globalMessages';
|
||||||
import defineMessages from '@app/utils/defineMessages';
|
import defineMessages from '@app/utils/defineMessages';
|
||||||
import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline';
|
import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline';
|
||||||
|
import axios from 'axios';
|
||||||
import { Field, Form, Formik } from 'formik';
|
import { Field, Form, Formik } from 'formik';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
@@ -72,23 +73,16 @@ const NotificationsDiscord = () => {
|
|||||||
validationSchema={NotificationsDiscordSchema}
|
validationSchema={NotificationsDiscordSchema}
|
||||||
onSubmit={async (values) => {
|
onSubmit={async (values) => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/v1/settings/notifications/discord', {
|
await axios.post('/api/v1/settings/notifications/discord', {
|
||||||
method: 'POST',
|
enabled: values.enabled,
|
||||||
headers: {
|
types: values.types,
|
||||||
'Content-Type': 'application/json',
|
options: {
|
||||||
|
botUsername: values.botUsername,
|
||||||
|
botAvatarUrl: values.botAvatarUrl,
|
||||||
|
webhookUrl: values.webhookUrl,
|
||||||
|
enableMentions: values.enableMentions,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
|
||||||
enabled: values.enabled,
|
|
||||||
types: values.types,
|
|
||||||
options: {
|
|
||||||
botUsername: values.botUsername,
|
|
||||||
botAvatarUrl: values.botAvatarUrl,
|
|
||||||
webhookUrl: values.webhookUrl,
|
|
||||||
enableMentions: values.enableMentions,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
|
|
||||||
addToast(intl.formatMessage(messages.discordsettingssaved), {
|
addToast(intl.formatMessage(messages.discordsettingssaved), {
|
||||||
appearance: 'success',
|
appearance: 'success',
|
||||||
@@ -127,26 +121,16 @@ const NotificationsDiscord = () => {
|
|||||||
toastId = id;
|
toastId = id;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const res = await fetch(
|
await axios.post('/api/v1/settings/notifications/discord/test', {
|
||||||
'/api/v1/settings/notifications/discord/test',
|
enabled: true,
|
||||||
{
|
types: values.types,
|
||||||
method: 'POST',
|
options: {
|
||||||
headers: {
|
botUsername: values.botUsername,
|
||||||
'Content-Type': 'application/json',
|
botAvatarUrl: values.botAvatarUrl,
|
||||||
},
|
webhookUrl: values.webhookUrl,
|
||||||
body: JSON.stringify({
|
enableMentions: values.enableMentions,
|
||||||
enabled: true,
|
},
|
||||||
types: values.types,
|
});
|
||||||
options: {
|
|
||||||
botUsername: values.botUsername,
|
|
||||||
botAvatarUrl: values.botAvatarUrl,
|
|
||||||
webhookUrl: values.webhookUrl,
|
|
||||||
enableMentions: values.enableMentions,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
|
|
||||||
if (toastId) {
|
if (toastId) {
|
||||||
removeToast(toastId);
|
removeToast(toastId);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import SettingsBadge from '@app/components/Settings/SettingsBadge';
|
|||||||
import globalMessages from '@app/i18n/globalMessages';
|
import globalMessages from '@app/i18n/globalMessages';
|
||||||
import defineMessages from '@app/utils/defineMessages';
|
import defineMessages from '@app/utils/defineMessages';
|
||||||
import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline';
|
import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline';
|
||||||
|
import axios from 'axios';
|
||||||
import { Field, Form, Formik } from 'formik';
|
import { Field, Form, Formik } from 'formik';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
@@ -147,31 +148,24 @@ const NotificationsEmail = () => {
|
|||||||
validationSchema={NotificationsEmailSchema}
|
validationSchema={NotificationsEmailSchema}
|
||||||
onSubmit={async (values) => {
|
onSubmit={async (values) => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/v1/settings/notifications/email', {
|
await axios.post('/api/v1/settings/notifications/email', {
|
||||||
method: 'POST',
|
enabled: values.enabled,
|
||||||
headers: {
|
options: {
|
||||||
'Content-Type': 'application/json',
|
userEmailRequired: values.userEmailRequired,
|
||||||
|
emailFrom: values.emailFrom,
|
||||||
|
smtpHost: values.smtpHost,
|
||||||
|
smtpPort: Number(values.smtpPort),
|
||||||
|
secure: values.encryption === 'implicit',
|
||||||
|
ignoreTls: values.encryption === 'none',
|
||||||
|
requireTls: values.encryption === 'opportunistic',
|
||||||
|
authUser: values.authUser,
|
||||||
|
authPass: values.authPass,
|
||||||
|
allowSelfSigned: values.allowSelfSigned,
|
||||||
|
senderName: values.senderName,
|
||||||
|
pgpPrivateKey: values.pgpPrivateKey,
|
||||||
|
pgpPassword: values.pgpPassword,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
|
||||||
enabled: values.enabled,
|
|
||||||
options: {
|
|
||||||
userEmailRequired: values.userEmailRequired,
|
|
||||||
emailFrom: values.emailFrom,
|
|
||||||
smtpHost: values.smtpHost,
|
|
||||||
smtpPort: Number(values.smtpPort),
|
|
||||||
secure: values.encryption === 'implicit',
|
|
||||||
ignoreTls: values.encryption === 'none',
|
|
||||||
requireTls: values.encryption === 'opportunistic',
|
|
||||||
authUser: values.authUser,
|
|
||||||
authPass: values.authPass,
|
|
||||||
allowSelfSigned: values.allowSelfSigned,
|
|
||||||
senderName: values.senderName,
|
|
||||||
pgpPrivateKey: values.pgpPrivateKey,
|
|
||||||
pgpPassword: values.pgpPassword,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
mutate('/api/v1/settings/public');
|
mutate('/api/v1/settings/public');
|
||||||
|
|
||||||
addToast(intl.formatMessage(messages.emailsettingssaved), {
|
addToast(intl.formatMessage(messages.emailsettingssaved), {
|
||||||
@@ -203,32 +197,22 @@ const NotificationsEmail = () => {
|
|||||||
toastId = id;
|
toastId = id;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const res = await fetch(
|
await axios.post('/api/v1/settings/notifications/email/test', {
|
||||||
'/api/v1/settings/notifications/email/test',
|
enabled: true,
|
||||||
{
|
options: {
|
||||||
method: 'POST',
|
emailFrom: values.emailFrom,
|
||||||
headers: {
|
smtpHost: values.smtpHost,
|
||||||
'Content-Type': 'application/json',
|
smtpPort: Number(values.smtpPort),
|
||||||
},
|
secure: values.encryption === 'implicit',
|
||||||
body: JSON.stringify({
|
ignoreTls: values.encryption === 'none',
|
||||||
enabled: true,
|
requireTls: values.encryption === 'opportunistic',
|
||||||
options: {
|
authUser: values.authUser,
|
||||||
emailFrom: values.emailFrom,
|
authPass: values.authPass,
|
||||||
smtpHost: values.smtpHost,
|
senderName: values.senderName,
|
||||||
smtpPort: Number(values.smtpPort),
|
pgpPrivateKey: values.pgpPrivateKey,
|
||||||
secure: values.encryption === 'implicit',
|
pgpPassword: values.pgpPassword,
|
||||||
ignoreTls: values.encryption === 'none',
|
},
|
||||||
requireTls: values.encryption === 'opportunistic',
|
});
|
||||||
authUser: values.authUser,
|
|
||||||
authPass: values.authPass,
|
|
||||||
senderName: values.senderName,
|
|
||||||
pgpPrivateKey: values.pgpPrivateKey,
|
|
||||||
pgpPassword: values.pgpPassword,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
|
|
||||||
if (toastId) {
|
if (toastId) {
|
||||||
removeToast(toastId);
|
removeToast(toastId);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import NotificationTypeSelector from '@app/components/NotificationTypeSelector';
|
|||||||
import globalMessages from '@app/i18n/globalMessages';
|
import globalMessages from '@app/i18n/globalMessages';
|
||||||
import defineMessages from '@app/utils/defineMessages';
|
import defineMessages from '@app/utils/defineMessages';
|
||||||
import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/solid';
|
import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/solid';
|
||||||
|
import axios from 'axios';
|
||||||
import { Field, Form, Formik } from 'formik';
|
import { Field, Form, Formik } from 'formik';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
@@ -82,21 +83,14 @@ const NotificationsGotify = () => {
|
|||||||
validationSchema={NotificationsGotifySchema}
|
validationSchema={NotificationsGotifySchema}
|
||||||
onSubmit={async (values) => {
|
onSubmit={async (values) => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/v1/settings/notifications/gotify', {
|
await axios.post('/api/v1/settings/notifications/gotify', {
|
||||||
method: 'POST',
|
enabled: values.enabled,
|
||||||
headers: {
|
types: values.types,
|
||||||
'Content-Type': 'application/json',
|
options: {
|
||||||
|
url: values.url,
|
||||||
|
token: values.token,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
|
||||||
enabled: values.enabled,
|
|
||||||
types: values.types,
|
|
||||||
options: {
|
|
||||||
url: values.url,
|
|
||||||
token: values.token,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
addToast(intl.formatMessage(messages.gotifysettingssaved), {
|
addToast(intl.formatMessage(messages.gotifysettingssaved), {
|
||||||
appearance: 'success',
|
appearance: 'success',
|
||||||
autoDismiss: true,
|
autoDismiss: true,
|
||||||
@@ -134,24 +128,14 @@ const NotificationsGotify = () => {
|
|||||||
toastId = id;
|
toastId = id;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const res = await fetch(
|
await axios.post('/api/v1/settings/notifications/gotify/test', {
|
||||||
'/api/v1/settings/notifications/gotify/test',
|
enabled: true,
|
||||||
{
|
types: values.types,
|
||||||
method: 'POST',
|
options: {
|
||||||
headers: {
|
url: values.url,
|
||||||
'Content-Type': 'application/json',
|
token: values.token,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
});
|
||||||
enabled: true,
|
|
||||||
types: values.types,
|
|
||||||
options: {
|
|
||||||
url: values.url,
|
|
||||||
token: values.token,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
|
|
||||||
if (toastId) {
|
if (toastId) {
|
||||||
removeToast(toastId);
|
removeToast(toastId);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import NotificationTypeSelector from '@app/components/NotificationTypeSelector';
|
|||||||
import globalMessages from '@app/i18n/globalMessages';
|
import globalMessages from '@app/i18n/globalMessages';
|
||||||
import defineMessages from '@app/utils/defineMessages';
|
import defineMessages from '@app/utils/defineMessages';
|
||||||
import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline';
|
import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline';
|
||||||
|
import axios from 'axios';
|
||||||
import { Field, Form, Formik } from 'formik';
|
import { Field, Form, Formik } from 'formik';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
@@ -68,21 +69,14 @@ const NotificationsLunaSea = () => {
|
|||||||
validationSchema={NotificationsLunaSeaSchema}
|
validationSchema={NotificationsLunaSeaSchema}
|
||||||
onSubmit={async (values) => {
|
onSubmit={async (values) => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/v1/settings/notifications/lunasea', {
|
await axios.post('/api/v1/settings/notifications/lunasea', {
|
||||||
method: 'POST',
|
enabled: values.enabled,
|
||||||
headers: {
|
types: values.types,
|
||||||
'Content-Type': 'application/json',
|
options: {
|
||||||
|
webhookUrl: values.webhookUrl,
|
||||||
|
profileName: values.profileName,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
|
||||||
enabled: values.enabled,
|
|
||||||
types: values.types,
|
|
||||||
options: {
|
|
||||||
webhookUrl: values.webhookUrl,
|
|
||||||
profileName: values.profileName,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
addToast(intl.formatMessage(messages.settingsSaved), {
|
addToast(intl.formatMessage(messages.settingsSaved), {
|
||||||
appearance: 'success',
|
appearance: 'success',
|
||||||
autoDismiss: true,
|
autoDismiss: true,
|
||||||
@@ -120,24 +114,14 @@ const NotificationsLunaSea = () => {
|
|||||||
toastId = id;
|
toastId = id;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const res = await fetch(
|
await axios.post('/api/v1/settings/notifications/lunasea/test', {
|
||||||
'/api/v1/settings/notifications/lunasea/test',
|
enabled: true,
|
||||||
{
|
types: values.types,
|
||||||
method: 'POST',
|
options: {
|
||||||
headers: {
|
webhookUrl: values.webhookUrl,
|
||||||
'Content-Type': 'application/json',
|
profileName: values.profileName,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
});
|
||||||
enabled: true,
|
|
||||||
types: values.types,
|
|
||||||
options: {
|
|
||||||
webhookUrl: values.webhookUrl,
|
|
||||||
profileName: values.profileName,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
|
|
||||||
if (toastId) {
|
if (toastId) {
|
||||||
removeToast(toastId);
|
removeToast(toastId);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import NotificationTypeSelector from '@app/components/NotificationTypeSelector';
|
|||||||
import globalMessages from '@app/i18n/globalMessages';
|
import globalMessages from '@app/i18n/globalMessages';
|
||||||
import defineMessages from '@app/utils/defineMessages';
|
import defineMessages from '@app/utils/defineMessages';
|
||||||
import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline';
|
import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline';
|
||||||
|
import axios from 'axios';
|
||||||
import { Field, Form, Formik } from 'formik';
|
import { Field, Form, Formik } from 'formik';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
@@ -67,21 +68,14 @@ const NotificationsPushbullet = () => {
|
|||||||
validationSchema={NotificationsPushbulletSchema}
|
validationSchema={NotificationsPushbulletSchema}
|
||||||
onSubmit={async (values) => {
|
onSubmit={async (values) => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/v1/settings/notifications/pushbullet', {
|
await axios.post('/api/v1/settings/notifications/pushbullet', {
|
||||||
method: 'POST',
|
enabled: values.enabled,
|
||||||
headers: {
|
types: values.types,
|
||||||
'Content-Type': 'application/json',
|
options: {
|
||||||
|
accessToken: values.accessToken,
|
||||||
|
channelTag: values.channelTag,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
|
||||||
enabled: values.enabled,
|
|
||||||
types: values.types,
|
|
||||||
options: {
|
|
||||||
accessToken: values.accessToken,
|
|
||||||
channelTag: values.channelTag,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
addToast(intl.formatMessage(messages.pushbulletSettingsSaved), {
|
addToast(intl.formatMessage(messages.pushbulletSettingsSaved), {
|
||||||
appearance: 'success',
|
appearance: 'success',
|
||||||
autoDismiss: true,
|
autoDismiss: true,
|
||||||
@@ -119,24 +113,14 @@ const NotificationsPushbullet = () => {
|
|||||||
toastId = id;
|
toastId = id;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const res = await fetch(
|
await axios.post('/api/v1/settings/notifications/pushbullet/test', {
|
||||||
'/api/v1/settings/notifications/pushbullet/test',
|
enabled: true,
|
||||||
{
|
types: values.types,
|
||||||
method: 'POST',
|
options: {
|
||||||
headers: {
|
accessToken: values.accessToken,
|
||||||
'Content-Type': 'application/json',
|
channelTag: values.channelTag,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
});
|
||||||
enabled: true,
|
|
||||||
types: values.types,
|
|
||||||
options: {
|
|
||||||
accessToken: values.accessToken,
|
|
||||||
channelTag: values.channelTag,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
|
|
||||||
if (toastId) {
|
if (toastId) {
|
||||||
removeToast(toastId);
|
removeToast(toastId);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import globalMessages from '@app/i18n/globalMessages';
|
|||||||
import defineMessages from '@app/utils/defineMessages';
|
import defineMessages from '@app/utils/defineMessages';
|
||||||
import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline';
|
import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline';
|
||||||
import type { PushoverSound } from '@server/api/pushover';
|
import type { PushoverSound } from '@server/api/pushover';
|
||||||
|
import axios from 'axios';
|
||||||
import { Field, Form, Formik } from 'formik';
|
import { Field, Form, Formik } from 'formik';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
@@ -93,21 +94,14 @@ const NotificationsPushover = () => {
|
|||||||
validationSchema={NotificationsPushoverSchema}
|
validationSchema={NotificationsPushoverSchema}
|
||||||
onSubmit={async (values) => {
|
onSubmit={async (values) => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/v1/settings/notifications/pushover', {
|
await axios.post('/api/v1/settings/notifications/pushover', {
|
||||||
method: 'POST',
|
enabled: values.enabled,
|
||||||
headers: {
|
types: values.types,
|
||||||
'Content-Type': 'application/json',
|
options: {
|
||||||
|
accessToken: values.accessToken,
|
||||||
|
userToken: values.userToken,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
|
||||||
enabled: values.enabled,
|
|
||||||
types: values.types,
|
|
||||||
options: {
|
|
||||||
accessToken: values.accessToken,
|
|
||||||
userToken: values.userToken,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
addToast(intl.formatMessage(messages.pushoversettingssaved), {
|
addToast(intl.formatMessage(messages.pushoversettingssaved), {
|
||||||
appearance: 'success',
|
appearance: 'success',
|
||||||
autoDismiss: true,
|
autoDismiss: true,
|
||||||
@@ -145,25 +139,16 @@ const NotificationsPushover = () => {
|
|||||||
toastId = id;
|
toastId = id;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const res = await fetch(
|
await axios.post('/api/v1/settings/notifications/pushover/test', {
|
||||||
'/api/v1/settings/notifications/pushover/test',
|
enabled: true,
|
||||||
{
|
types: values.types,
|
||||||
method: 'POST',
|
options: {
|
||||||
headers: {
|
accessToken: values.accessToken,
|
||||||
'Content-Type': 'application/json',
|
userToken: values.userToken,
|
||||||
},
|
sound: values.sound,
|
||||||
body: JSON.stringify({
|
},
|
||||||
enabled: true,
|
});
|
||||||
types: values.types,
|
|
||||||
options: {
|
|
||||||
accessToken: values.accessToken,
|
|
||||||
userToken: values.userToken,
|
|
||||||
sound: values.sound,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
if (toastId) {
|
if (toastId) {
|
||||||
removeToast(toastId);
|
removeToast(toastId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import NotificationTypeSelector from '@app/components/NotificationTypeSelector';
|
|||||||
import globalMessages from '@app/i18n/globalMessages';
|
import globalMessages from '@app/i18n/globalMessages';
|
||||||
import defineMessages from '@app/utils/defineMessages';
|
import defineMessages from '@app/utils/defineMessages';
|
||||||
import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline';
|
import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline';
|
||||||
|
import axios from 'axios';
|
||||||
import { Field, Form, Formik } from 'formik';
|
import { Field, Form, Formik } from 'formik';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
@@ -64,20 +65,13 @@ const NotificationsSlack = () => {
|
|||||||
validationSchema={NotificationsSlackSchema}
|
validationSchema={NotificationsSlackSchema}
|
||||||
onSubmit={async (values) => {
|
onSubmit={async (values) => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/v1/settings/notifications/slack', {
|
await axios.post('/api/v1/settings/notifications/slack', {
|
||||||
method: 'POST',
|
enabled: values.enabled,
|
||||||
headers: {
|
types: values.types,
|
||||||
'Content-Type': 'application/json',
|
options: {
|
||||||
|
webhookUrl: values.webhookUrl,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
|
||||||
enabled: values.enabled,
|
|
||||||
types: values.types,
|
|
||||||
options: {
|
|
||||||
webhookUrl: values.webhookUrl,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
addToast(intl.formatMessage(messages.slacksettingssaved), {
|
addToast(intl.formatMessage(messages.slacksettingssaved), {
|
||||||
appearance: 'success',
|
appearance: 'success',
|
||||||
autoDismiss: true,
|
autoDismiss: true,
|
||||||
@@ -115,23 +109,13 @@ const NotificationsSlack = () => {
|
|||||||
toastId = id;
|
toastId = id;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const res = await fetch(
|
await axios.post('/api/v1/settings/notifications/slack/test', {
|
||||||
'/api/v1/settings/notifications/slack/test',
|
enabled: true,
|
||||||
{
|
types: values.types,
|
||||||
method: 'POST',
|
options: {
|
||||||
headers: {
|
webhookUrl: values.webhookUrl,
|
||||||
'Content-Type': 'application/json',
|
},
|
||||||
},
|
});
|
||||||
body: JSON.stringify({
|
|
||||||
enabled: true,
|
|
||||||
types: values.types,
|
|
||||||
options: {
|
|
||||||
webhookUrl: values.webhookUrl,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
|
|
||||||
if (toastId) {
|
if (toastId) {
|
||||||
removeToast(toastId);
|
removeToast(toastId);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import NotificationTypeSelector from '@app/components/NotificationTypeSelector';
|
|||||||
import globalMessages from '@app/i18n/globalMessages';
|
import globalMessages from '@app/i18n/globalMessages';
|
||||||
import defineMessages from '@app/utils/defineMessages';
|
import defineMessages from '@app/utils/defineMessages';
|
||||||
import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline';
|
import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline';
|
||||||
|
import axios from 'axios';
|
||||||
import { Field, Form, Formik } from 'formik';
|
import { Field, Form, Formik } from 'formik';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
@@ -83,23 +84,16 @@ const NotificationsTelegram = () => {
|
|||||||
validationSchema={NotificationsTelegramSchema}
|
validationSchema={NotificationsTelegramSchema}
|
||||||
onSubmit={async (values) => {
|
onSubmit={async (values) => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/v1/settings/notifications/telegram', {
|
await axios.post('/api/v1/settings/notifications/telegram', {
|
||||||
method: 'POST',
|
enabled: values.enabled,
|
||||||
headers: {
|
types: values.types,
|
||||||
'Content-Type': 'application/json',
|
options: {
|
||||||
|
botAPI: values.botAPI,
|
||||||
|
chatId: values.chatId,
|
||||||
|
sendSilently: values.sendSilently,
|
||||||
|
botUsername: values.botUsername,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
|
||||||
enabled: values.enabled,
|
|
||||||
types: values.types,
|
|
||||||
options: {
|
|
||||||
botAPI: values.botAPI,
|
|
||||||
chatId: values.chatId,
|
|
||||||
sendSilently: values.sendSilently,
|
|
||||||
botUsername: values.botUsername,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
|
|
||||||
addToast(intl.formatMessage(messages.telegramsettingssaved), {
|
addToast(intl.formatMessage(messages.telegramsettingssaved), {
|
||||||
appearance: 'success',
|
appearance: 'success',
|
||||||
@@ -138,26 +132,16 @@ const NotificationsTelegram = () => {
|
|||||||
toastId = id;
|
toastId = id;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const res = await fetch(
|
await axios.post('/api/v1/settings/notifications/telegram/test', {
|
||||||
'/api/v1/settings/notifications/telegram/test',
|
enabled: true,
|
||||||
{
|
types: values.types,
|
||||||
method: 'POST',
|
options: {
|
||||||
headers: {
|
botAPI: values.botAPI,
|
||||||
'Content-Type': 'application/json',
|
chatId: values.chatId,
|
||||||
},
|
sendSilently: values.sendSilently,
|
||||||
body: JSON.stringify({
|
botUsername: values.botUsername,
|
||||||
enabled: true,
|
},
|
||||||
types: values.types,
|
});
|
||||||
options: {
|
|
||||||
botAPI: values.botAPI,
|
|
||||||
chatId: values.chatId,
|
|
||||||
sendSilently: values.sendSilently,
|
|
||||||
botUsername: values.botUsername,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
|
|
||||||
if (toastId) {
|
if (toastId) {
|
||||||
removeToast(toastId);
|
removeToast(toastId);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
|||||||
import globalMessages from '@app/i18n/globalMessages';
|
import globalMessages from '@app/i18n/globalMessages';
|
||||||
import defineMessages from '@app/utils/defineMessages';
|
import defineMessages from '@app/utils/defineMessages';
|
||||||
import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline';
|
import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline';
|
||||||
|
import axios from 'axios';
|
||||||
import { Field, Form, Formik } from 'formik';
|
import { Field, Form, Formik } from 'formik';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
@@ -57,17 +58,10 @@ const NotificationsWebPush = () => {
|
|||||||
}}
|
}}
|
||||||
onSubmit={async (values) => {
|
onSubmit={async (values) => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/v1/settings/notifications/webpush', {
|
await axios.post('/api/v1/settings/notifications/webpush', {
|
||||||
method: 'POST',
|
enabled: values.enabled,
|
||||||
headers: {
|
options: {},
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
enabled: values.enabled,
|
|
||||||
options: {},
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
mutate('/api/v1/settings/public');
|
mutate('/api/v1/settings/public');
|
||||||
addToast(intl.formatMessage(messages.webpushsettingssaved), {
|
addToast(intl.formatMessage(messages.webpushsettingssaved), {
|
||||||
appearance: 'success',
|
appearance: 'success',
|
||||||
@@ -98,20 +92,10 @@ const NotificationsWebPush = () => {
|
|||||||
toastId = id;
|
toastId = id;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const res = await fetch(
|
await axios.post('/api/v1/settings/notifications/webpush/test', {
|
||||||
'/api/v1/settings/notifications/webpush/test',
|
enabled: true,
|
||||||
{
|
options: {},
|
||||||
method: 'POST',
|
});
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
enabled: true,
|
|
||||||
options: {},
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
|
|
||||||
if (toastId) {
|
if (toastId) {
|
||||||
removeToast(toastId);
|
removeToast(toastId);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
ArrowPathIcon,
|
ArrowPathIcon,
|
||||||
QuestionMarkCircleIcon,
|
QuestionMarkCircleIcon,
|
||||||
} from '@heroicons/react/24/solid';
|
} from '@heroicons/react/24/solid';
|
||||||
|
import axios from 'axios';
|
||||||
import { Field, Form, Formik } from 'formik';
|
import { Field, Form, Formik } from 'formik';
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
@@ -149,22 +150,15 @@ const NotificationsWebhook = () => {
|
|||||||
validationSchema={NotificationsWebhookSchema}
|
validationSchema={NotificationsWebhookSchema}
|
||||||
onSubmit={async (values) => {
|
onSubmit={async (values) => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/v1/settings/notifications/webhook', {
|
await axios.post('/api/v1/settings/notifications/webhook', {
|
||||||
method: 'POST',
|
enabled: values.enabled,
|
||||||
headers: {
|
types: values.types,
|
||||||
'Content-Type': 'application/json',
|
options: {
|
||||||
|
webhookUrl: values.webhookUrl,
|
||||||
|
jsonPayload: JSON.stringify(values.jsonPayload),
|
||||||
|
authHeader: values.authHeader,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
|
||||||
enabled: values.enabled,
|
|
||||||
types: values.types,
|
|
||||||
options: {
|
|
||||||
webhookUrl: values.webhookUrl,
|
|
||||||
jsonPayload: JSON.stringify(values.jsonPayload),
|
|
||||||
authHeader: values.authHeader,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
addToast(intl.formatMessage(messages.webhooksettingssaved), {
|
addToast(intl.formatMessage(messages.webhooksettingssaved), {
|
||||||
appearance: 'success',
|
appearance: 'success',
|
||||||
autoDismiss: true,
|
autoDismiss: true,
|
||||||
@@ -213,25 +207,16 @@ const NotificationsWebhook = () => {
|
|||||||
toastId = id;
|
toastId = id;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const res = await fetch(
|
await axios.post('/api/v1/settings/notifications/webhook/test', {
|
||||||
'/api/v1/settings/notifications/webhook/test',
|
enabled: true,
|
||||||
{
|
types: values.types,
|
||||||
method: 'POST',
|
options: {
|
||||||
headers: {
|
webhookUrl: values.webhookUrl,
|
||||||
'Content-Type': 'application/json',
|
jsonPayload: JSON.stringify(values.jsonPayload),
|
||||||
},
|
authHeader: values.authHeader,
|
||||||
body: JSON.stringify({
|
},
|
||||||
enabled: true,
|
});
|
||||||
types: values.types,
|
|
||||||
options: {
|
|
||||||
webhookUrl: values.webhookUrl,
|
|
||||||
jsonPayload: JSON.stringify(values.jsonPayload),
|
|
||||||
authHeader: values.authHeader,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
if (toastId) {
|
if (toastId) {
|
||||||
removeToast(toastId);
|
removeToast(toastId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import globalMessages from '@app/i18n/globalMessages';
|
|||||||
import defineMessages from '@app/utils/defineMessages';
|
import defineMessages from '@app/utils/defineMessages';
|
||||||
import { Transition } from '@headlessui/react';
|
import { Transition } from '@headlessui/react';
|
||||||
import type { RadarrSettings } from '@server/lib/settings';
|
import type { RadarrSettings } from '@server/lib/settings';
|
||||||
|
import axios from 'axios';
|
||||||
import { Field, Formik } from 'formik';
|
import { Field, Formik } from 'formik';
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
@@ -165,24 +166,19 @@ const RadarrModal = ({ onClose, radarr, onSave }: RadarrModalProps) => {
|
|||||||
}) => {
|
}) => {
|
||||||
setIsTesting(true);
|
setIsTesting(true);
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/v1/settings/radarr/test', {
|
const response = await axios.post<TestResponse>(
|
||||||
method: 'POST',
|
'/api/v1/settings/radarr/test',
|
||||||
headers: {
|
{
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
hostname,
|
hostname,
|
||||||
apiKey,
|
apiKey,
|
||||||
port: Number(port),
|
port: Number(port),
|
||||||
baseUrl,
|
baseUrl,
|
||||||
useSsl,
|
useSsl,
|
||||||
}),
|
}
|
||||||
});
|
);
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
const data = await res.json();
|
|
||||||
|
|
||||||
setIsValidated(true);
|
setIsValidated(true);
|
||||||
setTestResponse(data);
|
setTestResponse(response.data);
|
||||||
if (initialLoad.current) {
|
if (initialLoad.current) {
|
||||||
addToast(intl.formatMessage(messages.toastRadarrTestSuccess), {
|
addToast(intl.formatMessage(messages.toastRadarrTestSuccess), {
|
||||||
appearance: 'success',
|
appearance: 'success',
|
||||||
@@ -275,23 +271,12 @@ const RadarrModal = ({ onClose, radarr, onSave }: RadarrModalProps) => {
|
|||||||
tagRequests: values.tagRequests,
|
tagRequests: values.tagRequests,
|
||||||
};
|
};
|
||||||
if (!radarr) {
|
if (!radarr) {
|
||||||
const res = await fetch('/api/v1/settings/radarr', {
|
await axios.post('/api/v1/settings/radarr', submission);
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(submission),
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
} else {
|
} else {
|
||||||
const res = await fetch(`/api/v1/settings/radarr/${radarr.id}`, {
|
await axios.put(
|
||||||
method: 'PUT',
|
`/api/v1/settings/radarr/${radarr.id}`,
|
||||||
headers: {
|
submission
|
||||||
'Content-Type': 'application/json',
|
);
|
||||||
},
|
|
||||||
body: JSON.stringify(submission),
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onSave();
|
onSave();
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import Badge from '@app/components/Common/Badge';
|
import Badge from '@app/components/Common/Badge';
|
||||||
import Button from '@app/components/Common/Button';
|
import Button from '@app/components/Common/Button';
|
||||||
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
||||||
import SensitiveInput from '@app/components/Common/SensitiveInput';
|
|
||||||
import LibraryItem from '@app/components/Settings/LibraryItem';
|
import LibraryItem from '@app/components/Settings/LibraryItem';
|
||||||
import globalMessages from '@app/i18n/globalMessages';
|
import globalMessages from '@app/i18n/globalMessages';
|
||||||
import defineMessages from '@app/utils/defineMessages';
|
import defineMessages from '@app/utils/defineMessages';
|
||||||
import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline';
|
import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline';
|
||||||
import { ApiErrorCode } from '@server/constants/error';
|
import { ApiErrorCode } from '@server/constants/error';
|
||||||
import type { JellyfinSettings } from '@server/lib/settings';
|
import type { JellyfinSettings } from '@server/lib/settings';
|
||||||
|
import axios from 'axios';
|
||||||
import { Field, Formik } from 'formik';
|
import { Field, Formik } from 'formik';
|
||||||
import getConfig from 'next/config';
|
import getConfig from 'next/config';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
@@ -31,14 +31,13 @@ const messages = defineMessages('components.Settings', {
|
|||||||
jellyfinSettingsSuccess: '{mediaServerName} settings saved successfully!',
|
jellyfinSettingsSuccess: '{mediaServerName} settings saved successfully!',
|
||||||
jellyfinSettings: '{mediaServerName} Settings',
|
jellyfinSettings: '{mediaServerName} Settings',
|
||||||
jellyfinSettingsDescription:
|
jellyfinSettingsDescription:
|
||||||
'Optionally configure the internal and external endpoints for your {mediaServerName} server. In most cases, the external URL is different to the internal URL. A custom password reset URL can also be set for {mediaServerName} login, in case you would like to redirect to a different password reset page. You can also change the Jellyfin API key, which was automatically generated previously.',
|
'Optionally configure the internal and external endpoints for your {mediaServerName} server. In most cases, the external URL is different to the internal URL. A custom password reset URL can also be set for {mediaServerName} login, in case you would like to redirect to a different password reset page.',
|
||||||
externalUrl: 'External URL',
|
externalUrl: 'External URL',
|
||||||
hostname: 'Hostname or IP Address',
|
hostname: 'Hostname or IP Address',
|
||||||
port: 'Port',
|
port: 'Port',
|
||||||
enablessl: 'Use SSL',
|
enablessl: 'Use SSL',
|
||||||
urlBase: 'URL Base',
|
urlBase: 'URL Base',
|
||||||
jellyfinForgotPasswordUrl: 'Forgot Password URL',
|
jellyfinForgotPasswordUrl: 'Forgot Password URL',
|
||||||
apiKey: 'API key',
|
|
||||||
jellyfinSyncFailedNoLibrariesFound: 'No libraries were found',
|
jellyfinSyncFailedNoLibrariesFound: 'No libraries were found',
|
||||||
jellyfinSyncFailedAutomaticGroupedFolders:
|
jellyfinSyncFailedAutomaticGroupedFolders:
|
||||||
'Custom authentication with Automatic Library Grouping not supported',
|
'Custom authentication with Automatic Library Grouping not supported',
|
||||||
@@ -168,25 +167,13 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const searchParams = new URLSearchParams({
|
await axios.get('/api/v1/settings/jellyfin/library', {
|
||||||
sync: params.sync ? 'true' : 'false',
|
params,
|
||||||
...(params.enable ? { enable: params.enable } : {}),
|
|
||||||
});
|
});
|
||||||
const res = await fetch(
|
|
||||||
`/api/v1/settings/jellyfin/library?${searchParams.toString()}`
|
|
||||||
);
|
|
||||||
if (!res.ok) throw new Error(res.statusText, { cause: res });
|
|
||||||
setIsSyncing(false);
|
setIsSyncing(false);
|
||||||
revalidate();
|
revalidate();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
let errorData;
|
if (e.response.data.message === 'SYNC_ERROR_GROUPED_FOLDERS') {
|
||||||
try {
|
|
||||||
errorData = await e.cause?.text();
|
|
||||||
errorData = JSON.parse(errorData);
|
|
||||||
} catch {
|
|
||||||
/* empty */
|
|
||||||
}
|
|
||||||
if (errorData?.message === 'SYNC_ERROR_GROUPED_FOLDERS') {
|
|
||||||
toasts.addToast(
|
toasts.addToast(
|
||||||
intl.formatMessage(
|
intl.formatMessage(
|
||||||
messages.jellyfinSyncFailedAutomaticGroupedFolders
|
messages.jellyfinSyncFailedAutomaticGroupedFolders
|
||||||
@@ -196,7 +183,7 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
|
|||||||
appearance: 'warning',
|
appearance: 'warning',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else if (errorData?.message === 'SYNC_ERROR_NO_LIBRARIES') {
|
} else if (e.response.data.message === 'SYNC_ERROR_NO_LIBRARIES') {
|
||||||
toasts.addToast(
|
toasts.addToast(
|
||||||
intl.formatMessage(messages.jellyfinSyncFailedNoLibrariesFound),
|
intl.formatMessage(messages.jellyfinSyncFailedNoLibrariesFound),
|
||||||
{
|
{
|
||||||
@@ -219,32 +206,16 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const startScan = async () => {
|
const startScan = async () => {
|
||||||
const res = await fetch('/api/v1/settings/jellyfin/sync', {
|
await axios.post('/api/v1/settings/jellyfin/sync', {
|
||||||
method: 'POST',
|
start: true,
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
start: true,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
|
|
||||||
revalidateSync();
|
revalidateSync();
|
||||||
};
|
};
|
||||||
|
|
||||||
const cancelScan = async () => {
|
const cancelScan = async () => {
|
||||||
const res = await fetch('/api/v1/settings/jellyfin/sync', {
|
await axios.post('/api/v1/settings/jellyfin/sync', {
|
||||||
method: 'POST',
|
cancel: true,
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
cancel: true,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
|
|
||||||
revalidateSync();
|
revalidateSync();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -259,19 +230,15 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
|
|||||||
.join(',');
|
.join(',');
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchParams = new URLSearchParams(params.enable ? params : {});
|
await axios.get('/api/v1/settings/jellyfin/library', {
|
||||||
const res = await fetch(
|
params,
|
||||||
`/api/v1/settings/jellyfin/library?${searchParams.toString()}`
|
});
|
||||||
);
|
} else {
|
||||||
if (!res.ok) throw new Error();
|
await axios.get('/api/v1/settings/jellyfin/library', {
|
||||||
} else {
|
params: {
|
||||||
const searchParams = new URLSearchParams({
|
enable: [...activeLibraries, libraryId].join(','),
|
||||||
enable: [...activeLibraries, libraryId].join(','),
|
},
|
||||||
});
|
});
|
||||||
const res = await fetch(
|
|
||||||
`/api/v1/settings/jellyfin/library?${searchParams.toString()}`
|
|
||||||
);
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
}
|
}
|
||||||
if (onComplete) {
|
if (onComplete) {
|
||||||
onComplete();
|
onComplete();
|
||||||
@@ -446,121 +413,105 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-10 mb-6">
|
{showAdvancedSettings && (
|
||||||
<h3 className="heading">
|
<>
|
||||||
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
<div className="mt-10 mb-6">
|
||||||
? intl.formatMessage(messages.jellyfinSettings, {
|
<h3 className="heading">
|
||||||
mediaServerName: 'Emby',
|
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||||
})
|
? intl.formatMessage(messages.jellyfinSettings, {
|
||||||
: intl.formatMessage(messages.jellyfinSettings, {
|
mediaServerName: 'Emby',
|
||||||
mediaServerName: 'Jellyfin',
|
})
|
||||||
})}
|
: intl.formatMessage(messages.jellyfinSettings, {
|
||||||
</h3>
|
mediaServerName: 'Jellyfin',
|
||||||
<p className="description">
|
})}
|
||||||
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
</h3>
|
||||||
? intl.formatMessage(messages.jellyfinSettingsDescription, {
|
<p className="description">
|
||||||
mediaServerName: 'Emby',
|
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||||
})
|
? intl.formatMessage(messages.jellyfinSettingsDescription, {
|
||||||
: intl.formatMessage(messages.jellyfinSettingsDescription, {
|
mediaServerName: 'Emby',
|
||||||
mediaServerName: 'Jellyfin',
|
})
|
||||||
})}
|
: intl.formatMessage(messages.jellyfinSettingsDescription, {
|
||||||
</p>
|
mediaServerName: 'Jellyfin',
|
||||||
</div>
|
})}
|
||||||
<Formik
|
</p>
|
||||||
initialValues={{
|
</div>
|
||||||
hostname: data?.ip,
|
<Formik
|
||||||
port: data?.port ?? 8096,
|
initialValues={{
|
||||||
useSsl: data?.useSsl,
|
hostname: data?.ip,
|
||||||
urlBase: data?.urlBase || '',
|
port: data?.port ?? 8096,
|
||||||
jellyfinExternalUrl: data?.externalHostname || '',
|
useSsl: data?.useSsl,
|
||||||
jellyfinForgotPasswordUrl: data?.jellyfinForgotPasswordUrl || '',
|
urlBase: data?.urlBase || '',
|
||||||
apiKey: data?.apiKey,
|
jellyfinExternalUrl: data?.externalHostname || '',
|
||||||
}}
|
jellyfinForgotPasswordUrl: data?.jellyfinForgotPasswordUrl || '',
|
||||||
validationSchema={JellyfinSettingsSchema}
|
}}
|
||||||
onSubmit={async (values) => {
|
validationSchema={JellyfinSettingsSchema}
|
||||||
try {
|
onSubmit={async (values) => {
|
||||||
const res = await fetch('/api/v1/settings/jellyfin', {
|
try {
|
||||||
method: 'POST',
|
await axios.post('/api/v1/settings/jellyfin', {
|
||||||
headers: {
|
ip: values.hostname,
|
||||||
'Content-Type': 'application/json',
|
port: Number(values.port),
|
||||||
},
|
useSsl: values.useSsl,
|
||||||
body: JSON.stringify({
|
urlBase: values.urlBase,
|
||||||
ip: values.hostname,
|
externalHostname: values.jellyfinExternalUrl,
|
||||||
port: Number(values.port),
|
jellyfinForgotPasswordUrl: values.jellyfinForgotPasswordUrl,
|
||||||
useSsl: values.useSsl,
|
} as JellyfinSettings);
|
||||||
urlBase: values.urlBase,
|
|
||||||
externalHostname: values.jellyfinExternalUrl,
|
|
||||||
jellyfinForgotPasswordUrl: values.jellyfinForgotPasswordUrl,
|
|
||||||
apiKey: values.apiKey,
|
|
||||||
} as JellyfinSettings),
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error(res.statusText, { cause: res });
|
|
||||||
|
|
||||||
addToast(
|
addToast(
|
||||||
intl.formatMessage(messages.jellyfinSettingsSuccess, {
|
intl.formatMessage(messages.jellyfinSettingsSuccess, {
|
||||||
mediaServerName:
|
mediaServerName:
|
||||||
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||||
? 'Emby'
|
? 'Emby'
|
||||||
: 'Jellyfin',
|
: 'Jellyfin',
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
autoDismiss: true,
|
autoDismiss: true,
|
||||||
appearance: 'success',
|
appearance: 'success',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
if (e.response?.data?.message === ApiErrorCode.InvalidUrl) {
|
||||||
|
addToast(
|
||||||
|
intl.formatMessage(messages.invalidurlerror, {
|
||||||
|
mediaServerName:
|
||||||
|
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||||
|
? 'Emby'
|
||||||
|
: 'Jellyfin',
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
autoDismiss: true,
|
||||||
|
appearance: 'error',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
addToast(
|
||||||
|
intl.formatMessage(messages.jellyfinSettingsFailure, {
|
||||||
|
mediaServerName:
|
||||||
|
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||||
|
? 'Emby'
|
||||||
|
: 'Jellyfin',
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
autoDismiss: true,
|
||||||
|
appearance: 'error',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
revalidate();
|
||||||
}
|
}
|
||||||
);
|
}}
|
||||||
} catch (e) {
|
>
|
||||||
let errorData;
|
{({
|
||||||
try {
|
errors,
|
||||||
errorData = await e.cause?.text();
|
touched,
|
||||||
errorData = JSON.parse(errorData);
|
values,
|
||||||
} catch {
|
setFieldValue,
|
||||||
/* empty */
|
handleSubmit,
|
||||||
}
|
isSubmitting,
|
||||||
if (errorData?.message === ApiErrorCode.InvalidUrl) {
|
isValid,
|
||||||
addToast(
|
}) => {
|
||||||
intl.formatMessage(messages.invalidurlerror, {
|
return (
|
||||||
mediaServerName:
|
<form className="section" onSubmit={handleSubmit}>
|
||||||
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
|
||||||
? 'Emby'
|
|
||||||
: 'Jellyfin',
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
autoDismiss: true,
|
|
||||||
appearance: 'error',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
addToast(
|
|
||||||
intl.formatMessage(messages.jellyfinSettingsFailure, {
|
|
||||||
mediaServerName:
|
|
||||||
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
|
||||||
? 'Emby'
|
|
||||||
: 'Jellyfin',
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
autoDismiss: true,
|
|
||||||
appearance: 'error',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
revalidate();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{({
|
|
||||||
errors,
|
|
||||||
touched,
|
|
||||||
values,
|
|
||||||
setFieldValue,
|
|
||||||
handleSubmit,
|
|
||||||
isSubmitting,
|
|
||||||
isValid,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<form className="section" onSubmit={handleSubmit}>
|
|
||||||
{showAdvancedSettings && (
|
|
||||||
<>
|
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<label htmlFor="hostname" className="text-label">
|
<label htmlFor="hostname" className="text-label">
|
||||||
{intl.formatMessage(messages.hostname)}
|
{intl.formatMessage(messages.hostname)}
|
||||||
@@ -622,29 +573,6 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<div className="form-row">
|
|
||||||
<label htmlFor="apiKey" className="text-label">
|
|
||||||
{intl.formatMessage(messages.apiKey)}
|
|
||||||
</label>
|
|
||||||
<div className="form-input-area">
|
|
||||||
<div className="form-input-field">
|
|
||||||
<SensitiveInput
|
|
||||||
as="field"
|
|
||||||
type="text"
|
|
||||||
inputMode="url"
|
|
||||||
id="apiKey"
|
|
||||||
name="apiKey"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{errors.apiKey && touched.apiKey && (
|
|
||||||
<div className="error">{errors.apiKey}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{showAdvancedSettings && (
|
|
||||||
<>
|
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<label htmlFor="urlBase" className="text-label">
|
<label htmlFor="urlBase" className="text-label">
|
||||||
{intl.formatMessage(messages.urlBase)}
|
{intl.formatMessage(messages.urlBase)}
|
||||||
@@ -665,73 +593,75 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
<div className="form-row">
|
||||||
)}
|
<label htmlFor="jellyfinExternalUrl" className="text-label">
|
||||||
<div className="form-row">
|
{intl.formatMessage(messages.externalUrl)}
|
||||||
<label htmlFor="jellyfinExternalUrl" className="text-label">
|
</label>
|
||||||
{intl.formatMessage(messages.externalUrl)}
|
<div className="form-input-area">
|
||||||
</label>
|
<div className="form-input-field">
|
||||||
<div className="form-input-area">
|
<Field
|
||||||
<div className="form-input-field">
|
type="text"
|
||||||
<Field
|
inputMode="url"
|
||||||
type="text"
|
id="jellyfinExternalUrl"
|
||||||
inputMode="url"
|
name="jellyfinExternalUrl"
|
||||||
id="jellyfinExternalUrl"
|
/>
|
||||||
name="jellyfinExternalUrl"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{errors.jellyfinExternalUrl &&
|
|
||||||
touched.jellyfinExternalUrl && (
|
|
||||||
<div className="error">{errors.jellyfinExternalUrl}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="form-row">
|
|
||||||
<label
|
|
||||||
htmlFor="jellyfinForgotPasswordUrl"
|
|
||||||
className="text-label"
|
|
||||||
>
|
|
||||||
{intl.formatMessage(messages.jellyfinForgotPasswordUrl)}
|
|
||||||
</label>
|
|
||||||
<div className="form-input-area">
|
|
||||||
<div className="form-input-field">
|
|
||||||
<Field
|
|
||||||
type="text"
|
|
||||||
inputMode="url"
|
|
||||||
id="jellyfinForgotPasswordUrl"
|
|
||||||
name="jellyfinForgotPasswordUrl"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{errors.jellyfinForgotPasswordUrl &&
|
|
||||||
touched.jellyfinForgotPasswordUrl && (
|
|
||||||
<div className="error">
|
|
||||||
{errors.jellyfinForgotPasswordUrl}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
{errors.jellyfinExternalUrl &&
|
||||||
</div>
|
touched.jellyfinExternalUrl && (
|
||||||
</div>
|
<div className="error">
|
||||||
<div className="actions">
|
{errors.jellyfinExternalUrl}
|
||||||
<div className="flex justify-end">
|
</div>
|
||||||
<span className="ml-3 inline-flex rounded-md shadow-sm">
|
)}
|
||||||
<Button
|
</div>
|
||||||
buttonType="primary"
|
</div>
|
||||||
type="submit"
|
<div className="form-row">
|
||||||
disabled={isSubmitting || !isValid}
|
<label
|
||||||
|
htmlFor="jellyfinForgotPasswordUrl"
|
||||||
|
className="text-label"
|
||||||
>
|
>
|
||||||
<ArrowDownOnSquareIcon />
|
{intl.formatMessage(messages.jellyfinForgotPasswordUrl)}
|
||||||
<span>
|
</label>
|
||||||
{isSubmitting
|
<div className="form-input-area">
|
||||||
? intl.formatMessage(globalMessages.saving)
|
<div className="form-input-field">
|
||||||
: intl.formatMessage(globalMessages.save)}
|
<Field
|
||||||
|
type="text"
|
||||||
|
inputMode="url"
|
||||||
|
id="jellyfinForgotPasswordUrl"
|
||||||
|
name="jellyfinForgotPasswordUrl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{errors.jellyfinForgotPasswordUrl &&
|
||||||
|
touched.jellyfinForgotPasswordUrl && (
|
||||||
|
<div className="error">
|
||||||
|
{errors.jellyfinForgotPasswordUrl}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="actions">
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<span className="ml-3 inline-flex rounded-md shadow-sm">
|
||||||
|
<Button
|
||||||
|
buttonType="primary"
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmitting || !isValid}
|
||||||
|
>
|
||||||
|
<ArrowDownOnSquareIcon />
|
||||||
|
<span>
|
||||||
|
{isSubmitting
|
||||||
|
? intl.formatMessage(globalMessages.saving)
|
||||||
|
: intl.formatMessage(globalMessages.save)}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</div>
|
||||||
</span>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
</div>
|
);
|
||||||
</form>
|
}}
|
||||||
);
|
</Formik>
|
||||||
}}
|
</>
|
||||||
</Formik>
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import type {
|
|||||||
CacheResponse,
|
CacheResponse,
|
||||||
} from '@server/interfaces/api/settingsInterfaces';
|
} from '@server/interfaces/api/settingsInterfaces';
|
||||||
import type { JobId } from '@server/lib/settings';
|
import type { JobId } from '@server/lib/settings';
|
||||||
|
import axios from 'axios';
|
||||||
import cronstrue from 'cronstrue/i18n';
|
import cronstrue from 'cronstrue/i18n';
|
||||||
import { Fragment, useReducer, useState } from 'react';
|
import { Fragment, useReducer, useState } from 'react';
|
||||||
import type { MessageDescriptor } from 'react-intl';
|
import type { MessageDescriptor } from 'react-intl';
|
||||||
@@ -81,7 +82,6 @@ const messages: { [messageName: string]: MessageDescriptor } = defineMessages(
|
|||||||
'When enabled in settings, Jellyseerr will proxy and cache images from pre-configured external sources. Cached images are saved into your config folder. You can find the files in <code>{appDataPath}/cache/images</code>.',
|
'When enabled in settings, Jellyseerr will proxy and cache images from pre-configured external sources. Cached images are saved into your config folder. You can find the files in <code>{appDataPath}/cache/images</code>.',
|
||||||
imagecachecount: 'Images Cached',
|
imagecachecount: 'Images Cached',
|
||||||
imagecachesize: 'Total Cache Size',
|
imagecachesize: 'Total Cache Size',
|
||||||
useravatars: 'User Avatars',
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -173,10 +173,7 @@ const SettingsJobs = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const runJob = async (job: Job) => {
|
const runJob = async (job: Job) => {
|
||||||
const res = await fetch(`/api/v1/settings/jobs/${job.id}/run`, {
|
await axios.post(`/api/v1/settings/jobs/${job.id}/run`);
|
||||||
method: 'POST',
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
addToast(
|
addToast(
|
||||||
intl.formatMessage(messages.jobstarted, {
|
intl.formatMessage(messages.jobstarted, {
|
||||||
jobname: intl.formatMessage(messages[job.id] ?? messages.unknownJob),
|
jobname: intl.formatMessage(messages[job.id] ?? messages.unknownJob),
|
||||||
@@ -190,10 +187,7 @@ const SettingsJobs = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const cancelJob = async (job: Job) => {
|
const cancelJob = async (job: Job) => {
|
||||||
const res = await fetch(`/api/v1/settings/jobs/${job.id}/cancel`, {
|
await axios.post(`/api/v1/settings/jobs/${job.id}/cancel`);
|
||||||
method: 'POST',
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
addToast(
|
addToast(
|
||||||
intl.formatMessage(messages.jobcancelled, {
|
intl.formatMessage(messages.jobcancelled, {
|
||||||
jobname: intl.formatMessage(messages[job.id] ?? messages.unknownJob),
|
jobname: intl.formatMessage(messages[job.id] ?? messages.unknownJob),
|
||||||
@@ -207,10 +201,7 @@ const SettingsJobs = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const flushCache = async (cache: CacheItem) => {
|
const flushCache = async (cache: CacheItem) => {
|
||||||
const res = await fetch(`/api/v1/settings/cache/${cache.id}/flush`, {
|
await axios.post(`/api/v1/settings/cache/${cache.id}/flush`);
|
||||||
method: 'POST',
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
addToast(
|
addToast(
|
||||||
intl.formatMessage(messages.cacheflushed, { cachename: cache.name }),
|
intl.formatMessage(messages.cacheflushed, { cachename: cache.name }),
|
||||||
{
|
{
|
||||||
@@ -237,19 +228,12 @@ const SettingsJobs = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
const res = await fetch(
|
await axios.post(
|
||||||
`/api/v1/settings/jobs/${jobModalState.job.id}/schedule`,
|
`/api/v1/settings/jobs/${jobModalState.job.id}/schedule`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
schedule: jobScheduleCron.join(' '),
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
schedule: jobScheduleCron.join(' '),
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
|
|
||||||
addToast(intl.formatMessage(messages.jobScheduleEditSaved), {
|
addToast(intl.formatMessage(messages.jobScheduleEditSaved), {
|
||||||
appearance: 'success',
|
appearance: 'success',
|
||||||
@@ -559,19 +543,6 @@ const SettingsJobs = () => {
|
|||||||
{formatBytes(cacheData?.imageCache.tmdb.size ?? 0)}
|
{formatBytes(cacheData?.imageCache.tmdb.size ?? 0)}
|
||||||
</Table.TD>
|
</Table.TD>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<Table.TD>
|
|
||||||
{intl.formatMessage(messages.useravatars)} (avatar)
|
|
||||||
</Table.TD>
|
|
||||||
<Table.TD>
|
|
||||||
{intl.formatNumber(
|
|
||||||
cacheData?.imageCache.avatar.imageCount ?? 0
|
|
||||||
)}
|
|
||||||
</Table.TD>
|
|
||||||
<Table.TD>
|
|
||||||
{formatBytes(cacheData?.imageCache.avatar.size ?? 0)}
|
|
||||||
</Table.TD>
|
|
||||||
</tr>
|
|
||||||
</Table.TBody>
|
</Table.TBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline';
|
|||||||
import { ArrowPathIcon } from '@heroicons/react/24/solid';
|
import { ArrowPathIcon } from '@heroicons/react/24/solid';
|
||||||
import type { UserSettingsGeneralResponse } from '@server/interfaces/api/userSettingsInterfaces';
|
import type { UserSettingsGeneralResponse } from '@server/interfaces/api/userSettingsInterfaces';
|
||||||
import type { MainSettings } from '@server/lib/settings';
|
import type { MainSettings } from '@server/lib/settings';
|
||||||
|
import axios from 'axios';
|
||||||
import { Field, Form, Formik } from 'formik';
|
import { Field, Form, Formik } from 'formik';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import { useToasts } from 'react-toast-notifications';
|
import { useToasts } from 'react-toast-notifications';
|
||||||
@@ -86,10 +87,7 @@ const SettingsMain = () => {
|
|||||||
|
|
||||||
const regenerate = async () => {
|
const regenerate = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/v1/settings/main/regenerate', {
|
await axios.post('/api/v1/settings/main/regenerate');
|
||||||
method: 'POST',
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
|
|
||||||
revalidate();
|
revalidate();
|
||||||
addToast(intl.formatMessage(messages.toastApiKeySuccess), {
|
addToast(intl.formatMessage(messages.toastApiKeySuccess), {
|
||||||
@@ -142,25 +140,18 @@ const SettingsMain = () => {
|
|||||||
validationSchema={MainSettingsSchema}
|
validationSchema={MainSettingsSchema}
|
||||||
onSubmit={async (values) => {
|
onSubmit={async (values) => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/v1/settings/main', {
|
await axios.post('/api/v1/settings/main', {
|
||||||
method: 'POST',
|
applicationTitle: values.applicationTitle,
|
||||||
headers: {
|
applicationUrl: values.applicationUrl,
|
||||||
'Content-Type': 'application/json',
|
csrfProtection: values.csrfProtection,
|
||||||
},
|
hideAvailable: values.hideAvailable,
|
||||||
body: JSON.stringify({
|
locale: values.locale,
|
||||||
applicationTitle: values.applicationTitle,
|
region: values.region,
|
||||||
applicationUrl: values.applicationUrl,
|
originalLanguage: values.originalLanguage,
|
||||||
csrfProtection: values.csrfProtection,
|
partialRequestsEnabled: values.partialRequestsEnabled,
|
||||||
hideAvailable: values.hideAvailable,
|
trustProxy: values.trustProxy,
|
||||||
locale: values.locale,
|
cacheImages: values.cacheImages,
|
||||||
region: values.region,
|
|
||||||
originalLanguage: values.originalLanguage,
|
|
||||||
partialRequestsEnabled: values.partialRequestsEnabled,
|
|
||||||
trustProxy: values.trustProxy,
|
|
||||||
cacheImages: values.cacheImages,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
mutate('/api/v1/settings/public');
|
mutate('/api/v1/settings/public');
|
||||||
mutate('/api/v1/status');
|
mutate('/api/v1/status');
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
} from '@heroicons/react/24/solid';
|
} from '@heroicons/react/24/solid';
|
||||||
import type { PlexDevice } from '@server/interfaces/api/plexInterfaces';
|
import type { PlexDevice } from '@server/interfaces/api/plexInterfaces';
|
||||||
import type { PlexSettings, TautulliSettings } from '@server/lib/settings';
|
import type { PlexSettings, TautulliSettings } from '@server/lib/settings';
|
||||||
|
import axios from 'axios';
|
||||||
import { Field, Formik } from 'formik';
|
import { Field, Formik } from 'formik';
|
||||||
import { orderBy } from 'lodash';
|
import { orderBy } from 'lodash';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
@@ -240,15 +241,9 @@ const SettingsPlex = ({ onComplete }: SettingsPlexProps) => {
|
|||||||
params.enable = activeLibraries.join(',');
|
params.enable = activeLibraries.join(',');
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchParams = new URLSearchParams({
|
await axios.get('/api/v1/settings/plex/library', {
|
||||||
sync: params.sync ? 'true' : 'false',
|
params,
|
||||||
...(params.enable ? { enable: params.enable } : {}),
|
|
||||||
});
|
});
|
||||||
const res = await fetch(
|
|
||||||
`/api/v1/settings/plex/library?${searchParams.toString()}`
|
|
||||||
);
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
|
|
||||||
setIsSyncing(false);
|
setIsSyncing(false);
|
||||||
revalidate();
|
revalidate();
|
||||||
};
|
};
|
||||||
@@ -267,12 +262,11 @@ const SettingsPlex = ({ onComplete }: SettingsPlexProps) => {
|
|||||||
toastId = id;
|
toastId = id;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const res = await fetch('/api/v1/settings/plex/devices/servers');
|
const response = await axios.get<PlexDevice[]>(
|
||||||
if (!res.ok) throw new Error();
|
'/api/v1/settings/plex/devices/servers'
|
||||||
const data: PlexDevice[] = await res.json();
|
);
|
||||||
|
if (response.data) {
|
||||||
if (data) {
|
setAvailableServers(response.data);
|
||||||
setAvailableServers(data);
|
|
||||||
}
|
}
|
||||||
if (toastId) {
|
if (toastId) {
|
||||||
removeToast(toastId);
|
removeToast(toastId);
|
||||||
@@ -295,30 +289,16 @@ const SettingsPlex = ({ onComplete }: SettingsPlexProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const startScan = async () => {
|
const startScan = async () => {
|
||||||
const res = await fetch('/api/v1/settings/plex/sync', {
|
await axios.post('/api/v1/settings/plex/sync', {
|
||||||
method: 'POST',
|
start: true,
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
start: true,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
revalidateSync();
|
revalidateSync();
|
||||||
};
|
};
|
||||||
|
|
||||||
const cancelScan = async () => {
|
const cancelScan = async () => {
|
||||||
const res = await fetch('/api/v1/settings/plex/sync', {
|
await axios.post('/api/v1/settings/plex/sync', {
|
||||||
method: 'POST',
|
cancel: true,
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
cancel: true,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
revalidateSync();
|
revalidateSync();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -333,19 +313,15 @@ const SettingsPlex = ({ onComplete }: SettingsPlexProps) => {
|
|||||||
.join(',');
|
.join(',');
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchParams = new URLSearchParams(params.enable ? params : {});
|
await axios.get('/api/v1/settings/plex/library', {
|
||||||
const res = await fetch(
|
params,
|
||||||
`/api/v1/settings/plex/library?${searchParams.toString()}`
|
});
|
||||||
);
|
} else {
|
||||||
if (!res.ok) throw new Error();
|
await axios.get('/api/v1/settings/plex/library', {
|
||||||
} else {
|
params: {
|
||||||
const searchParams = new URLSearchParams({
|
enable: [...activeLibraries, libraryId].join(','),
|
||||||
enable: [...activeLibraries, libraryId].join(','),
|
},
|
||||||
});
|
});
|
||||||
const res = await fetch(
|
|
||||||
`/api/v1/settings/plex/library?${searchParams.toString()}`
|
|
||||||
);
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
}
|
}
|
||||||
setIsSyncing(false);
|
setIsSyncing(false);
|
||||||
revalidate();
|
revalidate();
|
||||||
@@ -409,19 +385,12 @@ const SettingsPlex = ({ onComplete }: SettingsPlexProps) => {
|
|||||||
toastId = id;
|
toastId = id;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const res = await fetch('/api/v1/settings/plex', {
|
await axios.post('/api/v1/settings/plex', {
|
||||||
method: 'POST',
|
ip: values.hostname,
|
||||||
headers: {
|
port: Number(values.port),
|
||||||
'Content-Type': 'application/json',
|
useSsl: values.useSsl,
|
||||||
},
|
webAppUrl: values.webAppUrl,
|
||||||
body: JSON.stringify({
|
} as PlexSettings);
|
||||||
ip: values.hostname,
|
|
||||||
port: Number(values.port),
|
|
||||||
useSsl: values.useSsl,
|
|
||||||
webAppUrl: values.webAppUrl,
|
|
||||||
} as PlexSettings),
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
|
|
||||||
syncLibraries();
|
syncLibraries();
|
||||||
|
|
||||||
@@ -779,27 +748,14 @@ const SettingsPlex = ({ onComplete }: SettingsPlexProps) => {
|
|||||||
validationSchema={TautulliSettingsSchema}
|
validationSchema={TautulliSettingsSchema}
|
||||||
onSubmit={async (values) => {
|
onSubmit={async (values) => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/v1/settings/tautulli', {
|
await axios.post('/api/v1/settings/tautulli', {
|
||||||
method: 'POST',
|
hostname: values.tautulliHostname,
|
||||||
headers: {
|
port: Number(values.tautulliPort),
|
||||||
'Content-Type': 'application/json',
|
useSsl: values.tautulliUseSsl,
|
||||||
},
|
urlBase: values.tautulliUrlBase,
|
||||||
body: JSON.stringify({
|
apiKey: values.tautulliApiKey,
|
||||||
hostname: values.tautulliHostname,
|
externalUrl: values.tautulliExternalUrl,
|
||||||
port: Number(values.tautulliPort),
|
} as TautulliSettings);
|
||||||
useSsl: values.tautulliUseSsl,
|
|
||||||
urlBase: values.tautulliUrlBase,
|
|
||||||
apiKey: values.tautulliApiKey,
|
|
||||||
externalUrl: values.tautulliExternalUrl,
|
|
||||||
} as TautulliSettings),
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error('Failed to fetch');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Continue with any necessary processing
|
|
||||||
|
|
||||||
addToast(
|
addToast(
|
||||||
intl.formatMessage(messages.toastTautulliSettingsSuccess),
|
intl.formatMessage(messages.toastTautulliSettingsSuccess),
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user