Skip to content

Commit

Permalink
Merge pull request #285 from LaGodxy/Create-Music-Genre-Management-Sy…
Browse files Browse the repository at this point in the history
…stem-#252

Created Music Genre Management System 

Close #252
  • Loading branch information
Xaxxoo authored Feb 25, 2025
2 parents 4b59cbf + 4588896 commit e18a72a
Show file tree
Hide file tree
Showing 18 changed files with 765 additions and 6 deletions.
2 changes: 2 additions & 0 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { TournamentModule } from './tournament/tournament.module';
import { GameGateway } from './websocket-game comms/providers/gamegateway';
import { GameModule } from './websocket-game comms/game.module';
import { AchievementModule } from './achievement/achievement.module';
import { SongGenreModule } from './song-genre/song-genre.module';
import { SocialModule } from './social/social.module';
import { AchievementModule } from './achievement/achievement.module';

Expand Down Expand Up @@ -52,6 +53,7 @@ import { AchievementModule } from './achievement/achievement.module';
TournamentModule,
AchievementModule,
SocialModule,
SongGenreModule,
],
controllers: [AppController],
providers: [
Expand Down
9 changes: 9 additions & 0 deletions backend/src/auth/guard/jwt-auth.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';

@Injectable()
export class JwtAuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
// Static guard: Always allow access (replace with real authentication later)
return true;
}
}
30 changes: 30 additions & 0 deletions backend/src/song-genre/controllers/genre.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Controller, Get, Post, Body, Param, UseGuards } from '@nestjs/common';
import { GenreService } from '../services/genre.service';
import { JwtAuthGuard } from '../../auth/guard/jwt-auth.guard';
import { CurrentUser } from '../../auth/decorators/current-user.decorator';

@Controller('genres')
export class GenreController {
constructor(private readonly genreService: GenreService) {}

@Get()
findAll() {
return this.genreService.findAll();
}

@Post(':genreId/preferences')
@UseGuards(JwtAuthGuard)
updatePreference(
@CurrentUser() userId: string,
@Param('genreId') genreId: string,
@Body() performanceData: any,
) {
return this.genreService.updateUserPreference(userId, genreId, performanceData);
}

@Get('preferences')
@UseGuards(JwtAuthGuard)
getUserPreferences(@CurrentUser() userId: string) {
return this.genreService.getUserGenrePreferences(userId);
}
}
20 changes: 20 additions & 0 deletions backend/src/song-genre/controllers/song.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Test, TestingModule } from '@nestjs/testing';
import { SongGenreController } from './song.controller';
import { SongGenreService } from '../services/song.service';

describe('SongGenreController', () => {
let controller: SongGenreController;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [SongGenreController],
providers: [SongGenreService],
}).compile();

controller = module.get<SongGenreController>(SongGenreController);
});

it('should be defined', () => {
expect(controller).toBeDefined();
});
});
26 changes: 26 additions & 0 deletions backend/src/song-genre/controllers/song.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Controller, Post, Body, Get, Param, Patch, UseGuards } from '@nestjs/common';
import { SongService } from '../services/song.service';
import { CreateSongDto } from '../dtos/create-song.dto';
import { JwtAuthGuard } from '../../auth/guard/jwt-auth.guard';

@Controller('songs')
export class SongController {
constructor(private readonly songService: SongService) {}

@Post()
@UseGuards(JwtAuthGuard)
create(@Body() createSongDto: CreateSongDto) {
return this.songService.create(createSongDto);
}

@Get('genre/:genreId')
findByGenre(@Param('genreId') genreId: string) {
return this.songService.findByGenre(genreId);
}

@Patch(':id/difficulty')
@UseGuards(JwtAuthGuard)
updateDifficulty(@Param('id') id: string, @Body() performanceData: any[]) {
return this.songService.updateDifficulty(id, performanceData);
}
}
26 changes: 26 additions & 0 deletions backend/src/song-genre/dtos/create-song.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { IsString, IsNumber, IsObject, IsUUID, IsArray, IsOptional } from 'class-validator';

export class CreateSongDto {
@IsString()
title: string;

@IsString()
artist: string;

@IsNumber()
durationSeconds: number;

@IsNumber()
baseDifficulty: number;

@IsObject()
difficultyFactors: Record<string, number>;

@IsUUID()
genreId: string;

@IsOptional()
@IsArray()
@IsString({ each: true })
tagIds?: string[];
}
4 changes: 4 additions & 0 deletions backend/src/song-genre/dtos/update-song-genre.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateSongGenreDto } from './create-song.dto';

export class UpdateSongGenreDto extends PartialType(CreateSongGenreDto) {}
29 changes: 29 additions & 0 deletions backend/src/song-genre/entities/genre-challenge.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Entity, Column, PrimaryGeneratedColumn, ManyToOne, CreateDateColumn } from 'typeorm';
import { Genre } from './genre.entity';

@Entity()
export class GenreChallenge {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column()
title: string;

@ManyToOne(() => Genre)
genre: Genre;

@Column('text')
description: string;

@Column('json')
requirements: Record<string, any>;

@Column('int')
experienceReward: number;

@Column()
expiresAt: Date;

@CreateDateColumn()
createdAt: Date;
}
29 changes: 29 additions & 0 deletions backend/src/song-genre/entities/genre.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Entity, Column, PrimaryGeneratedColumn, OneToMany, CreateDateColumn, UpdateDateColumn } from 'typeorm';
import { Song } from './song.entity';

@Entity()
export class Genre {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column({ unique: true })
name: string;

@Column('text')
description: string;

@Column()
icon: string;

@Column('float', { default: 1.0 })
difficultyMultiplier: number;

@OneToMany(() => Song, song => song.genre)
songs: Song[];

@CreateDateColumn()
createdAt: Date;

@UpdateDateColumn()
updatedAt: Date;
}
49 changes: 49 additions & 0 deletions backend/src/song-genre/entities/song.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Entity, Column, PrimaryGeneratedColumn, ManyToOne, ManyToMany, JoinTable, CreateDateColumn, UpdateDateColumn } from 'typeorm';
import { Genre } from './genre.entity';
import { Tag } from './tag.entity';

@Entity()
export class Song {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column()
title: string;

@Column()
artist: string;

@Column('int')
durationSeconds: number;

@Column('float')
baseDifficulty: number;

@Column('float', { nullable: true })
calculatedDifficulty: number;

@Column('json')
difficultyFactors: Record<string, number>;

@ManyToOne(() => Genre, genre => genre.songs)
genre: Genre;

@ManyToMany(() => Tag)
@JoinTable()
tags: Tag[];

@Column('int', { default: 0 })
playCount: number;

@Column('float', { default: 0 })
averageScore: number;

@Column('float', { default: 0 })
completionRate: number;

@CreateDateColumn()
createdAt: Date;

@UpdateDateColumn()
updatedAt: Date;
}
13 changes: 13 additions & 0 deletions backend/src/song-genre/entities/tag.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class Tag {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column({ unique: true })
name: string;

@Column()
category: string;
}
29 changes: 29 additions & 0 deletions backend/src/song-genre/entities/user-genre-preference.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Entity, Column, PrimaryGeneratedColumn, ManyToOne, CreateDateColumn, UpdateDateColumn } from 'typeorm';
import { Genre } from './genre.entity';

@Entity()
export class UserGenrePreference {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column()
userId: string;

@ManyToOne(() => Genre)
genre: Genre;

@Column('float', { default: 0 })
preferenceScore: number;

@Column('int', { default: 0 })
playCount: number;

@Column('float', { default: 0 })
averagePerformance: number;

@CreateDateColumn()
createdAt: Date;

@UpdateDateColumn()
updatedAt: Date;
}
60 changes: 60 additions & 0 deletions backend/src/song-genre/services/genre.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Genre } from '../entities/genre.entity';
import { UserGenrePreference } from '../entities/user-genre-preference.entity';

@Injectable()
export class GenreService {
constructor(
@InjectRepository(Genre)
private genreRepository: Repository<Genre>,
@InjectRepository(UserGenrePreference)
private userPreferenceRepository: Repository<UserGenrePreference>,
) {}

async findAll(): Promise<Genre[]> {
return this.genreRepository.find();
}

async updateUserPreference(userId: string, genreId: string, performanceData: any): Promise<UserGenrePreference> {
let preference = await this.userPreferenceRepository.findOne({
where: { userId, genre: { id: genreId } },
relations: ['genre'],
});

if (!preference) {
const genre = await this.genreRepository.findOne({ where: { id: genreId } });
preference = this.userPreferenceRepository.create({
userId,
genre,
preferenceScore: 0,
playCount: 0,
averagePerformance: 0,
});
}

// Update stats
preference.playCount += 1;

// Update average performance
const totalPerformance = preference.averagePerformance * (preference.playCount - 1) + performanceData.score;
preference.averagePerformance = totalPerformance / preference.playCount;

// Update preference score (based on play count and performance)
preference.preferenceScore =
(preference.playCount * 0.1) +
(preference.averagePerformance * 0.01) +
(performanceData.enjoymentRating || 0);

return this.userPreferenceRepository.save(preference);
}

async getUserGenrePreferences(userId: string): Promise<UserGenrePreference[]> {
return this.userPreferenceRepository.find({
where: { userId },
relations: ['genre'],
order: { preferenceScore: 'DESC' },
});
}
}
Loading

0 comments on commit e18a72a

Please sign in to comment.