Files
website/src/lib/music/player.svelte.ts
T

146 lines
3.3 KiB
TypeScript

import type { Track } from './types';
import { pickRandomIndex } from './playlist';
class MusicPlayer {
tracks = $state<Track[]>([]);
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<number[]>([]);
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();