feat(api): decouple media requests from media info

pull/100/head
sct 4 years ago
parent 4aa74319e0
commit 8577db1be1

@ -0,0 +1,19 @@
export enum MediaRequestStatus {
PENDING = 1,
APPROVED,
DECLINED,
AVAILABLE,
}
export enum MediaType {
MOVIE = 'movie',
TV = 'tv',
}
export enum MediaStatus {
UNKNOWN = 1,
PENDING,
PROCESSING,
PARTIALLY_AVAILABLE,
AVAILABLE,
}

@ -0,0 +1,87 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
Index,
OneToMany,
CreateDateColumn,
UpdateDateColumn,
getRepository,
In,
} from 'typeorm';
import { MediaRequest } from './MediaRequest';
import { MediaStatus, MediaType } from '../constants/media';
@Entity()
class Media {
public static async getRelatedMedia(
tmdbIds: number | number[]
): Promise<Media[]> {
const mediaRepository = getRepository(Media);
try {
let finalIds: number[];
if (!Array.isArray(tmdbIds)) {
finalIds = [tmdbIds];
} else {
finalIds = tmdbIds;
}
const media = await mediaRepository.find({
tmdbId: In(finalIds),
});
return media;
} catch (e) {
console.error(e.messaage);
return [];
}
}
public static async getMedia(id: number): Promise<Media | undefined> {
const mediaRepository = getRepository(Media);
try {
const media = await mediaRepository.findOneOrFail({
where: { tmdbId: id },
});
return media;
} catch (e) {
console.error(e.messaage);
return undefined;
}
}
@PrimaryGeneratedColumn()
public id: number;
@Column({ type: 'varchar' })
public mediaType: MediaType;
@Column({ unique: true })
@Index()
public tmdbId: number;
@Column({ unique: true, nullable: true })
@Index()
public tvdbId: number;
@Column({ type: 'int', default: MediaStatus.UNKNOWN })
public status: MediaStatus;
@OneToMany(() => MediaRequest, (request) => request.media)
public requests: MediaRequest;
@CreateDateColumn()
public createdAt: Date;
@UpdateDateColumn()
public updatedAt: Date;
constructor(init?: Partial<Media>) {
Object.assign(this, init);
}
}
export default Media;

@ -5,144 +5,53 @@ import {
Column,
CreateDateColumn,
UpdateDateColumn,
getRepository,
In,
Index,
TableInheritance,
AfterUpdate,
AfterInsert,
getRepository,
} from 'typeorm';
import { User } from './User';
import RadarrAPI from '../api/radarr';
import { getSettings } from '../lib/settings';
import TheMovieDb from '../api/themoviedb';
export enum MediaRequestStatus {
PENDING = 1,
APPROVED,
DECLINED,
AVAILABLE,
}
import Media from './Media';
import { MediaStatus, MediaRequestStatus, MediaType } from '../constants/media';
@Entity()
@TableInheritance({ column: { type: 'varchar', name: 'type' } })
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 [];
}
}
public static async getRequest(
id: number
): Promise<MediaRequest | undefined> {
const requestRepository = getRepository(MediaRequest);
try {
const request = await requestRepository.findOneOrFail({
where: { mediaId: id },
});
return request;
} catch (e) {
console.error(e.messaage);
return undefined;
}
}
@PrimaryGeneratedColumn()
public id: number;
@Column({ unique: true })
@Index()
public mediaId: number;
@Column({ unique: true, nullable: true })
@Index()
public tvdbId: number;
@Column({ nullable: true })
public seasons?: string;
@Column()
public mediaType: 'movie' | 'tv';
@Column({ type: 'integer' })
public status: MediaRequestStatus;
@ManyToOne(() => Media, (media) => media.requests, { eager: true })
public media: Media;
@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;
@Column()
public type: MediaType;
constructor(init?: Partial<MediaRequest>) {
Object.assign(this, init);
}
@AfterUpdate()
@AfterInsert()
private async sendToRadarr() {
if (
this.mediaType === 'movie' &&
this.status === MediaRequestStatus.APPROVED
) {
try {
const settings = getSettings();
if (settings.radarr.length === 0 && !settings.radarr[0]) {
console.log(
'[MediaRequest] Skipped radarr request as there is no radarr configured'
);
return;
}
const tmdb = new TheMovieDb();
const radarrSettings = settings.radarr[0];
const radarr = new RadarrAPI({
apiKey: radarrSettings.apiKey,
url: `${radarrSettings.useSsl ? 'https' : 'http'}://${
radarrSettings.hostname
}:${radarrSettings.port}/api`,
});
const movie = await tmdb.getMovie({ movieId: this.mediaId });
await radarr.addMovie({
profileId: radarrSettings.activeProfileId,
qualityProfileId: radarrSettings.activeProfileId,
rootFolderPath: radarrSettings.activeDirectory,
title: movie.title,
tmdbId: movie.id,
year: Number(movie.release_date.slice(0, 4)),
monitored: true,
searchNow: true,
});
console.log('[MediaRequest] Sent request to Radarr');
} catch (e) {
throw new Error(
`[MediaRequest] Request failed to send to radarr: ${e.message}`
);
}
private async updateParentStatus() {
const mediaRepository = getRepository(Media);
if (this.status === MediaRequestStatus.APPROVED) {
this.media.status = MediaStatus.PROCESSING;
mediaRepository.save(this.media);
}
}
}

@ -0,0 +1,57 @@
import { MediaRequest } from './MediaRequest';
import { ChildEntity, AfterUpdate, AfterInsert } from 'typeorm';
import TheMovieDb from '../api/themoviedb';
import RadarrAPI from '../api/radarr';
import { getSettings } from '../lib/settings';
import { MediaType, MediaRequestStatus } from '../constants/media';
@ChildEntity(MediaType.MOVIE)
class MovieRequest extends MediaRequest {
constructor(init?: Partial<MovieRequest>) {
super(init);
}
@AfterUpdate()
@AfterInsert()
private async sendToRadarr() {
if (this.status === MediaRequestStatus.APPROVED) {
try {
const settings = getSettings();
if (settings.radarr.length === 0 && !settings.radarr[0]) {
console.log(
'[MediaRequest] Skipped radarr request as there is no radarr configured'
);
return;
}
const tmdb = new TheMovieDb();
const radarrSettings = settings.radarr[0];
const radarr = new RadarrAPI({
apiKey: radarrSettings.apiKey,
url: `${radarrSettings.useSsl ? 'https' : 'http'}://${
radarrSettings.hostname
}:${radarrSettings.port}/api`,
});
const movie = await tmdb.getMovie({ movieId: this.media.tmdbId });
await radarr.addMovie({
profileId: radarrSettings.activeProfileId,
qualityProfileId: radarrSettings.activeProfileId,
rootFolderPath: radarrSettings.activeDirectory,
title: movie.title,
tmdbId: movie.id,
year: Number(movie.release_date.slice(0, 4)),
monitored: true,
searchNow: true,
});
console.log('[MediaRequest] Sent request to Radarr');
} catch (e) {
throw new Error(
`[MediaRequest] Request failed to send to radarr: ${e.message}`
);
}
}
}
}
export default MovieRequest;

@ -0,0 +1,37 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
} from 'typeorm';
import TvRequest from './TvRequest';
import { MediaRequestStatus } from '../constants/media';
@Entity()
class SeasonRequest {
@PrimaryGeneratedColumn()
public id: number;
@Column()
public seasonNumber: number;
@Column({ type: 'int', default: MediaRequestStatus.PENDING })
public status: MediaRequestStatus;
@ManyToOne(() => TvRequest, (request) => request.seasons)
public request: TvRequest;
@CreateDateColumn()
public createdAt: Date;
@UpdateDateColumn()
public updatedAt: Date;
constructor(init?: Partial<SeasonRequest>) {
Object.assign(this, init);
}
}
export default SeasonRequest;

@ -0,0 +1,16 @@
import { MediaRequest } from './MediaRequest';
import { ChildEntity, OneToMany } from 'typeorm';
import SeasonRequest from './SeasonRequest';
import { MediaType } from '../constants/media';
@ChildEntity(MediaType.TV)
class TvRequest extends MediaRequest {
@OneToMany(() => SeasonRequest, (season) => season.request)
public seasons: SeasonRequest[];
constructor(init?: Partial<TvRequest>) {
super(init);
}
}
export default TvRequest;

@ -10,6 +10,7 @@ import {
ExternalIds,
mapExternalIds,
} from './common';
import Media from '../entity/Media';
export interface MovieDetails {
id: number;
@ -46,13 +47,13 @@ export interface MovieDetails {
cast: Cast[];
crew: Crew[];
};
request?: MediaRequest;
mediaInfo?: Media;
externalIds: ExternalIds;
}
export const mapMovieDetails = (
movie: TmdbMovieDetails,
request?: MediaRequest
media?: Media
): MovieDetails => ({
id: movie.id,
adult: movie.adult,
@ -88,5 +89,5 @@ export const mapMovieDetails = (
crew: movie.credits.crew.map(mapCrew),
},
externalIds: mapExternalIds(movie.external_ids),
request,
mediaInfo: media,
});

@ -4,6 +4,7 @@ import type {
TmdbTvResult,
} from '../api/themoviedb';
import type { MediaRequest } from '../entity/MediaRequest';
import Media from '../entity/Media';
export type MediaType = 'tv' | 'movie' | 'person';
@ -18,7 +19,7 @@ interface SearchResult {
genreIds: number[];
overview: string;
originalLanguage: string;
request?: MediaRequest;
mediaInfo?: Media;
}
export interface MovieResult extends SearchResult {
@ -28,7 +29,7 @@ export interface MovieResult extends SearchResult {
releaseDate: string;
adult: boolean;
video: boolean;
request?: MediaRequest;
mediaInfo?: Media;
}
export interface TvResult extends SearchResult {
@ -53,7 +54,7 @@ export type Results = MovieResult | TvResult | PersonResult;
export const mapMovieResult = (
movieResult: TmdbMovieResult,
request?: MediaRequest
media?: Media
): MovieResult => ({
id: movieResult.id,
mediaType: 'movie',
@ -70,12 +71,12 @@ export const mapMovieResult = (
voteCount: movieResult.vote_count,
backdropPath: movieResult.backdrop_path,
posterPath: movieResult.poster_path,
request,
mediaInfo: media,
});
export const mapTvResult = (
tvResult: TmdbTvResult,
request?: MediaRequest
media?: Media
): TvResult => ({
id: tvResult.id,
firstAirDate: tvResult.first_air_Date,
@ -92,7 +93,7 @@ export const mapTvResult = (
voteCount: tvResult.vote_count,
backdropPath: tvResult.backdrop_path,
posterPath: tvResult.poster_path,
request,
mediaInfo: media,
});
export const mapPersonResult = (
@ -115,19 +116,19 @@ export const mapPersonResult = (
export const mapSearchResults = (
results: (TmdbMovieResult | TmdbTvResult | TmdbPersonResult)[],
requests?: MediaRequest[]
media?: Media[]
): Results[] =>
results.map((result) => {
switch (result.media_type) {
case 'movie':
return mapMovieResult(
result,
requests?.find((req) => req.mediaId === result.id)
media?.find((req) => req.tmdbId === result.id)
);
case 'tv':
return mapTvResult(
result,
requests?.find((req) => req.mediaId === result.id)
media?.find((req) => req.tmdbId === result.id)
);
default:
return mapPersonResult(result);

@ -8,12 +8,12 @@ import {
ExternalIds,
mapExternalIds,
} from './common';
import { MediaRequest } from '../entity/MediaRequest';
import {
TmdbTvEpisodeDetails,
TmdbTvSeasonDetails,
TmdbTvDetails,
} from '../api/themoviedb';
import type Media from '../entity/Media';
interface Episode {
id: number;
@ -78,7 +78,7 @@ export interface TvDetails {
crew: Crew[];
};
externalIds: ExternalIds;
request?: MediaRequest;
mediaInfo?: Media;
}
const mapEpisodeDetails = (episode: TmdbTvEpisodeDetails): Episode => ({
@ -107,7 +107,7 @@ const mapSeasonDetails = (season: TmdbTvSeasonDetails): Season => ({
export const mapTvDetails = (
show: TmdbTvDetails,
request?: MediaRequest
media?: Media
): TvDetails => ({
createdBy: show.created_by,
episodeRunTime: show.episode_run_time,
@ -159,5 +159,5 @@ export const mapTvDetails = (
crew: show.credits.crew.map(mapCrew),
},
externalIds: mapExternalIds(show.external_ids),
request,
mediaInfo: media,
});

@ -263,8 +263,8 @@ components:
video:
type: boolean
example: false
request:
$ref: '#/components/schemas/MediaRequest'
mediaInfo:
$ref: '#/components/schemas/MediaInfo'
TvResult:
type: object
properties:
@ -306,8 +306,8 @@ components:
type: string
firstAirDate:
type: string
request:
$ref: '#/components/schemas/MediaRequest'
mediaInfo:
$ref: '#/components/schemas/MediaInfo'
PersonResult:
type: object
properties:
@ -435,8 +435,8 @@ components:
$ref: '#/components/schemas/Crew'
externalIds:
$ref: '#/components/schemas/ExternalIds'
request:
$ref: '#/components/schemas/MediaRequest'
mediaInfo:
$ref: '#/components/schemas/MediaInfo'
Episode:
type: object
properties:
@ -577,8 +577,8 @@ components:
$ref: '#/components/schemas/Crew'
externalIds:
$ref: '#/components/schemas/ExternalIds'
request:
$ref: '#/components/schemas/MediaRequest'
mediaInfo:
$ref: '#/components/schemas/MediaInfo'
MediaRequest:
type: object
properties:
@ -586,18 +586,13 @@ components:
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
description: Status of the request. 1 = PENDING APPROVAL, 2 = APPROVED, 3 = DECLINED, 4 = AVAILABLE
readOnly: true
media:
$ref: '#/components/schemas/MediaInfo'
createdAt:
type: string
example: '2020-09-12T10:00:27.000Z'
@ -616,9 +611,34 @@ components:
nullable: true
required:
- id
- mediaId
- mediaType
- status
MediaInfo:
type: object
properties:
id:
type: number
readOnly: true
tmdbId:
type: number
readOnly: true
tvdbId:
type: number
readOnly: true
status:
type: number
requests:
type: array
readOnly: true
items:
$ref: '#/components/schemas/MediaRequest'
createdAt:
type: string
example: '2020-09-12T10:00:27.000Z'
readOnly: true
updatedAt:
type: string
example: '2020-09-12T10:00:27.000Z'
readOnly: true
Cast:
type: object
properties:

@ -1,7 +1,7 @@
import { Router } from 'express';
import TheMovieDb from '../api/themoviedb';
import { mapMovieResult, mapTvResult } from '../models/Search';
import { MediaRequest } from '../entity/MediaRequest';
import Media from '../entity/Media';
const discoverRoutes = Router();
@ -13,7 +13,7 @@ discoverRoutes.get('/movies', async (req, res) => {
language: req.query.language as string,
});
const requests = await MediaRequest.getRelatedRequests(
const media = await Media.getRelatedMedia(
data.results.map((result) => result.id)
);
@ -24,7 +24,7 @@ discoverRoutes.get('/movies', async (req, res) => {
results: data.results.map((result) =>
mapMovieResult(
result,
requests.find((req) => req.mediaId === result.id)
media.find((req) => req.tmdbId === result.id)
)
),
});
@ -38,7 +38,7 @@ discoverRoutes.get('/tv', async (req, res) => {
language: req.query.language as string,
});
const requests = await MediaRequest.getRelatedRequests(
const media = await Media.getRelatedMedia(
data.results.map((result) => result.id)
);
@ -49,7 +49,7 @@ discoverRoutes.get('/tv', async (req, res) => {
results: data.results.map((result) =>
mapTvResult(
result,
requests.find((req) => req.mediaId === result.id)
media.find((req) => req.tmdbId === result.id)
)
),
});

@ -3,6 +3,7 @@ import TheMovieDb from '../api/themoviedb';
import { mapMovieDetails } from '../models/Movie';
import { MediaRequest } from '../entity/MediaRequest';
import { mapMovieResult } from '../models/Search';
import Media from '../entity/Media';
const movieRoutes = Router();
@ -14,9 +15,9 @@ movieRoutes.get('/:id', async (req, res) => {
language: req.query.language as string,
});
const request = await MediaRequest.getRequest(movie.id);
const media = await Media.getMedia(movie.id);
return res.status(200).json(mapMovieDetails(movie, request));
return res.status(200).json(mapMovieDetails(movie, media));
});
movieRoutes.get('/:id/recommendations', async (req, res) => {
@ -28,7 +29,7 @@ movieRoutes.get('/:id/recommendations', async (req, res) => {
language: req.query.language as string,
});
const requests = await MediaRequest.getRelatedRequests(
const media = await Media.getRelatedMedia(
results.results.map((result) => result.id)
);
@ -39,7 +40,7 @@ movieRoutes.get('/:id/recommendations', async (req, res) => {
results: results.results.map((result) =>
mapMovieResult(
result,
requests.find((req) => req.mediaId === result.id)
media.find((req) => req.tmdbId === result.id)
)
),
});
@ -54,7 +55,7 @@ movieRoutes.get('/:id/similar', async (req, res) => {
language: req.query.language as string,
});
const requests = await MediaRequest.getRelatedRequests(
const media = await Media.getRelatedMedia(
results.results.map((result) => result.id)
);
@ -65,7 +66,7 @@ movieRoutes.get('/:id/similar', async (req, res) => {
results: results.results.map((result) =>
mapMovieResult(
result,
requests.find((req) => req.mediaId === result.id)
media.find((req) => req.tmdbId === result.id)
)
),
});

@ -2,8 +2,12 @@ import { Router } from 'express';
import { isAuthenticated } from '../middleware/auth';
import { Permission } from '../lib/permissions';
import { getRepository } from 'typeorm';
import { MediaRequest, MediaRequestStatus } from '../entity/MediaRequest';
import { MediaRequest } from '../entity/MediaRequest';
import TheMovieDb from '../api/themoviedb';
import Media from '../entity/Media';
import MovieRequest from '../entity/MovieRequest';
import { MediaStatus, MediaRequestStatus, MediaType } from '../constants/media';
import TvRequest from '../entity/TvRequest';
const requestRoutes = Router();
@ -15,10 +19,12 @@ requestRoutes.get('/', async (req, res, next) => {
order: {
id: 'DESC',
},
relations: ['media'],
take: 20,
})
: await requestRepository.find({
where: { requestedBy: { id: req.user?.id } },
relations: ['media'],
order: {
id: 'DESC',
},
@ -36,26 +42,59 @@ requestRoutes.post(
isAuthenticated(Permission.REQUEST),
async (req, res, next) => {
const tmdb = new TheMovieDb();
const requestRepository = getRepository(MediaRequest);
const mediaRepository = getRepository(Media);
try {
const media =
const tmdbMedia =
req.body.mediaType === 'movie'
? await tmdb.getMovie({ movieId: req.body.mediaId })
: await tmdb.getTvShow({ tvId: req.body.mediaId });
const request = new MediaRequest({
mediaId: media.id,
mediaType: req.body.mediaType,
requestedBy: req.user,
// If the user is an admin or has the "auto approve" permission, automatically approve the request
status: req.user?.hasPermission(Permission.AUTO_APPROVE)
? MediaRequestStatus.APPROVED
: MediaRequestStatus.PENDING,
let media = await mediaRepository.findOne({
where: { tmdbId: req.body.mediaId },
});
await requestRepository.save(request);
if (!media) {
media = new Media({
tmdbId: tmdbMedia.id,
tvdbId: tmdbMedia.external_ids.tvdb_id,
status: MediaStatus.PENDING,
mediaType: req.body.mediaType,
});
await mediaRepository.save(media);
}
if (req.body.mediaType === 'movie') {
const requestRepository = getRepository(MovieRequest);
return res.status(201).json(request);
const request = new MovieRequest({
media,
requestedBy: req.user,
// If the user is an admin or has the "auto approve" permission, automatically approve the request
status: req.user?.hasPermission(Permission.AUTO_APPROVE)
? MediaRequestStatus.APPROVED
: MediaRequestStatus.PENDING,
});
await requestRepository.save(request);
return res.status(201).json(request);
} else if (req.body.mediaType === 'tv') {
const requestRepository = getRepository(TvRequest);
const request = new TvRequest({
media,
requestedBy: req.user,
// If the user is an admin or has the "auto approve" permission, automatically approve the request
status: req.user?.hasPermission(Permission.AUTO_APPROVE)
? MediaRequestStatus.APPROVED
: MediaRequestStatus.PENDING,
});
await requestRepository.save(request);
return res.status(201).json(request);
}
next({ status: 500, message: 'Invalid media type' });
} catch (e) {
next({ message: e.message, status: 500 });
}
@ -96,7 +135,7 @@ requestRoutes.delete('/:requestId', async (req, res, next) => {
});
}
requestRepository.delete(request.id);
await requestRepository.delete(request.id);
return res.status(200).json(request);
} catch (e) {
@ -106,7 +145,7 @@ requestRoutes.delete('/:requestId', async (req, res, next) => {
requestRoutes.get<{
requestId: string;
status: 'pending' | 'approve' | 'decline' | 'available';
status: 'pending' | 'approve' | 'decline';
}>(
'/:requestId/:status',
isAuthenticated(Permission.MANAGE_REQUESTS),
@ -131,9 +170,6 @@ requestRoutes.get<{
case 'decline':
newStatus = MediaRequestStatus.DECLINED;
break;
case 'available':
newStatus = MediaRequestStatus.AVAILABLE;
break;
}
request.status = newStatus;

@ -1,7 +1,7 @@
import { Router } from 'express';
import TheMovieDb from '../api/themoviedb';
import { mapSearchResults } from '../models/Search';
import { MediaRequest } from '../entity/MediaRequest';
import Media from '../entity/Media';
const searchRoutes = Router();
@ -14,7 +14,7 @@ searchRoutes.get('/', async (req, res) => {
language: req.query.language as string,
});
const requests = await MediaRequest.getRelatedRequests(
const media = await Media.getRelatedMedia(
results.results.map((result) => result.id)
);
@ -22,7 +22,7 @@ searchRoutes.get('/', async (req, res) => {
page: results.page,
totalPages: results.total_pages,
totalResults: results.total_results,
results: mapSearchResults(results.results, requests),
results: mapSearchResults(results.results, media),
});
});

@ -3,6 +3,7 @@ import TheMovieDb from '../api/themoviedb';
import { MediaRequest } from '../entity/MediaRequest';
import { mapTvDetails } from '../models/Tv';
import { mapTvResult } from '../models/Search';
import Media from '../entity/Media';
const tvRoutes = Router();
@ -14,9 +15,9 @@ tvRoutes.get('/:id', async (req, res) => {
language: req.query.language as string,
});
const request = await MediaRequest.getRequest(tv.id);
const media = await Media.getMedia(tv.id);
return res.status(200).json(mapTvDetails(tv, request));
return res.status(200).json(mapTvDetails(tv, media));
});
tvRoutes.get('/:id/recommendations', async (req, res) => {
@ -28,7 +29,7 @@ tvRoutes.get('/:id/recommendations', async (req, res) => {
language: req.query.language as string,
});
const requests = await MediaRequest.getRelatedRequests(
const media = await Media.getRelatedMedia(
results.results.map((result) => result.id)
);
@ -39,7 +40,7 @@ tvRoutes.get('/:id/recommendations', async (req, res) => {
results: results.results.map((result) =>
mapTvResult(
result,
requests.find((req) => req.mediaId === result.id)
media.find((req) => req.tmdbId === result.id)
)
),
});
@ -54,7 +55,7 @@ tvRoutes.get('/:id/similar', async (req, res) => {
language: req.query.language as string,
});
const requests = await MediaRequest.getRelatedRequests(
const media = await Media.getRelatedMedia(
results.results.map((result) => result.id)
);
@ -65,7 +66,7 @@ tvRoutes.get('/:id/similar', async (req, res) => {
results: results.results.map((result) =>
mapTvResult(
result,
requests.find((req) => req.mediaId === result.id)
media.find((req) => req.tmdbId === result.id)
)
),
});

@ -39,13 +39,12 @@ const ListView: React.FC<ListViewProps> = ({
<TitleCard
id={title.id}
image={title.posterPath}
status={title.request?.status}
status={title.mediaInfo?.status}
summary={title.overview}
title={title.title}
userScore={title.voteAverage}
year={title.releaseDate}
mediaType={title.mediaType}
requestId={title.request?.id}
/>
);
break;
@ -54,13 +53,12 @@ const ListView: React.FC<ListViewProps> = ({
<TitleCard
id={title.id}
image={title.posterPath}
status={title.request?.status}
status={title.mediaInfo?.status}
summary={title.overview}
title={title.name}
userScore={title.voteAverage}
year={title.firstAirDate}
mediaType={title.mediaType}
requestId={title.request?.id}
/>
);
break;

@ -76,8 +76,8 @@ const Discover: React.FC = () => {
items={requests?.map((request) => (
<RequestCard
key={`request-slider-item-${request.id}`}
tmdbId={request.mediaId}
type={request.mediaType}
tmdbId={request.media.tmdbId}
type={request.media.mediaType}
/>
))}
/>
@ -115,13 +115,12 @@ const Discover: React.FC = () => {
key={`popular-movie-slider-${title.id}`}
id={title.id}
image={title.posterPath}
status={title.request?.status}
status={title.mediaInfo?.status}
summary={title.overview}
title={title.title}
userScore={title.voteAverage}
year={title.releaseDate}
mediaType={title.mediaType}
requestId={title.request?.id}
/>
))}
/>
@ -159,13 +158,12 @@ const Discover: React.FC = () => {
key={`popular-tv-slider-${title.id}`}
id={title.id}
image={title.posterPath}
status={title.request?.status}
status={title.mediaInfo?.status}
summary={title.overview}
title={title.name}
userScore={title.voteAverage}
year={title.firstAirDate}
mediaType={title.mediaType}
requestId={title.request?.id}
/>
))}
/>

@ -23,6 +23,7 @@ import { LanguageContext } from '../../context/LanguageContext';
import LoadingSpinner from '../Common/LoadingSpinner';
import { useUser, Permission } from '../../hooks/useUser';
import PendingRequest from '../PendingRequest';
import { MediaStatus } from '../../../server/constants/media';
const messages = defineMessages({
releasedate: 'Release Date',
@ -101,13 +102,7 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
};
const cancelRequest = async () => {
const response = await axios.delete<MediaRequest>(
`/api/v1/request/${data?.request?.id}`
);
if (response.data.id) {
revalidate();
}
// fix this
};
if (!data && !error) {
@ -167,7 +162,8 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
</span>
</div>
<div className="flex-1 flex justify-end mt-4 md:mt-0">
{!data.request && (
{(!data.mediaInfo ||
data.mediaInfo?.status === MediaStatus.UNKNOWN) && (
<Button
buttonType="primary"
onClick={() => setShowRequestModal(true)}
@ -189,14 +185,8 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
<FormattedMessage {...messages.request} />
</Button>
)}
{data.request?.status === MediaRequestStatus.PENDING && (
<Button
buttonType="warning"
onClick={() => {
if (data.request?.requestedBy.id === user?.id)
setShowCancelModal(true);
}}
>
{data.mediaInfo?.status === MediaStatus.PENDING && (
<Button buttonType="warning">
<svg
className="w-4 mr-2"
fill="none"
@ -211,12 +201,10 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
/>
</svg>
{data.request?.requestedBy.id === user?.id
? intl.formatMessage(messages.cancelrequest)
: intl.formatMessage(messages.pending)}
<FormattedMessage {...messages.pending} />
</Button>
)}
{data.request?.status === MediaRequestStatus.APPROVED && (
{data.mediaInfo?.status === MediaStatus.PROCESSING && (
<Button buttonType="danger">
<svg
className="w-5 mr-1"
@ -235,7 +223,7 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
<FormattedMessage {...messages.unavailable} />
</Button>
)}
{data.request?.status === MediaRequestStatus.AVAILABLE && (
{data.mediaInfo?.status === MediaStatus.AVAILABLE && (
<Button buttonType="success">
<svg
className="w-5 mr-1"
@ -300,13 +288,13 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
</div>
<div className="flex pt-8 text-white flex-col md:flex-row pb-4">
<div className="flex-1 md:mr-8">
{data.request?.status === MediaRequestStatus.PENDING &&
{/* {data.mediaInfo?.status === MediaStatus.PENDING &&
hasPermission(Permission.MANAGE_REQUESTS) && (
<PendingRequest
request={data.request}
onUpdate={() => revalidate()}
/>
)}
)} */}
<h2 className="text-xl md:text-2xl">
<FormattedMessage {...messages.overview} />
</h2>
@ -463,13 +451,12 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
key={`recommended-${title.id}`}
id={title.id}
image={title.posterPath}
status={title.request?.status}
status={title.mediaInfo?.status}
summary={title.overview}
title={title.title}
userScore={title.voteAverage}
year={title.releaseDate}
mediaType={title.mediaType}
requestId={title.request?.id}
/>
))}
/>
@ -510,13 +497,12 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
key={`recommended-${title.id}`}
id={title.id}
image={title.posterPath}
status={title.request?.status}
status={title.mediaInfo?.status}
summary={title.overview}
title={title.title}
userScore={title.voteAverage}
year={title.releaseDate}
mediaType={title.mediaType}
requestId={title.request?.id}
/>
))}
/>

@ -34,25 +34,23 @@ const RequestCard: React.FC<TmdbTitleCardProps> = ({ tmdbId, type }) => {
<TitleCard
id={title.id}
image={title.posterPath}
status={title.request?.status}
status={title.mediaInfo?.status}
summary={title.overview}
title={title.title}
userScore={title.voteAverage}
year={title.releaseDate}
mediaType={'movie'}
requestId={title.request?.id}
/>
) : (
<TitleCard
id={title.id}
image={title.posterPath}
status={title.request?.status}
status={title.mediaInfo?.status}
summary={title.overview}
title={title.name}
userScore={title.voteAverage}
year={title.firstAirDate}
mediaType={'tv'}
requestId={title.request?.id}
/>
);
};

@ -11,6 +11,7 @@ import axios from 'axios';
import { MediaRequest } from '../../../server/entity/MediaRequest';
import MovieRequestModal from '../RequestModal/MovieRequestModal';
import Link from 'next/link';
import { MediaStatus } from '../../../server/constants/media';
interface TitleCardProps {
id: number;
@ -20,17 +21,10 @@ interface TitleCardProps {
title: string;
userScore: number;
mediaType: MediaType;
status?: MediaRequestStatus;
status?: MediaStatus;
requestId?: number;
}
enum MediaRequestStatus {
PENDING = 1,
APPROVED,
DECLINED,
AVAILABLE,
}
const TitleCard: React.FC<TitleCardProps> = ({
id,
image,
@ -56,7 +50,7 @@ const TitleCard: React.FC<TitleCardProps> = ({
});
if (response.data) {
setCurrentStatus(response.data.status);
setCurrentStatus(response.data.media.status);
addToast(
<span>
<strong>{title}</strong> succesfully requested!
@ -131,13 +125,13 @@ const TitleCard: React.FC<TitleCardProps> = ({
right: '-1px',
}}
>
{currentStatus === MediaRequestStatus.AVAILABLE && (
{currentStatus === MediaStatus.AVAILABLE && (
<Available className="rounded-tr-md" />
)}
{currentStatus === MediaRequestStatus.PENDING && (
{currentStatus === MediaStatus.PENDING && (
<Requested className="rounded-tr-md" />
)}
{currentStatus === MediaRequestStatus.APPROVED && (
{currentStatus === MediaStatus.PROCESSING && (
<Unavailable className="rounded-tr-md" />
)}
</div>
@ -251,7 +245,7 @@ const TitleCard: React.FC<TitleCardProps> = ({
</svg>
</button>
)}
{currentStatus === MediaRequestStatus.PENDING && (
{currentStatus === MediaStatus.PENDING && (
<button
onClick={() => setShowCancelModal(true)}
className="w-full h-7 text-center text-white bg-orange-400 hover:bg-orange-300 rounded-sm ml-1 focus:border-orange-700 focus:shadow-outline-orange active:bg-orange-700 transition ease-in-out duration-150"
@ -272,7 +266,7 @@ const TitleCard: React.FC<TitleCardProps> = ({
</svg>
</button>
)}
{currentStatus === MediaRequestStatus.APPROVED && (
{currentStatus === MediaStatus.AVAILABLE && (
<button className="w-full h-7 text-center text-white bg-red-500 rounded-sm ml-1">
<svg
className="w-4 mx-auto"
@ -290,7 +284,7 @@ const TitleCard: React.FC<TitleCardProps> = ({
</svg>
</button>
)}
{currentStatus === MediaRequestStatus.AVAILABLE && (
{currentStatus === MediaStatus.AVAILABLE && (
<button className="w-full h-7 text-center text-white bg-green-400 rounded-sm ml-1">
<svg
className="w-4 mx-auto"

@ -17,6 +17,7 @@ import LoadingSpinner from '../Common/LoadingSpinner';
import { useUser, Permission } from '../../hooks/useUser';
import PendingRequest from '../PendingRequest';
import { TvDetails as TvDetailsType } from '../../../server/models/Tv';
import { MediaStatus } from '../../../server/constants/media';
const messages = defineMessages({
userrating: 'User Rating',
@ -76,7 +77,7 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
const request = async () => {
const response = await axios.post<MediaRequest>('/api/v1/request', {
mediaId: data?.id,
mediaType: 'movie',
mediaType: 'tv',
});
if (response.data) {
@ -91,13 +92,7 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
};
const cancelRequest = async () => {
const response = await axios.delete<MediaRequest>(
`/api/v1/request/${data?.request?.id}`
);
if (response.data.id) {
revalidate();
}
// fix me
};
if (!data && !error) {
@ -148,7 +143,8 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
</span>
</div>
<div className="flex-1 flex justify-end mt-4 md:mt-0">
{!data.request && (
{(!data.mediaInfo ||
data.mediaInfo.status === MediaStatus.UNKNOWN) && (
<Button
buttonType="primary"
onClick={() => setShowRequestModal(true)}
@ -170,14 +166,8 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
<FormattedMessage {...messages.request} />
</Button>
)}
{data.request?.status === MediaRequestStatus.PENDING && (
<Button
buttonType="warning"
onClick={() => {
if (data.request?.requestedBy.id === user?.id)
setShowCancelModal(true);
}}
>
{data.mediaInfo?.status === MediaStatus.PENDING && (
<Button buttonType="warning">
<svg
className="w-4 mr-2"
fill="none"
@ -192,12 +182,10 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
/>
</svg>
{data.request?.requestedBy.id === user?.id
? intl.formatMessage(messages.cancelrequest)
: intl.formatMessage(messages.pending)}
<FormattedMessage {...messages.pending} />
</Button>
)}
{data.request?.status === MediaRequestStatus.APPROVED && (
{data.mediaInfo?.status === MediaStatus.PROCESSING && (
<Button buttonType="danger">
<svg
className="w-5 mr-1"
@ -216,7 +204,7 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
<FormattedMessage {...messages.unavailable} />
</Button>
)}
{data.request?.status === MediaRequestStatus.AVAILABLE && (
{data.mediaInfo?.status === MediaStatus.AVAILABLE && (
<Button buttonType="success">
<svg
className="w-5 mr-1"
@ -281,13 +269,13 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
</div>
<div className="flex pt-8 text-white flex-col md:flex-row pb-4">
<div className="flex-1 md:mr-8">
{data.request?.status === MediaRequestStatus.PENDING &&
{/* {data.mediaInfo?.status === MediaStatus.PENDING &&
hasPermission(Permission.MANAGE_REQUESTS) && (
<PendingRequest
request={data.request}
onUpdate={() => revalidate()}
/>
)}
)} */}
<h2 className="text-xl md:text-2xl">
<FormattedMessage {...messages.overview} />
</h2>
@ -403,13 +391,12 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
key={`recommended-${title.id}`}
id={title.id}
image={title.posterPath}
status={title.request?.status}
status={title.mediaInfo?.status}
summary={title.overview}
title={title.name}
userScore={title.voteAverage}
year={title.firstAirDate}
mediaType={title.mediaType}
requestId={title.request?.id}
/>
))}
/>
@ -447,13 +434,12 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
key={`recommended-${title.id}`}
id={title.id}
image={title.posterPath}
status={title.request?.status}
status={title.mediaInfo?.status}
summary={title.overview}
title={title.name}
userScore={title.voteAverage}
year={title.firstAirDate}
mediaType={title.mediaType}
requestId={title.request?.id}
/>
))}
/>

Loading…
Cancel
Save