feat: library page infinite scroll

This commit is contained in:
Aleksi Lassila
2025-03-30 00:04:42 +02:00
parent 3c775492ea
commit eed45c36b0
12 changed files with 365 additions and 173 deletions

View File

@@ -4,11 +4,16 @@ import {
ExecutionContext,
Type,
} from '@nestjs/common';
import { PaginatedResponseDto, PaginationParamsDto } from './common.dto';
import { ApiExtraModels, ApiOkResponse, getSchemaPath } from '@nestjs/swagger';
import { PaginatedResponseDto, PaginationDto } from './common.dto';
import {
ApiExtraModels,
ApiOkResponse,
ApiQuery,
getSchemaPath,
} from '@nestjs/swagger';
export const GetPaginationParams = createParamDecorator(
(data: number | undefined, ctx: ExecutionContext): PaginationParamsDto => {
(data: number | undefined, ctx: ExecutionContext): PaginationDto => {
const request = ctx.switchToHttp().getRequest();
const page = parseInt(request.query.page, 10) || 1;
const itemsPerPage = parseInt(request.query.itemsPerPage, 10) || data || 50;
@@ -20,6 +25,20 @@ export const GetPaginationParams = createParamDecorator(
},
);
export const PaginationApiQuery = () =>
applyDecorators(
ApiQuery({
name: 'page',
required: false,
type: 'number',
}),
ApiQuery({
name: 'itemsPerPage',
required: false,
type: 'number',
}),
);
export const PaginatedApiOkResponse = <GenericType extends Type<unknown>>(
data: GenericType,
) =>

View File

@@ -35,7 +35,7 @@ export class PaginatedResponseDto<T> implements PaginatedResponse<T> {
items: T[];
}
export class PaginationParamsDto implements PaginationParams {
export class PaginationDto implements PaginationParams {
@ApiProperty()
page: number;

View File

@@ -12,13 +12,14 @@ import {
import { ApiOkResponse, ApiQuery, ApiTags } from '@nestjs/swagger';
import { GetAuthToken, UserAccessControl } from 'src/auth/auth.guard';
import {
GetPaginationParams,
GetPaginationParams as GetPaginationQuery,
PaginatedApiOkResponse,
PaginationApiQuery,
} from 'src/common/common.decorator';
import {
MediaType,
PaginatedResponseDto,
PaginationParamsDto,
PaginationDto,
SuccessResponseDto,
} from 'src/common/common.dto';
import {
@@ -42,9 +43,10 @@ export class LibraryController {
@ApiQuery({ name: 'type', enum: MyListTypeFilter, required: false })
@ApiQuery({ name: 'order', enum: MyListOrder, required: false })
@ApiQuery({ name: 'direction', enum: OrderDirection, required: false })
@PaginationApiQuery()
@PaginatedApiOkResponse(LibraryItemDto)
async getMyList(
@GetPaginationParams() pagination: PaginationParamsDto,
@GetPaginationQuery() pagination: PaginationDto,
@Param('userId') userId: string,
@Query('status', new ParseEnumPipe(MyListStatusFilter, { optional: true }))
status?: MyListStatusFilter,
@@ -78,9 +80,10 @@ export class LibraryController {
@ApiQuery({ name: 'type', enum: CatalogueTypeFilter, required: false })
@ApiQuery({ name: 'order', required: false })
@ApiQuery({ name: 'direction', required: false })
@PaginationApiQuery()
@PaginatedApiOkResponse(LibraryItemDto)
async getCatalogue(
@GetPaginationParams() pagination: PaginationParamsDto,
@GetPaginationQuery() pagination: PaginationDto,
@Param('userId') userId: string,
@Param('sourceId') sourceId: string,
@GetAuthToken() token: string,
@@ -91,7 +94,7 @@ export class LibraryController {
@Query('direction')
direction?: string,
): Promise<PaginatedResponseDto<LibraryItemDto>> {
const items = this.libraryService.getCatalogueItems({
const items = await this.libraryService.getCatalogueItems({
sourceId,
token,
pagination,

View File

@@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import {
MediaType,
PaginatedResponseDto,
PaginationParamsDto,
PaginationDto,
} from 'src/common/common.dto';
import { MetadataService } from 'src/metadata/metadata.service';
import { MediaSourcesService } from 'src/users/media-sources/media-sources.service';
@@ -42,7 +42,7 @@ export class LibraryService {
/** TODO: decouple librayItem and movie/seriesItem */
async getMyList(options: {
userId: string;
pagination: PaginationParamsDto;
pagination: PaginationDto;
type?: MyListTypeFilter;
status?: MyListStatusFilter;
order?: MyListOrder;
@@ -244,7 +244,7 @@ export class LibraryService {
async getCatalogueItems(options: {
sourceId: string;
token: string;
pagination: PaginationParamsDto;
pagination: PaginationDto;
type?: CatalogueTypeFilter;
order?: string;
direction?: string;
@@ -260,7 +260,12 @@ export class LibraryService {
const connection = await this.mediaSourceService.getConnection(sourceId);
if (!connection) return;
if (!connection) {
console.error(
`No connection found for sourceId: ${sourceId}. Please check your media source configuration.`,
);
throw new Error('No connection found');
}
const combined = connection.provider.catalogueProvider.getCatalogue;
const movies = connection.provider.catalogueProvider.getMovieCatalogue;
@@ -360,6 +365,10 @@ export class LibraryService {
),
};
}
throw new Error(
`No catalogue provider found for type: ${type}. Please check your media source configuration.`,
);
}
async findByTmdbId(

View File

@@ -1,6 +1,10 @@
import { forwardRef, Inject, Injectable } from '@nestjs/common';
import {
SourceProvider,
ValidationResponse,
} from '@aleksilassila/reiverr-plugin';
import { Inject, Injectable } from '@nestjs/common';
import { SourceProvidersService } from 'src/source-providers/source-providers.service';
import { User } from 'src/users/user.entity';
import { UsersService } from 'src/users/users.service';
import { Repository } from 'typeorm';
import {
MediaSourceDto,
@@ -8,12 +12,6 @@ import {
} from './media-source.dto';
import { MediaSource } from './media-source.entity';
import { MEIDA_SOURCE_REPOSITORY } from './media-source.providers';
import { SourceProvidersService } from 'src/source-providers/source-providers.service';
import {
SourceProvider,
ValidationResponse,
} from '@aleksilassila/reiverr-plugin';
import { PaginationParamsDto } from 'src/common/common.dto';
export enum MediaSourcesServiceError {
SourceNotFound = 'SourceNotFound',