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",
"fomantic-ui-css": "2.8.8",
"howler": "2.2.3",
"idb-keyval": "^6.2.0",
"js-logger": "1.6.1",
"lodash-es": "4.17.21",
"moment": "2.29.4",

View File

@ -1,10 +1,9 @@
<script setup lang="ts">
import type { Track, QueueItemSource } from '~/types'
import type { QueueItemSource } from '~/types'
import { useStore } from '~/store'
import { nextTick, ref, computed, watchEffect, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import time from '~/utils/time'
import { useFocusTrap } from '@vueuse/integrations/useFocusTrap'
import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.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 QueueItem from '~/components/QueueItem.vue'
import { queue } from '~/composables/audio/queue'
const queueModal = ref()
const { activate, deactivate } = useFocusTrap(queueModal, { allowOutsideClick: true, preventScroll: true })
@ -114,24 +115,14 @@ const play = (index: unknown) => {
resume()
}
const getCover = (track: Track) => {
return store.getters['instance/absoluteUrl'](
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) => ({
const queueItems = computed(() => queue.value.map((track, index) => ({
...track,
id: `${index}-${track.id}`,
track,
coverUrl: getCover(track),
labels: {
remove: $pgettext('*/*/*', 'Remove'),
selectTrack: $pgettext('*/*/*', 'Select track'),
favorite: $pgettext('*/*/*', 'Favorite track')
},
duration: time.durationFormatted(track.uploads[0]?.duration ?? 0) ?? ''
}
}) as QueueItemSource))
const reorderTracks = async (from: number, to: number) => {

View File

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

View File

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

View File

@ -1,4 +1,5 @@
import { tryOnMounted, useIntervalFn, useRafFn, useStorage, whenever } from '@vueuse/core'
import { currentTrack, currentIndex } from '~/composables/audio/queue'
import { currentSound, createTrack } from '~/composables/audio/tracks'
import { computed, ref, watch, watchEffect, type Ref } from 'vue'
import { setGain } from './audio-api'
@ -6,10 +7,6 @@ import { setGain } from './audio-api'
import store from '~/store'
import axios from 'axios'
import useQueue from '~/composables/audio/useQueue'
const { currentIndex, currentTrack } = useQueue()
export const isPlaying = ref(false)
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 { currentSound } from '~/composables/audio/tracks'
import { toReactive, useStorage } from '@vueuse/core'
import { shuffle as shuffleArray } from 'lodash-es'
import { computedAsync, useStorage } from '@vueuse/core'
import { shuffle as shuffleArray, uniq } from 'lodash-es'
import { getMany, setMany } from 'idb-keyval'
import { useClamp } from '@vueuse/math'
import { computed } from 'vue'
import axios from 'axios'
// import useWebWorker from '~/composables/useWebWorker'
// const { post, onMessageReceived } = useWebWorker('queue')
// Queue
export const tracks = toReactive(useStorage('queue:tracks', [] as Track[]))
export const queue = computed(() => {
if (isShuffled.value) {
const tracksById = tracks.reduce((acc, track) => {
acc[track.id] = track
return acc
}, {} as Record<number, Track>)
export interface QueueTrackSource {
uuid: string
mimetype: string
bitrate?: number
url: string
duration?: number
}
return shuffledIds.value.map(id => tracksById[id])
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
export const tracks = useStorage('queue:tracks', [] as number[])
const tracksById = computedAsync(async () => {
const trackObjects = await getMany(uniq(tracks.value))
return trackObjects.reduce((acc, track) => {
acc[track.id] = track
return acc
}, {}) as Record<number, QueueTrack>
}, {})
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[]) => {
tracks.push(...newTracks)
const createQueueTrack = async (track: Track): Promise<QueueTrack> => {
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
if (isShuffled.value) {
shuffledIds.value.push(...shuffleIds(newTracks))
shuffledIds.value.push(...shuffleArray(ids))
}
}
// Current Index
export const currentIndex = useClamp(useStorage('queue:index', 0), 0, () => tracks.length)
export const currentTrack = computed(() => tracks[currentIndex.value])
export const currentIndex = useClamp(useStorage('queue:index', 0), 0, () => tracks.value.length)
export const currentTrack = computed(() => queue.value[currentIndex.value])
// Play track
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 (force && currentIndex.value === trackIndex) {
@ -54,25 +118,29 @@ export const playTrack = async (trackIndex: number, force = false) => {
}
// 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 () => {
const { looping, LoopingMode } = await import('~/composables/audio/player')
// 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)
if (tracks.value.length === 1) return playTrack(currentIndex.value, true)
return playTrack(tracks.value.length - 1)
}
return playTrack(currentIndex.value - 1)
}
// 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 () => {
const { looping, LoopingMode } = await import('~/composables/audio/player')
// 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
if (tracks.length === 1) return playTrack(currentIndex.value, true)
if (tracks.value.length === 1) return playTrack(currentIndex.value, true)
return playTrack(0)
}
@ -80,8 +148,7 @@ export const playNext = async () => {
}
// Shuffle
const shuffleIds = (tracks: Track[]) => shuffleArray(tracks.map(track => track.id))
const shuffledIds = useStorage('queue:shuffled-ids', [] as number[])
const shuffledIds = useStorage('queue:tracks:shuffled', [] as number[])
export const isShuffled = computed(() => shuffledIds.value.length !== 0)
export const shuffle = () => {
if (isShuffled.value) {
@ -89,5 +156,5 @@ export const shuffle = () => {
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 { Track, Upload } from '~/types'
import type { QueueTrack, QueueTrackSource } from '~/composables/audio/queue'
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 { 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 axios from 'axios'
const ALLOWED_PLAY_TYPES: (CanPlayTypeResult | undefined)[] = ['maybe', 'probably']
const AUDIO_ELEMENT = document.createElement('audio')
@ -16,42 +15,31 @@ const AUDIO_ELEMENT = document.createElement('audio')
const soundPromises = new Map<number, Promise<Sound>>()
const soundCache = shallowReactive(new Map<number, Sound>())
const getUploadSources = (uploads: Upload[]): SoundSource[] => {
const sources = uploads
const getTrackSources = (track: QueueTrack): QueueTrackSource[] => {
const sources: QueueTrackSource[] = track.sources
// 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}`)))
.map((upload): SoundSource => ({
...upload,
url: store.getters['instance/absoluteUrl'](upload.listen_url) as string
.map((source) => ({
...source,
url: store.getters['instance/absoluteUrl'](source.url) as string
}))
// NOTE: Add a transcoded MP3 src at the end for browsers
// that do not support other codecs to be able to play it :)
if (sources.length > 0 && !sources.some(({ mimetype }) => mimetype === 'audio/mpeg')) {
const url = new URL(sources[0].url)
if (sources.length > 0) {
const original = sources[0]
const url = new URL(original.url)
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
}
const getTrackSources = async (track: Track): Promise<SoundSource[]> => {
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> => {
export const createSound = async (track: QueueTrack): Promise<Sound> => {
if (soundCache.has(track.id)) {
return soundCache.get(track.id) as Sound
}
@ -61,7 +49,7 @@ export const createSound = async (track: Track): Promise<Sound> => {
}
const createSoundPromise = async () => {
const sources = await getTrackSources(track)
const sources = getTrackSources(track)
const SoundImplementation = soundImplementation.value
const sound = new SoundImplementation(sources)
@ -85,10 +73,10 @@ export const createSound = async (track: Track): Promise<Sound> => {
// Create track from queue
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')
const track = tracks[index]
const track = queue.value[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')
@ -105,12 +93,12 @@ export const createTrack = async (index: number) => {
}
// NOTE: Preload next track
if (index === currentIndex.value && index + 1 < tracks.length) {
if (index === currentIndex.value && index + 1 < queue.value.length) {
setTimeout(async () => {
const sound = await createSound(tracks[index + 1])
const sound = await createSound(queue.value[index + 1])
await sound.preload()
}, 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 { 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 { computed, markRaw, ref } from 'vue'
import { useGettext } from 'vue3-gettext'
import { useStore } from '~/store'
import axios from 'axios'
import jQuery from 'jquery'
import { enqueue as addToQueue, currentTrack } from '~/composables/audio/queue'
import { isPlaying } from '~/composables/audio/player'
export interface PlayOptionsProps {
isPlayable?: boolean
tracks?: Track[]
@ -24,8 +26,6 @@ export interface PlayOptionsProps {
export default (props: PlayOptionsProps) => {
const store = useStore()
const { resume, pause, playing } = usePlayer()
const { currentTrack } = useQueue()
const playable = computed(() => {
if (props.isPlayable) {
@ -134,7 +134,7 @@ export default (props: PlayOptionsProps) => {
jQuery(el.value).find('.ui.dropdown').dropdown('hide')
const tracks = await getPlayableTracks()
await store.dispatch('queue/appendMany', { tracks })
await addToQueue(...tracks)
addMessage(tracks)
}
@ -148,40 +148,33 @@ export default (props: PlayOptionsProps) => {
if (next && !wasEmpty) {
await store.dispatch('queue/next')
resume()
isPlaying.value = true
}
addMessage(tracks)
}
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')
const tracks = await getPlayableTracks()
await store.dispatch('queue/appendMany', { tracks })
const tracksToPlay = await getPlayableTracks()
await addToQueue(...tracksToPlay)
if (props.track && props.tracks?.length) {
// set queue position to selected track
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)
}
const trackIndex = props.tracks?.findIndex(track => track.id === props.track?.id && track.position === props.track?.position) ?? 0
await playTrack(trackIndex)
resume()
addMessage(tracks)
isPlaying.value = true
playTrack(0, true)
addMessage(tracksToPlay)
}
const activateTrack = (track: Track, index: number) => {
// TODO (wvffle): Check if position checking did not break anything
if (track.id === currentTrack.value?.id && track.position === currentTrack.value?.position) {
if (playing.value) {
return pause()
}
return resume()
isPlaying.value = true
}
replacePlay()

View File

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

View File

@ -3621,6 +3621,13 @@ iconv-lite@0.6.3:
dependencies:
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:
version "7.1.0"
resolved "https://registry.yarnpkg.com/idb/-/idb-7.1.0.tgz#2cc886be57738419e57f9aab58f647e5e2160270"
@ -4780,6 +4787,11 @@ run-parallel@^1.1.9:
dependencies:
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:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"