Files
client/plans/music-greyscale-to-gruuv-migration.md

11 KiB
Raw Permalink Blame History

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 — provides subscribe(), publishEvent(), publishRawEvent(), getPubkey(). The page already wires NDK, relay status UI, and the ndkEvent window-event stream.
  • 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).
  • www/js/blossom-ui.mjs — provides getBlossomServers() for the active Blossom target.
  • SimplePlayer — 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 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

  1. Confirm 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)

  1. Create www/js/gruuv-api.mjs exposing the same surface as GreyscaleAPI: searchTracks, searchAlbums, searchArtists, getAlbum, getArtist, getTrackStream, getCoverUrl, getArtistPictureUrl, prepareTrack.
  2. 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.
  3. 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.
  4. Implement getTrackStream to return { streamUrl: <url tag>, isDash: false }. Cover/artwork helpers return the image URL directly.

Phase 2 — Playlists

  1. Create www/js/gruuv-playlists.mjs mirroring the exports of 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)

  1. Create 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

  1. Swap imports: replace GreyscaleAPI, the greyscale playlists import block (lines ~13751380) with gruuv modules. Keep SimplePlayer (it works with direct URLs); the DASH branch simply goes unused.
  2. Update search handlers (~3117, combined search ~4516) and detail loaders (~3837~3963) for the gruuv id format and the grouped album/artist results.
  3. Update playlist subscription filter and event handling (subscribeAllPlaylists, handleMusicNdkEvent, hydratePlaylistFromEvent) to kind 34139 with t=gruuv and a-tag track refs.
  4. Add an Upload Track UI section (file input, metadata fields, progress/status), gated behind authentication (promptLoginIfNeeded), wired to gruuv-upload.mjs.
  5. Update the download (fetchDownloadBlobForTrack) and share flows to use the direct Blossom URL instead of Tidal stream manifests.

Phase 5 — Cleanup & verification

  1. Remove greyscale modules (greyscale-api.mjs, greyscale-playlists.mjs, and 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.)
  2. Run the build inline-check (build/.music-inline-check.mjs) and do a relay smoke test (cross-check against gruuv/list_gruuv_tracks.sh output). Verify an upload round-trips: publish 36787, then it appears in search.
  3. Update docs: docs/music.md and 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?