feat: M3.5 polish — genre splitting + radio button surface
Two carry-overs from M3 verification, bundled with the search-input
fix already on dev (b7a59a9).
1. Genre splitting in BuildSessionVector
The library has many tracks whose genre tag is a denormalized
multi-genre string ("Indie Pop; Pop; Alternative Pop"). Reading them
as one opaque tag means a single-genre "Pop" track and a multi-genre
track listing Pop both fail to share the Pop key, so the tags axis
in similarity scoring (weight 0.7!) returned 0 in nearly all cases
on real libraries. Result: contextual scoring couldn't differentiate.
Add splitGenres() that splits on ; and , and trims whitespace;
strings without a delimiter come back as a single-element slice
(so the existing single-genre cases keep working unchanged).
Concatenated-without-separator output ("ElectronicComplextroGlitch
Hop") stays opaque — that needs a genre dictionary, out of scope.
2. Play-radio button on TrackRow
playRadio() was only wired to the click handler on /search and
/search/tracks, leaving /library/liked, album pages, and search
results' inner clicks with no way to start a radio. Adds a small
radio button to TrackRow (between LikeButton and the +queue button).
Click → playRadio(track.id), with stopPropagation so the row's own
activate handler doesn't also fire.
Tests:
- 3 new sessionvector tests: TestSplitGenres, multi-genre semicolon
split, no-separator stays opaque. Existing single-genre tests pass
unchanged.
- 1 new TrackRow test: radio button calls playRadio with track id and
does NOT trigger row play.
Verified locally: go -short -race ./... clean, golangci-lint clean,
svelte-check 0/0, 175 vitest tests (was 174), web build succeeds.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type { TrackRef } from '$lib/api/types';
|
||||
import { formatDuration } from '$lib/media/duration';
|
||||
import { playQueue, enqueueTrack } from '$lib/player/store.svelte';
|
||||
import { playQueue, enqueueTrack, playRadio } from '$lib/player/store.svelte';
|
||||
import LikeButton from './LikeButton.svelte';
|
||||
|
||||
type PlayHandler = (tracks: TrackRef[], index: number) => void;
|
||||
@@ -34,6 +34,11 @@
|
||||
e.stopPropagation();
|
||||
enqueueTrack(track);
|
||||
}
|
||||
|
||||
function onRadioClick(e: MouseEvent) {
|
||||
e.stopPropagation();
|
||||
playRadio(track.id);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -42,13 +47,20 @@
|
||||
aria-label={track.title}
|
||||
onclick={onRowClick}
|
||||
onkeydown={onRowKey}
|
||||
class="grid w-full cursor-pointer grid-cols-[32px_1fr_auto_auto_auto] items-center gap-4 px-3 py-2 text-left text-sm odd:bg-surface/50 hover:bg-surface focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent"
|
||||
class="grid w-full cursor-pointer grid-cols-[32px_1fr_auto_auto_auto_auto] items-center gap-4 px-3 py-2 text-left text-sm odd:bg-surface/50 hover:bg-surface focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent"
|
||||
>
|
||||
<span class="text-right tabular-nums text-text-secondary">
|
||||
{track.track_number ?? '—'}
|
||||
</span>
|
||||
<span class="truncate">{track.title}</span>
|
||||
<LikeButton entityType="track" entityId={track.id} />
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Play radio from this track"
|
||||
title="Play radio"
|
||||
onclick={onRadioClick}
|
||||
class="rounded p-1 text-text-secondary hover:text-text-primary"
|
||||
>📻</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Add to queue"
|
||||
|
||||
@@ -5,7 +5,8 @@ import { readable } from 'svelte/store';
|
||||
|
||||
vi.mock('$lib/player/store.svelte', () => ({
|
||||
playQueue: vi.fn(),
|
||||
enqueueTrack: vi.fn()
|
||||
enqueueTrack: vi.fn(),
|
||||
playRadio: vi.fn()
|
||||
}));
|
||||
|
||||
vi.mock('$lib/api/likes', () => ({
|
||||
@@ -24,7 +25,7 @@ vi.mock('@tanstack/svelte-query', async (orig) => {
|
||||
});
|
||||
|
||||
import TrackRow from './TrackRow.svelte';
|
||||
import { playQueue, enqueueTrack } from '$lib/player/store.svelte';
|
||||
import { playQueue, enqueueTrack, playRadio } from '$lib/player/store.svelte';
|
||||
|
||||
const tracks: TrackRef[] = [
|
||||
{
|
||||
@@ -91,4 +92,11 @@ describe('TrackRow', () => {
|
||||
expect(enqueueTrack).toHaveBeenCalledWith(tracks[0]);
|
||||
expect(playQueue).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('radio button calls playRadio with track id and does NOT trigger row play', async () => {
|
||||
render(TrackRow, { props: { tracks, index: 0 } });
|
||||
await fireEvent.click(screen.getByRole('button', { name: /play radio/i }));
|
||||
expect(playRadio).toHaveBeenCalledWith('t1');
|
||||
expect(playQueue).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user