import { eq } from 'drizzle-orm'; import { describe, expect, it } from 'vitest'; import { db, schema } from '../src/db'; import { LISTEN_HISTORY_LIMIT, applyPollResult, getRecentPlays } from '../src/services/nowPlaying'; import { createUser, useTestApp } from './helpers'; const app = useTestApp(); const playing = (title: string) => ({ track: { title, artist: 'Joy Division' }, isPlaying: true, progressMs: 0 }); const titles = (plays: { track: { title: string } }[]) => plays.map((p) => p.track.title); describe('listening history', () => { it('records one play per track change, not per poll', async () => { const { user } = await createUser(app(), 'bob'); for (const title of ['Disorder', 'Disorder', 'Disorder', 'Shadowplay', 'Shadowplay']) { await applyPollResult(user.id, 'plex', playing(title)); } expect(await db.select().from(schema.listens)).toHaveLength(2); }); it("excludes what's playing right now from recent plays, but includes it once stopped", async () => { const { user } = await createUser(app(), 'bob'); await applyPollResult(user.id, 'plex', playing('Disorder')); await applyPollResult(user.id, 'plex', playing('Shadowplay')); expect(titles(await getRecentPlays(user.id))).toEqual(['Disorder']); await applyPollResult(user.id, 'plex', null); expect(titles(await getRecentPlays(user.id))).toEqual(['Shadowplay', 'Disorder']); }); it('pausing and resuming is the same play; coming back after a long break is a new one', async () => { const { user } = await createUser(app(), 'bob'); await applyPollResult(user.id, 'plex', playing('Atmosphere')); await applyPollResult(user.id, 'plex', { ...playing('Atmosphere'), isPlaying: false }); await applyPollResult(user.id, 'plex', playing('Atmosphere')); expect(await db.select().from(schema.listens)).toHaveLength(1); await applyPollResult(user.id, 'plex', { ...playing('Atmosphere'), isPlaying: false }); await db.update(schema.nowPlaying).set({ updatedAt: new Date(Date.now() - 11 * 60_000) }).where(eq(schema.nowPlaying.userId, user.id)); await applyPollResult(user.id, 'plex', playing('Atmosphere')); expect(await db.select().from(schema.listens)).toHaveLength(2); }); it(`keeps only the latest ${LISTEN_HISTORY_LIMIT} plays per user`, async () => { const { user } = await createUser(app(), 'bob'); const other = await createUser(app(), 'carol'); await applyPollResult(other.user.id, 'plex', playing('Not bob')); for (let i = 0; i < LISTEN_HISTORY_LIMIT + 5; i++) await applyPollResult(user.id, 'plex', playing(`Track ${i}`)); const rows = await db.select().from(schema.listens).where(eq(schema.listens.userId, user.id)); expect(rows).toHaveLength(LISTEN_HISTORY_LIMIT); expect(await db.select().from(schema.listens).where(eq(schema.listens.userId, other.user.id))).toHaveLength(1); // newest kept, oldest dropped const recent = await getRecentPlays(user.id, 3); expect(titles(recent)).toEqual([`Track ${LISTEN_HISTORY_LIMIT + 3}`, `Track ${LISTEN_HISTORY_LIMIT + 2}`, `Track ${LISTEN_HISTORY_LIMIT + 1}`]); }); it('shows on the profile, newest first, capped at 10', async () => { const { user } = await createUser(app(), 'bob'); for (let i = 0; i < 12; i++) await applyPollResult(user.id, 'plex', playing(`Track ${i}`)); const profile = (await app().inject({ url: '/users/bob' })).json(); expect(profile.nowPlaying.track.title).toBe('Track 11'); expect(profile.recent).toHaveLength(10); expect(profile.recent[0]).toMatchObject({ track: { title: 'Track 10' }, source: 'plex', playedAt: expect.any(String) }); }); it('is deleted with the account', async () => { const { user, token } = await createUser(app(), 'bob'); await applyPollResult(user.id, 'plex', playing('Disorder')); await app().inject({ method: 'DELETE', url: '/me', headers: { authorization: `Bearer ${token}` } }); expect(await db.select().from(schema.listens)).toHaveLength(0); }); }); describe('attribution', () => { it('links Last.fm-sourced plays back to the Last.fm profile (required by Last.fm)', async () => { const { user } = await createUser(app(), 'bob'); await db.insert(schema.connections).values({ userId: user.id, provider: 'lastfm', externalUserId: 'RJ', displayName: 'RJ' }); await applyPollResult(user.id, 'lastfm', playing('Disorder'), { sourceUrl: 'https://www.last.fm/user/RJ' }); const profile = (await app().inject({ url: '/users/bob' })).json(); expect(profile.nowPlaying.sourceUrl).toBe('https://www.last.fm/user/RJ'); expect(profile.attribution).toEqual({ lastfm: 'https://www.last.fm/user/RJ' }); }); });