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,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
OneToMany,
|
||||
} from 'typeorm';
|
||||
import { Permission, hasPermission } from '../lib/permissions';
|
||||
import { MediaRequest } from './MediaRequest';
|
||||
|
||||
@Entity()
|
||||
export class User {
|
||||
@@ -21,7 +23,13 @@ export class User {
|
||||
@Column({ unique: true })
|
||||
public email: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@Column()
|
||||
public username: string;
|
||||
|
||||
@Column({ select: false })
|
||||
public plexId: number;
|
||||
|
||||
@Column({ nullable: true, select: false })
|
||||
public plexToken?: string;
|
||||
|
||||
@Column({ type: 'integer', default: 0 })
|
||||
@@ -30,6 +38,9 @@ export class User {
|
||||
@Column()
|
||||
public avatar: string;
|
||||
|
||||
@OneToMany(() => MediaRequest, (request) => request.requestedBy)
|
||||
public requests: MediaRequest;
|
||||
|
||||
@CreateDateColumn()
|
||||
public createdAt: Date;
|
||||
|
||||
|
@@ -3,6 +3,7 @@ import type {
|
||||
TmdbPersonResult,
|
||||
TmdbTvResult,
|
||||
} from '../api/themoviedb';
|
||||
import { MediaRequest } from '../entity/MediaRequest';
|
||||
|
||||
export type MediaType = 'tv' | 'movie' | 'person';
|
||||
|
||||
@@ -17,6 +18,7 @@ interface SearchResult {
|
||||
genreIds: number[];
|
||||
overview: string;
|
||||
originalLanguage: string;
|
||||
request?: MediaRequest;
|
||||
}
|
||||
|
||||
export interface MovieResult extends SearchResult {
|
||||
@@ -26,6 +28,7 @@ export interface MovieResult extends SearchResult {
|
||||
releaseDate: string;
|
||||
adult: boolean;
|
||||
video: boolean;
|
||||
request?: MediaRequest;
|
||||
}
|
||||
|
||||
export interface TvResult extends SearchResult {
|
||||
@@ -48,7 +51,10 @@ export interface PersonResult {
|
||||
|
||||
export type Results = MovieResult | TvResult | PersonResult;
|
||||
|
||||
export const mapMovieResult = (movieResult: TmdbMovieResult): MovieResult => ({
|
||||
export const mapMovieResult = (
|
||||
movieResult: TmdbMovieResult,
|
||||
request?: MediaRequest
|
||||
): MovieResult => ({
|
||||
id: movieResult.id,
|
||||
mediaType: 'movie',
|
||||
adult: movieResult.adult,
|
||||
@@ -64,9 +70,13 @@ export const mapMovieResult = (movieResult: TmdbMovieResult): MovieResult => ({
|
||||
voteCount: movieResult.vote_count,
|
||||
backdropPath: movieResult.backdrop_path,
|
||||
posterPath: movieResult.poster_path,
|
||||
request,
|
||||
});
|
||||
|
||||
export const mapTvResult = (tvResult: TmdbTvResult): TvResult => ({
|
||||
export const mapTvResult = (
|
||||
tvResult: TmdbTvResult,
|
||||
request?: MediaRequest
|
||||
): TvResult => ({
|
||||
id: tvResult.id,
|
||||
firstAirDate: tvResult.first_air_Date,
|
||||
genreIds: tvResult.genre_ids,
|
||||
@@ -81,6 +91,7 @@ export const mapTvResult = (tvResult: TmdbTvResult): TvResult => ({
|
||||
voteCount: tvResult.vote_count,
|
||||
backdropPath: tvResult.backdrop_path,
|
||||
posterPath: tvResult.poster_path,
|
||||
request,
|
||||
});
|
||||
|
||||
export const mapPersonResult = (
|
||||
@@ -102,14 +113,21 @@ export const mapPersonResult = (
|
||||
});
|
||||
|
||||
export const mapSearchResults = (
|
||||
results: (TmdbMovieResult | TmdbTvResult | TmdbPersonResult)[]
|
||||
results: (TmdbMovieResult | TmdbTvResult | TmdbPersonResult)[],
|
||||
requests?: MediaRequest[]
|
||||
): Results[] =>
|
||||
results.map((result) => {
|
||||
switch (result.media_type) {
|
||||
case 'movie':
|
||||
return mapMovieResult(result);
|
||||
return mapMovieResult(
|
||||
result,
|
||||
requests?.find((req) => req.mediaId === result.id)
|
||||
);
|
||||
case 'tv':
|
||||
return mapTvResult(result);
|
||||
return mapTvResult(
|
||||
result,
|
||||
requests?.find((req) => req.mediaId === result.id)
|
||||
);
|
||||
default:
|
||||
return mapPersonResult(result);
|
||||
}
|
||||
|
@@ -260,6 +260,8 @@ components:
|
||||
video:
|
||||
type: boolean
|
||||
example: false
|
||||
request:
|
||||
$ref: '#/components/schemas/MediaRequest'
|
||||
TvResult:
|
||||
type: object
|
||||
properties:
|
||||
@@ -301,6 +303,8 @@ components:
|
||||
type: string
|
||||
firstAirDate:
|
||||
type: string
|
||||
request:
|
||||
$ref: '#/components/schemas/MediaRequest'
|
||||
PersonResult:
|
||||
type: object
|
||||
properties:
|
||||
@@ -321,6 +325,44 @@ components:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/MovieResult'
|
||||
- $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:
|
||||
cookieAuth:
|
||||
|
@@ -37,7 +37,7 @@ authRoutes.post('/login', async (req, res) => {
|
||||
|
||||
// Next let's see if the user already exists
|
||||
let user = await userRepository.findOne({
|
||||
where: { email: account.email },
|
||||
where: { plexId: account.id },
|
||||
});
|
||||
|
||||
if (user) {
|
||||
@@ -49,6 +49,8 @@ authRoutes.post('/login', async (req, res) => {
|
||||
|
||||
// Update the users avatar with their plex thumbnail (incase it changed)
|
||||
user.avatar = account.thumb;
|
||||
user.email = account.email;
|
||||
user.username = account.username;
|
||||
} else {
|
||||
// Here we check if it's the first user. If it is, we create the user with no check
|
||||
// and give them admin permissions
|
||||
@@ -57,6 +59,8 @@ authRoutes.post('/login', async (req, res) => {
|
||||
if (totalUsers === 0) {
|
||||
user = new User({
|
||||
email: account.email,
|
||||
username: account.username,
|
||||
plexId: account.id,
|
||||
plexToken: account.authToken,
|
||||
permissions: Permission.ADMIN,
|
||||
avatar: account.thumb,
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { Router } from 'express';
|
||||
import TheMovieDb from '../api/themoviedb';
|
||||
import { mapMovieResult, mapTvResult } from '../models/Search';
|
||||
import { MediaRequest } from '../entity/MediaRequest';
|
||||
|
||||
const discoverRoutes = Router();
|
||||
|
||||
@@ -9,11 +10,20 @@ discoverRoutes.get('/movies', async (req, res) => {
|
||||
|
||||
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({
|
||||
page: data.page,
|
||||
totalPages: data.total_pages,
|
||||
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 requests = await MediaRequest.getRelatedRequests(
|
||||
data.results.map((result) => result.id)
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
page: data.page,
|
||||
totalPages: data.total_pages,
|
||||
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 TheMovieDb from '../api/themoviedb';
|
||||
import { mapSearchResults } from '../models/Search';
|
||||
import { MediaRequest } from '../entity/MediaRequest';
|
||||
|
||||
const searchRoutes = Router();
|
||||
|
||||
@@ -12,11 +13,15 @@ searchRoutes.get('/', async (req, res) => {
|
||||
page: Number(req.query.page),
|
||||
});
|
||||
|
||||
const requests = await MediaRequest.getRelatedRequests(
|
||||
results.results.map((result) => result.id)
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
page: results.page,
|
||||
totalPages: results.total_pages,
|
||||
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>;
|
||||
}
|
||||
|
||||
if (!data && !error) {
|
||||
return <div>loading!</div>;
|
||||
}
|
||||
|
||||
const titles = data?.reduce(
|
||||
(a, v) => [...a, ...v.results],
|
||||
[] as MovieResult[]
|
||||
|
@@ -12,7 +12,10 @@
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve"
|
||||
"jsx": "preserve",
|
||||
"strictPropertyInitialization": false,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true
|
||||
},
|
||||
"include": ["next-env.d.ts", "src/**/*.ts", "src/**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
|
Reference in New Issue
Block a user