feat: pre-populate server info from plex.tv API (#563)

Co-authored-by: sct <sctsnipe@gmail.com>
pull/741/head
Chris Pritchard 3 years ago committed by GitHub
parent b3313a7f4d
commit 82ac76b054
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -32,6 +32,7 @@ module.exports = {
'formatjs/no-offset': 'error',
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': ['error'],
'jsx-a11y/no-onchange': 'off',
},
overrides: [
{

3
.gitignore vendored

@ -47,3 +47,6 @@ dist
# sqlite journal
config/db/db.sqlite3-journal
# VS Code
.vscode/launch.json

@ -113,6 +113,140 @@ components:
- machineId
- ip
- 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:
type: object
properties:
@ -1460,6 +1594,21 @@ paths:
type: array
items:
$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:
get:
summary: Get all radarr settings

@ -1,5 +1,5 @@
import NodePlexAPI from 'plex-api';
import { getSettings } from '../lib/settings';
import { getSettings, PlexSettings } from '../lib/settings';
export interface PlexLibraryItem {
ratingKey: string;
@ -80,13 +80,26 @@ interface PlexMetadataResponse {
class PlexAPI {
private plexClient: NodePlexAPI;
constructor({ plexToken }: { plexToken?: string }) {
constructor({
plexToken,
plexSettings,
timeout,
}: {
plexToken?: string;
plexSettings?: PlexSettings;
timeout?: number;
}) {
const settings = getSettings();
let settingsPlex: PlexSettings | undefined;
plexSettings
? (settingsPlex = plexSettings)
: (settingsPlex = getSettings().plex);
this.plexClient = new NodePlexAPI({
hostname: settings.plex.ip,
port: settings.plex.port,
https: settings.plex.useSsl,
hostname: settingsPlex.ip,
port: settingsPlex.port,
https: settingsPlex.useSsl,
timeout: timeout,
token: plexToken,
authenticator: {
authenticate: (
@ -111,6 +124,7 @@ class PlexAPI {
});
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
public async getStatus() {
return await this.plexClient.query('/');
}

@ -1,5 +1,6 @@
import axios, { AxiosInstance } from 'axios';
import xml2js from 'xml2js';
import { PlexDevice } from '../interfaces/api/plexInterfaces';
import { getSettings } from '../lib/settings';
import logger from '../logger';
@ -29,6 +30,45 @@ interface PlexUser {
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 {
$: {
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> {
try {
const account = await this.axios.get<PlexAccountResponse>(

@ -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 { User } from '../../entity/User';
import PlexAPI from '../../api/plexapi';
import PlexTvAPI from '../../api/plextv';
import { jobPlexFullSync } from '../../job/plexsync';
import SonarrAPI from '../../api/sonarr';
import RadarrAPI from '../../api/radarr';
@ -106,6 +107,69 @@ settingsRoutes.post('/plex', async (req, res, next) => {
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) => {
const settings = getSettings();
@ -156,7 +220,6 @@ settingsRoutes.get('/plex/sync', (req, res) => {
} else if (req.query.start) {
jobPlexFullSync.run();
}
return res.status(200).json(jobPlexFullSync.status());
});

@ -5,6 +5,7 @@ declare module 'plex-api' {
port: number;
token?: string;
https?: boolean;
timeout?: number;
authenticator: {
authenticate: (
_plexApi: PlexAPI,
@ -19,7 +20,7 @@ declare module 'plex-api' {
};
requestOptions?: Record<string, string | number>;
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
query: <T extends Record<string, any>>(endpoint: string) => Promise<T>;
}
}

@ -2,7 +2,7 @@ import React from 'react';
interface AlertProps {
title: string;
type?: 'warning' | 'info';
type?: 'warning' | 'info' | 'error';
}
const Alert: React.FC<AlertProps> = ({ title, children, type }) => {
@ -51,6 +51,29 @@ const Alert: React.FC<AlertProps> = ({ title, children, type }) => {
),
};
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 (

@ -1,7 +1,9 @@
import React, { useState } from 'react';
import React, { useMemo, useState } from 'react';
import LoadingSpinner from '../Common/LoadingSpinner';
import type { PlexSettings } from '../../../server/lib/settings';
import type { PlexDevice } from '../../../server/interfaces/api/plexInterfaces';
import useSWR from 'swr';
import { useToasts } from 'react-toast-notifications';
import { Formik, Field } from 'formik';
import Button from '../Common/Button';
import axios from 'axios';
@ -9,16 +11,38 @@ import LibraryItem from './LibraryItem';
import Badge from '../Common/Badge';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import * as Yup from 'yup';
import Alert from '../Common/Alert';
const messages = defineMessages({
plexsettings: 'Plex Settings',
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.',
servername: 'Server Name (Automatically set after you save)',
servername: 'Server Name (Retrieved from Plex)',
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',
port: 'Port',
ssl: 'SSL',
timeout: 'Timeout',
ms: 'ms',
save: 'Save Changes',
saving: 'Saving...',
plexlibraries: 'Plex Libraries',
@ -52,24 +76,41 @@ interface SyncStatus {
libraries: Library[];
}
interface PresetServerDisplay {
name: string;
ssl: boolean;
uri: string;
address: string;
host?: string;
port: number;
local: boolean;
status?: boolean;
message?: string;
}
interface SettingsPlexProps {
onComplete?: () => void;
}
const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
const intl = useIntl();
const { data, error, revalidate } = useSWR<PlexSettings>(
'/api/v1/settings/plex'
const [isSyncing, setIsSyncing] = useState(false);
const [isRefreshingPresets, setIsRefreshingPresets] = useState(false);
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>(
'/api/v1/settings/plex/sync',
{
refreshInterval: 1000,
}
);
const [isSyncing, setIsSyncing] = useState(false);
const [submitError, setSubmitError] = useState<string | null>(null);
const intl = useIntl();
const { addToast, removeToast } = useToasts();
const PlexSettingsSchema = Yup.object().shape({
hostname: Yup.string().required(
intl.formatMessage(messages.validationHostnameRequired)
@ -84,6 +125,33 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
.filter((library) => library.enabled)
.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 () => {
setIsSyncing(true);
@ -102,6 +170,46 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
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 () => {
await axios.get('/api/v1/settings/plex/sync', {
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">
<FormattedMessage {...messages.plexsettingsDescription} />
</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>
<Formik
initialValues={{
hostname: data?.ip,
port: data?.port,
useSsl: data?.useSsl,
selectedPreset: undefined,
}}
enableReinitialize
validationSchema={PlexSettingsSchema}
onSubmit={async (values) => {
setSubmitError(null);
let toastId: string | null = null;
try {
addToast(
intl.formatMessage(messages.toastPlexConnecting),
{
autoDismiss: false,
appearance: 'info',
},
(id) => {
toastId = id;
}
);
await axios.post('/api/v1/settings/plex', {
ip: values.hostname,
port: Number(values.port),
@ -176,10 +312,25 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
} as PlexSettings);
revalidate();
setSubmitError(null);
if (toastId) {
removeToast(toastId);
}
addToast(intl.formatMessage(messages.toastPlexConnectingSuccess), {
autoDismiss: true,
appearance: 'success',
});
if (onComplete) {
onComplete();
}
} catch (e) {
if (toastId) {
removeToast(toastId);
}
addToast(intl.formatMessage(messages.toastPlexConnectingFailure), {
autoDismiss: true,
appearance: 'error',
});
setSubmitError(e.response.data.message);
}
}}
@ -190,16 +341,12 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
values,
handleSubmit,
setFieldValue,
setFieldTouched,
isSubmitting,
}) => {
return (
<form onSubmit={handleSubmit}>
<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">
<label
htmlFor="name"
@ -225,71 +372,176 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
</div>
<div className="mt-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800">
<label
htmlFor="hostname"
htmlFor="preset"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
<FormattedMessage {...messages.hostname} />
<FormattedMessage {...messages.serverpreset} />
</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 className="flex max-w-lg rounded-md shadow-sm input-group">
<select
id="preset"
name="preset"
placeholder={intl.formatMessage(
messages.serverpresetPlaceholder
)}
value={values.selectedPreset}
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 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>
{errors.hostname && touched.hostname && (
<div className="mt-2 text-red-500">{errors.hostname}</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">
<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">
<label
htmlFor="hostname"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
<FormattedMessage {...messages.hostname} />
</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
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"
type="checkbox"
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>
{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
type="checkbox"
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"
/>
{submitError && (
<div className="mt-6 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200">
<Alert
title={intl.formatMessage(
messages.toastPlexConnectingFailure
)}
type="error"
>
{submitError}
</Alert>
</div>
</div>
)}
<div className="pt-5 mt-8 border-t border-gray-700">
<div className="flex justify-end">
<span className="inline-flex ml-3 rounded-md shadow-sm">

@ -403,6 +403,7 @@
"components.Settings.menuNotifications": "Notifications",
"components.Settings.menuPlexSettings": "Plex",
"components.Settings.menuServices": "Services",
"components.Settings.ms": "ms",
"components.Settings.nextexecution": "Next Execution",
"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.",
@ -423,16 +424,33 @@
"components.Settings.runnow": "Run Now",
"components.Settings.save": "Save Changes",
"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.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.sonarrsettings": "Sonarr Settings",
"components.Settings.ssl": "SSL",
"components.Settings.startscan": "Start Scan",
"components.Settings.sync": "Sync Plex Libraries",
"components.Settings.syncing": "Syncing…",
"components.Settings.timeout": "Timeout",
"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.toastSettingsSuccess": "Settings saved.",
"components.Settings.validationHostnameRequired": "You must provide a hostname/IP.",

Loading…
Cancel
Save