Compare commits

...

21 Commits

Author SHA1 Message Date
gauthier-th
7528a7c266 refactor: clean the rate limit utility 2024-07-13 20:31:31 +02:00
gauthier-th
8be79fcad8 fix(jellyfinapi): add missing argument in JellyfinAPI constructor 2024-07-13 02:03:46 +02:00
Gauthier
e9d6046997 feat: set IPv4 first as an option 2024-07-11 16:33:33 +02:00
Gauthier
eddbf2d0ec revert: remove retry to external API requests 2024-07-11 15:48:45 +02:00
gauthier-th
6d9a1c596e fix: imageProxy rate limit 2024-07-11 15:48:43 +02:00
Gauthier
c4054531cc fix: try to fix network errors with IPv6 2024-07-11 15:48:43 +02:00
gauthier-th
98c5500967 feat: add retry to external API requests 2024-07-11 15:48:42 +02:00
gauthier-th
943d84e4d5 fix: better log for ExternalAPI errors 2024-07-11 15:48:42 +02:00
gauthier-th
faec3260f6 fix: use the right search params for ExternalAPI 2024-07-11 15:48:41 +02:00
gauthier-th
6bcaa672ad fix: switch from URL objects to strings 2024-07-11 15:48:41 +02:00
gauthier-th
15568b1f6b refactor: switch frontend from axios to fetch api 2024-07-11 15:48:40 +02:00
gauthier-th
0f6ba544f8 refactor: switch server from axios to fetch api 2024-07-11 15:48:39 +02:00
gauthier-th
27cd125532 refactor: add rate limit to fetch api 2024-07-11 15:48:39 +02:00
gauthier-th
8f6cbefbb3 refactor: add rate limit to fetch api 2024-07-11 15:48:38 +02:00
gauthier-th
1b2b137760 refactor: make tautulli use the ExternalAPI class 2024-07-11 15:48:38 +02:00
gauthier-th
543341d9f9 ci: try to fix format checker 2024-07-11 15:48:37 +02:00
gauthier-th
5865bfac96 ci: ci: try to fix format checker 2024-07-11 15:48:37 +02:00
gauthier-th
c5e35aad05 ci: try to fix format checker 2024-07-11 15:48:36 +02:00
gauthier-th
a46cdb3a4f fix: send proper URL params 2024-07-11 15:48:36 +02:00
gauthier-th
bebbd690b4 fix: add missing auth token in Plex request 2024-07-11 15:48:35 +02:00
gauthier-th
99443d2796 refactor: switch ExternalAPI to Fetch API 2024-07-11 15:48:34 +02:00
100 changed files with 5380 additions and 10870 deletions

View File

@@ -8,3 +8,4 @@ pnpm-lock.yaml
# assets # assets
src/assets/ src/assets/
public/ public/
docs/

View File

@@ -3,6 +3,12 @@ module.exports = {
singleQuote: true, singleQuote: true,
trailingComma: 'es5', trailingComma: 'es5',
overrides: [ overrides: [
{
files: 'pnpm-lock.yaml',
options: {
rangeEnd: 0, // default: Infinity
},
},
{ {
files: 'gen-docs/pnpm-lock.yaml', files: 'gen-docs/pnpm-lock.yaml',
options: { options: {

View File

@@ -79,7 +79,7 @@ pnpm start
``` ```
</TabItem> </TabItem>
</Tabs> </Tabs>
:::info :::info
You can now access Jellyseerr by visiting `http://localhost:5055` in your web browser. You can now access Jellyseerr by visiting `http://localhost:5055` in your web browser.
::: :::
@@ -99,6 +99,9 @@ PORT=5055
## Uncomment if your media server is emby instead of jellyfin. ## Uncomment if your media server is emby instead of jellyfin.
# JELLYFIN_TYPE=emby # JELLYFIN_TYPE=emby
## Uncomment if you want to force Node.js to resolve IPv4 before IPv6 (advanced users only)
# FORCE_IPV4_FIRST=true
``` ```
2. Then run the following commands: 2. Then run the following commands:
```bash ```bash
@@ -312,7 +315,7 @@ node dist/index.js
Now, Jellyseerr will start when the computer boots up in the background. Now, Jellyseerr will start when the computer boots up in the background.
</TabItem> </TabItem>
<TabItem value="nssm" label="NSSM"> <TabItem value="nssm" label="NSSM">
To run jellyseerr as a service: To run jellyseerr as a service:
1. Download the [Non-Sucking Service Manager](https://nssm.cc/download) 1. Download the [Non-Sucking Service Manager](https://nssm.cc/download)

View File

@@ -4,6 +4,7 @@
module.exports = { module.exports = {
env: { env: {
commitTag: process.env.COMMIT_TAG || 'local', commitTag: process.env.COMMIT_TAG || 'local',
forceIpv4First: process.env.FORCE_IPV4_FIRST === 'true' ? 'true' : 'false',
}, },
publicRuntimeConfig: { publicRuntimeConfig: {
// Will be available on both server and client // Will be available on both server and client

View File

@@ -43,8 +43,6 @@
"@svgr/webpack": "6.5.1", "@svgr/webpack": "6.5.1",
"@tanem/react-nprogress": "5.0.30", "@tanem/react-nprogress": "5.0.30",
"ace-builds": "1.15.2", "ace-builds": "1.15.2",
"axios": "1.3.4",
"axios-rate-limit": "1.3.0",
"bcrypt": "5.1.0", "bcrypt": "5.1.0",
"bowser": "2.11.0", "bowser": "2.11.0",
"connect-typeorm": "1.1.4", "connect-typeorm": "1.1.4",
@@ -121,7 +119,7 @@
"@types/express": "4.17.17", "@types/express": "4.17.17",
"@types/express-session": "1.17.6", "@types/express-session": "1.17.6",
"@types/lodash": "4.14.191", "@types/lodash": "4.14.191",
"@types/node": "17.0.36", "@types/node": "20.14.8",
"@types/node-schedule": "2.1.0", "@types/node-schedule": "2.1.0",
"@types/nodemailer": "6.4.7", "@types/nodemailer": "6.4.7",
"@types/react": "^18.3.3", "@types/react": "^18.3.3",

12799
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,8 @@
import logger from '@server/logger'; import logger from '@server/logger';
import axios from 'axios'; import fs, { promises as fsp } from 'node:fs';
import fs, { promises as fsp } from 'fs'; import path from 'node:path';
import path from 'path'; import { Readable } from 'node:stream';
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
@@ -161,13 +162,18 @@ class AnimeListMapping {
label: 'Anime-List Sync', label: 'Anime-List Sync',
}); });
try { try {
const response = await axios.get(MAPPING_URL, { const response = await fetch(MAPPING_URL);
responseType: 'stream', if (!response.ok) {
}); throw new Error(`Failed to fetch: ${response.statusText}`);
await new Promise<void>((resolve) => { }
await new Promise<void>((resolve, reject) => {
const writer = fs.createWriteStream(LOCAL_PATH); const writer = fs.createWriteStream(LOCAL_PATH);
writer.on('finish', resolve); writer.on('finish', resolve);
response.data.pipe(writer); writer.on('error', reject);
if (!response.body) return reject();
Readable.fromWeb(response.body as ReadableStream<Uint8Array>).pipe(
writer
);
}); });
} catch (e) { } catch (e) {
throw new Error(`Failed to download Anime-List mapping: ${e.message}`); throw new Error(`Failed to download Anime-List mapping: ${e.message}`);

View File

@@ -1,6 +1,5 @@
import type { AxiosInstance, AxiosRequestConfig } from 'axios'; import type { RateLimitOptions } from '@server/utils/rateLimit';
import axios from 'axios'; import rateLimit from '@server/utils/rateLimit';
import rateLimit from 'axios-rate-limit';
import type NodeCache from 'node-cache'; import type NodeCache from 'node-cache';
// 5 minute default TTL (in seconds) // 5 minute default TTL (in seconds)
@@ -12,71 +11,84 @@ const DEFAULT_ROLLING_BUFFER = 10000;
interface ExternalAPIOptions { interface ExternalAPIOptions {
nodeCache?: NodeCache; nodeCache?: NodeCache;
headers?: Record<string, unknown>; headers?: Record<string, unknown>;
rateLimit?: { rateLimit?: RateLimitOptions;
maxRPS: number;
maxRequests: number;
};
} }
class ExternalAPI { class ExternalAPI {
protected axios: AxiosInstance; protected fetch: typeof fetch;
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, unknown>, params: Record<string, string> = {},
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.axios = rateLimit(this.axios, { this.fetch = rateLimit(fetch, options.rateLimit);
maxRequests: options.rateLimit.maxRequests, } else {
maxRPS: options.rateLimit.maxRPS, this.fetch = fetch;
});
} }
this.baseUrl = baseUrl; this.baseUrl = baseUrl;
this.params = params;
this.defaultHeaders = {
'Content-Type': 'application/json',
Accept: 'application/json',
...options.headers,
};
this.cache = options.nodeCache; this.cache = options.nodeCache;
} }
protected async get<T>( protected async get<T>(
endpoint: string, endpoint: string,
config?: AxiosRequestConfig, params?: Record<string, string>,
ttl?: number ttl?: number,
config?: RequestInit
): Promise<T> { ): Promise<T> {
const cacheKey = this.serializeCacheKey(endpoint, config?.params); const cacheKey = this.serializeCacheKey(endpoint, {
...this.params,
...params,
});
const cachedItem = this.cache?.get<T>(cacheKey); const cachedItem = this.cache?.get<T>(cacheKey);
if (cachedItem) { if (cachedItem) {
return cachedItem; return cachedItem;
} }
const response = await this.axios.get<T>(endpoint, config); const url = this.formatUrl(endpoint, params);
const response = await this.fetch(url, {
...config,
headers: {
...this.defaultHeaders,
...config?.headers,
},
});
if (!response.ok) {
const text = await response.text();
throw new Error(
`${response.status} ${response.statusText}${text ? ': ' + text : ''}`
);
}
const data = await this.getDataFromResponse(response);
if (this.cache) { if (this.cache) {
this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL); this.cache.set(cacheKey, data, ttl ?? DEFAULT_TTL);
} }
return response.data; return data;
} }
protected async post<T>( protected async post<T>(
endpoint: string, endpoint: string,
data: Record<string, unknown>, data: Record<string, unknown>,
config?: AxiosRequestConfig, params?: Record<string, string>,
ttl?: number ttl?: number,
config?: RequestInit
): Promise<T> { ): Promise<T> {
const cacheKey = this.serializeCacheKey(endpoint, { const cacheKey = this.serializeCacheKey(endpoint, {
config: config?.params, config: { ...this.params, ...params },
data, data,
}); });
const cachedItem = this.cache?.get<T>(cacheKey); const cachedItem = this.cache?.get<T>(cacheKey);
@@ -84,21 +96,89 @@ class ExternalAPI {
return cachedItem; return cachedItem;
} }
const response = await this.axios.post<T>(endpoint, data, config); const url = this.formatUrl(endpoint, params);
const response = await this.fetch(url, {
method: 'POST',
...config,
headers: {
...this.defaultHeaders,
...config?.headers,
},
body: JSON.stringify(data),
});
const resData = await this.getDataFromResponse(response);
if (this.cache) { if (this.cache) {
this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL); this.cache.set(cacheKey, resData, ttl ?? DEFAULT_TTL);
} }
return response.data; return resData;
}
protected async put<T>(
endpoint: string,
data: Record<string, unknown>,
params?: Record<string, string>,
ttl?: number,
config?: RequestInit
): Promise<T> {
const cacheKey = this.serializeCacheKey(endpoint, {
config: { ...this.params, ...params },
data,
});
const cachedItem = this.cache?.get<T>(cacheKey);
if (cachedItem) {
return cachedItem;
}
const url = this.formatUrl(endpoint, params);
const response = await this.fetch(url, {
method: 'PUT',
...config,
headers: {
...this.defaultHeaders,
...config?.headers,
},
body: JSON.stringify(data),
});
const resData = await this.getDataFromResponse(response);
if (this.cache) {
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, {
...config,
headers: {
...this.defaultHeaders,
...config?.headers,
},
});
const data = await this.getDataFromResponse(response);
return data;
} }
protected async getRolling<T>( protected async getRolling<T>(
endpoint: string, endpoint: string,
config?: AxiosRequestConfig, params?: Record<string, string>,
ttl?: number ttl?: number,
config?: RequestInit,
overwriteBaseUrl?: string
): Promise<T> { ): Promise<T> {
const cacheKey = this.serializeCacheKey(endpoint, config?.params); const cacheKey = this.serializeCacheKey(endpoint, {
...this.params,
...params,
});
const cachedItem = this.cache?.get<T>(cacheKey); const cachedItem = this.cache?.get<T>(cacheKey);
if (cachedItem) { if (cachedItem) {
@@ -109,20 +189,53 @@ class ExternalAPI {
keyTtl - (ttl ?? DEFAULT_TTL) * 1000 < keyTtl - (ttl ?? DEFAULT_TTL) * 1000 <
Date.now() - DEFAULT_ROLLING_BUFFER Date.now() - DEFAULT_ROLLING_BUFFER
) { ) {
this.axios.get<T>(endpoint, config).then((response) => { const url = this.formatUrl(endpoint, params, overwriteBaseUrl);
this.cache?.set(cacheKey, response.data, ttl ?? DEFAULT_TTL); this.fetch(url, {
...config,
headers: {
...this.defaultHeaders,
...config?.headers,
},
}).then(async (response) => {
const data = await this.getDataFromResponse(response);
this.cache?.set(cacheKey, data, ttl ?? DEFAULT_TTL);
}); });
} }
return cachedItem; return cachedItem;
} }
const response = await this.axios.get<T>(endpoint, config); const url = this.formatUrl(endpoint, params, overwriteBaseUrl);
const response = await this.fetch(url, {
...config,
headers: {
...this.defaultHeaders,
...config?.headers,
},
});
const data = await this.getDataFromResponse(response);
if (this.cache) { if (this.cache) {
this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL); this.cache.set(cacheKey, data, ttl ?? DEFAULT_TTL);
} }
return response.data; return data;
}
private formatUrl(
endpoint: string,
params?: Record<string, string>,
overwriteBaseUrl?: string
): string {
const baseUrl = overwriteBaseUrl || this.baseUrl;
const href =
baseUrl +
(baseUrl.endsWith('/') ? '' : '/') +
(endpoint.startsWith('/') ? endpoint.slice(1) : endpoint);
const searchParams = new URLSearchParams({
...this.params,
...params,
});
return `${href}?${searchParams.toString()}`;
} }
private serializeCacheKey( private serializeCacheKey(
@@ -135,6 +248,25 @@ class ExternalAPI {
return `${this.baseUrl}${endpoint}${JSON.stringify(params)}`; return `${this.baseUrl}${endpoint}${JSON.stringify(params)}`;
} }
private async getDataFromResponse(response: Response) {
const contentType = response.headers.get('Content-Type')?.split(';')[0];
if (contentType === 'application/json') {
return await response.json();
} else if (
contentType === 'application/xml' ||
contentType === 'text/html' ||
contentType === 'text/plain'
) {
return await response.text();
} else {
try {
return await response.json();
} catch {
return await response.blob();
}
}
}
} }
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,10 +67,6 @@ 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,
} }
); );
@@ -85,9 +81,7 @@ class GithubAPI extends ExternalAPI {
const data = await this.get<GitHubRelease[]>( const data = await this.get<GitHubRelease[]>(
'/repos/fallenbagel/jellyseerr/releases', '/repos/fallenbagel/jellyseerr/releases',
{ {
params: { per_page: take.toString(),
per_page: take,
},
} }
); );
@@ -112,10 +106,8 @@ class GithubAPI extends ExternalAPI {
const data = await this.get<GithubCommit[]>( const data = await this.get<GithubCommit[]>(
'/repos/fallenbagel/jellyseerr/commits', '/repos/fallenbagel/jellyseerr/commits',
{ {
params: { per_page: take.toString(),
per_page: take, branch,
branch,
},
} }
); );

View File

@@ -111,8 +111,6 @@ class JellyfinAPI extends ExternalAPI {
{ {
headers: { headers: {
'X-Emby-Authorization': authHeaderVal, 'X-Emby-Authorization': authHeaderVal,
'Content-Type': 'application/json',
Accept: 'application/json',
}, },
} }
); );
@@ -127,7 +125,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 = const headers: { [key: string]: string } =
useHeaders && ClientIP ? { 'X-Forwarded-For': ClientIP } : {}; useHeaders && ClientIP ? { 'X-Forwarded-For': ClientIP } : {};
return this.post<JellyfinLoginResponse>( return this.post<JellyfinLoginResponse>(
@@ -136,6 +134,8 @@ class JellyfinAPI extends ExternalAPI {
Username, Username,
Pw: Password, Pw: Password,
}, },
{},
undefined,
{ headers } { headers }
); );
}; };
@@ -296,7 +296,16 @@ class JellyfinAPI extends ExternalAPI {
public async getLibraryContents(id: string): Promise<JellyfinLibraryItem[]> { public async getLibraryContents(id: string): Promise<JellyfinLibraryItem[]> {
try { try {
const libraryItemsResponse = await this.get<any>( const libraryItemsResponse = await this.get<any>(
`/Users/${this.userId}/Items?SortBy=SortName&SortOrder=Ascending&IncludeItemTypes=Series,Movie,Others&Recursive=true&StartIndex=0&ParentId=${id}&collapseBoxSetItems=false` `/Users/${this.userId}/Items`,
{
SortBy: 'SortName',
SortOrder: 'Ascending',
IncludeItemTypes: 'Series,Movie,Others',
Recursive: 'true',
StartIndex: '0',
ParentId: id,
collapseBoxSetItems: 'false',
}
); );
return libraryItemsResponse.Items.filter( return libraryItemsResponse.Items.filter(
@@ -315,7 +324,11 @@ class JellyfinAPI extends ExternalAPI {
public async getRecentlyAdded(id: string): Promise<JellyfinLibraryItem[]> { public async getRecentlyAdded(id: string): Promise<JellyfinLibraryItem[]> {
try { try {
const itemResponse = await this.get<any>( const itemResponse = await this.get<any>(
`/Users/${this.userId}/Items/Latest?Limit=12&ParentId=${id}` `/Users/${this.userId}/Items/Latest`,
{
Limit: '12',
ParentId: id,
}
); );
return itemResponse; return itemResponse;
@@ -374,7 +387,10 @@ 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?seasonId=${seasonID}` `/Shows/${seriesID}/Episodes`,
{
seasonId: seasonID,
}
); );
return episodeResponse.Items.filter( return episodeResponse.Items.filter(

View File

@@ -1,9 +1,9 @@
import ExternalAPI from '@server/api/externalapi';
import type { PlexDevice } from '@server/interfaces/api/plexInterfaces'; import type { PlexDevice } from '@server/interfaces/api/plexInterfaces';
import cacheManager from '@server/lib/cache'; import cacheManager from '@server/lib/cache';
import { getSettings } from '@server/lib/settings'; import { getSettings } from '@server/lib/settings';
import logger from '@server/logger'; import logger from '@server/logger';
import xml2js from 'xml2js'; import xml2js from 'xml2js';
import ExternalAPI from './externalapi';
interface PlexAccountResponse { interface PlexAccountResponse {
user: PlexUser; user: PlexUser;
@@ -137,8 +137,6 @@ 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,
} }
@@ -149,15 +147,11 @@ class PlexTvAPI extends ExternalAPI {
public async getDevices(): Promise<PlexDevice[]> { public async getDevices(): Promise<PlexDevice[]> {
try { try {
const devicesResp = await this.axios.get( const devicesResp = await this.get('/api/resources', {
'/api/resources?includeHttps=1', includeHttps: '1',
{ });
transformResponse: [],
responseType: 'text',
}
);
const parsedXml = await xml2js.parseStringPromise( const parsedXml = await xml2js.parseStringPromise(
devicesResp.data as DeviceResponse devicesResp as DeviceResponse
); );
return parsedXml?.MediaContainer?.Device?.map((pxml: DeviceResponse) => ({ return parsedXml?.MediaContainer?.Device?.map((pxml: DeviceResponse) => ({
name: pxml.$.name, name: pxml.$.name,
@@ -205,11 +199,11 @@ class PlexTvAPI extends ExternalAPI {
public async getUser(): Promise<PlexUser> { public async getUser(): Promise<PlexUser> {
try { try {
const account = await this.axios.get<PlexAccountResponse>( const account = await this.get<PlexAccountResponse>(
'/users/account.json' '/users/account.json'
); );
return account.data.user; return account.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,13 +243,10 @@ class PlexTvAPI extends ExternalAPI {
} }
public async getUsers(): Promise<UsersResponse> { public async getUsers(): Promise<UsersResponse> {
const response = await this.axios.get('/api/users', { const data = await this.get('/api/users');
transformResponse: [],
responseType: 'text',
});
const parsedXml = (await xml2js.parseStringPromise( const parsedXml = (await xml2js.parseStringPromise(
response.data data as string
)) as UsersResponse; )) as UsersResponse;
return parsedXml; return parsedXml;
} }
@@ -270,49 +261,49 @@ class PlexTvAPI extends ExternalAPI {
items: PlexWatchlistItem[]; items: PlexWatchlistItem[];
}> { }> {
try { try {
const response = await this.axios.get<WatchlistResponse>( const params = new URLSearchParams({
'/library/sections/watchlist/all', 'X-Plex-Container-Start': offset.toString(),
'X-Plex-Container-Size': size.toString(),
});
const response = await this.fetch(
`https://metadata.provider.plex.tv/library/sections/watchlist/all?${params.toString()}`,
{ {
params: { headers: this.defaultHeaders,
'X-Plex-Container-Start': offset,
'X-Plex-Container-Size': size,
},
baseURL: 'https://metadata.provider.plex.tv',
} }
); );
const data = (await response.json()) as WatchlistResponse;
const watchlistDetails = await Promise.all( const watchlistDetails = await Promise.all(
(response.data.MediaContainer.Metadata ?? []).map( (data.MediaContainer.Metadata ?? []).map(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];
const tmdbString = metadata.Guid.find((guid) => const tmdbString = metadata.Guid.find((guid) =>
guid.id.startsWith('tmdb') guid.id.startsWith('tmdb')
); );
const tvdbString = metadata.Guid.find((guid) => const tvdbString = metadata.Guid.find((guid) =>
guid.id.startsWith('tvdb') guid.id.startsWith('tvdb')
); );
return { return {
ratingKey: metadata.ratingKey, ratingKey: metadata.ratingKey,
// This should always be set? But I guess it also cannot be? // This should always be set? But I guess it also cannot be?
// We will filter out the 0's afterwards // We will filter out the 0's afterwards
tmdbId: tmdbString ? Number(tmdbString.id.split('//')[1]) : 0, tmdbId: tmdbString ? Number(tmdbString.id.split('//')[1]) : 0,
tvdbId: tvdbString tvdbId: tvdbString
? Number(tvdbString.id.split('//')[1]) ? Number(tvdbString.id.split('//')[1])
: undefined, : undefined,
title: metadata.title, title: metadata.title,
type: metadata.type, type: metadata.type,
}; };
} })
)
); );
const filteredList = watchlistDetails.filter((detail) => detail.tmdbId); const filteredList = watchlistDetails.filter((detail) => detail.tmdbId);
@@ -320,7 +311,7 @@ class PlexTvAPI extends ExternalAPI {
return { return {
offset, offset,
size, size,
totalSize: response.data.MediaContainer.totalSize, totalSize: data.MediaContainer.totalSize,
items: filteredList, items: filteredList,
}; };
} catch (e) { } catch (e) {

View File

@@ -1,4 +1,4 @@
import ExternalAPI from './externalapi'; import ExternalAPI from '@server/api/externalapi';
interface PushoverSoundsResponse { interface PushoverSoundsResponse {
sounds: { sounds: {
@@ -26,24 +26,13 @@ export const mapSounds = (sounds: {
class PushoverAPI extends ExternalAPI { class PushoverAPI extends ExternalAPI {
constructor() { constructor() {
super( super('https://api.pushover.net/1');
'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', {
params: { token: appToken,
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('https://api.radarr.video/v1', { super(
headers: { 'https://api.radarr.video/v1',
'Content-Type': 'application/json', {},
Accept: 'application/json', {
}, nodeCache: cacheManager.getCache('imdb').data,
nodeCache: cacheManager.getCache('imdb').data, }
}); );
} }
/** /**

View File

@@ -63,15 +63,12 @@ class RottenTomatoes extends ExternalAPI {
super( super(
'https://79frdp12pn-dsn.algolia.net/1/indexes/*', 'https://79frdp12pn-dsn.algolia.net/1/indexes/*',
{ {
'x-algolia-agent': 'x-algolia-agent': 'Algolia for JavaScript (4.14.3); Browser (lite)',
'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 response = await this.axios.get<SystemStatus>('/system/status'); const data = await this.get<SystemStatus>('/system/status');
return response.data; return 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,16 +157,11 @@ class ServarrBase<QueueItemAppendT> extends ExternalAPI {
public getQueue = async (): Promise<(QueueItem & QueueItemAppendT)[]> => { public getQueue = async (): Promise<(QueueItem & QueueItemAppendT)[]> => {
try { try {
const response = await this.axios.get<QueueResponse<QueueItemAppendT>>( const data = await this.get<QueueResponse<QueueItemAppendT>>(`/queue`, {
`/queue`, includeEpisode: 'true',
{ });
params: {
includeEpisode: true,
},
}
);
return response.data.records; return 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}`
@@ -176,9 +171,9 @@ class ServarrBase<QueueItemAppendT> extends ExternalAPI {
public getTags = async (): Promise<Tag[]> => { public getTags = async (): Promise<Tag[]> => {
try { try {
const response = await this.axios.get<Tag[]>(`/tag`); const data = await this.get<Tag[]>(`/tag`);
return response.data; return 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}`
@@ -188,11 +183,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 response = await this.axios.post<Tag>(`/tag`, { const data = await this.post<Tag>(`/tag`, {
label, label,
}); });
return response.data; return 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}`);
} }
@@ -203,7 +198,7 @@ class ServarrBase<QueueItemAppendT> extends ExternalAPI {
options: Record<string, unknown> options: Record<string, unknown>
): Promise<void> { ): Promise<void> {
try { try {
await this.axios.post(`/command`, { await this.post(`/command`, {
name: commandName, name: commandName,
...options, ...options,
}); });

View File

@@ -37,9 +37,9 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
public getMovies = async (): Promise<RadarrMovie[]> => { public getMovies = async (): Promise<RadarrMovie[]> => {
try { try {
const response = await this.axios.get<RadarrMovie[]>('/movie'); const data = await this.get<RadarrMovie[]>('/movie');
return response.data; return data;
} catch (e) { } catch (e) {
throw new Error(`[Radarr] Failed to retrieve movies: ${e.message}`); throw new Error(`[Radarr] Failed to retrieve movies: ${e.message}`);
} }
@@ -47,9 +47,9 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
public getMovie = async ({ id }: { id: number }): Promise<RadarrMovie> => { public getMovie = async ({ id }: { id: number }): Promise<RadarrMovie> => {
try { try {
const response = await this.axios.get<RadarrMovie>(`/movie/${id}`); const data = await this.get<RadarrMovie>(`/movie/${id}`);
return response.data; return data;
} catch (e) { } catch (e) {
throw new Error(`[Radarr] Failed to retrieve movie: ${e.message}`); throw new Error(`[Radarr] Failed to retrieve movie: ${e.message}`);
} }
@@ -57,17 +57,15 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
public async getMovieByTmdbId(id: number): Promise<RadarrMovie> { public async getMovieByTmdbId(id: number): Promise<RadarrMovie> {
try { try {
const response = await this.axios.get<RadarrMovie[]>('/movie/lookup', { const data = await this.get<RadarrMovie[]>('/movie/lookup', {
params: { term: `tmdb:${id}`,
term: `tmdb:${id}`,
},
}); });
if (!response.data[0]) { if (!data[0]) {
throw new Error('Movie not found'); throw new Error('Movie not found');
} }
return response.data[0]; return 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',
@@ -97,7 +95,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 response = await this.axios.put<RadarrMovie>(`/movie`, { const data = await this.put<RadarrMovie>(`/movie`, {
...movie, ...movie,
title: options.title, title: options.title,
qualityProfileId: options.qualityProfileId, qualityProfileId: options.qualityProfileId,
@@ -114,25 +112,25 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
}, },
}); });
if (response.data.monitored) { if (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: response.data.id, movieId: data.id,
movieTitle: response.data.title, movieTitle: data.title,
} }
); );
logger.debug('Radarr update details', { logger.debug('Radarr update details', {
label: 'Radarr', label: 'Radarr',
movie: response.data, movie: data,
}); });
if (options.searchNow) { if (options.searchNow) {
this.searchMovie(response.data.id); this.searchMovie(data.id);
} }
return response.data; return 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',
@@ -150,7 +148,7 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
return movie; return movie;
} }
const response = await this.axios.post<RadarrMovie>(`/movie`, { const data = await this.post<RadarrMovie>(`/movie`, {
title: options.title, title: options.title,
qualityProfileId: options.qualityProfileId, qualityProfileId: options.qualityProfileId,
profileId: options.profileId, profileId: options.profileId,
@@ -166,11 +164,11 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
}, },
}); });
if (response.data.id) { if (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: response.data, movie: data,
}); });
} else { } else {
logger.error('Failed to add movie to Radarr', { logger.error('Failed to add movie to Radarr', {
@@ -179,7 +177,7 @@ 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 response.data; return data;
} catch (e) { } catch (e) {
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.',
@@ -216,11 +214,9 @@ 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.axios.delete(`/movie/${id}`, { await this.delete(`/movie/${id}`, {
params: { deleteFiles: 'true',
deleteFiles: true, addImportExclusion: 'false',
addImportExclusion: false,
},
}); });
logger.info(`[Radarr] Removed movie ${title}`); logger.info(`[Radarr] Removed movie ${title}`);
} catch (e) { } catch (e) {

View File

@@ -117,9 +117,9 @@ class SonarrAPI extends ServarrBase<{
public async getSeries(): Promise<SonarrSeries[]> { public async getSeries(): Promise<SonarrSeries[]> {
try { try {
const response = await this.axios.get<SonarrSeries[]>('/series'); const data = await this.get<SonarrSeries[]>('/series');
return response.data; return 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 response = await this.axios.get<SonarrSeries>(`/series/${id}`); const data = await this.get<SonarrSeries>(`/series/${id}`);
return response.data; return 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,17 +137,15 @@ class SonarrAPI extends ServarrBase<{
public async getSeriesByTitle(title: string): Promise<SonarrSeries[]> { public async getSeriesByTitle(title: string): Promise<SonarrSeries[]> {
try { try {
const response = await this.axios.get<SonarrSeries[]>('/series/lookup', { const data = await this.get<SonarrSeries[]>('/series/lookup', {
params: { term: title,
term: title,
},
}); });
if (!response.data[0]) { if (!data[0]) {
throw new Error('No series found'); throw new Error('No series found');
} }
return response.data; return 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',
@@ -160,17 +158,15 @@ class SonarrAPI extends ServarrBase<{
public async getSeriesByTvdbId(id: number): Promise<SonarrSeries> { public async getSeriesByTvdbId(id: number): Promise<SonarrSeries> {
try { try {
const response = await this.axios.get<SonarrSeries[]>('/series/lookup', { const data = await this.get<SonarrSeries[]>('/series/lookup', {
params: { term: `tvdb:${id}`,
term: `tvdb:${id}`,
},
}); });
if (!response.data[0]) { if (!data[0]) {
throw new Error('Series not found'); throw new Error('Series not found');
} }
return response.data[0]; return 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',
@@ -191,27 +187,27 @@ class SonarrAPI extends ServarrBase<{
series.tags = options.tags ?? series.tags; series.tags = options.tags ?? series.tags;
series.seasons = this.buildSeasonList(options.seasons, series.seasons); series.seasons = this.buildSeasonList(options.seasons, series.seasons);
const newSeriesResponse = await this.axios.put<SonarrSeries>( const newSeriesData = await this.put<SonarrSeries>(
'/series', '/series',
series series as any
); );
if (newSeriesResponse.data.id) { if (newSeriesData.id) {
logger.info('Updated existing series in Sonarr.', { logger.info('Updated existing series in Sonarr.', {
label: 'Sonarr', label: 'Sonarr',
seriesId: newSeriesResponse.data.id, seriesId: newSeriesData.id,
seriesTitle: newSeriesResponse.data.title, seriesTitle: newSeriesData.title,
}); });
logger.debug('Sonarr update details', { logger.debug('Sonarr update details', {
label: 'Sonarr', label: 'Sonarr',
movie: newSeriesResponse.data, movie: newSeriesData,
}); });
if (options.searchNow) { if (options.searchNow) {
this.searchSeries(newSeriesResponse.data.id); this.searchSeries(newSeriesData.id);
} }
return newSeriesResponse.data; return newSeriesData;
} else { } else {
logger.error('Failed to update series in Sonarr', { logger.error('Failed to update series in Sonarr', {
label: 'Sonarr', label: 'Sonarr',
@@ -221,38 +217,35 @@ class SonarrAPI extends ServarrBase<{
} }
} }
const createdSeriesResponse = await this.axios.post<SonarrSeries>( const createdSeriesData = await this.post<SonarrSeries>('/series', {
'/series', tvdbId: options.tvdbid,
{ title: options.title,
tvdbId: options.tvdbid, qualityProfileId: options.profileId,
title: options.title, languageProfileId: options.languageProfileId,
qualityProfileId: options.profileId, seasons: this.buildSeasonList(
languageProfileId: options.languageProfileId, options.seasons,
seasons: this.buildSeasonList( series.seasons.map((season) => ({
options.seasons, seasonNumber: season.seasonNumber,
series.seasons.map((season) => ({ // We force all seasons to false if its the first request
seasonNumber: season.seasonNumber, monitored: false,
// We force all seasons to false if its the first request }))
monitored: false, ),
})) tags: options.tags,
), seasonFolder: options.seasonFolder,
tags: options.tags, monitored: options.monitored,
seasonFolder: options.seasonFolder, rootFolderPath: options.rootFolderPath,
monitored: options.monitored, seriesType: options.seriesType,
rootFolderPath: options.rootFolderPath, addOptions: {
seriesType: options.seriesType, ignoreEpisodesWithFiles: true,
addOptions: { searchForMissingEpisodes: options.searchNow,
ignoreEpisodesWithFiles: true, },
searchForMissingEpisodes: options.searchNow, } as Partial<SonarrSeries>);
},
} as Partial<SonarrSeries>
);
if (createdSeriesResponse.data.id) { if (createdSeriesData.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: createdSeriesResponse.data, movie: createdSeriesData,
}); });
} else { } else {
logger.error('Failed to add movie to Sonarr', { logger.error('Failed to add movie to Sonarr', {
@@ -262,7 +255,7 @@ class SonarrAPI extends ServarrBase<{
throw new Error('Failed to add series to Sonarr'); throw new Error('Failed to add series to Sonarr');
} }
return createdSeriesResponse.data; return createdSeriesData;
} catch (e) { } catch (e) {
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',
@@ -340,14 +333,13 @@ 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.axios.delete(`/series/${id}`, { await this.delete(`/series/${id}`, {
params: { deleteFiles: 'true',
deleteFiles: true, addImportExclusion: 'false',
addImportExclusion: false,
},
}); });
logger.info(`[Radarr] Removed serie ${title}`); logger.info(`[Radarr] Removed serie ${title}`);
} catch (e) { } catch (e) {

View File

@@ -1,8 +1,7 @@
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 {
@@ -113,25 +112,25 @@ interface TautulliInfoResponse {
}; };
} }
class TautulliAPI { class TautulliAPI extends ExternalAPI {
private axios: AxiosInstance;
constructor(settings: TautulliSettings) { constructor(settings: TautulliSettings) {
this.axios = axios.create({ super(
baseURL: `${settings.useSsl ? 'https' : 'http'}://${settings.hostname}:${ `${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.axios.get<TautulliInfoResponse>('/api/v2', { await this.get<TautulliInfoResponse>('/api/v2', {
params: { cmd: 'get_tautulli_info' }, cmd: 'get_tautulli_info',
}) })
).data.response.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',
@@ -148,14 +147,12 @@ class TautulliAPI {
): Promise<TautulliWatchStats[]> { ): Promise<TautulliWatchStats[]> {
try { try {
return ( return (
await this.axios.get<TautulliWatchStatsResponse>('/api/v2', { await this.get<TautulliWatchStatsResponse>('/api/v2', {
params: { cmd: 'get_item_watch_time_stats',
cmd: 'get_item_watch_time_stats', rating_key: ratingKey,
rating_key: ratingKey, grouping: '1',
grouping: 1,
},
}) })
).data.response.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',
@@ -176,14 +173,12 @@ class TautulliAPI {
): Promise<TautulliWatchUser[]> { ): Promise<TautulliWatchUser[]> {
try { try {
return ( return (
await this.axios.get<TautulliWatchUsersResponse>('/api/v2', { await this.get<TautulliWatchUsersResponse>('/api/v2', {
params: { cmd: 'get_item_user_stats',
cmd: 'get_item_user_stats', rating_key: ratingKey,
rating_key: ratingKey, grouping: '1',
grouping: 1,
},
}) })
).data.response.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',
@@ -206,15 +201,13 @@ class TautulliAPI {
} }
return ( return (
await this.axios.get<TautulliWatchStatsResponse>('/api/v2', { await this.get<TautulliWatchStatsResponse>('/api/v2', {
params: { cmd: 'get_user_watch_time_stats',
cmd: 'get_user_watch_time_stats', user_id: user.plexId.toString(),
user_id: user.plexId, query_days: '0',
query_days: 0, grouping: '1',
grouping: 1,
},
}) })
).data.response.data[0]; ).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',
@@ -245,19 +238,17 @@ class TautulliAPI {
while (results.length < 20) { while (results.length < 20) {
const tautulliData = ( const tautulliData = (
await this.axios.get<TautulliHistoryResponse>('/api/v2', { await this.get<TautulliHistoryResponse>('/api/v2', {
params: { cmd: 'get_history',
cmd: 'get_history', grouping: '1',
grouping: 1, order_column: 'date',
order_column: 'date', order_dir: 'desc',
order_dir: 'desc', user_id: user.plexId.toString(),
user_id: user.plexId, media_type: 'movie,episode',
media_type: 'movie,episode', length: take.toString(),
length: take, start: start.toString(),
start,
},
}) })
).data.response.data.data; ).response.data.data;
if (!tautulliData.length) { if (!tautulliData.length) {
return results; return results;

View File

@@ -112,8 +112,8 @@ class TheMovieDb extends ExternalAPI {
{ {
nodeCache: cacheManager.getCache('tmdb').data, nodeCache: cacheManager.getCache('tmdb').data,
rateLimit: { rateLimit: {
maxRequests: 20,
maxRPS: 50, maxRPS: 50,
id: 'tmdb',
}, },
} }
); );
@@ -129,7 +129,10 @@ 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', {
params: { query, page, include_adult: includeAdult, language }, query,
page: page.toString(),
include_adult: includeAdult ? 'true' : 'false',
language,
}); });
return data; return data;
@@ -152,13 +155,11 @@ 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', {
params: { query,
query, page: page.toString(),
page, include_adult: includeAdult ? 'true' : 'false',
include_adult: includeAdult, language,
language, primary_release_year: year?.toString() || '',
primary_release_year: year,
},
}); });
return data; return data;
@@ -181,13 +182,11 @@ 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', {
params: { query,
query, page: page.toString(),
page, include_adult: includeAdult ? 'true' : 'false',
include_adult: includeAdult, language,
language, first_air_date_year: year?.toString() || '',
first_air_date_year: year,
},
}); });
return data; return data;
@@ -210,7 +209,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}`, {
params: { language }, language,
}); });
return data; return data;
@@ -230,7 +229,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`,
{ {
params: { language }, language,
} }
); );
@@ -253,11 +252,9 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbMovieDetails>( const data = await this.get<TmdbMovieDetails>(
`/movie/${movieId}`, `/movie/${movieId}`,
{ {
params: { language,
language, append_to_response:
append_to_response: 'credits,external_ids,videos,keywords,release_dates,watch/providers',
'credits,external_ids,videos,keywords,release_dates,watch/providers',
},
}, },
43200 43200
); );
@@ -279,11 +276,9 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbTvDetails>( const data = await this.get<TmdbTvDetails>(
`/tv/${tvId}`, `/tv/${tvId}`,
{ {
params: { language,
language, append_to_response:
append_to_response: 'aggregate_credits,credits,external_ids,keywords,videos,content_ratings,watch/providers',
'aggregate_credits,credits,external_ids,keywords,videos,content_ratings,watch/providers',
},
}, },
43200 43200
); );
@@ -307,10 +302,8 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbSeasonWithEpisodes>( const data = await this.get<TmdbSeasonWithEpisodes>(
`/tv/${tvId}/season/${seasonNumber}`, `/tv/${tvId}/season/${seasonNumber}`,
{ {
params: { language: language || '',
language, append_to_response: 'external_ids',
append_to_response: 'external_ids',
},
} }
); );
@@ -333,10 +326,8 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbSearchMovieResponse>( const data = await this.get<TmdbSearchMovieResponse>(
`/movie/${movieId}/recommendations`, `/movie/${movieId}/recommendations`,
{ {
params: { page: page.toString(),
page, language,
language,
},
} }
); );
@@ -359,10 +350,8 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbSearchMovieResponse>( const data = await this.get<TmdbSearchMovieResponse>(
`/movie/${movieId}/similar`, `/movie/${movieId}/similar`,
{ {
params: { page: page.toString(),
page, language,
language,
},
} }
); );
@@ -385,10 +374,8 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbSearchMovieResponse>( const data = await this.get<TmdbSearchMovieResponse>(
`/keyword/${keywordId}/movies`, `/keyword/${keywordId}/movies`,
{ {
params: { page: page.toString(),
page, language,
language,
},
} }
); );
@@ -411,10 +398,8 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbSearchTvResponse>( const data = await this.get<TmdbSearchTvResponse>(
`/tv/${tvId}/recommendations`, `/tv/${tvId}/recommendations`,
{ {
params: { page: page.toString(),
page, language,
language,
},
} }
); );
@@ -437,10 +422,8 @@ 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`, {
params: { page: page.toString(),
page, language,
language,
},
}); });
return data; return data;
@@ -481,40 +464,38 @@ 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', {
params: { sort_by: sortBy,
sort_by: sortBy, page: page.toString(),
page, include_adult: includeAdult ? 'true' : 'false',
include_adult: includeAdult, language,
language, region: this.region || '',
region: this.region, with_original_language:
with_original_language: originalLanguage && originalLanguage !== 'all'
originalLanguage && originalLanguage !== 'all' ? originalLanguage
? originalLanguage : originalLanguage === 'all'
: originalLanguage === 'all' ? ''
? undefined : this.originalLanguage || '',
: this.originalLanguage, // Set our release date values, but check if one is set and not the other,
// Set our release date values, but check if one is set and not the other, // so we can force a past date or a future date. TMDB Requires both values if one is set!
// so we can force a past date or a future date. TMDB Requires both values if one is set! 'primary_release_date.gte':
'primary_release_date.gte': !primaryReleaseDateGte && primaryReleaseDateLte
!primaryReleaseDateGte && primaryReleaseDateLte ? defaultPastDate
? defaultPastDate : primaryReleaseDateGte || '',
: primaryReleaseDateGte, 'primary_release_date.lte':
'primary_release_date.lte': !primaryReleaseDateLte && primaryReleaseDateGte
!primaryReleaseDateLte && primaryReleaseDateGte ? defaultFutureDate
? defaultFutureDate : primaryReleaseDateLte || '',
: primaryReleaseDateLte, with_genres: genre || '',
with_genres: genre, with_companies: studio || '',
with_companies: studio, 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, watch_region: watchRegion || '',
watch_region: watchRegion, with_watch_providers: watchProviders || '',
with_watch_providers: watchProviders,
},
}); });
return data; return data;
@@ -555,40 +536,40 @@ class TheMovieDb extends ExternalAPI {
.split('T')[0]; .split('T')[0];
const data = await this.get<TmdbSearchTvResponse>('/discover/tv', { const data = await this.get<TmdbSearchTvResponse>('/discover/tv', {
params: { sort_by: sortBy,
sort_by: sortBy, page: page.toString(),
page, language,
language, region: this.region || '',
region: this.region, // Set our release date values, but check if one is set and not the other,
// Set our release date values, but check if one is set and not the other, // so we can force a past date or a future date. TMDB Requires both values if one is set!
// so we can force a past date or a future date. TMDB Requires both values if one is set! 'first_air_date.gte':
'first_air_date.gte': !firstAirDateGte && firstAirDateLte
!firstAirDateGte && firstAirDateLte ? defaultPastDate
? defaultPastDate : firstAirDateGte || '',
: firstAirDateGte, 'first_air_date.lte':
'first_air_date.lte': !firstAirDateLte && firstAirDateGte
!firstAirDateLte && firstAirDateGte ? defaultFutureDate
? defaultFutureDate : firstAirDateLte || '',
: firstAirDateLte, with_original_language:
with_original_language: originalLanguage && originalLanguage !== 'all'
originalLanguage && originalLanguage !== 'all' ? originalLanguage
? originalLanguage : originalLanguage === 'all'
: originalLanguage === 'all' ? ''
? undefined : this.originalLanguage || '',
: this.originalLanguage, include_null_first_air_dates: includeEmptyReleaseDate
include_null_first_air_dates: includeEmptyReleaseDate, ? 'true'
with_genres: genre, : 'false',
with_networks: network, with_genres: genre || '',
with_keywords: keywords, with_networks: network?.toString() || '',
'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 || '',
with_watch_providers: watchProviders, 'vote_count.lte': voteCountLte || '',
watch_region: watchRegion, with_watch_providers: watchProviders || '',
}, watch_region: watchRegion || '',
}); });
return data; return data;
@@ -608,12 +589,10 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbUpcomingMoviesResponse>( const data = await this.get<TmdbUpcomingMoviesResponse>(
'/movie/upcoming', '/movie/upcoming',
{ {
params: { page: page.toString(),
page, language,
language, region: this.region || '',
region: this.region, originalLanguage: this.originalLanguage || '',
originalLanguage: this.originalLanguage,
},
} }
); );
@@ -636,11 +615,9 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbSearchMultiResponse>( const data = await this.get<TmdbSearchMultiResponse>(
`/trending/all/${timeWindow}`, `/trending/all/${timeWindow}`,
{ {
params: { page: page.toString(),
page, language,
language, region: this.region || '',
region: this.region,
},
} }
); );
@@ -661,9 +638,7 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbSearchMovieResponse>( const data = await this.get<TmdbSearchMovieResponse>(
`/trending/movie/${timeWindow}`, `/trending/movie/${timeWindow}`,
{ {
params: { page: page.toString(),
page,
},
} }
); );
@@ -684,9 +659,7 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbSearchTvResponse>( const data = await this.get<TmdbSearchTvResponse>(
`/trending/tv/${timeWindow}`, `/trending/tv/${timeWindow}`,
{ {
params: { page: page.toString(),
page,
},
} }
); );
@@ -715,10 +688,8 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbExternalIdResponse>( const data = await this.get<TmdbExternalIdResponse>(
`/find/${externalId}`, `/find/${externalId}`,
{ {
params: { external_source: type === 'imdb' ? 'imdb_id' : 'tvdb_id',
external_source: type === 'imdb' ? 'imdb_id' : 'tvdb_id', language,
language,
},
} }
); );
@@ -808,9 +779,7 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbCollection>( const data = await this.get<TmdbCollection>(
`/collection/${collectionId}`, `/collection/${collectionId}`,
{ {
params: { language,
language,
},
} }
); );
@@ -883,9 +852,7 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbGenresResult>( const data = await this.get<TmdbGenresResult>(
'/genre/movie/list', '/genre/movie/list',
{ {
params: { language,
language,
},
}, },
86400 // 24 hours 86400 // 24 hours
); );
@@ -897,9 +864,7 @@ class TheMovieDb extends ExternalAPI {
const englishData = await this.get<TmdbGenresResult>( const englishData = await this.get<TmdbGenresResult>(
'/genre/movie/list', '/genre/movie/list',
{ {
params: { language: 'en',
language: 'en',
},
}, },
86400 // 24 hours 86400 // 24 hours
); );
@@ -934,9 +899,7 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbGenresResult>( const data = await this.get<TmdbGenresResult>(
'/genre/tv/list', '/genre/tv/list',
{ {
params: { language,
language,
},
}, },
86400 // 24 hours 86400 // 24 hours
); );
@@ -948,9 +911,7 @@ class TheMovieDb extends ExternalAPI {
const englishData = await this.get<TmdbGenresResult>( const englishData = await this.get<TmdbGenresResult>(
'/genre/tv/list', '/genre/tv/list',
{ {
params: { language: 'en',
language: 'en',
},
}, },
86400 // 24 hours 86400 // 24 hours
); );
@@ -1005,10 +966,8 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbKeywordSearchResponse>( const data = await this.get<TmdbKeywordSearchResponse>(
'/search/keyword', '/search/keyword',
{ {
params: { query,
query, page: page.toString(),
page,
},
}, },
86400 // 24 hours 86400 // 24 hours
); );
@@ -1030,10 +989,8 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbCompanySearchResponse>( const data = await this.get<TmdbCompanySearchResponse>(
'/search/company', '/search/company',
{ {
params: { query,
query, page: page.toString(),
page,
},
}, },
86400 // 24 hours 86400 // 24 hours
); );
@@ -1053,9 +1010,7 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<{ results: TmdbWatchProviderRegion[] }>( const data = await this.get<{ results: TmdbWatchProviderRegion[] }>(
'/watch/providers/regions', '/watch/providers/regions',
{ {
params: { language: language ? this.originalLanguage || '' : '',
language: language ?? this.originalLanguage,
},
}, },
86400 // 24 hours 86400 // 24 hours
); );
@@ -1079,10 +1034,8 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<{ results: TmdbWatchProviderDetails[] }>( const data = await this.get<{ results: TmdbWatchProviderDetails[] }>(
'/watch/providers/movie', '/watch/providers/movie',
{ {
params: { language: language ? this.originalLanguage || '' : '',
language: language ?? this.originalLanguage, watch_region: watchRegion,
watch_region: watchRegion,
},
}, },
86400 // 24 hours 86400 // 24 hours
); );
@@ -1106,10 +1059,8 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<{ results: TmdbWatchProviderDetails[] }>( const data = await this.get<{ results: TmdbWatchProviderDetails[] }>(
'/watch/providers/tv', '/watch/providers/tv',
{ {
params: { language: language ? this.originalLanguage || '' : '',
language: language ?? this.originalLanguage, watch_region: watchRegion,
watch_region: watchRegion,
},
}, },
86400 // 24 hours 86400 // 24 hours
); );

View File

@@ -32,10 +32,17 @@ import * as OpenApiValidator from 'express-openapi-validator';
import type { Store } from 'express-session'; import type { Store } from 'express-session';
import session from 'express-session'; import session from 'express-session';
import next from 'next'; import next from 'next';
import dns from 'node:dns';
import net from 'node:net';
import path from 'path'; import path from 'path';
import swaggerUi from 'swagger-ui-express'; import swaggerUi from 'swagger-ui-express';
import YAML from 'yamljs'; import YAML from 'yamljs';
if (process.env.forceIpv4First === 'true') {
dns.setDefaultResultOrder('ipv4first');
net.setDefaultAutoSelectFamily(false);
}
const API_SPEC_PATH = path.join(__dirname, '../overseerr-api.yml'); const API_SPEC_PATH = path.join(__dirname, '../overseerr-api.yml');
logger.info(`Starting Overseerr version ${getAppVersion()}`); logger.info(`Starting Overseerr version ${getAppVersion()}`);
@@ -56,7 +63,7 @@ app
} }
// Load Settings // Load Settings
const settings = getSettings().load(); const settings = getSettings();
restartFlag.initializeSettings(settings.main); restartFlag.initializeSettings(settings.main);
// Migrate library types // Migrate library types

View File

@@ -1,6 +1,6 @@
import logger from '@server/logger'; import logger from '@server/logger';
import axios from 'axios'; import type { RateLimitOptions } from '@server/utils/rateLimit';
import rateLimit, { type rateLimitOptions } from 'axios-rate-limit'; import rateLimit from '@server/utils/rateLimit';
import { createHash } from 'crypto'; import { createHash } from 'crypto';
import { promises } from 'fs'; import { promises } from 'fs';
import path, { join } from 'path'; import path, { join } from 'path';
@@ -98,26 +98,29 @@ class ImageProxy {
return files.length; return files.length;
} }
private axios; private fetch: typeof fetch;
private cacheVersion; private cacheVersion;
private key; private key;
private baseUrl;
constructor( constructor(
key: string, key: string,
baseUrl: string, baseUrl: string,
options: { options: {
cacheVersion?: number; cacheVersion?: number;
rateLimitOptions?: rateLimitOptions; rateLimitOptions?: RateLimitOptions;
} = {} } = {}
) { ) {
this.cacheVersion = options.cacheVersion ?? 1; this.cacheVersion = options.cacheVersion ?? 1;
this.baseUrl = baseUrl;
this.key = key; this.key = key;
this.axios = axios.create({
baseURL: baseUrl,
});
if (options.rateLimitOptions) { if (options.rateLimitOptions) {
this.axios = rateLimit(this.axios, options.rateLimitOptions); this.fetch = rateLimit(fetch, {
...options.rateLimitOptions,
});
} else {
this.fetch = fetch;
} }
} }
@@ -182,17 +185,20 @@ class ImageProxy {
): Promise<ImageResponse | null> { ): Promise<ImageResponse | null> {
try { try {
const directory = join(this.getCacheDirectory(), cacheKey); const directory = join(this.getCacheDirectory(), cacheKey);
const response = await this.axios.get(path, { const href =
responseType: 'arraybuffer', this.baseUrl +
}); (this.baseUrl.endsWith('/') ? '' : '/') +
(path.startsWith('/') ? path.slice(1) : path);
const response = await this.fetch(href);
const arrayBuffer = await response.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
const buffer = Buffer.from(response.data, 'binary');
const extension = path.split('.').pop() ?? ''; const extension = path.split('.').pop() ?? '';
const maxAge = Number( const maxAge = Number(
(response.headers['cache-control'] ?? '0').split('=')[1] (response.headers.get('cache-control') ?? '0').split('=')[1]
); );
const expireAt = Date.now() + maxAge * 1000; const expireAt = Date.now() + maxAge * 1000;
const etag = (response.headers.etag ?? '').replace(/"/g, ''); const etag = (response.headers.get('etag') ?? '').replace(/"/g, '');
await this.writeToCacheDir( await this.writeToCacheDir(
directory, directory,

View File

@@ -4,7 +4,6 @@ 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,
@@ -292,14 +291,20 @@ class DiscordAgent
} }
} }
await axios.post(settings.options.webhookUrl, { await fetch(settings.options.webhookUrl, {
username: settings.options.botUsername method: 'POST',
? settings.options.botUsername headers: {
: getSettings().main.applicationTitle, 'Content-Type': 'application/json',
avatar_url: settings.options.botAvatarUrl, },
embeds: [this.buildEmbed(type, payload)], body: JSON.stringify({
content: userMentions.join(' '), username: settings.options.botUsername
} as DiscordWebhookPayload); ? settings.options.botUsername
: getSettings().main.applicationTitle,
avatar_url: settings.options.botAvatarUrl,
embeds: [this.buildEmbed(type, payload)],
content: userMentions.join(' '),
} as DiscordWebhookPayload),
});
return true; return true;
} catch (e) { } catch (e) {

View File

@@ -2,7 +2,6 @@ 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';
@@ -133,7 +132,13 @@ 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);
await axios.post(endpoint, notificationPayload); await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(notificationPayload),
});
return true; return true;
} catch (e) { } catch (e) {

View File

@@ -3,7 +3,6 @@ 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';
@@ -101,19 +100,20 @@ class LunaSeaAgent
}); });
try { try {
await axios.post( await fetch(settings.options.webhookUrl, {
settings.options.webhookUrl, method: 'POST',
this.buildPayload(type, payload), headers: settings.options.profileName
settings.options.profileName
? { ? {
headers: { 'Content-Type': 'application/json',
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)),
});
return true; return true;
} catch (e) { } catch (e) {

View File

@@ -5,7 +5,6 @@ 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,
@@ -123,15 +122,17 @@ class PushbulletAgent
}); });
try { try {
await axios.post( await fetch(endpoint, {
endpoint, method: 'POST',
{ ...notificationPayload, channel_tag: settings.options.channelTag }, headers: {
{ 'Content-Type': 'application/json',
headers: { 'Access-Token': settings.options.accessToken,
'Access-Token': settings.options.accessToken, },
}, body: JSON.stringify({
} ...notificationPayload,
); channel_tag: settings.options.channelTag,
}),
});
} catch (e) { } catch (e) {
logger.error('Error sending Pushbullet notification', { logger.error('Error sending Pushbullet notification', {
label: 'Notifications', label: 'Notifications',
@@ -163,10 +164,13 @@ class PushbulletAgent
}); });
try { try {
await axios.post(endpoint, notificationPayload, { await fetch(endpoint, {
method: 'POST',
headers: { headers: {
'Content-Type': 'application/json',
'Access-Token': payload.notifyUser.settings.pushbulletAccessToken, 'Access-Token': payload.notifyUser.settings.pushbulletAccessToken,
}, },
body: JSON.stringify(notificationPayload),
}); });
} catch (e) { } catch (e) {
logger.error('Error sending Pushbullet notification', { logger.error('Error sending Pushbullet notification', {
@@ -211,10 +215,13 @@ class PushbulletAgent
}); });
try { try {
await axios.post(endpoint, notificationPayload, { await fetch(endpoint, {
method: 'POST',
headers: { headers: {
'Content-Type': 'application/json',
'Access-Token': user.settings.pushbulletAccessToken, 'Access-Token': user.settings.pushbulletAccessToken,
}, },
body: JSON.stringify(notificationPayload),
}); });
} catch (e) { } catch (e) {
logger.error('Error sending Pushbullet notification', { logger.error('Error sending Pushbullet notification', {

View File

@@ -5,7 +5,6 @@ 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,
@@ -52,12 +51,12 @@ class PushoverAgent
imageUrl: string imageUrl: string
): Promise<Partial<PushoverImagePayload>> { ): Promise<Partial<PushoverImagePayload>> {
try { try {
const response = await axios.get(imageUrl, { const response = await fetch(imageUrl);
responseType: 'arraybuffer', const arrayBuffer = await response.arrayBuffer();
}); const base64 = Buffer.from(arrayBuffer).toString('base64');
const base64 = Buffer.from(response.data, 'binary').toString('base64');
const contentType = ( const contentType = (
response.headers['Content-Type'] || response.headers['content-type'] response.headers.get('Content-Type') ||
response.headers.get('content-type')
)?.toString(); )?.toString();
return { return {
@@ -201,12 +200,18 @@ class PushoverAgent
}); });
try { try {
await axios.post(endpoint, { await fetch(endpoint, {
...notificationPayload, method: 'POST',
token: settings.options.accessToken, headers: {
user: settings.options.userToken, 'Content-Type': 'application/json',
sound: settings.options.sound, },
} as PushoverPayload); body: JSON.stringify({
...notificationPayload,
token: settings.options.accessToken,
user: settings.options.userToken,
sound: settings.options.sound,
} as PushoverPayload),
});
} catch (e) { } catch (e) {
logger.error('Error sending Pushover notification', { logger.error('Error sending Pushover notification', {
label: 'Notifications', label: 'Notifications',
@@ -241,12 +246,18 @@ class PushoverAgent
}); });
try { try {
await axios.post(endpoint, { await fetch(endpoint, {
...notificationPayload, method: 'POST',
token: payload.notifyUser.settings.pushoverApplicationToken, headers: {
user: payload.notifyUser.settings.pushoverUserKey, 'Content-Type': 'application/json',
sound: payload.notifyUser.settings.pushoverSound, },
} as PushoverPayload); body: JSON.stringify({
...notificationPayload,
token: payload.notifyUser.settings.pushoverApplicationToken,
user: payload.notifyUser.settings.pushoverUserKey,
sound: payload.notifyUser.settings.pushoverSound,
} as PushoverPayload),
});
} catch (e) { } catch (e) {
logger.error('Error sending Pushover notification', { logger.error('Error sending Pushover notification', {
label: 'Notifications', label: 'Notifications',
@@ -291,11 +302,17 @@ class PushoverAgent
}); });
try { try {
await axios.post(endpoint, { await fetch(endpoint, {
...notificationPayload, method: 'POST',
token: user.settings.pushoverApplicationToken, headers: {
user: user.settings.pushoverUserKey, 'Content-Type': 'application/json',
} as PushoverPayload); },
body: JSON.stringify({
...notificationPayload,
token: user.settings.pushoverApplicationToken,
user: user.settings.pushoverUserKey,
} as PushoverPayload),
});
} catch (e) { } catch (e) {
logger.error('Error sending Pushover notification', { logger.error('Error sending Pushover notification', {
label: 'Notifications', label: 'Notifications',

View File

@@ -2,7 +2,6 @@ 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';
@@ -238,10 +237,13 @@ class SlackAgent
subject: payload.subject, subject: payload.subject,
}); });
try { try {
await axios.post( await fetch(settings.options.webhookUrl, {
settings.options.webhookUrl, method: 'POST',
this.buildEmbed(type, payload) headers: {
); 'Content-Type': 'application/json',
},
body: JSON.stringify(this.buildEmbed(type, payload)),
});
return true; return true;
} catch (e) { } catch (e) {

View File

@@ -5,7 +5,6 @@ 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,
@@ -175,11 +174,17 @@ class TelegramAgent
}); });
try { try {
await axios.post(endpoint, { await fetch(endpoint, {
...notificationPayload, method: 'POST',
chat_id: settings.options.chatId, headers: {
disable_notification: !!settings.options.sendSilently, 'Content-Type': 'application/json',
} as TelegramMessagePayload | TelegramPhotoPayload); },
body: JSON.stringify({
...notificationPayload,
chat_id: settings.options.chatId,
disable_notification: !!settings.options.sendSilently,
} as TelegramMessagePayload | TelegramPhotoPayload),
});
} catch (e) { } catch (e) {
logger.error('Error sending Telegram notification', { logger.error('Error sending Telegram notification', {
label: 'Notifications', label: 'Notifications',
@@ -210,12 +215,18 @@ class TelegramAgent
}); });
try { try {
await axios.post(endpoint, { await fetch(endpoint, {
...notificationPayload, method: 'POST',
chat_id: payload.notifyUser.settings.telegramChatId, headers: {
disable_notification: 'Content-Type': 'application/json',
!!payload.notifyUser.settings.telegramSendSilently, },
} as TelegramMessagePayload | TelegramPhotoPayload); body: JSON.stringify({
...notificationPayload,
chat_id: payload.notifyUser.settings.telegramChatId,
disable_notification:
!!payload.notifyUser.settings.telegramSendSilently,
} as TelegramMessagePayload | TelegramPhotoPayload),
});
} catch (e) { } catch (e) {
logger.error('Error sending Telegram notification', { logger.error('Error sending Telegram notification', {
label: 'Notifications', label: 'Notifications',
@@ -257,11 +268,17 @@ class TelegramAgent
}); });
try { try {
await axios.post(endpoint, { await fetch(endpoint, {
...notificationPayload, method: 'POST',
chat_id: user.settings.telegramChatId, headers: {
disable_notification: !!user.settings?.telegramSendSilently, 'Content-Type': 'application/json',
} as TelegramMessagePayload | TelegramPhotoPayload); },
body: JSON.stringify({
...notificationPayload,
chat_id: user.settings.telegramChatId,
disable_notification: !!user.settings?.telegramSendSilently,
} as TelegramMessagePayload | TelegramPhotoPayload),
});
} catch (e) { } catch (e) {
logger.error('Error sending Telegram notification', { logger.error('Error sending Telegram notification', {
label: 'Notifications', label: 'Notifications',

View File

@@ -3,7 +3,6 @@ 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';
@@ -178,17 +177,16 @@ class WebhookAgent
}); });
try { try {
await axios.post( await fetch(settings.options.webhookUrl, {
settings.options.webhookUrl, method: 'POST',
this.buildPayload(type, payload), headers: {
settings.options.authHeader 'Content-Type': 'application/json',
? { ...(settings.options.authHeader
headers: { ? { Authorization: settings.options.authHeader }
Authorization: settings.options.authHeader, : {}),
}, },
} body: JSON.stringify(this.buildPayload(type, payload)),
: undefined });
);
return true; return true;
} catch (e) { } catch (e) {

View File

@@ -595,10 +595,8 @@ class JellyfinScanner {
return this.log('No admin configured. Jellyfin sync skipped.', 'warn'); return this.log('No admin configured. Jellyfin sync skipped.', 'warn');
} }
const hostname = getHostname();
this.jfClient = new JellyfinAPI( this.jfClient = new JellyfinAPI(
hostname, getHostname(),
admin.jellyfinAuthToken, admin.jellyfinAuthToken,
admin.jellyfinDeviceId admin.jellyfinDeviceId
); );

View File

@@ -656,6 +656,7 @@ class Settings {
} }
} }
let loaded = false;
let settings: Settings | undefined; let settings: Settings | undefined;
export const getSettings = (initialSettings?: AllSettings): Settings => { export const getSettings = (initialSettings?: AllSettings): Settings => {
@@ -663,6 +664,11 @@ export const getSettings = (initialSettings?: AllSettings): Settings => {
settings = new Settings(initialSettings); settings = new Settings(initialSettings);
} }
if (!loaded) {
settings.load();
loaded = true;
}
return settings; return settings;
}; };

View File

@@ -5,7 +5,6 @@ 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

@@ -389,6 +389,7 @@ settingsRoutes.get('/jellyfin/users', async (req, res) => {
order: { id: 'ASC' }, order: { id: 'ASC' },
}); });
const jellyfinClient = new JellyfinAPI( const jellyfinClient = new JellyfinAPI(
getHostname(),
admin.jellyfinAuthToken ?? '', admin.jellyfinAuthToken ?? '',
admin.jellyfinDeviceId ?? '' admin.jellyfinDeviceId ?? ''
); );

View File

@@ -497,6 +497,7 @@ router.post(
order: { id: 'ASC' }, order: { id: 'ASC' },
}); });
const jellyfinClient = new JellyfinAPI( const jellyfinClient = new JellyfinAPI(
getHostname(),
admin.jellyfinAuthToken ?? '', admin.jellyfinAuthToken ?? '',
admin.jellyfinDeviceId ?? '' admin.jellyfinDeviceId ?? ''
); );

73
server/utils/rateLimit.ts Normal file
View File

@@ -0,0 +1,73 @@
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;
}[];
activeRequests: number;
timer: NodeJS.Timeout | null;
};
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: [], activeRequests: 0, timer: null };
if (options.id) {
rateLimitById[options.id] = state;
}
const processQueue = () => {
if (state.queue.length === 0) {
if (state.timer) {
clearInterval(state.timer);
state.timer = null;
}
return;
}
while (state.activeRequests < options.maxRPS) {
state.activeRequests++;
const item = state.queue.shift();
if (!item) break;
const { args, resolve } = item;
fn(...args)
.then(resolve)
.finally(() => {
state.activeRequests--;
if (state.queue.length > 0) {
if (!state.timer) {
state.timer = setInterval(processQueue, 1000);
}
} else {
if (state.timer) {
clearInterval(state.timer);
state.timer = null;
}
}
});
}
};
return (...args: Parameters<T>): Promise<U> => {
return new Promise<U>((resolve) => {
state.queue.push({ args, resolve });
processQueue();
});
};
}

View File

@@ -14,7 +14,6 @@ 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';
@@ -77,11 +76,9 @@ 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 keyword = await axios.get<Keyword>( const res = await fetch(`/api/v1/keyword/${keywordId}`);
`/api/v1/keyword/${keywordId}` const keyword: Keyword = await res.json();
); return keyword;
return keyword.data;
}) })
); );
@@ -98,15 +95,13 @@ const CreateSlider = ({ onCreate, slider }: CreateSliderProps) => {
return; return;
} }
const response = await axios.get<TmdbGenre[]>( const res = await fetch(
`/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 = response.data.find( const genre = genres.find((genre) => genre.id === Number(slider.data));
(genre) => genre.id === Number(slider.data)
);
setDefaultDataValue([ setDefaultDataValue([
{ {
@@ -121,11 +116,8 @@ const CreateSlider = ({ onCreate, slider }: CreateSliderProps) => {
return; return;
} }
const response = await axios.get<ProductionCompany>( const res = await fetch(`/api/v1/studio/${slider.data}`);
`/api/v1/studio/${slider.data}` const studio: ProductionCompany = await res.json();
);
const studio = response.data;
setDefaultDataValue([ setDefaultDataValue([
{ {
@@ -168,16 +160,17 @@ const CreateSlider = ({ onCreate, slider }: CreateSliderProps) => {
); );
const loadKeywordOptions = async (inputValue: string) => { const loadKeywordOptions = async (inputValue: string) => {
const results = await axios.get<TmdbKeywordSearchResponse>( const res = await fetch(
'/api/v1/search/keyword', `/api/v1/search/keyword?query=${encodeURIExtraParams(inputValue)}`,
{ {
params: { headers: {
query: encodeURIExtraParams(inputValue), 'Content-Type': 'application/json',
}, },
} }
); );
const results: TmdbKeywordSearchResponse = await res.json();
return results.data.results.map((result) => ({ return results.results.map((result) => ({
label: result.name, label: result.name,
value: result.id, value: result.id,
})); }));
@@ -188,38 +181,37 @@ const CreateSlider = ({ onCreate, slider }: CreateSliderProps) => {
return []; return [];
} }
const results = await axios.get<TmdbCompanySearchResponse>( const res = await fetch(
'/api/v1/search/company', `/api/v1/search/company?query=${encodeURIExtraParams(inputValue)}`,
{ {
params: { headers: {
query: encodeURIExtraParams(inputValue), 'Content-Type': 'application/json',
}, },
} }
); );
const results: TmdbCompanySearchResponse = await res.json();
return results.data.results.map((result) => ({ return results.results.map((result) => ({
label: result.name, label: result.name,
value: result.id, value: result.id,
})); }));
}; };
const loadMovieGenreOptions = async () => { const loadMovieGenreOptions = async () => {
const results = await axios.get<GenreSliderItem[]>( const res = await fetch('/api/v1/discover/genreslider/movie');
'/api/v1/discover/genreslider/movie' const results: GenreSliderItem[] = await res.json();
);
return results.data.map((result) => ({ return results.map((result) => ({
label: result.name, label: result.name,
value: result.id, value: result.id,
})); }));
}; };
const loadTvGenreOptions = async () => { const loadTvGenreOptions = async () => {
const results = await axios.get<GenreSliderItem[]>( const res = await fetch('/api/v1/discover/genreslider/tv');
'/api/v1/discover/genreslider/tv' const results: GenreSliderItem[] = await res.json();
);
return results.data.map((result) => ({ return results.map((result) => ({
label: result.name, label: result.name,
value: result.id, value: result.id,
})); }));
@@ -314,17 +306,31 @@ const CreateSlider = ({ onCreate, slider }: CreateSliderProps) => {
onSubmit={async (values, { resetForm }) => { onSubmit={async (values, { resetForm }) => {
try { try {
if (slider) { if (slider) {
await axios.put(`/api/v1/settings/discover/${slider.id}`, { const res = await fetch(`/api/v1/settings/discover/${slider.id}`, {
type: Number(values.sliderType), method: 'PUT',
title: values.title, headers: {
data: values.data, 'Content-Type': 'application/json',
},
body: JSON.stringify({
type: Number(values.sliderType),
title: values.title,
data: values.data,
}),
}); });
if (!res.ok) throw new Error();
} else { } else {
await axios.post('/api/v1/settings/discover/add', { const res = await fetch('/api/v1/settings/discover/add', {
type: Number(values.sliderType), method: 'POST',
title: values.title, headers: {
data: values.data, 'Content-Type': 'application/json',
},
body: JSON.stringify({
type: Number(values.sliderType),
title: values.title,
data: values.data,
}),
}); });
if (!res.ok) throw new Error();
} }
addToast( addToast(

View File

@@ -20,7 +20,6 @@ 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';
@@ -78,7 +77,10 @@ const DiscoverSliderEdit = ({
const deleteSlider = async () => { const deleteSlider = async () => {
try { try {
await axios.delete(`/api/v1/settings/discover/${slider.id}`); const res = await fetch(`/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,7 +28,6 @@ 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';
@@ -76,7 +75,14 @@ const Discover = () => {
const updateSliders = async () => { const updateSliders = async () => {
try { try {
await axios.post('/api/v1/settings/discover', sliders); const res = await fetch('/api/v1/settings/discover', {
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',
@@ -94,7 +100,10 @@ const Discover = () => {
const resetSliders = async () => { const resetSliders = async () => {
try { try {
await axios.get('/api/v1/settings/discover/reset'); const res = await fetch('/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

@@ -5,7 +5,6 @@ import defineMessages from '@app/utils/defineMessages';
import { Menu, Transition } from '@headlessui/react'; import { Menu, Transition } from '@headlessui/react';
import { EllipsisVerticalIcon } from '@heroicons/react/24/solid'; import { EllipsisVerticalIcon } from '@heroicons/react/24/solid';
import type { default as IssueCommentType } from '@server/entity/IssueComment'; import type { default as IssueCommentType } from '@server/entity/IssueComment';
import axios from 'axios';
import { Field, Form, Formik } from 'formik'; import { Field, Form, Formik } from 'formik';
import Image from 'next/image'; import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
@@ -49,7 +48,10 @@ const IssueComment = ({
const deleteComment = async () => { const deleteComment = async () => {
try { try {
await axios.delete(`/api/v1/issueComment/${comment.id}`); const res = await fetch(`/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 {
@@ -175,9 +177,14 @@ const IssueComment = ({
<Formik <Formik
initialValues={{ newMessage: comment.message }} initialValues={{ newMessage: comment.message }}
onSubmit={async (values) => { onSubmit={async (values) => {
await axios.put(`/api/v1/issueComment/${comment.id}`, { const res = await fetch(
message: values.newMessage, `/api/v1/issueComment/${comment.id}`,
}); {
method: 'PUT',
body: JSON.stringify({ message: values.newMessage }),
}
);
if (!res.ok) throw new Error();
if (onUpdate) { if (onUpdate) {
onUpdate(); onUpdate();

View File

@@ -11,7 +11,7 @@ import useDeepLinks from '@app/hooks/useDeepLinks';
import useSettings from '@app/hooks/useSettings'; import useSettings from '@app/hooks/useSettings';
import { Permission, useUser } from '@app/hooks/useUser'; import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages'; import globalMessages from '@app/i18n/globalMessages';
import Error from '@app/pages/_error'; import ErrorPage from '@app/pages/_error';
import defineMessages from '@app/utils/defineMessages'; import defineMessages from '@app/utils/defineMessages';
import { Transition } from '@headlessui/react'; import { Transition } from '@headlessui/react';
import { import {
@@ -27,7 +27,6 @@ import { MediaServerType } from '@server/constants/server';
import type Issue from '@server/entity/Issue'; import type Issue from '@server/entity/Issue';
import type { MovieDetails } from '@server/models/Movie'; import type { MovieDetails } from '@server/models/Movie';
import type { TvDetails } from '@server/models/Tv'; import type { TvDetails } from '@server/models/Tv';
import axios from 'axios';
import { Field, Form, Formik } from 'formik'; import { Field, Form, Formik } from 'formik';
import getConfig from 'next/config'; import getConfig from 'next/config';
import Image from 'next/image'; import Image from 'next/image';
@@ -116,7 +115,7 @@ const IssueDetails = () => {
} }
if (!data || !issueData) { if (!data || !issueData) {
return <Error statusCode={404} />; return <ErrorPage statusCode={404} />;
} }
const belongsToUser = issueData.createdBy.id === currentUser?.id; const belongsToUser = issueData.createdBy.id === currentUser?.id;
@@ -125,9 +124,11 @@ const IssueDetails = () => {
const editFirstComment = async (newMessage: string) => { const editFirstComment = async (newMessage: string) => {
try { try {
await axios.put(`/api/v1/issueComment/${firstComment.id}`, { const res = await fetch(`/api/v1/issueComment/${firstComment.id}`, {
message: newMessage, method: 'PUT',
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',
@@ -144,7 +145,10 @@ const IssueDetails = () => {
const updateIssueStatus = async (newStatus: 'open' | 'resolved') => { const updateIssueStatus = async (newStatus: 'open' | 'resolved') => {
try { try {
await axios.post(`/api/v1/issue/${issueData.id}/${newStatus}`); const res = await fetch(`/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',
@@ -161,7 +165,10 @@ const IssueDetails = () => {
const deleteIssue = async () => { const deleteIssue = async () => {
try { try {
await axios.delete(`/api/v1/issue/${issueData.id}`); const res = await fetch(`/api/v1/issue/${issueData.id}`, {
method: 'DELETE',
});
if (!res.ok) throw new Error();
addToast(intl.formatMessage(messages.toastissuedeleted), { addToast(intl.formatMessage(messages.toastissuedeleted), {
appearance: 'success', appearance: 'success',
@@ -490,9 +497,14 @@ const IssueDetails = () => {
}} }}
validationSchema={CommentSchema} validationSchema={CommentSchema}
onSubmit={async (values, { resetForm }) => { onSubmit={async (values, { resetForm }) => {
await axios.post(`/api/v1/issue/${issueData?.id}/comment`, { const res = await fetch(
message: values.message, `/api/v1/issue/${issueData?.id}/comment`,
}); {
method: 'POST',
body: JSON.stringify({ message: values.message }),
}
);
if (!res.ok) throw new Error();
revalidateIssue(); revalidateIssue();
resetForm(); resetForm();
}} }}

View File

@@ -11,7 +11,6 @@ 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';
@@ -101,14 +100,19 @@ const CreateIssueModal = ({
validationSchema={CreateIssueModalSchema} validationSchema={CreateIssueModalSchema}
onSubmit={async (values) => { onSubmit={async (values) => {
try { try {
const newIssue = await axios.post<Issue>('/api/v1/issue', { const res = await fetch('/api/v1/issue', {
issueType: values.selectedIssue.issueType, method: 'POST',
message: values.message, body: JSON.stringify({
mediaId: data?.mediaInfo?.id, issueType: values.selectedIssue.issueType,
problemSeason: values.problemSeason, message: values.message,
problemEpisode: mediaId: data?.mediaInfo?.id,
values.problemSeason > 0 ? values.problemEpisode : 0, 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(
@@ -119,7 +123,7 @@ const CreateIssueModal = ({
strong: (msg: React.ReactNode) => <strong>{msg}</strong>, strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
})} })}
</div> </div>
<Link href={`/issues/${newIssue.data.id}`} legacyBehavior> <Link href={`/issues/${newIssue.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

@@ -7,7 +7,6 @@ import {
ClockIcon, ClockIcon,
} from '@heroicons/react/24/outline'; } from '@heroicons/react/24/outline';
import { CogIcon, UserIcon } from '@heroicons/react/24/solid'; import { CogIcon, UserIcon } from '@heroicons/react/24/solid';
import axios from 'axios';
import Image from 'next/image'; import Image from 'next/image';
import type { LinkProps } from 'next/link'; import type { LinkProps } from 'next/link';
import Link from 'next/link'; import Link from 'next/link';
@@ -39,9 +38,13 @@ const UserDropdown = () => {
const { user, revalidate } = useUser(); const { user, revalidate } = useUser();
const logout = async () => { const logout = async () => {
const response = await axios.post('/api/v1/auth/logout'); const res = await fetch('/api/v1/auth/logout', {
method: 'POST',
});
if (!res.ok) throw new Error();
const data = await res.json();
if (response.data?.status === 'ok') { if (data?.status === 'ok') {
revalidate(); revalidate();
} }
}; };

View File

@@ -2,7 +2,6 @@ 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';
@@ -58,11 +57,15 @@ const AddEmailModal: React.FC<AddEmailModalProps> = ({
validationSchema={EmailSettingsSchema} validationSchema={EmailSettingsSchema}
onSubmit={async (values) => { onSubmit={async (values) => {
try { try {
await axios.post('/api/v1/auth/jellyfin', { const res = await fetch('/api/v1/auth/jellyfin', {
username: username, method: 'POST',
password: password, body: JSON.stringify({
email: values.email, username: username,
password: password,
email: values.email,
}),
}); });
if (!res.ok) throw new Error();
onSave(); onSave();
} catch (e) { } catch (e) {

View File

@@ -4,7 +4,6 @@ import useSettings from '@app/hooks/useSettings';
import defineMessages from '@app/utils/defineMessages'; import defineMessages from '@app/utils/defineMessages';
import { InformationCircleIcon } from '@heroicons/react/24/solid'; import { InformationCircleIcon } from '@heroicons/react/24/solid';
import { ApiErrorCode } from '@server/constants/error'; import { ApiErrorCode } from '@server/constants/error';
import axios from 'axios';
import { Field, Form, Formik } from 'formik'; import { Field, Form, Formik } from 'formik';
import getConfig from 'next/config'; import getConfig from 'next/config';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
@@ -105,15 +104,22 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
validationSchema={LoginSchema} validationSchema={LoginSchema}
onSubmit={async (values) => { onSubmit={async (values) => {
try { try {
await axios.post('/api/v1/auth/jellyfin', { const res = await fetch('/api/v1/auth/jellyfin', {
username: values.username, method: 'POST',
password: values.password, headers: {
hostname: values.hostname, 'Content-Type': 'application/json',
port: values.port, },
useSsl: values.useSsl, body: JSON.stringify({
urlBase: values.urlBase, username: values.username,
email: values.email, password: values.password,
hostname: values.hostname,
port: values.port,
useSsl: values.useSsl,
urlBase: values.urlBase,
email: values.email,
}),
}); });
if (!res.ok) throw new Error();
} catch (e) { } catch (e) {
let errorMessage = null; let errorMessage = null;
switch (e.response?.data?.message) { switch (e.response?.data?.message) {
@@ -339,11 +345,18 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
validationSchema={LoginSchema} validationSchema={LoginSchema}
onSubmit={async (values) => { onSubmit={async (values) => {
try { try {
await axios.post('/api/v1/auth/jellyfin', { const res = await fetch('/api/v1/auth/jellyfin', {
username: values.username, method: 'POST',
password: values.password, headers: {
email: values.username, 'Content-Type': 'application/json',
},
body: JSON.stringify({
username: values.username,
password: values.password,
email: values.username,
}),
}); });
if (!res.ok) throw new Error();
} catch (e) { } catch (e) {
toasts.addToast( toasts.addToast(
intl.formatMessage( intl.formatMessage(

View File

@@ -6,7 +6,6 @@ import {
ArrowLeftOnRectangleIcon, ArrowLeftOnRectangleIcon,
LifebuoyIcon, LifebuoyIcon,
} from '@heroicons/react/24/outline'; } from '@heroicons/react/24/outline';
import axios from 'axios';
import { Field, Form, Formik } from 'formik'; import { Field, Form, Formik } from 'formik';
import Link from 'next/link'; import Link from 'next/link';
import { useState } from 'react'; import { useState } from 'react';
@@ -56,10 +55,17 @@ const LocalLogin = ({ revalidate }: LocalLoginProps) => {
validationSchema={LoginSchema} validationSchema={LoginSchema}
onSubmit={async (values) => { onSubmit={async (values) => {
try { try {
await axios.post('/api/v1/auth/local', { const res = await fetch('/api/v1/auth/local', {
email: values.email, method: 'POST',
password: values.password, headers: {
'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

@@ -10,7 +10,6 @@ import defineMessages from '@app/utils/defineMessages';
import { Transition } from '@headlessui/react'; import { Transition } from '@headlessui/react';
import { XCircleIcon } from '@heroicons/react/24/solid'; import { XCircleIcon } from '@heroicons/react/24/solid';
import { MediaServerType } from '@server/constants/server'; import { MediaServerType } from '@server/constants/server';
import axios from 'axios';
import getConfig from 'next/config'; import getConfig from 'next/config';
import { useRouter } from 'next/dist/client/router'; import { useRouter } from 'next/dist/client/router';
import Image from 'next/image'; import Image from 'next/image';
@@ -44,9 +43,14 @@ const Login = () => {
const login = async () => { const login = async () => {
setProcessing(true); setProcessing(true);
try { try {
const response = await axios.post('/api/v1/auth/plex', { authToken }); const res = await fetch('/api/v1/auth/plex', {
method: 'POST',
body: JSON.stringify({ authToken }),
});
if (!res.ok) throw new Error();
const data = await res.json();
if (response.data?.id) { if (data?.id) {
revalidate(); revalidate();
} }
} catch (e) { } catch (e) {

View File

@@ -26,7 +26,6 @@ import type { MediaWatchDataResponse } from '@server/interfaces/api/mediaInterfa
import type { RadarrSettings, SonarrSettings } from '@server/lib/settings'; import type { RadarrSettings, SonarrSettings } from '@server/lib/settings';
import type { MovieDetails } from '@server/models/Movie'; import type { MovieDetails } from '@server/models/Movie';
import type { TvDetails } from '@server/models/Tv'; import type { TvDetails } from '@server/models/Tv';
import axios from 'axios';
import getConfig from 'next/config'; import getConfig from 'next/config';
import Image from 'next/image'; import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
@@ -113,15 +112,26 @@ const ManageSlideOver = ({
const deleteMedia = async () => { const deleteMedia = async () => {
if (data.mediaInfo) { if (data.mediaInfo) {
await axios.delete(`/api/v1/media/${data.mediaInfo.id}`); const res = await fetch(`/api/v1/media/${data.mediaInfo.id}`, {
method: 'DELETE',
});
if (!res.ok) throw new Error();
revalidate(); revalidate();
} }
}; };
const deleteMediaFile = async () => { const deleteMediaFile = async () => {
if (data.mediaInfo) { if (data.mediaInfo) {
await axios.delete(`/api/v1/media/${data.mediaInfo.id}/file`); const res1 = await fetch(`/api/v1/media/${data.mediaInfo.id}/file`, {
await axios.delete(`/api/v1/media/${data.mediaInfo.id}`); method: 'DELETE',
});
if (!res1.ok) throw new Error();
const res2 = await fetch(`/api/v1/media/${data.mediaInfo.id}`, {
method: 'DELETE',
});
if (!res2.ok) throw new Error();
revalidate(); revalidate();
} }
}; };
@@ -149,9 +159,16 @@ const ManageSlideOver = ({
const markAvailable = async (is4k = false) => { const markAvailable = async (is4k = false) => {
if (data.mediaInfo) { if (data.mediaInfo) {
await axios.post(`/api/v1/media/${data.mediaInfo?.id}/available`, { const res = await fetch(`/api/v1/media/${data.mediaInfo?.id}/available`, {
is4k, method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
is4k,
}),
}); });
if (!res.ok) throw new Error();
revalidate(); revalidate();
} }
}; };

View File

@@ -17,7 +17,6 @@ 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,7 +52,10 @@ 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);
await axios.post(`/api/v1/request/${request.id}/${type}`); const res = await fetch(`/api/v1/request/${request.id}/${type}`, {
method: 'POST',
});
if (!res.ok) throw new Error();
if (onUpdate) { if (onUpdate) {
onUpdate(); onUpdate();
@@ -63,7 +65,10 @@ const RequestBlock = ({ request, onUpdate }: RequestBlockProps) => {
const deleteRequest = async () => { const deleteRequest = async () => {
setIsUpdating(true); setIsUpdating(true);
await axios.delete(`/api/v1/request/${request.id}`); const res = await fetch(`/api/v1/request/${request.id}`, {
method: 'DELETE',
});
if (!res.ok) throw new Error();
if (onUpdate) { if (onUpdate) {
onUpdate(); onUpdate();

View File

@@ -13,7 +13,6 @@ 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';
@@ -94,9 +93,13 @@ const RequestButton = ({
request: MediaRequest, request: MediaRequest,
type: 'approve' | 'decline' type: 'approve' | 'decline'
) => { ) => {
const response = await axios.post(`/api/v1/request/${request.id}/${type}`); const res = await fetch(`/api/v1/request/${request.id}/${type}`, {
method: 'POST',
});
if (!res.ok) throw new Error();
const data = await res.json();
if (response) { if (data) {
onUpdate(); onUpdate();
} }
}; };
@@ -111,7 +114,11 @@ const RequestButton = ({
await Promise.all( await Promise.all(
requests.map(async (request) => { requests.map(async (request) => {
return axios.post(`/api/v1/request/${request.id}/${type}`); const res = await fetch(`/api/v1/request/${request.id}/${type}`, {
method: 'POST',
});
if (!res.ok) throw new Error();
return res.json();
}) })
); );

View File

@@ -21,7 +21,6 @@ import { MediaRequestStatus } from '@server/constants/media';
import type { MediaRequest } from '@server/entity/MediaRequest'; import type { MediaRequest } from '@server/entity/MediaRequest';
import type { MovieDetails } from '@server/models/Movie'; import type { MovieDetails } from '@server/models/Movie';
import type { TvDetails } from '@server/models/Tv'; import type { TvDetails } from '@server/models/Tv';
import axios from 'axios';
import Image from 'next/image'; import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -74,7 +73,10 @@ const RequestCardError = ({ requestData }: RequestCardErrorProps) => {
}); });
const deleteRequest = async () => { const deleteRequest = async () => {
await axios.delete(`/api/v1/media/${requestData?.media.id}`); const res = await fetch(`/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');
}; };
@@ -255,15 +257,22 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
}); });
const modifyRequest = async (type: 'approve' | 'decline') => { const modifyRequest = async (type: 'approve' | 'decline') => {
const response = await axios.post(`/api/v1/request/${request.id}/${type}`); const res = await fetch(`/api/v1/request/${request.id}/${type}`, {
method: 'POST',
});
if (!res.ok) throw new Error();
const data = await res.json();
if (response) { if (data) {
revalidate(); revalidate();
} }
}; };
const deleteRequest = async () => { const deleteRequest = async () => {
await axios.delete(`/api/v1/request/${request.id}`); const res = await fetch(`/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');
}; };
@@ -271,9 +280,13 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
setRetrying(true); setRetrying(true);
try { try {
const response = await axios.post(`/api/v1/request/${request.id}/retry`); const res = await fetch(`/api/v1/request/${request.id}/retry`, {
method: 'POST',
});
if (!res.ok) throw new Error();
const data = await res.json();
if (response) { if (data) {
revalidate(); revalidate();
} }
} catch (e) { } catch (e) {

View File

@@ -20,7 +20,6 @@ import { MediaRequestStatus } from '@server/constants/media';
import type { MediaRequest } from '@server/entity/MediaRequest'; import type { MediaRequest } from '@server/entity/MediaRequest';
import type { MovieDetails } from '@server/models/Movie'; import type { MovieDetails } from '@server/models/Movie';
import type { TvDetails } from '@server/models/Tv'; import type { TvDetails } from '@server/models/Tv';
import axios from 'axios';
import Image from 'next/image'; import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import { useState } from 'react'; import { useState } from 'react';
@@ -62,7 +61,10 @@ const RequestItemError = ({
const { hasPermission } = useUser(); const { hasPermission } = useUser();
const deleteRequest = async () => { const deleteRequest = async () => {
await axios.delete(`/api/v1/media/${requestData?.media.id}`); const res = await fetch(`/api/v1/media/${requestData?.media.id}`, {
method: 'DELETE',
});
if (!res.ok) throw new Error();
revalidateList(); revalidateList();
}; };
@@ -319,15 +321,22 @@ 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 response = await axios.post(`/api/v1/request/${request.id}/${type}`); const res = await fetch(`/api/v1/request/${request.id}/${type}`, {
method: 'POST',
});
if (!res.ok) throw new Error();
const data = await res.json();
if (response) { if (data) {
revalidate(); revalidate();
} }
}; };
const deleteRequest = async () => { const deleteRequest = async () => {
await axios.delete(`/api/v1/request/${request.id}`); const res = await fetch(`/api/v1/request/${request.id}`, {
method: 'DELETE',
});
if (!res.ok) throw new Error();
revalidateList(); revalidateList();
}; };
@@ -336,7 +345,12 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
setRetrying(true); setRetrying(true);
try { try {
const result = await axios.post(`/api/v1/request/${request.id}/retry`); const res = await fetch(`/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,7 +13,6 @@ 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';
@@ -197,12 +196,19 @@ const CollectionRequestModal = ({
( (
data?.parts.filter((part) => selectedParts.includes(part.id)) ?? [] data?.parts.filter((part) => selectedParts.includes(part.id)) ?? []
).map(async (part) => { ).map(async (part) => {
await axios.post<MediaRequest>('/api/v1/request', { const res = await fetch('/api/v1/request', {
mediaId: part.id, method: 'POST',
mediaType: 'movie', headers: {
is4k, 'Content-Type': 'application/json',
...overrideParams, },
body: JSON.stringify({
mediaId: part.id,
mediaType: 'movie',
is4k,
...overrideParams,
}),
}); });
if (!res.ok) throw new Error();
}) })
); );

View File

@@ -11,7 +11,6 @@ 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 { 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,15 +88,23 @@ const MovieRequestModal = ({
tags: requestOverrides.tags, tags: requestOverrides.tags,
}; };
} }
const response = await axios.post<MediaRequest>('/api/v1/request', { const res = await fetch('/api/v1/request', {
mediaId: data?.id, method: 'POST',
mediaType: 'movie', headers: {
is4k, 'Content-Type': 'application/json',
...overrideParams, },
body: JSON.stringify({
mediaId: data?.id,
mediaType: 'movie',
is4k,
...overrideParams,
}),
}); });
if (!res.ok) throw new Error();
const mediaRequest: MediaRequest = await res.json();
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0'); mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
if (response.data) { if (mediaRequest) {
if (onComplete) { if (onComplete) {
onComplete( onComplete(
hasPermission( hasPermission(
@@ -136,12 +143,14 @@ const MovieRequestModal = ({
setIsUpdating(true); setIsUpdating(true);
try { try {
const response = await axios.delete<MediaRequest>( const res = await fetch(`/api/v1/request/${editRequest?.id}`, {
`/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');
if (response.status === 204) { if (res.status === 204) {
if (onComplete) { if (onComplete) {
onComplete(MediaStatus.UNKNOWN); onComplete(MediaStatus.UNKNOWN);
} }
@@ -164,17 +173,27 @@ const MovieRequestModal = ({
setIsUpdating(true); setIsUpdating(true);
try { try {
await axios.put(`/api/v1/request/${editRequest?.id}`, { const res = await fetch(`/api/v1/request/${editRequest?.id}`, {
mediaType: 'movie', method: 'PUT',
serverId: requestOverrides?.server, headers: {
profileId: requestOverrides?.profile, 'Content-Type': 'application/json',
rootFolder: requestOverrides?.folder, },
userId: requestOverrides?.user?.id, body: JSON.stringify({
tags: requestOverrides?.tags, mediaType: 'movie',
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) {
await axios.post(`/api/v1/request/${editRequest?.id}/approve`); const res = await fetch(`/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');

View File

@@ -16,7 +16,6 @@ import type SeasonRequest from '@server/entity/SeasonRequest';
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,22 +110,35 @@ const TvRequestModal = ({
try { try {
if (selectedSeasons.length > 0) { if (selectedSeasons.length > 0) {
await axios.put(`/api/v1/request/${editRequest.id}`, { const res = await fetch(`/api/v1/request/${editRequest.id}`, {
mediaType: 'tv', method: 'PUT',
serverId: requestOverrides?.server, headers: {
profileId: requestOverrides?.profile, 'Content-Type': 'application/json',
rootFolder: requestOverrides?.folder, },
languageProfileId: requestOverrides?.language, body: JSON.stringify({
userId: requestOverrides?.user?.id, mediaType: 'tv',
tags: requestOverrides?.tags, serverId: requestOverrides?.server,
seasons: selectedSeasons, profileId: requestOverrides?.profile,
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) {
await axios.post(`/api/v1/request/${editRequest.id}/approve`); const res = await fetch(`/api/v1/request/${editRequest.id}/approve`, {
method: 'POST',
});
if (!res.ok) throw new Error();
} }
} else { } else {
await axios.delete(`/api/v1/request/${editRequest.id}`); const res = await fetch(`/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');
@@ -191,23 +203,32 @@ const TvRequestModal = ({
tags: requestOverrides.tags, tags: requestOverrides.tags,
}; };
} }
const response = await axios.post<MediaRequest>('/api/v1/request', { const res = await fetch('/api/v1/request', {
mediaId: data?.id, method: 'POST',
tvdbId: tvdbId ?? data?.externalIds.tvdbId, headers: {
mediaType: 'tv', 'Content-Type': 'application/json',
is4k, },
seasons: settings.currentSettings.partialRequestsEnabled body: JSON.stringify({
? selectedSeasons mediaId: data?.id,
: getAllSeasons().filter( tvdbId: tvdbId ?? data?.externalIds.tvdbId,
(season) => !getAllRequestedSeasons().includes(season) mediaType: 'tv',
), is4k,
...overrideParams, seasons: settings.currentSettings.partialRequestsEnabled
? selectedSeasons
: getAllSeasons().filter(
(season) => !getAllRequestedSeasons().includes(season)
),
...overrideParams,
}),
}); });
if (!res.ok) throw new Error();
const mediaRequest: MediaRequest = await res.json();
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0'); mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
if (response.data) { if (mediaRequest) {
if (onComplete) { if (onComplete) {
onComplete(response.data.media.status); onComplete(mediaRequest.media.status);
} }
addToast( addToast(
<span> <span>

View File

@@ -4,7 +4,6 @@ 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';
@@ -85,14 +84,18 @@ const ResetPassword = () => {
}} }}
validationSchema={ResetSchema} validationSchema={ResetSchema}
onSubmit={async (values) => { onSubmit={async (values) => {
const response = await axios.post( const res = await fetch(`/api/v1/auth/reset-password`, {
`/api/v1/auth/reset-password`, method: 'POST',
{ headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: values.email, email: values.email,
} }),
); });
if (!res.ok) throw new Error();
if (response.status === 200) { if (res.status === 200) {
setSubmitted(true); setSubmitted(true);
} }
}} }}

View File

@@ -5,7 +5,6 @@ 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';
@@ -100,14 +99,21 @@ const ResetPassword = () => {
}} }}
validationSchema={ResetSchema} validationSchema={ResetSchema}
onSubmit={async (values) => { onSubmit={async (values) => {
const response = await axios.post( const res = await fetch(
`/api/v1/auth/reset-password/${guid}`, `/api/v1/auth/reset-password/${guid}`,
{ {
password: values.password, method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
password: values.password,
}),
} }
); );
if (!res.ok) throw new Error();
if (response.status === 200) { if (res.status === 200) {
setSubmitted(true); setSubmitted(true);
} }
}} }}

View File

@@ -18,7 +18,6 @@ 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';
@@ -69,11 +68,9 @@ export const CompanySelector = ({
return; return;
} }
const response = await axios.get<ProductionCompany>( const res = await fetch(`/api/v1/studio/${defaultValue}`);
`/api/v1/studio/${defaultValue}` if (!res.ok) throw new Error();
); const studio: ProductionCompany = await res.json();
const studio = response.data;
setDefaultDataValue([ setDefaultDataValue([
{ {
@@ -91,16 +88,15 @@ export const CompanySelector = ({
return []; return [];
} }
const results = await axios.get<TmdbCompanySearchResponse>( const res = await fetch(
'/api/v1/search/company', `/api/v1/search/company?query=${encodeURIExtraParams(inputValue)}`
{
params: {
query: encodeURIExtraParams(inputValue),
},
}
); );
if (!res.ok) {
throw new Error('Network response was not ok');
}
const results: TmdbCompanySearchResponse = await res.json();
return results.data.results.map((result) => ({ return results.results.map((result) => ({
label: result.name, label: result.name,
value: result.id, value: result.id,
})); }));
@@ -154,11 +150,15 @@ export const GenreSelector = ({
const genres = defaultValue.split(','); const genres = defaultValue.split(',');
const response = await axios.get<TmdbGenre[]>(`/api/v1/genres/${type}`); const res = await fetch(`/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.data.find((gd) => gd.id === Number(genre))) .filter((genre) => response.find((gd) => gd.id === Number(genre)))
.map((g) => response.data.find((gd) => gd.id === Number(g))) .map((g) => response.find((gd) => gd.id === Number(g)))
.map((g) => ({ .map((g) => ({
label: g?.name ?? '', label: g?.name ?? '',
value: g?.id ?? 0, value: g?.id ?? 0,
@@ -171,11 +171,11 @@ export const GenreSelector = ({
}, [defaultValue, type]); }, [defaultValue, type]);
const loadGenreOptions = async (inputValue: string) => { const loadGenreOptions = async (inputValue: string) => {
const results = await axios.get<GenreSliderItem[]>( const res = await fetch(`/api/v1/discover/genreslider/${type}`);
`/api/v1/discover/genreslider/${type}` if (!res.ok) throw new Error();
); const results: GenreSliderItem[] = await res.json();
return results.data return results
.map((result) => ({ .map((result) => ({
label: result.name, label: result.name,
value: result.id, value: result.id,
@@ -222,11 +222,13 @@ export const KeywordSelector = ({
const keywords = await Promise.all( const keywords = await Promise.all(
defaultValue.split(',').map(async (keywordId) => { defaultValue.split(',').map(async (keywordId) => {
const keyword = await axios.get<Keyword>( const res = await fetch(`/api/v1/keyword/${keywordId}`);
`/api/v1/keyword/${keywordId}` if (!res.ok) {
); throw new Error('Network response was not ok');
}
const keyword: Keyword = await res.json();
return keyword.data; return keyword;
}) })
); );
@@ -242,16 +244,15 @@ export const KeywordSelector = ({
}, [defaultValue]); }, [defaultValue]);
const loadKeywordOptions = async (inputValue: string) => { const loadKeywordOptions = async (inputValue: string) => {
const results = await axios.get<TmdbKeywordSearchResponse>( const res = await fetch(
'/api/v1/search/keyword', `/api/v1/search/keyword?query=${encodeURIExtraParams(inputValue)}`
{
params: {
query: encodeURIExtraParams(inputValue),
},
}
); );
if (!res.ok) {
throw new Error('Network response was not ok');
}
const results: TmdbKeywordSearchResponse = await res.json();
return results.data.results.map((result) => ({ return results.results.map((result) => ({
label: result.name, label: result.name,
value: result.id, value: result.id,
})); }));

View File

@@ -1,7 +1,6 @@
/* eslint-disable no-console */ /* eslint-disable no-console */
import useSettings from '@app/hooks/useSettings'; import useSettings from '@app/hooks/useSettings';
import { useUser } from '@app/hooks/useUser'; import { useUser } from '@app/hooks/useUser';
import axios from 'axios';
import { useEffect } from 'react'; import { useEffect } from 'react';
const ServiceWorkerSetup = () => { const ServiceWorkerSetup = () => {
@@ -26,11 +25,18 @@ const ServiceWorkerSetup = () => {
const parsedSub = JSON.parse(JSON.stringify(sub)); const parsedSub = JSON.parse(JSON.stringify(sub));
if (parsedSub.keys.p256dh && parsedSub.keys.auth) { if (parsedSub.keys.p256dh && parsedSub.keys.auth) {
await axios.post('/api/v1/user/registerPushSubscription', { const res = await fetch('/api/v1/user/registerPushSubscription', {
endpoint: parsedSub.endpoint, method: 'POST',
p256dh: parsedSub.keys.p256dh, headers: {
auth: parsedSub.keys.auth, 'Content-Type': 'application/json',
},
body: JSON.stringify({
endpoint: parsedSub.endpoint,
p256dh: parsedSub.keys.p256dh,
auth: parsedSub.keys.auth,
}),
}); });
if (!res.ok) throw new Error();
} }
} }
}) })

View File

@@ -5,7 +5,6 @@ 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';
@@ -73,16 +72,23 @@ const NotificationsDiscord = () => {
validationSchema={NotificationsDiscordSchema} validationSchema={NotificationsDiscordSchema}
onSubmit={async (values) => { onSubmit={async (values) => {
try { try {
await axios.post('/api/v1/settings/notifications/discord', { const res = await fetch('/api/v1/settings/notifications/discord', {
enabled: values.enabled, method: 'POST',
types: values.types, headers: {
options: { 'Content-Type': 'application/json',
botUsername: values.botUsername,
botAvatarUrl: values.botAvatarUrl,
webhookUrl: values.webhookUrl,
enableMentions: values.enableMentions,
}, },
body: JSON.stringify({
enabled: values.enabled,
types: values.types,
options: {
botUsername: values.botUsername,
botAvatarUrl: values.botAvatarUrl,
webhookUrl: values.webhookUrl,
enableMentions: values.enableMentions,
},
}),
}); });
if (!res.ok) throw new Error();
addToast(intl.formatMessage(messages.discordsettingssaved), { addToast(intl.formatMessage(messages.discordsettingssaved), {
appearance: 'success', appearance: 'success',
@@ -121,16 +127,26 @@ const NotificationsDiscord = () => {
toastId = id; toastId = id;
} }
); );
await axios.post('/api/v1/settings/notifications/discord/test', { const res = await fetch(
enabled: true, '/api/v1/settings/notifications/discord/test',
types: values.types, {
options: { method: 'POST',
botUsername: values.botUsername, headers: {
botAvatarUrl: values.botAvatarUrl, 'Content-Type': 'application/json',
webhookUrl: values.webhookUrl, },
enableMentions: values.enableMentions, body: JSON.stringify({
}, enabled: true,
}); types: values.types,
options: {
botUsername: values.botUsername,
botAvatarUrl: values.botAvatarUrl,
webhookUrl: values.webhookUrl,
enableMentions: values.enableMentions,
},
}),
}
);
if (!res.ok) throw new Error();
if (toastId) { if (toastId) {
removeToast(toastId); removeToast(toastId);

View File

@@ -5,7 +5,6 @@ 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';
@@ -148,24 +147,31 @@ const NotificationsEmail = () => {
validationSchema={NotificationsEmailSchema} validationSchema={NotificationsEmailSchema}
onSubmit={async (values) => { onSubmit={async (values) => {
try { try {
await axios.post('/api/v1/settings/notifications/email', { const res = await fetch('/api/v1/settings/notifications/email', {
enabled: values.enabled, method: 'POST',
options: { headers: {
userEmailRequired: values.userEmailRequired, 'Content-Type': 'application/json',
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), {
@@ -197,22 +203,32 @@ const NotificationsEmail = () => {
toastId = id; toastId = id;
} }
); );
await axios.post('/api/v1/settings/notifications/email/test', { const res = await fetch(
enabled: true, '/api/v1/settings/notifications/email/test',
options: { {
emailFrom: values.emailFrom, method: 'POST',
smtpHost: values.smtpHost, headers: {
smtpPort: Number(values.smtpPort), 'Content-Type': 'application/json',
secure: values.encryption === 'implicit', },
ignoreTls: values.encryption === 'none', body: JSON.stringify({
requireTls: values.encryption === 'opportunistic', enabled: true,
authUser: values.authUser, options: {
authPass: values.authPass, emailFrom: values.emailFrom,
senderName: values.senderName, smtpHost: values.smtpHost,
pgpPrivateKey: values.pgpPrivateKey, smtpPort: Number(values.smtpPort),
pgpPassword: values.pgpPassword, secure: values.encryption === 'implicit',
}, ignoreTls: values.encryption === 'none',
}); requireTls: values.encryption === 'opportunistic',
authUser: values.authUser,
authPass: values.authPass,
senderName: values.senderName,
pgpPrivateKey: values.pgpPrivateKey,
pgpPassword: values.pgpPassword,
},
}),
}
);
if (!res.ok) throw new Error();
if (toastId) { if (toastId) {
removeToast(toastId); removeToast(toastId);

View File

@@ -4,7 +4,6 @@ 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';
@@ -83,14 +82,21 @@ const NotificationsGotify = () => {
validationSchema={NotificationsGotifySchema} validationSchema={NotificationsGotifySchema}
onSubmit={async (values) => { onSubmit={async (values) => {
try { try {
await axios.post('/api/v1/settings/notifications/gotify', { const res = await fetch('/api/v1/settings/notifications/gotify', {
enabled: values.enabled, method: 'POST',
types: values.types, headers: {
options: { 'Content-Type': 'application/json',
url: values.url,
token: values.token,
}, },
body: JSON.stringify({
enabled: values.enabled,
types: values.types,
options: {
url: values.url,
token: values.token,
},
}),
}); });
if (!res.ok) throw new Error();
addToast(intl.formatMessage(messages.gotifysettingssaved), { addToast(intl.formatMessage(messages.gotifysettingssaved), {
appearance: 'success', appearance: 'success',
autoDismiss: true, autoDismiss: true,
@@ -128,14 +134,24 @@ const NotificationsGotify = () => {
toastId = id; toastId = id;
} }
); );
await axios.post('/api/v1/settings/notifications/gotify/test', { const res = await fetch(
enabled: true, '/api/v1/settings/notifications/gotify/test',
types: values.types, {
options: { method: 'POST',
url: values.url, headers: {
token: values.token, 'Content-Type': 'application/json',
}, },
}); body: JSON.stringify({
enabled: true,
types: values.types,
options: {
url: values.url,
token: values.token,
},
}),
}
);
if (!res.ok) throw new Error();
if (toastId) { if (toastId) {
removeToast(toastId); removeToast(toastId);

View File

@@ -4,7 +4,6 @@ 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';
@@ -69,14 +68,21 @@ const NotificationsLunaSea = () => {
validationSchema={NotificationsLunaSeaSchema} validationSchema={NotificationsLunaSeaSchema}
onSubmit={async (values) => { onSubmit={async (values) => {
try { try {
await axios.post('/api/v1/settings/notifications/lunasea', { const res = await fetch('/api/v1/settings/notifications/lunasea', {
enabled: values.enabled, method: 'POST',
types: values.types, headers: {
options: { 'Content-Type': 'application/json',
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,
@@ -114,14 +120,24 @@ const NotificationsLunaSea = () => {
toastId = id; toastId = id;
} }
); );
await axios.post('/api/v1/settings/notifications/lunasea/test', { const res = await fetch(
enabled: true, '/api/v1/settings/notifications/lunasea/test',
types: values.types, {
options: { method: 'POST',
webhookUrl: values.webhookUrl, headers: {
profileName: values.profileName, 'Content-Type': 'application/json',
}, },
}); 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,7 +5,6 @@ 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,14 +67,21 @@ const NotificationsPushbullet = () => {
validationSchema={NotificationsPushbulletSchema} validationSchema={NotificationsPushbulletSchema}
onSubmit={async (values) => { onSubmit={async (values) => {
try { try {
await axios.post('/api/v1/settings/notifications/pushbullet', { const res = await fetch('/api/v1/settings/notifications/pushbullet', {
enabled: values.enabled, method: 'POST',
types: values.types, headers: {
options: { 'Content-Type': 'application/json',
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,
@@ -113,14 +119,24 @@ const NotificationsPushbullet = () => {
toastId = id; toastId = id;
} }
); );
await axios.post('/api/v1/settings/notifications/pushbullet/test', { const res = await fetch(
enabled: true, '/api/v1/settings/notifications/pushbullet/test',
types: values.types, {
options: { method: 'POST',
accessToken: values.accessToken, headers: {
channelTag: values.channelTag, 'Content-Type': 'application/json',
}, },
}); 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,7 +5,6 @@ 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';
@@ -94,14 +93,21 @@ const NotificationsPushover = () => {
validationSchema={NotificationsPushoverSchema} validationSchema={NotificationsPushoverSchema}
onSubmit={async (values) => { onSubmit={async (values) => {
try { try {
await axios.post('/api/v1/settings/notifications/pushover', { const res = await fetch('/api/v1/settings/notifications/pushover', {
enabled: values.enabled, method: 'POST',
types: values.types, headers: {
options: { 'Content-Type': 'application/json',
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,
@@ -139,16 +145,25 @@ const NotificationsPushover = () => {
toastId = id; toastId = id;
} }
); );
await axios.post('/api/v1/settings/notifications/pushover/test', { const res = await fetch(
enabled: true, '/api/v1/settings/notifications/pushover/test',
types: values.types, {
options: { method: 'POST',
accessToken: values.accessToken, headers: {
userToken: values.userToken, 'Content-Type': 'application/json',
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,7 +4,6 @@ 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';
@@ -65,13 +64,20 @@ const NotificationsSlack = () => {
validationSchema={NotificationsSlackSchema} validationSchema={NotificationsSlackSchema}
onSubmit={async (values) => { onSubmit={async (values) => {
try { try {
await axios.post('/api/v1/settings/notifications/slack', { const res = await fetch('/api/v1/settings/notifications/slack', {
enabled: values.enabled, method: 'POST',
types: values.types, headers: {
options: { 'Content-Type': 'application/json',
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,
@@ -109,13 +115,23 @@ const NotificationsSlack = () => {
toastId = id; toastId = id;
} }
); );
await axios.post('/api/v1/settings/notifications/slack/test', { const res = await fetch(
enabled: true, '/api/v1/settings/notifications/slack/test',
types: values.types, {
options: { method: 'POST',
webhookUrl: values.webhookUrl, headers: {
}, '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,7 +5,6 @@ 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';
@@ -84,16 +83,23 @@ const NotificationsTelegram = () => {
validationSchema={NotificationsTelegramSchema} validationSchema={NotificationsTelegramSchema}
onSubmit={async (values) => { onSubmit={async (values) => {
try { try {
await axios.post('/api/v1/settings/notifications/telegram', { const res = await fetch('/api/v1/settings/notifications/telegram', {
enabled: values.enabled, method: 'POST',
types: values.types, headers: {
options: { 'Content-Type': 'application/json',
botAPI: values.botAPI,
chatId: values.chatId,
sendSilently: values.sendSilently,
botUsername: values.botUsername,
}, },
body: JSON.stringify({
enabled: values.enabled,
types: values.types,
options: {
botAPI: values.botAPI,
chatId: values.chatId,
sendSilently: values.sendSilently,
botUsername: values.botUsername,
},
}),
}); });
if (!res.ok) throw new Error();
addToast(intl.formatMessage(messages.telegramsettingssaved), { addToast(intl.formatMessage(messages.telegramsettingssaved), {
appearance: 'success', appearance: 'success',
@@ -132,16 +138,26 @@ const NotificationsTelegram = () => {
toastId = id; toastId = id;
} }
); );
await axios.post('/api/v1/settings/notifications/telegram/test', { const res = await fetch(
enabled: true, '/api/v1/settings/notifications/telegram/test',
types: values.types, {
options: { method: 'POST',
botAPI: values.botAPI, headers: {
chatId: values.chatId, 'Content-Type': 'application/json',
sendSilently: values.sendSilently, },
botUsername: values.botUsername, body: JSON.stringify({
}, enabled: true,
}); types: values.types,
options: {
botAPI: values.botAPI,
chatId: values.chatId,
sendSilently: values.sendSilently,
botUsername: values.botUsername,
},
}),
}
);
if (!res.ok) throw new Error();
if (toastId) { if (toastId) {
removeToast(toastId); removeToast(toastId);

View File

@@ -4,7 +4,6 @@ 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';
@@ -58,10 +57,17 @@ const NotificationsWebPush = () => {
}} }}
onSubmit={async (values) => { onSubmit={async (values) => {
try { try {
await axios.post('/api/v1/settings/notifications/webpush', { const res = await fetch('/api/v1/settings/notifications/webpush', {
enabled: values.enabled, method: 'POST',
options: {}, headers: {
'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',
@@ -92,10 +98,20 @@ const NotificationsWebPush = () => {
toastId = id; toastId = id;
} }
); );
await axios.post('/api/v1/settings/notifications/webpush/test', { const res = await fetch(
enabled: true, '/api/v1/settings/notifications/webpush/test',
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,7 +8,6 @@ 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';
@@ -150,15 +149,22 @@ const NotificationsWebhook = () => {
validationSchema={NotificationsWebhookSchema} validationSchema={NotificationsWebhookSchema}
onSubmit={async (values) => { onSubmit={async (values) => {
try { try {
await axios.post('/api/v1/settings/notifications/webhook', { const res = await fetch('/api/v1/settings/notifications/webhook', {
enabled: values.enabled, method: 'POST',
types: values.types, headers: {
options: { 'Content-Type': 'application/json',
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,
@@ -207,16 +213,25 @@ const NotificationsWebhook = () => {
toastId = id; toastId = id;
} }
); );
await axios.post('/api/v1/settings/notifications/webhook/test', { const res = await fetch(
enabled: true, '/api/v1/settings/notifications/webhook/test',
types: values.types, {
options: { method: 'POST',
webhookUrl: values.webhookUrl, headers: {
jsonPayload: JSON.stringify(values.jsonPayload), 'Content-Type': 'application/json',
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

@@ -4,7 +4,6 @@ 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';
@@ -166,19 +165,24 @@ const RadarrModal = ({ onClose, radarr, onSave }: RadarrModalProps) => {
}) => { }) => {
setIsTesting(true); setIsTesting(true);
try { try {
const response = await axios.post<TestResponse>( const res = await fetch('/api/v1/settings/radarr/test', {
'/api/v1/settings/radarr/test', method: 'POST',
{ 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(response.data); setTestResponse(data);
if (initialLoad.current) { if (initialLoad.current) {
addToast(intl.formatMessage(messages.toastRadarrTestSuccess), { addToast(intl.formatMessage(messages.toastRadarrTestSuccess), {
appearance: 'success', appearance: 'success',
@@ -271,12 +275,23 @@ const RadarrModal = ({ onClose, radarr, onSave }: RadarrModalProps) => {
tagRequests: values.tagRequests, tagRequests: values.tagRequests,
}; };
if (!radarr) { if (!radarr) {
await axios.post('/api/v1/settings/radarr', submission); const res = await fetch('/api/v1/settings/radarr', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(submission),
});
if (!res.ok) throw new Error();
} else { } else {
await axios.put( const res = await fetch(`/api/v1/settings/radarr/${radarr.id}`, {
`/api/v1/settings/radarr/${radarr.id}`, method: 'PUT',
submission headers: {
); 'Content-Type': 'application/json',
},
body: JSON.stringify(submission),
});
if (!res.ok) throw new Error();
} }
onSave(); onSave();

View File

@@ -7,7 +7,6 @@ import defineMessages from '@app/utils/defineMessages';
import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline'; import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline';
import { ApiErrorCode } from '@server/constants/error'; import { ApiErrorCode } from '@server/constants/error';
import type { JellyfinSettings } from '@server/lib/settings'; import type { JellyfinSettings } from '@server/lib/settings';
import axios from 'axios';
import { Field, Formik } from 'formik'; import { Field, Formik } from 'formik';
import getConfig from 'next/config'; import getConfig from 'next/config';
import { useState } from 'react'; import { useState } from 'react';
@@ -167,9 +166,14 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
} }
try { try {
await axios.get('/api/v1/settings/jellyfin/library', { const searchParams = new URLSearchParams({
params, sync: params.sync ? 'true' : 'false',
...(params.enable ? { enable: params.enable } : {}),
}); });
const res = await fetch(
`/api/v1/settings/jellyfin/library?${searchParams.toString()}`
);
if (!res.ok) throw new Error();
setIsSyncing(false); setIsSyncing(false);
revalidate(); revalidate();
} catch (e) { } catch (e) {
@@ -206,16 +210,32 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
}; };
const startScan = async () => { const startScan = async () => {
await axios.post('/api/v1/settings/jellyfin/sync', { const res = await fetch('/api/v1/settings/jellyfin/sync', {
start: true, method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
start: true,
}),
}); });
if (!res.ok) throw new Error();
revalidateSync(); revalidateSync();
}; };
const cancelScan = async () => { const cancelScan = async () => {
await axios.post('/api/v1/settings/jellyfin/sync', { const res = await fetch('/api/v1/settings/jellyfin/sync', {
cancel: true, method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
cancel: true,
}),
}); });
if (!res.ok) throw new Error();
revalidateSync(); revalidateSync();
}; };
@@ -230,15 +250,19 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
.join(','); .join(',');
} }
await axios.get('/api/v1/settings/jellyfin/library', { const searchParams = new URLSearchParams(params.enable ? params : {});
params, const res = await fetch(
}); `/api/v1/settings/jellyfin/library?${searchParams.toString}`
);
if (!res.ok) throw new Error();
} else { } else {
await axios.get('/api/v1/settings/jellyfin/library', { const searchParams = new URLSearchParams({
params: { 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();
@@ -447,14 +471,21 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
validationSchema={JellyfinSettingsSchema} validationSchema={JellyfinSettingsSchema}
onSubmit={async (values) => { onSubmit={async (values) => {
try { try {
await axios.post('/api/v1/settings/jellyfin', { const res = await fetch('/api/v1/settings/jellyfin', {
ip: values.hostname, method: 'POST',
port: Number(values.port), headers: {
useSsl: values.useSsl, 'Content-Type': 'application/json',
urlBase: values.urlBase, },
externalHostname: values.jellyfinExternalUrl, body: JSON.stringify({
jellyfinForgotPasswordUrl: values.jellyfinForgotPasswordUrl, ip: values.hostname,
} as JellyfinSettings); port: Number(values.port),
useSsl: values.useSsl,
urlBase: values.urlBase,
externalHostname: values.jellyfinExternalUrl,
jellyfinForgotPasswordUrl: values.jellyfinForgotPasswordUrl,
} as JellyfinSettings),
});
if (!res.ok) throw new Error();
addToast( addToast(
intl.formatMessage(messages.jellyfinSettingsSuccess, { intl.formatMessage(messages.jellyfinSettingsSuccess, {

View File

@@ -19,7 +19,6 @@ 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';
@@ -173,7 +172,10 @@ const SettingsJobs = () => {
} }
const runJob = async (job: Job) => { const runJob = async (job: Job) => {
await axios.post(`/api/v1/settings/jobs/${job.id}/run`); const res = await fetch(`/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),
@@ -187,7 +189,10 @@ const SettingsJobs = () => {
}; };
const cancelJob = async (job: Job) => { const cancelJob = async (job: Job) => {
await axios.post(`/api/v1/settings/jobs/${job.id}/cancel`); const res = await fetch(`/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),
@@ -201,7 +206,10 @@ const SettingsJobs = () => {
}; };
const flushCache = async (cache: CacheItem) => { const flushCache = async (cache: CacheItem) => {
await axios.post(`/api/v1/settings/cache/${cache.id}/flush`); const res = await fetch(`/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 }),
{ {
@@ -228,12 +236,19 @@ const SettingsJobs = () => {
} }
setIsSaving(true); setIsSaving(true);
await axios.post( const res = await fetch(
`/api/v1/settings/jobs/${jobModalState.job.id}/schedule`, `/api/v1/settings/jobs/${jobModalState.job.id}/schedule`,
{ {
schedule: jobScheduleCron.join(' '), method: 'POST',
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

@@ -17,7 +17,6 @@ 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';
@@ -87,7 +86,10 @@ const SettingsMain = () => {
const regenerate = async () => { const regenerate = async () => {
try { try {
await axios.post('/api/v1/settings/main/regenerate'); const res = await fetch('/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,18 +142,25 @@ const SettingsMain = () => {
validationSchema={MainSettingsSchema} validationSchema={MainSettingsSchema}
onSubmit={async (values) => { onSubmit={async (values) => {
try { try {
await axios.post('/api/v1/settings/main', { const res = await fetch('/api/v1/settings/main', {
applicationTitle: values.applicationTitle, method: 'POST',
applicationUrl: values.applicationUrl, headers: {
csrfProtection: values.csrfProtection, 'Content-Type': 'application/json',
hideAvailable: values.hideAvailable, },
locale: values.locale, body: JSON.stringify({
region: values.region, applicationTitle: values.applicationTitle,
originalLanguage: values.originalLanguage, applicationUrl: values.applicationUrl,
partialRequestsEnabled: values.partialRequestsEnabled, csrfProtection: values.csrfProtection,
trustProxy: values.trustProxy, hideAvailable: values.hideAvailable,
cacheImages: values.cacheImages, locale: values.locale,
region: values.region,
originalLanguage: values.originalLanguage,
partialRequestsEnabled: values.partialRequestsEnabled,
trustProxy: values.trustProxy,
cacheImages: values.cacheImages,
}),
}); });
if (!res.ok) throw new Error();
mutate('/api/v1/settings/public'); mutate('/api/v1/settings/public');
mutate('/api/v1/status'); mutate('/api/v1/status');

View File

@@ -16,7 +16,6 @@ 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';
@@ -241,9 +240,15 @@ const SettingsPlex = ({ onComplete }: SettingsPlexProps) => {
params.enable = activeLibraries.join(','); params.enable = activeLibraries.join(',');
} }
await axios.get('/api/v1/settings/plex/library', { const searchParams = new URLSearchParams({
params, sync: params.sync ? 'true' : 'false',
...(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();
}; };
@@ -262,11 +267,12 @@ const SettingsPlex = ({ onComplete }: SettingsPlexProps) => {
toastId = id; toastId = id;
} }
); );
const response = await axios.get<PlexDevice[]>( const res = await fetch('/api/v1/settings/plex/devices/servers');
'/api/v1/settings/plex/devices/servers' if (!res.ok) throw new Error();
); const data: PlexDevice[] = await res.json();
if (response.data) {
setAvailableServers(response.data); if (data) {
setAvailableServers(data);
} }
if (toastId) { if (toastId) {
removeToast(toastId); removeToast(toastId);
@@ -289,16 +295,30 @@ const SettingsPlex = ({ onComplete }: SettingsPlexProps) => {
}; };
const startScan = async () => { const startScan = async () => {
await axios.post('/api/v1/settings/plex/sync', { const res = await fetch('/api/v1/settings/plex/sync', {
start: true, method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
start: true,
}),
}); });
if (!res.ok) throw new Error();
revalidateSync(); revalidateSync();
}; };
const cancelScan = async () => { const cancelScan = async () => {
await axios.post('/api/v1/settings/plex/sync', { const res = await fetch('/api/v1/settings/plex/sync', {
cancel: true, method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
cancel: true,
}),
}); });
if (!res.ok) throw new Error();
revalidateSync(); revalidateSync();
}; };
@@ -313,15 +333,19 @@ const SettingsPlex = ({ onComplete }: SettingsPlexProps) => {
.join(','); .join(',');
} }
await axios.get('/api/v1/settings/plex/library', { const searchParams = new URLSearchParams(params.enable ? params : {});
params, const res = await fetch(
}); `/api/v1/settings/plex/library?${searchParams.toString()}`
);
if (!res.ok) throw new Error();
} else { } else {
await axios.get('/api/v1/settings/plex/library', { const searchParams = new URLSearchParams({
params: { enable: [...activeLibraries, libraryId].join(','),
enable: [...activeLibraries, libraryId].join(','),
},
}); });
const res = await fetch(
`/api/v1/settings/plex/library?${searchParams.toString()}`
);
if (!res.ok) throw new Error();
} }
setIsSyncing(false); setIsSyncing(false);
revalidate(); revalidate();
@@ -385,12 +409,19 @@ const SettingsPlex = ({ onComplete }: SettingsPlexProps) => {
toastId = id; toastId = id;
} }
); );
await axios.post('/api/v1/settings/plex', { const res = await fetch('/api/v1/settings/plex', {
ip: values.hostname, method: 'POST',
port: Number(values.port), headers: {
useSsl: values.useSsl, 'Content-Type': 'application/json',
webAppUrl: values.webAppUrl, },
} as PlexSettings); body: JSON.stringify({
ip: values.hostname,
port: Number(values.port),
useSsl: values.useSsl,
webAppUrl: values.webAppUrl,
} as PlexSettings),
});
if (!res.ok) throw new Error();
syncLibraries(); syncLibraries();
@@ -748,14 +779,27 @@ const SettingsPlex = ({ onComplete }: SettingsPlexProps) => {
validationSchema={TautulliSettingsSchema} validationSchema={TautulliSettingsSchema}
onSubmit={async (values) => { onSubmit={async (values) => {
try { try {
await axios.post('/api/v1/settings/tautulli', { const res = await fetch('/api/v1/settings/tautulli', {
hostname: values.tautulliHostname, method: 'POST',
port: Number(values.tautulliPort), headers: {
useSsl: values.tautulliUseSsl, 'Content-Type': 'application/json',
urlBase: values.tautulliUrlBase, },
apiKey: values.tautulliApiKey, body: JSON.stringify({
externalUrl: values.tautulliExternalUrl, hostname: values.tautulliHostname,
} as TautulliSettings); port: Number(values.tautulliPort),
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

@@ -13,7 +13,6 @@ import defineMessages from '@app/utils/defineMessages';
import { Transition } from '@headlessui/react'; import { Transition } from '@headlessui/react';
import { PencilIcon, PlusIcon, TrashIcon } from '@heroicons/react/24/solid'; import { PencilIcon, PlusIcon, TrashIcon } from '@heroicons/react/24/solid';
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';
@@ -196,9 +195,14 @@ const SettingsServices = () => {
}); });
const deleteServer = async () => { const deleteServer = async () => {
await axios.delete( const res = await fetch(
`/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

@@ -9,7 +9,6 @@ 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 getConfig from 'next/config'; import getConfig from 'next/config';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
@@ -77,21 +76,28 @@ const SettingsUsers = () => {
enableReinitialize enableReinitialize
onSubmit={async (values) => { onSubmit={async (values) => {
try { try {
await axios.post('/api/v1/settings/main', { const res = await fetch('/api/v1/settings/main', {
localLogin: values.localLogin, method: 'POST',
newPlexLogin: values.newPlexLogin, headers: {
defaultQuotas: { 'Content-Type': 'application/json',
movie: {
quotaLimit: values.movieQuotaLimit,
quotaDays: values.movieQuotaDays,
},
tv: {
quotaLimit: values.tvQuotaLimit,
quotaDays: values.tvQuotaDays,
},
}, },
defaultPermissions: values.defaultPermissions, body: JSON.stringify({
localLogin: values.localLogin,
newPlexLogin: values.newPlexLogin,
defaultQuotas: {
movie: {
quotaLimit: values.movieQuotaLimit,
quotaDays: values.movieQuotaDays,
},
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

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

View File

@@ -1,7 +1,6 @@
import PlexLoginButton from '@app/components/PlexLoginButton'; import PlexLoginButton from '@app/components/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';
@@ -25,9 +24,19 @@ const LoginWithPlex = ({ onComplete }: LoginWithPlexProps) => {
useEffect(() => { useEffect(() => {
const login = async () => { const login = async () => {
const response = await axios.post('/api/v1/auth/plex', { authToken }); const res = await fetch('/api/v1/auth/plex', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
authToken,
}),
});
if (!res.ok) throw new Error();
const data = await res.json();
if (response.data?.id) { if (data?.id) {
revalidate(); revalidate();
} }
}; };

View File

@@ -4,7 +4,6 @@ import PlexLoginButton from '@app/components/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 { MediaServerType } from '@server/constants/server'; import { MediaServerType } from '@server/constants/server';
import axios from 'axios';
import getConfig from 'next/config'; import getConfig from 'next/config';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { FormattedMessage, useIntl } from 'react-intl'; import { FormattedMessage, useIntl } from 'react-intl';
@@ -34,11 +33,19 @@ const SetupLogin: React.FC<LoginWithMediaServerProps> = ({ onComplete }) => {
useEffect(() => { useEffect(() => {
const login = async () => { const login = async () => {
const response = await axios.post('/api/v1/auth/plex', { const res = await fetch('/api/v1/auth/plex', {
authToken: authToken, method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
authToken: authToken,
}),
}); });
if (!res.ok) throw new Error();
const data = await res.json();
if (response.data?.email) { if (data?.email) {
revalidate(); revalidate();
} }
}; };

View File

@@ -11,7 +11,6 @@ import SetupSteps from '@app/components/Setup/SetupSteps';
import useLocale from '@app/hooks/useLocale'; import useLocale from '@app/hooks/useLocale';
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 Image from 'next/image'; import Image from 'next/image';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useState } from 'react'; import { useState } from 'react';
@@ -46,15 +45,27 @@ const Setup = () => {
const finishSetup = async () => { const finishSetup = async () => {
setIsUpdating(true); setIsUpdating(true);
const response = await axios.post<{ initialized: boolean }>( const res = await fetch('/api/v1/settings/initialize', {
'/api/v1/settings/initialize' method: 'POST',
); headers: {
'Content-Type': 'application/json',
},
});
if (!res.ok) throw new Error();
const data: { initialized: boolean } = await res.json();
setIsUpdating(false); setIsUpdating(false);
if (response.data.initialized) { if (data.initialized) {
await axios.post('/api/v1/settings/main', { locale }); const mainRes = await fetch('/api/v1/settings/main', {
mutate('/api/v1/settings/public'); method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ locale }),
});
if (!mainRes.ok) throw new Error();
mutate('/api/v1/settings/public');
router.push('/'); router.push('/');
} }
}; };

View File

@@ -2,7 +2,6 @@ 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';
@@ -21,11 +20,14 @@ const messages = defineMessages('components.TitleCard', {
cleardata: 'Clear Data', cleardata: 'Clear Data',
}); });
const Error = ({ id, tmdbId, tvdbId, type, canExpand }: ErrorCardProps) => { const ErrorCard = ({ id, tmdbId, tvdbId, type, canExpand }: ErrorCardProps) => {
const intl = useIntl(); const intl = useIntl();
const deleteMedia = async () => { const deleteMedia = async () => {
await axios.delete(`/api/v1/media/${id}`); const res = await fetch(`/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');
}; };
@@ -129,4 +131,4 @@ const Error = ({ id, tmdbId, tvdbId, type, canExpand }: ErrorCardProps) => {
</div> </div>
); );
}; };
export default Error; export default ErrorCard;

View File

@@ -19,7 +19,6 @@ 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, useState } from 'react'; import { Fragment, useCallback, useEffect, useState } from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
@@ -98,13 +97,21 @@ const TitleCard = ({
const onClickWatchlistBtn = async (): Promise<void> => { const onClickWatchlistBtn = async (): Promise<void> => {
setIsUpdating(true); setIsUpdating(true);
try { try {
const response = await axios.post<Watchlist>('/api/v1/watchlist', { const res = await fetch('/api/v1/watchlist', {
tmdbId: id, method: 'POST',
mediaType, headers: {
title, 'Content-Type': 'application/json',
},
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 (response.data) { if (data) {
addToast( addToast(
<span> <span>
{intl.formatMessage(messages.watchlistSuccess, { {intl.formatMessage(messages.watchlistSuccess, {
@@ -129,9 +136,11 @@ const TitleCard = ({
const onClickDeleteWatchlistBtn = async (): Promise<void> => { const onClickDeleteWatchlistBtn = async (): Promise<void> => {
setIsUpdating(true); setIsUpdating(true);
try { try {
const response = await axios.delete<Watchlist>('/api/v1/watchlist/' + id); const res = await fetch('/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, {

View File

@@ -4,7 +4,6 @@ import type { User } from '@app/hooks/useUser';
import { useUser } from '@app/hooks/useUser'; 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 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,10 +44,18 @@ const BulkEditModal = ({
const updateUsers = async () => { const updateUsers = async () => {
try { try {
setIsSaving(true); setIsSaving(true);
const { data: updated } = await axios.put<User[]>(`/api/v1/user`, { const res = await fetch('/api/v1/user', {
ids: selectedUserIds, method: 'PUT',
permissions: currentPermission, headers: {
'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

@@ -4,7 +4,6 @@ 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 type { UserResultsResponse } from '@server/interfaces/api/userInterfaces'; import type { UserResultsResponse } from '@server/interfaces/api/userInterfaces';
import axios from 'axios';
import getConfig from 'next/config'; import getConfig from 'next/config';
import Image from 'next/image'; import Image from 'next/image';
import { useState } from 'react'; import { useState } from 'react';
@@ -69,10 +68,17 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
setImporting(true); setImporting(true);
try { try {
const { data: createdUsers } = await axios.post( const res = await fetch('/api/v1/user/import-from-jellyfin', {
'/api/v1/user/import-from-jellyfin', method: 'POST',
{ jellyfinUserIds: selectedUsers } headers: {
); 'Content-Type': 'application/json',
},
body: JSON.stringify({
jellyfinUserIds: selectedUsers,
}),
});
if (!res.ok) throw new Error();
const { data: 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,7 +3,6 @@ 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';
@@ -48,10 +47,17 @@ const PlexImportModal = ({ onCancel, onComplete }: PlexImportProps) => {
setImporting(true); setImporting(true);
try { try {
const { data: createdUsers } = await axios.post( const res = await fetch('/api/v1/user/import-from-plex', {
'/api/v1/user/import-from-plex', method: 'POST',
{ plexIds: selectedUsers } headers: {
); 'Content-Type': 'application/json',
},
body: JSON.stringify({
plexIds: selectedUsers,
}),
});
if (!res.ok) throw new Error();
const { data: createdUsers } = await res.json();
if (!createdUsers.length) { if (!createdUsers.length) {
throw new Error('No users were imported from Plex.'); throw new Error('No users were imported from Plex.');

View File

@@ -27,7 +27,6 @@ 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 getConfig from 'next/config'; import getConfig from 'next/config';
import Image from 'next/image'; import Image from 'next/image';
@@ -183,7 +182,10 @@ const UserList = () => {
setDeleting(true); setDeleting(true);
try { try {
await axios.delete(`/api/v1/user/${deleteModal.user?.id}`); const res = await fetch(`/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,
@@ -282,11 +284,18 @@ const UserList = () => {
validationSchema={CreateUserSchema} validationSchema={CreateUserSchema}
onSubmit={async (values) => { onSubmit={async (values) => {
try { try {
await axios.post('/api/v1/user', { const res = await fetch('/api/v1/user', {
username: values.displayName, method: 'POST',
email: values.email, headers: {
password: values.genpassword ? null : values.password, 'Content-Type': 'application/json',
},
body: JSON.stringify({
username: values.displayName,
email: values.email,
password: values.genpassword ? null : values.password,
}),
}); });
if (!res.ok) throw new Error();
addToast(intl.formatMessage(messages.usercreatedsuccess), { addToast(intl.formatMessage(messages.usercreatedsuccess), {
appearance: 'success', appearance: 'success',
autoDismiss: true, autoDismiss: true,

View File

@@ -11,11 +11,10 @@ import useLocale from '@app/hooks/useLocale';
import useSettings from '@app/hooks/useSettings'; import useSettings from '@app/hooks/useSettings';
import { Permission, UserType, useUser } from '@app/hooks/useUser'; import { Permission, UserType, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages'; import globalMessages from '@app/i18n/globalMessages';
import Error from '@app/pages/_error'; import ErrorPage from '@app/pages/_error';
import defineMessages from '@app/utils/defineMessages'; import defineMessages from '@app/utils/defineMessages';
import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline'; import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline';
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 getConfig from 'next/config'; import getConfig from 'next/config';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
@@ -116,7 +115,7 @@ const UserGeneralSettings = () => {
} }
if (!data) { if (!data) {
return <Error statusCode={500} />; return <ErrorPage statusCode={500} />;
} }
return ( return (
@@ -151,22 +150,31 @@ const UserGeneralSettings = () => {
enableReinitialize enableReinitialize
onSubmit={async (values) => { onSubmit={async (values) => {
try { try {
await axios.post(`/api/v1/user/${user?.id}/settings/main`, { const res = await fetch(`/api/v1/user/${user?.id}/settings/main`, {
username: values.displayName, method: 'POST',
email: values.email, headers: {
discordId: values.discordId, 'Content-Type': 'application/json',
locale: values.locale, },
region: values.region, body: JSON.stringify({
originalLanguage: values.originalLanguage, username: values.displayName,
movieQuotaLimit: movieQuotaEnabled email: values.email,
? values.movieQuotaLimit discordId: values.discordId,
: null, locale: values.locale,
movieQuotaDays: movieQuotaEnabled ? values.movieQuotaDays : null, region: values.region,
tvQuotaLimit: tvQuotaEnabled ? values.tvQuotaLimit : null, originalLanguage: values.originalLanguage,
tvQuotaDays: tvQuotaEnabled ? values.tvQuotaDays : null, movieQuotaLimit: movieQuotaEnabled
watchlistSyncMovies: values.watchlistSyncMovies, ? values.movieQuotaLimit
watchlistSyncTv: values.watchlistSyncTv, : null,
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();
if (currentUser?.id === user?.id && setLocale) { if (currentUser?.id === user?.id && setLocale) {
setLocale( setLocale(

View File

@@ -6,7 +6,6 @@ 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';
@@ -68,18 +67,28 @@ const UserNotificationsDiscord = () => {
enableReinitialize enableReinitialize
onSubmit={async (values) => { onSubmit={async (values) => {
try { try {
await axios.post(`/api/v1/user/${user?.id}/settings/notifications`, { const res = await fetch(
pgpKey: data?.pgpKey, `/api/v1/user/${user?.id}/settings/notifications`,
discordId: values.discordId, {
pushbulletAccessToken: data?.pushbulletAccessToken, method: 'POST',
pushoverApplicationToken: data?.pushoverApplicationToken, headers: {
pushoverUserKey: data?.pushoverUserKey, 'Content-Type': 'application/json',
telegramChatId: data?.telegramChatId, },
telegramSendSilently: data?.telegramSendSilently, body: JSON.stringify({
notificationTypes: { pgpKey: data?.pgpKey,
discord: values.types, discordId: values.discordId,
}, 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,7 +11,6 @@ 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';
@@ -67,18 +66,28 @@ const UserEmailSettings = () => {
enableReinitialize enableReinitialize
onSubmit={async (values) => { onSubmit={async (values) => {
try { try {
await axios.post(`/api/v1/user/${user?.id}/settings/notifications`, { const res = await fetch(
pgpKey: values.pgpKey, `/api/v1/user/${user?.id}/settings/notifications`,
discordId: data?.discordId, {
pushbulletAccessToken: data?.pushbulletAccessToken, method: 'POST',
pushoverApplicationToken: data?.pushoverApplicationToken, headers: {
pushoverUserKey: data?.pushoverUserKey, 'Content-Type': 'application/json',
telegramChatId: data?.telegramChatId, },
telegramSendSilently: data?.telegramSendSilently, body: JSON.stringify({
notificationTypes: { pgpKey: values.pgpKey,
email: values.types, discordId: data?.discordId,
}, 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,7 +6,6 @@ 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';
@@ -65,18 +64,28 @@ const UserPushbulletSettings = () => {
enableReinitialize enableReinitialize
onSubmit={async (values) => { onSubmit={async (values) => {
try { try {
await axios.post(`/api/v1/user/${user?.id}/settings/notifications`, { const res = await fetch(
pgpKey: data?.pgpKey, `/api/v1/user/${user?.id}/settings/notifications`,
discordId: data?.discordId, {
pushbulletAccessToken: values.pushbulletAccessToken, method: 'POST',
pushoverApplicationToken: data?.pushoverApplicationToken, headers: {
pushoverUserKey: data?.pushoverUserKey, 'Content-Type': 'application/json',
telegramChatId: data?.telegramChatId, },
telegramSendSilently: data?.telegramSendSilently, body: JSON.stringify({
notificationTypes: { pgpKey: data?.pgpKey,
pushbullet: values.types, discordId: data?.discordId,
}, 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,7 +7,6 @@ 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';
@@ -97,18 +96,28 @@ const UserPushoverSettings = () => {
enableReinitialize enableReinitialize
onSubmit={async (values) => { onSubmit={async (values) => {
try { try {
await axios.post(`/api/v1/user/${user?.id}/settings/notifications`, { const res = await fetch(
pgpKey: data?.pgpKey, `/api/v1/user/${user?.id}/settings/notifications`,
discordId: data?.discordId, {
pushbulletAccessToken: data?.pushbulletAccessToken, method: 'POST',
pushoverApplicationToken: values.pushoverApplicationToken, headers: {
pushoverUserKey: values.pushoverUserKey, 'Content-Type': 'application/json',
telegramChatId: data?.telegramChatId, },
telegramSendSilently: data?.telegramSendSilently, body: JSON.stringify({
notificationTypes: { pgpKey: data?.pgpKey,
pushover: values.types, discordId: data?.discordId,
}, 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,

View File

@@ -6,7 +6,6 @@ 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';
@@ -71,18 +70,28 @@ const UserTelegramSettings = () => {
enableReinitialize enableReinitialize
onSubmit={async (values) => { onSubmit={async (values) => {
try { try {
await axios.post(`/api/v1/user/${user?.id}/settings/notifications`, { const res = await fetch(
pgpKey: data?.pgpKey, `/api/v1/user/${user?.id}/settings/notifications`,
discordId: data?.discordId, {
pushbulletAccessToken: data?.pushbulletAccessToken, method: 'POST',
pushoverApplicationToken: data?.pushoverApplicationToken, headers: {
pushoverUserKey: data?.pushoverUserKey, 'Content-Type': 'application/json',
telegramChatId: values.telegramChatId, },
telegramSendSilently: values.telegramSendSilently, body: JSON.stringify({
notificationTypes: { pgpKey: data?.pgpKey,
telegram: values.types, discordId: data?.discordId,
}, pushbulletAccessToken: data?.pushbulletAccessToken,
}); pushoverApplicationToken: data?.pushoverApplicationToken,
pushoverUserKey: data?.pushoverUserKey,
telegramChatId: values.telegramChatId,
telegramSendSilently: values.telegramSendSilently,
notificationTypes: {
telegram: values.types,
},
}),
}
);
if (!res.ok) throw new Error();
addToast(intl.formatMessage(messages.telegramsettingssaved), { addToast(intl.formatMessage(messages.telegramsettingssaved), {
appearance: 'success', appearance: 'success',
autoDismiss: true, autoDismiss: true,

View File

@@ -8,7 +8,6 @@ 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';
@@ -48,18 +47,28 @@ const UserWebPushSettings = () => {
enableReinitialize enableReinitialize
onSubmit={async (values) => { onSubmit={async (values) => {
try { try {
await axios.post(`/api/v1/user/${user?.id}/settings/notifications`, { const res = await fetch(
pgpKey: data?.pgpKey, `/api/v1/user/${user?.id}/settings/notifications`,
discordId: data?.discordId, {
pushbulletAccessToken: data?.pushbulletAccessToken, method: 'POST',
pushoverApplicationToken: data?.pushoverApplicationToken, headers: {
pushoverUserKey: data?.pushoverUserKey, 'Content-Type': 'application/json',
telegramChatId: data?.telegramChatId, },
telegramSendSilently: data?.telegramSendSilently, body: JSON.stringify({
notificationTypes: { pgpKey: data?.pgpKey,
webpush: values.types, discordId: data?.discordId,
}, pushbulletAccessToken: data?.pushbulletAccessToken,
}); pushoverApplicationToken: data?.pushoverApplicationToken,
pushoverUserKey: data?.pushoverUserKey,
telegramChatId: data?.telegramChatId,
telegramSendSilently: data?.telegramSendSilently,
notificationTypes: {
webpush: values.types,
},
}),
}
);
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',

View File

@@ -5,10 +5,9 @@ import PageTitle from '@app/components/Common/PageTitle';
import SensitiveInput from '@app/components/Common/SensitiveInput'; import SensitiveInput from '@app/components/Common/SensitiveInput';
import { Permission, useUser } from '@app/hooks/useUser'; import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages'; import globalMessages from '@app/i18n/globalMessages';
import Error from '@app/pages/_error'; import ErrorPage from '@app/pages/_error';
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 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';
@@ -80,7 +79,7 @@ const UserPasswordChange = () => {
} }
if (!data) { if (!data) {
return <Error statusCode={500} />; return <ErrorPage statusCode={500} />;
} }
if ( if (
@@ -123,11 +122,21 @@ const UserPasswordChange = () => {
enableReinitialize enableReinitialize
onSubmit={async (values, { resetForm }) => { onSubmit={async (values, { resetForm }) => {
try { try {
await axios.post(`/api/v1/user/${user?.id}/settings/password`, { const res = await fetch(
currentPassword: values.currentPassword, `/api/v1/user/${user?.id}/settings/password`,
newPassword: values.newPassword, {
confirmPassword: values.confirmPassword, method: 'POST',
}); headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
currentPassword: values.currentPassword,
newPassword: values.newPassword,
confirmPassword: values.confirmPassword,
}),
}
);
if (!res.ok) throw new Error();
addToast(intl.formatMessage(messages.toastSettingsSuccess), { addToast(intl.formatMessage(messages.toastSettingsSuccess), {
autoDismiss: true, autoDismiss: true,

View File

@@ -5,10 +5,9 @@ import PageTitle from '@app/components/Common/PageTitle';
import PermissionEdit from '@app/components/PermissionEdit'; import PermissionEdit from '@app/components/PermissionEdit';
import { useUser } from '@app/hooks/useUser'; import { useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages'; import globalMessages from '@app/i18n/globalMessages';
import Error from '@app/pages/_error'; import ErrorPage from '@app/pages/_error';
import defineMessages from '@app/utils/defineMessages'; import defineMessages from '@app/utils/defineMessages';
import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline'; import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline';
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';
@@ -46,7 +45,7 @@ const UserPermissions = () => {
} }
if (!data) { if (!data) {
return <Error statusCode={500} />; return <ErrorPage statusCode={500} />;
} }
if (currentUser?.id !== 1 && currentUser?.id === user?.id) { if (currentUser?.id !== 1 && currentUser?.id === user?.id) {
@@ -84,10 +83,19 @@ const UserPermissions = () => {
enableReinitialize enableReinitialize
onSubmit={async (values) => { onSubmit={async (values) => {
try { try {
await axios.post(`/api/v1/user/${user?.id}/settings/permissions`, { const res = await fetch(
permissions: values.currentPermissions ?? 0, `/api/v1/user/${user?.id}/settings/permissions`,
}); {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
permissions: values.currentPermissions ?? 0,
}),
}
);
if (!res.ok) throw new Error();
addToast(intl.formatMessage(messages.toastSettingsSuccess), { addToast(intl.formatMessage(messages.toastSettingsSuccess), {
autoDismiss: true, autoDismiss: true,
appearance: 'success', appearance: 'success',

View File

@@ -15,7 +15,6 @@ import '@app/styles/globals.css';
import { polyfillIntl } from '@app/utils/polyfillIntl'; import { polyfillIntl } from '@app/utils/polyfillIntl';
import { MediaServerType } from '@server/constants/server'; import { MediaServerType } from '@server/constants/server';
import type { PublicSettingsResponse } from '@server/interfaces/api/settingsInterfaces'; import type { PublicSettingsResponse } from '@server/interfaces/api/settingsInterfaces';
import axios from 'axios';
import type { AppInitialProps, AppProps } from 'next/app'; import type { AppInitialProps, AppProps } from 'next/app';
import App from 'next/app'; import App from 'next/app';
import Head from 'next/head'; import Head from 'next/head';
@@ -139,7 +138,11 @@ const CoreApp: Omit<NextAppComponentType, 'origGetInitialProps'> = ({
return ( return (
<SWRConfig <SWRConfig
value={{ value={{
fetcher: (url) => axios.get(url).then((res) => res.data), fetcher: async (resource, init) => {
const res = await fetch(resource, init);
if (!res.ok) throw new Error();
return await res.json();
},
fallback: { fallback: {
'/api/v1/auth/me': user, '/api/v1/auth/me': user,
}, },
@@ -202,13 +205,13 @@ CoreApp.getInitialProps = async (initialProps) => {
if (ctx.res) { if (ctx.res) {
// Check if app is initialized and redirect if necessary // Check if app is initialized and redirect if necessary
const response = await axios.get<PublicSettingsResponse>( const res = await fetch(
`http://localhost:${process.env.PORT || 5055}/api/v1/settings/public` `http://localhost:${process.env.PORT || 5055}/api/v1/settings/public`
); );
if (!res.ok) throw new Error();
currentSettings = await res.json();
currentSettings = response.data; const initialized = currentSettings.initialized;
const initialized = response.data.initialized;
if (!initialized) { if (!initialized) {
if (!router.pathname.match(/(setup|login\/plex)/)) { if (!router.pathname.match(/(setup|login\/plex)/)) {
@@ -220,7 +223,7 @@ CoreApp.getInitialProps = async (initialProps) => {
} else { } else {
try { try {
// Attempt to get the user by running a request to the local api // Attempt to get the user by running a request to the local api
const response = await axios.get<User>( const res = await fetch(
`http://localhost:${process.env.PORT || 5055}/api/v1/auth/me`, `http://localhost:${process.env.PORT || 5055}/api/v1/auth/me`,
{ {
headers: headers:
@@ -229,7 +232,8 @@ CoreApp.getInitialProps = async (initialProps) => {
: undefined, : undefined,
} }
); );
user = response.data; if (!res.ok) throw new Error();
user = await res.json();
if (router.pathname.match(/(setup|login)/)) { if (router.pathname.match(/(setup|login)/)) {
ctx.res.writeHead(307, { ctx.res.writeHead(307, {

View File

@@ -1,6 +1,5 @@
import CollectionDetails from '@app/components/CollectionDetails'; import CollectionDetails from '@app/components/CollectionDetails';
import type { Collection } from '@server/models/Collection'; import type { Collection } from '@server/models/Collection';
import axios from 'axios';
import type { GetServerSideProps, NextPage } from 'next'; import type { GetServerSideProps, NextPage } from 'next';
interface CollectionPageProps { interface CollectionPageProps {
@@ -14,7 +13,7 @@ const CollectionPage: NextPage<CollectionPageProps> = ({ collection }) => {
export const getServerSideProps: GetServerSideProps< export const getServerSideProps: GetServerSideProps<
CollectionPageProps CollectionPageProps
> = async (ctx) => { > = async (ctx) => {
const response = await axios.get<Collection>( const res = await fetch(
`http://localhost:${process.env.PORT || 5055}/api/v1/collection/${ `http://localhost:${process.env.PORT || 5055}/api/v1/collection/${
ctx.query.collectionId ctx.query.collectionId
}`, }`,
@@ -24,10 +23,12 @@ export const getServerSideProps: GetServerSideProps<
: undefined, : undefined,
} }
); );
if (!res.ok) throw new Error();
const collection: Collection = await res.json();
return { return {
props: { props: {
collection: response.data, collection,
}, },
}; };
}; };

View File

@@ -1,6 +1,5 @@
import MovieDetails from '@app/components/MovieDetails'; import MovieDetails from '@app/components/MovieDetails';
import type { MovieDetails as MovieDetailsType } from '@server/models/Movie'; import type { MovieDetails as MovieDetailsType } from '@server/models/Movie';
import axios from 'axios';
import type { GetServerSideProps, NextPage } from 'next'; import type { GetServerSideProps, NextPage } from 'next';
interface MoviePageProps { interface MoviePageProps {
@@ -14,7 +13,7 @@ const MoviePage: NextPage<MoviePageProps> = ({ movie }) => {
export const getServerSideProps: GetServerSideProps<MoviePageProps> = async ( export const getServerSideProps: GetServerSideProps<MoviePageProps> = async (
ctx ctx
) => { ) => {
const response = await axios.get<MovieDetailsType>( const res = await fetch(
`http://localhost:${process.env.PORT || 5055}/api/v1/movie/${ `http://localhost:${process.env.PORT || 5055}/api/v1/movie/${
ctx.query.movieId ctx.query.movieId
}`, }`,
@@ -24,10 +23,12 @@ export const getServerSideProps: GetServerSideProps<MoviePageProps> = async (
: undefined, : undefined,
} }
); );
if (!res.ok) throw new Error();
const movie: MovieDetailsType = await res.json();
return { return {
props: { props: {
movie: response.data, movie,
}, },
}; };
}; };

View File

@@ -1,6 +1,5 @@
import TvDetails from '@app/components/TvDetails'; import TvDetails from '@app/components/TvDetails';
import type { TvDetails as TvDetailsType } from '@server/models/Tv'; import type { TvDetails as TvDetailsType } from '@server/models/Tv';
import axios from 'axios';
import type { GetServerSideProps, NextPage } from 'next'; import type { GetServerSideProps, NextPage } from 'next';
interface TvPageProps { interface TvPageProps {
@@ -14,7 +13,7 @@ const TvPage: NextPage<TvPageProps> = ({ tv }) => {
export const getServerSideProps: GetServerSideProps<TvPageProps> = async ( export const getServerSideProps: GetServerSideProps<TvPageProps> = async (
ctx ctx
) => { ) => {
const response = await axios.get<TvDetailsType>( const res = await fetch(
`http://localhost:${process.env.PORT || 5055}/api/v1/tv/${ctx.query.tvId}`, `http://localhost:${process.env.PORT || 5055}/api/v1/tv/${ctx.query.tvId}`,
{ {
headers: ctx.req?.headers?.cookie headers: ctx.req?.headers?.cookie
@@ -22,10 +21,12 @@ export const getServerSideProps: GetServerSideProps<TvPageProps> = async (
: undefined, : undefined,
} }
); );
if (!res.ok) throw new Error();
const tv: TvDetailsType = await res.json();
return { return {
props: { props: {
tv: response.data, tv,
}, },
}; };
}; };

View File

@@ -1,6 +1,3 @@
import type { AxiosError, AxiosResponse } from 'axios';
import axios from 'axios';
interface JellyfinAuthenticationResult { interface JellyfinAuthenticationResult {
Id: string; Id: string;
AccessToken: string; AccessToken: string;
@@ -18,30 +15,34 @@ class JellyAPI {
resolve: (result: JellyfinAuthenticationResult) => void, resolve: (result: JellyfinAuthenticationResult) => void,
reject: (e: Error) => void reject: (e: Error) => void
) => { ) => {
axios fetch(Hostname + '/Users/AuthenticateByName', {
.post( method: 'POST',
Hostname + '/Users/AuthenticateByName', headers: {
{ 'Content-Type': 'application/json',
Username: Username, 'X-Emby-Authorization':
Pw: Password, 'MediaBrowser Client="Jellyfin Web", Device="Firefox", DeviceId="TW96aWxsYS81LjAgKFdpbmRvd3MgTlQgMTAuMDsgV2luNjQ7IHg2NDsgcnY6ODUuMCkgR2Vja28vMjAxMDAxMDEgRmlyZWZveC84NS4wfDE2MTI5MjcyMDM5NzM1", Version="10.8.0"',
}, },
{ body: JSON.stringify({
headers: { Username: Username,
'X-Emby-Authorization': Pw: Password,
'MediaBrowser Client="Jellyfin Web", Device="Firefox", DeviceId="TW96aWxsYS81LjAgKFdpbmRvd3MgTlQgMTAuMDsgV2luNjQ7IHg2NDsgcnY6ODUuMCkgR2Vja28vMjAxMDAxMDEgRmlyZWZveC84NS4wfDE2MTI5MjcyMDM5NzM1", Version="10.8.0"', }),
}, })
.then((res) => {
if (!res.ok) {
throw new Error('Network response was not ok');
} }
) return res.json();
.then((resp: AxiosResponse) => { })
.then((data) => {
const response: JellyfinAuthenticationResult = { const response: JellyfinAuthenticationResult = {
Id: resp.data.User.Id, Id: data.User.Id,
AccessToken: resp.data.AccessToken, AccessToken: data.AccessToken,
ServerId: resp.data.ServerId, ServerId: data.ServerId,
}; };
resolve(response); resolve(response);
}) })
.catch((e: AxiosError) => { .catch((error) => {
reject(e); reject(error);
}); });
} }
); );

View File

@@ -1,4 +1,3 @@
import axios from 'axios';
import Bowser from 'bowser'; import Bowser from 'bowser';
interface PlexHeaders extends Record<string, string> { interface PlexHeaders extends Record<string, string> {
@@ -78,13 +77,14 @@ class PlexOAuth {
'You must initialize the plex headers clientside to login' 'You must initialize the plex headers clientside to login'
); );
} }
const response = await axios.post( const res = await fetch('https://plex.tv/api/v2/pins?strong=true', {
'https://plex.tv/api/v2/pins?strong=true', method: 'POST',
undefined, headers: this.plexHeaders,
{ headers: this.plexHeaders } });
); if (!res.ok) throw new Error();
const data = await res.json();
this.pin = { id: response.data.id, code: response.data.code }; this.pin = { id: data.id, code: data.code };
return this.pin; return this.pin;
} }
@@ -136,16 +136,17 @@ class PlexOAuth {
throw new Error('Unable to poll when pin is not initialized.'); throw new Error('Unable to poll when pin is not initialized.');
} }
const response = await axios.get( const res = await fetch(`https://plex.tv/api/v2/pins/${this.pin.id}`, {
`https://plex.tv/api/v2/pins/${this.pin.id}`, headers: this.plexHeaders,
{ headers: this.plexHeaders } });
); if (!res.ok) throw new Error();
const data = await res.json();
if (response.data?.authToken) { if (data?.authToken) {
this.authToken = response.data.authToken as string; this.authToken = data.authToken as string;
this.closePopup(); this.closePopup();
resolve(this.authToken); resolve(this.authToken);
} else if (!response.data?.authToken && !this.popup?.closed) { } else if (!data?.authToken && !this.popup?.closed) {
setTimeout(executePoll, 1000, resolve, reject); setTimeout(executePoll, 1000, resolve, reject);
} else { } else {
reject(new Error('Popup closed without completing login')); reject(new Error('Popup closed without completing login'));