Compare commits
37 Commits
fix/user-w
...
preview-je
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d802a5eff | ||
|
|
d5f817e734 | ||
|
|
422085523e | ||
|
|
fccfca6ed0 | ||
|
|
3fc14c9e22 | ||
|
|
e03edd2d1f | ||
|
|
2d9530c0ed | ||
|
|
1cc43cee93 | ||
|
|
5bcbc810d6 | ||
|
|
b8042ab700 | ||
|
|
c3a6c7d4b2 | ||
|
|
6636aeef45 | ||
|
|
6207a6a26d | ||
|
|
5875b2b5c2 | ||
|
|
62dbde448c | ||
|
|
0116c13e06 | ||
|
|
c96ca6742e | ||
|
|
c80d9a853a | ||
|
|
6cea8bba59 | ||
|
|
2be9c7dcc1 | ||
|
|
5cc4389825 | ||
|
|
dd6dbf1de9 | ||
|
|
c600566ac0 | ||
|
|
4db1df2ba5 | ||
|
|
3a363ae1ff | ||
|
|
084e1b224e | ||
|
|
635a5f019c | ||
|
|
b36bb3fa58 | ||
|
|
900e6110ad | ||
|
|
19e20749c1 | ||
|
|
1aac3c5f09 | ||
|
|
1bc4bd3d69 | ||
|
|
7a505813d5 | ||
|
|
45dbf84d7e | ||
|
|
a4f1f1203a | ||
|
|
504d8bd5fe | ||
|
|
395a91c2f0 |
@@ -8,3 +8,4 @@ pnpm-lock.yaml
|
||||
# assets
|
||||
src/assets/
|
||||
public/
|
||||
docs/
|
||||
|
||||
@@ -3,6 +3,12 @@ module.exports = {
|
||||
singleQuote: true,
|
||||
trailingComma: 'es5',
|
||||
overrides: [
|
||||
{
|
||||
files: 'pnpm-lock.yaml',
|
||||
options: {
|
||||
rangeEnd: 0, // default: Infinity
|
||||
},
|
||||
},
|
||||
{
|
||||
files: 'gen-docs/pnpm-lock.yaml',
|
||||
options: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const testUser = {
|
||||
displayName: 'Test User',
|
||||
username: 'Test User',
|
||||
emailAddress: 'test@seeerr.dev',
|
||||
password: 'test1234',
|
||||
};
|
||||
@@ -32,7 +32,7 @@ describe('User List', () => {
|
||||
|
||||
cy.get('[data-testid=modal-title]').should('contain', 'Create Local User');
|
||||
|
||||
cy.get('#displayName').type(testUser.displayName);
|
||||
cy.get('#username').type(testUser.username);
|
||||
cy.get('#email').type(testUser.emailAddress);
|
||||
cy.get('#password').type(testUser.password);
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ pnpm start
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
|
||||
:::info
|
||||
You can now access Jellyseerr by visiting `http://localhost:5055` in your web browser.
|
||||
:::
|
||||
@@ -99,6 +99,9 @@ PORT=5055
|
||||
|
||||
## Uncomment if your media server is emby instead of jellyfin.
|
||||
# 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
|
||||
@@ -312,7 +315,7 @@ node dist/index.js
|
||||
|
||||
Now, Jellyseerr will start when the computer boots up in the background.
|
||||
</TabItem>
|
||||
|
||||
|
||||
<TabItem value="nssm" label="NSSM">
|
||||
To run jellyseerr as a service:
|
||||
1. Download the [Non-Sucking Service Manager](https://nssm.cc/download)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
commitTag: process.env.COMMIT_TAG || 'local',
|
||||
forceIpv4First: process.env.FORCE_IPV4_FIRST === 'true' ? 'true' : 'false',
|
||||
},
|
||||
publicRuntimeConfig: {
|
||||
// Will be available on both server and client
|
||||
|
||||
@@ -3586,6 +3586,8 @@ paths:
|
||||
type: string
|
||||
email:
|
||||
type: string
|
||||
serverType:
|
||||
type: number
|
||||
required:
|
||||
- username
|
||||
- password
|
||||
|
||||
@@ -43,8 +43,6 @@
|
||||
"@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",
|
||||
@@ -121,7 +119,7 @@
|
||||
"@types/express": "4.17.17",
|
||||
"@types/express-session": "1.17.6",
|
||||
"@types/lodash": "4.14.191",
|
||||
"@types/node": "17.0.36",
|
||||
"@types/node": "20.14.8",
|
||||
"@types/node-schedule": "2.1.0",
|
||||
"@types/nodemailer": "6.4.7",
|
||||
"@types/react": "^18.3.3",
|
||||
|
||||
12799
pnpm-lock.yaml
generated
12799
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,8 @@
|
||||
import logger from '@server/logger';
|
||||
import axios from 'axios';
|
||||
import fs, { promises as fsp } from 'fs';
|
||||
import path from 'path';
|
||||
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 xml2js from 'xml2js';
|
||||
|
||||
const UPDATE_INTERVAL_MSEC = 24 * 3600 * 1000; // how often to download new mapping in milliseconds
|
||||
@@ -161,13 +162,18 @@ class AnimeListMapping {
|
||||
label: 'Anime-List Sync',
|
||||
});
|
||||
try {
|
||||
const response = await axios.get(MAPPING_URL, {
|
||||
responseType: 'stream',
|
||||
});
|
||||
await new Promise<void>((resolve) => {
|
||||
const response = await fetch(MAPPING_URL);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch: ${response.statusText}`);
|
||||
}
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const writer = fs.createWriteStream(LOCAL_PATH);
|
||||
writer.on('finish', resolve);
|
||||
response.data.pipe(writer);
|
||||
writer.on('error', reject);
|
||||
if (!response.body) return reject();
|
||||
Readable.fromWeb(response.body as ReadableStream<Uint8Array>).pipe(
|
||||
writer
|
||||
);
|
||||
});
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to download Anime-List mapping: ${e.message}`);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { AxiosInstance, AxiosRequestConfig } from 'axios';
|
||||
import axios from 'axios';
|
||||
import rateLimit from 'axios-rate-limit';
|
||||
import type { RateLimitOptions } from '@server/utils/rateLimit';
|
||||
import rateLimit from '@server/utils/rateLimit';
|
||||
import type NodeCache from 'node-cache';
|
||||
|
||||
// 5 minute default TTL (in seconds)
|
||||
@@ -12,71 +11,87 @@ const DEFAULT_ROLLING_BUFFER = 10000;
|
||||
interface ExternalAPIOptions {
|
||||
nodeCache?: NodeCache;
|
||||
headers?: Record<string, unknown>;
|
||||
rateLimit?: {
|
||||
maxRPS: number;
|
||||
maxRequests: number;
|
||||
};
|
||||
rateLimit?: RateLimitOptions;
|
||||
}
|
||||
|
||||
class ExternalAPI {
|
||||
protected axios: AxiosInstance;
|
||||
protected fetch: typeof fetch;
|
||||
protected params: Record<string, string>;
|
||||
protected defaultHeaders: { [key: string]: string };
|
||||
private baseUrl: string;
|
||||
private cache?: NodeCache;
|
||||
|
||||
constructor(
|
||||
baseUrl: string,
|
||||
params: Record<string, unknown>,
|
||||
params: Record<string, string> = {},
|
||||
options: ExternalAPIOptions = {}
|
||||
) {
|
||||
this.axios = axios.create({
|
||||
baseURL: baseUrl,
|
||||
params,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (options.rateLimit) {
|
||||
this.axios = rateLimit(this.axios, {
|
||||
maxRequests: options.rateLimit.maxRequests,
|
||||
maxRPS: options.rateLimit.maxRPS,
|
||||
});
|
||||
this.fetch = rateLimit(fetch, options.rateLimit);
|
||||
} else {
|
||||
this.fetch = fetch;
|
||||
}
|
||||
|
||||
this.baseUrl = baseUrl;
|
||||
this.params = params;
|
||||
this.defaultHeaders = {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
...options.headers,
|
||||
};
|
||||
this.cache = options.nodeCache;
|
||||
}
|
||||
|
||||
protected async get<T>(
|
||||
endpoint: string,
|
||||
config?: AxiosRequestConfig,
|
||||
ttl?: number
|
||||
params?: Record<string, string>,
|
||||
ttl?: number,
|
||||
config?: RequestInit
|
||||
): Promise<T> {
|
||||
const cacheKey = this.serializeCacheKey(endpoint, config?.params);
|
||||
const cacheKey = this.serializeCacheKey(endpoint, {
|
||||
...this.params,
|
||||
...params,
|
||||
});
|
||||
const cachedItem = this.cache?.get<T>(cacheKey);
|
||||
if (cachedItem) {
|
||||
return cachedItem;
|
||||
}
|
||||
|
||||
const response = await this.axios.get<T>(endpoint, config);
|
||||
const url = this.formatUrl(endpoint, params);
|
||||
const response = await this.fetch(url, {
|
||||
...config,
|
||||
headers: {
|
||||
...this.defaultHeaders,
|
||||
...config?.headers,
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error(
|
||||
`${response.status} ${response.statusText}${text ? ': ' + text : ''}`,
|
||||
{
|
||||
cause: response,
|
||||
}
|
||||
);
|
||||
}
|
||||
const data = await this.getDataFromResponse(response);
|
||||
|
||||
if (this.cache) {
|
||||
this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
|
||||
this.cache.set(cacheKey, data, ttl ?? DEFAULT_TTL);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
return data;
|
||||
}
|
||||
|
||||
protected async post<T>(
|
||||
endpoint: string,
|
||||
data: Record<string, unknown>,
|
||||
config?: AxiosRequestConfig,
|
||||
ttl?: number
|
||||
params?: Record<string, string>,
|
||||
ttl?: number,
|
||||
config?: RequestInit
|
||||
): Promise<T> {
|
||||
const cacheKey = this.serializeCacheKey(endpoint, {
|
||||
config: config?.params,
|
||||
config: { ...this.params, ...params },
|
||||
data,
|
||||
});
|
||||
const cachedItem = this.cache?.get<T>(cacheKey);
|
||||
@@ -84,21 +99,116 @@ class ExternalAPI {
|
||||
return cachedItem;
|
||||
}
|
||||
|
||||
const response = await this.axios.post<T>(endpoint, data, config);
|
||||
const url = this.formatUrl(endpoint, params);
|
||||
const response = await this.fetch(url, {
|
||||
method: 'POST',
|
||||
...config,
|
||||
headers: {
|
||||
...this.defaultHeaders,
|
||||
...config?.headers,
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error(
|
||||
`${response.status} ${response.statusText}${text ? ': ' + text : ''}`,
|
||||
{
|
||||
cause: response,
|
||||
}
|
||||
);
|
||||
}
|
||||
const resData = await this.getDataFromResponse(response);
|
||||
|
||||
if (this.cache) {
|
||||
this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
|
||||
this.cache.set(cacheKey, resData, ttl ?? DEFAULT_TTL);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
return resData;
|
||||
}
|
||||
|
||||
protected async put<T>(
|
||||
endpoint: string,
|
||||
data: Record<string, unknown>,
|
||||
params?: Record<string, string>,
|
||||
ttl?: number,
|
||||
config?: RequestInit
|
||||
): Promise<T> {
|
||||
const cacheKey = this.serializeCacheKey(endpoint, {
|
||||
config: { ...this.params, ...params },
|
||||
data,
|
||||
});
|
||||
const cachedItem = this.cache?.get<T>(cacheKey);
|
||||
if (cachedItem) {
|
||||
return cachedItem;
|
||||
}
|
||||
|
||||
const url = this.formatUrl(endpoint, params);
|
||||
const response = await this.fetch(url, {
|
||||
method: 'PUT',
|
||||
...config,
|
||||
headers: {
|
||||
...this.defaultHeaders,
|
||||
...config?.headers,
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error(
|
||||
`${response.status} ${response.statusText}${text ? ': ' + text : ''}`,
|
||||
{
|
||||
cause: response,
|
||||
}
|
||||
);
|
||||
}
|
||||
const resData = await this.getDataFromResponse(response);
|
||||
|
||||
if (this.cache) {
|
||||
this.cache.set(cacheKey, resData, ttl ?? DEFAULT_TTL);
|
||||
}
|
||||
|
||||
return resData;
|
||||
}
|
||||
|
||||
protected async delete<T>(
|
||||
endpoint: string,
|
||||
params?: Record<string, string>,
|
||||
config?: RequestInit
|
||||
): Promise<T> {
|
||||
const url = this.formatUrl(endpoint, params);
|
||||
const response = await this.fetch(url, {
|
||||
...config,
|
||||
headers: {
|
||||
...this.defaultHeaders,
|
||||
...config?.headers,
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error(
|
||||
`${response.status} ${response.statusText}${text ? ': ' + text : ''}`,
|
||||
{
|
||||
cause: response,
|
||||
}
|
||||
);
|
||||
}
|
||||
const data = await this.getDataFromResponse(response);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
protected async getRolling<T>(
|
||||
endpoint: string,
|
||||
config?: AxiosRequestConfig,
|
||||
ttl?: number
|
||||
params?: Record<string, string>,
|
||||
ttl?: number,
|
||||
config?: RequestInit,
|
||||
overwriteBaseUrl?: string
|
||||
): Promise<T> {
|
||||
const cacheKey = this.serializeCacheKey(endpoint, config?.params);
|
||||
const cacheKey = this.serializeCacheKey(endpoint, {
|
||||
...this.params,
|
||||
...params,
|
||||
});
|
||||
const cachedItem = this.cache?.get<T>(cacheKey);
|
||||
|
||||
if (cachedItem) {
|
||||
@@ -109,20 +219,73 @@ class ExternalAPI {
|
||||
keyTtl - (ttl ?? DEFAULT_TTL) * 1000 <
|
||||
Date.now() - DEFAULT_ROLLING_BUFFER
|
||||
) {
|
||||
this.axios.get<T>(endpoint, config).then((response) => {
|
||||
this.cache?.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
|
||||
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);
|
||||
});
|
||||
}
|
||||
return cachedItem;
|
||||
}
|
||||
|
||||
const response = await this.axios.get<T>(endpoint, config);
|
||||
const url = this.formatUrl(endpoint, params, overwriteBaseUrl);
|
||||
const response = await this.fetch(url, {
|
||||
...config,
|
||||
headers: {
|
||||
...this.defaultHeaders,
|
||||
...config?.headers,
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error(
|
||||
`${response.status} ${response.statusText}${text ? ': ' + text : ''}`,
|
||||
{
|
||||
cause: response,
|
||||
}
|
||||
);
|
||||
}
|
||||
const data = await this.getDataFromResponse(response);
|
||||
|
||||
if (this.cache) {
|
||||
this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
|
||||
this.cache.set(cacheKey, data, ttl ?? DEFAULT_TTL);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
return data;
|
||||
}
|
||||
|
||||
private formatUrl(
|
||||
endpoint: string,
|
||||
params?: Record<string, string>,
|
||||
overwriteBaseUrl?: string
|
||||
): string {
|
||||
const baseUrl = overwriteBaseUrl || this.baseUrl;
|
||||
const href =
|
||||
baseUrl +
|
||||
(baseUrl.endsWith('/') ? '' : '/') +
|
||||
(endpoint.startsWith('/') ? endpoint.slice(1) : endpoint);
|
||||
const searchParams = new URLSearchParams({
|
||||
...this.params,
|
||||
...params,
|
||||
});
|
||||
return `${href}?${searchParams.toString()}`;
|
||||
}
|
||||
|
||||
private serializeCacheKey(
|
||||
@@ -135,6 +298,25 @@ 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 {
|
||||
return await response.blob();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ExternalAPI;
|
||||
|
||||
@@ -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,10 +67,6 @@ class GithubAPI extends ExternalAPI {
|
||||
'https://api.github.com',
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
nodeCache: cacheManager.getCache('github').data,
|
||||
}
|
||||
);
|
||||
@@ -85,9 +81,7 @@ class GithubAPI extends ExternalAPI {
|
||||
const data = await this.get<GitHubRelease[]>(
|
||||
'/repos/fallenbagel/jellyseerr/releases',
|
||||
{
|
||||
params: {
|
||||
per_page: take,
|
||||
},
|
||||
per_page: take.toString(),
|
||||
}
|
||||
);
|
||||
|
||||
@@ -112,10 +106,8 @@ class GithubAPI extends ExternalAPI {
|
||||
const data = await this.get<GithubCommit[]>(
|
||||
'/repos/fallenbagel/jellyseerr/commits',
|
||||
{
|
||||
params: {
|
||||
per_page: take,
|
||||
branch,
|
||||
},
|
||||
per_page: take.toString(),
|
||||
branch,
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -111,8 +111,6 @@ class JellyfinAPI extends ExternalAPI {
|
||||
{
|
||||
headers: {
|
||||
'X-Emby-Authorization': authHeaderVal,
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -127,7 +125,7 @@ class JellyfinAPI extends ExternalAPI {
|
||||
ClientIP?: string
|
||||
): Promise<JellyfinLoginResponse> {
|
||||
const authenticate = async (useHeaders: boolean) => {
|
||||
const headers =
|
||||
const headers: { [key: string]: string } =
|
||||
useHeaders && ClientIP ? { 'X-Forwarded-For': ClientIP } : {};
|
||||
|
||||
return this.post<JellyfinLoginResponse>(
|
||||
@@ -136,6 +134,8 @@ class JellyfinAPI extends ExternalAPI {
|
||||
Username,
|
||||
Pw: Password,
|
||||
},
|
||||
{},
|
||||
undefined,
|
||||
{ headers }
|
||||
);
|
||||
};
|
||||
@@ -152,7 +152,7 @@ class JellyfinAPI extends ExternalAPI {
|
||||
try {
|
||||
return await authenticate(false);
|
||||
} catch (e) {
|
||||
const status = e.response?.status;
|
||||
const status = e.cause?.status;
|
||||
|
||||
const networkErrorCodes = new Set([
|
||||
'ECONNREFUSED',
|
||||
@@ -190,7 +190,7 @@ class JellyfinAPI extends ExternalAPI {
|
||||
|
||||
return systemInfoResponse;
|
||||
} catch (e) {
|
||||
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
|
||||
throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,7 +207,7 @@ class JellyfinAPI extends ExternalAPI {
|
||||
{ label: 'Jellyfin API' }
|
||||
);
|
||||
|
||||
throw new ApiError(e.response?.status, ApiErrorCode.Unknown);
|
||||
throw new ApiError(e.cause?.status, ApiErrorCode.Unknown);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,7 +222,7 @@ class JellyfinAPI extends ExternalAPI {
|
||||
{ label: 'Jellyfin API' }
|
||||
);
|
||||
|
||||
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
|
||||
throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,7 +238,7 @@ class JellyfinAPI extends ExternalAPI {
|
||||
{ label: 'Jellyfin API' }
|
||||
);
|
||||
|
||||
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
|
||||
throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,7 +296,16 @@ 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(
|
||||
@@ -308,14 +317,18 @@ class JellyfinAPI extends ExternalAPI {
|
||||
{ label: 'Jellyfin API' }
|
||||
);
|
||||
|
||||
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
|
||||
throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -325,7 +338,7 @@ class JellyfinAPI extends ExternalAPI {
|
||||
{ label: 'Jellyfin API' }
|
||||
);
|
||||
|
||||
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
|
||||
throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,7 +353,7 @@ class JellyfinAPI extends ExternalAPI {
|
||||
return itemResponse;
|
||||
} catch (e) {
|
||||
if (availabilitySync.running) {
|
||||
if (e.response && e.response.status === 500) {
|
||||
if (e.cause?.status === 500) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -349,7 +362,7 @@ class JellyfinAPI extends ExternalAPI {
|
||||
`Something went wrong while getting library content from the Jellyfin server: ${e.message}`,
|
||||
{ label: 'Jellyfin API' }
|
||||
);
|
||||
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
|
||||
throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,7 +377,7 @@ class JellyfinAPI extends ExternalAPI {
|
||||
{ label: 'Jellyfin API' }
|
||||
);
|
||||
|
||||
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
|
||||
throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,7 +387,10 @@ 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(
|
||||
@@ -386,7 +402,7 @@ class JellyfinAPI extends ExternalAPI {
|
||||
{ label: 'Jellyfin API' }
|
||||
);
|
||||
|
||||
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
|
||||
throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,8 +137,6 @@ class PlexTvAPI extends ExternalAPI {
|
||||
{
|
||||
headers: {
|
||||
'X-Plex-Token': authToken,
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
nodeCache: cacheManager.getCache('plextv').data,
|
||||
}
|
||||
@@ -149,15 +147,11 @@ class PlexTvAPI extends ExternalAPI {
|
||||
|
||||
public async getDevices(): Promise<PlexDevice[]> {
|
||||
try {
|
||||
const devicesResp = await this.axios.get(
|
||||
'/api/resources?includeHttps=1',
|
||||
{
|
||||
transformResponse: [],
|
||||
responseType: 'text',
|
||||
}
|
||||
);
|
||||
const devicesResp = await this.get('/api/resources', {
|
||||
includeHttps: '1',
|
||||
});
|
||||
const parsedXml = await xml2js.parseStringPromise(
|
||||
devicesResp.data as DeviceResponse
|
||||
devicesResp as DeviceResponse
|
||||
);
|
||||
return parsedXml?.MediaContainer?.Device?.map((pxml: DeviceResponse) => ({
|
||||
name: pxml.$.name,
|
||||
@@ -205,11 +199,11 @@ class PlexTvAPI extends ExternalAPI {
|
||||
|
||||
public async getUser(): Promise<PlexUser> {
|
||||
try {
|
||||
const account = await this.axios.get<PlexAccountResponse>(
|
||||
const account = await this.get<PlexAccountResponse>(
|
||||
'/users/account.json'
|
||||
);
|
||||
|
||||
return account.data.user;
|
||||
return account.user;
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`Something went wrong while getting the account from plex.tv: ${e.message}`,
|
||||
@@ -249,13 +243,10 @@ class PlexTvAPI extends ExternalAPI {
|
||||
}
|
||||
|
||||
public async getUsers(): Promise<UsersResponse> {
|
||||
const response = await this.axios.get('/api/users', {
|
||||
transformResponse: [],
|
||||
responseType: 'text',
|
||||
});
|
||||
const data = await this.get('/api/users');
|
||||
|
||||
const parsedXml = (await xml2js.parseStringPromise(
|
||||
response.data
|
||||
data as string
|
||||
)) as UsersResponse;
|
||||
return parsedXml;
|
||||
}
|
||||
@@ -270,49 +261,49 @@ class PlexTvAPI extends ExternalAPI {
|
||||
items: PlexWatchlistItem[];
|
||||
}> {
|
||||
try {
|
||||
const response = await this.axios.get<WatchlistResponse>(
|
||||
'/library/sections/watchlist/all',
|
||||
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()}`,
|
||||
{
|
||||
params: {
|
||||
'X-Plex-Container-Start': offset,
|
||||
'X-Plex-Container-Size': size,
|
||||
},
|
||||
baseURL: 'https://metadata.provider.plex.tv',
|
||||
headers: this.defaultHeaders,
|
||||
}
|
||||
);
|
||||
const data = (await response.json()) as WatchlistResponse;
|
||||
|
||||
const watchlistDetails = await Promise.all(
|
||||
(response.data.MediaContainer.Metadata ?? []).map(
|
||||
async (watchlistItem) => {
|
||||
const detailedResponse = await this.getRolling<MetadataResponse>(
|
||||
`/library/metadata/${watchlistItem.ratingKey}`,
|
||||
{
|
||||
baseURL: 'https://metadata.provider.plex.tv',
|
||||
}
|
||||
);
|
||||
(data.MediaContainer.Metadata ?? []).map(async (watchlistItem) => {
|
||||
const detailedResponse = await this.getRolling<MetadataResponse>(
|
||||
`/library/metadata/${watchlistItem.ratingKey}`,
|
||||
{},
|
||||
undefined,
|
||||
{},
|
||||
'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);
|
||||
@@ -320,7 +311,7 @@ class PlexTvAPI extends ExternalAPI {
|
||||
return {
|
||||
offset,
|
||||
size,
|
||||
totalSize: response.data.MediaContainer.totalSize,
|
||||
totalSize: data.MediaContainer.totalSize,
|
||||
items: filteredList,
|
||||
};
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import ExternalAPI from './externalapi';
|
||||
import ExternalAPI from '@server/api/externalapi';
|
||||
|
||||
interface PushoverSoundsResponse {
|
||||
sounds: {
|
||||
@@ -26,24 +26,13 @@ export const mapSounds = (sounds: {
|
||||
|
||||
class PushoverAPI extends ExternalAPI {
|
||||
constructor() {
|
||||
super(
|
||||
'https://api.pushover.net/1',
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
super('https://api.pushover.net/1');
|
||||
}
|
||||
|
||||
public async getSounds(appToken: string): Promise<PushoverSound[]> {
|
||||
try {
|
||||
const data = await this.get<PushoverSoundsResponse>('/sounds.json', {
|
||||
params: {
|
||||
token: appToken,
|
||||
},
|
||||
token: appToken,
|
||||
});
|
||||
|
||||
return mapSounds(data.sounds);
|
||||
|
||||
@@ -155,13 +155,13 @@ export interface IMDBRating {
|
||||
*/
|
||||
class IMDBRadarrProxy extends ExternalAPI {
|
||||
constructor() {
|
||||
super('https://api.radarr.video/v1', {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
nodeCache: cacheManager.getCache('imdb').data,
|
||||
});
|
||||
super(
|
||||
'https://api.radarr.video/v1',
|
||||
{},
|
||||
{
|
||||
nodeCache: cacheManager.getCache('imdb').data,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -63,15 +63,12 @@ class RottenTomatoes extends ExternalAPI {
|
||||
super(
|
||||
'https://79frdp12pn-dsn.algolia.net/1/indexes/*',
|
||||
{
|
||||
'x-algolia-agent':
|
||||
'Algolia%20for%20JavaScript%20(4.14.3)%3B%20Browser%20(lite)',
|
||||
'x-algolia-agent': 'Algolia for JavaScript (4.14.3); Browser (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,
|
||||
|
||||
@@ -113,9 +113,9 @@ class ServarrBase<QueueItemAppendT> extends ExternalAPI {
|
||||
|
||||
public getSystemStatus = async (): Promise<SystemStatus> => {
|
||||
try {
|
||||
const response = await this.axios.get<SystemStatus>('/system/status');
|
||||
const data = await this.get<SystemStatus>('/system/status');
|
||||
|
||||
return response.data;
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`[${this.apiName}] Failed to retrieve system status: ${e.message}`
|
||||
@@ -157,16 +157,11 @@ class ServarrBase<QueueItemAppendT> extends ExternalAPI {
|
||||
|
||||
public getQueue = async (): Promise<(QueueItem & QueueItemAppendT)[]> => {
|
||||
try {
|
||||
const response = await this.axios.get<QueueResponse<QueueItemAppendT>>(
|
||||
`/queue`,
|
||||
{
|
||||
params: {
|
||||
includeEpisode: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
const data = await this.get<QueueResponse<QueueItemAppendT>>(`/queue`, {
|
||||
includeEpisode: 'true',
|
||||
});
|
||||
|
||||
return response.data.records;
|
||||
return data.records;
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`[${this.apiName}] Failed to retrieve queue: ${e.message}`
|
||||
@@ -176,9 +171,9 @@ class ServarrBase<QueueItemAppendT> extends ExternalAPI {
|
||||
|
||||
public getTags = async (): Promise<Tag[]> => {
|
||||
try {
|
||||
const response = await this.axios.get<Tag[]>(`/tag`);
|
||||
const data = await this.get<Tag[]>(`/tag`);
|
||||
|
||||
return response.data;
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`[${this.apiName}] Failed to retrieve tags: ${e.message}`
|
||||
@@ -188,11 +183,11 @@ class ServarrBase<QueueItemAppendT> extends ExternalAPI {
|
||||
|
||||
public createTag = async ({ label }: { label: string }): Promise<Tag> => {
|
||||
try {
|
||||
const response = await this.axios.post<Tag>(`/tag`, {
|
||||
const data = await this.post<Tag>(`/tag`, {
|
||||
label,
|
||||
});
|
||||
|
||||
return response.data;
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[${this.apiName}] Failed to create tag: ${e.message}`);
|
||||
}
|
||||
@@ -203,7 +198,7 @@ class ServarrBase<QueueItemAppendT> extends ExternalAPI {
|
||||
options: Record<string, unknown>
|
||||
): Promise<void> {
|
||||
try {
|
||||
await this.axios.post(`/command`, {
|
||||
await this.post(`/command`, {
|
||||
name: commandName,
|
||||
...options,
|
||||
});
|
||||
|
||||
@@ -37,9 +37,9 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
|
||||
|
||||
public getMovies = async (): Promise<RadarrMovie[]> => {
|
||||
try {
|
||||
const response = await this.axios.get<RadarrMovie[]>('/movie');
|
||||
const data = await this.get<RadarrMovie[]>('/movie');
|
||||
|
||||
return response.data;
|
||||
return 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 response = await this.axios.get<RadarrMovie>(`/movie/${id}`);
|
||||
const data = await this.get<RadarrMovie>(`/movie/${id}`);
|
||||
|
||||
return response.data;
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[Radarr] Failed to retrieve movie: ${e.message}`);
|
||||
}
|
||||
@@ -57,17 +57,15 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
|
||||
|
||||
public async getMovieByTmdbId(id: number): Promise<RadarrMovie> {
|
||||
try {
|
||||
const response = await this.axios.get<RadarrMovie[]>('/movie/lookup', {
|
||||
params: {
|
||||
term: `tmdb:${id}`,
|
||||
},
|
||||
const data = await this.get<RadarrMovie[]>('/movie/lookup', {
|
||||
term: `tmdb:${id}`,
|
||||
});
|
||||
|
||||
if (!response.data[0]) {
|
||||
if (!data[0]) {
|
||||
throw new Error('Movie not found');
|
||||
}
|
||||
|
||||
return response.data[0];
|
||||
return data[0];
|
||||
} catch (e) {
|
||||
logger.error('Error retrieving movie by TMDB ID', {
|
||||
label: 'Radarr API',
|
||||
@@ -97,7 +95,7 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
|
||||
|
||||
// movie exists in Radarr but is neither downloaded nor monitored
|
||||
if (movie.id && !movie.monitored) {
|
||||
const response = await this.axios.put<RadarrMovie>(`/movie`, {
|
||||
const data = await this.put<RadarrMovie>(`/movie`, {
|
||||
...movie,
|
||||
title: options.title,
|
||||
qualityProfileId: options.qualityProfileId,
|
||||
@@ -114,25 +112,25 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
|
||||
},
|
||||
});
|
||||
|
||||
if (response.data.monitored) {
|
||||
if (data.monitored) {
|
||||
logger.info(
|
||||
'Found existing title in Radarr and set it to monitored.',
|
||||
{
|
||||
label: 'Radarr',
|
||||
movieId: response.data.id,
|
||||
movieTitle: response.data.title,
|
||||
movieId: data.id,
|
||||
movieTitle: data.title,
|
||||
}
|
||||
);
|
||||
logger.debug('Radarr update details', {
|
||||
label: 'Radarr',
|
||||
movie: response.data,
|
||||
movie: data,
|
||||
});
|
||||
|
||||
if (options.searchNow) {
|
||||
this.searchMovie(response.data.id);
|
||||
this.searchMovie(data.id);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
return data;
|
||||
} else {
|
||||
logger.error('Failed to update existing movie in Radarr.', {
|
||||
label: 'Radarr',
|
||||
@@ -150,7 +148,7 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
|
||||
return movie;
|
||||
}
|
||||
|
||||
const response = await this.axios.post<RadarrMovie>(`/movie`, {
|
||||
const data = await this.post<RadarrMovie>(`/movie`, {
|
||||
title: options.title,
|
||||
qualityProfileId: options.qualityProfileId,
|
||||
profileId: options.profileId,
|
||||
@@ -166,11 +164,11 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
|
||||
},
|
||||
});
|
||||
|
||||
if (response.data.id) {
|
||||
if (data.id) {
|
||||
logger.info('Radarr accepted request', { label: 'Radarr' });
|
||||
logger.debug('Radarr add details', {
|
||||
label: 'Radarr',
|
||||
movie: response.data,
|
||||
movie: data,
|
||||
});
|
||||
} else {
|
||||
logger.error('Failed to add movie to Radarr', {
|
||||
@@ -179,15 +177,22 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
|
||||
});
|
||||
throw new Error('Failed to add movie to Radarr');
|
||||
}
|
||||
return response.data;
|
||||
return data;
|
||||
} catch (e) {
|
||||
let errorData;
|
||||
try {
|
||||
errorData = await e.cause?.text();
|
||||
errorData = JSON.parse(errorData);
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
logger.error(
|
||||
'Failed to add movie to Radarr. This might happen if the movie already exists, in which case you can safely ignore this error.',
|
||||
{
|
||||
label: 'Radarr',
|
||||
errorMessage: e.message,
|
||||
options,
|
||||
response: e?.response?.data,
|
||||
response: errorData,
|
||||
}
|
||||
);
|
||||
throw new Error('Failed to add movie to Radarr');
|
||||
@@ -216,11 +221,9 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
|
||||
public removeMovie = async (movieId: number): Promise<void> => {
|
||||
try {
|
||||
const { id, title } = await this.getMovieByTmdbId(movieId);
|
||||
await this.axios.delete(`/movie/${id}`, {
|
||||
params: {
|
||||
deleteFiles: true,
|
||||
addImportExclusion: false,
|
||||
},
|
||||
await this.delete(`/movie/${id}`, {
|
||||
deleteFiles: 'true',
|
||||
addImportExclusion: 'false',
|
||||
});
|
||||
logger.info(`[Radarr] Removed movie ${title}`);
|
||||
} catch (e) {
|
||||
|
||||
@@ -117,9 +117,9 @@ class SonarrAPI extends ServarrBase<{
|
||||
|
||||
public async getSeries(): Promise<SonarrSeries[]> {
|
||||
try {
|
||||
const response = await this.axios.get<SonarrSeries[]>('/series');
|
||||
const data = await this.get<SonarrSeries[]>('/series');
|
||||
|
||||
return response.data;
|
||||
return 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 response = await this.axios.get<SonarrSeries>(`/series/${id}`);
|
||||
const data = await this.get<SonarrSeries>(`/series/${id}`);
|
||||
|
||||
return response.data;
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[Sonarr] Failed to retrieve series by ID: ${e.message}`);
|
||||
}
|
||||
@@ -137,17 +137,15 @@ class SonarrAPI extends ServarrBase<{
|
||||
|
||||
public async getSeriesByTitle(title: string): Promise<SonarrSeries[]> {
|
||||
try {
|
||||
const response = await this.axios.get<SonarrSeries[]>('/series/lookup', {
|
||||
params: {
|
||||
term: title,
|
||||
},
|
||||
const data = await this.get<SonarrSeries[]>('/series/lookup', {
|
||||
term: title,
|
||||
});
|
||||
|
||||
if (!response.data[0]) {
|
||||
if (!data[0]) {
|
||||
throw new Error('No series found');
|
||||
}
|
||||
|
||||
return response.data;
|
||||
return data;
|
||||
} catch (e) {
|
||||
logger.error('Error retrieving series by series title', {
|
||||
label: 'Sonarr API',
|
||||
@@ -160,17 +158,15 @@ class SonarrAPI extends ServarrBase<{
|
||||
|
||||
public async getSeriesByTvdbId(id: number): Promise<SonarrSeries> {
|
||||
try {
|
||||
const response = await this.axios.get<SonarrSeries[]>('/series/lookup', {
|
||||
params: {
|
||||
term: `tvdb:${id}`,
|
||||
},
|
||||
const data = await this.get<SonarrSeries[]>('/series/lookup', {
|
||||
term: `tvdb:${id}`,
|
||||
});
|
||||
|
||||
if (!response.data[0]) {
|
||||
if (!data[0]) {
|
||||
throw new Error('Series not found');
|
||||
}
|
||||
|
||||
return response.data[0];
|
||||
return data[0];
|
||||
} catch (e) {
|
||||
logger.error('Error retrieving series by tvdb ID', {
|
||||
label: 'Sonarr API',
|
||||
@@ -191,27 +187,27 @@ class SonarrAPI extends ServarrBase<{
|
||||
series.tags = options.tags ?? series.tags;
|
||||
series.seasons = this.buildSeasonList(options.seasons, series.seasons);
|
||||
|
||||
const newSeriesResponse = await this.axios.put<SonarrSeries>(
|
||||
const newSeriesData = await this.put<SonarrSeries>(
|
||||
'/series',
|
||||
series
|
||||
series as any
|
||||
);
|
||||
|
||||
if (newSeriesResponse.data.id) {
|
||||
if (newSeriesData.id) {
|
||||
logger.info('Updated existing series in Sonarr.', {
|
||||
label: 'Sonarr',
|
||||
seriesId: newSeriesResponse.data.id,
|
||||
seriesTitle: newSeriesResponse.data.title,
|
||||
seriesId: newSeriesData.id,
|
||||
seriesTitle: newSeriesData.title,
|
||||
});
|
||||
logger.debug('Sonarr update details', {
|
||||
label: 'Sonarr',
|
||||
movie: newSeriesResponse.data,
|
||||
movie: newSeriesData,
|
||||
});
|
||||
|
||||
if (options.searchNow) {
|
||||
this.searchSeries(newSeriesResponse.data.id);
|
||||
this.searchSeries(newSeriesData.id);
|
||||
}
|
||||
|
||||
return newSeriesResponse.data;
|
||||
return newSeriesData;
|
||||
} else {
|
||||
logger.error('Failed to update series in Sonarr', {
|
||||
label: 'Sonarr',
|
||||
@@ -221,38 +217,35 @@ class SonarrAPI extends ServarrBase<{
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
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>);
|
||||
|
||||
if (createdSeriesResponse.data.id) {
|
||||
if (createdSeriesData.id) {
|
||||
logger.info('Sonarr accepted request', { label: 'Sonarr' });
|
||||
logger.debug('Sonarr add details', {
|
||||
label: 'Sonarr',
|
||||
movie: createdSeriesResponse.data,
|
||||
movie: createdSeriesData,
|
||||
});
|
||||
} else {
|
||||
logger.error('Failed to add movie to Sonarr', {
|
||||
@@ -262,13 +255,20 @@ class SonarrAPI extends ServarrBase<{
|
||||
throw new Error('Failed to add series to Sonarr');
|
||||
}
|
||||
|
||||
return createdSeriesResponse.data;
|
||||
return createdSeriesData;
|
||||
} catch (e) {
|
||||
let errorData;
|
||||
try {
|
||||
errorData = await e.cause?.text();
|
||||
errorData = JSON.parse(errorData);
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
logger.error('Something went wrong while adding a series to Sonarr.', {
|
||||
label: 'Sonarr API',
|
||||
errorMessage: e.message,
|
||||
options,
|
||||
response: e?.response?.data,
|
||||
response: errorData,
|
||||
});
|
||||
throw new Error('Failed to add series');
|
||||
}
|
||||
@@ -340,14 +340,13 @@ class SonarrAPI extends ServarrBase<{
|
||||
|
||||
return newSeasons;
|
||||
}
|
||||
|
||||
public removeSerie = async (serieId: number): Promise<void> => {
|
||||
try {
|
||||
const { id, title } = await this.getSeriesByTvdbId(serieId);
|
||||
await this.axios.delete(`/series/${id}`, {
|
||||
params: {
|
||||
deleteFiles: true,
|
||||
addImportExclusion: false,
|
||||
},
|
||||
await this.delete(`/series/${id}`, {
|
||||
deleteFiles: 'true',
|
||||
addImportExclusion: 'false',
|
||||
});
|
||||
logger.info(`[Radarr] Removed serie ${title}`);
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
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 {
|
||||
@@ -113,25 +112,25 @@ interface TautulliInfoResponse {
|
||||
};
|
||||
}
|
||||
|
||||
class TautulliAPI {
|
||||
private axios: AxiosInstance;
|
||||
|
||||
class TautulliAPI extends ExternalAPI {
|
||||
constructor(settings: TautulliSettings) {
|
||||
this.axios = axios.create({
|
||||
baseURL: `${settings.useSsl ? 'https' : 'http'}://${settings.hostname}:${
|
||||
super(
|
||||
`${settings.useSsl ? 'https' : 'http'}://${settings.hostname}:${
|
||||
settings.port
|
||||
}${settings.urlBase ?? ''}`,
|
||||
params: { apikey: settings.apiKey },
|
||||
});
|
||||
{
|
||||
apikey: settings.apiKey || '',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public async getInfo(): Promise<TautulliInfo> {
|
||||
try {
|
||||
return (
|
||||
await this.axios.get<TautulliInfoResponse>('/api/v2', {
|
||||
params: { cmd: 'get_tautulli_info' },
|
||||
await this.get<TautulliInfoResponse>('/api/v2', {
|
||||
cmd: 'get_tautulli_info',
|
||||
})
|
||||
).data.response.data;
|
||||
).response.data;
|
||||
} catch (e) {
|
||||
logger.error('Something went wrong fetching Tautulli server info', {
|
||||
label: 'Tautulli API',
|
||||
@@ -148,14 +147,12 @@ class TautulliAPI {
|
||||
): Promise<TautulliWatchStats[]> {
|
||||
try {
|
||||
return (
|
||||
await this.axios.get<TautulliWatchStatsResponse>('/api/v2', {
|
||||
params: {
|
||||
cmd: 'get_item_watch_time_stats',
|
||||
rating_key: ratingKey,
|
||||
grouping: 1,
|
||||
},
|
||||
await this.get<TautulliWatchStatsResponse>('/api/v2', {
|
||||
cmd: 'get_item_watch_time_stats',
|
||||
rating_key: ratingKey,
|
||||
grouping: '1',
|
||||
})
|
||||
).data.response.data;
|
||||
).response.data;
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
'Something went wrong fetching media watch stats from Tautulli',
|
||||
@@ -176,14 +173,12 @@ class TautulliAPI {
|
||||
): Promise<TautulliWatchUser[]> {
|
||||
try {
|
||||
return (
|
||||
await this.axios.get<TautulliWatchUsersResponse>('/api/v2', {
|
||||
params: {
|
||||
cmd: 'get_item_user_stats',
|
||||
rating_key: ratingKey,
|
||||
grouping: 1,
|
||||
},
|
||||
await this.get<TautulliWatchUsersResponse>('/api/v2', {
|
||||
cmd: 'get_item_user_stats',
|
||||
rating_key: ratingKey,
|
||||
grouping: '1',
|
||||
})
|
||||
).data.response.data;
|
||||
).response.data;
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
'Something went wrong fetching media watch users from Tautulli',
|
||||
@@ -206,15 +201,13 @@ class TautulliAPI {
|
||||
}
|
||||
|
||||
return (
|
||||
await this.axios.get<TautulliWatchStatsResponse>('/api/v2', {
|
||||
params: {
|
||||
cmd: 'get_user_watch_time_stats',
|
||||
user_id: user.plexId,
|
||||
query_days: 0,
|
||||
grouping: 1,
|
||||
},
|
||||
await this.get<TautulliWatchStatsResponse>('/api/v2', {
|
||||
cmd: 'get_user_watch_time_stats',
|
||||
user_id: user.plexId.toString(),
|
||||
query_days: '0',
|
||||
grouping: '1',
|
||||
})
|
||||
).data.response.data[0];
|
||||
).response.data[0];
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
'Something went wrong fetching user watch stats from Tautulli',
|
||||
@@ -245,19 +238,17 @@ class TautulliAPI {
|
||||
|
||||
while (results.length < 20) {
|
||||
const tautulliData = (
|
||||
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,
|
||||
},
|
||||
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(),
|
||||
})
|
||||
).data.response.data.data;
|
||||
).response.data.data;
|
||||
|
||||
if (!tautulliData.length) {
|
||||
return results;
|
||||
|
||||
@@ -112,8 +112,8 @@ class TheMovieDb extends ExternalAPI {
|
||||
{
|
||||
nodeCache: cacheManager.getCache('tmdb').data,
|
||||
rateLimit: {
|
||||
maxRequests: 20,
|
||||
maxRPS: 50,
|
||||
id: 'tmdb',
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -129,7 +129,10 @@ class TheMovieDb extends ExternalAPI {
|
||||
}: SearchOptions): Promise<TmdbSearchMultiResponse> => {
|
||||
try {
|
||||
const data = await this.get<TmdbSearchMultiResponse>('/search/multi', {
|
||||
params: { query, page, include_adult: includeAdult, language },
|
||||
query,
|
||||
page: page.toString(),
|
||||
include_adult: includeAdult ? 'true' : 'false',
|
||||
language,
|
||||
});
|
||||
|
||||
return data;
|
||||
@@ -152,13 +155,11 @@ class TheMovieDb extends ExternalAPI {
|
||||
}: SingleSearchOptions): Promise<TmdbSearchMovieResponse> => {
|
||||
try {
|
||||
const data = await this.get<TmdbSearchMovieResponse>('/search/movie', {
|
||||
params: {
|
||||
query,
|
||||
page,
|
||||
include_adult: includeAdult,
|
||||
language,
|
||||
primary_release_year: year,
|
||||
},
|
||||
query,
|
||||
page: page.toString(),
|
||||
include_adult: includeAdult ? 'true' : 'false',
|
||||
language,
|
||||
primary_release_year: year?.toString() || '',
|
||||
});
|
||||
|
||||
return data;
|
||||
@@ -181,13 +182,11 @@ class TheMovieDb extends ExternalAPI {
|
||||
}: SingleSearchOptions): Promise<TmdbSearchTvResponse> => {
|
||||
try {
|
||||
const data = await this.get<TmdbSearchTvResponse>('/search/tv', {
|
||||
params: {
|
||||
query,
|
||||
page,
|
||||
include_adult: includeAdult,
|
||||
language,
|
||||
first_air_date_year: year,
|
||||
},
|
||||
query,
|
||||
page: page.toString(),
|
||||
include_adult: includeAdult ? 'true' : 'false',
|
||||
language,
|
||||
first_air_date_year: year?.toString() || '',
|
||||
});
|
||||
|
||||
return data;
|
||||
@@ -210,7 +209,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
}): Promise<TmdbPersonDetails> => {
|
||||
try {
|
||||
const data = await this.get<TmdbPersonDetails>(`/person/${personId}`, {
|
||||
params: { language },
|
||||
language,
|
||||
});
|
||||
|
||||
return data;
|
||||
@@ -230,7 +229,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
const data = await this.get<TmdbPersonCombinedCredits>(
|
||||
`/person/${personId}/combined_credits`,
|
||||
{
|
||||
params: { language },
|
||||
language,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -253,11 +252,9 @@ class TheMovieDb extends ExternalAPI {
|
||||
const data = await this.get<TmdbMovieDetails>(
|
||||
`/movie/${movieId}`,
|
||||
{
|
||||
params: {
|
||||
language,
|
||||
append_to_response:
|
||||
'credits,external_ids,videos,keywords,release_dates,watch/providers',
|
||||
},
|
||||
language,
|
||||
append_to_response:
|
||||
'credits,external_ids,videos,keywords,release_dates,watch/providers',
|
||||
},
|
||||
43200
|
||||
);
|
||||
@@ -279,11 +276,9 @@ class TheMovieDb extends ExternalAPI {
|
||||
const data = await this.get<TmdbTvDetails>(
|
||||
`/tv/${tvId}`,
|
||||
{
|
||||
params: {
|
||||
language,
|
||||
append_to_response:
|
||||
'aggregate_credits,credits,external_ids,keywords,videos,content_ratings,watch/providers',
|
||||
},
|
||||
language,
|
||||
append_to_response:
|
||||
'aggregate_credits,credits,external_ids,keywords,videos,content_ratings,watch/providers',
|
||||
},
|
||||
43200
|
||||
);
|
||||
@@ -307,10 +302,8 @@ class TheMovieDb extends ExternalAPI {
|
||||
const data = await this.get<TmdbSeasonWithEpisodes>(
|
||||
`/tv/${tvId}/season/${seasonNumber}`,
|
||||
{
|
||||
params: {
|
||||
language,
|
||||
append_to_response: 'external_ids',
|
||||
},
|
||||
language: language || '',
|
||||
append_to_response: 'external_ids',
|
||||
}
|
||||
);
|
||||
|
||||
@@ -333,10 +326,8 @@ class TheMovieDb extends ExternalAPI {
|
||||
const data = await this.get<TmdbSearchMovieResponse>(
|
||||
`/movie/${movieId}/recommendations`,
|
||||
{
|
||||
params: {
|
||||
page,
|
||||
language,
|
||||
},
|
||||
page: page.toString(),
|
||||
language,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -359,10 +350,8 @@ class TheMovieDb extends ExternalAPI {
|
||||
const data = await this.get<TmdbSearchMovieResponse>(
|
||||
`/movie/${movieId}/similar`,
|
||||
{
|
||||
params: {
|
||||
page,
|
||||
language,
|
||||
},
|
||||
page: page.toString(),
|
||||
language,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -385,10 +374,8 @@ class TheMovieDb extends ExternalAPI {
|
||||
const data = await this.get<TmdbSearchMovieResponse>(
|
||||
`/keyword/${keywordId}/movies`,
|
||||
{
|
||||
params: {
|
||||
page,
|
||||
language,
|
||||
},
|
||||
page: page.toString(),
|
||||
language,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -411,10 +398,8 @@ class TheMovieDb extends ExternalAPI {
|
||||
const data = await this.get<TmdbSearchTvResponse>(
|
||||
`/tv/${tvId}/recommendations`,
|
||||
{
|
||||
params: {
|
||||
page,
|
||||
language,
|
||||
},
|
||||
page: page.toString(),
|
||||
language,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -437,10 +422,8 @@ class TheMovieDb extends ExternalAPI {
|
||||
}): Promise<TmdbSearchTvResponse> {
|
||||
try {
|
||||
const data = await this.get<TmdbSearchTvResponse>(`/tv/${tvId}/similar`, {
|
||||
params: {
|
||||
page,
|
||||
language,
|
||||
},
|
||||
page: page.toString(),
|
||||
language,
|
||||
});
|
||||
|
||||
return data;
|
||||
@@ -481,40 +464,38 @@ class TheMovieDb extends ExternalAPI {
|
||||
.split('T')[0];
|
||||
|
||||
const data = await this.get<TmdbSearchMovieResponse>('/discover/movie', {
|
||||
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,
|
||||
},
|
||||
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 || '',
|
||||
});
|
||||
|
||||
return data;
|
||||
@@ -555,40 +536,40 @@ class TheMovieDb extends ExternalAPI {
|
||||
.split('T')[0];
|
||||
|
||||
const data = await this.get<TmdbSearchTvResponse>('/discover/tv', {
|
||||
params: {
|
||||
sort_by: sortBy,
|
||||
page,
|
||||
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'
|
||||
? undefined
|
||||
: this.originalLanguage,
|
||||
include_null_first_air_dates: includeEmptyReleaseDate,
|
||||
with_genres: genre,
|
||||
with_networks: network,
|
||||
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,
|
||||
},
|
||||
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 || '',
|
||||
});
|
||||
|
||||
return data;
|
||||
@@ -608,12 +589,10 @@ class TheMovieDb extends ExternalAPI {
|
||||
const data = await this.get<TmdbUpcomingMoviesResponse>(
|
||||
'/movie/upcoming',
|
||||
{
|
||||
params: {
|
||||
page,
|
||||
language,
|
||||
region: this.region,
|
||||
originalLanguage: this.originalLanguage,
|
||||
},
|
||||
page: page.toString(),
|
||||
language,
|
||||
region: this.region || '',
|
||||
originalLanguage: this.originalLanguage || '',
|
||||
}
|
||||
);
|
||||
|
||||
@@ -636,11 +615,9 @@ class TheMovieDb extends ExternalAPI {
|
||||
const data = await this.get<TmdbSearchMultiResponse>(
|
||||
`/trending/all/${timeWindow}`,
|
||||
{
|
||||
params: {
|
||||
page,
|
||||
language,
|
||||
region: this.region,
|
||||
},
|
||||
page: page.toString(),
|
||||
language,
|
||||
region: this.region || '',
|
||||
}
|
||||
);
|
||||
|
||||
@@ -661,9 +638,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
const data = await this.get<TmdbSearchMovieResponse>(
|
||||
`/trending/movie/${timeWindow}`,
|
||||
{
|
||||
params: {
|
||||
page,
|
||||
},
|
||||
page: page.toString(),
|
||||
}
|
||||
);
|
||||
|
||||
@@ -684,9 +659,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
const data = await this.get<TmdbSearchTvResponse>(
|
||||
`/trending/tv/${timeWindow}`,
|
||||
{
|
||||
params: {
|
||||
page,
|
||||
},
|
||||
page: page.toString(),
|
||||
}
|
||||
);
|
||||
|
||||
@@ -715,10 +688,8 @@ class TheMovieDb extends ExternalAPI {
|
||||
const data = await this.get<TmdbExternalIdResponse>(
|
||||
`/find/${externalId}`,
|
||||
{
|
||||
params: {
|
||||
external_source: type === 'imdb' ? 'imdb_id' : 'tvdb_id',
|
||||
language,
|
||||
},
|
||||
external_source: type === 'imdb' ? 'imdb_id' : 'tvdb_id',
|
||||
language,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -808,9 +779,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
const data = await this.get<TmdbCollection>(
|
||||
`/collection/${collectionId}`,
|
||||
{
|
||||
params: {
|
||||
language,
|
||||
},
|
||||
language,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -883,9 +852,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
const data = await this.get<TmdbGenresResult>(
|
||||
'/genre/movie/list',
|
||||
{
|
||||
params: {
|
||||
language,
|
||||
},
|
||||
language,
|
||||
},
|
||||
86400 // 24 hours
|
||||
);
|
||||
@@ -897,9 +864,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
const englishData = await this.get<TmdbGenresResult>(
|
||||
'/genre/movie/list',
|
||||
{
|
||||
params: {
|
||||
language: 'en',
|
||||
},
|
||||
language: 'en',
|
||||
},
|
||||
86400 // 24 hours
|
||||
);
|
||||
@@ -934,9 +899,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
const data = await this.get<TmdbGenresResult>(
|
||||
'/genre/tv/list',
|
||||
{
|
||||
params: {
|
||||
language,
|
||||
},
|
||||
language,
|
||||
},
|
||||
86400 // 24 hours
|
||||
);
|
||||
@@ -948,9 +911,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
const englishData = await this.get<TmdbGenresResult>(
|
||||
'/genre/tv/list',
|
||||
{
|
||||
params: {
|
||||
language: 'en',
|
||||
},
|
||||
language: 'en',
|
||||
},
|
||||
86400 // 24 hours
|
||||
);
|
||||
@@ -1005,10 +966,8 @@ class TheMovieDb extends ExternalAPI {
|
||||
const data = await this.get<TmdbKeywordSearchResponse>(
|
||||
'/search/keyword',
|
||||
{
|
||||
params: {
|
||||
query,
|
||||
page,
|
||||
},
|
||||
query,
|
||||
page: page.toString(),
|
||||
},
|
||||
86400 // 24 hours
|
||||
);
|
||||
@@ -1030,10 +989,8 @@ class TheMovieDb extends ExternalAPI {
|
||||
const data = await this.get<TmdbCompanySearchResponse>(
|
||||
'/search/company',
|
||||
{
|
||||
params: {
|
||||
query,
|
||||
page,
|
||||
},
|
||||
query,
|
||||
page: page.toString(),
|
||||
},
|
||||
86400 // 24 hours
|
||||
);
|
||||
@@ -1053,9 +1010,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
const data = await this.get<{ results: TmdbWatchProviderRegion[] }>(
|
||||
'/watch/providers/regions',
|
||||
{
|
||||
params: {
|
||||
language: language ?? this.originalLanguage,
|
||||
},
|
||||
language: language ? this.originalLanguage || '' : '',
|
||||
},
|
||||
86400 // 24 hours
|
||||
);
|
||||
@@ -1079,10 +1034,8 @@ class TheMovieDb extends ExternalAPI {
|
||||
const data = await this.get<{ results: TmdbWatchProviderDetails[] }>(
|
||||
'/watch/providers/movie',
|
||||
{
|
||||
params: {
|
||||
language: language ?? this.originalLanguage,
|
||||
watch_region: watchRegion,
|
||||
},
|
||||
language: language ? this.originalLanguage || '' : '',
|
||||
watch_region: watchRegion,
|
||||
},
|
||||
86400 // 24 hours
|
||||
);
|
||||
@@ -1106,10 +1059,8 @@ class TheMovieDb extends ExternalAPI {
|
||||
const data = await this.get<{ results: TmdbWatchProviderDetails[] }>(
|
||||
'/watch/providers/tv',
|
||||
{
|
||||
params: {
|
||||
language: language ?? this.originalLanguage,
|
||||
watch_region: watchRegion,
|
||||
},
|
||||
language: language ? this.originalLanguage || '' : '',
|
||||
watch_region: watchRegion,
|
||||
},
|
||||
86400 // 24 hours
|
||||
);
|
||||
|
||||
@@ -4,3 +4,8 @@ export enum MediaServerType {
|
||||
EMBY,
|
||||
NOT_CONFIGURED,
|
||||
}
|
||||
|
||||
export enum ServerType {
|
||||
JELLYFIN = 'Jellyfin',
|
||||
EMBY = 'Emby',
|
||||
}
|
||||
|
||||
@@ -2,4 +2,5 @@ export enum UserType {
|
||||
PLEX = 1,
|
||||
LOCAL = 2,
|
||||
JELLYFIN = 3,
|
||||
EMBY = 4,
|
||||
}
|
||||
|
||||
@@ -211,9 +211,10 @@ class Media {
|
||||
}
|
||||
} else {
|
||||
const pageName =
|
||||
process.env.JELLYFIN_TYPE === 'emby' ? 'item' : 'details';
|
||||
getSettings().main.mediaServerType == MediaServerType.EMBY
|
||||
? 'item'
|
||||
: 'details';
|
||||
const { serverId, externalHostname } = getSettings().jellyfin;
|
||||
|
||||
const jellyfinHost =
|
||||
externalHostname && externalHostname.length > 0
|
||||
? externalHostname
|
||||
|
||||
@@ -32,10 +32,17 @@ 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()}`);
|
||||
@@ -56,7 +63,7 @@ app
|
||||
}
|
||||
|
||||
// Load Settings
|
||||
const settings = getSettings().load();
|
||||
const settings = getSettings();
|
||||
restartFlag.initializeSettings(settings.main);
|
||||
|
||||
// Migrate library types
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import logger from '@server/logger';
|
||||
import axios from 'axios';
|
||||
import rateLimit, { type rateLimitOptions } from 'axios-rate-limit';
|
||||
import type { RateLimitOptions } from '@server/utils/rateLimit';
|
||||
import rateLimit from '@server/utils/rateLimit';
|
||||
import { createHash } from 'crypto';
|
||||
import { promises } from 'fs';
|
||||
import path, { join } from 'path';
|
||||
@@ -98,26 +98,29 @@ class ImageProxy {
|
||||
return files.length;
|
||||
}
|
||||
|
||||
private axios;
|
||||
private fetch: typeof fetch;
|
||||
private cacheVersion;
|
||||
private key;
|
||||
private baseUrl;
|
||||
|
||||
constructor(
|
||||
key: string,
|
||||
baseUrl: string,
|
||||
options: {
|
||||
cacheVersion?: number;
|
||||
rateLimitOptions?: rateLimitOptions;
|
||||
rateLimitOptions?: RateLimitOptions;
|
||||
} = {}
|
||||
) {
|
||||
this.cacheVersion = options.cacheVersion ?? 1;
|
||||
this.baseUrl = baseUrl;
|
||||
this.key = key;
|
||||
this.axios = axios.create({
|
||||
baseURL: baseUrl,
|
||||
});
|
||||
|
||||
if (options.rateLimitOptions) {
|
||||
this.axios = rateLimit(this.axios, options.rateLimitOptions);
|
||||
this.fetch = rateLimit(fetch, {
|
||||
...options.rateLimitOptions,
|
||||
});
|
||||
} else {
|
||||
this.fetch = fetch;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,17 +185,20 @@ class ImageProxy {
|
||||
): Promise<ImageResponse | null> {
|
||||
try {
|
||||
const directory = join(this.getCacheDirectory(), cacheKey);
|
||||
const response = await this.axios.get(path, {
|
||||
responseType: 'arraybuffer',
|
||||
});
|
||||
const href =
|
||||
this.baseUrl +
|
||||
(this.baseUrl.endsWith('/') ? '' : '/') +
|
||||
(path.startsWith('/') ? path.slice(1) : path);
|
||||
const response = await this.fetch(href);
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
const buffer = Buffer.from(response.data, 'binary');
|
||||
const extension = path.split('.').pop() ?? '';
|
||||
const maxAge = Number(
|
||||
(response.headers['cache-control'] ?? '0').split('=')[1]
|
||||
(response.headers.get('cache-control') ?? '0').split('=')[1]
|
||||
);
|
||||
const expireAt = Date.now() + maxAge * 1000;
|
||||
const etag = (response.headers.etag ?? '').replace(/"/g, '');
|
||||
const etag = (response.headers.get('etag') ?? '').replace(/"/g, '');
|
||||
|
||||
await this.writeToCacheDir(
|
||||
directory,
|
||||
|
||||
@@ -4,7 +4,6 @@ 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,
|
||||
@@ -292,23 +291,39 @@ class DiscordAgent
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
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 });
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
let errorData;
|
||||
try {
|
||||
errorData = await e.cause?.text();
|
||||
errorData = JSON.parse(errorData);
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
logger.error('Error sending Discord notification', {
|
||||
label: 'Notifications',
|
||||
type: Notification[type],
|
||||
subject: payload.subject,
|
||||
errorMessage: e.message,
|
||||
response: e.response?.data,
|
||||
response: errorData,
|
||||
});
|
||||
|
||||
return false;
|
||||
|
||||
@@ -2,7 +2,6 @@ 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';
|
||||
@@ -133,16 +132,32 @@ class GotifyAgent
|
||||
const endpoint = `${settings.options.url}/message?token=${settings.options.token}`;
|
||||
const notificationPayload = this.getNotificationPayload(type, payload);
|
||||
|
||||
await axios.post(endpoint, notificationPayload);
|
||||
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 });
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
let errorData;
|
||||
try {
|
||||
errorData = await e.cause?.text();
|
||||
errorData = JSON.parse(errorData);
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
logger.error('Error sending Gotify notification', {
|
||||
label: 'Notifications',
|
||||
type: Notification[type],
|
||||
subject: payload.subject,
|
||||
errorMessage: e.message,
|
||||
response: e.response?.data,
|
||||
response: errorData,
|
||||
});
|
||||
|
||||
return false;
|
||||
|
||||
@@ -3,7 +3,6 @@ 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';
|
||||
@@ -101,28 +100,39 @@ class LunaSeaAgent
|
||||
});
|
||||
|
||||
try {
|
||||
await axios.post(
|
||||
settings.options.webhookUrl,
|
||||
this.buildPayload(type, payload),
|
||||
settings.options.profileName
|
||||
const response = await fetch(settings.options.webhookUrl, {
|
||||
method: 'POST',
|
||||
headers: settings.options.profileName
|
||||
? {
|
||||
headers: {
|
||||
Authorization: `Basic ${Buffer.from(
|
||||
`${settings.options.profileName}:`
|
||||
).toString('base64')}`,
|
||||
},
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
: undefined
|
||||
);
|
||||
: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Basic ${Buffer.from(
|
||||
`${settings.options.profileName}:`
|
||||
).toString('base64')}`,
|
||||
},
|
||||
body: JSON.stringify(this.buildPayload(type, payload)),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(response.statusText, { cause: response });
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
let errorData;
|
||||
try {
|
||||
errorData = await e.cause?.text();
|
||||
errorData = JSON.parse(errorData);
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
logger.error('Error sending LunaSea notification', {
|
||||
label: 'Notifications',
|
||||
type: Notification[type],
|
||||
subject: payload.subject,
|
||||
errorMessage: e.message,
|
||||
response: e.response?.data,
|
||||
response: errorData,
|
||||
});
|
||||
|
||||
return false;
|
||||
|
||||
@@ -5,7 +5,6 @@ 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,
|
||||
@@ -123,22 +122,34 @@ class PushbulletAgent
|
||||
});
|
||||
|
||||
try {
|
||||
await axios.post(
|
||||
endpoint,
|
||||
{ ...notificationPayload, channel_tag: settings.options.channelTag },
|
||||
{
|
||||
headers: {
|
||||
'Access-Token': settings.options.accessToken,
|
||||
},
|
||||
}
|
||||
);
|
||||
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 });
|
||||
}
|
||||
} catch (e) {
|
||||
let errorData;
|
||||
try {
|
||||
errorData = await e.cause?.text();
|
||||
errorData = JSON.parse(errorData);
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
logger.error('Error sending Pushbullet notification', {
|
||||
label: 'Notifications',
|
||||
type: Notification[type],
|
||||
subject: payload.subject,
|
||||
errorMessage: e.message,
|
||||
response: e.response?.data,
|
||||
response: errorData,
|
||||
});
|
||||
|
||||
return false;
|
||||
@@ -163,19 +174,32 @@ class PushbulletAgent
|
||||
});
|
||||
|
||||
try {
|
||||
await axios.post(endpoint, notificationPayload, {
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
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 {
|
||||
errorData = await e.cause?.text();
|
||||
errorData = JSON.parse(errorData);
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
logger.error('Error sending Pushbullet notification', {
|
||||
label: 'Notifications',
|
||||
recipient: payload.notifyUser.displayName,
|
||||
type: Notification[type],
|
||||
subject: payload.subject,
|
||||
errorMessage: e.message,
|
||||
response: e.response?.data,
|
||||
response: errorData,
|
||||
});
|
||||
|
||||
return false;
|
||||
@@ -211,19 +235,32 @@ class PushbulletAgent
|
||||
});
|
||||
|
||||
try {
|
||||
await axios.post(endpoint, notificationPayload, {
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
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 {
|
||||
errorData = await e.cause?.text();
|
||||
errorData = JSON.parse(errorData);
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
logger.error('Error sending Pushbullet notification', {
|
||||
label: 'Notifications',
|
||||
recipient: user.displayName,
|
||||
type: Notification[type],
|
||||
subject: payload.subject,
|
||||
errorMessage: e.message,
|
||||
response: e.response?.data,
|
||||
response: errorData,
|
||||
});
|
||||
|
||||
return false;
|
||||
|
||||
@@ -5,7 +5,6 @@ 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,
|
||||
@@ -52,12 +51,15 @@ class PushoverAgent
|
||||
imageUrl: string
|
||||
): Promise<Partial<PushoverImagePayload>> {
|
||||
try {
|
||||
const response = await axios.get(imageUrl, {
|
||||
responseType: 'arraybuffer',
|
||||
});
|
||||
const base64 = Buffer.from(response.data, 'binary').toString('base64');
|
||||
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 contentType = (
|
||||
response.headers['Content-Type'] || response.headers['content-type']
|
||||
response.headers.get('Content-Type') ||
|
||||
response.headers.get('content-type')
|
||||
)?.toString();
|
||||
|
||||
return {
|
||||
@@ -65,10 +67,17 @@ class PushoverAgent
|
||||
attachment_type: contentType,
|
||||
};
|
||||
} catch (e) {
|
||||
let errorData;
|
||||
try {
|
||||
errorData = await e.cause?.text();
|
||||
errorData = JSON.parse(errorData);
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
logger.error('Error getting image payload', {
|
||||
label: 'Notifications',
|
||||
errorMessage: e.message,
|
||||
response: e.response?.data,
|
||||
response: errorData,
|
||||
});
|
||||
return {};
|
||||
}
|
||||
@@ -201,19 +210,35 @@ class PushoverAgent
|
||||
});
|
||||
|
||||
try {
|
||||
await axios.post(endpoint, {
|
||||
...notificationPayload,
|
||||
token: settings.options.accessToken,
|
||||
user: settings.options.userToken,
|
||||
sound: settings.options.sound,
|
||||
} as PushoverPayload);
|
||||
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 });
|
||||
}
|
||||
} catch (e) {
|
||||
let errorData;
|
||||
try {
|
||||
errorData = await e.cause?.text();
|
||||
errorData = JSON.parse(errorData);
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
logger.error('Error sending Pushover notification', {
|
||||
label: 'Notifications',
|
||||
type: Notification[type],
|
||||
subject: payload.subject,
|
||||
errorMessage: e.message,
|
||||
response: e.response?.data,
|
||||
response: errorData,
|
||||
});
|
||||
|
||||
return false;
|
||||
@@ -241,20 +266,36 @@ class PushoverAgent
|
||||
});
|
||||
|
||||
try {
|
||||
await axios.post(endpoint, {
|
||||
...notificationPayload,
|
||||
token: payload.notifyUser.settings.pushoverApplicationToken,
|
||||
user: payload.notifyUser.settings.pushoverUserKey,
|
||||
sound: payload.notifyUser.settings.pushoverSound,
|
||||
} as PushoverPayload);
|
||||
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 });
|
||||
}
|
||||
} catch (e) {
|
||||
let errorData;
|
||||
try {
|
||||
errorData = await e.cause?.text();
|
||||
errorData = JSON.parse(errorData);
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
logger.error('Error sending Pushover notification', {
|
||||
label: 'Notifications',
|
||||
recipient: payload.notifyUser.displayName,
|
||||
type: Notification[type],
|
||||
subject: payload.subject,
|
||||
errorMessage: e.message,
|
||||
response: e.response?.data,
|
||||
response: errorData,
|
||||
});
|
||||
|
||||
return false;
|
||||
@@ -291,19 +332,35 @@ class PushoverAgent
|
||||
});
|
||||
|
||||
try {
|
||||
await axios.post(endpoint, {
|
||||
...notificationPayload,
|
||||
token: user.settings.pushoverApplicationToken,
|
||||
user: user.settings.pushoverUserKey,
|
||||
} as PushoverPayload);
|
||||
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 });
|
||||
}
|
||||
} catch (e) {
|
||||
let errorData;
|
||||
try {
|
||||
errorData = await e.cause?.text();
|
||||
errorData = JSON.parse(errorData);
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
logger.error('Error sending Pushover notification', {
|
||||
label: 'Notifications',
|
||||
recipient: user.displayName,
|
||||
type: Notification[type],
|
||||
subject: payload.subject,
|
||||
errorMessage: e.message,
|
||||
response: e.response?.data,
|
||||
response: errorData,
|
||||
});
|
||||
|
||||
return false;
|
||||
|
||||
@@ -2,7 +2,6 @@ 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';
|
||||
@@ -238,19 +237,32 @@ class SlackAgent
|
||||
subject: payload.subject,
|
||||
});
|
||||
try {
|
||||
await axios.post(
|
||||
settings.options.webhookUrl,
|
||||
this.buildEmbed(type, payload)
|
||||
);
|
||||
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 });
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
let errorData;
|
||||
try {
|
||||
errorData = await e.cause?.text();
|
||||
errorData = JSON.parse(errorData);
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
logger.error('Error sending Slack notification', {
|
||||
label: 'Notifications',
|
||||
type: Notification[type],
|
||||
subject: payload.subject,
|
||||
errorMessage: e.message,
|
||||
response: e.response?.data,
|
||||
response: errorData,
|
||||
});
|
||||
|
||||
return false;
|
||||
|
||||
@@ -5,7 +5,6 @@ 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,
|
||||
@@ -175,18 +174,34 @@ class TelegramAgent
|
||||
});
|
||||
|
||||
try {
|
||||
await axios.post(endpoint, {
|
||||
...notificationPayload,
|
||||
chat_id: settings.options.chatId,
|
||||
disable_notification: !!settings.options.sendSilently,
|
||||
} as TelegramMessagePayload | TelegramPhotoPayload);
|
||||
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 });
|
||||
}
|
||||
} catch (e) {
|
||||
let errorData;
|
||||
try {
|
||||
errorData = await e.cause?.text();
|
||||
errorData = JSON.parse(errorData);
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
logger.error('Error sending Telegram notification', {
|
||||
label: 'Notifications',
|
||||
type: Notification[type],
|
||||
subject: payload.subject,
|
||||
errorMessage: e.message,
|
||||
response: e.response?.data,
|
||||
response: errorData,
|
||||
});
|
||||
|
||||
return false;
|
||||
@@ -210,20 +225,36 @@ class TelegramAgent
|
||||
});
|
||||
|
||||
try {
|
||||
await axios.post(endpoint, {
|
||||
...notificationPayload,
|
||||
chat_id: payload.notifyUser.settings.telegramChatId,
|
||||
disable_notification:
|
||||
!!payload.notifyUser.settings.telegramSendSilently,
|
||||
} as TelegramMessagePayload | TelegramPhotoPayload);
|
||||
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 });
|
||||
}
|
||||
} catch (e) {
|
||||
let errorData;
|
||||
try {
|
||||
errorData = await e.cause?.text();
|
||||
errorData = JSON.parse(errorData);
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
logger.error('Error sending Telegram notification', {
|
||||
label: 'Notifications',
|
||||
recipient: payload.notifyUser.displayName,
|
||||
type: Notification[type],
|
||||
subject: payload.subject,
|
||||
errorMessage: e.message,
|
||||
response: e.response?.data,
|
||||
response: errorData,
|
||||
});
|
||||
|
||||
return false;
|
||||
@@ -257,19 +288,35 @@ class TelegramAgent
|
||||
});
|
||||
|
||||
try {
|
||||
await axios.post(endpoint, {
|
||||
...notificationPayload,
|
||||
chat_id: user.settings.telegramChatId,
|
||||
disable_notification: !!user.settings?.telegramSendSilently,
|
||||
} as TelegramMessagePayload | TelegramPhotoPayload);
|
||||
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 });
|
||||
}
|
||||
} catch (e) {
|
||||
let errorData;
|
||||
try {
|
||||
errorData = await e.cause?.text();
|
||||
errorData = JSON.parse(errorData);
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
logger.error('Error sending Telegram notification', {
|
||||
label: 'Notifications',
|
||||
recipient: user.displayName,
|
||||
type: Notification[type],
|
||||
subject: payload.subject,
|
||||
errorMessage: e.message,
|
||||
response: e.response?.data,
|
||||
response: errorData,
|
||||
});
|
||||
|
||||
return false;
|
||||
|
||||
@@ -3,7 +3,6 @@ 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';
|
||||
@@ -178,26 +177,35 @@ class WebhookAgent
|
||||
});
|
||||
|
||||
try {
|
||||
await axios.post(
|
||||
settings.options.webhookUrl,
|
||||
this.buildPayload(type, payload),
|
||||
settings.options.authHeader
|
||||
? {
|
||||
headers: {
|
||||
Authorization: settings.options.authHeader,
|
||||
},
|
||||
}
|
||||
: undefined
|
||||
);
|
||||
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 });
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
let errorData;
|
||||
try {
|
||||
errorData = await e.cause?.text();
|
||||
errorData = JSON.parse(errorData);
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
logger.error('Error sending webhook notification', {
|
||||
label: 'Notifications',
|
||||
type: Notification[type],
|
||||
subject: payload.subject,
|
||||
errorMessage: e.message,
|
||||
response: e.response?.data,
|
||||
response: errorData,
|
||||
});
|
||||
|
||||
return false;
|
||||
|
||||
@@ -567,7 +567,10 @@ class JellyfinScanner {
|
||||
public async run(): Promise<void> {
|
||||
const settings = getSettings();
|
||||
|
||||
if (settings.main.mediaServerType != MediaServerType.JELLYFIN) {
|
||||
if (
|
||||
settings.main.mediaServerType != MediaServerType.JELLYFIN &&
|
||||
settings.main.mediaServerType != MediaServerType.EMBY
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -595,10 +598,8 @@ class JellyfinScanner {
|
||||
return this.log('No admin configured. Jellyfin sync skipped.', 'warn');
|
||||
}
|
||||
|
||||
const hostname = getHostname();
|
||||
|
||||
this.jfClient = new JellyfinAPI(
|
||||
hostname,
|
||||
getHostname(),
|
||||
admin.jellyfinAuthToken,
|
||||
admin.jellyfinDeviceId
|
||||
);
|
||||
|
||||
@@ -656,6 +656,7 @@ class Settings {
|
||||
}
|
||||
}
|
||||
|
||||
let loaded = false;
|
||||
let settings: Settings | undefined;
|
||||
|
||||
export const getSettings = (initialSettings?: AllSettings): Settings => {
|
||||
@@ -663,6 +664,11 @@ export const getSettings = (initialSettings?: AllSettings): Settings => {
|
||||
settings = new Settings(initialSettings);
|
||||
}
|
||||
|
||||
if (!loaded) {
|
||||
settings.load();
|
||||
loaded = true;
|
||||
}
|
||||
|
||||
return settings;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { MediaServerType } from '@server/constants/server';
|
||||
import type { AllSettings } from '@server/lib/settings';
|
||||
|
||||
const migrateHostname = (settings: any): AllSettings => {
|
||||
const oldMediaServerType = settings.main.mediaServerType;
|
||||
console.log('Migrating media server type', oldMediaServerType);
|
||||
if (
|
||||
oldMediaServerType === MediaServerType.JELLYFIN &&
|
||||
process.env.JELLYFIN_TYPE === 'emby'
|
||||
) {
|
||||
settings.main.mediaServerType = MediaServerType.EMBY;
|
||||
}
|
||||
|
||||
return settings;
|
||||
};
|
||||
|
||||
export default migrateHostname;
|
||||
@@ -1,7 +1,7 @@
|
||||
import JellyfinAPI from '@server/api/jellyfin';
|
||||
import PlexTvAPI from '@server/api/plextv';
|
||||
import { ApiErrorCode } from '@server/constants/error';
|
||||
import { MediaServerType } from '@server/constants/server';
|
||||
import { MediaServerType, ServerType } from '@server/constants/server';
|
||||
import { UserType } from '@server/constants/user';
|
||||
import { getRepository } from '@server/datasource';
|
||||
import { User } from '@server/entity/User';
|
||||
@@ -227,15 +227,20 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
urlBase?: string;
|
||||
useSsl?: boolean;
|
||||
email?: string;
|
||||
serverType?: number;
|
||||
};
|
||||
|
||||
//Make sure jellyfin login is enabled, but only if jellyfin is not already configured
|
||||
//Make sure jellyfin login is enabled, but only if jellyfin && Emby is not already configured
|
||||
if (
|
||||
settings.main.mediaServerType !== MediaServerType.JELLYFIN &&
|
||||
settings.main.mediaServerType != MediaServerType.NOT_CONFIGURED
|
||||
settings.main.mediaServerType !== MediaServerType.EMBY &&
|
||||
settings.main.mediaServerType != MediaServerType.NOT_CONFIGURED &&
|
||||
settings.jellyfin.ip !== ''
|
||||
) {
|
||||
return res.status(500).json({ error: 'Jellyfin login is disabled' });
|
||||
} else if (!body.username) {
|
||||
}
|
||||
|
||||
if (!body.username) {
|
||||
return res.status(500).json({ error: 'You must provide an username' });
|
||||
} else if (settings.jellyfin.ip !== '' && body.hostname) {
|
||||
return res
|
||||
@@ -273,7 +278,8 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
}
|
||||
|
||||
// First we need to attempt to log the user in to jellyfin
|
||||
const jellyfinserver = new JellyfinAPI(hostname, undefined, deviceId);
|
||||
const jellyfinserver = new JellyfinAPI(hostname ?? '', undefined, deviceId);
|
||||
|
||||
const jellyfinHost =
|
||||
externalHostname && externalHostname.length > 0
|
||||
? externalHostname
|
||||
@@ -317,20 +323,47 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
);
|
||||
|
||||
// User doesn't exist, and there are no users in the database, we'll create the user
|
||||
// with admin permission
|
||||
settings.main.mediaServerType = MediaServerType.JELLYFIN;
|
||||
user = new User({
|
||||
email: body.email,
|
||||
jellyfinUsername: account.User.Name,
|
||||
jellyfinUserId: account.User.Id,
|
||||
jellyfinDeviceId: deviceId,
|
||||
jellyfinAuthToken: account.AccessToken,
|
||||
permissions: Permission.ADMIN,
|
||||
avatar: account.User.PrimaryImageTag
|
||||
? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
|
||||
: gravatarUrl(body.email ?? '', { default: 'mm', size: 200 }),
|
||||
userType: UserType.JELLYFIN,
|
||||
});
|
||||
// with admin permissions
|
||||
switch (body.serverType) {
|
||||
case MediaServerType.EMBY:
|
||||
settings.main.mediaServerType = MediaServerType.EMBY;
|
||||
user = new User({
|
||||
email: body.email || account.User.Name,
|
||||
jellyfinUsername: account.User.Name,
|
||||
jellyfinUserId: account.User.Id,
|
||||
jellyfinDeviceId: deviceId,
|
||||
jellyfinAuthToken: account.AccessToken,
|
||||
permissions: Permission.ADMIN,
|
||||
avatar: account.User.PrimaryImageTag
|
||||
? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
|
||||
: gravatarUrl(body.email || account.User.Name, {
|
||||
default: 'mm',
|
||||
size: 200,
|
||||
}),
|
||||
userType: UserType.EMBY,
|
||||
});
|
||||
break;
|
||||
case MediaServerType.JELLYFIN:
|
||||
settings.main.mediaServerType = MediaServerType.JELLYFIN;
|
||||
user = new User({
|
||||
email: body.email || account.User.Name,
|
||||
jellyfinUsername: account.User.Name,
|
||||
jellyfinUserId: account.User.Id,
|
||||
jellyfinDeviceId: deviceId,
|
||||
jellyfinAuthToken: account.AccessToken,
|
||||
permissions: Permission.ADMIN,
|
||||
avatar: account.User.PrimaryImageTag
|
||||
? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
|
||||
: gravatarUrl(body.email || account.User.Name, {
|
||||
default: 'mm',
|
||||
size: 200,
|
||||
}),
|
||||
userType: UserType.JELLYFIN,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new Error('select_server_type');
|
||||
}
|
||||
|
||||
const serverName = await jellyfinserver.getServerName();
|
||||
|
||||
@@ -350,12 +383,12 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
logger.info(
|
||||
`Found matching ${
|
||||
settings.main.mediaServerType === MediaServerType.JELLYFIN
|
||||
? 'Jellyfin'
|
||||
: 'Emby'
|
||||
? ServerType.JELLYFIN
|
||||
: ServerType.EMBY
|
||||
} user; updating user with ${
|
||||
settings.main.mediaServerType === MediaServerType.JELLYFIN
|
||||
? 'Jellyfin'
|
||||
: 'Emby'
|
||||
? ServerType.JELLYFIN
|
||||
: ServerType.EMBY
|
||||
}`,
|
||||
{
|
||||
label: 'API',
|
||||
@@ -371,7 +404,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
if (account.User.PrimaryImageTag) {
|
||||
user.avatar = `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`;
|
||||
} else {
|
||||
user.avatar = gravatarUrl(user.email, {
|
||||
user.avatar = gravatarUrl(user.email || account.User.Name, {
|
||||
default: 'mm',
|
||||
size: 200,
|
||||
});
|
||||
@@ -413,10 +446,6 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
}
|
||||
);
|
||||
|
||||
if (!body.email) {
|
||||
throw new Error('add_email');
|
||||
}
|
||||
|
||||
user = new User({
|
||||
email: body.email,
|
||||
jellyfinUsername: account.User.Name,
|
||||
@@ -426,8 +455,14 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
permissions: settings.main.defaultPermissions,
|
||||
avatar: account.User.PrimaryImageTag
|
||||
? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
|
||||
: gravatarUrl(body.email, { default: 'mm', size: 200 }),
|
||||
userType: UserType.JELLYFIN,
|
||||
: gravatarUrl(body.email || account.User.Name, {
|
||||
default: 'mm',
|
||||
size: 200,
|
||||
}),
|
||||
userType:
|
||||
settings.main.mediaServerType === MediaServerType.JELLYFIN
|
||||
? UserType.JELLYFIN
|
||||
: UserType.EMBY,
|
||||
});
|
||||
//initialize Jellyfin/Emby users with local login
|
||||
const passedExplicitPassword = body.password && body.password.length > 0;
|
||||
@@ -730,6 +765,7 @@ authRoutes.post('/reset-password/:guid', async (req, res, next) => {
|
||||
});
|
||||
}
|
||||
user.recoveryLinkExpirationDate = null;
|
||||
await user.setPassword(req.body.password);
|
||||
userRepository.save(user);
|
||||
logger.info('Successfully reset password', {
|
||||
label: 'API',
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Router } from 'express';
|
||||
const router = Router();
|
||||
const tmdbImageProxy = new ImageProxy('tmdb', 'https://image.tmdb.org', {
|
||||
rateLimitOptions: {
|
||||
maxRequests: 20,
|
||||
maxRPS: 50,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -389,6 +389,7 @@ settingsRoutes.get('/jellyfin/users', async (req, res) => {
|
||||
order: { id: 'ASC' },
|
||||
});
|
||||
const jellyfinClient = new JellyfinAPI(
|
||||
getHostname(),
|
||||
admin.jellyfinAuthToken ?? '',
|
||||
admin.jellyfinDeviceId ?? ''
|
||||
);
|
||||
|
||||
@@ -41,7 +41,19 @@ router.get('/', async (req, res, next) => {
|
||||
break;
|
||||
case 'displayname':
|
||||
query = query.orderBy(
|
||||
"(CASE WHEN (user.username IS NULL OR user.username = '') THEN (CASE WHEN (user.plexUsername IS NULL OR user.plexUsername = '') THEN user.email ELSE LOWER(user.plexUsername) END) ELSE LOWER(user.username) END)",
|
||||
`CASE WHEN (user.username IS NULL OR user.username = '') THEN (
|
||||
CASE WHEN (user.plexUsername IS NULL OR user.plexUsername = '') THEN (
|
||||
CASE WHEN (user.jellyfinUsername IS NULL OR user.jellyfinUsername = '') THEN
|
||||
user.email
|
||||
ELSE
|
||||
LOWER(user.jellyfinUsername)
|
||||
END)
|
||||
ELSE
|
||||
LOWER(user.jellyfinUsername)
|
||||
END)
|
||||
ELSE
|
||||
LOWER(user.username)
|
||||
END`,
|
||||
'ASC'
|
||||
);
|
||||
break;
|
||||
@@ -90,12 +102,13 @@ router.post(
|
||||
const settings = getSettings();
|
||||
|
||||
const body = req.body;
|
||||
const email = body.email || body.username;
|
||||
const userRepository = getRepository(User);
|
||||
|
||||
const existingUser = await userRepository
|
||||
.createQueryBuilder('user')
|
||||
.where('user.email = :email', {
|
||||
email: body.email.toLowerCase(),
|
||||
email: email.toLowerCase(),
|
||||
})
|
||||
.getOne();
|
||||
|
||||
@@ -108,7 +121,7 @@ router.post(
|
||||
}
|
||||
|
||||
const passedExplicitPassword = body.password && body.password.length > 0;
|
||||
const avatar = gravatarUrl(body.email, { default: 'mm', size: 200 });
|
||||
const avatar = gravatarUrl(email, { default: 'mm', size: 200 });
|
||||
|
||||
if (
|
||||
!passedExplicitPassword &&
|
||||
@@ -118,9 +131,9 @@ router.post(
|
||||
}
|
||||
|
||||
const user = new User({
|
||||
email,
|
||||
avatar: body.avatar ?? avatar,
|
||||
username: body.username,
|
||||
email: body.email,
|
||||
password: body.password,
|
||||
permissions: settings.main.defaultPermissions,
|
||||
plexToken: '',
|
||||
@@ -497,6 +510,7 @@ router.post(
|
||||
order: { id: 'ASC' },
|
||||
});
|
||||
const jellyfinClient = new JellyfinAPI(
|
||||
getHostname(),
|
||||
admin.jellyfinAuthToken ?? '',
|
||||
admin.jellyfinDeviceId ?? ''
|
||||
);
|
||||
|
||||
@@ -98,7 +98,9 @@ userSettingsRoutes.post<
|
||||
}
|
||||
|
||||
user.username = req.body.username;
|
||||
user.email = req.body.email ?? user.email;
|
||||
if (user.jellyfinUsername) {
|
||||
user.email = req.body.email || user.jellyfinUsername || user.email;
|
||||
}
|
||||
|
||||
// Update quota values only if the user has the correct permissions
|
||||
if (
|
||||
|
||||
68
server/utils/rateLimit.ts
Normal file
68
server/utils/rateLimit.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
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();
|
||||
});
|
||||
};
|
||||
}
|
||||
47
src/assets/services/emby-icon-only.svg
Normal file
47
src/assets/services/emby-icon-only.svg
Normal file
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
version="1.1"
|
||||
id="svg2"
|
||||
viewBox="0 0 712.60077 712.5481"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<rect
|
||||
style="opacity:0;fill:#ffffff;stroke-width:4.12543"
|
||||
id="rect249"
|
||||
width="712.60077"
|
||||
height="712.5481"
|
||||
x="-0.00071160076"
|
||||
y="2.0223413e-11" />
|
||||
<rect
|
||||
style="fill:#ffffff"
|
||||
id="rect289"
|
||||
width="230.18982"
|
||||
height="229.82355"
|
||||
x="241.20476"
|
||||
y="241.36227" />
|
||||
<g
|
||||
id="layer1"
|
||||
transform="matrix(0.70249853,0,0,0.70249853,88.770447,96.84571)">
|
||||
<path
|
||||
id="path3427"
|
||||
d="m 327.06546,642.18589 c -45.39663,-45.86009 -82.73776,-83.3683 -82.98029,-83.3516 -0.24253,0.0167 -7.23324,6.65975 -15.53493,14.7623 l -15.09396,14.73193 -40.13624,-40.38805 C 151.24511,525.72706 108.73555,482.86504 78.854363,452.69158 l -54.329437,-54.86086 83.720394,-82.90796 83.72039,-82.90797 -15.19316,-15.20441 -15.19315,-15.20443 95.18008,-94.29313 c 52.34904,-51.86121 95.35849,-94.293118 95.57653,-94.293118 0.21805,0 37.39519,37.357576 82.61589,83.016832 45.22068,45.659256 82.53772,83.131956 82.92673,83.272666 0.38901,0.14071 7.46336,-6.49498 15.72077,-14.746 l 15.01348,-15.00184 7.14591,7.19902 c 73.95232,74.50189 181.50599,183.56427 181.36678,183.9109 -0.10065,0.25064 -37.54056,37.44106 -83.19981,82.64536 -45.65926,45.2043 -83.10802,82.41429 -83.21946,82.68884 -0.11145,0.27456 6.50478,7.34753 14.70272,15.71771 l 14.90534,15.21851 -15.3888,15.28883 c -21.09609,20.95904 -162.95155,161.27018 -169.79551,167.947 l -5.52526,5.39033 z m 89.8523,-204.1566 c 64.84836,-37.53366 117.81919,-68.54793 117.71294,-68.92058 -0.15927,-0.55862 -233.55022,-136.2489 -236.27084,-137.3646 -0.68441,-0.28068 -0.85761,27.45642 -0.85761,137.33982 0,99.83563 0.20749,137.62237 0.75471,137.43996 0.41509,-0.13837 53.81245,-30.96093 118.6608,-68.4946 z"
|
||||
style="fill:#52b54b;fill-opacity:1;stroke:none" />
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -1,46 +1,131 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
version="1.1"
|
||||
id="svg2"
|
||||
viewBox="0 0 712.60077 712.5481"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<rect
|
||||
style="opacity:0;fill:#ffffff;stroke-width:4.12543"
|
||||
id="rect249"
|
||||
width="712.60077"
|
||||
height="712.5481"
|
||||
x="-0.00071160076"
|
||||
y="2.0223413e-11" />
|
||||
<rect
|
||||
style="fill:#ffffff"
|
||||
id="rect289"
|
||||
width="230.18982"
|
||||
height="229.82355"
|
||||
x="241.20476"
|
||||
y="241.36227" />
|
||||
<g
|
||||
id="layer1"
|
||||
transform="matrix(0.70249853,0,0,0.70249853,88.770447,96.84571)">
|
||||
<path
|
||||
id="path3427"
|
||||
d="m 327.06546,642.18589 c -45.39663,-45.86009 -82.73776,-83.3683 -82.98029,-83.3516 -0.24253,0.0167 -7.23324,6.65975 -15.53493,14.7623 l -15.09396,14.73193 -40.13624,-40.38805 C 151.24511,525.72706 108.73555,482.86504 78.854363,452.69158 l -54.329437,-54.86086 83.720394,-82.90796 83.72039,-82.90797 -15.19316,-15.20441 -15.19315,-15.20443 95.18008,-94.29313 c 52.34904,-51.86121 95.35849,-94.293118 95.57653,-94.293118 0.21805,0 37.39519,37.357576 82.61589,83.016832 45.22068,45.659256 82.53772,83.131956 82.92673,83.272666 0.38901,0.14071 7.46336,-6.49498 15.72077,-14.746 l 15.01348,-15.00184 7.14591,7.19902 c 73.95232,74.50189 181.50599,183.56427 181.36678,183.9109 -0.10065,0.25064 -37.54056,37.44106 -83.19981,82.64536 -45.65926,45.2043 -83.10802,82.41429 -83.21946,82.68884 -0.11145,0.27456 6.50478,7.34753 14.70272,15.71771 l 14.90534,15.21851 -15.3888,15.28883 c -21.09609,20.95904 -162.95155,161.27018 -169.79551,167.947 l -5.52526,5.39033 z m 89.8523,-204.1566 c 64.84836,-37.53366 117.81919,-68.54793 117.71294,-68.92058 -0.15927,-0.55862 -233.55022,-136.2489 -236.27084,-137.3646 -0.68441,-0.28068 -0.85761,27.45642 -0.85761,137.33982 0,99.83563 0.20749,137.62237 0.75471,137.43996 0.41509,-0.13837 53.81245,-30.96093 118.6608,-68.4946 z"
|
||||
style="fill:#52b54b;fill-opacity:1;stroke:none" />
|
||||
</g>
|
||||
</svg>
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
|
||||
y="0px" width="100%" viewBox="0 0 617 188" enable-background="new 0 0 617 188" xml:space="preserve">
|
||||
<path fill="#52B54B" opacity="1.000000" stroke="none" d="
|
||||
M89.583336,1.000000
|
||||
C90.189529,1.685005 90.166168,2.574803 90.599510,3.025271
|
||||
C103.718315,16.662701 116.882103,30.256845 129.948212,43.764053
|
||||
C130.577850,43.523941 130.916519,43.491173 131.111343,43.306595
|
||||
C138.657471,36.157455 138.655273,36.156090 146.005478,43.505203
|
||||
C159.538589,57.036308 173.016449,70.623535 186.654617,84.047913
|
||||
C189.264145,86.616562 189.414017,88.253456 186.716782,90.895164
|
||||
C174.709808,102.655037 162.893280,114.609337 151.008514,126.493958
|
||||
C146.073502,131.428925 146.076691,131.427155 151.017944,136.523712
|
||||
C151.698944,137.226120 152.340485,137.966812 153.259171,138.973434
|
||||
C151.947098,140.380035 150.766312,141.712204 149.516266,142.975861
|
||||
C134.544815,158.110641 119.563087,173.235260 104.792023,188.681274
|
||||
C103.611107,189.000000 102.222221,189.000000 100.624634,188.681274
|
||||
C86.361732,174.796494 72.307518,161.230438 57.702755,147.132965
|
||||
C56.157101,149.136856 54.135899,151.757263 51.994804,154.533112
|
||||
C35.932781,138.457108 20.569420,123.048477 5.141897,107.704361
|
||||
C3.997114,106.565773 2.391420,105.890610 1.000000,105.000000
|
||||
C1.000000,103.611107 1.000000,102.222221 1.318741,100.624641
|
||||
C15.203506,86.361694 28.769531,72.307434 42.867004,57.702602
|
||||
C40.863205,56.156994 38.242813,54.135792 35.425343,51.962570
|
||||
C51.518696,35.908516 66.939468,20.557360 82.295547,5.141749
|
||||
C83.434830,3.998048 84.109390,2.391417 85.000000,0.999999
|
||||
C86.388893,1.000000 87.777779,1.000000 89.583336,1.000000
|
||||
M73.196465,79.500702
|
||||
C73.196465,96.254150 73.196465,113.007599 73.196465,130.872055
|
||||
C94.273178,118.764557 114.417175,107.192863 135.221664,95.241745
|
||||
C114.247169,83.251732 94.091187,71.729622 73.196594,59.785294
|
||||
C73.196594,66.631348 73.196594,72.566254 73.196465,79.500702
|
||||
z" />
|
||||
<path fill="#FDFDFD" opacity="1.000000" stroke="none" d="
|
||||
M618.000000,60.571537
|
||||
C617.004395,62.042580 615.613281,62.912964 615.073181,64.153824
|
||||
C608.143372,80.073746 601.328613,96.043816 594.498169,112.006920
|
||||
C586.973572,129.592300 579.343018,147.133865 571.999390,164.794601
|
||||
C568.632385,172.892075 568.893372,173.002594 560.133972,172.999832
|
||||
C555.470825,172.998367 550.807617,172.994385 546.144592,172.969360
|
||||
C545.841980,172.967712 545.540466,172.775543 544.836609,172.534256
|
||||
C548.592896,163.531219 551.714905,154.222061 556.286133,145.689255
|
||||
C559.733765,139.253830 559.138794,134.062668 556.454224,127.695969
|
||||
C546.360352,103.757523 536.803345,79.592712 526.837830,55.000847
|
||||
C534.817078,55.000847 542.437622,54.725182 550.003540,55.244331
|
||||
C551.436218,55.342628 553.169678,58.412052 553.885010,60.423309
|
||||
C558.720520,74.018005 563.307556,87.700912 568.003784,101.345413
|
||||
C569.107483,104.551987 570.321045,107.720764 571.976196,112.255157
|
||||
C573.889587,107.365631 575.415283,103.375916 577.007935,99.413109
|
||||
C582.693298,85.266724 588.344238,71.105591 594.218018,57.037624
|
||||
C594.650513,56.001743 596.734497,55.132927 598.079773,55.089733
|
||||
C604.401855,54.886726 610.734131,54.999401 617.531372,54.999699
|
||||
C618.000000,56.714359 618.000000,58.428715 618.000000,60.571537
|
||||
z" />
|
||||
<path fill="#FDFDFD" opacity="1.000000" stroke="none" d="
|
||||
M430.000122,99.002235
|
||||
C430.000122,112.477097 430.000122,125.452438 430.000122,138.713440
|
||||
C423.048126,138.713440 416.308685,138.713440 408.999878,138.713440
|
||||
C408.999878,129.350739 409.120758,119.916939 408.962219,110.487823
|
||||
C408.832153,102.753624 409.088898,94.909142 407.866791,87.324188
|
||||
C406.440887,78.474220 401.302399,74.201607 394.304291,74.000290
|
||||
C387.617249,73.807938 380.317963,79.297188 378.047363,86.438652
|
||||
C377.420715,88.409592 377.055725,90.550858 377.044647,92.616508
|
||||
C376.962494,107.913475 377.000122,123.211082 377.000122,138.753479
|
||||
C369.630646,138.753479 362.559692,138.753479 354.999878,138.753479
|
||||
C354.999878,123.256836 355.044769,107.816956 354.977661,92.377571
|
||||
C354.951050,86.251518 352.748199,80.799278 347.911346,77.066116
|
||||
C339.239685,70.373154 327.811401,74.635170 324.084412,84.471092
|
||||
C322.793915,87.876816 322.147491,91.713402 322.090881,95.366882
|
||||
C321.868958,109.685005 322.000122,124.008591 322.000122,138.665009
|
||||
C314.823853,138.665009 307.760773,138.665009 300.346558,138.665009
|
||||
C300.346558,111.006645 300.346558,83.281189 300.346558,55.001301
|
||||
C306.163818,55.001301 312.104645,54.855133 318.024780,55.139343
|
||||
C319.060455,55.189068 320.450378,56.891682 320.882477,58.112110
|
||||
C321.380768,59.519447 320.998291,61.238617 320.998291,64.136040
|
||||
C328.715179,54.407440 338.407898,52.804527 348.408875,54.206123
|
||||
C356.403381,55.326527 361.770447,57.638248 366.682190,66.544373
|
||||
C372.325470,62.972542 377.601440,58.269657 383.771973,56.014080
|
||||
C396.273407,51.444298 408.602570,53.673611 419.067657,61.818150
|
||||
C426.629364,67.703125 429.037811,76.770744 429.932556,86.011482
|
||||
C430.332214,90.138710 430.000122,94.336792 430.000122,99.002235
|
||||
z" />
|
||||
<path fill="#FDFDFD" opacity="1.000000" stroke="none" d="
|
||||
M462.000427,35.006332
|
||||
C462.000427,44.815434 462.000427,54.126144 462.000427,64.132019
|
||||
C468.844696,58.319965 476.100769,54.654530 484.669739,53.656227
|
||||
C496.686127,52.256294 507.565582,54.979622 516.927185,62.503853
|
||||
C534.236755,76.416115 535.360107,106.231667 523.651062,123.341644
|
||||
C516.745056,133.433182 506.539673,139.485458 493.555267,140.111023
|
||||
C483.836304,140.579254 474.670624,139.889420 466.610413,133.799713
|
||||
C465.039795,132.613068 463.390686,131.530289 461.957214,130.525391
|
||||
C461.633789,132.375305 461.105469,135.397171 460.522095,138.733841
|
||||
C454.446686,138.733841 448.017822,138.733841 441.292542,138.733841
|
||||
C441.292542,99.722672 441.292542,60.652122 441.292542,21.290209
|
||||
C447.943787,21.290209 454.684204,21.290209 462.000427,21.290209
|
||||
C462.000427,25.636984 462.000427,30.072460 462.000427,35.006332
|
||||
M480.890228,119.974937
|
||||
C485.426086,119.681152 490.365997,120.444260 494.421356,118.893707
|
||||
C506.182587,114.396866 510.858643,104.919495 509.036591,92.234833
|
||||
C507.422546,80.997993 496.539307,71.772278 483.551605,73.864754
|
||||
C469.724976,76.092384 464.376770,85.538391 463.152863,96.752327
|
||||
C462.120667,106.209480 469.961761,116.189537 480.890228,119.974937
|
||||
z" />
|
||||
<path fill="#FDFDFD" opacity="1.000000" stroke="none" d="
|
||||
M234.797928,54.654831
|
||||
C244.856339,52.605957 254.504562,52.040043 264.239868,54.923946
|
||||
C279.600891,59.474377 286.402191,68.163963 289.768585,81.937614
|
||||
C291.530579,89.146889 290.954620,96.927589 291.469940,105.005005
|
||||
C269.550385,105.005005 248.375092,105.005005 227.094437,105.005005
|
||||
C229.577957,116.288628 239.741562,120.764336 248.594757,121.034813
|
||||
C256.790771,121.285217 264.390472,119.882645 271.081848,114.731178
|
||||
C271.774902,114.197632 273.962708,114.659111 274.786041,115.402222
|
||||
C278.726318,118.958458 282.435333,122.770882 286.509888,126.770363
|
||||
C281.309174,132.968170 274.787445,135.946014 267.542938,138.175064
|
||||
C253.746231,142.420120 240.209259,142.317459 227.237503,135.935410
|
||||
C212.712891,128.789368 205.730453,116.523628 204.973831,100.473404
|
||||
C204.537735,91.222557 205.503754,82.283119 210.008469,74.017265
|
||||
C215.396210,64.131088 223.372589,57.511646 234.797928,54.654831
|
||||
M266.971497,78.708908
|
||||
C259.384399,70.789909 249.920425,70.480316 240.489548,73.410858
|
||||
C234.405487,75.301414 229.437546,79.631561 227.800247,86.722244
|
||||
C242.152313,86.722244 256.002747,86.722244 270.947815,86.722244
|
||||
C269.410950,83.870155 268.228943,81.676651 266.971497,78.708908
|
||||
z" />
|
||||
<path fill="#FCFEFC" opacity="1.000000" stroke="none" d="
|
||||
M73.196533,79.000931
|
||||
C73.196594,72.566254 73.196594,66.631348 73.196594,59.785294
|
||||
C94.091187,71.729622 114.247169,83.251732 135.221664,95.241745
|
||||
C114.417175,107.192863 94.273178,118.764557 73.196465,130.872055
|
||||
C73.196465,113.007599 73.196465,96.254150 73.196533,79.000931
|
||||
z" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 7.8 KiB |
@@ -1 +1,85 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320.03 103.61"><defs><style>.cls-1{fill:#fff}.cls-2{fill:url(#a)}.cls-3{fill:#e5a00d}</style><radialGradient id="a" cx="258.33" cy="51.76" r="42.95" gradientUnits="userSpaceOnUse"><stop offset=".17" stop-color="#f9be03"/><stop offset=".51" stop-color="#e8a50b"/><stop offset="1" stop-color="#cc7c19"/></radialGradient></defs><polygon points="320.03 -.09 289.96 -.09 259.88 51.76 289.96 103.61 320.01 103.61 289.96 51.79" class="cls-1"/><polygon points="226.7 -.09 256.78 -.09 289.96 51.76 256.78 103.61 226.7 103.61 259.88 51.76" class="cls-2"/><polygon points="226.7 -.09 256.78 -.09 289.96 51.76 256.78 103.61 226.7 103.61 259.88 51.76" class="cls-3"/><path d="M216.32,103.61H156.49V-.09h59.83v18h-37.8V40.69H213.7v18H178.52V85.45h37.8Z" class="cls-1"/><path d="M82.07,103.61V-.09h22V85.45h42.07v18.16Z" class="cls-1"/><path d="M71.66,32.25Q71.66,49,61.2,57.87T31.44,66.73H22v36.88H0V-.09H33.14Q52-.09,61.83,8T71.66,32.25ZM22,48.71h7.24q10.15,0,15.18-4c3.37-2.66,5-6.56,5-11.67s-1.41-9-4.22-11.42S38,17.93,32,17.93H22Z" class="cls-1"/></svg>
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Generator: Adobe Illustrator 26.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.1"
|
||||
id="plex-logo"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 1000 460.89727"
|
||||
xml:space="preserve"
|
||||
sodipodi:docname="plex-logo.svg"
|
||||
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"><metadata
|
||||
id="metadata25"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs23">
|
||||
</defs><sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#111111"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1017"
|
||||
id="namedview21"
|
||||
showgrid="false"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0"
|
||||
inkscape:zoom="0.27956081"
|
||||
inkscape:cx="783.06912"
|
||||
inkscape:cy="-132.85701"
|
||||
inkscape:window-x="1912"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="plex-logo" />
|
||||
<style
|
||||
type="text/css"
|
||||
id="style2">
|
||||
.st0{fill:#FFFFFF;}
|
||||
.st1{fill:#EBAF00;}
|
||||
</style>
|
||||
<path
|
||||
class="st0"
|
||||
d="m 164.18919,82.43243 c -39.86487,0 -65.540543,11.48648 -87.162163,38.51351 V 91.21621 H 0 v 366.21621 c 0,0 1.3513514,0.67567 5.4054053,1.35135 5.4054057,1.35135 33.7837827,7.43243 54.7297287,-10.13514 18.243244,-15.54054 22.297295,-33.78378 22.297295,-54.05405 v -52.7027 c 22.297301,23.64864 47.297301,33.78378 82.432431,33.78378 75.67567,0 133.78378,-61.48648 133.78378,-143.24323 0,-88.51352 -56.08108,-150 -134.45945,-150 z m -14.86487,223.64864 c -42.56756,0 -76.351351,-35.13513 -76.351351,-77.7027 0,-41.89189 39.864871,-75.67567 76.351351,-75.67567 43.24324,0 76.35135,33.1081 76.35135,76.35135 0,43.24324 -33.78378,77.02702 -76.35135,77.02702 z"
|
||||
id="path4"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#ffffff;stroke-width:6.75675678" /><path
|
||||
class="st0"
|
||||
d="m 408.1081,223.64864 c 0,31.75676 3.37838,70.27027 34.45946,112.16216 0.67567,0.67567 2.02702,2.7027 2.02702,2.7027 -12.83783,21.62162 -28.37837,36.48648 -49.32432,36.48648 -16.21622,0 -32.43243,-8.78378 -45.94595,-23.64864 -14.18918,-16.21622 -20.94594,-37.16216 -20.94594,-59.45946 V 0 h 79.05405 z"
|
||||
id="path6"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#ffffff;stroke-width:6.75675678" /><polygon
|
||||
class="st1"
|
||||
points="117.9,33.9 104.1,13.5 118.3,13.5 132,33.9 118.3,54.2 104.1,54.2 "
|
||||
id="polygon8"
|
||||
style="fill:#ebaf00"
|
||||
transform="scale(6.7567568)" /><polygon
|
||||
class="st0"
|
||||
points="135.7,31.6 148,13.5 133.8,13.5 128.7,21 "
|
||||
id="polygon10"
|
||||
style="fill:#ffffff"
|
||||
transform="scale(6.7567568)" /><path
|
||||
class="st0"
|
||||
d="m 869.59458,316.2162 c 0,0 16.2162,22.2973 16.2162,22.2973 15.54058,24.32432 35.8108,36.48648 59.45949,36.48648 25,-0.67567 42.56752,-22.29729 49.3243,-30.4054 0,0 -12.16218,-10.81081 -27.7027,-29.05405 -20.94598,-24.32432 -48.64868,-68.91892 -49.3243,-70.94594 z"
|
||||
id="path12"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#ffffff;stroke-width:6.75675678" /><path
|
||||
style="fill:#ffffff;stroke-width:6.75675678"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path16"
|
||||
d="m 632.43242,287.16215 c -16.21622,14.86486 -27.02703,22.97297 -49.32432,22.97297 -39.86487,0 -62.83784,-28.37837 -66.21622,-59.45945 h 211.4865 c 1.35131,-4.05406 2.027,-9.45946 2.027,-18.24324 0,-85.81082 -62.83783,-150 -145.27026,-150 -78.37837,0 -142.56756,65.54054 -142.56756,147.29729 0,81.08108 64.18919,145.27026 144.59459,145.27026 56.08108,0 104.72973,-31.75675 131.08105,-87.83783 z M 585.8108,147.29729 c 35.13513,0 61.48648,22.97297 67.56756,53.37838 H 519.59458 c 6.75676,-31.75676 31.75676,-53.37838 66.21622,-53.37838 z"
|
||||
class="st0" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 4.2 KiB |
@@ -14,7 +14,6 @@ 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';
|
||||
@@ -77,11 +76,9 @@ const CreateSlider = ({ onCreate, slider }: CreateSliderProps) => {
|
||||
|
||||
const keywords = await Promise.all(
|
||||
slider.data.split(',').map(async (keywordId) => {
|
||||
const keyword = await axios.get<Keyword>(
|
||||
`/api/v1/keyword/${keywordId}`
|
||||
);
|
||||
|
||||
return keyword.data;
|
||||
const res = await fetch(`/api/v1/keyword/${keywordId}`);
|
||||
const keyword: Keyword = await res.json();
|
||||
return keyword;
|
||||
})
|
||||
);
|
||||
|
||||
@@ -98,15 +95,13 @@ const CreateSlider = ({ onCreate, slider }: CreateSliderProps) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await axios.get<TmdbGenre[]>(
|
||||
const res = await fetch(
|
||||
`/api/v1/genres/${
|
||||
slider.type === DiscoverSliderType.TMDB_MOVIE_GENRE ? 'movie' : 'tv'
|
||||
}`
|
||||
);
|
||||
|
||||
const genre = response.data.find(
|
||||
(genre) => genre.id === Number(slider.data)
|
||||
);
|
||||
const genres: TmdbGenre[] = await res.json();
|
||||
const genre = genres.find((genre) => genre.id === Number(slider.data));
|
||||
|
||||
setDefaultDataValue([
|
||||
{
|
||||
@@ -121,11 +116,8 @@ const CreateSlider = ({ onCreate, slider }: CreateSliderProps) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await axios.get<ProductionCompany>(
|
||||
`/api/v1/studio/${slider.data}`
|
||||
);
|
||||
|
||||
const studio = response.data;
|
||||
const res = await fetch(`/api/v1/studio/${slider.data}`);
|
||||
const studio: ProductionCompany = await res.json();
|
||||
|
||||
setDefaultDataValue([
|
||||
{
|
||||
@@ -168,16 +160,17 @@ const CreateSlider = ({ onCreate, slider }: CreateSliderProps) => {
|
||||
);
|
||||
|
||||
const loadKeywordOptions = async (inputValue: string) => {
|
||||
const results = await axios.get<TmdbKeywordSearchResponse>(
|
||||
'/api/v1/search/keyword',
|
||||
const res = await fetch(
|
||||
`/api/v1/search/keyword?query=${encodeURIExtraParams(inputValue)}`,
|
||||
{
|
||||
params: {
|
||||
query: encodeURIExtraParams(inputValue),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
const results: TmdbKeywordSearchResponse = await res.json();
|
||||
|
||||
return results.data.results.map((result) => ({
|
||||
return results.results.map((result) => ({
|
||||
label: result.name,
|
||||
value: result.id,
|
||||
}));
|
||||
@@ -188,38 +181,37 @@ const CreateSlider = ({ onCreate, slider }: CreateSliderProps) => {
|
||||
return [];
|
||||
}
|
||||
|
||||
const results = await axios.get<TmdbCompanySearchResponse>(
|
||||
'/api/v1/search/company',
|
||||
const res = await fetch(
|
||||
`/api/v1/search/company?query=${encodeURIExtraParams(inputValue)}`,
|
||||
{
|
||||
params: {
|
||||
query: encodeURIExtraParams(inputValue),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
const results: TmdbCompanySearchResponse = await res.json();
|
||||
|
||||
return results.data.results.map((result) => ({
|
||||
return results.results.map((result) => ({
|
||||
label: result.name,
|
||||
value: result.id,
|
||||
}));
|
||||
};
|
||||
|
||||
const loadMovieGenreOptions = async () => {
|
||||
const results = await axios.get<GenreSliderItem[]>(
|
||||
'/api/v1/discover/genreslider/movie'
|
||||
);
|
||||
const res = await fetch('/api/v1/discover/genreslider/movie');
|
||||
const results: GenreSliderItem[] = await res.json();
|
||||
|
||||
return results.data.map((result) => ({
|
||||
return results.map((result) => ({
|
||||
label: result.name,
|
||||
value: result.id,
|
||||
}));
|
||||
};
|
||||
|
||||
const loadTvGenreOptions = async () => {
|
||||
const results = await axios.get<GenreSliderItem[]>(
|
||||
'/api/v1/discover/genreslider/tv'
|
||||
);
|
||||
const res = await fetch('/api/v1/discover/genreslider/tv');
|
||||
const results: GenreSliderItem[] = await res.json();
|
||||
|
||||
return results.data.map((result) => ({
|
||||
return results.map((result) => ({
|
||||
label: result.name,
|
||||
value: result.id,
|
||||
}));
|
||||
@@ -314,17 +306,31 @@ const CreateSlider = ({ onCreate, slider }: CreateSliderProps) => {
|
||||
onSubmit={async (values, { resetForm }) => {
|
||||
try {
|
||||
if (slider) {
|
||||
await axios.put(`/api/v1/settings/discover/${slider.id}`, {
|
||||
type: Number(values.sliderType),
|
||||
title: values.title,
|
||||
data: values.data,
|
||||
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,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
} else {
|
||||
await axios.post('/api/v1/settings/discover/add', {
|
||||
type: Number(values.sliderType),
|
||||
title: values.title,
|
||||
data: values.data,
|
||||
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,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
}
|
||||
|
||||
addToast(
|
||||
|
||||
@@ -48,11 +48,11 @@ const DiscoverTvNetwork = () => {
|
||||
<div className="mt-1 mb-5">
|
||||
<Header>
|
||||
{firstResultData?.network.logoPath ? (
|
||||
<div className="mb-6 flex justify-center">
|
||||
<div className="relative mb-6 flex h-24 justify-center sm:h-32">
|
||||
<Image
|
||||
src={`//image.tmdb.org/t/p/w780_filter(duotone,ffffff,bababa)${firstResultData.network.logoPath}`}
|
||||
src={`https://image.tmdb.org/t/p/w780_filter(duotone,ffffff,bababa)${firstResultData.network.logoPath}`}
|
||||
alt={firstResultData.network.name}
|
||||
className="max-h-24 sm:max-h-32"
|
||||
className="object-contain"
|
||||
fill
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -20,7 +20,6 @@ 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';
|
||||
@@ -78,7 +77,10 @@ const DiscoverSliderEdit = ({
|
||||
|
||||
const deleteSlider = async () => {
|
||||
try {
|
||||
await axios.delete(`/api/v1/settings/discover/${slider.id}`);
|
||||
const res = await fetch(`/api/v1/settings/discover/${slider.id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
addToast(intl.formatMessage(messages.deletesuccess), {
|
||||
appearance: 'success',
|
||||
autoDismiss: true,
|
||||
|
||||
@@ -48,11 +48,11 @@ const DiscoverMovieStudio = () => {
|
||||
<div className="mt-1 mb-5">
|
||||
<Header>
|
||||
{firstResultData?.studio.logoPath ? (
|
||||
<div className="mb-6 flex justify-center">
|
||||
<div className="relative mb-6 flex h-24 justify-center sm:h-32">
|
||||
<Image
|
||||
src={`//image.tmdb.org/t/p/w780_filter(duotone,ffffff,bababa)${firstResultData.studio.logoPath}`}
|
||||
src={`https://image.tmdb.org/t/p/w780_filter(duotone,ffffff,bababa)${firstResultData.studio.logoPath}`}
|
||||
alt={firstResultData.studio.name}
|
||||
className="max-h-24 sm:max-h-32"
|
||||
className="object-contain"
|
||||
fill
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -28,7 +28,6 @@ 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';
|
||||
@@ -76,7 +75,14 @@ const Discover = () => {
|
||||
|
||||
const updateSliders = async () => {
|
||||
try {
|
||||
await axios.post('/api/v1/settings/discover', sliders);
|
||||
const res = await fetch('/api/v1/settings/discover', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(sliders),
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
|
||||
addToast(intl.formatMessage(messages.updatesuccess), {
|
||||
appearance: 'success',
|
||||
@@ -94,7 +100,10 @@ const Discover = () => {
|
||||
|
||||
const resetSliders = async () => {
|
||||
try {
|
||||
await axios.get('/api/v1/settings/discover/reset');
|
||||
const res = await fetch('/api/v1/settings/discover/reset', {
|
||||
method: 'GET',
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
|
||||
addToast(intl.formatMessage(messages.resetsuccess), {
|
||||
appearance: 'success',
|
||||
|
||||
@@ -11,7 +11,6 @@ import useLocale from '@app/hooks/useLocale';
|
||||
import useSettings from '@app/hooks/useSettings';
|
||||
import { MediaType } from '@server/constants/media';
|
||||
import { MediaServerType } from '@server/constants/server';
|
||||
import getConfig from 'next/config';
|
||||
|
||||
interface ExternalLinkBlockProps {
|
||||
mediaType: 'movie' | 'tv';
|
||||
@@ -31,7 +30,6 @@ const ExternalLinkBlock = ({
|
||||
mediaUrl,
|
||||
}: ExternalLinkBlockProps) => {
|
||||
const settings = useSettings();
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
const { locale } = useLocale();
|
||||
|
||||
return (
|
||||
@@ -45,7 +43,8 @@ const ExternalLinkBlock = ({
|
||||
>
|
||||
{settings.currentSettings.mediaServerType === MediaServerType.PLEX ? (
|
||||
<PlexLogo />
|
||||
) : publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? (
|
||||
) : settings.currentSettings.mediaServerType ===
|
||||
MediaServerType.EMBY ? (
|
||||
<EmbyLogo />
|
||||
) : (
|
||||
<JellyfinLogo />
|
||||
|
||||
@@ -5,7 +5,6 @@ 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 Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
@@ -49,7 +48,10 @@ const IssueComment = ({
|
||||
|
||||
const deleteComment = async () => {
|
||||
try {
|
||||
await axios.delete(`/api/v1/issueComment/${comment.id}`);
|
||||
const res = await fetch(`/api/v1/issueComment/${comment.id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
} catch (e) {
|
||||
// something went wrong deleting the comment
|
||||
} finally {
|
||||
@@ -175,9 +177,17 @@ const IssueComment = ({
|
||||
<Formik
|
||||
initialValues={{ newMessage: comment.message }}
|
||||
onSubmit={async (values) => {
|
||||
await axios.put(`/api/v1/issueComment/${comment.id}`, {
|
||||
message: values.newMessage,
|
||||
});
|
||||
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();
|
||||
|
||||
if (onUpdate) {
|
||||
onUpdate();
|
||||
|
||||
@@ -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 Error from '@app/pages/_error';
|
||||
import ErrorPage from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { Transition } from '@headlessui/react';
|
||||
import {
|
||||
@@ -27,9 +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 getConfig from 'next/config';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
@@ -109,14 +107,13 @@ const IssueDetails = () => {
|
||||
(opt) => opt.issueType === issueData?.issueType
|
||||
);
|
||||
const settings = useSettings();
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
|
||||
if (!data && !error) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
|
||||
if (!data || !issueData) {
|
||||
return <Error statusCode={404} />;
|
||||
return <ErrorPage statusCode={404} />;
|
||||
}
|
||||
|
||||
const belongsToUser = issueData.createdBy.id === currentUser?.id;
|
||||
@@ -125,9 +122,14 @@ const IssueDetails = () => {
|
||||
|
||||
const editFirstComment = async (newMessage: string) => {
|
||||
try {
|
||||
await axios.put(`/api/v1/issueComment/${firstComment.id}`, {
|
||||
message: newMessage,
|
||||
const res = await fetch(`/api/v1/issueComment/${firstComment.id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ message: newMessage }),
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
|
||||
addToast(intl.formatMessage(messages.toasteditdescriptionsuccess), {
|
||||
appearance: 'success',
|
||||
@@ -144,7 +146,10 @@ const IssueDetails = () => {
|
||||
|
||||
const updateIssueStatus = async (newStatus: 'open' | 'resolved') => {
|
||||
try {
|
||||
await axios.post(`/api/v1/issue/${issueData.id}/${newStatus}`);
|
||||
const res = await fetch(`/api/v1/issue/${issueData.id}/${newStatus}`, {
|
||||
method: 'POST',
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
|
||||
addToast(intl.formatMessage(messages.toaststatusupdated), {
|
||||
appearance: 'success',
|
||||
@@ -161,7 +166,10 @@ const IssueDetails = () => {
|
||||
|
||||
const deleteIssue = async () => {
|
||||
try {
|
||||
await axios.delete(`/api/v1/issue/${issueData.id}`);
|
||||
const res = await fetch(`/api/v1/issue/${issueData.id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
|
||||
addToast(intl.formatMessage(messages.toastissuedeleted), {
|
||||
appearance: 'success',
|
||||
@@ -380,7 +388,8 @@ const IssueDetails = () => {
|
||||
>
|
||||
<PlayIcon />
|
||||
<span>
|
||||
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||
{settings.currentSettings.mediaServerType ===
|
||||
MediaServerType.EMBY
|
||||
? intl.formatMessage(messages.playonplex, {
|
||||
mediaServerName: 'Emby',
|
||||
})
|
||||
@@ -427,7 +436,8 @@ const IssueDetails = () => {
|
||||
>
|
||||
<PlayIcon />
|
||||
<span>
|
||||
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||
{settings.currentSettings.mediaServerType ===
|
||||
MediaServerType.EMBY
|
||||
? intl.formatMessage(messages.play4konplex, {
|
||||
mediaServerName: 'Emby',
|
||||
})
|
||||
@@ -490,9 +500,17 @@ const IssueDetails = () => {
|
||||
}}
|
||||
validationSchema={CommentSchema}
|
||||
onSubmit={async (values, { resetForm }) => {
|
||||
await axios.post(`/api/v1/issue/${issueData?.id}/comment`, {
|
||||
message: values.message,
|
||||
});
|
||||
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();
|
||||
revalidateIssue();
|
||||
resetForm();
|
||||
}}
|
||||
@@ -644,7 +662,8 @@ const IssueDetails = () => {
|
||||
>
|
||||
<PlayIcon />
|
||||
<span>
|
||||
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||
{settings.currentSettings.mediaServerType ===
|
||||
MediaServerType.EMBY
|
||||
? intl.formatMessage(messages.playonplex, {
|
||||
mediaServerName: 'Emby',
|
||||
})
|
||||
@@ -690,7 +709,8 @@ const IssueDetails = () => {
|
||||
>
|
||||
<PlayIcon />
|
||||
<span>
|
||||
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||
{settings.currentSettings.mediaServerType ===
|
||||
MediaServerType.EMBY
|
||||
? intl.formatMessage(messages.play4konplex, {
|
||||
mediaServerName: 'Emby',
|
||||
})
|
||||
|
||||
@@ -11,7 +11,6 @@ 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';
|
||||
@@ -101,14 +100,22 @@ const CreateIssueModal = ({
|
||||
validationSchema={CreateIssueModalSchema}
|
||||
onSubmit={async (values) => {
|
||||
try {
|
||||
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,
|
||||
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,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
const newIssue: Issue = await res.json();
|
||||
|
||||
if (data) {
|
||||
addToast(
|
||||
@@ -119,7 +126,7 @@ const CreateIssueModal = ({
|
||||
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
|
||||
})}
|
||||
</div>
|
||||
<Link href={`/issues/${newIssue.data.id}`} legacyBehavior>
|
||||
<Link href={`/issues/${newIssue.id}`} legacyBehavior>
|
||||
<Button as="a" className="mt-4">
|
||||
<span>{intl.formatMessage(messages.toastviewissue)}</span>
|
||||
<ArrowRightCircleIcon />
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
ClockIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { CogIcon, UserIcon } from '@heroicons/react/24/solid';
|
||||
import axios from 'axios';
|
||||
import Image from 'next/image';
|
||||
import type { LinkProps } from 'next/link';
|
||||
import Link from 'next/link';
|
||||
@@ -39,9 +38,13 @@ const UserDropdown = () => {
|
||||
const { user, revalidate } = useUser();
|
||||
|
||||
const logout = async () => {
|
||||
const response = await axios.post('/api/v1/auth/logout');
|
||||
const res = await fetch('/api/v1/auth/logout', {
|
||||
method: 'POST',
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
const data = await res.json();
|
||||
|
||||
if (response.data?.status === 'ok') {
|
||||
if (data?.status === 'ok') {
|
||||
revalidate();
|
||||
}
|
||||
};
|
||||
@@ -87,9 +90,11 @@ const UserDropdown = () => {
|
||||
<span className="truncate text-xl font-semibold text-gray-200">
|
||||
{user?.displayName}
|
||||
</span>
|
||||
<span className="truncate text-sm text-gray-400">
|
||||
{user?.email}
|
||||
</span>
|
||||
{user?.displayName?.toLowerCase() !== user?.email && (
|
||||
<span className="truncate text-sm text-gray-400">
|
||||
{user?.email}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{user && <MiniQuotaDisplay userId={user?.id} />}
|
||||
|
||||
@@ -2,7 +2,6 @@ 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';
|
||||
@@ -58,11 +57,18 @@ const AddEmailModal: React.FC<AddEmailModalProps> = ({
|
||||
validationSchema={EmailSettingsSchema}
|
||||
onSubmit={async (values) => {
|
||||
try {
|
||||
await axios.post('/api/v1/auth/jellyfin', {
|
||||
username: username,
|
||||
password: password,
|
||||
email: values.email,
|
||||
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,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
|
||||
onSave();
|
||||
} catch (e) {
|
||||
|
||||
@@ -4,10 +4,9 @@ import useSettings from '@app/hooks/useSettings';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { InformationCircleIcon } from '@heroicons/react/24/solid';
|
||||
import { ApiErrorCode } from '@server/constants/error';
|
||||
import axios from 'axios';
|
||||
import { MediaServerType, ServerType } from '@server/constants/server';
|
||||
import { Field, Form, Formik } from 'formik';
|
||||
import getConfig from 'next/config';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { FormattedMessage, useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
@@ -27,6 +26,7 @@ const messages = defineMessages('components.Login', {
|
||||
validationemailformat: 'Valid email required',
|
||||
validationusernamerequired: 'Username required',
|
||||
validationpasswordrequired: 'Password required',
|
||||
validationservertyperequired: 'Please select a server type',
|
||||
validationHostnameRequired: 'You must provide a valid hostname or IP address',
|
||||
validationPortRequired: 'You must provide a valid port number',
|
||||
validationUrlTrailingSlash: 'URL must not end in a trailing slash',
|
||||
@@ -41,42 +41,51 @@ const messages = defineMessages('components.Login', {
|
||||
initialsigningin: 'Connecting…',
|
||||
initialsignin: 'Connect',
|
||||
forgotpassword: 'Forgot Password?',
|
||||
servertype: 'Server Type',
|
||||
back: 'Go back',
|
||||
});
|
||||
|
||||
interface JellyfinLoginProps {
|
||||
revalidate: () => void;
|
||||
initial?: boolean;
|
||||
serverType?: MediaServerType;
|
||||
onCancel?: () => void;
|
||||
}
|
||||
|
||||
const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
|
||||
revalidate,
|
||||
initial,
|
||||
serverType,
|
||||
onCancel,
|
||||
}) => {
|
||||
const toasts = useToasts();
|
||||
const intl = useIntl();
|
||||
const settings = useSettings();
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
|
||||
const mediaServerFormatValues = {
|
||||
mediaServerName:
|
||||
serverType === MediaServerType.JELLYFIN
|
||||
? ServerType.JELLYFIN
|
||||
: serverType === MediaServerType.EMBY
|
||||
? ServerType.EMBY
|
||||
: 'Media Server',
|
||||
};
|
||||
|
||||
if (initial) {
|
||||
const LoginSchema = Yup.object().shape({
|
||||
hostname: Yup.string().required(
|
||||
intl.formatMessage(messages.validationhostrequired, {
|
||||
mediaServerName:
|
||||
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin',
|
||||
})
|
||||
intl.formatMessage(
|
||||
messages.validationhostrequired,
|
||||
mediaServerFormatValues
|
||||
)
|
||||
),
|
||||
port: Yup.number().required(
|
||||
intl.formatMessage(messages.validationPortRequired)
|
||||
),
|
||||
urlBase: Yup.string()
|
||||
.matches(
|
||||
/^(\/[^/].*[^/]$)/,
|
||||
intl.formatMessage(messages.validationUrlBaseLeadingSlash)
|
||||
)
|
||||
.matches(
|
||||
/^(.*[^/])$/,
|
||||
intl.formatMessage(messages.validationUrlBaseTrailingSlash)
|
||||
),
|
||||
urlBase: Yup.string().matches(
|
||||
/^(.*[^/])$/,
|
||||
intl.formatMessage(messages.validationUrlBaseTrailingSlash)
|
||||
),
|
||||
email: Yup.string()
|
||||
.email(intl.formatMessage(messages.validationemailformat))
|
||||
.required(intl.formatMessage(messages.validationemailrequired)),
|
||||
@@ -86,11 +95,6 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
|
||||
password: Yup.string(),
|
||||
});
|
||||
|
||||
const mediaServerFormatValues = {
|
||||
mediaServerName:
|
||||
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin',
|
||||
};
|
||||
|
||||
return (
|
||||
<Formik
|
||||
initialValues={{
|
||||
@@ -105,18 +109,38 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
|
||||
validationSchema={LoginSchema}
|
||||
onSubmit={async (values) => {
|
||||
try {
|
||||
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,
|
||||
// Check if serverType is either 'Jellyfin' or 'Emby'
|
||||
// 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,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error(res.statusText, { cause: res });
|
||||
} catch (e) {
|
||||
let errorData;
|
||||
try {
|
||||
errorData = await e.cause?.text();
|
||||
errorData = JSON.parse(errorData);
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
let errorMessage = null;
|
||||
switch (e.response?.data?.message) {
|
||||
switch (errorData?.message) {
|
||||
case ApiErrorCode.InvalidUrl:
|
||||
errorMessage = messages.invalidurlerror;
|
||||
break;
|
||||
@@ -299,7 +323,7 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8 border-t border-gray-700 pt-5">
|
||||
<div className="flex justify-end">
|
||||
<div className="flex flex-row-reverse justify-between">
|
||||
<span className="inline-flex rounded-md shadow-sm">
|
||||
<Button
|
||||
buttonType="primary"
|
||||
@@ -311,6 +335,13 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
|
||||
: intl.formatMessage(messages.signin)}
|
||||
</Button>
|
||||
</span>
|
||||
{onCancel && (
|
||||
<span className="inline-flex rounded-md shadow-sm">
|
||||
<Button buttonType="default" onClick={() => onCancel()}>
|
||||
<FormattedMessage {...messages.back} />
|
||||
</Button>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
@@ -339,11 +370,18 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
|
||||
validationSchema={LoginSchema}
|
||||
onSubmit={async (values) => {
|
||||
try {
|
||||
await axios.post('/api/v1/auth/jellyfin', {
|
||||
username: values.username,
|
||||
password: values.password,
|
||||
email: values.username,
|
||||
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,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
} catch (e) {
|
||||
toasts.addToast(
|
||||
intl.formatMessage(
|
||||
@@ -409,7 +447,8 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
|
||||
jellyfinForgotPasswordUrl
|
||||
? `${jellyfinForgotPasswordUrl}`
|
||||
: `${baseUrl}/web/index.html#!/${
|
||||
process.env.JELLYFIN_TYPE === 'emby'
|
||||
settings.currentSettings.mediaServerType ===
|
||||
MediaServerType.EMBY
|
||||
? 'startup/'
|
||||
: ''
|
||||
}forgotpassword.html`
|
||||
|
||||
@@ -6,7 +6,6 @@ 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';
|
||||
@@ -56,10 +55,17 @@ const LocalLogin = ({ revalidate }: LocalLoginProps) => {
|
||||
validationSchema={LoginSchema}
|
||||
onSubmit={async (values) => {
|
||||
try {
|
||||
await axios.post('/api/v1/auth/local', {
|
||||
email: values.email,
|
||||
password: values.password,
|
||||
const res = await fetch('/api/v1/auth/local', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: values.email,
|
||||
password: values.password,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
} catch (e) {
|
||||
setLoginError(intl.formatMessage(messages.loginerror));
|
||||
} finally {
|
||||
|
||||
@@ -10,8 +10,6 @@ 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 getConfig from 'next/config';
|
||||
import { useRouter } from 'next/dist/client/router';
|
||||
import Image from 'next/image';
|
||||
import { useEffect, useState } from 'react';
|
||||
@@ -35,7 +33,6 @@ const Login = () => {
|
||||
const { user, revalidate } = useUser();
|
||||
const router = useRouter();
|
||||
const settings = useSettings();
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
|
||||
// Effect that is triggered when the `authToken` comes back from the Plex OAuth
|
||||
// We take the token and attempt to sign in. If we get a success message, we will
|
||||
@@ -44,13 +41,28 @@ const Login = () => {
|
||||
const login = async () => {
|
||||
setProcessing(true);
|
||||
try {
|
||||
const response = await axios.post('/api/v1/auth/plex', { authToken });
|
||||
const res = await fetch('/api/v1/auth/plex', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ authToken }),
|
||||
});
|
||||
if (!res.ok) throw new Error(res.statusText, { cause: res });
|
||||
const data = await res.json();
|
||||
|
||||
if (response.data?.id) {
|
||||
if (data?.id) {
|
||||
revalidate();
|
||||
}
|
||||
} catch (e) {
|
||||
setError(e.response.data.message);
|
||||
let errorData;
|
||||
try {
|
||||
errorData = await e.cause?.text();
|
||||
errorData = JSON.parse(errorData);
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
setError(errorData?.message);
|
||||
setAuthToken(undefined);
|
||||
setProcessing(false);
|
||||
}
|
||||
@@ -74,6 +86,15 @@ const Login = () => {
|
||||
revalidateOnFocus: false,
|
||||
});
|
||||
|
||||
const mediaServerFormatValues = {
|
||||
mediaServerName:
|
||||
settings.currentSettings.mediaServerType === MediaServerType.JELLYFIN
|
||||
? 'Jellyfin'
|
||||
: settings.currentSettings.mediaServerType === MediaServerType.EMBY
|
||||
? 'Emby'
|
||||
: undefined,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative flex min-h-screen flex-col bg-gray-900 py-14">
|
||||
<PageTitle title={intl.formatMessage(messages.signin)} />
|
||||
@@ -140,12 +161,10 @@ const Login = () => {
|
||||
{settings.currentSettings.mediaServerType ==
|
||||
MediaServerType.PLEX
|
||||
? intl.formatMessage(messages.signinwithplex)
|
||||
: intl.formatMessage(messages.signinwithjellyfin, {
|
||||
mediaServerName:
|
||||
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||
? 'Emby'
|
||||
: 'Jellyfin',
|
||||
})}
|
||||
: intl.formatMessage(
|
||||
messages.signinwithjellyfin,
|
||||
mediaServerFormatValues
|
||||
)}
|
||||
</button>
|
||||
<AccordionContent isOpen={openIndexes.includes(0)}>
|
||||
<div className="px-10 py-8">
|
||||
|
||||
@@ -26,8 +26,6 @@ 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 getConfig from 'next/config';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { useIntl } from 'react-intl';
|
||||
@@ -96,7 +94,6 @@ const ManageSlideOver = ({
|
||||
const { user: currentUser, hasPermission } = useUser();
|
||||
const intl = useIntl();
|
||||
const settings = useSettings();
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
const { data: watchData } = useSWR<MediaWatchDataResponse>(
|
||||
settings.currentSettings.mediaServerType === MediaServerType.PLEX &&
|
||||
data.mediaInfo &&
|
||||
@@ -113,16 +110,29 @@ const ManageSlideOver = ({
|
||||
|
||||
const deleteMedia = async () => {
|
||||
if (data.mediaInfo) {
|
||||
await axios.delete(`/api/v1/media/${data.mediaInfo.id}`);
|
||||
const res = await fetch(`/api/v1/media/${data.mediaInfo.id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
revalidate();
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const deleteMediaFile = async () => {
|
||||
if (data.mediaInfo) {
|
||||
await axios.delete(`/api/v1/media/${data.mediaInfo.id}/file`);
|
||||
await axios.delete(`/api/v1/media/${data.mediaInfo.id}`);
|
||||
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();
|
||||
|
||||
revalidate();
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -149,9 +159,16 @@ const ManageSlideOver = ({
|
||||
|
||||
const markAvailable = async (is4k = false) => {
|
||||
if (data.mediaInfo) {
|
||||
await axios.post(`/api/v1/media/${data.mediaInfo?.id}/available`, {
|
||||
is4k,
|
||||
const res = await fetch(`/api/v1/media/${data.mediaInfo?.id}/available`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
is4k,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
revalidate();
|
||||
}
|
||||
};
|
||||
@@ -642,7 +659,8 @@ const ManageSlideOver = ({
|
||||
mediaType === 'movie' ? messages.movie : messages.tvshow
|
||||
),
|
||||
mediaServerName:
|
||||
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||
settings.currentSettings.mediaServerType ===
|
||||
MediaServerType.EMBY
|
||||
? 'Emby'
|
||||
: settings.currentSettings.mediaServerType ===
|
||||
MediaServerType.PLEX
|
||||
|
||||
@@ -57,44 +57,48 @@ const ShowMoreCard = ({ url, posters }: ShowMoreCardProps) => {
|
||||
>
|
||||
<div style={{ paddingBottom: '150%' }}>
|
||||
<div className="absolute inset-0 flex h-full w-full flex-col items-center p-2">
|
||||
<div className="relative z-10 flex h-full flex-wrap items-center justify-center opacity-30">
|
||||
<div className="relative z-10 grid h-full w-full grid-cols-2 items-center justify-center gap-2 opacity-30">
|
||||
{posters[0] && (
|
||||
<div className="w-1/2 p-1">
|
||||
<div className="">
|
||||
<Image
|
||||
src={`//image.tmdb.org/t/p/w300_and_h450_face${posters[0]}`}
|
||||
src={`https://image.tmdb.org/t/p/w300_and_h450_face${posters[0]}`}
|
||||
alt=""
|
||||
className="w-full rounded-md"
|
||||
fill
|
||||
className="rounded-md"
|
||||
width={300}
|
||||
height={450}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{posters[1] && (
|
||||
<div className="w-1/2 p-1">
|
||||
<div className="">
|
||||
<Image
|
||||
src={`//image.tmdb.org/t/p/w300_and_h450_face${posters[1]}`}
|
||||
src={`https://image.tmdb.org/t/p/w300_and_h450_face${posters[1]}`}
|
||||
alt=""
|
||||
className="w-full rounded-md"
|
||||
fill
|
||||
className="rounded-md"
|
||||
width={300}
|
||||
height={450}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{posters[2] && (
|
||||
<div className="w-1/2 p-1">
|
||||
<div className="">
|
||||
<Image
|
||||
src={`//image.tmdb.org/t/p/w300_and_h450_face${posters[2]}`}
|
||||
src={`https://image.tmdb.org/t/p/w300_and_h450_face${posters[2]}`}
|
||||
alt=""
|
||||
className="w-full rounded-md"
|
||||
fill
|
||||
className="rounded-md"
|
||||
width={300}
|
||||
height={450}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{posters[3] && (
|
||||
<div className="w-1/2 p-1">
|
||||
<div className="">
|
||||
<Image
|
||||
src={`//image.tmdb.org/t/p/w300_and_h450_face${posters[3]}`}
|
||||
src={`https://image.tmdb.org/t/p/w300_and_h450_face${posters[3]}`}
|
||||
alt=""
|
||||
className="w-full rounded-md"
|
||||
fill
|
||||
className="rounded-md"
|
||||
width={300}
|
||||
height={450}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -50,7 +50,6 @@ import type { MovieDetails as MovieDetailsType } from '@server/models/Movie';
|
||||
import { countries } from 'country-flag-icons';
|
||||
import 'country-flag-icons/3x2/flags.css';
|
||||
import { uniqBy } from 'lodash';
|
||||
import getConfig from 'next/config';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
@@ -112,7 +111,6 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
|
||||
const minStudios = 3;
|
||||
const [showMoreStudios, setShowMoreStudios] = useState(false);
|
||||
const [showIssueModal, setShowIssueModal] = useState(false);
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
|
||||
const {
|
||||
data,
|
||||
@@ -264,7 +262,7 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
|
||||
?.flatrate ?? [];
|
||||
|
||||
function getAvalaibleMediaServerName() {
|
||||
if (publicRuntimeConfig.JELLYFIN_TYPE === 'emby') {
|
||||
if (settings.currentSettings.mediaServerType === MediaServerType.EMBY) {
|
||||
return intl.formatMessage(messages.play, { mediaServerName: 'Emby' });
|
||||
}
|
||||
|
||||
@@ -276,8 +274,8 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
|
||||
}
|
||||
|
||||
function getAvalaible4kMediaServerName() {
|
||||
if (publicRuntimeConfig.JELLYFIN_TYPE === 'emby') {
|
||||
return intl.formatMessage(messages.play4k, { mediaServerName: 'Emby' });
|
||||
if (settings.currentSettings.mediaServerType === MediaServerType.EMBY) {
|
||||
return intl.formatMessage(messages.play, { mediaServerName: 'Emby' });
|
||||
}
|
||||
|
||||
if (settings.currentSettings.mediaServerType === MediaServerType.PLEX) {
|
||||
|
||||
@@ -17,7 +17,6 @@ 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';
|
||||
@@ -53,7 +52,10 @@ const RequestBlock = ({ request, onUpdate }: RequestBlockProps) => {
|
||||
|
||||
const updateRequest = async (type: 'approve' | 'decline'): Promise<void> => {
|
||||
setIsUpdating(true);
|
||||
await axios.post(`/api/v1/request/${request.id}/${type}`);
|
||||
const res = await fetch(`/api/v1/request/${request.id}/${type}`, {
|
||||
method: 'POST',
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
|
||||
if (onUpdate) {
|
||||
onUpdate();
|
||||
@@ -63,7 +65,10 @@ const RequestBlock = ({ request, onUpdate }: RequestBlockProps) => {
|
||||
|
||||
const deleteRequest = async () => {
|
||||
setIsUpdating(true);
|
||||
await axios.delete(`/api/v1/request/${request.id}`);
|
||||
const res = await fetch(`/api/v1/request/${request.id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
|
||||
if (onUpdate) {
|
||||
onUpdate();
|
||||
|
||||
@@ -13,7 +13,6 @@ 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';
|
||||
|
||||
@@ -94,9 +93,13 @@ const RequestButton = ({
|
||||
request: MediaRequest,
|
||||
type: 'approve' | 'decline'
|
||||
) => {
|
||||
const response = await axios.post(`/api/v1/request/${request.id}/${type}`);
|
||||
const res = await fetch(`/api/v1/request/${request.id}/${type}`, {
|
||||
method: 'POST',
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
const data = await res.json();
|
||||
|
||||
if (response) {
|
||||
if (data) {
|
||||
onUpdate();
|
||||
}
|
||||
};
|
||||
@@ -111,7 +114,11 @@ const RequestButton = ({
|
||||
|
||||
await Promise.all(
|
||||
requests.map(async (request) => {
|
||||
return axios.post(`/api/v1/request/${request.id}/${type}`);
|
||||
const res = await fetch(`/api/v1/request/${request.id}/${type}`, {
|
||||
method: 'POST',
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
return res.json();
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ import { MediaRequestStatus } from '@server/constants/media';
|
||||
import type { MediaRequest } from '@server/entity/MediaRequest';
|
||||
import type { MovieDetails } from '@server/models/Movie';
|
||||
import type { TvDetails } from '@server/models/Tv';
|
||||
import axios from 'axios';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { useEffect, useState } from 'react';
|
||||
@@ -74,7 +73,10 @@ const RequestCardError = ({ requestData }: RequestCardErrorProps) => {
|
||||
});
|
||||
|
||||
const deleteRequest = async () => {
|
||||
await axios.delete(`/api/v1/media/${requestData?.media.id}`);
|
||||
const res = await fetch(`/api/v1/media/${requestData?.media.id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
mutate('/api/v1/media?filter=allavailable&take=20&sort=mediaAdded');
|
||||
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
|
||||
};
|
||||
@@ -255,15 +257,22 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
|
||||
});
|
||||
|
||||
const modifyRequest = async (type: 'approve' | 'decline') => {
|
||||
const response = await axios.post(`/api/v1/request/${request.id}/${type}`);
|
||||
const res = await fetch(`/api/v1/request/${request.id}/${type}`, {
|
||||
method: 'POST',
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
const data = await res.json();
|
||||
|
||||
if (response) {
|
||||
if (data) {
|
||||
revalidate();
|
||||
}
|
||||
};
|
||||
|
||||
const deleteRequest = async () => {
|
||||
await axios.delete(`/api/v1/request/${request.id}`);
|
||||
const res = await fetch(`/api/v1/request/${request.id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
|
||||
};
|
||||
|
||||
@@ -271,9 +280,13 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
|
||||
setRetrying(true);
|
||||
|
||||
try {
|
||||
const response = await axios.post(`/api/v1/request/${request.id}/retry`);
|
||||
const res = await fetch(`/api/v1/request/${request.id}/retry`, {
|
||||
method: 'POST',
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
const data = await res.json();
|
||||
|
||||
if (response) {
|
||||
if (data) {
|
||||
revalidate();
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -20,7 +20,6 @@ import { MediaRequestStatus } from '@server/constants/media';
|
||||
import type { MediaRequest } from '@server/entity/MediaRequest';
|
||||
import type { MovieDetails } from '@server/models/Movie';
|
||||
import type { TvDetails } from '@server/models/Tv';
|
||||
import axios from 'axios';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { useState } from 'react';
|
||||
@@ -62,7 +61,10 @@ const RequestItemError = ({
|
||||
const { hasPermission } = useUser();
|
||||
|
||||
const deleteRequest = async () => {
|
||||
await axios.delete(`/api/v1/media/${requestData?.media.id}`);
|
||||
const res = await fetch(`/api/v1/media/${requestData?.media.id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
revalidateList();
|
||||
};
|
||||
|
||||
@@ -319,15 +321,22 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
||||
const [isRetrying, setRetrying] = useState(false);
|
||||
|
||||
const modifyRequest = async (type: 'approve' | 'decline') => {
|
||||
const response = await axios.post(`/api/v1/request/${request.id}/${type}`);
|
||||
const res = await fetch(`/api/v1/request/${request.id}/${type}`, {
|
||||
method: 'POST',
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
const data = await res.json();
|
||||
|
||||
if (response) {
|
||||
if (data) {
|
||||
revalidate();
|
||||
}
|
||||
};
|
||||
|
||||
const deleteRequest = async () => {
|
||||
await axios.delete(`/api/v1/request/${request.id}`);
|
||||
const res = await fetch(`/api/v1/request/${request.id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
|
||||
revalidateList();
|
||||
};
|
||||
@@ -336,7 +345,12 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
||||
setRetrying(true);
|
||||
|
||||
try {
|
||||
const result = await axios.post(`/api/v1/request/${request.id}/retry`);
|
||||
const res = await fetch(`/api/v1/request/${request.id}/retry`, {
|
||||
method: 'POST',
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
const result = await res.json();
|
||||
|
||||
revalidate(result.data);
|
||||
} catch (e) {
|
||||
addToast(intl.formatMessage(messages.failedretry), {
|
||||
|
||||
@@ -13,7 +13,6 @@ 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';
|
||||
@@ -197,12 +196,19 @@ const CollectionRequestModal = ({
|
||||
(
|
||||
data?.parts.filter((part) => selectedParts.includes(part.id)) ?? []
|
||||
).map(async (part) => {
|
||||
await axios.post<MediaRequest>('/api/v1/request', {
|
||||
mediaId: part.id,
|
||||
mediaType: 'movie',
|
||||
is4k,
|
||||
...overrideParams,
|
||||
const res = await fetch('/api/v1/request', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
mediaId: part.id,
|
||||
mediaType: 'movie',
|
||||
is4k,
|
||||
...overrideParams,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import type { MediaRequest } from '@server/entity/MediaRequest';
|
||||
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,15 +88,23 @@ const MovieRequestModal = ({
|
||||
tags: requestOverrides.tags,
|
||||
};
|
||||
}
|
||||
const response = await axios.post<MediaRequest>('/api/v1/request', {
|
||||
mediaId: data?.id,
|
||||
mediaType: 'movie',
|
||||
is4k,
|
||||
...overrideParams,
|
||||
const res = await fetch('/api/v1/request', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
mediaId: data?.id,
|
||||
mediaType: 'movie',
|
||||
is4k,
|
||||
...overrideParams,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
const mediaRequest: MediaRequest = await res.json();
|
||||
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
|
||||
|
||||
if (response.data) {
|
||||
if (mediaRequest) {
|
||||
if (onComplete) {
|
||||
onComplete(
|
||||
hasPermission(
|
||||
@@ -136,12 +143,14 @@ const MovieRequestModal = ({
|
||||
setIsUpdating(true);
|
||||
|
||||
try {
|
||||
const response = await axios.delete<MediaRequest>(
|
||||
`/api/v1/request/${editRequest?.id}`
|
||||
);
|
||||
const res = await fetch(`/api/v1/request/${editRequest?.id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
|
||||
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
|
||||
|
||||
if (response.status === 204) {
|
||||
if (res.status === 204) {
|
||||
if (onComplete) {
|
||||
onComplete(MediaStatus.UNKNOWN);
|
||||
}
|
||||
@@ -164,17 +173,27 @@ const MovieRequestModal = ({
|
||||
setIsUpdating(true);
|
||||
|
||||
try {
|
||||
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,
|
||||
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,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
|
||||
if (alsoApproveRequest) {
|
||||
await axios.post(`/api/v1/request/${editRequest?.id}/approve`);
|
||||
const res = await fetch(`/api/v1/request/${editRequest?.id}/approve`, {
|
||||
method: 'POST',
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
}
|
||||
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ import type SeasonRequest from '@server/entity/SeasonRequest';
|
||||
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,22 +110,35 @@ const TvRequestModal = ({
|
||||
|
||||
try {
|
||||
if (selectedSeasons.length > 0) {
|
||||
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,
|
||||
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,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
|
||||
if (alsoApproveRequest) {
|
||||
await axios.post(`/api/v1/request/${editRequest.id}/approve`);
|
||||
const res = await fetch(`/api/v1/request/${editRequest.id}/approve`, {
|
||||
method: 'POST',
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
}
|
||||
} else {
|
||||
await axios.delete(`/api/v1/request/${editRequest.id}`);
|
||||
const res = await fetch(`/api/v1/request/${editRequest.id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
}
|
||||
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
|
||||
|
||||
@@ -191,23 +203,32 @@ const TvRequestModal = ({
|
||||
tags: requestOverrides.tags,
|
||||
};
|
||||
}
|
||||
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,
|
||||
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,
|
||||
}),
|
||||
});
|
||||
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 (response.data) {
|
||||
if (mediaRequest) {
|
||||
if (onComplete) {
|
||||
onComplete(response.data.media.status);
|
||||
onComplete(mediaRequest.media.status);
|
||||
}
|
||||
addToast(
|
||||
<span>
|
||||
|
||||
@@ -4,7 +4,6 @@ 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';
|
||||
@@ -85,14 +84,18 @@ const ResetPassword = () => {
|
||||
}}
|
||||
validationSchema={ResetSchema}
|
||||
onSubmit={async (values) => {
|
||||
const response = await axios.post(
|
||||
`/api/v1/auth/reset-password`,
|
||||
{
|
||||
const res = await fetch(`/api/v1/auth/reset-password`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: values.email,
|
||||
}
|
||||
);
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
|
||||
if (response.status === 200) {
|
||||
if (res.status === 200) {
|
||||
setSubmitted(true);
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -5,7 +5,6 @@ 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';
|
||||
@@ -100,14 +99,21 @@ const ResetPassword = () => {
|
||||
}}
|
||||
validationSchema={ResetSchema}
|
||||
onSubmit={async (values) => {
|
||||
const response = await axios.post(
|
||||
const res = await fetch(
|
||||
`/api/v1/auth/reset-password/${guid}`,
|
||||
{
|
||||
password: values.password,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
password: values.password,
|
||||
}),
|
||||
}
|
||||
);
|
||||
if (!res.ok) throw new Error();
|
||||
|
||||
if (response.status === 200) {
|
||||
if (res.status === 200) {
|
||||
setSubmitted(true);
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -18,7 +18,6 @@ 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';
|
||||
@@ -69,11 +68,9 @@ export const CompanySelector = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await axios.get<ProductionCompany>(
|
||||
`/api/v1/studio/${defaultValue}`
|
||||
);
|
||||
|
||||
const studio = response.data;
|
||||
const res = await fetch(`/api/v1/studio/${defaultValue}`);
|
||||
if (!res.ok) throw new Error();
|
||||
const studio: ProductionCompany = await res.json();
|
||||
|
||||
setDefaultDataValue([
|
||||
{
|
||||
@@ -91,16 +88,15 @@ export const CompanySelector = ({
|
||||
return [];
|
||||
}
|
||||
|
||||
const results = await axios.get<TmdbCompanySearchResponse>(
|
||||
'/api/v1/search/company',
|
||||
{
|
||||
params: {
|
||||
query: encodeURIExtraParams(inputValue),
|
||||
},
|
||||
}
|
||||
const res = await fetch(
|
||||
`/api/v1/search/company?query=${encodeURIExtraParams(inputValue)}`
|
||||
);
|
||||
if (!res.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
const results: TmdbCompanySearchResponse = await res.json();
|
||||
|
||||
return results.data.results.map((result) => ({
|
||||
return results.results.map((result) => ({
|
||||
label: result.name,
|
||||
value: result.id,
|
||||
}));
|
||||
@@ -154,11 +150,15 @@ export const GenreSelector = ({
|
||||
|
||||
const genres = defaultValue.split(',');
|
||||
|
||||
const response = await axios.get<TmdbGenre[]>(`/api/v1/genres/${type}`);
|
||||
const res = await fetch(`/api/v1/genres/${type}`);
|
||||
if (!res.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
const response: TmdbGenre[] = await res.json();
|
||||
|
||||
const genreData = genres
|
||||
.filter((genre) => response.data.find((gd) => gd.id === Number(genre)))
|
||||
.map((g) => response.data.find((gd) => gd.id === Number(g)))
|
||||
.filter((genre) => response.find((gd) => gd.id === Number(genre)))
|
||||
.map((g) => response.find((gd) => gd.id === Number(g)))
|
||||
.map((g) => ({
|
||||
label: g?.name ?? '',
|
||||
value: g?.id ?? 0,
|
||||
@@ -171,11 +171,11 @@ export const GenreSelector = ({
|
||||
}, [defaultValue, type]);
|
||||
|
||||
const loadGenreOptions = async (inputValue: string) => {
|
||||
const results = await axios.get<GenreSliderItem[]>(
|
||||
`/api/v1/discover/genreslider/${type}`
|
||||
);
|
||||
const res = await fetch(`/api/v1/discover/genreslider/${type}`);
|
||||
if (!res.ok) throw new Error();
|
||||
const results: GenreSliderItem[] = await res.json();
|
||||
|
||||
return results.data
|
||||
return results
|
||||
.map((result) => ({
|
||||
label: result.name,
|
||||
value: result.id,
|
||||
@@ -222,11 +222,13 @@ export const KeywordSelector = ({
|
||||
|
||||
const keywords = await Promise.all(
|
||||
defaultValue.split(',').map(async (keywordId) => {
|
||||
const keyword = await axios.get<Keyword>(
|
||||
`/api/v1/keyword/${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();
|
||||
|
||||
return keyword.data;
|
||||
return keyword;
|
||||
})
|
||||
);
|
||||
|
||||
@@ -242,16 +244,15 @@ export const KeywordSelector = ({
|
||||
}, [defaultValue]);
|
||||
|
||||
const loadKeywordOptions = async (inputValue: string) => {
|
||||
const results = await axios.get<TmdbKeywordSearchResponse>(
|
||||
'/api/v1/search/keyword',
|
||||
{
|
||||
params: {
|
||||
query: encodeURIExtraParams(inputValue),
|
||||
},
|
||||
}
|
||||
const res = await fetch(
|
||||
`/api/v1/search/keyword?query=${encodeURIExtraParams(inputValue)}`
|
||||
);
|
||||
if (!res.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
const results: TmdbKeywordSearchResponse = await res.json();
|
||||
|
||||
return results.data.results.map((result) => ({
|
||||
return results.results.map((result) => ({
|
||||
label: result.name,
|
||||
value: result.id,
|
||||
}));
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/* 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 = () => {
|
||||
@@ -26,11 +25,18 @@ const ServiceWorkerSetup = () => {
|
||||
const parsedSub = JSON.parse(JSON.stringify(sub));
|
||||
|
||||
if (parsedSub.keys.p256dh && parsedSub.keys.auth) {
|
||||
await axios.post('/api/v1/user/registerPushSubscription', {
|
||||
endpoint: parsedSub.endpoint,
|
||||
p256dh: parsedSub.keys.p256dh,
|
||||
auth: 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,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -5,7 +5,6 @@ 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';
|
||||
@@ -73,16 +72,23 @@ const NotificationsDiscord = () => {
|
||||
validationSchema={NotificationsDiscordSchema}
|
||||
onSubmit={async (values) => {
|
||||
try {
|
||||
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,
|
||||
const res = await fetch('/api/v1/settings/notifications/discord', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
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',
|
||||
@@ -121,16 +127,26 @@ const NotificationsDiscord = () => {
|
||||
toastId = id;
|
||||
}
|
||||
);
|
||||
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,
|
||||
},
|
||||
});
|
||||
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();
|
||||
|
||||
if (toastId) {
|
||||
removeToast(toastId);
|
||||
|
||||
@@ -5,7 +5,6 @@ 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';
|
||||
@@ -148,24 +147,31 @@ const NotificationsEmail = () => {
|
||||
validationSchema={NotificationsEmailSchema}
|
||||
onSubmit={async (values) => {
|
||||
try {
|
||||
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,
|
||||
const res = await fetch('/api/v1/settings/notifications/email', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
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), {
|
||||
@@ -197,22 +203,32 @@ const NotificationsEmail = () => {
|
||||
toastId = id;
|
||||
}
|
||||
);
|
||||
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,
|
||||
},
|
||||
});
|
||||
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();
|
||||
|
||||
if (toastId) {
|
||||
removeToast(toastId);
|
||||
|
||||
@@ -4,7 +4,6 @@ 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';
|
||||
@@ -83,14 +82,21 @@ const NotificationsGotify = () => {
|
||||
validationSchema={NotificationsGotifySchema}
|
||||
onSubmit={async (values) => {
|
||||
try {
|
||||
await axios.post('/api/v1/settings/notifications/gotify', {
|
||||
enabled: values.enabled,
|
||||
types: values.types,
|
||||
options: {
|
||||
url: values.url,
|
||||
token: values.token,
|
||||
const res = await fetch('/api/v1/settings/notifications/gotify', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
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,
|
||||
@@ -128,14 +134,24 @@ const NotificationsGotify = () => {
|
||||
toastId = id;
|
||||
}
|
||||
);
|
||||
await axios.post('/api/v1/settings/notifications/gotify/test', {
|
||||
enabled: true,
|
||||
types: values.types,
|
||||
options: {
|
||||
url: values.url,
|
||||
token: values.token,
|
||||
},
|
||||
});
|
||||
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();
|
||||
|
||||
if (toastId) {
|
||||
removeToast(toastId);
|
||||
|
||||
@@ -4,7 +4,6 @@ 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';
|
||||
@@ -69,14 +68,21 @@ const NotificationsLunaSea = () => {
|
||||
validationSchema={NotificationsLunaSeaSchema}
|
||||
onSubmit={async (values) => {
|
||||
try {
|
||||
await axios.post('/api/v1/settings/notifications/lunasea', {
|
||||
enabled: values.enabled,
|
||||
types: values.types,
|
||||
options: {
|
||||
webhookUrl: values.webhookUrl,
|
||||
profileName: values.profileName,
|
||||
const res = await fetch('/api/v1/settings/notifications/lunasea', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
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,
|
||||
@@ -114,14 +120,24 @@ const NotificationsLunaSea = () => {
|
||||
toastId = id;
|
||||
}
|
||||
);
|
||||
await axios.post('/api/v1/settings/notifications/lunasea/test', {
|
||||
enabled: true,
|
||||
types: values.types,
|
||||
options: {
|
||||
webhookUrl: values.webhookUrl,
|
||||
profileName: values.profileName,
|
||||
},
|
||||
});
|
||||
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();
|
||||
|
||||
if (toastId) {
|
||||
removeToast(toastId);
|
||||
|
||||
@@ -5,7 +5,6 @@ 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,14 +67,21 @@ const NotificationsPushbullet = () => {
|
||||
validationSchema={NotificationsPushbulletSchema}
|
||||
onSubmit={async (values) => {
|
||||
try {
|
||||
await axios.post('/api/v1/settings/notifications/pushbullet', {
|
||||
enabled: values.enabled,
|
||||
types: values.types,
|
||||
options: {
|
||||
accessToken: values.accessToken,
|
||||
channelTag: values.channelTag,
|
||||
const res = await fetch('/api/v1/settings/notifications/pushbullet', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
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,
|
||||
@@ -113,14 +119,24 @@ const NotificationsPushbullet = () => {
|
||||
toastId = id;
|
||||
}
|
||||
);
|
||||
await axios.post('/api/v1/settings/notifications/pushbullet/test', {
|
||||
enabled: true,
|
||||
types: values.types,
|
||||
options: {
|
||||
accessToken: values.accessToken,
|
||||
channelTag: values.channelTag,
|
||||
},
|
||||
});
|
||||
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();
|
||||
|
||||
if (toastId) {
|
||||
removeToast(toastId);
|
||||
|
||||
@@ -5,7 +5,6 @@ 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';
|
||||
@@ -94,14 +93,21 @@ const NotificationsPushover = () => {
|
||||
validationSchema={NotificationsPushoverSchema}
|
||||
onSubmit={async (values) => {
|
||||
try {
|
||||
await axios.post('/api/v1/settings/notifications/pushover', {
|
||||
enabled: values.enabled,
|
||||
types: values.types,
|
||||
options: {
|
||||
accessToken: values.accessToken,
|
||||
userToken: values.userToken,
|
||||
const res = await fetch('/api/v1/settings/notifications/pushover', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
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,
|
||||
@@ -139,16 +145,25 @@ const NotificationsPushover = () => {
|
||||
toastId = id;
|
||||
}
|
||||
);
|
||||
await axios.post('/api/v1/settings/notifications/pushover/test', {
|
||||
enabled: true,
|
||||
types: values.types,
|
||||
options: {
|
||||
accessToken: values.accessToken,
|
||||
userToken: values.userToken,
|
||||
sound: values.sound,
|
||||
},
|
||||
});
|
||||
|
||||
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();
|
||||
if (toastId) {
|
||||
removeToast(toastId);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ 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';
|
||||
@@ -65,13 +64,20 @@ const NotificationsSlack = () => {
|
||||
validationSchema={NotificationsSlackSchema}
|
||||
onSubmit={async (values) => {
|
||||
try {
|
||||
await axios.post('/api/v1/settings/notifications/slack', {
|
||||
enabled: values.enabled,
|
||||
types: values.types,
|
||||
options: {
|
||||
webhookUrl: values.webhookUrl,
|
||||
const res = await fetch('/api/v1/settings/notifications/slack', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
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,
|
||||
@@ -109,13 +115,23 @@ const NotificationsSlack = () => {
|
||||
toastId = id;
|
||||
}
|
||||
);
|
||||
await axios.post('/api/v1/settings/notifications/slack/test', {
|
||||
enabled: true,
|
||||
types: values.types,
|
||||
options: {
|
||||
webhookUrl: values.webhookUrl,
|
||||
},
|
||||
});
|
||||
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();
|
||||
|
||||
if (toastId) {
|
||||
removeToast(toastId);
|
||||
|
||||
@@ -5,7 +5,6 @@ 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';
|
||||
@@ -84,16 +83,23 @@ const NotificationsTelegram = () => {
|
||||
validationSchema={NotificationsTelegramSchema}
|
||||
onSubmit={async (values) => {
|
||||
try {
|
||||
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,
|
||||
const res = await fetch('/api/v1/settings/notifications/telegram', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
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',
|
||||
@@ -132,16 +138,26 @@ const NotificationsTelegram = () => {
|
||||
toastId = id;
|
||||
}
|
||||
);
|
||||
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,
|
||||
},
|
||||
});
|
||||
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();
|
||||
|
||||
if (toastId) {
|
||||
removeToast(toastId);
|
||||
|
||||
@@ -4,7 +4,6 @@ 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';
|
||||
@@ -58,10 +57,17 @@ const NotificationsWebPush = () => {
|
||||
}}
|
||||
onSubmit={async (values) => {
|
||||
try {
|
||||
await axios.post('/api/v1/settings/notifications/webpush', {
|
||||
enabled: values.enabled,
|
||||
options: {},
|
||||
const res = await fetch('/api/v1/settings/notifications/webpush', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
enabled: values.enabled,
|
||||
options: {},
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
mutate('/api/v1/settings/public');
|
||||
addToast(intl.formatMessage(messages.webpushsettingssaved), {
|
||||
appearance: 'success',
|
||||
@@ -92,10 +98,20 @@ const NotificationsWebPush = () => {
|
||||
toastId = id;
|
||||
}
|
||||
);
|
||||
await axios.post('/api/v1/settings/notifications/webpush/test', {
|
||||
enabled: true,
|
||||
options: {},
|
||||
});
|
||||
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();
|
||||
|
||||
if (toastId) {
|
||||
removeToast(toastId);
|
||||
|
||||
@@ -8,7 +8,6 @@ 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';
|
||||
@@ -150,15 +149,22 @@ const NotificationsWebhook = () => {
|
||||
validationSchema={NotificationsWebhookSchema}
|
||||
onSubmit={async (values) => {
|
||||
try {
|
||||
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,
|
||||
const res = await fetch('/api/v1/settings/notifications/webhook', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
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,
|
||||
@@ -207,16 +213,25 @@ const NotificationsWebhook = () => {
|
||||
toastId = id;
|
||||
}
|
||||
);
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
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();
|
||||
if (toastId) {
|
||||
removeToast(toastId);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ 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';
|
||||
@@ -166,19 +165,24 @@ const RadarrModal = ({ onClose, radarr, onSave }: RadarrModalProps) => {
|
||||
}) => {
|
||||
setIsTesting(true);
|
||||
try {
|
||||
const response = await axios.post<TestResponse>(
|
||||
'/api/v1/settings/radarr/test',
|
||||
{
|
||||
const res = await fetch('/api/v1/settings/radarr/test', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
hostname,
|
||||
apiKey,
|
||||
port: Number(port),
|
||||
baseUrl,
|
||||
useSsl,
|
||||
}
|
||||
);
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
const data = await res.json();
|
||||
|
||||
setIsValidated(true);
|
||||
setTestResponse(response.data);
|
||||
setTestResponse(data);
|
||||
if (initialLoad.current) {
|
||||
addToast(intl.formatMessage(messages.toastRadarrTestSuccess), {
|
||||
appearance: 'success',
|
||||
@@ -271,12 +275,23 @@ const RadarrModal = ({ onClose, radarr, onSave }: RadarrModalProps) => {
|
||||
tagRequests: values.tagRequests,
|
||||
};
|
||||
if (!radarr) {
|
||||
await axios.post('/api/v1/settings/radarr', submission);
|
||||
const res = await fetch('/api/v1/settings/radarr', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(submission),
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
} else {
|
||||
await axios.put(
|
||||
`/api/v1/settings/radarr/${radarr.id}`,
|
||||
submission
|
||||
);
|
||||
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();
|
||||
}
|
||||
|
||||
onSave();
|
||||
|
||||
@@ -2,14 +2,14 @@ import Badge from '@app/components/Common/Badge';
|
||||
import Button from '@app/components/Common/Button';
|
||||
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
||||
import LibraryItem from '@app/components/Settings/LibraryItem';
|
||||
import useSettings from '@app/hooks/useSettings';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
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 getConfig from 'next/config';
|
||||
import { useState } from 'react';
|
||||
import { FormattedMessage, useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
@@ -101,7 +101,7 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
|
||||
);
|
||||
const intl = useIntl();
|
||||
const { addToast } = useToasts();
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
const settings = useSettings();
|
||||
|
||||
const JellyfinSettingsSchema = Yup.object().shape({
|
||||
hostname: Yup.string()
|
||||
@@ -167,13 +167,25 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
|
||||
}
|
||||
|
||||
try {
|
||||
await axios.get('/api/v1/settings/jellyfin/library', {
|
||||
params,
|
||||
const searchParams = new URLSearchParams({
|
||||
sync: params.sync ? 'true' : 'false',
|
||||
...(params.enable ? { enable: params.enable } : {}),
|
||||
});
|
||||
const res = await fetch(
|
||||
`/api/v1/settings/jellyfin/library?${searchParams.toString()}`
|
||||
);
|
||||
if (!res.ok) throw new Error(res.statusText, { cause: res });
|
||||
setIsSyncing(false);
|
||||
revalidate();
|
||||
} catch (e) {
|
||||
if (e.response.data.message === 'SYNC_ERROR_GROUPED_FOLDERS') {
|
||||
let errorData;
|
||||
try {
|
||||
errorData = await e.cause?.text();
|
||||
errorData = JSON.parse(errorData);
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
if (errorData?.message === 'SYNC_ERROR_GROUPED_FOLDERS') {
|
||||
toasts.addToast(
|
||||
intl.formatMessage(
|
||||
messages.jellyfinSyncFailedAutomaticGroupedFolders
|
||||
@@ -183,7 +195,7 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
|
||||
appearance: 'warning',
|
||||
}
|
||||
);
|
||||
} else if (e.response.data.message === 'SYNC_ERROR_NO_LIBRARIES') {
|
||||
} else if (errorData?.message === 'SYNC_ERROR_NO_LIBRARIES') {
|
||||
toasts.addToast(
|
||||
intl.formatMessage(messages.jellyfinSyncFailedNoLibrariesFound),
|
||||
{
|
||||
@@ -206,16 +218,32 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
|
||||
};
|
||||
|
||||
const startScan = async () => {
|
||||
await axios.post('/api/v1/settings/jellyfin/sync', {
|
||||
start: true,
|
||||
const res = await fetch('/api/v1/settings/jellyfin/sync', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
start: true,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
|
||||
revalidateSync();
|
||||
};
|
||||
|
||||
const cancelScan = async () => {
|
||||
await axios.post('/api/v1/settings/jellyfin/sync', {
|
||||
cancel: true,
|
||||
const res = await fetch('/api/v1/settings/jellyfin/sync', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
cancel: true,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
|
||||
revalidateSync();
|
||||
};
|
||||
|
||||
@@ -230,15 +258,19 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
|
||||
.join(',');
|
||||
}
|
||||
|
||||
await axios.get('/api/v1/settings/jellyfin/library', {
|
||||
params,
|
||||
});
|
||||
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 {
|
||||
await axios.get('/api/v1/settings/jellyfin/library', {
|
||||
params: {
|
||||
enable: [...activeLibraries, libraryId].join(','),
|
||||
},
|
||||
const searchParams = new URLSearchParams({
|
||||
enable: [...activeLibraries, libraryId].join(','),
|
||||
});
|
||||
const res = await fetch(
|
||||
`/api/v1/settings/jellyfin/library?${searchParams.toString()}`
|
||||
);
|
||||
if (!res.ok) throw new Error();
|
||||
}
|
||||
if (onComplete) {
|
||||
onComplete();
|
||||
@@ -251,26 +283,29 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
|
||||
const mediaServerFormatValues = {
|
||||
mediaServerName:
|
||||
settings.currentSettings.mediaServerType === MediaServerType.JELLYFIN
|
||||
? 'Jellyfin'
|
||||
: settings.currentSettings.mediaServerType === MediaServerType.EMBY
|
||||
? 'Emby'
|
||||
: undefined,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-6">
|
||||
<h3 className="heading">
|
||||
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||
? intl.formatMessage(messages.jellyfinlibraries, {
|
||||
mediaServerName: 'Emby',
|
||||
})
|
||||
: intl.formatMessage(messages.jellyfinlibraries, {
|
||||
mediaServerName: 'Jellyfin',
|
||||
})}
|
||||
{intl.formatMessage(
|
||||
messages.jellyfinlibraries,
|
||||
mediaServerFormatValues
|
||||
)}
|
||||
</h3>
|
||||
<p className="description">
|
||||
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||
? intl.formatMessage(messages.jellyfinlibrariesDescription, {
|
||||
mediaServerName: 'Emby',
|
||||
})
|
||||
: intl.formatMessage(messages.jellyfinlibrariesDescription, {
|
||||
mediaServerName: 'Jellyfin',
|
||||
})}
|
||||
{intl.formatMessage(
|
||||
messages.jellyfinlibrariesDescription,
|
||||
mediaServerFormatValues
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="section">
|
||||
@@ -307,13 +342,10 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
|
||||
<FormattedMessage {...messages.manualscanJellyfin} />
|
||||
</h3>
|
||||
<p className="description">
|
||||
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||
? intl.formatMessage(messages.manualscanDescriptionJellyfin, {
|
||||
mediaServerName: 'Emby',
|
||||
})
|
||||
: intl.formatMessage(messages.manualscanDescriptionJellyfin, {
|
||||
mediaServerName: 'Jellyfin',
|
||||
})}
|
||||
{intl.formatMessage(
|
||||
messages.manualscanDescriptionJellyfin,
|
||||
mediaServerFormatValues
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="section">
|
||||
@@ -417,22 +449,16 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
|
||||
<>
|
||||
<div className="mt-10 mb-6">
|
||||
<h3 className="heading">
|
||||
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||
? intl.formatMessage(messages.jellyfinSettings, {
|
||||
mediaServerName: 'Emby',
|
||||
})
|
||||
: intl.formatMessage(messages.jellyfinSettings, {
|
||||
mediaServerName: 'Jellyfin',
|
||||
})}
|
||||
{intl.formatMessage(
|
||||
messages.jellyfinSettings,
|
||||
mediaServerFormatValues
|
||||
)}
|
||||
</h3>
|
||||
<p className="description">
|
||||
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||
? intl.formatMessage(messages.jellyfinSettingsDescription, {
|
||||
mediaServerName: 'Emby',
|
||||
})
|
||||
: intl.formatMessage(messages.jellyfinSettingsDescription, {
|
||||
mediaServerName: 'Jellyfin',
|
||||
})}
|
||||
{intl.formatMessage(
|
||||
messages.jellyfinSettingsDescription,
|
||||
mediaServerFormatValues
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<Formik
|
||||
@@ -447,19 +473,27 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
|
||||
validationSchema={JellyfinSettingsSchema}
|
||||
onSubmit={async (values) => {
|
||||
try {
|
||||
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,
|
||||
} as JellyfinSettings);
|
||||
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,
|
||||
} as JellyfinSettings),
|
||||
});
|
||||
if (!res.ok) throw new Error(res.statusText, { cause: res });
|
||||
|
||||
addToast(
|
||||
intl.formatMessage(messages.jellyfinSettingsSuccess, {
|
||||
mediaServerName:
|
||||
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||
settings.currentSettings.mediaServerType ===
|
||||
MediaServerType.EMBY
|
||||
? 'Emby'
|
||||
: 'Jellyfin',
|
||||
}),
|
||||
@@ -469,14 +503,19 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
if (e.response?.data?.message === ApiErrorCode.InvalidUrl) {
|
||||
let errorData;
|
||||
try {
|
||||
errorData = await e.cause?.text();
|
||||
errorData = JSON.parse(errorData);
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
if (errorData?.message === ApiErrorCode.InvalidUrl) {
|
||||
addToast(
|
||||
intl.formatMessage(messages.invalidurlerror, {
|
||||
mediaServerName:
|
||||
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||
? 'Emby'
|
||||
: 'Jellyfin',
|
||||
}),
|
||||
intl.formatMessage(
|
||||
messages.invalidurlerror,
|
||||
mediaServerFormatValues
|
||||
),
|
||||
{
|
||||
autoDismiss: true,
|
||||
appearance: 'error',
|
||||
@@ -484,12 +523,10 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
|
||||
);
|
||||
} else {
|
||||
addToast(
|
||||
intl.formatMessage(messages.jellyfinSettingsFailure, {
|
||||
mediaServerName:
|
||||
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||
? 'Emby'
|
||||
: 'Jellyfin',
|
||||
}),
|
||||
intl.formatMessage(
|
||||
messages.jellyfinSettingsFailure,
|
||||
mediaServerFormatValues
|
||||
),
|
||||
{
|
||||
autoDismiss: true,
|
||||
appearance: 'error',
|
||||
|
||||
@@ -7,6 +7,7 @@ import PageTitle from '@app/components/Common/PageTitle';
|
||||
import Table from '@app/components/Common/Table';
|
||||
import useLocale from '@app/hooks/useLocale';
|
||||
import useSettings from '@app/hooks/useSettings';
|
||||
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { formatBytes } from '@app/utils/numberHelpers';
|
||||
@@ -19,7 +20,6 @@ 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';
|
||||
@@ -58,8 +58,8 @@ const messages: { [messageName: string]: MessageDescriptor } = defineMessages(
|
||||
'plex-recently-added-scan': 'Plex Recently Added Scan',
|
||||
'plex-full-scan': 'Plex Full Library Scan',
|
||||
'plex-watchlist-sync': 'Plex Watchlist Sync',
|
||||
'jellyfin-recently-added-scan': 'Jellyfin Recently Added Scan',
|
||||
'jellyfin-full-scan': 'Jellyfin Full Library Scan',
|
||||
'jellyfin-recently-added-scan': 'Jellyfin Recently Added Scan',
|
||||
'availability-sync': 'Media Availability Sync',
|
||||
'radarr-scan': 'Radarr Scan',
|
||||
'sonarr-scan': 'Sonarr Scan',
|
||||
@@ -168,12 +168,29 @@ const SettingsJobs = () => {
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const settings = useSettings();
|
||||
|
||||
if (settings.currentSettings.mediaServerType === MediaServerType.EMBY) {
|
||||
messages['jellyfin-recently-added-scan'] = {
|
||||
id: 'jellyfin-recently-added-scan',
|
||||
defaultMessage: 'Emby Recently Added Scan',
|
||||
};
|
||||
}
|
||||
|
||||
if (settings.currentSettings.mediaServerType === MediaServerType.EMBY) {
|
||||
messages['jellyfin-full-scan'] = {
|
||||
id: 'jellyfin-full-scan',
|
||||
defaultMessage: 'Emby Full Library Scan',
|
||||
};
|
||||
}
|
||||
|
||||
if (!data && !error) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
|
||||
const runJob = async (job: Job) => {
|
||||
await axios.post(`/api/v1/settings/jobs/${job.id}/run`);
|
||||
const res = await fetch(`/api/v1/settings/jobs/${job.id}/run`, {
|
||||
method: 'POST',
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
addToast(
|
||||
intl.formatMessage(messages.jobstarted, {
|
||||
jobname: intl.formatMessage(messages[job.id] ?? messages.unknownJob),
|
||||
@@ -187,7 +204,10 @@ const SettingsJobs = () => {
|
||||
};
|
||||
|
||||
const cancelJob = async (job: Job) => {
|
||||
await axios.post(`/api/v1/settings/jobs/${job.id}/cancel`);
|
||||
const res = await fetch(`/api/v1/settings/jobs/${job.id}/cancel`, {
|
||||
method: 'POST',
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
addToast(
|
||||
intl.formatMessage(messages.jobcancelled, {
|
||||
jobname: intl.formatMessage(messages[job.id] ?? messages.unknownJob),
|
||||
@@ -201,7 +221,10 @@ const SettingsJobs = () => {
|
||||
};
|
||||
|
||||
const flushCache = async (cache: CacheItem) => {
|
||||
await axios.post(`/api/v1/settings/cache/${cache.id}/flush`);
|
||||
const res = await fetch(`/api/v1/settings/cache/${cache.id}/flush`, {
|
||||
method: 'POST',
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
addToast(
|
||||
intl.formatMessage(messages.cacheflushed, { cachename: cache.name }),
|
||||
{
|
||||
@@ -228,12 +251,19 @@ const SettingsJobs = () => {
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
await axios.post(
|
||||
const res = await fetch(
|
||||
`/api/v1/settings/jobs/${jobModalState.job.id}/schedule`,
|
||||
{
|
||||
schedule: jobScheduleCron.join(' '),
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
schedule: jobScheduleCron.join(' '),
|
||||
}),
|
||||
}
|
||||
);
|
||||
if (!res.ok) throw new Error();
|
||||
|
||||
addToast(intl.formatMessage(messages.jobScheduleEditSaved), {
|
||||
appearance: 'success',
|
||||
|
||||
@@ -5,7 +5,6 @@ import useSettings from '@app/hooks/useSettings';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { MediaServerType } from '@server/constants/server';
|
||||
import getConfig from 'next/config';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
const messages = defineMessages('components.Settings', {
|
||||
@@ -26,7 +25,6 @@ type SettingsLayoutProps = {
|
||||
|
||||
const SettingsLayout = ({ children }: SettingsLayoutProps) => {
|
||||
const intl = useIntl();
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
const settings = useSettings();
|
||||
const settingsRoutes: SettingsRoute[] = [
|
||||
{
|
||||
@@ -89,7 +87,11 @@ const SettingsLayout = ({ children }: SettingsLayoutProps) => {
|
||||
function getAvailableMediaServerName() {
|
||||
return intl.formatMessage(messages.menuJellyfinSettings, {
|
||||
mediaServerName:
|
||||
publicRuntimeConfig.JELLYFIN_TYPE === 'emby' ? 'Emby' : 'Jellyfin',
|
||||
settings.currentSettings.mediaServerType === MediaServerType.JELLYFIN
|
||||
? 'Jellyfin'
|
||||
: settings.currentSettings.mediaServerType === MediaServerType.EMBY
|
||||
? 'Emby'
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -17,7 +17,6 @@ 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';
|
||||
@@ -87,7 +86,10 @@ const SettingsMain = () => {
|
||||
|
||||
const regenerate = async () => {
|
||||
try {
|
||||
await axios.post('/api/v1/settings/main/regenerate');
|
||||
const res = await fetch('/api/v1/settings/main/regenerate', {
|
||||
method: 'POST',
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
|
||||
revalidate();
|
||||
addToast(intl.formatMessage(messages.toastApiKeySuccess), {
|
||||
@@ -140,18 +142,25 @@ const SettingsMain = () => {
|
||||
validationSchema={MainSettingsSchema}
|
||||
onSubmit={async (values) => {
|
||||
try {
|
||||
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,
|
||||
const res = await fetch('/api/v1/settings/main', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
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,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
mutate('/api/v1/settings/public');
|
||||
mutate('/api/v1/status');
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ 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';
|
||||
@@ -241,9 +240,15 @@ const SettingsPlex = ({ onComplete }: SettingsPlexProps) => {
|
||||
params.enable = activeLibraries.join(',');
|
||||
}
|
||||
|
||||
await axios.get('/api/v1/settings/plex/library', {
|
||||
params,
|
||||
const searchParams = new URLSearchParams({
|
||||
sync: params.sync ? 'true' : 'false',
|
||||
...(params.enable ? { enable: params.enable } : {}),
|
||||
});
|
||||
const res = await fetch(
|
||||
`/api/v1/settings/plex/library?${searchParams.toString()}`
|
||||
);
|
||||
if (!res.ok) throw new Error();
|
||||
|
||||
setIsSyncing(false);
|
||||
revalidate();
|
||||
};
|
||||
@@ -262,11 +267,12 @@ const SettingsPlex = ({ onComplete }: SettingsPlexProps) => {
|
||||
toastId = id;
|
||||
}
|
||||
);
|
||||
const response = await axios.get<PlexDevice[]>(
|
||||
'/api/v1/settings/plex/devices/servers'
|
||||
);
|
||||
if (response.data) {
|
||||
setAvailableServers(response.data);
|
||||
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);
|
||||
}
|
||||
if (toastId) {
|
||||
removeToast(toastId);
|
||||
@@ -289,16 +295,30 @@ const SettingsPlex = ({ onComplete }: SettingsPlexProps) => {
|
||||
};
|
||||
|
||||
const startScan = async () => {
|
||||
await axios.post('/api/v1/settings/plex/sync', {
|
||||
start: true,
|
||||
const res = await fetch('/api/v1/settings/plex/sync', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
start: true,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
revalidateSync();
|
||||
};
|
||||
|
||||
const cancelScan = async () => {
|
||||
await axios.post('/api/v1/settings/plex/sync', {
|
||||
cancel: true,
|
||||
const res = await fetch('/api/v1/settings/plex/sync', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
cancel: true,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
revalidateSync();
|
||||
};
|
||||
|
||||
@@ -313,15 +333,19 @@ const SettingsPlex = ({ onComplete }: SettingsPlexProps) => {
|
||||
.join(',');
|
||||
}
|
||||
|
||||
await axios.get('/api/v1/settings/plex/library', {
|
||||
params,
|
||||
});
|
||||
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 {
|
||||
await axios.get('/api/v1/settings/plex/library', {
|
||||
params: {
|
||||
enable: [...activeLibraries, libraryId].join(','),
|
||||
},
|
||||
const searchParams = new URLSearchParams({
|
||||
enable: [...activeLibraries, libraryId].join(','),
|
||||
});
|
||||
const res = await fetch(
|
||||
`/api/v1/settings/plex/library?${searchParams.toString()}`
|
||||
);
|
||||
if (!res.ok) throw new Error();
|
||||
}
|
||||
setIsSyncing(false);
|
||||
revalidate();
|
||||
@@ -385,12 +409,19 @@ const SettingsPlex = ({ onComplete }: SettingsPlexProps) => {
|
||||
toastId = id;
|
||||
}
|
||||
);
|
||||
await axios.post('/api/v1/settings/plex', {
|
||||
ip: values.hostname,
|
||||
port: Number(values.port),
|
||||
useSsl: values.useSsl,
|
||||
webAppUrl: values.webAppUrl,
|
||||
} as PlexSettings);
|
||||
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();
|
||||
|
||||
syncLibraries();
|
||||
|
||||
@@ -748,14 +779,27 @@ const SettingsPlex = ({ onComplete }: SettingsPlexProps) => {
|
||||
validationSchema={TautulliSettingsSchema}
|
||||
onSubmit={async (values) => {
|
||||
try {
|
||||
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);
|
||||
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
|
||||
|
||||
addToast(
|
||||
intl.formatMessage(messages.toastTautulliSettingsSuccess),
|
||||
|
||||
@@ -13,7 +13,6 @@ 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';
|
||||
@@ -196,9 +195,14 @@ const SettingsServices = () => {
|
||||
});
|
||||
|
||||
const deleteServer = async () => {
|
||||
await axios.delete(
|
||||
`/api/v1/settings/${deleteServerModal.type}/${deleteServerModal.serverId}`
|
||||
const res = await fetch(
|
||||
`/api/v1/settings/${deleteServerModal.type}/${deleteServerModal.serverId}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
}
|
||||
);
|
||||
if (!res.ok) throw new Error();
|
||||
|
||||
setDeleteServerModal({ open: false, serverId: null, type: 'radarr' });
|
||||
revalidateRadarr();
|
||||
revalidateSonarr();
|
||||
|
||||
@@ -9,9 +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 getConfig from 'next/config';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import useSWR, { mutate } from 'swr';
|
||||
@@ -43,12 +41,20 @@ const SettingsUsers = () => {
|
||||
mutate: revalidate,
|
||||
} = useSWR<MainSettings>('/api/v1/settings/main');
|
||||
const settings = useSettings();
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
|
||||
if (!data && !error) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
|
||||
const mediaServerFormatValues = {
|
||||
mediaServerName:
|
||||
settings.currentSettings.mediaServerType === MediaServerType.JELLYFIN
|
||||
? 'Jellyfin'
|
||||
: settings.currentSettings.mediaServerType === MediaServerType.EMBY
|
||||
? 'Emby'
|
||||
: undefined,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageTitle
|
||||
@@ -77,21 +83,28 @@ const SettingsUsers = () => {
|
||||
enableReinitialize
|
||||
onSubmit={async (values) => {
|
||||
try {
|
||||
await axios.post('/api/v1/settings/main', {
|
||||
localLogin: values.localLogin,
|
||||
newPlexLogin: values.newPlexLogin,
|
||||
defaultQuotas: {
|
||||
movie: {
|
||||
quotaLimit: values.movieQuotaLimit,
|
||||
quotaDays: values.movieQuotaDays,
|
||||
},
|
||||
tv: {
|
||||
quotaLimit: values.tvQuotaLimit,
|
||||
quotaDays: values.tvQuotaDays,
|
||||
},
|
||||
const res = await fetch('/api/v1/settings/main', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
defaultPermissions: values.defaultPermissions,
|
||||
body: JSON.stringify({
|
||||
localLogin: values.localLogin,
|
||||
newPlexLogin: values.newPlexLogin,
|
||||
defaultQuotas: {
|
||||
movie: {
|
||||
quotaLimit: values.movieQuotaLimit,
|
||||
quotaDays: values.movieQuotaDays,
|
||||
},
|
||||
tv: {
|
||||
quotaLimit: values.tvQuotaLimit,
|
||||
quotaDays: values.tvQuotaDays,
|
||||
},
|
||||
},
|
||||
defaultPermissions: values.defaultPermissions,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
mutate('/api/v1/settings/public');
|
||||
|
||||
addToast(intl.formatMessage(messages.toastSettingsSuccess), {
|
||||
@@ -115,16 +128,10 @@ const SettingsUsers = () => {
|
||||
<label htmlFor="localLogin" className="checkbox-label">
|
||||
{intl.formatMessage(messages.localLogin)}
|
||||
<span className="label-tip">
|
||||
{intl.formatMessage(messages.localLoginTip, {
|
||||
mediaServerName:
|
||||
settings.currentSettings.mediaServerType ===
|
||||
MediaServerType.PLEX
|
||||
? 'Plex'
|
||||
: settings.currentSettings.mediaServerType ===
|
||||
MediaServerType.JELLYFIN
|
||||
? 'Jellyfin'
|
||||
: 'Emby',
|
||||
})}
|
||||
{intl.formatMessage(
|
||||
messages.localLoginTip,
|
||||
mediaServerFormatValues
|
||||
)}
|
||||
</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
@@ -140,25 +147,15 @@ const SettingsUsers = () => {
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="newPlexLogin" className="checkbox-label">
|
||||
{intl.formatMessage(messages.newPlexLogin, {
|
||||
mediaServerName:
|
||||
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||
? 'Emby'
|
||||
: settings.currentSettings.mediaServerType ===
|
||||
MediaServerType.PLEX
|
||||
? 'Plex'
|
||||
: 'Jellyfin',
|
||||
})}
|
||||
{intl.formatMessage(
|
||||
messages.newPlexLogin,
|
||||
mediaServerFormatValues
|
||||
)}
|
||||
<span className="label-tip">
|
||||
{intl.formatMessage(messages.newPlexLoginTip, {
|
||||
mediaServerName:
|
||||
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||
? 'Emby'
|
||||
: settings.currentSettings.mediaServerType ===
|
||||
MediaServerType.PLEX
|
||||
? 'Plex'
|
||||
: 'Jellyfin',
|
||||
})}
|
||||
{intl.formatMessage(
|
||||
messages.newPlexLoginTip,
|
||||
mediaServerFormatValues
|
||||
)}
|
||||
</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
|
||||
@@ -4,7 +4,6 @@ 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';
|
||||
@@ -177,19 +176,24 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
|
||||
}) => {
|
||||
setIsTesting(true);
|
||||
try {
|
||||
const response = await axios.post<TestResponse>(
|
||||
'/api/v1/settings/sonarr/test',
|
||||
{
|
||||
const res = await fetch('/api/v1/settings/sonarr/test', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
hostname,
|
||||
apiKey,
|
||||
port: Number(port),
|
||||
baseUrl,
|
||||
useSsl,
|
||||
}
|
||||
);
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
const data: TestResponse = await res.json();
|
||||
|
||||
setIsValidated(true);
|
||||
setTestResponse(response.data);
|
||||
setTestResponse(data);
|
||||
if (initialLoad.current) {
|
||||
addToast(intl.formatMessage(messages.toastSonarrTestSuccess), {
|
||||
appearance: 'success',
|
||||
@@ -306,12 +310,23 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
|
||||
tagRequests: values.tagRequests,
|
||||
};
|
||||
if (!sonarr) {
|
||||
await axios.post('/api/v1/settings/sonarr', submission);
|
||||
const res = await fetch('/api/v1/settings/sonarr', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(submission),
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
} else {
|
||||
await axios.put(
|
||||
`/api/v1/settings/sonarr/${sonarr.id}`,
|
||||
submission
|
||||
);
|
||||
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();
|
||||
}
|
||||
|
||||
onSave();
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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';
|
||||
|
||||
@@ -25,9 +24,19 @@ const LoginWithPlex = ({ onComplete }: LoginWithPlexProps) => {
|
||||
|
||||
useEffect(() => {
|
||||
const login = async () => {
|
||||
const response = await axios.post('/api/v1/auth/plex', { authToken });
|
||||
const res = await fetch('/api/v1/auth/plex', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
authToken,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
const data = await res.json();
|
||||
|
||||
if (response.data?.id) {
|
||||
if (data?.id) {
|
||||
revalidate();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,44 +1,58 @@
|
||||
import Accordion from '@app/components/Common/Accordion';
|
||||
import Button from '@app/components/Common/Button';
|
||||
import JellyfinLogin from '@app/components/Login/JellyfinLogin';
|
||||
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 getConfig from 'next/config';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { FormattedMessage, useIntl } from 'react-intl';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
const messages = defineMessages('components.Setup', {
|
||||
welcome: 'Welcome to Jellyseerr',
|
||||
signinMessage: 'Get started by signing in',
|
||||
signinWithJellyfin: 'Use your {mediaServerName} account',
|
||||
signinWithPlex: 'Use your Plex account',
|
||||
signin: 'Sign in to your account',
|
||||
signinWithJellyfin: 'Enter your Jellyfin details',
|
||||
signinWithEmby: 'Enter your Emby details',
|
||||
signinWithPlex: 'Enter your Plex details',
|
||||
back: 'Go back',
|
||||
});
|
||||
|
||||
interface LoginWithMediaServerProps {
|
||||
onComplete: (onComplete: MediaServerType) => void;
|
||||
serverType: MediaServerType;
|
||||
onCancel: () => void;
|
||||
onComplete: () => void;
|
||||
}
|
||||
|
||||
const SetupLogin: React.FC<LoginWithMediaServerProps> = ({ onComplete }) => {
|
||||
const SetupLogin: React.FC<LoginWithMediaServerProps> = ({
|
||||
serverType,
|
||||
onCancel,
|
||||
onComplete,
|
||||
}) => {
|
||||
const [authToken, setAuthToken] = useState<string | undefined>(undefined);
|
||||
const [mediaServerType, setMediaServerType] = useState<MediaServerType>(
|
||||
MediaServerType.NOT_CONFIGURED
|
||||
);
|
||||
const { user, revalidate } = useUser();
|
||||
const intl = useIntl();
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
|
||||
// Effect that is triggered when the `authToken` comes back from the Plex OAuth
|
||||
// We take the token and attempt to login. If we get a success message, we will
|
||||
// ask swr to revalidate the user which _shouid_ come back with a valid user.
|
||||
|
||||
useEffect(() => {
|
||||
const login = async () => {
|
||||
const response = await axios.post('/api/v1/auth/plex', {
|
||||
authToken: authToken,
|
||||
const res = await fetch('/api/v1/auth/plex', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
authToken: authToken,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
const data = await res.json();
|
||||
|
||||
if (response.data?.email) {
|
||||
if (data?.email) {
|
||||
revalidate();
|
||||
}
|
||||
};
|
||||
@@ -49,71 +63,60 @@ const SetupLogin: React.FC<LoginWithMediaServerProps> = ({ onComplete }) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
onComplete(mediaServerType);
|
||||
onComplete();
|
||||
}
|
||||
}, [user, mediaServerType, onComplete]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="p-4">
|
||||
<div className="mb-2 flex justify-center text-xl font-bold">
|
||||
<FormattedMessage {...messages.welcome} />
|
||||
<FormattedMessage {...messages.signin} />
|
||||
</div>
|
||||
<div className="mb-2 flex justify-center pb-6 text-sm">
|
||||
<FormattedMessage {...messages.signinMessage} />
|
||||
</div>
|
||||
<Accordion single atLeastOne>
|
||||
{({ openIndexes, handleClick, AccordionContent }) => (
|
||||
<>
|
||||
<button
|
||||
className={`w-full cursor-default bg-gray-900 py-2 text-center text-sm text-gray-400 transition-colors duration-200 hover:cursor-pointer hover:bg-gray-700 focus:outline-none sm:rounded-t-lg ${
|
||||
openIndexes.includes(0) && 'text-indigo-500'
|
||||
} ${openIndexes.includes(1) && 'border-b border-gray-500'}`}
|
||||
onClick={() => handleClick(0)}
|
||||
>
|
||||
<FormattedMessage {...messages.signinWithPlex} />
|
||||
</button>
|
||||
<AccordionContent isOpen={openIndexes.includes(0)}>
|
||||
<div
|
||||
className="px-10 py-8"
|
||||
style={{ backgroundColor: 'rgba(0,0,0,0.3)' }}
|
||||
>
|
||||
<PlexLoginButton
|
||||
onAuthToken={(authToken) => {
|
||||
setMediaServerType(MediaServerType.PLEX);
|
||||
setAuthToken(authToken);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
<div>
|
||||
<button
|
||||
className={`w-full cursor-default bg-gray-900 py-2 text-center text-sm text-gray-400 transition-colors duration-200 hover:cursor-pointer hover:bg-gray-700 focus:outline-none ${
|
||||
openIndexes.includes(1)
|
||||
? 'text-indigo-500'
|
||||
: 'sm:rounded-b-lg'
|
||||
}`}
|
||||
onClick={() => handleClick(1)}
|
||||
>
|
||||
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||
? intl.formatMessage(messages.signinWithJellyfin, {
|
||||
mediaServerName: 'Emby',
|
||||
})
|
||||
: intl.formatMessage(messages.signinWithJellyfin, {
|
||||
mediaServerName: 'Jellyfin',
|
||||
})}
|
||||
</button>
|
||||
<AccordionContent isOpen={openIndexes.includes(1)}>
|
||||
<div
|
||||
className="rounded-b-lg px-10 py-8"
|
||||
style={{ backgroundColor: 'rgba(0,0,0,0.3)' }}
|
||||
>
|
||||
<JellyfinLogin initial={true} revalidate={revalidate} />
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</div>
|
||||
</>
|
||||
{serverType === MediaServerType.JELLYFIN ? (
|
||||
<FormattedMessage {...messages.signinWithJellyfin} />
|
||||
) : serverType === MediaServerType.EMBY ? (
|
||||
<FormattedMessage {...messages.signinWithEmby} />
|
||||
) : (
|
||||
<FormattedMessage {...messages.signinWithPlex} />
|
||||
)}
|
||||
</Accordion>
|
||||
</div>
|
||||
{serverType === MediaServerType.PLEX && (
|
||||
<>
|
||||
<div
|
||||
className="px-10 py-8"
|
||||
style={{ backgroundColor: 'rgba(0,0,0,0.3)' }}
|
||||
>
|
||||
<PlexLoginButton
|
||||
onAuthToken={(authToken) => {
|
||||
setMediaServerType(MediaServerType.PLEX);
|
||||
setAuthToken(authToken);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<Button buttonType="default" onClick={() => onCancel()}>
|
||||
<FormattedMessage {...messages.back} />
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{serverType === MediaServerType.JELLYFIN && (
|
||||
<JellyfinLogin
|
||||
initial={true}
|
||||
revalidate={revalidate}
|
||||
serverType={serverType}
|
||||
onCancel={onCancel}
|
||||
/>
|
||||
)}
|
||||
{serverType === MediaServerType.EMBY && (
|
||||
<JellyfinLogin
|
||||
initial={true}
|
||||
revalidate={revalidate}
|
||||
serverType={serverType}
|
||||
onCancel={onCancel}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import EmbyLogo from '@app/assets/services/emby.svg';
|
||||
import JellyfinLogo from '@app/assets/services/jellyfin.svg';
|
||||
import PlexLogo from '@app/assets/services/plex.svg';
|
||||
import AppDataWarning from '@app/components/AppDataWarning';
|
||||
import Badge from '@app/components/Common/Badge';
|
||||
import Button from '@app/components/Common/Button';
|
||||
@@ -11,7 +14,6 @@ import SetupSteps from '@app/components/Setup/SetupSteps';
|
||||
import useLocale from '@app/hooks/useLocale';
|
||||
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 { useState } from 'react';
|
||||
@@ -20,10 +22,16 @@ import useSWR, { mutate } from 'swr';
|
||||
import SetupLogin from './SetupLogin';
|
||||
|
||||
const messages = defineMessages('components.Setup', {
|
||||
welcome: 'Welcome to Jellyseerr',
|
||||
subtitle: 'Get started by choosing your media server',
|
||||
configjellyfin: 'Configure Jellyfin',
|
||||
configplex: 'Configure Plex',
|
||||
configemby: 'Configure Emby',
|
||||
setup: 'Setup',
|
||||
finish: 'Finish Setup',
|
||||
finishing: 'Finishing…',
|
||||
continue: 'Continue',
|
||||
servertype: 'Choose Server Type',
|
||||
signin: 'Sign In',
|
||||
configuremediaserver: 'Configure Media Server',
|
||||
configureservices: 'Configure Services',
|
||||
@@ -46,15 +54,27 @@ const Setup = () => {
|
||||
|
||||
const finishSetup = async () => {
|
||||
setIsUpdating(true);
|
||||
const response = await axios.post<{ initialized: boolean }>(
|
||||
'/api/v1/settings/initialize'
|
||||
);
|
||||
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();
|
||||
|
||||
setIsUpdating(false);
|
||||
if (response.data.initialized) {
|
||||
await axios.post('/api/v1/settings/main', { locale });
|
||||
mutate('/api/v1/settings/public');
|
||||
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();
|
||||
|
||||
mutate('/api/v1/settings/public');
|
||||
router.push('/');
|
||||
}
|
||||
};
|
||||
@@ -90,35 +110,103 @@ const Setup = () => {
|
||||
>
|
||||
<SetupSteps
|
||||
stepNumber={1}
|
||||
description={intl.formatMessage(messages.signin)}
|
||||
description={intl.formatMessage(messages.servertype)}
|
||||
active={currentStep === 1}
|
||||
completed={currentStep > 1}
|
||||
/>
|
||||
<SetupSteps
|
||||
stepNumber={2}
|
||||
description={intl.formatMessage(messages.configuremediaserver)}
|
||||
description={intl.formatMessage(messages.signin)}
|
||||
active={currentStep === 2}
|
||||
completed={currentStep > 2}
|
||||
/>
|
||||
<SetupSteps
|
||||
stepNumber={3}
|
||||
description={intl.formatMessage(messages.configureservices)}
|
||||
description={intl.formatMessage(messages.configuremediaserver)}
|
||||
active={currentStep === 3}
|
||||
completed={currentStep > 3}
|
||||
/>
|
||||
<SetupSteps
|
||||
stepNumber={4}
|
||||
description={intl.formatMessage(messages.configureservices)}
|
||||
active={currentStep === 4}
|
||||
isLastStep
|
||||
/>
|
||||
</ul>
|
||||
</nav>
|
||||
<div className="mt-10 w-full rounded-md border border-gray-600 bg-gray-800 bg-opacity-50 p-4 text-white">
|
||||
{currentStep === 1 && (
|
||||
<SetupLogin
|
||||
onComplete={(mServerType) => {
|
||||
setMediaServerType(mServerType);
|
||||
setCurrentStep(2);
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-col items-center pb-6">
|
||||
<div className="mb-2 flex justify-center text-xl font-bold">
|
||||
{intl.formatMessage(messages.welcome)}
|
||||
</div>
|
||||
<div className="mb-2 flex justify-center pb-6 text-sm">
|
||||
{intl.formatMessage(messages.subtitle)}
|
||||
</div>
|
||||
<div className="grid grid-cols-3">
|
||||
<div className="flex flex-col divide-y divide-gray-600 rounded-l border border-gray-600 py-2">
|
||||
<div className="mb-2 flex flex-1 items-center justify-center py-2 px-2">
|
||||
<JellyfinLogo className="h-10" />
|
||||
</div>
|
||||
<div className="px-2 pt-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
setMediaServerType(MediaServerType.JELLYFIN);
|
||||
setCurrentStep(2);
|
||||
}}
|
||||
className="button-md relative z-10 inline-flex h-full w-full items-center justify-center rounded-md border border-gray-600 bg-transparent px-4 py-2 text-sm font-medium leading-5 text-white transition duration-150 ease-in-out hover:z-20 hover:border-gray-200 focus:z-20 focus:border-gray-100 focus:outline-none active:border-gray-100"
|
||||
>
|
||||
{intl.formatMessage(messages.configjellyfin)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col divide-y divide-gray-600 border-y border-gray-600 py-2">
|
||||
<div className="mb-2 flex flex-1 items-center justify-center py-2 px-2">
|
||||
<PlexLogo className="h-8" />
|
||||
</div>
|
||||
<div className="px-2 pt-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
setMediaServerType(MediaServerType.PLEX);
|
||||
setCurrentStep(2);
|
||||
}}
|
||||
className="button-md relative z-10 inline-flex h-full w-full items-center justify-center rounded-md border border-gray-600 bg-transparent px-4 py-2 text-sm font-medium leading-5 text-white transition duration-150 ease-in-out hover:z-20 hover:border-gray-200 focus:z-20 focus:border-gray-100 focus:outline-none active:border-gray-100"
|
||||
>
|
||||
{intl.formatMessage(messages.configplex)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col divide-y divide-gray-600 rounded-r border border-gray-600 py-2">
|
||||
<div className="mb-2 flex flex-1 items-center justify-center py-2 px-2">
|
||||
<EmbyLogo className="h-9" />
|
||||
</div>
|
||||
<div className="px-2 pt-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
setMediaServerType(MediaServerType.EMBY);
|
||||
setCurrentStep(2);
|
||||
}}
|
||||
className="button-md relative z-10 inline-flex h-full w-full items-center justify-center rounded-md border border-gray-600 bg-transparent px-4 py-2 text-sm font-medium leading-5 text-white transition duration-150 ease-in-out hover:z-20 hover:border-gray-200 focus:z-20 focus:border-gray-100 focus:outline-none active:border-gray-100"
|
||||
>
|
||||
{intl.formatMessage(messages.configemby)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{currentStep === 2 && (
|
||||
<div>
|
||||
<SetupLogin
|
||||
serverType={mediaServerType}
|
||||
onCancel={() => {
|
||||
setMediaServerType(MediaServerType.NOT_CONFIGURED);
|
||||
setCurrentStep(1);
|
||||
}}
|
||||
onComplete={() => setCurrentStep(3)}
|
||||
/>
|
||||
)}
|
||||
{currentStep === 3 && (
|
||||
<div className="p-2">
|
||||
{mediaServerType === MediaServerType.PLEX ? (
|
||||
<SettingsPlex
|
||||
onComplete={() => setMediaServerSettingsComplete(true)}
|
||||
@@ -141,7 +229,7 @@ const Setup = () => {
|
||||
<Button
|
||||
buttonType="primary"
|
||||
disabled={!mediaServerSettingsComplete}
|
||||
onClick={() => setCurrentStep(3)}
|
||||
onClick={() => setCurrentStep(4)}
|
||||
>
|
||||
{intl.formatMessage(messages.continue)}
|
||||
</Button>
|
||||
@@ -150,7 +238,7 @@ const Setup = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{currentStep === 3 && (
|
||||
{currentStep === 4 && (
|
||||
<div>
|
||||
<SettingsServices />
|
||||
<div className="actions">
|
||||
|
||||
@@ -9,7 +9,6 @@ import defineMessages from '@app/utils/defineMessages';
|
||||
import { MediaStatus } from '@server/constants/media';
|
||||
import { MediaServerType } from '@server/constants/server';
|
||||
import type { DownloadingItem } from '@server/lib/downloadtracker';
|
||||
import getConfig from 'next/config';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
const messages = defineMessages('components.StatusBadge', {
|
||||
@@ -47,7 +46,6 @@ const StatusBadge = ({
|
||||
const intl = useIntl();
|
||||
const { hasPermission } = useUser();
|
||||
const settings = useSettings();
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
|
||||
let mediaLink: string | undefined;
|
||||
let mediaLinkDescription: string | undefined;
|
||||
@@ -85,7 +83,7 @@ const StatusBadge = ({
|
||||
mediaLink = plexUrl;
|
||||
mediaLinkDescription = intl.formatMessage(messages.playonplex, {
|
||||
mediaServerName:
|
||||
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||
settings.currentSettings.mediaServerType === MediaServerType.EMBY
|
||||
? 'Emby'
|
||||
: settings.currentSettings.mediaServerType === MediaServerType.PLEX
|
||||
? 'Plex'
|
||||
|
||||
@@ -2,7 +2,6 @@ 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';
|
||||
|
||||
@@ -21,11 +20,14 @@ const messages = defineMessages('components.TitleCard', {
|
||||
cleardata: 'Clear Data',
|
||||
});
|
||||
|
||||
const Error = ({ id, tmdbId, tvdbId, type, canExpand }: ErrorCardProps) => {
|
||||
const ErrorCard = ({ id, tmdbId, tvdbId, type, canExpand }: ErrorCardProps) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const deleteMedia = async () => {
|
||||
await axios.delete(`/api/v1/media/${id}`);
|
||||
const res = await fetch(`/api/v1/media/${id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
mutate('/api/v1/media?filter=allavailable&take=20&sort=mediaAdded');
|
||||
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
|
||||
};
|
||||
@@ -129,4 +131,4 @@ const Error = ({ id, tmdbId, tvdbId, type, canExpand }: ErrorCardProps) => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default Error;
|
||||
export default ErrorCard;
|
||||
|
||||
@@ -19,7 +19,6 @@ 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, useState } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
@@ -98,13 +97,21 @@ const TitleCard = ({
|
||||
const onClickWatchlistBtn = async (): Promise<void> => {
|
||||
setIsUpdating(true);
|
||||
try {
|
||||
const response = await axios.post<Watchlist>('/api/v1/watchlist', {
|
||||
tmdbId: id,
|
||||
mediaType,
|
||||
title,
|
||||
const res = await fetch('/api/v1/watchlist', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
tmdbId: id,
|
||||
mediaType,
|
||||
title,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
const data: Watchlist = await res.json();
|
||||
mutate('/api/v1/discover/watchlist');
|
||||
if (response.data) {
|
||||
if (data) {
|
||||
addToast(
|
||||
<span>
|
||||
{intl.formatMessage(messages.watchlistSuccess, {
|
||||
@@ -129,9 +136,11 @@ const TitleCard = ({
|
||||
const onClickDeleteWatchlistBtn = async (): Promise<void> => {
|
||||
setIsUpdating(true);
|
||||
try {
|
||||
const response = await axios.delete<Watchlist>('/api/v1/watchlist/' + id);
|
||||
|
||||
if (response.status === 204) {
|
||||
const res = await fetch('/api/v1/watchlist/' + id, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
if (res.status === 204) {
|
||||
addToast(
|
||||
<span>
|
||||
{intl.formatMessage(messages.watchlistDeleted, {
|
||||
|
||||
@@ -50,7 +50,6 @@ import type { Crew } from '@server/models/common';
|
||||
import type { TvDetails as TvDetailsType } from '@server/models/Tv';
|
||||
import { countries } from 'country-flag-icons';
|
||||
import 'country-flag-icons/3x2/flags.css';
|
||||
import getConfig from 'next/config';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
@@ -106,7 +105,6 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
||||
router.query.manage == '1' ? true : false
|
||||
);
|
||||
const [showIssueModal, setShowIssueModal] = useState(false);
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
|
||||
const {
|
||||
data,
|
||||
@@ -279,7 +277,7 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
||||
?.flatrate ?? [];
|
||||
|
||||
function getAvalaibleMediaServerName() {
|
||||
if (publicRuntimeConfig.JELLYFIN_TYPE === 'emby') {
|
||||
if (settings.currentSettings.mediaServerType === MediaServerType.EMBY) {
|
||||
return intl.formatMessage(messages.play, { mediaServerName: 'Emby' });
|
||||
}
|
||||
|
||||
@@ -291,15 +289,15 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
||||
}
|
||||
|
||||
function getAvalaible4kMediaServerName() {
|
||||
if (publicRuntimeConfig.JELLYFIN_TYPE === 'emby') {
|
||||
return intl.formatMessage(messages.play4k, { mediaServerName: 'Emby' });
|
||||
if (settings.currentSettings.mediaServerType === MediaServerType.EMBY) {
|
||||
return intl.formatMessage(messages.play, { mediaServerName: 'Emby' });
|
||||
}
|
||||
|
||||
if (settings.currentSettings.mediaServerType === MediaServerType.PLEX) {
|
||||
return intl.formatMessage(messages.play4k, { mediaServerName: 'Plex' });
|
||||
}
|
||||
|
||||
return intl.formatMessage(messages.play4k, { mediaServerName: 'Jellyfin' });
|
||||
return intl.formatMessage(messages.play, { mediaServerName: 'Jellyfin' });
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user