Add radio support
This commit is contained in:
parent
c7f53df4af
commit
c828e106b0
|
@ -21,5 +21,6 @@ DEBUG=true
|
|||
# Django Environment Variables
|
||||
DATABASE_URL=postgresql://postgres@localhost:5432/postgres
|
||||
DJANGO_SECRET_KEY=gitpod
|
||||
THROTTLING_ENABLED=False
|
||||
|
||||
# Gitpod Environment Variables
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import type { IAudioContext, IAudioNode } from 'standardized-audio-context'
|
||||
|
||||
import { createEventHook, refDefault, type EventHookOn, useEventListener } from '@vueuse/core'
|
||||
import { createAudioSource } from '~/composables/audio/audio-api'
|
||||
import { reactive, ref, type Ref } from 'vue'
|
||||
import { createEventHook, refDefault, type EventHookOn } from '@vueuse/shared'
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
|
||||
export interface SoundSource {
|
||||
uuid: string
|
||||
|
|
|
@ -8,6 +8,9 @@ import { useGettext } from 'vue3-gettext'
|
|||
import { useRouter } from 'vue-router'
|
||||
import { useStore } from '~/store'
|
||||
|
||||
import { usePlayer } from '~/composables/audio/player'
|
||||
import { useQueue } from '~/composables/audio/queue'
|
||||
|
||||
import time from '~/utils/time'
|
||||
|
||||
// import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue'
|
||||
|
@ -16,18 +19,18 @@ import PlayerControls from '~/components/audio/PlayerControls.vue'
|
|||
import VirtualList from '~/components/vui/list/VirtualList.vue'
|
||||
import QueueItem from '~/components/QueueItem.vue'
|
||||
|
||||
import {
|
||||
const {
|
||||
isPlaying,
|
||||
currentTime,
|
||||
duration,
|
||||
progress,
|
||||
bufferProgress,
|
||||
seekTo,
|
||||
loading as isLoadingAudio,
|
||||
loading: isLoadingAudio,
|
||||
errored
|
||||
} from '~/composables/audio/player'
|
||||
} = usePlayer()
|
||||
|
||||
import {
|
||||
const {
|
||||
hasNext,
|
||||
currentTrack,
|
||||
currentIndex,
|
||||
|
@ -36,8 +39,8 @@ import {
|
|||
dequeue,
|
||||
playTrack,
|
||||
reorder,
|
||||
endsIn as timeLeft
|
||||
} from '~/composables/audio/queue'
|
||||
endsIn: timeLeft
|
||||
} = useQueue()
|
||||
|
||||
const queueModal = ref()
|
||||
const { activate, deactivate } = useFocusTrap(queueModal, { allowOutsideClick: true, preventScroll: true })
|
||||
|
@ -48,6 +51,7 @@ const store = useStore()
|
|||
|
||||
const labels = computed(() => ({
|
||||
queue: $pgettext('*/*/*', 'Queue'),
|
||||
populating: $pgettext('*/*/*', 'Fetching radio track'),
|
||||
duration: $pgettext('*/*/*', 'Duration'),
|
||||
addArtistContentFilter: $pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Hide content from this artist…'),
|
||||
restart: $pgettext('*/*/*', 'Restart track'),
|
||||
|
@ -332,10 +336,17 @@ const reorderTracks = async (from: number, to: number) => {
|
|||
@remove="dequeue"
|
||||
/>
|
||||
</template>
|
||||
</virtual-list>
|
||||
<template #footer>
|
||||
<div
|
||||
v-if="$store.state.radios.populating"
|
||||
class="radio-populating"
|
||||
>
|
||||
<i class="loading spinner icon" />
|
||||
{{ labels.populating }}
|
||||
</div>
|
||||
<div
|
||||
v-if="$store.state.radios.running"
|
||||
class="ui info message"
|
||||
class="ui info message radio-message"
|
||||
>
|
||||
<div class="content">
|
||||
<h3 class="header">
|
||||
|
@ -358,6 +369,8 @@ const reorderTracks = async (from: number, to: number) => {
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</virtual-list>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
@ -1,29 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import {
|
||||
LoopingMode,
|
||||
initializeFirstTrack,
|
||||
isPlaying,
|
||||
mute,
|
||||
volume,
|
||||
toggleLooping,
|
||||
looping,
|
||||
seekBy,
|
||||
seekTo,
|
||||
currentTime,
|
||||
duration,
|
||||
progress,
|
||||
bufferProgress,
|
||||
loading as isLoadingAudio
|
||||
} from '~/composables/audio/player'
|
||||
|
||||
import {
|
||||
playPrevious,
|
||||
playNext,
|
||||
queue,
|
||||
currentIndex,
|
||||
currentTrack,
|
||||
shuffle
|
||||
} from '~/composables/audio/queue'
|
||||
import { usePlayer } from '~/composables/audio/player'
|
||||
import { useQueue } from '~/composables/audio/queue'
|
||||
|
||||
import { useMouse, useWindowSize } from '@vueuse/core'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
|
@ -38,6 +15,32 @@ import time from '~/utils/time'
|
|||
import VolumeControl from './VolumeControl.vue'
|
||||
import PlayerControls from './PlayerControls.vue'
|
||||
|
||||
const {
|
||||
LoopingMode,
|
||||
initializeFirstTrack,
|
||||
isPlaying,
|
||||
mute,
|
||||
volume,
|
||||
toggleLooping,
|
||||
looping,
|
||||
seekBy,
|
||||
seekTo,
|
||||
currentTime,
|
||||
duration,
|
||||
progress,
|
||||
bufferProgress,
|
||||
loading: isLoadingAudio
|
||||
} = usePlayer()
|
||||
|
||||
const {
|
||||
playPrevious,
|
||||
playNext,
|
||||
queue,
|
||||
currentIndex,
|
||||
currentTrack,
|
||||
shuffle
|
||||
} = useQueue()
|
||||
|
||||
const store = useStore()
|
||||
const { $pgettext } = useGettext()
|
||||
|
||||
|
|
|
@ -2,14 +2,11 @@
|
|||
import { useGettext } from 'vue3-gettext'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { isPlaying } from '~/composables/audio/player'
|
||||
import {
|
||||
hasPrevious,
|
||||
playPrevious,
|
||||
hasNext,
|
||||
playNext,
|
||||
currentTrack
|
||||
} from '~/composables/audio/queue'
|
||||
import { usePlayer } from '~/composables/audio/player'
|
||||
import { useQueue } from '~/composables/audio/queue'
|
||||
|
||||
const { hasPrevious, playPrevious, hasNext, playNext, currentTrack } = useQueue()
|
||||
const { isPlaying } = usePlayer()
|
||||
|
||||
const { $pgettext } = useGettext()
|
||||
const labels = computed(() => ({
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
<script setup lang="ts">
|
||||
import { volume, mute } from '~/composables/audio/player'
|
||||
import { usePlayer } from '~/composables/audio/player'
|
||||
import { useTimeoutFn } from '@vueuse/core'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const { volume, mute } = usePlayer()
|
||||
const expanded = ref(false)
|
||||
|
||||
const { $pgettext } = useGettext()
|
||||
|
|
|
@ -3,7 +3,6 @@ import { useMouse, useCurrentElement, useRafFn, useElementByPoint } from '@vueus
|
|||
import { ref, watchEffect, reactive } from 'vue'
|
||||
|
||||
// @ts-expect-error no typings
|
||||
// import VirtualList from 'vue3-virtual-scroll-list'
|
||||
import { RecycleScroller } from 'vue-virtual-scroller'
|
||||
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
|
||||
|
||||
|
@ -164,7 +163,6 @@ defineExpose({
|
|||
<div>
|
||||
<recycle-scroller
|
||||
ref="virtualList"
|
||||
v-slot="{ item, index }"
|
||||
class="virtual-list drag-container"
|
||||
:items="list"
|
||||
:item-size="size"
|
||||
|
@ -175,11 +173,21 @@ defineExpose({
|
|||
@visible="emit('visible')"
|
||||
@hidden="emit('hidden')"
|
||||
>
|
||||
<template #before>
|
||||
<slot name="header" />
|
||||
</template>
|
||||
|
||||
<template #default="{ item, index }">
|
||||
<slot
|
||||
:class-list="[draggedItem && hoveredIndex === index && `drop-${position}`, 'drag-item']"
|
||||
:item="item"
|
||||
:index="index"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #after>
|
||||
<slot name="footer" />
|
||||
</template>
|
||||
</recycle-scroller>
|
||||
|
||||
<div
|
||||
|
|
|
@ -1,51 +1,13 @@
|
|||
import { tryOnMounted, useIntervalFn, useRafFn, useStorage, useTimeoutFn, whenever } from '@vueuse/core'
|
||||
import { currentTrack, currentIndex, playNext } from '~/composables/audio/queue'
|
||||
import { currentSound, createTrack } from '~/composables/audio/tracks'
|
||||
import { createGlobalState, tryOnMounted, useIntervalFn, useRafFn, useStorage, useTimeoutFn, whenever } from '@vueuse/core'
|
||||
import { useTracks } from '~/composables/audio/tracks'
|
||||
import { computed, ref, watch, watchEffect, type Ref } from 'vue'
|
||||
import { setGain } from './audio-api'
|
||||
|
||||
import store from '~/store'
|
||||
import { useQueue, currentIndex, currentTrack } from './queue'
|
||||
import { useStore } from '~/store'
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
export const isPlaying = ref(false)
|
||||
watchEffect(() => {
|
||||
const sound = currentSound.value
|
||||
if (!sound) return
|
||||
|
||||
if (isPlaying.value) {
|
||||
sound.play()
|
||||
return
|
||||
}
|
||||
|
||||
sound.pause()
|
||||
})
|
||||
|
||||
// Create first track when we initalize the page
|
||||
export const initializeFirstTrack = () => tryOnMounted(() => {
|
||||
createTrack(currentIndex.value)
|
||||
})
|
||||
|
||||
// Volume
|
||||
const lastVolume = useStorage('player:last-volume', 0.7)
|
||||
|
||||
export const volume: Ref<number> = useStorage('player:volume', 0.7)
|
||||
watch(volume, (to, from) => setGain(to))
|
||||
|
||||
export const mute = () => {
|
||||
if (volume.value > 0) {
|
||||
lastVolume.value = volume.value
|
||||
volume.value = 0
|
||||
return
|
||||
}
|
||||
|
||||
if (lastVolume.value === 0) {
|
||||
volume.value = 0.7
|
||||
return
|
||||
}
|
||||
|
||||
volume.value = lastVolume.value
|
||||
}
|
||||
|
||||
// Looping
|
||||
export enum LoopingMode {
|
||||
None,
|
||||
|
@ -61,21 +23,72 @@ export const toggleLooping = () => {
|
|||
looping.value %= MODE_MAX
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
// Is playing
|
||||
export const isPlaying = ref(false)
|
||||
|
||||
// Use Player
|
||||
export const usePlayer = createGlobalState(() => {
|
||||
const { currentSound, createTrack } = useTracks()
|
||||
const { playNext } = useQueue()
|
||||
const store = useStore()
|
||||
|
||||
watchEffect(() => {
|
||||
const sound = currentSound.value
|
||||
if (!sound) return
|
||||
|
||||
if (isPlaying.value) {
|
||||
sound.play()
|
||||
return
|
||||
}
|
||||
|
||||
sound.pause()
|
||||
})
|
||||
|
||||
// Create first track when we initalize the page
|
||||
// NOTE: We want to have it called only once, hence we're using createGlobalState
|
||||
const initializeFirstTrack = createGlobalState(() => tryOnMounted(() => {
|
||||
const { initialize } = useTracks()
|
||||
initialize()
|
||||
|
||||
createTrack(currentIndex.value)
|
||||
}))
|
||||
|
||||
// Volume
|
||||
const lastVolume = useStorage('player:last-volume', 0.7)
|
||||
|
||||
const volume: Ref<number> = useStorage('player:volume', 0.7)
|
||||
watch(volume, (to, from) => setGain(to))
|
||||
|
||||
const mute = () => {
|
||||
if (volume.value > 0) {
|
||||
lastVolume.value = volume.value
|
||||
volume.value = 0
|
||||
return
|
||||
}
|
||||
|
||||
if (lastVolume.value === 0) {
|
||||
volume.value = 0.7
|
||||
return
|
||||
}
|
||||
|
||||
volume.value = lastVolume.value
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
const sound = currentSound.value
|
||||
if (!sound) return
|
||||
sound.looping = looping.value === LoopingMode.LoopTrack
|
||||
})
|
||||
})
|
||||
|
||||
watch(currentSound, sound => {
|
||||
watch(currentSound, sound => {
|
||||
sound?.onSoundLoop(() => {
|
||||
currentTime.value = 0
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Duration
|
||||
export const duration = ref(0)
|
||||
watchEffect(() => {
|
||||
// Duration
|
||||
const duration = ref(0)
|
||||
watchEffect(() => {
|
||||
const sound = currentSound.value
|
||||
if (sound?.isLoaded.value === true) {
|
||||
duration.value = sound.duration ?? 0
|
||||
|
@ -84,11 +97,11 @@ watchEffect(() => {
|
|||
}
|
||||
|
||||
duration.value = 0
|
||||
})
|
||||
})
|
||||
|
||||
// Current time
|
||||
export const currentTime = ref(0)
|
||||
useIntervalFn(() => {
|
||||
// Current time
|
||||
const currentTime = ref(0)
|
||||
useIntervalFn(() => {
|
||||
const sound = currentSound.value
|
||||
if (!sound) {
|
||||
currentTime.value = 0
|
||||
|
@ -96,20 +109,20 @@ useIntervalFn(() => {
|
|||
}
|
||||
|
||||
currentTime.value = sound.currentTime
|
||||
}, 1000)
|
||||
}, 1000)
|
||||
|
||||
// Submit listens
|
||||
const listenSubmitted = ref(false)
|
||||
whenever(listenSubmitted, async () => {
|
||||
// Submit listens
|
||||
const listenSubmitted = ref(false)
|
||||
whenever(listenSubmitted, async () => {
|
||||
console.log('Listening submitted!')
|
||||
if (!store.state.auth.authenticated) return
|
||||
if (!currentTrack.value) return
|
||||
|
||||
await axios.post('history/listenings/', { track: currentTrack.value.id })
|
||||
.catch((error) => console.error('Could not record track in history', error))
|
||||
})
|
||||
})
|
||||
|
||||
watch(currentTime, (time) => {
|
||||
watch(currentTime, (time) => {
|
||||
const sound = currentSound.value
|
||||
if (!sound) {
|
||||
listenSubmitted.value = false
|
||||
|
@ -118,28 +131,28 @@ watch(currentTime, (time) => {
|
|||
|
||||
// https://listenbrainz.readthedocs.io/en/latest/users/api/core.html?highlight=half#post--1-submit-listens
|
||||
listenSubmitted.value = time > Math.min(sound.duration / 2, 4 * 60)
|
||||
})
|
||||
})
|
||||
|
||||
// Seeking
|
||||
export const seekBy = async (seconds: number) => {
|
||||
// Seeking
|
||||
const seekBy = async (seconds: number) => {
|
||||
const sound = currentSound.value
|
||||
if (!sound) return
|
||||
|
||||
await sound.seekBy(seconds)
|
||||
currentTime.value = sound.currentTime
|
||||
}
|
||||
}
|
||||
|
||||
export const seekTo = async (seconds: number) => {
|
||||
const seekTo = async (seconds: number) => {
|
||||
const sound = currentSound.value
|
||||
if (!sound) return
|
||||
|
||||
await sound.seekTo(seconds)
|
||||
currentTime.value = sound.currentTime
|
||||
}
|
||||
}
|
||||
|
||||
// Buffer progress
|
||||
export const bufferProgress = ref(0)
|
||||
useIntervalFn(() => {
|
||||
// Buffer progress
|
||||
const bufferProgress = ref(0)
|
||||
useIntervalFn(() => {
|
||||
const sound = currentSound.value
|
||||
if (!sound) {
|
||||
bufferProgress.value = 0
|
||||
|
@ -147,11 +160,11 @@ useIntervalFn(() => {
|
|||
}
|
||||
|
||||
bufferProgress.value = sound.buffered / sound.duration * 100
|
||||
}, 1000)
|
||||
}, 1000)
|
||||
|
||||
// Progress
|
||||
export const progress = ref(0)
|
||||
useRafFn(() => {
|
||||
// Progress
|
||||
const progress = ref(0)
|
||||
useRafFn(() => {
|
||||
const sound = currentSound.value
|
||||
if (!sound) {
|
||||
progress.value = 0
|
||||
|
@ -159,22 +172,41 @@ useRafFn(() => {
|
|||
}
|
||||
|
||||
progress.value = sound.currentTime / sound.duration * 100
|
||||
})
|
||||
})
|
||||
|
||||
// Loading
|
||||
export const loading = computed(() => {
|
||||
// Loading
|
||||
const loading = computed(() => {
|
||||
const sound = currentSound.value
|
||||
if (!sound) return false
|
||||
return !sound.isLoaded.value
|
||||
})
|
||||
})
|
||||
|
||||
// Errored
|
||||
export const errored = computed(() => {
|
||||
// Errored
|
||||
const errored = computed(() => {
|
||||
const sound = currentSound.value
|
||||
if (!sound) return false
|
||||
return sound.isErrored.value
|
||||
})
|
||||
})
|
||||
|
||||
const { start, stop } = useTimeoutFn(() => playNext(), 3000, { immediate: false })
|
||||
watch(currentIndex, stop)
|
||||
whenever(errored, start)
|
||||
const { start, stop } = useTimeoutFn(() => playNext(), 3000, { immediate: false })
|
||||
watch(currentIndex, stop)
|
||||
whenever(errored, start)
|
||||
|
||||
return {
|
||||
initializeFirstTrack,
|
||||
isPlaying,
|
||||
volume,
|
||||
mute,
|
||||
looping,
|
||||
LoopingMode,
|
||||
toggleLooping,
|
||||
duration,
|
||||
currentTime,
|
||||
seekBy,
|
||||
seekTo,
|
||||
bufferProgress,
|
||||
progress,
|
||||
loading,
|
||||
errored
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
import type { Track, Upload } from '~/types'
|
||||
|
||||
import { computedAsync, useNow, useStorage, useTimeAgo } from '@vueuse/core'
|
||||
import { computedAsync, createGlobalState, useNow, useStorage, useTimeAgo } from '@vueuse/core'
|
||||
import { shuffle as shuffleArray, sum, uniq } from 'lodash-es'
|
||||
import { getMany, setMany } from 'idb-keyval'
|
||||
import { computed, watchEffect } from 'vue'
|
||||
import { useClamp } from '@vueuse/math'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { looping, LoopingMode, isPlaying } from '~/composables/audio/player'
|
||||
import { useStore } from '~/store'
|
||||
|
||||
import axios from 'axios'
|
||||
import { useTracks } from './tracks'
|
||||
|
||||
// import useWebWorker from '~/composables/useWebWorker'
|
||||
|
||||
|
@ -35,7 +39,11 @@ export interface QueueTrack {
|
|||
}
|
||||
|
||||
// Queue
|
||||
export const tracks = useStorage('queue:tracks', [] as number[])
|
||||
const tracks = useStorage('queue:tracks', [] as number[])
|
||||
const shuffledIds = useStorage('queue:tracks:shuffled', [] as number[])
|
||||
|
||||
const isShuffled = computed(() => shuffledIds.value.length !== 0)
|
||||
|
||||
const tracksById = computedAsync(async () => {
|
||||
const trackObjects = await getMany(uniq(tracks.value))
|
||||
return trackObjects.reduce((acc, track) => {
|
||||
|
@ -44,7 +52,7 @@ const tracksById = computedAsync(async () => {
|
|||
}, {}) as Record<number, QueueTrack>
|
||||
}, {})
|
||||
|
||||
export const queue = computed(() => {
|
||||
const queue = computed(() => {
|
||||
const indexedTracks = tracksById.value
|
||||
|
||||
if (isShuffled.value) {
|
||||
|
@ -54,7 +62,16 @@ export const queue = computed(() => {
|
|||
return tracks.value.map(id => indexedTracks[id]).filter(i => i)
|
||||
})
|
||||
|
||||
const createQueueTrack = async (track: Track): Promise<QueueTrack> => {
|
||||
// Current Index
|
||||
export const currentIndex = useClamp(useStorage('queue:index', 0), 0, () => tracks.value.length)
|
||||
export const currentTrack = computed(() => queue.value[currentIndex.value])
|
||||
|
||||
// Use Queue
|
||||
export const useQueue = createGlobalState(() => {
|
||||
const { currentSound } = useTracks()
|
||||
const store = useStore()
|
||||
|
||||
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}/`)
|
||||
|
@ -82,10 +99,10 @@ const createQueueTrack = async (track: Track): Promise<QueueTrack> => {
|
|||
url: upload.listen_url
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Adding tracks
|
||||
export const enqueueAt = async (index: number, ...newTracks: Track[]) => {
|
||||
// Adding tracks
|
||||
const enqueueAt = async (index: number, ...newTracks: Track[]) => {
|
||||
const queueTracks = await Promise.all(newTracks.map(createQueueTrack))
|
||||
await setMany(queueTracks.map(track => [track.id, track]))
|
||||
|
||||
|
@ -103,14 +120,14 @@ export const enqueueAt = async (index: number, ...newTracks: Track[]) => {
|
|||
if (isShuffled.value) {
|
||||
shuffledIds.value.push(...shuffleArray(ids))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const enqueue = async (...newTracks: Track[]) => {
|
||||
const enqueue = async (...newTracks: Track[]) => {
|
||||
return enqueueAt(tracks.value.length, ...newTracks)
|
||||
}
|
||||
}
|
||||
|
||||
// Removing tracks
|
||||
export const dequeue = async (index: number) => {
|
||||
// Removing tracks
|
||||
const dequeue = async (index: number) => {
|
||||
if (currentIndex.value === index) {
|
||||
await playNext(true)
|
||||
}
|
||||
|
@ -122,17 +139,10 @@ export const dequeue = async (index: number) => {
|
|||
}
|
||||
|
||||
// TODO (wvffle): Check if removing last element works well
|
||||
}
|
||||
|
||||
// Current Index
|
||||
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')
|
||||
}
|
||||
|
||||
// Play track
|
||||
const playTrack = async (trackIndex: number, force = false) => {
|
||||
if (isPlaying.value) currentSound.value?.pause()
|
||||
|
||||
if (force && currentIndex.value === trackIndex) {
|
||||
|
@ -142,13 +152,11 @@ export const playTrack = async (trackIndex: number, force = false) => {
|
|||
}
|
||||
|
||||
currentIndex.value = trackIndex
|
||||
}
|
||||
|
||||
// Previous track
|
||||
export const hasPrevious = computed(() => /* looping.value === LoopingMode.LoopQueue || */ currentIndex.value !== 0)
|
||||
export const playPrevious = async (force = false) => {
|
||||
const { looping, LoopingMode } = await import('~/composables/audio/player')
|
||||
}
|
||||
|
||||
// Previous track
|
||||
const hasPrevious = computed(() => looping.value === LoopingMode.LoopQueue || currentIndex.value !== 0)
|
||||
const playPrevious = async (force = false) => {
|
||||
// Loop entire queue / change track to the next one
|
||||
if (looping.value === LoopingMode.LoopQueue && currentIndex.value === 0 && force !== true) {
|
||||
// Loop track programmatically if it is the only track in the queue
|
||||
|
@ -157,13 +165,11 @@ export const playPrevious = async (force = false) => {
|
|||
}
|
||||
|
||||
return playTrack(currentIndex.value - 1)
|
||||
}
|
||||
|
||||
// Next track
|
||||
export const hasNext = computed(() => /* looping.value === LoopingMode.LoopQueue || */ currentIndex.value !== tracks.value.length - 1)
|
||||
export const playNext = async (force = false) => {
|
||||
const { looping, LoopingMode } = await import('~/composables/audio/player')
|
||||
}
|
||||
|
||||
// Next track
|
||||
const hasNext = computed(() => looping.value === LoopingMode.LoopQueue || currentIndex.value !== tracks.value.length - 1)
|
||||
const playNext = async (force = false) => {
|
||||
// Loop entire queue / change track to the next one
|
||||
if (looping.value === LoopingMode.LoopQueue && currentIndex.value === tracks.value.length - 1 && force !== true) {
|
||||
// Loop track programmatically if it is the only track in the queue
|
||||
|
@ -172,10 +178,10 @@ export const playNext = async (force = false) => {
|
|||
}
|
||||
|
||||
return playTrack(currentIndex.value + 1)
|
||||
}
|
||||
}
|
||||
|
||||
// Reorder
|
||||
export const reorder = (from: number, to: number) => {
|
||||
// Reorder
|
||||
const reorder = (from: number, to: number) => {
|
||||
const [id] = tracks.value.splice(from, 1)
|
||||
tracks.value.splice(to, 0, id)
|
||||
|
||||
|
@ -194,32 +200,30 @@ export const reorder = (from: number, to: number) => {
|
|||
// item after was moved before
|
||||
currentIndex.value += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Shuffle
|
||||
const shuffledIds = useStorage('queue:tracks:shuffled', [] as number[])
|
||||
export const isShuffled = computed(() => shuffledIds.value.length !== 0)
|
||||
export const shuffle = () => {
|
||||
// Shuffle
|
||||
const shuffle = () => {
|
||||
if (isShuffled.value) {
|
||||
shuffledIds.value.length = 0
|
||||
return
|
||||
}
|
||||
|
||||
shuffledIds.value = shuffleArray(tracks.value)
|
||||
}
|
||||
}
|
||||
|
||||
export const reshuffleUpcomingTracks = () => {
|
||||
const reshuffleUpcomingTracks = () => {
|
||||
// TODO: Test if needed to add 1 to currentIndex
|
||||
const listenedTracks = shuffledIds.value.slice(0, currentIndex.value)
|
||||
const upcomingTracks = shuffledIds.value.slice(currentIndex.value)
|
||||
|
||||
listenedTracks.push(...shuffleArray(upcomingTracks))
|
||||
shuffledIds.value = listenedTracks
|
||||
}
|
||||
}
|
||||
|
||||
// Ends in
|
||||
const now = useNow()
|
||||
export const endsIn = useTimeAgo(computed(() => {
|
||||
// Ends in
|
||||
const now = useNow()
|
||||
const endsIn = useTimeAgo(computed(() => {
|
||||
const seconds = sum(
|
||||
queue.value
|
||||
.slice(currentIndex.value)
|
||||
|
@ -229,4 +233,33 @@ export const endsIn = useTimeAgo(computed(() => {
|
|||
const date = new Date(now.value)
|
||||
date.setSeconds(date.getSeconds() + seconds)
|
||||
return date
|
||||
}))
|
||||
}))
|
||||
|
||||
// Radio queue populating
|
||||
watchEffect(() => {
|
||||
if (store.state.radios.running && currentIndex.value === tracks.value.length - 1) {
|
||||
console.log('POPULATING QUEUE FROM RADIO')
|
||||
return store.dispatch('radios/populateQueue')
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
tracks,
|
||||
queue,
|
||||
enqueueAt,
|
||||
enqueue,
|
||||
dequeue,
|
||||
currentIndex,
|
||||
currentTrack,
|
||||
playTrack,
|
||||
hasPrevious,
|
||||
hasNext,
|
||||
playPrevious,
|
||||
playNext,
|
||||
isShuffled,
|
||||
shuffle,
|
||||
reshuffleUpcomingTracks,
|
||||
reorder,
|
||||
endsIn
|
||||
}
|
||||
})
|
||||
|
|
|
@ -2,11 +2,12 @@ import type { QueueTrack, QueueTrackSource } from '~/composables/audio/queue'
|
|||
import type { Sound } from '~/api/player'
|
||||
|
||||
import { soundImplementation } from '~/api/player'
|
||||
import { computed, watchEffect } from 'vue'
|
||||
import { createGlobalState, syncRef } from '@vueuse/core'
|
||||
import { computed, ref, watchEffect } from 'vue'
|
||||
|
||||
import { playNext, queue, currentTrack, currentIndex } from '~/composables/audio/queue'
|
||||
import { useQueue } from '~/composables/audio/queue'
|
||||
import { connectAudioSource } from '~/composables/audio/audio-api'
|
||||
import { isPlaying } from '~/composables/audio/player'
|
||||
import { usePlayer } from '~/composables/audio/player'
|
||||
|
||||
import useLRUCache from '~/composables/useLRUCache'
|
||||
import store from '~/store'
|
||||
|
@ -41,7 +42,9 @@ const getTrackSources = (track: QueueTrack): QueueTrackSource[] => {
|
|||
return sources
|
||||
}
|
||||
|
||||
export const createSound = async (track: QueueTrack): Promise<Sound> => {
|
||||
// Use Tracks
|
||||
export const useTracks = createGlobalState(() => {
|
||||
const createSound = async (track: QueueTrack): Promise<Sound> => {
|
||||
if (soundCache.has(track.id)) {
|
||||
return soundCache.get(track.id) as Sound
|
||||
}
|
||||
|
@ -52,6 +55,7 @@ export const createSound = async (track: QueueTrack): Promise<Sound> => {
|
|||
|
||||
const createSoundPromise = async () => {
|
||||
const sources = getTrackSources(track)
|
||||
const { playNext, currentIndex } = useQueue()
|
||||
|
||||
const SoundImplementation = soundImplementation.value
|
||||
const sound = new SoundImplementation(sources)
|
||||
|
@ -70,10 +74,11 @@ export const createSound = async (track: QueueTrack): Promise<Sound> => {
|
|||
const soundPromise = createSoundPromise()
|
||||
soundPromises.set(track.id, soundPromise)
|
||||
return soundPromise
|
||||
}
|
||||
}
|
||||
|
||||
// Create track from queue
|
||||
export const createTrack = async (index: number) => {
|
||||
// Create track from queue
|
||||
const createTrack = async (index: number) => {
|
||||
const { queue, currentIndex } = useQueue()
|
||||
if (queue.value.length <= index || index === -1) return
|
||||
console.log('LOADING TRACK', index)
|
||||
|
||||
|
@ -89,6 +94,7 @@ export const createTrack = async (index: number) => {
|
|||
sound.audioNode.disconnect()
|
||||
connectAudioSource(sound.audioNode)
|
||||
|
||||
const { isPlaying } = usePlayer()
|
||||
if (isPlaying.value && index === currentIndex.value) {
|
||||
await sound.play()
|
||||
}
|
||||
|
@ -100,8 +106,23 @@ export const createTrack = async (index: number) => {
|
|||
await sound.preload()
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watchEffect(async () => createTrack(currentIndex.value))
|
||||
const currentTrack = ref<QueueTrack>()
|
||||
|
||||
export const currentSound = computed(() => soundCache.get(currentTrack.value?.id ?? -1))
|
||||
// NOTE: We want to have it called only once, hence we're using createGlobalState
|
||||
const initialize = createGlobalState(() => {
|
||||
const { currentTrack: track, currentIndex } = useQueue()
|
||||
watchEffect(async () => createTrack(currentIndex.value))
|
||||
syncRef(currentTrack, track)
|
||||
})
|
||||
|
||||
const currentSound = computed(() => soundCache.get(currentTrack.value?.id ?? -1))
|
||||
|
||||
return {
|
||||
initialize,
|
||||
createSound,
|
||||
createTrack,
|
||||
currentSound
|
||||
}
|
||||
})
|
||||
|
|
|
@ -6,12 +6,12 @@ import { computed, markRaw, ref } from 'vue'
|
|||
import { useGettext } from 'vue3-gettext'
|
||||
import { useStore } from '~/store'
|
||||
|
||||
import { usePlayer } from '~/composables/audio/player'
|
||||
import { useQueue } from '~/composables/audio/queue'
|
||||
|
||||
import axios from 'axios'
|
||||
import jQuery from 'jquery'
|
||||
|
||||
import { enqueue as addToQueue, currentTrack, playNext, currentIndex, enqueueAt, queue } from '~/composables/audio/queue'
|
||||
import { isPlaying } from '~/composables/audio/player'
|
||||
|
||||
export interface PlayOptionsProps {
|
||||
isPlayable?: boolean
|
||||
tracks?: Track[]
|
||||
|
@ -25,6 +25,8 @@ export interface PlayOptionsProps {
|
|||
}
|
||||
|
||||
export default (props: PlayOptionsProps) => {
|
||||
const { enqueue: addToQueue, currentTrack, playNext, currentIndex, enqueueAt, queue, tracks, playTrack } = useQueue()
|
||||
const { isPlaying } = usePlayer()
|
||||
const store = useStore()
|
||||
|
||||
const playable = computed(() => {
|
||||
|
@ -155,7 +157,6 @@ export default (props: PlayOptionsProps) => {
|
|||
}
|
||||
|
||||
const replacePlay = async () => {
|
||||
const { tracks, playTrack } = await import('~/composables/audio/queue')
|
||||
tracks.value.length = 0
|
||||
|
||||
jQuery(el.value).find('.ui.dropdown').dropdown('hide')
|
||||
|
|
|
@ -62,12 +62,12 @@ export const install: InitModule = ({ store }) => {
|
|||
})
|
||||
})
|
||||
|
||||
useWebSocketHandler('Listen', (event) => {
|
||||
useWebSocketHandler('Listen', async (event) => {
|
||||
if (store.state.radios.current && store.state.radios.running) {
|
||||
const { current } = store.state.radios
|
||||
|
||||
if (current.clientOnly) {
|
||||
CLIENT_RADIOS[current.type].handleListen(current, event, store)
|
||||
await CLIENT_RADIOS[current.type].handleListen(current, event)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,13 +1,19 @@
|
|||
import type { Dispatch, Module } from 'vuex'
|
||||
import type { RootState } from '~/store/index'
|
||||
import type { Module } from 'vuex'
|
||||
import type { Track } from '~/types'
|
||||
|
||||
import { useQueue } from '~/composables/audio/queue'
|
||||
import { usePlayer } from '~/composables/audio/player'
|
||||
import { CLIENT_RADIOS } from '~/utils/clientRadios'
|
||||
|
||||
import axios from 'axios'
|
||||
import { CLIENT_RADIOS } from '~/utils/clientRadios'
|
||||
|
||||
import useLogger from '~/composables/useLogger'
|
||||
|
||||
export interface State {
|
||||
current: null | CurrentRadio
|
||||
running: boolean
|
||||
populating: boolean
|
||||
}
|
||||
|
||||
export interface ObjectId {
|
||||
|
@ -26,19 +32,14 @@ export interface CurrentRadio {
|
|||
|
||||
export type RadioConfig = { type: 'tag', names: string[] } | { type: 'artist', ids: string[] }
|
||||
|
||||
export interface PopulateQueuePayload {
|
||||
current: CurrentRadio
|
||||
playNow: boolean
|
||||
dispatch: Dispatch
|
||||
}
|
||||
|
||||
const logger = useLogger()
|
||||
|
||||
const store: Module<State, RootState> = {
|
||||
namespaced: true,
|
||||
state: {
|
||||
current: null,
|
||||
running: false
|
||||
running: false,
|
||||
populating: false
|
||||
},
|
||||
getters: {
|
||||
types: () => {
|
||||
|
@ -70,6 +71,7 @@ const store: Module<State, RootState> = {
|
|||
reset (state) {
|
||||
state.running = false
|
||||
state.current = null
|
||||
state.populating = false
|
||||
},
|
||||
current: (state, value) => {
|
||||
state.current = value
|
||||
|
@ -107,37 +109,45 @@ const store: Module<State, RootState> = {
|
|||
if (state.current?.clientOnly) {
|
||||
CLIENT_RADIOS[state.current.type].stop()
|
||||
}
|
||||
|
||||
commit('current', null)
|
||||
commit('running', false)
|
||||
},
|
||||
async populateQueue ({ commit, rootState, state, dispatch }, playNow) {
|
||||
if (!state.running) {
|
||||
async populateQueue ({ commit, state }, playNow) {
|
||||
if (!state.running || state.populating) {
|
||||
return
|
||||
}
|
||||
|
||||
if (rootState.player.errorCount >= rootState.player.maxConsecutiveErrors - 1) {
|
||||
return
|
||||
}
|
||||
state.populating = true
|
||||
|
||||
const { enqueue, playTrack, tracks } = useQueue()
|
||||
const { isPlaying } = usePlayer()
|
||||
|
||||
const params = { session: state.current?.session }
|
||||
|
||||
if (state.current?.clientOnly) {
|
||||
return CLIENT_RADIOS[state.current.type].populateQueue({ current: state.current, dispatch, playNow })
|
||||
try {
|
||||
logger.info('Adding track to queue from radio')
|
||||
|
||||
const track = state.current?.clientOnly
|
||||
? await CLIENT_RADIOS[state.current.type].fetchNextTrack(state.current)
|
||||
: await axios.post('radios/tracks/', params).then(response => response.data.track as Track)
|
||||
|
||||
if (track === undefined) {
|
||||
isPlaying.value = false
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.post('radios/tracks/', params)
|
||||
|
||||
logger.info('Adding track to queue from radio')
|
||||
await dispatch('queue/append', { track: response.data.track }, { root: true })
|
||||
await enqueue(track)
|
||||
|
||||
if (playNow) {
|
||||
await dispatch('queue/last', null, { root: true })
|
||||
await dispatch('player/resumePlayback', null, { root: true })
|
||||
await playTrack(tracks.value.length - 1)
|
||||
isPlaying.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error while adding track to queue from radio', error)
|
||||
commit('reset')
|
||||
} finally {
|
||||
state.populating = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -235,6 +235,16 @@
|
|||
}
|
||||
}
|
||||
|
||||
.radio-populating {
|
||||
margin-top: 1em;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.radio-message {
|
||||
margin-top: 1em !important;
|
||||
margin-right: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import type { CurrentRadio, PopulateQueuePayload } from '~/store/radios'
|
||||
import type { ListenWS } from '~/composables/useWebSocketHandler'
|
||||
import type { RootState } from '~/store'
|
||||
import type { Store } from 'vuex'
|
||||
import type { CurrentRadio } from '~/store/radios'
|
||||
import type { Track } from '~/types'
|
||||
|
||||
import { useQueue } from '~/composables/audio/queue'
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
|
@ -14,39 +15,45 @@ export const CLIENT_RADIOS = {
|
|||
// method by hand
|
||||
account: {
|
||||
offset: 1,
|
||||
populateQueue ({ current, dispatch, playNow }: PopulateQueuePayload) {
|
||||
const params = { scope: `actor:${current.objectId?.fullUsername}`, ordering: '-creation_date', page_size: 1, page: this.offset }
|
||||
axios.get('history/listenings', { params }).then(async (response) => {
|
||||
async fetchNextTrack (current: CurrentRadio) {
|
||||
const params = {
|
||||
scope: `actor:${current.objectId?.fullUsername}`,
|
||||
ordering: '-creation_date',
|
||||
page_size: 1,
|
||||
page: this.offset
|
||||
}
|
||||
|
||||
// NOTE: This is unhandled as we want to pass the exception further down
|
||||
const response = await axios.get('history/listenings', { params })
|
||||
|
||||
const latest = response.data.results[0]
|
||||
if (!latest) {
|
||||
logger.error('No more tracks')
|
||||
await dispatch('stop')
|
||||
return undefined
|
||||
}
|
||||
|
||||
this.offset += 1
|
||||
const append = dispatch('queue/append', { track: latest.track }, { root: true })
|
||||
if (playNow) {
|
||||
append.then(() => dispatch('queue/last', null, { root: true }))
|
||||
}
|
||||
}, async (error) => {
|
||||
logger.error('Error while fetching listenings', error)
|
||||
await dispatch('stop')
|
||||
})
|
||||
return latest.track as Track
|
||||
},
|
||||
|
||||
stop () {
|
||||
this.offset = 1
|
||||
},
|
||||
handleListen (current: CurrentRadio, event: ListenWS, store: Store<RootState>) {
|
||||
|
||||
async handleListen (current: CurrentRadio, event: ListenWS) {
|
||||
// TODO: handle actors from other pods
|
||||
if (event.actor.local_id === current.objectId?.username) {
|
||||
axios.get(`tracks/${event.object.local_id}`).then(async (response) => {
|
||||
try {
|
||||
const response = await axios.get(`tracks/${event.object.local_id}`)
|
||||
|
||||
if (response.data.uploads.length > 0) {
|
||||
await store.dispatch('queue/append', { track: response.data })
|
||||
const { enqueue } = useQueue()
|
||||
await enqueue(response.data as Track)
|
||||
this.offset += 1
|
||||
}
|
||||
}, (error) => {
|
||||
} catch (error) {
|
||||
logger.error('Cannot retrieve track info', error)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue