77 Get ,
88 Headers ,
99 HttpStatus ,
10+ Logger ,
1011 Param ,
1112 Patch ,
1213 Post ,
@@ -26,8 +27,6 @@ import {
2627 ApiBody ,
2728 ApiConsumes ,
2829 ApiOperation ,
29- ApiParam ,
30- ApiQuery ,
3130 ApiResponse ,
3231 ApiTags ,
3332} from '@nestjs/swagger' ;
@@ -41,8 +40,11 @@ import {
4140 UploadSongDto ,
4241 UploadSongResponseDto ,
4342 PageDto ,
43+ SongListQueryDTO ,
44+ SongSortType ,
45+ FeaturedSongsDto ,
4446} from '@nbw/database' ;
45- import type { FeaturedSongsDto , UserDocument } from '@nbw/database' ;
47+ import type { UserDocument } from '@nbw/database' ;
4648import { FileService } from '@server/file/file.service' ;
4749import { GetRequestToken , validateUser } from '@server/lib/GetRequestUser' ;
4850
@@ -51,6 +53,7 @@ import { SongService } from './song.service';
5153@Controller ( 'song' )
5254@ApiTags ( 'song' )
5355export class SongController {
56+ private logger = new Logger ( SongController . name ) ;
5457 static multerConfig : MulterOptions = {
5558 limits : { fileSize : UPLOAD_CONSTANTS . file . maxSize } ,
5659 fileFilter : ( req , file , cb ) => {
@@ -67,147 +70,128 @@ export class SongController {
6770
6871 @Get ( '/' )
6972 @ApiOperation ( {
70- summary : 'Get songs with various filtering and browsing options' ,
73+ summary : 'Get songs with filtering and sorting options' ,
7174 description : `
72- Retrieves songs based on the provided query parameters. Supports multiple modes:
73-
74- **Default mode** (no 'q' parameter): Returns paginated songs with sorting/filtering
75-
76- **Special query modes** (using 'q' parameter):
77- - \`featured\`: Get recent popular songs with pagination
78- - \`recent\`: Get recently uploaded songs with pagination
79- - \`categories\`:
80- - Without 'id': Returns a record of available categories and their song counts
81- - With 'id': Returns songs from the specified category with pagination
82- - \`random\`: Returns random songs (requires 'count' parameter, 1-10 songs, optionally filtered by 'category')
75+ Retrieves songs based on the provided query parameters.
8376
8477 **Query Parameters:**
85- - Standard pagination/sorting via PageQueryDTO (page, limit, sort, order, timespan)
86- - \`q\`: Special query mode ('featured', 'recent', 'categories', 'random')
87- - \`id\`: Category ID (used with q=categories to get songs from specific category)
88- - \`count\`: Number of random songs to return (1-10, used with q=random)
89- - \`category\`: Category filter for random songs (used with q=random)
78+ - \`q\`: Search string to filter songs by title or description (optional)
79+ - \`sort\`: Sort songs by criteria (recent, random, play-count, title, duration, note-count)
80+ - \`order\`: Sort order (asc, desc) - only applies if sort is not random
81+ - \`category\`: Filter by category - if left empty, returns songs in any category
82+ - \`uploader\`: Filter by uploader username - if provided, will only return songs uploaded by that user
83+ - \`page\`: Page number (default: 1)
84+ - \`limit\`: Number of items to return per page (default: 10)
9085
91- **Return Types:**
92- - SongPreviewDto[]: Array of song previews (most cases)
93- - Record<string, number>: Category name to count mapping (when q=categories without id)
86+ **Return Type:**
87+ - PageDto<SongPreviewDto>: Paginated list of song previews
9488 ` ,
9589 } )
96- @ApiQuery ( {
97- name : 'q' ,
98- required : false ,
99- enum : [ 'featured' , 'recent' , 'categories' , 'random' ] ,
100- description :
101- 'Special query mode. If not provided, returns standard paginated song list.' ,
102- example : 'recent' ,
103- } )
104- @ApiParam ( {
105- name : 'id' ,
106- required : false ,
107- type : 'string' ,
108- description :
109- 'Category ID. Only used when q=categories to get songs from a specific category.' ,
110- example : 'pop' ,
111- } )
112- @ApiQuery ( {
113- name : 'count' ,
114- required : false ,
115- type : 'string' ,
116- description :
117- 'Number of random songs to return (1-10). Only used when q=random.' ,
118- example : '5' ,
119- } )
120- @ApiQuery ( {
121- name : 'category' ,
122- required : false ,
123- type : 'string' ,
124- description : 'Category filter for random songs. Only used when q=random.' ,
125- example : 'electronic' ,
126- } )
12790 @ApiResponse ( {
12891 status : 200 ,
129- description :
130- 'Success. Returns either an array of song previews or category counts.' ,
131- schema : {
132- oneOf : [
133- {
134- type : 'array' ,
135- items : { $ref : '#/components/schemas/SongPreviewDto' } ,
136- description :
137- 'Array of song previews (default behavior and most query modes)' ,
138- } ,
139- {
140- type : 'object' ,
141- additionalProperties : { type : 'number' } ,
142- description :
143- 'Category name to song count mapping (only when q=categories without id)' ,
144- example : { pop : 42 , rock : 38 , electronic : 15 } ,
145- } ,
146- ] ,
147- } ,
92+ description : 'Success. Returns paginated list of song previews.' ,
93+ type : PageDto < SongPreviewDto > ,
14894 } )
14995 @ApiResponse ( {
15096 status : 400 ,
151- description :
152- 'Bad Request. Invalid query parameters (e.g., invalid count for random query).' ,
97+ description : 'Bad Request. Invalid query parameters.' ,
15398 } )
15499 public async getSongList (
155- @Query ( ) query : PageQueryDTO ,
156- @Query ( 'q' ) q ?: 'featured' | 'recent' | 'categories' | 'random' ,
157- @Param ( 'id' ) id ?: string ,
158- @Query ( 'category' ) category ?: string ,
159- ) : Promise <
160- PageDto < SongPreviewDto > | Record < string , number > | FeaturedSongsDto
161- > {
162- if ( q ) {
163- switch ( q ) {
164- case 'featured' :
165- return await this . songService . getFeaturedSongs ( ) ;
166- case 'recent' :
167- return new PageDto < SongPreviewDto > ( {
168- content : await this . songService . getRecentSongs (
169- query . page ,
170- query . limit ,
171- ) ,
172- page : query . page ,
173- limit : query . limit ,
174- total : 0 ,
175- } ) ;
176- case 'categories' :
177- if ( id ) {
178- return new PageDto < SongPreviewDto > ( {
179- content : await this . songService . getSongsByCategory (
180- category ,
181- query . page ,
182- query . limit ,
183- ) ,
184- page : query . page ,
185- limit : query . limit ,
186- total : 0 ,
187- } ) ;
188- }
189- return await this . songService . getCategories ( ) ;
190- case 'random' : {
191- if ( query . limit && ( query . limit < 1 || query . limit > 10 ) ) {
192- throw new BadRequestException ( 'Invalid query parameters' ) ;
193- }
194- const data = await this . songService . getRandomSongs (
195- query . limit ?? 1 ,
196- category ,
197- ) ;
198- return new PageDto < SongPreviewDto > ( {
199- content : data ,
200- page : query . page ,
201- limit : query . limit ,
202- total : data . length ,
203- } ) ;
204- }
205- default :
206- throw new BadRequestException ( 'Invalid query parameters' ) ;
100+ @Query ( ) query : SongListQueryDTO ,
101+ ) : Promise < PageDto < SongPreviewDto > > {
102+ // Handle search query
103+ if ( query . q ) {
104+ const sortFieldMap = new Map ( [
105+ [ SongSortType . RECENT , 'createdAt' ] ,
106+ [ SongSortType . PLAY_COUNT , 'playCount' ] ,
107+ [ SongSortType . TITLE , 'title' ] ,
108+ [ SongSortType . DURATION , 'duration' ] ,
109+ [ SongSortType . NOTE_COUNT , 'noteCount' ] ,
110+ ] ) ;
111+
112+ const sortField = sortFieldMap . get ( query . sort ) ?? 'createdAt' ;
113+
114+ const pageQuery = new PageQueryDTO ( {
115+ page : query . page ,
116+ limit : query . limit ,
117+ sort : sortField ,
118+ order : query . order === 'desc' ? false : true ,
119+ } ) ;
120+ const data = await this . songService . searchSongs ( pageQuery , query . q ) ;
121+ return new PageDto < SongPreviewDto > ( {
122+ content : data ,
123+ page : query . page ,
124+ limit : query . limit ,
125+ total : data . length ,
126+ } ) ;
127+ }
128+
129+ // Handle random sort
130+ if ( query . sort === SongSortType . RANDOM ) {
131+ if ( query . limit && ( query . limit < 1 || query . limit > 10 ) ) {
132+ throw new BadRequestException (
133+ 'Limit must be between 1 and 10 for random sort' ,
134+ ) ;
207135 }
136+ const data = await this . songService . getRandomSongs (
137+ query . limit ?? 1 ,
138+ query . category ,
139+ ) ;
140+
141+ return new PageDto < SongPreviewDto > ( {
142+ content : data ,
143+ page : query . page ,
144+ limit : query . limit ,
145+ total : data . length ,
146+ } ) ;
147+ }
148+
149+ // Handle recent sort
150+ if ( query . sort === SongSortType . RECENT ) {
151+ const data = await this . songService . getRecentSongs (
152+ query . page ,
153+ query . limit ,
154+ ) ;
155+ return new PageDto < SongPreviewDto > ( {
156+ content : data ,
157+ page : query . page ,
158+ limit : query . limit ,
159+ total : data . length ,
160+ } ) ;
161+ }
162+
163+ // Handle category filter
164+ if ( query . category ) {
165+ const data = await this . songService . getSongsByCategory (
166+ query . category ,
167+ query . page ,
168+ query . limit ,
169+ ) ;
170+ return new PageDto < SongPreviewDto > ( {
171+ content : data ,
172+ page : query . page ,
173+ limit : query . limit ,
174+ total : data . length ,
175+ } ) ;
208176 }
209177
210- const data = await this . songService . getSongByPage ( query ) ;
178+ // Default: get songs with standard pagination
179+ const sortFieldMap = new Map ( [
180+ [ SongSortType . PLAY_COUNT , 'playCount' ] ,
181+ [ SongSortType . TITLE , 'title' ] ,
182+ [ SongSortType . DURATION , 'duration' ] ,
183+ [ SongSortType . NOTE_COUNT , 'noteCount' ] ,
184+ ] ) ;
185+
186+ const sortField = sortFieldMap . get ( query . sort ) ?? 'createdAt' ;
187+
188+ const pageQuery = new PageQueryDTO ( {
189+ page : query . page ,
190+ limit : query . limit ,
191+ sort : sortField ,
192+ order : query . order === 'desc' ? false : true ,
193+ } ) ;
194+ const data = await this . songService . getSongByPage ( pageQuery ) ;
211195 return new PageDto < SongPreviewDto > ( {
212196 content : data ,
213197 page : query . page ,
@@ -216,6 +200,42 @@ export class SongController {
216200 } ) ;
217201 }
218202
203+ @Get ( '/featured' )
204+ @ApiOperation ( {
205+ summary : 'Get featured songs' ,
206+ description : `
207+ Returns featured songs with specific logic for showcasing popular/recent content.
208+ This endpoint has very specific business logic and is separate from the general song listing.
209+ ` ,
210+ } )
211+ @ApiResponse ( {
212+ status : 200 ,
213+ description : 'Success. Returns featured songs data.' ,
214+ type : FeaturedSongsDto ,
215+ } )
216+ public async getFeaturedSongs ( ) : Promise < FeaturedSongsDto > {
217+ return await this . songService . getFeaturedSongs ( ) ;
218+ }
219+
220+ @Get ( '/categories' )
221+ @ApiOperation ( {
222+ summary : 'Get available categories with song counts' ,
223+ description :
224+ 'Returns a record of available categories and their song counts.' ,
225+ } )
226+ @ApiResponse ( {
227+ status : 200 ,
228+ description : 'Success. Returns category name to count mapping.' ,
229+ schema : {
230+ type : 'object' ,
231+ additionalProperties : { type : 'number' } ,
232+ example : { pop : 42 , rock : 38 , electronic : 15 } ,
233+ } ,
234+ } )
235+ public async getCategories ( ) : Promise < Record < string , number > > {
236+ return await this . songService . getCategories ( ) ;
237+ }
238+
219239 @Get ( '/search' )
220240 @ApiOperation ( {
221241 summary : 'Search songs by keywords with pagination and sorting' ,
0 commit comments