# MusicRoom

Synchronized music streaming server built with Bun. Manages "channels" (virtual radio stations) that play through queues sequentially. Clients connect, receive now-playing state, download audio, and sync playback locally.

## Architecture

### Server

The server does NOT decode or play audio. It tracks time:
- `currentTimestamp = (Date.now() - channel.startedAt) / 1000`
- When `currentTimestamp >= track.duration`, advance to next track, reset `startedAt`
- A 1s `setInterval` checks if tracks need advancing and broadcasts state every 30s

### Channels

Channels are virtual radio stations. A **default channel** is created on server start with the full library:

```ts
interface Channel {
  id: string;
  name: string;
  description: string;
  queue: Track[];      // tracks in playback order
  currentIndex: number;
  startedAt: number;
  paused: boolean;
  clients: Set<WebSocket>;
  createdBy: number | null;  // user ID or null for default
  isDefault: boolean;
}
```

- **Default Channel**: Created on startup, plays all library tracks, cannot be deleted
- **Dynamic Channels**: Users can create channels via POST `/api/channels`
- **Channel Switching**: Clients switch channels via WebSocket `{ action: "switch", channelId }`

### Client

The player's role is simple: **play an arbitrary track by ID**. It does not manage queues or sync logic directly.

- Receives track ID and timestamp from server via WebSocket
- Downloads audio from `/api/tracks/:id`
- Syncs playback position to server timestamp
- Caches tracks locally in IndexedDB

### Library & Queue Views

Both views display tracks with real-time status indicators:
- **Green bar**: Track is fully cached locally (in IndexedDB)
- **Yellow bar**: Track is not cached

The buffer bar (below progress) shows 20 segments indicating download/buffer status:
- Segments fill as audio is buffered by browser or fetched via range requests
- When all segments are buffered, the track is automatically cached to IndexedDB

## Content-Addressed Tracks

All tracks are identified by a **content hash** (`sha256:` prefix + first 64KB hash):

| Field | Purpose |
|-------|---------|
| `track.id` | Content hash - primary key, used for API, caching, database |
| `track.filename` | Original filename - display only |
| `track.title` | Metadata title - display only |

Benefits:
- Deduplication (same file with different names = same track)
- Renaming files without breaking queues
- Reliable client-side caching by content hash

The client uses `track.id` for:
- Caching tracks in IndexedDB (`TrackStorage.set(trackId, blob)`)
- Fetching audio (`/api/tracks/:trackId`)
- Checking cache status (`M.cachedTracks.has(trackId)`)

## Client Caching System

### Segment-Based Buffering
- Tracks are divided into 20 virtual segments
- Browser's native buffering is synced to `M.trackCaches` Map
- Range requests fetch individual segments for seeking

### Full Track Caching
When all 20 segments are buffered:
1. Full track is downloaded via `downloadAndCacheTrack()`
2. Stored in IndexedDB via `TrackStorage`
3. Added to `M.cachedTracks` Set
4. UI indicators update to green

### Cache State (in-memory)
```js
M.trackCaches      // Map<trackId, Set<segmentIndex>> - per-track segment status
M.cachedTracks     // Set<trackId> - tracks fully cached in IndexedDB
M.trackBlobs       // Map<trackId, blobUrl> - blob URLs for cached tracks
```

## Routes

```
GET  /                        → Serves public/index.html
GET  /listen/:trackId         → Serves index.html (direct track link)
GET  /api/channels            → List all channels with listener counts
POST /api/channels            → Create a new channel
GET  /api/channels/:id        → Get channel state
PATCH /api/channels/:id       → Rename channel
DELETE /api/channels/:id      → Delete a channel (not default)
WS   /api/channels/:id/ws     → WebSocket: pushes state on connect and changes
GET  /api/tracks/:id          → Serve audio by content hash (supports Range)
GET  /api/library             → List all tracks with id, filename, title, duration
GET  /api/playlists           → List user's + shared playlists
POST /api/playlists           → Create new playlist
GET  /api/playlists/:id       → Get playlist details
PATCH /api/playlists/:id      → Update playlist (name, description, public)
DELETE /api/playlists/:id     → Delete playlist
PATCH /api/playlists/:id/tracks → Modify tracks (add/remove/set)
POST /api/playlists/:id/share → Generate share token
DELETE /api/playlists/:id/share → Remove sharing
GET  /api/playlists/shared/:token → Get shared playlist by token
POST /api/playlists/shared/:token → Copy shared playlist to own
```

## Files

### Server
- **server.ts** — Bun entrypoint. Imports and starts the server.
- **config.ts** — Config types, loading, and exports (port, musicDir, etc).
- **state.ts** — Shared application state (channels Map, userConnections Map, library).
- **init.ts** — Server initialization, channel loading, tick interval, library events.
- **broadcast.ts** — Broadcast utilities (broadcastToAll, sendToUser, broadcastChannelList).
- **websocket.ts** — WebSocket open/close/message handlers.
- **channel.ts** — `Channel` class. Queue, current index, time tracking, broadcasting.
- **library.ts** — `Library` class. Scans music directory, computes content hashes.
- **db.ts** — SQLite database for users, sessions, tracks.
- **auth.ts** — Auth helpers (getUser, requirePermission, session cookies).
- **ytdlp.ts** — yt-dlp integration for fetching audio from URLs.

### Routes (routes/)
- **index.ts** — Main router, dispatches to route handlers.
- **helpers.ts** — Shared route helpers (getOrCreateUser, userHasPermission).
- **auth.ts** — Auth endpoints (signup, login, logout, me, admin).
- **channels.ts** — Channel CRUD and control (list, create, delete, jump, seek, queue, mode).
- **tracks.ts** — Library listing, file upload, audio serving with range support.
- **fetch.ts** — yt-dlp fetch endpoints (check URL, confirm playlist, queue status).
- **playlists.ts** — Playlist CRUD and track management.
- **static.ts** — Static file serving (index.html, styles.css, JS files).

### Client (public/)
- **core.js** — Global state namespace (`window.MusicRoom`)
- **utils.js** — Helpers ($, fmt, showToast)
- **audioCache.js** — Track caching, segment downloads, prefetching
- **channelSync.js** — WebSocket connection, server sync, channel switching
- **ui.js** — Progress bar, buffer display, UI updates
- **queue.js** — Queue/library rendering, cache status, context menus
- **playlists.js** — Playlist UI, create/edit/delete, add tracks
- **controls.js** — Play, pause, seek, volume
- **auth.js** — Login, signup, logout
- **init.js** — App initialization
- **trackStorage.js** — IndexedDB abstraction for track blobs

### Config
- **config.json** — Server configuration (port, musicDir, permissions)
- **music/** — Audio files (.mp3, .ogg, .flac, .wav, .m4a, .aac)

## Key Types

```ts
interface Track { 
  id: string;        // Content hash (sha256:...)
  filename: string;  // Original filename
  title: string;     // Display title
  duration: number;
}

// Channel.getState() returns:
{ 
  track: Track | null, 
  currentTimestamp: number, 
  channelName: string, 
  channelId: string,
  description: string,
  paused: boolean,
  queue: Track[],
  currentIndex: number,
  listenerCount: number,
  listeners: string[],  // usernames of connected users
  isDefault: boolean
}
```

## WebSocket Messages

**Client → Server:**
```json
{ "action": "switch", "channelId": "abc123" }
{ "action": "pause" }
{ "action": "unpause" }
{ "action": "seek", "timestamp": 45.5 }
{ "action": "jump", "index": 3 }
```

**Server → Client:**
```json
{ "type": "channel_list", "channels": [...] }
{ "type": "switched", "channelId": "abc123" }
{ "track": {...}, "currentTimestamp": 45.5, ... }
```

## Client Sync Logic

On WebSocket message:
1. New track → load audio by `track.id`, seek to server timestamp, play
2. Same track, drift < 2s → ignore
3. Same track, drift >= 2s → seek to server timestamp

## Debug Functions

Available in browser console:
```js
MusicRoom.debugCacheMismatch()  // Compare queue IDs vs cached IDs
MusicRoom.debugTrack(index)     // Detailed cache state for track at index
MusicRoom.debugCacheStatus()    // Current track cache state
MusicRoom.clearAllCaches()      // Clear IndexedDB and in-memory caches
```

## Config

Default port 3001 (override with `PORT` env var). Track durations read from file metadata on startup with `music-metadata`.

## Test User

- **Username**: test
- **Password**: testuser

## UI/UX Design Principles

### No Alerts/Prompts
Never use `alert()` or `prompt()`. Use inline editing, toasts, or modals instead.
- Rename actions → inline text input that appears in place
- Confirmations → toast notifications
- Errors → toast with error styling

### Inline Editing
Editable fields should transform in-place:
- Click edit icon → field becomes input
- Enter to save, Escape to cancel
- Blur (click away) saves changes

### Drag and Drop
Support drag-and-drop for reordering and moving items:
- Tracks in queue can be reordered via drag
- Tracks from library can be dragged to queue
- Visual feedback with drop indicators

### Context Menus
Right-click context menus for all actionable items:
- Tracks (both library and queue) have context menus
- Context menu options are consistent across views
- Disabled options are hidden, not grayed out

### Track Behavior Consistency
Tracks in Library and Queue should behave identically where applicable:

| Feature | Library | Queue |
|---------|---------|-------|
| Click to select | ✓ | ✓ |
| Shift+click range select | ✓ | ✓ |
| Right-click context menu | ✓ | ✓ |
| Drag to reorder | ✗ | ✓ |
| Cache status indicator | ✓ | ✓ |

### Context Menu Options by View

**Library tracks:**
- ▶ Play track (local mode, single track)
- ⏭ Play next (insert after current)
- ＋ Add to queue (append to end)
- 📁 Add to Playlist... (submenu)
- 🔗 Generate listening link

**Queue tracks:**
- ▶ Play track (jump to track)
- ⏭ Play next (re-add after current)
- ＋ Add again (duplicate at end)
- 📁 Add to Playlist... (submenu)
- 🔗 Generate listening link
- ✕ Remove from queue

## Playlists

Playlists are reusable collections of tracks that can be added to the queue.

### Data Model
```ts
interface Playlist {
  id: string;
  name: string;
  description: string;
  ownerId: number;
  isPublic: boolean;
  shareToken: string | null;
  trackIds: string[];
}
```

### UI Structure
The Playlists tab has a dual-panel layout:
- **Left panel**: List of playlists (My Playlists + Shared)
- **Right panel**: Selected playlist's track list

### Context Menu Options
**Playlist (in list):**
- ▶ Add to Queue
- ⏭ Play Next
- ✏️ Rename (owner only)
- 🌐 Make Public / 🔒 Make Private
- 🔗 Copy Share Link / Generate Share Link
- 📋 Copy to My Playlists (shared only)
- 🗑️ Delete (owner only)

**Track (in playlist):**
- ▶ Play
- ➕ Add to Queue
- ⏭ Play Next
- 🗑️ Remove from Playlist (owner only)

### Mobile/Touch Support
- Larger touch targets (min 44px)
- No hover-dependent features (always show action buttons)
- Tab-based navigation for panels
- Sticky headers for scrollable lists

### Responsive Layout
- Desktop: side-by-side panels (Channels | Library | Queue)
- Mobile (<768px): tab-switched single panel view
- Player bar adapts: horizontal on desktop, stacked on mobile
