mirror of
https://github.com/aleksilassila/reiverr.git
synced 2026-04-27 11:05:13 +02:00
feat: Saving and displaying movie play-state, progress etc.
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
||||
} from 'src/common/common.decorator';
|
||||
import { UsersService } from '../users.service';
|
||||
import { MediaService } from 'src/media/media.service';
|
||||
import { PlayStateService } from '../play-state/play-state.service';
|
||||
|
||||
@ApiTags('users')
|
||||
@Controller('users/:userId/library')
|
||||
@@ -32,7 +33,7 @@ export class LibraryController {
|
||||
): Promise<PaginatedResponseDto<LibraryItemDto>> {
|
||||
// const user = await this.userService.findOne(userId);
|
||||
|
||||
const items = await this.libraryService.getLibraryItems(userId);
|
||||
const items = await this.libraryService.getLibraryItems(userId, pagination);
|
||||
|
||||
return {
|
||||
items,
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { MovieDto } from 'src/media/media.dto';
|
||||
import { PlayStateDto } from '../play-state/play-state.dto';
|
||||
|
||||
export class LibraryItemDto {
|
||||
@ApiProperty()
|
||||
tmdbId: string;
|
||||
|
||||
@ApiProperty({ type: [PlayStateDto], required: false })
|
||||
playStates?: PlayStateDto[];
|
||||
|
||||
@ApiProperty({ type: MovieDto, required: false })
|
||||
metadata?: MovieDto; // TODO
|
||||
}
|
||||
|
||||
@@ -4,13 +4,17 @@ import {
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
OneToOne,
|
||||
PrimaryColumn,
|
||||
PrimaryGeneratedColumn,
|
||||
Unique,
|
||||
} from 'typeorm';
|
||||
import { User } from '../user.entity';
|
||||
import { PlayState } from '../play-state/play-state.entity';
|
||||
|
||||
@Entity()
|
||||
@Unique(['tmdbId', 'userId'])
|
||||
export class LibraryItem {
|
||||
@ApiProperty({ required: false, type: 'string' })
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
@@ -28,4 +32,7 @@ export class LibraryItem {
|
||||
@ManyToOne(() => User, (user) => user.libraryItems, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'userId' })
|
||||
user: User;
|
||||
|
||||
@OneToMany(() => PlayState, (playState) => playState.libraryItem)
|
||||
playStates?: PlayState[];
|
||||
}
|
||||
|
||||
@@ -2,6 +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';
|
||||
|
||||
@Injectable()
|
||||
export class LibraryService {
|
||||
@@ -10,11 +11,24 @@ export class LibraryService {
|
||||
private readonly libraryRepository: Repository<LibraryItem>,
|
||||
) {}
|
||||
|
||||
async getLibraryItems(userId: string): Promise<LibraryItem[]> {
|
||||
return this.libraryRepository.find({ where: { userId } });
|
||||
async getLibraryItems(
|
||||
userId: string,
|
||||
pagination: PaginationParamsDto,
|
||||
): Promise<LibraryItem[]> {
|
||||
return this.libraryRepository.find({
|
||||
where: { userId },
|
||||
relations: {
|
||||
playStates: true,
|
||||
},
|
||||
take: pagination.itemsPerPage,
|
||||
skip: pagination.itemsPerPage * (pagination.page - 1),
|
||||
});
|
||||
}
|
||||
|
||||
async findByTmdbId(userId: string, tmdbId: string): Promise<LibraryItem | null> {
|
||||
async findByTmdbId(
|
||||
userId: string,
|
||||
tmdbId: string,
|
||||
): Promise<LibraryItem | null> {
|
||||
return this.libraryRepository.findOne({ where: { userId, tmdbId } });
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,40 @@
|
||||
import { Controller } from '@nestjs/common';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Param,
|
||||
Put,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { PlayStateService } from './play-state.service';
|
||||
import { UserAccessControl } from 'src/auth/auth.guard';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { UpdatePlayStateDto } from './play-state.dto';
|
||||
|
||||
@Controller('play-state')
|
||||
export class PlayStateController {}
|
||||
@ApiTags('users')
|
||||
@Controller('users/:userId/play-state')
|
||||
@UseGuards(UserAccessControl)
|
||||
export class PlayStateController {
|
||||
constructor(private playStateService: PlayStateService) {}
|
||||
|
||||
@Put('movie/tmdb/:tmdbId')
|
||||
async updateMoviePlayStateByTmdbId(
|
||||
@Param('userId') userId: string,
|
||||
@Param('tmdbId') tmdbId: string,
|
||||
@Body() playState: UpdatePlayStateDto,
|
||||
) {
|
||||
return this.playStateService.updateOrCreateMoviePlayState(
|
||||
userId,
|
||||
tmdbId,
|
||||
playState,
|
||||
);
|
||||
}
|
||||
|
||||
@Delete('movie/tmdb/:tmdbId')
|
||||
async deleteMoviePlayStateByTmdbId(
|
||||
@Param('userId') userId: string,
|
||||
@Param('tmdbId') tmdbId: string,
|
||||
) {
|
||||
return this.playStateService.deleteMoviePlayState(userId, tmdbId);
|
||||
}
|
||||
}
|
||||
|
||||
14
backend/src/users/play-state/play-state.dto.ts
Normal file
14
backend/src/users/play-state/play-state.dto.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { OmitType } from '@nestjs/swagger';
|
||||
import { PlayState } from './play-state.entity';
|
||||
|
||||
export class PlayStateDto extends PlayState {}
|
||||
|
||||
export class UpdatePlayStateDto extends OmitType(PlayState, [
|
||||
'id',
|
||||
'tmdbId',
|
||||
'episode',
|
||||
'season',
|
||||
'user',
|
||||
'userId',
|
||||
'lastPlayedAt',
|
||||
]) {}
|
||||
@@ -7,8 +7,10 @@ import {
|
||||
PrimaryColumn,
|
||||
PrimaryGeneratedColumn,
|
||||
Unique,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { User } from '../user.entity';
|
||||
import { LibraryItem } from '../library/library.entity';
|
||||
|
||||
@Entity()
|
||||
@Unique(['tmdbId', 'userId', 'season', 'episode'])
|
||||
@@ -53,6 +55,20 @@ export class PlayState {
|
||||
example: 0.5,
|
||||
description: 'A number between 0 and 1',
|
||||
})
|
||||
@Column({ default: 0 })
|
||||
@Column('double', { default: 0 })
|
||||
progress: number = 0;
|
||||
|
||||
@ApiProperty({
|
||||
type: 'date',
|
||||
description: 'Last time the user played this media',
|
||||
})
|
||||
@UpdateDateColumn()
|
||||
lastPlayedAt: Date;
|
||||
|
||||
@ManyToOne(() => LibraryItem, (libraryItem) => libraryItem.playStates)
|
||||
@JoinColumn([
|
||||
{ name: 'tmdbId', referencedColumnName: 'tmdbId' },
|
||||
{ name: 'userId', referencedColumnName: 'userId' },
|
||||
])
|
||||
libraryItem?: LibraryItem;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,43 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { UpdatePlayStateDto } from './play-state.dto';
|
||||
import { USER_PLAY_STATE_REPOSITORY } from '../user.providers';
|
||||
import { PlayState } from './play-state.entity';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
@Injectable()
|
||||
export class PlayStateService {}
|
||||
export class PlayStateService {
|
||||
constructor(
|
||||
@Inject(USER_PLAY_STATE_REPOSITORY)
|
||||
private readonly playStateRepository: Repository<PlayState>,
|
||||
) {}
|
||||
|
||||
async findMoviePlayState(userId: string, tmdbId: string) {
|
||||
return this.playStateRepository.findOne({
|
||||
where: { userId, tmdbId },
|
||||
});
|
||||
}
|
||||
|
||||
async updateOrCreateMoviePlayState(
|
||||
userId: string,
|
||||
tmdbId: string,
|
||||
playState: UpdatePlayStateDto,
|
||||
) {
|
||||
let state = await this.findMoviePlayState(userId, tmdbId);
|
||||
|
||||
if (!state) {
|
||||
state = this.playStateRepository.create();
|
||||
state.userId = userId;
|
||||
state.tmdbId = tmdbId;
|
||||
}
|
||||
|
||||
state.progress = playState.progress;
|
||||
state.watched = playState.watched;
|
||||
|
||||
return this.playStateRepository.save(state);
|
||||
}
|
||||
|
||||
async deleteMoviePlayState(userId: string, tmdbId: string) {
|
||||
const state = await this.findMoviePlayState(userId, tmdbId);
|
||||
return await this.playStateRepository.remove(state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ApiProperty, OmitType, PartialType, PickType } from '@nestjs/swagger';
|
||||
import { User } from './user.entity';
|
||||
import { PlayStateDto } from './play-state/play-state.dto';
|
||||
|
||||
export class UserDto extends OmitType(User, [
|
||||
'password',
|
||||
@@ -60,4 +61,7 @@ export class SignInDto extends PickType(User, ['name', 'password'] as const) {}
|
||||
export class MovieUserDataDto {
|
||||
@ApiProperty()
|
||||
inLibrary: boolean;
|
||||
|
||||
@ApiProperty({ type: PlayStateDto, required: false })
|
||||
playState?: PlayStateDto;
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@ import {
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
InternalServerErrorException,
|
||||
NotFoundException,
|
||||
Param,
|
||||
@@ -30,6 +28,7 @@ import {
|
||||
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';
|
||||
|
||||
@ApiTags('users')
|
||||
@Controller('users')
|
||||
@@ -37,6 +36,7 @@ export class UsersController {
|
||||
constructor(
|
||||
private usersService: UsersService,
|
||||
private libraryService: LibraryService,
|
||||
private playStateService: PlayStateService,
|
||||
) {}
|
||||
|
||||
// @UseGuards(AuthGuard)
|
||||
@@ -171,9 +171,14 @@ export class UsersController {
|
||||
@Param('tmdbId') tmdbId: string,
|
||||
): Promise<MovieUserDataDto> {
|
||||
const libraryItem = await this.libraryService.findByTmdbId(userId, tmdbId);
|
||||
const playState = await this.playStateService.findMoviePlayState(
|
||||
userId,
|
||||
tmdbId,
|
||||
);
|
||||
|
||||
return {
|
||||
inLibrary: !!libraryItem,
|
||||
playState: playState || undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user