feat(web): add enqueueTrack, enqueueTracks, playRadio to player store
enqueueTrack/enqueueTracks append to the queue without disturbing playback state (no auto-play on append, no state mutation when queue was non-empty). playRadio fetches /api/radio?seed_track=<id> and feeds the response into playQueue. Empty response leaves queue alone.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import type { TrackRef } from '$lib/api/types';
|
||||
import type { TrackRef, RadioResponse } from '$lib/api/types';
|
||||
import { api } from '$lib/api/client';
|
||||
|
||||
export type PlayerState = 'idle' | 'loading' | 'playing' | 'paused' | 'error';
|
||||
export type RepeatMode = 'off' | 'all' | 'one';
|
||||
@@ -196,3 +197,21 @@ export function reportStateFromAudio(
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export function enqueueTrack(t: TrackRef): void {
|
||||
_queue = [..._queue, t];
|
||||
if (_state === 'idle') _index = 0;
|
||||
}
|
||||
|
||||
export function enqueueTracks(ts: TrackRef[]): void {
|
||||
if (ts.length === 0) return;
|
||||
_queue = [..._queue, ...ts];
|
||||
if (_state === 'idle') _index = 0;
|
||||
}
|
||||
|
||||
export async function playRadio(seedTrackId: string): Promise<void> {
|
||||
const resp = await api.get<RadioResponse>(
|
||||
`/api/radio?seed_track=${encodeURIComponent(seedTrackId)}`
|
||||
);
|
||||
if (resp.tracks.length > 0) playQueue(resp.tracks, 0);
|
||||
}
|
||||
|
||||
@@ -279,3 +279,57 @@ describe('player store — shuffle + repeat + audio reports', () => {
|
||||
expect(player.error).toBe('network lost');
|
||||
});
|
||||
});
|
||||
|
||||
import { enqueueTrack, enqueueTracks, playRadio } from './store.svelte';
|
||||
import * as client from '$lib/api/client';
|
||||
|
||||
describe('player store — enqueue + playRadio', () => {
|
||||
test('enqueueTrack appends to a non-empty queue, _index unchanged', () => {
|
||||
playQueue([track('1'), track('2')], 1);
|
||||
enqueueTrack(track('3'));
|
||||
expect(player.queue.length).toBe(3);
|
||||
expect(player.queue[2].id).toBe('3');
|
||||
expect(player.index).toBe(1);
|
||||
});
|
||||
|
||||
test('enqueueTrack on idle (empty queue) sets index=0 and _state stays idle', () => {
|
||||
playQueue([]);
|
||||
expect(player.state).toBe('idle');
|
||||
enqueueTrack(track('1'));
|
||||
expect(player.queue.length).toBe(1);
|
||||
expect(player.index).toBe(0);
|
||||
expect(player.state).toBe('idle');
|
||||
});
|
||||
|
||||
test('enqueueTracks([]) is a no-op', () => {
|
||||
playQueue([track('1')]);
|
||||
enqueueTracks([]);
|
||||
expect(player.queue.length).toBe(1);
|
||||
});
|
||||
|
||||
test('enqueueTracks concatenates non-empty list', () => {
|
||||
playQueue([track('1')]);
|
||||
enqueueTracks([track('2'), track('3')]);
|
||||
expect(player.queue.map((t) => t.id)).toEqual(['1', '2', '3']);
|
||||
});
|
||||
|
||||
test('playRadio fetches /api/radio and feeds tracks into playQueue', async () => {
|
||||
const apiGet = vi.spyOn(client.api, 'get').mockResolvedValueOnce({
|
||||
tracks: [track('seed'), track('next')]
|
||||
});
|
||||
await playRadio('seed');
|
||||
expect(apiGet).toHaveBeenCalledWith('/api/radio?seed_track=seed');
|
||||
expect(player.queue.length).toBe(2);
|
||||
expect(player.index).toBe(0);
|
||||
expect(player.state).toBe('loading');
|
||||
apiGet.mockRestore();
|
||||
});
|
||||
|
||||
test('playRadio with empty response leaves queue alone', async () => {
|
||||
playQueue([track('a')]);
|
||||
const apiGet = vi.spyOn(client.api, 'get').mockResolvedValueOnce({ tracks: [] });
|
||||
await playRadio('seed');
|
||||
expect(player.queue.map((t) => t.id)).toEqual(['a']);
|
||||
apiGet.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user