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'; 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 scans your Plex libraries to see what content is available.', servername: 'Server Name', servernameTip: 'Automatically retrieved from Plex after saving', servernamePlaceholder: 'Plex Server Name', serverpreset: 'Server', serverpresetPlaceholder: 'Plex Server', serverLocal: 'local', serverRemote: 'remote', serverConnected: 'connected', serverpresetManualMessage: 'Manually configure', serverpresetRefreshing: 'Retrieving servers…', serverpresetLoad: 'Press the 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 set up Plex, you can either enter your details manually \ or select a server retrieved from plex.tv.\ Press the button to the right of the dropdown to check connectivity and retrieve available servers.', hostname: 'Hostname/IP', port: 'Port', ssl: 'SSL', timeout: 'Timeout', ms: 'ms', save: 'Save Changes', saving: 'Saving…', 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.', syncing: 'Syncing', sync: 'Sync Plex 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 hostname/IP', validationPortRequired: 'You must provide a port', }); 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; host?: 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 [submitError, setSubmitError] = useState(null); 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().required( intl.formatMessage(messages.validationHostnameRequired) ), port: Yup.number().required( intl.formatMessage(messages.validationPortRequired) ), }); 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' ? 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); 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.get('/api/v1/settings/plex/sync', { params: { start: true, }, }); revalidateSync(); }; const cancelScan = async () => { await axios.get('/api/v1/settings/plex/sync', { params: { 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.settingUpPlexDescription, { RegisterPlexTVLink: function RegisterPlexTVLink(msg) { return ( {msg} ); }, })} { 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, } 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); } }} > {({ errors, touched, values, handleSubmit, setFieldValue, setFieldTouched, isSubmitting, }) => { return ( { 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'); }} > {availableServers || isRefreshingPresets ? isRefreshingPresets ? intl.formatMessage( messages.serverpresetRefreshing ) : intl.formatMessage( messages.serverpresetManualMessage ) : intl.formatMessage(messages.serverpresetLoad)} {availablePresets.map((server, index) => ( {` ${server.name} (${server.address}) [${ server.local ? intl.formatMessage(messages.serverLocal) : intl.formatMessage(messages.serverRemote) }] ${server.status ? '' : '(' + server.message + ')'} `} ))} { 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" > {values.useSsl ? 'https://' : 'http://'} {errors.hostname && touched.hostname && ( {errors.hostname} )} {errors.port && touched.port && ( {errors.port} )} {intl.formatMessage(messages.ssl)} { setFieldValue('useSsl', !values.useSsl); }} className="w-6 h-6 text-indigo-600 transition duration-150 ease-in-out rounded-md form-checkbox" /> {submitError && ( {submitError} )} {isSubmitting ? intl.formatMessage(messages.saving) : intl.formatMessage(messages.save)} ); }} syncLibraries()} disabled={isSyncing}> {isSyncing ? intl.formatMessage(messages.syncing) : intl.formatMessage(messages.sync)} {data?.libraries.map((library) => ( toggleLibrary(library.id)} /> ))} {dataSync?.running && ( )} {dataSync?.running ? `${dataSync.progress} of ${dataSync.total}` : 'Not running'} {dataSync?.running && ( <> {dataSync.currentLibrary && ( )} library.id === dataSync.currentLibrary?.id ) + 1 ).length : 0, }} /> > )} {!dataSync?.running && ( startScan()}> )} {dataSync?.running && ( cancelScan()}> )} > ); }; export default SettingsPlex;