WIP Rewrite queue

This commit is contained in:
wvffle 2022-10-23 07:41:38 +00:00 committed by Georg Krause
parent 15f5056a59
commit bef0d1dec4
No known key found for this signature in database
GPG Key ID: 2970D504B2183D22
10 changed files with 188 additions and 163 deletions

View File

@ -33,6 +33,7 @@
"focus-trap": "7.0.0", "focus-trap": "7.0.0",
"fomantic-ui-css": "2.8.8", "fomantic-ui-css": "2.8.8",
"howler": "2.2.3", "howler": "2.2.3",
"idb-keyval": "^6.2.0",
"js-logger": "1.6.1", "js-logger": "1.6.1",
"lodash-es": "4.17.21", "lodash-es": "4.17.21",
"moment": "2.29.4", "moment": "2.29.4",

View File

@ -1,10 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Track, QueueItemSource } from '~/types' import type { QueueItemSource } from '~/types'
import { useStore } from '~/store' import { useStore } from '~/store'
import { nextTick, ref, computed, watchEffect, onMounted } from 'vue' import { nextTick, ref, computed, watchEffect, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import time from '~/utils/time'
import { useFocusTrap } from '@vueuse/integrations/useFocusTrap' import { useFocusTrap } from '@vueuse/integrations/useFocusTrap'
import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue' import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue'
import TrackPlaylistIcon from '~/components/playlists/TrackPlaylistIcon.vue' import TrackPlaylistIcon from '~/components/playlists/TrackPlaylistIcon.vue'
@ -16,6 +15,8 @@ import usePlayer from '~/composables/audio/usePlayer'
import VirtualList from '~/components/vui/list/VirtualList.vue' import VirtualList from '~/components/vui/list/VirtualList.vue'
import QueueItem from '~/components/QueueItem.vue' import QueueItem from '~/components/QueueItem.vue'
import { queue } from '~/composables/audio/queue'
const queueModal = ref() const queueModal = ref()
const { activate, deactivate } = useFocusTrap(queueModal, { allowOutsideClick: true, preventScroll: true }) const { activate, deactivate } = useFocusTrap(queueModal, { allowOutsideClick: true, preventScroll: true })
@ -114,24 +115,14 @@ const play = (index: unknown) => {
resume() resume()
} }
const getCover = (track: Track) => { const queueItems = computed(() => queue.value.map((track, index) => ({
return store.getters['instance/absoluteUrl']( ...track,
track.cover?.urls.medium_square_crop
?? track.album?.cover?.urls.medium_square_crop
?? new URL('../assets/audio/default-cover.png', import.meta.url).href
)
}
const queueItems = computed(() => tracks.value.map((track, index) => ({
id: `${index}-${track.id}`, id: `${index}-${track.id}`,
track,
coverUrl: getCover(track),
labels: { labels: {
remove: $pgettext('*/*/*', 'Remove'), remove: $pgettext('*/*/*', 'Remove'),
selectTrack: $pgettext('*/*/*', 'Select track'), selectTrack: $pgettext('*/*/*', 'Select track'),
favorite: $pgettext('*/*/*', 'Favorite track') favorite: $pgettext('*/*/*', 'Favorite track')
}, }
duration: time.durationFormatted(track.uploads[0]?.duration ?? 0) ?? ''
}) as QueueItemSource)) }) as QueueItemSource))
const reorderTracks = async (from: number, to: number) => { const reorderTracks = async (from: number, to: number) => {

View File

@ -36,17 +36,17 @@ defineProps<Props>()
<div @click="$emit('play', index)"> <div @click="$emit('play', index)">
<button <button
class="title reset ellipsis" class="title reset ellipsis"
:title="source.track.title" :title="source.title"
:aria-label="source.labels.selectTrack" :aria-label="source.labels.selectTrack"
> >
<strong>{{ source.track.title }}</strong><br> <strong>{{ source.title }}</strong><br>
<span> <span>
{{ source.track.artist?.name }} {{ source.artistName }}
</span> </span>
</button> </button>
</div> </div>
<div class="duration-cell"> <div class="duration-cell">
<template v-if="source.track.uploads.length > 0"> <template v-if="source.sources.length > 0">
{{ source.duration }} {{ source.duration }}
</template> </template>
</div> </div>
@ -55,10 +55,10 @@ defineProps<Props>()
:aria-label="source.labels.favorite" :aria-label="source.labels.favorite"
:title="source.labels.favorite" :title="source.labels.favorite"
class="ui really basic circular icon button" class="ui really basic circular icon button"
@click.stop="$store.dispatch('favorites/toggle', source.track.id)" @click.stop="$store.dispatch('favorites/toggle', source.id)"
> >
<i <i
:class="$store.getters['favorites/isFavorite'](source.track.id) ? 'pink' : ''" :class="$store.getters['favorites/isFavorite'](source.id) ? 'pink' : ''"
class="heart icon" class="heart icon"
/> />
</button> </button>

View File

@ -21,7 +21,7 @@ import {
playPrevious, playPrevious,
hasNext, hasNext,
playNext, playNext,
tracks, queue,
currentIndex, currentIndex,
currentTrack, currentTrack,
shuffle shuffle
@ -35,8 +35,8 @@ import { useStore } from '~/store'
import onKeyboardShortcut from '~/composables/onKeyboardShortcut' import onKeyboardShortcut from '~/composables/onKeyboardShortcut'
import time from '~/utils/time' import time from '~/utils/time'
import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue' // import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue'
import TrackPlaylistIcon from '~/components/playlists/TrackPlaylistIcon.vue' // import TrackPlaylistIcon from '~/components/playlists/TrackPlaylistIcon.vue'
import VolumeControl from './VolumeControl.vue' import VolumeControl from './VolumeControl.vue'
const store = useStore() const store = useStore()
@ -152,21 +152,9 @@ const currentTimeFormatted = computed(() => time.parse(Math.round(currentTime.va
@click.stop.prevent="$router.push({name: 'library.tracks.detail', params: {id: currentTrack.id }})" @click.stop.prevent="$router.push({name: 'library.tracks.detail', params: {id: currentTrack.id }})"
> >
<img <img
v-if="currentTrack.cover && currentTrack.cover.urls.original"
ref="cover" ref="cover"
alt="" alt=""
:src="$store.getters['instance/absoluteUrl'](currentTrack.cover.urls.medium_square_crop)" :src="$store.getters['instance/absoluteUrl'](currentTrack.coverUrl)"
>
<img
v-else-if="currentTrack.album && currentTrack.album.cover && currentTrack.album.cover.urls && currentTrack.album.cover.urls.original"
ref="cover"
alt=""
:src="$store.getters['instance/absoluteUrl'](currentTrack.album.cover.urls.medium_square_crop)"
>
<img
v-else
alt=""
src="../../assets/audio/default-cover.png"
> >
</div> </div>
<div <div
@ -185,19 +173,19 @@ const currentTimeFormatted = computed(() => time.parse(Math.round(currentTime.va
<div class="meta"> <div class="meta">
<router-link <router-link
class="discrete link" class="discrete link"
:to="{name: 'library.artists.detail', params: {id: currentTrack.artist?.id }}" :to="{name: 'library.artists.detail', params: {id: currentTrack.artistId }}"
@click.stop.prevent="" @click.stop.prevent=""
> >
{{ currentTrack.artist?.name }} {{ currentTrack.artistName }}
</router-link> </router-link>
<template v-if="currentTrack.album"> <template v-if="currentTrack.albumId !== -1">
/ /
<router-link <router-link
class="discrete link" class="discrete link"
:to="{name: 'library.albums.detail', params: {id: currentTrack.album.id }}" :to="{name: 'library.albums.detail', params: {id: currentTrack.albumId }}"
@click.stop.prevent="" @click.stop.prevent=""
> >
{{ currentTrack.album.title }} {{ currentTrack.albumTitle }}
</router-link> </router-link>
</template> </template>
</div> </div>
@ -206,21 +194,9 @@ const currentTimeFormatted = computed(() => time.parse(Math.round(currentTime.va
<div class="controls track-controls queue-not-focused desktop-and-below"> <div class="controls track-controls queue-not-focused desktop-and-below">
<div class="ui tiny image"> <div class="ui tiny image">
<img <img
v-if="currentTrack.cover && currentTrack.cover.urls.original"
ref="cover" ref="cover"
alt="" alt=""
:src="$store.getters['instance/absoluteUrl'](currentTrack.cover.urls.medium_square_crop)" :src="$store.getters['instance/absoluteUrl'](currentTrack.coverUrl)"
>
<img
v-else-if="currentTrack.album && currentTrack.album.cover && currentTrack.album.cover.urls.original"
ref="cover"
alt=""
:src="$store.getters['instance/absoluteUrl'](currentTrack.album.cover.urls.medium_square_crop)"
>
<img
v-else
alt=""
src="../../assets/audio/default-cover.png"
> >
</div> </div>
<div class="middle aligned content ellipsis"> <div class="middle aligned content ellipsis">
@ -228,8 +204,9 @@ const currentTimeFormatted = computed(() => time.parse(Math.round(currentTime.va
{{ currentTrack.title }} {{ currentTrack.title }}
</strong> </strong>
<div class="meta"> <div class="meta">
{{ currentTrack.artist?.name }}<template v-if="currentTrack.album"> {{ currentTrack.artistName }}
/ {{ currentTrack.album.title }} <template v-if="currentTrack.albumId !== -1">
/ {{ currentTrack.albumTitle }}
</template> </template>
</div> </div>
</div> </div>
@ -238,7 +215,8 @@ const currentTimeFormatted = computed(() => time.parse(Math.round(currentTime.va
v-if="$store.state.auth.authenticated" v-if="$store.state.auth.authenticated"
class="controls desktop-and-up fluid align-right" class="controls desktop-and-up fluid align-right"
> >
<track-favorite-icon <!-- TODO (wvffle): Uncomment -->
<!-- <track-favorite-icon
class="control white" class="control white"
:track="currentTrack" :track="currentTrack"
/> />
@ -253,7 +231,7 @@ const currentTimeFormatted = computed(() => time.parse(Math.round(currentTime.va
@click="$store.dispatch('moderation/hide', {type: 'artist', target: currentTrack.artist})" @click="$store.dispatch('moderation/hide', {type: 'artist', target: currentTrack.artist})"
> >
<i :class="['eye slash outline', 'basic', 'icon']" /> <i :class="['eye slash outline', 'basic', 'icon']" />
</button> </button> -->
</div> </div>
<div class="player-controls controls queue-not-focused"> <div class="player-controls controls queue-not-focused">
<button <button
@ -332,12 +310,12 @@ const currentTimeFormatted = computed(() => time.parse(Math.round(currentTime.va
<button <button
class="circular control button" class="circular control button"
:disabled="tracks.length === 0" :disabled="queue.length === 0"
:title="labels.shuffle" :title="labels.shuffle"
:aria-label="labels.shuffle" :aria-label="labels.shuffle"
@click.prevent.stop="shuffle()" @click.prevent.stop="shuffle()"
> >
<i :class="['ui', 'random', {'disabled': tracks.length === 0}, 'icon']" /> <i :class="['ui', 'random', {'disabled': queue.length === 0}, 'icon']" />
</button> </button>
</div> </div>
<div class="group"> <div class="group">
@ -350,7 +328,7 @@ const currentTimeFormatted = computed(() => time.parse(Math.round(currentTime.va
<i class="stream icon" /> <i class="stream icon" />
<translate <translate
translate-context="Sidebar/Queue/Text" translate-context="Sidebar/Queue/Text"
:translate-params="{index: currentIndex + 1, length: tracks.length}" :translate-params="{index: currentIndex + 1, length: queue.length}"
> >
%{ index } of %{ length } %{ index } of %{ length }
</translate> </translate>
@ -362,7 +340,7 @@ const currentTimeFormatted = computed(() => time.parse(Math.round(currentTime.va
<i class="stream icon" /> <i class="stream icon" />
<translate <translate
translate-context="Sidebar/Queue/Text" translate-context="Sidebar/Queue/Text"
:translate-params="{index: currentIndex + 1, length: tracks.length}" :translate-params="{index: currentIndex + 1, length: queue.length}"
> >
%{ index } of %{ length } %{ index } of %{ length }
</translate> </translate>

View File

@ -1,4 +1,5 @@
import { tryOnMounted, useIntervalFn, useRafFn, useStorage, whenever } from '@vueuse/core' import { tryOnMounted, useIntervalFn, useRafFn, useStorage, whenever } from '@vueuse/core'
import { currentTrack, currentIndex } from '~/composables/audio/queue'
import { currentSound, createTrack } from '~/composables/audio/tracks' import { currentSound, createTrack } from '~/composables/audio/tracks'
import { computed, ref, watch, watchEffect, type Ref } from 'vue' import { computed, ref, watch, watchEffect, type Ref } from 'vue'
import { setGain } from './audio-api' import { setGain } from './audio-api'
@ -6,10 +7,6 @@ import { setGain } from './audio-api'
import store from '~/store' import store from '~/store'
import axios from 'axios' import axios from 'axios'
import useQueue from '~/composables/audio/useQueue'
const { currentIndex, currentTrack } = useQueue()
export const isPlaying = ref(false) export const isPlaying = ref(false)
watch(isPlaying, (playing) => { watch(isPlaying, (playing) => {

View File

@ -1,46 +1,110 @@
import type { Track } from '~/types' import type { Track, Upload } from '~/types'
import { isPlaying, looping, LoopingMode } from '~/composables/audio/player' import { computedAsync, useStorage } from '@vueuse/core'
import { currentSound } from '~/composables/audio/tracks' import { shuffle as shuffleArray, uniq } from 'lodash-es'
import { toReactive, useStorage } from '@vueuse/core' import { getMany, setMany } from 'idb-keyval'
import { shuffle as shuffleArray } from 'lodash-es'
import { useClamp } from '@vueuse/math' import { useClamp } from '@vueuse/math'
import { computed } from 'vue' import { computed } from 'vue'
import axios from 'axios'
// import useWebWorker from '~/composables/useWebWorker' // import useWebWorker from '~/composables/useWebWorker'
// const { post, onMessageReceived } = useWebWorker('queue') // const { post, onMessageReceived } = useWebWorker('queue')
export interface QueueTrackSource {
uuid: string
mimetype: string
bitrate?: number
url: string
duration?: number
}
export interface QueueTrack {
id: number
title: string
artistName: string
albumTitle: string
// TODO: Add urls for those
coverUrl: string
artistId: number
albumId: number
sources: QueueTrackSource[]
}
// Queue // Queue
export const tracks = toReactive(useStorage('queue:tracks', [] as Track[])) export const tracks = useStorage('queue:tracks', [] as number[])
export const queue = computed(() => { const tracksById = computedAsync(async () => {
if (isShuffled.value) { const trackObjects = await getMany(uniq(tracks.value))
const tracksById = tracks.reduce((acc, track) => { return trackObjects.reduce((acc, track) => {
acc[track.id] = track acc[track.id] = track
return acc return acc
}, {} as Record<number, Track>) }, {}) as Record<number, QueueTrack>
}, {})
return shuffledIds.value.map(id => tracksById[id]) export const queue = computed(() => {
const indexedTracks = tracksById.value
if (isShuffled.value) {
return shuffledIds.value.map(id => indexedTracks[id]).filter(i => i)
} }
return tracks return tracks.value.map(id => indexedTracks[id]).filter(i => i)
}) })
export const enqueue = (...newTracks: Track[]) => { const createQueueTrack = async (track: Track): Promise<QueueTrack> => {
tracks.push(...newTracks) if (track.uploads.length === 0) {
// we don't have any information for this track, we need to fetch it
const { uploads } = await axios.get(`tracks/${track.id}/`)
.then(response => response.data as Track, () => ({ uploads: [] as Upload[] }))
track.uploads = uploads
}
return {
id: track.id,
title: track.title,
// TODO (wvffle): i18n
artistName: track.artist?.name ?? 'Unknown artist',
// TODO (wvffle): i18n
albumTitle: track.album?.title ?? 'Unknown album',
artistId: track.artist?.id ?? -1,
albumId: track.album?.id ?? -1,
coverUrl: (track.cover?.urls ?? track.album?.cover?.urls ?? track.artist?.cover?.urls)?.original
?? new URL('~/assets/audio/default-cover.png', import.meta.url).href,
sources: track.uploads.map(upload => ({
uuid: upload.uuid,
mimetype: upload.mimetype,
bitrate: upload.bitrate,
url: upload.listen_url
}))
}
}
export const enqueue = async (...newTracks: Track[]) => {
const queueTracks = await Promise.all(newTracks.map(createQueueTrack))
await setMany(queueTracks.map(track => [track.id, track]))
const ids = queueTracks.map(track => track.id)
tracks.value.push(...ids)
// Shuffle new tracks // Shuffle new tracks
if (isShuffled.value) { if (isShuffled.value) {
shuffledIds.value.push(...shuffleIds(newTracks)) shuffledIds.value.push(...shuffleArray(ids))
} }
} }
// Current Index // Current Index
export const currentIndex = useClamp(useStorage('queue:index', 0), 0, () => tracks.length) export const currentIndex = useClamp(useStorage('queue:index', 0), 0, () => tracks.value.length)
export const currentTrack = computed(() => tracks[currentIndex.value]) export const currentTrack = computed(() => queue.value[currentIndex.value])
// Play track // Play track
export const playTrack = async (trackIndex: number, force = false) => { export const playTrack = async (trackIndex: number, force = false) => {
const { currentSound } = await import('~/composables/audio/tracks')
const { isPlaying } = await import('~/composables/audio/player')
if (isPlaying.value) currentSound.value?.pause() if (isPlaying.value) currentSound.value?.pause()
if (force && currentIndex.value === trackIndex) { if (force && currentIndex.value === trackIndex) {
@ -54,25 +118,29 @@ export const playTrack = async (trackIndex: number, force = false) => {
} }
// Previous track // Previous track
export const hasPrevious = computed(() => looping.value === LoopingMode.LoopQueue || currentIndex.value !== 0) export const hasPrevious = computed(() => /* looping.value === LoopingMode.LoopQueue || */ currentIndex.value !== 0)
export const playPrevious = async () => { export const playPrevious = async () => {
const { looping, LoopingMode } = await import('~/composables/audio/player')
// Loop entire queue / change track to the next one // Loop entire queue / change track to the next one
if (looping.value === LoopingMode.LoopQueue && currentIndex.value === 0) { if (looping.value === LoopingMode.LoopQueue && currentIndex.value === 0) {
// Loop track programmatically if it is the only track in the queue // Loop track programmatically if it is the only track in the queue
if (tracks.length === 1) return playTrack(currentIndex.value, true) if (tracks.value.length === 1) return playTrack(currentIndex.value, true)
return playTrack(tracks.length - 1) return playTrack(tracks.value.length - 1)
} }
return playTrack(currentIndex.value - 1) return playTrack(currentIndex.value - 1)
} }
// Next track // Next track
export const hasNext = computed(() => looping.value === LoopingMode.LoopQueue || currentIndex.value !== tracks.length - 1) export const hasNext = computed(() => /* looping.value === LoopingMode.LoopQueue || */ currentIndex.value !== tracks.value.length - 1)
export const playNext = async () => { export const playNext = async () => {
const { looping, LoopingMode } = await import('~/composables/audio/player')
// Loop entire queue / change track to the next one // Loop entire queue / change track to the next one
if (looping.value === LoopingMode.LoopQueue && currentIndex.value === tracks.length - 1) { if (looping.value === LoopingMode.LoopQueue && currentIndex.value === tracks.value.length - 1) {
// Loop track programmatically if it is the only track in the queue // Loop track programmatically if it is the only track in the queue
if (tracks.length === 1) return playTrack(currentIndex.value, true) if (tracks.value.length === 1) return playTrack(currentIndex.value, true)
return playTrack(0) return playTrack(0)
} }
@ -80,8 +148,7 @@ export const playNext = async () => {
} }
// Shuffle // Shuffle
const shuffleIds = (tracks: Track[]) => shuffleArray(tracks.map(track => track.id)) const shuffledIds = useStorage('queue:tracks:shuffled', [] as number[])
const shuffledIds = useStorage('queue:shuffled-ids', [] as number[])
export const isShuffled = computed(() => shuffledIds.value.length !== 0) export const isShuffled = computed(() => shuffledIds.value.length !== 0)
export const shuffle = () => { export const shuffle = () => {
if (isShuffled.value) { if (isShuffled.value) {
@ -89,5 +156,5 @@ export const shuffle = () => {
return return
} }
shuffledIds.value = shuffleIds(tracks) shuffledIds.value = shuffleArray(tracks.value)
} }

View File

@ -1,14 +1,13 @@
import type { Sound, SoundSource } from '~/api/player' import type { QueueTrack, QueueTrackSource } from '~/composables/audio/queue'
import type { Track, Upload } from '~/types' import type { Sound } from '~/api/player'
import { connectAudioSource } from '~/composables/audio/audio-api'
import { isPlaying } from '~/composables/audio/player'
import { soundImplementation } from '~/api/player' import { soundImplementation } from '~/api/player'
import { computed, shallowReactive } from 'vue' import { computed, shallowReactive } from 'vue'
import { playNext, tracks, currentIndex } from '~/composables/audio/queue'
import { playNext, queue, currentTrack, currentIndex } from '~/composables/audio/queue'
import { connectAudioSource } from '~/composables/audio/audio-api'
import { isPlaying } from '~/composables/audio/player'
import store from '~/store' import store from '~/store'
import axios from 'axios'
const ALLOWED_PLAY_TYPES: (CanPlayTypeResult | undefined)[] = ['maybe', 'probably'] const ALLOWED_PLAY_TYPES: (CanPlayTypeResult | undefined)[] = ['maybe', 'probably']
const AUDIO_ELEMENT = document.createElement('audio') const AUDIO_ELEMENT = document.createElement('audio')
@ -16,42 +15,31 @@ const AUDIO_ELEMENT = document.createElement('audio')
const soundPromises = new Map<number, Promise<Sound>>() const soundPromises = new Map<number, Promise<Sound>>()
const soundCache = shallowReactive(new Map<number, Sound>()) const soundCache = shallowReactive(new Map<number, Sound>())
const getUploadSources = (uploads: Upload[]): SoundSource[] => { const getTrackSources = (track: QueueTrack): QueueTrackSource[] => {
const sources = uploads const sources: QueueTrackSource[] = track.sources
// NOTE: Filter out repeating and unplayable media types // NOTE: Filter out repeating and unplayable media types
.filter(({ mimetype }, index, array) => array.findIndex((upload) => upload.mimetype === mimetype) === index) .filter(({ mimetype, bitrate }, index, array) => array.findIndex((upload) => upload.mimetype + upload.bitrate === mimetype + bitrate) === index)
.filter(({ mimetype }) => ALLOWED_PLAY_TYPES.includes(AUDIO_ELEMENT.canPlayType(`${mimetype}`))) .filter(({ mimetype }) => ALLOWED_PLAY_TYPES.includes(AUDIO_ELEMENT.canPlayType(`${mimetype}`)))
.map((upload): SoundSource => ({ .map((source) => ({
...upload, ...source,
url: store.getters['instance/absoluteUrl'](upload.listen_url) as string url: store.getters['instance/absoluteUrl'](source.url) as string
})) }))
// NOTE: Add a transcoded MP3 src at the end for browsers // NOTE: Add a transcoded MP3 src at the end for browsers
// that do not support other codecs to be able to play it :) // that do not support other codecs to be able to play it :)
if (sources.length > 0 && !sources.some(({ mimetype }) => mimetype === 'audio/mpeg')) { if (sources.length > 0) {
const url = new URL(sources[0].url) const original = sources[0]
const url = new URL(original.url)
url.searchParams.set('to', 'mp3') url.searchParams.set('to', 'mp3')
sources.push({ uuid: 'transcoded', mimetype: 'audio/mpeg', url: url.toString() })
const bitrate = Math.min(320000, original.bitrate ?? Infinity)
sources.push({ uuid: 'transcoded', mimetype: 'audio/mpeg', url: url.toString(), bitrate })
} }
return sources return sources
} }
const getTrackSources = async (track: Track): Promise<SoundSource[]> => { export const createSound = async (track: QueueTrack): Promise<Sound> => {
if (track === undefined) return []
if (track.uploads.length === 0) {
// we don't have any information for this track, we need to fetch it
const { uploads } = await axios.get(`tracks/${track.id}/`)
.then(response => response.data as Track, () => ({ uploads: [] as Upload[] } as Track))
track.uploads = uploads
}
return getUploadSources(track.uploads)
}
export const createSound = async (track: Track): Promise<Sound> => {
if (soundCache.has(track.id)) { if (soundCache.has(track.id)) {
return soundCache.get(track.id) as Sound return soundCache.get(track.id) as Sound
} }
@ -61,7 +49,7 @@ export const createSound = async (track: Track): Promise<Sound> => {
} }
const createSoundPromise = async () => { const createSoundPromise = async () => {
const sources = await getTrackSources(track) const sources = getTrackSources(track)
const SoundImplementation = soundImplementation.value const SoundImplementation = soundImplementation.value
const sound = new SoundImplementation(sources) const sound = new SoundImplementation(sources)
@ -85,10 +73,10 @@ export const createSound = async (track: Track): Promise<Sound> => {
// Create track from queue // Create track from queue
export const createTrack = async (index: number) => { export const createTrack = async (index: number) => {
if (tracks.length <= index || index === -1) return if (queue.value.length <= index || index === -1) return
console.log('LOADING TRACK') console.log('LOADING TRACK')
const track = tracks[index] const track = queue.value[index]
if (!soundPromises.has(track.id) && !soundCache.has(track.id)) { if (!soundPromises.has(track.id) && !soundCache.has(track.id)) {
// TODO (wvffle): Resolve race condition - is it still here after adding soundPromises? // TODO (wvffle): Resolve race condition - is it still here after adding soundPromises?
console.log('NO TRACK IN CACHE, CREATING') console.log('NO TRACK IN CACHE, CREATING')
@ -105,12 +93,12 @@ export const createTrack = async (index: number) => {
} }
// NOTE: Preload next track // NOTE: Preload next track
if (index === currentIndex.value && index + 1 < tracks.length) { if (index === currentIndex.value && index + 1 < queue.value.length) {
setTimeout(async () => { setTimeout(async () => {
const sound = await createSound(tracks[index + 1]) const sound = await createSound(queue.value[index + 1])
await sound.preload() await sound.preload()
}, 100) }, 100)
} }
} }
export const currentSound = computed(() => soundCache.get(tracks[currentIndex.value]?.id ?? -1)) export const currentSound = computed(() => soundCache.get(currentTrack.value?.id ?? -1))

View File

@ -1,15 +1,17 @@
import type { Track, Artist, Album, Playlist, Library, Channel, Actor } from '~/types' import type { Track, Artist, Album, Playlist, Library, Channel, Actor } from '~/types'
import type { ContentFilter } from '~/store/moderation' import type { ContentFilter } from '~/store/moderation'
import { useStore } from '~/store'
import { useGettext } from 'vue3-gettext'
import { computed, markRaw, ref } from 'vue'
import axios from 'axios'
import usePlayer from '~/composables/audio/usePlayer'
import useQueue from '~/composables/audio/useQueue'
import { useCurrentElement } from '@vueuse/core' import { useCurrentElement } from '@vueuse/core'
import { computed, markRaw, ref } from 'vue'
import { useGettext } from 'vue3-gettext'
import { useStore } from '~/store'
import axios from 'axios'
import jQuery from 'jquery' import jQuery from 'jquery'
import { enqueue as addToQueue, currentTrack } from '~/composables/audio/queue'
import { isPlaying } from '~/composables/audio/player'
export interface PlayOptionsProps { export interface PlayOptionsProps {
isPlayable?: boolean isPlayable?: boolean
tracks?: Track[] tracks?: Track[]
@ -24,8 +26,6 @@ export interface PlayOptionsProps {
export default (props: PlayOptionsProps) => { export default (props: PlayOptionsProps) => {
const store = useStore() const store = useStore()
const { resume, pause, playing } = usePlayer()
const { currentTrack } = useQueue()
const playable = computed(() => { const playable = computed(() => {
if (props.isPlayable) { if (props.isPlayable) {
@ -134,7 +134,7 @@ export default (props: PlayOptionsProps) => {
jQuery(el.value).find('.ui.dropdown').dropdown('hide') jQuery(el.value).find('.ui.dropdown').dropdown('hide')
const tracks = await getPlayableTracks() const tracks = await getPlayableTracks()
await store.dispatch('queue/appendMany', { tracks }) await addToQueue(...tracks)
addMessage(tracks) addMessage(tracks)
} }
@ -148,40 +148,33 @@ export default (props: PlayOptionsProps) => {
if (next && !wasEmpty) { if (next && !wasEmpty) {
await store.dispatch('queue/next') await store.dispatch('queue/next')
resume() isPlaying.value = true
} }
addMessage(tracks) addMessage(tracks)
} }
const replacePlay = async () => { const replacePlay = async () => {
store.dispatch('queue/clean') const { tracks, playTrack } = await import('~/composables/audio/queue')
tracks.value.length = 0
jQuery(el.value).find('.ui.dropdown').dropdown('hide') jQuery(el.value).find('.ui.dropdown').dropdown('hide')
const tracks = await getPlayableTracks() const tracksToPlay = await getPlayableTracks()
await store.dispatch('queue/appendMany', { tracks }) await addToQueue(...tracksToPlay)
if (props.track && props.tracks?.length) { const trackIndex = props.tracks?.findIndex(track => track.id === props.track?.id && track.position === props.track?.position) ?? 0
// set queue position to selected track await playTrack(trackIndex)
const trackIndex = props.tracks.findIndex(track => track.id === props.track?.id && track.position === props.track?.position)
store.dispatch('queue/currentIndex', trackIndex)
} else {
store.dispatch('queue/currentIndex', 0)
}
resume() isPlaying.value = true
addMessage(tracks) playTrack(0, true)
addMessage(tracksToPlay)
} }
const activateTrack = (track: Track, index: number) => { const activateTrack = (track: Track, index: number) => {
// TODO (wvffle): Check if position checking did not break anything // TODO (wvffle): Check if position checking did not break anything
if (track.id === currentTrack.value?.id && track.position === currentTrack.value?.position) { if (track.id === currentTrack.value?.id && track.position === currentTrack.value?.position) {
if (playing.value) { isPlaying.value = true
return pause()
}
return resume()
} }
replacePlay() replacePlay()

View File

@ -6,6 +6,7 @@ import type { RootState } from '~/store'
// eslint-disable-next-line // eslint-disable-next-line
import type { ComponentPublicInstance } from '@vue/runtime-core' import type { ComponentPublicInstance } from '@vue/runtime-core'
import type { QueueTrack } from './composables/audio/queue'
export type FunctionRef = Element | ComponentPublicInstance | null export type FunctionRef = Element | ComponentPublicInstance | null
@ -18,11 +19,8 @@ export interface InitModuleContext {
export type InitModule = (ctx: InitModuleContext) => void | Promise<void> export type InitModule = (ctx: InitModuleContext) => void | Promise<void>
export interface QueueItemSource { export interface QueueItemSource extends Omit<QueueTrack, 'id'> {
id: string id: string
track: Track
duration: string
coverUrl: string
labels: { labels: {
remove: string remove: string

View File

@ -3621,6 +3621,13 @@ iconv-lite@0.6.3:
dependencies: dependencies:
safer-buffer ">= 2.1.2 < 3.0.0" safer-buffer ">= 2.1.2 < 3.0.0"
idb-keyval@^6.2.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-6.2.0.tgz#3af94a3cc0689d6ee0bc9e045d2a3340ea897173"
integrity sha512-uw+MIyQn2jl3+hroD7hF8J7PUviBU7BPKWw4f/ISf32D4LoGu98yHjrzWWJDASu9QNrX10tCJqk9YY0ClWm8Ng==
dependencies:
safari-14-idb-fix "^3.0.0"
idb@^7.0.1: idb@^7.0.1:
version "7.1.0" version "7.1.0"
resolved "https://registry.yarnpkg.com/idb/-/idb-7.1.0.tgz#2cc886be57738419e57f9aab58f647e5e2160270" resolved "https://registry.yarnpkg.com/idb/-/idb-7.1.0.tgz#2cc886be57738419e57f9aab58f647e5e2160270"
@ -4780,6 +4787,11 @@ run-parallel@^1.1.9:
dependencies: dependencies:
queue-microtask "^1.2.2" queue-microtask "^1.2.2"
safari-14-idb-fix@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/safari-14-idb-fix/-/safari-14-idb-fix-3.0.0.tgz#450fc049b996ec7f3fd9ca2f89d32e0761583440"
integrity sha512-eBNFLob4PMq8JA1dGyFn6G97q3/WzNtFK4RnzT1fnLq+9RyrGknzYiM/9B12MnKAxuj1IXr7UKYtTNtjyKMBog==
safe-buffer@^5.1.0: safe-buffer@^5.1.0:
version "5.2.1" version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"