From 27067652cf4cd460f68000568fde96c905e34580 Mon Sep 17 00:00:00 2001 From: Ryan-slither Date: Mon, 6 Oct 2025 22:57:18 -0400 Subject: [PATCH] Track Search & Fixed Memberships --- Taskfile.yml | 5 +++++ src/jukebox/juke-session/dto/membership.dto.ts | 4 ++++ .../juke-session/juke-session.controller.ts | 5 ++++- src/jukebox/juke-session/juke-session.service.ts | 1 + .../tests/juke-session.controller.spec.ts | 16 ++++++++++++++++ src/spotify/spotify.service.ts | 9 ++++++++- src/track/track.controller.ts | 15 +++++++++++---- src/track/track.service.ts | 7 +++++++ 8 files changed, 56 insertions(+), 6 deletions(-) diff --git a/Taskfile.yml b/Taskfile.yml index 5d34a99..c6369be 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -89,6 +89,11 @@ tasks: cmds: - docker-compose down --remove-orphans -v + clean:network: + desc: "Remove container and volumes for network mode" + cmds: + - docker-compose -f docker-compose.network.yml down --remove-orphans -v + ######################################### # Jukebox-Types Package ######################################### diff --git a/src/jukebox/juke-session/dto/membership.dto.ts b/src/jukebox/juke-session/dto/membership.dto.ts index ae26525..66e2382 100644 --- a/src/jukebox/juke-session/dto/membership.dto.ts +++ b/src/jukebox/juke-session/dto/membership.dto.ts @@ -3,6 +3,7 @@ import { EntityDtoBase } from 'src/config/dtos' import { JukeSessionMembership } from '../entities/membership.entity' import { JukeSessionDto } from './juke-session.dto' import { IsNotEmpty, IsNumber } from 'class-validator' +import { QueuedTrack } from 'src/jukebox/queue/entities/queued-track.entity' export class JukeSessionMembershipInlineDto { @Expose() @@ -21,6 +22,9 @@ export class JukeSessionMembershipDto extends EntityDtoBase + obj.queued_tracks ? obj.queued_tracks.map((track: QueuedTrack) => track.id) : [], + ) queued_tracks: number[] } diff --git a/src/jukebox/juke-session/juke-session.controller.ts b/src/jukebox/juke-session/juke-session.controller.ts index 4662dc6..efe59ff 100644 --- a/src/jukebox/juke-session/juke-session.controller.ts +++ b/src/jukebox/juke-session/juke-session.controller.ts @@ -1,6 +1,7 @@ import { Body, Controller, + DefaultValuePipe, Delete, Get, Param, @@ -10,7 +11,7 @@ import { UseGuards, UseInterceptors, } from '@nestjs/common' -import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger' +import { ApiBearerAuth, ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger' import { AuthInterceptor } from 'src/auth/auth.interceptor' import { NumberPipe } from 'src/pipes/int-pipe.pipe' import { UserDto } from 'src/shared' @@ -146,6 +147,8 @@ export class JukeSessionController { @ApiOperation({ summary: '[MEMBER] (PAGINATED: 0-indexed) Get members/memberships of a juke session', }) + @ApiQuery({ name: 'page', required: false, type: Number, schema: { default: 0, minimum: 0 } }) + @ApiQuery({ name: 'rows', required: false, type: Number, schema: { default: 7, minimum: 1 } }) getJukeSessionMembers( @Param('jukebox_id', new NumberPipe('jukebox_id')) jukeboxId: number, @Param('id', new NumberPipe('id')) id: number, diff --git a/src/jukebox/juke-session/juke-session.service.ts b/src/jukebox/juke-session/juke-session.service.ts index 787889f..6e71a16 100644 --- a/src/jukebox/juke-session/juke-session.service.ts +++ b/src/jukebox/juke-session/juke-session.service.ts @@ -275,6 +275,7 @@ export class JukeSessionService { ): Promise { const membership = await this.membershipRepo.findOne({ where: { user_id: userId, juke_session: { id: jukeSessionId } }, + relations: { juke_session: true, queued_tracks: true }, }) if (!membership) { throw new NotFoundException(`User ${userId} is not a member of juke session ${jukeSessionId}`) diff --git a/src/jukebox/juke-session/tests/juke-session.controller.spec.ts b/src/jukebox/juke-session/tests/juke-session.controller.spec.ts index e9b0ab1..4a991f1 100644 --- a/src/jukebox/juke-session/tests/juke-session.controller.spec.ts +++ b/src/jukebox/juke-session/tests/juke-session.controller.spec.ts @@ -277,4 +277,20 @@ describe('JukeSessionController', () => { expect(membership.user_id).toEqual(testUserId) }) + + it('should get queued tracks for a member', async () => { + const session = await createTestJukeSession() + const testUserId = 4 + const membership = await controller.addJukeSessionMemberByJoinCode( + jukebox.id, + session.join_code, + { + user_id: testUserId, + }, + ) + await queueService.queueTrack(session.id, { queued_by: { id: membership.id }, track }) + + const membershipWithQueued = await controller.getJukeSessionMember(jukebox.id, membership.id) + expect(membershipWithQueued.queued_tracks[0]).toEqual(track.id) + }) }) diff --git a/src/spotify/spotify.service.ts b/src/spotify/spotify.service.ts index 77601a2..706f821 100644 --- a/src/spotify/spotify.service.ts +++ b/src/spotify/spotify.service.ts @@ -3,6 +3,7 @@ import { Axios } from 'axios' import { JukeboxSearchDto } from 'src/jukebox/dto/jukebox-search.dto' import { SpotifyTokensDto } from './dto/spotify-tokens.dto' import { SpotifyBaseService } from './spotify-base.service' +import { MaxInt } from '@spotify/web-api-ts-sdk' export interface ISpotifyService { setPlayerDevice(spotifyAuth: SpotifyTokensDto, deviceId: string): Promise @@ -60,11 +61,17 @@ export class SpotifyService extends SpotifyBaseService implements ISpotifyServic return sdk.player.getUsersQueue() } - async searchTracks(spotifyAuth: SpotifyTokensDto, searchQuery: JukeboxSearchDto) { + async searchTracks( + spotifyAuth: SpotifyTokensDto, + searchQuery: JukeboxSearchDto, + limit: MaxInt<50> = 10, + ) { const sdk = this.getSdk(spotifyAuth) return sdk.search( `${searchQuery.trackQuery} artist:${searchQuery.artistQuery} album:${searchQuery.albumQuery}`, ['track'], + undefined, + limit, ) } diff --git a/src/track/track.controller.ts b/src/track/track.controller.ts index 858e32d..220605b 100644 --- a/src/track/track.controller.ts +++ b/src/track/track.controller.ts @@ -1,5 +1,5 @@ import { Body, Controller, Get, NotImplementedException, Query, UseGuards } from '@nestjs/common' -import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger' +import { ApiBearerAuth, ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger' import { JukeboxSearchDto } from 'src/jukebox/dto/jukebox-search.dto' import { Roles } from 'src/utils/decorators/roles.decorator' import { RolesGuard } from 'src/utils/guards/roles.guard' @@ -15,8 +15,15 @@ export class TrackController { @UseGuards(RolesGuard) @Get() @ApiOperation({ summary: '[MEMBER] Search tracks from the Spotify API' }) - searchTracks(@Query('jukeboxId') jukeboxId: string, @Body() body: JukeboxSearchDto) { - // return this.trackService.searchTracks(+jukeboxId, body) - throw new NotImplementedException() + @ApiQuery({ name: 'trackQuery', required: false, schema: { type: 'string', default: '' } }) + @ApiQuery({ name: 'albumQuery', required: false, schema: { type: 'string', default: '' } }) + @ApiQuery({ name: 'artistQuery', required: false, schema: { type: 'string', default: '' } }) + searchTracks( + @Query('jukeboxId') jukeboxId: string, + @Query('trackQuery') trackQuery: string = '', + @Query('albumQuery') albumQuery: string = '', + @Query('artistQuery') artistQuery: string = '', + ) { + return this.trackService.searchTracks(+jukeboxId, { trackQuery, albumQuery, artistQuery }) } } diff --git a/src/track/track.service.ts b/src/track/track.service.ts index ddcd553..07dae5c 100644 --- a/src/track/track.service.ts +++ b/src/track/track.service.ts @@ -6,6 +6,7 @@ import { SpotifyService } from 'src/spotify/spotify.service' import { Repository } from 'typeorm' import { CreateTrackDto, TrackDto } from './dto/track.dto' import { Track } from './entities/track.entity' +import { JukeboxSearchDto } from 'src/jukebox/dto/jukebox-search.dto' @Injectable() export class TrackService { @@ -36,6 +37,12 @@ export class TrackService { return plainToInstance(TrackDto, track) } + async searchTracks(jukeboxId: number, searchQuery: JukeboxSearchDto): Promise { + const link = await this.accountLinkService.getActiveAccount(jukeboxId) + const search = await this.spotifyService.searchTracks(link.spotify_account, searchQuery) + return plainToInstance(TrackDto, search) + } + // findAll() { // return `This action returns all track` // }