2022-10-28 07:34:24 +00:00
|
|
|
import { createGlobalState, tryOnMounted, useIntervalFn, useRafFn, useStorage, useTimeoutFn, whenever } from '@vueuse/core'
|
2022-10-28 14:31:50 +00:00
|
|
|
import { computed, ref, watch, watchEffect, type Ref } from 'vue'
|
2022-10-28 07:34:24 +00:00
|
|
|
import { useTracks } from '~/composables/audio/tracks'
|
2022-10-19 11:00:44 +00:00
|
|
|
import { setGain } from './audio-api'
|
|
|
|
|
2022-10-28 07:34:24 +00:00
|
|
|
import { useQueue, currentIndex, currentTrack } from './queue'
|
|
|
|
import { useStore } from '~/store'
|
2022-10-18 21:48:47 +00:00
|
|
|
|
2022-10-28 07:34:24 +00:00
|
|
|
import axios from 'axios'
|
2022-10-18 21:48:47 +00:00
|
|
|
|
|
|
|
// Looping
|
|
|
|
export enum LoopingMode {
|
|
|
|
None,
|
|
|
|
LoopTrack,
|
|
|
|
LoopQueue
|
|
|
|
}
|
|
|
|
|
|
|
|
const MODE_MAX = 1 + Math.max(...Object.values(LoopingMode).filter(mode => typeof mode === 'number') as number[])
|
|
|
|
|
|
|
|
export const looping: Ref<number> = useStorage('player:looping', LoopingMode.None)
|
|
|
|
export const toggleLooping = () => {
|
|
|
|
looping.value += 1
|
|
|
|
looping.value %= MODE_MAX
|
|
|
|
}
|
|
|
|
|
2022-10-28 07:34:24 +00:00
|
|
|
// Is playing
|
|
|
|
export const isPlaying = ref(false)
|
|
|
|
|
|
|
|
// Use Player
|
|
|
|
export const usePlayer = createGlobalState(() => {
|
2022-10-28 14:31:50 +00:00
|
|
|
const { currentSound } = useTracks()
|
2022-10-28 07:34:24 +00:00
|
|
|
const { playNext } = useQueue()
|
2022-10-18 22:21:14 +00:00
|
|
|
|
2022-10-28 07:34:24 +00:00
|
|
|
watchEffect(() => {
|
|
|
|
const sound = currentSound.value
|
|
|
|
if (!sound) return
|
|
|
|
|
|
|
|
if (isPlaying.value) {
|
2022-12-27 13:18:56 +00:00
|
|
|
return sound.play()
|
2022-10-28 07:34:24 +00:00
|
|
|
}
|
|
|
|
|
2022-12-27 13:18:56 +00:00
|
|
|
return sound.pause()
|
2022-10-18 22:21:14 +00:00
|
|
|
})
|
|
|
|
|
2022-11-24 00:32:57 +00:00
|
|
|
// Create first track when we initialize the page
|
2022-10-28 07:34:24 +00:00
|
|
|
// NOTE: We want to have it called only once, hence we're using createGlobalState
|
|
|
|
const initializeFirstTrack = createGlobalState(() => tryOnMounted(() => {
|
|
|
|
const { initialize } = useTracks()
|
|
|
|
initialize()
|
2022-10-18 21:48:47 +00:00
|
|
|
|
2022-10-28 10:40:55 +00:00
|
|
|
const { trackRadioPopulating } = useQueue()
|
|
|
|
trackRadioPopulating()
|
|
|
|
|
|
|
|
trackListenSubmissions()
|
2022-10-28 07:34:24 +00:00
|
|
|
}))
|
2022-10-18 21:48:47 +00:00
|
|
|
|
2022-10-28 07:34:24 +00:00
|
|
|
// Volume
|
|
|
|
const lastVolume = useStorage('player:last-volume', 0.7)
|
2022-10-18 21:48:47 +00:00
|
|
|
|
2022-10-28 07:34:24 +00:00
|
|
|
const volume: Ref<number> = useStorage('player:volume', 0.7)
|
2022-11-16 07:04:02 +00:00
|
|
|
watch(volume, (gain) => setGain(gain), { immediate: true })
|
2022-10-18 21:48:47 +00:00
|
|
|
|
2022-10-28 07:34:24 +00:00
|
|
|
const mute = () => {
|
|
|
|
if (volume.value > 0) {
|
|
|
|
lastVolume.value = volume.value
|
|
|
|
volume.value = 0
|
|
|
|
return
|
|
|
|
}
|
2022-10-19 11:00:44 +00:00
|
|
|
|
2022-10-28 07:34:24 +00:00
|
|
|
if (lastVolume.value === 0) {
|
|
|
|
volume.value = 0.7
|
|
|
|
return
|
|
|
|
}
|
2022-10-19 11:00:44 +00:00
|
|
|
|
2022-10-28 07:34:24 +00:00
|
|
|
volume.value = lastVolume.value
|
2022-10-19 11:00:44 +00:00
|
|
|
}
|
|
|
|
|
2022-10-28 07:34:24 +00:00
|
|
|
watchEffect(() => {
|
|
|
|
const sound = currentSound.value
|
|
|
|
if (!sound) return
|
|
|
|
sound.looping = looping.value === LoopingMode.LoopTrack
|
|
|
|
})
|
2022-10-19 11:00:44 +00:00
|
|
|
|
2022-10-28 07:34:24 +00:00
|
|
|
watch(currentSound, sound => {
|
|
|
|
sound?.onSoundLoop(() => {
|
|
|
|
currentTime.value = 0
|
|
|
|
})
|
|
|
|
})
|
2022-10-18 21:48:47 +00:00
|
|
|
|
2022-10-28 07:34:24 +00:00
|
|
|
// Duration
|
|
|
|
const duration = ref(0)
|
|
|
|
watchEffect(() => {
|
|
|
|
const sound = currentSound.value
|
|
|
|
if (sound?.isLoaded.value === true) {
|
|
|
|
duration.value = sound.duration ?? 0
|
|
|
|
currentTime.value = sound.currentTime
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
duration.value = 0
|
|
|
|
})
|
2022-10-18 21:48:47 +00:00
|
|
|
|
2022-10-28 07:34:24 +00:00
|
|
|
// Current time
|
|
|
|
const currentTime = ref(0)
|
|
|
|
useIntervalFn(() => {
|
|
|
|
const sound = currentSound.value
|
|
|
|
if (!sound) {
|
|
|
|
currentTime.value = 0
|
|
|
|
return
|
|
|
|
}
|
2022-10-07 15:22:22 +00:00
|
|
|
|
2022-10-28 07:34:24 +00:00
|
|
|
currentTime.value = sound.currentTime
|
|
|
|
}, 1000)
|
|
|
|
|
|
|
|
// Submit listens
|
|
|
|
const listenSubmitted = ref(false)
|
2022-10-28 10:40:55 +00:00
|
|
|
const trackListenSubmissions = () => {
|
|
|
|
const store = useStore()
|
|
|
|
whenever(listenSubmitted, async () => {
|
|
|
|
console.log('Listening submitted!')
|
|
|
|
if (!store.state.auth.authenticated) return
|
|
|
|
if (!currentTrack.value) return
|
|
|
|
|
|
|
|
await axios.post('history/listenings/', { track: currentTrack.value.id })
|
|
|
|
.catch((error) => console.error('Could not record track in history', error))
|
|
|
|
})
|
|
|
|
}
|
2022-10-28 07:34:24 +00:00
|
|
|
|
|
|
|
watch(currentTime, (time) => {
|
|
|
|
const sound = currentSound.value
|
|
|
|
if (!sound) {
|
|
|
|
listenSubmitted.value = false
|
|
|
|
return
|
|
|
|
}
|
2022-10-18 21:48:47 +00:00
|
|
|
|
2022-10-28 07:34:24 +00:00
|
|
|
// https://listenbrainz.readthedocs.io/en/latest/users/api/core.html?highlight=half#post--1-submit-listens
|
|
|
|
listenSubmitted.value = time > Math.min(sound.duration / 2, 4 * 60)
|
|
|
|
})
|
|
|
|
|
|
|
|
// Seeking
|
|
|
|
const seekBy = async (seconds: number) => {
|
|
|
|
const sound = currentSound.value
|
|
|
|
if (!sound) return
|
|
|
|
|
|
|
|
await sound.seekBy(seconds)
|
|
|
|
currentTime.value = sound.currentTime
|
2022-10-18 21:48:47 +00:00
|
|
|
}
|
|
|
|
|
2022-10-28 07:34:24 +00:00
|
|
|
const seekTo = async (seconds: number) => {
|
|
|
|
const sound = currentSound.value
|
|
|
|
if (!sound) return
|
2022-10-18 21:48:47 +00:00
|
|
|
|
2022-10-28 07:34:24 +00:00
|
|
|
await sound.seekTo(seconds)
|
|
|
|
currentTime.value = sound.currentTime
|
2022-10-18 21:48:47 +00:00
|
|
|
}
|
|
|
|
|
2022-10-28 07:34:24 +00:00
|
|
|
// Buffer progress
|
|
|
|
const bufferProgress = ref(0)
|
|
|
|
useIntervalFn(() => {
|
|
|
|
const sound = currentSound.value
|
|
|
|
if (!sound) {
|
|
|
|
bufferProgress.value = 0
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
bufferProgress.value = sound.buffered / sound.duration * 100
|
|
|
|
}, 1000)
|
|
|
|
|
|
|
|
// Progress
|
2023-03-01 00:17:16 +00:00
|
|
|
useRafFn(() => {
|
|
|
|
const sound = currentSound.value
|
|
|
|
document.documentElement.style.setProperty('--fw-track-progress', sound
|
|
|
|
? `${(sound.currentTime / sound.duration * 100).toFixed(Math.log(window.innerWidth))}%`
|
|
|
|
: '0'
|
|
|
|
)
|
|
|
|
})
|
2022-10-18 21:48:47 +00:00
|
|
|
|
2022-10-28 07:34:24 +00:00
|
|
|
// Loading
|
|
|
|
const loading = computed(() => {
|
|
|
|
const sound = currentSound.value
|
|
|
|
if (!sound) return false
|
|
|
|
return !sound.isLoaded.value
|
|
|
|
})
|
2022-10-25 19:07:36 +00:00
|
|
|
|
2022-10-28 07:34:24 +00:00
|
|
|
// Errored
|
|
|
|
const errored = computed(() => {
|
|
|
|
const sound = currentSound.value
|
|
|
|
if (!sound) return false
|
|
|
|
return sound.isErrored.value
|
|
|
|
})
|
2022-10-25 19:07:36 +00:00
|
|
|
|
2023-01-28 23:04:09 +00:00
|
|
|
const { start: startErrorTimeout, stop: stopErrorTimeout } = useTimeoutFn(() => {
|
|
|
|
if (looping.value !== LoopingMode.LoopTrack) {
|
|
|
|
return playNext()
|
|
|
|
}
|
|
|
|
|
|
|
|
isPlaying.value = false
|
|
|
|
}, 3000, { immediate: false })
|
|
|
|
|
2022-10-28 12:29:58 +00:00
|
|
|
watch(currentIndex, stopErrorTimeout)
|
|
|
|
whenever(errored, startErrorTimeout)
|
2022-10-28 07:34:24 +00:00
|
|
|
|
|
|
|
return {
|
|
|
|
initializeFirstTrack,
|
|
|
|
isPlaying,
|
|
|
|
volume,
|
|
|
|
mute,
|
|
|
|
looping,
|
|
|
|
LoopingMode,
|
|
|
|
toggleLooping,
|
|
|
|
duration,
|
|
|
|
currentTime,
|
|
|
|
seekBy,
|
|
|
|
seekTo,
|
|
|
|
bufferProgress,
|
|
|
|
loading,
|
|
|
|
errored
|
|
|
|
}
|
|
|
|
})
|