From 9fbc4074e491bbeba7880fd54c99d4e3c95c7d01 Mon Sep 17 00:00:00 2001 From: notfakie <103784113+notfakie@users.noreply.github.com> Date: Wed, 27 Apr 2022 08:06:41 +1200 Subject: [PATCH] feat: allow Jellyfin to set a playback URL different to the Jellyfin host specified during setup --- overseerr-api.yml | 5 +- server/entity/Media.ts | 10 +- server/lib/settings.ts | 2 + src/components/Settings/SettingsJellyfin.tsx | 138 ++++++++++++++++--- src/components/Setup/index.tsx | 1 + src/pages/settings/jellyfin.tsx | 2 +- 6 files changed, 136 insertions(+), 22 deletions(-) diff --git a/overseerr-api.yml b/overseerr-api.yml index c8797c820..cd7034b09 100644 --- a/overseerr-api.yml +++ b/overseerr-api.yml @@ -333,6 +333,9 @@ components: hostname: type: string example: 'http://my.jellyfin.host' + externalHostname: + type: string + example: 'http://my.jellyfin.host' adminUser: type: string example: 'admin' @@ -347,8 +350,6 @@ components: serverID: type: string readOnly: true - required: - - hostname TautulliSettings: type: object properties: diff --git a/server/entity/Media.ts b/server/entity/Media.ts index 77357b0fc..e0cadeef4 100644 --- a/server/entity/Media.ts +++ b/server/entity/Media.ts @@ -191,12 +191,16 @@ class Media { } else { const pageName = process.env.JELLYFIN_TYPE === 'emby' ? 'item' : 'details'; - const { hostname, serverId } = getSettings().jellyfin; + const { serverId, hostname, externalHostname } = getSettings().jellyfin; + const jellyfinHost = + externalHostname && externalHostname.length > 0 + ? externalHostname + : hostname; if (this.jellyfinMediaId) { - this.mediaUrl = `${hostname}/web/index.html#!/${pageName}?id=${this.jellyfinMediaId}&context=home&serverId=${serverId}`; + this.mediaUrl = `${jellyfinHost}/web/index.html#!/${pageName}?id=${this.jellyfinMediaId}&context=home&serverId=${serverId}`; } if (this.jellyfinMediaId4k) { - this.mediaUrl4k = `${hostname}/web/index.html#!/${pageName}?id=${this.jellyfinMediaId4k}&context=home&serverId=${serverId}`; + this.mediaUrl4k = `${jellyfinHost}/web/index.html#!/${pageName}?id=${this.jellyfinMediaId4k}&context=home&serverId=${serverId}`; } } } diff --git a/server/lib/settings.ts b/server/lib/settings.ts index b963c715e..501f9d432 100644 --- a/server/lib/settings.ts +++ b/server/lib/settings.ts @@ -39,6 +39,7 @@ export interface PlexSettings { export interface JellyfinSettings { name: string; hostname?: string; + externalHostname?: string; libraries: Library[]; serverId: string; } @@ -319,6 +320,7 @@ class Settings { jellyfin: { name: '', hostname: '', + externalHostname: '', libraries: [], serverId: '', }, diff --git a/src/components/Settings/SettingsJellyfin.tsx b/src/components/Settings/SettingsJellyfin.tsx index a9249eac2..9740762ce 100644 --- a/src/components/Settings/SettingsJellyfin.tsx +++ b/src/components/Settings/SettingsJellyfin.tsx @@ -1,8 +1,13 @@ +import { SaveIcon } from '@heroicons/react/outline'; import axios from 'axios'; +import { Field, Formik } from 'formik'; import React, { useState } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; +import { useToasts } from 'react-toast-notifications'; import useSWR from 'swr'; -import type { JellyfinSettings } from '../../../server/lib/settings'; +import * as Yup from 'yup'; +import { JellyfinSettings } from '../../../server/lib/settings'; +import globalMessages from '../../i18n/globalMessages'; import Badge from '../Common/Badge'; import Button from '../Common/Button'; import LoadingSpinner from '../Common/LoadingSpinner'; @@ -11,18 +16,26 @@ import LibraryItem from './LibraryItem'; const messages = defineMessages({ jellyfinsettings: 'Jellyfin Settings', jellyfinsettingsDescription: - 'Configure the settings for your Jellyfin server. Overseerr scans your Jellyfin libraries to see what content is available.', + 'Configure the settings for your Jellyfin server. Jellyfin scans your Jellyfin libraries to see what content is available.', timeout: 'Timeout', save: 'Save Changes', saving: 'Saving…', jellyfinlibraries: 'Jellyfin Libraries', jellyfinlibrariesDescription: - 'The libraries Overseerr scans for titles. Click the button below if no libraries are listed.', + 'The libraries Jellyfin scans for titles. Click the button below if no libraries are listed.', + jellyfinSettingsFailure: + 'Something went wrong while saving Jellyfin settings.', + jellyfinSettingsSuccess: 'Jellyfin settings saved successfully!', + jellyfinSettings: 'Jellyfin Settings', + jellyfinSettingsDescription: + 'Optionally configure an external player endpoint for your jellyfin server that is different to the internal URL used during setup', + externalUrl: 'External URL', + validationUrl: 'You must provide a valid URL', syncing: 'Syncing', syncJellyfin: 'Sync Libraries', manualscanJellyfin: 'Manual Library Scan', manualscanDescriptionJellyfin: - "Normally, this will only be run once every 24 hours. Overseerr will check your Jellyfin server's recently added more aggressively. If this is your first time configuring Jellyfin, a one-time full manual library scan is recommended!", + "Normally, this will only be run once every 24 hours. Jellyfin will check your Jellyfin server's recently added more aggressively. If this is your first time configuring Jellyfin, a one-time full manual library scan is recommended!", notrunning: 'Not Running', currentlibrary: 'Current Library: {name}', librariesRemaining: 'Libraries Remaining: {count}', @@ -44,26 +57,36 @@ interface SyncStatus { libraries: Library[]; } interface SettingsJellyfinProps { + showAdvancedSettings?: boolean; onComplete?: () => void; } -const SettingsJellyfin: React.FC = ({ onComplete }) => { +const SettingsJellyfin: React.FC = ({ + onComplete, + showAdvancedSettings, +}) => { const [isSyncing, setIsSyncing] = useState(false); const { - data: data, - error: error, - // revalidate: revalidate, + data, + error, + mutate: revalidate, } = useSWR('/api/v1/settings/jellyfin'); - const revalidate = () => undefined; //TODO - const revalidateSync = () => undefined; //TODO - - const { - data: dataSync, //, revalidate: revalidateSync - } = useSWR('/api/v1/settings/jellyfin/sync', { - refreshInterval: 1000, - }); + const { data: dataSync, mutate: revalidateSync } = useSWR( + '/api/v1/settings/jellyfin/sync', + { + refreshInterval: 1000, + } + ); const intl = useIntl(); + const { addToast } = useToasts(); + + const JellyfinSettingsSchema = Yup.object().shape({ + jellyfinExternalUrl: Yup.string().matches( + /^(?:(?:(?:https?):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*\.?)(?::\d{2,5})?(?:[/?#]\S*)?$/, + intl.formatMessage(messages.validationUrl) + ), + }); const activeLibraries = data?.libraries @@ -278,6 +301,89 @@ const SettingsJellyfin: React.FC = ({ onComplete }) => { + {showAdvancedSettings && ( + <> +
+

+ {intl.formatMessage(messages.jellyfinSettings)} +

+

+ {intl.formatMessage(messages.jellyfinSettingsDescription)} +

+
+ { + try { + await axios.post('/api/v1/settings/jellyfin', { + externalHostname: values.jellyfinExternalUrl, + } as JellyfinSettings); + + addToast(intl.formatMessage(messages.jellyfinSettingsSuccess), { + autoDismiss: true, + appearance: 'success', + }); + } catch (e) { + addToast(intl.formatMessage(messages.jellyfinSettingsFailure), { + autoDismiss: true, + appearance: 'error', + }); + } finally { + revalidate(); + } + }} + > + {({ errors, touched, handleSubmit, isSubmitting, isValid }) => { + return ( +
+
+ +
+
+ +
+ {errors.jellyfinExternalUrl && + touched.jellyfinExternalUrl && ( +
+ {errors.jellyfinExternalUrl} +
+ )} +
+
+
+
+ + + +
+
+
+ ); + }} +
+ + )} ); }; diff --git a/src/components/Setup/index.tsx b/src/components/Setup/index.tsx index c4c5ad091..6acdc4d49 100644 --- a/src/components/Setup/index.tsx +++ b/src/components/Setup/index.tsx @@ -128,6 +128,7 @@ const Setup: React.FC = () => { /> ) : ( setMediaServerSettingsComplete(true)} /> )} diff --git a/src/pages/settings/jellyfin.tsx b/src/pages/settings/jellyfin.tsx index b3c7f6f17..5c5acea4f 100644 --- a/src/pages/settings/jellyfin.tsx +++ b/src/pages/settings/jellyfin.tsx @@ -9,7 +9,7 @@ const JellyfinSettingsPage: NextPage = () => { useRouteGuard(Permission.MANAGE_SETTINGS); return ( - + ); };