From be2ffbbe18c975ae66565d75df79f7b4c9cadae6 Mon Sep 17 00:00:00 2001 From: 0xsysr3ll <0xsysr3ll@pm.me> Date: Sun, 22 Feb 2026 22:30:05 +0100 Subject: [PATCH] feat(settings): make code more dry --- seerr-api.yml | 10 + server/routes/auth.ts | 1 - server/routes/settings/index.ts | 274 ++++++------------ .../Settings/SwitchMediaServerSection.tsx | 215 +++++--------- 4 files changed, 178 insertions(+), 322 deletions(-) diff --git a/seerr-api.yml b/seerr-api.yml index 85e4f6f57..5aa1a1fb8 100644 --- a/seerr-api.yml +++ b/seerr-api.yml @@ -2303,6 +2303,16 @@ paths: summary: Switch media server tags: - settings + requestBody: + content: + application/json: + schema: + type: object + properties: + targetServerType: + type: string + enum: [jellyfin, emby, plex] + description: Target media server type. Required when switching from Plex (jellyfin or emby) or from Jellyfin/Emby (plex, jellyfin, or emby). responses: '200': description: Media server cleared diff --git a/server/routes/auth.ts b/server/routes/auth.ts index 9d867bbbf..b029ecfc3 100644 --- a/server/routes/auth.ts +++ b/server/routes/auth.ts @@ -60,7 +60,6 @@ authRoutes.post('/plex', async (req, res, next) => { const mediaServerType = settings.main.mediaServerType; - // When main server is Jellyfin/Emby, allow admin to store Plex token for settings (linking) if ( mediaServerType === MediaServerType.JELLYFIN || mediaServerType === MediaServerType.EMBY diff --git a/server/routes/settings/index.ts b/server/routes/settings/index.ts index 1c4f5e655..ac686b3e6 100644 --- a/server/routes/settings/index.ts +++ b/server/routes/settings/index.ts @@ -451,6 +451,27 @@ settingsRoutes.post('/jellyfin/sync', (req, res) => { return res.status(200).json(jellyfinFullScanner.status()); }); +const EMPTY_PLEX_SETTINGS = { + name: '', + ip: '', + port: 32400, + useSsl: false, + libraries: [] as never[], +}; + +const EMPTY_JELLYFIN_SETTINGS = { + name: '', + ip: '', + port: 8096, + useSsl: false, + urlBase: '', + externalHostname: '', + jellyfinForgotPasswordUrl: '', + libraries: [] as never[], + serverId: '', + apiKey: '', +}; + settingsRoutes.post( '/switch-media-server', isAuthenticated(Permission.ADMIN), @@ -458,12 +479,14 @@ settingsRoutes.post( const settings = getSettings(); const current = settings.main.mediaServerType; const body = (req.body as { targetServerType?: string }) ?? {}; + const target = body.targetServerType; + if (current === MediaServerType.NOT_CONFIGURED) { return res.status(400).json({ error: 'No media server is configured.', }); } - const target = body.targetServerType; + if (current === MediaServerType.PLEX) { if (target !== 'jellyfin' && target !== 'emby') { return res.status(400).json({ @@ -502,26 +525,20 @@ settingsRoutes.post( } } } + try { if (current === MediaServerType.PLEX) { - const useEmby = body.targetServerType === 'emby'; + const useEmby = target === 'emby'; settings.main.mediaServerType = useEmby ? MediaServerType.EMBY : MediaServerType.JELLYFIN; - settings.plex = { - name: '', - ip: '', - port: 32400, - useSsl: false, - libraries: [], - }; - const userRepository = getRepository(User); - await userRepository + settings.plex = { ...EMPTY_PLEX_SETTINGS }; + await getRepository(User) .createQueryBuilder() .update(User) .set({ plexId: null, plexUsername: null, plexToken: null }) .execute(); - await userRepository + await getRepository(User) .createQueryBuilder() .update(User) .set({ @@ -529,15 +546,13 @@ settingsRoutes.post( }) .where('user.jellyfinUserId IS NOT NULL') .execute(); - const mediaRepository = getRepository(Media); - await mediaRepository + await getRepository(Media) .createQueryBuilder() .update(Media) .set({ ratingKey: null, ratingKey4k: null }) .where('media.ratingKey IS NOT NULL OR media.ratingKey4k IS NOT NULL') .execute(); - const watchlistRepository = getRepository(Watchlist); - await watchlistRepository + await getRepository(Watchlist) .createQueryBuilder() .update(Watchlist) .set({ ratingKey: '' }) @@ -555,184 +570,81 @@ settingsRoutes.post( ? 'Switched to Emby. All users have been logged out. Restart the server, then sign in with the new media server.' : 'Switched to Jellyfin. All users have been logged out. Restart the server, then sign in with the new media server.', }); - } else if ( + } + + if ( current === MediaServerType.JELLYFIN || current === MediaServerType.EMBY ) { - const targetJellyfinType = body.targetServerType; - const switchToJellyfin = targetJellyfinType === 'jellyfin'; - const switchToEmby = targetJellyfinType === 'emby'; - const switchToPlex = targetJellyfinType === 'plex'; + const newType = + target === 'plex' + ? MediaServerType.PLEX + : target === 'emby' + ? MediaServerType.EMBY + : MediaServerType.JELLYFIN; + const newUserType = + target === 'plex' + ? UserType.PLEX + : target === 'emby' + ? UserType.EMBY + : UserType.JELLYFIN; + const serverName = + target === 'plex' ? 'Plex' : target === 'emby' ? 'Emby' : 'Jellyfin'; - if (switchToJellyfin && current !== MediaServerType.JELLYFIN) { - // Emby => Jellyfin - settings.main.mediaServerType = MediaServerType.JELLYFIN; - settings.jellyfin = { - name: '', - ip: '', - port: 8096, - useSsl: false, - urlBase: '', - externalHostname: '', - jellyfinForgotPasswordUrl: '', - libraries: [], - serverId: '', - apiKey: '', - }; - const userRepository = getRepository(User); - await userRepository - .createQueryBuilder() - .update(User) - .set({ - jellyfinUserId: null, - jellyfinUsername: null, - jellyfinAuthToken: null, - jellyfinDeviceId: null, - }) - .execute(); - await userRepository - .createQueryBuilder() - .update(User) - .set({ userType: UserType.JELLYFIN }) - .where('user.jellyfinUserId IS NOT NULL') - .execute(); - const mediaRepository = getRepository(Media); - await mediaRepository - .createQueryBuilder() - .update(Media) - .set({ jellyfinMediaId: null, jellyfinMediaId4k: null }) - .where( - 'media.jellyfinMediaId IS NOT NULL OR media.jellyfinMediaId4k IS NOT NULL' - ) - .execute(); - await settings.save(); - await getRepository(Session) - .createQueryBuilder() - .delete() - .from(Session) - .execute(); - startJobs(); - return res.status(200).json({ - message: - 'Switched to Jellyfin. All users have been logged out. Restart the server, then reconfigure and sign in with the new media server.', + if ( + (target === 'jellyfin' && current === MediaServerType.JELLYFIN) || + (target === 'emby' && current === MediaServerType.EMBY) + ) { + return res.status(400).json({ + error: `Already using ${serverName}. Choose a different target.`, }); } - if (switchToEmby && current !== MediaServerType.EMBY) { - // Jellyfin => Emby - settings.main.mediaServerType = MediaServerType.EMBY; - settings.jellyfin = { - name: '', - ip: '', - port: 8096, - useSsl: false, - urlBase: '', - externalHostname: '', - jellyfinForgotPasswordUrl: '', - libraries: [], - serverId: '', - apiKey: '', - }; - const userRepository = getRepository(User); - await userRepository - .createQueryBuilder() - .update(User) - .set({ - jellyfinUserId: null, - jellyfinUsername: null, - jellyfinAuthToken: null, - jellyfinDeviceId: null, - }) - .execute(); - await userRepository - .createQueryBuilder() - .update(User) - .set({ userType: UserType.EMBY }) - .execute(); - const mediaRepository = getRepository(Media); - await mediaRepository - .createQueryBuilder() - .update(Media) - .set({ jellyfinMediaId: null, jellyfinMediaId4k: null }) - .where( - 'media.jellyfinMediaId IS NOT NULL OR media.jellyfinMediaId4k IS NOT NULL' - ) - .execute(); - await settings.save(); - await getRepository(Session) - .createQueryBuilder() - .delete() - .from(Session) - .execute(); - startJobs(); - return res.status(200).json({ - message: - 'Switched to Emby. All users have been logged out. Restart the server, then reconfigure and sign in with the new media server.', - }); - } + settings.main.mediaServerType = newType; + settings.jellyfin = { ...EMPTY_JELLYFIN_SETTINGS }; + await getRepository(User) + .createQueryBuilder() + .update(User) + .set({ + jellyfinUserId: null, + jellyfinUsername: null, + jellyfinAuthToken: null, + jellyfinDeviceId: null, + }) + .execute(); + await getRepository(User) + .createQueryBuilder() + .update(User) + .set({ userType: newUserType }) + .execute(); + await getRepository(Media) + .createQueryBuilder() + .update(Media) + .set({ jellyfinMediaId: null, jellyfinMediaId4k: null }) + .where( + 'media.jellyfinMediaId IS NOT NULL OR media.jellyfinMediaId4k IS NOT NULL' + ) + .execute(); + await settings.save(); + await getRepository(Session) + .createQueryBuilder() + .delete() + .from(Session) + .execute(); + startJobs(); - if (switchToPlex) { - // Jellyfin/Emby => Plex - settings.main.mediaServerType = MediaServerType.PLEX; - settings.jellyfin = { - name: '', - ip: '', - port: 8096, - useSsl: false, - urlBase: '', - externalHostname: '', - jellyfinForgotPasswordUrl: '', - libraries: [], - serverId: '', - apiKey: '', - }; - const userRepository = getRepository(User); - await userRepository - .createQueryBuilder() - .update(User) - .set({ - jellyfinUserId: null, - jellyfinUsername: null, - jellyfinAuthToken: null, - jellyfinDeviceId: null, - }) - .execute(); - await userRepository - .createQueryBuilder() - .update(User) - .set({ userType: UserType.PLEX }) - .execute(); - const mediaRepository = getRepository(Media); - await mediaRepository - .createQueryBuilder() - .update(Media) - .set({ jellyfinMediaId: null, jellyfinMediaId4k: null }) - .where( - 'media.jellyfinMediaId IS NOT NULL OR media.jellyfinMediaId4k IS NOT NULL' - ) - .execute(); - await settings.save(); - await getRepository(Session) - .createQueryBuilder() - .delete() - .from(Session) - .execute(); - startJobs(); - return res.status(200).json({ - message: - 'Switched to Plex. All users have been logged out. Restart the server, then sign in with the new media server.', - }); - } - - return res.status(400).json({ - error: - 'Specify targetServerType: "plex", "jellyfin", or "emby" to switch media server.', + const reconfigure = + target === 'jellyfin' || target === 'emby' + ? ' Restart the server, then reconfigure and sign in with the new media server.' + : ' Restart the server, then sign in with the new media server.'; + return res.status(200).json({ + message: `Switched to ${serverName}. All users have been logged out.${reconfigure}`, }); } } catch (e) { logger.error('Switch media server failed', { label: 'Settings', - errorMessage: e.message, + errorMessage: (e as Error).message, }); return next({ status: 500, message: 'Failed to switch media server.' }); } diff --git a/src/components/Settings/SwitchMediaServerSection.tsx b/src/components/Settings/SwitchMediaServerSection.tsx index dc039e63e..d0652635e 100644 --- a/src/components/Settings/SwitchMediaServerSection.tsx +++ b/src/components/Settings/SwitchMediaServerSection.tsx @@ -16,6 +16,14 @@ import useSWR from 'swr'; type SwitchTargetServerType = 'jellyfin' | 'emby' | 'plex'; +function getTargetLabel(target: SwitchTargetServerType): string { + return target === 'plex' + ? 'Plex' + : target === 'jellyfin' + ? 'Jellyfin' + : 'Emby'; +} + const messages = defineMessages('components.Settings', { switchMediaServerError: 'Something went wrong while switching media server. Please try again.', @@ -49,6 +57,43 @@ const messages = defineMessages('components.Settings', { checkUsersLink: 'Users', }); +type StepsVariant = 'plex' | 'jellyfinEmbyToPlex' | 'jellyfinEmbyToOther'; + +const STEP_KEYS: Record< + StepsVariant, + [ + keyof typeof messages, + keyof typeof messages, + keyof typeof messages, + keyof typeof messages, + ] +> = { + plex: [ + 'switchMediaServerStep1Plex', + 'switchMediaServerStep2Plex', + 'switchMediaServerStep3Plex', + 'switchMediaServerStep4Plex', + ], + jellyfinEmbyToPlex: [ + 'switchMediaServerStep1JellyfinEmby', + 'switchMediaServerStep2JellyfinEmby', + 'switchMediaServerStep3JellyfinEmby', + 'switchMediaServerStep4JellyfinEmby', + ], + jellyfinEmbyToOther: [ + 'switchMediaServerStep1JellyfinEmbyToOther', + 'switchMediaServerStep2JellyfinEmbyToOther', + 'switchMediaServerStep3JellyfinEmbyToOther', + 'switchMediaServerStep4JellyfinEmbyToOther', + ], +}; + +const STEP_LINK_VALUES_INDEX: Record = { + plex: [1, 2], + jellyfinEmbyToPlex: [1, 2], + jellyfinEmbyToOther: [], +}; + const SwitchMediaServerSection = () => { const settings = useSettings(); const intl = useIntl(); @@ -97,9 +142,13 @@ const SwitchMediaServerSection = () => { const effectiveTarget = validTargets.includes(switchTargetServerType) ? switchTargetServerType : validTargets[0]; - const targetPayload: { targetServerType: SwitchTargetServerType } = { - targetServerType: effectiveTarget, - }; + const targetPayload = { targetServerType: effectiveTarget }; + const showPlexOnly = validTargets.length === 1 && validTargets[0] === 'plex'; + const stepsVariant: StepsVariant = isPlex + ? 'plex' + : (isJellyfin || isEmby) && effectiveTarget !== 'plex' + ? 'jellyfinEmbyToOther' + : 'jellyfinEmbyToPlex'; const handleSwitch = async () => { setSubmitting(true); @@ -115,17 +164,12 @@ const SwitchMediaServerSection = () => { setModalOpen(false); window.location.reload(); } catch (err: unknown) { - const extracted = axios.isAxiosError(err) - ? (err.response?.data?.error ?? - err.response?.data?.message ?? - err.message) - : err instanceof Error - ? err.message - : null; const message = - extracted != null && String(extracted).trim() !== '' - ? String(extracted) - : intl.formatMessage(messages.switchMediaServerError); + axios.isAxiosError(err) && err.response?.data?.message + ? String(err.response.data.message) + : axios.isAxiosError(err) && err.response?.data?.error + ? String(err.response.data.error) + : intl.formatMessage(messages.switchMediaServerError); addToast(message, { appearance: 'error' }); } finally { setSubmitting(false); @@ -150,7 +194,7 @@ const SwitchMediaServerSection = () => { return (