Compare commits
3 Commits
preview-mo
...
preview-pr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e741051f3 | ||
|
|
daecb6b6cf | ||
|
|
d99ae35c2e |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -34,7 +34,6 @@ yarn-error.log*
|
|||||||
# database
|
# database
|
||||||
config/db/*.sqlite3*
|
config/db/*.sqlite3*
|
||||||
config/settings.json
|
config/settings.json
|
||||||
config/settings.old.json
|
|
||||||
|
|
||||||
# logs
|
# logs
|
||||||
config/logs/*.log*
|
config/logs/*.log*
|
||||||
|
|||||||
@@ -6,10 +6,6 @@ sidebar_position: 4
|
|||||||
|
|
||||||
# AUR (Arch User Repository)
|
# AUR (Arch User Repository)
|
||||||
|
|
||||||
:::note Disclaimer
|
|
||||||
This AUR package is not maintained by us but by a third party. Please refer to the maintainer for any issues.
|
|
||||||
:::
|
|
||||||
|
|
||||||
:::info
|
:::info
|
||||||
This method is not recommended for most users. It is intended for advanced users who are using Arch Linux or an Arch-based distribution.
|
This method is not recommended for most users. It is intended for advanced users who are using Arch Linux or an Arch-based distribution.
|
||||||
:::
|
:::
|
||||||
|
|||||||
@@ -1988,9 +1988,6 @@ paths:
|
|||||||
appDataPath:
|
appDataPath:
|
||||||
type: string
|
type: string
|
||||||
example: /app/config
|
example: /app/config
|
||||||
appDataPermissions:
|
|
||||||
type: boolean
|
|
||||||
example: true
|
|
||||||
/settings/main:
|
/settings/main:
|
||||||
get:
|
get:
|
||||||
summary: Get main settings
|
summary: Get main settings
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ class PlexAPI {
|
|||||||
settings.plex.libraries = [];
|
settings.plex.libraries = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
await settings.save();
|
settings.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getLibraryContents(
|
public async getLibraryContents(
|
||||||
|
|||||||
@@ -21,9 +21,7 @@ import clearCookies from '@server/middleware/clearcookies';
|
|||||||
import routes from '@server/routes';
|
import routes from '@server/routes';
|
||||||
import avatarproxy from '@server/routes/avatarproxy';
|
import avatarproxy from '@server/routes/avatarproxy';
|
||||||
import imageproxy from '@server/routes/imageproxy';
|
import imageproxy from '@server/routes/imageproxy';
|
||||||
import { appDataPermissions } from '@server/utils/appDataVolume';
|
|
||||||
import { getAppVersion } from '@server/utils/appVersion';
|
import { getAppVersion } from '@server/utils/appVersion';
|
||||||
import createCustomProxyAgent from '@server/utils/customProxyAgent';
|
|
||||||
import restartFlag from '@server/utils/restartFlag';
|
import restartFlag from '@server/utils/restartFlag';
|
||||||
import { getClientIp } from '@supercharge/request-ip';
|
import { getClientIp } from '@supercharge/request-ip';
|
||||||
import { TypeormStore } from 'connect-typeorm/out';
|
import { TypeormStore } from 'connect-typeorm/out';
|
||||||
@@ -39,6 +37,7 @@ import dns from 'node:dns';
|
|||||||
import net from 'node:net';
|
import net from 'node:net';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import swaggerUi from 'swagger-ui-express';
|
import swaggerUi from 'swagger-ui-express';
|
||||||
|
import { ProxyAgent, setGlobalDispatcher } from 'undici';
|
||||||
import YAML from 'yamljs';
|
import YAML from 'yamljs';
|
||||||
|
|
||||||
if (process.env.forceIpv4First === 'true') {
|
if (process.env.forceIpv4First === 'true') {
|
||||||
@@ -53,12 +52,6 @@ const dev = process.env.NODE_ENV !== 'production';
|
|||||||
const app = next({ dev });
|
const app = next({ dev });
|
||||||
const handle = app.getRequestHandler();
|
const handle = app.getRequestHandler();
|
||||||
|
|
||||||
if (!appDataPermissions()) {
|
|
||||||
logger.error(
|
|
||||||
'Something went wrong while checking config folder! Please ensure the config folder is set up properly.\nhttps://docs.jellyseerr.dev/getting-started'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
app
|
app
|
||||||
.prepare()
|
.prepare()
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
@@ -76,8 +69,8 @@ app
|
|||||||
restartFlag.initializeSettings(settings.main);
|
restartFlag.initializeSettings(settings.main);
|
||||||
|
|
||||||
// Register HTTP proxy
|
// Register HTTP proxy
|
||||||
if (settings.main.proxy.enabled) {
|
if (settings.main.httpProxy) {
|
||||||
await createCustomProxyAgent(settings.main.proxy);
|
setGlobalDispatcher(new ProxyAgent(settings.main.httpProxy));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrate library types
|
// Migrate library types
|
||||||
|
|||||||
@@ -135,7 +135,6 @@ class ImageProxy {
|
|||||||
private cacheVersion;
|
private cacheVersion;
|
||||||
private key;
|
private key;
|
||||||
private baseUrl;
|
private baseUrl;
|
||||||
private headers: HeadersInit | null = null;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
key: string,
|
key: string,
|
||||||
@@ -143,7 +142,6 @@ class ImageProxy {
|
|||||||
options: {
|
options: {
|
||||||
cacheVersion?: number;
|
cacheVersion?: number;
|
||||||
rateLimitOptions?: RateLimitOptions;
|
rateLimitOptions?: RateLimitOptions;
|
||||||
headers?: HeadersInit;
|
|
||||||
} = {}
|
} = {}
|
||||||
) {
|
) {
|
||||||
this.cacheVersion = options.cacheVersion ?? 1;
|
this.cacheVersion = options.cacheVersion ?? 1;
|
||||||
@@ -157,13 +155,9 @@ class ImageProxy {
|
|||||||
} else {
|
} else {
|
||||||
this.fetch = fetch;
|
this.fetch = fetch;
|
||||||
}
|
}
|
||||||
this.headers = options.headers || null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getImage(
|
public async getImage(path: string): Promise<ImageResponse> {
|
||||||
path: string,
|
|
||||||
fallbackPath?: string
|
|
||||||
): Promise<ImageResponse> {
|
|
||||||
const cacheKey = this.getCacheKey(path);
|
const cacheKey = this.getCacheKey(path);
|
||||||
|
|
||||||
const imageResponse = await this.get(cacheKey);
|
const imageResponse = await this.get(cacheKey);
|
||||||
@@ -172,12 +166,8 @@ class ImageProxy {
|
|||||||
const newImage = await this.set(path, cacheKey);
|
const newImage = await this.set(path, cacheKey);
|
||||||
|
|
||||||
if (!newImage) {
|
if (!newImage) {
|
||||||
if (fallbackPath) {
|
|
||||||
return await this.getImage(fallbackPath);
|
|
||||||
} else {
|
|
||||||
throw new Error('Failed to load image');
|
throw new Error('Failed to load image');
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return newImage;
|
return newImage;
|
||||||
}
|
}
|
||||||
@@ -257,12 +247,7 @@ class ImageProxy {
|
|||||||
: '/'
|
: '/'
|
||||||
: '') +
|
: '') +
|
||||||
(path.startsWith('/') ? path.slice(1) : path);
|
(path.startsWith('/') ? path.slice(1) : path);
|
||||||
const response = await this.fetch(href, {
|
const response = await this.fetch(href);
|
||||||
headers: this.headers || undefined,
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const arrayBuffer = await response.arrayBuffer();
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
const buffer = Buffer.from(arrayBuffer);
|
const buffer = Buffer.from(arrayBuffer);
|
||||||
|
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ class PlexScanner
|
|||||||
});
|
});
|
||||||
|
|
||||||
settings.plex.libraries = newLibraries;
|
settings.plex.libraries = newLibraries;
|
||||||
await settings.save();
|
settings.save();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (const library of this.libraries) {
|
for (const library of this.libraries) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { MediaServerType } from '@server/constants/server';
|
|||||||
import { Permission } from '@server/lib/permissions';
|
import { Permission } from '@server/lib/permissions';
|
||||||
import { runMigrations } from '@server/lib/settings/migrator';
|
import { runMigrations } from '@server/lib/settings/migrator';
|
||||||
import { randomUUID } from 'crypto';
|
import { randomUUID } from 'crypto';
|
||||||
import fs from 'fs/promises';
|
import fs from 'fs';
|
||||||
import { merge } from 'lodash';
|
import { merge } from 'lodash';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import webpush from 'web-push';
|
import webpush from 'web-push';
|
||||||
@@ -99,17 +99,6 @@ interface Quota {
|
|||||||
quotaDays?: number;
|
quotaDays?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProxySettings {
|
|
||||||
enabled: boolean;
|
|
||||||
hostname: string;
|
|
||||||
port: number;
|
|
||||||
useSsl: boolean;
|
|
||||||
user: string;
|
|
||||||
password: string;
|
|
||||||
bypassFilter: string;
|
|
||||||
bypassLocalAddresses: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MainSettings {
|
export interface MainSettings {
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
applicationTitle: string;
|
applicationTitle: string;
|
||||||
@@ -130,7 +119,7 @@ export interface MainSettings {
|
|||||||
mediaServerType: number;
|
mediaServerType: number;
|
||||||
partialRequestsEnabled: boolean;
|
partialRequestsEnabled: boolean;
|
||||||
locale: string;
|
locale: string;
|
||||||
proxy: ProxySettings;
|
httpProxy: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PublicSettings {
|
interface PublicSettings {
|
||||||
@@ -337,16 +326,7 @@ class Settings {
|
|||||||
mediaServerType: MediaServerType.NOT_CONFIGURED,
|
mediaServerType: MediaServerType.NOT_CONFIGURED,
|
||||||
partialRequestsEnabled: true,
|
partialRequestsEnabled: true,
|
||||||
locale: 'en',
|
locale: 'en',
|
||||||
proxy: {
|
httpProxy: '',
|
||||||
enabled: false,
|
|
||||||
hostname: '',
|
|
||||||
port: 8080,
|
|
||||||
useSsl: false,
|
|
||||||
user: '',
|
|
||||||
password: '',
|
|
||||||
bypassFilter: '',
|
|
||||||
bypassLocalAddresses: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
plex: {
|
plex: {
|
||||||
name: '',
|
name: '',
|
||||||
@@ -501,6 +481,10 @@ class Settings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get main(): MainSettings {
|
get main(): MainSettings {
|
||||||
|
if (!this.data.main.apiKey) {
|
||||||
|
this.data.main.apiKey = this.generateApiKey();
|
||||||
|
this.save();
|
||||||
|
}
|
||||||
return this.data.main;
|
return this.data.main;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -602,20 +586,29 @@ class Settings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get clientId(): string {
|
get clientId(): string {
|
||||||
|
if (!this.data.clientId) {
|
||||||
|
this.data.clientId = randomUUID();
|
||||||
|
this.save();
|
||||||
|
}
|
||||||
|
|
||||||
return this.data.clientId;
|
return this.data.clientId;
|
||||||
}
|
}
|
||||||
|
|
||||||
get vapidPublic(): string {
|
get vapidPublic(): string {
|
||||||
|
this.generateVapidKeys();
|
||||||
|
|
||||||
return this.data.vapidPublic;
|
return this.data.vapidPublic;
|
||||||
}
|
}
|
||||||
|
|
||||||
get vapidPrivate(): string {
|
get vapidPrivate(): string {
|
||||||
|
this.generateVapidKeys();
|
||||||
|
|
||||||
return this.data.vapidPrivate;
|
return this.data.vapidPrivate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async regenerateApiKey(): Promise<MainSettings> {
|
public regenerateApiKey(): MainSettings {
|
||||||
this.main.apiKey = this.generateApiKey();
|
this.main.apiKey = this.generateApiKey();
|
||||||
await this.save();
|
this.save();
|
||||||
return this.main;
|
return this.main;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -627,6 +620,15 @@ class Settings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private generateVapidKeys(force = false): void {
|
||||||
|
if (!this.data.vapidPublic || !this.data.vapidPrivate || force) {
|
||||||
|
const vapidKeys = webpush.generateVAPIDKeys();
|
||||||
|
this.data.vapidPrivate = vapidKeys.privateKey;
|
||||||
|
this.data.vapidPublic = vapidKeys.publicKey;
|
||||||
|
this.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Settings Load
|
* Settings Load
|
||||||
*
|
*
|
||||||
@@ -641,51 +643,30 @@ class Settings {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
let data;
|
if (!fs.existsSync(SETTINGS_PATH)) {
|
||||||
try {
|
this.save();
|
||||||
data = await fs.readFile(SETTINGS_PATH, 'utf-8');
|
|
||||||
} catch {
|
|
||||||
await this.save();
|
|
||||||
}
|
}
|
||||||
|
const data = fs.readFileSync(SETTINGS_PATH, 'utf-8');
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
const parsedJson = JSON.parse(data);
|
const parsedJson = JSON.parse(data);
|
||||||
const migratedData = await runMigrations(parsedJson, SETTINGS_PATH);
|
this.data = await runMigrations(parsedJson, SETTINGS_PATH);
|
||||||
this.data = merge(this.data, migratedData);
|
|
||||||
}
|
|
||||||
|
|
||||||
// generate keys and ids if it's missing
|
this.data = merge(this.data, parsedJson);
|
||||||
let change = false;
|
|
||||||
if (!this.data.main.apiKey) {
|
if (process.env.API_KEY) {
|
||||||
this.data.main.apiKey = this.generateApiKey();
|
|
||||||
change = true;
|
|
||||||
} else if (process.env.API_KEY) {
|
|
||||||
if (this.main.apiKey != process.env.API_KEY) {
|
if (this.main.apiKey != process.env.API_KEY) {
|
||||||
this.main.apiKey = process.env.API_KEY;
|
this.main.apiKey = process.env.API_KEY;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!this.data.clientId) {
|
|
||||||
this.data.clientId = randomUUID();
|
|
||||||
change = true;
|
|
||||||
}
|
|
||||||
if (!this.data.vapidPublic || !this.data.vapidPrivate) {
|
|
||||||
const vapidKeys = webpush.generateVAPIDKeys();
|
|
||||||
this.data.vapidPrivate = vapidKeys.privateKey;
|
|
||||||
this.data.vapidPublic = vapidKeys.publicKey;
|
|
||||||
change = true;
|
|
||||||
}
|
|
||||||
if (change) {
|
|
||||||
await this.save();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
this.save();
|
||||||
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async save(): Promise<void> {
|
public save(): void {
|
||||||
await fs.writeFile(
|
fs.writeFileSync(SETTINGS_PATH, JSON.stringify(this.data, undefined, ' '));
|
||||||
SETTINGS_PATH,
|
|
||||||
JSON.stringify(this.data, undefined, ' ')
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import type { AllSettings } from '@server/lib/settings';
|
import type { AllSettings } from '@server/lib/settings';
|
||||||
|
|
||||||
const migrateHostname = (settings: any): AllSettings => {
|
const migrateHostname = (settings: any): AllSettings => {
|
||||||
if (settings.jellyfin?.hostname) {
|
const oldJellyfinSettings = settings.jellyfin;
|
||||||
const { hostname } = settings.jellyfin;
|
if (oldJellyfinSettings && oldJellyfinSettings.hostname) {
|
||||||
|
const { hostname } = oldJellyfinSettings;
|
||||||
const protocolMatch = hostname.match(/^(https?):\/\//i);
|
const protocolMatch = hostname.match(/^(https?):\/\//i);
|
||||||
const useSsl = protocolMatch && protocolMatch[1].toLowerCase() === 'https';
|
const useSsl = protocolMatch && protocolMatch[1].toLowerCase() === 'https';
|
||||||
const remainingUrl = hostname.replace(/^(https?):\/\//i, '');
|
const remainingUrl = hostname.replace(/^(https?):\/\//i, '');
|
||||||
const urlMatch = remainingUrl.match(/^([^:]+)(:([0-9]+))?(\/.*)?$/);
|
const urlMatch = remainingUrl.match(/^([^:]+)(:([0-9]+))?(\/.*)?$/);
|
||||||
|
|
||||||
delete settings.jellyfin.hostname;
|
delete oldJellyfinSettings.hostname;
|
||||||
if (urlMatch) {
|
if (urlMatch) {
|
||||||
const [, ip, , port, urlBase] = urlMatch;
|
const [, ip, , port, urlBase] = urlMatch;
|
||||||
settings.jellyfin = {
|
settings.jellyfin = {
|
||||||
@@ -20,7 +21,9 @@ const migrateHostname = (settings: any): AllSettings => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (settings.jellyfin && settings.jellyfin.hostname) {
|
||||||
|
delete settings.jellyfin.hostname;
|
||||||
|
}
|
||||||
return settings;
|
return settings;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
import type { AllSettings } from '@server/lib/settings';
|
import type { AllSettings } from '@server/lib/settings';
|
||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
import fs from 'fs/promises';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
const migrationsDir = path.join(__dirname, 'migrations');
|
const migrationsDir = path.join(__dirname, 'migrations');
|
||||||
@@ -10,46 +10,19 @@ export const runMigrations = async (
|
|||||||
settings: AllSettings,
|
settings: AllSettings,
|
||||||
SETTINGS_PATH: string
|
SETTINGS_PATH: string
|
||||||
): Promise<AllSettings> => {
|
): Promise<AllSettings> => {
|
||||||
|
const migrations = fs
|
||||||
|
.readdirSync(migrationsDir)
|
||||||
|
.filter((file) => file.endsWith('.js') || file.endsWith('.ts'))
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
.map((file) => require(path.join(migrationsDir, file)).default);
|
||||||
|
|
||||||
let migrated = settings;
|
let migrated = settings;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// we read old backup and create a backup of currents settings
|
|
||||||
const BACKUP_PATH = SETTINGS_PATH.replace('.json', '.old.json');
|
|
||||||
let oldBackup: string | null = null;
|
|
||||||
try {
|
|
||||||
oldBackup = await fs.readFile(BACKUP_PATH, 'utf-8');
|
|
||||||
} catch {
|
|
||||||
/* empty */
|
|
||||||
}
|
|
||||||
await fs.writeFile(BACKUP_PATH, JSON.stringify(settings, undefined, ' '));
|
|
||||||
|
|
||||||
const migrations = (await fs.readdir(migrationsDir)).filter(
|
|
||||||
(file) => file.endsWith('.js') || file.endsWith('.ts')
|
|
||||||
);
|
|
||||||
|
|
||||||
const settingsBefore = JSON.stringify(migrated);
|
const settingsBefore = JSON.stringify(migrated);
|
||||||
|
|
||||||
for (const migration of migrations) {
|
for (const migration of migrations) {
|
||||||
try {
|
migrated = await migration(migrated);
|
||||||
logger.debug(`Checking migration '${migration}'...`, {
|
|
||||||
label: 'Settings Migrator',
|
|
||||||
});
|
|
||||||
const { default: migrationFn } = await import(
|
|
||||||
path.join(migrationsDir, migration)
|
|
||||||
);
|
|
||||||
const newSettings = await migrationFn(structuredClone(migrated));
|
|
||||||
if (JSON.stringify(migrated) !== JSON.stringify(newSettings)) {
|
|
||||||
logger.debug(`Migration '${migration}' has been applied.`, {
|
|
||||||
label: 'Settings Migrator',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
migrated = newSettings;
|
|
||||||
} catch (e) {
|
|
||||||
logger.error(`Error while running migration '${migration}'`, {
|
|
||||||
label: 'Settings Migrator',
|
|
||||||
});
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const settingsAfter = JSON.stringify(migrated);
|
const settingsAfter = JSON.stringify(migrated);
|
||||||
@@ -57,19 +30,12 @@ export const runMigrations = async (
|
|||||||
if (settingsBefore !== settingsAfter) {
|
if (settingsBefore !== settingsAfter) {
|
||||||
// a migration occured
|
// a migration occured
|
||||||
// we check that the new config will be saved
|
// we check that the new config will be saved
|
||||||
await fs.writeFile(
|
fs.writeFileSync(SETTINGS_PATH, JSON.stringify(migrated, undefined, ' '));
|
||||||
SETTINGS_PATH,
|
const fileSaved = JSON.parse(fs.readFileSync(SETTINGS_PATH, 'utf-8'));
|
||||||
JSON.stringify(migrated, undefined, ' ')
|
|
||||||
);
|
|
||||||
const fileSaved = JSON.parse(await fs.readFile(SETTINGS_PATH, 'utf-8'));
|
|
||||||
if (JSON.stringify(fileSaved) !== settingsAfter) {
|
if (JSON.stringify(fileSaved) !== settingsAfter) {
|
||||||
// something went wrong while saving file
|
// something went wrong while saving file
|
||||||
throw new Error('Unable to save settings after migration.');
|
throw new Error('Unable to save settings after migration.');
|
||||||
}
|
}
|
||||||
} else if (oldBackup) {
|
|
||||||
// no migration occured
|
|
||||||
// we save the old backup (to avoid settings.json and settings.old.json being the same)
|
|
||||||
await fs.writeFile(BACKUP_PATH, oldBackup.toString());
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(
|
logger.error(
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { UserType } from '@server/constants/user';
|
|||||||
import { getRepository } from '@server/datasource';
|
import { getRepository } from '@server/datasource';
|
||||||
import { User } from '@server/entity/User';
|
import { User } from '@server/entity/User';
|
||||||
import { startJobs } from '@server/job/schedule';
|
import { startJobs } from '@server/job/schedule';
|
||||||
|
import ImageProxy from '@server/lib/imageproxy';
|
||||||
import { Permission } from '@server/lib/permissions';
|
import { Permission } from '@server/lib/permissions';
|
||||||
import { getSettings } from '@server/lib/settings';
|
import { getSettings } from '@server/lib/settings';
|
||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
@@ -14,6 +15,7 @@ import { ApiError } from '@server/types/error';
|
|||||||
import { getHostname } from '@server/utils/getHostname';
|
import { getHostname } from '@server/utils/getHostname';
|
||||||
import * as EmailValidator from 'email-validator';
|
import * as EmailValidator from 'email-validator';
|
||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
|
import gravatarUrl from 'gravatar-url';
|
||||||
import net from 'net';
|
import net from 'net';
|
||||||
|
|
||||||
const authRoutes = Router();
|
const authRoutes = Router();
|
||||||
@@ -87,7 +89,7 @@ authRoutes.post('/plex', async (req, res, next) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
settings.main.mediaServerType = MediaServerType.PLEX;
|
settings.main.mediaServerType = MediaServerType.PLEX;
|
||||||
await settings.save();
|
settings.save();
|
||||||
startJobs();
|
startJobs();
|
||||||
|
|
||||||
await userRepository.save(user);
|
await userRepository.save(user);
|
||||||
@@ -326,7 +328,12 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
jellyfinDeviceId: deviceId,
|
jellyfinDeviceId: deviceId,
|
||||||
jellyfinAuthToken: account.AccessToken,
|
jellyfinAuthToken: account.AccessToken,
|
||||||
permissions: Permission.ADMIN,
|
permissions: Permission.ADMIN,
|
||||||
avatar: `/avatarproxy/${account.User.Id}`,
|
avatar: account.User.PrimaryImageTag
|
||||||
|
? `/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,
|
userType: UserType.EMBY,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -340,7 +347,12 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
jellyfinDeviceId: deviceId,
|
jellyfinDeviceId: deviceId,
|
||||||
jellyfinAuthToken: account.AccessToken,
|
jellyfinAuthToken: account.AccessToken,
|
||||||
permissions: Permission.ADMIN,
|
permissions: Permission.ADMIN,
|
||||||
avatar: `/avatarproxy/${account.User.Id}`,
|
avatar: account.User.PrimaryImageTag
|
||||||
|
? `/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,
|
userType: UserType.JELLYFIN,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -366,7 +378,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
settings.jellyfin.urlBase = body.urlBase ?? '';
|
settings.jellyfin.urlBase = body.urlBase ?? '';
|
||||||
settings.jellyfin.useSsl = body.useSsl ?? false;
|
settings.jellyfin.useSsl = body.useSsl ?? false;
|
||||||
settings.jellyfin.apiKey = apiKey;
|
settings.jellyfin.apiKey = apiKey;
|
||||||
await settings.save();
|
settings.save();
|
||||||
startJobs();
|
startJobs();
|
||||||
|
|
||||||
await userRepository.save(user);
|
await userRepository.save(user);
|
||||||
@@ -389,7 +401,27 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
jellyfinUsername: account.User.Name,
|
jellyfinUsername: account.User.Name,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
user.avatar = `/avatarproxy/${account.User.Id}`;
|
// Update the users avatar with their jellyfin profile pic (incase it changed)
|
||||||
|
if (account.User.PrimaryImageTag) {
|
||||||
|
const avatar = `/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`;
|
||||||
|
if (avatar !== user.avatar) {
|
||||||
|
const avatarProxy = new ImageProxy('avatar', '');
|
||||||
|
avatarProxy.clearCachedImage(user.avatar);
|
||||||
|
}
|
||||||
|
user.avatar = avatar;
|
||||||
|
} else {
|
||||||
|
const avatar = gravatarUrl(user.email || account.User.Name, {
|
||||||
|
default: 'mm',
|
||||||
|
size: 200,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (avatar !== user.avatar) {
|
||||||
|
const avatarProxy = new ImageProxy('avatar', '');
|
||||||
|
avatarProxy.clearCachedImage(user.avatar);
|
||||||
|
}
|
||||||
|
|
||||||
|
user.avatar = avatar;
|
||||||
|
}
|
||||||
user.jellyfinUsername = account.User.Name;
|
user.jellyfinUsername = account.User.Name;
|
||||||
|
|
||||||
if (user.username === account.User.Name) {
|
if (user.username === account.User.Name) {
|
||||||
@@ -427,7 +459,12 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
jellyfinUserId: account.User.Id,
|
jellyfinUserId: account.User.Id,
|
||||||
jellyfinDeviceId: deviceId,
|
jellyfinDeviceId: deviceId,
|
||||||
permissions: settings.main.defaultPermissions,
|
permissions: settings.main.defaultPermissions,
|
||||||
avatar: `/avatarproxy/${account.User.Id}`,
|
avatar: account.User.PrimaryImageTag
|
||||||
|
? `/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
|
||||||
|
: gravatarUrl(body.email || account.User.Name, {
|
||||||
|
default: 'mm',
|
||||||
|
size: 200,
|
||||||
|
}),
|
||||||
userType:
|
userType:
|
||||||
settings.main.mediaServerType === MediaServerType.JELLYFIN
|
settings.main.mediaServerType === MediaServerType.JELLYFIN
|
||||||
? UserType.JELLYFIN
|
? UserType.JELLYFIN
|
||||||
|
|||||||
@@ -1,39 +1,21 @@
|
|||||||
import { MediaServerType } from '@server/constants/server';
|
import { MediaServerType } from '@server/constants/server';
|
||||||
import { getRepository } from '@server/datasource';
|
|
||||||
import { User } from '@server/entity/User';
|
|
||||||
import ImageProxy from '@server/lib/imageproxy';
|
import ImageProxy from '@server/lib/imageproxy';
|
||||||
import { getSettings } from '@server/lib/settings';
|
import { getSettings } from '@server/lib/settings';
|
||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
import { getAppVersion } from '@server/utils/appVersion';
|
|
||||||
import { getHostname } from '@server/utils/getHostname';
|
import { getHostname } from '@server/utils/getHostname';
|
||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import gravatarUrl from 'gravatar-url';
|
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
let _avatarImageProxy: ImageProxy | null = null;
|
const avatarImageProxy = new ImageProxy('avatar', '');
|
||||||
async function initAvatarImageProxy() {
|
// Proxy avatar images
|
||||||
if (!_avatarImageProxy) {
|
router.get('/*', async (req, res) => {
|
||||||
const userRepository = getRepository(User);
|
let imagePath = '';
|
||||||
const admin = await userRepository.findOne({
|
|
||||||
where: { id: 1 },
|
|
||||||
select: ['id', 'jellyfinUserId', 'jellyfinDeviceId'],
|
|
||||||
order: { id: 'ASC' },
|
|
||||||
});
|
|
||||||
const deviceId = admin?.jellyfinDeviceId;
|
|
||||||
const authToken = getSettings().jellyfin.apiKey;
|
|
||||||
_avatarImageProxy = new ImageProxy('avatar', '', {
|
|
||||||
headers: {
|
|
||||||
'X-Emby-Authorization': `MediaBrowser Client="Jellyseerr", Device="Jellyseerr", DeviceId="${deviceId}", Version="${getAppVersion()}", Token="${authToken}"`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return _avatarImageProxy;
|
|
||||||
}
|
|
||||||
|
|
||||||
router.get('/:jellyfinUserId', async (req, res) => {
|
|
||||||
try {
|
try {
|
||||||
if (!req.params.jellyfinUserId.match(/^[a-f0-9]{32}$/)) {
|
const jellyfinAvatar = req.url.match(
|
||||||
|
/(\/Users\/\w+\/Images\/Primary\/?\?tag=\w+&quality=90)$/
|
||||||
|
)?.[1];
|
||||||
|
if (!jellyfinAvatar) {
|
||||||
const mediaServerType = getSettings().main.mediaServerType;
|
const mediaServerType = getSettings().main.mediaServerType;
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Provided URL is not ${
|
`Provided URL is not ${
|
||||||
@@ -44,28 +26,10 @@ router.get('/:jellyfinUserId', async (req, res) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const avatarImageCache = await initAvatarImageProxy();
|
const imageUrl = new URL(jellyfinAvatar, getHostname());
|
||||||
|
imagePath = imageUrl.toString();
|
||||||
|
|
||||||
const user = await getRepository(User).findOne({
|
const imageData = await avatarImageProxy.getImage(imagePath);
|
||||||
where: { jellyfinUserId: req.params.jellyfinUserId },
|
|
||||||
});
|
|
||||||
|
|
||||||
const fallbackUrl = gravatarUrl(user?.email || 'none', {
|
|
||||||
default: 'mm',
|
|
||||||
size: 200,
|
|
||||||
});
|
|
||||||
const jellyfinAvatarUrl = `${getHostname()}/UserImage?UserId=${
|
|
||||||
req.params.jellyfinUserId
|
|
||||||
}`;
|
|
||||||
let imageData = await avatarImageCache.getImage(
|
|
||||||
jellyfinAvatarUrl,
|
|
||||||
fallbackUrl
|
|
||||||
);
|
|
||||||
|
|
||||||
if (imageData.meta.extension === 'json') {
|
|
||||||
// this is a 404
|
|
||||||
imageData = await avatarImageCache.getImage(fallbackUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
res.writeHead(200, {
|
res.writeHead(200, {
|
||||||
'Content-Type': `image/${imageData.meta.extension}`,
|
'Content-Type': `image/${imageData.meta.extension}`,
|
||||||
@@ -78,6 +42,7 @@ router.get('/:jellyfinUserId', async (req, res) => {
|
|||||||
res.end(imageData.imageBuffer);
|
res.end(imageData.imageBuffer);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Failed to proxy avatar image', {
|
logger.error('Failed to proxy avatar image', {
|
||||||
|
imagePath,
|
||||||
errorMessage: e.message,
|
errorMessage: e.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,11 +17,7 @@ import { mapProductionCompany } from '@server/models/Movie';
|
|||||||
import { mapNetwork } from '@server/models/Tv';
|
import { mapNetwork } from '@server/models/Tv';
|
||||||
import settingsRoutes from '@server/routes/settings';
|
import settingsRoutes from '@server/routes/settings';
|
||||||
import watchlistRoutes from '@server/routes/watchlist';
|
import watchlistRoutes from '@server/routes/watchlist';
|
||||||
import {
|
import { appDataPath, appDataStatus } from '@server/utils/appDataVolume';
|
||||||
appDataPath,
|
|
||||||
appDataPermissions,
|
|
||||||
appDataStatus,
|
|
||||||
} from '@server/utils/appDataVolume';
|
|
||||||
import { getAppVersion, getCommitTag } from '@server/utils/appVersion';
|
import { getAppVersion, getCommitTag } from '@server/utils/appVersion';
|
||||||
import restartFlag from '@server/utils/restartFlag';
|
import restartFlag from '@server/utils/restartFlag';
|
||||||
import { isPerson } from '@server/utils/typeHelpers';
|
import { isPerson } from '@server/utils/typeHelpers';
|
||||||
@@ -97,7 +93,6 @@ router.get('/status/appdata', (_req, res) => {
|
|||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
appData: appDataStatus(),
|
appData: appDataStatus(),
|
||||||
appDataPath: appDataPath(),
|
appDataPath: appDataPath(),
|
||||||
appDataPermissions: appDataPermissions(),
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -123,13 +123,9 @@ serviceRoutes.get<{ sonarrId: string }>(
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const systemStatus = await sonarr.getSystemStatus();
|
|
||||||
const sonarrMajorVersion = Number(systemStatus.version.split('.')[0]);
|
|
||||||
|
|
||||||
const profiles = await sonarr.getProfiles();
|
const profiles = await sonarr.getProfiles();
|
||||||
const rootFolders = await sonarr.getRootFolders();
|
const rootFolders = await sonarr.getRootFolders();
|
||||||
const languageProfiles =
|
const languageProfiles = await sonarr.getLanguageProfiles();
|
||||||
sonarrMajorVersion <= 3 ? await sonarr.getLanguageProfiles() : null;
|
|
||||||
const tags = await sonarr.getTags();
|
const tags = await sonarr.getTags();
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import { getHostname } from '@server/utils/getHostname';
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import rateLimit from 'express-rate-limit';
|
import rateLimit from 'express-rate-limit';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
import gravatarUrl from 'gravatar-url';
|
||||||
import { escapeRegExp, merge, omit, set, sortBy } from 'lodash';
|
import { escapeRegExp, merge, omit, set, sortBy } from 'lodash';
|
||||||
import { rescheduleJob } from 'node-schedule';
|
import { rescheduleJob } from 'node-schedule';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
@@ -69,19 +70,19 @@ settingsRoutes.get('/main', (req, res, next) => {
|
|||||||
res.status(200).json(filteredMainSettings(req.user, settings.main));
|
res.status(200).json(filteredMainSettings(req.user, settings.main));
|
||||||
});
|
});
|
||||||
|
|
||||||
settingsRoutes.post('/main', async (req, res) => {
|
settingsRoutes.post('/main', (req, res) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
settings.main = merge(settings.main, req.body);
|
settings.main = merge(settings.main, req.body);
|
||||||
await settings.save();
|
settings.save();
|
||||||
|
|
||||||
return res.status(200).json(settings.main);
|
return res.status(200).json(settings.main);
|
||||||
});
|
});
|
||||||
|
|
||||||
settingsRoutes.post('/main/regenerate', async (req, res, next) => {
|
settingsRoutes.post('/main/regenerate', (req, res, next) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
const main = await settings.regenerateApiKey();
|
const main = settings.regenerateApiKey();
|
||||||
|
|
||||||
if (!req.user) {
|
if (!req.user) {
|
||||||
return next({ status: 500, message: 'User missing from request.' });
|
return next({ status: 500, message: 'User missing from request.' });
|
||||||
@@ -118,7 +119,7 @@ settingsRoutes.post('/plex', async (req, res, next) => {
|
|||||||
settings.plex.machineId = result.MediaContainer.machineIdentifier;
|
settings.plex.machineId = result.MediaContainer.machineIdentifier;
|
||||||
settings.plex.name = result.MediaContainer.friendlyName;
|
settings.plex.name = result.MediaContainer.friendlyName;
|
||||||
|
|
||||||
await settings.save();
|
settings.save();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Something went wrong testing Plex connection', {
|
logger.error('Something went wrong testing Plex connection', {
|
||||||
label: 'API',
|
label: 'API',
|
||||||
@@ -231,7 +232,7 @@ settingsRoutes.get('/plex/library', async (req, res) => {
|
|||||||
...library,
|
...library,
|
||||||
enabled: enabledLibraries.includes(library.id),
|
enabled: enabledLibraries.includes(library.id),
|
||||||
}));
|
}));
|
||||||
await settings.save();
|
settings.save();
|
||||||
return res.status(200).json(settings.plex.libraries);
|
return res.status(200).json(settings.plex.libraries);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -282,7 +283,7 @@ settingsRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
Object.assign(settings.jellyfin, req.body);
|
Object.assign(settings.jellyfin, req.body);
|
||||||
settings.jellyfin.serverId = result.Id;
|
settings.jellyfin.serverId = result.Id;
|
||||||
settings.jellyfin.name = result.ServerName;
|
settings.jellyfin.name = result.ServerName;
|
||||||
await settings.save();
|
settings.save();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof ApiError) {
|
if (e instanceof ApiError) {
|
||||||
logger.error('Something went wrong testing Jellyfin connection', {
|
logger.error('Something went wrong testing Jellyfin connection', {
|
||||||
@@ -370,7 +371,7 @@ settingsRoutes.get('/jellyfin/library', async (req, res, next) => {
|
|||||||
...library,
|
...library,
|
||||||
enabled: enabledLibraries.includes(library.id),
|
enabled: enabledLibraries.includes(library.id),
|
||||||
}));
|
}));
|
||||||
await settings.save();
|
settings.save();
|
||||||
return res.status(200).json(settings.jellyfin.libraries);
|
return res.status(200).json(settings.jellyfin.libraries);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -394,7 +395,9 @@ settingsRoutes.get('/jellyfin/users', async (req, res) => {
|
|||||||
const users = resp.users.map((user) => ({
|
const users = resp.users.map((user) => ({
|
||||||
username: user.Name,
|
username: user.Name,
|
||||||
id: user.Id,
|
id: user.Id,
|
||||||
thumb: `/avatarproxy/${user.Id}`,
|
thumb: user.PrimaryImageTag
|
||||||
|
? `/Users/${user.Id}/Images/Primary/?tag=${user.PrimaryImageTag}&quality=90`
|
||||||
|
: gravatarUrl(user.Name, { default: 'mm', size: 200 }),
|
||||||
email: user.Name,
|
email: user.Name,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -434,7 +437,7 @@ settingsRoutes.post('/tautulli', async (req, res, next) => {
|
|||||||
throw new Error('Tautulli version not supported');
|
throw new Error('Tautulli version not supported');
|
||||||
}
|
}
|
||||||
|
|
||||||
await settings.save();
|
settings.save();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Something went wrong testing Tautulli connection', {
|
logger.error('Something went wrong testing Tautulli connection', {
|
||||||
label: 'API',
|
label: 'API',
|
||||||
@@ -695,7 +698,7 @@ settingsRoutes.post<{ jobId: JobId }>(
|
|||||||
|
|
||||||
settingsRoutes.post<{ jobId: JobId }>(
|
settingsRoutes.post<{ jobId: JobId }>(
|
||||||
'/jobs/:jobId/schedule',
|
'/jobs/:jobId/schedule',
|
||||||
async (req, res, next) => {
|
(req, res, next) => {
|
||||||
const scheduledJob = scheduledJobs.find(
|
const scheduledJob = scheduledJobs.find(
|
||||||
(job) => job.id === req.params.jobId
|
(job) => job.id === req.params.jobId
|
||||||
);
|
);
|
||||||
@@ -709,7 +712,7 @@ settingsRoutes.post<{ jobId: JobId }>(
|
|||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
settings.jobs[scheduledJob.id].schedule = req.body.schedule;
|
settings.jobs[scheduledJob.id].schedule = req.body.schedule;
|
||||||
await settings.save();
|
settings.save();
|
||||||
|
|
||||||
scheduledJob.cronSchedule = req.body.schedule;
|
scheduledJob.cronSchedule = req.body.schedule;
|
||||||
|
|
||||||
@@ -766,11 +769,11 @@ settingsRoutes.post<{ cacheId: AvailableCacheIds }>(
|
|||||||
settingsRoutes.post(
|
settingsRoutes.post(
|
||||||
'/initialize',
|
'/initialize',
|
||||||
isAuthenticated(Permission.ADMIN),
|
isAuthenticated(Permission.ADMIN),
|
||||||
async (_req, res) => {
|
(_req, res) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
settings.public.initialized = true;
|
settings.public.initialized = true;
|
||||||
await settings.save();
|
settings.save();
|
||||||
|
|
||||||
return res.status(200).json(settings.public);
|
return res.status(200).json(settings.public);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,11 +31,11 @@ notificationRoutes.get('/discord', (_req, res) => {
|
|||||||
res.status(200).json(settings.notifications.agents.discord);
|
res.status(200).json(settings.notifications.agents.discord);
|
||||||
});
|
});
|
||||||
|
|
||||||
notificationRoutes.post('/discord', async (req, res) => {
|
notificationRoutes.post('/discord', (req, res) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
settings.notifications.agents.discord = req.body;
|
settings.notifications.agents.discord = req.body;
|
||||||
await settings.save();
|
settings.save();
|
||||||
|
|
||||||
res.status(200).json(settings.notifications.agents.discord);
|
res.status(200).json(settings.notifications.agents.discord);
|
||||||
});
|
});
|
||||||
@@ -65,11 +65,11 @@ notificationRoutes.get('/slack', (_req, res) => {
|
|||||||
res.status(200).json(settings.notifications.agents.slack);
|
res.status(200).json(settings.notifications.agents.slack);
|
||||||
});
|
});
|
||||||
|
|
||||||
notificationRoutes.post('/slack', async (req, res) => {
|
notificationRoutes.post('/slack', (req, res) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
settings.notifications.agents.slack = req.body;
|
settings.notifications.agents.slack = req.body;
|
||||||
await settings.save();
|
settings.save();
|
||||||
|
|
||||||
res.status(200).json(settings.notifications.agents.slack);
|
res.status(200).json(settings.notifications.agents.slack);
|
||||||
});
|
});
|
||||||
@@ -99,11 +99,11 @@ notificationRoutes.get('/telegram', (_req, res) => {
|
|||||||
res.status(200).json(settings.notifications.agents.telegram);
|
res.status(200).json(settings.notifications.agents.telegram);
|
||||||
});
|
});
|
||||||
|
|
||||||
notificationRoutes.post('/telegram', async (req, res) => {
|
notificationRoutes.post('/telegram', (req, res) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
settings.notifications.agents.telegram = req.body;
|
settings.notifications.agents.telegram = req.body;
|
||||||
await settings.save();
|
settings.save();
|
||||||
|
|
||||||
res.status(200).json(settings.notifications.agents.telegram);
|
res.status(200).json(settings.notifications.agents.telegram);
|
||||||
});
|
});
|
||||||
@@ -133,11 +133,11 @@ notificationRoutes.get('/pushbullet', (_req, res) => {
|
|||||||
res.status(200).json(settings.notifications.agents.pushbullet);
|
res.status(200).json(settings.notifications.agents.pushbullet);
|
||||||
});
|
});
|
||||||
|
|
||||||
notificationRoutes.post('/pushbullet', async (req, res) => {
|
notificationRoutes.post('/pushbullet', (req, res) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
settings.notifications.agents.pushbullet = req.body;
|
settings.notifications.agents.pushbullet = req.body;
|
||||||
await settings.save();
|
settings.save();
|
||||||
|
|
||||||
res.status(200).json(settings.notifications.agents.pushbullet);
|
res.status(200).json(settings.notifications.agents.pushbullet);
|
||||||
});
|
});
|
||||||
@@ -167,11 +167,11 @@ notificationRoutes.get('/pushover', (_req, res) => {
|
|||||||
res.status(200).json(settings.notifications.agents.pushover);
|
res.status(200).json(settings.notifications.agents.pushover);
|
||||||
});
|
});
|
||||||
|
|
||||||
notificationRoutes.post('/pushover', async (req, res) => {
|
notificationRoutes.post('/pushover', (req, res) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
settings.notifications.agents.pushover = req.body;
|
settings.notifications.agents.pushover = req.body;
|
||||||
await settings.save();
|
settings.save();
|
||||||
|
|
||||||
res.status(200).json(settings.notifications.agents.pushover);
|
res.status(200).json(settings.notifications.agents.pushover);
|
||||||
});
|
});
|
||||||
@@ -201,11 +201,11 @@ notificationRoutes.get('/email', (_req, res) => {
|
|||||||
res.status(200).json(settings.notifications.agents.email);
|
res.status(200).json(settings.notifications.agents.email);
|
||||||
});
|
});
|
||||||
|
|
||||||
notificationRoutes.post('/email', async (req, res) => {
|
notificationRoutes.post('/email', (req, res) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
settings.notifications.agents.email = req.body;
|
settings.notifications.agents.email = req.body;
|
||||||
await settings.save();
|
settings.save();
|
||||||
|
|
||||||
res.status(200).json(settings.notifications.agents.email);
|
res.status(200).json(settings.notifications.agents.email);
|
||||||
});
|
});
|
||||||
@@ -235,11 +235,11 @@ notificationRoutes.get('/webpush', (_req, res) => {
|
|||||||
res.status(200).json(settings.notifications.agents.webpush);
|
res.status(200).json(settings.notifications.agents.webpush);
|
||||||
});
|
});
|
||||||
|
|
||||||
notificationRoutes.post('/webpush', async (req, res) => {
|
notificationRoutes.post('/webpush', (req, res) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
settings.notifications.agents.webpush = req.body;
|
settings.notifications.agents.webpush = req.body;
|
||||||
await settings.save();
|
settings.save();
|
||||||
|
|
||||||
res.status(200).json(settings.notifications.agents.webpush);
|
res.status(200).json(settings.notifications.agents.webpush);
|
||||||
});
|
});
|
||||||
@@ -284,7 +284,7 @@ notificationRoutes.get('/webhook', (_req, res) => {
|
|||||||
res.status(200).json(response);
|
res.status(200).json(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
notificationRoutes.post('/webhook', async (req, res, next) => {
|
notificationRoutes.post('/webhook', (req, res, next) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
try {
|
try {
|
||||||
JSON.parse(req.body.options.jsonPayload);
|
JSON.parse(req.body.options.jsonPayload);
|
||||||
@@ -300,7 +300,7 @@ notificationRoutes.post('/webhook', async (req, res, next) => {
|
|||||||
authHeader: req.body.options.authHeader,
|
authHeader: req.body.options.authHeader,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
await settings.save();
|
settings.save();
|
||||||
|
|
||||||
res.status(200).json(settings.notifications.agents.webhook);
|
res.status(200).json(settings.notifications.agents.webhook);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -351,11 +351,11 @@ notificationRoutes.get('/lunasea', (_req, res) => {
|
|||||||
res.status(200).json(settings.notifications.agents.lunasea);
|
res.status(200).json(settings.notifications.agents.lunasea);
|
||||||
});
|
});
|
||||||
|
|
||||||
notificationRoutes.post('/lunasea', async (req, res) => {
|
notificationRoutes.post('/lunasea', (req, res) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
settings.notifications.agents.lunasea = req.body;
|
settings.notifications.agents.lunasea = req.body;
|
||||||
await settings.save();
|
settings.save();
|
||||||
|
|
||||||
res.status(200).json(settings.notifications.agents.lunasea);
|
res.status(200).json(settings.notifications.agents.lunasea);
|
||||||
});
|
});
|
||||||
@@ -385,11 +385,11 @@ notificationRoutes.get('/gotify', (_req, res) => {
|
|||||||
res.status(200).json(settings.notifications.agents.gotify);
|
res.status(200).json(settings.notifications.agents.gotify);
|
||||||
});
|
});
|
||||||
|
|
||||||
notificationRoutes.post('/gotify', async (req, res) => {
|
notificationRoutes.post('/gotify', (req, res) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
settings.notifications.agents.gotify = req.body;
|
settings.notifications.agents.gotify = req.body;
|
||||||
await settings.save();
|
settings.save();
|
||||||
|
|
||||||
res.status(200).json(settings.notifications.agents.gotify);
|
res.status(200).json(settings.notifications.agents.gotify);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ radarrRoutes.get('/', (_req, res) => {
|
|||||||
res.status(200).json(settings.radarr);
|
res.status(200).json(settings.radarr);
|
||||||
});
|
});
|
||||||
|
|
||||||
radarrRoutes.post('/', async (req, res) => {
|
radarrRoutes.post('/', (req, res) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
const newRadarr = req.body as RadarrSettings;
|
const newRadarr = req.body as RadarrSettings;
|
||||||
@@ -31,7 +31,7 @@ radarrRoutes.post('/', async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
settings.radarr = [...settings.radarr, newRadarr];
|
settings.radarr = [...settings.radarr, newRadarr];
|
||||||
await settings.save();
|
settings.save();
|
||||||
|
|
||||||
return res.status(201).json(newRadarr);
|
return res.status(201).json(newRadarr);
|
||||||
});
|
});
|
||||||
@@ -76,7 +76,7 @@ radarrRoutes.post<
|
|||||||
|
|
||||||
radarrRoutes.put<{ id: string }, RadarrSettings, RadarrSettings>(
|
radarrRoutes.put<{ id: string }, RadarrSettings, RadarrSettings>(
|
||||||
'/:id',
|
'/:id',
|
||||||
async (req, res, next) => {
|
(req, res, next) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
const radarrIndex = settings.radarr.findIndex(
|
const radarrIndex = settings.radarr.findIndex(
|
||||||
@@ -102,7 +102,7 @@ radarrRoutes.put<{ id: string }, RadarrSettings, RadarrSettings>(
|
|||||||
...req.body,
|
...req.body,
|
||||||
id: Number(req.params.id),
|
id: Number(req.params.id),
|
||||||
} as RadarrSettings;
|
} as RadarrSettings;
|
||||||
await settings.save();
|
settings.save();
|
||||||
|
|
||||||
return res.status(200).json(settings.radarr[radarrIndex]);
|
return res.status(200).json(settings.radarr[radarrIndex]);
|
||||||
}
|
}
|
||||||
@@ -134,7 +134,7 @@ radarrRoutes.get<{ id: string }>('/:id/profiles', async (req, res, next) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
radarrRoutes.delete<{ id: string }>('/:id', async (req, res, next) => {
|
radarrRoutes.delete<{ id: string }>('/:id', (req, res, next) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
const radarrIndex = settings.radarr.findIndex(
|
const radarrIndex = settings.radarr.findIndex(
|
||||||
@@ -146,7 +146,7 @@ radarrRoutes.delete<{ id: string }>('/:id', async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const removed = settings.radarr.splice(radarrIndex, 1);
|
const removed = settings.radarr.splice(radarrIndex, 1);
|
||||||
await settings.save();
|
settings.save();
|
||||||
|
|
||||||
return res.status(200).json(removed[0]);
|
return res.status(200).json(removed[0]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ sonarrRoutes.get('/', (_req, res) => {
|
|||||||
res.status(200).json(settings.sonarr);
|
res.status(200).json(settings.sonarr);
|
||||||
});
|
});
|
||||||
|
|
||||||
sonarrRoutes.post('/', async (req, res) => {
|
sonarrRoutes.post('/', (req, res) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
const newSonarr = req.body as SonarrSettings;
|
const newSonarr = req.body as SonarrSettings;
|
||||||
@@ -31,7 +31,7 @@ sonarrRoutes.post('/', async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
settings.sonarr = [...settings.sonarr, newSonarr];
|
settings.sonarr = [...settings.sonarr, newSonarr];
|
||||||
await settings.save();
|
settings.save();
|
||||||
|
|
||||||
return res.status(201).json(newSonarr);
|
return res.status(201).json(newSonarr);
|
||||||
});
|
});
|
||||||
@@ -43,14 +43,13 @@ sonarrRoutes.post('/test', async (req, res, next) => {
|
|||||||
url: SonarrAPI.buildUrl(req.body, '/api/v3'),
|
url: SonarrAPI.buildUrl(req.body, '/api/v3'),
|
||||||
});
|
});
|
||||||
|
|
||||||
const systemStatus = await sonarr.getSystemStatus();
|
const urlBase = await sonarr
|
||||||
const sonarrMajorVersion = Number(systemStatus.version.split('.')[0]);
|
.getSystemStatus()
|
||||||
|
.then((value) => value.urlBase)
|
||||||
const urlBase = systemStatus.urlBase;
|
.catch(() => req.body.baseUrl);
|
||||||
const profiles = await sonarr.getProfiles();
|
const profiles = await sonarr.getProfiles();
|
||||||
const folders = await sonarr.getRootFolders();
|
const folders = await sonarr.getRootFolders();
|
||||||
const languageProfiles =
|
const languageProfiles = await sonarr.getLanguageProfiles();
|
||||||
sonarrMajorVersion <= 3 ? await sonarr.getLanguageProfiles() : null;
|
|
||||||
const tags = await sonarr.getTags();
|
const tags = await sonarr.getTags();
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
@@ -73,7 +72,7 @@ sonarrRoutes.post('/test', async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
sonarrRoutes.put<{ id: string }>('/:id', async (req, res) => {
|
sonarrRoutes.put<{ id: string }>('/:id', (req, res) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
const sonarrIndex = settings.sonarr.findIndex(
|
const sonarrIndex = settings.sonarr.findIndex(
|
||||||
@@ -101,12 +100,12 @@ sonarrRoutes.put<{ id: string }>('/:id', async (req, res) => {
|
|||||||
...req.body,
|
...req.body,
|
||||||
id: Number(req.params.id),
|
id: Number(req.params.id),
|
||||||
} as SonarrSettings;
|
} as SonarrSettings;
|
||||||
await settings.save();
|
settings.save();
|
||||||
|
|
||||||
return res.status(200).json(settings.sonarr[sonarrIndex]);
|
return res.status(200).json(settings.sonarr[sonarrIndex]);
|
||||||
});
|
});
|
||||||
|
|
||||||
sonarrRoutes.delete<{ id: string }>('/:id', async (req, res) => {
|
sonarrRoutes.delete<{ id: string }>('/:id', (req, res) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
const sonarrIndex = settings.sonarr.findIndex(
|
const sonarrIndex = settings.sonarr.findIndex(
|
||||||
@@ -120,7 +119,7 @@ sonarrRoutes.delete<{ id: string }>('/:id', async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const removed = settings.sonarr.splice(sonarrIndex, 1);
|
const removed = settings.sonarr.splice(sonarrIndex, 1);
|
||||||
await settings.save();
|
settings.save();
|
||||||
|
|
||||||
return res.status(200).json(removed[0]);
|
return res.status(200).json(removed[0]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -539,7 +539,12 @@ router.post(
|
|||||||
).toString('base64'),
|
).toString('base64'),
|
||||||
email: jellyfinUser?.Name,
|
email: jellyfinUser?.Name,
|
||||||
permissions: settings.main.defaultPermissions,
|
permissions: settings.main.defaultPermissions,
|
||||||
avatar: `/avatarproxy/${jellyfinUser?.Id}`,
|
avatar: jellyfinUser?.PrimaryImageTag
|
||||||
|
? `/Users/${jellyfinUser.Id}/Images/Primary/?tag=${jellyfinUser.PrimaryImageTag}&quality=90`
|
||||||
|
: gravatarUrl(jellyfinUser?.Name ?? '', {
|
||||||
|
default: 'mm',
|
||||||
|
size: 200,
|
||||||
|
}),
|
||||||
userType:
|
userType:
|
||||||
settings.main.mediaServerType === MediaServerType.JELLYFIN
|
settings.main.mediaServerType === MediaServerType.JELLYFIN
|
||||||
? UserType.JELLYFIN
|
? UserType.JELLYFIN
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { accessSync, existsSync } from 'fs';
|
import { existsSync } from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
const CONFIG_PATH = process.env.CONFIG_DIRECTORY
|
const CONFIG_PATH = process.env.CONFIG_DIRECTORY
|
||||||
@@ -14,12 +14,3 @@ export const appDataStatus = (): boolean => {
|
|||||||
export const appDataPath = (): string => {
|
export const appDataPath = (): string => {
|
||||||
return CONFIG_PATH;
|
return CONFIG_PATH;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const appDataPermissions = (): boolean => {
|
|
||||||
try {
|
|
||||||
accessSync(CONFIG_PATH);
|
|
||||||
return true;
|
|
||||||
} catch (err) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,111 +0,0 @@
|
|||||||
import type { ProxySettings } from '@server/lib/settings';
|
|
||||||
import logger from '@server/logger';
|
|
||||||
import type { Dispatcher } from 'undici';
|
|
||||||
import { Agent, ProxyAgent, setGlobalDispatcher } from 'undici';
|
|
||||||
|
|
||||||
export default async function createCustomProxyAgent(
|
|
||||||
proxySettings: ProxySettings
|
|
||||||
) {
|
|
||||||
const defaultAgent = new Agent();
|
|
||||||
|
|
||||||
const skipUrl = (url: string) => {
|
|
||||||
const hostname = new URL(url).hostname;
|
|
||||||
|
|
||||||
if (proxySettings.bypassLocalAddresses && isLocalAddress(hostname)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const address of proxySettings.bypassFilter.split(',')) {
|
|
||||||
const trimmedAddress = address.trim();
|
|
||||||
if (!trimmedAddress) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (trimmedAddress.startsWith('*')) {
|
|
||||||
const domain = trimmedAddress.slice(1);
|
|
||||||
if (hostname.endsWith(domain)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} else if (hostname === trimmedAddress) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const noProxyInterceptor = (
|
|
||||||
dispatch: Dispatcher['dispatch']
|
|
||||||
): Dispatcher['dispatch'] => {
|
|
||||||
return (opts, handler) => {
|
|
||||||
const url = opts.origin?.toString();
|
|
||||||
return url && skipUrl(url)
|
|
||||||
? defaultAgent.dispatch(opts, handler)
|
|
||||||
: dispatch(opts, handler);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const token =
|
|
||||||
proxySettings.user && proxySettings.password
|
|
||||||
? `Basic ${Buffer.from(
|
|
||||||
`${proxySettings.user}:${proxySettings.password}`
|
|
||||||
).toString('base64')}`
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const proxyAgent = new ProxyAgent({
|
|
||||||
uri:
|
|
||||||
(proxySettings.useSsl ? 'https://' : 'http://') +
|
|
||||||
proxySettings.hostname +
|
|
||||||
':' +
|
|
||||||
proxySettings.port,
|
|
||||||
token,
|
|
||||||
interceptors: {
|
|
||||||
Client: [noProxyInterceptor],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
setGlobalDispatcher(proxyAgent);
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('Failed to connect to the proxy: ' + e.message, {
|
|
||||||
label: 'Proxy',
|
|
||||||
});
|
|
||||||
setGlobalDispatcher(defaultAgent);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await fetch('https://www.google.com', { method: 'HEAD' });
|
|
||||||
if (res.ok) {
|
|
||||||
logger.debug('HTTP(S) proxy connected successfully', { label: 'Proxy' });
|
|
||||||
} else {
|
|
||||||
logger.error('Proxy responded, but with a non-OK status: ' + res.status, {
|
|
||||||
label: 'Proxy',
|
|
||||||
});
|
|
||||||
setGlobalDispatcher(defaultAgent);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
logger.error(
|
|
||||||
'Failed to connect to the proxy: ' + e.message + ': ' + e.cause,
|
|
||||||
{ label: 'Proxy' }
|
|
||||||
);
|
|
||||||
setGlobalDispatcher(defaultAgent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isLocalAddress(hostname: string) {
|
|
||||||
if (hostname === 'localhost' || hostname === '127.0.0.1') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const privateIpRanges = [
|
|
||||||
/^10\./, // 10.x.x.x
|
|
||||||
/^172\.(1[6-9]|2[0-9]|3[0-1])\./, // 172.16.x.x - 172.31.x.x
|
|
||||||
/^192\.168\./, // 192.168.x.x
|
|
||||||
];
|
|
||||||
if (privateIpRanges.some((regex) => regex.test(hostname))) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
@@ -14,7 +14,7 @@ class RestartFlag {
|
|||||||
return (
|
return (
|
||||||
this.settings.csrfProtection !== settings.csrfProtection ||
|
this.settings.csrfProtection !== settings.csrfProtection ||
|
||||||
this.settings.trustProxy !== settings.trustProxy ||
|
this.settings.trustProxy !== settings.trustProxy ||
|
||||||
this.settings.proxy.enabled !== settings.proxy.enabled
|
this.settings.httpProxy !== settings.httpProxy
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,8 +25,11 @@ const CachedImage = ({ src, type, ...props }: CachedImageProps) => {
|
|||||||
? src.replace(/^https:\/\/image\.tmdb\.org\//, '/imageproxy/')
|
? src.replace(/^https:\/\/image\.tmdb\.org\//, '/imageproxy/')
|
||||||
: src;
|
: src;
|
||||||
} else if (type === 'avatar') {
|
} else if (type === 'avatar') {
|
||||||
// jellyfin avatar (if any)
|
// jellyfin avatar (in any)
|
||||||
imageUrl = src;
|
const jellyfinAvatar = src.match(
|
||||||
|
/(\/Users\/\w+\/Images\/Primary\/?\?tag=\w+&quality=90)$/
|
||||||
|
)?.[1];
|
||||||
|
imageUrl = jellyfinAvatar ? `/avatarproxy` + jellyfinAvatar : src;
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,17 +55,8 @@ const messages = defineMessages('components.Settings.SettingsMain', {
|
|||||||
validationApplicationUrlTrailingSlash: 'URL must not end in a trailing slash',
|
validationApplicationUrlTrailingSlash: 'URL must not end in a trailing slash',
|
||||||
partialRequestsEnabled: 'Allow Partial Series Requests',
|
partialRequestsEnabled: 'Allow Partial Series Requests',
|
||||||
locale: 'Display Language',
|
locale: 'Display Language',
|
||||||
proxyEnabled: 'HTTP(S) Proxy',
|
httpProxy: 'HTTP Proxy',
|
||||||
proxyHostname: 'Proxy Hostname',
|
httpProxyTip: 'Tooltip to write',
|
||||||
proxyPort: 'Proxy Port',
|
|
||||||
proxySsl: 'Use SSL For Proxy',
|
|
||||||
proxyUser: 'Proxy Username',
|
|
||||||
proxyPassword: 'Proxy Password',
|
|
||||||
proxyBypassFilter: 'Proxy Ignored Addresses',
|
|
||||||
proxyBypassFilterTip:
|
|
||||||
"Use ',' as a separator, and '*.' as a wildcard for subdomains",
|
|
||||||
proxyBypassLocalAddresses: 'Bypass Proxy for Local Addresses',
|
|
||||||
validationProxyPort: 'You must provide a valid port',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const SettingsMain = () => {
|
const SettingsMain = () => {
|
||||||
@@ -93,12 +84,9 @@ const SettingsMain = () => {
|
|||||||
intl.formatMessage(messages.validationApplicationUrlTrailingSlash),
|
intl.formatMessage(messages.validationApplicationUrlTrailingSlash),
|
||||||
(value) => !value || !value.endsWith('/')
|
(value) => !value || !value.endsWith('/')
|
||||||
),
|
),
|
||||||
proxyPort: Yup.number().when('proxyEnabled', {
|
httpProxy: Yup.string().url(
|
||||||
is: (proxyEnabled: boolean) => proxyEnabled,
|
intl.formatMessage(messages.validationApplicationUrl)
|
||||||
then: Yup.number().required(
|
|
||||||
intl.formatMessage(messages.validationProxyPort)
|
|
||||||
),
|
),
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const regenerate = async () => {
|
const regenerate = async () => {
|
||||||
@@ -154,14 +142,7 @@ const SettingsMain = () => {
|
|||||||
partialRequestsEnabled: data?.partialRequestsEnabled,
|
partialRequestsEnabled: data?.partialRequestsEnabled,
|
||||||
trustProxy: data?.trustProxy,
|
trustProxy: data?.trustProxy,
|
||||||
cacheImages: data?.cacheImages,
|
cacheImages: data?.cacheImages,
|
||||||
proxyEnabled: data?.proxy?.enabled,
|
httpProxy: data?.httpProxy,
|
||||||
proxyHostname: data?.proxy?.hostname,
|
|
||||||
proxyPort: data?.proxy?.port,
|
|
||||||
proxySsl: data?.proxy?.useSsl,
|
|
||||||
proxyUser: data?.proxy?.user,
|
|
||||||
proxyPassword: data?.proxy?.password,
|
|
||||||
proxyBypassFilter: data?.proxy?.bypassFilter,
|
|
||||||
proxyBypassLocalAddresses: data?.proxy?.bypassLocalAddresses,
|
|
||||||
}}
|
}}
|
||||||
enableReinitialize
|
enableReinitialize
|
||||||
validationSchema={MainSettingsSchema}
|
validationSchema={MainSettingsSchema}
|
||||||
@@ -183,16 +164,7 @@ const SettingsMain = () => {
|
|||||||
partialRequestsEnabled: values.partialRequestsEnabled,
|
partialRequestsEnabled: values.partialRequestsEnabled,
|
||||||
trustProxy: values.trustProxy,
|
trustProxy: values.trustProxy,
|
||||||
cacheImages: values.cacheImages,
|
cacheImages: values.cacheImages,
|
||||||
proxy: {
|
httpProxy: values.httpProxy,
|
||||||
enabled: values.proxyEnabled,
|
|
||||||
hostname: values.proxyHostname,
|
|
||||||
port: values.proxyPort,
|
|
||||||
useSsl: values.proxySsl,
|
|
||||||
user: values.proxyUser,
|
|
||||||
password: values.proxyPassword,
|
|
||||||
bypassFilter: values.proxyBypassFilter,
|
|
||||||
bypassLocalAddresses: values.proxyBypassLocalAddresses,
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error();
|
if (!res.ok) throw new Error();
|
||||||
@@ -473,175 +445,27 @@ const SettingsMain = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<label htmlFor="proxyEnabled" className="checkbox-label">
|
<label htmlFor="httpProxy" className="checkbox-label">
|
||||||
<span className="mr-2">
|
<span className="mr-2">
|
||||||
{intl.formatMessage(messages.proxyEnabled)}
|
{intl.formatMessage(messages.httpProxy)}
|
||||||
</span>
|
</span>
|
||||||
<SettingsBadge badgeType="advanced" className="mr-2" />
|
<SettingsBadge badgeType="advanced" className="mr-2" />
|
||||||
<SettingsBadge badgeType="restartRequired" />
|
<SettingsBadge badgeType="restartRequired" />
|
||||||
</label>
|
<span className="label-tip">
|
||||||
<div className="form-input-area">
|
{intl.formatMessage(messages.httpProxyTip)}
|
||||||
<Field
|
|
||||||
type="checkbox"
|
|
||||||
id="proxyEnabled"
|
|
||||||
name="proxyEnabled"
|
|
||||||
onChange={() => {
|
|
||||||
setFieldValue('proxyEnabled', !values.proxyEnabled);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{values.proxyEnabled && (
|
|
||||||
<>
|
|
||||||
<div className="form-row">
|
|
||||||
<label htmlFor="proxyHostname" className="checkbox-label">
|
|
||||||
<span className="mr-2 ml-4">
|
|
||||||
{intl.formatMessage(messages.proxyHostname)}
|
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<div className="form-input-area">
|
<div className="form-input-area">
|
||||||
<div className="form-input-field">
|
<div className="form-input-field">
|
||||||
<Field
|
<Field id="httpProxy" name="httpProxy" type="text" />
|
||||||
id="proxyHostname"
|
|
||||||
name="proxyHostname"
|
|
||||||
type="text"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{errors.proxyHostname &&
|
{errors.httpProxy &&
|
||||||
touched.proxyHostname &&
|
touched.httpProxy &&
|
||||||
typeof errors.proxyHostname === 'string' && (
|
typeof errors.httpProxy === 'string' && (
|
||||||
<div className="error">{errors.proxyHostname}</div>
|
<div className="error">{errors.httpProxy}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-row">
|
|
||||||
<label htmlFor="proxyPort" className="checkbox-label">
|
|
||||||
<span className="mr-2 ml-4">
|
|
||||||
{intl.formatMessage(messages.proxyPort)}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<div className="form-input-area">
|
|
||||||
<div className="form-input-field">
|
|
||||||
<Field id="proxyPort" name="proxyPort" type="text" />
|
|
||||||
</div>
|
|
||||||
{errors.proxyPort &&
|
|
||||||
touched.proxyPort &&
|
|
||||||
typeof errors.proxyPort === 'string' && (
|
|
||||||
<div className="error">{errors.proxyPort}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="form-row">
|
|
||||||
<label htmlFor="proxySsl" className="checkbox-label">
|
|
||||||
<span className="mr-2 ml-4">
|
|
||||||
{intl.formatMessage(messages.proxySsl)}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<div className="form-input-area">
|
|
||||||
<Field
|
|
||||||
type="checkbox"
|
|
||||||
id="proxySsl"
|
|
||||||
name="proxySsl"
|
|
||||||
onChange={() => {
|
|
||||||
setFieldValue('proxySsl', !values.proxySsl);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="form-row">
|
|
||||||
<label htmlFor="proxyUser" className="checkbox-label">
|
|
||||||
<span className="mr-2 ml-4">
|
|
||||||
{intl.formatMessage(messages.proxyUser)}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<div className="form-input-area">
|
|
||||||
<div className="form-input-field">
|
|
||||||
<Field id="proxyUser" name="proxyUser" type="text" />
|
|
||||||
</div>
|
|
||||||
{errors.proxyUser &&
|
|
||||||
touched.proxyUser &&
|
|
||||||
typeof errors.proxyUser === 'string' && (
|
|
||||||
<div className="error">{errors.proxyUser}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="form-row">
|
|
||||||
<label htmlFor="proxyPassword" className="checkbox-label">
|
|
||||||
<span className="mr-2 ml-4">
|
|
||||||
{intl.formatMessage(messages.proxyPassword)}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<div className="form-input-area">
|
|
||||||
<div className="form-input-field">
|
|
||||||
<Field
|
|
||||||
id="proxyPassword"
|
|
||||||
name="proxyPassword"
|
|
||||||
type="password"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{errors.proxyPassword &&
|
|
||||||
touched.proxyPassword &&
|
|
||||||
typeof errors.proxyPassword === 'string' && (
|
|
||||||
<div className="error">{errors.proxyPassword}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="form-row">
|
|
||||||
<label
|
|
||||||
htmlFor="proxyBypassFilter"
|
|
||||||
className="checkbox-label"
|
|
||||||
>
|
|
||||||
<span className="mr-2 ml-4">
|
|
||||||
{intl.formatMessage(messages.proxyBypassFilter)}
|
|
||||||
</span>
|
|
||||||
<span className="label-tip ml-4">
|
|
||||||
{intl.formatMessage(messages.proxyBypassFilterTip)}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<div className="form-input-area">
|
|
||||||
<div className="form-input-field">
|
|
||||||
<Field
|
|
||||||
id="proxyBypassFilter"
|
|
||||||
name="proxyBypassFilter"
|
|
||||||
type="text"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{errors.proxyBypassFilter &&
|
|
||||||
touched.proxyBypassFilter &&
|
|
||||||
typeof errors.proxyBypassFilter === 'string' && (
|
|
||||||
<div className="error">
|
|
||||||
{errors.proxyBypassFilter}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="form-row">
|
|
||||||
<label
|
|
||||||
htmlFor="proxyBypassLocalAddresses"
|
|
||||||
className="checkbox-label"
|
|
||||||
>
|
|
||||||
<span className="mr-2 ml-4">
|
|
||||||
{intl.formatMessage(
|
|
||||||
messages.proxyBypassLocalAddresses
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<div className="form-input-area">
|
|
||||||
<Field
|
|
||||||
type="checkbox"
|
|
||||||
id="proxyBypassLocalAddresses"
|
|
||||||
name="proxyBypassLocalAddresses"
|
|
||||||
onChange={() => {
|
|
||||||
setFieldValue(
|
|
||||||
'proxyBypassLocalAddresses',
|
|
||||||
!values.proxyBypassLocalAddresses
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<div className="actions">
|
<div className="actions">
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<span className="ml-3 inline-flex rounded-md shadow-sm">
|
<span className="ml-3 inline-flex rounded-md shadow-sm">
|
||||||
|
|||||||
@@ -86,12 +86,10 @@ interface TestResponse {
|
|||||||
id: number;
|
id: number;
|
||||||
path: string;
|
path: string;
|
||||||
}[];
|
}[];
|
||||||
languageProfiles:
|
languageProfiles: {
|
||||||
| {
|
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
}[]
|
}[];
|
||||||
| null;
|
|
||||||
tags: {
|
tags: {
|
||||||
id: number;
|
id: number;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -114,7 +112,7 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
|
|||||||
const [testResponse, setTestResponse] = useState<TestResponse>({
|
const [testResponse, setTestResponse] = useState<TestResponse>({
|
||||||
profiles: [],
|
profiles: [],
|
||||||
rootFolders: [],
|
rootFolders: [],
|
||||||
languageProfiles: null,
|
languageProfiles: [],
|
||||||
tags: [],
|
tags: [],
|
||||||
});
|
});
|
||||||
const SonarrSettingsSchema = Yup.object().shape({
|
const SonarrSettingsSchema = Yup.object().shape({
|
||||||
@@ -139,11 +137,9 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
|
|||||||
activeProfileId: Yup.string().required(
|
activeProfileId: Yup.string().required(
|
||||||
intl.formatMessage(messages.validationProfileRequired)
|
intl.formatMessage(messages.validationProfileRequired)
|
||||||
),
|
),
|
||||||
activeLanguageProfileId: testResponse.languageProfiles
|
activeLanguageProfileId: Yup.number().required(
|
||||||
? Yup.number().required(
|
|
||||||
intl.formatMessage(messages.validationLanguageProfileRequired)
|
intl.formatMessage(messages.validationLanguageProfileRequired)
|
||||||
)
|
),
|
||||||
: Yup.number(),
|
|
||||||
externalUrl: Yup.string()
|
externalUrl: Yup.string()
|
||||||
.url(intl.formatMessage(messages.validationApplicationUrl))
|
.url(intl.formatMessage(messages.validationApplicationUrl))
|
||||||
.test(
|
.test(
|
||||||
@@ -662,7 +658,6 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{testResponse.languageProfiles && (
|
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<label
|
<label
|
||||||
htmlFor="activeLanguageProfileId"
|
htmlFor="activeLanguageProfileId"
|
||||||
@@ -711,7 +706,6 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<label htmlFor="tags" className="text-label">
|
<label htmlFor="tags" className="text-label">
|
||||||
{intl.formatMessage(messages.tags)}
|
{intl.formatMessage(messages.tags)}
|
||||||
@@ -869,7 +863,6 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{testResponse.languageProfiles && (
|
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<label
|
<label
|
||||||
htmlFor="activeAnimeLanguageProfileId"
|
htmlFor="activeAnimeLanguageProfileId"
|
||||||
@@ -917,7 +910,6 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<label htmlFor="tags" className="text-label">
|
<label htmlFor="tags" className="text-label">
|
||||||
{intl.formatMessage(messages.animeTags)}
|
{intl.formatMessage(messages.animeTags)}
|
||||||
|
|||||||
Reference in New Issue
Block a user