diff --git a/next.config.js b/next.config.js index 380eb897c..35a316c63 100644 --- a/next.config.js +++ b/next.config.js @@ -6,10 +6,6 @@ module.exports = { commitTag: process.env.COMMIT_TAG || 'local', forceIpv4First: process.env.FORCE_IPV4_FIRST === 'true' ? 'true' : 'false', }, - publicRuntimeConfig: { - // Will be available on both server and client - JELLYFIN_TYPE: process.env.JELLYFIN_TYPE, - }, images: { remotePatterns: [ { hostname: 'gravatar.com' }, diff --git a/overseerr-api.yml b/overseerr-api.yml index 3cb42284c..5cfd163c8 100644 --- a/overseerr-api.yml +++ b/overseerr-api.yml @@ -3586,6 +3586,8 @@ paths: type: string email: type: string + serverType: + type: number required: - username - password diff --git a/server/constants/server.ts b/server/constants/server.ts index 7b2f9f1ff..eed1939f6 100644 --- a/server/constants/server.ts +++ b/server/constants/server.ts @@ -4,3 +4,8 @@ export enum MediaServerType { EMBY, NOT_CONFIGURED, } + +export enum ServerType { + JELLYFIN = 'Jellyfin', + EMBY = 'Emby', +} diff --git a/server/constants/user.ts b/server/constants/user.ts index 5a0a4bd56..90b33dc8d 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 102185be1..723eb213d 100644 --- a/server/entity/Media.ts +++ b/server/entity/Media.ts @@ -211,9 +211,10 @@ class Media { } } else { const pageName = - process.env.JELLYFIN_TYPE === 'emby' ? 'item' : 'details'; + getSettings().main.mediaServerType == MediaServerType.EMBY + ? 'item' + : 'details'; const { serverId, externalHostname } = getSettings().jellyfin; - const jellyfinHost = externalHostname && externalHostname.length > 0 ? externalHostname diff --git a/server/lib/scanners/jellyfin/index.ts b/server/lib/scanners/jellyfin/index.ts index 4ccf54850..f48de70ef 100644 --- a/server/lib/scanners/jellyfin/index.ts +++ b/server/lib/scanners/jellyfin/index.ts @@ -567,7 +567,10 @@ class JellyfinScanner { public async run(): Promise { const settings = getSettings(); - if (settings.main.mediaServerType != MediaServerType.JELLYFIN) { + if ( + settings.main.mediaServerType != MediaServerType.JELLYFIN && + settings.main.mediaServerType != MediaServerType.EMBY + ) { return; } diff --git a/server/lib/settings/migrations/0002_emby_media_server_type.ts b/server/lib/settings/migrations/0002_emby_media_server_type.ts new file mode 100644 index 000000000..2bfd2cda9 --- /dev/null +++ b/server/lib/settings/migrations/0002_emby_media_server_type.ts @@ -0,0 +1,17 @@ +import { MediaServerType } from '@server/constants/server'; +import type { AllSettings } from '@server/lib/settings'; + +const migrateHostname = (settings: any): AllSettings => { + const oldMediaServerType = settings.main.mediaServerType; + console.log('Migrating media server type', oldMediaServerType); + if ( + oldMediaServerType === MediaServerType.JELLYFIN && + process.env.JELLYFIN_TYPE === 'emby' + ) { + settings.main.mediaServerType = MediaServerType.EMBY; + } + + return settings; +}; + +export default migrateHostname; diff --git a/server/routes/auth.ts b/server/routes/auth.ts index 6f01135de..cd931c254 100644 --- a/server/routes/auth.ts +++ b/server/routes/auth.ts @@ -1,7 +1,7 @@ import JellyfinAPI from '@server/api/jellyfin'; import PlexTvAPI from '@server/api/plextv'; import { ApiErrorCode } from '@server/constants/error'; -import { MediaServerType } from '@server/constants/server'; +import { MediaServerType, ServerType } from '@server/constants/server'; import { UserType } from '@server/constants/user'; import { getRepository } from '@server/datasource'; import { User } from '@server/entity/User'; @@ -227,15 +227,20 @@ authRoutes.post('/jellyfin', async (req, res, next) => { urlBase?: string; useSsl?: boolean; email?: string; + serverType?: number; }; - //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.NOT_CONFIGURED + settings.main.mediaServerType !== MediaServerType.EMBY && + settings.main.mediaServerType != MediaServerType.NOT_CONFIGURED && + settings.jellyfin.ip !== '' ) { return res.status(500).json({ error: 'Jellyfin login is disabled' }); - } else if (!body.username) { + } + + if (!body.username) { return res.status(500).json({ error: 'You must provide an username' }); } else if (settings.jellyfin.ip !== '' && body.hostname) { return res @@ -273,7 +278,8 @@ authRoutes.post('/jellyfin', async (req, res, next) => { } // First we need to attempt to log the user in to jellyfin - const jellyfinserver = new JellyfinAPI(hostname, undefined, deviceId); + const jellyfinserver = new JellyfinAPI(hostname ?? '', undefined, deviceId); + const jellyfinHost = externalHostname && externalHostname.length > 0 ? externalHostname @@ -317,22 +323,47 @@ authRoutes.post('/jellyfin', async (req, res, next) => { ); // User doesn't exist, and there are no users in the database, we'll create the user - // with admin permission - settings.main.mediaServerType = MediaServerType.JELLYFIN; - user = new User({ - email: body.email || account.User.Name, - jellyfinUsername: account.User.Name, - jellyfinUserId: account.User.Id, - jellyfinDeviceId: deviceId, - permissions: Permission.ADMIN, - avatar: account.User.PrimaryImageTag - ? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90` - : gravatarUrl(body.email || account.User.Name, { - default: 'mm', - size: 200, - }), - userType: UserType.JELLYFIN, - }); + // with admin permissions + switch (body.serverType) { + case MediaServerType.EMBY: + settings.main.mediaServerType = MediaServerType.EMBY; + user = new User({ + email: body.email || account.User.Name, + jellyfinUsername: account.User.Name, + jellyfinUserId: account.User.Id, + jellyfinDeviceId: deviceId, + jellyfinAuthToken: account.AccessToken, + permissions: Permission.ADMIN, + avatar: account.User.PrimaryImageTag + ? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90` + : gravatarUrl(body.email || account.User.Name, { + default: 'mm', + size: 200, + }), + userType: UserType.EMBY, + }); + break; + case MediaServerType.JELLYFIN: + settings.main.mediaServerType = MediaServerType.JELLYFIN; + user = new User({ + email: body.email || account.User.Name, + jellyfinUsername: account.User.Name, + jellyfinUserId: account.User.Id, + jellyfinDeviceId: deviceId, + jellyfinAuthToken: account.AccessToken, + permissions: Permission.ADMIN, + avatar: account.User.PrimaryImageTag + ? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90` + : gravatarUrl(body.email || account.User.Name, { + default: 'mm', + size: 200, + }), + userType: UserType.JELLYFIN, + }); + break; + default: + throw new Error('select_server_type'); + } // Create an API key on Jellyfin from this admin user const jellyfinClient = new JellyfinAPI( @@ -361,12 +392,12 @@ authRoutes.post('/jellyfin', async (req, res, next) => { logger.info( `Found matching ${ settings.main.mediaServerType === MediaServerType.JELLYFIN - ? 'Jellyfin' - : 'Emby' + ? ServerType.JELLYFIN + : ServerType.EMBY } user; updating user with ${ settings.main.mediaServerType === MediaServerType.JELLYFIN - ? 'Jellyfin' - : 'Emby' + ? ServerType.JELLYFIN + : ServerType.EMBY }`, { label: 'API', @@ -389,12 +420,6 @@ authRoutes.post('/jellyfin', async (req, res, next) => { user.username = ''; } - // TODO: 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( @@ -432,7 +457,10 @@ authRoutes.post('/jellyfin', async (req, res, next) => { default: 'mm', size: 200, }), - userType: UserType.JELLYFIN, + userType: + settings.main.mediaServerType === MediaServerType.JELLYFIN + ? UserType.JELLYFIN + : UserType.EMBY, }); //initialize Jellyfin/Emby users with local login const passedExplicitPassword = body.password && body.password.length > 0; diff --git a/src/assets/services/emby-icon-only.svg b/src/assets/services/emby-icon-only.svg new file mode 100644 index 000000000..e2f2cf2e4 --- /dev/null +++ b/src/assets/services/emby-icon-only.svg @@ -0,0 +1,47 @@ + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/src/assets/services/emby.svg b/src/assets/services/emby.svg index eddc540cd..2aac8662e 100644 --- a/src/assets/services/emby.svg +++ b/src/assets/services/emby.svg @@ -1,46 +1,131 @@ - - - - - - - image/svg+xml - - - - - - - - - - + + + + + + + + \ No newline at end of file diff --git a/src/components/ExternalLinkBlock/index.tsx b/src/components/ExternalLinkBlock/index.tsx index 9782d186c..9199da7d0 100644 --- a/src/components/ExternalLinkBlock/index.tsx +++ b/src/components/ExternalLinkBlock/index.tsx @@ -11,7 +11,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'; @@ -31,7 +30,6 @@ const ExternalLinkBlock = ({ mediaUrl, }: ExternalLinkBlockProps) => { const settings = useSettings(); - const { publicRuntimeConfig } = getConfig(); const { locale } = useLocale(); return ( @@ -45,7 +43,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 a5ec6391a..ca0337e59 100644 --- a/src/components/IssueDetails/index.tsx +++ b/src/components/IssueDetails/index.tsx @@ -28,7 +28,6 @@ import type Issue from '@server/entity/Issue'; import type { MovieDetails } from '@server/models/Movie'; import type { TvDetails } from '@server/models/Tv'; import { Field, Form, Formik } from 'formik'; -import getConfig from 'next/config'; import Image from 'next/image'; import Link from 'next/link'; import { useRouter } from 'next/router'; @@ -108,7 +107,6 @@ const IssueDetails = () => { (opt) => opt.issueType === issueData?.issueType ); const settings = useSettings(); - const { publicRuntimeConfig } = getConfig(); if (!data && !error) { return ; @@ -390,7 +388,8 @@ const IssueDetails = () => { > - {publicRuntimeConfig.JELLYFIN_TYPE == 'emby' + {settings.currentSettings.mediaServerType === + MediaServerType.EMBY ? intl.formatMessage(messages.playonplex, { mediaServerName: 'Emby', }) @@ -437,7 +436,8 @@ const IssueDetails = () => { > - {publicRuntimeConfig.JELLYFIN_TYPE == 'emby' + {settings.currentSettings.mediaServerType === + MediaServerType.EMBY ? intl.formatMessage(messages.play4konplex, { mediaServerName: 'Emby', }) @@ -662,7 +662,8 @@ const IssueDetails = () => { > - {publicRuntimeConfig.JELLYFIN_TYPE == 'emby' + {settings.currentSettings.mediaServerType === + MediaServerType.EMBY ? intl.formatMessage(messages.playonplex, { mediaServerName: 'Emby', }) @@ -708,7 +709,8 @@ const IssueDetails = () => { > - {publicRuntimeConfig.JELLYFIN_TYPE == 'emby' + {settings.currentSettings.mediaServerType === + MediaServerType.EMBY ? intl.formatMessage(messages.play4konplex, { mediaServerName: 'Emby', }) diff --git a/src/components/Login/JellyfinLogin.tsx b/src/components/Login/JellyfinLogin.tsx index dd08e53d3..ba59d11b1 100644 --- a/src/components/Login/JellyfinLogin.tsx +++ b/src/components/Login/JellyfinLogin.tsx @@ -4,9 +4,9 @@ import useSettings from '@app/hooks/useSettings'; import defineMessages from '@app/utils/defineMessages'; import { InformationCircleIcon } from '@heroicons/react/24/solid'; import { ApiErrorCode } from '@server/constants/error'; +import { MediaServerType, ServerType } from '@server/constants/server'; import { Field, Form, Formik } from 'formik'; -import getConfig from 'next/config'; -import { useIntl } from 'react-intl'; +import { FormattedMessage, useIntl } from 'react-intl'; import { useToasts } from 'react-toast-notifications'; import * as Yup from 'yup'; @@ -26,6 +26,7 @@ const messages = defineMessages('components.Login', { validationemailformat: 'Valid email required', validationusernamerequired: 'Username required', validationpasswordrequired: 'Password required', + validationservertyperequired: 'Please select a server type', validationHostnameRequired: 'You must provide a valid hostname or IP address', validationPortRequired: 'You must provide a valid port number', validationUrlTrailingSlash: 'URL must not end in a trailing slash', @@ -40,42 +41,51 @@ const messages = defineMessages('components.Login', { initialsigningin: 'Connecting…', initialsignin: 'Connect', forgotpassword: 'Forgot Password?', + servertype: 'Server Type', + back: 'Go back', }); interface JellyfinLoginProps { revalidate: () => void; initial?: boolean; + serverType?: MediaServerType; + onCancel?: () => void; } const JellyfinLogin: React.FC = ({ revalidate, initial, + serverType, + onCancel, }) => { const toasts = useToasts(); const intl = useIntl(); const settings = useSettings(); - const { publicRuntimeConfig } = getConfig(); + + const mediaServerFormatValues = { + mediaServerName: + serverType === MediaServerType.JELLYFIN + ? ServerType.JELLYFIN + : serverType === MediaServerType.EMBY + ? ServerType.EMBY + : 'Media Server', + }; if (initial) { const LoginSchema = Yup.object().shape({ hostname: Yup.string().required( - intl.formatMessage(messages.validationhostrequired, { - mediaServerName: - publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin', - }) + intl.formatMessage( + messages.validationhostrequired, + mediaServerFormatValues + ) ), port: Yup.number().required( intl.formatMessage(messages.validationPortRequired) ), - urlBase: Yup.string() - .matches( - /^(\/[^/].*[^/]$)/, - intl.formatMessage(messages.validationUrlBaseLeadingSlash) - ) - .matches( - /^(.*[^/])$/, - intl.formatMessage(messages.validationUrlBaseTrailingSlash) - ), + urlBase: Yup.string().matches( + /^(.*[^/])$/, + intl.formatMessage(messages.validationUrlBaseTrailingSlash) + ), email: Yup.string() .email(intl.formatMessage(messages.validationemailformat)) .required(intl.formatMessage(messages.validationemailrequired)), @@ -85,11 +95,6 @@ const JellyfinLogin: React.FC = ({ password: Yup.string(), }); - const mediaServerFormatValues = { - mediaServerName: - publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin', - }; - return ( = ({ validationSchema={LoginSchema} onSubmit={async (values) => { try { + // Check if serverType is either 'Jellyfin' or 'Emby' + // if (serverType !== 'Jellyfin' && serverType !== 'Emby') { + // throw new Error('Invalid serverType'); // You can customize the error message + // } + const res = await fetch('/api/v1/auth/jellyfin', { method: 'POST', headers: { @@ -117,6 +127,7 @@ const JellyfinLogin: React.FC = ({ useSsl: values.useSsl, urlBase: values.urlBase, email: values.email, + serverType: serverType, }), }); if (!res.ok) throw new Error(res.statusText, { cause: res }); @@ -312,7 +323,7 @@ const JellyfinLogin: React.FC = ({
-
+
+ {onCancel && ( + + + + )}
@@ -429,7 +447,8 @@ const JellyfinLogin: React.FC = ({ jellyfinForgotPasswordUrl ? `${jellyfinForgotPasswordUrl}` : `${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 eca7b6acf..7b95b9fcd 100644 --- a/src/components/Login/index.tsx +++ b/src/components/Login/index.tsx @@ -10,7 +10,6 @@ import defineMessages from '@app/utils/defineMessages'; import { Transition } from '@headlessui/react'; import { XCircleIcon } from '@heroicons/react/24/solid'; import { MediaServerType } from '@server/constants/server'; -import getConfig from 'next/config'; import { useRouter } from 'next/dist/client/router'; import Image from 'next/image'; import { useEffect, useState } from 'react'; @@ -34,7 +33,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 @@ -88,6 +86,15 @@ const Login = () => { revalidateOnFocus: false, }); + const mediaServerFormatValues = { + mediaServerName: + settings.currentSettings.mediaServerType === MediaServerType.JELLYFIN + ? 'Jellyfin' + : settings.currentSettings.mediaServerType === MediaServerType.EMBY + ? 'Emby' + : undefined, + }; + return (
@@ -154,12 +161,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 35c8bc1c9..b669ebb43 100644 --- a/src/components/ManageSlideOver/index.tsx +++ b/src/components/ManageSlideOver/index.tsx @@ -26,7 +26,6 @@ import type { MediaWatchDataResponse } from '@server/interfaces/api/mediaInterfa import type { RadarrSettings, SonarrSettings } from '@server/lib/settings'; import type { MovieDetails } from '@server/models/Movie'; import type { TvDetails } from '@server/models/Tv'; -import getConfig from 'next/config'; import Image from 'next/image'; import Link from 'next/link'; import { useIntl } from 'react-intl'; @@ -95,7 +94,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 && @@ -661,7 +659,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 b18d506ce..e4bc991ef 100644 --- a/src/components/MovieDetails/index.tsx +++ b/src/components/MovieDetails/index.tsx @@ -53,7 +53,6 @@ import type { MovieDetails as MovieDetailsType } from '@server/models/Movie'; import { countries } 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'; @@ -126,7 +125,6 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => { const [toggleWatchlist, setToggleWatchlist] = useState( !movie?.onUserWatchlist ); - const { publicRuntimeConfig } = getConfig(); const { addToast } = useToasts(); const { @@ -279,7 +277,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' }); } @@ -291,8 +289,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/Settings/SettingsJellyfin.tsx b/src/components/Settings/SettingsJellyfin.tsx index a627f6d31..316dc48ef 100644 --- a/src/components/Settings/SettingsJellyfin.tsx +++ b/src/components/Settings/SettingsJellyfin.tsx @@ -3,13 +3,14 @@ import Button from '@app/components/Common/Button'; import LoadingSpinner from '@app/components/Common/LoadingSpinner'; import SensitiveInput from '@app/components/Common/SensitiveInput'; import LibraryItem from '@app/components/Settings/LibraryItem'; +import useSettings from '@app/hooks/useSettings'; import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline'; import { ApiErrorCode } from '@server/constants/error'; +import { MediaServerType } from '@server/constants/server'; import type { JellyfinSettings } from '@server/lib/settings'; import { Field, Formik } from 'formik'; -import getConfig from 'next/config'; import { useState } from 'react'; import { FormattedMessage, useIntl } from 'react-intl'; import { useToasts } from 'react-toast-notifications'; @@ -61,6 +62,9 @@ const messages = defineMessages('components.Settings', { validationUrlTrailingSlash: 'URL must not end in a trailing slash', validationUrlBaseLeadingSlash: 'URL base must have a leading slash', validationUrlBaseTrailingSlash: 'URL base must not end in a trailing slash', + tip: 'Tip', + scanbackground: + 'Scanning will run in the background. You can continue the setup process in the meantime.', }); interface Library { @@ -78,13 +82,13 @@ interface SyncStatus { } interface SettingsJellyfinProps { - showAdvancedSettings?: boolean; + isSetupSettings?: boolean; onComplete?: () => void; } const SettingsJellyfin: React.FC = ({ onComplete, - showAdvancedSettings, + isSetupSettings, }) => { const [isSyncing, setIsSyncing] = useState(false); const toasts = useToasts(); @@ -102,7 +106,7 @@ const SettingsJellyfin: React.FC = ({ ); const intl = useIntl(); const { addToast } = useToasts(); - const { publicRuntimeConfig } = getConfig(); + const settings = useSettings(); const JellyfinSettingsSchema = Yup.object().shape({ hostname: Yup.string() @@ -284,26 +288,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 + )}

@@ -340,13 +347,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 + )}

@@ -446,24 +450,26 @@ const SettingsJellyfin: React.FC = ({
+ {isSetupSettings && ( +
+ + {intl.formatMessage(messages.tip)} + + {intl.formatMessage(messages.scanbackground)} +
+ )}

- {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 + )}

= ({ if (!res.ok) throw new Error(res.statusText, { cause: res }); addToast( - intl.formatMessage(messages.jellyfinSettingsSuccess, { - mediaServerName: - publicRuntimeConfig.JELLYFIN_TYPE == 'emby' - ? 'Emby' - : 'Jellyfin', - }), + intl.formatMessage( + messages.jellyfinSettingsSuccess, + mediaServerFormatValues + ), { autoDismiss: true, appearance: 'success', @@ -518,12 +522,10 @@ const SettingsJellyfin: React.FC = ({ } if (errorData?.message === ApiErrorCode.InvalidUrl) { addToast( - intl.formatMessage(messages.invalidurlerror, { - mediaServerName: - publicRuntimeConfig.JELLYFIN_TYPE == 'emby' - ? 'Emby' - : 'Jellyfin', - }), + intl.formatMessage( + messages.invalidurlerror, + mediaServerFormatValues + ), { autoDismiss: true, appearance: 'error', @@ -531,12 +533,10 @@ const SettingsJellyfin: React.FC = ({ ); } else { addToast( - intl.formatMessage(messages.jellyfinSettingsFailure, { - mediaServerName: - publicRuntimeConfig.JELLYFIN_TYPE == 'emby' - ? 'Emby' - : 'Jellyfin', - }), + intl.formatMessage( + messages.jellyfinSettingsFailure, + mediaServerFormatValues + ), { autoDismiss: true, appearance: 'error', @@ -559,7 +559,7 @@ const SettingsJellyfin: React.FC = ({ }) => { return (
- {showAdvancedSettings && ( + {!isSetupSettings && ( <>
- {showAdvancedSettings && ( + {!isSetupSettings && ( <>
-
+
- -
- { - setMediaServerType(MediaServerType.PLEX); - setAuthToken(authToken); - }} - /> -
-
-
- - -
- -
-
-
- + {serverType === MediaServerType.JELLYFIN ? ( + + ) : serverType === MediaServerType.EMBY ? ( + + ) : ( + )} - +
+ {serverType === MediaServerType.PLEX && ( + <> +
+ { + setMediaServerType(MediaServerType.PLEX); + setAuthToken(authToken); + }} + /> +
+
+ +
+ + )} + {serverType === MediaServerType.JELLYFIN && ( + + )} + {serverType === MediaServerType.EMBY && ( + + )}
); }; diff --git a/src/components/Setup/index.tsx b/src/components/Setup/index.tsx index 6ec227c95..936114671 100644 --- a/src/components/Setup/index.tsx +++ b/src/components/Setup/index.tsx @@ -1,5 +1,7 @@ +import EmbyLogo from '@app/assets/services/emby.svg'; +import JellyfinLogo from '@app/assets/services/jellyfin.svg'; +import PlexLogo from '@app/assets/services/plex.svg'; import AppDataWarning from '@app/components/AppDataWarning'; -import Badge from '@app/components/Common/Badge'; import Button from '@app/components/Common/Button'; import ImageFader from '@app/components/Common/ImageFader'; import PageTitle from '@app/components/Common/PageTitle'; @@ -9,26 +11,30 @@ import SettingsPlex from '@app/components/Settings/SettingsPlex'; import SettingsServices from '@app/components/Settings/SettingsServices'; import SetupSteps from '@app/components/Setup/SetupSteps'; import useLocale from '@app/hooks/useLocale'; +import useSettings from '@app/hooks/useSettings'; import defineMessages from '@app/utils/defineMessages'; import { MediaServerType } from '@server/constants/server'; import Image from 'next/image'; import { useRouter } from 'next/router'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { useIntl } from 'react-intl'; import useSWR, { mutate } from 'swr'; import SetupLogin from './SetupLogin'; const messages = defineMessages('components.Setup', { + welcome: 'Welcome to Jellyseerr', + subtitle: 'Get started by choosing your media server', + configjellyfin: 'Configure Jellyfin', + configplex: 'Configure Plex', + configemby: 'Configure Emby', setup: 'Setup', finish: 'Finish Setup', finishing: 'Finishing…', continue: 'Continue', + servertype: 'Choose Server Type', signin: 'Sign In', configuremediaserver: 'Configure Media Server', configureservices: 'Configure Services', - tip: 'Tip', - scanbackground: - 'Scanning will run in the background. You can continue the setup process in the meantime.', }); const Setup = () => { @@ -42,6 +48,7 @@ const Setup = () => { ); const router = useRouter(); const { locale } = useLocale(); + const settings = useSettings(); const finishSetup = async () => { setIsUpdating(true); @@ -76,6 +83,23 @@ const Setup = () => { revalidateOnFocus: false, }); + useEffect(() => { + if (settings.currentSettings.initialized) { + router.push('/'); + } + if ( + settings.currentSettings.mediaServerType !== + MediaServerType.NOT_CONFIGURED + ) { + setCurrentStep(3); + setMediaServerType(settings.currentSettings.mediaServerType); + } + }, [ + settings.currentSettings.mediaServerType, + settings.currentSettings.initialized, + router, + ]); + return (
@@ -101,58 +125,120 @@ const Setup = () => { > 1} /> 2} /> 3} + /> +
{currentStep === 1 && ( +
+
+ {intl.formatMessage(messages.welcome)} +
+
+ {intl.formatMessage(messages.subtitle)} +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ )} + {currentStep === 2 && ( { - setMediaServerType(mServerType); - setCurrentStep(2); + serverType={mediaServerType} + onCancel={() => { + setMediaServerType(MediaServerType.NOT_CONFIGURED); + setCurrentStep(1); }} + onComplete={() => setCurrentStep(3)} /> )} - {currentStep === 2 && ( -
+ {currentStep === 3 && ( +
{mediaServerType === MediaServerType.PLEX ? ( setMediaServerSettingsComplete(true)} /> ) : ( setMediaServerSettingsComplete(true)} /> )} -
- - {intl.formatMessage(messages.tip)} - - {intl.formatMessage(messages.scanbackground)} -
@@ -161,7 +247,7 @@ const Setup = () => {
)} - {currentStep === 3 && ( + {currentStep === 4 && (
diff --git a/src/components/StatusBadge/index.tsx b/src/components/StatusBadge/index.tsx index 0061a903a..1d280d289 100644 --- a/src/components/StatusBadge/index.tsx +++ b/src/components/StatusBadge/index.tsx @@ -9,7 +9,6 @@ import defineMessages from '@app/utils/defineMessages'; 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 { useIntl } from 'react-intl'; const messages = defineMessages('components.StatusBadge', { @@ -48,7 +47,6 @@ const StatusBadge = ({ const intl = useIntl(); const { hasPermission } = useUser(); const settings = useSettings(); - const { publicRuntimeConfig } = getConfig(); let mediaLink: string | undefined; let mediaLinkDescription: string | undefined; @@ -86,7 +84,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 af253f589..634c72d05 100644 --- a/src/components/TvDetails/index.tsx +++ b/src/components/TvDetails/index.tsx @@ -59,7 +59,6 @@ import type { Crew } from '@server/models/common'; import type { TvDetails as TvDetailsType } from '@server/models/Tv'; import { countries } 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'; @@ -126,7 +125,6 @@ const TvDetails = ({ tv }: TvDetailsProps) => { const [toggleWatchlist, setToggleWatchlist] = useState( !tv?.onUserWatchlist ); - const { publicRuntimeConfig } = getConfig(); const { addToast } = useToasts(); const { @@ -300,7 +298,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' }); } @@ -312,15 +310,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' }); } const onClickWatchlistBtn = async (): Promise => { diff --git a/src/components/UserList/JellyfinImportModal.tsx b/src/components/UserList/JellyfinImportModal.tsx index 64ca18616..36dbe0aaa 100644 --- a/src/components/UserList/JellyfinImportModal.tsx +++ b/src/components/UserList/JellyfinImportModal.tsx @@ -3,8 +3,8 @@ import Modal from '@app/components/Common/Modal'; import useSettings from '@app/hooks/useSettings'; import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; +import { MediaServerType } from '@server/constants/server'; import type { UserResultsResponse } from '@server/interfaces/api/userInterfaces'; -import getConfig from 'next/config'; import Image from 'next/image'; import { useState } from 'react'; import { 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([]); @@ -81,7 +80,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, @@ -96,7 +97,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, @@ -134,7 +137,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(); @@ -151,7 +156,8 @@ const JellyfinImportModal: React.FC = ({ ( @@ -277,7 +283,9 @@ const JellyfinImportModal: React.FC = ({ diff --git a/src/components/UserList/index.tsx b/src/components/UserList/index.tsx index 66df469be..7a91a1036 100644 --- a/src/components/UserList/index.tsx +++ b/src/components/UserList/index.tsx @@ -28,7 +28,6 @@ import { MediaServerType } from '@server/constants/server'; import type { UserResultsResponse } from '@server/interfaces/api/userInterfaces'; import { hasPermission } from '@server/lib/permissions'; import { Field, Form, Formik } from 'formik'; -import getConfig from 'next/config'; import Image from 'next/image'; import Link from 'next/link'; import { useRouter } from 'next/router'; @@ -90,7 +89,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'); @@ -535,7 +533,8 @@ const UserList = () => { > - {publicRuntimeConfig.JELLYFIN_TYPE == 'emby' + {settings.currentSettings.mediaServerType === + MediaServerType.EMBY ? intl.formatMessage(messages.importfrommediaserver, { mediaServerName: 'Emby', }) @@ -690,7 +689,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 3bcf1a049..15d960714 100644 --- a/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx +++ b/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx @@ -16,7 +16,6 @@ import defineMessages from '@app/utils/defineMessages'; import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline'; import type { UserSettingsGeneralResponse } from '@server/interfaces/api/userSettingsInterfaces'; import { Field, Form, Formik } from 'formik'; -import getConfig from 'next/config'; import { useRouter } from 'next/router'; import { useEffect, useState } from 'react'; import { useIntl } from 'react-intl'; @@ -69,7 +68,6 @@ const messages = defineMessages( const UserGeneralSettings = () => { const intl = useIntl(); - const { publicRuntimeConfig } = getConfig(); const { addToast } = useToasts(); const { locale, setLocale } = useLocale(); const [movieQuotaEnabled, setMovieQuotaEnabled] = useState(false); @@ -229,7 +227,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/i18n/locale/en.json b/src/i18n/locale/en.json index 5c9aa6fe2..681567b44 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -220,6 +220,7 @@ "components.Layout.VersionStatus.streamdevelop": "Jellyseerr Develop", "components.Layout.VersionStatus.streamstable": "Jellyseerr Stable", "components.Login.adminerror": "You must use an admin account to sign in.", + "components.Login.back": "Go back", "components.Login.credentialerror": "The username or password is incorrect.", "components.Login.description": "Since this is your first time logging into {applicationName}, you are required to add a valid email address.", "components.Login.email": "Email Address", @@ -235,6 +236,7 @@ "components.Login.port": "Port", "components.Login.save": "Add", "components.Login.saving": "Adding…", + "components.Login.servertype": "Server Type", "components.Login.signin": "Sign In", "components.Login.signingin": "Signing In…", "components.Login.signinheader": "Sign in to continue", @@ -256,6 +258,7 @@ "components.Login.validationhostformat": "Valid URL required", "components.Login.validationhostrequired": "{mediaServerName} URL required", "components.Login.validationpasswordrequired": "You must provide a password", + "components.Login.validationservertyperequired": "Please select a server type", "components.Login.validationusernamerequired": "Username required", "components.ManageSlideOver.alltime": "All Time", "components.ManageSlideOver.downloadstatus": "Downloads", @@ -1039,17 +1042,24 @@ "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.webpush": "Web Push", + "components.Setup.back": "Go back", + "components.Setup.configemby": "Configure Emby", + "components.Setup.configjellyfin": "Configure Jellyfin", + "components.Setup.configplex": "Configure Plex", "components.Setup.configuremediaserver": "Configure Media Server", "components.Setup.configureservices": "Configure Services", "components.Setup.continue": "Continue", "components.Setup.finish": "Finish Setup", "components.Setup.finishing": "Finishing…", "components.Setup.scanbackground": "Scanning will run in the background. You can continue the setup process in the meantime.", + "components.Setup.servertype": "Choose Server Type", "components.Setup.setup": "Setup", "components.Setup.signin": "Sign In", "components.Setup.signinMessage": "Get started by signing in", - "components.Setup.signinWithJellyfin": "Use your {mediaServerName} account", - "components.Setup.signinWithPlex": "Use your Plex account", + "components.Setup.signinWithEmby": "Enter your Emby details", + "components.Setup.signinWithJellyfin": "Enter your Jellyfin details", + "components.Setup.signinWithPlex": "Enter your Plex details", + "components.Setup.subtitle": "Get started by choosing your media server", "components.Setup.tip": "Tip", "components.Setup.welcome": "Welcome to Jellyseerr", "components.StatusBadge.managemedia": "Manage {mediaType}", diff --git a/src/pages/settings/jellyfin.tsx b/src/pages/settings/jellyfin.tsx index 8f1377daf..2490c8635 100644 --- a/src/pages/settings/jellyfin.tsx +++ b/src/pages/settings/jellyfin.tsx @@ -8,7 +8,7 @@ const JellyfinSettingsPage: NextPage = () => { useRouteGuard(Permission.MANAGE_SETTINGS); return ( - + ); }; diff --git a/src/styles/globals.css b/src/styles/globals.css index 66c023d9a..1e99d53df 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -83,6 +83,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;