refactor: pretty much the whole backend module hierarchy

This commit is contained in:
Aleksi Lassila
2025-02-11 02:40:41 +02:00
parent 6969525464
commit fa27f19975
96 changed files with 1786 additions and 2033 deletions

View File

@@ -1,99 +0,0 @@
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,
} from 'src/common/common.dto';
import { LibraryItemDto } from './library.dto';
import {
GetPaginationParams,
PaginatedApiOkResponse,
} from 'src/common/common.decorator';
import { UsersService } from '../users.service';
import { MetadataService } from 'src/metadata/metadata.service';
import { PlayStateService } from '../play-state/play-state.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.getLibraryItemsWithMetadata(
userId,
pagination,
);
return {
items,
itemsPerPage: pagination.itemsPerPage,
page: pagination.page,
total: items.length,
};
}
@Put('tmdb/:tmdbId')
@ApiQuery({ name: 'mediaType', enum: MediaType })
@ApiOkResponse({
description: 'Library item added',
type: SuccessResponseDto,
})
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,
mediaType,
);
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,
};
}
}

View File

@@ -1,22 +0,0 @@
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';
import { Series } from 'src/metadata/metadata.entity';
export class LibraryItemDto {
@ApiProperty()
tmdbId: string;
@ApiProperty({ enum: MediaType })
mediaType: MediaType;
@ApiProperty({ type: [PlayStateDto], required: false })
playStates?: PlayStateDto[];
@ApiProperty({ type: MovieDto, required: false })
movieMetadata?: MovieDto;
@ApiProperty({ type: Series, required: false })
seriesMetadata?: Series;
}

View File

@@ -1,43 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import {
Column,
Entity,
JoinColumn,
ManyToOne,
OneToMany,
OneToOne,
PrimaryColumn,
PrimaryGeneratedColumn,
Unique,
} 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'])
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, enum: MediaType })
@Column()
mediaType: MediaType;
@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;
@OneToMany(() => PlayState, (playState) => playState.libraryItem)
playStates?: PlayState[];
}

View File

@@ -1,83 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import { USER_LIBRARY_REPOSITORY } from '../user.providers';
import { Repository } from 'typeorm';
import { LibraryItem } from './library.entity';
import { MediaType, PaginationParamsDto } from 'src/common/common.dto';
import { LibraryItemDto } from './library.dto';
import { MetadataService } from 'src/metadata/metadata.service';
@Injectable()
export class LibraryService {
constructor(
@Inject(USER_LIBRARY_REPOSITORY)
private readonly libraryRepository: Repository<LibraryItem>,
private readonly metadataService: MetadataService,
) {}
async getLibraryItemsWithMetadata(
userId: string,
pagination: PaginationParamsDto,
): Promise<LibraryItemDto[]> {
const items = await this.getLibraryItems(userId, pagination);
return Promise.all(
items.map(async (item) => {
return {
...item,
movieMetadata:
item.mediaType === MediaType.Movie
? await this.metadataService.getMovieByTmdbId(item.tmdbId)
: undefined,
seriesMetadata:
item.mediaType === MediaType.Series
? await this.metadataService.getSeriesByTmdbId(item.tmdbId)
: undefined,
};
}),
);
}
private 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> {
return this.libraryRepository.findOne({ where: { userId, tmdbId } });
}
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,
mediaType,
});
await this.libraryRepository.save(libraryItem);
}
return libraryItem;
}
async deleteByTmdbId(userId: string, tmdbId: string) {
return await this.libraryRepository.delete({ userId, tmdbId });
}
}

View File

@@ -1,79 +0,0 @@
import {
Body,
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 { 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')
@UseGuards(UserAccessControl)
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,
);
}
@Delete('movie/tmdb/:tmdbId')
async deleteMoviePlayStateByTmdbId(
@Param('userId') userId: string,
@Param('tmdbId') tmdbId: string,
) {
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,
);
}
}

View File

@@ -1,26 +0,0 @@
import { OmitType, PartialType } from '@nestjs/swagger';
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 PartialType(
OmitType(PlayStateDto, ['userId']),
) {}

View File

@@ -1,81 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import {
Column,
Entity,
JoinColumn,
ManyToOne,
PrimaryColumn,
PrimaryGeneratedColumn,
Unique,
UpdateDateColumn,
} 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'])
export class PlayState {
@ApiProperty({ type: 'string' })
@PrimaryGeneratedColumn('uuid')
id: string;
@ApiProperty({ required: true, type: 'number' })
@Column()
tmdbId: string;
@ApiProperty({ enum: MediaType })
@Column()
mediaType: MediaType;
@ApiProperty({ required: true, type: 'string' })
@Column()
userId: string;
// @ApiProperty({ required: false, type: UserDto })
@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({
type: 'boolean',
default: false,
description: 'Whether the user has watched this media',
})
@Column({ default: false })
watched: boolean = false;
@ApiProperty({
default: false,
example: 0.5,
description: 'A number between 0 and 1',
})
@Column('double', { default: 0 })
progress: number = 0;
@ApiProperty({
type: 'string',
description: 'Last time the user played this media',
})
@UpdateDateColumn()
lastPlayedAt: Date;
@ManyToOne(() => LibraryItem, (libraryItem) => libraryItem.playStates, {
nullable: true,
createForeignKeyConstraints: false,
})
@JoinColumn([
{ name: 'tmdbId', referencedColumnName: 'tmdbId' },
{ name: 'userId', referencedColumnName: 'userId' },
])
libraryItem?: LibraryItem;
}

View File

@@ -1,120 +0,0 @@
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';
import { MediaType } from 'src/common/common.dto';
@Injectable()
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 findSeriesPlayStates(
userId: string,
tmdbId: string,
season?: number,
episode?: number,
): Promise<PlayState[]> {
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;
}
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.mediaType = MediaType.Movie;
}
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.findSeriesPlayStates(
userId,
tmdbId,
season,
episode,
).then((states) =>
states.find(
(state) => state.season === season && state.episode === episode,
),
);
if (!state) {
state = this.playStateRepository.create();
state.userId = userId;
state.tmdbId = tmdbId;
state.season = season;
state.episode = episode;
state.mediaType = MediaType.Episode;
}
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);
}
async deleteEpisodePlayState(
userId: string,
tmdbId: string,
season: number,
episode: number,
) {
const state = await this.findSeriesPlayStates(
userId,
tmdbId,
season,
episode,
);
return await this.playStateRepository.remove(state);
}
}

View File

@@ -1,22 +0,0 @@
import {
ApiProperty,
IntersectionType,
OmitType,
PartialType,
PickType,
} from '@nestjs/swagger';
import { MediaSource } from './user-source.entity';
import { Type } from '@nestjs/common';
import { PickAndPartial } from 'src/common/common.dto';
export class SourceDto extends PickAndPartial(
MediaSource,
['id', 'userId', 'adminControlled', 'enabled'],
['pluginSettings'],
) {}
export class CreateSourceDto extends PickAndPartial(
MediaSource,
['pluginSettings'],
['enabled', 'adminControlled'],
) {}

View File

@@ -1,33 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { PluginSettings } from 'plugins/plugin-types';
import { User } from 'src/users/user.entity';
import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm';
@Entity()
export class MediaSource {
@ApiProperty({ required: true, type: 'string' })
@PrimaryColumn()
id: string;
@ApiProperty({ required: true, type: 'string' })
@PrimaryColumn()
userId: string;
@ApiProperty({ required: true, type: 'string' })
@ManyToOne(() => User, (user) => user.mediaSources)
@JoinColumn({ name: 'userId' })
user: User;
@ApiProperty({ required: false, type: 'boolean', default: false })
@Column({ default: false })
enabled: boolean = false;
@ApiProperty({ required: false, type: 'boolean', default: false })
@Column({ default: false })
adminControlled: boolean;
@ApiProperty({ required: false, type: 'object' })
@Column('json', { default: '{}' })
pluginSettings: PluginSettings = {};
// Add other fields as necessary
}

View File

@@ -1,79 +0,0 @@
import {
Body,
Controller,
Delete,
Get,
InternalServerErrorException,
NotFoundException,
Param,
Put,
UnauthorizedException,
UseGuards,
} from '@nestjs/common';
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { UserAccessControl, GetAuthUser } from 'src/auth/auth.guard';
import { UsersService } from '../users.service';
import { User } from '../user.entity';
import { UserDto } from '../user.dto';
import { CreateSourceDto } from './user-source.dto';
import {
UserSourcesService,
UserSourcesServiceError,
} from './user-sources.service';
@ApiTags('users')
@Controller('users/:userId/sources')
@UseGuards(UserAccessControl)
export class UserSourcesController {
constructor(
private usersService: UsersService,
private userSourcesService: UserSourcesService,
) {}
@Put(':sourceId')
@ApiOkResponse({ description: 'Source updated', type: UserDto })
async updateSource(
@GetAuthUser() callerUser: User,
@Param('sourceId') sourceId: string,
@Param('userId') userId: string,
@Body() sourceDto: CreateSourceDto,
): Promise<UserDto> {
const user = await this.usersService.findOne(userId);
if (!user) {
throw new NotFoundException('User not found');
}
const updatedUser = await this.userSourcesService
.updateUserSource(user, sourceId, sourceDto, callerUser)
.catch((e) => {
if (e === UserSourcesServiceError.Unauthorized) {
throw new UnauthorizedException();
} else {
throw new InternalServerErrorException('Failed to update source');
}
});
if (!updatedUser) {
throw new InternalServerErrorException('Failed to update source');
}
return UserDto.fromEntity(updatedUser);
}
@Delete(':sourceId')
@ApiOkResponse({ description: 'Source deleted', type: UserDto })
async deleteSource(
@GetAuthUser() callerUser: User,
@Param('sourceId') sourceId: string,
@Param('userId') userId: string,
): Promise<UserDto> {
const updatedUser = await this.userSourcesService.deleteUserSource(
userId,
sourceId,
callerUser,
);
return UserDto.fromEntity(updatedUser);
}
}

View File

@@ -1,96 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import { Repository } from 'typeorm';
import { User } from '../user.entity';
import { CreateSourceDto } from './user-source.dto';
import { USER_REPOSITORY, USER_SOURCE_REPOSITORY } from '../user.providers';
import { MediaSource } from './user-source.entity';
import { UsersService } from '../users.service';
export enum UserSourcesServiceError {
SourceNotFound = 'SourceNotFound',
Unauthorized = 'Unauthorized',
}
@Injectable()
export class UserSourcesService {
constructor(
private readonly userService: UsersService,
@Inject(USER_SOURCE_REPOSITORY)
private readonly userSourceRepository: Repository<MediaSource>,
) {}
private async findUserSource(
userId: string,
sourceId: string,
): Promise<MediaSource> {
const source = await this.userSourceRepository.findOne({
where: {
id: sourceId,
userId: userId,
},
});
return source;
}
async deleteUserSource(
userId: string,
sourceId: string,
callerUser: User,
): Promise<User> {
if (!callerUser.isAdmin || callerUser.id !== userId) {
throw UserSourcesServiceError.Unauthorized;
}
const source = await this.findUserSource(userId, sourceId);
if (!source) {
throw UserSourcesServiceError.SourceNotFound;
}
await this.userSourceRepository.remove(source);
return this.userService.findOne(userId);
}
async updateUserSource(
user: User,
sourceId: string,
sourceDto: CreateSourceDto,
callerUser: User = user,
): Promise<User> {
if (!callerUser.isAdmin || callerUser.id !== user.id) {
throw UserSourcesServiceError.Unauthorized;
}
let source = await this.findUserSource(user.id, sourceId);
// Create new if doesn't exist
if (!source) {
source = new MediaSource();
source.user = user;
source.id = sourceId;
source.adminControlled = false;
}
// Check for unauthorized access
if (source.adminControlled && !callerUser.isAdmin) {
throw UserSourcesServiceError.Unauthorized;
} else if (sourceDto.adminControlled !== undefined && !callerUser.isAdmin) {
throw UserSourcesServiceError.Unauthorized;
}
source.adminControlled =
sourceDto.adminControlled ?? source.adminControlled;
source.enabled = sourceDto.enabled ?? source.enabled;
source.pluginSettings = sourceDto.pluginSettings ?? source.pluginSettings;
await this.userSourceRepository.save(source);
return this.userService.findOne(user.id);
}
getSourceSettings(user: User, sourceId: string) {
return user.mediaSources
?.filter((s) => s?.enabled)
?.find((source) => source.id === sourceId)?.pluginSettings;
}
}

View File

@@ -1,6 +1,5 @@
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',
@@ -57,25 +56,3 @@ export class UpdateUserDto extends PartialType(
}
export class SignInDto extends PickType(User, ['name', 'password'] as const) {}
export class MovieUserDataDto {
@ApiProperty()
tmdbId: string;
@ApiProperty()
inLibrary: boolean;
@ApiProperty({ type: PlayStateDto, required: false })
playState?: PlayStateDto;
}
export class SeriesUserDataDto {
@ApiProperty()
tmdbId: string;
@ApiProperty()
inLibrary: boolean;
@ApiProperty({ type: [PlayStateDto] })
playStates: PlayStateDto[];
}

View File

@@ -1,16 +1,8 @@
import {
Column,
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';
import { MediaSource } from 'src/media-sources/media-source.entity';
import { LibraryItem } from 'src/user-data/library/library.entity';
import { PlayState } from 'src/user-data/play-state/play-state.entity';
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
export class SonarrSettings {
@ApiProperty({ required: true })

View File

@@ -1,14 +1,8 @@
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';
import { User } from './user.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 = [
{
@@ -16,21 +10,4 @@ export const userProviders = [
useFactory: (dataSource: DataSource) => dataSource.getRepository(User),
inject: [DATA_SOURCE],
},
{
provide: USER_SOURCE_REPOSITORY,
useFactory: (dataSource: DataSource) =>
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],
},
];

View File

@@ -1,3 +1,4 @@
import { ApiException } from '@nanogiants/nestjs-swagger-api-exception-decorator';
import {
BadRequestException,
Body,
@@ -7,41 +8,25 @@ import {
InternalServerErrorException,
NotFoundException,
Param,
ParseIntPipe,
Post,
Put,
UnauthorizedException,
UseGuards,
} from '@nestjs/common';
import { UserServiceError, UsersService } from './users.service';
import {
UserAccessControl,
GetAuthUser,
OptionalAccessControl,
} from '../auth/auth.guard';
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
import {
CreateUserDto,
MovieUserDataDto,
SeriesUserDataDto,
UpdateUserDto,
UserDto,
} from './user.dto';
GetAuthUser,
OptionalAccessControl,
UserAccessControl,
} from '../auth/auth.guard';
import { CreateUserDto, 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';
import { PlayStateService } from './play-state/play-state.service';
import { PlayState } from './play-state/play-state.entity';
import { PlayStateDto } from './play-state/play-state.dto';
import { UserServiceError, UsersService } from './users.service';
@ApiTags('users')
@Controller('users')
export class UsersController {
constructor(
private usersService: UsersService,
private libraryService: LibraryService,
private playStateService: PlayStateService,
) {}
constructor(private usersService: UsersService) {}
// @UseGuards(AuthGuard)
// @Get()
@@ -161,76 +146,4 @@ export class UsersController {
await this.usersService.remove(id);
}
@UseGuards(UserAccessControl)
@Get(':userId/user-data/movie/tmdb/:tmdbId')
@ApiOkResponse({
description: 'User movie data found',
type: MovieUserDataDto,
})
async getMovieUserData(
@Param('userId') userId: string,
@Param('tmdbId') tmdbId: string,
): Promise<MovieUserDataDto> {
const libraryItem = await this.libraryService.findByTmdbId(userId, tmdbId);
const playState = await this.playStateService.findMoviePlayState(
userId,
tmdbId,
);
return {
tmdbId,
inLibrary: !!libraryItem,
playState: playState,
};
}
@UseGuards(UserAccessControl)
@Get(':userId/user-data/series/tmdb/:tmdbId')
@ApiOkResponse({
description: 'User series data found',
type: SeriesUserDataDto,
})
async getSeriesUserData(
@Param('userId') userId: string,
@Param('tmdbId') tmdbId: string,
): Promise<SeriesUserDataDto> {
const libraryItem = await this.libraryService.findByTmdbId(userId, tmdbId);
const playState = await this.playStateService.findSeriesPlayStates(
userId,
tmdbId,
);
return {
tmdbId,
inLibrary: !!libraryItem,
playStates: playState,
};
}
@UseGuards(UserAccessControl)
@Get(':userId/user-data/series/tmdb/:tmdbId/season/:season/episode/:episode')
@ApiOkResponse({
description: 'User series data found',
type: MovieUserDataDto,
})
async getEpisodeUserData(
@Param('userId') userId: string,
@Param('tmdbId') tmdbId: string,
@Param('season', ParseIntPipe) season: number,
@Param('episode', ParseIntPipe) episode: number,
): Promise<MovieUserDataDto> {
const libraryItem = await this.libraryService.findByTmdbId(userId, tmdbId);
const playState = await this.playStateService
.findSeriesPlayStates(userId, tmdbId, season, episode)
.then((states) =>
states.find((s) => s.season === season && s.episode === episode),
);
return {
tmdbId,
inLibrary: !!libraryItem,
playState: playState,
};
}
}

View File

@@ -1,36 +1,13 @@
import { forwardRef, Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { SourceProvidersModule } from 'src/source-providers/source-providers.module';
import { userProviders } from './user.providers';
import { UsersController } from './users.controller';
import { DatabaseModule } from '../database/database.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';
import { SourcePluginsModule } from 'src/source-plugins/source-plugins.module';
import { MetadataModule } from 'src/metadata/metadata.module';
import { UsersService } from './users.service';
@Module({
imports: [
DatabaseModule,
forwardRef(() => SourcePluginsModule),
forwardRef(() => MetadataModule),
],
providers: [
...userProviders,
UsersService,
UserSourcesService,
LibraryService,
PlayStateService,
],
controllers: [
UsersController,
UserSourcesController,
LibraryController,
PlayStateController,
],
exports: [UsersService, UserSourcesService, LibraryService],
imports: [forwardRef(() => SourceProvidersModule)],
providers: [...userProviders, UsersService],
controllers: [UsersController],
exports: [UsersService],
})
export class UsersModule {}

View File

@@ -3,7 +3,7 @@ import { Repository } from 'typeorm';
import { User } from './user.entity';
import { CreateUserDto, UpdateUserDto } from './user.dto';
import { USER_REPOSITORY } from './user.providers';
import { SourcePluginsService } from 'src/source-plugins/source-plugins.service';
import { SourceProvidersService } from 'src/source-providers/source-providers.service';
export enum UserServiceError {
PasswordMismatch = 'PasswordMismatch',
@@ -16,8 +16,8 @@ export class UsersService {
constructor(
@Inject(USER_REPOSITORY)
private readonly userRepository: Repository<User>,
@Inject(forwardRef(() => SourcePluginsService))
private readonly sourcePluginsService: SourcePluginsService,
@Inject(forwardRef(() => SourceProvidersService))
private readonly sourceProvidersService: SourceProvidersService,
) {}
// Finds
@@ -143,12 +143,12 @@ export class UsersService {
}
private async filterMediaSources(user: User): Promise<User> {
const mediaSources = await this.sourcePluginsService.getPlugins();
const providers = await this.sourceProvidersService.getProviders();
return {
...user,
mediaSources: user.mediaSources.filter(
(source) => !!mediaSources[source.id],
(source) => !!providers[source.pluginId],
),
};
}