From e481982a23a61891fdef780b4cd025b0b821909f Mon Sep 17 00:00:00 2001 From: Adeyemi Gbenga Date: Tue, 18 Feb 2025 22:43:38 +0100 Subject: [PATCH] feat: song lyrics resource --- backend/src/app.module.ts | 2 + backend/src/songs/dto/create-song.dto.ts | 45 +++++++++++++ backend/src/songs/dto/update-song.dto.ts | 4 ++ backend/src/songs/entities/song.entity.ts | 40 ++++++++++++ backend/src/songs/songs.controller.spec.ts | 20 ++++++ backend/src/songs/songs.controller.ts | 66 +++++++++++++++++++ backend/src/songs/songs.module.ts | 14 +++++ backend/src/songs/songs.service.spec.ts | 18 ++++++ backend/src/songs/songs.service.ts | 73 ++++++++++++++++++++++ 9 files changed, 282 insertions(+) create mode 100644 backend/src/songs/dto/create-song.dto.ts create mode 100644 backend/src/songs/dto/update-song.dto.ts create mode 100644 backend/src/songs/entities/song.entity.ts create mode 100644 backend/src/songs/songs.controller.spec.ts create mode 100644 backend/src/songs/songs.controller.ts create mode 100644 backend/src/songs/songs.module.ts create mode 100644 backend/src/songs/songs.service.spec.ts create mode 100644 backend/src/songs/songs.service.ts diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index e1311caf..eab4e7ee 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -17,6 +17,7 @@ import { AccessTokenGuard } from './auth/guard/access-token/access-token.guard'; import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core'; import { ConfigModule } from './config/config.module'; import { GlobalInterceptor } from './interceptors/global.interceptor'; +import { SongsModule } from './songs/songs.module'; @Module({ imports: [ @@ -37,6 +38,7 @@ import { GlobalInterceptor } from './interceptors/global.interceptor'; autoLoadEntities: true, synchronize: process.env.NODE_ENV === 'development', }), + SongsModule, ], controllers: [AppController], providers: [ diff --git a/backend/src/songs/dto/create-song.dto.ts b/backend/src/songs/dto/create-song.dto.ts new file mode 100644 index 00000000..2b866c99 --- /dev/null +++ b/backend/src/songs/dto/create-song.dto.ts @@ -0,0 +1,45 @@ +import { IsNotEmpty, IsString, IsNumber, IsArray, Min, Max, IsOptional } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class CreateSongDto { + @ApiProperty() + @IsNotEmpty() + @IsString() + title: string; + + @ApiProperty() + @IsNotEmpty() + @IsString() + artist: string; + + @ApiProperty() + @IsNotEmpty() + @IsString() + lyrics: string; + + @ApiProperty() + @IsNotEmpty() + @IsString() + genre: string; + + @ApiProperty() + @IsNumber() + @Min(1) + @Max(10) + difficulty: number; + + @ApiProperty() + @IsNumber() + @Min(1900) + releaseYear: number; + + @ApiProperty() + @IsString() + language: string; + + @ApiProperty() + @IsArray() + @IsOptional() + tags: string[]; +} + diff --git a/backend/src/songs/dto/update-song.dto.ts b/backend/src/songs/dto/update-song.dto.ts new file mode 100644 index 00000000..c12be691 --- /dev/null +++ b/backend/src/songs/dto/update-song.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateSongDto } from './create-song.dto'; + +export class UpdateSongDto extends PartialType(CreateSongDto) {} \ No newline at end of file diff --git a/backend/src/songs/entities/song.entity.ts b/backend/src/songs/entities/song.entity.ts new file mode 100644 index 00000000..1c014252 --- /dev/null +++ b/backend/src/songs/entities/song.entity.ts @@ -0,0 +1,40 @@ +import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm'; + +@Entity('songs') +export class Song { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column() + title: string; + + @Column() + artist: string; + + @Column('text') + lyrics: string; + + @Column() + genre: string; + + @Column() + difficulty: number; + + @Column({ default: 0 }) + playCount: number; + + @Column('simple-array', { nullable: true }) + tags: string[]; + + @Column() + releaseYear: number; + + @Column() + language: string; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; +} \ No newline at end of file diff --git a/backend/src/songs/songs.controller.spec.ts b/backend/src/songs/songs.controller.spec.ts new file mode 100644 index 00000000..d2d0bafb --- /dev/null +++ b/backend/src/songs/songs.controller.spec.ts @@ -0,0 +1,20 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { SongsController } from './songs.controller'; +import { SongsService } from './songs.service'; + +describe('SongsController', () => { + let controller: SongsController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [SongsController], + providers: [SongsService], + }).compile(); + + controller = module.get(SongsController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/backend/src/songs/songs.controller.ts b/backend/src/songs/songs.controller.ts new file mode 100644 index 00000000..51e65233 --- /dev/null +++ b/backend/src/songs/songs.controller.ts @@ -0,0 +1,66 @@ +// src/songs/songs.controller.ts +import { Controller, Get, Post, Body, Patch, Param, Delete, Query } from '@nestjs/common'; +import { SongsService } from './songs.service'; +import { CreateSongDto } from './dto/create-song.dto'; +import { UpdateSongDto } from './dto/update-song.dto'; +import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; + +@ApiTags('songs') +@Controller('songs') +export class SongsController { + constructor(private readonly songsService: SongsService) {} + + @Post() + @ApiOperation({ summary: 'Create a new song' }) + create(@Body() createSongDto: CreateSongDto) { + return this.songsService.create(createSongDto); + } + + @Get() + @ApiOperation({ summary: 'Get all songs' }) + findAll() { + return this.songsService.findAll(); + } + + @Get('search') + @ApiOperation({ summary: 'Search songs' }) + search(@Query('q') query: string) { + return this.songsService.searchSongs(query); + } + + @Get('genre/:genre') + @ApiOperation({ summary: 'Get songs by genre' }) + findByGenre(@Param('genre') genre: string) { + return this.songsService.findByGenre(genre); + } + + @Get('random') + @ApiOperation({ summary: 'Get a random song' }) + getRandomSong() { + return this.songsService.getRandomSong(); + } + + @Get(':id') + @ApiOperation({ summary: 'Get a song by id' }) + findOne(@Param('id') id: string) { + return this.songsService.findOne(id); + } + + @Patch(':id') + @ApiOperation({ summary: 'Update a song' }) + update(@Param('id') id: string, @Body() updateSongDto: UpdateSongDto) { + return this.songsService.update(id, updateSongDto); + } + + @Delete(':id') + @ApiOperation({ summary: 'Delete a song' }) + remove(@Param('id') id: string) { + return this.songsService.remove(id); + } + + @Post(':id/play') + @ApiOperation({ summary: 'Increment play count' }) + incrementPlayCount(@Param('id') id: string) { + return this.songsService.updatePlayCount(id); + } +} \ No newline at end of file diff --git a/backend/src/songs/songs.module.ts b/backend/src/songs/songs.module.ts new file mode 100644 index 00000000..8fe9c6bf --- /dev/null +++ b/backend/src/songs/songs.module.ts @@ -0,0 +1,14 @@ +// src/songs/songs.module.ts +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { SongsService } from './songs.service'; +import { SongsController } from './songs.controller'; +import { Song } from './entities/song.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([Song])], + controllers: [SongsController], + providers: [SongsService], + exports: [SongsService], +}) +export class SongsModule {} \ No newline at end of file diff --git a/backend/src/songs/songs.service.spec.ts b/backend/src/songs/songs.service.spec.ts new file mode 100644 index 00000000..a7c31145 --- /dev/null +++ b/backend/src/songs/songs.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { SongsService } from './songs.service'; + +describe('SongsService', () => { + let service: SongsService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [SongsService], + }).compile(); + + service = module.get(SongsService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/backend/src/songs/songs.service.ts b/backend/src/songs/songs.service.ts new file mode 100644 index 00000000..d706d1bc --- /dev/null +++ b/backend/src/songs/songs.service.ts @@ -0,0 +1,73 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { CreateSongDto } from './dto/create-song.dto'; +import { UpdateSongDto } from './dto/update-song.dto'; +import { Song } from './entities/song.entity'; + +@Injectable() +export class SongsService { + constructor( + @InjectRepository(Song) + private songsRepository: Repository, + ) {} + + create(createSongDto: CreateSongDto) { + const song = this.songsRepository.create(createSongDto); + return this.songsRepository.save(song); + } + + findAll() { + return this.songsRepository.find(); + } + + async findOne(id: string) { + const song = await this.songsRepository.findOne({ where: { id } }); + if (!song) { + throw new NotFoundException(`Song with ID ${id} not found`); + } + return song; + } + + async update(id: string, updateSongDto: UpdateSongDto) { + const song = await this.findOne(id); + Object.assign(song, updateSongDto); + return this.songsRepository.save(song); + } + + async remove(id: string) { + const song = await this.findOne(id); + return this.songsRepository.remove(song); + } + + async findByGenre(genre: string) { + return this.songsRepository.find({ where: { genre } }); + } + + async searchSongs(query: string) { + return this.songsRepository + .createQueryBuilder('song') + .where('song.title ILIKE :query OR song.artist ILIKE :query', { + query: `%${query}%`, + }) + .getMany(); + } + + async updatePlayCount(id: string) { + const song = await this.findOne(id); + song.playCount += 1; + return this.songsRepository.save(song); + } + + async findByDifficulty(level: number) { + return this.songsRepository.find({ where: { difficulty: level } }); + } + + async getRandomSong() { + return this.songsRepository + .createQueryBuilder('song') + .orderBy('RANDOM()') + .take(1) + .getOne(); + } +} \ No newline at end of file