From 38ad875dd7848b4e92ac3ccdd16dbf785f6a5c4d Mon Sep 17 00:00:00 2001 From: Fallenbagel <98979876+Fallenbagel@users.noreply.github.com> Date: Thu, 13 Jun 2024 22:06:33 +0500 Subject: [PATCH] refactor(jellyfin): abstract jellyfin hostname, updated ui to reflect it, better validation (#773) * refactor(jellyfinsettings): abstract jellyfin hostname, updated ui to reflect it, better validation This PR refactors and abstracts jellyfin hostname into, jellyfin ip, jellyfin port, jellyfin useSsl, and jellyfin urlBase. This makes it more consistent with how plex settings are stored as well. In addition, this improves validation as validation can be applied seperately to them instead of as one whole regex doing the work to validate the url. UI was updated to reflect this. BREAKING CHANGE: Jellyfin settings now does not include a hostname. Instead it abstracted it to ip, port, useSsl, and urlBase. However, migration of old settings to new settings should work automatically. * refactor: remove console logs and use getHostname and ApiErrorCodes * fix: store req.body jellyfin settings temporarily and store only if valid This should fix the issue where settings are saved even if the url was invalid. Now the settings will only be saved if the url is valid. Sort of like a test connection. * refactor: clean up commented out code * refactor(i18n): extract translation keys * fix(auth): auth failing with jellyfin login is disabled * fix(settings): jellyfin migrations replacing the rest of the settings * fix(settings): jellyfin hostname should be carried out if hostname exists * fix(settings): merging the wrong settings source * refactor(settings): use migrator for dynamic settings migrations * refactor(settingsmigrator): settings migration handler and the migrations * test(cypress): fix cypress tests failing cypress settings were lacking some of the jobs so when the startJobs() is called when the app starts, it was failing to schedule the jobs where their cron timings were not specified in the cypress settings. Therefore, this commit adds those jobs back. In addition, other setting options were added to keep cypress settings consistent with a normal user. * chore(prettierignore): ignore cypress/config/settings.cypress.json as it does not need prettier * chore(prettier): ran formatter on cypress config to fix format check error format check locally passes on this file. However, it fails during the github actions format check. Therefore, json language features formatter was run instead of prettier to see if that fixes the issue. * test(cypress): add only missing jobs to the cypress settings * ci: attempt at trying to get formatter to pass on cypress config json file * refactor: revert the changes brought to try and fix formatter added back the rest of the cypress settings and removed cypress settings from .prettierignore * refactor(settings): better erorr logging when jellyfin connection test fails in settings page --- cypress/config/settings.cypress.json | 27 +++ server/api/jellyfin.ts | 10 + server/constants/error.ts | 2 + server/entity/Media.ts | 12 +- server/lib/availabilitySync.ts | 3 +- server/lib/scanners/jellyfin/index.ts | 5 +- server/lib/{settings.ts => settings/index.ts} | 26 ++- .../migrations/0001_migrate_hostname.ts | 30 +++ server/lib/settings/migrator.ts | 21 ++ server/routes/auth.ts | 43 ++-- server/routes/settings/index.ts | 77 +++++-- server/routes/user/index.ts | 11 +- server/utils/getHostname.ts | 18 ++ src/components/Login/JellyfinLogin.tsx | 140 +++++++++--- src/components/Settings/SettingsJellyfin.tsx | 208 ++++++++++++++---- src/i18n/locale/en.json | 14 +- 16 files changed, 529 insertions(+), 118 deletions(-) rename server/lib/{settings.ts => settings/index.ts} (97%) create mode 100644 server/lib/settings/migrations/0001_migrate_hostname.ts create mode 100644 server/lib/settings/migrator.ts create mode 100644 server/utils/getHostname.ts 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, + }) => (
-