Request Model (#79)

* feat(api): request model

Also adds request binding to search/discover results

* fix(api): rename Request to MediaRequest and update nextjs tsconfig

* refactor(api): move related request fetching code into MediaRequest entity
pull/71/head
sct 4 years ago committed by GitHub
parent 342d1a3c75
commit 1910876706
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,71 @@
import {
Entity,
PrimaryGeneratedColumn,
ManyToOne,
Column,
CreateDateColumn,
UpdateDateColumn,
getRepository,
In,
} from 'typeorm';
import { User } from './User';
export enum Status {
PENDING,
APPROVED,
DECLINED,
AVAILABLE,
}
@Entity()
export class MediaRequest {
public static async getRelatedRequests(
mediaIds: number | number[]
): Promise<MediaRequest[]> {
const requestRepository = getRepository(MediaRequest);
try {
let finalIds: number[];
if (!Array.isArray(mediaIds)) {
finalIds = [mediaIds];
} else {
finalIds = mediaIds;
}
const requests = await requestRepository.find({ mediaId: In(finalIds) });
return requests;
} catch (e) {
console.error(e.messaage);
return [];
}
}
@PrimaryGeneratedColumn()
public id: number;
@Column()
public mediaId: number;
@Column()
public mediaType: 'movie' | 'tv';
@Column({ type: 'integer' })
public status: Status;
@ManyToOne(() => User, (user) => user.requests, { eager: true })
public requestedBy: User;
@ManyToOne(() => User, { nullable: true })
public modifiedBy?: User;
@CreateDateColumn()
public createdAt: Date;
@UpdateDateColumn()
public updatedAt: Date;
constructor(init?: Partial<User>) {
Object.assign(this, init);
}
}

@ -4,8 +4,10 @@ import {
Column, Column,
CreateDateColumn, CreateDateColumn,
UpdateDateColumn, UpdateDateColumn,
OneToMany,
} from 'typeorm'; } from 'typeorm';
import { Permission, hasPermission } from '../lib/permissions'; import { Permission, hasPermission } from '../lib/permissions';
import { MediaRequest } from './MediaRequest';
@Entity() @Entity()
export class User { export class User {
@ -21,7 +23,13 @@ export class User {
@Column({ unique: true }) @Column({ unique: true })
public email: string; public email: string;
@Column({ nullable: true }) @Column()
public username: string;
@Column({ select: false })
public plexId: number;
@Column({ nullable: true, select: false })
public plexToken?: string; public plexToken?: string;
@Column({ type: 'integer', default: 0 }) @Column({ type: 'integer', default: 0 })
@ -30,6 +38,9 @@ export class User {
@Column() @Column()
public avatar: string; public avatar: string;
@OneToMany(() => MediaRequest, (request) => request.requestedBy)
public requests: MediaRequest;
@CreateDateColumn() @CreateDateColumn()
public createdAt: Date; public createdAt: Date;

@ -3,6 +3,7 @@ import type {
TmdbPersonResult, TmdbPersonResult,
TmdbTvResult, TmdbTvResult,
} from '../api/themoviedb'; } from '../api/themoviedb';
import { MediaRequest } from '../entity/MediaRequest';
export type MediaType = 'tv' | 'movie' | 'person'; export type MediaType = 'tv' | 'movie' | 'person';
@ -17,6 +18,7 @@ interface SearchResult {
genreIds: number[]; genreIds: number[];
overview: string; overview: string;
originalLanguage: string; originalLanguage: string;
request?: MediaRequest;
} }
export interface MovieResult extends SearchResult { export interface MovieResult extends SearchResult {
@ -26,6 +28,7 @@ export interface MovieResult extends SearchResult {
releaseDate: string; releaseDate: string;
adult: boolean; adult: boolean;
video: boolean; video: boolean;
request?: MediaRequest;
} }
export interface TvResult extends SearchResult { export interface TvResult extends SearchResult {
@ -48,7 +51,10 @@ export interface PersonResult {
export type Results = MovieResult | TvResult | PersonResult; export type Results = MovieResult | TvResult | PersonResult;
export const mapMovieResult = (movieResult: TmdbMovieResult): MovieResult => ({ export const mapMovieResult = (
movieResult: TmdbMovieResult,
request?: MediaRequest
): MovieResult => ({
id: movieResult.id, id: movieResult.id,
mediaType: 'movie', mediaType: 'movie',
adult: movieResult.adult, adult: movieResult.adult,
@ -64,9 +70,13 @@ export const mapMovieResult = (movieResult: TmdbMovieResult): MovieResult => ({
voteCount: movieResult.vote_count, voteCount: movieResult.vote_count,
backdropPath: movieResult.backdrop_path, backdropPath: movieResult.backdrop_path,
posterPath: movieResult.poster_path, posterPath: movieResult.poster_path,
request,
}); });
export const mapTvResult = (tvResult: TmdbTvResult): TvResult => ({ export const mapTvResult = (
tvResult: TmdbTvResult,
request?: MediaRequest
): TvResult => ({
id: tvResult.id, id: tvResult.id,
firstAirDate: tvResult.first_air_Date, firstAirDate: tvResult.first_air_Date,
genreIds: tvResult.genre_ids, genreIds: tvResult.genre_ids,
@ -81,6 +91,7 @@ export const mapTvResult = (tvResult: TmdbTvResult): TvResult => ({
voteCount: tvResult.vote_count, voteCount: tvResult.vote_count,
backdropPath: tvResult.backdrop_path, backdropPath: tvResult.backdrop_path,
posterPath: tvResult.poster_path, posterPath: tvResult.poster_path,
request,
}); });
export const mapPersonResult = ( export const mapPersonResult = (
@ -102,14 +113,21 @@ export const mapPersonResult = (
}); });
export const mapSearchResults = ( export const mapSearchResults = (
results: (TmdbMovieResult | TmdbTvResult | TmdbPersonResult)[] results: (TmdbMovieResult | TmdbTvResult | TmdbPersonResult)[],
requests?: MediaRequest[]
): Results[] => ): Results[] =>
results.map((result) => { results.map((result) => {
switch (result.media_type) { switch (result.media_type) {
case 'movie': case 'movie':
return mapMovieResult(result); return mapMovieResult(
result,
requests?.find((req) => req.mediaId === result.id)
);
case 'tv': case 'tv':
return mapTvResult(result); return mapTvResult(
result,
requests?.find((req) => req.mediaId === result.id)
);
default: default:
return mapPersonResult(result); return mapPersonResult(result);
} }

@ -260,6 +260,8 @@ components:
video: video:
type: boolean type: boolean
example: false example: false
request:
$ref: '#/components/schemas/MediaRequest'
TvResult: TvResult:
type: object type: object
properties: properties:
@ -301,6 +303,8 @@ components:
type: string type: string
firstAirDate: firstAirDate:
type: string type: string
request:
$ref: '#/components/schemas/MediaRequest'
PersonResult: PersonResult:
type: object type: object
properties: properties:
@ -321,6 +325,44 @@ components:
oneOf: oneOf:
- $ref: '#/components/schemas/MovieResult' - $ref: '#/components/schemas/MovieResult'
- $ref: '#/components/schemas/TvResult' - $ref: '#/components/schemas/TvResult'
MediaRequest:
type: object
properties:
id:
type: number
example: 123
readOnly: true
mediaId:
type: number
example: 123
description: TMDB Movie ID
mediaType:
type: string
enum: [movie, tv]
status:
type: number
example: 0
description: Status of the request. 0 = PENDING APPROVAL, 1 = APPROVED, 2 = DECLINED, 3 = AVAILABLE
readOnly: true
createdAt:
type: string
example: '2020-09-12T10:00:27.000Z'
readOnly: true
updatedAt:
type: string
example: '2020-09-12T10:00:27.000Z'
readOnly: true
requestedBy:
$ref: '#/components/schemas/User'
readOnly: true
modifiedBy:
$ref: '#/components/schemas/User'
readOnly: true
required:
- id
- mediaId
- mediaType
- status
securitySchemes: securitySchemes:
cookieAuth: cookieAuth:

@ -37,7 +37,7 @@ authRoutes.post('/login', async (req, res) => {
// Next let's see if the user already exists // Next let's see if the user already exists
let user = await userRepository.findOne({ let user = await userRepository.findOne({
where: { email: account.email }, where: { plexId: account.id },
}); });
if (user) { if (user) {
@ -49,6 +49,8 @@ authRoutes.post('/login', async (req, res) => {
// Update the users avatar with their plex thumbnail (incase it changed) // Update the users avatar with their plex thumbnail (incase it changed)
user.avatar = account.thumb; user.avatar = account.thumb;
user.email = account.email;
user.username = account.username;
} else { } else {
// Here we check if it's the first user. If it is, we create the user with no check // Here we check if it's the first user. If it is, we create the user with no check
// and give them admin permissions // and give them admin permissions
@ -57,6 +59,8 @@ authRoutes.post('/login', async (req, res) => {
if (totalUsers === 0) { if (totalUsers === 0) {
user = new User({ user = new User({
email: account.email, email: account.email,
username: account.username,
plexId: account.id,
plexToken: account.authToken, plexToken: account.authToken,
permissions: Permission.ADMIN, permissions: Permission.ADMIN,
avatar: account.thumb, avatar: account.thumb,

@ -1,6 +1,7 @@
import { Router } from 'express'; import { Router } from 'express';
import TheMovieDb from '../api/themoviedb'; import TheMovieDb from '../api/themoviedb';
import { mapMovieResult, mapTvResult } from '../models/Search'; import { mapMovieResult, mapTvResult } from '../models/Search';
import { MediaRequest } from '../entity/MediaRequest';
const discoverRoutes = Router(); const discoverRoutes = Router();
@ -9,11 +10,20 @@ discoverRoutes.get('/movies', async (req, res) => {
const data = await tmdb.getDiscoverMovies({ page: Number(req.query.page) }); const data = await tmdb.getDiscoverMovies({ page: Number(req.query.page) });
const requests = await MediaRequest.getRelatedRequests(
data.results.map((result) => result.id)
);
return res.status(200).json({ return res.status(200).json({
page: data.page, page: data.page,
totalPages: data.total_pages, totalPages: data.total_pages,
totalResults: data.total_results, totalResults: data.total_results,
results: data.results.map(mapMovieResult), results: data.results.map((result) =>
mapMovieResult(
result,
requests.find((req) => req.mediaId === result.id)
)
),
}); });
}); });
@ -22,11 +32,20 @@ discoverRoutes.get('/tv', async (req, res) => {
const data = await tmdb.getDiscoverTv({ page: Number(req.query.page) }); const data = await tmdb.getDiscoverTv({ page: Number(req.query.page) });
const requests = await MediaRequest.getRelatedRequests(
data.results.map((result) => result.id)
);
return res.status(200).json({ return res.status(200).json({
page: data.page, page: data.page,
totalPages: data.total_pages, totalPages: data.total_pages,
totalResults: data.total_results, totalResults: data.total_results,
results: data.results.map(mapTvResult), results: data.results.map((result) =>
mapTvResult(
result,
requests.find((req) => req.mediaId === result.id)
)
),
}); });
}); });

@ -1,6 +1,7 @@
import { Router } from 'express'; import { Router } from 'express';
import TheMovieDb from '../api/themoviedb'; import TheMovieDb from '../api/themoviedb';
import { mapSearchResults } from '../models/Search'; import { mapSearchResults } from '../models/Search';
import { MediaRequest } from '../entity/MediaRequest';
const searchRoutes = Router(); const searchRoutes = Router();
@ -12,11 +13,15 @@ searchRoutes.get('/', async (req, res) => {
page: Number(req.query.page), page: Number(req.query.page),
}); });
const requests = await MediaRequest.getRelatedRequests(
results.results.map((result) => result.id)
);
return res.status(200).json({ return res.status(200).json({
page: results.page, page: results.page,
totalPages: results.total_pages, totalPages: results.total_pages,
totalResults: results.total_results, totalResults: results.total_results,
results: mapSearchResults(results.results), results: mapSearchResults(results.results, requests),
}); });
}); });

@ -37,10 +37,6 @@ const Discover: React.FC = () => {
return <div>{error}</div>; return <div>{error}</div>;
} }
if (!data && !error) {
return <div>loading!</div>;
}
const titles = data?.reduce( const titles = data?.reduce(
(a, v) => [...a, ...v.results], (a, v) => [...a, ...v.results],
[] as MovieResult[] [] as MovieResult[]

@ -12,7 +12,10 @@
"moduleResolution": "node", "moduleResolution": "node",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"jsx": "preserve" "jsx": "preserve",
"strictPropertyInitialization": false,
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}, },
"include": ["next-env.d.ts", "src/**/*.ts", "src/**/*.tsx"], "include": ["next-env.d.ts", "src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules"] "exclude": ["node_modules"]

Loading…
Cancel
Save