feat(plex): add support for custom Plex Web App URLs (#1581)

* feat(plex): add support for custom Plex Web App URLs

* refactor: clean up Yup validation in *arr modals & email settings

* fix(lang): change Web App URL tip

* fix: remove web app URL validation and add 'Advanced' badge
This commit is contained in:
TheCatLady
2021-05-06 04:40:22 -04:00
committed by GitHub
parent 93c441ef66
commit a640a91390
8 changed files with 90 additions and 84 deletions

View File

@@ -171,6 +171,9 @@ components:
readOnly: true readOnly: true
items: items:
$ref: '#/components/schemas/PlexLibrary' $ref: '#/components/schemas/PlexLibrary'
webAppUrl:
type: string
example: 'https://app.plex.tv/desktop'
required: required:
- name - name
- machineId - machineId

View File

@@ -147,12 +147,22 @@ class Media {
@AfterLoad() @AfterLoad()
public setPlexUrls(): void { public setPlexUrls(): void {
const machineId = getSettings().plex.machineId; const { machineId, webAppUrl } = getSettings().plex;
if (this.ratingKey) { if (this.ratingKey) {
this.plexUrl = `https://app.plex.tv/desktop#!/server/${machineId}/details?key=%2Flibrary%2Fmetadata%2F${this.ratingKey}`; this.plexUrl = `${
webAppUrl ? webAppUrl : 'https://app.plex.tv/desktop'
}#!/server/${machineId}/details?key=%2Flibrary%2Fmetadata%2F${
this.ratingKey
}`;
} }
if (this.ratingKey4k) { if (this.ratingKey4k) {
this.plexUrl4k = `https://app.plex.tv/desktop#!/server/${machineId}/details?key=%2Flibrary%2Fmetadata%2F${this.ratingKey4k}`; this.plexUrl4k = `${
webAppUrl ? webAppUrl : 'https://app.plex.tv/desktop'
}#!/server/${machineId}/details?key=%2Flibrary%2Fmetadata%2F${
this.ratingKey4k
}`;
} }
} }

View File

@@ -30,6 +30,7 @@ export interface PlexSettings {
port: number; port: number;
useSsl?: boolean; useSsl?: boolean;
libraries: Library[]; libraries: Library[];
webAppUrl?: string;
} }
export interface DVRSettings { export interface DVRSettings {

View File

@@ -92,15 +92,13 @@ const NotificationsEmail: React.FC = () => {
/^(([a-z]|\d|_|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*)?([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])$/i, /^(([a-z]|\d|_|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*)?([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])$/i,
intl.formatMessage(messages.validationSmtpHostRequired) intl.formatMessage(messages.validationSmtpHostRequired)
), ),
smtpPort: Yup.number() smtpPort: Yup.number().when('enabled', {
.typeError(intl.formatMessage(messages.validationSmtpPortRequired)) is: true,
.when('enabled', { then: Yup.number()
is: true, .nullable()
then: Yup.number().required( .required(intl.formatMessage(messages.validationSmtpPortRequired)),
intl.formatMessage(messages.validationSmtpPortRequired) otherwise: Yup.number().nullable(),
), }),
otherwise: Yup.number().nullable(),
}),
pgpPrivateKey: Yup.string() pgpPrivateKey: Yup.string()
.when('pgpPassword', { .when('pgpPassword', {
is: (value: unknown) => !!value, is: (value: unknown) => !!value,

View File

@@ -41,7 +41,7 @@ const messages = defineMessages({
servername: 'Server Name', servername: 'Server Name',
hostname: 'Hostname or IP Address', hostname: 'Hostname or IP Address',
port: 'Port', port: 'Port',
ssl: 'Enable SSL', ssl: 'Use SSL',
apiKey: 'API Key', apiKey: 'API Key',
baseUrl: 'URL Base', baseUrl: 'URL Base',
syncEnabled: 'Enable Scan', syncEnabled: 'Enable Scan',
@@ -116,7 +116,7 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
intl.formatMessage(messages.validationHostnameRequired) intl.formatMessage(messages.validationHostnameRequired)
), ),
port: Yup.number() port: Yup.number()
.typeError(intl.formatMessage(messages.validationPortRequired)) .nullable()
.required(intl.formatMessage(messages.validationPortRequired)), .required(intl.formatMessage(messages.validationPortRequired)),
apiKey: Yup.string().required( apiKey: Yup.string().required(
intl.formatMessage(messages.validationApiKeyRequired) intl.formatMessage(messages.validationApiKeyRequired)
@@ -135,33 +135,18 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
.test( .test(
'no-trailing-slash', 'no-trailing-slash',
intl.formatMessage(messages.validationApplicationUrlTrailingSlash), intl.formatMessage(messages.validationApplicationUrlTrailingSlash),
(value) => { (value) => !value || !value.endsWith('/')
if (value?.substr(value.length - 1) === '/') {
return false;
}
return true;
}
), ),
baseUrl: Yup.string() baseUrl: Yup.string()
.test( .test(
'leading-slash', 'leading-slash',
intl.formatMessage(messages.validationBaseUrlLeadingSlash), intl.formatMessage(messages.validationBaseUrlLeadingSlash),
(value) => { (value) => !value || value.startsWith('/')
if (value && value?.substr(0, 1) !== '/') {
return false;
}
return true;
}
) )
.test( .test(
'no-trailing-slash', 'no-trailing-slash',
intl.formatMessage(messages.validationBaseUrlTrailingSlash), intl.formatMessage(messages.validationBaseUrlTrailingSlash),
(value) => { (value) => !value || !value.endsWith('/')
if (value?.substr(value.length - 1) === '/') {
return false;
}
return true;
}
), ),
}); });

View File

@@ -22,8 +22,6 @@ const messages = defineMessages({
plexsettings: 'Plex Settings', plexsettings: 'Plex Settings',
plexsettingsDescription: plexsettingsDescription:
'Configure the settings for your Plex server. Overseerr scans your Plex libraries to determine content availability.', 'Configure the settings for your Plex server. Overseerr scans your Plex libraries to determine content availability.',
servername: 'Server Name',
servernameTip: 'Automatically retrieved from Plex after saving',
serverpreset: 'Server', serverpreset: 'Server',
serverLocal: 'local', serverLocal: 'local',
serverRemote: 'remote', serverRemote: 'remote',
@@ -41,7 +39,7 @@ const messages = defineMessages({
'To set up Plex, you can either enter the details manually or select a server retrieved from <RegisterPlexTVLink>plex.tv</RegisterPlexTVLink>. Press the button to the right of the dropdown to fetch the list of available servers.', 'To set up Plex, you can either enter the details manually or select a server retrieved from <RegisterPlexTVLink>plex.tv</RegisterPlexTVLink>. Press the button to the right of the dropdown to fetch the list of available servers.',
hostname: 'Hostname or IP Address', hostname: 'Hostname or IP Address',
port: 'Port', port: 'Port',
enablessl: 'Enable SSL', enablessl: 'Use SSL',
plexlibraries: 'Plex Libraries', plexlibraries: 'Plex Libraries',
plexlibrariesDescription: plexlibrariesDescription:
'The libraries Overseerr scans for titles. Set up and save your Plex connection settings, then click the button below if no libraries are listed.', 'The libraries Overseerr scans for titles. Set up and save your Plex connection settings, then click the button below if no libraries are listed.',
@@ -57,6 +55,10 @@ const messages = defineMessages({
cancelscan: 'Cancel Scan', cancelscan: 'Cancel Scan',
validationHostnameRequired: 'You must provide a valid hostname or IP address', validationHostnameRequired: 'You must provide a valid hostname or IP address',
validationPortRequired: 'You must provide a valid port number', validationPortRequired: 'You must provide a valid port number',
webAppUrl: '<WebAppLink>Web App</WebAppLink> URL',
webAppUrlTip:
'Optionally direct users to the web app on your server instead of the "hosted" web app',
validationWebAppUrl: 'You must provide a valid Plex Web App URL',
}); });
interface Library { interface Library {
@@ -108,14 +110,18 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
const { addToast, removeToast } = useToasts(); const { addToast, removeToast } = useToasts();
const PlexSettingsSchema = Yup.object().shape({ const PlexSettingsSchema = Yup.object().shape({
hostname: Yup.string() hostname: Yup.string()
.nullable()
.required(intl.formatMessage(messages.validationHostnameRequired)) .required(intl.formatMessage(messages.validationHostnameRequired))
.matches( .matches(
/^(([a-z]|\d|_|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*)?([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])$/i, /^(([a-z]|\d|_|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*)?([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])$/i,
intl.formatMessage(messages.validationHostnameRequired) intl.formatMessage(messages.validationHostnameRequired)
), ),
port: Yup.number() port: Yup.number()
.typeError(intl.formatMessage(messages.validationPortRequired)) .nullable()
.required(intl.formatMessage(messages.validationPortRequired)), .required(intl.formatMessage(messages.validationPortRequired)),
webAppUrl: Yup.string()
.nullable()
.url(intl.formatMessage(messages.validationWebAppUrl)),
}); });
const activeLibraries = const activeLibraries =
@@ -282,6 +288,7 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
port: data?.port ?? 32400, port: data?.port ?? 32400,
useSsl: data?.useSsl, useSsl: data?.useSsl,
selectedPreset: undefined, selectedPreset: undefined,
webAppUrl: data?.webAppUrl,
}} }}
validationSchema={PlexSettingsSchema} validationSchema={PlexSettingsSchema}
onSubmit={async (values) => { onSubmit={async (values) => {
@@ -301,6 +308,7 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
ip: values.hostname, ip: values.hostname,
port: Number(values.port), port: Number(values.port),
useSsl: values.useSsl, useSsl: values.useSsl,
webAppUrl: values.webAppUrl,
} as PlexSettings); } as PlexSettings);
revalidate(); revalidate();
@@ -336,34 +344,12 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
}) => { }) => {
return ( return (
<form className="section" onSubmit={handleSubmit}> <form className="section" onSubmit={handleSubmit}>
<div className="form-row">
<label htmlFor="name" className="text-label">
<div className="flex flex-col">
<span>{intl.formatMessage(messages.servername)}</span>
<span className="text-gray-500">
{intl.formatMessage(messages.servernameTip)}
</span>
</div>
</label>
<div className="form-input">
<div className="form-input-field">
<input
type="text"
id="name"
name="name"
className="cursor-not-allowed"
value={data?.name}
readOnly
/>
</div>
</div>
</div>
<div className="form-row"> <div className="form-row">
<label htmlFor="preset" className="text-label"> <label htmlFor="preset" className="text-label">
{intl.formatMessage(messages.serverpreset)} {intl.formatMessage(messages.serverpreset)}
</label> </label>
<div className="form-input"> <div className="form-input">
<div className="form-input-field input-group"> <div className="form-input-field">
<select <select
id="preset" id="preset"
name="preset" name="preset"
@@ -489,6 +475,43 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
/> />
</div> </div>
</div> </div>
<div className="form-row">
<label htmlFor="webAppUrl" className="text-label">
{intl.formatMessage(messages.webAppUrl, {
WebAppLink: function WebAppLink(msg) {
return (
<a
href="https://support.plex.tv/articles/200288666-opening-plex-web-app/"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
);
},
})}
<Badge badgeType="danger" className="ml-2">
{intl.formatMessage(globalMessages.advanced)}
</Badge>
<span className="label-tip">
{intl.formatMessage(messages.webAppUrlTip)}
</span>
</label>
<div className="form-input">
<div className="form-input-field">
<Field
type="text"
inputMode="url"
id="webAppUrl"
name="webAppUrl"
placeholder="https://app.plex.tv/desktop"
/>
</div>
{errors.webAppUrl && touched.webAppUrl && (
<div className="error">{errors.webAppUrl}</div>
)}
</div>
</div>
<div className="actions"> <div className="actions">
<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">

View File

@@ -40,7 +40,7 @@ const messages = defineMessages({
servername: 'Server Name', servername: 'Server Name',
hostname: 'Hostname or IP Address', hostname: 'Hostname or IP Address',
port: 'Port', port: 'Port',
ssl: 'Enable SSL', ssl: 'Use SSL',
apiKey: 'API Key', apiKey: 'API Key',
baseUrl: 'URL Base', baseUrl: 'URL Base',
qualityprofile: 'Quality Profile', qualityprofile: 'Quality Profile',
@@ -127,7 +127,7 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
intl.formatMessage(messages.validationHostnameRequired) intl.formatMessage(messages.validationHostnameRequired)
), ),
port: Yup.number() port: Yup.number()
.typeError(intl.formatMessage(messages.validationPortRequired)) .nullable()
.required(intl.formatMessage(messages.validationPortRequired)), .required(intl.formatMessage(messages.validationPortRequired)),
apiKey: Yup.string().required( apiKey: Yup.string().required(
intl.formatMessage(messages.validationApiKeyRequired) intl.formatMessage(messages.validationApiKeyRequired)
@@ -146,33 +146,18 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
.test( .test(
'no-trailing-slash', 'no-trailing-slash',
intl.formatMessage(messages.validationApplicationUrlTrailingSlash), intl.formatMessage(messages.validationApplicationUrlTrailingSlash),
(value) => { (value) => !value || !value.endsWith('/')
if (value?.substr(value.length - 1) === '/') {
return false;
}
return true;
}
), ),
baseUrl: Yup.string() baseUrl: Yup.string()
.test( .test(
'leading-slash', 'leading-slash',
intl.formatMessage(messages.validationBaseUrlLeadingSlash), intl.formatMessage(messages.validationBaseUrlLeadingSlash),
(value) => { (value) => !value || value.startsWith('/')
if (value && value?.substr(0, 1) !== '/') {
return false;
}
return true;
}
) )
.test( .test(
'no-trailing-slash', 'no-trailing-slash',
intl.formatMessage(messages.validationBaseUrlTrailingSlash), intl.formatMessage(messages.validationBaseUrlTrailingSlash),
(value) => { (value) => !value || !value.endsWith('/')
if (value?.substr(value.length - 1) === '/') {
return false;
}
return true;
}
), ),
}); });

View File

@@ -389,7 +389,7 @@
"components.Settings.RadarrModal.selecttags": "Select tags", "components.Settings.RadarrModal.selecttags": "Select tags",
"components.Settings.RadarrModal.server4k": "4K Server", "components.Settings.RadarrModal.server4k": "4K Server",
"components.Settings.RadarrModal.servername": "Server Name", "components.Settings.RadarrModal.servername": "Server Name",
"components.Settings.RadarrModal.ssl": "Enable SSL", "components.Settings.RadarrModal.ssl": "Use SSL",
"components.Settings.RadarrModal.syncEnabled": "Enable Scan", "components.Settings.RadarrModal.syncEnabled": "Enable Scan",
"components.Settings.RadarrModal.tags": "Tags", "components.Settings.RadarrModal.tags": "Tags",
"components.Settings.RadarrModal.testFirstQualityProfiles": "Test connection to load quality profiles", "components.Settings.RadarrModal.testFirstQualityProfiles": "Test connection to load quality profiles",
@@ -519,7 +519,7 @@
"components.Settings.SonarrModal.selecttags": "Select tags", "components.Settings.SonarrModal.selecttags": "Select tags",
"components.Settings.SonarrModal.server4k": "4K Server", "components.Settings.SonarrModal.server4k": "4K Server",
"components.Settings.SonarrModal.servername": "Server Name", "components.Settings.SonarrModal.servername": "Server Name",
"components.Settings.SonarrModal.ssl": "Enable SSL", "components.Settings.SonarrModal.ssl": "Use SSL",
"components.Settings.SonarrModal.syncEnabled": "Enable Scan", "components.Settings.SonarrModal.syncEnabled": "Enable Scan",
"components.Settings.SonarrModal.tags": "Tags", "components.Settings.SonarrModal.tags": "Tags",
"components.Settings.SonarrModal.testFirstLanguageProfiles": "Test connection to load language profiles", "components.Settings.SonarrModal.testFirstLanguageProfiles": "Test connection to load language profiles",
@@ -558,7 +558,7 @@
"components.Settings.default4k": "Default 4K", "components.Settings.default4k": "Default 4K",
"components.Settings.deleteserverconfirm": "Are you sure you want to delete this server?", "components.Settings.deleteserverconfirm": "Are you sure you want to delete this server?",
"components.Settings.email": "Email", "components.Settings.email": "Email",
"components.Settings.enablessl": "Enable SSL", "components.Settings.enablessl": "Use SSL",
"components.Settings.general": "General", "components.Settings.general": "General",
"components.Settings.generalsettings": "General Settings", "components.Settings.generalsettings": "General Settings",
"components.Settings.generalsettingsDescription": "Configure global and default settings for Overseerr.", "components.Settings.generalsettingsDescription": "Configure global and default settings for Overseerr.",
@@ -603,8 +603,6 @@
"components.Settings.serverLocal": "local", "components.Settings.serverLocal": "local",
"components.Settings.serverRemote": "remote", "components.Settings.serverRemote": "remote",
"components.Settings.serverSecure": "secure", "components.Settings.serverSecure": "secure",
"components.Settings.servername": "Server Name",
"components.Settings.servernameTip": "Automatically retrieved from Plex after saving",
"components.Settings.serverpreset": "Server", "components.Settings.serverpreset": "Server",
"components.Settings.serverpresetLoad": "Press the button to load available servers", "components.Settings.serverpresetLoad": "Press the button to load available servers",
"components.Settings.serverpresetManualMessage": "Manual configuration", "components.Settings.serverpresetManualMessage": "Manual configuration",
@@ -632,6 +630,9 @@
"components.Settings.validationApplicationUrlTrailingSlash": "URL must not end in a trailing slash", "components.Settings.validationApplicationUrlTrailingSlash": "URL must not end in a trailing slash",
"components.Settings.validationHostnameRequired": "You must provide a valid hostname or IP address", "components.Settings.validationHostnameRequired": "You must provide a valid hostname or IP address",
"components.Settings.validationPortRequired": "You must provide a valid port number", "components.Settings.validationPortRequired": "You must provide a valid port number",
"components.Settings.validationWebAppUrl": "You must provide a valid Plex Web App URL",
"components.Settings.webAppUrl": "<WebAppLink>Web App</WebAppLink> URL",
"components.Settings.webAppUrlTip": "Optionally direct users to the web app on your server instead of the \"hosted\" web app",
"components.Settings.webhook": "Webhook", "components.Settings.webhook": "Webhook",
"components.Settings.webpush": "Web Push", "components.Settings.webpush": "Web Push",
"components.Setup.configureplex": "Configure Plex", "components.Setup.configureplex": "Configure Plex",