import { RefreshIcon, SearchIcon, XIcon } from '@heroicons/react/solid'; import axios from 'axios'; import { Field, Formik } from 'formik'; import { orderBy } from 'lodash'; import React, { useMemo, useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import { useToasts } from 'react-toast-notifications'; import useSWR from 'swr'; import * as Yup from 'yup'; import type { PlexDevice } from '../../../server/interfaces/api/plexInterfaces'; import type { PlexSettings } from '../../../server/lib/settings'; import globalMessages from '../../i18n/globalMessages'; import Alert from '../Common/Alert'; import Badge from '../Common/Badge'; import Button from '../Common/Button'; import LoadingSpinner from '../Common/LoadingSpinner'; import PageTitle from '../Common/PageTitle'; import LibraryItem from './LibraryItem'; const messages = defineMessages({ plex: 'Plex', plexsettings: 'Plex Settings', plexsettingsDescription: 'Configure the settings for your Plex server. Overseerr scans your Plex libraries to determine content availability.', serverpreset: 'Server', serverLocal: 'local', serverRemote: 'remote', serverSecure: 'secure', serverpresetManualMessage: 'Manual configuration', serverpresetRefreshing: 'Retrieving servers…', serverpresetLoad: 'Press the button to load available servers', toastPlexRefresh: 'Retrieving server list from Plex…', toastPlexRefreshSuccess: 'Plex server list retrieved successfully!', toastPlexRefreshFailure: 'Failed to retrieve Plex server list.', toastPlexConnecting: 'Attempting to connect to Plex…', toastPlexConnectingSuccess: 'Plex connection established successfully!', toastPlexConnectingFailure: 'Failed to connect to Plex.', settingUpPlexDescription: 'To set up Plex, you can either enter the details manually or select a server retrieved from plex.tv. Press the button to the right of the dropdown to fetch the list of available servers.', hostname: 'Hostname or IP Address', port: 'Port', enablessl: 'Use SSL', plexlibraries: 'Plex Libraries', 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.', scanning: 'Syncing…', scan: 'Sync Libraries', manualscan: 'Manual Library Scan', manualscanDescription: "Normally, this will only be run once every 24 hours. Overseerr will check your Plex server's recently added more aggressively. If this is your first time configuring Plex, a one-time full manual library scan is recommended!", notrunning: 'Not Running', currentlibrary: 'Current Library: {name}', librariesRemaining: 'Libraries Remaining: {count}', startscan: 'Start Scan', cancelscan: 'Cancel Scan', validationHostnameRequired: 'You must provide a valid hostname or IP address', validationPortRequired: 'You must provide a valid port number', webAppUrl: 'Web App 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 { id: string; name: string; enabled: boolean; } interface SyncStatus { running: boolean; progress: number; total: number; currentLibrary?: Library; libraries: Library[]; } interface PresetServerDisplay { name: string; ssl: boolean; uri: string; address: string; port: number; local: boolean; status?: boolean; message?: string; } interface SettingsPlexProps { onComplete?: () => void; } const SettingsPlex: React.FC = ({ onComplete }) => { const [isSyncing, setIsSyncing] = useState(false); const [isRefreshingPresets, setIsRefreshingPresets] = useState(false); const [availableServers, setAvailableServers] = useState(null); const { data: data, error: error, revalidate: revalidate, } = useSWR('/api/v1/settings/plex'); const { data: dataSync, revalidate: revalidateSync } = useSWR( '/api/v1/settings/plex/sync', { refreshInterval: 1000, } ); const intl = useIntl(); const { addToast, removeToast } = useToasts(); const PlexSettingsSchema = Yup.object().shape({ hostname: Yup.string() .nullable() .required(intl.formatMessage(messages.validationHostnameRequired)) .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, intl.formatMessage(messages.validationHostnameRequired) ), port: Yup.number() .nullable() .required(intl.formatMessage(messages.validationPortRequired)), webAppUrl: Yup.string() .nullable() .url(intl.formatMessage(messages.validationWebAppUrl)), }); const activeLibraries = data?.libraries .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', uri: conn.uri, address: conn.address, port: conn.port, local: conn.local, status: conn.status === 200, message: conn.message, }) ); }); return orderBy(finalPresets, ['status', 'ssl'], ['desc', 'desc']); }, [availableServers]); const syncLibraries = async () => { setIsSyncing(true); const params: { sync: boolean; enable?: string } = { sync: true, }; if (activeLibraries.length > 0) { params.enable = activeLibraries.join(','); } await axios.get('/api/v1/settings/plex/library', { params, }); setIsSyncing(false); 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( '/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.post('/api/v1/settings/plex/sync', { start: true, }); revalidateSync(); }; const cancelScan = async () => { await axios.post('/api/v1/settings/plex/sync', { cancel: true, }); revalidateSync(); }; const toggleLibrary = async (libraryId: string) => { setIsSyncing(true); if (activeLibraries.includes(libraryId)) { const params: { enable?: string } = {}; if (activeLibraries.length > 1) { params.enable = activeLibraries .filter((id) => id !== libraryId) .join(','); } await axios.get('/api/v1/settings/plex/library', { params, }); } else { await axios.get('/api/v1/settings/plex/library', { params: { enable: [...activeLibraries, libraryId].join(','), }, }); } setIsSyncing(false); revalidate(); }; if (!data && !error) { return ; } return ( <>

{intl.formatMessage(messages.plexsettings)}

{intl.formatMessage(messages.plexsettingsDescription)}

{!!onComplete && (
{msg} ); }, })} type="info" />
)}
{ 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), useSsl: values.useSsl, webAppUrl: values.webAppUrl, } as PlexSettings); syncLibraries(); 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', }); } }} > {({ errors, touched, values, handleSubmit, setFieldValue, isSubmitting, }) => { return (
{values.useSsl ? 'https://' : 'http://'}
{errors.hostname && touched.hostname && (
{errors.hostname}
)}
{errors.port && touched.port && (
{errors.port}
)}
{ setFieldValue('useSsl', !values.useSsl); }} />
{errors.webAppUrl && touched.webAppUrl && (
{errors.webAppUrl}
)}
); }}

{intl.formatMessage(messages.plexlibraries)}

{intl.formatMessage(messages.plexlibrariesDescription)}

    {data?.libraries.map((library) => ( toggleLibrary(library.id)} /> ))}

{intl.formatMessage(messages.manualscan)}

{intl.formatMessage(messages.manualscanDescription)}

{dataSync?.running && (
)}
{dataSync?.running ? `${dataSync.progress} of ${dataSync.total}` : 'Not running'}
{dataSync?.running && ( <> {dataSync.currentLibrary && (
{intl.formatMessage(messages.currentlibrary, { name: dataSync.currentLibrary.name, })}
)}
{intl.formatMessage(messages.librariesRemaining, { count: dataSync.currentLibrary ? dataSync.libraries.slice( dataSync.libraries.findIndex( (library) => library.id === dataSync.currentLibrary?.id ) + 1 ).length : 0, })}
)}
{!dataSync?.running ? ( ) : ( )}
); }; export default SettingsPlex;