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
pull/1584/head
TheCatLady 3 years ago committed by GitHub
parent 93c441ef66
commit a640a91390
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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

@ -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
}`;
} }
} }

@ -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 {

@ -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,

@ -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;
}
), ),
}); });

@ -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">

@ -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;
}
), ),
}); });

@ -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",

Loading…
Cancel
Save