# 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 /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