From 395a91c2f03837c260927448501d53d37fe34674 Mon Sep 17 00:00:00 2001 From: fallenbagel <98979876+Fallenbagel@users.noreply.github.com> Date: Sat, 18 Nov 2023 02:20:05 +0500 Subject: [PATCH] feat: add Media Server Selection to Setup Page Introduce the ability to select the media server type on the setup page. Users can now choose their preferred media server (e.g., Plex through the Plex sign-in or Emby/Jellyfin sign-in to select either Emby or Jellyfin). The selected media server type is then reflected in the application settings. This enhancement provides users with increased flexibility and customization options during the initial setup process, eliminating the need to rely on environment variables (which cannot be set if using platforms like snaps). Existing Emby users, who use the environment variable, should log out and log back in after updating to set their mediaServerType to Emby. BREAKING CHANGE: This commit deprecates the JELLYFIN_TYPE variable to identify Emby media server and instead rely on the mediaServerType that is set in the `settings.json`. Existing environment variable users can log out and log back in to set the mediaServerType to `3` (Emby). --- server/constants/user.ts | 1 + server/entity/Media.ts | 4 +- server/routes/auth.ts | 37 ++++++- src/assets/services/emby-inverted.svg | 22 +++++ .../services/jellyfin-icon-only-inverted.svg | 28 ++++++ src/assets/services/jellyfin-icon-only.svg | 24 +++++ src/components/ExternalLinkBlock/index.tsx | 5 +- src/components/IssueDetails/index.tsx | 26 ++--- src/components/Login/JellyfinLogin.tsx | 96 +++++++++++++++++-- src/components/Login/index.tsx | 21 ++-- src/components/ManageSlideOver/index.tsx | 5 +- src/components/MovieDetails/index.tsx | 8 +- src/components/PermissionEdit/index.tsx | 88 +++++++++-------- src/components/Settings/SettingsJellyfin.tsx | 82 ++++++++-------- .../Settings/SettingsJobsCache/index.tsx | 17 +++- src/components/Settings/SettingsLayout.tsx | 8 +- .../Settings/SettingsUsers/index.tsx | 51 ++++------ src/components/Setup/SetupLogin.tsx | 29 ++++-- src/components/StatusBadge/index.tsx | 4 +- src/components/TvDetails/index.tsx | 10 +- .../UserList/JellyfinImportModal.tsx | 22 +++-- src/components/UserList/index.tsx | 7 +- .../UserGeneralSettings/index.tsx | 4 +- src/styles/globals.css | 10 ++ 24 files changed, 412 insertions(+), 197 deletions(-) create mode 100644 src/assets/services/emby-inverted.svg create mode 100644 src/assets/services/jellyfin-icon-only-inverted.svg create mode 100644 src/assets/services/jellyfin-icon-only.svg diff --git a/server/constants/user.ts b/server/constants/user.ts index 5a0a4bd5..90b33dc8 100644 --- a/server/constants/user.ts +++ b/server/constants/user.ts @@ -2,4 +2,5 @@ export enum UserType { PLEX = 1, LOCAL = 2, JELLYFIN = 3, + EMBY = 4, } diff --git a/server/entity/Media.ts b/server/entity/Media.ts index 68a5622c..183712ee 100644 --- a/server/entity/Media.ts +++ b/server/entity/Media.ts @@ -210,7 +210,9 @@ class Media { } } else { const pageName = - process.env.JELLYFIN_TYPE === 'emby' ? 'item' : 'details'; + getSettings().main.mediaServerType == MediaServerType.EMBY + ? 'item' + : 'details'; const { serverId, hostname, externalHostname } = getSettings().jellyfin; let jellyfinHost = externalHostname && externalHostname.length > 0 diff --git a/server/routes/auth.ts b/server/routes/auth.ts index 7adcc73a..b709bd78 100644 --- a/server/routes/auth.ts +++ b/server/routes/auth.ts @@ -219,14 +219,18 @@ authRoutes.post('/jellyfin', async (req, res, next) => { password?: string; hostname?: string; email?: string; + selectedservice?: string; }; - //Make sure jellyfin login is enabled, but only if jellyfin is not already configured + //Make sure jellyfin login is enabled, but only if jellyfin && Emby is not already configured if ( settings.main.mediaServerType !== MediaServerType.JELLYFIN && + settings.main.mediaServerType !== MediaServerType.EMBY && settings.jellyfin.hostname !== '' ) { return res.status(500).json({ error: 'Jellyfin login is disabled' }); + } else if (!body.selectedservice) { + return res.status(500).json({ error: 'No server type provided.' }); } else if (!body.username) { return res.status(500).json({ error: 'You must provide an username' }); } else if (settings.jellyfin.hostname !== '' && body.hostname) { @@ -292,6 +296,13 @@ authRoutes.post('/jellyfin', async (req, res, next) => { if (user.username === account.User.Name) { user.username = ''; } + + // If JELLYFIN_TYPE is set to 'emby' then set mediaServerType to EMBY + if (process.env.JELLYFIN_TYPE === 'emby') { + settings.main.mediaServerType = MediaServerType.EMBY; + settings.save(); + } + await userRepository.save(user); } else if (!settings.main.newPlexLogin) { logger.warn( @@ -320,6 +331,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => { jellyfinUsername: account.User.Name, } ); + user = new User({ email: body.email, jellyfinUsername: account.User.Name, @@ -337,7 +349,16 @@ authRoutes.post('/jellyfin', async (req, res, next) => { //Update hostname in settings if it doesn't exist (initial configuration) //Also set mediaservertype to JELLYFIN if (settings.jellyfin.hostname === '') { - settings.main.mediaServerType = MediaServerType.JELLYFIN; + // If JELLYFIN_TYPE is set to 'emby' then set mediaServerType to EMBY + if ( + process.env.JELLYFIN_TYPE === 'emby' || + body.selectedservice === 'Emby' + ) { + settings.main.mediaServerType = MediaServerType.EMBY; + } else if (body.selectedservice === 'Jellyfin') { + settings.main.mediaServerType = MediaServerType.JELLYFIN; + } + settings.jellyfin.hostname = body.hostname ?? ''; settings.jellyfin.serverId = account.User.ServerId; settings.save(); @@ -350,6 +371,13 @@ authRoutes.post('/jellyfin', async (req, res, next) => { throw new Error('add_email'); } + if ( + !body.selectedservice && + (body.selectedservice !== 'Emby' || 'Jellyfin') + ) { + throw new Error('select_server_type'); + } + user = new User({ email: body.email, jellyfinUsername: account.User.Name, @@ -400,6 +428,11 @@ authRoutes.post('/jellyfin', async (req, res, next) => { status: 406, message: 'CREDENTIAL_ERROR_ADD_EMAIL', }); + } else if (e.message === 'select_server_type') { + return next({ + status: 406, + message: 'CREDENTIAL_ERROR_NO_SERVER_TYPE', + }); } else { logger.error(e.message, { label: 'Auth' }); return next({ diff --git a/src/assets/services/emby-inverted.svg b/src/assets/services/emby-inverted.svg new file mode 100644 index 00000000..8f33a586 --- /dev/null +++ b/src/assets/services/emby-inverted.svg @@ -0,0 +1,22 @@ + + + + + + + image/svg+xml + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/services/jellyfin-icon-only-inverted.svg b/src/assets/services/jellyfin-icon-only-inverted.svg new file mode 100644 index 00000000..1826872f --- /dev/null +++ b/src/assets/services/jellyfin-icon-only-inverted.svg @@ -0,0 +1,28 @@ + + + + + + + + + + icon-transparent + + + + + \ No newline at end of file diff --git a/src/assets/services/jellyfin-icon-only.svg b/src/assets/services/jellyfin-icon-only.svg new file mode 100644 index 00000000..d4d7f017 --- /dev/null +++ b/src/assets/services/jellyfin-icon-only.svg @@ -0,0 +1,24 @@ + + + + + + + + + + icon-transparent + + + + + diff --git a/src/components/ExternalLinkBlock/index.tsx b/src/components/ExternalLinkBlock/index.tsx index 1de0b5a3..b27ea695 100644 --- a/src/components/ExternalLinkBlock/index.tsx +++ b/src/components/ExternalLinkBlock/index.tsx @@ -10,7 +10,6 @@ import useLocale from '@app/hooks/useLocale'; import useSettings from '@app/hooks/useSettings'; import { MediaType } from '@server/constants/media'; import { MediaServerType } from '@server/constants/server'; -import getConfig from 'next/config'; interface ExternalLinkBlockProps { mediaType: 'movie' | 'tv'; @@ -30,7 +29,6 @@ const ExternalLinkBlock = ({ mediaUrl, }: ExternalLinkBlockProps) => { const settings = useSettings(); - const { publicRuntimeConfig } = getConfig(); const { locale } = useLocale(); return ( @@ -44,7 +42,8 @@ const ExternalLinkBlock = ({ > {settings.currentSettings.mediaServerType === MediaServerType.PLEX ? ( - ) : publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? ( + ) : settings.currentSettings.mediaServerType === + MediaServerType.EMBY ? ( ) : ( diff --git a/src/components/IssueDetails/index.tsx b/src/components/IssueDetails/index.tsx index c978ff23..d8162e9c 100644 --- a/src/components/IssueDetails/index.tsx +++ b/src/components/IssueDetails/index.tsx @@ -28,7 +28,6 @@ import type { MovieDetails } from '@server/models/Movie'; import type { TvDetails } from '@server/models/Tv'; import axios from 'axios'; import { Field, Form, Formik } from 'formik'; -import getConfig from 'next/config'; import Link from 'next/link'; import { useRouter } from 'next/router'; import { useState } from 'react'; @@ -107,7 +106,6 @@ const IssueDetails = () => { (opt) => opt.issueType === issueData?.issueType ); const settings = useSettings(); - const { publicRuntimeConfig } = getConfig(); if (!data && !error) { return ; @@ -375,7 +373,8 @@ const IssueDetails = () => { > - {publicRuntimeConfig.JELLYFIN_TYPE == 'emby' + {settings.currentSettings.mediaServerType === + MediaServerType.EMBY ? intl.formatMessage(messages.playonplex, { mediaServerName: 'Emby', }) @@ -422,16 +421,17 @@ const IssueDetails = () => { > - {publicRuntimeConfig.JELLYFIN_TYPE == 'emby' - ? intl.formatMessage(messages.play4konplex, { + {settings.currentSettings.mediaServerType === + MediaServerType.EMBY + ? intl.formatMessage(messages.playonplex, { mediaServerName: 'Emby', }) : settings.currentSettings.mediaServerType === MediaServerType.PLEX - ? intl.formatMessage(messages.play4konplex, { + ? intl.formatMessage(messages.playonplex, { mediaServerName: 'Plex', }) - : intl.formatMessage(messages.play4konplex, { + : intl.formatMessage(messages.playonplex, { mediaServerName: 'Jellyfin', })} @@ -639,7 +639,8 @@ const IssueDetails = () => { > - {publicRuntimeConfig.JELLYFIN_TYPE == 'emby' + {settings.currentSettings.mediaServerType === + MediaServerType.EMBY ? intl.formatMessage(messages.playonplex, { mediaServerName: 'Emby', }) @@ -685,16 +686,17 @@ const IssueDetails = () => { > - {publicRuntimeConfig.JELLYFIN_TYPE == 'emby' - ? intl.formatMessage(messages.play4konplex, { + {settings.currentSettings.mediaServerType === + MediaServerType.EMBY + ? intl.formatMessage(messages.playonplex, { mediaServerName: 'Emby', }) : settings.currentSettings.mediaServerType === MediaServerType.PLEX - ? intl.formatMessage(messages.play4konplex, { + ? intl.formatMessage(messages.playonplex, { mediaServerName: 'Plex', }) - : intl.formatMessage(messages.play4konplex, { + : intl.formatMessage(messages.playonplex, { mediaServerName: 'Jellyfin', })} diff --git a/src/components/Login/JellyfinLogin.tsx b/src/components/Login/JellyfinLogin.tsx index 126fa4f7..b743edb5 100644 --- a/src/components/Login/JellyfinLogin.tsx +++ b/src/components/Login/JellyfinLogin.tsx @@ -1,10 +1,14 @@ +import EmbyLogoInverted from '@app/assets/services/emby-inverted.svg'; +import EmbyLogo from '@app/assets/services/emby.svg'; +import JellyfinLogoInverted from '@app/assets/services/jellyfin-icon-only-inverted.svg'; +import JellyfinLogo from '@app/assets/services/jellyfin-icon-only.svg'; import Button from '@app/components/Common/Button'; import Tooltip from '@app/components/Common/Tooltip'; import useSettings from '@app/hooks/useSettings'; import { InformationCircleIcon } from '@heroicons/react/24/solid'; +import { MediaServerType } from '@server/constants/server'; import axios from 'axios'; import { Field, Form, Formik } from 'formik'; -import getConfig from 'next/config'; import type React from 'react'; import { defineMessages, useIntl } from 'react-intl'; import { useToasts } from 'react-toast-notifications'; @@ -23,6 +27,7 @@ const messages = defineMessages({ validationemailformat: 'Valid email required', validationusernamerequired: 'Username required', validationpasswordrequired: 'Password required', + validationselectedservicerequired: 'Please select a server type', loginerror: 'Something went wrong while trying to sign in.', credentialerror: 'The username or password is incorrect.', signingin: 'Signing in…', @@ -30,21 +35,25 @@ const messages = defineMessages({ initialsigningin: 'Connecting…', initialsignin: 'Connect', forgotpassword: 'Forgot Password?', + servertype: 'Server Type', }); interface JellyfinLoginProps { revalidate: () => void; initial?: boolean; + onToggle?: (option: string) => void; + selectedService?: string; } const JellyfinLogin: React.FC = ({ revalidate, initial, + onToggle, + selectedService, }) => { const toasts = useToasts(); const intl = useIntl(); const settings = useSettings(); - const { publicRuntimeConfig } = getConfig(); if (initial) { const LoginSchema = Yup.object().shape({ @@ -55,8 +64,7 @@ const JellyfinLogin: React.FC = ({ ) .required( intl.formatMessage(messages.validationhostrequired, { - mediaServerName: - publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin', + mediaServerName: selectedService, }) ), email: Yup.string() @@ -66,11 +74,18 @@ const JellyfinLogin: React.FC = ({ intl.formatMessage(messages.validationusernamerequired) ), password: Yup.string(), + selectedservice: Yup.string().required( + intl.formatMessage(messages.validationselectedservicerequired) + ), }); const mediaServerFormatValues = { mediaServerName: - publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin', + selectedService === 'Jellyfin' + ? 'Jellyfin' + : selectedService === 'Emby' + ? 'Emby' + : 'Media Server', }; return ( = ({ password: '', host: '', email: '', + selectedservice: '', }} + initialErrors={{ selectedservice: 'Please select a server type' }} // Initialize errors with an empty object + initialTouched={{ selectedservice: true }} validationSchema={LoginSchema} onSubmit={async (values) => { try { + // Check if selectedService is either 'Jellyfin' or 'Emby' + // if (selectedService !== 'Jellyfin' && selectedService !== 'Emby') { + // throw new Error('Invalid selectedService'); // You can customize the error message + // } + await axios.post('/api/v1/auth/jellyfin', { username: values.username, password: values.password, hostname: values.host, email: values.email, + selectedservice: selectedService, }); } catch (e) { toasts.addToast( @@ -106,9 +130,66 @@ const JellyfinLogin: React.FC = ({ } }} > - {({ errors, touched, isSubmitting, isValid }) => ( + {({ + errors, + touched, + isSubmitting, + isValid, + values, + setFieldValue, + }) => (
+ +
+
+ + +
+ {/* Hidden field */} + + {!values.selectedservice && errors.selectedservice && ( +
{errors.selectedservice}
+ )} +
@@ -299,7 +380,8 @@ const JellyfinLogin: React.FC = ({ as="a" buttonType="ghost" href={`${baseUrl}/web/index.html#!/${ - process.env.JELLYFIN_TYPE === 'emby' + settings.currentSettings.mediaServerType === + MediaServerType.EMBY ? 'startup/' : '' }forgotpassword.html`} diff --git a/src/components/Login/index.tsx b/src/components/Login/index.tsx index da4344ef..70c1458d 100644 --- a/src/components/Login/index.tsx +++ b/src/components/Login/index.tsx @@ -10,7 +10,6 @@ import { Transition } from '@headlessui/react'; import { XCircleIcon } from '@heroicons/react/24/solid'; import { MediaServerType } from '@server/constants/server'; import axios from 'axios'; -import getConfig from 'next/config'; import { useRouter } from 'next/dist/client/router'; import { useEffect, useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; @@ -33,7 +32,6 @@ const Login = () => { const { user, revalidate } = useUser(); const router = useRouter(); const settings = useSettings(); - const { publicRuntimeConfig } = getConfig(); // Effect that is triggered when the `authToken` comes back from the Plex OAuth // We take the token and attempt to sign in. If we get a success message, we will @@ -72,6 +70,15 @@ const Login = () => { revalidateOnFocus: false, }); + const mediaServerFormatValues = { + mediaServerName: + settings.currentSettings.mediaServerType === MediaServerType.JELLYFIN + ? 'Jellyfin' + : settings.currentSettings.mediaServerType === MediaServerType.EMBY + ? 'Emby' + : undefined, + }; + return (
@@ -136,12 +143,10 @@ const Login = () => { {settings.currentSettings.mediaServerType == MediaServerType.PLEX ? intl.formatMessage(messages.signinwithplex) - : intl.formatMessage(messages.signinwithjellyfin, { - mediaServerName: - publicRuntimeConfig.JELLYFIN_TYPE == 'emby' - ? 'Emby' - : 'Jellyfin', - })} + : intl.formatMessage( + messages.signinwithjellyfin, + mediaServerFormatValues + )}
diff --git a/src/components/ManageSlideOver/index.tsx b/src/components/ManageSlideOver/index.tsx index e6487195..1a8a9918 100644 --- a/src/components/ManageSlideOver/index.tsx +++ b/src/components/ManageSlideOver/index.tsx @@ -26,7 +26,6 @@ import type { RadarrSettings, SonarrSettings } from '@server/lib/settings'; import type { MovieDetails } from '@server/models/Movie'; import type { TvDetails } from '@server/models/Tv'; import axios from 'axios'; -import getConfig from 'next/config'; import Link from 'next/link'; import { defineMessages, useIntl } from 'react-intl'; import useSWR from 'swr'; @@ -94,7 +93,6 @@ const ManageSlideOver = ({ const { user: currentUser, hasPermission } = useUser(); const intl = useIntl(); const settings = useSettings(); - const { publicRuntimeConfig } = getConfig(); const { data: watchData } = useSWR( settings.currentSettings.mediaServerType === MediaServerType.PLEX && data.mediaInfo && @@ -638,7 +636,8 @@ const ManageSlideOver = ({ mediaType === 'movie' ? messages.movie : messages.tvshow ), mediaServerName: - publicRuntimeConfig.JELLYFIN_TYPE == 'emby' + settings.currentSettings.mediaServerType === + MediaServerType.EMBY ? 'Emby' : settings.currentSettings.mediaServerType === MediaServerType.PLEX diff --git a/src/components/MovieDetails/index.tsx b/src/components/MovieDetails/index.tsx index b7dc5917..10879107 100644 --- a/src/components/MovieDetails/index.tsx +++ b/src/components/MovieDetails/index.tsx @@ -49,7 +49,6 @@ import type { MovieDetails as MovieDetailsType } from '@server/models/Movie'; import { hasFlag } from 'country-flag-icons'; import 'country-flag-icons/3x2/flags.css'; import { uniqBy } from 'lodash'; -import getConfig from 'next/config'; import Link from 'next/link'; import { useRouter } from 'next/router'; import { useEffect, useMemo, useState } from 'react'; @@ -111,7 +110,6 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => { const minStudios = 3; const [showMoreStudios, setShowMoreStudios] = useState(false); const [showIssueModal, setShowIssueModal] = useState(false); - const { publicRuntimeConfig } = getConfig(); const { data, @@ -259,7 +257,7 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => { ?.flatrate ?? []; function getAvalaibleMediaServerName() { - if (publicRuntimeConfig.JELLYFIN_TYPE === 'emby') { + if (settings.currentSettings.mediaServerType === MediaServerType.EMBY) { return intl.formatMessage(messages.play, { mediaServerName: 'Emby' }); } @@ -271,8 +269,8 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => { } function getAvalaible4kMediaServerName() { - if (publicRuntimeConfig.JELLYFIN_TYPE === 'emby') { - return intl.formatMessage(messages.play4k, { mediaServerName: 'Emby' }); + if (settings.currentSettings.mediaServerType === MediaServerType.EMBY) { + return intl.formatMessage(messages.play, { mediaServerName: 'Emby' }); } if (settings.currentSettings.mediaServerType === MediaServerType.PLEX) { diff --git a/src/components/PermissionEdit/index.tsx b/src/components/PermissionEdit/index.tsx index 82971bf0..b328cc78 100644 --- a/src/components/PermissionEdit/index.tsx +++ b/src/components/PermissionEdit/index.tsx @@ -138,19 +138,13 @@ export const PermissionEdit = ({ mediaServerName: settings.currentSettings.mediaServerType === MediaServerType.PLEX ? 'Plex' - : settings.currentSettings.mediaServerType === - MediaServerType.JELLYFIN - ? 'Jellyfin' - : 'Emby', + : 'Local', }), description: intl.formatMessage(messages.viewwatchlistsDescription, { mediaServerName: settings.currentSettings.mediaServerType === MediaServerType.PLEX ? 'Plex' - : settings.currentSettings.mediaServerType === - MediaServerType.JELLYFIN - ? 'Jellyfin' - : 'Emby', + : 'Local', }), permission: Permission.WATCHLIST_VIEW, }, @@ -213,43 +207,47 @@ export const PermissionEdit = ({ }, ], }, - { - id: 'autorequest', - name: intl.formatMessage(messages.autorequest), - description: intl.formatMessage(messages.autorequestDescription), - permission: Permission.AUTO_REQUEST, - requires: [{ permissions: [Permission.REQUEST] }], - children: [ - { - id: 'autorequestmovies', - name: intl.formatMessage(messages.autorequestMovies), - description: intl.formatMessage( - messages.autorequestMoviesDescription - ), - permission: Permission.AUTO_REQUEST_MOVIE, - requires: [ - { - permissions: [Permission.REQUEST, Permission.REQUEST_MOVIE], - type: 'or', - }, - ], - }, - { - id: 'autorequesttv', - name: intl.formatMessage(messages.autorequestSeries), - description: intl.formatMessage( - messages.autorequestSeriesDescription - ), - permission: Permission.AUTO_REQUEST_TV, - requires: [ - { - permissions: [Permission.REQUEST, Permission.REQUEST_TV], - type: 'or', - }, - ], - }, - ], - }, + ...(settings.currentSettings.mediaServerType === MediaServerType.PLEX + ? [ + { + id: 'autorequest', + name: intl.formatMessage(messages.autorequest), + description: intl.formatMessage(messages.autorequestDescription), + permission: Permission.AUTO_REQUEST, + requires: [{ permissions: [Permission.REQUEST] }], + children: [ + { + id: 'autorequestmovies', + name: intl.formatMessage(messages.autorequestMovies), + description: intl.formatMessage( + messages.autorequestMoviesDescription + ), + permission: Permission.AUTO_REQUEST_MOVIE, + requires: [ + { + permissions: [Permission.REQUEST, Permission.REQUEST_MOVIE], + type: 'or', + }, + ], + }, + { + id: 'autorequesttv', + name: intl.formatMessage(messages.autorequestSeries), + description: intl.formatMessage( + messages.autorequestSeriesDescription + ), + permission: Permission.AUTO_REQUEST_TV, + requires: [ + { + permissions: [Permission.REQUEST, Permission.REQUEST_TV], + type: 'or', + }, + ], + }, + ], + } as PermissionItem, + ] + : []), { id: 'request4k', name: intl.formatMessage(messages.request4k), diff --git a/src/components/Settings/SettingsJellyfin.tsx b/src/components/Settings/SettingsJellyfin.tsx index 5d56431f..fb439a56 100644 --- a/src/components/Settings/SettingsJellyfin.tsx +++ b/src/components/Settings/SettingsJellyfin.tsx @@ -2,12 +2,13 @@ import Badge from '@app/components/Common/Badge'; import Button from '@app/components/Common/Button'; import LoadingSpinner from '@app/components/Common/LoadingSpinner'; import LibraryItem from '@app/components/Settings/LibraryItem'; +import useSettings from '@app/hooks/useSettings'; import globalMessages from '@app/i18n/globalMessages'; import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline'; +import { MediaServerType } from '@server/constants/server'; import type { JellyfinSettings } from '@server/lib/settings'; import axios from 'axios'; import { Field, Formik } from 'formik'; -import getConfig from 'next/config'; import type React from 'react'; import { useState } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; @@ -83,7 +84,7 @@ const SettingsJellyfin: React.FC = ({ ); const intl = useIntl(); const { addToast } = useToasts(); - const { publicRuntimeConfig } = getConfig(); + const settings = useSettings(); const JellyfinSettingsSchema = Yup.object().shape({ jellyfinExternalUrl: Yup.string().matches( @@ -165,26 +166,29 @@ const SettingsJellyfin: React.FC = ({ return ; } + const mediaServerFormatValues = { + mediaServerName: + settings.currentSettings.mediaServerType === MediaServerType.JELLYFIN + ? 'Jellyfin' + : settings.currentSettings.mediaServerType === MediaServerType.EMBY + ? 'Emby' + : undefined, + }; + return ( <>

- {publicRuntimeConfig.JELLYFIN_TYPE == 'emby' - ? intl.formatMessage(messages.jellyfinlibraries, { - mediaServerName: 'Emby', - }) - : intl.formatMessage(messages.jellyfinlibraries, { - mediaServerName: 'Jellyfin', - })} + {intl.formatMessage( + messages.jellyfinlibraries, + mediaServerFormatValues + )}

- {publicRuntimeConfig.JELLYFIN_TYPE == 'emby' - ? intl.formatMessage(messages.jellyfinlibrariesDescription, { - mediaServerName: 'Emby', - }) - : intl.formatMessage(messages.jellyfinlibrariesDescription, { - mediaServerName: 'Jellyfin', - })} + {intl.formatMessage( + messages.jellyfinlibrariesDescription, + mediaServerFormatValues + )}

@@ -221,13 +225,10 @@ const SettingsJellyfin: React.FC = ({

- {publicRuntimeConfig.JELLYFIN_TYPE == 'emby' - ? intl.formatMessage(messages.manualscanDescriptionJellyfin, { - mediaServerName: 'Emby', - }) - : intl.formatMessage(messages.manualscanDescriptionJellyfin, { - mediaServerName: 'Jellyfin', - })} + {intl.formatMessage( + messages.manualscanDescriptionJellyfin, + mediaServerFormatValues + )}

@@ -331,22 +332,16 @@ const SettingsJellyfin: React.FC = ({ <>

- {publicRuntimeConfig.JELLYFIN_TYPE == 'emby' - ? intl.formatMessage(messages.jellyfinSettings, { - mediaServerName: 'Emby', - }) - : intl.formatMessage(messages.jellyfinSettings, { - mediaServerName: 'Jellyfin', - })} + {intl.formatMessage( + messages.jellyfinSettings, + mediaServerFormatValues + )}

- {publicRuntimeConfig.JELLYFIN_TYPE == 'emby' - ? intl.formatMessage(messages.jellyfinSettingsDescription, { - mediaServerName: 'Emby', - }) - : intl.formatMessage(messages.jellyfinSettingsDescription, { - mediaServerName: 'Jellyfin', - })} + {intl.formatMessage( + messages.jellyfinSettingsDescription, + mediaServerFormatValues + )}

= ({ addToast( intl.formatMessage(messages.jellyfinSettingsSuccess, { mediaServerName: - publicRuntimeConfig.JELLYFIN_TYPE == 'emby' + settings.currentSettings.mediaServerType === + MediaServerType.EMBY ? 'Emby' : 'Jellyfin', }), @@ -376,12 +372,10 @@ const SettingsJellyfin: React.FC = ({ ); } catch (e) { addToast( - intl.formatMessage(messages.jellyfinSettingsFailure, { - mediaServerName: - publicRuntimeConfig.JELLYFIN_TYPE == 'emby' - ? 'Emby' - : 'Jellyfin', - }), + intl.formatMessage( + messages.jellyfinSettingsFailure, + mediaServerFormatValues + ), { autoDismiss: true, appearance: 'error', diff --git a/src/components/Settings/SettingsJobsCache/index.tsx b/src/components/Settings/SettingsJobsCache/index.tsx index 1686fce2..b367c64b 100644 --- a/src/components/Settings/SettingsJobsCache/index.tsx +++ b/src/components/Settings/SettingsJobsCache/index.tsx @@ -7,6 +7,7 @@ import PageTitle from '@app/components/Common/PageTitle'; import Table from '@app/components/Common/Table'; import useLocale from '@app/hooks/useLocale'; import useSettings from '@app/hooks/useSettings'; + import globalMessages from '@app/i18n/globalMessages'; import { formatBytes } from '@app/utils/numberHelpers'; import { Transition } from '@headlessui/react'; @@ -55,8 +56,8 @@ const messages: { [messageName: string]: MessageDescriptor } = defineMessages({ 'plex-recently-added-scan': 'Plex Recently Added Scan', 'plex-full-scan': 'Plex Full Library Scan', 'plex-watchlist-sync': 'Plex Watchlist Sync', - 'jellyfin-recently-added-scan': 'Jellyfin Recently Added Scan', 'jellyfin-full-scan': 'Jellyfin Full Library Scan', + 'jellyfin-recently-added-scan': 'Jellyfin Recently Added Scan', 'availability-sync': 'Media Availability Sync', 'radarr-scan': 'Radarr Scan', 'sonarr-scan': 'Sonarr Scan', @@ -164,6 +165,20 @@ const SettingsJobs = () => { const [isSaving, setIsSaving] = useState(false); const settings = useSettings(); + if (settings.currentSettings.mediaServerType === MediaServerType.EMBY) { + messages['jellyfin-recently-added-scan'] = { + id: 'jellyfin-recently-added-scan', + defaultMessage: 'Emby Recently Added Scan', + }; + } + + if (settings.currentSettings.mediaServerType === MediaServerType.EMBY) { + messages['jellyfin-full-scan'] = { + id: 'jellyfin-full-scan', + defaultMessage: 'Emby Full Library Scan', + }; + } + if (!data && !error) { return ; } diff --git a/src/components/Settings/SettingsLayout.tsx b/src/components/Settings/SettingsLayout.tsx index cddc35ef..06604e26 100644 --- a/src/components/Settings/SettingsLayout.tsx +++ b/src/components/Settings/SettingsLayout.tsx @@ -4,7 +4,6 @@ import SettingsTabs from '@app/components/Common/SettingsTabs'; import useSettings from '@app/hooks/useSettings'; import globalMessages from '@app/i18n/globalMessages'; import { MediaServerType } from '@server/constants/server'; -import getConfig from 'next/config'; import type React from 'react'; import { defineMessages, useIntl } from 'react-intl'; @@ -26,7 +25,6 @@ type SettingsLayoutProps = { const SettingsLayout = ({ children }: SettingsLayoutProps) => { const intl = useIntl(); - const { publicRuntimeConfig } = getConfig(); const settings = useSettings(); const settingsRoutes: SettingsRoute[] = [ { @@ -89,7 +87,11 @@ const SettingsLayout = ({ children }: SettingsLayoutProps) => { function getAvailableMediaServerName() { return intl.formatMessage(messages.menuJellyfinSettings, { mediaServerName: - publicRuntimeConfig.JELLYFIN_TYPE === 'emby' ? 'Emby' : 'Jellyfin', + settings.currentSettings.mediaServerType === MediaServerType.JELLYFIN + ? 'Jellyfin' + : settings.currentSettings.mediaServerType === MediaServerType.EMBY + ? 'Emby' + : undefined, }); } }; diff --git a/src/components/Settings/SettingsUsers/index.tsx b/src/components/Settings/SettingsUsers/index.tsx index ff6126c5..9d360cb3 100644 --- a/src/components/Settings/SettingsUsers/index.tsx +++ b/src/components/Settings/SettingsUsers/index.tsx @@ -10,7 +10,6 @@ import { MediaServerType } from '@server/constants/server'; import type { MainSettings } from '@server/lib/settings'; import axios from 'axios'; import { Field, Form, Formik } from 'formik'; -import getConfig from 'next/config'; import { defineMessages, useIntl } from 'react-intl'; import { useToasts } from 'react-toast-notifications'; import useSWR, { mutate } from 'swr'; @@ -42,12 +41,20 @@ const SettingsUsers = () => { mutate: revalidate, } = useSWR('/api/v1/settings/main'); const settings = useSettings(); - const { publicRuntimeConfig } = getConfig(); if (!data && !error) { return ; } + const mediaServerFormatValues = { + mediaServerName: + settings.currentSettings.mediaServerType === MediaServerType.JELLYFIN + ? 'Jellyfin' + : settings.currentSettings.mediaServerType === MediaServerType.EMBY + ? 'Emby' + : undefined, + }; + return ( <> {
@@ -139,25 +140,15 @@ const SettingsUsers = () => {
diff --git a/src/components/Setup/SetupLogin.tsx b/src/components/Setup/SetupLogin.tsx index 46bf7246..a87bcb33 100644 --- a/src/components/Setup/SetupLogin.tsx +++ b/src/components/Setup/SetupLogin.tsx @@ -4,7 +4,6 @@ import PlexLoginButton from '@app/components/PlexLoginButton'; import { useUser } from '@app/hooks/useUser'; import { MediaServerType } from '@server/constants/server'; import axios from 'axios'; -import getConfig from 'next/config'; import type React from 'react'; import { useEffect, useState } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; @@ -27,7 +26,16 @@ const SetupLogin: React.FC = ({ onComplete }) => { ); const { user, revalidate } = useUser(); const intl = useIntl(); - const { publicRuntimeConfig } = getConfig(); + const [selectedService, setSelectedService] = useState( + undefined + ); + + // Function to handle toggle changes + const handleToggle = (option: string) => { + // Toggle between 'emby' and 'jellyfin' + setSelectedService(option); + }; + // Effect that is triggered when the `authToken` comes back from the Plex OAuth // We take the token and attempt to login. If we get a success message, we will // ask swr to revalidate the user which _shouid_ come back with a valid user. @@ -94,20 +102,21 @@ const SetupLogin: React.FC = ({ onComplete }) => { }`} onClick={() => handleClick(1)} > - {publicRuntimeConfig.JELLYFIN_TYPE == 'emby' - ? intl.formatMessage(messages.signinWithJellyfin, { - mediaServerName: 'Emby', - }) - : intl.formatMessage(messages.signinWithJellyfin, { - mediaServerName: 'Jellyfin', - })} + {intl.formatMessage(messages.signinWithJellyfin, { + mediaServerName: selectedService ?? 'Jellyfin / Emby', + })}
- +
diff --git a/src/components/StatusBadge/index.tsx b/src/components/StatusBadge/index.tsx index 58e722bd..869727bd 100644 --- a/src/components/StatusBadge/index.tsx +++ b/src/components/StatusBadge/index.tsx @@ -8,7 +8,6 @@ import globalMessages from '@app/i18n/globalMessages'; import { MediaStatus } from '@server/constants/media'; import { MediaServerType } from '@server/constants/server'; import type { DownloadingItem } from '@server/lib/downloadtracker'; -import getConfig from 'next/config'; import { defineMessages, useIntl } from 'react-intl'; const messages = defineMessages({ @@ -46,7 +45,6 @@ const StatusBadge = ({ const intl = useIntl(); const { hasPermission } = useUser(); const settings = useSettings(); - const { publicRuntimeConfig } = getConfig(); let mediaLink: string | undefined; let mediaLinkDescription: string | undefined; @@ -84,7 +82,7 @@ const StatusBadge = ({ mediaLink = plexUrl; mediaLinkDescription = intl.formatMessage(messages.playonplex, { mediaServerName: - publicRuntimeConfig.JELLYFIN_TYPE == 'emby' + settings.currentSettings.mediaServerType === MediaServerType.EMBY ? 'Emby' : settings.currentSettings.mediaServerType === MediaServerType.PLEX ? 'Plex' diff --git a/src/components/TvDetails/index.tsx b/src/components/TvDetails/index.tsx index daceb9c8..cae7447e 100644 --- a/src/components/TvDetails/index.tsx +++ b/src/components/TvDetails/index.tsx @@ -49,7 +49,6 @@ import type { Crew } from '@server/models/common'; import type { TvDetails as TvDetailsType } from '@server/models/Tv'; import { hasFlag } from 'country-flag-icons'; import 'country-flag-icons/3x2/flags.css'; -import getConfig from 'next/config'; import Link from 'next/link'; import { useRouter } from 'next/router'; import { useEffect, useMemo, useState } from 'react'; @@ -105,7 +104,6 @@ const TvDetails = ({ tv }: TvDetailsProps) => { router.query.manage == '1' ? true : false ); const [showIssueModal, setShowIssueModal] = useState(false); - const { publicRuntimeConfig } = getConfig(); const { data, @@ -274,7 +272,7 @@ const TvDetails = ({ tv }: TvDetailsProps) => { ?.flatrate ?? []; function getAvalaibleMediaServerName() { - if (publicRuntimeConfig.JELLYFIN_TYPE === 'emby') { + if (settings.currentSettings.mediaServerType === MediaServerType.EMBY) { return intl.formatMessage(messages.play, { mediaServerName: 'Emby' }); } @@ -286,15 +284,15 @@ const TvDetails = ({ tv }: TvDetailsProps) => { } function getAvalaible4kMediaServerName() { - if (publicRuntimeConfig.JELLYFIN_TYPE === 'emby') { - return intl.formatMessage(messages.play4k, { mediaServerName: 'Emby' }); + if (settings.currentSettings.mediaServerType === MediaServerType.EMBY) { + return intl.formatMessage(messages.play, { mediaServerName: 'Emby' }); } if (settings.currentSettings.mediaServerType === MediaServerType.PLEX) { return intl.formatMessage(messages.play4k, { mediaServerName: 'Plex' }); } - return intl.formatMessage(messages.play4k, { mediaServerName: 'Jellyfin' }); + return intl.formatMessage(messages.play, { mediaServerName: 'Jellyfin' }); } return ( diff --git a/src/components/UserList/JellyfinImportModal.tsx b/src/components/UserList/JellyfinImportModal.tsx index 5d293e60..ddac823c 100644 --- a/src/components/UserList/JellyfinImportModal.tsx +++ b/src/components/UserList/JellyfinImportModal.tsx @@ -2,9 +2,9 @@ import Alert from '@app/components/Common/Alert'; import Modal from '@app/components/Common/Modal'; import useSettings from '@app/hooks/useSettings'; import globalMessages from '@app/i18n/globalMessages'; +import { MediaServerType } from '@server/constants/server'; import type { UserResultsResponse } from '@server/interfaces/api/userInterfaces'; import axios from 'axios'; -import getConfig from 'next/config'; import type React from 'react'; import { useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; @@ -36,7 +36,6 @@ const JellyfinImportModal: React.FC = ({ }) => { const intl = useIntl(); const settings = useSettings(); - const { publicRuntimeConfig } = getConfig(); const { addToast } = useToasts(); const [isImporting, setImporting] = useState(false); const [selectedUsers, setSelectedUsers] = useState([]); @@ -82,7 +81,9 @@ const JellyfinImportModal: React.FC = ({ userCount: createdUsers.length, strong: (msg: React.ReactNode) => {msg}, mediaServerName: - publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin', + settings.currentSettings.mediaServerType === MediaServerType.EMBY + ? 'Emby' + : 'Jellyfin', }), { autoDismiss: true, @@ -97,7 +98,9 @@ const JellyfinImportModal: React.FC = ({ addToast( intl.formatMessage(messages.importfromJellyfinerror, { mediaServerName: - publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin', + settings.currentSettings.mediaServerType === MediaServerType.EMBY + ? 'Emby' + : 'Jellyfin', }), { autoDismiss: true, @@ -135,7 +138,9 @@ const JellyfinImportModal: React.FC = ({ loading={!data && !error} title={intl.formatMessage(messages.importfromJellyfin, { mediaServerName: - publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin', + settings.currentSettings.mediaServerType === MediaServerType.EMBY + ? 'Emby' + : 'Jellyfin', })} onOk={() => { importUsers(); @@ -152,7 +157,8 @@ const JellyfinImportModal: React.FC = ({ ( @@ -269,7 +275,9 @@ const JellyfinImportModal: React.FC = ({ diff --git a/src/components/UserList/index.tsx b/src/components/UserList/index.tsx index d4901ed4..18f96b49 100644 --- a/src/components/UserList/index.tsx +++ b/src/components/UserList/index.tsx @@ -28,7 +28,6 @@ import type { UserResultsResponse } from '@server/interfaces/api/userInterfaces' import { hasPermission } from '@server/lib/permissions'; import axios from 'axios'; import { Field, Form, Formik } from 'formik'; -import getConfig from 'next/config'; import Link from 'next/link'; import { useRouter } from 'next/router'; import { useEffect, useState } from 'react'; @@ -88,7 +87,6 @@ const UserList = () => { const intl = useIntl(); const router = useRouter(); const settings = useSettings(); - const { publicRuntimeConfig } = getConfig(); const { addToast } = useToasts(); const { user: currentUser, hasPermission: currentHasPermission } = useUser(); const [currentSort, setCurrentSort] = useState('displayname'); @@ -514,7 +512,8 @@ const UserList = () => { > - {publicRuntimeConfig.JELLYFIN_TYPE == 'emby' + {settings.currentSettings.mediaServerType === + MediaServerType.EMBY ? intl.formatMessage(messages.importfrommediaserver, { mediaServerName: 'Emby', }) @@ -659,7 +658,7 @@ const UserList = () => { {intl.formatMessage(messages.localuser)} - ) : publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? ( + ) : user.userType === UserType.EMBY ? ( {intl.formatMessage(messages.mediaServerUser, { mediaServerName: 'Emby', diff --git a/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx b/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx index 960746ad..20d67265 100644 --- a/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx +++ b/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx @@ -16,7 +16,6 @@ import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline'; import type { UserSettingsGeneralResponse } from '@server/interfaces/api/userSettingsInterfaces'; import axios from 'axios'; import { Field, Form, Formik } from 'formik'; -import getConfig from 'next/config'; import { useRouter } from 'next/router'; import { useEffect, useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; @@ -64,7 +63,6 @@ const messages = defineMessages({ const UserGeneralSettings = () => { const intl = useIntl(); - const { publicRuntimeConfig } = getConfig(); const { addToast } = useToasts(); const { locale, setLocale } = useLocale(); const [movieQuotaEnabled, setMovieQuotaEnabled] = useState(false); @@ -206,7 +204,7 @@ const UserGeneralSettings = () => { {intl.formatMessage(messages.localuser)} - ) : publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? ( + ) : user?.userType === UserType.EMBY ? ( {intl.formatMessage(messages.mediaServerUser, { mediaServerName: 'Emby', diff --git a/src/styles/globals.css b/src/styles/globals.css index 8110e87e..d7176d41 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -60,6 +60,16 @@ background: #f19a30; } + .server-type-button { + @apply rounded-md border border-gray-500 bg-gray-700 px-4 py-2 text-white transition duration-150 ease-in-out hover:bg-gray-500; + } + .jellyfin-server svg { + @apply h-6 w-6; + } + .emby-server svg { + @apply h-7 w-7; + } + ul.cards-vertical, ul.cards-horizontal { @apply grid gap-4;