# Music: Greyscale → Gruuv API Migration Plan

## Goal

Fully replace the Greyscale (Tidal-proxy) music backend used by [`www/music.html`](../www/music.html) with the **Gruuv** Nostr-native protocol. This includes search, browse, streaming, playlists, and a new track upload capability. Greyscale code is removed once the gruuv path is verified.

A backup of the original page is preserved as [`www/music-greyscale.html`](../www/music-greyscale.html).

---

## Background: Why this is an architecture shift, not a swap

The two backends are fundamentally different in nature.

| Aspect | Greyscale (current) | Gruuv (target) |
|---|---|---|
| Catalog source | Tidal proxy REST API over HTTP instance pool | Nostr relays: `kind 36787` track events tagged `t=gruuv` |
| Search | Server endpoint `/search/?s=` | Relay query + client-side text filter on title/artist/album |
| Streaming | DASH/MP3 manifest decoded from instance | Direct Blossom blob URL (the `url` tag) |
| Track ID | Numeric Tidal ID | Addressable coordinate `36787:pubkey:dTag` |
| Album | Rich `/album/?id=` endpoint | Derived by grouping track events on the `album` tag |
| Artist | Rich `/artist/?id=` endpoint | Derived by grouping track events on the `artist` tag |
| Cover art | `resources.tidal.com/images/...` | The `image` tag on the track event |
| Playlists | `kind 30004`, `i` tags `tidal:track:ID` | `kind 34139`, `a` tags `36787:pubkey:dTag`, `t=gruuv` |
| Upload | N/A (read-only Tidal) | Blossom `PUT /upload` + publish `kind 36787` |

### What already exists in the project (reused, not rebuilt)

- [`www/js/init-ndk.mjs`](../www/js/init-ndk.mjs) — provides `subscribe()`, `publishEvent()`, `publishRawEvent()`, `getPubkey()`. The page already wires NDK, relay status UI, and the `ndkEvent` window-event stream.
- [`www/js/blossom-api.mjs`](../www/js/blossom-api.mjs) — provides `calculateSHA256()` and `createBlossomAuth()`, which already builds the **`kind 24242`** authorization event that Gruuv's upload spec requires (see [`gruuv/README.md`](../gruuv/README.md)).
- [`www/js/blossom-ui.mjs`](../www/js/blossom-ui.mjs) — provides `getBlossomServers()` for the active Blossom target.
- [`SimplePlayer`](../www/js/greyscale-player.mjs) — its direct-URL playback path already handles plain audio URLs (Blossom URLs are plain `https`), so playback needs no DASH logic for gruuv.

### Gruuv protocol reference (from `gruuv/`)

`kind 36787` track event tags (per [`gruuv/upload_gruuv.sh`](../gruuv/upload_gruuv.sh) and [`gruuv/README.md`](../gruuv/README.md)):

```
d            stable track id / dTag
url          blossom file url
title        song title
artist       artist name
album        album name
duration     seconds (string)
size         bytes (string)
alt          "Song: <title> - <artist>"
format       mp3 | flac | ...
m            audio mime (audio/mpeg, audio/flac, ...)
x            sha256 hex
t            music
t            gruuv
image        (optional) cover art url
genre        (optional) + t=<genre-lowercase>
released     (optional) date-like
track_number (optional) plain numeric string
disc_number  (optional, omit when disc 1)
```

`kind 34139` playlist event tags:

```
d            playlist id / dTag
title
description
alt          "Playlist: <title>"
t            gruuv
image        (optional)
a            36787:<pubkey>:<dTag>   (repeated, one per track)
```

Default relays observed: `wss://relay.primal.net`, `wss://nos.lol`, `wss://relay.damus.io`. Default Blossom: `https://blossom.primal.net`. (The page's own NDK relay config / Blossom server selection should be used in practice.)

---

## Design strategy: drop-in module replacement

To keep [`www/music.html`](../www/music.html) changes minimal and low-risk, the new gruuv modules will expose the **same method names and return shapes** as the current greyscale modules. Then the migration in `music.html` is mostly an import swap plus targeted fixes for the few places where the data model genuinely differs (IDs, derived albums/artists, upload UI).

```mermaid
graph LR
  subgraph page [music.html UI - mostly unchanged]
    Search[Search box]
    Browse[Album / Artist views]
    Player[SimplePlayer audio]
    Playlists[Playlist panel]
    Upload[NEW Upload panel]
  end

  subgraph mods [New gruuv modules]
    API[gruuv-api.mjs]
    PL[gruuv-playlists.mjs]
    UP[gruuv-upload.mjs]
  end

  subgraph infra [Existing project infra]
    NDK[init-ndk.mjs subscribe publishEvent]
    BLOB[blossom-api.mjs sha256 kind 24242 auth]
    BUI[blossom-ui.mjs getBlossomServers]
  end

  Search --> API
  Browse --> API
  Player --> API
  Playlists --> PL
  Upload --> UP
  API --> NDK
  PL --> NDK
  UP --> BLOB
  UP --> BUI
  UP --> NDK
```

---

## Data model mapping (gruuv track event → music.html track shape)

The UI expects the normalized shape produced by [`GreyscaleAPI.prepareTrack()`](../www/js/greyscale-api.mjs:186):
`{ id, title, artist, duration, cover, albumTitle, raw }`.

Mapping from a `kind 36787` event:

| UI field | Source |
|---|---|
| `id` | addressable coordinate `36787:<pubkey>:<d-tag>` (fallback to event id) |
| `title` | `title` tag |
| `artist` | `artist` tag (fallback "Unknown artist") |
| `duration` | `duration` tag parsed to number |
| `cover` | `image` tag (no Tidal URL transform) |
| `albumTitle` | `album` tag |
| `streamUrl` | `url` tag (used by `getTrackStream`) |
| `raw` | full event for later reference |

`getCoverUrl()` / `getArtistPictureUrl()` become pass-through helpers (return the URL as-is) so existing render code keeps working.

---

## Implementation steps

### Phase 0 — Safety
1. Confirm [`www/music-greyscale.html`](../www/music-greyscale.html) is a faithful backup of the original greyscale page so it can be referenced/restored.

### Phase 1 — Read path (search, browse, stream)
2. Create [`www/js/gruuv-api.mjs`](../www/js/gruuv-api.mjs) exposing the same surface as `GreyscaleAPI`:
   `searchTracks`, `searchAlbums`, `searchArtists`, `getAlbum`, `getArtist`, `getTrackStream`, `getCoverUrl`, `getArtistPictureUrl`, `prepareTrack`.
3. Implement search by subscribing to `kind 36787` with `#t: ['gruuv']`, collecting events until EOSE (with a timeout), then client-side filtering on title/artist/album. Provide a small in-memory cache of fetched track events to back album/artist derivation.
4. Implement `getAlbum` / `getArtist` by grouping the cached/fetched track events on the `album` and `artist` tags respectively (gruuv has no album/artist DB). `getAlbum(id)` returns `{ album, tracks }`; `getArtist(id)` returns `{ ...artist, albums, eps, tracks }` to match existing render expectations.
5. Implement `getTrackStream` to return `{ streamUrl: <url tag>, isDash: false }`. Cover/artwork helpers return the `image` URL directly.

### Phase 2 — Playlists
6. Create [`www/js/gruuv-playlists.mjs`](../www/js/gruuv-playlists.mjs) mirroring the exports of [`greyscale-playlists.mjs`](../www/js/greyscale-playlists.mjs):
   `PLAYLIST_KIND` (now `34139`), `buildPlaylistEvent`, `parsePlaylistEvent`, `isMusicPlaylistEvent`, `createPlaylistIdentifier`, `playlistEventAddress`, and any helper the page imports.
   - Track references use `a` tags of form `36787:<pubkey>:<dTag>` instead of `i`/`tidal:track:` tags.
   - Topic tag becomes `t=gruuv` (keep `t=music` for discoverability if useful).
   - Keep a content JSON cache of track metadata (title/artist/cover/duration) like the greyscale version so playlists render before track events resolve.

### Phase 3 — Upload (new capability)
7. Create [`www/js/gruuv-upload.mjs`](../www/js/gruuv-upload.mjs):
   - `calculateSHA256()` (reuse from blossom-api).
   - Build `kind 24242` auth via `createBlossomAuth('upload', sha256, 'Upload <filename>')`.
   - `PUT <activeBlossomServer>/upload` with the `Authorization: Nostr <base64>` header; fall back to `<server>/<sha256>` for the URL if the response lacks one.
   - Optionally read duration via an `<audio>`/`AudioContext` decode client-side; allow manual title/artist/album entry.
   - Publish `kind 36787` via `publishEvent()` with the full tag set (including `t=music`, `t=gruuv`, `x`, `m`, `format`, `size`, `alt`).

### Phase 4 — Wire into music.html
8. Swap imports: replace [`GreyscaleAPI`](../www/music.html:1371), the greyscale playlists import block ([lines ~1375–1380](../www/music.html:1375)) with gruuv modules. Keep `SimplePlayer` (it works with direct URLs); the DASH branch simply goes unused.
9. Update search handlers ([~3117](../www/music.html:3117), combined search [~4516](../www/music.html:4516)) and detail loaders ([~3837](../www/music.html:3837)–[~3963](../www/music.html:3963)) for the gruuv id format and the grouped album/artist results.
10. Update playlist subscription filter and event handling ([`subscribeAllPlaylists`](../www/music.html:3288), [`handleMusicNdkEvent`](../www/music.html:3299), [`hydratePlaylistFromEvent`](../www/music.html:3245)) to `kind 34139` with `t=gruuv` and `a`-tag track refs.
11. Add an **Upload Track** UI section (file input, metadata fields, progress/status), gated behind authentication (`promptLoginIfNeeded`), wired to `gruuv-upload.mjs`.
12. Update the download ([`fetchDownloadBlobForTrack`](../www/music.html:4342)) and share flows to use the direct Blossom URL instead of Tidal stream manifests.

### Phase 5 — Cleanup & verification
13. Remove greyscale modules ([`greyscale-api.mjs`](../www/js/greyscale-api.mjs), [`greyscale-playlists.mjs`](../www/js/greyscale-playlists.mjs), and [`greyscale-player.mjs`](../www/js/greyscale-player.mjs) if no longer referenced) and any greyscale-only code paths — only after the gruuv path is verified. (Player may be retained/renamed if still used for playback.)
14. Run the build inline-check ([`build/.music-inline-check.mjs`](../build/.music-inline-check.mjs)) and do a relay smoke test (cross-check against [`gruuv/list_gruuv_tracks.sh`](../gruuv/list_gruuv_tracks.sh) output). Verify an upload round-trips: publish `36787`, then it appears in search.
15. Update docs: [`docs/music.md`](../docs/music.md) and [`docs/MUSIC_URL_STRUCTURE.md`](../docs/MUSIC_URL_STRUCTURE.md) to reflect gruuv kinds/IDs and the new upload feature.

---

## Risks & decisions

- **Relay-native search has no ranking/pagination like a server endpoint.** Mitigation: fetch with a sensible `limit`, cache events, filter client-side, debounce queries. Result completeness depends on relay coverage.
- **Albums/artists are derived, not authoritative.** Grouping by free-text `album`/`artist` tags can produce duplicates/variants. Mitigation: normalize/trim, dedupe like the existing `deduplicateAlbums`.
- **Track addressing change** (`36787:pubkey:dTag` vs numeric ID) affects deep links and URL structure — covered by the `MUSIC_URL_STRUCTURE.md` update.
- **Upload duration/metadata** extraction is best-effort in the browser (no ffprobe); manual fields are the reliable fallback.
- **Default relay/Blossom set**: plan uses the page's existing NDK relays and `getBlossomServers()`; gruuv defaults are documented as fallback. Confirm if a specific relay/Blossom set is required.

## Open question for confirmation
- Should the deep-link/URL scheme keep numeric-style IDs (with a translation layer) or move fully to `36787:pubkey:dTag` coordinates?
