# Greyscale PoC — Minimal Search & Play Web App

## Goal

Build a minimal browser-based proof of concept that can **search for music** and **play tracks** using the same API endpoints that Monochrome uses. No build tools, no npm, no frameworks — pure vanilla JS. This is Step 1 of the larger Greyscale project.

## Architecture Overview

```mermaid
flowchart TB
    subgraph greyscale-app - Pure Vanilla JS
        HTML[index.html - single page app]
        APP[js/app.js - wires UI to API + player]
        API[js/api.js - fetch-based API calls]
        PLAYER[js/player.js - audio element + dash.js]
        STYLE[style.css - dark minimal theme]
    end
    subgraph External
        CDN[dash.js via CDN]
        INSTANCES[Tidal uptime workers - instance list]
        TIDAL[Tidal API instances - search + streaming]
    end
    subgraph monochrome - git submodule - reference only
        MONO_API[api.js - reference for API patterns]
        MONO_STORAGE[storage.js - reference for instance URLs]
    end
    API -->|fetch| INSTANCES
    API -->|fetch| TIDAL
    PLAYER -->|uses| CDN
    APP --> API
    APP --> PLAYER
    HTML --> APP
    HTML --> STYLE
    API -.->|patterns from| MONO_API
```

## Key Design Decisions

### 1. Pure Vanilla JS — No Build Tools
- No Vite, no npm, no node_modules, no bundler
- Just `<script type="module">` tags in HTML
- dash.js loaded from CDN
- Open `index.html` directly or serve with `python -m http.server`
- Total app code: ~200-300 lines of JS

### 2. Git Submodule for Monochrome (Reference Only)
- Monochrome stays as a git submodule from `https://github.com/monochrome-music/monochrome`
- We do NOT import from it — it serves as **reference documentation**
- When upstream changes their API, we check the submodule diff to see what changed
- Our `js/api.js` re-implements only the ~60 lines of fetch logic we need

### 3. Self-Contained API Module
`js/api.js` contains all the API logic:
- Fetches instance list from the same uptime workers Monochrome uses
- Search: `GET {instance}/search/?s={query}`
- Track stream: `GET {instance}/track/?id={id}&quality={quality}`
- Manifest parsing: base64 decode -> extract URL or create DASH blob
- Cover art URL construction

### 4. Minimal Player
- `<audio>` element for direct stream URLs
- `dashjs.MediaPlayer` for DASH manifest streams
- Simple queue: search results = queue
- Play/pause, next, prev — thats it

## File Structure

```
greyscale/
├── monochrome/                  # git submodule (reference only)
├── greyscale-app/
│   ├── index.html               # single page: search + results + player bar
│   ├── style.css                # dark theme, minimal
│   └── js/
│       ├── app.js               # entry point: wires everything together
│       ├── api.js               # vanilla fetch API (instances, search, streaming)
│       └── player.js            # audio element + dash.js playback
├── plans/
│   ├── option-a-minimal-fork.md
│   └── greyscale-poc-plan.md
├── .gitmodules                  # submodule config
└── .gitignore
```

## Detailed Implementation

### Step 1: Repository Setup

```bash
# Remove existing monochrome directory (plain copy)
rm -rf monochrome/

# Add as git submodule
git submodule add https://github.com/monochrome-music/monochrome monochrome

# Commit
git add .gitmodules monochrome
git commit -m "Replace monochrome copy with git submodule"
```

### Step 2: index.html

Single HTML file with three sections:

```
┌─────────────────────────────────────┐
│  🔍 [Search box............] [Go]   │
├─────────────────────────────────────┤
│                                     │
│  Search Results                     │
│  ┌─────────────────────────────┐    │
│  │ 🎵 Track Name               │    │
│  │    Artist — Album    3:42   │    │
│  ├─────────────────────────────┤    │
│  │ 🎵 Track Name               │    │
│  │    Artist — Album    4:15   │    │
│  └─────────────────────────────┘    │
│                                     │
├─────────────────────────────────────┤
│ [◀] [▶/❚❚] [▶] Track — Artist  0:00│
│ ═══════════════════○────────── 3:42  │
└─────────────────────────────────────┘
```

Key elements:
- `<script src="https://cdn.dashjs.org/latest/dash.all.min.js">` for DASH playback
- `<script type="module" src="js/app.js">` as entry point
- `<audio id="audio-player">` hidden element
- Semantic HTML, no framework

### Step 3: js/api.js — The API Layer

This is the core module. Based on analysis of Monochrome's [`LosslessAPI`](monochrome/js/api.js:19) and [`apiSettings`](monochrome/js/storage.js:2), the actual HTTP API is straightforward:

```javascript
// Instance discovery
const UPTIME_URLS = [
    'https://tidal-uptime.jiffy-puffs-1j.workers.dev/',
    'https://tidal-uptime.props-76styles.workers.dev/',
];

// Fallback instances (from monochrome/js/storage.js lines 60-83)
const FALLBACK_INSTANCES = {
    api: [
        'https://eu-central.monochrome.tf',
        'https://us-west.monochrome.tf',
        // ... more
    ],
    streaming: [
        'https://arran.monochrome.tf',
        'https://triton.squid.wtf',
        // ... more
    ]
};
```

**API endpoints** (derived from [`api.js`](monochrome/js/api.js:19)):
- Search tracks: `GET /search/?s={query}`
- Search albums: `GET /search/?al={query}`
- Search artists: `GET /search/?a={query}`
- Get track + stream info: `GET /track/?id={id}&quality={quality}` (uses streaming instances)
- Get album: `GET /album/?id={id}`
- Get track metadata: `GET /info/?id={id}`
- Cover art: `https://resources.tidal.com/images/{uuid}/320x320.jpg`

**Stream URL resolution** (from [`extractStreamUrlFromManifest()`](monochrome/js/api.js:266)):
1. Base64 decode the manifest string
2. If it contains `<MPD` → its a DASH manifest → create blob URL
3. Otherwise try JSON parse for `urls[0]`
4. Fallback: regex match for HTTP URL

**Quality options**: `HI_RES_LOSSLESS`, `LOSSLESS`, `HIGH`, `LOW`

### Step 4: js/player.js — Minimal Player

Based on [`Player.playTrackFromQueue()`](monochrome/js/player.js:405):

```javascript
export class SimplePlayer {
    constructor(audioElement) {
        this.audio = audioElement;
        this.dashPlayer = dashjs.MediaPlayer().create();
        this.dashInitialized = false;
        this.queue = [];
        this.currentIndex = -1;
    }

    async play(streamUrl) {
        if (this.dashInitialized) {
            this.dashPlayer.reset();
            this.dashInitialized = false;
        }

        if (streamUrl.startsWith('blob:')) {
            // DASH manifest
            this.dashPlayer.initialize(this.audio, streamUrl, true);
            this.dashInitialized = true;
        } else {
            // Direct URL
            this.audio.src = streamUrl;
            await this.audio.play();
        }
    }
}
```

### Step 5: js/app.js — Glue Code

Wires the search input to the API, renders results, handles click-to-play:

1. On page load: fetch instance list
2. On search submit: call `api.searchTracks(query)`
3. Render results as list items with track info + cover art
4. On result click: call `api.getStreamUrl(trackId)` then `player.play(url)`
5. Wire play/pause/next/prev buttons to player methods
6. Update progress bar on `timeupdate` event

### Step 6: style.css

Minimal dark theme:
- Background: `#121212`, text: `#e0e0e0`
- Results list with hover highlight
- Fixed bottom player bar
- Cover art thumbnails (48x48 in results, 56x56 in player bar)
- Responsive layout

## What We're NOT Building

- ❌ Authentication (Nostr or otherwise)
- ❌ PocketBase sync / cloud storage
- ❌ Library management / favorites / playlists
- ❌ Qobuz support (Tidal only for PoC)
- ❌ Downloads
- ❌ Lyrics, visualizer, equalizer
- ❌ Settings UI
- ❌ Queue persistence across sessions
- ❌ Scrobbling
- ❌ PWA / service worker
- ❌ Desktop wrapper
- ❌ npm / build tools / bundler

## Upstream Sync Strategy

### How It Works
The monochrome submodule is **reference only** — we don't import from it. When upstream updates:

1. `git submodule update --remote` pulls latest monochrome
2. Check `git diff monochrome/js/api.js` and `monochrome/js/storage.js` for changes
3. If API endpoints or response formats changed → update our `js/api.js` accordingly
4. If no API changes → nothing to do

### What Could Break
Our `js/api.js` depends on these upstream patterns:
1. **Instance URLs** — the uptime worker URLs in `storage.js`
2. **Search endpoint** — `/search/?s={query}` format
3. **Track endpoint** — `/track/?id={id}&quality={quality}` format
4. **Manifest format** — base64-encoded DASH XML or JSON with URLs
5. **Cover art URL pattern** — `resources.tidal.com/images/{uuid}/{size}.jpg`

### Future: Automated Agent Sync
When the project matures, add a CI job that:
1. Watches upstream for commits
2. Diffs the API-relevant files
3. If interface changed → creates an issue with the diff
4. Optionally uses an LLM to suggest updates to our `js/api.js`

## Testing Checklist

- [ ] `index.html` opens in browser without errors (no 404s, no import failures)
- [ ] Instance list loads successfully on page load
- [ ] Search box accepts input and returns results from Tidal
- [ ] Results display track name, artist, album, cover art thumbnail
- [ ] Clicking a track starts audio playback
- [ ] Both DASH streams and direct URL streams play correctly
- [ ] Play/pause button toggles playback
- [ ] Next/prev buttons navigate through results as queue
- [ ] Progress bar shows current position and allows seeking
- [ ] No CORS errors (API instances should allow cross-origin)
- [ ] Works when served via `python -m http.server`
