From e45d5ed2a11a79d6d566756b5c11a82b719dbaf4 Mon Sep 17 00:00:00 2001 From: Lucas Santana Date: Sun, 12 Apr 2026 01:00:01 -0300 Subject: [PATCH 1/3] feat(bot): add vc blend and blended last.fm seed slices --- .../src/functions/music/commands/autoplay.ts | 25 +++- .../functions/music/commands/play/index.ts | 6 + packages/bot/src/lastfm/index.ts | 5 +- packages/bot/src/types/QueueMetadata.ts | 1 + .../utils/music/autoplay/lastFmSeeds.spec.ts | 102 +++++++++++++ .../src/utils/music/autoplay/lastFmSeeds.ts | 26 ++++ .../src/utils/music/queueManipulation.spec.ts | 135 ++++++++++++++++++ .../bot/src/utils/music/queueManipulation.ts | 45 +++++- 8 files changed, 337 insertions(+), 8 deletions(-) diff --git a/packages/bot/src/functions/music/commands/autoplay.ts b/packages/bot/src/functions/music/commands/autoplay.ts index 6b89db539..b5fedd188 100644 --- a/packages/bot/src/functions/music/commands/autoplay.ts +++ b/packages/bot/src/functions/music/commands/autoplay.ts @@ -1,7 +1,7 @@ import { SlashCommandBuilder } from '@discordjs/builders' import Command from '../../../models/Command' import { interactionReply } from '../../../utils/general/interactionReply' -import { guildSettingsService } from '@lucky/shared/services' +import { guildSettingsService, lastFmLinkService } from '@lucky/shared/services' import { recommendationFeedbackService } from '../../../services/musicRecommendation/feedbackService' import { createEmbed, @@ -16,6 +16,7 @@ import { resolveGuildQueue } from '../../../utils/music/queueResolver' import { trackHistoryService } from '@lucky/shared/services' import type { ColorResolvable, ChatInputCommandInteraction } from 'discord.js' import type { GuildQueue } from 'discord-player' +import type { QueueMetadata } from '../../../types/QueueMetadata' function isAutoplayTrack(track: any): boolean { return (track?.metadata as any)?.isAutoplay === true @@ -154,9 +155,29 @@ async function handleAutoplayStatus( } } + let descriptionLines = [ + `**Autoplay tracks queued:** ${autoplayCount}`, + '**Last.fm integration:** Connected', + ] + + const metadata = queue.metadata as QueueMetadata + const vcMemberIds = metadata?.vcMemberIds ?? [] + if (vcMemberIds.length > 1) { + const linkedUsers = await Promise.all( + vcMemberIds.map(async (id) => { + const link = await lastFmLinkService.getByDiscordId(id) + return link?.lastFmUsername ? id : null + }), + ) + const linkedCount = linkedUsers.filter((id) => id !== null).length + if (linkedCount > 1) { + descriptionLines.push(`🎭 Blending taste for ${linkedCount} users`) + } + } + const statusEmbed = createEmbed({ title: '📊 Autoplay Status', - description: `**Autoplay tracks queued:** ${autoplayCount}\n**Last.fm integration:** Connected`, + description: descriptionLines.join('\n'), color: EMBED_COLORS.AUTOPLAY as ColorResolvable, emoji: EMOJIS.AUTOPLAY, timestamp: true, diff --git a/packages/bot/src/functions/music/commands/play/index.ts b/packages/bot/src/functions/music/commands/play/index.ts index 4081cd2ad..dd1a0c246 100644 --- a/packages/bot/src/functions/music/commands/play/index.ts +++ b/packages/bot/src/functions/music/commands/play/index.ts @@ -128,11 +128,17 @@ export default new Command({ ) const searchEngine = resolveSearchEngine(query, provider) + const vcMemberIds = voiceChannel.members + ? Array.from(voiceChannel.members.values()) + .filter((m) => m.id !== client.user?.id) + .map((m) => m.id) + : [] const playOptions = { nodeOptions: { metadata: { channel: interaction.channel, requestedBy: interaction.user, + vcMemberIds, }, connectionTimeout: ENVIRONMENT_CONFIG.PLAYER.CONNECTION_TIMEOUT, diff --git a/packages/bot/src/lastfm/index.ts b/packages/bot/src/lastfm/index.ts index 6f5cde47b..bae49a9c9 100644 --- a/packages/bot/src/lastfm/index.ts +++ b/packages/bot/src/lastfm/index.ts @@ -12,4 +12,7 @@ export { scrobble, } from './lastFmApi' export type { LastFmTopTrack, LastFmPeriod } from './lastFmApi' -export { consumeLastFmSeedSlice } from '../utils/music/autoplay/lastFmSeeds' +export { + consumeLastFmSeedSlice, + consumeBlendedSeedSlice, +} from '../utils/music/autoplay/lastFmSeeds' diff --git a/packages/bot/src/types/QueueMetadata.ts b/packages/bot/src/types/QueueMetadata.ts index f953a810e..982b5d95e 100644 --- a/packages/bot/src/types/QueueMetadata.ts +++ b/packages/bot/src/types/QueueMetadata.ts @@ -5,4 +5,5 @@ export interface QueueMetadata { channel?: TextChannel | null requestedBy?: User | null client?: CustomClient | null + vcMemberIds?: string[] } diff --git a/packages/bot/src/utils/music/autoplay/lastFmSeeds.spec.ts b/packages/bot/src/utils/music/autoplay/lastFmSeeds.spec.ts index ea74eb641..28246c844 100644 --- a/packages/bot/src/utils/music/autoplay/lastFmSeeds.spec.ts +++ b/packages/bot/src/utils/music/autoplay/lastFmSeeds.spec.ts @@ -12,6 +12,7 @@ import { advanceLastFmSeedOffset, getLastFmCacheOffset, consumeLastFmSeedSlice, + consumeBlendedSeedSlice, LASTFM_SEED_COUNT, } from './lastFmSeeds' @@ -428,3 +429,104 @@ describe('consumeLastFmSeedSlice', () => { expect(slice).toEqual([]) }) }) + +describe('consumeBlendedSeedSlice', () => { + beforeEach(() => { + jest.clearAllMocks() + getRecentTracksMock.mockResolvedValue([]) + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + it('returns empty array when userIds is empty', async () => { + const slice = await consumeBlendedSeedSlice([], 5) + + expect(slice).toEqual([]) + }) + + it('interleaves tracks from two users round-robin style', async () => { + getByDiscordIdMock.mockResolvedValue({ lastFmUsername: 'user123' }) + getTopTracksMock.mockResolvedValue([ + { artist: 'UserA1', title: 'SongA1', playCount: 1 }, + { artist: 'UserA2', title: 'SongA2', playCount: 2 }, + { artist: 'UserA3', title: 'SongA3', playCount: 3 }, + { artist: 'UserA4', title: 'SongA4', playCount: 4 }, + ]) + + const slice = await consumeBlendedSeedSlice( + ['user-alpha', 'user-beta'], + 4, + ) + + expect(slice.length).toBeGreaterThan(0) + expect(slice[0]).toEqual( + expect.objectContaining({ + artist: expect.any(String), + title: expect.any(String), + }), + ) + }) + + it('deduplicates identical tracks across users', async () => { + const sharedTrack = { + artist: 'Shared Artist', + title: 'Shared Song', + playCount: 5, + } + getByDiscordIdMock.mockResolvedValue({ lastFmUsername: 'user123' }) + getTopTracksMock.mockResolvedValue([sharedTrack]) + + const slice = await consumeBlendedSeedSlice(['user-x', 'user-y'], 2) + + expect(slice).toHaveLength(1) + expect(slice[0]).toEqual({ + artist: 'Shared Artist', + title: 'Shared Song', + }) + }) + + it('respects count limit', async () => { + getByDiscordIdMock.mockResolvedValue({ lastFmUsername: 'user123' }) + getTopTracksMock.mockResolvedValue([ + { artist: 'A1', title: 'S1', playCount: 1 }, + { artist: 'A2', title: 'S2', playCount: 2 }, + { artist: 'A3', title: 'S3', playCount: 3 }, + ]) + + const slice = await consumeBlendedSeedSlice(['user-p', 'user-q'], 2) + + expect(slice.length).toBeLessThanOrEqual(2) + }) + + it('falls back to single-user mode when only one user provided', async () => { + getByDiscordIdMock.mockResolvedValue({ lastFmUsername: 'solo-user' }) + getTopTracksMock.mockResolvedValue([ + { artist: 'Solo1', title: 'Track1', playCount: 1 }, + { artist: 'Solo2', title: 'Track2', playCount: 2 }, + ]) + + const slice = await consumeBlendedSeedSlice(['solo-user'], 2) + + expect(slice.length).toBeGreaterThan(0) + expect(slice[0]).toEqual( + expect.objectContaining({ + artist: expect.stringMatching(/^Solo[12]$/), + title: expect.any(String), + }), + ) + }) + + it('returns empty array when all users have no Last.fm link', async () => { + getByDiscordIdMock.mockResolvedValue(null) + getTopTracksMock.mockResolvedValue([]) + + const slice = await consumeBlendedSeedSlice( + ['no-link-1', 'no-link-2'], + 5, + ) + + expect(slice).toEqual([]) + }) +}) diff --git a/packages/bot/src/utils/music/autoplay/lastFmSeeds.ts b/packages/bot/src/utils/music/autoplay/lastFmSeeds.ts index afb4f2c87..44e742a15 100644 --- a/packages/bot/src/utils/music/autoplay/lastFmSeeds.ts +++ b/packages/bot/src/utils/music/autoplay/lastFmSeeds.ts @@ -129,3 +129,29 @@ export async function consumeLastFmSeedSlice( consumeLocks.set(userId, next) return next } + +export async function consumeBlendedSeedSlice( + userIds: string[], + count: number, +): Promise<{ artist: string; title: string }[]> { + if (userIds.length === 0) return [] + + const perUserCount = Math.ceil(count / userIds.length) + const slices = await Promise.all( + userIds.map((id) => consumeLastFmSeedSlice(id, perUserCount)), + ) + + const interleaved: { artist: string; title: string }[] = [] + const maxLen = Math.max(...slices.map((s) => s.length)) + + for (let i = 0; i < maxLen; i++) { + for (const slice of slices) { + if (i < slice.length) { + interleaved.push(slice[i]) + } + } + } + + const deduped = deduplicateTracks(interleaved) + return deduped.slice(0, count) +} diff --git a/packages/bot/src/utils/music/queueManipulation.spec.ts b/packages/bot/src/utils/music/queueManipulation.spec.ts index 80faa667e..962f491c0 100644 --- a/packages/bot/src/utils/music/queueManipulation.spec.ts +++ b/packages/bot/src/utils/music/queueManipulation.spec.ts @@ -49,6 +49,8 @@ const getTrackHistoryMock = jest.fn() const addTrackToHistoryMock = jest.fn().mockResolvedValue(true) const getGuildSettingsMock = jest.fn() +const getLastFmLinkMock = jest.fn() + jest.mock('@lucky/shared/services', () => ({ trackHistoryService: { getTrackHistory: (...args: unknown[]) => getTrackHistoryMock(...args), @@ -58,13 +60,19 @@ jest.mock('@lucky/shared/services', () => ({ guildSettingsService: { getGuildSettings: (...args: unknown[]) => getGuildSettingsMock(...args), }, + lastFmLinkService: { + getByDiscordId: (...args: unknown[]) => getLastFmLinkMock(...args), + }, })) const consumeLastFmSeedSliceMock = jest.fn() +const consumeBlendedSeedSliceMock = jest.fn() jest.mock('./autoplay/lastFmSeeds', () => ({ consumeLastFmSeedSlice: (...args: unknown[]) => consumeLastFmSeedSliceMock(...args), + consumeBlendedSeedSlice: (...args: unknown[]) => + consumeBlendedSeedSliceMock(...args), })) const getSimilarTracksMock = jest.fn() @@ -2361,3 +2369,130 @@ describe('queueManipulation — genre candidate collection', () => { expect(getTagTopTracksMock).toHaveBeenCalledTimes(3) }) }) + +describe('queueManipulation — multi-user VC blend', () => { + beforeEach(() => { + jest.clearAllMocks() + consumeLastFmSeedSliceMock.mockResolvedValue([]) + getTrackHistoryMock.mockResolvedValue([]) + getGuildSettingsMock.mockResolvedValue({ + autoplayMode: 'similar', + }) + dislikedTrackKeysMock.mockResolvedValue(new Set()) + likedTrackKeysMock.mockResolvedValue(new Set()) + getSimilarTracksMock.mockResolvedValue([]) + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + it('uses blended seeds when multiple VC members have Last.fm linked', async () => { + const currentTrack = { + url: 'https://example.com/track', + title: 'Test Song', + author: 'Test Artist', + id: 'track-123', + requestedBy: { id: 'user-1' }, + } + + const searchMock = jest.fn().mockResolvedValue({ + tracks: [ + { + title: 'Similar Song', + author: 'Similar Artist', + url: 'https://youtube.com/watch?v=123', + id: 'yt-similar', + source: 'youtube', + durationMS: 180000, + }, + ], + }) + + const queue = createQueueMock({ + currentTrack, + metadata: { + requestedBy: { id: 'user-1' }, + vcMemberIds: ['user-1', 'user-2'], + }, + player: { search: searchMock }, + addTrack: jest.fn(), + }) + + await replenishQueue(queue as unknown as GuildQueue) + + expect(searchMock).toHaveBeenCalled() + }) + + it('falls back to single-user when VC has only one user', async () => { + const currentTrack = { + url: 'https://example.com/track', + title: 'Test Song', + author: 'Test Artist', + id: 'track-123', + requestedBy: { id: 'user-1' }, + } + + const searchMock = jest.fn().mockResolvedValue({ + tracks: [ + { + title: 'Similar Song', + author: 'Similar Artist', + url: 'https://youtube.com/watch?v=123', + id: 'yt-similar', + source: 'youtube', + durationMS: 180000, + }, + ], + }) + + const queue = createQueueMock({ + currentTrack, + metadata: { + requestedBy: { id: 'user-1' }, + vcMemberIds: ['user-1'], + }, + player: { search: searchMock }, + addTrack: jest.fn(), + }) + + consumeLastFmSeedSliceMock.mockResolvedValue([ + { artist: 'Artist A', title: 'Song A' }, + ]) + + await replenishQueue(queue as unknown as GuildQueue) + + expect(consumeLastFmSeedSliceMock).toHaveBeenCalledWith( + 'user-1', + expect.any(Number), + ) + }) + + it('uses metadata vcMemberIds when available', async () => { + const currentTrack = { + url: 'https://example.com/track', + title: 'Test Song', + author: 'Test Artist', + id: 'track-123', + requestedBy: { id: 'user-1' }, + } + + const searchMock = jest.fn().mockResolvedValue({ + tracks: [], + }) + + const queue = createQueueMock({ + currentTrack, + metadata: { + requestedBy: { id: 'user-1' }, + vcMemberIds: ['user-1', 'user-2', 'user-3'], + }, + player: { search: searchMock }, + addTrack: jest.fn(), + }) + + await replenishQueue(queue as unknown as GuildQueue) + + expect(searchMock).toHaveBeenCalled() + }) +}) diff --git a/packages/bot/src/utils/music/queueManipulation.ts b/packages/bot/src/utils/music/queueManipulation.ts index 12bccf4ed..e64b886c4 100644 --- a/packages/bot/src/utils/music/queueManipulation.ts +++ b/packages/bot/src/utils/music/queueManipulation.ts @@ -11,8 +11,12 @@ import { recommendationFeedbackService } from '../../services/musicRecommendatio import { trackHistoryService, guildSettingsService, + lastFmLinkService, } from '@lucky/shared/services' -import { consumeLastFmSeedSlice } from './autoplay/lastFmSeeds' +import { + consumeLastFmSeedSlice, + consumeBlendedSeedSlice, +} from './autoplay/lastFmSeeds' import { getSimilarTracks, getTagTopTracks } from '../../lastfm' import { cleanSearchQuery, cleanTitle, cleanAuthor } from './searchQueryCleaner' import type { QueueMetadata } from '../../types/QueueMetadata' @@ -682,10 +686,41 @@ async function collectLastFmCandidates( candidates: Map, autoplayMode: 'similar' | 'discover' | 'popular' = 'similar', ): Promise { - const seedSlice = await consumeLastFmSeedSlice( - requestedBy.id, - LASTFM_SEED_COUNT, - ) + const metadata = queue.metadata as QueueMetadata + const vcMemberIds = metadata?.vcMemberIds ?? [] + + const otherUserIds = vcMemberIds.filter((id) => id !== requestedBy.id) + + let seedSlice: { artist: string; title: string }[] = [] + if (otherUserIds.length > 0) { + const linkedUsers = await Promise.all( + [requestedBy.id, ...otherUserIds].map(async (id) => { + const link = await lastFmLinkService.getByDiscordId(id) + return link?.lastFmUsername ? id : null + }), + ) + const linkedUserIds = linkedUsers.filter( + (id) => id !== null, + ) as string[] + + if (linkedUserIds.length > 1) { + seedSlice = await consumeBlendedSeedSlice( + linkedUserIds, + LASTFM_SEED_COUNT, + ) + } else if (linkedUserIds.length === 1) { + seedSlice = await consumeLastFmSeedSlice( + linkedUserIds[0], + LASTFM_SEED_COUNT, + ) + } + } else { + seedSlice = await consumeLastFmSeedSlice( + requestedBy.id, + LASTFM_SEED_COUNT, + ) + } + if (seedSlice.length === 0) return // Search for each seed via track search From e0e7b54b8f7025f4e1116b1f67ce831059b47d71 Mon Sep 17 00:00:00 2001 From: Lucas Santana Date: Sun, 12 Apr 2026 02:19:23 -0300 Subject: [PATCH 2/3] =?UTF-8?q?test(bot):=20fix=20blend=20coverage=20?= =?UTF-8?q?=E2=80=94=20mock=20lastfm=20links=20for=20blend=20paths?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/utils/music/queueManipulation.spec.ts | 74 ++++++++++++++----- 1 file changed, 57 insertions(+), 17 deletions(-) diff --git a/packages/bot/src/utils/music/queueManipulation.spec.ts b/packages/bot/src/utils/music/queueManipulation.spec.ts index 962f491c0..ad99d2cea 100644 --- a/packages/bot/src/utils/music/queueManipulation.spec.ts +++ b/packages/bot/src/utils/music/queueManipulation.spec.ts @@ -2374,12 +2374,18 @@ describe('queueManipulation — multi-user VC blend', () => { beforeEach(() => { jest.clearAllMocks() consumeLastFmSeedSliceMock.mockResolvedValue([]) + consumeBlendedSeedSliceMock.mockResolvedValue([]) + getLastFmLinkMock.mockResolvedValue(null) getTrackHistoryMock.mockResolvedValue([]) getGuildSettingsMock.mockResolvedValue({ autoplayMode: 'similar', + autoplayGenres: [], }) + getTagTopTracksMock.mockResolvedValue([]) dislikedTrackKeysMock.mockResolvedValue(new Set()) likedTrackKeysMock.mockResolvedValue(new Set()) + getPreferredArtistKeysMock.mockResolvedValue(new Set()) + getBlockedArtistKeysMock.mockResolvedValue(new Set()) getSimilarTracksMock.mockResolvedValue([]) }) @@ -2388,6 +2394,10 @@ describe('queueManipulation — multi-user VC blend', () => { }) it('uses blended seeds when multiple VC members have Last.fm linked', async () => { + getLastFmLinkMock.mockResolvedValue({ lastFmUsername: 'someuser' }) + consumeBlendedSeedSliceMock.mockResolvedValue([ + { artist: 'Artist A', title: 'Song A' }, + ]) const currentTrack = { url: 'https://example.com/track', title: 'Test Song', @@ -2395,33 +2405,63 @@ describe('queueManipulation — multi-user VC blend', () => { id: 'track-123', requestedBy: { id: 'user-1' }, } - - const searchMock = jest.fn().mockResolvedValue({ - tracks: [ - { - title: 'Similar Song', - author: 'Similar Artist', - url: 'https://youtube.com/watch?v=123', - id: 'yt-similar', - source: 'youtube', - durationMS: 180000, - }, - ], - }) - const queue = createQueueMock({ currentTrack, metadata: { requestedBy: { id: 'user-1' }, vcMemberIds: ['user-1', 'user-2'], }, - player: { search: searchMock }, + player: { + search: jest.fn().mockResolvedValue({ + tracks: [ + { + title: 'Song A', + author: 'Artist A', + url: 'https://youtube.com/watch?v=blend', + id: 'yt-blend', + source: 'youtube', + durationMS: 180000, + }, + ], + }), + }, addTrack: jest.fn(), }) - await replenishQueue(queue as unknown as GuildQueue) + expect(consumeBlendedSeedSliceMock).toHaveBeenCalledWith( + ['user-1', 'user-2'], + expect.any(Number), + ) + }) - expect(searchMock).toHaveBeenCalled() + it('uses single-user seed when only one VC member has Last.fm linked', async () => { + getLastFmLinkMock + .mockResolvedValueOnce({ lastFmUsername: 'user1fm' }) + .mockResolvedValueOnce(null) + consumeLastFmSeedSliceMock.mockResolvedValue([ + { artist: 'Artist B', title: 'Song B' }, + ]) + const queue = createQueueMock({ + currentTrack: { + url: 'https://example.com/t', + title: 'T', + author: 'A', + id: 't', + requestedBy: { id: 'user-1' }, + } as any, + metadata: { + requestedBy: { id: 'user-1' }, + vcMemberIds: ['user-1', 'user-2'], + }, + player: { search: jest.fn().mockResolvedValue({ tracks: [] }) }, + addTrack: jest.fn(), + }) + await replenishQueue(queue as unknown as GuildQueue) + expect(consumeLastFmSeedSliceMock).toHaveBeenCalledWith( + 'user-1', + expect.any(Number), + ) + expect(consumeBlendedSeedSliceMock).not.toHaveBeenCalled() }) it('falls back to single-user when VC has only one user', async () => { From 07e864df1947b2adf6ad95495d8a312bc4301bd8 Mon Sep 17 00:00:00 2001 From: Lucas Santana Date: Sun, 12 Apr 2026 16:22:09 -0300 Subject: [PATCH 3/3] test(bot): add blend status and vcmemberids coverage tests --- .../functions/music/commands/autoplay.spec.ts | 44 ++++++++++++++++++- .../music/commands/play/index.spec.ts | 32 ++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/packages/bot/src/functions/music/commands/autoplay.spec.ts b/packages/bot/src/functions/music/commands/autoplay.spec.ts index c2f91cef5..7cccea570 100644 --- a/packages/bot/src/functions/music/commands/autoplay.spec.ts +++ b/packages/bot/src/functions/music/commands/autoplay.spec.ts @@ -49,6 +49,7 @@ jest.mock('@lucky/shared/utils', () => ({ const getGuildSettingsMock = jest.fn() const updateGuildSettingsMock = jest.fn() +const getLastFmLinkMock = jest.fn() jest.mock('@lucky/shared/services', () => ({ guildSettingsService: { @@ -60,6 +61,9 @@ jest.mock('@lucky/shared/services', () => ({ getAutoplayStats: (...args: unknown[]) => trackHistoryServiceMock(...args), }, + lastFmLinkService: { + getByDiscordId: (...args: unknown[]) => getLastFmLinkMock(...args), + }, })) jest.mock('../../../services/musicRecommendation/feedbackService', () => ({ @@ -94,10 +98,11 @@ function createInteraction( return interaction as any } -function createQueue() { +function createQueue(metadata: Record = {}) { return { guild: { id: 'guild-1' }, currentTrack: { title: 'Current Song' }, + metadata, tracks: { size: 3, at: jest.fn((index: number) => { @@ -131,6 +136,7 @@ describe('autoplay command', () => { jest.clearAllMocks() getGuildSettingsMock.mockResolvedValue({ autoplayMode: 'similar' }) updateGuildSettingsMock.mockResolvedValue(true) + getLastFmLinkMock.mockResolvedValue(null) }) describe('structure', () => { @@ -232,6 +238,42 @@ describe('autoplay command', () => { expect(embed.title).toContain('Status') expect(interactionReplyMock).toHaveBeenCalled() }) + + it('should show blend info when multiple vc members have last.fm', async () => { + getLastFmLinkMock.mockResolvedValue({ lastFmUsername: 'user' }) + const interaction = createInteraction('status') + const queue = createQueue({ vcMemberIds: ['user-1', 'user-2'] }) + const client = createClient() + + resolveGuildQueueMock.mockReturnValue({ queue }) + + await autoplayCommand.execute({ client, interaction } as any) + + expect(createEmbedMock).toHaveBeenCalledWith( + expect.objectContaining({ + description: expect.stringContaining('Blending taste'), + }), + ) + }) + + it('should not show blend when only one member has last.fm', async () => { + getLastFmLinkMock + .mockResolvedValueOnce({ lastFmUsername: 'user1' }) + .mockResolvedValueOnce(null) + const interaction = createInteraction('status') + const queue = createQueue({ vcMemberIds: ['user-1', 'user-2'] }) + const client = createClient() + + resolveGuildQueueMock.mockReturnValue({ queue }) + + await autoplayCommand.execute({ client, interaction } as any) + + expect(createEmbedMock).toHaveBeenCalledWith( + expect.objectContaining({ + description: expect.not.stringContaining('Blending taste'), + }), + ) + }) }) describe('error handling', () => { diff --git a/packages/bot/src/functions/music/commands/play/index.spec.ts b/packages/bot/src/functions/music/commands/play/index.spec.ts index a66773350..b31a2fd5c 100644 --- a/packages/bot/src/functions/music/commands/play/index.spec.ts +++ b/packages/bot/src/functions/music/commands/play/index.spec.ts @@ -805,4 +805,36 @@ describe('play command', () => { }), ) }) + + it('captures vc member ids in queue metadata when voiceChannel.members is set', async () => { + let capturedPlayOptions: any + + const client = createClient( + (_track: unknown, _query: unknown, opts: unknown) => { + capturedPlayOptions = opts + return Promise.resolve() + }, + ) + + const interaction = createInteraction('guild-1') + interaction.member.voice.channel.members = new Map([ + ['user-1', { id: 'user-1' }], + ['user-2', { id: 'user-2' }], + ['bot-1', { id: 'bot-1' }], + ]) + client.user = { id: 'bot-1' } + + getGuildSettingsMock.mockResolvedValue({ autoPlayEnabled: false }) + resolveGuildQueueMock.mockReturnValue({ queue: null }) + canAddTracksMock.mockReturnValue({ allowed: true, limit: 10 }) + + await playCommand.execute({ client, interaction } as any) + + const vcMemberIds = + capturedPlayOptions?.nodeOptions?.metadata?.vcMemberIds + expect(vcMemberIds).toBeDefined() + expect(vcMemberIds).toContain('user-1') + expect(vcMemberIds).toContain('user-2') + expect(vcMemberIds).not.toContain('bot-1') + }) })