at main 1.9 kB view raw
1import type { Track } from './types'; 2import { API_URL } from './config'; 3 4// global player state using Svelte 5 runes 5class PlayerState { 6 currentTrack = $state<Track | null>(null); 7 audioElement = $state<HTMLAudioElement | undefined>(undefined); 8 paused = $state(true); 9 10 currentTime = $state(0); 11 duration = $state(0); 12 volume = $state(0.7); 13 playCountedForTrack = $state<number | null>(null); 14 15 // synchronous guard to prevent duplicate play count requests 16 // (reactive state updates are batched, so we need this to block rapid-fire calls) 17 private _playCountPending: number | null = null; 18 19 playTrack(track: Track) { 20 if (this.currentTrack?.id === track.id) { 21 // toggle play/pause for same track 22 this.paused = !this.paused; 23 } else { 24 // switch tracks 25 this.currentTrack = track; 26 this.paused = false; 27 } 28 } 29 30 togglePlayPause() { 31 this.paused = !this.paused; 32 } 33 34 incrementPlayCount() { 35 if (!this.currentTrack || this.playCountedForTrack === this.currentTrack.id || !this.duration) { 36 return; 37 } 38 39 // synchronous check to prevent race condition from batched reactive updates 40 if (this._playCountPending === this.currentTrack.id) { 41 return; 42 } 43 44 // threshold: minimum of 30 seconds or 50% of track duration 45 const threshold = Math.min(30, this.duration * 0.5); 46 47 if (this.currentTime >= threshold) { 48 // set synchronous guard immediately (before async fetch) 49 this._playCountPending = this.currentTrack.id; 50 this.playCountedForTrack = this.currentTrack.id; 51 52 fetch(`${API_URL}/tracks/${this.currentTrack.id}/play`, { 53 method: 'POST', 54 credentials: 'include' 55 }).catch(err => { 56 console.error('failed to increment play count:', err); 57 }); 58 } 59 } 60 61 resetPlayCount() { 62 this.playCountedForTrack = null; 63 this._playCountPending = null; 64 } 65 66 reset() { 67 this.currentTime = 0; 68 this.paused = true; 69 } 70} 71 72export const player = new PlayerState();