music: near-realtime now playing (currently-playing every 5 s while playing, 30 s idle backoff, Retry-After, recent plays ~10 s after a track change, holder-based lease) and a public SSE stream /api/listening/stream feeding the listening card live with client-side progress
All checks were successful
Deploy production / deploy (push) Successful in 2m35s
All checks were successful
Deploy production / deploy (push) Successful in 2m35s
This commit is contained in:
80
src/lib/music/now-playing.test.ts
Normal file
80
src/lib/music/now-playing.test.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
detectChange,
|
||||
estimateProgress,
|
||||
formatClock,
|
||||
nextDelay,
|
||||
sseEvent,
|
||||
toLive,
|
||||
type PlayerSample
|
||||
} from './now-playing';
|
||||
|
||||
const track = (id: string) => ({
|
||||
spotifyId: id,
|
||||
name: `Song ${id}`,
|
||||
durationMs: 200_000,
|
||||
explicit: false,
|
||||
artists: [
|
||||
{ spotifyId: null, name: 'A' },
|
||||
{ spotifyId: null, name: 'B' }
|
||||
],
|
||||
album: {
|
||||
spotifyId: null,
|
||||
name: 'Album',
|
||||
imageUrl: 'https://i/640',
|
||||
imageUrlSmall: 'https://i/300',
|
||||
releaseDate: null
|
||||
}
|
||||
});
|
||||
const sample = (over: Partial<PlayerSample> = {}): PlayerSample => ({
|
||||
trackId: 1,
|
||||
track: track('x'),
|
||||
isPlaying: true,
|
||||
progressMs: 10_000,
|
||||
sampledAt: 1_000_000,
|
||||
...over
|
||||
});
|
||||
|
||||
describe('now playing', () => {
|
||||
it('builds the public payload', () => {
|
||||
expect(toLive(sample())).toEqual({
|
||||
isPlaying: true,
|
||||
track: 'Song x',
|
||||
artists: 'A, B',
|
||||
album: 'Album',
|
||||
image: 'https://i/300',
|
||||
url: 'https://open.spotify.com/track/x',
|
||||
progressMs: 10_000,
|
||||
durationMs: 200_000,
|
||||
sampledAt: 1_000_000
|
||||
});
|
||||
expect(toLive(sample({ track: null, trackId: null }))).toMatchObject({
|
||||
isPlaying: false,
|
||||
track: null
|
||||
});
|
||||
});
|
||||
it('detects track changes, play/pause and seeks, not normal progress', () => {
|
||||
const a = sample();
|
||||
expect(detectChange(null, a).any).toBe(true);
|
||||
expect(detectChange(a, sample({ progressMs: 15_000, sampledAt: 1_005_000 })).any).toBe(false);
|
||||
expect(detectChange(a, sample({ progressMs: 90_000, sampledAt: 1_005_000 })).seek).toBe(true);
|
||||
expect(detectChange(a, sample({ isPlaying: false })).playing).toBe(true);
|
||||
expect(detectChange(a, sample({ track: track('y') })).track).toBe(true);
|
||||
});
|
||||
it('estimates progress between events and clamps to the duration', () => {
|
||||
const l = toLive(sample());
|
||||
expect(estimateProgress(l, 0, 5_000)).toBe(15_000);
|
||||
expect(estimateProgress(l, 0, 10_000_000)).toBe(200_000);
|
||||
expect(estimateProgress({ ...l, isPlaying: false }, 0, 5_000)).toBe(10_000);
|
||||
expect(formatClock(75_000)).toBe('1:15');
|
||||
});
|
||||
it('formats SSE events with the sample age, and backs off when idle', () => {
|
||||
const event = sseEvent(toLive(sample()), 1_002_000);
|
||||
expect(event.startsWith('event: now\ndata: ')).toBe(true);
|
||||
expect(event.endsWith('\n\n')).toBe(true);
|
||||
expect(JSON.parse(event.split('data: ')[1]).ageMs).toBe(2_000);
|
||||
expect(nextDelay(true, 9)).toBe(5_000);
|
||||
expect(nextDelay(false, 0)).toBe(10_000);
|
||||
expect(nextDelay(false, 50)).toBe(30_000);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user