76 lines
3.1 KiB
Markdown
76 lines
3.1 KiB
Markdown
# MusicRoom
|
|
|
|
Synchronized music streaming server built with Bun. Manages "streams" (virtual radio stations) that play through playlists sequentially. Clients connect, receive now-playing state, download audio, and sync playback locally.
|
|
|
|
## Architecture
|
|
|
|
The server does NOT decode or play audio. It tracks time:
|
|
- `currentTimestamp = (Date.now() - stream.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
|
|
|
|
## Content-Addressed Tracks
|
|
|
|
All tracks are identified by a **content hash** (SHA-256 of first 64KB), not by filename:
|
|
- `track.id` = Content hash (primary key in database, used for caching, API requests)
|
|
- `track.filename` = Original filename (display only)
|
|
- `track.title` = Metadata title or filename without extension (display only)
|
|
|
|
This allows:
|
|
- Deduplication (same file with different names = same track)
|
|
- Renaming files without breaking playlists
|
|
- Reliable client-side caching by content hash
|
|
|
|
The client must use `track.id` for:
|
|
- Caching tracks in IndexedDB (`TrackStorage.set(track.id, blob)`)
|
|
- Fetching audio (`/api/tracks/:id`)
|
|
- Checking cache status
|
|
|
|
## Routes
|
|
|
|
```
|
|
GET / → Serves public/index.html
|
|
GET /api/streams → List active streams (id, name, trackCount)
|
|
GET /api/streams/:id → Current stream state (track, currentTimestamp, streamName)
|
|
WS /api/streams/:id/ws → WebSocket: pushes state on connect, every 30s, and on track change
|
|
GET /api/tracks/:id → Serve audio file by content hash with Range request support
|
|
GET /api/library → List all tracks with id, filename, title, duration
|
|
```
|
|
|
|
## Files
|
|
|
|
- **server.ts** — Bun entrypoint. HTTP routes and WebSocket handlers.
|
|
- **stream.ts** — `Stream` class. Playlist, current index, time tracking, broadcasting.
|
|
- **library.ts** — `Library` class. Scans music directory, computes content hashes, caches metadata.
|
|
- **db.ts** — SQLite database for users, sessions, playlists, tracks.
|
|
- **playlist.json** — Config file. Stream definitions.
|
|
- **public/** — Client files (modular JS: core.js, utils.js, audioCache.js, etc.)
|
|
- **music/** — Directory for audio files (.mp3, .ogg, .flac, .wav, .m4a, .aac).
|
|
|
|
## Key types
|
|
|
|
```ts
|
|
interface Track {
|
|
id: string; // Content hash (primary key)
|
|
filename: string; // Original filename
|
|
title: string; // Display title
|
|
duration: number;
|
|
}
|
|
|
|
// Stream.getState() returns:
|
|
{ track: Track | null, currentTimestamp: number, streamName: string, paused: boolean }
|
|
```
|
|
|
|
## Client sync logic
|
|
|
|
On WebSocket message:
|
|
1. New track → load audio, seek to server timestamp, play
|
|
2. Same track, drift < 2s → ignore
|
|
3. Same track, drift >= 2s → seek to server timestamp
|
|
|
|
Progress bar updates from `audio.currentTime` when playing, from extrapolated server time when not playing (grey vs green color).
|
|
|
|
## Config
|
|
|
|
Default port 3001 (override with `PORT` env var). Track durations read from file metadata on startup with `music-metadata` (`duration: true` for full-file scan, needed for accurate OGG durations).
|