Skip to content

Commit fb4fbcb

Browse files
committed
feat: add song search functionality with pagination and sorting, update API routes for Swagger documentation
1 parent b709f6d commit fb4fbcb

File tree

4 files changed

+56
-2
lines changed

4 files changed

+56
-2
lines changed

apps/backend/src/main.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const logger: Logger = new Logger('main.ts');
1010

1111
async function bootstrap() {
1212
const app = await NestFactory.create(AppModule);
13-
app.setGlobalPrefix('api/v1');
13+
app.setGlobalPrefix('v1');
1414

1515
const parseTokenPipe = app.get<ParseTokenPipe>(ParseTokenPipe);
1616

@@ -56,7 +56,7 @@ bootstrap()
5656
logger.warn(`Application is running on: http://localhost:${port}`);
5757

5858
if (process.env.NODE_ENV === 'development') {
59-
logger.warn(`Swagger is running on: http://localhost:${port}/api/doc`);
59+
logger.warn(`Swagger is running on: http://localhost:${port}/docs`);
6060
}
6161
})
6262
.catch((error) => {

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,17 @@ export class SongController {
7676
return await this.songService.getSongByPage(query);
7777
}
7878

79+
@Get('/search')
80+
@ApiOperation({
81+
summary: 'Search songs by keywords with pagination and sorting',
82+
})
83+
public async searchSongs(
84+
@Query() query: PageQueryDTO,
85+
@Query('q') q: string,
86+
): Promise<SongPreviewDto[]> {
87+
return await this.songService.searchSongs(query, q ?? '');
88+
}
89+
7990
@Get('/:id')
8091
@ApiOperation({ summary: 'Get song info by ID' })
8192
public async getSong(

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

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,48 @@ export class SongService {
202202
})
203203
.skip(page * limit - limit)
204204
.limit(limit)
205+
.populate('uploader', 'username publicName profileImage -_id')
206+
.exec()) as unknown as SongWithUser[];
207+
208+
return songs.map((song) => SongPreviewDto.fromSongDocumentWithUser(song));
209+
}
210+
211+
public async searchSongs(
212+
query: PageQueryDTO,
213+
q: string,
214+
): Promise<SongPreviewDto[]> {
215+
const page = parseInt(query.page?.toString() ?? '1');
216+
const limit = parseInt(query.limit?.toString() ?? '10');
217+
const order = query.order ? query.order : false;
218+
const allowedSorts = new Set(['likeCount', 'createdAt', 'playCount']);
219+
const sortField = allowedSorts.has(query.sort ?? '')
220+
? (query.sort as string)
221+
: 'createdAt';
222+
223+
const terms = (q || '')
224+
.split(/\s+/)
225+
.map((t) => t.trim())
226+
.filter((t) => t.length > 0);
227+
228+
// Build Google-like search: all words must appear across any of the fields
229+
const andClauses = terms.map((word) => ({
230+
$or: [
231+
{ title: { $regex: word, $options: 'i' } },
232+
{ originalAuthor: { $regex: word, $options: 'i' } },
233+
{ description: { $regex: word, $options: 'i' } },
234+
],
235+
}));
236+
237+
const mongoQuery: any = {
238+
visibility: 'public',
239+
...(andClauses.length > 0 ? { $and: andClauses } : {}),
240+
};
241+
242+
const songs = (await this.songModel
243+
.find(mongoQuery)
244+
.sort({ [sortField]: order ? 1 : -1 })
245+
.skip(limit * (page - 1))
246+
.limit(limit)
205247
.populate('uploader', 'username profileImage -_id')
206248
.exec()) as unknown as SongWithUser[];
207249

packages/database/src/song/dto/SongView.dto.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type { CategoryType, LicenseType, VisibilityType } from './types';
1414

1515
export type SongViewUploader = {
1616
username: string;
17+
publicName: string;
1718
profileImage: string;
1819
};
1920

0 commit comments

Comments
 (0)