WIP Rewrite queue
This commit is contained in:
parent
15f5056a59
commit
bef0d1dec4
|
@ -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",
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue