Compare commits

...

2 Commits

Author SHA1 Message Date
Gauthier
dd5a9fad4f fix: remove useless Fetch override
Remove the fetch override that was handling CSRF tokens as Axios handle this natively.
2024-11-07 13:22:25 +01:00
Gauthier
9803bc40df refactor: switch from Fetch API to Axios 2024-11-07 13:16:36 +01:00
103 changed files with 5327 additions and 6850 deletions

View File

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

View File

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

View File

@@ -3,7 +3,9 @@ title: Build From Source (Advanced)
description: Install Jellyseerr by building from source
sidebar_position: 2
---
# Build from Source (Advanced)
:::warning
This method is not recommended for most users. It is intended for advanced users who are familiar with managing their own server infrastructure.
:::
@@ -12,31 +14,43 @@ import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
### Prerequisites
- [Node.js 20.x](https://nodejs.org/en/download/)
- [Pnpm 9.x](https://pnpm.io/installation)
- [Git](https://git-scm.com/downloads)
## Unix (Linux, macOS)
### Installation
1. Assuming you want the working directory to be `/opt/jellyseerr`, create the directory and navigate to it:
```bash
sudo mkdir -p /opt/jellyseerr && cd /opt/jellyseerr
```
2. Clone the Jellyseerr repository and checkout the develop branch:
```bash
git clone https://github.com/Fallenbagel/jellyseerr.git
cd jellyseerr
git checkout develop # by default, you are on the develop branch so this step is not necessary
```
3. Install the dependencies:
```bash
CYPRESS_INSTALL_BINARY=0 pnpm install --frozen-lockfile
```
4. Build the project:
```bash
pnpm build
```
5. Start Jellyseerr:
```bash
pnpm start
```
@@ -46,6 +60,7 @@ You can now access Jellyseerr by visiting `http://localhost:5055` in your web br
:::
#### Extending the installation
<Tabs groupId="unix-extensions" queryString>
<TabItem value="linux" label="Linux">
To run jellyseerr as a systemd service:
@@ -56,21 +71,23 @@ To run jellyseerr as a systemd service:
PORT=5055
## specify on which interface to listen, by default jellyseerr listens on all interfaces
#HOST=127.0.0.1
## Uncomment if your media server is emby instead of jellyfin.
# 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:
```bash
which node
```
````
Copy the path to node, it should be something like `/usr/bin/node`.
3. Create the systemd service file at `/etc/systemd/system/jellyseerr.service`, using either `sudo systemctl edit jellyseerr` or `sudo nano /etc/systemd/system/jellyseerr.service`:
```bash
[Unit]
Description=Jellyseerr Service
@@ -88,15 +105,18 @@ ExecStart=/usr/bin/node dist/index.js
[Install]
WantedBy=multi-user.target
```
:::note
If you are using a different path to node, replace `/usr/bin/node` with the path to node.
:::
4. Enable and start the service:
```bash
sudo systemctl enable jellyseerr
sudo systemctl start jellyseerr
```
</TabItem>
<TabItem value="macos" label="macOS">
To run jellyseerr as a launchd service:
@@ -107,6 +127,7 @@ which node
Copy the path to node, it should be something like `/usr/local/bin/node`.
2. Create a launchd plist file at `~/Library/LaunchAgents/com.jellyseerr.plist`:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -135,21 +156,27 @@ Copy the path to node, it should be something like `/usr/local/bin/node`.
</dict>
</plist>
```
:::note
If you are using a different path to node, replace `/usr/local/bin/node` with the path to node.
:::
3. Load the service:
::: 3. Load the service:
```bash
sudo launchctl load ~/Library/LaunchAgents/com.jellyseerr.plist
```
3. Start the service:
```bash
sudo launchctl start com.jellyseerr
```
4. To ensure the service starts on boot, run the following command:
```bash
sudo lauchctl load
```
</TabItem>
<TabItem value="pm2" label="PM2">
To run jellyseerr as a PM2 service:
@@ -194,27 +221,38 @@ pm2 status jellyseerr
</Tabs>
## Windows
### Installation
1. Assuming you want the working directory to be `C:\jellyseerr`, create the directory and navigate to it:
```powershell
mkdir C:\jellyseerr
cd C:\jellyseerr
```
2. Clone the Jellyseerr repository and checkout the develop branch:
```powershell
git clone https://github.com/Fallenbagel/jellyseerr.git .
git checkout develop # by default, you are on the develop branch so this step is not necessary
```
3. Install the dependencies:
```powershell
npm install -g win-node-env
set CYPRESS_INSTALL_BINARY=0 && pnpm install --frozen-lockfile
```
4. Build the project:
```powershell
pnpm build
```
5. Start Jellyseerr:
```powershell
pnpm start
```
@@ -228,6 +266,7 @@ You can now access Jellyseerr by visiting `http://localhost:5055` in your web br
:::
#### Extending the installation
<Tabs groupId="windows-extensions" queryString>
<TabItem value="task-scheduler" label="Task Scheduler">
To run jellyseerr as a bat script:
@@ -249,6 +288,7 @@ node dist/index.js
- Click "Finish"
Now, Jellyseerr will start when the computer boots up in the background.
</TabItem>
<TabItem value="nssm" label="NSSM">
@@ -311,9 +351,11 @@ pm2 status jellyseerr
</Tabs>
### Updating
To update Jellyseerr, navigate to the Jellyseerr directory and run the following commands:
```bash
git pull
```
Then, follow the steps in the installation section to rebuild and restart Jellyseerr.
Then, follow the steps in the installation section to rebuild and restart Jellyseerr.

2
next-env.d.ts vendored
View File

@@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.

View File

@@ -4,7 +4,6 @@
module.exports = {
env: {
commitTag: process.env.COMMIT_TAG || 'local',
forceIpv4First: process.env.FORCE_IPV4_FIRST === 'true' ? 'true' : 'false',
},
images: {
remotePatterns: [

View File

@@ -43,6 +43,8 @@
"@svgr/webpack": "6.5.1",
"@tanem/react-nprogress": "5.0.30",
"ace-builds": "1.15.2",
"axios": "1.3.4",
"axios-rate-limit": "1.3.0",
"bcrypt": "5.1.0",
"bowser": "2.11.0",
"connect-typeorm": "1.1.4",
@@ -60,6 +62,7 @@
"express-rate-limit": "6.7.0",
"express-session": "1.17.3",
"formik": "^2.4.6",
"global-agent": "^3.0.0",
"gravatar-url": "3.1.0",
"lodash": "4.17.21",
"mime": "3",

8126
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,5 +1,6 @@
import type { RateLimitOptions } from '@server/utils/rateLimit';
import rateLimit from '@server/utils/rateLimit';
import type { AxiosInstance, AxiosRequestConfig } from 'axios';
import axios from 'axios';
import rateLimit from 'axios-rate-limit';
import type NodeCache from 'node-cache';
// 5 minute default TTL (in seconds)
@@ -11,101 +12,71 @@ const DEFAULT_ROLLING_BUFFER = 10000;
interface ExternalAPIOptions {
nodeCache?: NodeCache;
headers?: Record<string, unknown>;
rateLimit?: RateLimitOptions;
rateLimit?: {
maxRPS: number;
maxRequests: number;
};
}
class ExternalAPI {
protected fetch: typeof fetch;
protected params: Record<string, string>;
protected defaultHeaders: { [key: string]: string };
protected axios: AxiosInstance;
private baseUrl: string;
private cache?: NodeCache;
constructor(
baseUrl: string,
params: Record<string, string> = {},
params: Record<string, unknown>,
options: ExternalAPIOptions = {}
) {
this.axios = axios.create({
baseURL: baseUrl,
params,
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
...options.headers,
},
});
if (options.rateLimit) {
this.fetch = rateLimit(fetch, options.rateLimit);
} else {
this.fetch = fetch;
}
const url = new URL(baseUrl);
this.defaultHeaders = {
'Content-Type': 'application/json',
Accept: 'application/json',
...((url.username || url.password) && {
Authorization: `Basic ${Buffer.from(
`${url.username}:${url.password}`
).toString('base64')}`,
}),
...options.headers,
};
if (url.username || url.password) {
url.username = '';
url.password = '';
baseUrl = url.toString();
this.axios = rateLimit(this.axios, {
maxRequests: options.rateLimit.maxRequests,
maxRPS: options.rateLimit.maxRPS,
});
}
this.baseUrl = baseUrl;
this.params = params;
this.cache = options.nodeCache;
}
protected async get<T>(
endpoint: string,
params?: Record<string, string>,
ttl?: number,
config?: RequestInit
config?: AxiosRequestConfig,
ttl?: number
): Promise<T> {
const cacheKey = this.serializeCacheKey(endpoint, {
...this.params,
...params,
});
const cacheKey = this.serializeCacheKey(endpoint, config?.params);
const cachedItem = this.cache?.get<T>(cacheKey);
if (cachedItem) {
return cachedItem;
}
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 : ''}`,
{
cause: response,
}
);
}
const data = await this.getDataFromResponse(response);
const response = await this.axios.get<T>(endpoint, config);
if (this.cache && ttl !== 0) {
this.cache.set(cacheKey, data, ttl ?? DEFAULT_TTL);
if (this.cache) {
this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
}
return data;
return response.data;
}
protected async post<T>(
endpoint: string,
data?: Record<string, unknown>,
params?: Record<string, string>,
ttl?: number,
config?: RequestInit
config?: AxiosRequestConfig,
ttl?: number
): Promise<T> {
const cacheKey = this.serializeCacheKey(endpoint, {
config: { ...this.params, ...params },
config: config?.params,
data,
});
const cachedItem = this.cache?.get<T>(cacheKey);
@@ -113,117 +84,21 @@ class ExternalAPI {
return cachedItem;
}
const url = this.formatUrl(endpoint, params);
const response = await this.fetch(url, {
method: 'POST',
...config,
headers: {
...this.defaultHeaders,
...config?.headers,
},
body: data ? JSON.stringify(data) : undefined,
});
if (!response.ok) {
const text = await response.text();
throw new Error(
`${response.status} ${response.statusText}${text ? ': ' + text : ''}`,
{
cause: response,
}
);
}
const resData = await this.getDataFromResponse(response);
const response = await this.axios.post<T>(endpoint, data, config);
if (this.cache && ttl !== 0) {
this.cache.set(cacheKey, resData, ttl ?? DEFAULT_TTL);
if (this.cache) {
this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
}
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),
});
if (!response.ok) {
const text = await response.text();
throw new Error(
`${response.status} ${response.statusText}${text ? ': ' + text : ''}`,
{
cause: response,
}
);
}
const resData = await this.getDataFromResponse(response);
if (this.cache && ttl !== 0) {
this.cache.set(cacheKey, resData, ttl ?? DEFAULT_TTL);
}
return resData;
}
protected async delete<T>(
endpoint: string,
params?: Record<string, string>,
config?: RequestInit
): Promise<T> {
const url = this.formatUrl(endpoint, params);
const response = await this.fetch(url, {
method: 'DELETE',
...config,
headers: {
...this.defaultHeaders,
...config?.headers,
},
});
if (!response.ok) {
const text = await response.text();
throw new Error(
`${response.status} ${response.statusText}${text ? ': ' + text : ''}`,
{
cause: response,
}
);
}
const data = await this.getDataFromResponse(response);
return data;
return response.data;
}
protected async getRolling<T>(
endpoint: string,
params?: Record<string, string>,
ttl?: number,
config?: RequestInit,
overwriteBaseUrl?: string
config?: AxiosRequestConfig,
ttl?: number
): Promise<T> {
const cacheKey = this.serializeCacheKey(endpoint, {
...this.params,
...params,
});
const cacheKey = this.serializeCacheKey(endpoint, config?.params);
const cachedItem = this.cache?.get<T>(cacheKey);
if (cachedItem) {
@@ -234,78 +109,20 @@ class ExternalAPI {
keyTtl - (ttl ?? DEFAULT_TTL) * 1000 <
Date.now() - DEFAULT_ROLLING_BUFFER
) {
const url = this.formatUrl(endpoint, params, overwriteBaseUrl);
this.fetch(url, {
...config,
headers: {
...this.defaultHeaders,
...config?.headers,
},
}).then(async (response) => {
if (!response.ok) {
const text = await response.text();
throw new Error(
`${response.status} ${response.statusText}${
text ? ': ' + text : ''
}`,
{
cause: response,
}
);
}
const data = await this.getDataFromResponse(response);
this.cache?.set(cacheKey, data, ttl ?? DEFAULT_TTL);
this.axios.get<T>(endpoint, config).then((response) => {
this.cache?.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
});
}
return cachedItem;
}
const url = this.formatUrl(endpoint, params, overwriteBaseUrl);
const response = await this.fetch(url, {
...config,
headers: {
...this.defaultHeaders,
...config?.headers,
},
});
if (!response.ok) {
const text = await response.text();
throw new Error(
`${response.status} ${response.statusText}${text ? ': ' + text : ''}`,
{
cause: response,
}
);
}
const data = await this.getDataFromResponse(response);
const response = await this.axios.get<T>(endpoint, config);
if (this.cache) {
this.cache.set(cacheKey, data, ttl ?? DEFAULT_TTL);
this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
}
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().length
? '?' + searchParams.toString()
: searchParams.toString())
);
return response.data;
}
private serializeCacheKey(
@@ -318,29 +135,6 @@ class ExternalAPI {
return `${this.baseUrl}${endpoint}${JSON.stringify(params)}`;
}
private async getDataFromResponse(response: Response) {
const contentType = response.headers.get('Content-Type');
if (contentType?.includes('application/json')) {
return await response.json();
} else if (
contentType?.includes('application/xml') ||
contentType?.includes('text/html') ||
contentType?.includes('text/plain')
) {
return await response.text();
} else {
try {
return await response.json();
} catch {
try {
return await response.blob();
} catch {
return null;
}
}
}
}
}
export default ExternalAPI;

View File

@@ -1,6 +1,6 @@
import ExternalAPI from '@server/api/externalapi';
import cacheManager from '@server/lib/cache';
import logger from '@server/logger';
import ExternalAPI from './externalapi';
interface GitHubRelease {
url: string;
@@ -67,6 +67,10 @@ class GithubAPI extends ExternalAPI {
'https://api.github.com',
{},
{
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
nodeCache: cacheManager.getCache('github').data,
}
);
@@ -81,7 +85,9 @@ class GithubAPI extends ExternalAPI {
const data = await this.get<GitHubRelease[]>(
'/repos/fallenbagel/jellyseerr/releases',
{
per_page: take.toString(),
params: {
per_page: take,
},
}
);
@@ -106,8 +112,10 @@ class GithubAPI extends ExternalAPI {
const data = await this.get<GithubCommit[]>(
'/repos/fallenbagel/jellyseerr/commits',
{
per_page: take.toString(),
branch,
params: {
per_page: take,
branch,
},
}
);

View File

@@ -109,6 +109,8 @@ class JellyfinAPI extends ExternalAPI {
{
headers: {
'X-Emby-Authorization': authHeaderVal,
'Content-Type': 'application/json',
Accept: 'application/json',
},
}
);
@@ -120,7 +122,7 @@ class JellyfinAPI extends ExternalAPI {
ClientIP?: string
): Promise<JellyfinLoginResponse> {
const authenticate = async (useHeaders: boolean) => {
const headers: { [key: string]: string } =
const headers =
useHeaders && ClientIP ? { 'X-Forwarded-For': ClientIP } : {};
return this.post<JellyfinLoginResponse>(
@@ -129,8 +131,6 @@ class JellyfinAPI extends ExternalAPI {
Username,
Pw: Password,
},
{},
undefined,
{ headers }
);
};
@@ -291,16 +291,7 @@ class JellyfinAPI extends ExternalAPI {
public async getLibraryContents(id: string): Promise<JellyfinLibraryItem[]> {
try {
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(
@@ -319,11 +310,7 @@ class JellyfinAPI extends ExternalAPI {
public async getRecentlyAdded(id: string): Promise<JellyfinLibraryItem[]> {
try {
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;
@@ -382,10 +369,7 @@ class JellyfinAPI extends ExternalAPI {
): Promise<JellyfinLibraryItem[]> {
try {
const episodeResponse = await this.get<any>(
`/Shows/${seriesID}/Episodes`,
{
seasonId: seasonID,
}
`/Shows/${seriesID}/Episodes?seasonId=${seasonID}`
);
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 cacheManager from '@server/lib/cache';
import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
import xml2js from 'xml2js';
import ExternalAPI from './externalapi';
interface PlexAccountResponse {
user: PlexUser;
@@ -137,6 +137,8 @@ class PlexTvAPI extends ExternalAPI {
{
headers: {
'X-Plex-Token': authToken,
'Content-Type': 'application/json',
Accept: 'application/json',
},
nodeCache: cacheManager.getCache('plextv').data,
}
@@ -147,11 +149,15 @@ class PlexTvAPI extends ExternalAPI {
public async getDevices(): Promise<PlexDevice[]> {
try {
const devicesResp = await this.get('/api/resources', {
includeHttps: '1',
});
const devicesResp = await this.axios.get(
'/api/resources?includeHttps=1',
{
transformResponse: [],
responseType: 'text',
}
);
const parsedXml = await xml2js.parseStringPromise(
devicesResp as DeviceResponse
devicesResp.data as DeviceResponse
);
return parsedXml?.MediaContainer?.Device?.map((pxml: DeviceResponse) => ({
name: pxml.$.name,
@@ -199,11 +205,11 @@ class PlexTvAPI extends ExternalAPI {
public async getUser(): Promise<PlexUser> {
try {
const account = await this.get<PlexAccountResponse>(
const account = await this.axios.get<PlexAccountResponse>(
'/users/account.json'
);
return account.user;
return account.data.user;
} catch (e) {
logger.error(
`Something went wrong while getting the account from plex.tv: ${e.message}`,
@@ -243,10 +249,13 @@ class PlexTvAPI extends ExternalAPI {
}
public async getUsers(): Promise<UsersResponse> {
const data = await this.get('/api/users');
const response = await this.axios.get('/api/users', {
transformResponse: [],
responseType: 'text',
});
const parsedXml = (await xml2js.parseStringPromise(
data as string
response.data
)) as UsersResponse;
return parsedXml;
}
@@ -261,49 +270,49 @@ class PlexTvAPI extends ExternalAPI {
items: PlexWatchlistItem[];
}> {
try {
const params = new URLSearchParams({
'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()}`,
const response = await this.axios.get<WatchlistResponse>(
'/library/sections/watchlist/all',
{
headers: this.defaultHeaders,
params: {
'X-Plex-Container-Start': offset,
'X-Plex-Container-Size': size,
},
baseURL: 'https://metadata.provider.plex.tv',
}
);
const data = (await response.json()) as WatchlistResponse;
const watchlistDetails = await Promise.all(
(data.MediaContainer.Metadata ?? []).map(async (watchlistItem) => {
const detailedResponse = await this.getRolling<MetadataResponse>(
`/library/metadata/${watchlistItem.ratingKey}`,
{},
undefined,
{},
'https://metadata.provider.plex.tv'
);
(response.data.MediaContainer.Metadata ?? []).map(
async (watchlistItem) => {
const detailedResponse = await this.getRolling<MetadataResponse>(
`/library/metadata/${watchlistItem.ratingKey}`,
{
baseURL: 'https://metadata.provider.plex.tv',
}
);
const metadata = detailedResponse.MediaContainer.Metadata[0];
const metadata = detailedResponse.MediaContainer.Metadata[0];
const tmdbString = metadata.Guid.find((guid) =>
guid.id.startsWith('tmdb')
);
const tvdbString = metadata.Guid.find((guid) =>
guid.id.startsWith('tvdb')
);
const tmdbString = metadata.Guid.find((guid) =>
guid.id.startsWith('tmdb')
);
const tvdbString = metadata.Guid.find((guid) =>
guid.id.startsWith('tvdb')
);
return {
ratingKey: metadata.ratingKey,
// This should always be set? But I guess it also cannot be?
// We will filter out the 0's afterwards
tmdbId: tmdbString ? Number(tmdbString.id.split('//')[1]) : 0,
tvdbId: tvdbString
? Number(tvdbString.id.split('//')[1])
: undefined,
title: metadata.title,
type: metadata.type,
};
})
return {
ratingKey: metadata.ratingKey,
// This should always be set? But I guess it also cannot be?
// We will filter out the 0's afterwards
tmdbId: tmdbString ? Number(tmdbString.id.split('//')[1]) : 0,
tvdbId: tvdbString
? Number(tvdbString.id.split('//')[1])
: undefined,
title: metadata.title,
type: metadata.type,
};
}
)
);
const filteredList = watchlistDetails.filter((detail) => detail.tmdbId);
@@ -311,7 +320,7 @@ class PlexTvAPI extends ExternalAPI {
return {
offset,
size,
totalSize: data.MediaContainer.totalSize,
totalSize: response.data.MediaContainer.totalSize,
items: filteredList,
};
} catch (e) {

View File

@@ -1,4 +1,4 @@
import ExternalAPI from '@server/api/externalapi';
import ExternalAPI from './externalapi';
interface PushoverSoundsResponse {
sounds: {
@@ -26,13 +26,24 @@ export const mapSounds = (sounds: {
class PushoverAPI extends ExternalAPI {
constructor() {
super('https://api.pushover.net/1');
super(
'https://api.pushover.net/1',
{},
{
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
}
);
}
public async getSounds(appToken: string): Promise<PushoverSound[]> {
try {
const data = await this.get<PushoverSoundsResponse>('/sounds.json', {
token: appToken,
params: {
token: appToken,
},
});
return mapSounds(data.sounds);

View File

@@ -155,13 +155,13 @@ export interface IMDBRating {
*/
class IMDBRadarrProxy extends ExternalAPI {
constructor() {
super(
'https://api.radarr.video/v1',
{},
{
nodeCache: cacheManager.getCache('imdb').data,
}
);
super('https://api.radarr.video/v1', {
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
nodeCache: cacheManager.getCache('imdb').data,
});
}
/**

View File

@@ -63,12 +63,15 @@ class RottenTomatoes extends ExternalAPI {
super(
'https://79frdp12pn-dsn.algolia.net/1/indexes/*',
{
'x-algolia-agent': 'Algolia for JavaScript (4.14.3); Browser (lite)',
'x-algolia-agent':
'Algolia%20for%20JavaScript%20(4.14.3)%3B%20Browser%20(lite)',
'x-algolia-api-key': '175588f6e5f8319b27702e4cc4013561',
'x-algolia-application-id': '79FRDP12PN',
},
{
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
'x-algolia-usertoken': settings.clientId,
},
nodeCache: cacheManager.getCache('rt').data,

View File

@@ -113,9 +113,9 @@ class ServarrBase<QueueItemAppendT> extends ExternalAPI {
public getSystemStatus = async (): Promise<SystemStatus> => {
try {
const data = await this.get<SystemStatus>('/system/status');
const response = await this.axios.get<SystemStatus>('/system/status');
return data;
return response.data;
} catch (e) {
throw new Error(
`[${this.apiName}] Failed to retrieve system status: ${e.message}`
@@ -157,15 +157,16 @@ class ServarrBase<QueueItemAppendT> extends ExternalAPI {
public getQueue = async (): Promise<(QueueItem & QueueItemAppendT)[]> => {
try {
const data = await this.get<QueueResponse<QueueItemAppendT>>(
const response = await this.axios.get<QueueResponse<QueueItemAppendT>>(
`/queue`,
{
includeEpisode: 'true',
},
0
params: {
includeEpisode: true,
},
}
);
return data.records;
return response.data.records;
} catch (e) {
throw new Error(
`[${this.apiName}] Failed to retrieve queue: ${e.message}`
@@ -175,9 +176,9 @@ class ServarrBase<QueueItemAppendT> extends ExternalAPI {
public getTags = async (): Promise<Tag[]> => {
try {
const data = await this.get<Tag[]>(`/tag`);
const response = await this.axios.get<Tag[]>(`/tag`);
return data;
return response.data;
} catch (e) {
throw new Error(
`[${this.apiName}] Failed to retrieve tags: ${e.message}`
@@ -187,11 +188,11 @@ class ServarrBase<QueueItemAppendT> extends ExternalAPI {
public createTag = async ({ label }: { label: string }): Promise<Tag> => {
try {
const data = await this.post<Tag>(`/tag`, {
const response = await this.axios.post<Tag>(`/tag`, {
label,
});
return data;
return response.data;
} catch (e) {
throw new Error(`[${this.apiName}] Failed to create tag: ${e.message}`);
}
@@ -206,15 +207,10 @@ class ServarrBase<QueueItemAppendT> extends ExternalAPI {
options: Record<string, unknown>
): Promise<void> {
try {
await this.post(
`/command`,
{
name: commandName,
...options,
},
{},
0
);
await this.axios.post(`/command`, {
name: commandName,
...options,
});
} catch (e) {
throw new Error(`[${this.apiName}] Failed to run command: ${e.message}`);
}

View File

@@ -37,9 +37,9 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
public getMovies = async (): Promise<RadarrMovie[]> => {
try {
const data = await this.get<RadarrMovie[]>('/movie');
const response = await this.axios.get<RadarrMovie[]>('/movie');
return data;
return response.data;
} catch (e) {
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> => {
try {
const data = await this.get<RadarrMovie>(`/movie/${id}`);
const response = await this.axios.get<RadarrMovie>(`/movie/${id}`);
return data;
return response.data;
} catch (e) {
throw new Error(`[Radarr] Failed to retrieve movie: ${e.message}`);
}
@@ -57,15 +57,17 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
public async getMovieByTmdbId(id: number): Promise<RadarrMovie> {
try {
const data = await this.get<RadarrMovie[]>('/movie/lookup', {
term: `tmdb:${id}`,
const response = await this.axios.get<RadarrMovie[]>('/movie/lookup', {
params: {
term: `tmdb:${id}`,
},
});
if (!data[0]) {
if (!response.data[0]) {
throw new Error('Movie not found');
}
return data[0];
return response.data[0];
} catch (e) {
logger.error('Error retrieving movie by TMDB ID', {
label: 'Radarr API',
@@ -95,7 +97,7 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
// movie exists in Radarr but is neither downloaded nor monitored
if (movie.id && !movie.monitored) {
const data = await this.put<RadarrMovie>(`/movie`, {
const response = await this.axios.put<RadarrMovie>(`/movie`, {
...movie,
title: options.title,
qualityProfileId: options.qualityProfileId,
@@ -112,25 +114,25 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
},
});
if (data.monitored) {
if (response.data.monitored) {
logger.info(
'Found existing title in Radarr and set it to monitored.',
{
label: 'Radarr',
movieId: data.id,
movieTitle: data.title,
movieId: response.data.id,
movieTitle: response.data.title,
}
);
logger.debug('Radarr update details', {
label: 'Radarr',
movie: data,
movie: response.data,
});
if (options.searchNow) {
this.searchMovie(data.id);
this.searchMovie(response.data.id);
}
return data;
return response.data;
} else {
logger.error('Failed to update existing movie in Radarr.', {
label: 'Radarr',
@@ -148,7 +150,7 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
return movie;
}
const data = await this.post<RadarrMovie>(`/movie`, {
const response = await this.axios.post<RadarrMovie>(`/movie`, {
title: options.title,
qualityProfileId: options.qualityProfileId,
profileId: options.profileId,
@@ -164,11 +166,11 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
},
});
if (data.id) {
if (response.data.id) {
logger.info('Radarr accepted request', { label: 'Radarr' });
logger.debug('Radarr add details', {
label: 'Radarr',
movie: data,
movie: response.data,
});
} else {
logger.error('Failed to add movie to Radarr', {
@@ -177,7 +179,7 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
});
throw new Error('Failed to add movie to Radarr');
}
return data;
return response.data;
} catch (e) {
let errorData;
try {
@@ -221,9 +223,11 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
public removeMovie = async (movieId: number): Promise<void> => {
try {
const { id, title } = await this.getMovieByTmdbId(movieId);
await this.delete(`/movie/${id}`, {
deleteFiles: 'true',
addImportExclusion: 'false',
await this.axios.delete(`/movie/${id}`, {
params: {
deleteFiles: true,
addImportExclusion: false,
},
});
logger.info(`[Radarr] Removed movie ${title}`);
} catch (e) {

View File

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

View File

@@ -1,7 +1,8 @@
import ExternalAPI from '@server/api/externalapi';
import type { User } from '@server/entity/User';
import type { TautulliSettings } from '@server/lib/settings';
import logger from '@server/logger';
import type { AxiosInstance } from 'axios';
import axios from 'axios';
import { uniqWith } from 'lodash';
export interface TautulliHistoryRecord {
@@ -112,25 +113,25 @@ interface TautulliInfoResponse {
};
}
class TautulliAPI extends ExternalAPI {
class TautulliAPI {
private axios: AxiosInstance;
constructor(settings: TautulliSettings) {
super(
`${settings.useSsl ? 'https' : 'http'}://${settings.hostname}:${
this.axios = axios.create({
baseURL: `${settings.useSsl ? 'https' : 'http'}://${settings.hostname}:${
settings.port
}${settings.urlBase ?? ''}`,
{
apikey: settings.apiKey || '',
}
);
params: { apikey: settings.apiKey },
});
}
public async getInfo(): Promise<TautulliInfo> {
try {
return (
await this.get<TautulliInfoResponse>('/api/v2', {
cmd: 'get_tautulli_info',
await this.axios.get<TautulliInfoResponse>('/api/v2', {
params: { cmd: 'get_tautulli_info' },
})
).response.data;
).data.response.data;
} catch (e) {
logger.error('Something went wrong fetching Tautulli server info', {
label: 'Tautulli API',
@@ -147,12 +148,14 @@ class TautulliAPI extends ExternalAPI {
): Promise<TautulliWatchStats[]> {
try {
return (
await this.get<TautulliWatchStatsResponse>('/api/v2', {
cmd: 'get_item_watch_time_stats',
rating_key: ratingKey,
grouping: '1',
await this.axios.get<TautulliWatchStatsResponse>('/api/v2', {
params: {
cmd: 'get_item_watch_time_stats',
rating_key: ratingKey,
grouping: 1,
},
})
).response.data;
).data.response.data;
} catch (e) {
logger.error(
'Something went wrong fetching media watch stats from Tautulli',
@@ -173,12 +176,14 @@ class TautulliAPI extends ExternalAPI {
): Promise<TautulliWatchUser[]> {
try {
return (
await this.get<TautulliWatchUsersResponse>('/api/v2', {
cmd: 'get_item_user_stats',
rating_key: ratingKey,
grouping: '1',
await this.axios.get<TautulliWatchUsersResponse>('/api/v2', {
params: {
cmd: 'get_item_user_stats',
rating_key: ratingKey,
grouping: 1,
},
})
).response.data;
).data.response.data;
} catch (e) {
logger.error(
'Something went wrong fetching media watch users from Tautulli',
@@ -201,13 +206,15 @@ class TautulliAPI extends ExternalAPI {
}
return (
await this.get<TautulliWatchStatsResponse>('/api/v2', {
cmd: 'get_user_watch_time_stats',
user_id: user.plexId.toString(),
query_days: '0',
grouping: '1',
await this.axios.get<TautulliWatchStatsResponse>('/api/v2', {
params: {
cmd: 'get_user_watch_time_stats',
user_id: user.plexId,
query_days: 0,
grouping: 1,
},
})
).response.data[0];
).data.response.data[0];
} catch (e) {
logger.error(
'Something went wrong fetching user watch stats from Tautulli',
@@ -238,17 +245,19 @@ class TautulliAPI extends ExternalAPI {
while (results.length < 20) {
const tautulliData = (
await this.get<TautulliHistoryResponse>('/api/v2', {
cmd: 'get_history',
grouping: '1',
order_column: 'date',
order_dir: 'desc',
user_id: user.plexId.toString(),
media_type: 'movie,episode',
length: take.toString(),
start: start.toString(),
await this.axios.get<TautulliHistoryResponse>('/api/v2', {
params: {
cmd: 'get_history',
grouping: 1,
order_column: 'date',
order_dir: 'desc',
user_id: user.plexId,
media_type: 'movie,episode',
length: take,
start,
},
})
).response.data.data;
).data.response.data.data;
if (!tautulliData.length) {
return results;

View File

@@ -113,8 +113,8 @@ class TheMovieDb extends ExternalAPI {
{
nodeCache: cacheManager.getCache('tmdb').data,
rateLimit: {
maxRequests: 20,
maxRPS: 50,
id: 'tmdb',
},
}
);
@@ -130,10 +130,7 @@ class TheMovieDb extends ExternalAPI {
}: SearchOptions): Promise<TmdbSearchMultiResponse> => {
try {
const data = await this.get<TmdbSearchMultiResponse>('/search/multi', {
query,
page: page.toString(),
include_adult: includeAdult ? 'true' : 'false',
language,
params: { query, page, include_adult: includeAdult, language },
});
return data;
@@ -156,11 +153,13 @@ class TheMovieDb extends ExternalAPI {
}: SingleSearchOptions): Promise<TmdbSearchMovieResponse> => {
try {
const data = await this.get<TmdbSearchMovieResponse>('/search/movie', {
query,
page: page.toString(),
include_adult: includeAdult ? 'true' : 'false',
language,
primary_release_year: year?.toString() || '',
params: {
query,
page,
include_adult: includeAdult,
language,
primary_release_year: year,
},
});
return data;
@@ -183,11 +182,13 @@ class TheMovieDb extends ExternalAPI {
}: SingleSearchOptions): Promise<TmdbSearchTvResponse> => {
try {
const data = await this.get<TmdbSearchTvResponse>('/search/tv', {
query,
page: page.toString(),
include_adult: includeAdult ? 'true' : 'false',
language,
first_air_date_year: year?.toString() || '',
params: {
query,
page,
include_adult: includeAdult,
language,
first_air_date_year: year,
},
});
return data;
@@ -210,7 +211,7 @@ class TheMovieDb extends ExternalAPI {
}): Promise<TmdbPersonDetails> => {
try {
const data = await this.get<TmdbPersonDetails>(`/person/${personId}`, {
language,
params: { language },
});
return data;
@@ -230,7 +231,7 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbPersonCombinedCredits>(
`/person/${personId}/combined_credits`,
{
language,
params: { language },
}
);
@@ -253,9 +254,11 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbMovieDetails>(
`/movie/${movieId}`,
{
language,
append_to_response:
'credits,external_ids,videos,keywords,release_dates,watch/providers',
params: {
language,
append_to_response:
'credits,external_ids,videos,keywords,release_dates,watch/providers',
},
},
43200
);
@@ -277,9 +280,11 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbTvDetails>(
`/tv/${tvId}`,
{
language,
append_to_response:
'aggregate_credits,credits,external_ids,keywords,videos,content_ratings,watch/providers',
params: {
language,
append_to_response:
'aggregate_credits,credits,external_ids,keywords,videos,content_ratings,watch/providers',
},
},
43200
);
@@ -303,8 +308,10 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbSeasonWithEpisodes>(
`/tv/${tvId}/season/${seasonNumber}`,
{
language: language || '',
append_to_response: 'external_ids',
params: {
language,
append_to_response: 'external_ids',
},
}
);
@@ -327,8 +334,10 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbSearchMovieResponse>(
`/movie/${movieId}/recommendations`,
{
page: page.toString(),
language,
params: {
page,
language,
},
}
);
@@ -351,8 +360,10 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbSearchMovieResponse>(
`/movie/${movieId}/similar`,
{
page: page.toString(),
language,
params: {
page,
language,
},
}
);
@@ -375,8 +386,10 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbSearchMovieResponse>(
`/keyword/${keywordId}/movies`,
{
page: page.toString(),
language,
params: {
page,
language,
},
}
);
@@ -399,8 +412,10 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbSearchTvResponse>(
`/tv/${tvId}/recommendations`,
{
page: page.toString(),
language,
params: {
page,
language,
},
}
);
@@ -423,8 +438,10 @@ class TheMovieDb extends ExternalAPI {
}): Promise<TmdbSearchTvResponse> {
try {
const data = await this.get<TmdbSearchTvResponse>(`/tv/${tvId}/similar`, {
page: page.toString(),
language,
params: {
page,
language,
},
});
return data;
@@ -465,38 +482,40 @@ class TheMovieDb extends ExternalAPI {
.split('T')[0];
const data = await this.get<TmdbSearchMovieResponse>('/discover/movie', {
sort_by: sortBy,
page: page.toString(),
include_adult: includeAdult ? 'true' : 'false',
language,
region: this.region || '',
with_original_language:
originalLanguage && originalLanguage !== 'all'
? originalLanguage
: originalLanguage === 'all'
? ''
: this.originalLanguage || '',
// 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!
'primary_release_date.gte':
!primaryReleaseDateGte && primaryReleaseDateLte
? defaultPastDate
: primaryReleaseDateGte || '',
'primary_release_date.lte':
!primaryReleaseDateLte && primaryReleaseDateGte
? defaultFutureDate
: primaryReleaseDateLte || '',
with_genres: genre || '',
with_companies: studio || '',
with_keywords: keywords || '',
'with_runtime.gte': withRuntimeGte || '',
'with_runtime.lte': withRuntimeLte || '',
'vote_average.gte': voteAverageGte || '',
'vote_average.lte': voteAverageLte || '',
'vote_count.gte': voteCountGte || '',
'vote_count.lte': voteCountLte || '',
watch_region: watchRegion || '',
with_watch_providers: watchProviders || '',
params: {
sort_by: sortBy,
page,
include_adult: includeAdult,
language,
region: this.region,
with_original_language:
originalLanguage && originalLanguage !== 'all'
? originalLanguage
: originalLanguage === 'all'
? undefined
: this.originalLanguage,
// 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!
'primary_release_date.gte':
!primaryReleaseDateGte && primaryReleaseDateLte
? defaultPastDate
: primaryReleaseDateGte,
'primary_release_date.lte':
!primaryReleaseDateLte && primaryReleaseDateGte
? defaultFutureDate
: primaryReleaseDateLte,
with_genres: genre,
with_companies: studio,
with_keywords: keywords,
'with_runtime.gte': withRuntimeGte,
'with_runtime.lte': withRuntimeLte,
'vote_average.gte': voteAverageGte,
'vote_average.lte': voteAverageLte,
'vote_count.gte': voteCountGte,
'vote_count.lte': voteCountLte,
watch_region: watchRegion,
with_watch_providers: watchProviders,
},
});
return data;
@@ -538,41 +557,43 @@ class TheMovieDb extends ExternalAPI {
.split('T')[0];
const data = await this.get<TmdbSearchTvResponse>('/discover/tv', {
sort_by: sortBy,
page: page.toString(),
language,
region: this.region || '',
// 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!
'first_air_date.gte':
!firstAirDateGte && firstAirDateLte
? defaultPastDate
: firstAirDateGte || '',
'first_air_date.lte':
!firstAirDateLte && firstAirDateGte
? defaultFutureDate
: firstAirDateLte || '',
with_original_language:
originalLanguage && originalLanguage !== 'all'
? originalLanguage
: originalLanguage === 'all'
? ''
: this.originalLanguage || '',
include_null_first_air_dates: includeEmptyReleaseDate
? 'true'
: 'false',
with_genres: genre || '',
with_networks: network?.toString() || '',
with_keywords: keywords || '',
'with_runtime.gte': withRuntimeGte || '',
'with_runtime.lte': withRuntimeLte || '',
'vote_average.gte': voteAverageGte || '',
'vote_average.lte': voteAverageLte || '',
'vote_count.gte': voteCountGte || '',
'vote_count.lte': voteCountLte || '',
with_watch_providers: watchProviders || '',
watch_region: watchRegion || '',
with_status: withStatus || '',
params: {
sort_by: sortBy,
page: page.toString(),
language,
region: this.region || '',
// 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!
'first_air_date.gte':
!firstAirDateGte && firstAirDateLte
? defaultPastDate
: firstAirDateGte || '',
'first_air_date.lte':
!firstAirDateLte && firstAirDateGte
? defaultFutureDate
: firstAirDateLte || '',
with_original_language:
originalLanguage && originalLanguage !== 'all'
? originalLanguage
: originalLanguage === 'all'
? ''
: this.originalLanguage || '',
include_null_first_air_dates: includeEmptyReleaseDate
? 'true'
: 'false',
with_genres: genre || '',
with_networks: network?.toString() || '',
with_keywords: keywords || '',
'with_runtime.gte': withRuntimeGte || '',
'with_runtime.lte': withRuntimeLte || '',
'vote_average.gte': voteAverageGte || '',
'vote_average.lte': voteAverageLte || '',
'vote_count.gte': voteCountGte || '',
'vote_count.lte': voteCountLte || '',
with_watch_providers: watchProviders || '',
watch_region: watchRegion || '',
with_status: withStatus || '',
},
});
return data;
@@ -592,10 +613,12 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbUpcomingMoviesResponse>(
'/movie/upcoming',
{
page: page.toString(),
language,
region: this.region || '',
originalLanguage: this.originalLanguage || '',
params: {
page,
language,
region: this.region,
originalLanguage: this.originalLanguage,
},
}
);
@@ -618,9 +641,11 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbSearchMultiResponse>(
`/trending/all/${timeWindow}`,
{
page: page.toString(),
language,
region: this.region || '',
params: {
page,
language,
region: this.region,
},
}
);
@@ -641,7 +666,9 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbSearchMovieResponse>(
`/trending/movie/${timeWindow}`,
{
page: page.toString(),
params: {
page,
},
}
);
@@ -662,7 +689,9 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbSearchTvResponse>(
`/trending/tv/${timeWindow}`,
{
page: page.toString(),
params: {
page,
},
}
);
@@ -691,8 +720,10 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbExternalIdResponse>(
`/find/${externalId}`,
{
external_source: type === 'imdb' ? 'imdb_id' : 'tvdb_id',
language,
params: {
external_source: type === 'imdb' ? 'imdb_id' : 'tvdb_id',
language,
},
}
);
@@ -782,7 +813,9 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbCollection>(
`/collection/${collectionId}`,
{
language,
params: {
language,
},
}
);
@@ -855,7 +888,9 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbGenresResult>(
'/genre/movie/list',
{
language,
params: {
language,
},
},
86400 // 24 hours
);
@@ -867,7 +902,9 @@ class TheMovieDb extends ExternalAPI {
const englishData = await this.get<TmdbGenresResult>(
'/genre/movie/list',
{
language: 'en',
params: {
language: 'en',
},
},
86400 // 24 hours
);
@@ -902,7 +939,9 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbGenresResult>(
'/genre/tv/list',
{
language,
params: {
language,
},
},
86400 // 24 hours
);
@@ -914,7 +953,9 @@ class TheMovieDb extends ExternalAPI {
const englishData = await this.get<TmdbGenresResult>(
'/genre/tv/list',
{
language: 'en',
params: {
language: 'en',
},
},
86400 // 24 hours
);
@@ -969,8 +1010,10 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbKeywordSearchResponse>(
'/search/keyword',
{
query,
page: page.toString(),
params: {
query,
page,
},
},
86400 // 24 hours
);
@@ -992,8 +1035,10 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<TmdbCompanySearchResponse>(
'/search/company',
{
query,
page: page.toString(),
params: {
query,
page,
},
},
86400 // 24 hours
);
@@ -1013,7 +1058,9 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<{ results: TmdbWatchProviderRegion[] }>(
'/watch/providers/regions',
{
language: language ? this.originalLanguage || '' : '',
params: {
language: language ?? this.originalLanguage,
},
},
86400 // 24 hours
);
@@ -1037,8 +1084,10 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<{ results: TmdbWatchProviderDetails[] }>(
'/watch/providers/movie',
{
language: language ? this.originalLanguage || '' : '',
watch_region: watchRegion,
params: {
language: language ?? this.originalLanguage,
watch_region: watchRegion,
},
},
86400 // 24 hours
);
@@ -1062,8 +1111,10 @@ class TheMovieDb extends ExternalAPI {
const data = await this.get<{ results: TmdbWatchProviderDetails[] }>(
'/watch/providers/tv',
{
language: language ? this.originalLanguage || '' : '',
watch_region: watchRegion,
params: {
language: language ?? this.originalLanguage,
watch_region: watchRegion,
},
},
86400 // 24 hours
);

View File

@@ -35,17 +35,10 @@ import * as OpenApiValidator from 'express-openapi-validator';
import type { Store } from 'express-session';
import session from 'express-session';
import next from 'next';
import dns from 'node:dns';
import net from 'node:net';
import path from 'path';
import swaggerUi from 'swagger-ui-express';
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');
logger.info(`Starting Overseerr version ${getAppVersion()}`);

View File

@@ -106,7 +106,7 @@ class DownloadTracker {
{ label: 'Download Tracker' }
);
}
} catch {
} catch (e) {
logger.error(
`Unable to get queue from Radarr server: ${server.name}`,
{

View File

@@ -1,6 +1,6 @@
import logger from '@server/logger';
import type { RateLimitOptions } from '@server/utils/rateLimit';
import rateLimit from '@server/utils/rateLimit';
import axios from 'axios';
import rateLimit, { type rateLimitOptions } from 'axios-rate-limit';
import { createHash } from 'crypto';
import { promises } from 'fs';
import mime from 'mime/lite';
@@ -131,33 +131,29 @@ class ImageProxy {
return 0;
}
private fetch: typeof fetch;
private axios;
private cacheVersion;
private key;
private baseUrl;
private headers: HeadersInit | null = null;
constructor(
key: string,
baseUrl: string,
options: {
cacheVersion?: number;
rateLimitOptions?: RateLimitOptions;
headers?: HeadersInit;
rateLimitOptions?: rateLimitOptions;
headers?: Record<string, unknown>;
} = {}
) {
this.cacheVersion = options.cacheVersion ?? 1;
this.baseUrl = baseUrl;
this.key = key;
this.axios = axios.create({
baseURL: baseUrl,
headers: options.headers,
});
if (options.rateLimitOptions) {
this.fetch = rateLimit(fetch, {
...options.rateLimitOptions,
});
} else {
this.fetch = fetch;
this.axios = rateLimit(this.axios, options.rateLimitOptions);
}
this.headers = options.headers || null;
}
public async getImage(
@@ -249,34 +245,23 @@ class ImageProxy {
): Promise<ImageResponse | null> {
try {
const directory = join(this.getCacheDirectory(), cacheKey);
const href =
this.baseUrl +
(this.baseUrl.length > 0
? this.baseUrl.endsWith('/')
? ''
: '/'
: '') +
(path.startsWith('/') ? path.slice(1) : path);
const response = await this.fetch(href, {
headers: this.headers || undefined,
const response = await this.axios.get(path, {
responseType: 'arraybuffer',
});
if (!response.ok) {
return null;
}
const arrayBuffer = await response.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
const buffer = Buffer.from(response.data, 'binary');
const extension = mime.getExtension(
response.headers.get('content-type') ?? ''
response.headers['Content-Type']?.toString() ?? ''
);
let maxAge = Number(
(response.headers.get('cache-control') ?? '0').split('=')[1]
(response.headers['Cache-Control']?.toString() ?? '0').split('=')[1]
);
if (!maxAge) maxAge = 86400;
const expireAt = Date.now() + maxAge * 1000;
const etag = (response.headers.get('etag') ?? '').replace(/"/g, '');
const etag = (response.headers.etag ?? '').replace(/"/g, '');
await this.writeToCacheDir(
directory,

View File

@@ -4,6 +4,7 @@ import { User } from '@server/entity/User';
import type { NotificationAgentDiscord } from '@server/lib/settings';
import { getSettings, NotificationAgentKey } from '@server/lib/settings';
import logger from '@server/logger';
import axios from 'axios';
import {
hasNotificationType,
Notification,
@@ -291,23 +292,14 @@ class DiscordAgent
}
}
const response = await fetch(settings.options.webhookUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username: settings.options.botUsername
? settings.options.botUsername
: getSettings().main.applicationTitle,
avatar_url: settings.options.botAvatarUrl,
embeds: [this.buildEmbed(type, payload)],
content: userMentions.join(' '),
} as DiscordWebhookPayload),
});
if (!response.ok) {
throw new Error(response.statusText, { cause: response });
}
await axios.post(settings.options.webhookUrl, {
username: settings.options.botUsername
? settings.options.botUsername
: getSettings().main.applicationTitle,
avatar_url: settings.options.botAvatarUrl,
embeds: [this.buildEmbed(type, payload)],
content: userMentions.join(' '),
} as DiscordWebhookPayload);
return true;
} catch (e) {

View File

@@ -2,6 +2,7 @@ import { IssueStatus, IssueTypeName } from '@server/constants/issue';
import type { NotificationAgentGotify } from '@server/lib/settings';
import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
import axios from 'axios';
import { hasNotificationType, Notification } from '..';
import type { NotificationAgent, NotificationPayload } from './agent';
import { BaseAgent } from './agent';
@@ -132,16 +133,7 @@ class GotifyAgent
const endpoint = `${settings.options.url}/message?token=${settings.options.token}`;
const notificationPayload = this.getNotificationPayload(type, payload);
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(notificationPayload),
});
if (!response.ok) {
throw new Error(response.statusText, { cause: response });
}
await axios.post(endpoint, notificationPayload);
return true;
} catch (e) {

View File

@@ -3,6 +3,7 @@ import { MediaStatus } from '@server/constants/media';
import type { NotificationAgentLunaSea } from '@server/lib/settings';
import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
import axios from 'axios';
import { hasNotificationType, Notification } from '..';
import type { NotificationAgent, NotificationPayload } from './agent';
import { BaseAgent } from './agent';
@@ -100,23 +101,19 @@ class LunaSeaAgent
});
try {
const response = await fetch(settings.options.webhookUrl, {
method: 'POST',
headers: settings.options.profileName
await axios.post(
settings.options.webhookUrl,
this.buildPayload(type, payload),
settings.options.profileName
? {
'Content-Type': 'application/json',
headers: {
Authorization: `Basic ${Buffer.from(
`${settings.options.profileName}:`
).toString('base64')}`,
},
}
: {
'Content-Type': 'application/json',
Authorization: `Basic ${Buffer.from(
`${settings.options.profileName}:`
).toString('base64')}`,
},
body: JSON.stringify(this.buildPayload(type, payload)),
});
if (!response.ok) {
throw new Error(response.statusText, { cause: response });
}
: undefined
);
return true;
} catch (e) {

View File

@@ -5,6 +5,7 @@ import { User } from '@server/entity/User';
import type { NotificationAgentPushbullet } from '@server/lib/settings';
import { getSettings, NotificationAgentKey } from '@server/lib/settings';
import logger from '@server/logger';
import axios from 'axios';
import {
hasNotificationType,
Notification,
@@ -122,20 +123,15 @@ class PushbulletAgent
});
try {
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Access-Token': settings.options.accessToken,
},
body: JSON.stringify({
...notificationPayload,
channel_tag: settings.options.channelTag,
}),
});
if (!response.ok) {
throw new Error(response.statusText, { cause: response });
}
await axios.post(
endpoint,
{ ...notificationPayload, channel_tag: settings.options.channelTag },
{
headers: {
'Access-Token': settings.options.accessToken,
},
}
);
} catch (e) {
let errorData;
try {
@@ -174,17 +170,11 @@ class PushbulletAgent
});
try {
const response = await fetch(endpoint, {
method: 'POST',
await axios.post(endpoint, notificationPayload, {
headers: {
'Content-Type': 'application/json',
'Access-Token': payload.notifyUser.settings.pushbulletAccessToken,
},
body: JSON.stringify(notificationPayload),
});
if (!response.ok) {
throw new Error(response.statusText, { cause: response });
}
} catch (e) {
let errorData;
try {
@@ -235,17 +225,11 @@ class PushbulletAgent
});
try {
const response = await fetch(endpoint, {
method: 'POST',
await axios.post(endpoint, notificationPayload, {
headers: {
'Content-Type': 'application/json',
'Access-Token': user.settings.pushbulletAccessToken,
},
body: JSON.stringify(notificationPayload),
});
if (!response.ok) {
throw new Error(response.statusText, { cause: response });
}
} catch (e) {
let errorData;
try {

View File

@@ -5,6 +5,7 @@ import { User } from '@server/entity/User';
import type { NotificationAgentPushover } from '@server/lib/settings';
import { getSettings, NotificationAgentKey } from '@server/lib/settings';
import logger from '@server/logger';
import axios from 'axios';
import {
hasNotificationType,
Notification,
@@ -51,15 +52,12 @@ class PushoverAgent
imageUrl: string
): Promise<Partial<PushoverImagePayload>> {
try {
const response = await fetch(imageUrl);
if (!response.ok) {
throw new Error(response.statusText, { cause: response });
}
const arrayBuffer = await response.arrayBuffer();
const base64 = Buffer.from(arrayBuffer).toString('base64');
const response = await axios.get(imageUrl, {
responseType: 'arraybuffer',
});
const base64 = Buffer.from(response.data, 'binary').toString('base64');
const contentType = (
response.headers.get('Content-Type') ||
response.headers.get('content-type')
response.headers['Content-Type'] || response.headers['content-type']
)?.toString();
return {
@@ -210,21 +208,12 @@ class PushoverAgent
});
try {
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...notificationPayload,
token: settings.options.accessToken,
user: settings.options.userToken,
sound: settings.options.sound,
} as PushoverPayload),
});
if (!response.ok) {
throw new Error(response.statusText, { cause: response });
}
await axios.post(endpoint, {
...notificationPayload,
token: settings.options.accessToken,
user: settings.options.userToken,
sound: settings.options.sound,
} as PushoverPayload);
} catch (e) {
let errorData;
try {
@@ -266,21 +255,12 @@ class PushoverAgent
});
try {
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...notificationPayload,
token: payload.notifyUser.settings.pushoverApplicationToken,
user: payload.notifyUser.settings.pushoverUserKey,
sound: payload.notifyUser.settings.pushoverSound,
} as PushoverPayload),
});
if (!response.ok) {
throw new Error(response.statusText, { cause: response });
}
await axios.post(endpoint, {
...notificationPayload,
token: payload.notifyUser.settings.pushoverApplicationToken,
user: payload.notifyUser.settings.pushoverUserKey,
sound: payload.notifyUser.settings.pushoverSound,
} as PushoverPayload);
} catch (e) {
let errorData;
try {
@@ -332,20 +312,11 @@ class PushoverAgent
});
try {
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...notificationPayload,
token: user.settings.pushoverApplicationToken,
user: user.settings.pushoverUserKey,
} as PushoverPayload),
});
if (!response.ok) {
throw new Error(response.statusText, { cause: response });
}
await axios.post(endpoint, {
...notificationPayload,
token: user.settings.pushoverApplicationToken,
user: user.settings.pushoverUserKey,
} as PushoverPayload);
} catch (e) {
let errorData;
try {

View File

@@ -2,6 +2,7 @@ import { IssueStatus, IssueTypeName } from '@server/constants/issue';
import type { NotificationAgentSlack } from '@server/lib/settings';
import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
import axios from 'axios';
import { hasNotificationType, Notification } from '..';
import type { NotificationAgent, NotificationPayload } from './agent';
import { BaseAgent } from './agent';
@@ -237,16 +238,10 @@ class SlackAgent
subject: payload.subject,
});
try {
const response = await fetch(settings.options.webhookUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(this.buildEmbed(type, payload)),
});
if (!response.ok) {
throw new Error(response.statusText, { cause: response });
}
await axios.post(
settings.options.webhookUrl,
this.buildEmbed(type, payload)
);
return true;
} catch (e) {

View File

@@ -5,6 +5,7 @@ import { User } from '@server/entity/User';
import type { NotificationAgentTelegram } from '@server/lib/settings';
import { getSettings, NotificationAgentKey } from '@server/lib/settings';
import logger from '@server/logger';
import axios from 'axios';
import {
hasNotificationType,
Notification,
@@ -174,20 +175,11 @@ class TelegramAgent
});
try {
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...notificationPayload,
chat_id: settings.options.chatId,
disable_notification: !!settings.options.sendSilently,
} as TelegramMessagePayload | TelegramPhotoPayload),
});
if (!response.ok) {
throw new Error(response.statusText, { cause: response });
}
await axios.post(endpoint, {
...notificationPayload,
chat_id: settings.options.chatId,
disable_notification: !!settings.options.sendSilently,
} as TelegramMessagePayload | TelegramPhotoPayload);
} catch (e) {
let errorData;
try {
@@ -225,21 +217,12 @@ class TelegramAgent
});
try {
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...notificationPayload,
chat_id: payload.notifyUser.settings.telegramChatId,
disable_notification:
!!payload.notifyUser.settings.telegramSendSilently,
} as TelegramMessagePayload | TelegramPhotoPayload),
});
if (!response.ok) {
throw new Error(response.statusText, { cause: response });
}
await axios.post(endpoint, {
...notificationPayload,
chat_id: payload.notifyUser.settings.telegramChatId,
disable_notification:
!!payload.notifyUser.settings.telegramSendSilently,
} as TelegramMessagePayload | TelegramPhotoPayload);
} catch (e) {
let errorData;
try {
@@ -288,20 +271,11 @@ class TelegramAgent
});
try {
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...notificationPayload,
chat_id: user.settings.telegramChatId,
disable_notification: !!user.settings?.telegramSendSilently,
} as TelegramMessagePayload | TelegramPhotoPayload),
});
if (!response.ok) {
throw new Error(response.statusText, { cause: response });
}
await axios.post(endpoint, {
...notificationPayload,
chat_id: user.settings.telegramChatId,
disable_notification: !!user.settings?.telegramSendSilently,
} as TelegramMessagePayload | TelegramPhotoPayload);
} catch (e) {
let errorData;
try {

View File

@@ -3,6 +3,7 @@ import { MediaStatus } from '@server/constants/media';
import type { NotificationAgentWebhook } from '@server/lib/settings';
import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
import axios from 'axios';
import { get } from 'lodash';
import { hasNotificationType, Notification } from '..';
import type { NotificationAgent, NotificationPayload } from './agent';
@@ -177,19 +178,17 @@ class WebhookAgent
});
try {
const response = await fetch(settings.options.webhookUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(settings.options.authHeader
? { Authorization: settings.options.authHeader }
: {}),
},
body: JSON.stringify(this.buildPayload(type, payload)),
});
if (!response.ok) {
throw new Error(response.statusText, { cause: response });
}
await axios.post(
settings.options.webhookUrl,
this.buildPayload(type, payload),
settings.options.authHeader
? {
headers: {
Authorization: settings.options.authHeader,
},
}
: undefined
);
return true;
} catch (e) {

View File

@@ -5,6 +5,7 @@ import { Router } from 'express';
const router = Router();
const tmdbImageProxy = new ImageProxy('tmdb', 'https://image.tmdb.org', {
rateLimitOptions: {
maxRequests: 20,
maxRPS: 50,
},
});

View File

@@ -1,68 +0,0 @@
export type RateLimitOptions = {
maxRPS: number;
id?: string;
};
type RateLimiteState<T extends (...args: Parameters<T>) => Promise<U>, U> = {
queue: {
args: Parameters<T>;
resolve: (value: U) => void;
reject: (reason?: unknown) => void;
}[];
lastTimestamps: number[];
timeout: ReturnType<typeof setTimeout>;
};
const rateLimitById: Record<string, unknown> = {};
/**
* Add a rate limit to a function so it doesn't exceed a maximum number of requests per second. Function calls exceeding the rate will be delayed.
* @param fn The function to rate limit
* @param options.maxRPS Maximum number of Requests Per Second
* @param options.id An ID to share between rate limits, so it uses the same request queue.
* @returns The function with a rate limit
*/
export default function rateLimit<
T extends (...args: Parameters<T>) => Promise<U>,
U
>(fn: T, options: RateLimitOptions): (...args: Parameters<T>) => Promise<U> {
const state: RateLimiteState<T, U> = (rateLimitById[
options.id || ''
] as RateLimiteState<T, U>) || { queue: [], lastTimestamps: [] };
if (options.id) {
rateLimitById[options.id] = state;
}
const processQueue = () => {
// remove old timestamps
state.lastTimestamps = state.lastTimestamps.filter(
(timestamp) => Date.now() - timestamp < 1000
);
if (state.lastTimestamps.length < options.maxRPS) {
// process requests if RPS not exceeded
const item = state.queue.shift();
if (!item) return;
state.lastTimestamps.push(Date.now());
const { args, resolve, reject } = item;
fn(...args)
.then(resolve)
.catch(reject);
processQueue();
} else {
// rerun once the oldest item in queue is older than 1s
if (state.timeout) clearTimeout(state.timeout);
state.timeout = setTimeout(
processQueue,
1000 - (Date.now() - state.lastTimestamps[0])
);
}
};
return (...args: Parameters<T>): Promise<U> => {
return new Promise<U>((resolve, reject) => {
state.queue.push({ args, resolve, reject });
processQueue();
});
};
}

View File

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

View File

@@ -6,6 +6,7 @@ import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { CalendarIcon, TrashIcon, UserIcon } from '@heroicons/react/24/solid';
import type { Blacklist } from '@server/entity/Blacklist';
import axios from 'axios';
import Link from 'next/link';
import { useState } from 'react';
import { useIntl } from 'react-intl';
@@ -35,11 +36,8 @@ const BlacklistBlock = ({
const removeFromBlacklist = async (tmdbId: number, title?: string) => {
setIsUpdating(true);
const res = await fetch('/api/v1/blacklist/' + tmdbId, {
method: 'DELETE',
});
if (res.status === 204) {
try {
await axios.delete('/api/v1/blacklist/' + tmdbId);
addToast(
<span>
{intl.formatMessage(globalMessages.removeFromBlacklistSuccess, {
@@ -49,7 +47,7 @@ const BlacklistBlock = ({
</span>,
{ appearance: 'success', autoDismiss: true }
);
} else {
} catch {
addToast(intl.formatMessage(globalMessages.blacklistError), {
appearance: 'error',
autoDismiss: true,

View File

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

View File

@@ -20,6 +20,7 @@ import {
} from '@heroicons/react/24/solid';
import { DiscoverSliderType } from '@server/constants/discover';
import type DiscoverSlider from '@server/entity/DiscoverSlider';
import axios from 'axios';
import { useRef, useState } from 'react';
import { useDrag, useDrop } from 'react-aria';
import { useIntl } from 'react-intl';
@@ -77,10 +78,7 @@ const DiscoverSliderEdit = ({
const deleteSlider = async () => {
try {
const res = await fetch(`/api/v1/settings/discover/${slider.id}`, {
method: 'DELETE',
});
if (!res.ok) throw new Error();
await axios.delete(`/api/v1/settings/discover/${slider.id}`);
addToast(intl.formatMessage(messages.deletesuccess), {
appearance: 'success',
autoDismiss: true,

View File

@@ -28,6 +28,7 @@ import {
} from '@heroicons/react/24/solid';
import { DiscoverSliderType } from '@server/constants/discover';
import type DiscoverSlider from '@server/entity/DiscoverSlider';
import axios from 'axios';
import { useEffect, useState } from 'react';
import { useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
@@ -75,14 +76,7 @@ const Discover = () => {
const updateSliders = async () => {
try {
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();
await axios.post('/api/v1/settings/discover', sliders);
addToast(intl.formatMessage(messages.updatesuccess), {
appearance: 'success',
@@ -100,10 +94,7 @@ const Discover = () => {
const resetSliders = async () => {
try {
const res = await fetch('/api/v1/settings/discover/reset', {
method: 'GET',
});
if (!res.ok) throw new Error();
await axios.get('/api/v1/settings/discover/reset');
addToast(intl.formatMessage(messages.resetsuccess), {
appearance: 'success',

View File

@@ -6,6 +6,7 @@ import defineMessages from '@app/utils/defineMessages';
import { Menu, Transition } from '@headlessui/react';
import { EllipsisVerticalIcon } from '@heroicons/react/24/solid';
import type { default as IssueCommentType } from '@server/entity/IssueComment';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import Link from 'next/link';
import { Fragment, useState } from 'react';
@@ -48,10 +49,7 @@ const IssueComment = ({
const deleteComment = async () => {
try {
const res = await fetch(`/api/v1/issueComment/${comment.id}`, {
method: 'DELETE',
});
if (!res.ok) throw new Error();
await axios.delete(`/api/v1/issueComment/${comment.id}`);
} catch (e) {
// something went wrong deleting the comment
} finally {
@@ -178,17 +176,9 @@ const IssueComment = ({
<Formik
initialValues={{ newMessage: comment.message }}
onSubmit={async (values) => {
const res = await fetch(
`/api/v1/issueComment/${comment.id}`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ message: values.newMessage }),
}
);
if (!res.ok) throw new Error();
await axios.put(`/api/v1/issueComment/${comment.id}`, {
message: values.newMessage,
});
if (onUpdate) {
onUpdate();

View File

@@ -11,7 +11,7 @@ import useDeepLinks from '@app/hooks/useDeepLinks';
import useSettings from '@app/hooks/useSettings';
import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import ErrorPage from '@app/pages/_error';
import Error from '@app/pages/_error';
import defineMessages from '@app/utils/defineMessages';
import { Transition } from '@headlessui/react';
import {
@@ -27,6 +27,7 @@ import { MediaServerType } from '@server/constants/server';
import type Issue from '@server/entity/Issue';
import type { MovieDetails } from '@server/models/Movie';
import type { TvDetails } from '@server/models/Tv';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import Link from 'next/link';
import { useRouter } from 'next/router';
@@ -112,7 +113,7 @@ const IssueDetails = () => {
}
if (!data || !issueData) {
return <ErrorPage statusCode={404} />;
return <Error statusCode={404} />;
}
const belongsToUser = issueData.createdBy.id === currentUser?.id;
@@ -121,14 +122,9 @@ const IssueDetails = () => {
const editFirstComment = async (newMessage: string) => {
try {
const res = await fetch(`/api/v1/issueComment/${firstComment.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ message: newMessage }),
await axios.put(`/api/v1/issueComment/${firstComment.id}`, {
message: newMessage,
});
if (!res.ok) throw new Error();
addToast(intl.formatMessage(messages.toasteditdescriptionsuccess), {
appearance: 'success',
@@ -145,10 +141,7 @@ const IssueDetails = () => {
const updateIssueStatus = async (newStatus: 'open' | 'resolved') => {
try {
const res = await fetch(`/api/v1/issue/${issueData.id}/${newStatus}`, {
method: 'POST',
});
if (!res.ok) throw new Error();
await axios.post(`/api/v1/issue/${issueData.id}/${newStatus}`);
addToast(intl.formatMessage(messages.toaststatusupdated), {
appearance: 'success',
@@ -165,10 +158,7 @@ const IssueDetails = () => {
const deleteIssue = async () => {
try {
const res = await fetch(`/api/v1/issue/${issueData.id}`, {
method: 'DELETE',
});
if (!res.ok) throw new Error();
await axios.delete(`/api/v1/issue/${issueData.id}`);
addToast(intl.formatMessage(messages.toastissuedeleted), {
appearance: 'success',
@@ -502,17 +492,9 @@ const IssueDetails = () => {
}}
validationSchema={CommentSchema}
onSubmit={async (values, { resetForm }) => {
const res = await fetch(
`/api/v1/issue/${issueData?.id}/comment`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ message: values.message }),
}
);
if (!res.ok) throw new Error();
await axios.post(`/api/v1/issue/${issueData?.id}/comment`, {
message: values.message,
});
revalidateIssue();
resetForm();
}}

View File

@@ -11,6 +11,7 @@ import { MediaStatus } from '@server/constants/media';
import type Issue from '@server/entity/Issue';
import type { MovieDetails } from '@server/models/Movie';
import type { TvDetails } from '@server/models/Tv';
import axios from 'axios';
import { Field, Formik } from 'formik';
import Link from 'next/link';
import { useIntl } from 'react-intl';
@@ -100,22 +101,14 @@ const CreateIssueModal = ({
validationSchema={CreateIssueModalSchema}
onSubmit={async (values) => {
try {
const res = await fetch('/api/v1/issue', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
issueType: values.selectedIssue.issueType,
message: values.message,
mediaId: data?.mediaInfo?.id,
problemSeason: values.problemSeason,
problemEpisode:
values.problemSeason > 0 ? values.problemEpisode : 0,
}),
const newIssue = await axios.post<Issue>('/api/v1/issue', {
issueType: values.selectedIssue.issueType,
message: values.message,
mediaId: data?.mediaInfo?.id,
problemSeason: values.problemSeason,
problemEpisode:
values.problemSeason > 0 ? values.problemEpisode : 0,
});
if (!res.ok) throw new Error();
const newIssue: Issue = await res.json();
if (data) {
addToast(
@@ -126,7 +119,7 @@ const CreateIssueModal = ({
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
})}
</div>
<Link href={`/issues/${newIssue.id}`} legacyBehavior>
<Link href={`/issues/${newIssue.data.id}`} legacyBehavior>
<Button as="a" className="mt-4">
<span>{intl.formatMessage(messages.toastviewissue)}</span>
<ArrowRightCircleIcon />

View File

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

View File

@@ -2,6 +2,7 @@ import Modal from '@app/components/Common/Modal';
import useSettings from '@app/hooks/useSettings';
import defineMessages from '@app/utils/defineMessages';
import { Transition } from '@headlessui/react';
import axios from 'axios';
import { Field, Formik } from 'formik';
import { useIntl } from 'react-intl';
import * as Yup from 'yup';
@@ -57,18 +58,11 @@ const AddEmailModal: React.FC<AddEmailModalProps> = ({
validationSchema={EmailSettingsSchema}
onSubmit={async (values) => {
try {
const res = await fetch('/api/v1/auth/jellyfin', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username: username,
password: password,
email: values.email,
}),
await axios.post('/api/v1/auth/jellyfin', {
username: username,
password: password,
email: values.email,
});
if (!res.ok) throw new Error();
onSave();
} catch (e) {

View File

@@ -5,6 +5,7 @@ import defineMessages from '@app/utils/defineMessages';
import { InformationCircleIcon } from '@heroicons/react/24/solid';
import { ApiErrorCode } from '@server/constants/error';
import { MediaServerType, ServerType } from '@server/constants/server';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import { FormattedMessage, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
@@ -113,24 +114,16 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
// if (serverType !== 'Jellyfin' && serverType !== 'Emby') {
// throw new Error('Invalid serverType'); // You can customize the error message
// }
const res = await fetch('/api/v1/auth/jellyfin', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username: values.username,
password: values.password,
hostname: values.hostname,
port: values.port,
useSsl: values.useSsl,
urlBase: values.urlBase,
email: values.email,
serverType: serverType,
}),
await axios.post('/api/v1/auth/jellyfin', {
username: values.username,
password: values.password,
hostname: values.hostname,
port: values.port,
useSsl: values.useSsl,
urlBase: values.urlBase,
email: values.email,
serverType: serverType,
});
if (!res.ok) throw new Error(res.statusText, { cause: res });
} catch (e) {
let errorData;
try {
@@ -370,18 +363,11 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
validationSchema={LoginSchema}
onSubmit={async (values) => {
try {
const res = await fetch('/api/v1/auth/jellyfin', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username: values.username,
password: values.password,
email: values.username,
}),
await axios.post('/api/v1/auth/jellyfin', {
username: values.username,
password: values.password,
email: values.username,
});
if (!res.ok) throw new Error();
} catch (e) {
toasts.addToast(
intl.formatMessage(

View File

@@ -6,6 +6,7 @@ import {
ArrowLeftOnRectangleIcon,
LifebuoyIcon,
} from '@heroicons/react/24/outline';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import Link from 'next/link';
import { useState } from 'react';
@@ -55,17 +56,10 @@ const LocalLogin = ({ revalidate }: LocalLoginProps) => {
validationSchema={LoginSchema}
onSubmit={async (values) => {
try {
const res = await fetch('/api/v1/auth/local', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: values.email,
password: values.password,
}),
await axios.post('/api/v1/auth/local', {
email: values.email,
password: values.password,
});
if (!res.ok) throw new Error();
} catch (e) {
setLoginError(intl.formatMessage(messages.loginerror));
} finally {

View File

@@ -10,6 +10,7 @@ import defineMessages from '@app/utils/defineMessages';
import { Transition } from '@headlessui/react';
import { XCircleIcon } from '@heroicons/react/24/solid';
import { MediaServerType } from '@server/constants/server';
import axios from 'axios';
import { useRouter } from 'next/dist/client/router';
import Image from 'next/image';
import { useEffect, useState } from 'react';
@@ -41,17 +42,9 @@ const Login = () => {
const login = async () => {
setProcessing(true);
try {
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(res.statusText, { cause: res });
const data = await res.json();
const response = await axios.post('/api/v1/auth/plex', { authToken });
if (data?.id) {
if (response.data?.id) {
revalidate();
}
} catch (e) {

View File

@@ -28,6 +28,7 @@ import type { MediaWatchDataResponse } from '@server/interfaces/api/mediaInterfa
import type { RadarrSettings, SonarrSettings } from '@server/lib/settings';
import type { MovieDetails } from '@server/models/Movie';
import type { TvDetails } from '@server/models/Tv';
import axios from 'axios';
import Link from 'next/link';
import { useIntl } from 'react-intl';
import useSWR from 'swr';
@@ -111,10 +112,7 @@ const ManageSlideOver = ({
const deleteMedia = async () => {
if (data.mediaInfo) {
const res = await fetch(`/api/v1/media/${data.mediaInfo.id}`, {
method: 'DELETE',
});
if (!res.ok) throw new Error();
await axios.delete(`/api/v1/media/${data.mediaInfo.id}`);
revalidate();
onClose();
}
@@ -122,16 +120,8 @@ const ManageSlideOver = ({
const deleteMediaFile = async () => {
if (data.mediaInfo) {
const res1 = await fetch(`/api/v1/media/${data.mediaInfo.id}/file`, {
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();
await axios.delete(`/api/v1/media/${data.mediaInfo.id}/file`);
await axios.delete(`/api/v1/media/${data.mediaInfo.id}`);
revalidate();
onClose();
}
@@ -160,16 +150,9 @@ const ManageSlideOver = ({
const markAvailable = async (is4k = false) => {
if (data.mediaInfo) {
const res = await fetch(`/api/v1/media/${data.mediaInfo?.id}/available`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
is4k,
}),
await axios.post(`/api/v1/media/${data.mediaInfo?.id}/available`, {
is4k,
});
if (!res.ok) throw new Error();
revalidate();
}
};

View File

@@ -52,6 +52,7 @@ import { IssueStatus } from '@server/constants/issue';
import { MediaStatus, MediaType } from '@server/constants/media';
import { MediaServerType } from '@server/constants/server';
import type { MovieDetails as MovieDetailsType } from '@server/models/Movie';
import axios from 'axios';
import { countries } from 'country-flag-icons';
import 'country-flag-icons/3x2/flags.css';
import { uniqBy } from 'lodash';
@@ -313,20 +314,25 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
const onClickWatchlistBtn = async (): Promise<void> => {
setIsUpdating(true);
const res = await fetch('/api/v1/watchlist', {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
try {
const watchlist = await axios.post('/api/v1/watchlist', {
tmdbId: movie?.id,
mediaType: MediaType.MOVIE,
title: movie?.title,
}),
});
});
if (!res.ok) {
if (watchlist.data) {
addToast(
<span>
{intl.formatMessage(messages.watchlistSuccess, {
title: movie?.title,
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
})}
</span>,
{ appearance: 'success', autoDismiss: true }
);
}
} catch {
addToast(intl.formatMessage(messages.watchlistError), {
appearance: 'error',
autoDismiss: true,
@@ -336,20 +342,6 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
return;
}
const data = await res.json();
if (data) {
addToast(
<span>
{intl.formatMessage(messages.watchlistSuccess, {
title: movie?.title,
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
})}
</span>,
{ appearance: 'success', autoDismiss: true }
);
}
setIsUpdating(false);
setToggleWatchlist((prevState) => !prevState);
};
@@ -357,22 +349,17 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
const onClickDeleteWatchlistBtn = async (): Promise<void> => {
setIsUpdating(true);
try {
const res = await fetch(`/api/v1/watchlist/${movie?.id}`, {
method: 'DELETE',
});
if (!res.ok) throw new Error();
await axios.delete(`/api/v1/watchlist/${movie?.id}`);
if (res.status === 204) {
addToast(
<span>
{intl.formatMessage(messages.watchlistDeleted, {
title: movie?.title,
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
})}
</span>,
{ appearance: 'info', autoDismiss: true }
);
}
addToast(
<span>
{intl.formatMessage(messages.watchlistDeleted, {
title: movie?.title,
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
})}
</span>,
{ appearance: 'info', autoDismiss: true }
);
} catch (e) {
addToast(intl.formatMessage(messages.watchlistError), {
appearance: 'error',
@@ -387,21 +374,21 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
const onClickHideItemBtn = async (): Promise<void> => {
setIsBlacklistUpdating(true);
const res = await fetch('/api/v1/blacklist', {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
tmdbId: movie?.id,
mediaType: 'movie',
title: movie?.title,
user: user?.id,
}),
});
try {
await axios.post('/api/v1/blacklist', {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
tmdbId: movie?.id,
mediaType: 'movie',
title: movie?.title,
user: user?.id,
}),
});
if (res.status === 201) {
addToast(
<span>
{intl.formatMessage(globalMessages.blacklistSuccess, {
@@ -413,21 +400,23 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
);
revalidate();
} else if (res.status === 412) {
addToast(
<span>
{intl.formatMessage(globalMessages.blacklistDuplicateError, {
title: movie?.title,
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
})}
</span>,
{ appearance: 'info', autoDismiss: true }
);
} else {
addToast(intl.formatMessage(globalMessages.blacklistError), {
appearance: 'error',
autoDismiss: true,
});
} catch (e) {
if (e?.response?.status === 412) {
addToast(
<span>
{intl.formatMessage(globalMessages.blacklistDuplicateError, {
title: movie?.title,
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
})}
</span>,
{ appearance: 'info', autoDismiss: true }
);
} else {
addToast(intl.formatMessage(globalMessages.blacklistError), {
appearance: 'error',
autoDismiss: true,
});
}
}
setIsBlacklistUpdating(false);

View File

@@ -17,6 +17,7 @@ import {
} from '@heroicons/react/24/solid';
import { MediaRequestStatus } from '@server/constants/media';
import type { MediaRequest } from '@server/entity/MediaRequest';
import axios from 'axios';
import Link from 'next/link';
import { useState } from 'react';
import { useIntl } from 'react-intl';
@@ -52,10 +53,7 @@ const RequestBlock = ({ request, onUpdate }: RequestBlockProps) => {
const updateRequest = async (type: 'approve' | 'decline'): Promise<void> => {
setIsUpdating(true);
const res = await fetch(`/api/v1/request/${request.id}/${type}`, {
method: 'POST',
});
if (!res.ok) throw new Error();
await axios.post(`/api/v1/request/${request.id}/${type}`);
if (onUpdate) {
onUpdate();
@@ -65,10 +63,7 @@ const RequestBlock = ({ request, onUpdate }: RequestBlockProps) => {
const deleteRequest = async () => {
setIsUpdating(true);
const res = await fetch(`/api/v1/request/${request.id}`, {
method: 'DELETE',
});
if (!res.ok) throw new Error();
await axios.delete(`/api/v1/request/${request.id}`);
if (onUpdate) {
onUpdate();

View File

@@ -13,6 +13,7 @@ import {
import { MediaRequestStatus, MediaStatus } from '@server/constants/media';
import type Media from '@server/entity/Media';
import type { MediaRequest } from '@server/entity/MediaRequest';
import axios from 'axios';
import { useMemo, useState } from 'react';
import { useIntl } from 'react-intl';
@@ -93,13 +94,9 @@ const RequestButton = ({
request: MediaRequest,
type: 'approve' | 'decline'
) => {
const res = await fetch(`/api/v1/request/${request.id}/${type}`, {
method: 'POST',
});
if (!res.ok) throw new Error();
const data = await res.json();
const response = await axios.post(`/api/v1/request/${request.id}/${type}`);
if (data) {
if (response) {
onUpdate();
}
};
@@ -114,11 +111,7 @@ const RequestButton = ({
await Promise.all(
requests.map(async (request) => {
const res = await fetch(`/api/v1/request/${request.id}/${type}`, {
method: 'POST',
});
if (!res.ok) throw new Error();
return res.json();
return axios.post(`/api/v1/request/${request.id}/${type}`);
})
);

View File

@@ -22,6 +22,7 @@ import type { MediaRequest } from '@server/entity/MediaRequest';
import type { NonFunctionProperties } from '@server/interfaces/api/common';
import type { MovieDetails } from '@server/models/Movie';
import type { TvDetails } from '@server/models/Tv';
import axios from 'axios';
import Link from 'next/link';
import { useEffect, useState } from 'react';
import { useInView } from 'react-intersection-observer';
@@ -73,10 +74,7 @@ const RequestCardError = ({ requestData }: RequestCardErrorProps) => {
});
const deleteRequest = async () => {
const res = await fetch(`/api/v1/media/${requestData?.media.id}`, {
method: 'DELETE',
});
if (!res.ok) throw new Error();
await axios.delete(`/api/v1/media/${requestData?.media.id}`);
mutate('/api/v1/media?filter=allavailable&take=20&sort=mediaAdded');
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
};
@@ -261,22 +259,15 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
});
const modifyRequest = async (type: 'approve' | 'decline') => {
const res = await fetch(`/api/v1/request/${request.id}/${type}`, {
method: 'POST',
});
if (!res.ok) throw new Error();
const data = await res.json();
const response = await axios.post(`/api/v1/request/${request.id}/${type}`);
if (data) {
if (response) {
revalidate();
}
};
const deleteRequest = async () => {
const res = await fetch(`/api/v1/request/${request.id}`, {
method: 'DELETE',
});
if (!res.ok) throw new Error();
await axios.delete(`/api/v1/request/${request.id}`);
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
};
@@ -284,13 +275,9 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
setRetrying(true);
try {
const res = await fetch(`/api/v1/request/${request.id}/retry`, {
method: 'POST',
});
if (!res.ok) throw new Error();
const data = await res.json();
const response = await axios.post(`/api/v1/request/${request.id}/retry`);
if (data) {
if (response) {
revalidate();
}
} catch (e) {

View File

@@ -21,6 +21,7 @@ import type { MediaRequest } from '@server/entity/MediaRequest';
import type { NonFunctionProperties } from '@server/interfaces/api/common';
import type { MovieDetails } from '@server/models/Movie';
import type { TvDetails } from '@server/models/Tv';
import axios from 'axios';
import Link from 'next/link';
import { useState } from 'react';
import { useInView } from 'react-intersection-observer';
@@ -63,10 +64,7 @@ const RequestItemError = ({
const { hasPermission } = useUser();
const deleteRequest = async () => {
const res = await fetch(`/api/v1/media/${requestData?.media.id}`, {
method: 'DELETE',
});
if (!res.ok) throw new Error();
await axios.delete(`/api/v1/media/${requestData?.media.id}`);
revalidateList();
};
@@ -324,34 +322,27 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
const [isRetrying, setRetrying] = useState(false);
const modifyRequest = async (type: 'approve' | 'decline') => {
const res = await fetch(`/api/v1/request/${request.id}/${type}`, {
method: 'POST',
});
if (!res.ok) throw new Error();
const data = await res.json();
const response = await axios.post(`/api/v1/request/${request.id}/${type}`);
if (data) {
if (response) {
revalidate();
}
};
const deleteRequest = async () => {
const res = await fetch(`/api/v1/request/${request.id}`, {
method: 'DELETE',
});
if (!res.ok) throw new Error();
await axios.delete(`/api/v1/request/${request.id}`);
revalidateList();
};
const deleteMediaFile = async () => {
if (request.media) {
await fetch(`/api/v1/media/${request.media.id}/file`, {
method: 'DELETE',
});
await fetch(`/api/v1/media/${request.media.id}`, {
method: 'DELETE',
});
try {
await axios.delete(`/api/v1/media/${request.media.id}/file`);
await axios.delete(`/api/v1/media/${request.media.id}`);
} catch {
/* empty */
}
revalidateList();
}
};
@@ -360,12 +351,7 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
setRetrying(true);
try {
const res = await fetch(`/api/v1/request/${request.id}/retry`, {
method: 'POST',
});
if (!res.ok) throw new Error();
const result = await res.json();
const result = await axios.post(`/api/v1/request/${request.id}/retry`);
revalidate(result.data);
} catch (e) {
addToast(intl.formatMessage(messages.failedretry), {

View File

@@ -13,6 +13,7 @@ import type { MediaRequest } from '@server/entity/MediaRequest';
import type { QuotaResponse } from '@server/interfaces/api/userInterfaces';
import { Permission } from '@server/lib/permissions';
import type { Collection } from '@server/models/Collection';
import axios from 'axios';
import { useCallback, useEffect, useState } from 'react';
import { useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
@@ -198,19 +199,12 @@ const CollectionRequestModal = ({
(
data?.parts.filter((part) => selectedParts.includes(part.id)) ?? []
).map(async (part) => {
const res = await fetch('/api/v1/request', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
mediaId: part.id,
mediaType: 'movie',
is4k,
...overrideParams,
}),
await axios.post<MediaRequest>('/api/v1/request', {
mediaId: part.id,
mediaType: 'movie',
is4k,
...overrideParams,
});
if (!res.ok) throw new Error();
})
);

View File

@@ -12,6 +12,7 @@ import type { NonFunctionProperties } from '@server/interfaces/api/common';
import type { QuotaResponse } from '@server/interfaces/api/userInterfaces';
import { Permission } from '@server/lib/permissions';
import type { MovieDetails } from '@server/models/Movie';
import axios from 'axios';
import { useCallback, useEffect, useState } from 'react';
import { useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
@@ -89,23 +90,15 @@ const MovieRequestModal = ({
tags: requestOverrides.tags,
};
}
const res = await fetch('/api/v1/request', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
mediaId: data?.id,
mediaType: 'movie',
is4k,
...overrideParams,
}),
const response = await axios.post<MediaRequest>('/api/v1/request', {
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');
if (mediaRequest) {
if (response.data) {
if (onComplete) {
onComplete(
hasPermission(
@@ -144,14 +137,12 @@ const MovieRequestModal = ({
setIsUpdating(true);
try {
const res = await fetch(`/api/v1/request/${editRequest?.id}`, {
method: 'DELETE',
});
if (!res.ok) throw new Error();
const response = await axios.delete<MediaRequest>(
`/api/v1/request/${editRequest?.id}`
);
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
if (res.status === 204) {
if (response.status === 204) {
if (onComplete) {
onComplete(MediaStatus.UNKNOWN);
}
@@ -174,27 +165,17 @@ const MovieRequestModal = ({
setIsUpdating(true);
try {
const res = await fetch(`/api/v1/request/${editRequest?.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
mediaType: 'movie',
serverId: requestOverrides?.server,
profileId: requestOverrides?.profile,
rootFolder: requestOverrides?.folder,
userId: requestOverrides?.user?.id,
tags: requestOverrides?.tags,
}),
await axios.put(`/api/v1/request/${editRequest?.id}`, {
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) {
const res = await fetch(`/api/v1/request/${editRequest?.id}/approve`, {
method: 'POST',
});
if (!res.ok) throw new Error();
await axios.post(`/api/v1/request/${editRequest?.id}/approve`);
}
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');

View File

@@ -17,6 +17,7 @@ import type { NonFunctionProperties } from '@server/interfaces/api/common';
import type { QuotaResponse } from '@server/interfaces/api/userInterfaces';
import { Permission } from '@server/lib/permissions';
import type { TvDetails } from '@server/models/Tv';
import axios from 'axios';
import { useState } from 'react';
import { useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
@@ -111,35 +112,22 @@ const TvRequestModal = ({
try {
if (selectedSeasons.length > 0) {
const res = await fetch(`/api/v1/request/${editRequest.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
mediaType: 'tv',
serverId: requestOverrides?.server,
profileId: requestOverrides?.profile,
rootFolder: requestOverrides?.folder,
languageProfileId: requestOverrides?.language,
userId: requestOverrides?.user?.id,
tags: requestOverrides?.tags,
seasons: selectedSeasons,
}),
await axios.put(`/api/v1/request/${editRequest.id}`, {
mediaType: 'tv',
serverId: requestOverrides?.server,
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) {
const res = await fetch(`/api/v1/request/${editRequest.id}/approve`, {
method: 'POST',
});
if (!res.ok) throw new Error();
await axios.post(`/api/v1/request/${editRequest.id}/approve`);
}
} else {
const res = await fetch(`/api/v1/request/${editRequest.id}`, {
method: 'DELETE',
});
if (!res.ok) throw new Error();
await axios.delete(`/api/v1/request/${editRequest.id}`);
}
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
@@ -204,32 +192,23 @@ const TvRequestModal = ({
tags: requestOverrides.tags,
};
}
const res = await fetch('/api/v1/request', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
mediaId: data?.id,
tvdbId: tvdbId ?? data?.externalIds.tvdbId,
mediaType: 'tv',
is4k,
seasons: settings.currentSettings.partialRequestsEnabled
? selectedSeasons
: getAllSeasons().filter(
(season) => !getAllRequestedSeasons().includes(season)
),
...overrideParams,
}),
const response = await axios.post<MediaRequest>('/api/v1/request', {
mediaId: data?.id,
tvdbId: tvdbId ?? data?.externalIds.tvdbId,
mediaType: 'tv',
is4k,
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');
if (mediaRequest) {
if (response.data) {
if (onComplete) {
onComplete(mediaRequest.media.status);
onComplete(response.data.media.status);
}
addToast(
<span>

View File

@@ -4,6 +4,7 @@ import PageTitle from '@app/components/Common/PageTitle';
import LanguagePicker from '@app/components/Layout/LanguagePicker';
import defineMessages from '@app/utils/defineMessages';
import { ArrowLeftIcon, EnvelopeIcon } from '@heroicons/react/24/solid';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import Image from 'next/image';
import Link from 'next/link';
@@ -84,18 +85,14 @@ const ResetPassword = () => {
}}
validationSchema={ResetSchema}
onSubmit={async (values) => {
const res = await fetch(`/api/v1/auth/reset-password`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
const response = await axios.post(
`/api/v1/auth/reset-password`,
{
email: values.email,
}),
});
if (!res.ok) throw new Error();
}
);
if (res.status === 200) {
if (response.status === 200) {
setSubmitted(true);
}
}}

View File

@@ -5,6 +5,7 @@ import LanguagePicker from '@app/components/Layout/LanguagePicker';
import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { LifebuoyIcon } from '@heroicons/react/24/outline';
import axios from 'axios';
import { Form, Formik } from 'formik';
import Image from 'next/image';
import Link from 'next/link';
@@ -99,21 +100,14 @@ const ResetPassword = () => {
}}
validationSchema={ResetSchema}
onSubmit={async (values) => {
const res = await fetch(
const response = await axios.post(
`/api/v1/auth/reset-password/${guid}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
password: values.password,
}),
password: values.password,
}
);
if (!res.ok) throw new Error();
if (res.status === 200) {
if (response.status === 200) {
setSubmitted(true);
}
}}

View File

@@ -18,6 +18,7 @@ import type {
ProductionCompany,
WatchProviderDetails,
} from '@server/models/common';
import axios from 'axios';
import { orderBy } from 'lodash';
import { useEffect, useMemo, useState } from 'react';
import { useIntl } from 'react-intl';
@@ -75,9 +76,11 @@ export const CompanySelector = ({
return;
}
const res = await fetch(`/api/v1/studio/${defaultValue}`);
if (!res.ok) throw new Error();
const studio: ProductionCompany = await res.json();
const response = await axios.get<ProductionCompany>(
`/api/v1/studio/${defaultValue}`
);
const studio = response.data;
setDefaultDataValue([
{
@@ -95,15 +98,16 @@ export const CompanySelector = ({
return [];
}
const res = await fetch(
`/api/v1/search/company?query=${encodeURIExtraParams(inputValue)}`
const results = await axios.get<TmdbCompanySearchResponse>(
'/api/v1/search/company',
{
params: {
query: encodeURIExtraParams(inputValue),
},
}
);
if (!res.ok) {
throw new Error('Network response was not ok');
}
const results: TmdbCompanySearchResponse = await res.json();
return results.results.map((result) => ({
return results.data.results.map((result) => ({
label: result.name,
value: result.id,
}));
@@ -157,15 +161,11 @@ export const GenreSelector = ({
const genres = defaultValue.split(',');
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 response = await axios.get<TmdbGenre[]>(`/api/v1/genres/${type}`);
const genreData = genres
.filter((genre) => response.find((gd) => gd.id === Number(genre)))
.map((g) => response.find((gd) => gd.id === Number(g)))
.filter((genre) => response.data.find((gd) => gd.id === Number(genre)))
.map((g) => response.data.find((gd) => gd.id === Number(g)))
.map((g) => ({
label: g?.name ?? '',
value: g?.id ?? 0,
@@ -178,11 +178,11 @@ export const GenreSelector = ({
}, [defaultValue, type]);
const loadGenreOptions = async (inputValue: string) => {
const res = await fetch(`/api/v1/discover/genreslider/${type}`);
if (!res.ok) throw new Error();
const results: GenreSliderItem[] = await res.json();
const results = await axios.get<GenreSliderItem[]>(
`/api/v1/discover/genreslider/${type}`
);
return results
return results.data
.map((result) => ({
label: result.name,
value: result.id,
@@ -298,13 +298,11 @@ export const KeywordSelector = ({
const keywords = await Promise.all(
defaultValue.split(',').map(async (keywordId) => {
const res = await fetch(`/api/v1/keyword/${keywordId}`);
if (!res.ok) {
throw new Error('Network response was not ok');
}
const keyword: Keyword = await res.json();
const keyword = await axios.get<Keyword>(
`/api/v1/keyword/${keywordId}`
);
return keyword;
return keyword.data;
})
);
@@ -320,15 +318,16 @@ export const KeywordSelector = ({
}, [defaultValue]);
const loadKeywordOptions = async (inputValue: string) => {
const res = await fetch(
`/api/v1/search/keyword?query=${encodeURIExtraParams(inputValue)}`
const results = await axios.get<TmdbKeywordSearchResponse>(
'/api/v1/search/keyword',
{
params: {
query: encodeURIExtraParams(inputValue),
},
}
);
if (!res.ok) {
throw new Error('Network response was not ok');
}
const results: TmdbKeywordSearchResponse = await res.json();
return results.results.map((result) => ({
return results.data.results.map((result) => ({
label: result.name,
value: result.id,
}));

View File

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

View File

@@ -5,6 +5,7 @@ import useSettings from '@app/hooks/useSettings';
import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import { useState } from 'react';
import { useIntl } from 'react-intl';
@@ -72,23 +73,16 @@ const NotificationsDiscord = () => {
validationSchema={NotificationsDiscordSchema}
onSubmit={async (values) => {
try {
const res = await fetch('/api/v1/settings/notifications/discord', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
await axios.post('/api/v1/settings/notifications/discord', {
enabled: values.enabled,
types: values.types,
options: {
botUsername: values.botUsername,
botAvatarUrl: values.botAvatarUrl,
webhookUrl: values.webhookUrl,
enableMentions: values.enableMentions,
},
body: JSON.stringify({
enabled: values.enabled,
types: values.types,
options: {
botUsername: values.botUsername,
botAvatarUrl: values.botAvatarUrl,
webhookUrl: values.webhookUrl,
enableMentions: values.enableMentions,
},
}),
});
if (!res.ok) throw new Error();
addToast(intl.formatMessage(messages.discordsettingssaved), {
appearance: 'success',
@@ -127,26 +121,16 @@ const NotificationsDiscord = () => {
toastId = id;
}
);
const res = await fetch(
'/api/v1/settings/notifications/discord/test',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
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();
await axios.post('/api/v1/settings/notifications/discord/test', {
enabled: true,
types: values.types,
options: {
botUsername: values.botUsername,
botAvatarUrl: values.botAvatarUrl,
webhookUrl: values.webhookUrl,
enableMentions: values.enableMentions,
},
});
if (toastId) {
removeToast(toastId);

View File

@@ -5,6 +5,7 @@ import SettingsBadge from '@app/components/Settings/SettingsBadge';
import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import { useState } from 'react';
import { useIntl } from 'react-intl';
@@ -147,31 +148,24 @@ const NotificationsEmail = () => {
validationSchema={NotificationsEmailSchema}
onSubmit={async (values) => {
try {
const res = await fetch('/api/v1/settings/notifications/email', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
await axios.post('/api/v1/settings/notifications/email', {
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,
},
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');
addToast(intl.formatMessage(messages.emailsettingssaved), {
@@ -203,32 +197,22 @@ const NotificationsEmail = () => {
toastId = id;
}
);
const res = await fetch(
'/api/v1/settings/notifications/email/test',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
enabled: true,
options: {
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,
senderName: values.senderName,
pgpPrivateKey: values.pgpPrivateKey,
pgpPassword: values.pgpPassword,
},
}),
}
);
if (!res.ok) throw new Error();
await axios.post('/api/v1/settings/notifications/email/test', {
enabled: true,
options: {
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,
senderName: values.senderName,
pgpPrivateKey: values.pgpPrivateKey,
pgpPassword: values.pgpPassword,
},
});
if (toastId) {
removeToast(toastId);

View File

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

View File

@@ -4,6 +4,7 @@ import NotificationTypeSelector from '@app/components/NotificationTypeSelector';
import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import { useState } from 'react';
import { useIntl } from 'react-intl';
@@ -68,21 +69,14 @@ const NotificationsLunaSea = () => {
validationSchema={NotificationsLunaSeaSchema}
onSubmit={async (values) => {
try {
const res = await fetch('/api/v1/settings/notifications/lunasea', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
await axios.post('/api/v1/settings/notifications/lunasea', {
enabled: values.enabled,
types: values.types,
options: {
webhookUrl: values.webhookUrl,
profileName: values.profileName,
},
body: JSON.stringify({
enabled: values.enabled,
types: values.types,
options: {
webhookUrl: values.webhookUrl,
profileName: values.profileName,
},
}),
});
if (!res.ok) throw new Error();
addToast(intl.formatMessage(messages.settingsSaved), {
appearance: 'success',
autoDismiss: true,
@@ -120,24 +114,14 @@ const NotificationsLunaSea = () => {
toastId = id;
}
);
const res = await fetch(
'/api/v1/settings/notifications/lunasea/test',
{
method: 'POST',
headers: {
'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();
await axios.post('/api/v1/settings/notifications/lunasea/test', {
enabled: true,
types: values.types,
options: {
webhookUrl: values.webhookUrl,
profileName: values.profileName,
},
});
if (toastId) {
removeToast(toastId);

View File

@@ -5,6 +5,7 @@ import NotificationTypeSelector from '@app/components/NotificationTypeSelector';
import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import { useState } from 'react';
import { useIntl } from 'react-intl';
@@ -67,21 +68,14 @@ const NotificationsPushbullet = () => {
validationSchema={NotificationsPushbulletSchema}
onSubmit={async (values) => {
try {
const res = await fetch('/api/v1/settings/notifications/pushbullet', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
await axios.post('/api/v1/settings/notifications/pushbullet', {
enabled: values.enabled,
types: values.types,
options: {
accessToken: values.accessToken,
channelTag: values.channelTag,
},
body: JSON.stringify({
enabled: values.enabled,
types: values.types,
options: {
accessToken: values.accessToken,
channelTag: values.channelTag,
},
}),
});
if (!res.ok) throw new Error();
addToast(intl.formatMessage(messages.pushbulletSettingsSaved), {
appearance: 'success',
autoDismiss: true,
@@ -119,24 +113,14 @@ const NotificationsPushbullet = () => {
toastId = id;
}
);
const res = await fetch(
'/api/v1/settings/notifications/pushbullet/test',
{
method: 'POST',
headers: {
'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();
await axios.post('/api/v1/settings/notifications/pushbullet/test', {
enabled: true,
types: values.types,
options: {
accessToken: values.accessToken,
channelTag: values.channelTag,
},
});
if (toastId) {
removeToast(toastId);

View File

@@ -5,6 +5,7 @@ import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline';
import type { PushoverSound } from '@server/api/pushover';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import { useState } from 'react';
import { useIntl } from 'react-intl';
@@ -93,21 +94,14 @@ const NotificationsPushover = () => {
validationSchema={NotificationsPushoverSchema}
onSubmit={async (values) => {
try {
const res = await fetch('/api/v1/settings/notifications/pushover', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
await axios.post('/api/v1/settings/notifications/pushover', {
enabled: values.enabled,
types: values.types,
options: {
accessToken: values.accessToken,
userToken: values.userToken,
},
body: JSON.stringify({
enabled: values.enabled,
types: values.types,
options: {
accessToken: values.accessToken,
userToken: values.userToken,
},
}),
});
if (!res.ok) throw new Error();
addToast(intl.formatMessage(messages.pushoversettingssaved), {
appearance: 'success',
autoDismiss: true,
@@ -145,25 +139,16 @@ const NotificationsPushover = () => {
toastId = id;
}
);
const res = await fetch(
'/api/v1/settings/notifications/pushover/test',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
enabled: true,
types: values.types,
options: {
accessToken: values.accessToken,
userToken: values.userToken,
sound: values.sound,
},
}),
}
);
if (!res.ok) throw new Error();
await axios.post('/api/v1/settings/notifications/pushover/test', {
enabled: true,
types: values.types,
options: {
accessToken: values.accessToken,
userToken: values.userToken,
sound: values.sound,
},
});
if (toastId) {
removeToast(toastId);
}

View File

@@ -4,6 +4,7 @@ import NotificationTypeSelector from '@app/components/NotificationTypeSelector';
import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import { useState } from 'react';
import { useIntl } from 'react-intl';
@@ -64,20 +65,13 @@ const NotificationsSlack = () => {
validationSchema={NotificationsSlackSchema}
onSubmit={async (values) => {
try {
const res = await fetch('/api/v1/settings/notifications/slack', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
await axios.post('/api/v1/settings/notifications/slack', {
enabled: values.enabled,
types: values.types,
options: {
webhookUrl: values.webhookUrl,
},
body: JSON.stringify({
enabled: values.enabled,
types: values.types,
options: {
webhookUrl: values.webhookUrl,
},
}),
});
if (!res.ok) throw new Error();
addToast(intl.formatMessage(messages.slacksettingssaved), {
appearance: 'success',
autoDismiss: true,
@@ -115,23 +109,13 @@ const NotificationsSlack = () => {
toastId = id;
}
);
const res = await fetch(
'/api/v1/settings/notifications/slack/test',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
enabled: true,
types: values.types,
options: {
webhookUrl: values.webhookUrl,
},
}),
}
);
if (!res.ok) throw new Error();
await axios.post('/api/v1/settings/notifications/slack/test', {
enabled: true,
types: values.types,
options: {
webhookUrl: values.webhookUrl,
},
});
if (toastId) {
removeToast(toastId);

View File

@@ -5,6 +5,7 @@ import NotificationTypeSelector from '@app/components/NotificationTypeSelector';
import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import { useState } from 'react';
import { useIntl } from 'react-intl';
@@ -83,23 +84,16 @@ const NotificationsTelegram = () => {
validationSchema={NotificationsTelegramSchema}
onSubmit={async (values) => {
try {
const res = await fetch('/api/v1/settings/notifications/telegram', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
await axios.post('/api/v1/settings/notifications/telegram', {
enabled: values.enabled,
types: values.types,
options: {
botAPI: values.botAPI,
chatId: values.chatId,
sendSilently: values.sendSilently,
botUsername: values.botUsername,
},
body: JSON.stringify({
enabled: values.enabled,
types: values.types,
options: {
botAPI: values.botAPI,
chatId: values.chatId,
sendSilently: values.sendSilently,
botUsername: values.botUsername,
},
}),
});
if (!res.ok) throw new Error();
addToast(intl.formatMessage(messages.telegramsettingssaved), {
appearance: 'success',
@@ -138,26 +132,16 @@ const NotificationsTelegram = () => {
toastId = id;
}
);
const res = await fetch(
'/api/v1/settings/notifications/telegram/test',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
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();
await axios.post('/api/v1/settings/notifications/telegram/test', {
enabled: true,
types: values.types,
options: {
botAPI: values.botAPI,
chatId: values.chatId,
sendSilently: values.sendSilently,
botUsername: values.botUsername,
},
});
if (toastId) {
removeToast(toastId);

View File

@@ -4,6 +4,7 @@ import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import { useEffect, useState } from 'react';
import { useIntl } from 'react-intl';
@@ -57,17 +58,10 @@ const NotificationsWebPush = () => {
}}
onSubmit={async (values) => {
try {
const res = await fetch('/api/v1/settings/notifications/webpush', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
enabled: values.enabled,
options: {},
}),
await axios.post('/api/v1/settings/notifications/webpush', {
enabled: values.enabled,
options: {},
});
if (!res.ok) throw new Error();
mutate('/api/v1/settings/public');
addToast(intl.formatMessage(messages.webpushsettingssaved), {
appearance: 'success',
@@ -98,20 +92,10 @@ const NotificationsWebPush = () => {
toastId = id;
}
);
const res = await fetch(
'/api/v1/settings/notifications/webpush/test',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
enabled: true,
options: {},
}),
}
);
if (!res.ok) throw new Error();
await axios.post('/api/v1/settings/notifications/webpush/test', {
enabled: true,
options: {},
});
if (toastId) {
removeToast(toastId);

View File

@@ -8,6 +8,7 @@ import {
ArrowPathIcon,
QuestionMarkCircleIcon,
} from '@heroicons/react/24/solid';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import dynamic from 'next/dynamic';
import Link from 'next/link';
@@ -149,22 +150,15 @@ const NotificationsWebhook = () => {
validationSchema={NotificationsWebhookSchema}
onSubmit={async (values) => {
try {
const res = await fetch('/api/v1/settings/notifications/webhook', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
await axios.post('/api/v1/settings/notifications/webhook', {
enabled: values.enabled,
types: values.types,
options: {
webhookUrl: values.webhookUrl,
jsonPayload: JSON.stringify(values.jsonPayload),
authHeader: values.authHeader,
},
body: JSON.stringify({
enabled: values.enabled,
types: values.types,
options: {
webhookUrl: values.webhookUrl,
jsonPayload: JSON.stringify(values.jsonPayload),
authHeader: values.authHeader,
},
}),
});
if (!res.ok) throw new Error();
addToast(intl.formatMessage(messages.webhooksettingssaved), {
appearance: 'success',
autoDismiss: true,
@@ -213,25 +207,16 @@ const NotificationsWebhook = () => {
toastId = id;
}
);
const res = await fetch(
'/api/v1/settings/notifications/webhook/test',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
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();
await axios.post('/api/v1/settings/notifications/webhook/test', {
enabled: true,
types: values.types,
options: {
webhookUrl: values.webhookUrl,
jsonPayload: JSON.stringify(values.jsonPayload),
authHeader: values.authHeader,
},
});
if (toastId) {
removeToast(toastId);
}

View File

@@ -4,6 +4,7 @@ import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { Transition } from '@headlessui/react';
import type { RadarrSettings } from '@server/lib/settings';
import axios from 'axios';
import { Field, Formik } from 'formik';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useIntl } from 'react-intl';
@@ -165,24 +166,19 @@ const RadarrModal = ({ onClose, radarr, onSave }: RadarrModalProps) => {
}) => {
setIsTesting(true);
try {
const res = await fetch('/api/v1/settings/radarr/test', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
const response = await axios.post<TestResponse>(
'/api/v1/settings/radarr/test',
{
hostname,
apiKey,
port: Number(port),
baseUrl,
useSsl,
}),
});
if (!res.ok) throw new Error();
const data = await res.json();
}
);
setIsValidated(true);
setTestResponse(data);
setTestResponse(response.data);
if (initialLoad.current) {
addToast(intl.formatMessage(messages.toastRadarrTestSuccess), {
appearance: 'success',
@@ -275,23 +271,12 @@ const RadarrModal = ({ onClose, radarr, onSave }: RadarrModalProps) => {
tagRequests: values.tagRequests,
};
if (!radarr) {
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();
await axios.post('/api/v1/settings/radarr', submission);
} else {
const res = await fetch(`/api/v1/settings/radarr/${radarr.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(submission),
});
if (!res.ok) throw new Error();
await axios.put(
`/api/v1/settings/radarr/${radarr.id}`,
submission
);
}
onSave();

View File

@@ -10,6 +10,7 @@ import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline';
import { ApiErrorCode } from '@server/constants/error';
import { MediaServerType } from '@server/constants/server';
import type { JellyfinSettings } from '@server/lib/settings';
import axios from 'axios';
import { Field, Formik } from 'formik';
import { useState } from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
@@ -172,14 +173,9 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
}
try {
const searchParams = new URLSearchParams({
sync: params.sync ? 'true' : 'false',
...(params.enable ? { enable: params.enable } : {}),
await axios.get('/api/v1/settings/jellyfin/library', {
params,
});
const res = await fetch(
`/api/v1/settings/jellyfin/library?${searchParams.toString()}`
);
if (!res.ok) throw new Error(res.statusText, { cause: res });
setIsSyncing(false);
revalidate();
} catch (e) {
@@ -223,32 +219,16 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
};
const startScan = async () => {
const res = await fetch('/api/v1/settings/jellyfin/sync', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
start: true,
}),
await axios.post('/api/v1/settings/jellyfin/sync', {
start: true,
});
if (!res.ok) throw new Error();
revalidateSync();
};
const cancelScan = async () => {
const res = await fetch('/api/v1/settings/jellyfin/sync', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
cancel: true,
}),
await axios.post('/api/v1/settings/jellyfin/sync', {
cancel: true,
});
if (!res.ok) throw new Error();
revalidateSync();
};
@@ -263,19 +243,15 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
.join(',');
}
const searchParams = new URLSearchParams(params.enable ? params : {});
const res = await fetch(
`/api/v1/settings/jellyfin/library?${searchParams.toString()}`
);
if (!res.ok) throw new Error();
} else {
const searchParams = new URLSearchParams({
enable: [...activeLibraries, libraryId].join(','),
await axios.get('/api/v1/settings/jellyfin/library', {
params,
});
} else {
await axios.get('/api/v1/settings/jellyfin/library', {
params: {
enable: [...activeLibraries, libraryId].join(','),
},
});
const res = await fetch(
`/api/v1/settings/jellyfin/library?${searchParams.toString()}`
);
if (!res.ok) throw new Error();
}
if (onComplete) {
onComplete();
@@ -485,22 +461,15 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
validationSchema={JellyfinSettingsSchema}
onSubmit={async (values) => {
try {
const res = await fetch('/api/v1/settings/jellyfin', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
ip: values.hostname,
port: Number(values.port),
useSsl: values.useSsl,
urlBase: values.urlBase,
externalHostname: values.jellyfinExternalUrl,
jellyfinForgotPasswordUrl: values.jellyfinForgotPasswordUrl,
apiKey: values.apiKey,
} as JellyfinSettings),
});
if (!res.ok) throw new Error(res.statusText, { cause: res });
await axios.post('/api/v1/settings/jellyfin', {
ip: values.hostname,
port: Number(values.port),
useSsl: values.useSsl,
urlBase: values.urlBase,
externalHostname: values.jellyfinExternalUrl,
jellyfinForgotPasswordUrl: values.jellyfinForgotPasswordUrl,
apiKey: values.apiKey,
} as JellyfinSettings);
addToast(
intl.formatMessage(

View File

@@ -20,6 +20,7 @@ import type {
CacheResponse,
} from '@server/interfaces/api/settingsInterfaces';
import type { JobId } from '@server/lib/settings';
import axios from 'axios';
import cronstrue from 'cronstrue/i18n';
import { Fragment, useReducer, useState } from 'react';
import type { MessageDescriptor } from 'react-intl';
@@ -188,10 +189,7 @@ const SettingsJobs = () => {
}
const runJob = async (job: Job) => {
const res = await fetch(`/api/v1/settings/jobs/${job.id}/run`, {
method: 'POST',
});
if (!res.ok) throw new Error();
await axios.post(`/api/v1/settings/jobs/${job.id}/run`);
addToast(
intl.formatMessage(messages.jobstarted, {
jobname: intl.formatMessage(messages[job.id] ?? messages.unknownJob),
@@ -205,10 +203,7 @@ const SettingsJobs = () => {
};
const cancelJob = async (job: Job) => {
const res = await fetch(`/api/v1/settings/jobs/${job.id}/cancel`, {
method: 'POST',
});
if (!res.ok) throw new Error();
await axios.post(`/api/v1/settings/jobs/${job.id}/cancel`);
addToast(
intl.formatMessage(messages.jobcancelled, {
jobname: intl.formatMessage(messages[job.id] ?? messages.unknownJob),
@@ -222,10 +217,7 @@ const SettingsJobs = () => {
};
const flushCache = async (cache: CacheItem) => {
const res = await fetch(`/api/v1/settings/cache/${cache.id}/flush`, {
method: 'POST',
});
if (!res.ok) throw new Error();
await axios.post(`/api/v1/settings/cache/${cache.id}/flush`);
addToast(
intl.formatMessage(messages.cacheflushed, { cachename: cache.name }),
{
@@ -252,19 +244,12 @@ const SettingsJobs = () => {
}
setIsSaving(true);
const res = await fetch(
await axios.post(
`/api/v1/settings/jobs/${jobModalState.job.id}/schedule`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
schedule: jobScheduleCron.join(' '),
}),
schedule: jobScheduleCron.join(' '),
}
);
if (!res.ok) throw new Error();
addToast(intl.formatMessage(messages.jobScheduleEditSaved), {
appearance: 'success',

View File

@@ -17,6 +17,7 @@ import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline';
import { ArrowPathIcon } from '@heroicons/react/24/solid';
import type { UserSettingsGeneralResponse } from '@server/interfaces/api/userSettingsInterfaces';
import type { MainSettings } from '@server/lib/settings';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import { useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
@@ -103,10 +104,7 @@ const SettingsMain = () => {
const regenerate = async () => {
try {
const res = await fetch('/api/v1/settings/main/regenerate', {
method: 'POST',
});
if (!res.ok) throw new Error();
await axios.post('/api/v1/settings/main/regenerate');
revalidate();
addToast(intl.formatMessage(messages.toastApiKeySuccess), {
@@ -167,35 +165,28 @@ const SettingsMain = () => {
validationSchema={MainSettingsSchema}
onSubmit={async (values) => {
try {
const res = await fetch('/api/v1/settings/main', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
await axios.post('/api/v1/settings/main', {
applicationTitle: values.applicationTitle,
applicationUrl: values.applicationUrl,
csrfProtection: values.csrfProtection,
hideAvailable: values.hideAvailable,
locale: values.locale,
region: values.region,
originalLanguage: values.originalLanguage,
partialRequestsEnabled: values.partialRequestsEnabled,
trustProxy: values.trustProxy,
cacheImages: values.cacheImages,
proxy: {
enabled: values.proxyEnabled,
hostname: values.proxyHostname,
port: values.proxyPort,
useSsl: values.proxySsl,
user: values.proxyUser,
password: values.proxyPassword,
bypassFilter: values.proxyBypassFilter,
bypassLocalAddresses: values.proxyBypassLocalAddresses,
},
body: JSON.stringify({
applicationTitle: values.applicationTitle,
applicationUrl: values.applicationUrl,
csrfProtection: values.csrfProtection,
hideAvailable: values.hideAvailable,
locale: values.locale,
region: values.region,
originalLanguage: values.originalLanguage,
partialRequestsEnabled: values.partialRequestsEnabled,
trustProxy: values.trustProxy,
cacheImages: values.cacheImages,
proxy: {
enabled: values.proxyEnabled,
hostname: values.proxyHostname,
port: values.proxyPort,
useSsl: values.proxySsl,
user: values.proxyUser,
password: values.proxyPassword,
bypassFilter: values.proxyBypassFilter,
bypassLocalAddresses: values.proxyBypassLocalAddresses,
},
}),
});
if (!res.ok) throw new Error();
mutate('/api/v1/settings/public');
mutate('/api/v1/status');

View File

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

View File

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

View File

@@ -9,6 +9,7 @@ import defineMessages from '@app/utils/defineMessages';
import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline';
import { MediaServerType } from '@server/constants/server';
import type { MainSettings } from '@server/lib/settings';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import { useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
@@ -83,28 +84,21 @@ const SettingsUsers = () => {
enableReinitialize
onSubmit={async (values) => {
try {
const res = await fetch('/api/v1/settings/main', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
localLogin: values.localLogin,
newPlexLogin: values.newPlexLogin,
defaultQuotas: {
movie: {
quotaLimit: values.movieQuotaLimit,
quotaDays: values.movieQuotaDays,
},
tv: {
quotaLimit: values.tvQuotaLimit,
quotaDays: values.tvQuotaDays,
},
await axios.post('/api/v1/settings/main', {
localLogin: values.localLogin,
newPlexLogin: values.newPlexLogin,
defaultQuotas: {
movie: {
quotaLimit: values.movieQuotaLimit,
quotaDays: values.movieQuotaDays,
},
defaultPermissions: values.defaultPermissions,
}),
tv: {
quotaLimit: values.tvQuotaLimit,
quotaDays: values.tvQuotaDays,
},
},
defaultPermissions: values.defaultPermissions,
});
if (!res.ok) throw new Error();
mutate('/api/v1/settings/public');
addToast(intl.formatMessage(messages.toastSettingsSuccess), {

View File

@@ -4,6 +4,7 @@ import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { Transition } from '@headlessui/react';
import type { SonarrSettings } from '@server/lib/settings';
import axios from 'axios';
import { Field, Formik } from 'formik';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useIntl } from 'react-intl';
@@ -180,24 +181,19 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
}) => {
setIsTesting(true);
try {
const res = await fetch('/api/v1/settings/sonarr/test', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
const response = await axios.post<TestResponse>(
'/api/v1/settings/sonarr/test',
{
hostname,
apiKey,
port: Number(port),
baseUrl,
useSsl,
}),
});
if (!res.ok) throw new Error();
const data: TestResponse = await res.json();
}
);
setIsValidated(true);
setTestResponse(data);
setTestResponse(response.data);
if (initialLoad.current) {
addToast(intl.formatMessage(messages.toastSonarrTestSuccess), {
appearance: 'success',
@@ -314,23 +310,12 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
tagRequests: values.tagRequests,
};
if (!sonarr) {
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();
await axios.post('/api/v1/settings/sonarr', submission);
} else {
const res = await fetch(`/api/v1/settings/sonarr/${sonarr.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(submission),
});
if (!res.ok) throw new Error();
await axios.put(
`/api/v1/settings/sonarr/${sonarr.id}`,
submission
);
}
onSave();

View File

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

View File

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

View File

@@ -14,6 +14,7 @@ import useLocale from '@app/hooks/useLocale';
import useSettings from '@app/hooks/useSettings';
import defineMessages from '@app/utils/defineMessages';
import { MediaServerType } from '@server/constants/server';
import axios from 'axios';
import Image from 'next/image';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
@@ -52,27 +53,15 @@ const Setup = () => {
const finishSetup = async () => {
setIsUpdating(true);
const res = await fetch('/api/v1/settings/initialize', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
});
if (!res.ok) throw new Error();
const data: { initialized: boolean } = await res.json();
const response = await axios.post<{ initialized: boolean }>(
'/api/v1/settings/initialize'
);
setIsUpdating(false);
if (data.initialized) {
const mainRes = await fetch('/api/v1/settings/main', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ locale }),
});
if (!mainRes.ok) throw new Error();
if (response.data.initialized) {
await axios.post('/api/v1/settings/main', { locale });
mutate('/api/v1/settings/public');
router.push('/');
}
};

View File

@@ -2,6 +2,7 @@ import Button from '@app/components/Common/Button';
import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { CheckIcon, TrashIcon } from '@heroicons/react/24/solid';
import axios from 'axios';
import { useIntl } from 'react-intl';
import { mutate } from 'swr';
@@ -20,14 +21,11 @@ const messages = defineMessages('components.TitleCard', {
cleardata: 'Clear Data',
});
const ErrorCard = ({ id, tmdbId, tvdbId, type, canExpand }: ErrorCardProps) => {
const Error = ({ id, tmdbId, tvdbId, type, canExpand }: ErrorCardProps) => {
const intl = useIntl();
const deleteMedia = async () => {
const res = await fetch(`/api/v1/media/${id}`, {
method: 'DELETE',
});
if (!res.ok) throw new Error();
await axios.delete(`/api/v1/media/${id}`);
mutate('/api/v1/media?filter=allavailable&take=20&sort=mediaAdded');
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
};
@@ -131,4 +129,4 @@ const ErrorCard = ({ id, tmdbId, tvdbId, type, canExpand }: ErrorCardProps) => {
</div>
);
};
export default ErrorCard;
export default Error;

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ import type { User } from '@app/hooks/useUser';
import { useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import axios from 'axios';
import { useEffect, useState } from 'react';
import { useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
@@ -44,18 +45,10 @@ const BulkEditModal = ({
const updateUsers = async () => {
try {
setIsSaving(true);
const res = await fetch('/api/v1/user', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
ids: selectedUserIds,
permissions: currentPermission,
}),
const { data: updated } = await axios.put<User[]>(`/api/v1/user`, {
ids: selectedUserIds,
permissions: currentPermission,
});
if (!res.ok) throw new Error();
const updated: User[] = await res.json();
if (onComplete) {
onComplete(updated);
}

View File

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

View File

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

View File

@@ -28,6 +28,7 @@ import {
import { MediaServerType } from '@server/constants/server';
import type { UserResultsResponse } from '@server/interfaces/api/userInterfaces';
import { hasPermission } from '@server/lib/permissions';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import Link from 'next/link';
import { useRouter } from 'next/router';
@@ -181,10 +182,7 @@ const UserList = () => {
setDeleting(true);
try {
const res = await fetch(`/api/v1/user/${deleteModal.user?.id}`, {
method: 'DELETE',
});
if (!res.ok) throw new Error();
await axios.delete(`/api/v1/user/${deleteModal.user?.id}`);
addToast(intl.formatMessage(messages.userdeleted), {
autoDismiss: true,
@@ -284,18 +282,11 @@ const UserList = () => {
validationSchema={CreateUserSchema}
onSubmit={async (values) => {
try {
const res = await fetch('/api/v1/user', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username: values.username,
email: values.email,
password: values.genpassword ? null : values.password,
}),
await axios.post('/api/v1/user', {
username: values.username,
email: values.email,
password: values.genpassword ? null : values.password,
});
if (!res.ok) throw new Error(res.statusText, { cause: res });
addToast(intl.formatMessage(messages.usercreatedsuccess), {
appearance: 'success',
autoDismiss: true,

View File

@@ -11,11 +11,12 @@ import useLocale from '@app/hooks/useLocale';
import useSettings from '@app/hooks/useSettings';
import { Permission, UserType, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import ErrorPage from '@app/pages/_error';
import Error from '@app/pages/_error';
import defineMessages from '@app/utils/defineMessages';
import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline';
import { ApiErrorCode } from '@server/constants/error';
import type { UserSettingsGeneralResponse } from '@server/interfaces/api/userSettingsInterfaces';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
@@ -120,7 +121,7 @@ const UserGeneralSettings = () => {
}
if (!data) {
return <ErrorPage statusCode={500} />;
return <Error statusCode={500} />;
}
return (
@@ -155,32 +156,23 @@ const UserGeneralSettings = () => {
enableReinitialize
onSubmit={async (values) => {
try {
const res = await fetch(`/api/v1/user/${user?.id}/settings/main`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username: values.displayName,
email:
values.email || user?.jellyfinUsername || user?.plexUsername,
discordId: values.discordId,
locale: values.locale,
region: values.region,
originalLanguage: values.originalLanguage,
movieQuotaLimit: movieQuotaEnabled
? values.movieQuotaLimit
: null,
movieQuotaDays: movieQuotaEnabled
? values.movieQuotaDays
: null,
tvQuotaLimit: tvQuotaEnabled ? values.tvQuotaLimit : null,
tvQuotaDays: tvQuotaEnabled ? values.tvQuotaDays : null,
watchlistSyncMovies: values.watchlistSyncMovies,
watchlistSyncTv: values.watchlistSyncTv,
}),
await axios.post(`/api/v1/user/${user?.id}/settings/main`, {
username: values.displayName,
email:
values.email || user?.jellyfinUsername || user?.plexUsername,
discordId: values.discordId,
locale: values.locale,
region: values.region,
originalLanguage: values.originalLanguage,
movieQuotaLimit: movieQuotaEnabled
? values.movieQuotaLimit
: 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(res.statusText, { cause: res });
if (currentUser?.id === user?.id && setLocale) {
setLocale(

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline';
import type { UserSettingsNotificationsResponse } from '@server/interfaces/api/userSettingsInterfaces';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import { useRouter } from 'next/router';
import { useIntl } from 'react-intl';
@@ -70,28 +71,18 @@ const UserTelegramSettings = () => {
enableReinitialize
onSubmit={async (values) => {
try {
const res = await fetch(
`/api/v1/user/${user?.id}/settings/notifications`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
pgpKey: data?.pgpKey,
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();
await axios.post(`/api/v1/user/${user?.id}/settings/notifications`, {
pgpKey: data?.pgpKey,
discordId: data?.discordId,
pushbulletAccessToken: data?.pushbulletAccessToken,
pushoverApplicationToken: data?.pushoverApplicationToken,
pushoverUserKey: data?.pushoverUserKey,
telegramChatId: values.telegramChatId,
telegramSendSilently: values.telegramSendSilently,
notificationTypes: {
telegram: values.types,
},
});
addToast(intl.formatMessage(messages.telegramsettingssaved), {
appearance: 'success',
autoDismiss: true,

View File

@@ -8,6 +8,7 @@ import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline';
import type { UserSettingsNotificationsResponse } from '@server/interfaces/api/userSettingsInterfaces';
import axios from 'axios';
import { Form, Formik } from 'formik';
import { useRouter } from 'next/router';
import { useIntl } from 'react-intl';
@@ -47,28 +48,18 @@ const UserWebPushSettings = () => {
enableReinitialize
onSubmit={async (values) => {
try {
const res = await fetch(
`/api/v1/user/${user?.id}/settings/notifications`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
pgpKey: data?.pgpKey,
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();
await axios.post(`/api/v1/user/${user?.id}/settings/notifications`, {
pgpKey: data?.pgpKey,
discordId: data?.discordId,
pushbulletAccessToken: data?.pushbulletAccessToken,
pushoverApplicationToken: data?.pushoverApplicationToken,
pushoverUserKey: data?.pushoverUserKey,
telegramChatId: data?.telegramChatId,
telegramSendSilently: data?.telegramSendSilently,
notificationTypes: {
webpush: values.types,
},
});
mutate('/api/v1/settings/public');
addToast(intl.formatMessage(messages.webpushsettingssaved), {
appearance: 'success',

View File

@@ -5,9 +5,10 @@ import PageTitle from '@app/components/Common/PageTitle';
import SensitiveInput from '@app/components/Common/SensitiveInput';
import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import ErrorPage from '@app/pages/_error';
import Error from '@app/pages/_error';
import defineMessages from '@app/utils/defineMessages';
import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline';
import axios from 'axios';
import { Form, Formik } from 'formik';
import { useRouter } from 'next/router';
import { useIntl } from 'react-intl';
@@ -79,7 +80,7 @@ const UserPasswordChange = () => {
}
if (!data) {
return <ErrorPage statusCode={500} />;
return <Error statusCode={500} />;
}
if (
@@ -122,21 +123,11 @@ const UserPasswordChange = () => {
enableReinitialize
onSubmit={async (values, { resetForm }) => {
try {
const res = await fetch(
`/api/v1/user/${user?.id}/settings/password`,
{
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();
await axios.post(`/api/v1/user/${user?.id}/settings/password`, {
currentPassword: values.currentPassword,
newPassword: values.newPassword,
confirmPassword: values.confirmPassword,
});
addToast(intl.formatMessage(messages.toastSettingsSuccess), {
autoDismiss: true,

View File

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

View File

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

View File

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

View File

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

View File

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

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