mirror of
https://github.com/aleksilassila/reiverr.git
synced 2026-04-27 11:05:13 +02:00
feat: tmdb cache, plugin support changes, series page, episode page, movie page streaming updated
This commit is contained in:
@@ -1,8 +1,18 @@
|
||||
import { Controller, Delete, Get, Param, Put, UseGuards } from '@nestjs/common';
|
||||
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
|
||||
import {
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
ParseEnumPipe,
|
||||
Put,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOkResponse, ApiQuery, ApiTags } from '@nestjs/swagger';
|
||||
import { UserAccessControl } from 'src/auth/auth.guard';
|
||||
import { LibraryService } from './library.service';
|
||||
import {
|
||||
MediaType,
|
||||
PaginatedResponseDto,
|
||||
PaginationParamsDto,
|
||||
SuccessResponseDto,
|
||||
@@ -44,6 +54,7 @@ export class LibraryController {
|
||||
}
|
||||
|
||||
@Put('tmdb/:tmdbId')
|
||||
@ApiQuery({ name: 'mediaType', enum: MediaType })
|
||||
@ApiOkResponse({
|
||||
description: 'Library item added',
|
||||
type: SuccessResponseDto,
|
||||
@@ -51,8 +62,13 @@ export class LibraryController {
|
||||
async addLibraryItem(
|
||||
@Param('userId') userId: string,
|
||||
@Param('tmdbId') tmdbId: string,
|
||||
@Query('mediaType', new ParseEnumPipe(MediaType)) mediaType: MediaType,
|
||||
): Promise<SuccessResponseDto> {
|
||||
const item = await this.libraryService.findOrCreateByTmdbId(userId, tmdbId);
|
||||
const item = await this.libraryService.findOrCreateByTmdbId(
|
||||
userId,
|
||||
tmdbId,
|
||||
mediaType,
|
||||
);
|
||||
|
||||
return {
|
||||
success: !!item,
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { MovieDto } from 'src/metadata/metadata.dto';
|
||||
import { PlayStateDto } from '../play-state/play-state.dto';
|
||||
import { MediaType } from 'src/common/common.dto';
|
||||
|
||||
export class LibraryItemDto {
|
||||
@ApiProperty()
|
||||
tmdbId: string;
|
||||
|
||||
@ApiProperty({ enum: MediaType })
|
||||
mediaType: MediaType;
|
||||
|
||||
@ApiProperty({ type: [PlayStateDto], required: false })
|
||||
playStates?: PlayStateDto[];
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from 'typeorm';
|
||||
import { User } from '../user.entity';
|
||||
import { PlayState } from '../play-state/play-state.entity';
|
||||
import { MediaType } from 'src/common/common.dto';
|
||||
|
||||
@Entity()
|
||||
@Unique(['tmdbId', 'userId'])
|
||||
@@ -24,6 +25,10 @@ export class LibraryItem {
|
||||
@Column({ unique: true })
|
||||
tmdbId: string;
|
||||
|
||||
@ApiProperty({ required: true, enum: MediaType })
|
||||
@Column()
|
||||
mediaType: MediaType;
|
||||
|
||||
@ApiProperty({ required: true, type: 'string' })
|
||||
@PrimaryColumn()
|
||||
userId: string;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import { USER_LIBRARY_REPOSITORY } from '../user.providers';
|
||||
import { Repository } from 'typeorm';
|
||||
import { LibraryItem } from './library.entity';
|
||||
import { PaginationParamsDto } from 'src/common/common.dto';
|
||||
import { MediaType, PaginationParamsDto } from 'src/common/common.dto';
|
||||
|
||||
@Injectable()
|
||||
export class LibraryService {
|
||||
@@ -35,11 +35,16 @@ export class LibraryService {
|
||||
async findOrCreateByTmdbId(
|
||||
userId: string,
|
||||
tmdbId: string,
|
||||
mediaType: MediaType,
|
||||
): Promise<LibraryItem> {
|
||||
let libraryItem = await this.findByTmdbId(userId, tmdbId);
|
||||
|
||||
if (!libraryItem) {
|
||||
libraryItem = this.libraryRepository.create({ userId, tmdbId });
|
||||
libraryItem = this.libraryRepository.create({
|
||||
userId,
|
||||
tmdbId,
|
||||
mediaType,
|
||||
});
|
||||
await this.libraryRepository.save(libraryItem);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,13 +3,17 @@ import {
|
||||
Controller,
|
||||
Delete,
|
||||
Param,
|
||||
ParseEnumPipe,
|
||||
ParseIntPipe,
|
||||
Put,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { PlayStateService } from './play-state.service';
|
||||
import { UserAccessControl } from 'src/auth/auth.guard';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { ApiQuery, ApiTags } from '@nestjs/swagger';
|
||||
import { UpdatePlayStateDto } from './play-state.dto';
|
||||
import { MediaType } from 'src/common/common.dto';
|
||||
|
||||
@ApiTags('users')
|
||||
@Controller('users/:userId/play-state')
|
||||
@@ -18,15 +22,19 @@ export class PlayStateController {
|
||||
constructor(private playStateService: PlayStateService) {}
|
||||
|
||||
@Put('movie/tmdb/:tmdbId')
|
||||
@ApiQuery({ name: 'mediaType', enum: MediaType, required: false })
|
||||
async updateMoviePlayStateByTmdbId(
|
||||
@Param('userId') userId: string,
|
||||
@Param('tmdbId') tmdbId: string,
|
||||
@Body() playState: UpdatePlayStateDto,
|
||||
@Query('mediaType', new ParseEnumPipe(MediaType, { optional: true }))
|
||||
mediaType?: MediaType,
|
||||
) {
|
||||
return this.playStateService.updateOrCreateMoviePlayState(
|
||||
userId,
|
||||
tmdbId,
|
||||
playState,
|
||||
mediaType,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -37,4 +45,36 @@ export class PlayStateController {
|
||||
) {
|
||||
return this.playStateService.deleteMoviePlayState(userId, tmdbId);
|
||||
}
|
||||
|
||||
@Put('show/tmdb/:tmdbId/season/:season/episode/:episode')
|
||||
async updateEpisodePlayStateByTmdbId(
|
||||
@Param('userId') userId: string,
|
||||
@Param('tmdbId') tmdbId: string,
|
||||
@Param('season', ParseIntPipe) season: number,
|
||||
@Param('episode', ParseIntPipe) episode: number,
|
||||
@Body() playState: UpdatePlayStateDto,
|
||||
) {
|
||||
return this.playStateService.updateOrCreateEpisodePlayState(
|
||||
userId,
|
||||
tmdbId,
|
||||
season,
|
||||
episode,
|
||||
playState,
|
||||
);
|
||||
}
|
||||
|
||||
@Delete('show/tmdb/:tmdbId/season/:season/episode/:episode')
|
||||
async deleteEpisodePlayStateByTmdbId(
|
||||
@Param('userId') userId: string,
|
||||
@Param('tmdbId') tmdbId: string,
|
||||
@Param('season', ParseIntPipe) season: number,
|
||||
@Param('episode', ParseIntPipe) episode: number,
|
||||
) {
|
||||
return this.playStateService.deleteEpisodePlayState(
|
||||
userId,
|
||||
tmdbId,
|
||||
season,
|
||||
episode,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,24 @@ import { PlayState } from './play-state.entity';
|
||||
|
||||
export class PlayStateDto extends PlayState {}
|
||||
|
||||
// export class PlayStateDto extends IntersectionType(
|
||||
// OmitType(PlayState, ['id']),
|
||||
// PartialType(PickType(PlayState, ['id'])),
|
||||
// ) {
|
||||
// constructor(
|
||||
// tmdbId: string,
|
||||
// userId: string,
|
||||
// season?: number,
|
||||
// episode?: number,
|
||||
// ) {
|
||||
// super();
|
||||
// this.tmdbId = tmdbId;
|
||||
// this.userId = userId;
|
||||
// if (season !== undefined) this.season = season;
|
||||
// if (episode !== undefined) this.episode = episode;
|
||||
// }
|
||||
// }
|
||||
|
||||
export class UpdatePlayStateDto extends OmitType(PlayState, [
|
||||
'id',
|
||||
'tmdbId',
|
||||
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
} from 'typeorm';
|
||||
import { User } from '../user.entity';
|
||||
import { LibraryItem } from '../library/library.entity';
|
||||
import { UserDto } from '../user.dto';
|
||||
import { MediaType } from 'src/common/common.dto';
|
||||
|
||||
@Entity()
|
||||
@Unique(['tmdbId', 'userId', 'season', 'episode'])
|
||||
@@ -23,14 +25,18 @@ export class PlayState {
|
||||
@Column({ unique: true })
|
||||
tmdbId: string;
|
||||
|
||||
@ApiProperty({ required: false, enum: MediaType })
|
||||
@Column({ nullable: true })
|
||||
mediaType: MediaType;
|
||||
|
||||
@ApiProperty({ required: true, type: 'string' })
|
||||
@Column()
|
||||
userId: string;
|
||||
|
||||
@ApiProperty({ required: true, type: 'string' })
|
||||
// @ApiProperty({ required: false, type: UserDto })
|
||||
@ManyToOne(() => User, (user) => user.playStates, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'userId' })
|
||||
user: User;
|
||||
user?: User;
|
||||
|
||||
@ApiProperty({ required: false, type: 'number' })
|
||||
@PrimaryColumn({ default: 0 })
|
||||
@@ -61,6 +67,7 @@ export class PlayState {
|
||||
@ApiProperty({
|
||||
type: 'string',
|
||||
description: 'Last time the user played this media',
|
||||
required: false,
|
||||
})
|
||||
@UpdateDateColumn()
|
||||
lastPlayedAt: Date;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { UpdatePlayStateDto } from './play-state.dto';
|
||||
import { USER_PLAY_STATE_REPOSITORY } from '../user.providers';
|
||||
import { PlayState } from './play-state.entity';
|
||||
import { Repository } from 'typeorm';
|
||||
import { MediaType } from 'src/common/common.dto';
|
||||
|
||||
@Injectable()
|
||||
export class PlayStateService {
|
||||
@@ -17,10 +18,38 @@ export class PlayStateService {
|
||||
});
|
||||
}
|
||||
|
||||
async findShowPlayState(
|
||||
userId: string,
|
||||
tmdbId: string,
|
||||
season?: number,
|
||||
episode?: number,
|
||||
): Promise<PlayState | undefined> {
|
||||
const playStates =
|
||||
(await this.playStateRepository.find({
|
||||
where: {
|
||||
userId,
|
||||
tmdbId,
|
||||
...(season ? { season } : {}),
|
||||
...(episode ? { episode } : {}),
|
||||
},
|
||||
})) ?? [];
|
||||
|
||||
playStates.sort((a, b) => {
|
||||
if (a.season !== b.season) {
|
||||
return a.season - b.season;
|
||||
}
|
||||
|
||||
return a.episode - b.episode;
|
||||
});
|
||||
|
||||
return playStates[0];
|
||||
}
|
||||
|
||||
async updateOrCreateMoviePlayState(
|
||||
userId: string,
|
||||
tmdbId: string,
|
||||
playState: UpdatePlayStateDto,
|
||||
mediaType?: MediaType,
|
||||
) {
|
||||
let state = await this.findMoviePlayState(userId, tmdbId);
|
||||
|
||||
@@ -28,6 +57,32 @@ export class PlayStateService {
|
||||
state = this.playStateRepository.create();
|
||||
state.userId = userId;
|
||||
state.tmdbId = tmdbId;
|
||||
if (mediaType) {
|
||||
state.mediaType = mediaType;
|
||||
}
|
||||
}
|
||||
|
||||
state.progress = playState.progress;
|
||||
state.watched = playState.watched;
|
||||
|
||||
return this.playStateRepository.save(state);
|
||||
}
|
||||
|
||||
async updateOrCreateEpisodePlayState(
|
||||
userId: string,
|
||||
tmdbId: string,
|
||||
season: number,
|
||||
episode: number,
|
||||
playState: UpdatePlayStateDto,
|
||||
) {
|
||||
let state = await this.findShowPlayState(userId, tmdbId, season, episode);
|
||||
|
||||
if (!state) {
|
||||
state = this.playStateRepository.create();
|
||||
state.userId = userId;
|
||||
state.tmdbId = tmdbId;
|
||||
state.season = season;
|
||||
state.episode = episode;
|
||||
}
|
||||
|
||||
state.progress = playState.progress;
|
||||
@@ -40,4 +95,14 @@ export class PlayStateService {
|
||||
const state = await this.findMoviePlayState(userId, tmdbId);
|
||||
return await this.playStateRepository.remove(state);
|
||||
}
|
||||
|
||||
async deleteEpisodePlayState(
|
||||
userId: string,
|
||||
tmdbId: string,
|
||||
season: number,
|
||||
episode: number,
|
||||
) {
|
||||
const state = await this.findShowPlayState(userId, tmdbId, season, episode);
|
||||
return await this.playStateRepository.remove(state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,10 @@ export class UpdateUserDto extends PartialType(
|
||||
|
||||
export class SignInDto extends PickType(User, ['name', 'password'] as const) {}
|
||||
|
||||
export class MovieUserDataDto {
|
||||
export class MediaUserDataDto {
|
||||
@ApiProperty()
|
||||
tmdbId: string;
|
||||
|
||||
@ApiProperty()
|
||||
inLibrary: boolean;
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
InternalServerErrorException,
|
||||
NotFoundException,
|
||||
Param,
|
||||
ParseIntPipe,
|
||||
Post,
|
||||
Put,
|
||||
UnauthorizedException,
|
||||
@@ -21,7 +22,7 @@ import {
|
||||
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
|
||||
import {
|
||||
CreateUserDto,
|
||||
MovieUserDataDto,
|
||||
MediaUserDataDto,
|
||||
UpdateUserDto,
|
||||
UserDto,
|
||||
} from './user.dto';
|
||||
@@ -29,6 +30,8 @@ import { User } from './user.entity';
|
||||
import { ApiException } from '@nanogiants/nestjs-swagger-api-exception-decorator';
|
||||
import { LibraryService } from './library/library.service';
|
||||
import { PlayStateService } from './play-state/play-state.service';
|
||||
import { PlayState } from './play-state/play-state.entity';
|
||||
import { PlayStateDto } from './play-state/play-state.dto';
|
||||
|
||||
@ApiTags('users')
|
||||
@Controller('users')
|
||||
@@ -164,12 +167,12 @@ export class UsersController {
|
||||
@Get(':userId/user-data/movie/tmdb/:tmdbId')
|
||||
@ApiOkResponse({
|
||||
description: 'User movie data found',
|
||||
type: MovieUserDataDto,
|
||||
type: MediaUserDataDto,
|
||||
})
|
||||
async getUserMovieData(
|
||||
@Param('userId') userId: string,
|
||||
@Param('tmdbId') tmdbId: string,
|
||||
): Promise<MovieUserDataDto> {
|
||||
): Promise<MediaUserDataDto> {
|
||||
const libraryItem = await this.libraryService.findByTmdbId(userId, tmdbId);
|
||||
const playState = await this.playStateService.findMoviePlayState(
|
||||
userId,
|
||||
@@ -177,8 +180,59 @@ export class UsersController {
|
||||
);
|
||||
|
||||
return {
|
||||
tmdbId,
|
||||
inLibrary: !!libraryItem,
|
||||
playState: playState || undefined,
|
||||
playState: playState,
|
||||
};
|
||||
}
|
||||
|
||||
@UseGuards(UserAccessControl)
|
||||
@Get(':userId/user-data/show/tmdb/:tmdbId')
|
||||
@ApiOkResponse({
|
||||
description: 'User show data found',
|
||||
type: MediaUserDataDto,
|
||||
})
|
||||
async getShowUserData(
|
||||
@Param('userId') userId: string,
|
||||
@Param('tmdbId') tmdbId: string,
|
||||
): Promise<MediaUserDataDto> {
|
||||
const libraryItem = await this.libraryService.findByTmdbId(userId, tmdbId);
|
||||
const playState = await this.playStateService.findShowPlayState(
|
||||
userId,
|
||||
tmdbId,
|
||||
);
|
||||
|
||||
return {
|
||||
tmdbId,
|
||||
inLibrary: !!libraryItem,
|
||||
playState: playState,
|
||||
};
|
||||
}
|
||||
|
||||
@UseGuards(UserAccessControl)
|
||||
@Get(':userId/user-data/show/tmdb/:tmdbId/season/:season/episode/:episode')
|
||||
@ApiOkResponse({
|
||||
description: 'User show data found',
|
||||
type: MediaUserDataDto,
|
||||
})
|
||||
async getEpisodeUserData(
|
||||
@Param('userId') userId: string,
|
||||
@Param('tmdbId') tmdbId: string,
|
||||
@Param('season', ParseIntPipe) season: number,
|
||||
@Param('episode', ParseIntPipe) episode: number,
|
||||
): Promise<MediaUserDataDto> {
|
||||
const libraryItem = await this.libraryService.findByTmdbId(userId, tmdbId);
|
||||
const playState = await this.playStateService.findShowPlayState(
|
||||
userId,
|
||||
tmdbId,
|
||||
season,
|
||||
episode,
|
||||
);
|
||||
|
||||
return {
|
||||
tmdbId,
|
||||
inLibrary: !!libraryItem,
|
||||
playState: playState,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user