14 KiB
Music Search Expansion — Artists & Albums in www/music.html
Chosen Approach: Option B — Unified Sectioned Results with Intent-Aware Ordering
Problem
The www/music.html page currently only searches tracks via searchTracks(). The greyscale standalone app (greyscale/src/app.js) already supports searching albums and artists with searchAlbums() and searchArtists(), plus detail pages for each. We need to bring this capability into the integrated music page.
Design: Unified Sectioned Results
Show all result types in a single scrollable view — artists, albums, and tracks — grouped under section headers. The order and prominence of sections adapts based on what the user is likely searching for.
flowchart TB
SearchBox[Search Input] -->|query| Intent[Classify Query Intent]
Intent -->|parallel| TracksAPI[searchTracks]
Intent -->|parallel| AlbumsAPI[searchAlbums]
Intent -->|parallel| ArtistsAPI[searchArtists]
TracksAPI --> State[musicSearchState]
AlbumsAPI --> State
ArtistsAPI --> State
State --> Render[renderUnifiedResults]
Intent -->|intent hint| Render
Render --> Section1[Primary section - full display]
Render --> Section2[Secondary section - compact strip]
Render --> Section3[Tertiary section - compact strip or hidden]
Section1 -->|click artist| DrillDown1[Replace results with artist top tracks + albums]
Section1 -->|click album| DrillDown2[Replace results with album tracks]
Intent-Aware Section Ordering
The key insight: the structure of the query reveals what the user wants. A single word is likely an artist name. Multiple words with a recognizable song-like pattern are likely a track search. We can use simple heuristics to reorder sections so the most relevant results appear first.
Intent Classification Heuristics
classifySearchIntent(query, results) -> 'artist' | 'album' | 'track'
Phase 1 — Query structure analysis (before results arrive):
| Pattern | Likely Intent | Example |
|---|---|---|
| Single word or short name | Artist | radiohead, beyonce, tool |
Two words that look like artist + song |
Track | radiohead creep, nirvana lithium |
| Words with common album markers | Album | ok computer, dark side of the moon |
| Quoted string | Exact track title | "karma police" |
| Very long query with multiple words | Track | everything in its right place |
Phase 2 — Results-based refinement (after results arrive):
| Signal | Adjustment |
|---|---|
| Artist results have a high-confidence exact name match | Boost artist intent |
| No artist results returned | Suppress artist section |
| Album results have exact title match | Boost album intent |
| Track results have exact title match | Boost track intent |
| Only one result type has items | Show only that section prominently |
Implementation
function classifySearchIntent(query, results) {
const q = query.trim().toLowerCase();
const words = q.split(/\s+/);
const { artists, albums, tracks } = results;
// Exact artist name match is strongest signal
const exactArtist = artists.some(a =>
a.name?.toLowerCase() === q
);
if (exactArtist && words.length <= 3) return 'artist';
// Single word — almost always an artist search
if (words.length === 1 && artists.length > 0) return 'artist';
// Exact album title match
const exactAlbum = albums.some(a =>
a.title?.toLowerCase() === q
);
if (exactAlbum) return 'album';
// If tracks have a strong match and query is multi-word, it is a track
const exactTrack = tracks.some(t =>
t.title?.toLowerCase() === q
);
if (exactTrack && words.length >= 2) return 'track';
// Default: if 2+ words, lean toward tracks; otherwise artist
return words.length >= 2 ? 'track' : 'artist';
}
Section Order by Intent
| Intent | Section Order | Primary Display | Secondary Display |
|---|---|---|---|
| artist | Artists → Albums → Tracks | Artist cards full-width | Albums as horizontal strip, tracks as list |
| album | Albums → Artists → Tracks | Album cards full-width | Artists as horizontal chips, tracks as list |
| track | Tracks → Artists → Albums | Track list full-width | Artists as horizontal chips, albums as horizontal strip |
Visual Layout
When intent = artist (e.g., searching "radiohead"):
┌─────────────────────────────────────────────┐
│ [Search: "radiohead" ] [Search] │
│ Found 17 results. │
├─────────────────────────────────────────────┤
│ ARTISTS │
│ ┌──────────┐ ┌──────────┐ │
│ │ │ │ │ │
│ │ pic │ │ pic │ │
│ │ │ │ │ │
│ │Radiohead │ │On a │ │
│ │ │ │Friday │ │
│ └──────────┘ └──────────┘ │
│ │
│ ALBUMS ──scroll► │
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │ covr │ │ covr │ │ covr │ │ covr │ │
│ │OK Com│ │Bends │ │Kid A │ │Amnes│ │
│ └──────┘ └──────┘ └──────┘ └──────┘ │
│ │
│ TRACKS │
│ ┌──┐ Creep 3:56 [+P][+Q][+A] │
│ └──┘ Radiohead — Pablo Honey │
│ ┌──┐ Karma Police 4:22 [+P][+Q][+A] │
│ └──┘ Radiohead — OK Computer │
└─────────────────────────────────────────────┘
When intent = track (e.g., searching "radiohead creep"):
┌─────────────────────────────────────────────┐
│ [Search: "radiohead creep" ] [Search] │
│ Found 8 results. │
├─────────────────────────────────────────────┤
│ TRACKS │
│ ┌──┐ Creep 3:56 [+P][+Q][+A] │
│ └──┘ Radiohead — Pablo Honey │
│ ┌──┐ Creep (acoustic) 4:01 [+P][+Q][+A] │
│ └──┘ Radiohead — Live Sessions │
│ │
│ ARTISTS │
│ ┌─────┐ │
│ │ pic │ Radiohead │
│ └─────┘ │
│ │
│ ALBUMS ──scroll► │
│ ┌──────┐ ┌──────┐ │
│ │ covr │ │ covr │ │
│ │Pablo │ │Creep │ │
│ │Honey │ │Singl │ │
│ └──────┘ └──────┘ │
└─────────────────────────────────────────────┘
Drill-Down Behavior
When a user clicks an artist card or album card, the results section replaces with that entity's detail:
- Click artist → fetch via
getArtist(id)→ show artist top tracks as a full track list (with +P/+Q/+A buttons), plus their albums as a horizontal strip below - Click album → fetch via
getAlbum(id)→ show album tracks as a full track list (with +P/+Q/+A buttons) - Back button → restore the previous search results (kept in state)
stateDiagram-v2
[*] --> SearchResults: User searches
SearchResults --> ArtistDetail: Click artist card
SearchResults --> AlbumDetail: Click album card
ArtistDetail --> AlbumDetail: Click album in artist view
ArtistDetail --> SearchResults: Back button
AlbumDetail --> SearchResults: Back button
AlbumDetail --> ArtistDetail: Back button - if came from artist
Implementation Plan
1. API Layer — Port to greyscale-api.mjs
Port from greyscale/src/api.js to www/js/greyscale-api.mjs:
-
Helper functions (module-level):
findSearchSection(source, key, visited)— recursive response normalizerbuildSearchResponse(section)— extract items/limit/offset/total
-
Instance methods on
GreyscaleAPI:normalizeSearchResponse(data, key)— wraps findSearchSection + buildSearchResponseprepareTrack(track)— normalize track fieldsprepareAlbum(album)— normalize album fieldsprepareArtist(artist)— normalize artist fieldsdeduplicateAlbums(albums)— remove duplicate albumssearchAlbums(query)—/search/?al=<query>searchArtists(query)—/search/?a=<query>getAlbum(id)—/album/?id=<id>returns{ album, tracks }getArtist(id)—/artist/?id=<id>+/artist/?f=<id>returns artist + albums + eps + tracksgetArtistPictureUrl(id, size)— artist image URL builder
-
Update existing
searchTracks()to usenormalizeSearchResponse+prepareTrackpipeline for consistency
2. UI Layer — music.html Changes
2.1 New State Variables
let musicSearchState = {
query: '',
intent: 'track',
tracks: [],
albums: [],
artists: [],
};
let musicViewStack = []; // for back navigation from drill-downs
2.2 New HTML Elements
Add between #musicSearchHeader and #musicResultsSection:
- No new structural elements needed — the unified view renders entirely into
#musicResultsList
Add a back button inside #musicSearchHeader (hidden by default, shown during drill-down):
<button id="musicBackBtn" class="btn" type="button" style="display:none;">← Back</button>
2.3 New CSS
.musicSectionHeader— section label styling (ARTISTS, ALBUMS, TRACKS).musicArtistStrip— horizontal scrollable artist card container.musicArtistCard— individual artist card (picture + name).musicAlbumStrip— horizontal scrollable album card container.musicAlbumCard— individual album card (cover + title + artist).musicSectionDivider— visual separator between sections
2.4 New JavaScript Functions
classifySearchIntent(query, results)— returnsartist|album|trackrenderUnifiedResults(state)— renders all three sections in intent-based orderrenderArtistSection(artists, prominent)— renders artist cards (full or compact strip)renderAlbumSection(albums, prominent)— renders album cards (full or compact strip)renderTrackSection(tracks)— renders track list (reuses existingrenderMusicResultslogic)drillDownToArtist(id)— fetches artist detail, pushes current view to stack, renders artist viewdrillDownToAlbum(id)— fetches album detail, pushes current view to stack, renders album viewnavigateBack()— pops view stack, restores previous results
2.5 Refactored runMusicSearch()
async function runMusicSearch(query) {
const trimmed = query.trim();
if (!trimmed) return;
setMusicSearchLoading(true);
setMusicStatus('Searching...', 'neutral');
try {
const [tracksRes, albumsRes, artistsRes] = await Promise.all([
musicApi.searchTracks(trimmed).catch(() => ({ items: [] })),
musicApi.searchAlbums(trimmed).catch(() => ({ items: [] })),
musicApi.searchArtists(trimmed).catch(() => ({ items: [] })),
]);
musicSearchState = {
query: trimmed,
intent: classifySearchIntent(trimmed, {
tracks: tracksRes.items || [],
albums: albumsRes.items || [],
artists: artistsRes.items || [],
}),
tracks: tracksRes.items || [],
albums: albumsRes.items || [],
artists: artistsRes.items || [],
};
// Also update musicTracks for playlist/queue compatibility
musicTracks = musicSearchState.tracks;
musicViewStack = [];
setResultsContext('search');
renderUnifiedResults(musicSearchState);
const total = musicSearchState.tracks.length
+ musicSearchState.albums.length
+ musicSearchState.artists.length;
setMusicStatus('Found ' + total + ' results.', 'ok');
} catch (error) {
// ... error handling
} finally {
setMusicSearchLoading(false);
}
}
3. Playlist/Queue Integration
The existing track action buttons (+P, +Q, +A) continue to work exactly as before for track rows. For album and artist cards:
- Album card → click drills down to album tracks (which then have +P/+Q/+A buttons)
- Artist card → click drills down to artist top tracks (which then have +P/+Q/+A buttons)
- Album card +Q button → queue all album tracks directly (fetches album, adds all tracks to queue)
- Artist card +Q button → not shown (too many tracks to blindly queue)
4. Search Placeholder Update
Change from:
<input id="musicSearchInput" type="text" placeholder="Search songs..." required />
To:
<input id="musicSearchInput" type="text" placeholder="Search songs, albums, artists..." required />