From 3aab8e94e3661dbbafa2815122b09615bd0609d8 Mon Sep 17 00:00:00 2001 From: Gauthier Date: Thu, 8 Aug 2024 11:34:15 +0200 Subject: [PATCH] feat(jellyfinapi): add API key field to Jellyfin settings --- server/api/jellyfin.ts | 5 - server/routes/auth.ts | 9 + server/routes/settings/index.ts | 2 +- src/components/Settings/SettingsJellyfin.tsx | 377 ++++++++++--------- src/i18n/locale/en.json | 2 +- 5 files changed, 210 insertions(+), 185 deletions(-) diff --git a/server/api/jellyfin.ts b/server/api/jellyfin.ts index e5283517..f6550347 100644 --- a/server/api/jellyfin.ts +++ b/server/api/jellyfin.ts @@ -93,9 +93,7 @@ export interface JellyfinLibraryItemExtended extends JellyfinLibraryItem { } class JellyfinAPI extends ExternalAPI { - private authToken?: string; private userId?: string; - private jellyfinHost: string; constructor(jellyfinHost: string, authToken?: string, deviceId?: string) { let authHeaderVal: string; @@ -114,9 +112,6 @@ class JellyfinAPI extends ExternalAPI { }, } ); - - this.jellyfinHost = jellyfinHost; - this.authToken = authToken; } public async login( diff --git a/server/routes/auth.ts b/server/routes/auth.ts index e859f744..6f01135d 100644 --- a/server/routes/auth.ts +++ b/server/routes/auth.ts @@ -334,6 +334,14 @@ authRoutes.post('/jellyfin', async (req, res, next) => { userType: UserType.JELLYFIN, }); + // Create an API key on Jellyfin from this admin user + const jellyfinClient = new JellyfinAPI( + hostname, + account.AccessToken, + deviceId + ); + const apiKey = await jellyfinClient.createApiToken('Jellyseerr'); + const serverName = await jellyfinserver.getServerName(); settings.jellyfin.name = serverName; @@ -342,6 +350,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => { settings.jellyfin.port = body.port ?? 8096; settings.jellyfin.urlBase = body.urlBase ?? ''; settings.jellyfin.useSsl = body.useSsl ?? false; + settings.jellyfin.apiKey = apiKey; settings.save(); startJobs(); diff --git a/server/routes/settings/index.ts b/server/routes/settings/index.ts index 1bdb31d7..30898d2a 100644 --- a/server/routes/settings/index.ts +++ b/server/routes/settings/index.ts @@ -270,7 +270,7 @@ settingsRoutes.post('/jellyfin', async (req, res, next) => { const jellyfinClient = new JellyfinAPI( getHostname(tempJellyfinSettings), - settings.jellyfin.apiKey, + tempJellyfinSettings.apiKey, admin.jellyfinDeviceId ?? '' ); diff --git a/src/components/Settings/SettingsJellyfin.tsx b/src/components/Settings/SettingsJellyfin.tsx index 91c44e12..74f75714 100644 --- a/src/components/Settings/SettingsJellyfin.tsx +++ b/src/components/Settings/SettingsJellyfin.tsx @@ -1,6 +1,7 @@ import Badge from '@app/components/Common/Badge'; 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 globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; @@ -30,13 +31,14 @@ const messages = defineMessages('components.Settings', { jellyfinSettingsSuccess: '{mediaServerName} settings saved successfully!', jellyfinSettings: '{mediaServerName} Settings', jellyfinSettingsDescription: - 'Optionally configure the internal and external endpoints for your {mediaServerName} server. In most cases, the external URL is different to the internal URL. A custom password reset URL can also be set for {mediaServerName} login, in case you would like to redirect to a different password reset page.', + 'Optionally configure the internal and external endpoints for your {mediaServerName} server. In most cases, the external URL is different to the internal URL. A custom password reset URL can also be set for {mediaServerName} login, in case you would like to redirect to a different password reset page. You can also change the Jellyfin API key, which was automatically generated previously.', externalUrl: 'External URL', hostname: 'Hostname or IP Address', port: 'Port', enablessl: 'Use SSL', urlBase: 'URL Base', jellyfinForgotPasswordUrl: 'Forgot Password URL', + apiKey: 'API key', jellyfinSyncFailedNoLibrariesFound: 'No libraries were found', jellyfinSyncFailedAutomaticGroupedFolders: 'Custom authentication with Automatic Library Grouping not supported', @@ -444,119 +446,121 @@ const SettingsJellyfin: React.FC = ({ - {showAdvancedSettings && ( - <> -
-

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

-

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

-
- { - try { - const res = await fetch('/api/v1/settings/jellyfin', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - ip: values.hostname, - port: Number(values.port), - useSsl: values.useSsl, - urlBase: values.urlBase, - externalHostname: values.jellyfinExternalUrl, - jellyfinForgotPasswordUrl: values.jellyfinForgotPasswordUrl, - } as JellyfinSettings), - }); - if (!res.ok) throw new Error(res.statusText, { cause: res }); +
+

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

+

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

+
+ { + try { + const res = await fetch('/api/v1/settings/jellyfin', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + ip: values.hostname, + port: Number(values.port), + useSsl: values.useSsl, + urlBase: values.urlBase, + externalHostname: values.jellyfinExternalUrl, + jellyfinForgotPasswordUrl: values.jellyfinForgotPasswordUrl, + apiKey: values.apiKey, + } as JellyfinSettings), + }); + if (!res.ok) throw new Error(res.statusText, { cause: res }); - addToast( - intl.formatMessage(messages.jellyfinSettingsSuccess, { - mediaServerName: - publicRuntimeConfig.JELLYFIN_TYPE == 'emby' - ? 'Emby' - : 'Jellyfin', - }), - { - autoDismiss: true, - appearance: 'success', - } - ); - } catch (e) { - let errorData; - try { - errorData = await e.cause?.text(); - errorData = JSON.parse(errorData); - } catch { - /* empty */ - } - if (errorData?.message === ApiErrorCode.InvalidUrl) { - addToast( - intl.formatMessage(messages.invalidurlerror, { - mediaServerName: - publicRuntimeConfig.JELLYFIN_TYPE == 'emby' - ? 'Emby' - : 'Jellyfin', - }), - { - autoDismiss: true, - appearance: 'error', - } - ); - } else { - addToast( - intl.formatMessage(messages.jellyfinSettingsFailure, { - mediaServerName: - publicRuntimeConfig.JELLYFIN_TYPE == 'emby' - ? 'Emby' - : 'Jellyfin', - }), - { - autoDismiss: true, - appearance: 'error', - } - ); - } - } finally { - revalidate(); + addToast( + intl.formatMessage(messages.jellyfinSettingsSuccess, { + mediaServerName: + publicRuntimeConfig.JELLYFIN_TYPE == 'emby' + ? 'Emby' + : 'Jellyfin', + }), + { + autoDismiss: true, + appearance: 'success', } - }} - > - {({ - errors, - touched, - values, - setFieldValue, - handleSubmit, - isSubmitting, - isValid, - }) => { - return ( -
+ ); + } catch (e) { + let errorData; + try { + errorData = await e.cause?.text(); + errorData = JSON.parse(errorData); + } catch { + /* empty */ + } + if (errorData?.message === ApiErrorCode.InvalidUrl) { + addToast( + intl.formatMessage(messages.invalidurlerror, { + mediaServerName: + publicRuntimeConfig.JELLYFIN_TYPE == 'emby' + ? 'Emby' + : 'Jellyfin', + }), + { + autoDismiss: true, + appearance: 'error', + } + ); + } else { + addToast( + intl.formatMessage(messages.jellyfinSettingsFailure, { + mediaServerName: + publicRuntimeConfig.JELLYFIN_TYPE == 'emby' + ? 'Emby' + : 'Jellyfin', + }), + { + autoDismiss: true, + appearance: 'error', + } + ); + } + } finally { + revalidate(); + } + }} + > + {({ + errors, + touched, + values, + setFieldValue, + handleSubmit, + isSubmitting, + isValid, + }) => { + return ( + + {showAdvancedSettings && ( + <>
-
- -
-
- -
- {errors.jellyfinExternalUrl && - touched.jellyfinExternalUrl && ( -
- {errors.jellyfinExternalUrl} -
- )} -
+ + )} +
+ +
+
+
-
-
+ )} +
+
+
+ +
+
+ +
+ {errors.jellyfinForgotPasswordUrl && + touched.jellyfinForgotPasswordUrl && ( +
+ {errors.jellyfinForgotPasswordUrl} +
+ )} +
+
+
+ +
+
+ +
+ {errors.apiKey && touched.apiKey && ( +
{errors.apiKey}
+ )} +
+
+
+
+ +
-
-
- - + + + {isSubmitting + ? intl.formatMessage(globalMessages.saving) + : intl.formatMessage(globalMessages.save)} -
-
- - ); - }} - - - )} + + +
+
+ + ); + }} +
); }; diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index a7bf780c..017835d4 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -950,7 +950,7 @@ "components.Settings.is4k": "4K", "components.Settings.jellyfinForgotPasswordUrl": "Forgot Password URL", "components.Settings.jellyfinSettings": "{mediaServerName} Settings", - "components.Settings.jellyfinSettingsDescription": "Optionally configure the internal and external endpoints for your {mediaServerName} server. In most cases, the external URL is different to the internal URL. A custom password reset URL can also be set for {mediaServerName} login, in case you would like to redirect to a different password reset page.", + "components.Settings.jellyfinSettingsDescription": "Optionally configure the internal and external endpoints for your {mediaServerName} server. In most cases, the external URL is different to the internal URL. A custom password reset URL can also be set for {mediaServerName} login, in case you would like to redirect to a different password reset page. You can also change the Jellyfin API key, which was automatically generated previously.", "components.Settings.jellyfinSettingsFailure": "Something went wrong while saving {mediaServerName} settings.", "components.Settings.jellyfinSettingsSuccess": "{mediaServerName} settings saved successfully!", "components.Settings.jellyfinSyncFailedAutomaticGroupedFolders": "Custom authentication with Automatic Library Grouping not supported",