feat: add metadata to libraryItems, server side library sorting and filtering

This commit is contained in:
Aleksi Lassila
2025-03-26 18:22:25 +02:00
parent fc4581ba41
commit 5200b9f805
13 changed files with 590 additions and 270 deletions

View File

@@ -21,7 +21,12 @@ import {
SuccessResponseDto,
} from 'src/common/common.dto';
import { LibraryItemDto } from './library.dto';
import { LibraryService } from './library.service';
import {
LibraryService,
LibrarySortBy,
MyListFilter as MyListFilter,
SortByDirection,
} from './library.service';
@ApiTags('users')
@Controller('users/:userId/library')
@@ -29,25 +34,32 @@ import { LibraryService } from './library.service';
export class LibraryController {
constructor(private libraryService: LibraryService) {}
@Get()
@Get('my-list')
@ApiQuery({ name: 'filter', enum: MyListFilter, required: false })
@ApiQuery({ name: 'sortBy', enum: LibrarySortBy, required: false })
@ApiQuery({ name: 'direction', enum: SortByDirection, required: false })
@PaginatedApiOkResponse(LibraryItemDto)
async getLibraryItems(
@GetPaginationParams() pagination: PaginationParamsDto,
@Param('userId') userId: string,
@Query('filter', new ParseEnumPipe(MyListFilter, { optional: true }))
filter?: MyListFilter,
@Query('sortBy', new ParseEnumPipe(LibrarySortBy, { optional: true }))
sortBy?: LibrarySortBy,
@Query('direction', new ParseEnumPipe(SortByDirection, { optional: true }))
direction?: SortByDirection,
): Promise<PaginatedResponseDto<LibraryItemDto>> {
// const user = await this.userService.findOne(userId);
const items = await this.libraryService.getLibraryItemDtos(
const items = await this.libraryService.getMyListDtos({
userId,
pagination,
);
filter,
sortBy,
direction,
});
return {
items,
itemsPerPage: pagination.itemsPerPage,
page: pagination.page,
total: items.length,
};
return items;
}
@Put('tmdb/:tmdbId')

View File

@@ -15,6 +15,7 @@ import {
} from 'typeorm';
import { PlayState } from '../play-state/play-state.entity';
import { PlayStateDto } from '../play-state/play-state.dto';
import { MovieMetadata, SeriesMetadata } from 'src/metadata/metadata.entity';
@Entity()
@Unique(['tmdbId', 'userId'])
@@ -31,6 +32,22 @@ export class LibraryItem {
@Column()
mediaType: MediaType;
@ApiProperty({ required: false, type: MovieMetadata })
@ManyToOne(() => MovieMetadata, {
createForeignKeyConstraints: false,
nullable: true,
})
@JoinColumn({ name: 'tmdbId', referencedColumnName: 'tmdbId' })
movieMetadata?: MovieMetadata;
@ApiProperty({ required: false, type: SeriesMetadata })
@ManyToOne(() => SeriesMetadata, {
createForeignKeyConstraints: false,
nullable: true,
})
@JoinColumn({ name: 'tmdbId', referencedColumnName: 'tmdbId' })
seriesMetadata?: SeriesMetadata;
@ApiProperty({ required: true, type: 'string' })
@PrimaryColumn()
userId: string;

View File

@@ -1,11 +1,39 @@
import { Inject, Injectable } from '@nestjs/common';
import { MediaType, PaginationParamsDto } from 'src/common/common.dto';
import {
MediaType,
PaginatedResponseDto,
PaginationParamsDto,
} from 'src/common/common.dto';
import { MetadataService } from 'src/metadata/metadata.service';
import { Repository } from 'typeorm';
import { IsNull, Not, Repository } from 'typeorm';
import { LibraryItemDto } from './library.dto';
import { LibraryItem } from './library.entity';
import { USER_LIBRARY_REPOSITORY } from './library.providers';
export enum SortByDirection {
Asc = 'asc',
Desc = 'desc',
}
export enum LibrarySortBy {
DateAdded = 'dateAdded',
Name = 'name',
FirstReleaseDate = 'firstReleaseDate',
LastReleaseDate = 'lastReleaseDate',
}
export enum MyListFilter {
Movie = 'movie',
Series = 'series',
}
export enum CatalogueFilter {
All = 'all',
Movies = 'movies',
Series = 'series',
Unavailable = 'unavailable',
}
@Injectable()
export class LibraryService {
constructor(
@@ -14,14 +42,13 @@ export class LibraryService {
private readonly metadataService: MetadataService,
) {}
async getLibraryItemDtos(
userId: string,
pagination: PaginationParamsDto,
): Promise<LibraryItemDto[]> {
const items = await this.getLibraryItems(userId, pagination);
async getMyListDtos(
...args: Parameters<LibraryService['getMyList']>
): Promise<PaginatedResponseDto<LibraryItemDto>> {
const paginatedItems = await this.getMyList(...args);
return Promise.all(
items.map(async (item) => {
const items = await Promise.all(
paginatedItems.items.map(async (item) => {
const seriesMetadata =
item.mediaType === MediaType.Series
? await this.metadataService.getSeriesByTmdbId(item.tmdbId)
@@ -38,21 +65,95 @@ export class LibraryService {
});
}),
);
return { ...paginatedItems, items };
}
private async getLibraryItems(
userId: string,
pagination: PaginationParamsDto,
): Promise<LibraryItem[]> {
return this.libraryRepository.find({
where: { userId },
/** TODO: decouple librayItem and movie/seriesItem */
private async getMyList(options: {
userId: string;
pagination: PaginationParamsDto;
filter?: MyListFilter;
sortBy?: LibrarySortBy;
direction?: SortByDirection;
}): Promise<PaginatedResponseDto<LibraryItem>> {
const {
userId,
pagination,
filter,
sortBy,
direction = SortByDirection.Desc,
} = options;
const directon = direction === SortByDirection.Asc ? 'ASC' : 'DESC';
const order = {
[LibrarySortBy.DateAdded]: { createdAt: directon } as const,
[LibrarySortBy.Name]: {
seriesMetadata: {
name: directon,
},
movieMetadata: {
name: directon,
},
} as const,
[LibrarySortBy.FirstReleaseDate]: {
movieMetadata: {
releaseDate: directon,
},
seriesMetadata: {
firstReleaseDate: directon,
},
} as const,
[LibrarySortBy.LastReleaseDate]: {
movieMetadata: {
releaseDate: directon,
},
seriesMetadata: {
lastReleaseDate: directon,
},
} as const,
}[sortBy];
const mediaType = filter
? filter === MyListFilter.Movie
? MediaType.Movie
: MediaType.Series
: undefined;
const [items, total] = await this.libraryRepository.findAndCount({
relations: {
playStates: true,
seriesMetadata: true,
movieMetadata: true,
},
// TODO: Implement pagination
// take: pagination.itemsPerPage,
// skip: pagination.itemsPerPage * (pagination.page - 1),
select: {
seriesMetadata: {
firstReleaseDate: true,
lastReleaseDate: true,
name: true,
},
movieMetadata: {
releaseDate: true,
name: true,
},
},
where: {
userId,
...(mediaType ? { mediaType } : {}),
},
order,
take: pagination.itemsPerPage,
skip: pagination.itemsPerPage * (pagination.page - 1),
});
return {
items,
total,
itemsPerPage: pagination.itemsPerPage,
page: pagination.page,
};
}
async findByTmdbId(