diff --git a/packages/bot/src/functions/music/commands/nowplaying.spec.ts b/packages/bot/src/functions/music/commands/nowplaying.spec.ts
index e7b535e4a..f28a8c64c 100644
--- a/packages/bot/src/functions/music/commands/nowplaying.spec.ts
+++ b/packages/bot/src/functions/music/commands/nowplaying.spec.ts
@@ -5,7 +5,7 @@ const requireQueueMock = jest.fn()
const requireCurrentTrackMock = jest.fn()
const interactionReplyMock = jest.fn()
const resolveGuildQueueMock = jest.fn()
-const trackToDataMock = jest.fn((track: unknown) => ({
+const playerTrackToDataMock = jest.fn((track: unknown) => ({
title: (track as { title: string }).title,
}))
const buildTrackEmbedMock = jest.fn(() => ({ embed: 'nowplaying' }))
@@ -25,7 +25,7 @@ jest.mock('../../../utils/music/queueResolver', () => ({
}))
jest.mock('../../../utils/general/responseEmbeds', () => ({
- trackToData: (...args: unknown[]) => trackToDataMock(...args),
+ playerTrackToData: (...args: unknown[]) => playerTrackToDataMock(...args),
buildTrackEmbed: (...args: unknown[]) => buildTrackEmbedMock(...args),
}))
@@ -89,7 +89,7 @@ describe('nowplaying command', () => {
interaction: makeInteraction(),
} as any)
- expect(trackToDataMock).toHaveBeenCalledWith(track)
+ expect(playerTrackToDataMock).toHaveBeenCalledWith(track)
expect(createProgressBar).toHaveBeenCalledWith({
length: 18,
timecodes: true,
diff --git a/packages/bot/src/functions/music/commands/songinfo.ts b/packages/bot/src/functions/music/commands/songinfo.ts
index 2226500f9..eb0fb2ccb 100644
--- a/packages/bot/src/functions/music/commands/songinfo.ts
+++ b/packages/bot/src/functions/music/commands/songinfo.ts
@@ -4,7 +4,7 @@ import { assertDefined } from '@lucky/shared/utils/guards'
import { interactionReply } from '../../../utils/general/interactionReply'
import {
buildTrackEmbed,
- trackToData,
+ playerTrackToData,
} from '../../../utils/general/responseEmbeds'
import type { CommandExecuteParams } from '../../../types/CommandData'
import {
@@ -27,7 +27,7 @@ export default new Command({
if (!(await requireQueue(queue, interaction))) return
if (!(await requireCurrentTrack(queue, interaction))) return
- const trackData = trackToData(
+ const trackData = playerTrackToData(
assertDefined(
track,
'track present after requireCurrentTrack guard',
diff --git a/packages/bot/src/handlers/webMusic/mappers.spec.ts b/packages/bot/src/handlers/webMusic/mappers.spec.ts
index b85eeba90..6be844e8f 100644
--- a/packages/bot/src/handlers/webMusic/mappers.spec.ts
+++ b/packages/bot/src/handlers/webMusic/mappers.spec.ts
@@ -81,6 +81,20 @@ describe('mapTrack', () => {
const track = mapTrack(makeRawTrack({ requestedBy: null }))
expect(track.requestedBy).toBeUndefined()
})
+
+ it('forwards recommendationReason from track metadata', () => {
+ const track = mapTrack(
+ makeRawTrack({
+ metadata: { isAutoplay: true, recommendationReason: 'similar vibes' },
+ }),
+ )
+ expect(track.recommendationReason).toBe('similar vibes')
+ })
+
+ it('omits recommendationReason when metadata has none', () => {
+ const track = mapTrack(makeRawTrack({ metadata: { isAutoplay: true } }))
+ expect(track.recommendationReason).toBeUndefined()
+ })
})
describe('repeatModeToString', () => {
diff --git a/packages/bot/src/handlers/webMusic/mappers.ts b/packages/bot/src/handlers/webMusic/mappers.ts
index b68763a72..7ea50b5b8 100644
--- a/packages/bot/src/handlers/webMusic/mappers.ts
+++ b/packages/bot/src/handlers/webMusic/mappers.ts
@@ -16,11 +16,18 @@ interface RawTrack {
durationMS: number
requestedBy?: { username?: string } | null
source?: string
+ metadata?: {
+ isAutoplay?: boolean
+ recommendationReason?: string
+ } | null
}
const KNOWN_SOURCES = ['youtube', 'spotify', 'soundcloud']
export function mapTrack(track: RawTrack): TrackInfo {
+ const rawReason = track.metadata?.recommendationReason
+ const reason =
+ typeof rawReason === 'string' ? rawReason : undefined
return {
id: track.id,
title: track.title,
@@ -33,6 +40,7 @@ export function mapTrack(track: RawTrack): TrackInfo {
source: (KNOWN_SOURCES.includes(track.source ?? '')
? track.source
: 'unknown') as TrackInfo['source'],
+ ...(reason ? { recommendationReason: reason } : {}),
}
}
diff --git a/packages/bot/src/utils/general/responseEmbeds/buildTrackEmbed.spec.ts b/packages/bot/src/utils/general/responseEmbeds/buildTrackEmbed.spec.ts
index dcaaee1da..1b2003446 100644
--- a/packages/bot/src/utils/general/responseEmbeds/buildTrackEmbed.spec.ts
+++ b/packages/bot/src/utils/general/responseEmbeds/buildTrackEmbed.spec.ts
@@ -2,6 +2,7 @@ import { describe, it, expect } from '@jest/globals'
import {
buildTrackEmbed,
buildCommandTrackEmbed,
+ playerTrackToData,
trackToData,
} from './buildTrackEmbed'
import { detectSource } from '../../music/nowPlayingEmbed'
@@ -67,6 +68,16 @@ describe('buildTrackEmbed', () => {
expect(progress?.inline).toBe(false)
})
+ it('adds a Why this track field when recommendationReason is set', () => {
+ const embed = buildTrackEmbed(
+ { ...baseTrack, recommendationReason: 'similar vibes' },
+ 'playing',
+ fakeUser,
+ )
+ const why = embed.data.fields?.find((f) => f.name === 'Why this track')
+ expect(why?.value).toBe('similar vibes')
+ })
+
it('omits the Progress field when no/null progress bar', () => {
const noOpts = buildTrackEmbed(baseTrack, 'playing', fakeUser)
const nullBar = buildTrackEmbed(baseTrack, 'playing', fakeUser, {
@@ -134,7 +145,7 @@ describe('trackToData', () => {
}
it('maps all fields and formats duration correctly', () => {
- const data = trackToData(fakeTrack as never)
+ const data = trackToData(fakeTrack)
expect(data.title).toBe('Test Song')
expect(data.author).toBe('Test Artist')
expect(data.url).toBe(fakeTrack.url)
@@ -143,17 +154,33 @@ describe('trackToData', () => {
expect(data.duration).toBe('3:35')
})
+ it('forwards recommendationReason from track metadata', () => {
+ const data = trackToData({
+ ...fakeTrack,
+ metadata: { recommendationReason: 'same artist' },
+ })
+ expect(data.recommendationReason).toBe('same artist')
+ })
+
+ it('rejects invalid recommendation metadata from discord-player', () => {
+ const data = playerTrackToData({
+ ...fakeTrack,
+ metadata: { recommendationReason: 123 },
+ })
+ expect(data.recommendationReason).toBeUndefined()
+ })
+
it.each([
[0, undefined],
[65000, '1:05'],
])('handles edge cases: durationMS=%i → %s', (durationMS, expected) => {
- const data = trackToData({ ...fakeTrack, durationMS } as never)
+ const data = trackToData({ ...fakeTrack, durationMS })
expect(data.duration).toBe(expected)
if (durationMS === 0) {
const noSource = trackToData({
...fakeTrack,
source: undefined,
- } as never)
+ })
expect(noSource.source).toBeNull()
}
})
@@ -170,11 +197,7 @@ describe('buildCommandTrackEmbed', () => {
}
it('builds embed with status label and track data', () => {
- const embed = buildCommandTrackEmbed(
- track as never,
- '⏸️ Paused',
- fakeUser,
- )
+ const embed = buildCommandTrackEmbed(track, '⏸️ Paused', fakeUser)
expect(embed.data.author?.name).toBe('⏸️ Paused')
expect(embed.data.title).toBe(track.title)
expect(embed.data.footer?.text).toContain(fakeUser.tag)
diff --git a/packages/bot/src/utils/general/responseEmbeds/buildTrackEmbed.ts b/packages/bot/src/utils/general/responseEmbeds/buildTrackEmbed.ts
index 59276af5f..3ce20b4d2 100644
--- a/packages/bot/src/utils/general/responseEmbeds/buildTrackEmbed.ts
+++ b/packages/bot/src/utils/general/responseEmbeds/buildTrackEmbed.ts
@@ -1,6 +1,7 @@
import { EmbedBuilder } from 'discord.js'
import type { User } from 'discord.js'
import type { Track } from 'discord-player'
+import type { TrackMetadata } from '@lucky/shared/types'
import { detectSource } from '../../music/nowPlayingEmbed'
import { trackSource } from '../../music/trackFields'
import { formatDurationClock } from '../formatDuration'
@@ -14,6 +15,8 @@ export type TrackData = {
thumbnail?: string
duration?: string
source?: string | null
+ /** Why autoplay picked this track, when present. */
+ recommendationReason?: string
}
const KIND_LABELS: Record
+ {track.recommendationReason} +
+ ) : null} diff --git a/packages/frontend/src/pages/Music.tsx b/packages/frontend/src/pages/Music.tsx index 952796e13..5ec4440e5 100644 --- a/packages/frontend/src/pages/Music.tsx +++ b/packages/frontend/src/pages/Music.tsx @@ -157,7 +157,9 @@ function NowPlayingHero({ onVolumeChange: (v: number) => void }) { const { t } = useTranslation() - const currentTrack = state.tracks[0] + // Prefer the live currentTrack from SSE; fall back to the head of the + // upcoming queue for older state payloads that only filled tracks[]. + const currentTrack = state.currentTrack ?? state.tracks[0] if (!currentTrack) { return ( @@ -208,9 +210,16 @@ function NowPlayingHero({+
{currentTrack.author || t('music.unknown')}
+ {currentTrack.recommendationReason ? ( ++ {currentTrack.recommendationReason} +
+ ) : ( + + )}