mirror of
https://github.com/aleksilassila/reiverr.git
synced 2026-04-27 11:05:13 +02:00
feat: Add to library, my library
This commit is contained in:
79
backend/src/users/library/library.controller.ts
Normal file
79
backend/src/users/library/library.controller.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { Controller, Delete, Get, Param, Put, UseGuards } from '@nestjs/common';
|
||||
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { UserAccessControl } from 'src/auth/auth.guard';
|
||||
import { LibraryService } from './library.service';
|
||||
import {
|
||||
PaginatedResponseDto,
|
||||
PaginationParamsDto,
|
||||
SuccessResponseDto,
|
||||
} from 'src/common/common.dto';
|
||||
import { LibraryItemDto } from './library.dto';
|
||||
import {
|
||||
GetPaginationParams,
|
||||
PaginatedApiOkResponse,
|
||||
} from 'src/common/common.decorator';
|
||||
import { UsersService } from '../users.service';
|
||||
import { MediaService } from 'src/media/media.service';
|
||||
|
||||
@ApiTags('users')
|
||||
@Controller('users/:userId/library')
|
||||
@UseGuards(UserAccessControl)
|
||||
export class LibraryController {
|
||||
constructor(
|
||||
private userService: UsersService,
|
||||
private libraryService: LibraryService,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@PaginatedApiOkResponse(LibraryItemDto)
|
||||
async getLibraryItems(
|
||||
@GetPaginationParams() pagination: PaginationParamsDto,
|
||||
@Param('userId') userId: string,
|
||||
): Promise<PaginatedResponseDto<LibraryItemDto>> {
|
||||
// const user = await this.userService.findOne(userId);
|
||||
|
||||
const items = await this.libraryService.getLibraryItems(userId);
|
||||
|
||||
return {
|
||||
items,
|
||||
itemsPerPage: pagination.itemsPerPage,
|
||||
page: pagination.page,
|
||||
total: items.length,
|
||||
};
|
||||
}
|
||||
|
||||
@Put('tmdb/:tmdbId')
|
||||
@ApiOkResponse({
|
||||
description: 'Library item added',
|
||||
type: SuccessResponseDto,
|
||||
})
|
||||
async addLibraryItem(
|
||||
@Param('userId') userId: string,
|
||||
@Param('tmdbId') tmdbId: string,
|
||||
): Promise<SuccessResponseDto> {
|
||||
const item = await this.libraryService.findOrCreateByTmdbId(userId, tmdbId);
|
||||
|
||||
return {
|
||||
success: !!item,
|
||||
};
|
||||
}
|
||||
|
||||
@Delete('tmdb/:tmdbId')
|
||||
@ApiOkResponse({
|
||||
description: 'Library item removed',
|
||||
type: SuccessResponseDto,
|
||||
})
|
||||
async removeLibraryItem(
|
||||
@Param('userId') userId: string,
|
||||
@Param('tmdbId') tmdbId: string,
|
||||
): Promise<SuccessResponseDto> {
|
||||
const deleteAction = await this.libraryService.deleteByTmdbId(
|
||||
userId,
|
||||
tmdbId,
|
||||
);
|
||||
|
||||
return {
|
||||
success: deleteAction.affected > 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
10
backend/src/users/library/library.dto.ts
Normal file
10
backend/src/users/library/library.dto.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { MovieDto } from 'src/media/media.dto';
|
||||
|
||||
export class LibraryItemDto {
|
||||
@ApiProperty()
|
||||
tmdbId: string;
|
||||
|
||||
@ApiProperty({ type: MovieDto, required: false })
|
||||
metadata?: MovieDto; // TODO
|
||||
}
|
||||
31
backend/src/users/library/library.entity.ts
Normal file
31
backend/src/users/library/library.entity.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
OneToOne,
|
||||
PrimaryColumn,
|
||||
PrimaryGeneratedColumn,
|
||||
} from 'typeorm';
|
||||
import { User } from '../user.entity';
|
||||
|
||||
@Entity()
|
||||
export class LibraryItem {
|
||||
@ApiProperty({ required: false, type: 'string' })
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@ApiProperty({ required: true, type: 'number' })
|
||||
@Column({ unique: true })
|
||||
tmdbId: string;
|
||||
|
||||
@ApiProperty({ required: true, type: 'string' })
|
||||
@PrimaryColumn()
|
||||
userId: string;
|
||||
|
||||
@ApiProperty({ required: false, type: 'string' })
|
||||
@ManyToOne(() => User, (user) => user.libraryItems, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'userId' })
|
||||
user: User;
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
@Module({})
|
||||
export class LibraryModule {}
|
||||
38
backend/src/users/library/library.service.ts
Normal file
38
backend/src/users/library/library.service.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { USER_LIBRARY_REPOSITORY } from '../user.providers';
|
||||
import { Repository } from 'typeorm';
|
||||
import { LibraryItem } from './library.entity';
|
||||
|
||||
@Injectable()
|
||||
export class LibraryService {
|
||||
constructor(
|
||||
@Inject(USER_LIBRARY_REPOSITORY)
|
||||
private readonly libraryRepository: Repository<LibraryItem>,
|
||||
) {}
|
||||
|
||||
async getLibraryItems(userId: string): Promise<LibraryItem[]> {
|
||||
return this.libraryRepository.find({ where: { userId } });
|
||||
}
|
||||
|
||||
async findByTmdbId(userId: string, tmdbId: string): Promise<LibraryItem | null> {
|
||||
return this.libraryRepository.findOne({ where: { userId, tmdbId } });
|
||||
}
|
||||
|
||||
async findOrCreateByTmdbId(
|
||||
userId: string,
|
||||
tmdbId: string,
|
||||
): Promise<LibraryItem> {
|
||||
let libraryItem = await this.findByTmdbId(userId, tmdbId);
|
||||
|
||||
if (!libraryItem) {
|
||||
libraryItem = this.libraryRepository.create({ userId, tmdbId });
|
||||
await this.libraryRepository.save(libraryItem);
|
||||
}
|
||||
|
||||
return libraryItem;
|
||||
}
|
||||
|
||||
async deleteByTmdbId(userId: string, tmdbId: string) {
|
||||
return await this.libraryRepository.delete({ userId, tmdbId });
|
||||
}
|
||||
}
|
||||
4
backend/src/users/play-state/play-state.controller.ts
Normal file
4
backend/src/users/play-state/play-state.controller.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { Controller } from '@nestjs/common';
|
||||
|
||||
@Controller('play-state')
|
||||
export class PlayStateController {}
|
||||
58
backend/src/users/play-state/play-state.entity.ts
Normal file
58
backend/src/users/play-state/play-state.entity.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryColumn,
|
||||
PrimaryGeneratedColumn,
|
||||
Unique,
|
||||
} from 'typeorm';
|
||||
import { User } from '../user.entity';
|
||||
|
||||
@Entity()
|
||||
@Unique(['tmdbId', 'userId', 'season', 'episode'])
|
||||
export class PlayState {
|
||||
@ApiProperty({ required: false, type: 'string' })
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@ApiProperty({ required: true, type: 'number' })
|
||||
@Column({ unique: true })
|
||||
tmdbId: string;
|
||||
|
||||
@ApiProperty({ required: true, type: 'string' })
|
||||
@Column()
|
||||
userId: string;
|
||||
|
||||
@ApiProperty({ required: true, type: 'string' })
|
||||
@ManyToOne(() => User, (user) => user.playStates, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'userId' })
|
||||
user: User;
|
||||
|
||||
@ApiProperty({ required: false, type: 'number' })
|
||||
@PrimaryColumn({ default: 0 })
|
||||
season: number = 0;
|
||||
|
||||
@ApiProperty({ required: false, type: 'number' })
|
||||
@PrimaryColumn({ default: 0 })
|
||||
episode: number = 0;
|
||||
|
||||
@ApiProperty({
|
||||
required: false,
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether the user has watched this media',
|
||||
})
|
||||
@Column({ default: false })
|
||||
watched: boolean = false;
|
||||
|
||||
@ApiProperty({
|
||||
required: false,
|
||||
default: false,
|
||||
example: 0.5,
|
||||
description: 'A number between 0 and 1',
|
||||
})
|
||||
@Column({ default: 0 })
|
||||
progress: number = 0;
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
@Module({})
|
||||
export class PlayStateModule {}
|
||||
4
backend/src/users/play-state/play-state.service.ts
Normal file
4
backend/src/users/play-state/play-state.service.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class PlayStateService {}
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { AuthGuard, GetUser } from 'src/auth/auth.guard';
|
||||
import { UserAccessControl, GetAuthUser } from 'src/auth/auth.guard';
|
||||
import { UsersService } from '../users.service';
|
||||
import { User } from '../user.entity';
|
||||
import { UserDto } from '../user.dto';
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
|
||||
@ApiTags('users')
|
||||
@Controller('users/:userId/sources')
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(UserAccessControl)
|
||||
export class UserSourcesController {
|
||||
constructor(
|
||||
private usersService: UsersService,
|
||||
@@ -33,7 +33,7 @@ export class UserSourcesController {
|
||||
@Put(':sourceId')
|
||||
@ApiOkResponse({ description: 'Source updated', type: UserDto })
|
||||
async updateSource(
|
||||
@GetUser() callerUser: User,
|
||||
@GetAuthUser() callerUser: User,
|
||||
@Param('sourceId') sourceId: string,
|
||||
@Param('userId') userId: string,
|
||||
@Body() sourceDto: CreateSourceDto,
|
||||
@@ -64,7 +64,7 @@ export class UserSourcesController {
|
||||
@Delete(':sourceId')
|
||||
@ApiOkResponse({ description: 'Source deleted', type: UserDto })
|
||||
async deleteSource(
|
||||
@GetUser() callerUser: User,
|
||||
@GetAuthUser() callerUser: User,
|
||||
@Param('sourceId') sourceId: string,
|
||||
@Param('userId') userId: string,
|
||||
): Promise<UserDto> {
|
||||
|
||||
@@ -9,17 +9,23 @@ export class UserDto extends OmitType(User, [
|
||||
profilePicture: string | null;
|
||||
|
||||
static fromEntity(entity: User, caller: User = entity): UserDto {
|
||||
return {
|
||||
id: entity.id,
|
||||
name: entity.name,
|
||||
isAdmin: entity.isAdmin,
|
||||
settings: entity.settings,
|
||||
onboardingDone: entity.onboardingDone,
|
||||
const out = {
|
||||
...entity,
|
||||
// id: entity.id,
|
||||
// name: entity.name,
|
||||
// isAdmin: entity.isAdmin,
|
||||
// settings: entity.settings,
|
||||
// onboardingDone: entity.onboardingDone,
|
||||
// mediaSources: entity.mediaSources,
|
||||
password: '',
|
||||
profilePicture:
|
||||
'data:image;base64,' + entity.profilePicture?.toString('base64'),
|
||||
// pluginSettings: entity.pluginSettings,
|
||||
mediaSources: entity.mediaSources,
|
||||
};
|
||||
|
||||
delete out.password;
|
||||
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,3 +56,8 @@ export class UpdateUserDto extends PartialType(
|
||||
}
|
||||
|
||||
export class SignInDto extends PickType(User, ['name', 'password'] as const) {}
|
||||
|
||||
export class MovieUserDataDto {
|
||||
@ApiProperty()
|
||||
inLibrary: boolean;
|
||||
}
|
||||
|
||||
@@ -3,11 +3,14 @@ import {
|
||||
Entity,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
OneToOne,
|
||||
PrimaryColumn,
|
||||
PrimaryGeneratedColumn,
|
||||
} from 'typeorm';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { MediaSource } from 'src/users/user-sources/user-source.entity';
|
||||
import { LibraryItem } from './library/library.entity';
|
||||
import { PlayState } from './play-state/play-state.entity';
|
||||
|
||||
export class SonarrSettings {
|
||||
@ApiProperty({ required: true })
|
||||
@@ -143,4 +146,12 @@ export class User {
|
||||
@ApiProperty({ required: false, type: MediaSource, isArray: true })
|
||||
@OneToMany(() => MediaSource, (mediaSource) => mediaSource.user)
|
||||
mediaSources: MediaSource[];
|
||||
|
||||
@ApiProperty({ required: false, type: PlayState, isArray: true })
|
||||
@OneToMany(() => PlayState, (playState) => playState.user)
|
||||
playStates: PlayState[];
|
||||
|
||||
@ApiProperty({ required: false, type: LibraryItem, isArray: true })
|
||||
@OneToMany(() => LibraryItem, (library) => library.user)
|
||||
libraryItems: LibraryItem[];
|
||||
}
|
||||
|
||||
@@ -2,9 +2,13 @@ import { DataSource } from 'typeorm';
|
||||
import { User } from './user.entity';
|
||||
import { DATA_SOURCE } from '../database/database.providers';
|
||||
import { MediaSource } from './user-sources/user-source.entity';
|
||||
import { PlayState } from './play-state/play-state.entity';
|
||||
import { LibraryItem } from './library/library.entity';
|
||||
|
||||
export const USER_REPOSITORY = 'USER_REPOSITORY';
|
||||
export const USER_SOURCE_REPOSITORY = 'USER_SOURCE_REPOSITORY';
|
||||
export const USER_LIBRARY_REPOSITORY = 'USER_LIBRARY_REPOSITORY';
|
||||
export const USER_PLAY_STATE_REPOSITORY = 'USER_PLAY_STATE_REPOSITORY';
|
||||
|
||||
export const userProviders = [
|
||||
{
|
||||
@@ -18,4 +22,15 @@ export const userProviders = [
|
||||
dataSource.getRepository(MediaSource),
|
||||
inject: [DATA_SOURCE],
|
||||
},
|
||||
{
|
||||
provide: USER_PLAY_STATE_REPOSITORY,
|
||||
useFactory: (dataSource: DataSource) => dataSource.getRepository(PlayState),
|
||||
inject: [DATA_SOURCE],
|
||||
},
|
||||
{
|
||||
provide: USER_LIBRARY_REPOSITORY,
|
||||
useFactory: (dataSource: DataSource) =>
|
||||
dataSource.getRepository(LibraryItem),
|
||||
inject: [DATA_SOURCE],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -15,16 +15,29 @@ import {
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { UserServiceError, UsersService } from './users.service';
|
||||
import { AuthGuard, GetUser, OptionalAuthGuard } from '../auth/auth.guard';
|
||||
import {
|
||||
UserAccessControl,
|
||||
GetAuthUser,
|
||||
OptionalAccessControl,
|
||||
} from '../auth/auth.guard';
|
||||
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { CreateUserDto, UpdateUserDto, UserDto } from './user.dto';
|
||||
import {
|
||||
CreateUserDto,
|
||||
MovieUserDataDto,
|
||||
UpdateUserDto,
|
||||
UserDto,
|
||||
} from './user.dto';
|
||||
import { User } from './user.entity';
|
||||
import { ApiException } from '@nanogiants/nestjs-swagger-api-exception-decorator';
|
||||
import { LibraryService } from './library/library.service';
|
||||
|
||||
@ApiTags('users')
|
||||
@Controller('users')
|
||||
export class UsersController {
|
||||
constructor(private usersService: UsersService) {}
|
||||
constructor(
|
||||
private usersService: UsersService,
|
||||
private libraryService: LibraryService,
|
||||
) {}
|
||||
|
||||
// @UseGuards(AuthGuard)
|
||||
// @Get()
|
||||
@@ -40,14 +53,14 @@ export class UsersController {
|
||||
// return UserDto.fromEntity(user);
|
||||
// }
|
||||
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(UserAccessControl)
|
||||
@Get('')
|
||||
@ApiOkResponse({
|
||||
description: 'All users found',
|
||||
type: UserDto,
|
||||
isArray: true,
|
||||
})
|
||||
async findAllUsers(@GetUser() callerUser: User): Promise<UserDto[]> {
|
||||
async findAllUsers(@GetAuthUser() callerUser: User): Promise<UserDto[]> {
|
||||
if (!callerUser.isAdmin) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
@@ -57,13 +70,13 @@ export class UsersController {
|
||||
return users.map((user) => UserDto.fromEntity(user));
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(UserAccessControl)
|
||||
@Get(':id')
|
||||
@ApiOkResponse({ description: 'User found', type: UserDto })
|
||||
@ApiException(() => NotFoundException, { description: 'User not found' })
|
||||
async findUserById(
|
||||
@Param('id') id: string,
|
||||
@GetUser() callerUser: User,
|
||||
@GetAuthUser() callerUser: User,
|
||||
): Promise<UserDto> {
|
||||
if (!callerUser.isAdmin && callerUser.id !== id) {
|
||||
throw new NotFoundException();
|
||||
@@ -84,7 +97,7 @@ export class UsersController {
|
||||
// return this.userService.noPreviousAdmins();
|
||||
// }
|
||||
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@UseGuards(OptionalAccessControl)
|
||||
@Post()
|
||||
@ApiOkResponse({ description: 'User created', type: UserDto })
|
||||
@ApiException(() => UnauthorizedException, { description: 'Unauthorized' })
|
||||
@@ -92,7 +105,7 @@ export class UsersController {
|
||||
async createUser(
|
||||
@Body()
|
||||
userCreateDto: CreateUserDto,
|
||||
@GetUser() callerUser: User | undefined,
|
||||
@GetAuthUser() callerUser: User | undefined,
|
||||
) {
|
||||
const canCreateUser =
|
||||
(await this.usersService.noPreviousAdmins()) || callerUser?.isAdmin;
|
||||
@@ -108,14 +121,14 @@ export class UsersController {
|
||||
return UserDto.fromEntity(user);
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(UserAccessControl)
|
||||
@Put(':id')
|
||||
@ApiOkResponse({ description: 'User updated', type: UserDto })
|
||||
@ApiException(() => NotFoundException, { description: 'User not found' })
|
||||
async updateUser(
|
||||
@Param('id') id: string,
|
||||
@Body() updateUserDto: UpdateUserDto,
|
||||
@GetUser() callerUser: User,
|
||||
@GetAuthUser() callerUser: User,
|
||||
): Promise<UserDto> {
|
||||
if ((!callerUser.isAdmin && callerUser.id !== id) || !id) {
|
||||
throw new NotFoundException();
|
||||
@@ -135,15 +148,32 @@ export class UsersController {
|
||||
return UserDto.fromEntity(updated);
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(UserAccessControl)
|
||||
@Delete(':id')
|
||||
@ApiOkResponse({ description: 'User deleted' })
|
||||
@ApiException(() => NotFoundException, { description: 'User not found' })
|
||||
async deleteUser(@Param('id') id: string, @GetUser() callerUser: User) {
|
||||
async deleteUser(@Param('id') id: string, @GetAuthUser() callerUser: User) {
|
||||
if ((!callerUser.isAdmin && callerUser.id !== id) || !id) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await this.usersService.remove(id);
|
||||
}
|
||||
|
||||
@UseGuards(UserAccessControl)
|
||||
@Get(':userId/user-data/movie/tmdb/:tmdbId')
|
||||
@ApiOkResponse({
|
||||
description: 'User movie data found',
|
||||
type: MovieUserDataDto,
|
||||
})
|
||||
async getUserMovieData(
|
||||
@Param('userId') userId: string,
|
||||
@Param('tmdbId') tmdbId: string,
|
||||
): Promise<MovieUserDataDto> {
|
||||
const libraryItem = await this.libraryService.findByTmdbId(userId, tmdbId);
|
||||
|
||||
return {
|
||||
inLibrary: !!libraryItem,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,15 +3,28 @@ import { UsersService } from './users.service';
|
||||
import { userProviders } from './user.providers';
|
||||
import { UsersController } from './users.controller';
|
||||
import { DatabaseModule } from '../database/database.module';
|
||||
import { LibraryModule } from './library/library.module';
|
||||
import { PlayStateModule } from './play-state/play-state.module';
|
||||
import { UserSourcesService } from './user-sources/user-sources.service';
|
||||
import { UserSourcesController } from './user-sources/user-sources.controller';
|
||||
import { LibraryService } from './library/library.service';
|
||||
import { PlayStateService } from './play-state/play-state.service';
|
||||
import { LibraryController } from './library/library.controller';
|
||||
import { PlayStateController } from './play-state/play-state.controller';
|
||||
|
||||
@Module({
|
||||
imports: [DatabaseModule, LibraryModule, PlayStateModule],
|
||||
providers: [...userProviders, UsersService, UserSourcesService],
|
||||
controllers: [UsersController, UserSourcesController],
|
||||
exports: [UsersService, UserSourcesService],
|
||||
imports: [DatabaseModule],
|
||||
providers: [
|
||||
...userProviders,
|
||||
UsersService,
|
||||
UserSourcesService,
|
||||
LibraryService,
|
||||
PlayStateService,
|
||||
],
|
||||
controllers: [
|
||||
UsersController,
|
||||
UserSourcesController,
|
||||
LibraryController,
|
||||
PlayStateController,
|
||||
],
|
||||
exports: [UsersService, UserSourcesService, LibraryService],
|
||||
})
|
||||
export class UsersModule {}
|
||||
|
||||
Reference in New Issue
Block a user