mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00
feat(tautulli): validate upon saving settings (#2511)
This commit is contained in:
@@ -67,6 +67,7 @@
|
|||||||
"react-use-clipboard": "1.0.7",
|
"react-use-clipboard": "1.0.7",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"secure-random-password": "^0.2.3",
|
"secure-random-password": "^0.2.3",
|
||||||
|
"semver": "^7.3.5",
|
||||||
"sqlite3": "^5.0.2",
|
"sqlite3": "^5.0.2",
|
||||||
"swagger-ui-express": "^4.3.0",
|
"swagger-ui-express": "^4.3.0",
|
||||||
"swr": "^1.2.1",
|
"swr": "^1.2.1",
|
||||||
@@ -104,6 +105,7 @@
|
|||||||
"@types/react-dom": "^17.0.11",
|
"@types/react-dom": "^17.0.11",
|
||||||
"@types/react-transition-group": "^4.4.4",
|
"@types/react-transition-group": "^4.4.4",
|
||||||
"@types/secure-random-password": "^0.2.1",
|
"@types/secure-random-password": "^0.2.1",
|
||||||
|
"@types/semver": "^7.3.9",
|
||||||
"@types/swagger-ui-express": "^4.1.3",
|
"@types/swagger-ui-express": "^4.1.3",
|
||||||
"@types/web-push": "^3.3.2",
|
"@types/web-push": "^3.3.2",
|
||||||
"@types/xml2js": "^0.4.9",
|
"@types/xml2js": "^0.4.9",
|
||||||
|
@@ -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 {
|
class TautulliAPI {
|
||||||
private axios: AxiosInstance;
|
private axios: AxiosInstance;
|
||||||
|
|
||||||
@@ -102,6 +123,24 @@ class TautulliAPI {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getInfo(): Promise<TautulliInfo> {
|
||||||
|
try {
|
||||||
|
return (
|
||||||
|
await this.axios.get<TautulliInfoResponse>('/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(
|
public async getMediaWatchStats(
|
||||||
ratingKey: string
|
ratingKey: string
|
||||||
): Promise<TautulliWatchStats[]> {
|
): Promise<TautulliWatchStats[]> {
|
||||||
|
@@ -4,10 +4,12 @@ import fs from 'fs';
|
|||||||
import { merge, omit, set, sortBy } from 'lodash';
|
import { merge, omit, set, sortBy } from 'lodash';
|
||||||
import { rescheduleJob } from 'node-schedule';
|
import { rescheduleJob } from 'node-schedule';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import semver from 'semver';
|
||||||
import { getRepository } from 'typeorm';
|
import { getRepository } from 'typeorm';
|
||||||
import { URL } from 'url';
|
import { URL } from 'url';
|
||||||
import PlexAPI from '../../api/plexapi';
|
import PlexAPI from '../../api/plexapi';
|
||||||
import PlexTvAPI from '../../api/plextv';
|
import PlexTvAPI from '../../api/plextv';
|
||||||
|
import TautulliAPI from '../../api/tautulli';
|
||||||
import Media from '../../entity/Media';
|
import Media from '../../entity/Media';
|
||||||
import { MediaRequest } from '../../entity/MediaRequest';
|
import { MediaRequest } from '../../entity/MediaRequest';
|
||||||
import { User } from '../../entity/User';
|
import { User } from '../../entity/User';
|
||||||
@@ -50,7 +52,7 @@ settingsRoutes.get('/main', (req, res, next) => {
|
|||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
if (!req.user) {
|
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));
|
res.status(200).json(filteredMainSettings(req.user, settings.main));
|
||||||
@@ -71,7 +73,7 @@ settingsRoutes.post('/main/regenerate', (req, res, next) => {
|
|||||||
const main = settings.regenerateApiKey();
|
const main = settings.regenerateApiKey();
|
||||||
|
|
||||||
if (!req.user) {
|
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));
|
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();
|
const result = await plexClient.getStatus();
|
||||||
|
|
||||||
if (result?.MediaContainer?.machineIdentifier) {
|
if (!result?.MediaContainer?.machineIdentifier) {
|
||||||
settings.plex.machineId = result.MediaContainer.machineIdentifier;
|
throw new Error('Server not found');
|
||||||
settings.plex.name = result.MediaContainer.friendlyName;
|
|
||||||
|
|
||||||
settings.save();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
settings.plex.machineId = result.MediaContainer.machineIdentifier;
|
||||||
|
settings.plex.name = result.MediaContainer.friendlyName;
|
||||||
|
|
||||||
|
settings.save();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('Something went wrong testing Plex connection', {
|
||||||
|
label: 'API',
|
||||||
|
errorMessage: e.message,
|
||||||
|
});
|
||||||
return next({
|
return next({
|
||||||
status: 500,
|
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);
|
return res.status(200).json(devices);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('Something went wrong retrieving Plex server list', {
|
||||||
|
label: 'API',
|
||||||
|
errorMessage: e.message,
|
||||||
|
});
|
||||||
return next({
|
return next({
|
||||||
status: 500,
|
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);
|
res.status(200).json(settings.tautulli);
|
||||||
});
|
});
|
||||||
|
|
||||||
settingsRoutes.post('/tautulli', async (req, res) => {
|
settingsRoutes.post('/tautulli', async (req, res, next) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
Object.assign(settings.tautulli, req.body);
|
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);
|
return res.status(200).json(settings.tautulli);
|
||||||
});
|
});
|
||||||
@@ -379,13 +411,13 @@ settingsRoutes.get(
|
|||||||
results: displayedLogs,
|
results: displayedLogs,
|
||||||
} as LogsResultsResponse);
|
} as LogsResultsResponse);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Something went wrong while fetching the logs', {
|
logger.error('Something went wrong while retrieving logs', {
|
||||||
label: 'Logs',
|
label: 'Logs',
|
||||||
errorMessage: error.message,
|
errorMessage: error.message,
|
||||||
});
|
});
|
||||||
return next({
|
return next({
|
||||||
status: 500,
|
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);
|
const scheduledJob = scheduledJobs.find((job) => job.id === req.params.jobId);
|
||||||
|
|
||||||
if (!scheduledJob) {
|
if (!scheduledJob) {
|
||||||
return next({ status: 404, message: 'Job not found' });
|
return next({ status: 404, message: 'Job not found.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
scheduledJob.job.invoke();
|
scheduledJob.job.invoke();
|
||||||
@@ -431,7 +463,7 @@ settingsRoutes.post<{ jobId: string }>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!scheduledJob) {
|
if (!scheduledJob) {
|
||||||
return next({ status: 404, message: 'Job not found' });
|
return next({ status: 404, message: 'Job not found.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scheduledJob.cancelFn) {
|
if (scheduledJob.cancelFn) {
|
||||||
@@ -457,7 +489,7 @@ settingsRoutes.post<{ jobId: string }>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!scheduledJob) {
|
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);
|
const result = rescheduleJob(scheduledJob.job, req.body.schedule);
|
||||||
@@ -476,7 +508,7 @@ settingsRoutes.post<{ jobId: string }>(
|
|||||||
running: scheduledJob.running ? scheduledJob.running() : false,
|
running: scheduledJob.running ? scheduledJob.running() : false,
|
||||||
});
|
});
|
||||||
} else {
|
} 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();
|
return res.status(204).send();
|
||||||
}
|
}
|
||||||
|
|
||||||
next({ status: 404, message: 'Cache does not exist.' });
|
next({ status: 404, message: 'Cache not found.' });
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@@ -2606,6 +2606,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/secure-random-password/-/secure-random-password-0.2.1.tgz#c01a96d5c2667c3fa896533207bceb157e3b87bc"
|
resolved "https://registry.yarnpkg.com/@types/secure-random-password/-/secure-random-password-0.2.1.tgz#c01a96d5c2667c3fa896533207bceb157e3b87bc"
|
||||||
integrity sha512-tpG5oVF+NpIS9UJ9ttXAokafyhE/MCZBg65D345qu3gOM4YoJ/mFNVzUDUNBfb1hIi598bNOzvY04BbfS7VKwA==
|
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@*":
|
"@types/serve-static@*":
|
||||||
version "1.13.10"
|
version "1.13.10"
|
||||||
resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.10.tgz#f5e0ce8797d2d7cc5ebeda48a52c96c4fa47a8d9"
|
resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.10.tgz#f5e0ce8797d2d7cc5ebeda48a52c96c4fa47a8d9"
|
||||||
|
Reference in New Issue
Block a user