mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2026-01-01 12:18:35 -05:00
* fix(jellyfinapi): refactors jellyfin library sync to support automatic grouping and collections Previously, #450 added support for automatic library grouping. However, some users reported that they were getting a 401 when using custom authentication such as LDAP. Therefore, that PR was reverted (#524). This PR adds back the support for automatic library grouping for jellyfin authentication users using the endpoint `/Library/MediaFolders` and fallsback to User views endpoint if they're unable to sync the libraries (some, not all LDAP users had issues. Some reported that it worked despite having custom authentication). Once it falls back to user views endpoint for syncing, now it will detect if automatic grouping is enabled giving a warning that its not supported when using some custom authentication methods. This PR also fixed collection syncing by expanding the boxsets when syncing. fix #256, fix #489, re #450, #524, fix #515, fix #474, fix #473 * refactor(i18n): adds the suffix "jellyfin" to jellyfin library sync message keys * refactor(i18n): extract translation keys * refactor: remove console logs * refactor: remove more console logs * refactor: apply review suggestions * chore: fix prettier failing on .github file
751 lines
21 KiB
TypeScript
751 lines
21 KiB
TypeScript
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 { getRepository } from '@server/datasource';
|
|
import Media from '@server/entity/Media';
|
|
import { MediaRequest } from '@server/entity/MediaRequest';
|
|
import { User } from '@server/entity/User';
|
|
import type { PlexConnection } from '@server/interfaces/api/plexInterfaces';
|
|
import type {
|
|
LogMessage,
|
|
LogsResultsResponse,
|
|
SettingsAboutResponse,
|
|
} from '@server/interfaces/api/settingsInterfaces';
|
|
import { scheduledJobs } from '@server/job/schedule';
|
|
import type { AvailableCacheIds } from '@server/lib/cache';
|
|
import cacheManager from '@server/lib/cache';
|
|
import ImageProxy from '@server/lib/imageproxy';
|
|
import { Permission } from '@server/lib/permissions';
|
|
import { jellyfinFullScanner } from '@server/lib/scanners/jellyfin';
|
|
import { plexFullScanner } from '@server/lib/scanners/plex';
|
|
import type { JobId, Library, MainSettings } from '@server/lib/settings';
|
|
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 { appDataPath } from '@server/utils/appDataVolume';
|
|
import { getAppVersion } from '@server/utils/appVersion';
|
|
import { Router } from 'express';
|
|
import rateLimit from 'express-rate-limit';
|
|
import fs from 'fs';
|
|
import gravatarUrl from 'gravatar-url';
|
|
import { escapeRegExp, merge, omit, set, sortBy } from 'lodash';
|
|
import { rescheduleJob } from 'node-schedule';
|
|
import path from 'path';
|
|
import semver from 'semver';
|
|
import { URL } from 'url';
|
|
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);
|
|
settingsRoutes.use('/discover', discoverSettingRoutes);
|
|
|
|
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: 400, 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: true, plexToken: true },
|
|
where: { id: 1 },
|
|
});
|
|
|
|
Object.assign(settings.plex, req.body);
|
|
|
|
const plexClient = new PlexAPI({ plexToken: admin.plexToken });
|
|
|
|
const result = await plexClient.getStatus();
|
|
|
|
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: 'Unable to connect to Plex.',
|
|
});
|
|
}
|
|
|
|
return res.status(200).json(settings.plex);
|
|
});
|
|
|
|
settingsRoutes.get('/plex/devices/servers', async (req, res, next) => {
|
|
const userRepository = getRepository(User);
|
|
try {
|
|
const admin = await userRepository.findOneOrFail({
|
|
select: { id: true, plexToken: true },
|
|
where: { id: 1 },
|
|
});
|
|
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) => {
|
|
const plexDirectConnections: PlexConnection[] = [];
|
|
|
|
device.connection.forEach((connection) => {
|
|
const url = new URL(connection.uri);
|
|
|
|
if (url.hostname !== connection.address) {
|
|
const plexDirectConnection = { ...connection };
|
|
plexDirectConnection.address = url.hostname;
|
|
plexDirectConnections.push(plexDirectConnection);
|
|
|
|
// Connect to IP addresses over HTTP
|
|
connection.protocol = 'http';
|
|
}
|
|
});
|
|
|
|
plexDirectConnections.forEach((plexDirectConnection) => {
|
|
device.connection.push(plexDirectConnection);
|
|
});
|
|
|
|
await Promise.all(
|
|
device.connection.map(async (connection) => {
|
|
const plexDeviceSettings = {
|
|
...settings.plex,
|
|
ip: connection.address,
|
|
port: connection.port,
|
|
useSsl: connection.protocol === 'https',
|
|
};
|
|
const plexClient = new PlexAPI({
|
|
plexToken: admin.plexToken,
|
|
plexSettings: plexDeviceSettings,
|
|
timeout: 5000,
|
|
});
|
|
|
|
try {
|
|
await plexClient.getStatus();
|
|
connection.status = 200;
|
|
connection.message = 'OK';
|
|
} catch (e) {
|
|
connection.status = 500;
|
|
connection.message = e.message.split(':')[0];
|
|
}
|
|
})
|
|
);
|
|
})
|
|
);
|
|
}
|
|
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: 'Unable to retrieve Plex server list.',
|
|
});
|
|
}
|
|
});
|
|
|
|
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: true, plexToken: true },
|
|
where: { id: 1 },
|
|
});
|
|
const plexapi = new PlexAPI({ plexToken: admin.plexToken });
|
|
|
|
await plexapi.syncLibraries();
|
|
}
|
|
|
|
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('/jellyfin', (_req, res) => {
|
|
const settings = getSettings();
|
|
|
|
res.status(200).json(settings.jellyfin);
|
|
});
|
|
|
|
settingsRoutes.post('/jellyfin', (req, res) => {
|
|
const settings = getSettings();
|
|
|
|
settings.jellyfin = merge(settings.jellyfin, req.body);
|
|
settings.save();
|
|
|
|
return res.status(200).json(settings.jellyfin);
|
|
});
|
|
|
|
settingsRoutes.get('/jellyfin/library', async (req, res, next) => {
|
|
const settings = getSettings();
|
|
|
|
if (req.query.sync) {
|
|
const userRepository = getRepository(User);
|
|
const admin = await userRepository.findOneOrFail({
|
|
select: ['id', 'jellyfinAuthToken', 'jellyfinDeviceId', 'jellyfinUserId'],
|
|
where: { id: 1 },
|
|
order: { id: 'ASC' },
|
|
});
|
|
const jellyfinClient = new JellyfinAPI(
|
|
settings.jellyfin.hostname ?? '',
|
|
admin.jellyfinAuthToken ?? '',
|
|
admin.jellyfinDeviceId ?? ''
|
|
);
|
|
|
|
jellyfinClient.setUserId(admin.jellyfinUserId ?? '');
|
|
|
|
const libraries = await jellyfinClient.getLibraries();
|
|
|
|
if (libraries.length === 0) {
|
|
// Check if no libraries are found due to the fallback to user views
|
|
// This only affects LDAP users
|
|
const account = await jellyfinClient.getUser();
|
|
|
|
// 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: 404, message: 'SYNC_ERROR_NO_LIBRARIES' });
|
|
}
|
|
|
|
const newLibraries: Library[] = libraries.map((library) => {
|
|
const existing = settings.jellyfin.libraries.find(
|
|
(l) => l.id === library.key && l.name === library.title
|
|
);
|
|
|
|
return {
|
|
id: library.key,
|
|
name: library.title,
|
|
enabled: existing?.enabled ?? false,
|
|
type: library.type,
|
|
};
|
|
});
|
|
|
|
settings.jellyfin.libraries = newLibraries;
|
|
}
|
|
|
|
const enabledLibraries = req.query.enable
|
|
? (req.query.enable as string).split(',')
|
|
: [];
|
|
settings.jellyfin.libraries = settings.jellyfin.libraries.map((library) => ({
|
|
...library,
|
|
enabled: enabledLibraries.includes(library.id),
|
|
}));
|
|
settings.save();
|
|
return res.status(200).json(settings.jellyfin.libraries);
|
|
});
|
|
|
|
settingsRoutes.get('/jellyfin/users', async (req, res) => {
|
|
const settings = getSettings();
|
|
const { hostname, externalHostname } = getSettings().jellyfin;
|
|
let jellyfinHost =
|
|
externalHostname && externalHostname.length > 0
|
|
? externalHostname
|
|
: hostname;
|
|
|
|
jellyfinHost = jellyfinHost.endsWith('/')
|
|
? jellyfinHost.slice(0, -1)
|
|
: jellyfinHost;
|
|
const userRepository = getRepository(User);
|
|
const admin = await userRepository.findOneOrFail({
|
|
select: ['id', 'jellyfinAuthToken', 'jellyfinDeviceId', 'jellyfinUserId'],
|
|
where: { id: 1 },
|
|
order: { id: 'ASC' },
|
|
});
|
|
const jellyfinClient = new JellyfinAPI(
|
|
settings.jellyfin.hostname ?? '',
|
|
admin.jellyfinAuthToken ?? '',
|
|
admin.jellyfinDeviceId ?? ''
|
|
);
|
|
|
|
jellyfinClient.setUserId(admin.jellyfinUserId ?? '');
|
|
const resp = await jellyfinClient.getUsers();
|
|
const users = resp.users.map((user) => ({
|
|
username: user.Name,
|
|
id: user.Id,
|
|
thumb: user.PrimaryImageTag
|
|
? `${jellyfinHost}/Users/${user.Id}/Images/Primary/?tag=${user.PrimaryImageTag}&quality=90`
|
|
: gravatarUrl(user.Name, { default: 'mm', size: 200 }),
|
|
email: user.Name,
|
|
}));
|
|
|
|
return res.status(200).json(users);
|
|
});
|
|
|
|
settingsRoutes.get('/jellyfin/sync', (_req, res) => {
|
|
return res.status(200).json(jellyfinFullScanner.status());
|
|
});
|
|
|
|
settingsRoutes.post('/jellyfin/sync', (req, res) => {
|
|
if (req.body.cancel) {
|
|
jellyfinFullScanner.cancel();
|
|
} else if (req.body.start) {
|
|
jellyfinFullScanner.run();
|
|
}
|
|
return res.status(200).json(jellyfinFullScanner.status());
|
|
});
|
|
settingsRoutes.get('/tautulli', (_req, res) => {
|
|
const settings = getSettings();
|
|
|
|
res.status(200).json(settings.tautulli);
|
|
});
|
|
|
|
settingsRoutes.post('/tautulli', async (req, res, next) => {
|
|
const settings = getSettings();
|
|
|
|
Object.assign(settings.tautulli, req.body);
|
|
|
|
if (settings.tautulli.hostname) {
|
|
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);
|
|
});
|
|
|
|
settingsRoutes.get(
|
|
'/plex/users',
|
|
isAuthenticated(Permission.MANAGE_USERS),
|
|
async (req, res, next) => {
|
|
const userRepository = getRepository(User);
|
|
const qb = userRepository.createQueryBuilder('user');
|
|
|
|
try {
|
|
const admin = await userRepository.findOneOrFail({
|
|
select: { id: true, plexToken: true },
|
|
where: { id: 1 },
|
|
});
|
|
const plexApi = new PlexTvAPI(admin.plexToken ?? '');
|
|
const plexUsers = (await plexApi.getUsers()).MediaContainer.User.map(
|
|
(user) => user.$
|
|
).filter((user) => user.email);
|
|
|
|
const unimportedPlexUsers: {
|
|
id: string;
|
|
title: string;
|
|
username: string;
|
|
email: string;
|
|
thumb: string;
|
|
}[] = [];
|
|
|
|
const existingUsers = await qb
|
|
.where('user.plexId IN (:...plexIds)', {
|
|
plexIds: plexUsers.map((plexUser) => plexUser.id),
|
|
})
|
|
.orWhere('user.email IN (:...plexEmails)', {
|
|
plexEmails: plexUsers.map((plexUser) => plexUser.email.toLowerCase()),
|
|
})
|
|
.getMany();
|
|
|
|
await Promise.all(
|
|
plexUsers.map(async (plexUser) => {
|
|
if (
|
|
!existingUsers.find(
|
|
(user) =>
|
|
user.plexId === parseInt(plexUser.id) ||
|
|
user.email === plexUser.email.toLowerCase()
|
|
) &&
|
|
(await plexApi.checkUserAccess(parseInt(plexUser.id)))
|
|
) {
|
|
unimportedPlexUsers.push(plexUser);
|
|
}
|
|
})
|
|
);
|
|
|
|
return res.status(200).json(sortBy(unimportedPlexUsers, 'username'));
|
|
} catch (e) {
|
|
logger.error('Something went wrong getting unimported Plex users', {
|
|
label: 'API',
|
|
errorMessage: e.message,
|
|
});
|
|
next({
|
|
status: 500,
|
|
message: 'Unable to retrieve unimported Plex users.',
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
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;
|
|
const search = (req.query.search as string) ?? '';
|
|
const searchRegexp = new RegExp(escapeRegExp(search), 'i');
|
|
|
|
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/.machinelogs.json`
|
|
: path.join(__dirname, '../../../config/logs/.machinelogs.json');
|
|
const logs: LogMessage[] = [];
|
|
const logMessageProperties = [
|
|
'timestamp',
|
|
'level',
|
|
'label',
|
|
'message',
|
|
'data',
|
|
];
|
|
|
|
const deepValueStrings = (obj: Record<string, unknown>): string[] => {
|
|
const values = [];
|
|
|
|
for (const val of Object.values(obj)) {
|
|
if (typeof val === 'string') {
|
|
values.push(val);
|
|
} else if (typeof val === 'number') {
|
|
values.push(val.toString());
|
|
} else if (val !== null && typeof val === 'object') {
|
|
values.push(...deepValueStrings(val as Record<string, unknown>));
|
|
}
|
|
}
|
|
|
|
return values;
|
|
};
|
|
|
|
try {
|
|
fs.readFileSync(logFile, 'utf-8')
|
|
.split('\n')
|
|
.forEach((line) => {
|
|
if (!line.length) return;
|
|
|
|
const logMessage = JSON.parse(line);
|
|
|
|
if (!filter.includes(logMessage.level)) {
|
|
return;
|
|
}
|
|
|
|
if (
|
|
!Object.keys(logMessage).every((key) =>
|
|
logMessageProperties.includes(key)
|
|
)
|
|
) {
|
|
Object.keys(logMessage)
|
|
.filter((prop) => !logMessageProperties.includes(prop))
|
|
.forEach((prop) => {
|
|
set(logMessage, `data.${prop}`, logMessage[prop]);
|
|
});
|
|
}
|
|
|
|
if (req.query.search) {
|
|
if (
|
|
// label and data are sometimes undefined
|
|
!searchRegexp.test(logMessage.label ?? '') &&
|
|
!searchRegexp.test(logMessage.message) &&
|
|
!deepValueStrings(logMessage.data ?? {}).some((val) =>
|
|
searchRegexp.test(val)
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
logs.push(logMessage);
|
|
});
|
|
|
|
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 retrieving logs', {
|
|
label: 'Logs',
|
|
errorMessage: error.message,
|
|
});
|
|
return next({
|
|
status: 500,
|
|
message: 'Unable to retrieve logs.',
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
settingsRoutes.get('/jobs', (_req, res) => {
|
|
return res.status(200).json(
|
|
scheduledJobs.map((job) => ({
|
|
id: job.id,
|
|
name: job.name,
|
|
type: job.type,
|
|
interval: job.interval,
|
|
cronSchedule: job.cronSchedule,
|
|
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,
|
|
interval: scheduledJob.interval,
|
|
cronSchedule: scheduledJob.cronSchedule,
|
|
nextExecutionTime: scheduledJob.job.nextInvocation(),
|
|
running: scheduledJob.running ? scheduledJob.running() : false,
|
|
});
|
|
});
|
|
|
|
settingsRoutes.post<{ jobId: JobId }>(
|
|
'/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,
|
|
interval: scheduledJob.interval,
|
|
cronSchedule: scheduledJob.cronSchedule,
|
|
nextExecutionTime: scheduledJob.job.nextInvocation(),
|
|
running: scheduledJob.running ? scheduledJob.running() : false,
|
|
});
|
|
}
|
|
);
|
|
|
|
settingsRoutes.post<{ jobId: JobId }>(
|
|
'/jobs/:jobId/schedule',
|
|
(req, res, next) => {
|
|
const scheduledJob = scheduledJobs.find(
|
|
(job) => job.id === req.params.jobId
|
|
);
|
|
|
|
if (!scheduledJob) {
|
|
return next({ status: 404, message: 'Job not found.' });
|
|
}
|
|
|
|
const result = rescheduleJob(scheduledJob.job, req.body.schedule);
|
|
const settings = getSettings();
|
|
|
|
if (result) {
|
|
settings.jobs[scheduledJob.id].schedule = req.body.schedule;
|
|
settings.save();
|
|
|
|
scheduledJob.cronSchedule = req.body.schedule;
|
|
|
|
return res.status(200).json({
|
|
id: scheduledJob.id,
|
|
name: scheduledJob.name,
|
|
type: scheduledJob.type,
|
|
interval: scheduledJob.interval,
|
|
cronSchedule: scheduledJob.cronSchedule,
|
|
nextExecutionTime: scheduledJob.job.nextInvocation(),
|
|
running: scheduledJob.running ? scheduledJob.running() : false,
|
|
});
|
|
} else {
|
|
return next({ status: 400, message: 'Invalid job schedule.' });
|
|
}
|
|
}
|
|
);
|
|
|
|
settingsRoutes.get('/cache', async (_req, res) => {
|
|
const cacheManagerCaches = cacheManager.getAllCaches();
|
|
|
|
const apiCaches = Object.values(cacheManagerCaches).map((cache) => ({
|
|
id: cache.id,
|
|
name: cache.name,
|
|
stats: cache.getStats(),
|
|
}));
|
|
|
|
const tmdbImageCache = await ImageProxy.getImageStats('tmdb');
|
|
|
|
return res.status(200).json({
|
|
apiCaches,
|
|
imageCache: {
|
|
tmdb: tmdbImageCache,
|
|
},
|
|
});
|
|
});
|
|
|
|
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 not found.' });
|
|
}
|
|
);
|
|
|
|
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,
|
|
appDataPath: appDataPath(),
|
|
} as SettingsAboutResponse);
|
|
});
|
|
|
|
export default settingsRoutes;
|