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/media.ts

308 lines
8.2 KiB

import TautulliAPI from '@server/api/tautulli';
import {
MediaRequestStatus,
MediaStatus,
MediaType,
} from '@server/constants/media';
import { getRepository } from '@server/datasource';
import Media from '@server/entity/Media';
import MediaRequest from '@server/entity/MediaRequest';
import SeasonRequest from '@server/entity/SeasonRequest';
import { User } from '@server/entity/User';
import type {
MediaResultsResponse,
MediaWatchDataResponse,
} from '@server/interfaces/api/mediaInterfaces';
import { Permission } from '@server/lib/permissions';
import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
import { isAuthenticated } from '@server/middleware/auth';
import { Router } from 'express';
import type { FindOneOptions } from 'typeorm';
import { In } from 'typeorm';
const mediaRoutes = Router();
mediaRoutes.get('/', async (req, res, next) => {
const mediaRepository = getRepository(Media);
const pageSize = req.query.take ? Number(req.query.take) : 20;
const skip = req.query.skip ? Number(req.query.skip) : 0;
let statusFilter = undefined;
switch (req.query.filter) {
case 'available':
statusFilter = MediaStatus.AVAILABLE;
break;
case 'partial':
statusFilter = MediaStatus.PARTIALLY_AVAILABLE;
break;
case 'allavailable':
statusFilter = In([
MediaStatus.AVAILABLE,
MediaStatus.PARTIALLY_AVAILABLE,
]);
break;
case 'processing':
statusFilter = MediaStatus.PROCESSING;
break;
case 'pending':
statusFilter = MediaStatus.PENDING;
break;
default:
statusFilter = undefined;
}
let sortFilter: FindOneOptions<Media>['order'] = {
id: 'DESC',
};
switch (req.query.sort) {
case 'modified':
sortFilter = {
updatedAt: 'DESC',
};
break;
case 'mediaAdded':
sortFilter = {
mediaAddedAt: 'DESC',
};
}
try {
const [media, mediaCount] = await mediaRepository.findAndCount({
order: sortFilter,
where: statusFilter && {
status: statusFilter,
},
take: pageSize,
skip,
});
return res.status(200).json({
pageInfo: {
pages: Math.ceil(mediaCount / pageSize),
pageSize,
results: mediaCount,
page: Math.ceil(skip / pageSize) + 1,
},
results: media,
} as MediaResultsResponse);
} catch (e) {
next({ status: 500, message: e.message });
}
});
mediaRoutes.post<
{
id: string;
status: 'available' | 'partial' | 'processing' | 'pending' | 'unknown';
},
Media
>(
'/:id/:status',
isAuthenticated(Permission.MANAGE_REQUESTS),
async (req, res, next) => {
const mediaRepository = getRepository(Media);
const requestRepository = getRepository(MediaRequest);
const seasonRequestRepository = getRepository(SeasonRequest);
const media = await mediaRepository.findOne({
where: { id: Number(req.params.id) },
});
if (!media) {
return next({ status: 404, message: 'Media does not exist.' });
}
const is4k = Boolean(req.body.is4k);
switch (req.params.status) {
case 'available':
media[is4k ? 'status4k' : 'status'] = MediaStatus.AVAILABLE;
if (media.mediaType === MediaType.TV) {
// Mark all seasons available
media.seasons.forEach((season) => {
season[is4k ? 'status4k' : 'status'] = MediaStatus.AVAILABLE;
});
}
break;
case 'partial':
if (media.mediaType === MediaType.MOVIE) {
return next({
status: 400,
message: 'Only series can be set to be partially available',
});
}
media.status = MediaStatus.PARTIALLY_AVAILABLE;
break;
case 'processing':
media.status = MediaStatus.PROCESSING;
break;
case 'pending':
media.status = MediaStatus.PENDING;
break;
case 'unknown':
media.status = MediaStatus.UNKNOWN;
}
if (req.params.status === 'available') {
// Here we check all related media requests and
// then set to completed if the media is marked
// as available
const requests = await requestRepository.find({
relations: {
media: true,
},
where: { media: { id: media.id }, is4k: is4k },
});
const requestIds = requests.map((request) => request.id);
if (requestIds.length > 0) {
await requestRepository.update(
{ id: In(requestIds) },
{ status: MediaRequestStatus.COMPLETED }
);
}
requests
.flatMap((request) => request.seasons)
.forEach(async (season) => {
await seasonRequestRepository.update(season.id, {
status: MediaRequestStatus.COMPLETED,
});
});
}
await mediaRepository.save(media);
return res.status(200).json(media);
}
);
mediaRoutes.delete(
'/:id',
isAuthenticated(Permission.MANAGE_REQUESTS),
async (req, res, next) => {
try {
const mediaRepository = getRepository(Media);
const media = await mediaRepository.findOneOrFail({
where: { id: Number(req.params.id) },
});
await mediaRepository.remove(media);
return res.status(204).send();
} catch (e) {
logger.error('Something went wrong fetching media in delete request', {
label: 'Media',
message: e.message,
});
next({ status: 404, message: 'Media not found' });
}
}
);
mediaRoutes.get<{ id: string }, MediaWatchDataResponse>(
'/:id/watch_data',
isAuthenticated(Permission.ADMIN),
async (req, res, next) => {
const settings = getSettings().tautulli;
if (!settings.hostname || !settings.port || !settings.apiKey) {
return next({
status: 404,
message: 'Tautulli API not configured.',
});
}
const media = await getRepository(Media).findOne({
where: { id: Number(req.params.id) },
});
if (!media) {
return next({ status: 404, message: 'Media does not exist.' });
}
try {
const tautulli = new TautulliAPI(settings);
const userRepository = getRepository(User);
const response: MediaWatchDataResponse = {};
if (media.ratingKey) {
const watchStats = await tautulli.getMediaWatchStats(media.ratingKey);
const watchUsers = await tautulli.getMediaWatchUsers(media.ratingKey);
const users = await userRepository
.createQueryBuilder('user')
.where('user.plexId IN (:...plexIds)', {
plexIds: watchUsers.map((u) => u.user_id),
})
.getMany();
const playCount =
watchStats.find((i) => i.query_days == 0)?.total_plays ?? 0;
const playCount7Days =
watchStats.find((i) => i.query_days == 7)?.total_plays ?? 0;
const playCount30Days =
watchStats.find((i) => i.query_days == 30)?.total_plays ?? 0;
response.data = {
users: users,
playCount,
playCount7Days,
playCount30Days,
};
}
if (media.ratingKey4k) {
const watchStats4k = await tautulli.getMediaWatchStats(
media.ratingKey4k
);
const watchUsers4k = await tautulli.getMediaWatchUsers(
media.ratingKey4k
);
const users = await userRepository
.createQueryBuilder('user')
.where('user.plexId IN (:...plexIds)', {
plexIds: watchUsers4k.map((u) => u.user_id),
})
.getMany();
const playCount =
watchStats4k.find((i) => i.query_days == 0)?.total_plays ?? 0;
const playCount7Days =
watchStats4k.find((i) => i.query_days == 7)?.total_plays ?? 0;
const playCount30Days =
watchStats4k.find((i) => i.query_days == 30)?.total_plays ?? 0;
response.data4k = {
users,
playCount,
playCount7Days,
playCount30Days,
};
}
return res.status(200).json(response);
} catch (e) {
logger.error('Something went wrong fetching media watch data', {
label: 'API',
errorMessage: e.message,
mediaId: req.params.id,
});
next({ status: 500, message: 'Failed to fetch watch data.' });
}
}
);
export default mediaRoutes;