# VJ Page — Streaming Spec Alignment Plan

Aligns [`vj.html`](www/vj.html) and [`vj-stream.mjs`](www/js/vj-stream.mjs) with the updated self-streaming spec at [`self-streaming/README.md`](../self-streaming/README.md).

## Current State vs Spec

| Area | Current | Spec requires |
|------|---------|---------------|
| Show identity | Hardcoded `DEFAULT_STREAM_D_TAG = 'stream-main'` | User-selectable show slug as d-tag |
| Show selector UI | None | Dropdown of user's published kind:30311 events + create new |
| Episode tracking | None | `episode` tag, auto-increment on each Go Live |
| Default streaming URL | `https://laantungir.net/hls/stream.m3u8` | `https://laantungir.net/stream/{slug}/stream.m3u8` |
| Video element src | Hardcoded `src="https://laantungir.net/hls/stream.m3u8"` | Dynamic, set after slug is known |
| Stats URL | `https://laantungir.net/stream/stats` | `https://laantungir.net/api/stream/stats?show={slug}` |
| Stats response shape | `{ viewers, hls_viewers, rtmp_viewers, live }` | `{ live, viewers, variants: { src: {viewers}, ... } }` |
| Viewer count display | `TOTAL: N \| HLS: N \| RTMP: N` | `TOTAL: N \| src: N \| 720p: N \| 480p: N` |
| NIP-53 tags | `d, title, status, summary, image, streaming` | Add `episode`, `web` tags |
| OBS config display | None | Show OBS server + key derived from slug |
| Kind:30078 playlist | None | Episode playlist with track + viewers tags |
| URL derivation | Manual input | All URLs derived from slug automatically |
| OBS server format | N/A | `rtmp://laantungir.net:1935/publish` with key `{slug}/src/{SECRET_KEY}` |

## Implementation Steps

### Step 1 — Remove hardcoded d-tag and add show slug state

In [`vj-stream.mjs`](www/js/vj-stream.mjs):

- Remove `const DEFAULT_STREAM_D_TAG = 'stream-main'` at [line 3](www/js/vj-stream.mjs:3)
- Add a helper to derive all URLs from a slug:
  ```js
  const STREAM_BASE_URL = 'https://laantungir.net';

  function deriveShowUrls(slug) {
    return {
      masterPlaylist: `${STREAM_BASE_URL}/stream/${slug}/stream.m3u8`,
      viewerPage: `${STREAM_BASE_URL}/stream/${slug}`,
      stats: `${STREAM_BASE_URL}/api/stream/stats?show=${encodeURIComponent(slug)}`,
      obsServer: 'rtmp://laantungir.net:1935/publish',
      obsKeyTemplate: `${slug}/src/{SECRET_KEY}`,
    };
  }
  ```
- Replace every reference to `DEFAULT_STREAM_D_TAG` with the selected show slug
- Replace the hardcoded `STATS_URL` constant with a dynamic URL derived from the slug

### Step 2 — Add show selector/creator UI to vj.html

Add new HTML elements at the top of [`#vjStreamCard`](www/vj.html:1311), before the video element:

```html
<div class="musicSidebarTitle">SHOW</div>
<select id="selectShow" class="streamInput">
  <option value="">— Select a show —</option>
</select>
<div style="display:flex; gap:6px; align-items:center;">
  <input id="inputNewShowSlug" class="streamInput" placeholder="new-show-slug" style="flex:1;" />
  <button id="btnCreateShow" class="btn" type="button">Create</button>
</div>
```

Wire up in [`vj-stream.mjs`](www/js/vj-stream.mjs):

- Add `selectShow`, `inputNewShowSlug`, `btnCreateShow` to the `els` object
- On initialize, fetch the user's published `kind:30311` events and populate the dropdown
- On show selection: set `streamDTag` to the slug, derive URLs, pre-fill title/summary/image/episode from the last event
- On create: validate slug format (lowercase, hyphens, no reserved words), add to dropdown, select it
- Slug validation regex: `/^[a-z0-9][a-z0-9-]*[a-z0-9]$/` (min 2 chars, no leading/trailing hyphens)

### Step 3 — Add episode tracking

In [`vj.html`](www/vj.html), add episode field after the title field (around [line 1322](www/vj.html:1322)):

```html
<div id="streamEpisodeIdDisplay" class="streamFieldDisplay" title="Episode ID (timestamp)">Episode ID: —</div>
<div id="streamEpisodeDescriptionDisplay" class="streamFieldDisplay" title="Click to edit episode description">Episode description (optional)</div>
<input id="inputStreamEpisodeDescription" class="streamInput hidden" placeholder="Episode description (optional)" />
```

In [`vj-stream.mjs`](www/js/vj-stream.mjs):

- Add `streamEpisodeIdDisplay`, `streamEpisodeDescriptionDisplay`, and `inputStreamEpisodeDescription` to `els`
- Keep **episode ID** as a timestamp (generated on Go Live; reused through the live session)
- Keep **episode description** as optional editable text (`001`, `Season 2`, freeform)
- Store/persist only the optional episode description in local draft/settings

### Step 4 — Update `buildStreamTags()` to include `episode` and `web` tags

In [`buildStreamTags()`](www/js/vj-stream.mjs:116):

```js
function buildStreamTags({ dTag, title, summary, image, streamingUrl, episode, status }) {
  const tags = [
    ['d', dTag],
    ['title', String(title || '').trim() || 'Untitled stream'],
    ['status', String(status || 'planned').trim() || 'planned']
  ];

  const safeSummary = String(summary || '').trim();
  const safeImage = String(image || '').trim();
  const safeStreaming = String(streamingUrl || '').trim();
  const safeEpisode = String(episode || '').trim();

  if (safeSummary) tags.push(['summary', safeSummary]);
  if (safeImage) tags.push(['image', safeImage]);
  if (safeStreaming) tags.push(['streaming', safeStreaming]);
  if (safeEpisode) tags.push(['episode', safeEpisode]);

  // web tag: viewer page URL derived from slug
  const webUrl = `${STREAM_BASE_URL}/stream/${dTag}`;
  tags.push(['web', webUrl]);

  return tags;
}
```

Update [`publishStreamStatus()`](www/js/vj-stream.mjs:376) to pass `episode` from the input field.

### Step 5 — Update all hardcoded URLs to slug-derived URLs

Replace every occurrence of `https://laantungir.net/hls/stream.m3u8`:

| Location | Change |
|----------|--------|
| [`vj-stream.mjs:290`](www/js/vj-stream.mjs:290) | Use `deriveShowUrls(streamDTag).masterPlaylist` |
| [`vj-stream.mjs:336`](www/js/vj-stream.mjs:336) | Same |
| [`vj-stream.mjs:459`](www/js/vj-stream.mjs:459) | Same |
| [`vj-stream.mjs:786`](www/js/vj-stream.mjs:786) | Same |
| [`vj.html:1313`](www/vj.html:1313) | Remove hardcoded `src` attribute from `<video>` |
| [`vj.html:1323`](www/vj.html:1323) | Remove hardcoded display text |
| [`vj.html:1324`](www/vj.html:1324) | Remove hardcoded `value` from input |

The streaming URL input should auto-populate when a show is selected, and the video player source should be set dynamically.

### Step 6 — Update stats URL and response parsing

In [`vj-stream.mjs`](www/js/vj-stream.mjs):

- Remove the static `const STATS_URL = 'https://laantungir.net/stream/stats'` at [line 4](www/js/vj-stream.mjs:4)
- In [`pollViewerCount()`](www/js/vj-stream.mjs:440), use the slug-derived stats URL:
  ```js
  const statsUrl = deriveShowUrls(streamDTag).stats;
  const res = await fetch(statsUrl);
  ```
- Update response parsing to handle the new shape:
  ```js
  const data = await res.json();
  viewerCount = Number(data.viewers) || 0;
  const isLive = Boolean(data.live);
  const variants = data.variants || {};

  if (els.divViewerCount) {
    const variantParts = Object.entries(variants)
      .map(([name, v]) => `${name}: ${v.viewers || 0}`)
      .join(' | ');
    els.divViewerCount.textContent = `TOTAL: ${viewerCount}${variantParts ? ' | ' + variantParts : ''}`;
  }
  ```

### Step 7 — Add OBS configuration display

In [`vj.html`](www/vj.html), add after the stream controls div (around [line 1340](www/vj.html:1340)):

```html
<div id="divObsConfig" style="font-family:monospace; font-size:11px; padding:6px 8px; background:rgba(255,255,255,0.02); border-radius:6px; color:var(--text-color); line-height:1.6;">
  <div>OBS Server: <span id="spanObsServer">rtmp://laantungir.net:1935/publish</span></div>
  <div>OBS Key: <span id="spanObsKey">{slug}/src/{SECRET_KEY}</span></div>
</div>
```

In [`vj-stream.mjs`](www/js/vj-stream.mjs):

- Add `spanObsServer` and `spanObsKey` to `els`
- Update OBS key display whenever the show slug changes:
  ```js
  if (els.spanObsKey) {
    els.spanObsKey.textContent = `${streamDTag}/src/{SECRET_KEY}`;
  }
  ```

### Step 8 — Update video element in HTML

In [`vj.html:1313`](www/vj.html:1313), change:

```html
<video id="videoStream" playsinline src="https://laantungir.net/hls/stream.m3u8" muted></video>
```

To:

```html
<video id="videoStream" playsinline muted></video>
```

The source is set dynamically by [`setStreamPlayerSource()`](www/js/vj-stream.mjs:204) after the show slug is known.

### Step 9 — Add kind:30078 episode playlist support (song-triggered updates)

Add to [`vj-stream.mjs`](www/js/vj-stream.mjs):

**New constants/state:**
```js
const PLAYLIST_KIND = 30078;
let playlistDTag = ''; // show-playlist:{show}:{episodeTimestamp}
let playlistTracks = [];
let playlistViewerSnapshots = [];
```

**Behavior change (approved):**

- No periodic 30s timer.
- Update/publish the `kind:30078` event **only when a new song starts**.
- On each song start:
  1. Append next `track` tag
  2. Capture one viewer snapshot (`viewers` tag) from latest stats data
  3. Republish full replaceable event for this episode

**Integration points:**

- On "Go Live": generate timestamp episode ID and initialize 30078 state for this episode
- In [`announceNowPlaying()`](www/js/vj-stream.mjs:573): append track + viewer snapshot and republish `kind:30078`
- On "End Stream": publish final `kind:30078` with `status=ended`

This keeps event volume low and naturally aligns viewer snapshots with track changes.

### Step 10 — Fetch user's shows on initialization

In [`initialize()`](www/js/vj-stream.mjs:771), after auth is resolved:

- Subscribe to the user's `kind:30311` events (all d-tags, not just one)
- Collect unique d-tags as the user's shows
- Populate the show selector dropdown
- If a URL parameter specifies a show (via `?a=` or `?naddr=`), auto-select it
- If no URL parameter, select the most recently updated show (or leave blank for new users)

This requires a one-time query:
```js
subscribe({
  kinds: [STREAM_KIND],
  authors: [streamAuthorPubkey],
  limit: 50
}, { closeOnEose: true, cacheUsage: 'CACHE_FIRST' });
```

Collect results, deduplicate by d-tag (keep latest), populate dropdown.

## File Change Summary

| File | Changes |
|------|---------|
| [`www/js/vj-stream.mjs`](www/js/vj-stream.mjs) | Remove `DEFAULT_STREAM_D_TAG` and `STATS_URL` constants; add `deriveShowUrls()` helper; add show selector logic; add episode tracking; update `buildStreamTags()` with `episode` + `web` tags; update `pollViewerCount()` for new stats shape; add kind:30078 playlist support; update `initialize()` to fetch user's shows; replace all hardcoded URLs |
| [`www/vj.html`](www/vj.html) | Add show selector/creator UI; add episode field; add OBS config display; remove hardcoded `src` from video element; remove hardcoded URL defaults from display/input elements |

## Dependency Diagram

```mermaid
flowchart TD
    S1["Step 1: Remove hardcoded d-tag, add deriveShowUrls"] --> S2["Step 2: Show selector UI"]
    S1 --> S5["Step 5: Replace all hardcoded URLs"]
    S1 --> S6["Step 6: Update stats URL + parsing"]
    S2 --> S3["Step 3: Episode tracking"]
    S2 --> S10["Step 10: Fetch user shows on init"]
    S3 --> S4["Step 4: Update buildStreamTags with episode + web"]
    S5 --> S8["Step 8: Update video element"]
    S2 --> S7["Step 7: OBS config display"]
    S4 --> S9["Step 9: Kind:30078 episode playlist"]
    S6 --> S9
```

Steps 1-8 are the core alignment work. Step 9 (kind:30078 playlist) is the largest addition and can be implemented as a follow-up if needed. Step 10 is required for the show selector to be useful.
