Add recently played to profiles
A play is recorded when a new track starts (pausing and resuming doesn't count; returning after 10+ minutes does). Profiles show the last 10, excluding the track playing now; 50 are kept per user. Recent plays are free on every plan; the paid flag is renamed to fullHistory. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MzFywujuH9TiuXbr9vjoX8
This commit is contained in:
@@ -17,7 +17,7 @@ function assertTestTargets() {
|
||||
|
||||
export async function resetState() {
|
||||
assertTestTargets();
|
||||
await sql`truncate users, tracks, track_links, sessions, follows, connections, now_playing, featured_songs restart identity cascade`;
|
||||
await sql`truncate users, tracks, track_links, sessions, follows, connections, now_playing, featured_songs, listens restart identity cascade`;
|
||||
await redis.flushdb();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user