mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00
feat: pre-populate server info from plex.tv API (#563)
Co-authored-by: sct <sctsnipe@gmail.com>
This commit is contained in:
@@ -32,6 +32,7 @@ module.exports = {
|
|||||||
'formatjs/no-offset': 'error',
|
'formatjs/no-offset': 'error',
|
||||||
'no-unused-vars': 'off',
|
'no-unused-vars': 'off',
|
||||||
'@typescript-eslint/no-unused-vars': ['error'],
|
'@typescript-eslint/no-unused-vars': ['error'],
|
||||||
|
'jsx-a11y/no-onchange': 'off',
|
||||||
},
|
},
|
||||||
overrides: [
|
overrides: [
|
||||||
{
|
{
|
||||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@@ -47,3 +47,6 @@ dist
|
|||||||
|
|
||||||
# sqlite journal
|
# sqlite journal
|
||||||
config/db/db.sqlite3-journal
|
config/db/db.sqlite3-journal
|
||||||
|
|
||||||
|
# VS Code
|
||||||
|
.vscode/launch.json
|
||||||
|
@@ -113,6 +113,140 @@ components:
|
|||||||
- machineId
|
- machineId
|
||||||
- ip
|
- ip
|
||||||
- port
|
- port
|
||||||
|
PlexStatus:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
settings:
|
||||||
|
$ref: '#/components/schemas/PlexSettings'
|
||||||
|
status:
|
||||||
|
type: number
|
||||||
|
example: 200
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
example: 'OK'
|
||||||
|
PlexConnection:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
protocol:
|
||||||
|
type: string
|
||||||
|
example: 'https'
|
||||||
|
address:
|
||||||
|
type: string
|
||||||
|
example: '127.0.0.1'
|
||||||
|
port:
|
||||||
|
type: number
|
||||||
|
example: 32400
|
||||||
|
uri:
|
||||||
|
type: string
|
||||||
|
example: 'https://127-0-0-1.2ab6ce1a093d465e910def96cf4e4799.plex.direct:32400'
|
||||||
|
local:
|
||||||
|
type: boolean
|
||||||
|
example: true
|
||||||
|
status:
|
||||||
|
type: number
|
||||||
|
example: 200
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
example: 'OK'
|
||||||
|
host:
|
||||||
|
type: string
|
||||||
|
example: '127-0-0-1.2ab6ce1a093d465e910def96cf4e4799.plex.direct'
|
||||||
|
required:
|
||||||
|
- protocol
|
||||||
|
- address
|
||||||
|
- port
|
||||||
|
- uri
|
||||||
|
- local
|
||||||
|
PlexDevice:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
example: 'My Plex Server'
|
||||||
|
product:
|
||||||
|
type: string
|
||||||
|
example: 'Plex Media Server'
|
||||||
|
productVersion:
|
||||||
|
type: string
|
||||||
|
example: '1.21'
|
||||||
|
platform:
|
||||||
|
type: string
|
||||||
|
example: 'Linux'
|
||||||
|
platformVersion:
|
||||||
|
type: string
|
||||||
|
example: 'default/linux/amd64/17.1/systemd'
|
||||||
|
device:
|
||||||
|
type: string
|
||||||
|
example: 'PC'
|
||||||
|
clientIdentifier:
|
||||||
|
type: string
|
||||||
|
example: '85a943ce-a0cc-4d2a-a4ec-f74f06e40feb'
|
||||||
|
createdAt:
|
||||||
|
type: string
|
||||||
|
example: '2021-01-01T00:00:00.000Z'
|
||||||
|
lastSeenAt:
|
||||||
|
type: string
|
||||||
|
example: '2021-01-01T00:00:00.000Z'
|
||||||
|
provides:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
example: 'server'
|
||||||
|
owned:
|
||||||
|
type: boolean
|
||||||
|
example: true
|
||||||
|
ownerID:
|
||||||
|
type: string
|
||||||
|
example: '12345'
|
||||||
|
home:
|
||||||
|
type: boolean
|
||||||
|
example: true
|
||||||
|
sourceTitle:
|
||||||
|
type: string
|
||||||
|
example: 'xyzabc'
|
||||||
|
accessToken:
|
||||||
|
type: string
|
||||||
|
example: 'supersecretaccesstoken'
|
||||||
|
publicAddress:
|
||||||
|
type: string
|
||||||
|
example: '127.0.0.1'
|
||||||
|
httpsRequired:
|
||||||
|
type: boolean
|
||||||
|
example: true
|
||||||
|
synced:
|
||||||
|
type: boolean
|
||||||
|
example: true
|
||||||
|
relay:
|
||||||
|
type: boolean
|
||||||
|
example: true
|
||||||
|
dnsRebindingProtection:
|
||||||
|
type: boolean
|
||||||
|
example: false
|
||||||
|
natLoopbackSupported:
|
||||||
|
type: boolean
|
||||||
|
example: false
|
||||||
|
publicAddressMatches:
|
||||||
|
type: boolean
|
||||||
|
example: false
|
||||||
|
presence:
|
||||||
|
type: boolean
|
||||||
|
example: true
|
||||||
|
connection:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/PlexConnection'
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
- product
|
||||||
|
- productVersion
|
||||||
|
- platform
|
||||||
|
- device
|
||||||
|
- clientIdentifier
|
||||||
|
- createdAt
|
||||||
|
- lastSeenAt
|
||||||
|
- provides
|
||||||
|
- owned
|
||||||
|
- connection
|
||||||
RadarrSettings:
|
RadarrSettings:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -1460,6 +1594,21 @@ paths:
|
|||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/PlexLibrary'
|
$ref: '#/components/schemas/PlexLibrary'
|
||||||
|
/settings/plex/devices/servers:
|
||||||
|
get:
|
||||||
|
summary: Gets the user's available plex servers
|
||||||
|
description: Returns a list of available plex servers and their connectivity state
|
||||||
|
tags:
|
||||||
|
- settings
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: OK
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/PlexDevice'
|
||||||
/settings/radarr:
|
/settings/radarr:
|
||||||
get:
|
get:
|
||||||
summary: Get all radarr settings
|
summary: Get all radarr settings
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import NodePlexAPI from 'plex-api';
|
import NodePlexAPI from 'plex-api';
|
||||||
import { getSettings } from '../lib/settings';
|
import { getSettings, PlexSettings } from '../lib/settings';
|
||||||
|
|
||||||
export interface PlexLibraryItem {
|
export interface PlexLibraryItem {
|
||||||
ratingKey: string;
|
ratingKey: string;
|
||||||
@@ -80,13 +80,26 @@ interface PlexMetadataResponse {
|
|||||||
class PlexAPI {
|
class PlexAPI {
|
||||||
private plexClient: NodePlexAPI;
|
private plexClient: NodePlexAPI;
|
||||||
|
|
||||||
constructor({ plexToken }: { plexToken?: string }) {
|
constructor({
|
||||||
|
plexToken,
|
||||||
|
plexSettings,
|
||||||
|
timeout,
|
||||||
|
}: {
|
||||||
|
plexToken?: string;
|
||||||
|
plexSettings?: PlexSettings;
|
||||||
|
timeout?: number;
|
||||||
|
}) {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
let settingsPlex: PlexSettings | undefined;
|
||||||
|
plexSettings
|
||||||
|
? (settingsPlex = plexSettings)
|
||||||
|
: (settingsPlex = getSettings().plex);
|
||||||
|
|
||||||
this.plexClient = new NodePlexAPI({
|
this.plexClient = new NodePlexAPI({
|
||||||
hostname: settings.plex.ip,
|
hostname: settingsPlex.ip,
|
||||||
port: settings.plex.port,
|
port: settingsPlex.port,
|
||||||
https: settings.plex.useSsl,
|
https: settingsPlex.useSsl,
|
||||||
|
timeout: timeout,
|
||||||
token: plexToken,
|
token: plexToken,
|
||||||
authenticator: {
|
authenticator: {
|
||||||
authenticate: (
|
authenticate: (
|
||||||
@@ -111,6 +124,7 @@ class PlexAPI {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
public async getStatus() {
|
public async getStatus() {
|
||||||
return await this.plexClient.query('/');
|
return await this.plexClient.query('/');
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import axios, { AxiosInstance } from 'axios';
|
import axios, { AxiosInstance } from 'axios';
|
||||||
import xml2js from 'xml2js';
|
import xml2js from 'xml2js';
|
||||||
|
import { PlexDevice } from '../interfaces/api/plexInterfaces';
|
||||||
import { getSettings } from '../lib/settings';
|
import { getSettings } from '../lib/settings';
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
|
|
||||||
@@ -29,6 +30,45 @@ interface PlexUser {
|
|||||||
entitlements: string[];
|
entitlements: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ConnectionResponse {
|
||||||
|
$: {
|
||||||
|
protocol: string;
|
||||||
|
address: string;
|
||||||
|
port: string;
|
||||||
|
uri: string;
|
||||||
|
local: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeviceResponse {
|
||||||
|
$: {
|
||||||
|
name: string;
|
||||||
|
product: string;
|
||||||
|
productVersion: string;
|
||||||
|
platform: string;
|
||||||
|
platformVersion: string;
|
||||||
|
device: string;
|
||||||
|
clientIdentifier: string;
|
||||||
|
createdAt: string;
|
||||||
|
lastSeenAt: string;
|
||||||
|
provides: string;
|
||||||
|
owned: string;
|
||||||
|
accessToken?: string;
|
||||||
|
publicAddress?: string;
|
||||||
|
httpsRequired?: string;
|
||||||
|
synced?: string;
|
||||||
|
relay?: string;
|
||||||
|
dnsRebindingProtection?: string;
|
||||||
|
natLoopbackSupported?: string;
|
||||||
|
publicAddressMatches?: string;
|
||||||
|
presence?: string;
|
||||||
|
ownerID?: string;
|
||||||
|
home?: string;
|
||||||
|
sourceTitle?: string;
|
||||||
|
};
|
||||||
|
Connection: ConnectionResponse[];
|
||||||
|
}
|
||||||
|
|
||||||
interface ServerResponse {
|
interface ServerResponse {
|
||||||
$: {
|
$: {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -87,6 +127,62 @@ class PlexTvAPI {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getDevices(): Promise<PlexDevice[]> {
|
||||||
|
try {
|
||||||
|
const devicesResp = await this.axios.get(
|
||||||
|
'/api/resources?includeHttps=1',
|
||||||
|
{
|
||||||
|
transformResponse: [],
|
||||||
|
responseType: 'text',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const parsedXml = await xml2js.parseStringPromise(
|
||||||
|
devicesResp.data as DeviceResponse
|
||||||
|
);
|
||||||
|
return parsedXml?.MediaContainer?.Device?.map((pxml: DeviceResponse) => ({
|
||||||
|
name: pxml.$.name,
|
||||||
|
product: pxml.$.product,
|
||||||
|
productVersion: pxml.$.productVersion,
|
||||||
|
platform: pxml.$?.platform,
|
||||||
|
platformVersion: pxml.$?.platformVersion,
|
||||||
|
device: pxml.$?.device,
|
||||||
|
clientIdentifier: pxml.$.clientIdentifier,
|
||||||
|
createdAt: new Date(parseInt(pxml.$?.createdAt, 10) * 1000),
|
||||||
|
lastSeenAt: new Date(parseInt(pxml.$?.lastSeenAt, 10) * 1000),
|
||||||
|
provides: pxml.$.provides.split(','),
|
||||||
|
owned: pxml.$.owned == '1' ? true : false,
|
||||||
|
accessToken: pxml.$?.accessToken,
|
||||||
|
publicAddress: pxml.$?.publicAddress,
|
||||||
|
publicAddressMatches:
|
||||||
|
pxml.$?.publicAddressMatches == '1' ? true : false,
|
||||||
|
httpsRequired: pxml.$?.httpsRequired == '1' ? true : false,
|
||||||
|
synced: pxml.$?.synced == '1' ? true : false,
|
||||||
|
relay: pxml.$?.relay == '1' ? true : false,
|
||||||
|
dnsRebindingProtection:
|
||||||
|
pxml.$?.dnsRebindingProtection == '1' ? true : false,
|
||||||
|
natLoopbackSupported:
|
||||||
|
pxml.$?.natLoopbackSupported == '1' ? true : false,
|
||||||
|
presence: pxml.$?.presence == '1' ? true : false,
|
||||||
|
ownerID: pxml.$?.ownerID,
|
||||||
|
home: pxml.$?.home == '1' ? true : false,
|
||||||
|
sourceTitle: pxml.$?.sourceTitle,
|
||||||
|
connection: pxml?.Connection?.map((conn: ConnectionResponse) => ({
|
||||||
|
protocol: conn.$.protocol,
|
||||||
|
address: conn.$.address,
|
||||||
|
port: parseInt(conn.$.port, 10),
|
||||||
|
uri: conn.$.uri,
|
||||||
|
local: conn.$.local == '1' ? true : false,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('Something went wrong getting the devices from plex.tv', {
|
||||||
|
label: 'Plex.tv API',
|
||||||
|
errorMessage: e.message,
|
||||||
|
});
|
||||||
|
throw new Error('Invalid auth token');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async getUser(): Promise<PlexUser> {
|
public async getUser(): Promise<PlexUser> {
|
||||||
try {
|
try {
|
||||||
const account = await this.axios.get<PlexAccountResponse>(
|
const account = await this.axios.get<PlexAccountResponse>(
|
||||||
|
45
server/interfaces/api/plexInterfaces.ts
Normal file
45
server/interfaces/api/plexInterfaces.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { PlexSettings } from '../../lib/settings';
|
||||||
|
|
||||||
|
export interface PlexStatus {
|
||||||
|
settings: PlexSettings;
|
||||||
|
status: number;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlexConnection {
|
||||||
|
protocol: string;
|
||||||
|
address: string;
|
||||||
|
port: number;
|
||||||
|
uri: string;
|
||||||
|
local: boolean;
|
||||||
|
status?: number;
|
||||||
|
message?: string;
|
||||||
|
host?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlexDevice {
|
||||||
|
name: string;
|
||||||
|
product: string;
|
||||||
|
productVersion: string;
|
||||||
|
platform: string;
|
||||||
|
platformVersion: string;
|
||||||
|
device: string;
|
||||||
|
clientIdentifier: string;
|
||||||
|
createdAt: Date;
|
||||||
|
lastSeenAt: Date;
|
||||||
|
provides: string[];
|
||||||
|
owned: boolean;
|
||||||
|
accessToken?: string;
|
||||||
|
publicAddress?: string;
|
||||||
|
httpsRequired?: boolean;
|
||||||
|
synced?: boolean;
|
||||||
|
relay?: boolean;
|
||||||
|
dnsRebindingProtection?: boolean;
|
||||||
|
natLoopbackSupported?: boolean;
|
||||||
|
publicAddressMatches?: boolean;
|
||||||
|
presence?: boolean;
|
||||||
|
ownerID?: string;
|
||||||
|
home?: boolean;
|
||||||
|
sourceTitle?: string;
|
||||||
|
connection: PlexConnection[];
|
||||||
|
}
|
@@ -9,6 +9,7 @@ import {
|
|||||||
import { getRepository } from 'typeorm';
|
import { getRepository } from 'typeorm';
|
||||||
import { User } from '../../entity/User';
|
import { User } from '../../entity/User';
|
||||||
import PlexAPI from '../../api/plexapi';
|
import PlexAPI from '../../api/plexapi';
|
||||||
|
import PlexTvAPI from '../../api/plextv';
|
||||||
import { jobPlexFullSync } from '../../job/plexsync';
|
import { jobPlexFullSync } from '../../job/plexsync';
|
||||||
import SonarrAPI from '../../api/sonarr';
|
import SonarrAPI from '../../api/sonarr';
|
||||||
import RadarrAPI from '../../api/radarr';
|
import RadarrAPI from '../../api/radarr';
|
||||||
@@ -106,6 +107,69 @@ settingsRoutes.post('/plex', async (req, res, next) => {
|
|||||||
return res.status(200).json(settings.plex);
|
return res.status(200).json(settings.plex);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
settingsRoutes.get('/plex/devices/servers', async (req, res, next) => {
|
||||||
|
const userRepository = getRepository(User);
|
||||||
|
const regexp = /(http(s?):\/\/)(.*)(:[0-9]*)/;
|
||||||
|
try {
|
||||||
|
const admin = await userRepository.findOneOrFail({
|
||||||
|
select: ['id', 'plexToken'],
|
||||||
|
order: { id: 'ASC' },
|
||||||
|
});
|
||||||
|
const plexTvClient = admin.plexToken
|
||||||
|
? new PlexTvAPI(admin.plexToken)
|
||||||
|
: null;
|
||||||
|
const devices = (await plexTvClient?.getDevices())?.filter((device) => {
|
||||||
|
return device.provides.includes('server') && device.owned;
|
||||||
|
});
|
||||||
|
const settings = getSettings();
|
||||||
|
if (devices) {
|
||||||
|
await Promise.all(
|
||||||
|
devices.map(async (device) => {
|
||||||
|
await Promise.all(
|
||||||
|
device.connection.map(async (connection) => {
|
||||||
|
connection.host = connection.uri.replace(regexp, '$3');
|
||||||
|
let msg:
|
||||||
|
| { status: number; message: string }
|
||||||
|
| undefined = undefined;
|
||||||
|
const plexDeviceSettings = {
|
||||||
|
...settings.plex,
|
||||||
|
ip: connection.host,
|
||||||
|
port: connection.port,
|
||||||
|
useSsl: connection.protocol === 'https' ? true : false,
|
||||||
|
};
|
||||||
|
const plexClient = new PlexAPI({
|
||||||
|
plexToken: admin.plexToken,
|
||||||
|
plexSettings: plexDeviceSettings,
|
||||||
|
timeout: 5000,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await plexClient.getStatus();
|
||||||
|
msg = {
|
||||||
|
status: 200,
|
||||||
|
message: 'OK',
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
msg = {
|
||||||
|
status: 500,
|
||||||
|
message: e.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
connection.status = msg?.status;
|
||||||
|
connection.message = msg?.message;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return res.status(200).json(devices);
|
||||||
|
} catch (e) {
|
||||||
|
return next({
|
||||||
|
status: 500,
|
||||||
|
message: `Failed to connect to Plex: ${e.message}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
settingsRoutes.get('/plex/library', async (req, res) => {
|
settingsRoutes.get('/plex/library', async (req, res) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
@@ -156,7 +220,6 @@ settingsRoutes.get('/plex/sync', (req, res) => {
|
|||||||
} else if (req.query.start) {
|
} else if (req.query.start) {
|
||||||
jobPlexFullSync.run();
|
jobPlexFullSync.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).json(jobPlexFullSync.status());
|
return res.status(200).json(jobPlexFullSync.status());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
3
server/types/plex-api.d.ts
vendored
3
server/types/plex-api.d.ts
vendored
@@ -5,6 +5,7 @@ declare module 'plex-api' {
|
|||||||
port: number;
|
port: number;
|
||||||
token?: string;
|
token?: string;
|
||||||
https?: boolean;
|
https?: boolean;
|
||||||
|
timeout?: number;
|
||||||
authenticator: {
|
authenticator: {
|
||||||
authenticate: (
|
authenticate: (
|
||||||
_plexApi: PlexAPI,
|
_plexApi: PlexAPI,
|
||||||
@@ -19,7 +20,7 @@ declare module 'plex-api' {
|
|||||||
};
|
};
|
||||||
requestOptions?: Record<string, string | number>;
|
requestOptions?: Record<string, string | number>;
|
||||||
});
|
});
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
query: <T extends Record<string, any>>(endpoint: string) => Promise<T>;
|
query: <T extends Record<string, any>>(endpoint: string) => Promise<T>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
|
|
||||||
interface AlertProps {
|
interface AlertProps {
|
||||||
title: string;
|
title: string;
|
||||||
type?: 'warning' | 'info';
|
type?: 'warning' | 'info' | 'error';
|
||||||
}
|
}
|
||||||
|
|
||||||
const Alert: React.FC<AlertProps> = ({ title, children, type }) => {
|
const Alert: React.FC<AlertProps> = ({ title, children, type }) => {
|
||||||
@@ -51,6 +51,29 @@ const Alert: React.FC<AlertProps> = ({ title, children, type }) => {
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
case 'error':
|
||||||
|
design = {
|
||||||
|
bgColor: 'bg-red-600',
|
||||||
|
titleColor: 'text-red-200',
|
||||||
|
textColor: 'text-red-300',
|
||||||
|
svg: (
|
||||||
|
<svg
|
||||||
|
className="w-5 h-5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@@ -1,7 +1,9 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import LoadingSpinner from '../Common/LoadingSpinner';
|
import LoadingSpinner from '../Common/LoadingSpinner';
|
||||||
import type { PlexSettings } from '../../../server/lib/settings';
|
import type { PlexSettings } from '../../../server/lib/settings';
|
||||||
|
import type { PlexDevice } from '../../../server/interfaces/api/plexInterfaces';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
import { useToasts } from 'react-toast-notifications';
|
||||||
import { Formik, Field } from 'formik';
|
import { Formik, Field } from 'formik';
|
||||||
import Button from '../Common/Button';
|
import Button from '../Common/Button';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
@@ -9,16 +11,38 @@ import LibraryItem from './LibraryItem';
|
|||||||
import Badge from '../Common/Badge';
|
import Badge from '../Common/Badge';
|
||||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
|
import Alert from '../Common/Alert';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
plexsettings: 'Plex Settings',
|
plexsettings: 'Plex Settings',
|
||||||
plexsettingsDescription:
|
plexsettingsDescription:
|
||||||
'Configure the settings for your Plex server. Overseerr uses your Plex server to scan your library at an interval and see what content is available.',
|
'Configure the settings for your Plex server. Overseerr uses your Plex server to scan your library at an interval and see what content is available.',
|
||||||
servername: 'Server Name (Automatically set after you save)',
|
servername: 'Server Name (Retrieved from Plex)',
|
||||||
servernamePlaceholder: 'Plex Server Name',
|
servernamePlaceholder: 'Plex Server Name',
|
||||||
|
serverpreset: 'Available Server',
|
||||||
|
serverpresetPlaceholder: 'Plex Server (Retrieved Automatically)',
|
||||||
|
serverLocal: 'local',
|
||||||
|
serverRemote: 'remote',
|
||||||
|
serverConnected: 'connected',
|
||||||
|
serverpresetManualMessage: 'Manually configure',
|
||||||
|
serverpresetRefreshing: 'Retrieving servers...',
|
||||||
|
serverpresetLoad: 'Press button to load available servers',
|
||||||
|
toastPlexRefresh: 'Retrieving server list from Plex',
|
||||||
|
toastPlexRefreshSuccess: 'Retrieved server list from Plex',
|
||||||
|
toastPlexRefreshFailure: 'Unable to retrieve server list from Plex',
|
||||||
|
toastPlexConnecting: 'Attempting to connect to Plex server',
|
||||||
|
toastPlexConnectingSuccess: 'Connected to Plex server',
|
||||||
|
toastPlexConnectingFailure: 'Unable to connect to Plex server',
|
||||||
|
settingUpPlex: 'Setting up Plex',
|
||||||
|
settingUpPlexDescription:
|
||||||
|
'To setup Plex you can enter your details manually, \
|
||||||
|
or choose from one of the available servers retrieved from <RegisterPlexTVLink>plex.tv</RegisterPlexTVLink>.\
|
||||||
|
Press the button next to the dropdown to retrieve available servers and check connectivity.',
|
||||||
hostname: 'Hostname/IP',
|
hostname: 'Hostname/IP',
|
||||||
port: 'Port',
|
port: 'Port',
|
||||||
ssl: 'SSL',
|
ssl: 'SSL',
|
||||||
|
timeout: 'Timeout',
|
||||||
|
ms: 'ms',
|
||||||
save: 'Save Changes',
|
save: 'Save Changes',
|
||||||
saving: 'Saving...',
|
saving: 'Saving...',
|
||||||
plexlibraries: 'Plex Libraries',
|
plexlibraries: 'Plex Libraries',
|
||||||
@@ -52,24 +76,41 @@ interface SyncStatus {
|
|||||||
libraries: Library[];
|
libraries: Library[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface PresetServerDisplay {
|
||||||
|
name: string;
|
||||||
|
ssl: boolean;
|
||||||
|
uri: string;
|
||||||
|
address: string;
|
||||||
|
host?: string;
|
||||||
|
port: number;
|
||||||
|
local: boolean;
|
||||||
|
status?: boolean;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
interface SettingsPlexProps {
|
interface SettingsPlexProps {
|
||||||
onComplete?: () => void;
|
onComplete?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
|
const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
|
||||||
const intl = useIntl();
|
const [isSyncing, setIsSyncing] = useState(false);
|
||||||
const { data, error, revalidate } = useSWR<PlexSettings>(
|
const [isRefreshingPresets, setIsRefreshingPresets] = useState(false);
|
||||||
'/api/v1/settings/plex'
|
const [submitError, setSubmitError] = useState<string | null>(null);
|
||||||
|
const [availableServers, setAvailableServers] = useState<PlexDevice[] | null>(
|
||||||
|
null
|
||||||
);
|
);
|
||||||
|
const {
|
||||||
|
data: data,
|
||||||
|
error: error,
|
||||||
|
revalidate: revalidate,
|
||||||
|
} = useSWR<PlexSettings>('/api/v1/settings/plex');
|
||||||
const { data: dataSync, revalidate: revalidateSync } = useSWR<SyncStatus>(
|
const { data: dataSync, revalidate: revalidateSync } = useSWR<SyncStatus>(
|
||||||
'/api/v1/settings/plex/sync',
|
'/api/v1/settings/plex/sync',
|
||||||
{
|
{
|
||||||
refreshInterval: 1000,
|
refreshInterval: 1000,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const [isSyncing, setIsSyncing] = useState(false);
|
const intl = useIntl();
|
||||||
const [submitError, setSubmitError] = useState<string | null>(null);
|
const { addToast, removeToast } = useToasts();
|
||||||
|
|
||||||
const PlexSettingsSchema = Yup.object().shape({
|
const PlexSettingsSchema = Yup.object().shape({
|
||||||
hostname: Yup.string().required(
|
hostname: Yup.string().required(
|
||||||
intl.formatMessage(messages.validationHostnameRequired)
|
intl.formatMessage(messages.validationHostnameRequired)
|
||||||
@@ -84,6 +125,33 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
|
|||||||
.filter((library) => library.enabled)
|
.filter((library) => library.enabled)
|
||||||
.map((library) => library.id) ?? [];
|
.map((library) => library.id) ?? [];
|
||||||
|
|
||||||
|
const availablePresets = useMemo(() => {
|
||||||
|
const finalPresets: PresetServerDisplay[] = [];
|
||||||
|
availableServers?.forEach((dev) => {
|
||||||
|
dev.connection.forEach((conn) =>
|
||||||
|
finalPresets.push({
|
||||||
|
name: dev.name,
|
||||||
|
ssl: conn.protocol === 'https' ? true : false,
|
||||||
|
uri: conn.uri,
|
||||||
|
address: conn.address,
|
||||||
|
port: conn.port,
|
||||||
|
local: conn.local,
|
||||||
|
host: conn.host,
|
||||||
|
status: conn.status === 200 ? true : false,
|
||||||
|
message: conn.message,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
finalPresets.sort((a, b) => {
|
||||||
|
if (a.status && !b.status) {
|
||||||
|
return -1;
|
||||||
|
} else {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return finalPresets;
|
||||||
|
}, [availableServers]);
|
||||||
|
|
||||||
const syncLibraries = async () => {
|
const syncLibraries = async () => {
|
||||||
setIsSyncing(true);
|
setIsSyncing(true);
|
||||||
|
|
||||||
@@ -102,6 +170,46 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
|
|||||||
revalidate();
|
revalidate();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const refreshPresetServers = async () => {
|
||||||
|
setIsRefreshingPresets(true);
|
||||||
|
let toastId: string | undefined;
|
||||||
|
try {
|
||||||
|
addToast(
|
||||||
|
intl.formatMessage(messages.toastPlexRefresh),
|
||||||
|
{
|
||||||
|
autoDismiss: false,
|
||||||
|
appearance: 'info',
|
||||||
|
},
|
||||||
|
(id) => {
|
||||||
|
toastId = id;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const response = await axios.get<PlexDevice[]>(
|
||||||
|
'/api/v1/settings/plex/devices/servers'
|
||||||
|
);
|
||||||
|
if (response.data) {
|
||||||
|
setAvailableServers(response.data);
|
||||||
|
}
|
||||||
|
if (toastId) {
|
||||||
|
removeToast(toastId);
|
||||||
|
}
|
||||||
|
addToast(intl.formatMessage(messages.toastPlexRefreshSuccess), {
|
||||||
|
autoDismiss: true,
|
||||||
|
appearance: 'success',
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
if (toastId) {
|
||||||
|
removeToast(toastId);
|
||||||
|
}
|
||||||
|
addToast(intl.formatMessage(messages.toastPlexRefreshFailure), {
|
||||||
|
autoDismiss: true,
|
||||||
|
appearance: 'error',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsRefreshingPresets(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const startScan = async () => {
|
const startScan = async () => {
|
||||||
await axios.get('/api/v1/settings/plex/sync', {
|
await axios.get('/api/v1/settings/plex/sync', {
|
||||||
params: {
|
params: {
|
||||||
@@ -157,18 +265,46 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
|
|||||||
<p className="max-w-2xl mt-1 text-sm leading-5 text-gray-500">
|
<p className="max-w-2xl mt-1 text-sm leading-5 text-gray-500">
|
||||||
<FormattedMessage {...messages.plexsettingsDescription} />
|
<FormattedMessage {...messages.plexsettingsDescription} />
|
||||||
</p>
|
</p>
|
||||||
|
<div className="mt-6 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200">
|
||||||
|
<Alert title={intl.formatMessage(messages.settingUpPlex)} type="info">
|
||||||
|
{intl.formatMessage(messages.settingUpPlexDescription, {
|
||||||
|
RegisterPlexTVLink: function RegisterPlexTVLink(msg) {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href="https://plex.tv"
|
||||||
|
className="text-indigo-100 hover:text-white hover:underline"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
{msg}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
</Alert>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={{
|
initialValues={{
|
||||||
hostname: data?.ip,
|
hostname: data?.ip,
|
||||||
port: data?.port,
|
port: data?.port,
|
||||||
useSsl: data?.useSsl,
|
useSsl: data?.useSsl,
|
||||||
|
selectedPreset: undefined,
|
||||||
}}
|
}}
|
||||||
enableReinitialize
|
|
||||||
validationSchema={PlexSettingsSchema}
|
validationSchema={PlexSettingsSchema}
|
||||||
onSubmit={async (values) => {
|
onSubmit={async (values) => {
|
||||||
setSubmitError(null);
|
let toastId: string | null = null;
|
||||||
try {
|
try {
|
||||||
|
addToast(
|
||||||
|
intl.formatMessage(messages.toastPlexConnecting),
|
||||||
|
{
|
||||||
|
autoDismiss: false,
|
||||||
|
appearance: 'info',
|
||||||
|
},
|
||||||
|
(id) => {
|
||||||
|
toastId = id;
|
||||||
|
}
|
||||||
|
);
|
||||||
await axios.post('/api/v1/settings/plex', {
|
await axios.post('/api/v1/settings/plex', {
|
||||||
ip: values.hostname,
|
ip: values.hostname,
|
||||||
port: Number(values.port),
|
port: Number(values.port),
|
||||||
@@ -176,10 +312,25 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
|
|||||||
} as PlexSettings);
|
} as PlexSettings);
|
||||||
|
|
||||||
revalidate();
|
revalidate();
|
||||||
|
setSubmitError(null);
|
||||||
|
if (toastId) {
|
||||||
|
removeToast(toastId);
|
||||||
|
}
|
||||||
|
addToast(intl.formatMessage(messages.toastPlexConnectingSuccess), {
|
||||||
|
autoDismiss: true,
|
||||||
|
appearance: 'success',
|
||||||
|
});
|
||||||
if (onComplete) {
|
if (onComplete) {
|
||||||
onComplete();
|
onComplete();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (toastId) {
|
||||||
|
removeToast(toastId);
|
||||||
|
}
|
||||||
|
addToast(intl.formatMessage(messages.toastPlexConnectingFailure), {
|
||||||
|
autoDismiss: true,
|
||||||
|
appearance: 'error',
|
||||||
|
});
|
||||||
setSubmitError(e.response.data.message);
|
setSubmitError(e.response.data.message);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -190,16 +341,12 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
|
|||||||
values,
|
values,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
setFieldValue,
|
setFieldValue,
|
||||||
|
setFieldTouched,
|
||||||
isSubmitting,
|
isSubmitting,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<div className="mt-6 sm:mt-5">
|
<div className="mt-6 sm:mt-5">
|
||||||
{submitError && (
|
|
||||||
<div className="p-4 mb-6 text-white bg-red-700 rounded-md">
|
|
||||||
{submitError}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800">
|
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800">
|
||||||
<label
|
<label
|
||||||
htmlFor="name"
|
htmlFor="name"
|
||||||
@@ -225,71 +372,176 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="mt-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800">
|
<div className="mt-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800">
|
||||||
<label
|
<label
|
||||||
htmlFor="hostname"
|
htmlFor="preset"
|
||||||
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
|
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
|
||||||
>
|
>
|
||||||
<FormattedMessage {...messages.hostname} />
|
<FormattedMessage {...messages.serverpreset} />
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
||||||
<div className="flex max-w-lg rounded-md shadow-sm">
|
<div className="flex max-w-lg rounded-md shadow-sm input-group">
|
||||||
<span className="inline-flex items-center px-3 text-gray-100 bg-gray-800 border border-r-0 border-gray-500 cursor-default rounded-l-md sm:text-sm">
|
<select
|
||||||
{values.useSsl ? 'https://' : 'http://'}
|
id="preset"
|
||||||
</span>
|
name="preset"
|
||||||
<Field
|
placeholder={intl.formatMessage(
|
||||||
type="text"
|
messages.serverpresetPlaceholder
|
||||||
id="hostname"
|
)}
|
||||||
name="hostname"
|
value={values.selectedPreset}
|
||||||
placeholder="127.0.0.1"
|
disabled={!availableServers || isRefreshingPresets}
|
||||||
className="flex-1 block w-full min-w-0 transition duration-150 ease-in-out bg-gray-700 border border-gray-500 form-input rounded-r-md sm:text-sm sm:leading-5"
|
className="flex-1 block w-full min-w-0 transition duration-150 ease-in-out bg-gray-700 border border-gray-500 rounded-none rounded-l-md form-input sm:text-sm sm:leading-5"
|
||||||
/>
|
onChange={async (e) => {
|
||||||
|
const targPreset =
|
||||||
|
availablePresets[Number(e.target.value)];
|
||||||
|
if (targPreset) {
|
||||||
|
setFieldValue('hostname', targPreset.host);
|
||||||
|
setFieldValue('port', targPreset.port);
|
||||||
|
setFieldValue('useSsl', targPreset.ssl);
|
||||||
|
}
|
||||||
|
setFieldTouched('hostname');
|
||||||
|
setFieldTouched('port');
|
||||||
|
setFieldTouched('useSsl');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="manual">
|
||||||
|
{availableServers || isRefreshingPresets
|
||||||
|
? isRefreshingPresets
|
||||||
|
? intl.formatMessage(
|
||||||
|
messages.serverpresetRefreshing
|
||||||
|
)
|
||||||
|
: intl.formatMessage(
|
||||||
|
messages.serverpresetManualMessage
|
||||||
|
)
|
||||||
|
: intl.formatMessage(messages.serverpresetLoad)}
|
||||||
|
</option>
|
||||||
|
{availablePresets.map((server, index) => (
|
||||||
|
<option
|
||||||
|
key={`preset-server-${index}`}
|
||||||
|
value={index}
|
||||||
|
disabled={!server.status}
|
||||||
|
>
|
||||||
|
{`
|
||||||
|
${server.name} (${server.address})
|
||||||
|
[${
|
||||||
|
server.local
|
||||||
|
? intl.formatMessage(messages.serverLocal)
|
||||||
|
: intl.formatMessage(messages.serverRemote)
|
||||||
|
}]
|
||||||
|
${server.status ? '' : '(' + server.message + ')'}
|
||||||
|
`}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
refreshPresetServers();
|
||||||
|
}}
|
||||||
|
className="relative inline-flex items-center px-4 py-2 -ml-px text-sm font-medium leading-5 text-white transition duration-150 ease-in-out bg-indigo-500 border border-gray-500 rounded-r-md hover:bg-indigo-400 focus:outline-none focus:ring-blue focus:border-blue-300 active:bg-gray-100 active:text-gray-700"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className={`w-5 h-5 ${
|
||||||
|
isRefreshingPresets ? 'animate-spin' : ''
|
||||||
|
}`}
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{errors.hostname && touched.hostname && (
|
|
||||||
<div className="mt-2 text-red-500">{errors.hostname}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200">
|
<div>
|
||||||
<label
|
<div>
|
||||||
htmlFor="port"
|
<div className="mt-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800">
|
||||||
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
|
<label
|
||||||
>
|
htmlFor="hostname"
|
||||||
<FormattedMessage {...messages.port} />
|
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
|
||||||
</label>
|
>
|
||||||
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
<FormattedMessage {...messages.hostname} />
|
||||||
<div className="max-w-lg rounded-md shadow-sm sm:max-w-xs">
|
</label>
|
||||||
|
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
||||||
|
<div className="flex max-w-lg rounded-md shadow-sm">
|
||||||
|
<span className="inline-flex items-center px-3 text-gray-100 bg-gray-800 border border-r-0 border-gray-500 cursor-default rounded-l-md sm:text-sm">
|
||||||
|
{values.useSsl ? 'https://' : 'http://'}
|
||||||
|
</span>
|
||||||
|
<Field
|
||||||
|
type="text"
|
||||||
|
id="hostname"
|
||||||
|
name="hostname"
|
||||||
|
placeholder="127.0.0.1"
|
||||||
|
className="flex-1 block w-full min-w-0 transition duration-150 ease-in-out bg-gray-700 border border-gray-500 form-input rounded-r-md sm:text-sm sm:leading-5"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{errors.hostname && touched.hostname && (
|
||||||
|
<div className="mt-2 text-red-500">
|
||||||
|
{errors.hostname}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200">
|
||||||
|
<label
|
||||||
|
htmlFor="port"
|
||||||
|
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
|
||||||
|
>
|
||||||
|
<FormattedMessage {...messages.port} />
|
||||||
|
</label>
|
||||||
|
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
||||||
|
<div className="max-w-lg rounded-md shadow-sm sm:max-w-xs">
|
||||||
|
<Field
|
||||||
|
type="text"
|
||||||
|
id="port"
|
||||||
|
name="port"
|
||||||
|
placeholder="32400"
|
||||||
|
className="block w-24 transition duration-150 ease-in-out bg-gray-700 border border-gray-500 rounded-md form-input sm:text-sm sm:leading-5"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{errors.port && touched.port && (
|
||||||
|
<div className="mt-2 text-red-500">{errors.port}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200">
|
||||||
|
<label
|
||||||
|
htmlFor="ssl"
|
||||||
|
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
|
||||||
|
>
|
||||||
|
{intl.formatMessage(messages.ssl)}
|
||||||
|
</label>
|
||||||
|
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
||||||
<Field
|
<Field
|
||||||
type="text"
|
type="checkbox"
|
||||||
id="port"
|
id="useSsl"
|
||||||
name="port"
|
name="useSsl"
|
||||||
placeholder="32400"
|
onChange={() => {
|
||||||
className="block w-24 transition duration-150 ease-in-out bg-gray-700 border border-gray-500 rounded-md form-input sm:text-sm sm:leading-5"
|
setFieldValue('useSsl', !values.useSsl);
|
||||||
|
}}
|
||||||
|
className="w-6 h-6 text-indigo-600 transition duration-150 ease-in-out rounded-md form-checkbox"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{errors.port && touched.port && (
|
|
||||||
<div className="mt-2 text-red-500">{errors.port}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200">
|
{submitError && (
|
||||||
<label
|
<div className="mt-6 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200">
|
||||||
htmlFor="ssl"
|
<Alert
|
||||||
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
|
title={intl.formatMessage(
|
||||||
>
|
messages.toastPlexConnectingFailure
|
||||||
{intl.formatMessage(messages.ssl)}
|
)}
|
||||||
</label>
|
type="error"
|
||||||
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
>
|
||||||
<Field
|
{submitError}
|
||||||
type="checkbox"
|
</Alert>
|
||||||
id="useSsl"
|
|
||||||
name="useSsl"
|
|
||||||
onChange={() => {
|
|
||||||
setFieldValue('useSsl', !values.useSsl);
|
|
||||||
}}
|
|
||||||
className="w-6 h-6 text-indigo-600 transition duration-150 ease-in-out rounded-md form-checkbox"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
<div className="pt-5 mt-8 border-t border-gray-700">
|
<div className="pt-5 mt-8 border-t border-gray-700">
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<span className="inline-flex ml-3 rounded-md shadow-sm">
|
<span className="inline-flex ml-3 rounded-md shadow-sm">
|
||||||
|
@@ -403,6 +403,7 @@
|
|||||||
"components.Settings.menuNotifications": "Notifications",
|
"components.Settings.menuNotifications": "Notifications",
|
||||||
"components.Settings.menuPlexSettings": "Plex",
|
"components.Settings.menuPlexSettings": "Plex",
|
||||||
"components.Settings.menuServices": "Services",
|
"components.Settings.menuServices": "Services",
|
||||||
|
"components.Settings.ms": "ms",
|
||||||
"components.Settings.nextexecution": "Next Execution",
|
"components.Settings.nextexecution": "Next Execution",
|
||||||
"components.Settings.nodefault": "No default server selected!",
|
"components.Settings.nodefault": "No default server selected!",
|
||||||
"components.Settings.nodefaultdescription": "At least one server must be marked as default before any requests will make it to your services.",
|
"components.Settings.nodefaultdescription": "At least one server must be marked as default before any requests will make it to your services.",
|
||||||
@@ -423,16 +424,33 @@
|
|||||||
"components.Settings.runnow": "Run Now",
|
"components.Settings.runnow": "Run Now",
|
||||||
"components.Settings.save": "Save Changes",
|
"components.Settings.save": "Save Changes",
|
||||||
"components.Settings.saving": "Saving…",
|
"components.Settings.saving": "Saving…",
|
||||||
"components.Settings.servername": "Server Name (Automatically set after you save)",
|
"components.Settings.serverConnected": "connected",
|
||||||
|
"components.Settings.serverLocal": "local",
|
||||||
|
"components.Settings.serverRemote": "remote",
|
||||||
|
"components.Settings.servername": "Server Name (Retrieved from Plex)",
|
||||||
"components.Settings.servernamePlaceholder": "Plex Server Name",
|
"components.Settings.servernamePlaceholder": "Plex Server Name",
|
||||||
|
"components.Settings.serverpreset": "Available Server",
|
||||||
|
"components.Settings.serverpresetLoad": "Press button to load available servers",
|
||||||
|
"components.Settings.serverpresetManualMessage": "Manually configure",
|
||||||
|
"components.Settings.serverpresetPlaceholder": "Plex server (retrieved automatically)",
|
||||||
|
"components.Settings.serverpresetRefreshing": "Retrieving servers...",
|
||||||
|
"components.Settings.settingUpPlex": "Setting up Plex",
|
||||||
|
"components.Settings.settingUpPlexDescription": "To setup Plex you can enter your details manually, or choose from one of your available servers retrieved from <RegisterPlexTVLink>plex.tv</RegisterPlexTVLink>. Press the button next to the dropdown to refresh the list and recheck server connectivity.",
|
||||||
"components.Settings.sonarrSettingsDescription": "Set up your Sonarr connection below. You can have multiple, but only two active as defaults at any time (one for standard HD and one for 4K). Administrators can override which server is used for new requests.",
|
"components.Settings.sonarrSettingsDescription": "Set up your Sonarr connection below. You can have multiple, but only two active as defaults at any time (one for standard HD and one for 4K). Administrators can override which server is used for new requests.",
|
||||||
"components.Settings.sonarrsettings": "Sonarr Settings",
|
"components.Settings.sonarrsettings": "Sonarr Settings",
|
||||||
"components.Settings.ssl": "SSL",
|
"components.Settings.ssl": "SSL",
|
||||||
"components.Settings.startscan": "Start Scan",
|
"components.Settings.startscan": "Start Scan",
|
||||||
"components.Settings.sync": "Sync Plex Libraries",
|
"components.Settings.sync": "Sync Plex Libraries",
|
||||||
"components.Settings.syncing": "Syncing…",
|
"components.Settings.syncing": "Syncing…",
|
||||||
|
"components.Settings.timeout": "Timeout",
|
||||||
"components.Settings.toastApiKeyFailure": "Something went wrong generating a new API Key.",
|
"components.Settings.toastApiKeyFailure": "Something went wrong generating a new API Key.",
|
||||||
"components.Settings.toastApiKeySuccess": "New API key generated!",
|
"components.Settings.toastApiKeySuccess": "New API Key generated!",
|
||||||
|
"components.Settings.toastPlexConnecting": "Attempting to connect to Plex server",
|
||||||
|
"components.Settings.toastPlexConnectingFailure": "Unable to connect to Plex server",
|
||||||
|
"components.Settings.toastPlexConnectingSuccess": "Connected to Plex server",
|
||||||
|
"components.Settings.toastPlexRefresh": "Retrieving server list from Plex",
|
||||||
|
"components.Settings.toastPlexRefreshFailure": "Unable to retrieve server list from Plex",
|
||||||
|
"components.Settings.toastPlexRefreshSuccess": "Retrieved server list from Plex",
|
||||||
"components.Settings.toastSettingsFailure": "Something went wrong saving settings.",
|
"components.Settings.toastSettingsFailure": "Something went wrong saving settings.",
|
||||||
"components.Settings.toastSettingsSuccess": "Settings saved.",
|
"components.Settings.toastSettingsSuccess": "Settings saved.",
|
||||||
"components.Settings.validationHostnameRequired": "You must provide a hostname/IP.",
|
"components.Settings.validationHostnameRequired": "You must provide a hostname/IP.",
|
||||||
|
Reference in New Issue
Block a user