diff --git a/cypress/config/settings.cypress.json b/cypress/config/settings.cypress.json index 7a4bbef5d..45e38a29e 100644 --- a/cypress/config/settings.cypress.json +++ b/cypress/config/settings.cypress.json @@ -19,6 +19,7 @@ "region": "", "originalLanguage": "", "trustProxy": false, + "mediaServerType": 1, "partialRequestsEnabled": true, "locale": "en" }, @@ -37,6 +38,17 @@ ], "machineId": "test" }, + "jellyfin": { + "name": "", + "ip": "", + "port": 8096, + "useSsl": false, + "urlBase": "", + "externalHostname": "", + "jellyfinForgotPasswordUrl": "", + "libraries": [], + "serverId": "" + }, "tautulli": {}, "radarr": [], "sonarr": [], @@ -139,11 +151,26 @@ "sonarr-scan": { "schedule": "0 30 4 * * *" }, + "plex-watchlist-sync": { + "schedule": "0 */10 * * * *" + }, + "availability-sync": { + "schedule": "0 0 5 * * *" + }, "download-sync": { "schedule": "0 * * * * *" }, "download-sync-reset": { "schedule": "0 0 1 * * *" + }, + "jellyfin-recently-added-scan": { + "schedule": "0 */5 * * * *" + }, + "jellyfin-full-scan": { + "schedule": "0 0 3 * * *" + }, + "image-cache-cleanup": { + "schedule": "0 0 5 * * *" } } } diff --git a/server/api/jellyfin.ts b/server/api/jellyfin.ts index 81b505f11..6c72ad577 100644 --- a/server/api/jellyfin.ts +++ b/server/api/jellyfin.ts @@ -184,6 +184,16 @@ class JellyfinAPI extends ExternalAPI { return; } + public async getSystemInfo(): Promise { + try { + const systemInfoResponse = await this.get('/System/Info'); + + return systemInfoResponse; + } catch (e) { + throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken); + } + } + public async getServerName(): Promise { try { const serverResponse = await this.get( diff --git a/server/constants/error.ts b/server/constants/error.ts index 22b9ad60a..ac18c3ec8 100644 --- a/server/constants/error.ts +++ b/server/constants/error.ts @@ -3,5 +3,7 @@ export enum ApiErrorCode { InvalidCredentials = 'INVALID_CREDENTIALS', InvalidAuthToken = 'INVALID_AUTH_TOKEN', NotAdmin = 'NOT_ADMIN', + SyncErrorGroupedFolders = 'SYNC_ERROR_GROUPED_FOLDERS', + SyncErrorNoLibraries = 'SYNC_ERROR_NO_LIBRARIES', Unknown = 'UNKNOWN', } diff --git a/server/entity/Media.ts b/server/entity/Media.ts index 1932670e4..102185be1 100644 --- a/server/entity/Media.ts +++ b/server/entity/Media.ts @@ -9,6 +9,7 @@ import type { DownloadingItem } from '@server/lib/downloadtracker'; import downloadTracker from '@server/lib/downloadtracker'; import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; +import { getHostname } from '@server/utils/getHostname'; import { AfterLoad, Column, @@ -211,15 +212,12 @@ class Media { } else { const pageName = process.env.JELLYFIN_TYPE === 'emby' ? 'item' : 'details'; - const { serverId, hostname, externalHostname } = getSettings().jellyfin; - let jellyfinHost = + const { serverId, externalHostname } = getSettings().jellyfin; + + const jellyfinHost = externalHostname && externalHostname.length > 0 ? externalHostname - : hostname; - - jellyfinHost = jellyfinHost.endsWith('/') - ? jellyfinHost.slice(0, -1) - : jellyfinHost; + : getHostname(); if (this.jellyfinMediaId) { this.mediaUrl = `${jellyfinHost}/web/index.html#!/${pageName}?id=${this.jellyfinMediaId}&context=home&serverId=${serverId}`; diff --git a/server/lib/availabilitySync.ts b/server/lib/availabilitySync.ts index 8b37bc85e..1aa37cf9a 100644 --- a/server/lib/availabilitySync.ts +++ b/server/lib/availabilitySync.ts @@ -16,6 +16,7 @@ import { User } from '@server/entity/User'; import type { RadarrSettings, SonarrSettings } from '@server/lib/settings'; import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; +import { getHostname } from '@server/utils/getHostname'; class AvailabilitySync { public running = false; @@ -84,7 +85,7 @@ class AvailabilitySync { ) { if (admin) { this.jellyfinClient = new JellyfinAPI( - settings.jellyfin.hostname ?? '', + getHostname(), admin.jellyfinAuthToken, admin.jellyfinDeviceId ); diff --git a/server/lib/scanners/jellyfin/index.ts b/server/lib/scanners/jellyfin/index.ts index 8007e6ef3..fa7cdb225 100644 --- a/server/lib/scanners/jellyfin/index.ts +++ b/server/lib/scanners/jellyfin/index.ts @@ -12,6 +12,7 @@ import type { Library } from '@server/lib/settings'; import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; import AsyncLock from '@server/utils/asyncLock'; +import { getHostname } from '@server/utils/getHostname'; import { randomUUID as uuid } from 'crypto'; import { uniqWith } from 'lodash'; @@ -594,8 +595,10 @@ class JellyfinScanner { return this.log('No admin configured. Jellyfin sync skipped.', 'warn'); } + const hostname = getHostname(); + this.jfClient = new JellyfinAPI( - settings.jellyfin.hostname ?? '', + hostname, admin.jellyfinAuthToken, admin.jellyfinDeviceId ); diff --git a/server/lib/settings.ts b/server/lib/settings/index.ts similarity index 97% rename from server/lib/settings.ts rename to server/lib/settings/index.ts index 63f952363..ad613cc30 100644 --- a/server/lib/settings.ts +++ b/server/lib/settings/index.ts @@ -1,10 +1,11 @@ import { MediaServerType } from '@server/constants/server'; +import { Permission } from '@server/lib/permissions'; +import { runMigrations } from '@server/lib/settings/migrator'; import { randomUUID } from 'crypto'; import fs from 'fs'; import { merge } from 'lodash'; import path from 'path'; import webpush from 'web-push'; -import { Permission } from './permissions'; export interface Library { id: string; @@ -38,7 +39,10 @@ export interface PlexSettings { export interface JellyfinSettings { name: string; - hostname: string; + ip: string; + port: number; + useSsl?: boolean; + urlBase?: string; externalHostname?: string; jellyfinForgotPasswordUrl?: string; libraries: Library[]; @@ -130,7 +134,6 @@ interface FullPublicSettings extends PublicSettings { region: string; originalLanguage: string; mediaServerType: number; - jellyfinHost?: string; jellyfinExternalHost?: string; jellyfinForgotPasswordUrl?: string; jellyfinServerName?: string; @@ -274,7 +277,7 @@ export type JobId = | 'image-cache-cleanup' | 'availability-sync'; -interface AllSettings { +export interface AllSettings { clientId: string; vapidPublic: string; vapidPrivate: string; @@ -291,7 +294,7 @@ interface AllSettings { const SETTINGS_PATH = process.env.CONFIG_DIRECTORY ? `${process.env.CONFIG_DIRECTORY}/settings.json` - : path.join(__dirname, '../../config/settings.json'); + : path.join(__dirname, '../../../config/settings.json'); class Settings { private data: AllSettings; @@ -331,7 +334,10 @@ class Settings { }, jellyfin: { name: '', - hostname: '', + ip: '', + port: 8096, + useSsl: false, + urlBase: '', externalHostname: '', jellyfinForgotPasswordUrl: '', libraries: [], @@ -547,8 +553,6 @@ class Settings { region: this.data.main.region, originalLanguage: this.data.main.originalLanguage, mediaServerType: this.main.mediaServerType, - jellyfinHost: this.jellyfin.hostname, - jellyfinExternalHost: this.jellyfin.externalHostname, partialRequestsEnabled: this.data.main.partialRequestsEnabled, cacheImages: this.data.main.cacheImages, vapidPublic: this.vapidPublic, @@ -637,7 +641,11 @@ class Settings { const data = fs.readFileSync(SETTINGS_PATH, 'utf-8'); if (data) { - this.data = merge(this.data, JSON.parse(data)); + const parsedJson = JSON.parse(data); + this.data = runMigrations(parsedJson); + + this.data = merge(this.data, parsedJson); + this.save(); } return this; diff --git a/server/lib/settings/migrations/0001_migrate_hostname.ts b/server/lib/settings/migrations/0001_migrate_hostname.ts new file mode 100644 index 000000000..c514ac2db --- /dev/null +++ b/server/lib/settings/migrations/0001_migrate_hostname.ts @@ -0,0 +1,30 @@ +import type { AllSettings } from '@server/lib/settings'; + +const migrateHostname = (settings: any): AllSettings => { + const oldJellyfinSettings = settings.jellyfin; + if (oldJellyfinSettings && oldJellyfinSettings.hostname) { + const { hostname } = oldJellyfinSettings; + const protocolMatch = hostname.match(/^(https?):\/\//i); + const useSsl = protocolMatch && protocolMatch[1].toLowerCase() === 'https'; + const remainingUrl = hostname.replace(/^(https?):\/\//i, ''); + const urlMatch = remainingUrl.match(/^([^:]+)(:([0-9]+))?(\/.*)?$/); + + delete oldJellyfinSettings.hostname; + if (urlMatch) { + const [, ip, , port, urlBase] = urlMatch; + settings.jellyfin = { + ...settings.jellyfin, + ip, + port: port || (useSsl ? 443 : 80), + useSsl, + urlBase: urlBase ? urlBase.replace(/\/$/, '') : '', + }; + } + } + if (settings.jellyfin && settings.jellyfin.hostname) { + delete settings.jellyfin.hostname; + } + return settings; +}; + +export default migrateHostname; diff --git a/server/lib/settings/migrator.ts b/server/lib/settings/migrator.ts new file mode 100644 index 000000000..9d709590d --- /dev/null +++ b/server/lib/settings/migrator.ts @@ -0,0 +1,21 @@ +import type { AllSettings } from '@server/lib/settings'; +import fs from 'fs'; +import path from 'path'; + +const migrationsDir = path.join(__dirname, 'migrations'); + +export const runMigrations = (settings: AllSettings): AllSettings => { + const migrations = fs + .readdirSync(migrationsDir) + .filter((file) => file.endsWith('.js') || file.endsWith('.ts')) + // eslint-disable-next-line @typescript-eslint/no-var-requires + .map((file) => require(path.join(migrationsDir, file)).default); + + let migrated = settings; + + for (const migration of migrations) { + migrated = migration(migrated); + } + + return migrated; +}; diff --git a/server/routes/auth.ts b/server/routes/auth.ts index 52c63ff29..3b0d7e382 100644 --- a/server/routes/auth.ts +++ b/server/routes/auth.ts @@ -11,6 +11,7 @@ import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; import { isAuthenticated } from '@server/middleware/auth'; import { ApiError } from '@server/types/error'; +import { getHostname } from '@server/utils/getHostname'; import * as EmailValidator from 'email-validator'; import { Router } from 'express'; import gravatarUrl from 'gravatar-url'; @@ -222,30 +223,39 @@ authRoutes.post('/jellyfin', async (req, res, next) => { username?: string; password?: string; hostname?: string; + port?: number; + urlBase?: string; + useSsl?: boolean; email?: string; }; //Make sure jellyfin login is enabled, but only if jellyfin is not already configured if ( settings.main.mediaServerType !== MediaServerType.JELLYFIN && - settings.jellyfin.hostname !== '' + settings.main.mediaServerType != MediaServerType.NOT_CONFIGURED ) { return res.status(500).json({ error: 'Jellyfin login is disabled' }); } else if (!body.username) { return res.status(500).json({ error: 'You must provide an username' }); - } else if (settings.jellyfin.hostname !== '' && body.hostname) { + } else if (settings.jellyfin.ip !== '' && body.hostname) { return res .status(500) .json({ error: 'Jellyfin hostname already configured' }); - } else if (settings.jellyfin.hostname === '' && !body.hostname) { + } else if (settings.jellyfin.ip === '' && !body.hostname) { return res.status(500).json({ error: 'No hostname provided.' }); } try { const hostname = - settings.jellyfin.hostname !== '' - ? settings.jellyfin.hostname - : body.hostname ?? ''; + settings.jellyfin.ip !== '' + ? getHostname() + : getHostname({ + useSsl: body.useSsl, + ip: body.hostname, + port: body.port, + urlBase: body.urlBase, + }); + const { externalHostname } = getSettings().jellyfin; // Try to find deviceId that corresponds to jellyfin user, else generate a new one @@ -261,17 +271,14 @@ authRoutes.post('/jellyfin', async (req, res, next) => { 'base64' ); } + // First we need to attempt to log the user in to jellyfin - const jellyfinserver = new JellyfinAPI(hostname ?? '', undefined, deviceId); - let jellyfinHost = + const jellyfinserver = new JellyfinAPI(hostname, undefined, deviceId); + const jellyfinHost = externalHostname && externalHostname.length > 0 ? externalHostname : hostname; - jellyfinHost = jellyfinHost.endsWith('/') - ? jellyfinHost.slice(0, -1) - : jellyfinHost; - const ip = req.ip; let clientIp; @@ -328,8 +335,11 @@ authRoutes.post('/jellyfin', async (req, res, next) => { const serverName = await jellyfinserver.getServerName(); settings.jellyfin.name = serverName; - settings.jellyfin.hostname = body.hostname ?? ''; settings.jellyfin.serverId = account.User.ServerId; + settings.jellyfin.ip = body.hostname ?? ''; + settings.jellyfin.port = body.port ?? 8096; + settings.jellyfin.urlBase = body.urlBase ?? ''; + settings.jellyfin.useSsl = body.useSsl ?? false; settings.save(); startJobs(); @@ -444,7 +454,12 @@ authRoutes.post('/jellyfin', async (req, res, next) => { label: 'Auth', error: e.errorCode, status: e.statusCode, - hostname: body.hostname, + hostname: getHostname({ + useSsl: body.useSsl, + ip: body.hostname, + port: body.port, + urlBase: body.urlBase, + }), } ); return next({ diff --git a/server/routes/settings/index.ts b/server/routes/settings/index.ts index 41821dcac..64fd83a61 100644 --- a/server/routes/settings/index.ts +++ b/server/routes/settings/index.ts @@ -2,6 +2,7 @@ import JellyfinAPI from '@server/api/jellyfin'; import PlexAPI from '@server/api/plexapi'; import PlexTvAPI from '@server/api/plextv'; import TautulliAPI from '@server/api/tautulli'; +import { ApiErrorCode } from '@server/constants/error'; import { getRepository } from '@server/datasource'; import Media from '@server/entity/Media'; import { MediaRequest } from '@server/entity/MediaRequest'; @@ -24,8 +25,10 @@ import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; import { isAuthenticated } from '@server/middleware/auth'; import discoverSettingRoutes from '@server/routes/settings/discover'; +import { ApiError } from '@server/types/error'; import { appDataPath } from '@server/utils/appDataVolume'; import { getAppVersion } from '@server/utils/appVersion'; +import { getHostname } from '@server/utils/getHostname'; import { Router } from 'express'; import rateLimit from 'express-rate-limit'; import fs from 'fs'; @@ -252,11 +255,59 @@ settingsRoutes.get('/jellyfin', (_req, res) => { res.status(200).json(settings.jellyfin); }); -settingsRoutes.post('/jellyfin', (req, res) => { +settingsRoutes.post('/jellyfin', async (req, res, next) => { + const userRepository = getRepository(User); const settings = getSettings(); - settings.jellyfin = merge(settings.jellyfin, req.body); - settings.save(); + try { + const admin = await userRepository.findOneOrFail({ + where: { id: 1 }, + select: ['id', 'jellyfinAuthToken', 'jellyfinUserId', 'jellyfinDeviceId'], + order: { id: 'ASC' }, + }); + + const tempJellyfinSettings = { ...settings.jellyfin, ...req.body }; + + const jellyfinClient = new JellyfinAPI( + getHostname(tempJellyfinSettings), + admin.jellyfinAuthToken ?? '', + admin.jellyfinDeviceId ?? '' + ); + + const result = await jellyfinClient.getSystemInfo(); + + if (!result?.Id) { + throw new ApiError(result?.status, ApiErrorCode.InvalidUrl); + } + + Object.assign(settings.jellyfin, req.body); + settings.jellyfin.serverId = result.Id; + settings.jellyfin.name = result.ServerName; + settings.save(); + } catch (e) { + if (e instanceof ApiError) { + logger.error('Something went wrong testing Jellyfin connection', { + label: 'API', + status: e.statusCode, + errorMessage: ApiErrorCode.InvalidUrl, + }); + + return next({ + status: e.statusCode, + message: ApiErrorCode.InvalidUrl, + }); + } else { + logger.error('Something went wrong', { + label: 'API', + errorMessage: e.message, + }); + + return next({ + status: e.statusCode ?? 500, + message: ApiErrorCode.Unknown, + }); + } + } return res.status(200).json(settings.jellyfin); }); @@ -272,7 +323,7 @@ settingsRoutes.get('/jellyfin/library', async (req, res, next) => { order: { id: 'ASC' }, }); const jellyfinClient = new JellyfinAPI( - settings.jellyfin.hostname ?? '', + getHostname(), admin.jellyfinAuthToken ?? '', admin.jellyfinDeviceId ?? '' ); @@ -288,10 +339,13 @@ settingsRoutes.get('/jellyfin/library', async (req, res, next) => { // Automatic Library grouping is not supported when user views are used to get library if (account.Configuration.GroupedFolders.length > 0) { - return next({ status: 501, message: 'SYNC_ERROR_GROUPED_FOLDERS' }); + return next({ + status: 501, + message: ApiErrorCode.SyncErrorGroupedFolders, + }); } - return next({ status: 404, message: 'SYNC_ERROR_NO_LIBRARIES' }); + return next({ status: 404, message: ApiErrorCode.SyncErrorNoLibraries }); } const newLibraries: Library[] = libraries.map((library) => { @@ -322,16 +376,12 @@ settingsRoutes.get('/jellyfin/library', async (req, res, next) => { }); settingsRoutes.get('/jellyfin/users', async (req, res) => { - const settings = getSettings(); - const { hostname, externalHostname } = getSettings().jellyfin; - let jellyfinHost = + const { externalHostname } = getSettings().jellyfin; + const jellyfinHost = externalHostname && externalHostname.length > 0 ? externalHostname - : hostname; + : getHostname(); - jellyfinHost = jellyfinHost.endsWith('/') - ? jellyfinHost.slice(0, -1) - : jellyfinHost; const userRepository = getRepository(User); const admin = await userRepository.findOneOrFail({ select: ['id', 'jellyfinAuthToken', 'jellyfinDeviceId', 'jellyfinUserId'], @@ -339,7 +389,6 @@ settingsRoutes.get('/jellyfin/users', async (req, res) => { order: { id: 'ASC' }, }); const jellyfinClient = new JellyfinAPI( - settings.jellyfin.hostname ?? '', admin.jellyfinAuthToken ?? '', admin.jellyfinDeviceId ?? '' ); diff --git a/server/routes/user/index.ts b/server/routes/user/index.ts index 789c90765..6b0953e68 100644 --- a/server/routes/user/index.ts +++ b/server/routes/user/index.ts @@ -20,6 +20,7 @@ import { hasPermission, Permission } from '@server/lib/permissions'; import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; import { isAuthenticated } from '@server/middleware/auth'; +import { getHostname } from '@server/utils/getHostname'; import { Router } from 'express'; import gravatarUrl from 'gravatar-url'; import { findIndex, sortBy } from 'lodash'; @@ -496,7 +497,6 @@ router.post( order: { id: 'ASC' }, }); const jellyfinClient = new JellyfinAPI( - settings.jellyfin.hostname ?? '', admin.jellyfinAuthToken ?? '', admin.jellyfinDeviceId ?? '' ); @@ -504,15 +504,14 @@ router.post( //const jellyfinUsersResponse = await jellyfinClient.getUsers(); const createdUsers: User[] = []; - const { hostname, externalHostname } = getSettings().jellyfin; - let jellyfinHost = + const { externalHostname } = getSettings().jellyfin; + const hostname = getHostname(); + + const jellyfinHost = externalHostname && externalHostname.length > 0 ? externalHostname : hostname; - jellyfinHost = jellyfinHost.endsWith('/') - ? jellyfinHost.slice(0, -1) - : jellyfinHost; jellyfinClient.setUserId(admin.jellyfinUserId ?? ''); const jellyfinUsers = await jellyfinClient.getUsers(); diff --git a/server/utils/getHostname.ts b/server/utils/getHostname.ts new file mode 100644 index 000000000..9fa110cd1 --- /dev/null +++ b/server/utils/getHostname.ts @@ -0,0 +1,18 @@ +import { getSettings } from '@server/lib/settings'; + +interface HostnameParams { + useSsl?: boolean; + ip?: string; + port?: number; + urlBase?: string; +} + +export const getHostname = (params?: HostnameParams): string => { + const settings = params ? params : getSettings().jellyfin; + + const { useSsl, ip, port, urlBase } = settings; + + const hostname = `${useSsl ? 'https' : 'http'}://${ip}:${port}${urlBase}`; + + return hostname; +}; diff --git a/src/components/Login/JellyfinLogin.tsx b/src/components/Login/JellyfinLogin.tsx index 7403392e9..d3945be54 100644 --- a/src/components/Login/JellyfinLogin.tsx +++ b/src/components/Login/JellyfinLogin.tsx @@ -14,7 +14,10 @@ import * as Yup from 'yup'; const messages = defineMessages({ username: 'Username', password: 'Password', - host: '{mediaServerName} URL', + hostname: '{mediaServerName} URL', + port: 'Port', + enablessl: 'Use SSL', + urlBase: 'URL Base', email: 'Email', emailtooltip: 'Address does not need to be associated with your {mediaServerName} instance.', @@ -24,6 +27,11 @@ const messages = defineMessages({ validationemailformat: 'Valid email required', validationusernamerequired: 'Username required', validationpasswordrequired: 'Password required', + validationHostnameRequired: 'You must provide a valid hostname or IP address', + validationPortRequired: 'You must provide a valid port number', + validationUrlTrailingSlash: 'URL must not end in a trailing slash', + validationUrlBaseLeadingSlash: 'URL base must have a leading slash', + validationUrlBaseTrailingSlash: 'URL base must not end in a trailing slash', loginerror: 'Something went wrong while trying to sign in.', adminerror: 'You must use an admin account to sign in.', credentialerror: 'The username or password is incorrect.', @@ -51,16 +59,23 @@ const JellyfinLogin: React.FC = ({ if (initial) { const LoginSchema = Yup.object().shape({ - host: Yup.string() + hostname: Yup.string().required( + intl.formatMessage(messages.validationhostrequired, { + mediaServerName: + publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin', + }) + ), + port: Yup.number().required( + intl.formatMessage(messages.validationPortRequired) + ), + urlBase: 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.validationhostformat) + /^(\/[^/].*[^/]$)/, + intl.formatMessage(messages.validationUrlBaseLeadingSlash) ) - .required( - intl.formatMessage(messages.validationhostrequired, { - mediaServerName: - publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin', - }) + .matches( + /^(.*[^/])$/, + intl.formatMessage(messages.validationUrlBaseTrailingSlash) ), email: Yup.string() .email(intl.formatMessage(messages.validationemailformat)) @@ -75,12 +90,16 @@ const JellyfinLogin: React.FC = ({ mediaServerName: publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin', }; + return ( = ({ await axios.post('/api/v1/auth/jellyfin', { username: values.username, password: values.password, - hostname: values.host, + hostname: values.hostname, + port: values.port, + useSsl: values.useSsl, + urlBase: values.urlBase, email: values.email, }); } catch (e) { @@ -121,32 +143,100 @@ const JellyfinLogin: React.FC = ({ } }} > - {({ errors, touched, isSubmitting, isValid }) => ( + {({ + errors, + touched, + values, + setFieldValue, + isSubmitting, + isValid, + }) => (
-