# Music Playlists on Nostr — Architecture Plan

## Goal

Add the ability to **create, save, and publish playlists** on Nostr from the music page, and **view playlists from followed users**.

## NIP Analysis — Which Event Kind?

After reviewing the NIPs, there are several viable approaches:

### Option A: NIP-51 Curation Sets — kind `30004` ✅ RECOMMENDED

**NIP-51 "Curation sets"** are addressable events designed for exactly this: user-curated collections of content. They already support `title`, `image`, `description` tags and use the `d` tag as a unique identifier.

The pattern from NIP-51:
```json
{
  "kind": 30004,
  "tags": [
    ["d", "my-playlist-id"],
    ["title", "Chill Vibes"],
    ["image", "https://resources.tidal.com/images/.../320x320.jpg"],
    ["description", "My favorite chill tracks"],
    ["i", "tidal:track:12345", "https://tidal.com/track/12345"],
    ["i", "tidal:track:67890", "https://tidal.com/track/67890"],
    ["t", "chill"],
    ["t", "music"]
  ],
  "content": ""
}
```

**Why this is the best fit:**
- Addressable events — can be updated without creating duplicates
- Multiple playlists per user via different `d` tags
- Built-in `title`, `image`, `description` support
- Already used by other Nostr clients for curation
- Friends' playlists discoverable via follow list + kind filter
- NIP-73 `i` tags with `tidal:track:<id>` give us external content IDs

**Trade-offs:**
- Kind 30004 is shared with article curation — we differentiate via `t` tag containing `music` or a custom label
- Track metadata like title/artist/duration must be stored per-item or re-fetched from the API

### Option B: NIP-78 Application-specific Data — kind `30078`

We already use kind 30078 for user settings. We could store playlists as app-specific data:
```json
{
  "kind": 30078,
  "tags": [["d", "greyscale:playlist:my-playlist-id"]],
  "content": "{\"title\":\"Chill Vibes\",\"tracks\":[...]}"
}
```

**Pros:** Full control over format, no interop concerns
**Cons:** Not discoverable by other music clients, not standard, JSON blob in content

### Option C: Custom kind in the 30000-39999 range

Define our own addressable kind for music playlists. No existing NIP defines a music-specific playlist kind.

**Pros:** Clean separation
**Cons:** Non-standard, no interop, relays may not store unknown kinds

### Recommendation: Option A — NIP-51 kind 30004

Use **kind 30004 curation sets** with NIP-73 external content IDs. This is the most Nostr-native approach, gives us interoperability with other clients, and the infrastructure already exists.

## Event Format Design

### Playlist Event — kind 30004

```json
{
  "kind": 30004,
  "tags": [
    ["d", "<unique-playlist-slug>"],
    ["title", "My Playlist Name"],
    ["description", "Optional description"],
    ["image", "https://resources.tidal.com/images/<cover-uuid>/320x320.jpg"],
    ["t", "music"],
    ["t", "playlist"],

    ["i", "tidal:track:12345"],
    ["i", "tidal:track:67890"],
    ["i", "tidal:track:11111"]
  ],
  "content": "<optional JSON with track metadata cache>"
}
```

**Tag breakdown:**
- `d` — unique playlist identifier per user, e.g. `chill-vibes` or a random ID
- `title` — playlist display name
- `description` — optional description
- `image` — playlist cover image URL
- `t` — hashtags; always include `music` and `playlist` for discoverability
- `i` — one per track, using NIP-73 external content ID format: `tidal:track:<tidal_id>`

**Content field — track metadata cache:**

To avoid re-fetching every track from the Tidal API when displaying a playlist, we store a JSON metadata cache in `content`:

```json
{
  "tracks": [
    {
      "id": 12345,
      "title": "Song Name",
      "artist": "Artist Name",
      "duration": 245,
      "cover": "uuid-string",
      "albumTitle": "Album Name"
    }
  ]
}
```

This is a **cache** — the `i` tags are the source of truth for which tracks are in the playlist. The content JSON provides display metadata so we don't need API calls just to render the list.

## Architecture

```mermaid
flowchart TB
    subgraph Music Page
        SEARCH[Search + Play - existing]
        PLMGR[Playlist Manager UI]
        PLIST[Playlist View/Browse]
    end
    subgraph www/js modules
        API[greyscale-api.mjs]
        PLAYER[greyscale-player.mjs]
        PLMOD[greyscale-playlists.mjs - NEW]
        NDK[init-ndk.mjs]
    end
    subgraph Nostr
        PUB[publishEvent - kind 30004]
        SUB[subscribe - kind 30004]
        FOLLOWS[Follow list - kind 3]
    end
    SEARCH --> API
    SEARCH --> PLAYER
    PLMGR --> PLMOD
    PLIST --> PLMOD
    PLMOD --> NDK
    PLMOD --> API
    NDK --> PUB
    NDK --> SUB
    NDK --> FOLLOWS
```

## UI Design

The music page layout expands to include a playlist sidebar or tab system:

```
┌─────────────────────────────────────────────────────┐
│  ☰  Music                                           │
├──────────────┬──────────────────────────────────────┤
│              │                                      │
│  PLAYLISTS   │  [Search input...........] [Search]  │
│              │  Status: Ready.                      │
│  + New       │                                      │
│              │  ┌──────────────────────────────┐    │
│  My Lists    │  │ [img] Track Title      3:42  │    │
│  ─────────   │  │       Artist — Album    [+]  │    │
│  Chill Vibes │  ├──────────────────────────────┤    │
│  Workout     │  │ [img] Track Title      4:15  │    │
│  Focus       │  │       Artist — Album    [+]  │    │
│              │  └──────────────────────────────┘    │
│  Friends     │                                      │
│  ─────────   │                                      │
│  @alice      │                                      │
│    Party Mix │                                      │
│  @bob        │                                      │
│    Road Trip │                                      │
│              │                                      │
├──────────────┴──────────────────────────────────────┤
│ [img] Title    [◀][▶/❚❚][▶▶]                       │
│       Artist   0:00 ═══════○═══ 3:42                │
├─────────────────────────────────────────────────────┤
│  [relay dots]  [center]  [right]           [sats]   │
└─────────────────────────────────────────────────────┘
```

### Key UI interactions:
1. **[+] button** on each search result — adds track to the currently selected playlist
2. **"+ New" button** — creates a new empty playlist with a name prompt
3. **Click a playlist** — loads it into the results area for playback
4. **"My Lists"** — playlists authored by the current user
5. **"Friends"** — playlists from followed users, grouped by author
6. **Right-click / long-press playlist** — rename, delete, publish options

## Implementation Steps

### Step 1: Create `www/js/greyscale-playlists.mjs`

New module with these functions:

- `buildPlaylistEvent(playlist)` — construct a kind 30004 event from playlist data
- `parsePlaylistEvent(event)` — parse a kind 30004 event into playlist data
- `isPlaylistEvent(event)` — check if a kind 30004 event is a music playlist via `t` tags

### Step 2: Add playlist state management to music.html

- Track the current user's playlists in memory
- Subscribe to kind 30004 events from the current user on page load
- Subscribe to kind 30004 events from followed users with `#t` filter for `music`

### Step 3: Add playlist sidebar HTML/CSS

- Left sidebar panel with "My Lists" and "Friends" sections
- Playlist items with title and track count
- "+ New" button at top
- Responsive: collapse sidebar on narrow screens

### Step 4: Add "add to playlist" button on search results

- Each search result gets a [+] button
- Clicking opens a dropdown of user's playlists to add to
- Adding a track appends the `i` tag and updates the content JSON cache

### Step 5: Implement playlist save/publish

- When user modifies a playlist, auto-save locally
- "Publish" button publishes the kind 30004 event via `publishEvent()`
- Updating an existing playlist replaces the event with same `d` tag

### Step 6: Implement playlist loading and playback

- Clicking a playlist loads its tracks into the player queue
- Track metadata comes from the content JSON cache
- If cache is stale or missing, re-fetch from API using track IDs

### Step 7: Implement friends' playlist discovery

- On page load, get follow list via kind 3
- Subscribe to kind 30004 from followed pubkeys with `#t` filter `music`
- Group results by author in the sidebar

### Step 8: Implement playlist deletion

- Publish a kind 5 deletion request event referencing the playlist event

## Files Changed

| File | Action | Description |
|------|--------|-------------|
| `www/js/greyscale-playlists.mjs` | **New** | Playlist event builder/parser module |
| `www/music.html` | **Modified** | Add sidebar UI, playlist state, save/load/publish logic |

## Nostr Queries

### Load my playlists
```js
subscribe(
  { kinds: [30004], authors: [myPubkey], '#t': ['music'] },
  { closeOnEose: true, cacheUsage: 'CACHE_FIRST' }
);
```

### Load friends' playlists
```js
// First get follow list
const follows = getFollowedPubkeys(); // from kind 3
subscribe(
  { kinds: [30004], authors: follows, '#t': ['music'] },
  { closeOnEose: true, cacheUsage: 'CACHE_FIRST' }
);
```

### Publish a playlist
```js
const event = {
  kind: 30004,
  created_at: Math.floor(Date.now() / 1000),
  tags: [
    ['d', playlistId],
    ['title', playlistTitle],
    ['t', 'music'],
    ['t', 'playlist'],
    ...tracks.map(t => ['i', `tidal:track:${t.id}`])
  ],
  content: JSON.stringify({ tracks: tracksMetadata })
};
await publishEvent(event);
```
