mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00
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
This commit is contained in:
71
server/entity/MediaRequest.ts
Normal file
71
server/entity/MediaRequest.ts
Normal file
@@ -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"]
|
||||||
|
Reference in New Issue
Block a user