feat: throw 404 when movie/tv show doesnt exist

also adds site webmanifest for mobile icons and title changes for tv/movie pages
This commit is contained in:
sct
2020-11-24 02:19:04 +00:00
parent 886389a361
commit 0601b44687
15 changed files with 128 additions and 55 deletions

View File

@@ -33,6 +33,10 @@ components:
type: string type: string
example: '2020-09-02T05:02:23.000Z' example: '2020-09-02T05:02:23.000Z'
readOnly: true readOnly: true
requestCount:
type: number
example: 5
readOnly: true
requests: requests:
type: array type: array
readOnly: true readOnly: true
@@ -1656,9 +1660,14 @@ paths:
content: content:
application/json: application/json:
schema: schema:
type: array type: object
items: properties:
$ref: '#/components/schemas/MediaRequest' pageInfo:
$ref: '#/components/schemas/PageInfo'
results:
type: array
items:
$ref: '#/components/schemas/MediaRequest'
post: post:
summary: Create a new request summary: Create a new request
description: | description: |

1
public/site.webmanifest Normal file
View File

@@ -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"}

View File

@@ -5,6 +5,7 @@ import {
CreateDateColumn, CreateDateColumn,
UpdateDateColumn, UpdateDateColumn,
OneToMany, OneToMany,
RelationCount,
} from 'typeorm'; } from 'typeorm';
import { Permission, hasPermission } from '../lib/permissions'; import { Permission, hasPermission } from '../lib/permissions';
import { MediaRequest } from './MediaRequest'; import { MediaRequest } from './MediaRequest';
@@ -38,6 +39,9 @@ export class User {
@Column() @Column()
public avatar: string; public avatar: string;
@RelationCount((user: User) => user.requests)
public requestCount: number;
@OneToMany(() => MediaRequest, (request) => request.requestedBy) @OneToMany(() => MediaRequest, (request) => request.requestedBy)
public requests: MediaRequest[]; public requests: MediaRequest[];

View File

@@ -0,0 +1,10 @@
interface PageInfo {
pages: number;
page: number;
results: number;
pageSize: number;
}
export interface PaginatedResponse {
pageInfo: PageInfo;
}

View File

@@ -1,11 +1,6 @@
import type Media from '../../entity/Media'; import type Media from '../../entity/Media';
import { PaginatedResponse } from './common';
export interface MediaResultsResponse { export interface MediaResultsResponse extends PaginatedResponse {
pageInfo: {
pages: number;
page: number;
results: number;
pageSize: number;
};
results: Media[]; results: Media[];
} }

View File

@@ -0,0 +1,6 @@
import type { PaginatedResponse } from './common';
import type { MediaRequest } from '../../entity/MediaRequest';
export interface RequestResultsResponse extends PaginatedResponse {
results: MediaRequest[];
}

View File

@@ -8,17 +8,21 @@ import RottenTomatoes from '../api/rottentomatoes';
const movieRoutes = Router(); const movieRoutes = Router();
movieRoutes.get('/:id', async (req, res) => { movieRoutes.get('/:id', async (req, res, next) => {
const tmdb = new TheMovieDb(); const tmdb = new TheMovieDb();
const movie = await tmdb.getMovie({ try {
movieId: Number(req.params.id), const movie = await tmdb.getMovie({
language: req.query.language as string, 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) => { 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) => { movieRoutes.get('/:id/ratings', async (req, res, next) => {
const tmdb = new TheMovieDb(); try {
const rtapi = new RottenTomatoes(); const tmdb = new TheMovieDb();
const rtapi = new RottenTomatoes();
const movie = await tmdb.getMovie({ const movie = await tmdb.getMovie({
movieId: Number(req.params.id), movieId: Number(req.params.id),
}); });
if (!movie) { 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' });
}
return res.status(200).json(rtratings);
} catch (e) {
return next({ status: 404, message: 'Movie does not exist' }); return next({ status: 404, message: 'Movie does not exist' });
} }
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' });
}
return res.status(200).json(rtratings);
}); });
export default movieRoutes; export default movieRoutes;

View File

@@ -8,12 +8,16 @@ import Media from '../entity/Media';
import { MediaStatus, MediaRequestStatus, MediaType } from '../constants/media'; import { MediaStatus, MediaRequestStatus, MediaType } from '../constants/media';
import SeasonRequest from '../entity/SeasonRequest'; import SeasonRequest from '../entity/SeasonRequest';
import logger from '../logger'; import logger from '../logger';
import { RequestResultsResponse } from '../interfaces/api/requestInterfaces';
const requestRoutes = Router(); const requestRoutes = Router();
requestRoutes.get('/', async (req, res, next) => { requestRoutes.get('/', async (req, res, next) => {
const requestRepository = getRepository(MediaRequest); const requestRepository = getRepository(MediaRequest);
try { try {
const pageSize = Number(req.query.take) ?? 20;
const skip = Number(req.query.skip) ?? 0;
let statusFilter: let statusFilter:
| MediaRequestStatus | MediaRequestStatus
| FindOperator<MediaRequestStatus> | FindOperator<MediaRequestStatus>
@@ -51,23 +55,33 @@ requestRoutes.get('/', async (req, res, next) => {
break; break;
} }
const requests = req.user?.hasPermission(Permission.MANAGE_REQUESTS) const [requests, requestCount] = req.user?.hasPermission(
? await requestRepository.find({ Permission.MANAGE_REQUESTS
)
? await requestRepository.findAndCount({
order: sortFilter, order: sortFilter,
relations: ['media', 'modifiedBy'], relations: ['media', 'modifiedBy'],
where: { status: statusFilter }, where: { status: statusFilter },
take: Number(req.query.take) ?? 20, 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 }, where: { requestedBy: { id: req.user?.id }, status: statusFilter },
relations: ['media', 'modifiedBy'], relations: ['media', 'modifiedBy'],
order: sortFilter, order: sortFilter,
take: Number(req.query.limit) ?? 20, 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) { } catch (e) {
next({ status: 500, message: e.message }); next({ status: 500, message: e.message });
} }

View File

@@ -8,17 +8,20 @@ import RottenTomatoes from '../api/rottentomatoes';
const tvRoutes = Router(); const tvRoutes = Router();
tvRoutes.get('/:id', async (req, res) => { tvRoutes.get('/:id', async (req, res, next) => {
const tmdb = new TheMovieDb(); 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({ const media = await Media.getMedia(tv.id);
tvId: Number(req.params.id),
language: req.query.language as string,
});
const media = await Media.getMedia(tv.id); return res.status(200).json(mapTvDetails(tv, media));
} catch (e) {
return res.status(200).json(mapTvDetails(tv, media)); return next({ status: 404, message: 'TV Show does not exist' });
}
}); });
tvRoutes.get('/:id/season/:seasonNumber', async (req, res) => { tvRoutes.get('/:id/season/:seasonNumber', async (req, res) => {

View File

@@ -8,7 +8,7 @@ const router = Router();
router.get('/', async (req, res) => { router.get('/', async (req, res) => {
const userRepository = getRepository(User); const userRepository = getRepository(User);
const users = await userRepository.find({ relations: ['requests'] }); const users = await userRepository.find();
return res.status(200).json(User.filterMany(users)); return res.status(200).json(User.filterMany(users));
}); });

View File

@@ -15,6 +15,7 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { LanguageContext } from '../../context/LanguageContext'; import { LanguageContext } from '../../context/LanguageContext';
import type Media from '../../../server/entity/Media'; import type Media from '../../../server/entity/Media';
import type { MediaResultsResponse } from '../../../server/interfaces/api/mediaInterfaces'; import type { MediaResultsResponse } from '../../../server/interfaces/api/mediaInterfaces';
import type { RequestResultsResponse } from '../../../server/interfaces/api/requestInterfaces';
import RequestCard from '../RequestCard'; import RequestCard from '../RequestCard';
const messages = defineMessages({ const messages = defineMessages({
@@ -70,9 +71,9 @@ const Discover: React.FC = () => {
'/api/v1/media?filter=available&take=20&sort=modified' '/api/v1/media?filter=available&take=20&sort=modified'
); );
const { data: requests, error: requestError } = useSWR<MediaRequest[]>( const { data: requests, error: requestError } = useSWR<
'/api/v1/request?filter=unavailable&take=20&sort=modified&skip=0' RequestResultsResponse
); >('/api/v1/request?filter=unavailable&take=20&sort=modified&skip=0');
return ( return (
<> <>
@@ -125,8 +126,8 @@ const Discover: React.FC = () => {
<Slider <Slider
sliderKey="requests" sliderKey="requests"
isLoading={!requests && !requestError} isLoading={!requests && !requestError}
isEmpty={!!requests && !requestError && requests.length === 0} isEmpty={!!requests && !requestError && requests.results.length === 0}
items={requests?.map((request) => ( items={(requests?.results ?? []).map((request) => (
<RequestCard <RequestCard
key={`request-slider-item-${request.id}`} key={`request-slider-item-${request.id}`}
request={request} request={request}

View File

@@ -35,6 +35,7 @@ import RTAudFresh from '../../assets/rt_aud_fresh.svg';
import RTAudRotten from '../../assets/rt_aud_rotten.svg'; import RTAudRotten from '../../assets/rt_aud_rotten.svg';
import type { RTRating } from '../../../server/api/rottentomatoes'; import type { RTRating } from '../../../server/api/rottentomatoes';
import Error from '../../pages/_error'; import Error from '../../pages/_error';
import Head from 'next/head';
const messages = defineMessages({ const messages = defineMessages({
releasedate: 'Release Date', releasedate: 'Release Date',
@@ -128,6 +129,9 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ 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})`, 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})`,
}} }}
> >
<Head>
<title>{data.title} - Overseerr</title>
</Head>
<RequestModal <RequestModal
tmdbId={data.id} tmdbId={data.id}
show={showRequestModal} show={showRequestModal}

View File

@@ -26,6 +26,7 @@ import RTRotten from '../../assets/rt_rotten.svg';
import RTAudFresh from '../../assets/rt_aud_fresh.svg'; import RTAudFresh from '../../assets/rt_aud_fresh.svg';
import RTAudRotten from '../../assets/rt_aud_rotten.svg'; import RTAudRotten from '../../assets/rt_aud_rotten.svg';
import type { RTRating } from '../../../server/api/rottentomatoes'; import type { RTRating } from '../../../server/api/rottentomatoes';
import Head from 'next/head';
const messages = defineMessages({ const messages = defineMessages({
userrating: 'User Rating', userrating: 'User Rating',
@@ -131,6 +132,9 @@ const TvDetails: React.FC<TvDetailsProps> = ({ 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})`, 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})`,
}} }}
> >
<Head>
<title>{data.name} - Overseerr</title>
</Head>
<RequestModal <RequestModal
tmdbId={data.id} tmdbId={data.id}
show={showRequestModal} show={showRequestModal}

View File

@@ -73,7 +73,7 @@ const UserList: React.FC = () => {
</td> </td>
<td className="px-6 py-4 whitespace-nowrap"> <td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm leading-5 text-white"> <div className="text-sm leading-5 text-white">
{user.requests.length} {user.requestCount}
</div> </div>
</td> </td>
<td className="px-6 py-4 whitespace-nowrap"> <td className="px-6 py-4 whitespace-nowrap">

View File

@@ -22,6 +22,24 @@ class MyDocument extends Document {
<Html> <Html>
<Head> <Head>
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" /> <link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
<link
rel="apple-touch-icon"
sizes="180x180"
href="/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/favicon-16x16.png"
/>
<link rel="manifest" href="/site.webmanifest"></link>
</Head> </Head>
<body> <body>
<Main /> <Main />