fix: M3.5 polish — genre splitting, radio button, search focus #24

Merged
bvandeusen merged 262 commits from dev into main 2026-04-28 00:04:14 -04:00
bvandeusen commented 2026-04-27 23:50:26 -04:00 (Migrated from git.fabledsword.com)

Three carry-over fixes from the M3 milestone gate verification, bundled into one focused PR.

What this ships

1. Genre splitting in BuildSessionVector

The library has many tracks whose tracks.genre is a denormalized multi-genre string like "Indie Pop; Pop; Alternative Pop" or "Boom Bap; Downtempo; Hip Hop; Lo-Fi". The previous code treated the whole string as a single opaque tag, so:

  • Single-genre "Pop" track and multi-genre "Indie Pop; Pop; Alternative Pop" track failed to share the Pop tag
  • Tags axis in similarity scoring (weight 0.7!) returned 0 for nearly all comparisons
  • Result: contextual radio recommendations were indistinguishable from weighted random shuffle

internal/recommendation/sessionvector.go now exports splitGenres() that splits on ; and ,, trims whitespace, and drops empty fragments. Single-genre strings still return as a one-element slice, so existing single-genre tracks ("Pop", "Rock", etc.) work unchanged. Concatenated-without-separator strings ("ElectronicComplextroGlitch Hop") stay as one opaque tag — needs a genre dictionary, deferred.

3 new tests: TestSplitGenres (table-driven), TestBuildSessionVector_MultiGenreSplitsOnSemicolon, TestBuildSessionVector_NoSeparatorStaysOpaque. All existing tests pass unchanged.

2. Play-radio button on TrackRow

playRadio() was only wired to track-click in /search and /search/tracks. /library/liked, album pages, and any search-results inner-click had no way to start a radio.

web/src/lib/components/TrackRow.svelte gains a small 📻 button between the LikeButton and the +-queue button. Click handler calls playRadio(track.id) with stopPropagation so the row's row-click activator doesn't also fire.

1 new test: radio button calls playRadio with track id and does NOT trigger row play.

3. Search field focus loss (already on dev as b7a59a9)

SvelteKit's goto() resets focus by default on every navigation. Debounced typeahead-style URL updates blurred the search input every 250ms. Fix: pass keepFocus: true to both goto() calls in SearchInput.svelte::navigate(). Tests updated to assert the new arg.

Test plan

  • go test -short -race ./... — all 13 packages pass
  • golangci-lint run ./... — clean
  • cd web && npm run check — 0 errors / 0 warnings
  • cd web && npm test175 tests across 29 files (was 174)
  • cd web && npm run build — succeeds
  • Manual: search field stays focused while typing across debounce; radio button on /library/liked and album pages triggers radio; radio recommendations now visibly differ from shuffle on libraries with semicolon-separated multi-genre tracks

Notes

  • Out of scope for this PR: scanner-side genre normalization (write-time fix), AlbumCard radio button, the "no-separator" parsing case ("ElectronicComplextroGlitch Hop" and friends — needs a genre dictionary, deferred to M4).
  • Existing data compatibility: contextual_likes captured before this change have single-string tags; once the new code is deployed, freshly-built session vectors will have multi-tag entries. Old likes won't match new sessions where the genre string differs. Acceptable: the manual-testing corpus is tiny and gets rebuilt naturally by continued listening.
  • No schema changes, no new migrations.

🤖 Generated with Claude Code

Three carry-over fixes from the M3 milestone gate verification, bundled into one focused PR. ## What this ships ### 1. Genre splitting in `BuildSessionVector` The library has many tracks whose `tracks.genre` is a denormalized multi-genre string like `"Indie Pop; Pop; Alternative Pop"` or `"Boom Bap; Downtempo; Hip Hop; Lo-Fi"`. The previous code treated the whole string as a single opaque tag, so: - Single-genre `"Pop"` track and multi-genre `"Indie Pop; Pop; Alternative Pop"` track failed to share the `Pop` tag - Tags axis in similarity scoring (weight **0.7**!) returned `0` for nearly all comparisons - Result: contextual `radio` recommendations were indistinguishable from weighted random shuffle `internal/recommendation/sessionvector.go` now exports `splitGenres()` that splits on `;` and `,`, trims whitespace, and drops empty fragments. Single-genre strings still return as a one-element slice, so existing single-genre tracks (`"Pop"`, `"Rock"`, etc.) work unchanged. Concatenated-without-separator strings (`"ElectronicComplextroGlitch Hop"`) stay as one opaque tag — needs a genre dictionary, deferred. 3 new tests: `TestSplitGenres` (table-driven), `TestBuildSessionVector_MultiGenreSplitsOnSemicolon`, `TestBuildSessionVector_NoSeparatorStaysOpaque`. All existing tests pass unchanged. ### 2. Play-radio button on `TrackRow` `playRadio()` was only wired to track-click in `/search` and `/search/tracks`. `/library/liked`, album pages, and any search-results inner-click had no way to start a radio. `web/src/lib/components/TrackRow.svelte` gains a small 📻 button between the LikeButton and the `+`-queue button. Click handler calls `playRadio(track.id)` with `stopPropagation` so the row's row-click activator doesn't also fire. 1 new test: `radio button calls playRadio with track id and does NOT trigger row play`. ### 3. Search field focus loss (already on dev as `b7a59a9`) SvelteKit's `goto()` resets focus by default on every navigation. Debounced typeahead-style URL updates blurred the search input every 250ms. Fix: pass `keepFocus: true` to both `goto()` calls in `SearchInput.svelte::navigate()`. Tests updated to assert the new arg. ## Test plan - [x] `go test -short -race ./...` — all 13 packages pass - [x] `golangci-lint run ./...` — clean - [x] `cd web && npm run check` — 0 errors / 0 warnings - [x] `cd web && npm test` — **175 tests** across 29 files (was 174) - [x] `cd web && npm run build` — succeeds - [ ] Manual: search field stays focused while typing across debounce; radio button on `/library/liked` and album pages triggers radio; radio recommendations now visibly differ from shuffle on libraries with semicolon-separated multi-genre tracks ## Notes - **Out of scope for this PR**: scanner-side genre normalization (write-time fix), `AlbumCard` radio button, the "no-separator" parsing case (`"ElectronicComplextroGlitch Hop"` and friends — needs a genre dictionary, deferred to M4). - **Existing data compatibility**: contextual_likes captured before this change have single-string tags; once the new code is deployed, freshly-built session vectors will have multi-tag entries. Old likes won't match new sessions where the genre string differs. Acceptable: the manual-testing corpus is tiny and gets rebuilt naturally by continued listening. - **No schema changes**, no new migrations. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: bvandeusen/minstrel#24