# 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; 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> - per-track segment status M.cachedTracks // Set - tracks fully cached in IndexedDB M.trackBlobs // Map - blob URLs for cached tracks ``` ## Routes ``` GET / → Serves public/index.html GET /api/channels → List all channels with listener counts POST /api/channels → Create a new channel GET /api/channels/:id → Get channel state 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 ``` ## Files ### Server - **server.ts** — Bun entrypoint. HTTP routes and WebSocket 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. ### 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 - **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`.