Skip to content

Commit 0fcbfbe

Browse files
committed
refactor: implement pagination and featured songs retrieval in SongController and SongService, including new PageDto for consistent response structure
1 parent 93f9d84 commit 0fcbfbe

File tree

5 files changed

+219
-17
lines changed

5 files changed

+219
-17
lines changed

apps/backend/src/song/song.controller.ts

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { UPLOAD_CONSTANTS } from '@nbw/config';
2-
import type { UserDocument } from '@nbw/database';
3-
import { PageQueryDTO, SongPreviewDto, SongViewDto, UploadSongDto, UploadSongResponseDto,} from '@nbw/database';
2+
import { PageDto, UserDocument , PageQueryDTO, SongPreviewDto, SongViewDto, UploadSongDto, UploadSongResponseDto, FeaturedSongsDto,} from '@nbw/database';
43
import type { RawBodyRequest } from '@nestjs/common';
54
import { BadRequestException, Body, Controller, Delete, Get, Headers, HttpStatus, Param, Patch, Post, Query, Req, Res, UnauthorizedException, UploadedFile, UseGuards, UseInterceptors,} from '@nestjs/common';
65
import { AuthGuard } from '@nestjs/passport';
@@ -12,7 +11,6 @@ import type { Response } from 'express';
1211
import { FileService } from '@server/file/file.service';
1312
import { GetRequestToken, validateUser } from '@server/lib/GetRequestUser';
1413

15-
import { SongBrowserService } from './song-browser/song-browser.service';
1614
import { SongService } from './song.service';
1715

1816
// Handles public-facing song routes.
@@ -29,7 +27,7 @@ export class SongController {
2927
},
3028
};
3129

32-
constructor( public readonly songService: SongService, public readonly fileService: FileService, public readonly songBrowserService: SongBrowserService,) {}
30+
constructor( public readonly songService: SongService, public readonly fileService: FileService, ) {}
3331

3432
@Get('/')
3533
@ApiOperation({
@@ -95,33 +93,59 @@ export class SongController {
9593
@Query('q') q?: 'featured' | 'recent' | 'categories' | 'random',
9694
@Param('id') id?: string,
9795
@Query('category') category?: string,
98-
): Promise<SongPreviewDto[] | Record<string, number>> {
96+
): Promise<PageDto<SongPreviewDto> | Record<string, number> | FeaturedSongsDto> {
9997
if (q) {
10098
switch (q) {
10199
case 'featured':
102-
return await this.songBrowserService.getRecentSongs(query);
100+
return await this.songService.getFeaturedSongs();
103101
case 'recent':
104-
return await this.songBrowserService.getRecentSongs(query);
102+
return new PageDto<SongPreviewDto>({
103+
content: await this.songService.getRecentSongs( query.page, query.limit, ),
104+
page : query.page,
105+
limit : query.limit,
106+
total : 0,
107+
});
105108
case 'categories':
106109
if (id) {
107-
return await this.songBrowserService.getSongsByCategory(id, query);
110+
return new PageDto<SongPreviewDto>({
111+
content: await this.songService.getSongsByCategory(
112+
category,
113+
query.page,
114+
query.limit,
115+
),
116+
page : query.page,
117+
limit: query.limit,
118+
total: 0,
119+
});
108120
}
109-
return await this.songBrowserService.getCategories();
121+
return await this.songService.getCategories();
110122
case 'random': {
111123
if (query.limit && (query.limit < 1 || query.limit > 10)) {
112124
throw new BadRequestException('Invalid query parameters');
113125
}
114-
return await this.songBrowserService.getRandomSongs(
126+
const data = await this.songService.getRandomSongs(
115127
query.limit ?? 1,
116128
category,
117129
);
130+
return new PageDto<SongPreviewDto>({
131+
content: data,
132+
page : query.page,
133+
limit : query.limit,
134+
total : data.length,
135+
});
118136
}
119137
default:
120138
throw new BadRequestException('Invalid query parameters');
121139
}
122140
}
123141

124-
return await this.songService.getSongByPage(query);
142+
const data = await this.songService.getSongByPage(query);
143+
return new PageDto<SongPreviewDto>({
144+
content: data,
145+
page : query.page,
146+
limit : query.limit,
147+
total : data.length,
148+
});
125149
}
126150

127151
@Get('/search')
@@ -131,8 +155,14 @@ export class SongController {
131155
public async searchSongs(
132156
@Query() query: PageQueryDTO,
133157
@Query('q') q: string,
134-
): Promise<SongPreviewDto[]> {
135-
return await this.songService.searchSongs(query, q ?? '');
158+
): Promise<PageDto<SongPreviewDto>> {
159+
const data = await this.songService.searchSongs(query, q ?? '');
160+
return new PageDto<SongPreviewDto>({
161+
content: data,
162+
page : query.page,
163+
limit : query.limit,
164+
total : data.length,
165+
});
136166
}
137167

138168
@Get('/:id')

apps/backend/src/song/song.service.spec.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1047,4 +1047,30 @@ describe('SongService', () => {
10471047
expect(mockFind.exec).toHaveBeenCalled();
10481048
});
10491049
});
1050+
1051+
describe('getFeaturedSongs', () => {
1052+
it('should return featured songs', async () => {
1053+
const songWithUser: SongWithUser = {
1054+
title: 'Test Song',
1055+
uploader: { username: 'testuser', profileImage: 'testimage' },
1056+
stats: {
1057+
duration: 100,
1058+
noteCount: 100,
1059+
},
1060+
} as any;
1061+
1062+
jest
1063+
.spyOn(songService, 'getSongsForTimespan')
1064+
.mockResolvedValue([songWithUser]);
1065+
1066+
jest
1067+
.spyOn(songService, 'getSongsBeforeTimespan')
1068+
.mockResolvedValue([songWithUser]);
1069+
1070+
await service.getFeaturedSongs();
1071+
1072+
expect(songService.getSongsForTimespan).toHaveBeenCalled();
1073+
expect(songService.getSongsBeforeTimespan).toHaveBeenCalled();
1074+
});
1075+
});
10501076
});

apps/backend/src/song/song.service.ts

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { BROWSER_SONGS } from '@nbw/config';
2-
import type { UserDocument } from '@nbw/database';
3-
import {
2+
import { FeaturedSongsDto, TimespanType, UserDocument ,
43
PageQueryDTO,
54
Song as SongEntity,
65
SongPageDto,
@@ -518,7 +517,75 @@ export class SongService {
518517
return songs.map((song) => SongPreviewDto.fromSongDocumentWithUser(song));
519518
}
520519

521-
public async getAllSongs() {
522-
return this.songModel.find({});
520+
public async getFeaturedSongs(): Promise<FeaturedSongsDto> {
521+
const now = new Date(Date.now());
522+
523+
const times: Record<TimespanType, number> = {
524+
hour : new Date(Date.now()).setHours(now.getHours() - 1),
525+
day : new Date(Date.now()).setDate(now.getDate() - 1),
526+
week : new Date(Date.now()).setDate(now.getDate() - 7),
527+
month: new Date(Date.now()).setMonth(now.getMonth() - 1),
528+
year : new Date(Date.now()).setFullYear(now.getFullYear() - 1),
529+
all : new Date(0).getTime(),
530+
};
531+
532+
const songs: Record<TimespanType, SongWithUser[]> = {
533+
hour : [],
534+
day : [],
535+
week : [],
536+
month: [],
537+
year : [],
538+
all : [],
539+
};
540+
541+
for (const [timespan, time] of Object.entries(times)) {
542+
const songPage = await this.getSongsForTimespan(time);
543+
544+
// If the length is 0, send an empty array (no songs available in that timespan)
545+
// If the length is less than the page size, pad it with songs "borrowed"
546+
// from the nearest timestamp, regardless of view count
547+
if (
548+
songPage.length > 0 &&
549+
songPage.length < BROWSER_SONGS.paddedFeaturedPageSize
550+
) {
551+
const missing = BROWSER_SONGS.paddedFeaturedPageSize - songPage.length;
552+
553+
const additionalSongs = await this.getSongsBeforeTimespan(
554+
time,
555+
);
556+
557+
songPage.push(...additionalSongs.slice(0, missing));
558+
}
559+
560+
songs[timespan as TimespanType] = songPage;
561+
}
562+
563+
const featuredSongs = FeaturedSongsDto.create();
564+
565+
featuredSongs.hour = songs.hour.map((song) =>
566+
SongPreviewDto.fromSongDocumentWithUser(song),
567+
);
568+
569+
featuredSongs.day = songs.day.map((song) =>
570+
SongPreviewDto.fromSongDocumentWithUser(song),
571+
);
572+
573+
featuredSongs.week = songs.week.map((song) =>
574+
SongPreviewDto.fromSongDocumentWithUser(song),
575+
);
576+
577+
featuredSongs.month = songs.month.map((song) =>
578+
SongPreviewDto.fromSongDocumentWithUser(song),
579+
);
580+
581+
featuredSongs.year = songs.year.map((song) =>
582+
SongPreviewDto.fromSongDocumentWithUser(song),
583+
);
584+
585+
featuredSongs.all = songs.all.map((song) =>
586+
SongPreviewDto.fromSongDocumentWithUser(song),
587+
);
588+
589+
return featuredSongs;
523590
}
524591
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
import {
3+
IsArray,
4+
IsBoolean,
5+
IsNotEmpty,
6+
IsNumber,
7+
IsOptional,
8+
IsString,
9+
ValidateNested,
10+
} from 'class-validator';
11+
12+
export class PageDto<T> {
13+
@IsNotEmpty()
14+
@IsNumber({
15+
allowNaN : false,
16+
allowInfinity : false,
17+
maxDecimalPlaces: 0,
18+
})
19+
@ApiProperty({
20+
example : 150,
21+
description: 'Total number of items available',
22+
})
23+
total: number;
24+
25+
@IsNotEmpty()
26+
@IsNumber({
27+
allowNaN : false,
28+
allowInfinity : false,
29+
maxDecimalPlaces: 0,
30+
})
31+
@ApiProperty({
32+
example : 1,
33+
description: 'Current page number',
34+
})
35+
page: number;
36+
37+
@IsNotEmpty()
38+
@IsNumber({
39+
allowNaN : false,
40+
allowInfinity : false,
41+
maxDecimalPlaces: 0,
42+
})
43+
@ApiProperty({
44+
example : 20,
45+
description: 'Number of items per page',
46+
})
47+
limit: number;
48+
49+
@IsOptional()
50+
@IsString()
51+
@ApiProperty({
52+
example : 'createdAt',
53+
description: 'Field used for sorting',
54+
required : false,
55+
})
56+
sort?: string;
57+
58+
@IsNotEmpty()
59+
@IsBoolean()
60+
@ApiProperty({
61+
example : false,
62+
description: 'Sort order: true for ascending, false for descending',
63+
})
64+
order: boolean;
65+
66+
@IsNotEmpty()
67+
@IsArray()
68+
@ValidateNested({ each: true })
69+
@ApiProperty({
70+
description: 'Array of items for the current page',
71+
isArray : true,
72+
})
73+
content: T[];
74+
75+
constructor(partial: Partial<PageDto<T>>) {
76+
Object.assign(this, partial);
77+
}
78+
}

packages/database/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export * from './common/dto/Page.dto';
12
export * from './common/dto/PageQuery.dto';
23
export * from './common/dto/types';
34

0 commit comments

Comments
 (0)