diff --git a/package.json b/package.json index 608187d43..e812ec6f3 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "react-use-clipboard": "1.0.7", "reflect-metadata": "^0.1.13", "secure-random-password": "^0.2.3", + "semver": "^7.3.5", "sqlite3": "^5.0.2", "swagger-ui-express": "^4.3.0", "swr": "^1.2.1", @@ -104,6 +105,7 @@ "@types/react-dom": "^17.0.11", "@types/react-transition-group": "^4.4.4", "@types/secure-random-password": "^0.2.1", + "@types/semver": "^7.3.9", "@types/swagger-ui-express": "^4.1.3", "@types/web-push": "^3.3.2", "@types/xml2js": "^0.4.9", diff --git a/server/api/tautulli.ts b/server/api/tautulli.ts index 727c7d08c..96caee399 100644 --- a/server/api/tautulli.ts +++ b/server/api/tautulli.ts @@ -90,6 +90,27 @@ interface TautulliWatchUsersResponse { }; } +interface TautulliInfo { + tautulli_install_type: string; + tautulli_version: string; + tautulli_branch: string; + tautulli_commit: string; + tautulli_platform: string; + tautulli_platform_release: string; + tautulli_platform_version: string; + tautulli_platform_linux_distro: string; + tautulli_platform_device_name: string; + tautulli_python_version: string; +} + +interface TautulliInfoResponse { + response: { + result: string; + message?: string; + data: TautulliInfo; + }; +} + class TautulliAPI { private axios: AxiosInstance; @@ -102,6 +123,24 @@ class TautulliAPI { }); } + public async getInfo(): Promise { + try { + return ( + await this.axios.get('/api/v2', { + params: { cmd: 'get_tautulli_info' }, + }) + ).data.response.data; + } catch (e) { + logger.error('Something went wrong fetching Tautulli server info', { + label: 'Tautulli API', + errorMessage: e.message, + }); + throw new Error( + `[Tautulli] Failed to fetch Tautulli server info: ${e.message}` + ); + } + } + public async getMediaWatchStats( ratingKey: string ): Promise { diff --git a/server/routes/settings/index.ts b/server/routes/settings/index.ts index ccd25d2cb..6532980cc 100644 --- a/server/routes/settings/index.ts +++ b/server/routes/settings/index.ts @@ -4,10 +4,12 @@ import fs from 'fs'; import { merge, omit, set, sortBy } from 'lodash'; import { rescheduleJob } from 'node-schedule'; import path from 'path'; +import semver from 'semver'; import { getRepository } from 'typeorm'; import { URL } from 'url'; import PlexAPI from '../../api/plexapi'; import PlexTvAPI from '../../api/plextv'; +import TautulliAPI from '../../api/tautulli'; import Media from '../../entity/Media'; import { MediaRequest } from '../../entity/MediaRequest'; import { User } from '../../entity/User'; @@ -50,7 +52,7 @@ settingsRoutes.get('/main', (req, res, next) => { const settings = getSettings(); if (!req.user) { - return next({ status: 400, message: 'User missing from request' }); + return next({ status: 400, message: 'User missing from request.' }); } res.status(200).json(filteredMainSettings(req.user, settings.main)); @@ -71,7 +73,7 @@ settingsRoutes.post('/main/regenerate', (req, res, next) => { const main = settings.regenerateApiKey(); if (!req.user) { - return next({ status: 500, message: 'User missing from request' }); + return next({ status: 500, message: 'User missing from request.' }); } return res.status(200).json(filteredMainSettings(req.user, main)); @@ -98,16 +100,22 @@ settingsRoutes.post('/plex', async (req, res, next) => { const result = await plexClient.getStatus(); - if (result?.MediaContainer?.machineIdentifier) { - settings.plex.machineId = result.MediaContainer.machineIdentifier; - settings.plex.name = result.MediaContainer.friendlyName; - - settings.save(); + if (!result?.MediaContainer?.machineIdentifier) { + throw new Error('Server not found'); } + + settings.plex.machineId = result.MediaContainer.machineIdentifier; + settings.plex.name = result.MediaContainer.friendlyName; + + settings.save(); } catch (e) { + logger.error('Something went wrong testing Plex connection', { + label: 'API', + errorMessage: e.message, + }); return next({ status: 500, - message: `Failed to connect to Plex: ${e.message}`, + message: 'Unable to connect to Plex.', }); } @@ -180,9 +188,13 @@ settingsRoutes.get('/plex/devices/servers', async (req, res, next) => { } return res.status(200).json(devices); } catch (e) { + logger.error('Something went wrong retrieving Plex server list', { + label: 'API', + errorMessage: e.message, + }); return next({ status: 500, - message: `Failed to connect to Plex: ${e.message}`, + message: 'Unable to retrieve Plex server list.', }); } }); @@ -231,11 +243,31 @@ settingsRoutes.get('/tautulli', (_req, res) => { res.status(200).json(settings.tautulli); }); -settingsRoutes.post('/tautulli', async (req, res) => { +settingsRoutes.post('/tautulli', async (req, res, next) => { const settings = getSettings(); Object.assign(settings.tautulli, req.body); - settings.save(); + + try { + const tautulliClient = new TautulliAPI(settings.tautulli); + + const result = await tautulliClient.getInfo(); + + if (!semver.gte(semver.coerce(result?.tautulli_version) ?? '', '2.9.0')) { + throw new Error('Tautulli version not supported'); + } + + settings.save(); + } catch (e) { + logger.error('Something went wrong testing Tautulli connection', { + label: 'API', + errorMessage: e.message, + }); + return next({ + status: 500, + message: 'Unable to connect to Tautulli.', + }); + } return res.status(200).json(settings.tautulli); }); @@ -379,13 +411,13 @@ settingsRoutes.get( results: displayedLogs, } as LogsResultsResponse); } catch (error) { - logger.error('Something went wrong while fetching the logs', { + logger.error('Something went wrong while retrieving logs', { label: 'Logs', errorMessage: error.message, }); return next({ status: 500, - message: 'Something went wrong while fetching the logs', + message: 'Unable to retrieve logs.', }); } } @@ -408,7 +440,7 @@ settingsRoutes.post<{ jobId: string }>('/jobs/:jobId/run', (req, res, next) => { const scheduledJob = scheduledJobs.find((job) => job.id === req.params.jobId); if (!scheduledJob) { - return next({ status: 404, message: 'Job not found' }); + return next({ status: 404, message: 'Job not found.' }); } scheduledJob.job.invoke(); @@ -431,7 +463,7 @@ settingsRoutes.post<{ jobId: string }>( ); if (!scheduledJob) { - return next({ status: 404, message: 'Job not found' }); + return next({ status: 404, message: 'Job not found.' }); } if (scheduledJob.cancelFn) { @@ -457,7 +489,7 @@ settingsRoutes.post<{ jobId: string }>( ); if (!scheduledJob) { - return next({ status: 404, message: 'Job not found' }); + return next({ status: 404, message: 'Job not found.' }); } const result = rescheduleJob(scheduledJob.job, req.body.schedule); @@ -476,7 +508,7 @@ settingsRoutes.post<{ jobId: string }>( running: scheduledJob.running ? scheduledJob.running() : false, }); } else { - return next({ status: 400, message: 'Invalid job schedule' }); + return next({ status: 400, message: 'Invalid job schedule.' }); } } ); @@ -503,7 +535,7 @@ settingsRoutes.post<{ cacheId: AvailableCacheIds }>( return res.status(204).send(); } - next({ status: 404, message: 'Cache does not exist.' }); + next({ status: 404, message: 'Cache not found.' }); } ); diff --git a/yarn.lock b/yarn.lock index c2f87c236..599361d9d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2606,6 +2606,11 @@ resolved "https://registry.yarnpkg.com/@types/secure-random-password/-/secure-random-password-0.2.1.tgz#c01a96d5c2667c3fa896533207bceb157e3b87bc" integrity sha512-tpG5oVF+NpIS9UJ9ttXAokafyhE/MCZBg65D345qu3gOM4YoJ/mFNVzUDUNBfb1hIi598bNOzvY04BbfS7VKwA== +"@types/semver@^7.3.9": + version "7.3.9" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.9.tgz#152c6c20a7688c30b967ec1841d31ace569863fc" + integrity sha512-L/TMpyURfBkf+o/526Zb6kd/tchUP3iBDEPjqjb+U2MAJhVRxxrmr2fwpe08E7QsV7YLcpq0tUaQ9O9x97ZIxQ== + "@types/serve-static@*": version "1.13.10" resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.10.tgz#f5e0ce8797d2d7cc5ebeda48a52c96c4fa47a8d9"