From 0601b446873e2eaf042044dd6a995b713586b0cc Mon Sep 17 00:00:00 2001 From: sct Date: Tue, 24 Nov 2020 02:19:04 +0000 Subject: [PATCH] feat: throw 404 when movie/tv show doesnt exist also adds site webmanifest for mobile icons and title changes for tv/movie pages --- overseerr-api.yml | 15 +++++-- public/site.webmanifest | 1 + server/entity/User.ts | 4 ++ server/interfaces/api/common.ts | 10 +++++ server/interfaces/api/mediaInterfaces.ts | 9 +--- server/interfaces/api/requestInterfaces.ts | 6 +++ server/routes/movie.ts | 50 ++++++++++++---------- server/routes/request.ts | 26 ++++++++--- server/routes/tv.ts | 19 ++++---- server/routes/user.ts | 2 +- src/components/Discover/index.tsx | 11 ++--- src/components/MovieDetails/index.tsx | 4 ++ src/components/TvDetails/index.tsx | 4 ++ src/components/UserList/index.tsx | 2 +- src/pages/_document.tsx | 18 ++++++++ 15 files changed, 127 insertions(+), 54 deletions(-) create mode 100644 public/site.webmanifest create mode 100644 server/interfaces/api/common.ts create mode 100644 server/interfaces/api/requestInterfaces.ts diff --git a/overseerr-api.yml b/overseerr-api.yml index 3c0ff7756..2a5d8deb6 100644 --- a/overseerr-api.yml +++ b/overseerr-api.yml @@ -33,6 +33,10 @@ components: type: string example: '2020-09-02T05:02:23.000Z' readOnly: true + requestCount: + type: number + example: 5 + readOnly: true requests: type: array readOnly: true @@ -1656,9 +1660,14 @@ paths: content: application/json: schema: - type: array - items: - $ref: '#/components/schemas/MediaRequest' + type: object + properties: + pageInfo: + $ref: '#/components/schemas/PageInfo' + results: + type: array + items: + $ref: '#/components/schemas/MediaRequest' post: summary: Create a new request description: | diff --git a/public/site.webmanifest b/public/site.webmanifest new file mode 100644 index 000000000..45dc8a206 --- /dev/null +++ b/public/site.webmanifest @@ -0,0 +1 @@ +{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} \ No newline at end of file diff --git a/server/entity/User.ts b/server/entity/User.ts index b04ec3afe..35ad36e67 100644 --- a/server/entity/User.ts +++ b/server/entity/User.ts @@ -5,6 +5,7 @@ import { CreateDateColumn, UpdateDateColumn, OneToMany, + RelationCount, } from 'typeorm'; import { Permission, hasPermission } from '../lib/permissions'; import { MediaRequest } from './MediaRequest'; @@ -38,6 +39,9 @@ export class User { @Column() public avatar: string; + @RelationCount((user: User) => user.requests) + public requestCount: number; + @OneToMany(() => MediaRequest, (request) => request.requestedBy) public requests: MediaRequest[]; diff --git a/server/interfaces/api/common.ts b/server/interfaces/api/common.ts new file mode 100644 index 000000000..9ae25939b --- /dev/null +++ b/server/interfaces/api/common.ts @@ -0,0 +1,10 @@ +interface PageInfo { + pages: number; + page: number; + results: number; + pageSize: number; +} + +export interface PaginatedResponse { + pageInfo: PageInfo; +} diff --git a/server/interfaces/api/mediaInterfaces.ts b/server/interfaces/api/mediaInterfaces.ts index 9a441409e..e530d2d2c 100644 --- a/server/interfaces/api/mediaInterfaces.ts +++ b/server/interfaces/api/mediaInterfaces.ts @@ -1,11 +1,6 @@ import type Media from '../../entity/Media'; +import { PaginatedResponse } from './common'; -export interface MediaResultsResponse { - pageInfo: { - pages: number; - page: number; - results: number; - pageSize: number; - }; +export interface MediaResultsResponse extends PaginatedResponse { results: Media[]; } diff --git a/server/interfaces/api/requestInterfaces.ts b/server/interfaces/api/requestInterfaces.ts new file mode 100644 index 000000000..ca39515bd --- /dev/null +++ b/server/interfaces/api/requestInterfaces.ts @@ -0,0 +1,6 @@ +import type { PaginatedResponse } from './common'; +import type { MediaRequest } from '../../entity/MediaRequest'; + +export interface RequestResultsResponse extends PaginatedResponse { + results: MediaRequest[]; +} diff --git a/server/routes/movie.ts b/server/routes/movie.ts index 7e6f6fb79..eefdc8dbb 100644 --- a/server/routes/movie.ts +++ b/server/routes/movie.ts @@ -8,17 +8,21 @@ import RottenTomatoes from '../api/rottentomatoes'; const movieRoutes = Router(); -movieRoutes.get('/:id', async (req, res) => { +movieRoutes.get('/:id', async (req, res, next) => { const tmdb = new TheMovieDb(); - const movie = await tmdb.getMovie({ - movieId: Number(req.params.id), - language: req.query.language as string, - }); + try { + const movie = await tmdb.getMovie({ + movieId: Number(req.params.id), + language: req.query.language as string, + }); - const media = await Media.getMedia(movie.id); + const media = await Media.getMedia(movie.id); - return res.status(200).json(mapMovieDetails(movie, media)); + return res.status(200).json(mapMovieDetails(movie, media)); + } catch (e) { + return next({ status: 404, message: 'Movie does not exist' }); + } }); movieRoutes.get('/:id/recommendations', async (req, res) => { @@ -74,27 +78,27 @@ movieRoutes.get('/:id/similar', async (req, res) => { }); movieRoutes.get('/:id/ratings', async (req, res, next) => { - const tmdb = new TheMovieDb(); - const rtapi = new RottenTomatoes(); + try { + const tmdb = new TheMovieDb(); + const rtapi = new RottenTomatoes(); - const movie = await tmdb.getMovie({ - movieId: Number(req.params.id), - }); + const movie = await tmdb.getMovie({ + movieId: Number(req.params.id), + }); - if (!movie) { - return next({ status: 404, message: 'Movie does not exist' }); - } + const rtratings = await rtapi.getMovieRatings( + movie.title, + Number(movie.release_date.slice(0, 4)) + ); - const rtratings = await rtapi.getMovieRatings( - movie.title, - Number(movie.release_date.slice(0, 4)) - ); + if (!rtratings) { + return next({ status: 404, message: 'Unable to retrieve ratings' }); + } - if (!rtratings) { - return next({ status: 404, message: 'Unable to retrieve ratings' }); + return res.status(200).json(rtratings); + } catch (e) { + return next({ status: 404, message: 'Movie does not exist' }); } - - return res.status(200).json(rtratings); }); export default movieRoutes; diff --git a/server/routes/request.ts b/server/routes/request.ts index f7bf32d61..35b84c88b 100644 --- a/server/routes/request.ts +++ b/server/routes/request.ts @@ -8,12 +8,16 @@ 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 = Number(req.query.take) ?? 20; + const skip = Number(req.query.skip) ?? 0; + let statusFilter: | MediaRequestStatus | FindOperator @@ -51,23 +55,33 @@ requestRoutes.get('/', async (req, res, next) => { break; } - const requests = req.user?.hasPermission(Permission.MANAGE_REQUESTS) - ? await requestRepository.find({ + 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: Number(req.query.skip) ?? 0, + skip, }) - : await requestRepository.find({ + : await requestRepository.findAndCount({ where: { requestedBy: { id: req.user?.id }, status: statusFilter }, relations: ['media', 'modifiedBy'], order: sortFilter, take: Number(req.query.limit) ?? 20, - skip: Number(req.query.skip) ?? 0, + skip, }); - return res.status(200).json(requests); + 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 }); } diff --git a/server/routes/tv.ts b/server/routes/tv.ts index 88acea0bf..d8988700d 100644 --- a/server/routes/tv.ts +++ b/server/routes/tv.ts @@ -8,17 +8,20 @@ import RottenTomatoes from '../api/rottentomatoes'; const tvRoutes = Router(); -tvRoutes.get('/:id', async (req, res) => { +tvRoutes.get('/:id', async (req, res, next) => { const tmdb = new TheMovieDb(); + try { + const tv = await tmdb.getTvShow({ + tvId: Number(req.params.id), + language: req.query.language as string, + }); - const tv = await tmdb.getTvShow({ - tvId: Number(req.params.id), - language: req.query.language as string, - }); + const media = await Media.getMedia(tv.id); - const media = await Media.getMedia(tv.id); - - return res.status(200).json(mapTvDetails(tv, media)); + return res.status(200).json(mapTvDetails(tv, media)); + } catch (e) { + return next({ status: 404, message: 'TV Show does not exist' }); + } }); tvRoutes.get('/:id/season/:seasonNumber', async (req, res) => { diff --git a/server/routes/user.ts b/server/routes/user.ts index 86e5920ba..ef8990cb3 100644 --- a/server/routes/user.ts +++ b/server/routes/user.ts @@ -8,7 +8,7 @@ const router = Router(); router.get('/', async (req, res) => { const userRepository = getRepository(User); - const users = await userRepository.find({ relations: ['requests'] }); + const users = await userRepository.find(); return res.status(200).json(User.filterMany(users)); }); diff --git a/src/components/Discover/index.tsx b/src/components/Discover/index.tsx index 15ea54c81..e80a997eb 100644 --- a/src/components/Discover/index.tsx +++ b/src/components/Discover/index.tsx @@ -15,6 +15,7 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { LanguageContext } from '../../context/LanguageContext'; import type Media from '../../../server/entity/Media'; import type { MediaResultsResponse } from '../../../server/interfaces/api/mediaInterfaces'; +import type { RequestResultsResponse } from '../../../server/interfaces/api/requestInterfaces'; import RequestCard from '../RequestCard'; const messages = defineMessages({ @@ -70,9 +71,9 @@ const Discover: React.FC = () => { '/api/v1/media?filter=available&take=20&sort=modified' ); - const { data: requests, error: requestError } = useSWR( - '/api/v1/request?filter=unavailable&take=20&sort=modified&skip=0' - ); + const { data: requests, error: requestError } = useSWR< + RequestResultsResponse + >('/api/v1/request?filter=unavailable&take=20&sort=modified&skip=0'); return ( <> @@ -125,8 +126,8 @@ const Discover: React.FC = () => { ( + isEmpty={!!requests && !requestError && requests.results.length === 0} + items={(requests?.results ?? []).map((request) => ( = ({ movie }) => { backgroundImage: `linear-gradient(180deg, rgba(17, 24, 39, 0.47) 0%, rgba(17, 24, 39, 1) 100%), url(//image.tmdb.org/t/p/w1920_and_h800_multi_faces/${data.backdropPath})`, }} > + + {data.title} - Overseerr + = ({ tv }) => { backgroundImage: `linear-gradient(180deg, rgba(17, 24, 39, 0.47) 0%, rgba(17, 24, 39, 1) 100%), url(//image.tmdb.org/t/p/w1920_and_h800_multi_faces/${data.backdropPath})`, }} > + + {data.name} - Overseerr + {
- {user.requests.length} + {user.requestCount}
diff --git a/src/pages/_document.tsx b/src/pages/_document.tsx index feb9c040e..f11144c99 100644 --- a/src/pages/_document.tsx +++ b/src/pages/_document.tsx @@ -22,6 +22,24 @@ class MyDocument extends Document { + + + +