import type { ServerWebSocket } from "bun"; export interface Track { filename: string; title: string; duration: number; } export interface StreamConfig { id: string; name: string; tracks: Track[]; } export class Stream { id: string; name: string; playlist: Track[]; currentIndex: number = 0; startedAt: number = Date.now(); clients: Set> = new Set(); paused: boolean = false; pausedAt: number = 0; private lastPlaylistBroadcast: number = 0; private playlistDirty: boolean = false; constructor(config: StreamConfig) { this.id = config.id; this.name = config.name; this.playlist = config.tracks; } get currentTrack(): Track | null { if (this.playlist.length === 0) return null; return this.playlist[this.currentIndex]; } get currentTimestamp(): number { if (this.paused) return this.pausedAt; return (Date.now() - this.startedAt) / 1000; } tick(): boolean { if (this.paused) return false; const track = this.currentTrack; if (!track) return false; if (this.currentTimestamp >= track.duration) { this.advance(); return true; } return false; } advance() { if (this.playlist.length === 0) return; this.currentIndex = (this.currentIndex + 1) % this.playlist.length; this.startedAt = Date.now(); this.broadcast(); } getState(includePlaylist: boolean = false) { const state: Record = { track: this.currentTrack, currentTimestamp: this.currentTimestamp, streamName: this.name, paused: this.paused, currentIndex: this.currentIndex, }; if (includePlaylist) { state.playlist = this.playlist; } return state; } pause() { if (this.paused) return; this.pausedAt = this.currentTimestamp; this.paused = true; this.broadcast(); } unpause() { if (!this.paused) return; this.paused = false; this.startedAt = Date.now() - this.pausedAt * 1000; this.broadcast(); } jumpTo(index: number) { if (index < 0 || index >= this.playlist.length) return; this.currentIndex = index; if (this.paused) { this.pausedAt = 0; } else { this.startedAt = Date.now(); } this.broadcast(); } seek(timestamp: number) { const track = this.currentTrack; if (!track) return; const clamped = Math.max(0, Math.min(timestamp, track.duration)); if (this.paused) { this.pausedAt = clamped; } else { this.startedAt = Date.now() - clamped * 1000; } this.broadcast(); } markPlaylistDirty() { this.playlistDirty = true; } broadcast() { const now = Date.now(); const includePlaylist = this.playlistDirty || (now - this.lastPlaylistBroadcast >= 60000); if (includePlaylist) { this.lastPlaylistBroadcast = now; this.playlistDirty = false; } const msg = JSON.stringify(this.getState(includePlaylist)); for (const ws of this.clients) { ws.send(msg); } } addClient(ws: ServerWebSocket<{ streamId: string }>) { this.clients.add(ws); // Always send full state with playlist on connect ws.send(JSON.stringify(this.getState(true))); // Reset timer so next playlist broadcast is in 60s this.lastPlaylistBroadcast = Date.now(); } removeClient(ws: ServerWebSocket<{ streamId: string }>) { this.clients.delete(ws); } }