Compare commits

...

9 Commits

Author SHA1 Message Date
gauthier-th
6315ba5246 fix: resolve review comments 2025-04-07 11:23:08 +02:00
Gauthier
f6b0147049 fix: correct rebase issue 2025-04-07 11:23:07 +02:00
gauthier-th
f50f4af4ad fix: add back custom proxy agent 2025-04-07 11:23:07 +02:00
gauthier-th
97784e568a fix: add back custom proxy agent 2025-04-07 11:23:06 +02:00
Gauthier
470c96a4a0 style: run prettier 2025-04-07 11:23:05 +02:00
Gauthier
a46860ed15 style: run prettier 2025-04-07 11:23:05 +02:00
Gauthier
5c0d5262d5 fix: rewrite error handling for Axios and remove IPv4 first setting 2025-04-07 11:23:04 +02:00
gauthier-th
e3e5c2028e fix: remove unwanted changes 2025-04-07 11:23:03 +02:00
Gauthier
f03c85ad34 refactor: switch from Fetch API to Axios 2025-04-07 11:23:01 +02:00
112 changed files with 1654 additions and 3032 deletions

View File

@@ -23,7 +23,6 @@
"mediaServerType": 1, "mediaServerType": 1,
"partialRequestsEnabled": true, "partialRequestsEnabled": true,
"enableSpecialEpisodes": false, "enableSpecialEpisodes": false,
"forceIpv4First": false,
"locale": "en" "locale": "en"
}, },
"plex": { "plex": {

View File

@@ -97,37 +97,7 @@ You can try them all and see which one works for your network.
</TabItem> </TabItem>
</Tabs> </Tabs>
### Option 2: Force IPV4 resolution first ### Option 2: Use Jellyseerr through a proxy
Sometimes there are configuration issues with IPV6 that prevent the hostname resolution from working correctly.
You can try to force the resolution to use IPV4 first by going to `Settings > Networking > Advanced Networking` and enabling `Force IPv4 Resolution First` setting and restarting. You can also add the environment variable, `FORCE_IPV4_FIRST=true`:
<Tabs groupId="methods" queryString>
<TabItem value="docker-cli" label="Docker CLI">
Add the following to your `docker run` command:
```bash
-e "FORCE_IPV4_FIRST=true"
```
</TabItem>
<TabItem value="docker-compose" label="Docker Compose">
Add the following to your `compose.yaml`:
```yaml
---
services:
jellyseerr:
environment:
- FORCE_IPV4_FIRST=true
```
</TabItem>
</Tabs>
### Option 3: Use Jellyseerr through a proxy
If you can't change your DNS servers or force IPV4 resolution, you can use Jellyseerr through a proxy. If you can't change your DNS servers or force IPV4 resolution, you can use Jellyseerr through a proxy.

View File

@@ -191,9 +191,6 @@ components:
csrfProtection: csrfProtection:
type: boolean type: boolean
example: false example: false
forceIpv4First:
type: boolean
example: false
trustProxy: trustProxy:
type: boolean type: boolean
example: true example: true

View File

@@ -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',
}, },
images: { images: {
remotePatterns: [ remotePatterns: [

View File

@@ -46,6 +46,8 @@
"@types/wink-jaro-distance": "^2.0.2", "@types/wink-jaro-distance": "^2.0.2",
"@types/ua-parser-js": "^0.7.36", "@types/ua-parser-js": "^0.7.36",
"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",

52
pnpm-lock.yaml generated
View File

@@ -50,6 +50,12 @@ importers:
ace-builds: ace-builds:
specifier: 1.15.2 specifier: 1.15.2
version: 1.15.2 version: 1.15.2
axios:
specifier: 1.3.4
version: 1.3.4
axios-rate-limit:
specifier: 1.3.0
version: 1.3.0(axios@1.3.4)
bcrypt: bcrypt:
specifier: 5.1.0 specifier: 5.1.0
version: 5.1.0(encoding@0.1.13) version: 5.1.0(encoding@0.1.13)
@@ -3873,6 +3879,14 @@ packages:
resolution: {integrity: sha512-QbUdXJVTpvUTHU7871ppZkdOLBeGUKBQWHkHrvN2V9IQWGMt61zf3B45BtzjxEJzYuj0JBjBZP/hmYS/R9pmAw==} resolution: {integrity: sha512-QbUdXJVTpvUTHU7871ppZkdOLBeGUKBQWHkHrvN2V9IQWGMt61zf3B45BtzjxEJzYuj0JBjBZP/hmYS/R9pmAw==}
engines: {node: '>=4'} engines: {node: '>=4'}
axios-rate-limit@1.3.0:
resolution: {integrity: sha512-cKR5wTbU/CeeyF1xVl5hl6FlYsmzDVqxlN4rGtfO5x7J83UxKDckudsW0yW21/ZJRcO0Qrfm3fUFbhEbWTLayw==}
peerDependencies:
axios: '*'
axios@1.3.4:
resolution: {integrity: sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==}
axobject-query@3.1.1: axobject-query@3.1.1:
resolution: {integrity: sha512-goKlv8DZrK9hUh975fnHzhNIO4jUnFCfv/dszV5VwUGDFjI6vQ2VwoyjYjYNEbBE8AH87TduWP5uyDR1D+Iteg==} resolution: {integrity: sha512-goKlv8DZrK9hUh975fnHzhNIO4jUnFCfv/dszV5VwUGDFjI6vQ2VwoyjYjYNEbBE8AH87TduWP5uyDR1D+Iteg==}
@@ -5385,6 +5399,15 @@ packages:
fn.name@1.1.0: fn.name@1.1.0:
resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==}
follow-redirects@1.15.9:
resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
peerDependenciesMeta:
debug:
optional: true
for-each@0.3.3: for-each@0.3.3:
resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
@@ -7849,6 +7872,9 @@ packages:
proxy-from-env@1.0.0: proxy-from-env@1.0.0:
resolution: {integrity: sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==} resolution: {integrity: sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==}
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
psl@1.15.0: psl@1.15.0:
resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==}
@@ -14285,6 +14311,18 @@ snapshots:
axe-core@4.9.1: {} axe-core@4.9.1: {}
axios-rate-limit@1.3.0(axios@1.3.4):
dependencies:
axios: 1.3.4
axios@1.3.4:
dependencies:
follow-redirects: 1.15.9
form-data: 4.0.2
proxy-from-env: 1.1.0
transitivePeerDependencies:
- debug
axobject-query@3.1.1: axobject-query@3.1.1:
dependencies: dependencies:
deep-equal: 2.2.3 deep-equal: 2.2.3
@@ -15647,7 +15685,7 @@ snapshots:
es-get-iterator@1.1.3: es-get-iterator@1.1.3:
dependencies: dependencies:
call-bind: 1.0.7 call-bind: 1.0.7
get-intrinsic: 1.2.4 get-intrinsic: 1.3.0
has-symbols: 1.0.3 has-symbols: 1.0.3
is-arguments: 1.1.1 is-arguments: 1.1.1
is-map: 2.0.3 is-map: 2.0.3
@@ -15756,7 +15794,7 @@ snapshots:
debug: 4.3.5 debug: 4.3.5
enhanced-resolve: 5.17.0 enhanced-resolve: 5.17.0
eslint: 8.35.0 eslint: 8.35.0
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.35.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.35.0) eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.35.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.35.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.35.0))(eslint@8.35.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.54.0(eslint@8.35.0)(typescript@4.9.5))(eslint@8.35.0) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.54.0(eslint@8.35.0)(typescript@4.9.5))(eslint@8.35.0)
fast-glob: 3.3.2 fast-glob: 3.3.2
get-tsconfig: 4.7.5 get-tsconfig: 4.7.5
@@ -15778,7 +15816,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
eslint-module-utils@2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.35.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.35.0): eslint-module-utils@2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.35.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.35.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.35.0))(eslint@8.35.0):
dependencies: dependencies:
debug: 3.2.7(supports-color@8.1.1) debug: 3.2.7(supports-color@8.1.1)
optionalDependencies: optionalDependencies:
@@ -16339,6 +16377,8 @@ snapshots:
fn.name@1.1.0: {} fn.name@1.1.0: {}
follow-redirects@1.15.9: {}
for-each@0.3.3: for-each@0.3.3:
dependencies: dependencies:
is-callable: 1.2.7 is-callable: 1.2.7
@@ -17113,7 +17153,7 @@ snapshots:
is-weakset@2.0.3: is-weakset@2.0.3:
dependencies: dependencies:
call-bind: 1.0.7 call-bind: 1.0.7
get-intrinsic: 1.2.4 get-intrinsic: 1.3.0
is-windows@1.0.2: {} is-windows@1.0.2: {}
@@ -19015,6 +19055,8 @@ snapshots:
proxy-from-env@1.0.0: {} proxy-from-env@1.0.0: {}
proxy-from-env@1.1.0: {}
psl@1.15.0: psl@1.15.0:
dependencies: dependencies:
punycode: 2.3.1 punycode: 2.3.1
@@ -19525,7 +19567,7 @@ snapshots:
define-properties: 1.2.1 define-properties: 1.2.1
es-abstract: 1.23.3 es-abstract: 1.23.3
es-errors: 1.3.0 es-errors: 1.3.0
get-intrinsic: 1.2.4 get-intrinsic: 1.3.0
globalthis: 1.0.4 globalthis: 1.0.4
which-builtin-type: 1.1.3 which-builtin-type: 1.1.3

View File

@@ -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,14 @@ 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, reject) => { 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); writer.on('error', reject);
if (!response.body) return reject(); response.data.pipe(writer);
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}`);

View File

@@ -1,7 +1,6 @@
import { MediaServerType } from '@server/constants/server'; import type { AxiosInstance, AxiosRequestConfig } from 'axios';
import { getSettings } from '@server/lib/settings'; import axios from 'axios';
import type { RateLimitOptions } from '@server/utils/rateLimit'; import rateLimit from 'axios-rate-limit';
import rateLimit from '@server/utils/rateLimit';
import type NodeCache from 'node-cache'; import type NodeCache from 'node-cache';
// 5 minute default TTL (in seconds) // 5 minute default TTL (in seconds)
@@ -13,109 +12,75 @@ 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,
} });
const url = new URL(baseUrl);
const settings = getSettings();
this.defaultHeaders = {
'Content-Type': 'application/json',
Accept: 'application/json',
...((url.username || url.password) && {
Authorization: `Basic ${Buffer.from(
`${url.username}:${url.password}`
).toString('base64')}`,
}),
...(settings.main.mediaServerType === MediaServerType.EMBY && {
'Accept-Encoding': 'gzip',
}),
...options.headers,
};
if (url.username || url.password) {
url.username = '';
url.password = '';
baseUrl = url.toString();
} }
this.baseUrl = baseUrl; this.baseUrl = baseUrl;
this.params = params;
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 headers = { ...this.defaultHeaders, ...config?.headers };
const cacheKey = this.serializeCacheKey(endpoint, { const cacheKey = this.serializeCacheKey(endpoint, {
...this.params, ...config?.params,
...params, headers: config?.headers,
headers,
}); });
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,
});
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 && ttl !== 0) { if (this.cache && ttl !== 0) {
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, 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 headers = { ...this.defaultHeaders, ...config?.headers };
const cacheKey = this.serializeCacheKey(endpoint, { const cacheKey = this.serializeCacheKey(endpoint, {
config: { ...this.params, ...params }, config: config?.params,
headers, ...(data ? { data } : {}),
data,
}); });
const cachedItem = this.cache?.get<T>(cacheKey); const cachedItem = this.cache?.get<T>(cacheKey);
@@ -123,115 +88,23 @@ 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: 'POST',
...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 && ttl !== 0) { if (this.cache && ttl !== 0) {
this.cache.set(cacheKey, resData, ttl ?? DEFAULT_TTL); this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
} }
return resData; return response.data;
}
protected async put<T>(
endpoint: string,
data: Record<string, unknown>,
params?: Record<string, string>,
ttl?: number,
config?: RequestInit
): Promise<T> {
const headers = { ...this.defaultHeaders, ...config?.headers };
const cacheKey = this.serializeCacheKey(endpoint, {
config: { ...this.params, ...params },
data,
headers,
});
const cachedItem = this.cache?.get<T>(cacheKey);
if (cachedItem) {
return cachedItem;
}
const url = this.formatUrl(endpoint, params);
const response = await this.fetch(url, {
method: 'PUT',
...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 && ttl !== 0) {
this.cache.set(cacheKey, resData, ttl ?? DEFAULT_TTL);
}
return resData;
}
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 headers = { ...this.defaultHeaders, ...config?.headers };
const cacheKey = this.serializeCacheKey(endpoint, { const cacheKey = this.serializeCacheKey(endpoint, {
...this.params, ...config?.params,
...params, headers: config?.headers,
headers,
}); });
const cachedItem = this.cache?.get<T>(cacheKey); const cachedItem = this.cache?.get<T>(cacheKey);
@@ -243,82 +116,29 @@ 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,
}).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,
});
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 && ttl !== 0) {
this.cache.set(cacheKey, data, ttl ?? DEFAULT_TTL); this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
} }
return data; return response.data;
} }
protected removeCache(endpoint: string, options?: Record<string, unknown>) { protected removeCache(endpoint: string, options?: Record<string, unknown>) {
const cacheKey = this.serializeCacheKey(endpoint, { const cacheKey = this.serializeCacheKey(endpoint, {
...this.params,
...options, ...options,
}); });
this.cache?.del(cacheKey); this.cache?.del(cacheKey);
} }
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(
endpoint: string, endpoint: string,
options?: Record<string, unknown> options?: Record<string, unknown>
@@ -329,29 +149,6 @@ class ExternalAPI {
return `${this.baseUrl}${endpoint}${JSON.stringify(options)}`; return `${this.baseUrl}${endpoint}${JSON.stringify(options)}`;
} }
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;

View File

@@ -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,
},
} }
); );

View File

@@ -130,6 +130,8 @@ class JellyfinAPI extends ExternalAPI {
{ {
headers: { headers: {
'X-Emby-Authorization': authHeaderVal, 'X-Emby-Authorization': authHeaderVal,
'Content-Type': 'application/json',
Accept: 'application/json',
}, },
} }
); );
@@ -143,7 +145,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>(
@@ -152,8 +154,6 @@ class JellyfinAPI extends ExternalAPI {
Username, Username,
Pw: Password, Pw: Password,
}, },
{},
undefined,
{ headers } { headers }
); );
}; };
@@ -163,36 +163,36 @@ class JellyfinAPI extends ExternalAPI {
} catch (e) { } catch (e) {
logger.debug('Failed to authenticate with headers', { logger.debug('Failed to authenticate with headers', {
label: 'Jellyfin API', label: 'Jellyfin API',
error: e.cause.message ?? e.cause.statusText, error: e.response?.statusText,
ip: ClientIP, ip: ClientIP,
}); });
if (!e.cause.status) { if (!e.response?.status) {
throw new ApiError(404, ApiErrorCode.InvalidUrl); throw new ApiError(404, ApiErrorCode.InvalidUrl);
} }
if (e.cause.status === 401) { if (e.response?.status === 401) {
throw new ApiError(e.cause.status, ApiErrorCode.InvalidCredentials); throw new ApiError(e.response?.status, ApiErrorCode.InvalidCredentials);
} }
} }
try { try {
return await authenticate(false); return await authenticate(false);
} catch (e) { } catch (e) {
if (e.cause.status === 401) { if (e.response?.status === 401) {
throw new ApiError(e.cause.status, ApiErrorCode.InvalidCredentials); throw new ApiError(e.response?.status, ApiErrorCode.InvalidCredentials);
} }
logger.error( logger.error(
'Something went wrong while authenticating with the Jellyfin server', `Something went wrong while authenticating with the Jellyfin server: ${e.message}`,
{ {
label: 'Jellyfin API', label: 'Jellyfin API',
error: e.cause.message ?? e.cause.statusText, error: e.response?.status,
ip: ClientIP, ip: ClientIP,
} }
); );
throw new ApiError(e.cause.status, ApiErrorCode.Unknown); throw new ApiError(e.response?.status, ApiErrorCode.Unknown);
} }
} }
@@ -207,7 +207,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);
} }
} }
@@ -220,11 +220,11 @@ class JellyfinAPI extends ExternalAPI {
return serverResponse.ServerName; return serverResponse.ServerName;
} catch (e) { } catch (e) {
logger.error( logger.error(
'Something went wrong while getting the server name from the Jellyfin server', `Something went wrong while getting the server name from the Jellyfin server: ${e.message}`,
{ label: 'Jellyfin API', error: e.cause.message ?? e.cause.statusText } { label: 'Jellyfin API', error: e.response?.status }
); );
throw new ApiError(e.cause?.status, ApiErrorCode.Unknown); throw new ApiError(e.response?.status, ApiErrorCode.Unknown);
} }
} }
@@ -235,11 +235,11 @@ class JellyfinAPI extends ExternalAPI {
return { users: userReponse }; return { users: userReponse };
} catch (e) { } catch (e) {
logger.error( logger.error(
'Something went wrong while getting the account from the Jellyfin server', `Something went wrong while getting the account from the Jellyfin server: ${e.message}`,
{ label: 'Jellyfin API', error: e.cause.message ?? e.cause.statusText } { label: 'Jellyfin API', error: e.response?.status }
); );
throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken); throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
} }
} }
@@ -251,11 +251,11 @@ class JellyfinAPI extends ExternalAPI {
return userReponse; return userReponse;
} catch (e) { } catch (e) {
logger.error( logger.error(
'Something went wrong while getting the account from the Jellyfin server', `Something went wrong while getting the account from the Jellyfin server: ${e.message}`,
{ label: 'Jellyfin API', error: e.cause.message ?? e.cause.statusText } { label: 'Jellyfin API', error: e.response?.status }
); );
throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken); throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
} }
} }
@@ -275,10 +275,10 @@ class JellyfinAPI extends ExternalAPI {
return this.mapLibraries(mediaFolderResponse.Items); return this.mapLibraries(mediaFolderResponse.Items);
} catch (e) { } catch (e) {
logger.error( logger.error(
'Something went wrong while getting libraries from the Jellyfin server', `Something went wrong while getting libraries from the Jellyfin server: ${e.message}`,
{ {
label: 'Jellyfin API', label: 'Jellyfin API',
error: e.cause.message ?? e.cause.statusText, error: e.response?.status,
} }
); );
@@ -315,26 +315,20 @@ 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>(`/Items`, { const libraryItemsResponse = await this.get<any>(
SortBy: 'SortName', `/Items?SortBy=SortName&SortOrder=Ascending&IncludeItemTypes=Series,Movie,Others&Recursive=true&StartIndex=0&ParentId=${id}&collapseBoxSetItems=false`
SortOrder: 'Ascending', );
IncludeItemTypes: 'Series,Movie,Others',
Recursive: 'true',
StartIndex: '0',
ParentId: id,
collapseBoxSetItems: 'false',
});
return libraryItemsResponse.Items.filter( return libraryItemsResponse.Items.filter(
(item: JellyfinLibraryItem) => item.LocationType !== 'Virtual' (item: JellyfinLibraryItem) => item.LocationType !== 'Virtual'
); );
} catch (e) { } catch (e) {
logger.error( logger.error(
'Something went wrong while getting library content from the Jellyfin server', `Something went wrong while getting library content from the Jellyfin server: ${e.message}`,
{ label: 'Jellyfin API', error: e.cause.message ?? e.cause.statusText } { label: 'Jellyfin API', error: e?.response?.status }
); );
throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken); throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
} }
} }
@@ -344,27 +338,22 @@ class JellyfinAPI extends ExternalAPI {
this.mediaServerType === MediaServerType.JELLYFIN this.mediaServerType === MediaServerType.JELLYFIN
? `/Items/Latest` ? `/Items/Latest`
: `/Users/${this.userId}/Items/Latest`; : `/Users/${this.userId}/Items/Latest`;
const itemResponse = await this.get<any>(
const baseParams = { `${endpoint}?Limit=12&ParentId=${id}${
Limit: '12', this.mediaServerType === MediaServerType.JELLYFIN
ParentId: id, ? `&userId=${this.userId ?? 'Me'}`
}; : ''
}`
const params = );
this.mediaServerType === MediaServerType.JELLYFIN
? { ...baseParams, userId: this.userId ?? `Me` }
: baseParams;
const itemResponse = await this.get<any>(endpoint, params);
return itemResponse; return itemResponse;
} catch (e) { } catch (e) {
logger.error( logger.error(
'Something went wrong while getting library content from the Jellyfin server', `Something went wrong while getting library content from the Jellyfin server: ${e.message}`,
{ label: 'Jellyfin API', error: e.cause.message ?? e.cause.statusText } { label: 'Jellyfin API', error: e.response?.status }
); );
throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken); throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
} }
} }
@@ -373,23 +362,25 @@ class JellyfinAPI extends ExternalAPI {
): Promise<JellyfinLibraryItemExtended | undefined> { ): Promise<JellyfinLibraryItemExtended | undefined> {
try { try {
const itemResponse = await this.get<JellyfinItemsReponse>(`/Items`, { const itemResponse = await this.get<JellyfinItemsReponse>(`/Items`, {
ids: id, params: {
fields: 'ProviderIds,MediaSources,Width,Height,IsHD,DateCreated', ids: id,
fields: 'ProviderIds,MediaSources,Width,Height,IsHD,DateCreated',
},
}); });
return itemResponse.Items?.[0]; return itemResponse.Items?.[0];
} catch (e) { } catch (e) {
if (availabilitySync.running) { if (availabilitySync.running) {
if (e.cause?.status === 500) { if (e.response?.status === 500) {
return undefined; return undefined;
} }
} }
logger.error( logger.error(
'Something went wrong while getting library content from the Jellyfin server', `Something went wrong while getting library content from the Jellyfin server: ${e.message}`,
{ label: 'Jellyfin API', error: e.cause.message ?? e.cause.statusText } { label: 'Jellyfin API', error: e.response?.status }
); );
throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken); throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
} }
} }
@@ -400,11 +391,11 @@ class JellyfinAPI extends ExternalAPI {
return seasonResponse.Items; return seasonResponse.Items;
} catch (e) { } catch (e) {
logger.error( logger.error(
'Something went wrong while getting the list of seasons from the Jellyfin server', `Something went wrong while getting the list of seasons from the Jellyfin server: ${e.message}`,
{ label: 'Jellyfin API', error: e.cause.message ?? e.cause.statusText } { label: 'Jellyfin API', error: e.response?.status }
); );
throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken); throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
} }
} }
@@ -414,10 +405,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(
@@ -425,11 +413,11 @@ class JellyfinAPI extends ExternalAPI {
); );
} catch (e) { } catch (e) {
logger.error( logger.error(
'Something went wrong while getting the list of episodes from the Jellyfin server', `Something went wrong while getting the list of episodes from the Jellyfin server: ${e.message}`,
{ label: 'Jellyfin API', error: e.cause.message ?? e.cause.statusText } { label: 'Jellyfin API', error: e.response?.status }
); );
throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken); throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
} }
} }
@@ -442,8 +430,8 @@ class JellyfinAPI extends ExternalAPI {
).AccessToken; ).AccessToken;
} catch (e) { } catch (e) {
logger.error( logger.error(
'Something went wrong while creating an API key from the Jellyfin server', `Something went wrong while creating an API key from the Jellyfin server: ${e.message}`,
{ label: 'Jellyfin API', error: e.cause.message ?? e.cause.statusText } { label: 'Jellyfin API', error: e.response?.status }
); );
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken); throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);

View File

@@ -1,10 +1,10 @@
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 { randomUUID } from 'node:crypto'; import { randomUUID } from 'node:crypto';
import xml2js from 'xml2js'; import xml2js from 'xml2js';
import ExternalAPI from './externalapi';
interface PlexAccountResponse { interface PlexAccountResponse {
user: PlexUser; user: PlexUser;
@@ -143,6 +143,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,
} }
@@ -153,11 +155,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,
@@ -205,11 +211,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}`,
@@ -249,10 +255,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;
} }
@@ -272,28 +281,26 @@ class PlexTvAPI extends ExternalAPI {
this.authToken this.authToken
); );
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: { params: {
...this.defaultHeaders, 'X-Plex-Container-Start': offset,
...(cachedWatchlist?.etag 'X-Plex-Container-Size': size,
? { 'If-None-Match': cachedWatchlist.etag }
: {}),
}, },
headers: {
'If-None-Match': cachedWatchlist?.etag,
},
baseURL: 'https://metadata.provider.plex.tv',
validateStatus: (status) => status < 400, // Allow HTTP 304 to return without error
} }
); );
const data = (await response.json()) as WatchlistResponse;
// If we don't recieve HTTP 304, the watchlist has been updated and we need to update the cache. // If we don't recieve HTTP 304, the watchlist has been updated and we need to update the cache.
if (response.status >= 200 && response.status <= 299) { if (response.status >= 200 && response.status <= 299) {
cachedWatchlist = { cachedWatchlist = {
etag: response.headers.get('etag') ?? '', etag: response.headers.etag,
response: data, response: response.data,
}; };
watchlistCache.data.set<PlexWatchlistCache>( watchlistCache.data.set<PlexWatchlistCache>(
@@ -307,10 +314,9 @@ class PlexTvAPI extends ExternalAPI {
async (watchlistItem) => { async (watchlistItem) => {
const detailedResponse = await this.getRolling<MetadataResponse>( const detailedResponse = await this.getRolling<MetadataResponse>(
`/library/metadata/${watchlistItem.ratingKey}`, `/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];
@@ -361,16 +367,11 @@ class PlexTvAPI extends ExternalAPI {
public async pingToken() { public async pingToken() {
try { try {
const data: { pong: unknown } = await this.get( const data: { pong: unknown } = await this.get('/api/v2/ping', {
'/api/v2/ping', headers: {
{}, 'X-Plex-Client-Identifier': randomUUID(),
undefined, },
{ });
headers: {
'X-Plex-Client-Identifier': randomUUID(),
},
}
);
if (!data?.pong) { if (!data?.pong) {
throw new Error('No pong response'); throw new Error('No pong response');
} }

View File

@@ -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);

View File

@@ -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,
); });
} }
/** /**

View File

@@ -105,12 +105,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,

View File

@@ -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,15 +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>>( const response = await this.axios.get<QueueResponse<QueueItemAppendT>>(
`/queue`, `/queue`,
{ {
includeEpisode: 'true', params: {
}, includeEpisode: true,
0 },
}
); );
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}`
@@ -175,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}`
@@ -187,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}`);
} }
@@ -206,15 +207,10 @@ class ServarrBase<QueueItemAppendT> extends ExternalAPI {
options: Record<string, unknown> options: Record<string, unknown>
): Promise<void> { ): Promise<void> {
try { try {
await this.post( await this.axios.post(`/command`, {
`/command`, name: commandName,
{ ...options,
name: commandName, });
...options,
},
{},
0
);
} catch (e) { } catch (e) {
throw new Error(`[${this.apiName}] Failed to run command: ${e.message}`); throw new Error(`[${this.apiName}] Failed to run command: ${e.message}`);
} }

View File

@@ -70,9 +70,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}`);
} }
@@ -80,9 +80,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}`);
} }
@@ -90,15 +90,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',
@@ -128,7 +130,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,
@@ -145,25 +147,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',
@@ -181,7 +183,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,
@@ -197,11 +199,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', {
@@ -210,22 +212,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');
@@ -254,9 +249,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) {
@@ -274,13 +271,10 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
if (tmdbId) { if (tmdbId) {
this.removeCache('/movie/lookup', { this.removeCache('/movie/lookup', {
term: `tmdb:${tmdbId}`, term: `tmdb:${tmdbId}`,
headers: this.defaultHeaders,
}); });
} }
if (externalId) { if (externalId) {
this.removeCache(`/movie/${externalId}`, { this.removeCache(`/movie/${externalId}`);
headers: this.defaultHeaders,
});
} }
}; };
} }

View File

@@ -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',
@@ -189,27 +193,27 @@ class SonarrAPI extends ServarrBase<{
: series.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, series: 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',
@@ -219,35 +223,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, series: createdSeriesResponse.data,
}); });
} else { } else {
logger.error('Failed to add movie to Sonarr', { logger.error('Failed to add movie to Sonarr', {
@@ -257,20 +264,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');
} }
@@ -342,13 +342,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) {
@@ -368,18 +369,14 @@ class SonarrAPI extends ServarrBase<{
if (tvdbId) { if (tvdbId) {
this.removeCache('/series/lookup', { this.removeCache('/series/lookup', {
term: `tvdb:${tvdbId}`, term: `tvdb:${tvdbId}`,
headers: this.defaultHeaders,
}); });
} }
if (externalId) { if (externalId) {
this.removeCache(`/series/${externalId}`, { this.removeCache(`/series/${externalId}`);
headers: this.defaultHeaders,
});
} }
if (title) { if (title) {
this.removeCache('/series/lookup', { this.removeCache('/series/lookup', {
term: title, term: title,
headers: this.defaultHeaders,
}); });
} }
}; };

View File

@@ -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;

View File

@@ -115,8 +115,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',
}, },
} }
); );
@@ -133,10 +133,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;
@@ -159,11 +156,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;
@@ -186,11 +185,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;
@@ -213,7 +214,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;
@@ -233,7 +234,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 },
} }
); );
@@ -256,10 +257,12 @@ 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:
include_video_language: language + ', en', 'credits,external_ids,videos,keywords,release_dates,watch/providers',
include_video_language: language + ', en',
},
}, },
43200 43200
); );
@@ -281,10 +284,12 @@ 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:
include_video_language: language + ', en', 'aggregate_credits,credits,external_ids,keywords,videos,content_ratings,watch/providers',
include_video_language: language + ', en',
},
}, },
43200 43200
); );
@@ -308,8 +313,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',
},
} }
); );
@@ -332,8 +339,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,
},
} }
); );
@@ -356,8 +365,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,
},
} }
); );
@@ -380,8 +391,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,
},
} }
); );
@@ -404,8 +417,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,
},
} }
); );
@@ -428,8 +443,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;
@@ -470,38 +487,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.discoverRegion || '', language,
with_original_language: region: this.discoverRegion || '',
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;
@@ -543,41 +562,41 @@ 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.discoverRegion || '', language,
// Set our release date values, but check if one is set and not the other, region: this.discoverRegion || '',
// 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 || '', with_status: withStatus,
with_status: withStatus || '', },
}); });
return data; return data;
@@ -597,10 +616,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.discoverRegion || '', language,
originalLanguage: this.originalLanguage || '', region: this.discoverRegion,
originalLanguage: this.originalLanguage,
},
} }
); );
@@ -623,9 +644,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.discoverRegion || '', language,
region: this.discoverRegion,
},
} }
); );
@@ -646,7 +669,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,
},
} }
); );
@@ -667,7 +692,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,
},
} }
); );
@@ -696,8 +723,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,
},
} }
); );
@@ -787,7 +816,9 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbCollection>( const data = await this.get<TmdbCollection>(
`/collection/${collectionId}`, `/collection/${collectionId}`,
{ {
language, params: {
language,
},
} }
); );
@@ -860,7 +891,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
); );
@@ -872,7 +905,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
); );
@@ -907,7 +942,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
); );
@@ -919,7 +956,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
); );
@@ -974,8 +1013,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
); );
@@ -997,8 +1038,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
); );
@@ -1018,7 +1061,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
); );
@@ -1042,8 +1087,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
); );
@@ -1067,8 +1114,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
); );

View File

@@ -35,8 +35,6 @@ 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';
@@ -74,15 +72,6 @@ app
const settings = await getSettings().load(); const settings = await getSettings().load();
restartFlag.initializeSettings(settings); restartFlag.initializeSettings(settings);
// Check if we force IPv4 first
if (
process.env.forceIpv4First === 'true' ||
settings.network.forceIpv4First
) {
dns.setDefaultResultOrder('ipv4first');
net.setDefaultAutoSelectFamily(false);
}
// Register HTTP proxy // Register HTTP proxy
if (settings.network.proxy.enabled) { if (settings.network.proxy.enabled) {
await createCustomProxyAgent(settings.network.proxy); await createCustomProxyAgent(settings.network.proxy);

View File

@@ -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 = {
@@ -131,33 +130,29 @@ class ImageProxy {
return 0; return 0;
} }
private fetch: typeof fetch; private axios;
private cacheVersion; private cacheVersion;
private key; private key;
private baseUrl;
private headers: HeadersInit | null = null;
constructor( constructor(
key: string, key: string,
baseUrl: string, baseUrl: string,
options: { options: {
cacheVersion?: number; cacheVersion?: number;
rateLimitOptions?: RateLimitOptions; rateLimitOptions?: rateLimitOptions;
headers?: HeadersInit; headers?: Record<string, string>;
} = {} } = {}
) { ) {
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,
headers: options.headers,
});
if (options.rateLimitOptions) { if (options.rateLimitOptions) {
this.fetch = rateLimit(fetch, { this.axios = rateLimit(this.axios, options.rateLimitOptions);
...options.rateLimitOptions,
});
} else {
this.fetch = fetch;
} }
this.headers = options.headers || null;
} }
public async getImage( public async getImage(
@@ -269,34 +264,19 @@ 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, {
headers: this.headers || undefined,
}); });
if (!response.ok) {
return null;
}
const arrayBuffer = await response.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
const extension = mime.getExtension(
response.headers.get('content-type') ?? ''
);
const buffer = Buffer.from(response.data, 'binary');
const extension = path.split('.').pop() ?? '';
let maxAge = Number( let maxAge = Number(
(response.headers.get('cache-control') ?? '0').split('=')[1] (response.headers['cache-control'] ?? '0').split('=')[1]
); );
if (!maxAge) maxAge = 86400; 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,

View File

@@ -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,
@@ -297,39 +298,23 @@ class DiscordAgent
userMentions.push(`<@&${settings.options.webhookRoleId}>`); userMentions.push(`<@&${settings.options.webhookRoleId}>`);
} }
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;

View File

@@ -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';
@@ -139,32 +140,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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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,
@@ -176,35 +177,19 @@ 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', message_thread_id: settings.options.messageThreadId,
}, disable_notification: !!settings.options.sendSilently,
body: JSON.stringify({ } as TelegramMessagePayload | TelegramPhotoPayload);
...notificationPayload,
chat_id: settings.options.chatId,
message_thread_id: settings.options.messageThreadId,
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;
@@ -228,38 +213,22 @@ 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', message_thread_id:
}, payload.notifyUser.settings.telegramMessageThreadId,
body: JSON.stringify({ disable_notification:
...notificationPayload, !!payload.notifyUser.settings.telegramSendSilently,
chat_id: payload.notifyUser.settings.telegramChatId, } as TelegramMessagePayload | TelegramPhotoPayload);
message_thread_id:
payload.notifyUser.settings.telegramMessageThreadId,
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;
@@ -293,36 +262,20 @@ 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', message_thread_id: user.settings.telegramMessageThreadId,
}, disable_notification: !!user.settings?.telegramSendSilently,
body: JSON.stringify({ } as TelegramMessagePayload | TelegramPhotoPayload);
...notificationPayload,
chat_id: user.settings.telegramChatId,
message_thread_id: user.settings.telegramMessageThreadId,
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;

View File

@@ -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;

View File

@@ -136,7 +136,6 @@ export interface MainSettings {
export interface NetworkSettings { export interface NetworkSettings {
csrfProtection: boolean; csrfProtection: boolean;
forceIpv4First: boolean;
trustProxy: boolean; trustProxy: boolean;
proxy: ProxySettings; proxy: ProxySettings;
} }
@@ -510,7 +509,6 @@ class Settings {
network: { network: {
csrfProtection: false, csrfProtection: false,
trustProxy: false, trustProxy: false,
forceIpv4First: false,
proxy: { proxy: {
enabled: false, enabled: false,
hostname: '', hostname: '',

View File

@@ -6,6 +6,7 @@ import { getSettings } from '@server/lib/settings';
import logger from '@server/logger'; import logger from '@server/logger';
import { getAppVersion } from '@server/utils/appVersion'; import { getAppVersion } from '@server/utils/appVersion';
import { getHostname } from '@server/utils/getHostname'; import { getHostname } from '@server/utils/getHostname';
import axios from 'axios';
import { Router } from 'express'; import { Router } from 'express';
import gravatarUrl from 'gravatar-url'; import gravatarUrl from 'gravatar-url';
import { createHash } from 'node:crypto'; import { createHash } from 'node:crypto';
@@ -54,22 +55,26 @@ export async function checkAvatarChanged(
const jellyfinAvatarUrl = getJellyfinAvatarUrl(user.jellyfinUserId); const jellyfinAvatarUrl = getJellyfinAvatarUrl(user.jellyfinUserId);
const headResponse = await fetch(jellyfinAvatarUrl, { method: 'HEAD' }); let headResponse;
if (!headResponse.ok) { try {
headResponse = await axios.head(jellyfinAvatarUrl);
if (headResponse.status !== 200) {
return { changed: false };
}
} catch (error) {
return { changed: false }; return { changed: false };
} }
const settings = getSettings(); const settings = getSettings();
let remoteVersion: string; let remoteVersion: string;
if (settings.main.mediaServerType === MediaServerType.JELLYFIN) { if (settings.main.mediaServerType === MediaServerType.JELLYFIN) {
const remoteLastModifiedStr = const remoteLastModifiedStr = headResponse.headers['last-modified'] || '';
headResponse.headers.get('last-modified') || '';
remoteVersion = ( remoteVersion = (
Date.parse(remoteLastModifiedStr) || Date.now() Date.parse(remoteLastModifiedStr) || Date.now()
).toString(); ).toString();
} else if (settings.main.mediaServerType === MediaServerType.EMBY) { } else if (settings.main.mediaServerType === MediaServerType.EMBY) {
remoteVersion = remoteVersion =
headResponse.headers.get('etag')?.replace(/"/g, '') || headResponse.headers['etag']?.replace(/"/g, '') ||
Date.now().toString(); Date.now().toString();
} else { } else {
remoteVersion = Date.now().toString(); remoteVersion = Date.now().toString();

View File

@@ -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,
}, },
}); });

View File

@@ -1,5 +1,6 @@
import type { ProxySettings } from '@server/lib/settings'; import type { ProxySettings } from '@server/lib/settings';
import logger from '@server/logger'; import logger from '@server/logger';
import axios from 'axios';
import type { Dispatcher } from 'undici'; import type { Dispatcher } from 'undici';
import { Agent, ProxyAgent, setGlobalDispatcher } from 'undici'; import { Agent, ProxyAgent, setGlobalDispatcher } from 'undici';
@@ -73,15 +74,8 @@ export default async function createCustomProxyAgent(
} }
try { try {
const res = await fetch('https://www.google.com', { method: 'HEAD' }); await axios.head('https://www.google.com');
if (res.ok) { logger.debug('HTTP(S) proxy connected successfully', { label: 'Proxy' });
logger.debug('HTTP(S) proxy connected successfully', { label: 'Proxy' });
} else {
logger.error('Proxy responded, but with a non-OK status: ' + res.status, {
label: 'Proxy',
});
setGlobalDispatcher(defaultAgent);
}
} catch (e) { } catch (e) {
logger.error( logger.error(
'Failed to connect to the proxy: ' + e.message + ': ' + e.cause, 'Failed to connect to the proxy: ' + e.message + ': ' + e.cause,

View File

@@ -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();
});
};
}

View File

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

View File

@@ -23,6 +23,7 @@ import type {
} from '@server/interfaces/api/blacklistInterfaces'; } from '@server/interfaces/api/blacklistInterfaces';
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 Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
@@ -238,11 +239,9 @@ const BlacklistedItem = ({ item, revalidateList }: BlacklistedItemProps) => {
const removeFromBlacklist = async (tmdbId: number, title?: string) => { const removeFromBlacklist = async (tmdbId: number, title?: string) => {
setIsUpdating(true); setIsUpdating(true);
const res = await fetch('/api/v1/blacklist/' + tmdbId, { try {
method: 'DELETE', await axios.delete(`/api/v1/blacklist/${tmdbId}`);
});
if (res.status === 204) {
addToast( addToast(
<span> <span>
{intl.formatMessage(globalMessages.removeFromBlacklistSuccess, { {intl.formatMessage(globalMessages.removeFromBlacklistSuccess, {
@@ -252,7 +251,7 @@ const BlacklistedItem = ({ item, revalidateList }: BlacklistedItemProps) => {
</span>, </span>,
{ appearance: 'success', autoDismiss: true } { appearance: 'success', autoDismiss: true }
); );
} else { } catch {
addToast(intl.formatMessage(globalMessages.blacklistError), { addToast(intl.formatMessage(globalMessages.blacklistError), {
appearance: 'error', appearance: 'error',
autoDismiss: true, autoDismiss: true,

View File

@@ -7,6 +7,7 @@ import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages'; import defineMessages from '@app/utils/defineMessages';
import { CalendarIcon, TrashIcon, UserIcon } from '@heroicons/react/24/solid'; import { CalendarIcon, TrashIcon, UserIcon } from '@heroicons/react/24/solid';
import type { Blacklist } from '@server/entity/Blacklist'; import type { Blacklist } from '@server/entity/Blacklist';
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';
@@ -38,11 +39,9 @@ const BlacklistBlock = ({
const removeFromBlacklist = async (tmdbId: number, title?: string) => { const removeFromBlacklist = async (tmdbId: number, title?: string) => {
setIsUpdating(true); setIsUpdating(true);
const res = await fetch('/api/v1/blacklist/' + tmdbId, { try {
method: 'DELETE', await axios.delete('/api/v1/blacklist/' + tmdbId);
});
if (res.status === 204) {
addToast( addToast(
<span> <span>
{intl.formatMessage(globalMessages.removeFromBlacklistSuccess, { {intl.formatMessage(globalMessages.removeFromBlacklistSuccess, {
@@ -52,7 +51,7 @@ const BlacklistBlock = ({
</span>, </span>,
{ appearance: 'success', autoDismiss: true } { appearance: 'success', autoDismiss: true }
); );
} else { } catch {
addToast(intl.formatMessage(globalMessages.blacklistError), { addToast(intl.formatMessage(globalMessages.blacklistError), {
appearance: 'error', appearance: 'error',
autoDismiss: true, autoDismiss: true,

View File

@@ -4,6 +4,7 @@ import defineMessages from '@app/utils/defineMessages';
import { Transition } from '@headlessui/react'; import { Transition } from '@headlessui/react';
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 { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
@@ -44,12 +45,8 @@ const BlacklistModal = ({
if (!show) return; if (!show) return;
try { try {
setError(null); setError(null);
const response = await fetch(`/api/v1/${type}/${tmdbId}`); const response = await axios.get(`/api/v1/${type}/${tmdbId}`);
if (!response.ok) { setData(response.data);
throw new Error();
}
const result = await response.json();
setData(result);
} catch (err) { } catch (err) {
setError(err); setError(err);
} }

View File

@@ -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(

View File

@@ -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,

View File

@@ -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',

View File

@@ -6,6 +6,7 @@ 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 Link from 'next/link'; import Link from 'next/link';
import { Fragment, useState } from 'react'; import { Fragment, useState } from 'react';
@@ -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 {
@@ -178,17 +176,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();

View File

@@ -27,6 +27,7 @@ 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 Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
@@ -121,14 +122,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',
@@ -145,10 +141,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',
@@ -166,10 +159,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();
mutate('/api/v1/issue/count'); mutate('/api/v1/issue/count');
addToast(intl.formatMessage(messages.toastissuedeleted), { addToast(intl.formatMessage(messages.toastissuedeleted), {
@@ -504,17 +494,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();
}} }}

View File

@@ -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 />

View File

@@ -8,6 +8,7 @@ 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 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();
} }
}; };

View File

@@ -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) {

View File

@@ -5,6 +5,7 @@ import defineMessages from '@app/utils/defineMessages';
import { ArrowLeftOnRectangleIcon } from '@heroicons/react/24/outline'; import { ArrowLeftOnRectangleIcon } from '@heroicons/react/24/outline';
import { ApiErrorCode } from '@server/constants/error'; import { ApiErrorCode } from '@server/constants/error';
import { MediaServerType, ServerType } from '@server/constants/server'; import { MediaServerType, ServerType } from '@server/constants/server';
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';
@@ -71,28 +72,14 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
validateOnBlur={false} validateOnBlur={false}
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(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;

View File

@@ -3,6 +3,7 @@ import SensitiveInput from '@app/components/Common/SensitiveInput';
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 { ArrowLeftOnRectangleIcon } from '@heroicons/react/24/outline'; import { ArrowLeftOnRectangleIcon } 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';
@@ -54,17 +55,10 @@ const LocalLogin = ({ revalidate }: LocalLoginProps) => {
validateOnBlur={false} validateOnBlur={false}
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 {

View File

@@ -14,6 +14,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 { useRouter } from 'next/dist/client/router'; import { useRouter } from 'next/dist/client/router';
import Image from 'next/image'; import Image from 'next/image';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
@@ -50,28 +51,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);
} }

View File

@@ -28,6 +28,7 @@ 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 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';
@@ -111,10 +112,7 @@ 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(); onClose();
} }
@@ -122,14 +120,8 @@ const ManageSlideOver = ({
const deleteMediaFile = async () => { const deleteMediaFile = async () => {
if (data.mediaInfo) { if (data.mediaInfo) {
// we don't check if the response is ok here because there may be no file to delete await axios.delete(`/api/v1/media/${data.mediaInfo.id}/file`);
await fetch(`/api/v1/media/${data.mediaInfo.id}/file`, { await axios.delete(`/api/v1/media/${data.mediaInfo.id}`);
method: 'DELETE',
});
await fetch(`/api/v1/media/${data.mediaInfo.id}`, {
method: 'DELETE',
});
revalidate(); revalidate();
onClose(); onClose();
} }
@@ -158,16 +150,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();
} }
}; };

View File

@@ -52,6 +52,7 @@ import { IssueStatus } from '@server/constants/issue';
import { MediaStatus, MediaType } from '@server/constants/media'; import { MediaStatus, MediaType } 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 axios from 'axios';
import { countries } from 'country-flag-icons'; import { countries } from 'country-flag-icons';
import 'country-flag-icons/3x2/flags.css'; import 'country-flag-icons/3x2/flags.css';
import { uniqBy } from 'lodash'; import { uniqBy } from 'lodash';
@@ -319,41 +320,29 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
const onClickWatchlistBtn = async (): Promise<void> => { const onClickWatchlistBtn = async (): Promise<void> => {
setIsUpdating(true); setIsUpdating(true);
const res = await fetch('/api/v1/watchlist', { try {
method: 'POST', const response = await axios.post('/api/v1/watchlist', {
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
tmdbId: movie?.id, tmdbId: movie?.id,
mediaType: MediaType.MOVIE, mediaType: MediaType.MOVIE,
title: movie?.title, title: movie?.title,
}), });
});
if (!res.ok) { if (response.data) {
addToast(
<span>
{intl.formatMessage(messages.watchlistSuccess, {
title: movie?.title,
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
})}
</span>,
{ appearance: 'success', autoDismiss: true }
);
}
} catch (e) {
addToast(intl.formatMessage(messages.watchlistError), { addToast(intl.formatMessage(messages.watchlistError), {
appearance: 'error', appearance: 'error',
autoDismiss: true, 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); setIsUpdating(false);
@@ -363,22 +352,17 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
const onClickDeleteWatchlistBtn = async (): Promise<void> => { const onClickDeleteWatchlistBtn = async (): Promise<void> => {
setIsUpdating(true); setIsUpdating(true);
try { try {
const res = await fetch(`/api/v1/watchlist/${movie?.id}`, { await axios.delete(`/api/v1/watchlist/${movie?.id}`);
method: 'DELETE',
});
if (!res.ok) throw new Error();
if (res.status === 204) { addToast(
addToast( <span>
<span> {intl.formatMessage(messages.watchlistDeleted, {
{intl.formatMessage(messages.watchlistDeleted, { title: movie?.title,
title: movie?.title, strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
strong: (msg: React.ReactNode) => <strong>{msg}</strong>, })}
})} </span>,
</span>, { appearance: 'info', autoDismiss: true }
{ appearance: 'info', autoDismiss: true } );
);
}
} catch (e) { } catch (e) {
addToast(intl.formatMessage(messages.watchlistError), { addToast(intl.formatMessage(messages.watchlistError), {
appearance: 'error', appearance: 'error',
@@ -393,21 +377,14 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
const onClickHideItemBtn = async (): Promise<void> => { const onClickHideItemBtn = async (): Promise<void> => {
setIsBlacklistUpdating(true); setIsBlacklistUpdating(true);
const res = await fetch('/api/v1/blacklist', { try {
method: 'POST', await axios.post('/api/v1/blacklist', {
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
tmdbId: movie?.id, tmdbId: movie?.id,
mediaType: 'movie', mediaType: 'movie',
title: movie?.title, title: movie?.title,
user: user?.id, user: user?.id,
}), });
});
if (res.status === 201) {
addToast( addToast(
<span> <span>
{intl.formatMessage(globalMessages.blacklistSuccess, { {intl.formatMessage(globalMessages.blacklistSuccess, {
@@ -419,21 +396,23 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
); );
revalidate(); revalidate();
} else if (res.status === 412) { } catch (e) {
addToast( if (e?.response?.status === 412) {
<span> addToast(
{intl.formatMessage(globalMessages.blacklistDuplicateError, { <span>
title: movie?.title, {intl.formatMessage(globalMessages.blacklistDuplicateError, {
strong: (msg: React.ReactNode) => <strong>{msg}</strong>, title: movie?.title,
})} strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
</span>, })}
{ appearance: 'info', autoDismiss: true } </span>,
); { appearance: 'info', autoDismiss: true }
} else { );
addToast(intl.formatMessage(globalMessages.blacklistError), { } else {
appearance: 'error', addToast(intl.formatMessage(globalMessages.blacklistError), {
autoDismiss: true, appearance: 'error',
}); autoDismiss: true,
});
}
} }
setIsBlacklistUpdating(false); setIsBlacklistUpdating(false);

View File

@@ -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';
@@ -53,10 +54,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();
@@ -67,10 +65,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();

View File

@@ -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';
import { mutate } from 'swr'; import { mutate } from 'swr';
@@ -94,13 +95,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();
mutate('/api/v1/request/count'); mutate('/api/v1/request/count');
} }
@@ -116,11 +113,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();
}) })
); );

View File

@@ -23,6 +23,7 @@ import type { MediaRequest } from '@server/entity/MediaRequest';
import type { NonFunctionProperties } from '@server/interfaces/api/common'; import type { NonFunctionProperties } from '@server/interfaces/api/common';
import type { 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 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';
@@ -74,10 +75,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');
mutate('/api/v1/request/count'); mutate('/api/v1/request/count');
@@ -264,23 +262,16 @@ 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();
mutate('/api/v1/request/count'); mutate('/api/v1/request/count');
} }
}; };
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');
mutate('/api/v1/request/count'); mutate('/api/v1/request/count');
}; };
@@ -289,13 +280,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) {

View File

@@ -23,6 +23,7 @@ import type { NonFunctionProperties } from '@server/interfaces/api/common';
import type { RequestResultsResponse } from '@server/interfaces/api/requestInterfaces'; import type { RequestResultsResponse } from '@server/interfaces/api/requestInterfaces';
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 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';
@@ -65,10 +66,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();
mutate('/api/v1/request/count'); mutate('/api/v1/request/count');
}; };
@@ -328,23 +326,16 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
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();
mutate('/api/v1/request/count'); mutate('/api/v1/request/count');
} }
}; };
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();
mutate('/api/v1/request/count'); mutate('/api/v1/request/count');
@@ -352,13 +343,8 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
const deleteMediaFile = async () => { const deleteMediaFile = async () => {
if (request.media) { if (request.media) {
// we don't check if the response is ok here because there may be no file to delete await axios.delete(`/api/v1/media/${request.media.id}/file`);
await fetch(`/api/v1/media/${request.media.id}/file`, { await axios.delete(`/api/v1/media/${request.media.id}`);
method: 'DELETE',
});
await fetch(`/api/v1/media/${request.media.id}`, {
method: 'DELETE',
});
revalidateList(); revalidateList();
} }
}; };
@@ -367,12 +353,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), {

View File

@@ -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';
@@ -198,19 +199,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();
}) })
); );

View File

@@ -12,6 +12,7 @@ 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';
@@ -89,24 +90,16 @@ 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');
mutate('/api/v1/request/count'); mutate('/api/v1/request/count');
if (mediaRequest) { if (response.data) {
if (onComplete) { if (onComplete) {
onComplete( onComplete(
hasPermission( hasPermission(
@@ -154,15 +147,13 @@ 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');
mutate('/api/v1/request/count'); mutate('/api/v1/request/count');
if (res.status === 204) { if (response.status === 204) {
if (onComplete) { if (onComplete) {
onComplete(MediaStatus.UNKNOWN); onComplete(MediaStatus.UNKNOWN);
} }
@@ -185,27 +176,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');
mutate('/api/v1/request/count'); mutate('/api/v1/request/count');

View File

@@ -17,6 +17,7 @@ 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';
@@ -111,35 +112,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');
mutate('/api/v1/request/count'); mutate('/api/v1/request/count');
@@ -206,32 +194,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.sort((a, b) => a - b)
tvdbId: tvdbId ?? data?.externalIds.tvdbId, : getAllSeasons().filter(
mediaType: 'tv', (season) => !getAllRequestedSeasons().includes(season)
is4k, ),
seasons: settings.currentSettings.partialRequestsEnabled ...overrideParams,
? selectedSeasons.sort((a, b) => a - b)
: 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>

View File

@@ -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);
} }
}} }}

View File

@@ -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);
} }
}} }}

View File

@@ -19,6 +19,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';
@@ -80,9 +81,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([
{ {
@@ -100,15 +103,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,
})); }));
@@ -164,15 +168,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,
@@ -185,11 +185,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,
@@ -309,13 +309,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;
}) })
); );
@@ -331,15 +329,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,
})); }));
@@ -578,14 +577,10 @@ export const UserSelector = ({
const users = defaultValue.split(','); const users = defaultValue.split(',');
const res = await fetch( const res = await axios.get(
`/api/v1/user?includeIds=${encodeURIComponent(defaultValue)}` `/api/v1/user?includeIds=${encodeURIComponent(defaultValue)}`
); );
const response: UserResultsResponse = res.data;
if (!res.ok) {
throw new Error('Network response was not ok');
}
const response: UserResultsResponse = await res.json();
const genreData = users const genreData = users
.filter((u) => response.results.find((user) => user.id === Number(u))) .filter((u) => response.results.find((user) => user.id === Number(u)))
@@ -602,11 +597,10 @@ export const UserSelector = ({
}, [defaultValue]); }, [defaultValue]);
const loadUserOptions = async (inputValue: string) => { const loadUserOptions = async (inputValue: string) => {
const res = await fetch( const res = await axios.get(
`/api/v1/user${inputValue ? `?q=${encodeURIComponent(inputValue)}` : ''}` `/api/v1/user${inputValue ? `?q=${encodeURIComponent(inputValue)}` : ''}`
); );
if (!res.ok) throw new Error(); const results: UserResultsResponse = res.data;
const results: UserResultsResponse = await res.json();
return results.results return results.results
.map((result) => ({ .map((result) => ({

View File

@@ -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';
@@ -83,24 +84,17 @@ 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,
webhookRoleId: values.webhookRoleId,
enableMentions: values.enableMentions,
}, },
body: JSON.stringify({
enabled: values.enabled,
types: values.types,
options: {
botUsername: values.botUsername,
botAvatarUrl: values.botAvatarUrl,
webhookUrl: values.webhookUrl,
webhookRoleId: values.webhookRoleId,
enableMentions: values.enableMentions,
},
}),
}); });
if (!res.ok) throw new Error();
addToast(intl.formatMessage(messages.discordsettingssaved), { addToast(intl.formatMessage(messages.discordsettingssaved), {
appearance: 'success', appearance: 'success',
@@ -139,27 +133,17 @@ 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({ webhookRoleId: values.webhookRoleId,
enabled: true, enableMentions: values.enableMentions,
types: values.types, },
options: { });
botUsername: values.botUsername,
botAvatarUrl: values.botAvatarUrl,
webhookUrl: values.webhookUrl,
webhookRoleId: values.webhookRoleId,
enableMentions: values.enableMentions,
},
}),
}
);
if (!res.ok) throw new Error();
if (toastId) { if (toastId) {
removeToast(toastId); removeToast(toastId);

View File

@@ -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,33 +197,23 @@ 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, allowSelfSigned: values.allowSelfSigned,
smtpPort: Number(values.smtpPort), senderName: values.senderName,
secure: values.encryption === 'implicit', pgpPrivateKey: values.pgpPrivateKey,
ignoreTls: values.encryption === 'none', pgpPassword: values.pgpPassword,
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();
if (toastId) { if (toastId) {
removeToast(toastId); removeToast(toastId);

View File

@@ -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';
@@ -94,22 +95,15 @@ 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,
priority: Number(values.priority),
}, },
body: JSON.stringify({
enabled: values.enabled,
types: values.types,
options: {
url: values.url,
token: values.token,
priority: Number(values.priority),
},
}),
}); });
if (!res.ok) throw new Error();
addToast(intl.formatMessage(messages.gotifysettingssaved), { addToast(intl.formatMessage(messages.gotifysettingssaved), {
appearance: 'success', appearance: 'success',
autoDismiss: true, autoDismiss: true,
@@ -147,25 +141,15 @@ 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,
}, priority: Number(values.priority),
body: JSON.stringify({ },
enabled: true, });
types: values.types,
options: {
url: values.url,
token: values.token,
priority: Number(values.priority),
},
}),
}
);
if (!res.ok) throw new Error();
if (toastId) { if (toastId) {
removeToast(toastId); removeToast(toastId);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);
} }

View File

@@ -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);

View File

@@ -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';
@@ -98,24 +99,17 @@ 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,
messageThreadId: values.messageThreadId,
sendSilently: values.sendSilently,
botUsername: values.botUsername,
}, },
body: JSON.stringify({
enabled: values.enabled,
types: values.types,
options: {
botAPI: values.botAPI,
chatId: values.chatId,
messageThreadId: values.messageThreadId,
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',
@@ -154,27 +148,17 @@ 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,
}, messageThreadId: values.messageThreadId,
body: JSON.stringify({ sendSilently: values.sendSilently,
enabled: true, botUsername: values.botUsername,
types: values.types, },
options: { });
botAPI: values.botAPI,
chatId: values.chatId,
messageThreadId: values.messageThreadId,
sendSilently: values.sendSilently,
botUsername: values.botUsername,
},
}),
}
);
if (!res.ok) throw new Error();
if (toastId) { if (toastId) {
removeToast(toastId); removeToast(toastId);

View File

@@ -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);

View File

@@ -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);
} }

View File

@@ -16,6 +16,7 @@ import type {
RadarrSettings, RadarrSettings,
SonarrSettings, SonarrSettings,
} from '@server/lib/settings'; } from '@server/lib/settings';
import axios from 'axios';
import { Field, Formik } from 'formik'; import { Field, 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';
@@ -95,24 +96,19 @@ const OverrideRuleModal = ({
}) => { }) => {
setIsTesting(true); setIsTesting(true);
try { try {
const res = await fetch('/api/v1/settings/sonarr/test', { const response = await axios.post<DVRTestResponse>(
method: 'POST', '/api/v1/settings/sonarr/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: DVRTestResponse = await res.json();
setIsValidated(true); setIsValidated(true);
setTestResponse(data); setTestResponse(response.data);
} catch (e) { } catch (e) {
setIsValidated(false); setIsValidated(false);
} finally { } finally {
@@ -179,27 +175,13 @@ const OverrideRuleModal = ({
sonarrServiceId: values.sonarrServiceId, sonarrServiceId: values.sonarrServiceId,
}; };
if (!rule) { if (!rule) {
const res = await fetch('/api/v1/overrideRule', { await axios.post('/api/v1/overrideRule', submission);
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(submission),
});
if (!res.ok) throw new Error();
addToast(intl.formatMessage(messages.ruleCreated), { addToast(intl.formatMessage(messages.ruleCreated), {
appearance: 'success', appearance: 'success',
autoDismiss: true, autoDismiss: true,
}); });
} else { } else {
const res = await fetch(`/api/v1/overrideRule/${rule.id}`, { await axios.put(`/api/v1/overrideRule/${rule.id}`, submission);
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(submission),
});
if (!res.ok) throw new Error();
addToast(intl.formatMessage(messages.ruleUpdated), { addToast(intl.formatMessage(messages.ruleUpdated), {
appearance: 'success', appearance: 'success',
autoDismiss: true, autoDismiss: true,

View File

@@ -12,6 +12,7 @@ import type {
SonarrSettings, SonarrSettings,
} from '@server/lib/settings'; } from '@server/lib/settings';
import type { Keyword } from '@server/models/common'; import type { Keyword } from '@server/models/common';
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 useSWR from 'swr'; import useSWR from 'swr';
@@ -64,34 +65,26 @@ const OverrideRuleTiles = ({
for (const service of services) { for (const service of services) {
const { hostname, port, apiKey, baseUrl, useSsl = false } = service; const { hostname, port, apiKey, baseUrl, useSsl = false } = service;
try { try {
const res = await fetch( const response = await axios.post<DVRTestResponse>(
`/api/v1/settings/${ `/api/v1/settings/${
radarrServices.includes(service as RadarrSettings) radarrServices.includes(service as RadarrSettings)
? 'radarr' ? 'radarr'
: 'sonarr' : 'sonarr'
}/test`, }/test`,
{ {
method: 'POST', hostname,
headers: { apiKey,
'Content-Type': 'application/json', port: Number(port),
}, baseUrl,
body: JSON.stringify({ useSsl,
hostname,
apiKey,
port: Number(port),
baseUrl,
useSsl,
}),
} }
); );
if (!res.ok) throw new Error();
const data: DVRTestResponse = await res.json();
results.push({ results.push({
type: radarrServices.includes(service as RadarrSettings) type: radarrServices.includes(service as RadarrSettings)
? 'radarr' ? 'radarr'
: 'sonarr', : 'sonarr',
id: service.id, id: service.id,
...data, ...response.data,
}); });
} catch { } catch {
results.push({ results.push({
@@ -120,9 +113,8 @@ const OverrideRuleTiles = ({
.flat() .flat()
.filter((keywordId) => keywordId) .filter((keywordId) => keywordId)
.map(async (keywordId) => { .map(async (keywordId) => {
const res = await fetch(`/api/v1/keyword/${keywordId}`); const response = await axios.get(`/api/v1/keyword/${keywordId}`);
if (!res.ok) throw new Error(); const keyword: Keyword = response.data;
const keyword: Keyword = await res.json();
return keyword; return keyword;
}) })
); );
@@ -132,11 +124,10 @@ const OverrideRuleTiles = ({
.filter((users) => users) .filter((users) => users)
.join(','); .join(',');
if (allUsersFromRules) { if (allUsersFromRules) {
const res = await fetch( const response = await axios.get(
`/api/v1/user?includeIds=${encodeURIComponent(allUsersFromRules)}` `/api/v1/user?includeIds=${encodeURIComponent(allUsersFromRules)}`
); );
if (!res.ok) throw new Error(); const users: User[] = response.data.results;
const users: User[] = (await res.json()).results;
setUsers(users); setUsers(users);
} }
setUsers(users); setUsers(users);
@@ -295,10 +286,7 @@ const OverrideRuleTiles = ({
<div className="-ml-px flex w-0 flex-1"> <div className="-ml-px flex w-0 flex-1">
<button <button
onClick={async () => { onClick={async () => {
const res = await fetch(`/api/v1/overrideRule/${rule.id}`, { await axios.delete(`/api/v1/overrideRule/${rule.id}`);
method: 'DELETE',
});
if (!res.ok) throw new Error();
revalidate(); revalidate();
}} }}
className="focus:ring-blue relative inline-flex w-0 flex-1 items-center justify-center rounded-br-lg border border-transparent py-4 text-sm font-medium leading-5 text-gray-200 transition duration-150 ease-in-out hover:text-white focus:z-10 focus:border-gray-500 focus:outline-none" className="focus:ring-blue relative inline-flex w-0 flex-1 items-center justify-center rounded-br-lg border border-transparent py-4 text-sm font-medium leading-5 text-gray-200 transition duration-150 ease-in-out hover:text-white focus:z-10 focus:border-gray-500 focus:outline-none"

View File

@@ -5,6 +5,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';
@@ -154,24 +155,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<RadarrTestResponse>(
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',
@@ -264,23 +260,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();

View File

@@ -10,6 +10,7 @@ import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline';
import { ApiErrorCode } from '@server/constants/error'; import { ApiErrorCode } from '@server/constants/error';
import { MediaServerType } from '@server/constants/server'; import { MediaServerType } from '@server/constants/server';
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 { useState } from 'react'; import { useState } from 'react';
import { FormattedMessage, useIntl } from 'react-intl'; import { FormattedMessage, useIntl } from 'react-intl';
@@ -178,25 +179,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
@@ -206,7 +195,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),
{ {
@@ -229,32 +218,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();
}; };
@@ -269,19 +242,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();
@@ -491,22 +460,15 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
validationSchema={JellyfinSettingsSchema} validationSchema={JellyfinSettingsSchema}
onSubmit={async (values) => { onSubmit={async (values) => {
try { try {
const res = await fetch('/api/v1/settings/jellyfin', { await axios.post('/api/v1/settings/jellyfin', {
method: 'POST', ip: values.hostname,
headers: { port: Number(values.port),
'Content-Type': 'application/json', useSsl: values.useSsl,
}, urlBase: values.urlBase,
body: JSON.stringify({ externalHostname: values.jellyfinExternalUrl,
ip: values.hostname, jellyfinForgotPasswordUrl: values.jellyfinForgotPasswordUrl,
port: Number(values.port), apiKey: values.apiKey,
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( intl.formatMessage(
@@ -519,14 +481,7 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
} }
); );
} catch (e) { } catch (e) {
let errorData; if (e?.response?.data?.message === ApiErrorCode.InvalidUrl) {
try {
errorData = await e.cause?.text();
errorData = JSON.parse(errorData);
} catch {
/* empty */
}
if (errorData?.message === ApiErrorCode.InvalidUrl) {
addToast( addToast(
intl.formatMessage( intl.formatMessage(
messages.invalidurlerror, messages.invalidurlerror,

View File

@@ -20,6 +20,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';
@@ -189,10 +190,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),
@@ -206,10 +204,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),
@@ -223,10 +218,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 }),
{ {
@@ -253,19 +245,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',

View File

@@ -16,6 +16,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';
@@ -84,10 +85,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), {
@@ -140,25 +138,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', hideAvailable: values.hideAvailable,
}, locale: values.locale,
body: JSON.stringify({ discoverRegion: values.discoverRegion,
applicationTitle: values.applicationTitle, streamingRegion: values.streamingRegion,
applicationUrl: values.applicationUrl, originalLanguage: values.originalLanguage,
hideAvailable: values.hideAvailable, partialRequestsEnabled: values.partialRequestsEnabled,
locale: values.locale, enableSpecialEpisodes: values.enableSpecialEpisodes,
discoverRegion: values.discoverRegion, cacheImages: values.cacheImages,
streamingRegion: values.streamingRegion,
originalLanguage: values.originalLanguage,
partialRequestsEnabled: values.partialRequestsEnabled,
enableSpecialEpisodes: values.enableSpecialEpisodes,
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');

View File

@@ -7,6 +7,7 @@ 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 type { NetworkSettings } from '@server/lib/settings'; import type { NetworkSettings } 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';
@@ -38,13 +39,9 @@ const messages = defineMessages('components.Settings.SettingsNetwork', {
"Use ',' as a separator, and '*.' as a wildcard for subdomains", "Use ',' as a separator, and '*.' as a wildcard for subdomains",
proxyBypassLocalAddresses: 'Bypass Proxy for Local Addresses', proxyBypassLocalAddresses: 'Bypass Proxy for Local Addresses',
validationProxyPort: 'You must provide a valid port', validationProxyPort: 'You must provide a valid port',
advancedNetworkSettings: 'Advanced Network Settings',
networkDisclaimer: networkDisclaimer:
'Network parameters from your container/system should be used instead of these settings. See the {docs} for more information.', 'Network parameters from your container/system should be used instead of these settings. See the {docs} for more information.',
docs: 'documentation', docs: 'documentation',
forceIpv4First: 'Force IPv4 Resolution First',
forceIpv4FirstTip:
'Force Jellyseerr to resolve IPv4 addresses first instead of IPv6',
}); });
const SettingsNetwork = () => { const SettingsNetwork = () => {
@@ -89,7 +86,6 @@ const SettingsNetwork = () => {
<Formik <Formik
initialValues={{ initialValues={{
csrfProtection: data?.csrfProtection, csrfProtection: data?.csrfProtection,
forceIpv4First: data?.forceIpv4First,
trustProxy: data?.trustProxy, trustProxy: data?.trustProxy,
proxyEnabled: data?.proxy?.enabled, proxyEnabled: data?.proxy?.enabled,
proxyHostname: data?.proxy?.hostname, proxyHostname: data?.proxy?.hostname,
@@ -104,28 +100,20 @@ const SettingsNetwork = () => {
validationSchema={NetworkSettingsSchema} validationSchema={NetworkSettingsSchema}
onSubmit={async (values) => { onSubmit={async (values) => {
try { try {
const res = await fetch('/api/v1/settings/network', { await axios.post('/api/v1/settings/network', {
method: 'POST', csrfProtection: values.csrfProtection,
headers: { trustProxy: values.trustProxy,
'Content-Type': 'application/json', proxy: {
enabled: values.proxyEnabled,
hostname: values.proxyHostname,
port: values.proxyPort,
useSsl: values.proxySsl,
user: values.proxyUser,
password: values.proxyPassword,
bypassFilter: values.proxyBypassFilter,
bypassLocalAddresses: values.proxyBypassLocalAddresses,
}, },
body: JSON.stringify({
csrfProtection: values.csrfProtection,
forceIpv4First: values.forceIpv4First,
trustProxy: values.trustProxy,
proxy: {
enabled: values.proxyEnabled,
hostname: values.proxyHostname,
port: values.proxyPort,
useSsl: values.proxySsl,
user: values.proxyUser,
password: values.proxyPassword,
bypassFilter: values.proxyBypassFilter,
bypassLocalAddresses: values.proxyBypassLocalAddresses,
},
}),
}); });
if (!res.ok) throw new Error();
mutate('/api/v1/settings/public'); mutate('/api/v1/settings/public');
mutate('/api/v1/status'); mutate('/api/v1/status');
@@ -381,46 +369,6 @@ const SettingsNetwork = () => {
</div> </div>
</> </>
)} )}
<h3 className="heading mt-10">
{intl.formatMessage(messages.advancedNetworkSettings)}
</h3>
<p className="description">
{intl.formatMessage(messages.networkDisclaimer, {
docs: (
<a
href="https://docs.jellyseerr.dev/troubleshooting"
target="_blank"
rel="noreferrer"
className="text-white"
>
{intl.formatMessage(messages.docs)}
</a>
),
})}
</p>
<div className="form-row">
<label htmlFor="forceIpv4First" className="checkbox-label">
<span className="mr-2">
{intl.formatMessage(messages.forceIpv4First)}
</span>
<SettingsBadge badgeType="advanced" className="mr-2" />
<SettingsBadge badgeType="restartRequired" />
<SettingsBadge badgeType="experimental" />
<span className="label-tip">
{intl.formatMessage(messages.forceIpv4FirstTip)}
</span>
</label>
<div className="form-input-area">
<Field
type="checkbox"
id="forceIpv4First"
name="forceIpv4First"
onChange={() => {
setFieldValue('forceIpv4First', !values.forceIpv4First);
}}
/>
</div>
</div>
<div className="actions"> <div className="actions">
<div className="flex justify-end"> <div className="flex justify-end">
<span className="ml-3 inline-flex rounded-md shadow-sm"> <span className="ml-3 inline-flex rounded-md shadow-sm">

View File

@@ -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';
@@ -243,15 +244,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();
}; };
@@ -270,12 +265,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);
@@ -298,30 +292,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();
}; };
@@ -336,19 +316,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();
} }
if (onComplete) { if (onComplete) {
@@ -416,19 +392,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();
@@ -782,27 +751,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),

View File

@@ -17,6 +17,7 @@ import { PencilIcon, PlusIcon, TrashIcon } from '@heroicons/react/24/solid';
import type OverrideRule from '@server/entity/OverrideRule'; import type OverrideRule from '@server/entity/OverrideRule';
import type { OverrideRuleResultsResponse } from '@server/interfaces/api/overrideRuleInterfaces'; import type { OverrideRuleResultsResponse } from '@server/interfaces/api/overrideRuleInterfaces';
import type { RadarrSettings, SonarrSettings } from '@server/lib/settings'; import type { RadarrSettings, SonarrSettings } from '@server/lib/settings';
import axios from 'axios';
import { Fragment, useState } from 'react'; import { Fragment, useState } from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import useSWR, { mutate } from 'swr'; import useSWR, { mutate } from 'swr';
@@ -248,14 +249,9 @@ const SettingsServices = () => {
}); });
const deleteServer = async () => { const deleteServer = async () => {
const res = await fetch( await axios.delete(
`/api/v1/settings/${deleteServerModal.type}/${deleteServerModal.serverId}`, `/api/v1/settings/${deleteServerModal.type}/${deleteServerModal.serverId}`
{
method: 'DELETE',
}
); );
if (!res.ok) throw new Error();
setDeleteServerModal({ open: false, serverId: null, type: 'radarr' }); setDeleteServerModal({ open: false, serverId: null, type: 'radarr' });
revalidateRadarr(); revalidateRadarr();
revalidateSonarr(); revalidateSonarr();

View File

@@ -10,6 +10,7 @@ import defineMessages from '@app/utils/defineMessages';
import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline'; import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline';
import { MediaServerType } from '@server/constants/server'; import { MediaServerType } from '@server/constants/server';
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';
@@ -116,29 +117,22 @@ const SettingsUsers = () => {
enableReinitialize enableReinitialize
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', localLogin: values.localLogin,
headers: { mediaServerLogin: values.mediaServerLogin,
'Content-Type': 'application/json', newPlexLogin: values.newPlexLogin,
}, defaultQuotas: {
body: JSON.stringify({ movie: {
localLogin: values.localLogin, quotaLimit: values.movieQuotaLimit,
mediaServerLogin: values.mediaServerLogin, quotaDays: values.movieQuotaDays,
newPlexLogin: values.newPlexLogin,
defaultQuotas: {
movie: {
quotaLimit: values.movieQuotaLimit,
quotaDays: values.movieQuotaDays,
},
tv: {
quotaLimit: values.tvQuotaLimit,
quotaDays: values.tvQuotaDays,
},
}, },
defaultPermissions: values.defaultPermissions, tv: {
}), quotaLimit: values.tvQuotaLimit,
quotaDays: values.tvQuotaDays,
},
},
defaultPermissions: values.defaultPermissions,
}); });
if (!res.ok) throw new Error();
mutate('/api/v1/settings/public'); mutate('/api/v1/settings/public');
addToast(intl.formatMessage(messages.toastSettingsSuccess), { addToast(intl.formatMessage(messages.toastSettingsSuccess), {

View File

@@ -5,6 +5,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 { SonarrSettings } from '@server/lib/settings'; import type { SonarrSettings } 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';
@@ -163,24 +164,19 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
}) => { }) => {
setIsTesting(true); setIsTesting(true);
try { try {
const res = await fetch('/api/v1/settings/sonarr/test', { const response = await axios.post<SonarrTestResponse>(
method: 'POST', '/api/v1/settings/sonarr/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: SonarrTestResponse = await res.json();
setIsValidated(true); setIsValidated(true);
setTestResponse(data); setTestResponse(response.data);
if (initialLoad.current) { if (initialLoad.current) {
addToast(intl.formatMessage(messages.toastSonarrTestSuccess), { addToast(intl.formatMessage(messages.toastSonarrTestSuccess), {
appearance: 'success', appearance: 'success',
@@ -297,23 +293,12 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
tagRequests: values.tagRequests, tagRequests: values.tagRequests,
}; };
if (!sonarr) { if (!sonarr) {
const res = await fetch('/api/v1/settings/sonarr', { await axios.post('/api/v1/settings/sonarr', 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/sonarr/${sonarr.id}`, { await axios.put(
method: 'PUT', `/api/v1/settings/sonarr/${sonarr.id}`,
headers: { submission
'Content-Type': 'application/json', );
},
body: JSON.stringify(submission),
});
if (!res.ok) throw new Error();
} }
onSave(); onSave();

View File

@@ -4,6 +4,7 @@ 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 { MediaServerType, ServerType } from '@server/constants/server'; import { MediaServerType, ServerType } from '@server/constants/server';
import axios from 'axios';
import { Field, Form, Formik } from 'formik'; import { Field, Form, Formik } from 'formik';
import { FormattedMessage, useIntl } from 'react-intl'; import { FormattedMessage, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications'; import { useToasts } from 'react-toast-notifications';
@@ -112,38 +113,19 @@ function JellyfinSetup({
validationSchema={LoginSchema} validationSchema={LoginSchema}
onSubmit={async (values) => { onSubmit={async (values) => {
try { try {
// Check if serverType is either 'Jellyfin' or 'Emby' await axios.post('/api/v1/auth/jellyfin', {
// if (serverType !== 'Jellyfin' && serverType !== 'Emby') { username: values.username,
// throw new Error('Invalid serverType'); // You can customize the error message password: values.password,
// } hostname: values.hostname,
port: values.port,
const res = await fetch('/api/v1/auth/jellyfin', { useSsl: values.useSsl,
method: 'POST', urlBase: values.urlBase,
headers: { email: values.email,
'Content-Type': 'application/json', serverType: serverType,
},
body: JSON.stringify({
username: values.username,
password: values.password,
hostname: values.hostname,
port: values.port,
useSsl: values.useSsl,
urlBase: values.urlBase,
email: values.email,
serverType: serverType,
}),
}); });
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;

View File

@@ -1,6 +1,7 @@
import PlexLoginButton from '@app/components/Login/PlexLoginButton'; import PlexLoginButton from '@app/components/Login/PlexLoginButton';
import { useUser } from '@app/hooks/useUser'; import { useUser } from '@app/hooks/useUser';
import defineMessages from '@app/utils/defineMessages'; import defineMessages from '@app/utils/defineMessages';
import axios from 'axios';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
@@ -24,19 +25,9 @@ const LoginWithPlex = ({ onComplete }: LoginWithPlexProps) => {
useEffect(() => { useEffect(() => {
const login = async () => { const login = async () => {
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();
const data = await res.json();
if (data?.id) { if (response.data?.id) {
revalidate(); revalidate();
} }
}; };

View File

@@ -4,6 +4,7 @@ import JellyfinSetup from '@app/components/Setup/JellyfinSetup';
import { useUser } from '@app/hooks/useUser'; import { useUser } from '@app/hooks/useUser';
import defineMessages from '@app/utils/defineMessages'; import defineMessages from '@app/utils/defineMessages';
import { MediaServerType } from '@server/constants/server'; import { MediaServerType } from '@server/constants/server';
import axios from 'axios';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
@@ -40,19 +41,11 @@ const SetupLogin: React.FC<LoginWithMediaServerProps> = ({
useEffect(() => { useEffect(() => {
const login = async () => { const login = async () => {
const res = await fetch('/api/v1/auth/plex', { const response = await axios.post('/api/v1/auth/plex', {
method: 'POST', authToken: authToken,
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
authToken: authToken,
}),
}); });
if (!res.ok) throw new Error();
const data = await res.json();
if (data?.email) { if (response.data?.email) {
revalidate(); revalidate();
} }
}; };

View File

@@ -15,6 +15,7 @@ import useSettings from '@app/hooks/useSettings';
import defineMessages from '@app/utils/defineMessages'; import defineMessages from '@app/utils/defineMessages';
import { MediaServerType } from '@server/constants/server'; import { MediaServerType } from '@server/constants/server';
import type { Library } from '@server/lib/settings'; import type { Library } from '@server/lib/settings';
import axios from 'axios';
import Image from 'next/image'; import Image from 'next/image';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
@@ -57,27 +58,15 @@ const Setup = () => {
const finishSetup = async () => { const finishSetup = async () => {
setIsUpdating(true); setIsUpdating(true);
const res = await fetch('/api/v1/settings/initialize', { const response = await axios.post<{ initialized: boolean }>(
method: 'POST', '/api/v1/settings/initialize'
headers: { );
'Content-Type': 'application/json',
},
});
if (!res.ok) throw new Error();
const data: { initialized: boolean } = await res.json();
setIsUpdating(false); setIsUpdating(false);
if (data.initialized) { if (response.data.initialized) {
const mainRes = await fetch('/api/v1/settings/main', { await axios.post('/api/v1/settings/main', { locale });
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ locale }),
});
if (!mainRes.ok) throw new Error();
mutate('/api/v1/settings/public'); mutate('/api/v1/settings/public');
router.push('/'); router.push('/');
} }
}; };
@@ -94,11 +83,9 @@ const Setup = () => {
const endpoint = endpointMap[mediaServerType]; const endpoint = endpointMap[mediaServerType];
if (!endpoint) return; if (!endpoint) return;
const res = await fetch(endpoint); const response = await axios.get(endpoint);
if (!res.ok) throw new Error('Fetch failed');
const data = await res.json();
const hasEnabledLibraries = data?.libraries?.some( const hasEnabledLibraries = response.data?.libraries?.some(
(library: Library) => library.enabled (library: Library) => library.enabled
); );

View File

@@ -2,6 +2,7 @@ import Button from '@app/components/Common/Button';
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 { CheckIcon, TrashIcon } from '@heroicons/react/24/solid'; import { CheckIcon, TrashIcon } from '@heroicons/react/24/solid';
import axios from 'axios';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { mutate } from 'swr'; import { mutate } from 'swr';
@@ -24,10 +25,7 @@ const ErrorCard = ({ id, tmdbId, tvdbId, type, canExpand }: ErrorCardProps) => {
const intl = useIntl(); const intl = useIntl();
const deleteMedia = async () => { const deleteMedia = async () => {
const res = await fetch(`/api/v1/media/${id}`, { await axios.delete(`/api/v1/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');
}; };

View File

@@ -23,6 +23,7 @@ import {
import { MediaStatus } from '@server/constants/media'; import { MediaStatus } from '@server/constants/media';
import type { Watchlist } from '@server/entity/Watchlist'; import type { Watchlist } from '@server/entity/Watchlist';
import type { MediaType } from '@server/models/Search'; import type { MediaType } from '@server/models/Search';
import axios from 'axios';
import Link from 'next/link'; import Link from 'next/link';
import { Fragment, useCallback, useEffect, useRef, useState } from 'react'; import { Fragment, useCallback, useEffect, useRef, useState } from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
@@ -108,21 +109,13 @@ const TitleCard = ({
const onClickWatchlistBtn = async (): Promise<void> => { const onClickWatchlistBtn = async (): Promise<void> => {
setIsUpdating(true); setIsUpdating(true);
try { try {
const res = await fetch('/api/v1/watchlist', { const response = await axios.post<Watchlist>('/api/v1/watchlist', {
method: 'POST', tmdbId: id,
headers: { mediaType,
'Content-Type': 'application/json', title,
},
body: JSON.stringify({
tmdbId: id,
mediaType,
title,
}),
}); });
if (!res.ok) throw new Error();
const data: Watchlist = await res.json();
mutate('/api/v1/discover/watchlist'); mutate('/api/v1/discover/watchlist');
if (data) { if (response.data) {
addToast( addToast(
<span> <span>
{intl.formatMessage(messages.watchlistSuccess, { {intl.formatMessage(messages.watchlistSuccess, {
@@ -147,11 +140,9 @@ const TitleCard = ({
const onClickDeleteWatchlistBtn = async (): Promise<void> => { const onClickDeleteWatchlistBtn = async (): Promise<void> => {
setIsUpdating(true); setIsUpdating(true);
try { try {
const res = await fetch('/api/v1/watchlist/' + id, { const response = await axios.delete<Watchlist>('/api/v1/watchlist/' + id);
method: 'DELETE',
}); if (response.status === 204) {
if (!res.ok) throw new Error();
if (res.status === 204) {
addToast( addToast(
<span> <span>
{intl.formatMessage(messages.watchlistDeleted, { {intl.formatMessage(messages.watchlistDeleted, {
@@ -182,21 +173,13 @@ const TitleCard = ({
const topNode = cardRef.current; const topNode = cardRef.current;
if (topNode) { if (topNode) {
const res = await fetch('/api/v1/blacklist', { try {
method: 'POST', await axios.post('/api/v1/blacklist', {
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
tmdbId: id, tmdbId: id,
mediaType, mediaType,
title, title,
user: user?.id, user: user?.id,
}), });
});
if (res.status === 201) {
addToast( addToast(
<span> <span>
{intl.formatMessage(globalMessages.blacklistSuccess, { {intl.formatMessage(globalMessages.blacklistSuccess, {
@@ -207,21 +190,23 @@ const TitleCard = ({
{ appearance: 'success', autoDismiss: true } { appearance: 'success', autoDismiss: true }
); );
setCurrentStatus(MediaStatus.BLACKLISTED); setCurrentStatus(MediaStatus.BLACKLISTED);
} else if (res.status === 412) { } catch (e) {
addToast( if (e?.response?.status === 412) {
<span> addToast(
{intl.formatMessage(globalMessages.blacklistDuplicateError, { <span>
title, {intl.formatMessage(globalMessages.blacklistDuplicateError, {
strong: (msg: React.ReactNode) => <strong>{msg}</strong>, title,
})} strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
</span>, })}
{ appearance: 'info', autoDismiss: true } </span>,
); { appearance: 'info', autoDismiss: true }
} else { );
addToast(intl.formatMessage(globalMessages.blacklistError), { } else {
appearance: 'error', addToast(intl.formatMessage(globalMessages.blacklistError), {
autoDismiss: true, appearance: 'error',
}); autoDismiss: true,
});
}
} }
setIsUpdating(false); setIsUpdating(false);
@@ -239,9 +224,7 @@ const TitleCard = ({
const topNode = cardRef.current; const topNode = cardRef.current;
if (topNode) { if (topNode) {
const res = await fetch('/api/v1/blacklist/' + id, { const res = await axios.delete('/api/v1/blacklist/' + id);
method: 'DELETE',
});
if (res.status === 204) { if (res.status === 204) {
addToast( addToast(

View File

@@ -57,6 +57,7 @@ import {
import { MediaServerType } from '@server/constants/server'; import { MediaServerType } from '@server/constants/server';
import type { Crew } from '@server/models/common'; import type { Crew } from '@server/models/common';
import type { TvDetails as TvDetailsType } from '@server/models/Tv'; import type { TvDetails as TvDetailsType } from '@server/models/Tv';
import axios from 'axios';
import { countries } from 'country-flag-icons'; import { countries } from 'country-flag-icons';
import 'country-flag-icons/3x2/flags.css'; import 'country-flag-icons/3x2/flags.css';
import Link from 'next/link'; import Link from 'next/link';
@@ -349,32 +350,12 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
const onClickWatchlistBtn = async (): Promise<void> => { const onClickWatchlistBtn = async (): Promise<void> => {
setIsUpdating(true); setIsUpdating(true);
const res = await fetch('/api/v1/watchlist', { try {
method: 'POST', await axios.post('/api/v1/watchlist', {
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
tmdbId: tv?.id, tmdbId: tv?.id,
mediaType: MediaType.TV, mediaType: MediaType.TV,
title: tv?.name, title: tv?.name,
}),
});
if (!res.ok) {
addToast(intl.formatMessage(messages.watchlistError), {
appearance: 'error',
autoDismiss: true,
}); });
setIsUpdating(false);
return;
}
const data = await res.json();
if (data) {
addToast( addToast(
<span> <span>
{intl.formatMessage(messages.watchlistSuccess, { {intl.formatMessage(messages.watchlistSuccess, {
@@ -384,30 +365,25 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
</span>, </span>,
{ appearance: 'success', autoDismiss: true } { appearance: 'success', autoDismiss: true }
); );
}
setIsUpdating(false); setIsUpdating(false);
setToggleWatchlist((prevState) => !prevState); setToggleWatchlist((prevState) => !prevState);
}; } catch {
const onClickDeleteWatchlistBtn = async (): Promise<void> => {
setIsUpdating(true);
const res = await fetch('/api/v1/watchlist/' + tv?.id, {
method: 'DELETE',
});
if (!res.ok) {
addToast(intl.formatMessage(messages.watchlistError), { addToast(intl.formatMessage(messages.watchlistError), {
appearance: 'error', appearance: 'error',
autoDismiss: true, autoDismiss: true,
}); });
setIsUpdating(false); setIsUpdating(false);
return;
} }
};
const onClickDeleteWatchlistBtn = async (): Promise<void> => {
setIsUpdating(true);
try {
await axios.delete('/api/v1/watchlist/' + tv?.id);
if (res.status === 204) {
addToast( addToast(
<span> <span>
{intl.formatMessage(messages.watchlistDeleted, { {intl.formatMessage(messages.watchlistDeleted, {
@@ -417,55 +393,60 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
</span>, </span>,
{ appearance: 'info', autoDismiss: true } { appearance: 'info', autoDismiss: true }
); );
setIsUpdating(false); setIsUpdating(false);
setToggleWatchlist((prevState) => !prevState); setToggleWatchlist((prevState) => !prevState);
} catch {
addToast(intl.formatMessage(messages.watchlistError), {
appearance: 'error',
autoDismiss: true,
});
setIsUpdating(false);
} }
}; };
const onClickHideItemBtn = async (): Promise<void> => { const onClickHideItemBtn = async (): Promise<void> => {
setIsBlacklistUpdating(true); setIsBlacklistUpdating(true);
const res = await fetch('/api/v1/blacklist', { try {
method: 'POST', const res = await axios.post('/api/v1/blacklist', {
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
tmdbId: tv?.id, tmdbId: tv?.id,
mediaType: 'tv', mediaType: 'tv',
title: tv?.name, title: tv?.name,
user: user?.id, user: user?.id,
}),
});
if (res.status === 201) {
addToast(
<span>
{intl.formatMessage(globalMessages.blacklistSuccess, {
title: tv?.name,
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
})}
</span>,
{ appearance: 'success', autoDismiss: true }
);
revalidate();
} else if (res.status === 412) {
addToast(
<span>
{intl.formatMessage(globalMessages.blacklistDuplicateError, {
title: tv?.name,
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
})}
</span>,
{ appearance: 'info', autoDismiss: true }
);
} else {
addToast(intl.formatMessage(globalMessages.blacklistError), {
appearance: 'error',
autoDismiss: true,
}); });
if (res.status === 201) {
addToast(
<span>
{intl.formatMessage(globalMessages.blacklistSuccess, {
title: tv?.name,
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
})}
</span>,
{ appearance: 'success', autoDismiss: true }
);
revalidate();
}
} catch (e) {
if (e?.response?.status === 412) {
addToast(
<span>
{intl.formatMessage(globalMessages.blacklistDuplicateError, {
title: tv?.name,
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
})}
</span>,
{ appearance: 'info', autoDismiss: true }
);
} else {
addToast(intl.formatMessage(globalMessages.blacklistError), {
appearance: 'error',
autoDismiss: true,
});
}
} }
setIsBlacklistUpdating(false); setIsBlacklistUpdating(false);

View File

@@ -5,6 +5,7 @@ import { Permission, useUser } from '@app/hooks/useUser';
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 { hasPermission } from '@server/lib/permissions'; import { hasPermission } from '@server/lib/permissions';
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';
@@ -45,18 +46,10 @@ const BulkEditModal = ({
const updateUsers = async () => { const updateUsers = async () => {
try { try {
setIsSaving(true); setIsSaving(true);
const res = await fetch('/api/v1/user', { const { data: updated } = await axios.put<User[]>(`/api/v1/user`, {
method: 'PUT', ids: selectedUserIds,
headers: { permissions: currentPermission,
'Content-Type': 'application/json',
},
body: JSON.stringify({
ids: selectedUserIds,
permissions: currentPermission,
}),
}); });
if (!res.ok) throw new Error();
const updated: User[] = await res.json();
if (onComplete) { if (onComplete) {
onComplete(updated); onComplete(updated);
} }

View File

@@ -6,6 +6,7 @@ import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages'; import defineMessages from '@app/utils/defineMessages';
import { MediaServerType } from '@server/constants/server'; import { MediaServerType } from '@server/constants/server';
import type { UserResultsResponse } from '@server/interfaces/api/userInterfaces'; import type { UserResultsResponse } from '@server/interfaces/api/userInterfaces';
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';
@@ -59,17 +60,10 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
setImporting(true); setImporting(true);
try { try {
const res = await fetch('/api/v1/user/import-from-jellyfin', { const { data: createdUsers } = await axios.post(
method: 'POST', '/api/v1/user/import-from-jellyfin',
headers: { { jellyfinUserIds: selectedUsers }
'Content-Type': 'application/json', );
},
body: JSON.stringify({
jellyfinUserIds: selectedUsers,
}),
});
if (!res.ok) throw new Error();
const createdUsers = await res.json();
if (!createdUsers.length) { if (!createdUsers.length) {
throw new Error('No users were imported from Jellyfin.'); throw new Error('No users were imported from Jellyfin.');

View File

@@ -3,6 +3,7 @@ import Modal from '@app/components/Common/Modal';
import useSettings from '@app/hooks/useSettings'; 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 axios from 'axios';
import Image from 'next/image'; import Image from 'next/image';
import { useState } from 'react'; import { useState } from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
@@ -47,17 +48,10 @@ const PlexImportModal = ({ onCancel, onComplete }: PlexImportProps) => {
setImporting(true); setImporting(true);
try { try {
const res = await fetch('/api/v1/user/import-from-plex', { const { data: createdUsers } = await axios.post(
method: 'POST', '/api/v1/user/import-from-plex',
headers: { { plexIds: selectedUsers }
'Content-Type': 'application/json', );
},
body: JSON.stringify({
plexIds: selectedUsers,
}),
});
if (!res.ok) throw new Error();
const createdUsers = await res.json();
if (!Array.isArray(createdUsers) || createdUsers.length === 0) { if (!Array.isArray(createdUsers) || createdUsers.length === 0) {
throw new Error('No users were imported from Plex.'); throw new Error('No users were imported from Plex.');

View File

@@ -28,6 +28,7 @@ import {
import { MediaServerType } from '@server/constants/server'; import { MediaServerType } from '@server/constants/server';
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 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 { useRouter } from 'next/router'; import { useRouter } from 'next/router';
@@ -181,10 +182,7 @@ const UserList = () => {
setDeleting(true); setDeleting(true);
try { try {
const res = await fetch(`/api/v1/user/${deleteModal.user?.id}`, { await axios.delete(`/api/v1/user/${deleteModal.user?.id}`);
method: 'DELETE',
});
if (!res.ok) throw new Error();
addToast(intl.formatMessage(messages.userdeleted), { addToast(intl.formatMessage(messages.userdeleted), {
autoDismiss: true, autoDismiss: true,
@@ -286,34 +284,20 @@ const UserList = () => {
validationSchema={CreateUserSchema} validationSchema={CreateUserSchema}
onSubmit={async (values) => { onSubmit={async (values) => {
try { try {
const res = await fetch('/api/v1/user', { await axios.post('/api/v1/user', {
method: 'POST', username: values.username,
headers: { email: values.email,
'Content-Type': 'application/json', password: values.genpassword ? null : values.password,
},
body: JSON.stringify({
username: values.username,
email: values.email,
password: values.genpassword ? null : values.password,
}),
}); });
if (!res.ok) throw new Error(res.statusText, { cause: res });
addToast(intl.formatMessage(messages.usercreatedsuccess), { addToast(intl.formatMessage(messages.usercreatedsuccess), {
appearance: 'success', appearance: 'success',
autoDismiss: true, autoDismiss: true,
}); });
setCreateModal({ isOpen: false }); setCreateModal({ isOpen: false });
} catch (e) { } catch (e) {
let errorData;
try {
errorData = await e.cause?.text();
errorData = JSON.parse(errorData);
} catch {
/* empty */
}
addToast( addToast(
intl.formatMessage( intl.formatMessage(
errorData.errors?.includes('USER_EXISTS') e?.response?.data?.errors?.includes('USER_EXISTS')
? messages.usercreatedfailedexisting ? messages.usercreatedfailedexisting
: messages.usercreatedfailed : messages.usercreatedfailed
), ),

View File

@@ -16,6 +16,7 @@ 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 { UserSettingsGeneralResponse } from '@server/interfaces/api/userSettingsInterfaces'; import type { UserSettingsGeneralResponse } from '@server/interfaces/api/userSettingsInterfaces';
import axios from 'axios';
import { Field, Form, Formik } from 'formik'; import { Field, Form, Formik } from 'formik';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -164,33 +165,24 @@ const UserGeneralSettings = () => {
enableReinitialize enableReinitialize
onSubmit={async (values) => { onSubmit={async (values) => {
try { try {
const res = await fetch(`/api/v1/user/${user?.id}/settings/main`, { await axios.post(`/api/v1/user/${user?.id}/settings/main`, {
method: 'POST', username: values.displayName,
headers: { email:
'Content-Type': 'application/json', values.email || user?.jellyfinUsername || user?.plexUsername,
}, discordId: values.discordId,
body: JSON.stringify({ locale: values.locale,
username: values.displayName, discoverRegion: values.discoverRegion,
email: streamingRegion: values.streamingRegion,
values.email || user?.jellyfinUsername || user?.plexUsername, originalLanguage: values.originalLanguage,
discordId: values.discordId, movieQuotaLimit: movieQuotaEnabled
locale: values.locale, ? values.movieQuotaLimit
discoverRegion: values.discoverRegion, : null,
streamingRegion: values.streamingRegion, movieQuotaDays: movieQuotaEnabled ? values.movieQuotaDays : null,
originalLanguage: values.originalLanguage, tvQuotaLimit: tvQuotaEnabled ? values.tvQuotaLimit : null,
movieQuotaLimit: movieQuotaEnabled tvQuotaDays: tvQuotaEnabled ? values.tvQuotaDays : null,
? values.movieQuotaLimit watchlistSyncMovies: values.watchlistSyncMovies,
: null, watchlistSyncTv: values.watchlistSyncTv,
movieQuotaDays: movieQuotaEnabled
? values.movieQuotaDays
: null,
tvQuotaLimit: tvQuotaEnabled ? values.tvQuotaLimit : null,
tvQuotaDays: tvQuotaEnabled ? values.tvQuotaDays : null,
watchlistSyncMovies: values.watchlistSyncMovies,
watchlistSyncTv: values.watchlistSyncTv,
}),
}); });
if (!res.ok) throw new Error(res.statusText, { cause: res });
if (currentUser?.id === user?.id && setLocale) { if (currentUser?.id === user?.id && setLocale) {
setLocale( setLocale(
@@ -205,14 +197,7 @@ const UserGeneralSettings = () => {
appearance: 'success', appearance: 'success',
}); });
} catch (e) { } catch (e) {
let errorData; if (e?.response?.data?.message === ApiErrorCode.InvalidEmail) {
try {
errorData = await e.cause?.text();
errorData = JSON.parse(errorData);
} catch {
/* empty */
}
if (errorData?.message === ApiErrorCode.InvalidEmail) {
if (values.email) { if (values.email) {
addToast( addToast(
intl.formatMessage(messages.toastSettingsFailureEmail), intl.formatMessage(messages.toastSettingsFailureEmail),

View File

@@ -5,6 +5,7 @@ import { useUser } from '@app/hooks/useUser';
import defineMessages from '@app/utils/defineMessages'; import defineMessages from '@app/utils/defineMessages';
import { Transition } from '@headlessui/react'; import { Transition } from '@headlessui/react';
import { MediaServerType } from '@server/constants/server'; import { MediaServerType } from '@server/constants/server';
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';
@@ -80,38 +81,28 @@ const LinkJellyfinModal: React.FC<LinkJellyfinModalProps> = ({
onSubmit={async ({ username, password }) => { onSubmit={async ({ username, password }) => {
try { try {
setError(null); setError(null);
const res = await fetch( await axios.post(
`/api/v1/user/${user?.id}/settings/linked-accounts/jellyfin`, `/api/v1/user/${user?.id}/settings/linked-accounts/jellyfin`,
{ {
method: 'POST', username,
headers: { password,
'Content-Type': 'application/json',
},
body: JSON.stringify({
username,
password,
}),
} }
); );
if (!res.ok) { onSave();
if (res.status === 401) {
setError(
intl.formatMessage(messages.errorUnauthorized, {
mediaServerName,
})
);
} else if (res.status === 422) {
setError(
intl.formatMessage(messages.errorExists, { applicationName })
);
} else {
setError(intl.formatMessage(messages.errorUnknown));
}
} else {
onSave();
}
} catch (e) { } catch (e) {
setError(intl.formatMessage(messages.errorUnknown)); if (e?.response?.status === 401) {
setError(
intl.formatMessage(messages.errorUnauthorized, {
mediaServerName,
})
);
} else if (e?.response?.status === 422) {
setError(
intl.formatMessage(messages.errorExists, { applicationName })
);
} else {
setError(intl.formatMessage(messages.errorUnknown));
}
} }
}} }}
> >

View File

@@ -12,6 +12,7 @@ import defineMessages from '@app/utils/defineMessages';
import PlexOAuth from '@app/utils/plex'; import PlexOAuth from '@app/utils/plex';
import { TrashIcon } from '@heroicons/react/24/solid'; import { TrashIcon } from '@heroicons/react/24/solid';
import { MediaServerType } from '@server/constants/server'; import { MediaServerType } from '@server/constants/server';
import axios from 'axios';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
@@ -91,25 +92,21 @@ const UserLinkedAccountsSettings = () => {
setError(null); setError(null);
try { try {
const authToken = await plexOAuth.login(); const authToken = await plexOAuth.login();
const res = await fetch( await axios.post(
`/api/v1/user/${user?.id}/settings/linked-accounts/plex`, `/api/v1/user/${user?.id}/settings/linked-accounts/plex`,
{ {
method: 'POST', authToken,
body: JSON.stringify({ authToken }),
} }
); );
if (!res.ok) { await revalidateUser();
if (res.status === 401) {
setError(intl.formatMessage(messages.plexErrorUnauthorized));
} else if (res.status === 422) {
setError(intl.formatMessage(messages.plexErrorExists));
} else {
setError(intl.formatMessage(messages.errorUnknown));
}
} else {
await revalidateUser();
}
} catch (e) { } catch (e) {
if (e?.response?.status === 401) {
setError(intl.formatMessage(messages.plexErrorUnauthorized));
} else if (e?.response?.status === 422) {
setError(intl.formatMessage(messages.plexErrorExists));
} else {
setError(intl.formatMessage(messages.errorUnknown));
}
setError(intl.formatMessage(messages.errorUnknown)); setError(intl.formatMessage(messages.errorUnknown));
} }
}; };
@@ -143,11 +140,9 @@ const UserLinkedAccountsSettings = () => {
const deleteRequest = async (account: string) => { const deleteRequest = async (account: string) => {
try { try {
const res = await fetch( await axios.delete(
`/api/v1/user/${user?.id}/settings/linked-accounts/${account}`, `/api/v1/user/${user?.id}/settings/linked-accounts/${account}`
{ method: 'DELETE' }
); );
if (!res.ok) throw new Error();
} catch { } catch {
setError(intl.formatMessage(messages.deleteFailed)); setError(intl.formatMessage(messages.deleteFailed));
} }

View File

@@ -6,6 +6,7 @@ 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 type { UserSettingsNotificationsResponse } from '@server/interfaces/api/userSettingsInterfaces'; import type { UserSettingsNotificationsResponse } from '@server/interfaces/api/userSettingsInterfaces';
import axios from 'axios';
import { Field, Form, Formik } from 'formik'; import { Field, Form, Formik } from 'formik';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
@@ -67,28 +68,18 @@ const UserNotificationsDiscord = () => {
enableReinitialize enableReinitialize
onSubmit={async (values) => { onSubmit={async (values) => {
try { try {
const res = await fetch( await axios.post(`/api/v1/user/${user?.id}/settings/notifications`, {
`/api/v1/user/${user?.id}/settings/notifications`, pgpKey: data?.pgpKey,
{ discordId: values.discordId,
method: 'POST', pushbulletAccessToken: data?.pushbulletAccessToken,
headers: { pushoverApplicationToken: data?.pushoverApplicationToken,
'Content-Type': 'application/json', pushoverUserKey: data?.pushoverUserKey,
}, telegramChatId: data?.telegramChatId,
body: JSON.stringify({ telegramSendSilently: data?.telegramSendSilently,
pgpKey: data?.pgpKey, notificationTypes: {
discordId: values.discordId, discord: values.types,
pushbulletAccessToken: data?.pushbulletAccessToken, },
pushoverApplicationToken: data?.pushoverApplicationToken, });
pushoverUserKey: data?.pushoverUserKey,
telegramChatId: data?.telegramChatId,
telegramSendSilently: data?.telegramSendSilently,
notificationTypes: {
discord: values.types,
},
}),
}
);
if (!res.ok) throw new Error();
addToast(intl.formatMessage(messages.discordsettingssaved), { addToast(intl.formatMessage(messages.discordsettingssaved), {
appearance: 'success', appearance: 'success',
autoDismiss: true, autoDismiss: true,

View File

@@ -11,6 +11,7 @@ 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 type { UserSettingsNotificationsResponse } from '@server/interfaces/api/userSettingsInterfaces'; import type { UserSettingsNotificationsResponse } from '@server/interfaces/api/userSettingsInterfaces';
import axios from 'axios';
import { Form, Formik } from 'formik'; import { Form, Formik } from 'formik';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
@@ -66,28 +67,18 @@ const UserEmailSettings = () => {
enableReinitialize enableReinitialize
onSubmit={async (values) => { onSubmit={async (values) => {
try { try {
const res = await fetch( await axios.post(`/api/v1/user/${user?.id}/settings/notifications`, {
`/api/v1/user/${user?.id}/settings/notifications`, pgpKey: values.pgpKey,
{ discordId: data?.discordId,
method: 'POST', pushbulletAccessToken: data?.pushbulletAccessToken,
headers: { pushoverApplicationToken: data?.pushoverApplicationToken,
'Content-Type': 'application/json', pushoverUserKey: data?.pushoverUserKey,
}, telegramChatId: data?.telegramChatId,
body: JSON.stringify({ telegramSendSilently: data?.telegramSendSilently,
pgpKey: values.pgpKey, notificationTypes: {
discordId: data?.discordId, email: values.types,
pushbulletAccessToken: data?.pushbulletAccessToken, },
pushoverApplicationToken: data?.pushoverApplicationToken, });
pushoverUserKey: data?.pushoverUserKey,
telegramChatId: data?.telegramChatId,
telegramSendSilently: data?.telegramSendSilently,
notificationTypes: {
email: values.types,
},
}),
}
);
if (!res.ok) throw new Error();
addToast(intl.formatMessage(messages.emailsettingssaved), { addToast(intl.formatMessage(messages.emailsettingssaved), {
appearance: 'success', appearance: 'success',
autoDismiss: true, autoDismiss: true,

View File

@@ -6,6 +6,7 @@ import { useUser } from '@app/hooks/useUser';
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 type { UserSettingsNotificationsResponse } from '@server/interfaces/api/userSettingsInterfaces'; import type { UserSettingsNotificationsResponse } from '@server/interfaces/api/userSettingsInterfaces';
import axios from 'axios';
import { Form, Formik } from 'formik'; import { Form, Formik } from 'formik';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
@@ -64,28 +65,18 @@ const UserPushbulletSettings = () => {
enableReinitialize enableReinitialize
onSubmit={async (values) => { onSubmit={async (values) => {
try { try {
const res = await fetch( await axios.post(`/api/v1/user/${user?.id}/settings/notifications`, {
`/api/v1/user/${user?.id}/settings/notifications`, pgpKey: data?.pgpKey,
{ discordId: data?.discordId,
method: 'POST', pushbulletAccessToken: values.pushbulletAccessToken,
headers: { pushoverApplicationToken: data?.pushoverApplicationToken,
'Content-Type': 'application/json', pushoverUserKey: data?.pushoverUserKey,
}, telegramChatId: data?.telegramChatId,
body: JSON.stringify({ telegramSendSilently: data?.telegramSendSilently,
pgpKey: data?.pgpKey, notificationTypes: {
discordId: data?.discordId, pushbullet: values.types,
pushbulletAccessToken: values.pushbulletAccessToken, },
pushoverApplicationToken: data?.pushoverApplicationToken, });
pushoverUserKey: data?.pushoverUserKey,
telegramChatId: data?.telegramChatId,
telegramSendSilently: data?.telegramSendSilently,
notificationTypes: {
pushbullet: values.types,
},
}),
}
);
if (!res.ok) throw new Error();
addToast(intl.formatMessage(messages.pushbulletsettingssaved), { addToast(intl.formatMessage(messages.pushbulletsettingssaved), {
appearance: 'success', appearance: 'success',
autoDismiss: true, autoDismiss: true,

View File

@@ -7,6 +7,7 @@ import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages'; import defineMessages from '@app/utils/defineMessages';
import type { PushoverSound } from '@server/api/pushover'; import type { PushoverSound } from '@server/api/pushover';
import type { UserSettingsNotificationsResponse } from '@server/interfaces/api/userSettingsInterfaces'; import type { UserSettingsNotificationsResponse } from '@server/interfaces/api/userSettingsInterfaces';
import axios from 'axios';
import { Field, Form, Formik } from 'formik'; import { Field, Form, Formik } from 'formik';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
@@ -96,28 +97,18 @@ const UserPushoverSettings = () => {
enableReinitialize enableReinitialize
onSubmit={async (values) => { onSubmit={async (values) => {
try { try {
const res = await fetch( await axios.post(`/api/v1/user/${user?.id}/settings/notifications`, {
`/api/v1/user/${user?.id}/settings/notifications`, pgpKey: data?.pgpKey,
{ discordId: data?.discordId,
method: 'POST', pushbulletAccessToken: data?.pushbulletAccessToken,
headers: { pushoverApplicationToken: values.pushoverApplicationToken,
'Content-Type': 'application/json', pushoverUserKey: values.pushoverUserKey,
}, telegramChatId: data?.telegramChatId,
body: JSON.stringify({ telegramSendSilently: data?.telegramSendSilently,
pgpKey: data?.pgpKey, notificationTypes: {
discordId: data?.discordId, pushover: values.types,
pushbulletAccessToken: data?.pushbulletAccessToken, },
pushoverApplicationToken: values.pushoverApplicationToken, });
pushoverUserKey: values.pushoverUserKey,
telegramChatId: data?.telegramChatId,
telegramSendSilently: data?.telegramSendSilently,
notificationTypes: {
pushover: values.types,
},
}),
}
);
if (!res.ok) throw new Error();
addToast(intl.formatMessage(messages.pushoversettingssaved), { addToast(intl.formatMessage(messages.pushoversettingssaved), {
appearance: 'success', appearance: 'success',
autoDismiss: true, autoDismiss: true,

Some files were not shown because too many files have changed in this diff Show More