blastoise/AGENTS.md

203 lines
6.5 KiB
Markdown

# 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 /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`.
## Test User
- **Username**: test
- **Password**: testuser