You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
overseerr/server/routes/request.ts

328 lines
9.9 KiB

import { Router } from 'express';
import { isAuthenticated } from '../middleware/auth';
import { Permission } from '../lib/permissions';
import { getRepository, FindOperator, FindOneOptions, In } from 'typeorm';
import { MediaRequest } from '../entity/MediaRequest';
import TheMovieDb from '../api/themoviedb';
import Media from '../entity/Media';
import { MediaStatus, MediaRequestStatus, MediaType } from '../constants/media';
import SeasonRequest from '../entity/SeasonRequest';
import logger from '../logger';
import { RequestResultsResponse } from '../interfaces/api/requestInterfaces';
const requestRoutes = Router();
requestRoutes.get('/', async (req, res, next) => {
const requestRepository = getRepository(MediaRequest);
try {
const pageSize = req.query.take ? Number(req.query.take) : 20;
const skip = req.query.skip ? Number(req.query.skip) : 0;
let statusFilter:
| MediaRequestStatus
| FindOperator<string | MediaRequestStatus>
| undefined = undefined;
switch (req.query.filter) {
case 'available':
statusFilter = MediaRequestStatus.AVAILABLE;
break;
case 'approved':
statusFilter = MediaRequestStatus.APPROVED;
break;
case 'pending':
statusFilter = MediaRequestStatus.PENDING;
break;
case 'unavailable':
statusFilter = In([
MediaRequestStatus.PENDING,
MediaRequestStatus.APPROVED,
]);
break;
default:
statusFilter = In(Object.values(MediaRequestStatus));
}
let sortFilter: FindOneOptions<MediaRequest>['order'] = {
id: 'DESC',
};
switch (req.query.sort) {
case 'modified':
sortFilter = {
updatedAt: 'DESC',
};
break;
}
const [requests, requestCount] = req.user?.hasPermission(
Permission.MANAGE_REQUESTS
)
? await requestRepository.findAndCount({
order: sortFilter,
relations: ['media', 'modifiedBy'],
where: { status: statusFilter },
take: Number(req.query.take) ?? 20,
skip,
})
: await requestRepository.findAndCount({
where: { requestedBy: { id: req.user?.id }, status: statusFilter },
relations: ['media', 'modifiedBy'],
order: sortFilter,
take: Number(req.query.limit) ?? 20,
skip,
});
return res.status(200).json({
pageInfo: {
pages: Math.ceil(requestCount / pageSize),
pageSize,
results: requestCount,
page: Math.ceil(skip / pageSize) + 1,
},
results: requests,
} as RequestResultsResponse);
} catch (e) {
next({ status: 500, message: e.message });
}
});
requestRoutes.post(
'/',
isAuthenticated(Permission.REQUEST),
async (req, res, next) => {
const tmdb = new TheMovieDb();
const mediaRepository = getRepository(Media);
const requestRepository = getRepository(MediaRequest);
try {
const tmdbMedia =
req.body.mediaType === 'movie'
? await tmdb.getMovie({ movieId: req.body.mediaId })
: await tmdb.getTvShow({ tvId: req.body.mediaId });
let media = await mediaRepository.findOne({
where: { tmdbId: req.body.mediaId },
relations: ['requests'],
});
if (!media) {
media = new Media({
tmdbId: tmdbMedia.id,
tvdbId: tmdbMedia.external_ids.tvdb_id,
status: MediaStatus.PENDING,
mediaType: req.body.mediaType,
});
await mediaRepository.save(media);
} else {
if (media.status === MediaStatus.UNKNOWN) {
media.status = MediaStatus.PENDING;
await mediaRepository.save(media);
}
}
if (req.body.mediaType === 'movie') {
const request = new MediaRequest({
type: MediaType.MOVIE,
media,
requestedBy: req.user,
// If the user is an admin or has the "auto approve" permission, automatically approve the request
status:
req.user?.hasPermission(Permission.AUTO_APPROVE) ||
req.user?.hasPermission(Permission.AUTO_APPROVE_MOVIE)
? MediaRequestStatus.APPROVED
: MediaRequestStatus.PENDING,
modifiedBy:
req.user?.hasPermission(Permission.AUTO_APPROVE) ||
req.user?.hasPermission(Permission.AUTO_APPROVE_MOVIE)
? req.user
: undefined,
});
await requestRepository.save(request);
return res.status(201).json(request);
} else if (req.body.mediaType === 'tv') {
const requestedSeasons = req.body.seasons as number[];
let existingSeasons: number[] = [];
// We need to check existing requests on this title to make sure we don't double up on seasons that were
// already requested. In the case they were, we just throw out any duplicates but still approve the request.
// (Unless there are no seasons, in which case we abort)
if (media.requests) {
existingSeasons = media.requests.reduce((seasons, request) => {
const combinedSeasons = request.seasons.map(
(season) => season.seasonNumber
);
return [...seasons, ...combinedSeasons];
}, [] as number[]);
}
const finalSeasons = requestedSeasons.filter(
(rs) => !existingSeasons.includes(rs)
);
if (finalSeasons.length === 0) {
return next({
status: 500,
message: 'No seasons available to request',
});
}
const request = new MediaRequest({
type: MediaType.TV,
media: {
id: media.id,
} as Media,
requestedBy: req.user,
// If the user is an admin or has the "auto approve" permission, automatically approve the request
status:
req.user?.hasPermission(Permission.AUTO_APPROVE) ||
req.user?.hasPermission(Permission.AUTO_APPROVE_TV)
? MediaRequestStatus.APPROVED
: MediaRequestStatus.PENDING,
modifiedBy:
req.user?.hasPermission(Permission.AUTO_APPROVE) ||
req.user?.hasPermission(Permission.AUTO_APPROVE_TV)
? req.user
: undefined,
seasons: finalSeasons.map(
(sn) =>
new SeasonRequest({
seasonNumber: sn,
status:
req.user?.hasPermission(Permission.AUTO_APPROVE) ||
req.user?.hasPermission(Permission.AUTO_APPROVE_TV)
? MediaRequestStatus.APPROVED
: MediaRequestStatus.PENDING,
})
),
});
await requestRepository.save(request);
return res.status(201).json(request);
}
next({ status: 500, message: 'Invalid media type' });
} catch (e) {
next({ message: e.message, status: 500 });
}
}
);
requestRoutes.get('/:requestId', async (req, res, next) => {
const requestRepository = getRepository(MediaRequest);
try {
const request = await requestRepository.findOneOrFail({
where: { id: Number(req.params.requestId) },
relations: ['requestedBy', 'modifiedBy'],
});
return res.status(200).json(request);
} catch (e) {
next({ status: 404, message: 'Request not found' });
}
});
requestRoutes.delete('/:requestId', async (req, res, next) => {
const requestRepository = getRepository(MediaRequest);
try {
const request = await requestRepository.findOneOrFail({
where: { id: Number(req.params.requestId) },
relations: ['requestedBy', 'modifiedBy'],
});
if (
!req.user?.hasPermission(Permission.MANAGE_REQUESTS) &&
request.requestedBy.id !== req.user?.id &&
request.status !== 1
) {
return next({
status: 401,
message: 'You do not have permission to remove this request',
});
}
await requestRepository.remove(request);
return res.status(204).send();
} catch (e) {
logger.error(e.message);
next({ status: 404, message: 'Request not found' });
}
});
requestRoutes.post<{
requestId: string;
}>(
'/:requestId/retry',
isAuthenticated(Permission.MANAGE_REQUESTS),
async (req, res, next) => {
const requestRepository = getRepository(MediaRequest);
try {
const request = await requestRepository.findOneOrFail({
where: { id: Number(req.params.requestId) },
relations: ['requestedBy', 'modifiedBy'],
});
await request.updateParentStatus();
await request.sendMedia();
return res.status(200).json(request);
} catch (e) {
logger.error('Error processing request retry', {
label: 'Media Request',
message: e.message,
});
next({ status: 404, message: 'Request not found' });
}
}
);
requestRoutes.get<{
requestId: string;
status: 'pending' | 'approve' | 'decline';
}>(
'/:requestId/:status',
isAuthenticated(Permission.MANAGE_REQUESTS),
async (req, res, next) => {
const requestRepository = getRepository(MediaRequest);
try {
const request = await requestRepository.findOneOrFail({
where: { id: Number(req.params.requestId) },
relations: ['requestedBy', 'modifiedBy'],
});
let newStatus: MediaRequestStatus;
switch (req.params.status) {
case 'pending':
newStatus = MediaRequestStatus.PENDING;
break;
case 'approve':
newStatus = MediaRequestStatus.APPROVED;
break;
case 'decline':
newStatus = MediaRequestStatus.DECLINED;
break;
}
request.status = newStatus;
request.modifiedBy = req.user;
await requestRepository.save(request);
return res.status(200).json(request);
} catch (e) {
logger.error('Error processing request update', {
label: 'Media Request',
message: e.message,
});
next({ status: 404, message: 'Request not found' });
}
}
);
export default requestRoutes;