WIP: Rewrite queue

This commit is contained in:
wvffle 2022-10-20 08:51:41 +00:00 committed by Georg Krause
parent 3bf7dd98a2
commit c08e1fad94
No known key found for this signature in database
GPG Key ID: 2970D504B2183D22
5 changed files with 136 additions and 77 deletions

View File

@ -22,9 +22,10 @@
"@sentry/tracing": "7.17.2",
"@sentry/vue": "7.17.2",
"@vue/runtime-core": "3.2.41",
"@vueuse/core": "9.3.0",
"@vueuse/integrations": "9.3.0",
"@vueuse/router": "9.3.0",
"@vueuse/core": "9.3.1",
"@vueuse/integrations": "9.3.1",
"@vueuse/math": "9.3.1",
"@vueuse/router": "9.3.1",
"axios": "0.27.2",
"axios-auth-refresh": "3.3.4",
"diff": "5.1.0",

View File

@ -16,13 +16,24 @@ import {
loading as isLoadingAudio
} from '~/composables/audio/player'
import {
hasPrevious,
playPrevious,
hasNext,
playNext,
tracks,
currentIndex,
currentTrack,
isShuffling,
shuffle
} from '~/composables/audio/queue'
import { useMouse, useWindowSize } from '@vueuse/core'
import { useGettext } from 'vue3-gettext'
import { computed, ref } from 'vue'
import { useStore } from '~/store'
import onKeyboardShortcut from '~/composables/onKeyboardShortcut'
import useQueue from '~/composables/audio/useQueue'
import time from '~/utils/time'
import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue'
@ -36,19 +47,6 @@ const toggleMobilePlayer = () => {
store.commit('ui/queueFocused', ['queue', 'player'].includes(store.state.ui.queueFocused as string) ? null : 'player')
}
const {
isShuffling,
shuffle,
previous,
isEmpty: queueIsEmpty,
hasNext,
hasPrevious,
currentTrack,
currentIndex,
tracks,
next
} = useQueue()
// Key binds
onKeyboardShortcut('e', toggleMobilePlayer)
onKeyboardShortcut('p', () => { isPlaying.value = !isPlaying.value })
@ -67,8 +65,8 @@ onKeyboardShortcut(['shift', 'right'], () => seekBy(30), true)
onKeyboardShortcut('left', () => seekBy(-5), true)
onKeyboardShortcut(['shift', 'left'], () => seekBy(-30), true)
onKeyboardShortcut(['ctrl', 'shift', 'left'], previous, true)
onKeyboardShortcut(['ctrl', 'shift', 'right'], next, true)
onKeyboardShortcut(['ctrl', 'shift', 'left'], playPrevious, true)
onKeyboardShortcut(['ctrl', 'shift', 'right'], playNext, true)
const labels = computed(() => ({
audioPlayer: $pgettext('Sidebar/Player/Hidden text', 'Media player'),
@ -188,10 +186,10 @@ const currentTimeFormatted = computed(() => time.parse(Math.round(currentTime.va
<div class="meta">
<router-link
class="discrete link"
:to="{name: 'library.artists.detail', params: {id: currentTrack.artist.id }}"
:to="{name: 'library.artists.detail', params: {id: currentTrack.artist?.id }}"
@click.stop.prevent=""
>
{{ currentTrack.artist.name }}
{{ currentTrack.artist?.name }}
</router-link>
<template v-if="currentTrack.album">
/
@ -231,7 +229,7 @@ const currentTimeFormatted = computed(() => time.parse(Math.round(currentTime.va
{{ currentTrack.title }}
</strong>
<div class="meta">
{{ currentTrack.artist.name }}<template v-if="currentTrack.album">
{{ currentTrack.artist?.name }}<template v-if="currentTrack.album">
/ {{ currentTrack.album.title }}
</template>
</div>
@ -264,7 +262,7 @@ const currentTimeFormatted = computed(() => time.parse(Math.round(currentTime.va
:aria-label="labels.previous"
:disabled="!hasPrevious"
class="circular button control tablet-and-up"
@click.prevent.stop="$store.dispatch('queue/previous')"
@click.prevent.stop="playPrevious"
>
<i :class="['ui', 'large', {'disabled': !hasPrevious}, 'backward step', 'icon']" />
</button>
@ -291,7 +289,7 @@ const currentTimeFormatted = computed(() => time.parse(Math.round(currentTime.va
:aria-label="labels.next"
:disabled="!hasNext"
class="circular button control"
@click.prevent.stop="$store.dispatch('queue/next')"
@click.prevent.stop="playNext"
>
<i :class="['ui', 'large', {'disabled': !hasNext}, 'forward step', 'icon']" />
</button>
@ -335,7 +333,7 @@ const currentTimeFormatted = computed(() => time.parse(Math.round(currentTime.va
<button
class="circular control button"
:disabled="queueIsEmpty || null"
:disabled="tracks.length === 0"
:title="labels.shuffle"
:aria-label="labels.shuffle"
@click.prevent.stop="shuffle()"
@ -346,7 +344,7 @@ const currentTimeFormatted = computed(() => time.parse(Math.round(currentTime.va
/>
<i
v-else
:class="['ui', 'random', {'disabled': queueIsEmpty}, 'icon']"
:class="['ui', 'random', {'disabled': tracks.length === 0}, 'icon']"
/>
</button>
</div>

View File

@ -0,0 +1,59 @@
import type { Track } from '~/types'
import { isPlaying, looping, LoopingMode } from '~/composables/audio/player'
import { currentSound } from '~/composables/audio/tracks'
import { toReactive, useStorage } from '@vueuse/core'
import { useClamp } from '@vueuse/math'
import { computed, ref } from 'vue'
// Queue
export const tracks = toReactive(useStorage('queue:tracks', [] as Track[]))
// Current Index
export const currentIndex = useClamp(useStorage('queue:index', 0), 0, () => tracks.length)
export const currentTrack = computed(() => tracks[currentIndex.value])
// Play track
export const playTrack = async (trackIndex: number, force = false) => {
if (isPlaying.value) currentSound.value?.pause()
if (force && currentIndex.value === trackIndex) {
currentSound.value?.seekTo(0)
if (isPlaying.value) currentSound.value?.play()
return
}
currentIndex.value = trackIndex
if (isPlaying.value) currentSound.value?.play()
}
// Previous track
export const hasPrevious = computed(() => looping.value === LoopingMode.LoopQueue || currentIndex.value !== 0)
export const playPrevious = async () => {
// Loop entire queue / change track to the next one
if (looping.value === LoopingMode.LoopQueue && currentIndex.value === 0) {
// Loop track programmatically if it is the only track in the queue
if (tracks.length === 1) return playTrack(currentIndex.value, true)
return playTrack(tracks.length - 1)
}
return playTrack(currentIndex.value - 1)
}
// Next track
export const hasNext = computed(() => looping.value === LoopingMode.LoopQueue || currentIndex.value !== tracks.length - 1)
export const playNext = async () => {
// Loop entire queue / change track to the next one
if (looping.value === LoopingMode.LoopQueue && currentIndex.value === tracks.length - 1) {
// Loop track programmatically if it is the only track in the queue
if (tracks.length === 1) return playTrack(currentIndex.value, true)
return playTrack(0)
}
return playTrack(currentIndex.value + 1)
}
// Shuffle
export const isShuffling = ref(false)
// TODO: Shuffle
export const shuffle = () => undefined

View File

@ -5,27 +5,14 @@ import { connectAudioSource } from '~/composables/audio/audio-api'
import { isPlaying } from '~/composables/audio/player'
import { soundImplementation } from '~/api/player'
import { computed, shallowReactive } from 'vue'
import useQueue from '~/composables/audio/useQueue'
import { playNext, tracks, currentIndex } from '~/composables/audio/queue'
import store from '~/store'
import axios from 'axios'
const createOnEndHandler = (sound: Sound) => () => {
console.log('TRACK ENDED, PLAYING NEXT')
createTrack(currentIndex.value + 1)
// NOTE: We push it to the end of the job queue
setTimeout(() => {
store.dispatch('queue/next')
}, 0)
}
const ALLOWED_PLAY_TYPES: (CanPlayTypeResult | undefined)[] = ['maybe', 'probably']
const AUDIO_ELEMENT = document.createElement('audio')
const { tracks, currentIndex } = useQueue()
const soundPromises = new Map<number, Promise<Sound>>()
const soundCache = shallowReactive(new Map<number, Sound>())
@ -78,7 +65,13 @@ export const createSound = async (track: Track): Promise<Sound> => {
const SoundImplementation = soundImplementation.value
const sound = new SoundImplementation(sources)
sound.onSoundEnd(createOnEndHandler(sound))
sound.onSoundEnd(() => {
console.log('TRACK ENDED, PLAYING NEXT')
createTrack(currentIndex.value + 1)
// NOTE: We push it to the end of the job queue
setTimeout(playNext, 0)
})
soundCache.set(track.id, sound)
soundPromises.delete(track.id)
@ -92,10 +85,10 @@ export const createSound = async (track: Track): Promise<Sound> => {
// Create track from queue
export const createTrack = async (index: number) => {
if (tracks.value.length <= index || index === -1) return
if (tracks.length <= index || index === -1) return
console.log('LOADING TRACK')
const track = tracks.value[index]
const track = tracks[index]
if (!soundPromises.has(track.id) && !soundCache.has(track.id)) {
// TODO (wvffle): Resolve race condition - is it still here after adding soundPromises?
console.log('NO TRACK IN CACHE, CREATING')
@ -112,12 +105,12 @@ export const createTrack = async (index: number) => {
}
// NOTE: Preload next track
if (index === currentIndex.value && index + 1 < tracks.value.length) {
if (index === currentIndex.value && index + 1 < tracks.length) {
setTimeout(async () => {
const sound = await createSound(tracks.value[index + 1])
const sound = await createSound(tracks[index + 1])
await sound.preload()
}, 100)
}
}
export const currentSound = computed(() => soundCache.get(tracks.value[currentIndex.value]?.id ?? -1))
export const currentSound = computed(() => soundCache.get(tracks[currentIndex.value]?.id ?? -1))

View File

@ -1516,10 +1516,10 @@
resolved "https://registry.yarnpkg.com/@earltp/vue-virtual-scroller/-/vue-virtual-scroller-1.0.1.tgz#75116ef9b091457a654d92ff0688e991b3cd9e8a"
integrity sha512-7UsmP2JALnkfWlheuWRDywuBUTLJcVPE86X5ogA3djUmYFybE6qximgQ7OgyJnrKLteWR7+1Cp0GUXHhdDKaDQ==
"@types/web-bluetooth@^0.0.15":
version "0.0.15"
resolved "https://registry.yarnpkg.com/@types/web-bluetooth/-/web-bluetooth-0.0.15.tgz#d60330046a6ed8a13b4a53df3813c44942ebdf72"
integrity sha512-w7hEHXnPMEZ+4nGKl/KDRVpxkwYxYExuHOYXyzIzCDzEZ9ZCGMAewulr9IqJu2LR4N37fcnb1XVeuZ09qgOxhA==
"@types/web-bluetooth@^0.0.16":
version "0.0.16"
resolved "https://registry.yarnpkg.com/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz#1d12873a8e49567371f2a75fe3e7f7edca6662d8"
integrity sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==
"@typescript-eslint/eslint-plugin@5.41.0", "@typescript-eslint/eslint-plugin@^5.0.0":
version "5.41.0"
@ -1888,42 +1888,50 @@
resolved "https://registry.yarnpkg.com/@vue/tsconfig/-/tsconfig-0.1.3.tgz#4a61dbd29783d01ddab504276dcf0c2b6988654f"
integrity sha512-kQVsh8yyWPvHpb8gIc9l/HIDiiVUy1amynLNpCy8p+FoCiZXCo6fQos5/097MmnNZc9AtseDsCrfkhqCrJ8Olg==
"@vueuse/core@9.3.0":
version "9.3.0"
resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-9.3.0.tgz#74d855bd19cb5eadd2edb30c871918fac881e8b8"
integrity sha512-64Rna8IQDWpdrJxgitDg7yv1yTp41ZmvV8zlLEylK4QQLWAhz1OFGZDPZ8bU4lwcGgbEJ2sGi2jrdNh4LttUSQ==
"@vueuse/core@9.3.1":
version "9.3.1"
resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-9.3.1.tgz#47bc65da9c705fef7b3beca4af764491f0479647"
integrity sha512-xriyD+v3D2ObH/UtnkEl+1sbcLBVHNaZaLi/rqoNEe/B92hggDEFQIGXoQUjdRzYOjASHSezf9uCDtmd7LeWyA==
dependencies:
"@types/web-bluetooth" "^0.0.15"
"@vueuse/metadata" "9.3.0"
"@vueuse/shared" "9.3.0"
"@types/web-bluetooth" "^0.0.16"
"@vueuse/metadata" "9.3.1"
"@vueuse/shared" "9.3.1"
vue-demi "*"
"@vueuse/integrations@9.3.0":
version "9.3.0"
resolved "https://registry.yarnpkg.com/@vueuse/integrations/-/integrations-9.3.0.tgz#d08f0b96335746242e0a979d58964f374eed303a"
integrity sha512-KkJpC97VioZUpSw7rvgnqoLgTztLlLLGdYp6WQKn69cJiItsJVSRZrmI+X9YVxPBzuLvRymYZfp0RMyISVFHTw==
"@vueuse/integrations@9.3.1":
version "9.3.1"
resolved "https://registry.yarnpkg.com/@vueuse/integrations/-/integrations-9.3.1.tgz#c339e1a862d328629eb3daefd7c38cccd3d18e91"
integrity sha512-ydVHxJpLZrO9WbRXs1mNBbgEby8ERkiQumR2LebYVtY/1AFEn+mQ8MkL9pvcXwd9mrQSms+wzssu760n7DaDAw==
dependencies:
"@vueuse/core" "9.3.0"
"@vueuse/shared" "9.3.0"
"@vueuse/core" "9.3.1"
"@vueuse/shared" "9.3.1"
vue-demi "*"
"@vueuse/metadata@9.3.0":
version "9.3.0"
resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-9.3.0.tgz#c107fe77a577e1f221536cd1b291039c0c7c4bce"
integrity sha512-GnnfjbzIPJIh9ngL9s9oGU1+Hx/h5/KFqTfJykzh/1xjaHkedV9g0MASpdmPZIP+ynNhKAcEfA6g5i8KXwtoMA==
"@vueuse/router@9.3.0":
version "9.3.0"
resolved "https://registry.yarnpkg.com/@vueuse/router/-/router-9.3.0.tgz#731b2e88718f8ca13202e527864686a122ccc68f"
integrity sha512-UFN2MFciprH21oYsAgNHeDJ4Bd86HpRm9gximSN8j6h4fc2aa62fvfhprfHqdTxYAcgcGkMwcc9TO75jOvr8gg==
"@vueuse/math@9.3.1":
version "9.3.1"
resolved "https://registry.yarnpkg.com/@vueuse/math/-/math-9.3.1.tgz#547c6932ff0a7433e7a10bd6879a29b8adf07542"
integrity sha512-6ojNHKlhs57NViEzTOQMAOZ0QpBxRalx4kFgIV5M+5DHAwi2AAwZpYwtI+PKKHrIvfdB3fx+pkz7pcjnh9GBDQ==
dependencies:
"@vueuse/shared" "9.3.0"
"@vueuse/shared" "9.3.1"
vue-demi "*"
"@vueuse/shared@9.3.0":
version "9.3.0"
resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-9.3.0.tgz#40fc138ba4e379c894075830aa2e15404aaa8a5b"
integrity sha512-caGUWLY0DpPC6l31KxeUy6vPVNA0yKxx81jFYLoMpyP6cF84FG5Dkf69DfSUqL57wX8JcUkJDMnQaQIZPWFEQQ==
"@vueuse/metadata@9.3.1":
version "9.3.1"
resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-9.3.1.tgz#4e04d76df1e16f1aede28590c8b7a113ea3d3a7e"
integrity sha512-G1BPhtx3OHaL/y4OZBofh6Xt02G1VA9PuOO8nac9sTKMkMqfyez5VfkF3D9GUjSRNO7cVWyH4rceeGXfr2wdMg==
"@vueuse/router@9.3.1":
version "9.3.1"
resolved "https://registry.yarnpkg.com/@vueuse/router/-/router-9.3.1.tgz#67d90df3b0d1bdca23eaf69b1a40fc72283a4658"
integrity sha512-iyxyGNAeK8y4xaKjidnTI2H6eFiCiHypjoKPDEE8kdfESoxjegigp/DYfrbFt3IIeAHXPvBMq11m9IFuZ+LyLw==
dependencies:
"@vueuse/shared" "9.3.1"
vue-demi "*"
"@vueuse/shared@9.3.1":
version "9.3.1"
resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-9.3.1.tgz#78c6cf41d8b75f05460e18a80819bf8164b597a5"
integrity sha512-YFu3qcnVeu0S2L4XdQJtBpDcjz6xwqHZtTv/XRhu66/yge1XVhxskUcc7VZbX52xF9A34V6KCfwncP9YDqYFiw==
dependencies:
vue-demi "*"