feat(jellyfinapi): add API key field to Jellyfin settings
This commit is contained in:
@@ -93,9 +93,7 @@ export interface JellyfinLibraryItemExtended extends JellyfinLibraryItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class JellyfinAPI extends ExternalAPI {
|
class JellyfinAPI extends ExternalAPI {
|
||||||
private authToken?: string;
|
|
||||||
private userId?: string;
|
private userId?: string;
|
||||||
private jellyfinHost: string;
|
|
||||||
|
|
||||||
constructor(jellyfinHost: string, authToken?: string, deviceId?: string) {
|
constructor(jellyfinHost: string, authToken?: string, deviceId?: string) {
|
||||||
let authHeaderVal: string;
|
let authHeaderVal: string;
|
||||||
@@ -114,9 +112,6 @@ class JellyfinAPI extends ExternalAPI {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
this.jellyfinHost = jellyfinHost;
|
|
||||||
this.authToken = authToken;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async login(
|
public async login(
|
||||||
|
|||||||
@@ -334,6 +334,14 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
userType: UserType.JELLYFIN,
|
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();
|
const serverName = await jellyfinserver.getServerName();
|
||||||
|
|
||||||
settings.jellyfin.name = serverName;
|
settings.jellyfin.name = serverName;
|
||||||
@@ -342,6 +350,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
settings.jellyfin.port = body.port ?? 8096;
|
settings.jellyfin.port = body.port ?? 8096;
|
||||||
settings.jellyfin.urlBase = body.urlBase ?? '';
|
settings.jellyfin.urlBase = body.urlBase ?? '';
|
||||||
settings.jellyfin.useSsl = body.useSsl ?? false;
|
settings.jellyfin.useSsl = body.useSsl ?? false;
|
||||||
|
settings.jellyfin.apiKey = apiKey;
|
||||||
settings.save();
|
settings.save();
|
||||||
startJobs();
|
startJobs();
|
||||||
|
|
||||||
|
|||||||
@@ -270,7 +270,7 @@ settingsRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
|
|
||||||
const jellyfinClient = new JellyfinAPI(
|
const jellyfinClient = new JellyfinAPI(
|
||||||
getHostname(tempJellyfinSettings),
|
getHostname(tempJellyfinSettings),
|
||||||
settings.jellyfin.apiKey,
|
tempJellyfinSettings.apiKey,
|
||||||
admin.jellyfinDeviceId ?? ''
|
admin.jellyfinDeviceId ?? ''
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import Badge from '@app/components/Common/Badge';
|
import Badge from '@app/components/Common/Badge';
|
||||||
import Button from '@app/components/Common/Button';
|
import Button from '@app/components/Common/Button';
|
||||||
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
||||||
|
import SensitiveInput from '@app/components/Common/SensitiveInput';
|
||||||
import LibraryItem from '@app/components/Settings/LibraryItem';
|
import LibraryItem from '@app/components/Settings/LibraryItem';
|
||||||
import globalMessages from '@app/i18n/globalMessages';
|
import globalMessages from '@app/i18n/globalMessages';
|
||||||
import defineMessages from '@app/utils/defineMessages';
|
import defineMessages from '@app/utils/defineMessages';
|
||||||
@@ -30,13 +31,14 @@ const messages = defineMessages('components.Settings', {
|
|||||||
jellyfinSettingsSuccess: '{mediaServerName} settings saved successfully!',
|
jellyfinSettingsSuccess: '{mediaServerName} settings saved successfully!',
|
||||||
jellyfinSettings: '{mediaServerName} Settings',
|
jellyfinSettings: '{mediaServerName} Settings',
|
||||||
jellyfinSettingsDescription:
|
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',
|
externalUrl: 'External URL',
|
||||||
hostname: 'Hostname or IP Address',
|
hostname: 'Hostname or IP Address',
|
||||||
port: 'Port',
|
port: 'Port',
|
||||||
enablessl: 'Use SSL',
|
enablessl: 'Use SSL',
|
||||||
urlBase: 'URL Base',
|
urlBase: 'URL Base',
|
||||||
jellyfinForgotPasswordUrl: 'Forgot Password URL',
|
jellyfinForgotPasswordUrl: 'Forgot Password URL',
|
||||||
|
apiKey: 'API key',
|
||||||
jellyfinSyncFailedNoLibrariesFound: 'No libraries were found',
|
jellyfinSyncFailedNoLibrariesFound: 'No libraries were found',
|
||||||
jellyfinSyncFailedAutomaticGroupedFolders:
|
jellyfinSyncFailedAutomaticGroupedFolders:
|
||||||
'Custom authentication with Automatic Library Grouping not supported',
|
'Custom authentication with Automatic Library Grouping not supported',
|
||||||
@@ -444,119 +446,121 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{showAdvancedSettings && (
|
<div className="mt-10 mb-6">
|
||||||
<>
|
<h3 className="heading">
|
||||||
<div className="mt-10 mb-6">
|
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||||
<h3 className="heading">
|
? intl.formatMessage(messages.jellyfinSettings, {
|
||||||
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
mediaServerName: 'Emby',
|
||||||
? intl.formatMessage(messages.jellyfinSettings, {
|
})
|
||||||
mediaServerName: 'Emby',
|
: intl.formatMessage(messages.jellyfinSettings, {
|
||||||
})
|
mediaServerName: 'Jellyfin',
|
||||||
: intl.formatMessage(messages.jellyfinSettings, {
|
})}
|
||||||
mediaServerName: 'Jellyfin',
|
</h3>
|
||||||
})}
|
<p className="description">
|
||||||
</h3>
|
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||||
<p className="description">
|
? intl.formatMessage(messages.jellyfinSettingsDescription, {
|
||||||
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
mediaServerName: 'Emby',
|
||||||
? intl.formatMessage(messages.jellyfinSettingsDescription, {
|
})
|
||||||
mediaServerName: 'Emby',
|
: intl.formatMessage(messages.jellyfinSettingsDescription, {
|
||||||
})
|
mediaServerName: 'Jellyfin',
|
||||||
: intl.formatMessage(messages.jellyfinSettingsDescription, {
|
})}
|
||||||
mediaServerName: 'Jellyfin',
|
</p>
|
||||||
})}
|
</div>
|
||||||
</p>
|
<Formik
|
||||||
</div>
|
initialValues={{
|
||||||
<Formik
|
hostname: data?.ip,
|
||||||
initialValues={{
|
port: data?.port ?? 8096,
|
||||||
hostname: data?.ip,
|
useSsl: data?.useSsl,
|
||||||
port: data?.port ?? 8096,
|
urlBase: data?.urlBase || '',
|
||||||
useSsl: data?.useSsl,
|
jellyfinExternalUrl: data?.externalHostname || '',
|
||||||
urlBase: data?.urlBase || '',
|
jellyfinForgotPasswordUrl: data?.jellyfinForgotPasswordUrl || '',
|
||||||
jellyfinExternalUrl: data?.externalHostname || '',
|
apiKey: data?.apiKey,
|
||||||
jellyfinForgotPasswordUrl: data?.jellyfinForgotPasswordUrl || '',
|
}}
|
||||||
}}
|
validationSchema={JellyfinSettingsSchema}
|
||||||
validationSchema={JellyfinSettingsSchema}
|
onSubmit={async (values) => {
|
||||||
onSubmit={async (values) => {
|
try {
|
||||||
try {
|
const res = await fetch('/api/v1/settings/jellyfin', {
|
||||||
const res = await fetch('/api/v1/settings/jellyfin', {
|
method: 'POST',
|
||||||
method: 'POST',
|
headers: {
|
||||||
headers: {
|
'Content-Type': 'application/json',
|
||||||
'Content-Type': 'application/json',
|
},
|
||||||
},
|
body: JSON.stringify({
|
||||||
body: JSON.stringify({
|
ip: values.hostname,
|
||||||
ip: values.hostname,
|
port: Number(values.port),
|
||||||
port: Number(values.port),
|
useSsl: values.useSsl,
|
||||||
useSsl: values.useSsl,
|
urlBase: values.urlBase,
|
||||||
urlBase: values.urlBase,
|
externalHostname: values.jellyfinExternalUrl,
|
||||||
externalHostname: values.jellyfinExternalUrl,
|
jellyfinForgotPasswordUrl: values.jellyfinForgotPasswordUrl,
|
||||||
jellyfinForgotPasswordUrl: values.jellyfinForgotPasswordUrl,
|
apiKey: values.apiKey,
|
||||||
} as JellyfinSettings),
|
} as JellyfinSettings),
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error(res.statusText, { cause: res });
|
if (!res.ok) throw new Error(res.statusText, { cause: res });
|
||||||
|
|
||||||
addToast(
|
addToast(
|
||||||
intl.formatMessage(messages.jellyfinSettingsSuccess, {
|
intl.formatMessage(messages.jellyfinSettingsSuccess, {
|
||||||
mediaServerName:
|
mediaServerName:
|
||||||
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||||
? 'Emby'
|
? 'Emby'
|
||||||
: 'Jellyfin',
|
: 'Jellyfin',
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
autoDismiss: true,
|
autoDismiss: true,
|
||||||
appearance: 'success',
|
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();
|
|
||||||
}
|
}
|
||||||
}}
|
);
|
||||||
>
|
} catch (e) {
|
||||||
{({
|
let errorData;
|
||||||
errors,
|
try {
|
||||||
touched,
|
errorData = await e.cause?.text();
|
||||||
values,
|
errorData = JSON.parse(errorData);
|
||||||
setFieldValue,
|
} catch {
|
||||||
handleSubmit,
|
/* empty */
|
||||||
isSubmitting,
|
}
|
||||||
isValid,
|
if (errorData?.message === ApiErrorCode.InvalidUrl) {
|
||||||
}) => {
|
addToast(
|
||||||
return (
|
intl.formatMessage(messages.invalidurlerror, {
|
||||||
<form className="section" onSubmit={handleSubmit}>
|
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 (
|
||||||
|
<form className="section" onSubmit={handleSubmit}>
|
||||||
|
{showAdvancedSettings && (
|
||||||
|
<>
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<label htmlFor="hostname" className="text-label">
|
<label htmlFor="hostname" className="text-label">
|
||||||
{intl.formatMessage(messages.hostname)}
|
{intl.formatMessage(messages.hostname)}
|
||||||
@@ -638,75 +642,92 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-row">
|
</>
|
||||||
<label htmlFor="jellyfinExternalUrl" className="text-label">
|
)}
|
||||||
{intl.formatMessage(messages.externalUrl)}
|
<div className="form-row">
|
||||||
</label>
|
<label htmlFor="jellyfinExternalUrl" className="text-label">
|
||||||
<div className="form-input-area">
|
{intl.formatMessage(messages.externalUrl)}
|
||||||
<div className="form-input-field">
|
</label>
|
||||||
<Field
|
<div className="form-input-area">
|
||||||
type="text"
|
<div className="form-input-field">
|
||||||
inputMode="url"
|
<Field
|
||||||
id="jellyfinExternalUrl"
|
type="text"
|
||||||
name="jellyfinExternalUrl"
|
inputMode="url"
|
||||||
/>
|
id="jellyfinExternalUrl"
|
||||||
</div>
|
name="jellyfinExternalUrl"
|
||||||
{errors.jellyfinExternalUrl &&
|
/>
|
||||||
touched.jellyfinExternalUrl && (
|
|
||||||
<div className="error">
|
|
||||||
{errors.jellyfinExternalUrl}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-row">
|
{errors.jellyfinExternalUrl &&
|
||||||
<label
|
touched.jellyfinExternalUrl && (
|
||||||
htmlFor="jellyfinForgotPasswordUrl"
|
<div className="error">{errors.jellyfinExternalUrl}</div>
|
||||||
className="text-label"
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<label
|
||||||
|
htmlFor="jellyfinForgotPasswordUrl"
|
||||||
|
className="text-label"
|
||||||
|
>
|
||||||
|
{intl.formatMessage(messages.jellyfinForgotPasswordUrl)}
|
||||||
|
</label>
|
||||||
|
<div className="form-input-area">
|
||||||
|
<div className="form-input-field">
|
||||||
|
<Field
|
||||||
|
type="text"
|
||||||
|
inputMode="url"
|
||||||
|
id="jellyfinForgotPasswordUrl"
|
||||||
|
name="jellyfinForgotPasswordUrl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{errors.jellyfinForgotPasswordUrl &&
|
||||||
|
touched.jellyfinForgotPasswordUrl && (
|
||||||
|
<div className="error">
|
||||||
|
{errors.jellyfinForgotPasswordUrl}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<label htmlFor="apiKey" className="text-label">
|
||||||
|
{intl.formatMessage(messages.apiKey)}
|
||||||
|
</label>
|
||||||
|
<div className="form-input-area">
|
||||||
|
<div className="form-input-field">
|
||||||
|
<SensitiveInput
|
||||||
|
as="field"
|
||||||
|
type="text"
|
||||||
|
inputMode="url"
|
||||||
|
id="apiKey"
|
||||||
|
name="apiKey"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{errors.apiKey && touched.apiKey && (
|
||||||
|
<div className="error">{errors.apiKey}</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}
|
||||||
>
|
>
|
||||||
{intl.formatMessage(messages.jellyfinForgotPasswordUrl)}
|
<ArrowDownOnSquareIcon />
|
||||||
</label>
|
<span>
|
||||||
<div className="form-input-area">
|
{isSubmitting
|
||||||
<div className="form-input-field">
|
? intl.formatMessage(globalMessages.saving)
|
||||||
<Field
|
: intl.formatMessage(globalMessages.save)}
|
||||||
type="text"
|
|
||||||
inputMode="url"
|
|
||||||
id="jellyfinForgotPasswordUrl"
|
|
||||||
name="jellyfinForgotPasswordUrl"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{errors.jellyfinForgotPasswordUrl &&
|
|
||||||
touched.jellyfinForgotPasswordUrl && (
|
|
||||||
<div className="error">
|
|
||||||
{errors.jellyfinForgotPasswordUrl}
|
|
||||||
</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}
|
|
||||||
>
|
|
||||||
<ArrowDownOnSquareIcon />
|
|
||||||
<span>
|
|
||||||
{isSubmitting
|
|
||||||
? intl.formatMessage(globalMessages.saving)
|
|
||||||
: intl.formatMessage(globalMessages.save)}
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</Button>
|
||||||
</div>
|
</span>
|
||||||
</form>
|
</div>
|
||||||
);
|
</div>
|
||||||
}}
|
</form>
|
||||||
</Formik>
|
);
|
||||||
</>
|
}}
|
||||||
)}
|
</Formik>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -950,7 +950,7 @@
|
|||||||
"components.Settings.is4k": "4K",
|
"components.Settings.is4k": "4K",
|
||||||
"components.Settings.jellyfinForgotPasswordUrl": "Forgot Password URL",
|
"components.Settings.jellyfinForgotPasswordUrl": "Forgot Password URL",
|
||||||
"components.Settings.jellyfinSettings": "{mediaServerName} Settings",
|
"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.jellyfinSettingsFailure": "Something went wrong while saving {mediaServerName} settings.",
|
||||||
"components.Settings.jellyfinSettingsSuccess": "{mediaServerName} settings saved successfully!",
|
"components.Settings.jellyfinSettingsSuccess": "{mediaServerName} settings saved successfully!",
|
||||||
"components.Settings.jellyfinSyncFailedAutomaticGroupedFolders": "Custom authentication with Automatic Library Grouping not supported",
|
"components.Settings.jellyfinSyncFailedAutomaticGroupedFolders": "Custom authentication with Automatic Library Grouping not supported",
|
||||||
|
|||||||
Reference in New Issue
Block a user