feat: view other users' watchlists (#2959)

* feat: view other users' watchlists

* test: add cypress tests

* feat(lang): translation keys

* refactor: yarn format

* fix: manage requests perm is parent of view watchlist perm
This commit is contained in:
TheCatLady
2022-08-21 22:50:27 -07:00
committed by GitHub
parent 950b1712b7
commit 0839718806
17 changed files with 346 additions and 57 deletions

View File

@@ -10,3 +10,10 @@ export interface WatchlistItem {
mediaType: 'movie' | 'tv';
title: string;
}
export interface WatchlistResponse {
page: number;
totalPages: number;
totalResults: number;
results: WatchlistItem[];
}

View File

@@ -23,6 +23,7 @@ export interface QuotaResponse {
movie: QuotaStatus;
tv: QuotaStatus;
}
export interface UserWatchDataResponse {
recentlyWatched: Media[];
playCount: number;

View File

@@ -25,6 +25,7 @@ export enum Permission {
AUTO_REQUEST_MOVIE = 16777216,
AUTO_REQUEST_TV = 33554432,
RECENT_VIEW = 67108864,
WATCHLIST_VIEW = 134217728,
}
export interface PermissionCheckOptions {

View File

@@ -6,7 +6,7 @@ import Media from '@server/entity/Media';
import { User } from '@server/entity/User';
import type {
GenreSliderItem,
WatchlistItem,
WatchlistResponse,
} from '@server/interfaces/api/discoverInterfaces';
import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
@@ -713,50 +713,45 @@ discoverRoutes.get<{ language: string }, GenreSliderItem[]>(
}
);
discoverRoutes.get<
{ page?: number },
{
page: number;
totalPages: number;
totalResults: number;
results: WatchlistItem[];
}
>('/watchlist', async (req, res) => {
const userRepository = getRepository(User);
const itemsPerPage = 20;
const page = req.params.page ?? 1;
const offset = (page - 1) * itemsPerPage;
discoverRoutes.get<{ page?: number }, WatchlistResponse>(
'/watchlist',
async (req, res) => {
const userRepository = getRepository(User);
const itemsPerPage = 20;
const page = req.params.page ?? 1;
const offset = (page - 1) * itemsPerPage;
const activeUser = await userRepository.findOne({
where: { id: req.user?.id },
select: ['id', 'plexToken'],
});
const activeUser = await userRepository.findOne({
where: { id: req.user?.id },
select: ['id', 'plexToken'],
});
if (!activeUser?.plexToken) {
// We will just return an empty array if the user has no Plex token
return res.json({
page: 1,
totalPages: 1,
totalResults: 0,
results: [],
});
}
const plexTV = new PlexTvAPI(activeUser.plexToken);
const watchlist = await plexTV.getWatchlist({ offset });
if (!activeUser?.plexToken) {
// We will just return an empty array if the user has no plex token
return res.json({
page: 1,
totalPages: 1,
totalResults: 0,
results: [],
page,
totalPages: Math.ceil(watchlist.size / itemsPerPage),
totalResults: watchlist.size,
results: watchlist.items.map((item) => ({
ratingKey: item.ratingKey,
title: item.title,
mediaType: item.type === 'show' ? 'tv' : 'movie',
tmdbId: item.tmdbId,
})),
});
}
const plexTV = new PlexTvAPI(activeUser?.plexToken);
const watchlist = await plexTV.getWatchlist({ offset });
return res.json({
page,
totalPages: Math.ceil(watchlist.size / itemsPerPage),
totalResults: watchlist.size,
results: watchlist.items.map((item) => ({
ratingKey: item.ratingKey,
title: item.title,
mediaType: item.type === 'show' ? 'tv' : 'movie',
tmdbId: item.tmdbId,
})),
});
});
);
export default discoverRoutes;

View File

@@ -7,6 +7,7 @@ import Media from '@server/entity/Media';
import { MediaRequest } from '@server/entity/MediaRequest';
import { User } from '@server/entity/User';
import { UserPushSubscription } from '@server/entity/UserPushSubscription';
import type { WatchlistResponse } from '@server/interfaces/api/discoverInterfaces';
import type {
QuotaResponse,
UserRequestsResponse,
@@ -606,4 +607,60 @@ router.get<{ id: string }, UserWatchDataResponse>(
}
);
router.get<{ id: string; page?: number }, WatchlistResponse>(
'/:id/watchlist',
async (req, res, next) => {
if (
Number(req.params.id) !== req.user?.id &&
!req.user?.hasPermission(
[Permission.MANAGE_REQUESTS, Permission.WATCHLIST_VIEW],
{
type: 'or',
}
)
) {
return next({
status: 403,
message:
"You do not have permission to view this user's Plex Watchlist.",
});
}
const itemsPerPage = 20;
const page = req.params.page ?? 1;
const offset = (page - 1) * itemsPerPage;
const user = await getRepository(User).findOneOrFail({
where: { id: Number(req.params.id) },
select: { id: true, plexToken: true },
});
if (!user?.plexToken) {
// We will just return an empty array if the user has no Plex token
return res.json({
page: 1,
totalPages: 1,
totalResults: 0,
results: [],
});
}
const plexTV = new PlexTvAPI(user.plexToken);
const watchlist = await plexTV.getWatchlist({ offset });
return res.json({
page,
totalPages: Math.ceil(watchlist.size / itemsPerPage),
totalResults: watchlist.size,
results: watchlist.items.map((item) => ({
ratingKey: item.ratingKey,
title: item.title,
mediaType: item.type === 'show' ? 'tv' : 'movie',
tmdbId: item.tmdbId,
})),
});
}
);
export default router;

View File

@@ -2,6 +2,7 @@ import { UserType } from '@server/constants/user';
import dataSource, { getRepository } from '@server/datasource';
import { User } from '@server/entity/User';
import { copyFileSync } from 'fs';
import gravatarUrl from 'gravatar-url';
import path from 'path';
const prepareDb = async () => {
@@ -27,9 +28,17 @@ const prepareDb = async () => {
const userRepository = getRepository(User);
const admin = await userRepository.findOne({
select: { id: true, plexId: true },
where: { id: 1 },
});
// Create the admin user
const user = new User();
user.plexId = 1;
const user =
(await userRepository.findOne({
where: { email: 'admin@seerr.dev' },
})) ?? new User();
user.plexId = admin?.plexId ?? 1;
user.plexToken = '1234';
user.plexUsername = 'admin';
user.username = 'admin';
@@ -37,12 +46,15 @@ const prepareDb = async () => {
user.userType = UserType.PLEX;
await user.setPassword('test1234');
user.permissions = 2;
user.avatar = 'https://plex.tv/assets/images/avatar/default.png';
user.avatar = gravatarUrl('admin@seerr.dev', { default: 'mm', size: 200 });
await userRepository.save(user);
// Create the other user
const otherUser = new User();
otherUser.plexId = 1;
const otherUser =
(await userRepository.findOne({
where: { email: 'friend@seerr.dev' },
})) ?? new User();
otherUser.plexId = admin?.plexId ?? 1;
otherUser.plexToken = '1234';
otherUser.plexUsername = 'friend';
otherUser.username = 'friend';
@@ -50,7 +62,10 @@ const prepareDb = async () => {
otherUser.userType = UserType.PLEX;
await otherUser.setPassword('test1234');
otherUser.permissions = 32;
otherUser.avatar = 'https://plex.tv/assets/images/avatar/default.png';
otherUser.avatar = gravatarUrl('friend@seerr.dev', {
default: 'mm',
size: 200,
});
await userRepository.save(otherUser);
};