|
|
|
import { Router } from 'express';
|
|
|
|
import rateLimit from 'express-rate-limit';
|
|
|
|
import fs from 'fs';
|
|
|
|
import { merge, omit } from 'lodash';
|
|
|
|
import path from 'path';
|
|
|
|
import { getRepository } from 'typeorm';
|
|
|
|
import PlexAPI from '../../api/plexapi';
|
|
|
|
import PlexTvAPI from '../../api/plextv';
|
|
|
|
import Media from '../../entity/Media';
|
|
|
|
import { MediaRequest } from '../../entity/MediaRequest';
|
|
|
|
import { User } from '../../entity/User';
|
|
|
|
import {
|
|
|
|
LogMessage,
|
|
|
|
LogsResultsResponse,
|
|
|
|
SettingsAboutResponse,
|
|
|
|
} from '../../interfaces/api/settingsInterfaces';
|
|
|
|
import { scheduledJobs } from '../../job/schedule';
|
|
|
|
import cacheManager, { AvailableCacheIds } from '../../lib/cache';
|
|
|
|
import { Permission } from '../../lib/permissions';
|
|
|
|
import { plexFullScanner } from '../../lib/scanners/plex';
|
|
|
|
import { getSettings, Library, MainSettings } from '../../lib/settings';
|
|
|
|
import logger from '../../logger';
|
|
|
|
import { isAuthenticated } from '../../middleware/auth';
|
|
|
|
import { getAppVersion } from '../../utils/appVersion';
|
|
|
|
import notificationRoutes from './notifications';
|
|
|
|
import radarrRoutes from './radarr';
|
|
|
|
import sonarrRoutes from './sonarr';
|
|
|
|
|
|
|
|
const settingsRoutes = Router();
|
|
|
|
|
|
|
|
settingsRoutes.use('/notifications', notificationRoutes);
|
|
|
|
settingsRoutes.use('/radarr', radarrRoutes);
|
|
|
|
settingsRoutes.use('/sonarr', sonarrRoutes);
|
|
|
|
|
|
|
|
const filteredMainSettings = (
|
|
|
|
user: User,
|
|
|
|
main: MainSettings
|
|
|
|
): Partial<MainSettings> => {
|
|
|
|
if (!user?.hasPermission(Permission.ADMIN)) {
|
|
|
|
return omit(main, 'apiKey');
|
|
|
|
}
|
|
|
|
|
|
|
|
return main;
|
|
|
|
};
|
|
|
|
|
|
|
|
settingsRoutes.get('/main', (req, res, next) => {
|
|
|
|
const settings = getSettings();
|
|
|
|
|
|
|
|
if (!req.user) {
|
|
|
|
return next({ status: 500, message: 'User missing from request' });
|
|
|
|
}
|
|
|
|
|
|
|
|
res.status(200).json(filteredMainSettings(req.user, settings.main));
|
|
|
|
});
|
|
|
|
|
|
|
|
settingsRoutes.post('/main', (req, res) => {
|
|
|
|
const settings = getSettings();
|
|
|
|
|
|
|
|
settings.main = merge(settings.main, req.body);
|
|
|
|
settings.save();
|
|
|
|
|
|
|
|
return res.status(200).json(settings.main);
|
|
|
|
});
|
|
|
|
|
|
|
|
settingsRoutes.post('/main/regenerate', (req, res, next) => {
|
|
|
|
const settings = getSettings();
|
|
|
|
|
|
|
|
const main = settings.regenerateApiKey();
|
|
|
|
|
|
|
|
if (!req.user) {
|
|
|
|
return next({ status: 500, message: 'User missing from request' });
|
|
|
|
}
|
|
|
|
|
|
|
|
return res.status(200).json(filteredMainSettings(req.user, main));
|
|
|
|
});
|
|
|
|
|
|
|
|
settingsRoutes.get('/plex', (_req, res) => {
|
|
|
|
const settings = getSettings();
|
|
|
|
|
|
|
|
res.status(200).json(settings.plex);
|
|
|
|
});
|
|
|
|
|
|
|
|
settingsRoutes.post('/plex', async (req, res, next) => {
|
|
|
|
const userRepository = getRepository(User);
|
|
|
|
const settings = getSettings();
|
|
|
|
try {
|
|
|
|
const admin = await userRepository.findOneOrFail({
|
|
|
|
select: ['id', 'plexToken'],
|
|
|
|
order: { id: 'ASC' },
|
|
|
|
});
|
|
|
|
|
|
|
|
Object.assign(settings.plex, req.body);
|
|
|
|
|
|
|
|
const plexClient = new PlexAPI({ plexToken: admin.plexToken });
|
|
|
|
|
|
|
|
const result = await plexClient.getStatus();
|
|
|
|
|
|
|
|
if (result?.MediaContainer?.machineIdentifier) {
|
|
|
|
settings.plex.machineId = result.MediaContainer.machineIdentifier;
|
|
|
|
settings.plex.name = result.MediaContainer.friendlyName;
|
|
|
|
|
|
|
|
settings.save();
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
return next({
|
|
|
|
status: 500,
|
|
|
|
message: `Failed to connect to Plex: ${e.message}`,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return res.status(200).json(settings.plex);
|
|
|
|
});
|
|
|
|
|
|
|
|
settingsRoutes.get('/plex/devices/servers', async (req, res, next) => {
|
|
|
|
const userRepository = getRepository(User);
|
|
|
|
const regexp = /(http(s?):\/\/)(.*)(:[0-9]*)/;
|
|
|
|
try {
|
|
|
|
const admin = await userRepository.findOneOrFail({
|
|
|
|
select: ['id', 'plexToken'],
|
|
|
|
order: { id: 'ASC' },
|
|
|
|
});
|
|
|
|
const plexTvClient = admin.plexToken
|
|
|
|
? new PlexTvAPI(admin.plexToken)
|
|
|
|
: null;
|
|
|
|
const devices = (await plexTvClient?.getDevices())?.filter((device) => {
|
|
|
|
return device.provides.includes('server') && device.owned;
|
|
|
|
});
|
|
|
|
const settings = getSettings();
|
|
|
|
if (devices) {
|
|
|
|
await Promise.all(
|
|
|
|
devices.map(async (device) => {
|
|
|
|
await Promise.all(
|
|
|
|
device.connection.map(async (connection) => {
|
|
|
|
connection.host = connection.uri.replace(regexp, '$3');
|
|
|
|
let msg:
|
|
|
|
| { status: number; message: string }
|
|
|
|
| undefined = undefined;
|
|
|
|
const plexDeviceSettings = {
|
|
|
|
...settings.plex,
|
|
|
|
ip: connection.host,
|
|
|
|
port: connection.port,
|
|
|
|
useSsl: connection.protocol === 'https' ? true : false,
|
|
|
|
};
|
|
|
|
const plexClient = new PlexAPI({
|
|
|
|
plexToken: admin.plexToken,
|
|
|
|
plexSettings: plexDeviceSettings,
|
|
|
|
timeout: 5000,
|
|
|
|
});
|
|
|
|
try {
|
|
|
|
await plexClient.getStatus();
|
|
|
|
msg = {
|
|
|
|
status: 200,
|
|
|
|
message: 'OK',
|
|
|
|
};
|
|
|
|
} catch (e) {
|
|
|
|
msg = {
|
|
|
|
status: 500,
|
|
|
|
message: e.message,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
connection.status = msg?.status;
|
|
|
|
connection.message = msg?.message;
|
|
|
|
})
|
|
|
|
);
|
|
|
|
})
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return res.status(200).json(devices);
|
|
|
|
} catch (e) {
|
|
|
|
return next({
|
|
|
|
status: 500,
|
|
|
|
message: `Failed to connect to Plex: ${e.message}`,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
settingsRoutes.get('/plex/library', async (req, res) => {
|
|
|
|
const settings = getSettings();
|
|
|
|
|
|
|
|
if (req.query.sync) {
|
|
|
|
const userRepository = getRepository(User);
|
|
|
|
const admin = await userRepository.findOneOrFail({
|
|
|
|
select: ['id', 'plexToken'],
|
|
|
|
order: { id: 'ASC' },
|
|
|
|
});
|
|
|
|
const plexapi = new PlexAPI({ plexToken: admin.plexToken });
|
|
|
|
|
|
|
|
const libraries = await plexapi.getLibraries();
|
|
|
|
|
|
|
|
const newLibraries: Library[] = libraries
|
|
|
|
// Remove libraries that are not movie or show
|
|
|
|
.filter((library) => library.type === 'movie' || library.type === 'show')
|
|
|
|
// Remove libraries that do not have a metadata agent set (usually personal video libraries)
|
|
|
|
.filter((library) => library.agent !== 'com.plexapp.agents.none')
|
|
|
|
.map((library) => {
|
|
|
|
const existing = settings.plex.libraries.find(
|
|
|
|
(l) => l.id === library.key && l.name === library.title
|
|
|
|
);
|
|
|
|
|
|
|
|
return {
|
|
|
|
id: library.key,
|
|
|
|
name: library.title,
|
|
|
|
enabled: existing?.enabled ?? false,
|
|
|
|
};
|
|
|
|
});
|
|
|
|
|
|
|
|
settings.plex.libraries = newLibraries;
|
|
|
|
}
|
|
|
|
|
|
|
|
const enabledLibraries = req.query.enable
|
|
|
|
? (req.query.enable as string).split(',')
|
|
|
|
: [];
|
|
|
|
settings.plex.libraries = settings.plex.libraries.map((library) => ({
|
|
|
|
...library,
|
|
|
|
enabled: enabledLibraries.includes(library.id),
|
|
|
|
}));
|
|
|
|
settings.save();
|
|
|
|
return res.status(200).json(settings.plex.libraries);
|
|
|
|
});
|
|
|
|
|
|
|
|
settingsRoutes.get('/plex/sync', (_req, res) => {
|
|
|
|
return res.status(200).json(plexFullScanner.status());
|
|
|
|
});
|
|
|
|
|
|
|
|
settingsRoutes.post('/plex/sync', (req, res) => {
|
|
|
|
if (req.body.cancel) {
|
|
|
|
plexFullScanner.cancel();
|
|
|
|
} else if (req.body.start) {
|
|
|
|
plexFullScanner.run();
|
|
|
|
}
|
|
|
|
return res.status(200).json(plexFullScanner.status());
|
|
|
|
});
|
|
|
|
|
|
|
|
settingsRoutes.get(
|
|
|
|
'/logs',
|
|
|
|
rateLimit({ windowMs: 60 * 1000, max: 50 }),
|
|
|
|
(req, res, next) => {
|
|
|
|
const pageSize = req.query.take ? Number(req.query.take) : 25;
|
|
|
|
const skip = req.query.skip ? Number(req.query.skip) : 0;
|
|
|
|
|
|
|
|
let filter: string[] = [];
|
|
|
|
switch (req.query.filter) {
|
|
|
|
case 'debug':
|
|
|
|
filter.push('debug');
|
|
|
|
// falls through
|
|
|
|
case 'info':
|
|
|
|
filter.push('info');
|
|
|
|
// falls through
|
|
|
|
case 'warn':
|
|
|
|
filter.push('warn');
|
|
|
|
// falls through
|
|
|
|
case 'error':
|
|
|
|
filter.push('error');
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
filter = ['debug', 'info', 'warn', 'error'];
|
|
|
|
}
|
|
|
|
|
|
|
|
const logFile = process.env.CONFIG_DIRECTORY
|
|
|
|
? `${process.env.CONFIG_DIRECTORY}/logs/overseerr.log`
|
|
|
|
: path.join(__dirname, '../../../config/logs/overseerr.log');
|
|
|
|
const logs: LogMessage[] = [];
|
|
|
|
|
|
|
|
try {
|
|
|
|
fs.readFileSync(logFile)
|
|
|
|
.toString()
|
|
|
|
.split('\n')
|
|
|
|
.forEach((line) => {
|
|
|
|
if (!line.length) return;
|
|
|
|
|
|
|
|
const timestamp = line.match(new RegExp(/^.{24}/)) || [];
|
|
|
|
const level = line.match(new RegExp(/\s\[\w+\]/)) || [];
|
|
|
|
const label = line.match(new RegExp(/\]\[.+\]/)) || [];
|
|
|
|
const message = line.match(new RegExp(/:\s([^{}]+)({.*})?/)) || [];
|
|
|
|
|
|
|
|
if (level.length && filter.includes(level[0].slice(2, -1))) {
|
|
|
|
logs.push({
|
|
|
|
timestamp: timestamp[0],
|
|
|
|
level: level.length ? level[0].slice(2, -1) : '',
|
|
|
|
label: label.length ? label[0].slice(2, -1) : '',
|
|
|
|
message: message.length && message[1] ? message[1] : '',
|
|
|
|
data:
|
|
|
|
message.length && message[2]
|
|
|
|
? JSON.parse(message[2])
|
|
|
|
: undefined,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
const displayedLogs = logs.reverse().slice(skip, skip + pageSize);
|
|
|
|
|
|
|
|
return res.status(200).json({
|
|
|
|
pageInfo: {
|
|
|
|
pages: Math.ceil(logs.length / pageSize),
|
|
|
|
pageSize,
|
|
|
|
results: logs.length,
|
|
|
|
page: Math.ceil(skip / pageSize) + 1,
|
|
|
|
},
|
|
|
|
results: displayedLogs,
|
|
|
|
} as LogsResultsResponse);
|
|
|
|
} catch (error) {
|
|
|
|
logger.error('Something went wrong while fetching the logs', {
|
|
|
|
label: 'Logs',
|
|
|
|
errorMessage: error.message,
|
|
|
|
});
|
|
|
|
return next({
|
|
|
|
status: 500,
|
|
|
|
message: 'Something went wrong while fetching the logs',
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
settingsRoutes.get('/jobs', (_req, res) => {
|
|
|
|
return res.status(200).json(
|
|
|
|
scheduledJobs.map((job) => ({
|
|
|
|
id: job.id,
|
|
|
|
name: job.name,
|
|
|
|
type: job.type,
|
|
|
|
nextExecutionTime: job.job.nextInvocation(),
|
|
|
|
running: job.running ? job.running() : false,
|
|
|
|
}))
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
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' });
|
|
|
|
}
|
|
|
|
|
|
|
|
scheduledJob.job.invoke();
|
|
|
|
|
|
|
|
return res.status(200).json({
|
|
|
|
id: scheduledJob.id,
|
|
|
|
name: scheduledJob.name,
|
|
|
|
type: scheduledJob.type,
|
|
|
|
nextExecutionTime: scheduledJob.job.nextInvocation(),
|
|
|
|
running: scheduledJob.running ? scheduledJob.running() : false,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
settingsRoutes.post<{ jobId: string }>(
|
|
|
|
'/jobs/:jobId/cancel',
|
|
|
|
(req, res, next) => {
|
|
|
|
const scheduledJob = scheduledJobs.find(
|
|
|
|
(job) => job.id === req.params.jobId
|
|
|
|
);
|
|
|
|
|
|
|
|
if (!scheduledJob) {
|
|
|
|
return next({ status: 404, message: 'Job not found' });
|
|
|
|
}
|
|
|
|
|
|
|
|
if (scheduledJob.cancelFn) {
|
|
|
|
scheduledJob.cancelFn();
|
|
|
|
}
|
|
|
|
|
|
|
|
return res.status(200).json({
|
|
|
|
id: scheduledJob.id,
|
|
|
|
name: scheduledJob.name,
|
|
|
|
type: scheduledJob.type,
|
|
|
|
nextExecutionTime: scheduledJob.job.nextInvocation(),
|
|
|
|
running: scheduledJob.running ? scheduledJob.running() : false,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
settingsRoutes.get('/cache', (req, res) => {
|
|
|
|
const caches = cacheManager.getAllCaches();
|
|
|
|
|
|
|
|
return res.status(200).json(
|
|
|
|
Object.values(caches).map((cache) => ({
|
|
|
|
id: cache.id,
|
|
|
|
name: cache.name,
|
|
|
|
stats: cache.getStats(),
|
|
|
|
}))
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
settingsRoutes.post<{ cacheId: AvailableCacheIds }>(
|
|
|
|
'/cache/:cacheId/flush',
|
|
|
|
(req, res, next) => {
|
|
|
|
const cache = cacheManager.getCache(req.params.cacheId);
|
|
|
|
|
|
|
|
if (cache) {
|
|
|
|
cache.flush();
|
|
|
|
return res.status(204).send();
|
|
|
|
}
|
|
|
|
|
|
|
|
next({ status: 404, message: 'Cache does not exist.' });
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
settingsRoutes.post(
|
|
|
|
'/initialize',
|
|
|
|
isAuthenticated(Permission.ADMIN),
|
|
|
|
(_req, res) => {
|
|
|
|
const settings = getSettings();
|
|
|
|
|
|
|
|
settings.public.initialized = true;
|
|
|
|
settings.save();
|
|
|
|
|
|
|
|
return res.status(200).json(settings.public);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
settingsRoutes.get('/about', async (req, res) => {
|
|
|
|
const mediaRepository = getRepository(Media);
|
|
|
|
const mediaRequestRepository = getRepository(MediaRequest);
|
|
|
|
|
|
|
|
const totalMediaItems = await mediaRepository.count();
|
|
|
|
const totalRequests = await mediaRequestRepository.count();
|
|
|
|
|
|
|
|
return res.status(200).json({
|
|
|
|
version: getAppVersion(),
|
|
|
|
totalMediaItems,
|
|
|
|
totalRequests,
|
|
|
|
tz: process.env.TZ,
|
|
|
|
} as SettingsAboutResponse);
|
|
|
|
});
|
|
|
|
|
|
|
|
export default settingsRoutes;
|