# 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`](www/music.html:1) page currently only searches tracks via [`searchTracks()`](www/js/greyscale-api.mjs:125). The greyscale standalone app ([`greyscale/src/app.js`](greyscale/src/app.js:1)) already supports searching albums and artists with [`searchAlbums()`](greyscale/src/api.js:249) and [`searchArtists()`](greyscale/src/api.js:268), 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.

```mermaid
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

```javascript
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)

```mermaid
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`](greyscale/src/api.js:1) to [`www/js/greyscale-api.mjs`](www/js/greyscale-api.mjs:1):

- **Helper functions** (module-level):
  - `findSearchSection(source, key, visited)` — recursive response normalizer
  - `buildSearchResponse(section)` — extract items/limit/offset/total
  
- **Instance methods on `GreyscaleAPI`**:
  - `normalizeSearchResponse(data, key)` — wraps findSearchSection + buildSearchResponse
  - `prepareTrack(track)` — normalize track fields
  - `prepareAlbum(album)` — normalize album fields  
  - `prepareArtist(artist)` — normalize artist fields
  - `deduplicateAlbums(albums)` — remove duplicate albums
  - `searchAlbums(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 + tracks
  - `getArtistPictureUrl(id, size)` — artist image URL builder

- **Update existing `searchTracks()`** to use `normalizeSearchResponse` + `prepareTrack` pipeline for consistency

### 2. UI Layer — music.html Changes

#### 2.1 New State Variables

```javascript
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):
```html
<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)` — returns `artist` | `album` | `track`
- `renderUnifiedResults(state)` — renders all three sections in intent-based order
- `renderArtistSection(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 existing `renderMusicResults` logic)
- `drillDownToArtist(id)` — fetches artist detail, pushes current view to stack, renders artist view
- `drillDownToAlbum(id)` — fetches album detail, pushes current view to stack, renders album view
- `navigateBack()` — pops view stack, restores previous results

#### 2.5 Refactored `runMusicSearch()`

```javascript
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:
```html
<input id="musicSearchInput" type="text" placeholder="Search songs..." required />
```
To:
```html
<input id="musicSearchInput" type="text" placeholder="Search songs, albums, artists..." required />
```
