Add radio support

This commit is contained in:
wvffle 2022-10-28 07:34:24 +00:00 committed by Georg Krause
parent c7f53df4af
commit c828e106b0
No known key found for this signature in database
GPG Key ID: 2970D504B2183D22
15 changed files with 623 additions and 487 deletions

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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()

View File

@ -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(() => ({

View File

@ -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()

View File

@ -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

View File

@ -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
}
})

View File

@ -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
}
})

View File

@ -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
}
})

View File

@ -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')

View File

@ -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)
}
}
})

View File

@ -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
}
}
}

View File

@ -235,6 +235,16 @@
}
}
.radio-populating {
margin-top: 1em;
color: #333;
text-align: center;
}
.radio-message {
margin-top: 1em !important;
margin-right: 1em;
}
}

View File

@ -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)
})
}
}
}
}