Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/bot/src/lastfm/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export {
getSimilarTracks,
getTagTopTracks,
getLovedTracks,
getArtistTopTracks,
isLastFmInvalidSessionError,
normalizeLastFmArtist,
normalizeLastFmTitle,
Expand Down
29 changes: 29 additions & 0 deletions packages/bot/src/lastfm/lastFmApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,3 +320,32 @@ export async function getLovedTracks(
return []
}
}

export async function getArtistTopTracks(
artist: string,
limit = 10,
): Promise<{ artist: string; title: string }[]> {
const config = getApiConfig()
if (!config) return []
try {
const response = await fetch(
`${API_BASE}?method=artist.getTopTracks&artist=${encodeURIComponent(artist)}&limit=${limit}&format=json&api_key=${config.apiKey}`,
)
const data = (await response.json()) as {
toptracks?: {
track?: Array<{
name: string
artist: { name: string }
}>
}
}
return (data.toptracks?.track ?? []).map((t) => ({
artist: t.artist.name,
title: t.name,
}))
} catch (err) {
logAndSwallow(err, 'lastfm.getArtistTopTracks', { artist })
return []
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,72 @@ describe('implicit feedback', () => {
const bigMap: Record<string, { type: string; updatedAt: number }> = {}
for (let i = 0; i < 201; i++) {
bigMap[`track${i}::artist`] = { type: 'implicit_like', updatedAt: i }

describe('getPreferredArtistNames', () => {
it('returns preferred artist names from Prisma', async () => {
const mockDb = {
userArtistPreference: {
findMany: jest.fn().mockResolvedValue([
{ artistName: 'Artist A' },
{ artistName: 'Artist B' },
]),
},
}
jest.spyOn(module, 'getPrismaClient').mockReturnValue(mockDb as any)

const result = await service.getPreferredArtistNames('guild-1', 'user-1')

expect(result).toEqual(new Set(['Artist A', 'Artist B']))
expect(mockDb.userArtistPreference.findMany).toHaveBeenCalledWith({
where: { discordUserId: 'user-1', guildId: 'guild-1', preference: 'prefer' },
select: { artistName: true },
})
})

it('returns empty set when no preferences found', async () => {
const mockDb = {
userArtistPreference: {
findMany: jest.fn().mockResolvedValue([]),
},
}
jest.spyOn(module, 'getPrismaClient').mockReturnValue(mockDb as any)

const result = await service.getPreferredArtistNames('guild-1', 'user-1')

expect(result).toEqual(new Set())
})

it('trims artist names and filters empty values', async () => {
const mockDb = {
userArtistPreference: {
findMany: jest.fn().mockResolvedValue([
{ artistName: ' Artist A ' },
{ artistName: ' ' },
{ artistName: 'Artist B' },
]),
},
}
jest.spyOn(module, 'getPrismaClient').mockReturnValue(mockDb as any)

const result = await service.getPreferredArtistNames('guild-1', 'user-1')

expect(result).toEqual(new Set(['Artist A', 'Artist B']))
})

it('returns empty set on Prisma error', async () => {
const mockDb = {
userArtistPreference: {
findMany: jest.fn().mockRejectedValue(new Error('DB error')),
},
}
jest.spyOn(module, 'getPrismaClient').mockReturnValue(mockDb as any)

const result = await service.getPreferredArtistNames('guild-1', 'user-1')

expect(result).toEqual(new Set())
})
})

}
getMock.mockResolvedValue(JSON.stringify(bigMap))
setexMock.mockResolvedValue('OK')
Expand Down
16 changes: 16 additions & 0 deletions packages/bot/src/services/musicRecommendation/feedbackService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,22 @@ export class RecommendationFeedbackService {
async getImplicitLikeKeys(userId: string): Promise<Set<string>> {
return this.getImplicitKeysByType(userId, 'implicit_like')
}

async getPreferredArtistNames(
guildId: string,
userId: string,
): Promise<Set<string>> {
try {
const db = getPrismaClient()
const prefs = await db.userArtistPreference.findMany({
where: { discordUserId: userId, guildId, preference: 'prefer' },
select: { artistName: true },
})
return new Set(prefs.map((p) => p.artistName.trim()).filter(Boolean))
} catch {
return new Set<string>()
}
}
}

export const recommendationFeedbackService = new RecommendationFeedbackService(
Expand Down
Loading
Loading