feat: allow Jellyfin to set a playback URL different to the Jellyfin host specified during setup

This commit is contained in:
notfakie
2022-04-27 08:06:41 +12:00
parent 9e2f3f0639
commit 9fbc4074e4
6 changed files with 136 additions and 22 deletions

View File

@@ -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:

View File

@@ -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}`;
}
}
}

View File

@@ -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: '',
},

View File

@@ -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<SettingsJellyfinProps> = ({ onComplete }) => {
const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
onComplete,
showAdvancedSettings,
}) => {
const [isSyncing, setIsSyncing] = useState(false);
const {
data: data,
error: error,
// revalidate: revalidate,
data,
error,
mutate: revalidate,
} = useSWR<JellyfinSettings>('/api/v1/settings/jellyfin');
const revalidate = () => undefined; //TODO
const revalidateSync = () => undefined; //TODO
const {
data: dataSync, //, revalidate: revalidateSync
} = useSWR<SyncStatus>('/api/v1/settings/jellyfin/sync', {
refreshInterval: 1000,
});
const { data: dataSync, mutate: revalidateSync } = useSWR<SyncStatus>(
'/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<SettingsJellyfinProps> = ({ onComplete }) => {
</div>
</div>
</div>
{showAdvancedSettings && (
<>
<div className="mt-10 mb-6">
<h3 className="heading">
{intl.formatMessage(messages.jellyfinSettings)}
</h3>
<p className="description">
{intl.formatMessage(messages.jellyfinSettingsDescription)}
</p>
</div>
<Formik
initialValues={{
jellyfinExternalUrl: data?.externalHostname || '',
}}
validationSchema={JellyfinSettingsSchema}
onSubmit={async (values) => {
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 (
<form className="section" onSubmit={handleSubmit}>
<div className="form-row">
<label htmlFor="jellyfinExternalUrl" className="text-label">
{intl.formatMessage(messages.externalUrl)}
</label>
<div className="form-input-area">
<div className="form-input-field">
<Field
type="text"
inputMode="url"
id="jellyfinExternalUrl"
name="jellyfinExternalUrl"
/>
</div>
{errors.jellyfinExternalUrl &&
touched.jellyfinExternalUrl && (
<div className="error">
{errors.jellyfinExternalUrl}
</div>
)}
</div>
</div>
<div className="actions">
<div className="flex justify-end">
<span className="ml-3 inline-flex rounded-md shadow-sm">
<Button
buttonType="primary"
type="submit"
disabled={isSubmitting || !isValid}
>
<SaveIcon />
<span>
{isSubmitting
? intl.formatMessage(globalMessages.saving)
: intl.formatMessage(globalMessages.save)}
</span>
</Button>
</span>
</div>
</div>
</form>
);
}}
</Formik>
</>
)}
</>
);
};

View File

@@ -128,6 +128,7 @@ const Setup: React.FC = () => {
/>
) : (
<SettingsJellyfin
showAdvancedSettings={false}
onComplete={() => setMediaServerSettingsComplete(true)}
/>
)}

View File

@@ -9,7 +9,7 @@ const JellyfinSettingsPage: NextPage = () => {
useRouteGuard(Permission.MANAGE_SETTINGS);
return (
<SettingsLayout>
<SettingsJellyfin />
<SettingsJellyfin showAdvancedSettings={true} />
</SettingsLayout>
);
};