mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2026-01-01 04:08:45 -05:00
feat: allow Jellyfin to set a playback URL different to the Jellyfin host specified during setup
This commit is contained in:
@@ -333,6 +333,9 @@ components:
|
|||||||
hostname:
|
hostname:
|
||||||
type: string
|
type: string
|
||||||
example: 'http://my.jellyfin.host'
|
example: 'http://my.jellyfin.host'
|
||||||
|
externalHostname:
|
||||||
|
type: string
|
||||||
|
example: 'http://my.jellyfin.host'
|
||||||
adminUser:
|
adminUser:
|
||||||
type: string
|
type: string
|
||||||
example: 'admin'
|
example: 'admin'
|
||||||
@@ -347,8 +350,6 @@ components:
|
|||||||
serverID:
|
serverID:
|
||||||
type: string
|
type: string
|
||||||
readOnly: true
|
readOnly: true
|
||||||
required:
|
|
||||||
- hostname
|
|
||||||
TautulliSettings:
|
TautulliSettings:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
|||||||
@@ -191,12 +191,16 @@ class Media {
|
|||||||
} else {
|
} else {
|
||||||
const pageName =
|
const pageName =
|
||||||
process.env.JELLYFIN_TYPE === 'emby' ? 'item' : 'details';
|
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) {
|
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) {
|
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}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export interface PlexSettings {
|
|||||||
export interface JellyfinSettings {
|
export interface JellyfinSettings {
|
||||||
name: string;
|
name: string;
|
||||||
hostname?: string;
|
hostname?: string;
|
||||||
|
externalHostname?: string;
|
||||||
libraries: Library[];
|
libraries: Library[];
|
||||||
serverId: string;
|
serverId: string;
|
||||||
}
|
}
|
||||||
@@ -319,6 +320,7 @@ class Settings {
|
|||||||
jellyfin: {
|
jellyfin: {
|
||||||
name: '',
|
name: '',
|
||||||
hostname: '',
|
hostname: '',
|
||||||
|
externalHostname: '',
|
||||||
libraries: [],
|
libraries: [],
|
||||||
serverId: '',
|
serverId: '',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
|
import { SaveIcon } from '@heroicons/react/outline';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import { Field, Formik } from 'formik';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
import { useToasts } from 'react-toast-notifications';
|
||||||
import useSWR from 'swr';
|
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 Badge from '../Common/Badge';
|
||||||
import Button from '../Common/Button';
|
import Button from '../Common/Button';
|
||||||
import LoadingSpinner from '../Common/LoadingSpinner';
|
import LoadingSpinner from '../Common/LoadingSpinner';
|
||||||
@@ -11,18 +16,26 @@ import LibraryItem from './LibraryItem';
|
|||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
jellyfinsettings: 'Jellyfin Settings',
|
jellyfinsettings: 'Jellyfin Settings',
|
||||||
jellyfinsettingsDescription:
|
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',
|
timeout: 'Timeout',
|
||||||
save: 'Save Changes',
|
save: 'Save Changes',
|
||||||
saving: 'Saving…',
|
saving: 'Saving…',
|
||||||
jellyfinlibraries: 'Jellyfin Libraries',
|
jellyfinlibraries: 'Jellyfin Libraries',
|
||||||
jellyfinlibrariesDescription:
|
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',
|
syncing: 'Syncing',
|
||||||
syncJellyfin: 'Sync Libraries',
|
syncJellyfin: 'Sync Libraries',
|
||||||
manualscanJellyfin: 'Manual Library Scan',
|
manualscanJellyfin: 'Manual Library Scan',
|
||||||
manualscanDescriptionJellyfin:
|
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',
|
notrunning: 'Not Running',
|
||||||
currentlibrary: 'Current Library: {name}',
|
currentlibrary: 'Current Library: {name}',
|
||||||
librariesRemaining: 'Libraries Remaining: {count}',
|
librariesRemaining: 'Libraries Remaining: {count}',
|
||||||
@@ -44,26 +57,36 @@ interface SyncStatus {
|
|||||||
libraries: Library[];
|
libraries: Library[];
|
||||||
}
|
}
|
||||||
interface SettingsJellyfinProps {
|
interface SettingsJellyfinProps {
|
||||||
|
showAdvancedSettings?: boolean;
|
||||||
onComplete?: () => void;
|
onComplete?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({ onComplete }) => {
|
const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
|
||||||
|
onComplete,
|
||||||
|
showAdvancedSettings,
|
||||||
|
}) => {
|
||||||
const [isSyncing, setIsSyncing] = useState(false);
|
const [isSyncing, setIsSyncing] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: data,
|
data,
|
||||||
error: error,
|
error,
|
||||||
// revalidate: revalidate,
|
mutate: revalidate,
|
||||||
} = useSWR<JellyfinSettings>('/api/v1/settings/jellyfin');
|
} = useSWR<JellyfinSettings>('/api/v1/settings/jellyfin');
|
||||||
const revalidate = () => undefined; //TODO
|
const { data: dataSync, mutate: revalidateSync } = useSWR<SyncStatus>(
|
||||||
const revalidateSync = () => undefined; //TODO
|
'/api/v1/settings/jellyfin/sync',
|
||||||
|
{
|
||||||
const {
|
refreshInterval: 1000,
|
||||||
data: dataSync, //, revalidate: revalidateSync
|
}
|
||||||
} = useSWR<SyncStatus>('/api/v1/settings/jellyfin/sync', {
|
);
|
||||||
refreshInterval: 1000,
|
|
||||||
});
|
|
||||||
const intl = useIntl();
|
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 =
|
const activeLibraries =
|
||||||
data?.libraries
|
data?.libraries
|
||||||
@@ -278,6 +301,89 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({ onComplete }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ const Setup: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<SettingsJellyfin
|
<SettingsJellyfin
|
||||||
|
showAdvancedSettings={false}
|
||||||
onComplete={() => setMediaServerSettingsComplete(true)}
|
onComplete={() => setMediaServerSettingsComplete(true)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const JellyfinSettingsPage: NextPage = () => {
|
|||||||
useRouteGuard(Permission.MANAGE_SETTINGS);
|
useRouteGuard(Permission.MANAGE_SETTINGS);
|
||||||
return (
|
return (
|
||||||
<SettingsLayout>
|
<SettingsLayout>
|
||||||
<SettingsJellyfin />
|
<SettingsJellyfin showAdvancedSettings={true} />
|
||||||
</SettingsLayout>
|
</SettingsLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user