import type { Track } from './types'; import { pickRandomIndex } from './playlist'; class MusicPlayer { tracks = $state([]); currentIndex = $state(-1); startOffset = $state(0); streamKey = $state(0); elapsedSinceStart = $state(0); paused = $state(true); muted = $state(true); volume = $state(0.1); unavailable = $state(false); history = $state([]); repeat = $state(false); audioElement: HTMLAudioElement | null = null; private failedTrackCount = 0; private loading = false; get currentTrack(): Track | undefined { return this.tracks[this.currentIndex]; } get streamUrl(): string { const track = this.currentTrack; if (!track) return ''; return this.startOffset > 0 ? `/api/music/${track.id}?start=${this.startOffset}` : `/api/music/${track.id}`; } get position(): number { return this.startOffset + this.elapsedSinceStart; } get duration(): number { return this.currentTrack?.durationSeconds ?? 0; } get canGoBack(): boolean { return this.history.length > 0; } async start() { if (this.loading || this.tracks.length > 0) return; this.loading = true; try { const response = await fetch('/api/music'); if (response.ok) this.tracks = await response.json(); } catch { this.tracks = []; } this.loading = false; if (this.tracks.length > 0) this.changeTrack(pickRandomIndex(this.tracks.length, -1)); } private changeTrack(index: number) { this.currentIndex = index; this.startOffset = 0; this.elapsedSinceStart = 0; this.streamKey += 1; } playTrack(index: number) { if (index !== this.currentIndex) this.history.push(this.currentIndex); this.changeTrack(index); this.paused = false; } playRandom() { this.playTrack(pickRandomIndex(this.tracks.length, this.currentIndex)); } handleEnded() { if (this.repeat) { this.seek(0); return; } this.playRandom(); } toggleRepeat() { this.repeat = !this.repeat; } skipBy(seconds: number) { this.seek(this.position + seconds); } playPrevious() { const previousIndex = this.history.pop(); if (previousIndex === undefined) return; this.changeTrack(previousIndex); this.paused = false; } togglePlay() { this.paused = !this.paused; } toggleMute() { this.muted = !this.muted; if (!this.muted) this.paused = false; } setVolume(volume: number) { this.volume = volume; this.muted = volume === 0; if (!this.muted) this.paused = false; } seek(seconds: number) { const targetSeconds = Math.max(0, Math.min(Math.floor(seconds), this.duration - 1)); if (this.currentTrack?.cached && this.audioElement) { this.audioElement.currentTime = targetSeconds; this.paused = false; return; } this.startOffset = targetSeconds; this.elapsedSinceStart = 0; this.streamKey += 1; this.paused = false; } handleTimeUpdate(elapsedSeconds: number) { this.elapsedSinceStart = elapsedSeconds; } handleTrackLoaded() { this.failedTrackCount = 0; } handleTrackError(mediaError: MediaError | null) { console.error(`Music stream failed for ${this.currentTrack?.id}:`, mediaError?.message || mediaError?.code); this.failedTrackCount += 1; if (this.failedTrackCount >= this.tracks.length) { this.unavailable = true; this.paused = true; return; } this.playRandom(); } } export const musicPlayer = new MusicPlayer();