mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2025-12-31 19:59:31 -05:00
Merge branch 'develop' into features/deleteMediaFile
This commit is contained in:
@@ -89,13 +89,28 @@ authRoutes.post('/plex', async (req, res, next) => {
|
||||
await userRepository.save(user);
|
||||
} else {
|
||||
const mainUser = await userRepository.findOneOrFail({
|
||||
select: { id: true, plexToken: true, plexId: true },
|
||||
select: { id: true, plexToken: true, plexId: true, email: true },
|
||||
where: { id: 1 },
|
||||
});
|
||||
const mainPlexTv = new PlexTvAPI(mainUser.plexToken ?? '');
|
||||
|
||||
if (!account.id) {
|
||||
logger.error('Plex ID was missing from Plex.tv response', {
|
||||
label: 'API',
|
||||
ip: req.ip,
|
||||
email: account.email,
|
||||
plexUsername: account.username,
|
||||
});
|
||||
|
||||
return next({
|
||||
status: 500,
|
||||
message: 'Something went wrong. Try again.',
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
account.id === mainUser.plexId ||
|
||||
(account.email === mainUser.email && !mainUser.plexId) ||
|
||||
(await mainPlexTv.checkUserAccess(account.id))
|
||||
) {
|
||||
if (user) {
|
||||
@@ -226,7 +241,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
const hostname =
|
||||
settings.jellyfin.hostname !== ''
|
||||
? settings.jellyfin.hostname
|
||||
: body.hostname;
|
||||
: body.hostname ?? '';
|
||||
const { externalHostname } = getSettings().jellyfin;
|
||||
|
||||
// Try to find deviceId that corresponds to jellyfin user, else generate a new one
|
||||
@@ -244,11 +259,15 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
}
|
||||
// First we need to attempt to log the user in to jellyfin
|
||||
const jellyfinserver = new JellyfinAPI(hostname ?? '', undefined, deviceId);
|
||||
const jellyfinHost =
|
||||
let jellyfinHost =
|
||||
externalHostname && externalHostname.length > 0
|
||||
? externalHostname
|
||||
: hostname;
|
||||
|
||||
jellyfinHost = jellyfinHost.endsWith('/')
|
||||
? jellyfinHost.slice(0, -1)
|
||||
: jellyfinHost;
|
||||
|
||||
const account = await jellyfinserver.login(body.username, body.password);
|
||||
// Next let's see if the user already exists
|
||||
user = await userRepository.findOne({
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import PlexTvAPI from '@server/api/plextv';
|
||||
import type { SortOptions } from '@server/api/themoviedb';
|
||||
import TheMovieDb from '@server/api/themoviedb';
|
||||
import type { TmdbKeyword } from '@server/api/themoviedb/interfaces';
|
||||
import { MediaType } from '@server/constants/media';
|
||||
import { getRepository } from '@server/datasource';
|
||||
import Media from '@server/entity/Media';
|
||||
@@ -20,6 +22,7 @@ import { mapNetwork } from '@server/models/Tv';
|
||||
import { isMovie, isPerson } from '@server/utils/typeHelpers';
|
||||
import { Router } from 'express';
|
||||
import { sortBy } from 'lodash';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const createTmdbWithRegionLanguage = (user?: User): TheMovieDb => {
|
||||
const settings = getSettings();
|
||||
@@ -46,25 +49,76 @@ export const createTmdbWithRegionLanguage = (user?: User): TheMovieDb => {
|
||||
|
||||
const discoverRoutes = Router();
|
||||
|
||||
const QueryFilterOptions = z.object({
|
||||
page: z.coerce.string().optional(),
|
||||
sortBy: z.coerce.string().optional(),
|
||||
primaryReleaseDateGte: z.coerce.string().optional(),
|
||||
primaryReleaseDateLte: z.coerce.string().optional(),
|
||||
firstAirDateGte: z.coerce.string().optional(),
|
||||
firstAirDateLte: z.coerce.string().optional(),
|
||||
studio: z.coerce.string().optional(),
|
||||
genre: z.coerce.string().optional(),
|
||||
keywords: z.coerce.string().optional(),
|
||||
language: z.coerce.string().optional(),
|
||||
withRuntimeGte: z.coerce.string().optional(),
|
||||
withRuntimeLte: z.coerce.string().optional(),
|
||||
voteAverageGte: z.coerce.string().optional(),
|
||||
voteAverageLte: z.coerce.string().optional(),
|
||||
network: z.coerce.string().optional(),
|
||||
watchProviders: z.coerce.string().optional(),
|
||||
watchRegion: z.coerce.string().optional(),
|
||||
});
|
||||
|
||||
export type FilterOptions = z.infer<typeof QueryFilterOptions>;
|
||||
|
||||
discoverRoutes.get('/movies', async (req, res, next) => {
|
||||
const tmdb = createTmdbWithRegionLanguage(req.user);
|
||||
|
||||
try {
|
||||
const query = QueryFilterOptions.parse(req.query);
|
||||
const keywords = query.keywords;
|
||||
const data = await tmdb.getDiscoverMovies({
|
||||
page: Number(req.query.page),
|
||||
language: req.locale ?? (req.query.language as string),
|
||||
genre: req.query.genre ? Number(req.query.genre) : undefined,
|
||||
studio: req.query.studio ? Number(req.query.studio) : undefined,
|
||||
page: Number(query.page),
|
||||
sortBy: query.sortBy as SortOptions,
|
||||
language: req.locale ?? query.language,
|
||||
originalLanguage: query.language,
|
||||
genre: query.genre,
|
||||
studio: query.studio,
|
||||
primaryReleaseDateLte: query.primaryReleaseDateLte
|
||||
? new Date(query.primaryReleaseDateLte).toISOString().split('T')[0]
|
||||
: undefined,
|
||||
primaryReleaseDateGte: query.primaryReleaseDateGte
|
||||
? new Date(query.primaryReleaseDateGte).toISOString().split('T')[0]
|
||||
: undefined,
|
||||
keywords,
|
||||
withRuntimeGte: query.withRuntimeGte,
|
||||
withRuntimeLte: query.withRuntimeLte,
|
||||
voteAverageGte: query.voteAverageGte,
|
||||
voteAverageLte: query.voteAverageLte,
|
||||
watchProviders: query.watchProviders,
|
||||
watchRegion: query.watchRegion,
|
||||
});
|
||||
|
||||
const media = await Media.getRelatedMedia(
|
||||
data.results.map((result) => result.id)
|
||||
);
|
||||
|
||||
let keywordData: TmdbKeyword[] = [];
|
||||
if (keywords) {
|
||||
const splitKeywords = keywords.split(',');
|
||||
|
||||
keywordData = await Promise.all(
|
||||
splitKeywords.map(async (keywordId) => {
|
||||
return await tmdb.getKeywordDetails({ keywordId: Number(keywordId) });
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
page: data.page,
|
||||
totalPages: data.total_pages,
|
||||
totalResults: data.total_results,
|
||||
keywords: keywordData,
|
||||
results: data.results.map((result) =>
|
||||
mapMovieResult(
|
||||
result,
|
||||
@@ -163,7 +217,7 @@ discoverRoutes.get<{ genreId: string }>(
|
||||
const data = await tmdb.getDiscoverMovies({
|
||||
page: Number(req.query.page),
|
||||
language: req.locale ?? (req.query.language as string),
|
||||
genre: Number(req.params.genreId),
|
||||
genre: req.params.genreId as string,
|
||||
});
|
||||
|
||||
const media = await Media.getRelatedMedia(
|
||||
@@ -210,7 +264,7 @@ discoverRoutes.get<{ studioId: string }>(
|
||||
const data = await tmdb.getDiscoverMovies({
|
||||
page: Number(req.query.page),
|
||||
language: req.locale ?? (req.query.language as string),
|
||||
studio: Number(req.params.studioId),
|
||||
studio: req.params.studioId as string,
|
||||
});
|
||||
|
||||
const media = await Media.getRelatedMedia(
|
||||
@@ -296,21 +350,50 @@ discoverRoutes.get('/tv', async (req, res, next) => {
|
||||
const tmdb = createTmdbWithRegionLanguage(req.user);
|
||||
|
||||
try {
|
||||
const query = QueryFilterOptions.parse(req.query);
|
||||
const keywords = query.keywords;
|
||||
const data = await tmdb.getDiscoverTv({
|
||||
page: Number(req.query.page),
|
||||
language: req.locale ?? (req.query.language as string),
|
||||
genre: req.query.genre ? Number(req.query.genre) : undefined,
|
||||
network: req.query.network ? Number(req.query.network) : undefined,
|
||||
page: Number(query.page),
|
||||
sortBy: query.sortBy as SortOptions,
|
||||
language: req.locale ?? query.language,
|
||||
genre: query.genre,
|
||||
network: query.network ? Number(query.network) : undefined,
|
||||
firstAirDateLte: query.firstAirDateLte
|
||||
? new Date(query.firstAirDateLte).toISOString().split('T')[0]
|
||||
: undefined,
|
||||
firstAirDateGte: query.firstAirDateGte
|
||||
? new Date(query.firstAirDateGte).toISOString().split('T')[0]
|
||||
: undefined,
|
||||
originalLanguage: query.language,
|
||||
keywords,
|
||||
withRuntimeGte: query.withRuntimeGte,
|
||||
withRuntimeLte: query.withRuntimeLte,
|
||||
voteAverageGte: query.voteAverageGte,
|
||||
voteAverageLte: query.voteAverageLte,
|
||||
watchProviders: query.watchProviders,
|
||||
watchRegion: query.watchRegion,
|
||||
});
|
||||
|
||||
const media = await Media.getRelatedMedia(
|
||||
data.results.map((result) => result.id)
|
||||
);
|
||||
|
||||
let keywordData: TmdbKeyword[] = [];
|
||||
if (keywords) {
|
||||
const splitKeywords = keywords.split(',');
|
||||
|
||||
keywordData = await Promise.all(
|
||||
splitKeywords.map(async (keywordId) => {
|
||||
return await tmdb.getKeywordDetails({ keywordId: Number(keywordId) });
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
page: data.page,
|
||||
totalPages: data.total_pages,
|
||||
totalResults: data.total_results,
|
||||
keywords: keywordData,
|
||||
results: data.results.map((result) =>
|
||||
mapTvResult(
|
||||
result,
|
||||
@@ -408,7 +491,7 @@ discoverRoutes.get<{ genreId: string }>(
|
||||
const data = await tmdb.getDiscoverTv({
|
||||
page: Number(req.query.page),
|
||||
language: req.locale ?? (req.query.language as string),
|
||||
genre: Number(req.params.genreId),
|
||||
genre: req.params.genreId,
|
||||
});
|
||||
|
||||
const media = await Media.getRelatedMedia(
|
||||
@@ -643,7 +726,9 @@ discoverRoutes.get<{ language: string }, GenreSliderItem[]>(
|
||||
|
||||
await Promise.all(
|
||||
genres.map(async (genre) => {
|
||||
const genreData = await tmdb.getDiscoverMovies({ genre: genre.id });
|
||||
const genreData = await tmdb.getDiscoverMovies({
|
||||
genre: genre.id.toString(),
|
||||
});
|
||||
|
||||
mappedGenres.push({
|
||||
id: genre.id,
|
||||
@@ -685,7 +770,9 @@ discoverRoutes.get<{ language: string }, GenreSliderItem[]>(
|
||||
|
||||
await Promise.all(
|
||||
genres.map(async (genre) => {
|
||||
const genreData = await tmdb.getDiscoverTv({ genre: genre.id });
|
||||
const genreData = await tmdb.getDiscoverTv({
|
||||
genre: genre.id.toString(),
|
||||
});
|
||||
|
||||
mappedGenres.push({
|
||||
id: genre.id,
|
||||
|
||||
39
server/routes/imageproxy.ts
Normal file
39
server/routes/imageproxy.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import ImageProxy from '@server/lib/imageproxy';
|
||||
import logger from '@server/logger';
|
||||
import { Router } from 'express';
|
||||
|
||||
const router = Router();
|
||||
const tmdbImageProxy = new ImageProxy('tmdb', 'https://image.tmdb.org', {
|
||||
rateLimitOptions: {
|
||||
maxRequests: 20,
|
||||
maxRPS: 50,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Image Proxy
|
||||
*/
|
||||
router.get('/*', async (req, res) => {
|
||||
const imagePath = req.path.replace('/image', '');
|
||||
try {
|
||||
const imageData = await tmdbImageProxy.getImage(imagePath);
|
||||
|
||||
res.writeHead(200, {
|
||||
'Content-Type': `image/${imageData.meta.extension}`,
|
||||
'Content-Length': imageData.imageBuffer.length,
|
||||
'Cache-Control': `public, max-age=${imageData.meta.curRevalidate}`,
|
||||
'OS-Cache-Key': imageData.meta.cacheKey,
|
||||
'OS-Cache-Status': imageData.meta.cacheMiss ? 'MISS' : 'HIT',
|
||||
});
|
||||
|
||||
res.end(imageData.imageBuffer);
|
||||
} catch (e) {
|
||||
logger.error('Failed to proxy image', {
|
||||
imagePath,
|
||||
errorMessage: e.message,
|
||||
});
|
||||
res.status(500).send();
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -4,11 +4,14 @@ import type {
|
||||
TmdbMovieResult,
|
||||
TmdbTvResult,
|
||||
} from '@server/api/themoviedb/interfaces';
|
||||
import { getRepository } from '@server/datasource';
|
||||
import DiscoverSlider from '@server/entity/DiscoverSlider';
|
||||
import type { StatusResponse } from '@server/interfaces/api/settingsInterfaces';
|
||||
import { Permission } from '@server/lib/permissions';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import { checkUser, isAuthenticated } from '@server/middleware/auth';
|
||||
import { mapWatchProviderDetails } from '@server/models/common';
|
||||
import { mapProductionCompany } from '@server/models/Movie';
|
||||
import { mapNetwork } from '@server/models/Tv';
|
||||
import settingsRoutes from '@server/routes/settings';
|
||||
@@ -102,6 +105,13 @@ router.get('/settings/public', async (req, res) => {
|
||||
return res.status(200).json(settings.fullPublicSettings);
|
||||
}
|
||||
});
|
||||
router.get('/settings/discover', isAuthenticated(), async (_req, res) => {
|
||||
const sliderRepository = getRepository(DiscoverSlider);
|
||||
|
||||
const sliders = await sliderRepository.find({ order: { order: 'ASC' } });
|
||||
|
||||
return res.json(sliders);
|
||||
});
|
||||
router.use('/settings', isAuthenticated(Permission.ADMIN), settingsRoutes);
|
||||
router.use('/search', isAuthenticated(), searchRoutes);
|
||||
router.use('/discover', isAuthenticated(), discoverRoutes);
|
||||
@@ -269,6 +279,87 @@ router.get('/backdrops', async (req, res, next) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/keyword/:keywordId', async (req, res, next) => {
|
||||
const tmdb = createTmdbWithRegionLanguage();
|
||||
|
||||
try {
|
||||
const result = await tmdb.getKeywordDetails({
|
||||
keywordId: Number(req.params.keywordId),
|
||||
});
|
||||
|
||||
return res.status(200).json(result);
|
||||
} catch (e) {
|
||||
logger.debug('Something went wrong retrieving keyword data', {
|
||||
label: 'API',
|
||||
errorMessage: e.message,
|
||||
});
|
||||
return next({
|
||||
status: 500,
|
||||
message: 'Unable to retrieve keyword data.',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/watchproviders/regions', async (req, res, next) => {
|
||||
const tmdb = createTmdbWithRegionLanguage();
|
||||
|
||||
try {
|
||||
const result = await tmdb.getAvailableWatchProviderRegions({});
|
||||
return res.status(200).json(result);
|
||||
} catch (e) {
|
||||
logger.debug('Something went wrong retrieving watch provider regions', {
|
||||
label: 'API',
|
||||
errorMessage: e.message,
|
||||
});
|
||||
return next({
|
||||
status: 500,
|
||||
message: 'Unable to retrieve watch provider regions.',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/watchproviders/movies', async (req, res, next) => {
|
||||
const tmdb = createTmdbWithRegionLanguage();
|
||||
|
||||
try {
|
||||
const result = await tmdb.getMovieWatchProviders({
|
||||
watchRegion: req.query.watchRegion as string,
|
||||
});
|
||||
|
||||
return res.status(200).json(mapWatchProviderDetails(result));
|
||||
} catch (e) {
|
||||
logger.debug('Something went wrong retrieving movie watch providers', {
|
||||
label: 'API',
|
||||
errorMessage: e.message,
|
||||
});
|
||||
return next({
|
||||
status: 500,
|
||||
message: 'Unable to retrieve movie watch providers.',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/watchproviders/tv', async (req, res, next) => {
|
||||
const tmdb = createTmdbWithRegionLanguage();
|
||||
|
||||
try {
|
||||
const result = await tmdb.getTvWatchProviders({
|
||||
watchRegion: req.query.watchRegion as string,
|
||||
});
|
||||
|
||||
return res.status(200).json(mapWatchProviderDetails(result));
|
||||
} catch (e) {
|
||||
logger.debug('Something went wrong retrieving tv watch providers', {
|
||||
label: 'API',
|
||||
errorMessage: e.message,
|
||||
});
|
||||
return next({
|
||||
status: 500,
|
||||
message: 'Unable to retrieve tv watch providers.',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/', (_req, res) => {
|
||||
return res.status(200).json({
|
||||
api: 'Overseerr API',
|
||||
|
||||
@@ -308,7 +308,9 @@ issueRoutes.post<{ issueId: string }, Issue, { message: string }>(
|
||||
|
||||
issueRoutes.post<{ issueId: string; status: string }, Issue>(
|
||||
'/:issueId/:status',
|
||||
isAuthenticated(Permission.MANAGE_ISSUES),
|
||||
isAuthenticated([Permission.MANAGE_ISSUES, Permission.CREATE_ISSUES], {
|
||||
type: 'or',
|
||||
}),
|
||||
async (req, res, next) => {
|
||||
const issueRepository = getRepository(Issue);
|
||||
// Satisfy typescript here. User is set, we assure you!
|
||||
@@ -321,6 +323,16 @@ issueRoutes.post<{ issueId: string; status: string }, Issue>(
|
||||
where: { id: Number(req.params.issueId) },
|
||||
});
|
||||
|
||||
if (
|
||||
!req.user?.hasPermission(Permission.MANAGE_ISSUES) &&
|
||||
issue.createdBy.id !== req.user?.id
|
||||
) {
|
||||
return next({
|
||||
status: 401,
|
||||
message: 'You do not have permission to modify this issue.',
|
||||
});
|
||||
}
|
||||
|
||||
let newStatus: IssueStatus | undefined;
|
||||
|
||||
switch (req.params.status) {
|
||||
|
||||
@@ -492,8 +492,10 @@ requestRoutes.post<{
|
||||
relations: { requestedBy: true, modifiedBy: true },
|
||||
});
|
||||
|
||||
await request.updateParentStatus();
|
||||
await request.sendMedia();
|
||||
// this also triggers updating the parent media's status & sending to *arr
|
||||
request.status = MediaRequestStatus.APPROVED;
|
||||
await requestRepository.save(request);
|
||||
|
||||
return res.status(200).json(request);
|
||||
} catch (e) {
|
||||
logger.error('Error processing request retry', {
|
||||
|
||||
@@ -56,4 +56,50 @@ searchRoutes.get('/', async (req, res, next) => {
|
||||
}
|
||||
});
|
||||
|
||||
searchRoutes.get('/keyword', async (req, res, next) => {
|
||||
const tmdb = new TheMovieDb();
|
||||
|
||||
try {
|
||||
const results = await tmdb.searchKeyword({
|
||||
query: req.query.query as string,
|
||||
page: Number(req.query.page),
|
||||
});
|
||||
|
||||
return res.status(200).json(results);
|
||||
} catch (e) {
|
||||
logger.debug('Something went wrong retrieving keyword search results', {
|
||||
label: 'API',
|
||||
errorMessage: e.message,
|
||||
query: req.query.query,
|
||||
});
|
||||
return next({
|
||||
status: 500,
|
||||
message: 'Unable to retrieve keyword search results.',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
searchRoutes.get('/company', async (req, res, next) => {
|
||||
const tmdb = new TheMovieDb();
|
||||
|
||||
try {
|
||||
const results = await tmdb.searchCompany({
|
||||
query: req.query.query as string,
|
||||
page: Number(req.query.page),
|
||||
});
|
||||
|
||||
return res.status(200).json(results);
|
||||
} catch (e) {
|
||||
logger.debug('Something went wrong retrieving company search results', {
|
||||
label: 'API',
|
||||
errorMessage: e.message,
|
||||
query: req.query.query,
|
||||
});
|
||||
return next({
|
||||
status: 500,
|
||||
message: 'Unable to retrieve company search results.',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default searchRoutes;
|
||||
|
||||
131
server/routes/settings/discover.ts
Normal file
131
server/routes/settings/discover.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { getRepository } from '@server/datasource';
|
||||
import DiscoverSlider from '@server/entity/DiscoverSlider';
|
||||
import logger from '@server/logger';
|
||||
import { Router } from 'express';
|
||||
|
||||
const discoverSettingRoutes = Router();
|
||||
|
||||
discoverSettingRoutes.post('/', async (req, res) => {
|
||||
const sliderRepository = getRepository(DiscoverSlider);
|
||||
|
||||
const sliders = req.body as DiscoverSlider[];
|
||||
|
||||
if (!Array.isArray(sliders)) {
|
||||
return res.status(400).json({ message: 'Invalid request body.' });
|
||||
}
|
||||
|
||||
for (let x = 0; x < sliders.length; x++) {
|
||||
const slider = sliders[x];
|
||||
const existingSlider = await sliderRepository.findOne({
|
||||
where: {
|
||||
id: slider.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingSlider && slider.id) {
|
||||
existingSlider.enabled = slider.enabled;
|
||||
existingSlider.order = x;
|
||||
|
||||
// Only allow changes to the following when the slider is not built in
|
||||
if (!existingSlider.isBuiltIn) {
|
||||
existingSlider.title = slider.title;
|
||||
existingSlider.data = slider.data;
|
||||
existingSlider.type = slider.type;
|
||||
}
|
||||
|
||||
await sliderRepository.save(existingSlider);
|
||||
} else {
|
||||
const newSlider = new DiscoverSlider({
|
||||
isBuiltIn: false,
|
||||
data: slider.data,
|
||||
title: slider.title,
|
||||
enabled: slider.enabled,
|
||||
order: x,
|
||||
type: slider.type,
|
||||
});
|
||||
await sliderRepository.save(newSlider);
|
||||
}
|
||||
}
|
||||
|
||||
return res.json(sliders);
|
||||
});
|
||||
|
||||
discoverSettingRoutes.post('/add', async (req, res) => {
|
||||
const sliderRepository = getRepository(DiscoverSlider);
|
||||
|
||||
const slider = req.body as DiscoverSlider;
|
||||
|
||||
const newSlider = new DiscoverSlider({
|
||||
isBuiltIn: false,
|
||||
data: slider.data,
|
||||
title: slider.title,
|
||||
enabled: false,
|
||||
order: -1,
|
||||
type: slider.type,
|
||||
});
|
||||
await sliderRepository.save(newSlider);
|
||||
|
||||
return res.json(newSlider);
|
||||
});
|
||||
|
||||
discoverSettingRoutes.get('/reset', async (_req, res) => {
|
||||
const sliderRepository = getRepository(DiscoverSlider);
|
||||
|
||||
await sliderRepository.clear();
|
||||
await DiscoverSlider.bootstrapSliders();
|
||||
|
||||
return res.status(204).send();
|
||||
});
|
||||
|
||||
discoverSettingRoutes.put('/:sliderId', async (req, res, next) => {
|
||||
const sliderRepository = getRepository(DiscoverSlider);
|
||||
|
||||
const slider = req.body as DiscoverSlider;
|
||||
|
||||
try {
|
||||
const existingSlider = await sliderRepository.findOneOrFail({
|
||||
where: {
|
||||
id: Number(req.params.sliderId),
|
||||
},
|
||||
});
|
||||
|
||||
// Only allow changes to the following when the slider is not built in
|
||||
if (!existingSlider.isBuiltIn) {
|
||||
existingSlider.title = slider.title;
|
||||
existingSlider.data = slider.data;
|
||||
existingSlider.type = slider.type;
|
||||
}
|
||||
|
||||
await sliderRepository.save(existingSlider);
|
||||
|
||||
return res.status(200).json(existingSlider);
|
||||
} catch (e) {
|
||||
logger.error('Something went wrong updating a slider.', {
|
||||
label: 'API',
|
||||
errorMessage: e.message,
|
||||
});
|
||||
next({ status: 404, message: 'Slider not found or cannot be updated.' });
|
||||
}
|
||||
});
|
||||
|
||||
discoverSettingRoutes.delete('/:sliderId', async (req, res, next) => {
|
||||
const sliderRepository = getRepository(DiscoverSlider);
|
||||
|
||||
try {
|
||||
const slider = await sliderRepository.findOneOrFail({
|
||||
where: { id: Number(req.params.sliderId), isBuiltIn: false },
|
||||
});
|
||||
|
||||
await sliderRepository.remove(slider);
|
||||
|
||||
return res.status(204).send();
|
||||
} catch (e) {
|
||||
logger.error('Something went wrong deleting a slider.', {
|
||||
label: 'API',
|
||||
errorMessage: e.message,
|
||||
});
|
||||
next({ status: 404, message: 'Slider not found or cannot be deleted.' });
|
||||
}
|
||||
});
|
||||
|
||||
export default discoverSettingRoutes;
|
||||
@@ -16,12 +16,14 @@ import { jobJellyfinFullSync } from '@server/job/jellyfinsync';
|
||||
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 { plexFullScanner } from '@server/lib/scanners/plex';
|
||||
import type { Library, MainSettings } from '@server/lib/settings';
|
||||
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';
|
||||
@@ -41,6 +43,7 @@ const settingsRoutes = Router();
|
||||
settingsRoutes.use('/notifications', notificationRoutes);
|
||||
settingsRoutes.use('/radarr', radarrRoutes);
|
||||
settingsRoutes.use('/sonarr', sonarrRoutes);
|
||||
settingsRoutes.use('/discover', discoverSettingRoutes);
|
||||
|
||||
const filteredMainSettings = (
|
||||
user: User,
|
||||
@@ -307,11 +310,14 @@ settingsRoutes.get('/jellyfin/library', async (req, res) => {
|
||||
settingsRoutes.get('/jellyfin/users', async (req, res) => {
|
||||
const settings = getSettings();
|
||||
const { hostname, externalHostname } = getSettings().jellyfin;
|
||||
const jellyfinHost =
|
||||
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'],
|
||||
@@ -601,7 +607,7 @@ settingsRoutes.post<{ jobId: string }>('/jobs/:jobId/run', (req, res, next) => {
|
||||
});
|
||||
});
|
||||
|
||||
settingsRoutes.post<{ jobId: string }>(
|
||||
settingsRoutes.post<{ jobId: JobId }>(
|
||||
'/jobs/:jobId/cancel',
|
||||
(req, res, next) => {
|
||||
const scheduledJob = scheduledJobs.find(
|
||||
@@ -628,7 +634,7 @@ settingsRoutes.post<{ jobId: string }>(
|
||||
}
|
||||
);
|
||||
|
||||
settingsRoutes.post<{ jobId: string }>(
|
||||
settingsRoutes.post<{ jobId: JobId }>(
|
||||
'/jobs/:jobId/schedule',
|
||||
(req, res, next) => {
|
||||
const scheduledJob = scheduledJobs.find(
|
||||
@@ -663,16 +669,23 @@ settingsRoutes.post<{ jobId: string }>(
|
||||
}
|
||||
);
|
||||
|
||||
settingsRoutes.get('/cache', (req, res) => {
|
||||
const caches = cacheManager.getAllCaches();
|
||||
settingsRoutes.get('/cache', async (_req, res) => {
|
||||
const cacheManagerCaches = cacheManager.getAllCaches();
|
||||
|
||||
return res.status(200).json(
|
||||
Object.values(caches).map((cache) => ({
|
||||
id: cache.id,
|
||||
name: cache.name,
|
||||
stats: cache.getStats(),
|
||||
}))
|
||||
);
|
||||
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 }>(
|
||||
|
||||
@@ -497,11 +497,14 @@ router.post(
|
||||
//const jellyfinUsersResponse = await jellyfinClient.getUsers();
|
||||
const createdUsers: User[] = [];
|
||||
const { hostname, externalHostname } = getSettings().jellyfin;
|
||||
const jellyfinHost =
|
||||
let jellyfinHost =
|
||||
externalHostname && externalHostname.length > 0
|
||||
? externalHostname
|
||||
: hostname;
|
||||
|
||||
jellyfinHost = jellyfinHost.endsWith('/')
|
||||
? jellyfinHost.slice(0, -1)
|
||||
: jellyfinHost;
|
||||
jellyfinClient.setUserId(admin.jellyfinUserId ?? '');
|
||||
const jellyfinUsers = await jellyfinClient.getUsers();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user