11 KiB
Music: Greyscale → Gruuv API Migration Plan
Goal
Fully replace the Greyscale (Tidal-proxy) music backend used by 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.
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— providessubscribe(),publishEvent(),publishRawEvent(),getPubkey(). The page already wires NDK, relay status UI, and thendkEventwindow-event stream.www/js/blossom-api.mjs— providescalculateSHA256()andcreateBlossomAuth(), which already builds thekind 24242authorization event that Gruuv's upload spec requires (seegruuv/README.md).www/js/blossom-ui.mjs— providesgetBlossomServers()for the active Blossom target.SimplePlayer— its direct-URL playback path already handles plain audio URLs (Blossom URLs are plainhttps), so playback needs no DASH logic for gruuv.
Gruuv protocol reference (from gruuv/)
kind 36787 track event tags (per gruuv/upload_gruuv.sh and 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 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).
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():
{ 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
- Confirm
www/music-greyscale.htmlis a faithful backup of the original greyscale page so it can be referenced/restored.
Phase 1 — Read path (search, browse, stream)
- Create
www/js/gruuv-api.mjsexposing the same surface asGreyscaleAPI:searchTracks,searchAlbums,searchArtists,getAlbum,getArtist,getTrackStream,getCoverUrl,getArtistPictureUrl,prepareTrack. - Implement search by subscribing to
kind 36787with#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. - Implement
getAlbum/getArtistby grouping the cached/fetched track events on thealbumandartisttags respectively (gruuv has no album/artist DB).getAlbum(id)returns{ album, tracks };getArtist(id)returns{ ...artist, albums, eps, tracks }to match existing render expectations. - Implement
getTrackStreamto return{ streamUrl: <url tag>, isDash: false }. Cover/artwork helpers return theimageURL directly.
Phase 2 — Playlists
- Create
www/js/gruuv-playlists.mjsmirroring the exports ofgreyscale-playlists.mjs:PLAYLIST_KIND(now34139),buildPlaylistEvent,parsePlaylistEvent,isMusicPlaylistEvent,createPlaylistIdentifier,playlistEventAddress, and any helper the page imports.- Track references use
atags of form36787:<pubkey>:<dTag>instead ofi/tidal:track:tags. - Topic tag becomes
t=gruuv(keept=musicfor 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.
- Track references use
Phase 3 — Upload (new capability)
- Create
www/js/gruuv-upload.mjs:calculateSHA256()(reuse from blossom-api).- Build
kind 24242auth viacreateBlossomAuth('upload', sha256, 'Upload <filename>'). PUT <activeBlossomServer>/uploadwith theAuthorization: Nostr <base64>header; fall back to<server>/<sha256>for the URL if the response lacks one.- Optionally read duration via an
<audio>/AudioContextdecode client-side; allow manual title/artist/album entry. - Publish
kind 36787viapublishEvent()with the full tag set (includingt=music,t=gruuv,x,m,format,size,alt).
Phase 4 — Wire into music.html
- Swap imports: replace
GreyscaleAPI, the greyscale playlists import block (lines ~1375–1380) with gruuv modules. KeepSimplePlayer(it works with direct URLs); the DASH branch simply goes unused. - Update search handlers (~3117, combined search ~4516) and detail loaders (~3837–~3963) for the gruuv id format and the grouped album/artist results.
- Update playlist subscription filter and event handling (
subscribeAllPlaylists,handleMusicNdkEvent,hydratePlaylistFromEvent) tokind 34139witht=gruuvanda-tag track refs. - Add an Upload Track UI section (file input, metadata fields, progress/status), gated behind authentication (
promptLoginIfNeeded), wired togruuv-upload.mjs. - Update the download (
fetchDownloadBlobForTrack) and share flows to use the direct Blossom URL instead of Tidal stream manifests.
Phase 5 — Cleanup & verification
- Remove greyscale modules (
greyscale-api.mjs,greyscale-playlists.mjs, andgreyscale-player.mjsif 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.) - Run the build inline-check (
build/.music-inline-check.mjs) and do a relay smoke test (cross-check againstgruuv/list_gruuv_tracks.shoutput). Verify an upload round-trips: publish36787, then it appears in search. - Update docs:
docs/music.mdanddocs/MUSIC_URL_STRUCTURE.mdto 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/artisttags can produce duplicates/variants. Mitigation: normalize/trim, dedupe like the existingdeduplicateAlbums. - Track addressing change (
36787:pubkey:dTagvs numeric ID) affects deep links and URL structure — covered by theMUSIC_URL_STRUCTURE.mdupdate. - 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:dTagcoordinates?