blastoise/AGENTS.md

11 KiB
Raw Permalink Blame History

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:

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)

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.tsChannel class. Queue, current index, time tracking, broadcasting.
  • library.tsLibrary 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

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:

{ "action": "switch", "channelId": "abc123" }
{ "action": "pause" }
{ "action": "unpause" }
{ "action": "seek", "timestamp": 45.5 }
{ "action": "jump", "index": 3 }

Server → Client:

{ "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:

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

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