Add music player
This commit is contained in:
1 parent
c820672803
commit
40604b4a04
17 files changed
+1153
No files matched your search
@@ -0,0 +1,137 @@
|
||||
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.6);
|
||||
unavailable = $state(false);
|
||||
history = $state<number[]>([]);
|
||||
repeat = $state(false);
|
||||
|
||||
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) {
|
||||
this.startOffset = Math.max(0, Math.min(Math.floor(seconds), this.duration - 1));
|
||||
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();
|
||||
Reference in new issue
Block a user