mirror of
https://github.com/koel/koel
synced 2024-12-01 00:09:17 +00:00
fix(test): fix playbackService tests
This commit is contained in:
parent
235362ba30
commit
be68021723
5 changed files with 127 additions and 359 deletions
|
@ -103,8 +103,11 @@ export default abstract class UnitTestCase {
|
|||
}
|
||||
|
||||
protected setReadOnlyProperty<T> (obj: T, prop: keyof T, value: any) {
|
||||
return Object.defineProperty(obj, prop, {
|
||||
get: () => value
|
||||
return Object.defineProperties(obj, {
|
||||
[prop]: {
|
||||
value,
|
||||
configurable: true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -1,221 +0,0 @@
|
|||
import factory from '@/__tests__/factory'
|
||||
|
||||
const currentUser = factory<User>('user', {
|
||||
id: 1,
|
||||
name: 'Phan An',
|
||||
email: 'me@phanan.net',
|
||||
is_admin: true
|
||||
})
|
||||
|
||||
const unknownArtist = factory<Artist>('artist', { id: 1, name: 'Unknown Artist' })
|
||||
const variousArtist = factory<Artist>('artist', { id: 2, name: 'Various Artist' })
|
||||
const all4One = factory<Artist>('artist', { id: 3, name: 'All-4-One' })
|
||||
const bobDylan = factory<Artist>('artist', { id: 4, name: 'Bob Dylan' })
|
||||
const jamesBlunt = factory<Artist>('artist', { id: 5, name: 'James Blunt' })
|
||||
|
||||
const all4OneAlbum = factory<Album>('album', {
|
||||
id: 1193,
|
||||
artist_id: 3,
|
||||
name: 'All-4-One',
|
||||
cover: '/img/covers/565c0f7067425.jpeg'
|
||||
})
|
||||
|
||||
const musicSpeaks = factory<Album>('album', {
|
||||
id: 1194,
|
||||
artist_id: 3,
|
||||
name: 'And The Music Speaks',
|
||||
cover: '/img/covers/unknown-album.png'
|
||||
})
|
||||
|
||||
const spaceJam = factory<Album>('album', {
|
||||
id: 1195,
|
||||
artist_id: 3,
|
||||
name: 'Space Jam',
|
||||
cover: '/img/covers/565c0f7115e0f.png'
|
||||
})
|
||||
|
||||
const highway = factory<Album>('album', {
|
||||
id: 1217,
|
||||
artist_id: 4,
|
||||
name: 'Highway 61 Revisited',
|
||||
cover: '/img/covers/565c0f76dc6e8.jpeg'
|
||||
})
|
||||
|
||||
const patGarrett = factory<Album>('album', {
|
||||
id: 1218,
|
||||
artist_id: 4,
|
||||
name: 'Pat Garrett & Billy the Kid',
|
||||
cover: '/img/covers/unknown-album.png'
|
||||
})
|
||||
|
||||
const theTimes = factory<Album>('album', {
|
||||
id: 1219,
|
||||
artist_id: 4,
|
||||
name: 'The Times They Are A-Changin',
|
||||
cover: '/img/covers/unknown-album.png'
|
||||
})
|
||||
|
||||
const backToBedlam = factory<Album>('album', {
|
||||
id: 1268,
|
||||
artist_id: 5,
|
||||
name: 'Back To Bedlam',
|
||||
cover: '/img/covers/unknown-album.png'
|
||||
})
|
||||
|
||||
export default {
|
||||
artists: [unknownArtist, variousArtist, all4One, bobDylan, jamesBlunt],
|
||||
albums: [
|
||||
all4OneAlbum,
|
||||
musicSpeaks,
|
||||
spaceJam,
|
||||
highway,
|
||||
patGarrett,
|
||||
theTimes,
|
||||
backToBedlam
|
||||
],
|
||||
|
||||
songs: [
|
||||
factory<Song>('song', {
|
||||
id: '39189f4545f9d5671fb3dc964f0080a0',
|
||||
album_id: all4OneAlbum.id,
|
||||
artist_id: all4One.id,
|
||||
title: 'I Swear',
|
||||
length: 259.92,
|
||||
play_count: 4
|
||||
}),
|
||||
factory<Song>('song', {
|
||||
id: 'a6a550f7d950d2a2520f9bf1a60f025a',
|
||||
album_id: musicSpeaks.id,
|
||||
artist_id: all4One.id,
|
||||
title: 'I can love you like that',
|
||||
length: 262.61,
|
||||
play_count: 2
|
||||
}),
|
||||
factory<Song>('song', {
|
||||
id: 'd86c30fd34f13c1aff8db59b7fc9c610',
|
||||
album_id: spaceJam.id,
|
||||
artist_id: all4One.id,
|
||||
title: 'I turn to you',
|
||||
length: 293.04
|
||||
}),
|
||||
factory<Song>('song', {
|
||||
id: 'e6d3977f3ffa147801ca5d1fdf6fa55e',
|
||||
album_id: highway.id,
|
||||
artist_id: bobDylan.id,
|
||||
title: 'Like a rolling stone',
|
||||
length: 373.63
|
||||
}),
|
||||
factory<Song>('song', {
|
||||
id: 'aa16bbef6a9710eb9a0f41ecc534fad5',
|
||||
album_id: patGarrett.id,
|
||||
artist_id: bobDylan.id,
|
||||
title: 'Knockin\' on heaven\'s door',
|
||||
length: 151.9
|
||||
}),
|
||||
factory<Song>('song', {
|
||||
id: 'cb7edeac1f097143e65b1b2cde102482',
|
||||
album_id: theTimes.id,
|
||||
artist_id: bobDylan.id,
|
||||
title: 'The times they are a-changin\'',
|
||||
length: 196
|
||||
}),
|
||||
factory<Song>('song', {
|
||||
id: '0ba9fb128427b32683b9eb9140912a70',
|
||||
album_id: backToBedlam.id,
|
||||
artist_id: jamesBlunt.id,
|
||||
title: 'No bravery',
|
||||
length: 243.12
|
||||
}),
|
||||
factory<Song>('song', {
|
||||
id: '123fd1ad32240ecab28a4e86ed5173',
|
||||
album_id: backToBedlam.id,
|
||||
artist_id: jamesBlunt.id,
|
||||
title: 'So long, Jimmy',
|
||||
length: 265.04
|
||||
}),
|
||||
factory<Song>('song', {
|
||||
id: '6a54c674d8b16732f26df73f59c63e21',
|
||||
album_id: backToBedlam.id,
|
||||
artist_id: jamesBlunt.id,
|
||||
title: 'Wisemen',
|
||||
length: 223.14
|
||||
}),
|
||||
factory<Song>('song', {
|
||||
id: '6df7d82a9a8701e40d1c291cf14a16bc',
|
||||
album_id: backToBedlam.id,
|
||||
artist_id: jamesBlunt.id,
|
||||
title: 'Goodbye my lover',
|
||||
length: 258.61
|
||||
}),
|
||||
factory<Song>('song', {
|
||||
id: '74a2000d343e4587273d3ad14e2fd741',
|
||||
album_id: backToBedlam.id,
|
||||
artist_id: jamesBlunt.id,
|
||||
title: 'High',
|
||||
length: 245.86
|
||||
}),
|
||||
factory<Song>('song', {
|
||||
id: '7900ab518f51775fe6cf06092c074ee5',
|
||||
album_id: backToBedlam.id,
|
||||
artist_id: jamesBlunt.id,
|
||||
title: 'You\'re beautiful',
|
||||
length: 213.29
|
||||
}),
|
||||
factory<Song>('song', {
|
||||
id: '803910a51f9893347e087af851e38777',
|
||||
album_id: backToBedlam.id,
|
||||
artist_id: jamesBlunt.id,
|
||||
title: 'Cry',
|
||||
length: 246.91
|
||||
}),
|
||||
factory<Song>('song', {
|
||||
id: 'd82b0d4d4803ebbcb61000a5b6a868f5',
|
||||
album_id: backToBedlam.id,
|
||||
artist_id: jamesBlunt.id,
|
||||
title: 'Tears and rain',
|
||||
length: 244.45
|
||||
})
|
||||
],
|
||||
interactions: [
|
||||
{
|
||||
id: 1,
|
||||
song_id: '7900ab518f51775fe6cf06092c074ee5',
|
||||
liked: false,
|
||||
play_count: 1
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
song_id: '95c0ffc33c08c8c14ea5de0a44d5df3c',
|
||||
liked: false,
|
||||
play_count: 2
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
song_id: 'c83b201502eb36f1084f207761fa195c',
|
||||
liked: false,
|
||||
play_count: 1
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
song_id: 'cb7edeac1f097143e65b1b2cde102482',
|
||||
liked: true,
|
||||
play_count: 3
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
song_id: 'ccc38cc14bb95aefdf6da4b34adcf548',
|
||||
liked: false,
|
||||
play_count: 4
|
||||
}
|
||||
] as Interaction[],
|
||||
currentUser,
|
||||
users: [
|
||||
currentUser,
|
||||
factory<User>('user', {
|
||||
id: 2,
|
||||
name: 'John Doe',
|
||||
email: 'john@doe.tld',
|
||||
is_admin: false
|
||||
})
|
||||
]
|
||||
}
|
|
@ -5,7 +5,7 @@ import { eventBus, noop } from '@/utils'
|
|||
import router from '@/router'
|
||||
import factory from '@/__tests__/factory'
|
||||
import UnitTestCase from '@/__tests__/UnitTestCase'
|
||||
import { nextTick } from 'vue'
|
||||
import { nextTick, reactive } from 'vue'
|
||||
import { socketService } from '@/services'
|
||||
import { playbackService } from './playbackService'
|
||||
|
||||
|
@ -43,11 +43,22 @@ new class extends UnitTestCase {
|
|||
super.beforeEach(() => this.setupEnvironment())
|
||||
}
|
||||
|
||||
private setCurrentSong (song?: Song) {
|
||||
song = reactive(song || factory<Song>('song', {
|
||||
playback_state: 'Playing'
|
||||
}))
|
||||
|
||||
queueStore.state.songs = reactive([song])
|
||||
return song
|
||||
}
|
||||
|
||||
protected test () {
|
||||
it('only initializes once', () => {
|
||||
const spy = vi.spyOn(plyr, 'setup')
|
||||
|
||||
playbackService.init()
|
||||
expect(spy).toHaveBeenCalled()
|
||||
|
||||
playbackService.init()
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
@ -60,23 +71,23 @@ new class extends UnitTestCase {
|
|||
])(
|
||||
'when playCountRegistered is %s, isTranscoding is %s, current media time is %d, media duration is %d, then registerPlay() should be call %d times',
|
||||
(playCountRegistered, isTranscoding, currentTime, duration, numberOfCalls) => {
|
||||
this.setCurrentSong(factory<Song>('song', {
|
||||
play_count_registered: playCountRegistered,
|
||||
playback_state: 'Playing'
|
||||
}))
|
||||
|
||||
this.setReadOnlyProperty(playbackService, 'isTranscoding', isTranscoding)
|
||||
playbackService.init()
|
||||
const mediaElement = playbackService.player!.media
|
||||
|
||||
const mediaElement = playbackService.player.media
|
||||
|
||||
// we can't set mediaElement.currentTime|duration directly because they're read-only
|
||||
Object.defineProperties(mediaElement, {
|
||||
currentTime: {
|
||||
value: currentTime,
|
||||
configurable: true
|
||||
},
|
||||
duration: {
|
||||
value: duration,
|
||||
configurable: true
|
||||
}
|
||||
})
|
||||
this.setReadOnlyProperty(mediaElement, 'currentTime', currentTime)
|
||||
this.setReadOnlyProperty(mediaElement, 'duration', duration)
|
||||
|
||||
const registerPlayMock = this.mock(playbackService, 'registerPlay')
|
||||
mediaElement.dispatchEvent(new Event('timeupdate'))
|
||||
|
||||
expect(registerPlayMock).toHaveBeenCalledTimes(numberOfCalls)
|
||||
})
|
||||
|
||||
|
@ -89,11 +100,11 @@ new class extends UnitTestCase {
|
|||
|
||||
it('scrobbles if current song ends', () => {
|
||||
commonStore.state.use_last_fm = true
|
||||
userStore.current = factory<User>('user', {
|
||||
userStore.state.current = reactive(factory<User>('user', {
|
||||
preferences: {
|
||||
lastfm_session_key: 'foo'
|
||||
}
|
||||
})
|
||||
}))
|
||||
|
||||
playbackService.init()
|
||||
const scrobbleMock = this.mock(songStore, 'scrobble')
|
||||
|
@ -109,7 +120,9 @@ new class extends UnitTestCase {
|
|||
playbackService.init()
|
||||
const restartMock = this.mock(playbackService, 'restart')
|
||||
const playNextMock = this.mock(playbackService, 'playNext')
|
||||
|
||||
playbackService.player!.media.dispatchEvent(new Event('ended'))
|
||||
|
||||
expect(restartMock).toHaveBeenCalledTimes(restartCalls)
|
||||
expect(playNextMock).toHaveBeenCalledTimes(playNextCalls)
|
||||
})
|
||||
|
@ -128,16 +141,8 @@ new class extends UnitTestCase {
|
|||
|
||||
const mediaElement = playbackService.player!.media
|
||||
|
||||
Object.defineProperties(mediaElement, {
|
||||
currentTime: {
|
||||
value: currentTime,
|
||||
configurable: true
|
||||
},
|
||||
duration: {
|
||||
value: duration,
|
||||
configurable: true
|
||||
}
|
||||
})
|
||||
this.setReadOnlyProperty(mediaElement, 'currentTime', currentTime)
|
||||
this.setReadOnlyProperty(mediaElement, 'duration', duration)
|
||||
|
||||
const preloadMock = this.mock(playbackService, 'preload')
|
||||
mediaElement.dispatchEvent(new Event('timeupdate'))
|
||||
|
@ -148,14 +153,12 @@ new class extends UnitTestCase {
|
|||
|
||||
it('registers play', () => {
|
||||
const recentlyPlayedStoreAddMock = this.mock(recentlyPlayedStore, 'add')
|
||||
const recentlyPlayedStoreFetchAllMock = this.mock(recentlyPlayedStore, 'fetch')
|
||||
const registerPlayMock = this.mock(songStore, 'registerPlay')
|
||||
const song = factory<Song>('song')
|
||||
|
||||
playbackService.registerPlay(song)
|
||||
|
||||
expect(recentlyPlayedStoreAddMock).toHaveBeenCalledWith(song)
|
||||
expect(recentlyPlayedStoreFetchAllMock).toHaveBeenCalled()
|
||||
expect(registerPlayMock).toHaveBeenCalledWith(song)
|
||||
expect(song.play_count_registered).toBe(true)
|
||||
})
|
||||
|
@ -180,13 +183,11 @@ new class extends UnitTestCase {
|
|||
})
|
||||
|
||||
it('restarts a song', async () => {
|
||||
const song = factory<Song>('song')
|
||||
const song = this.setCurrentSong()
|
||||
this.mock(Math, 'floor', 1000)
|
||||
const emitMock = this.mock(eventBus, 'emit')
|
||||
const broadcastMock = this.mock(socketService, 'broadcast')
|
||||
const showNotificationMock = this.mock(playbackService, 'showNotification')
|
||||
const dataToBroadcast = {}
|
||||
this.mock(songStore, 'generateDataToBroadcast', dataToBroadcast)
|
||||
const restartMock = this.mock(playbackService.player!, 'restart')
|
||||
const playMock = this.mock(window.HTMLMediaElement.prototype, 'play')
|
||||
|
||||
|
@ -195,7 +196,7 @@ new class extends UnitTestCase {
|
|||
expect(song.play_start_time).toEqual(1000)
|
||||
expect(song.play_count_registered).toBe(false)
|
||||
expect(emitMock).toHaveBeenCalledWith('SONG_STARTED', song)
|
||||
expect(broadcastMock).toHaveBeenCalledWith('SOCKET_SONG', dataToBroadcast)
|
||||
expect(broadcastMock).toHaveBeenCalledWith('SOCKET_SONG', song)
|
||||
expect(showNotificationMock).toHaveBeenCalled()
|
||||
expect(restartMock).toHaveBeenCalled()
|
||||
expect(playMock).toHaveBeenCalled()
|
||||
|
@ -279,23 +280,22 @@ new class extends UnitTestCase {
|
|||
})
|
||||
|
||||
it('pauses playback', () => {
|
||||
const currentSong = factory<Song>('song')
|
||||
const dataToBroadcast = {}
|
||||
this.mock(songStore, 'generateDataToBroadcast', dataToBroadcast)
|
||||
const song = this.setCurrentSong()
|
||||
const pauseMock = this.mock(playbackService.player!, 'pause')
|
||||
const broadcastMock = this.mock(socketService, 'broadcast')
|
||||
|
||||
playbackService.pause()
|
||||
|
||||
expect(currentSong.playback_state).toEqual('Paused')
|
||||
expect(broadcastMock).toHaveBeenCalledWith('SOCKET_SONG', dataToBroadcast)
|
||||
expect(song.playback_state).toEqual('Paused')
|
||||
expect(broadcastMock).toHaveBeenCalledWith('SOCKET_SONG', song)
|
||||
expect(pauseMock).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('resumes playback', async () => {
|
||||
const currentSong = factory<Song>('song')
|
||||
const dataToBroadcast = {}
|
||||
this.mock(songStore, 'generateDataToBroadcast', dataToBroadcast)
|
||||
const song = this.setCurrentSong(factory<Song>('song', {
|
||||
playback_state: 'Paused'
|
||||
}))
|
||||
|
||||
const playMock = this.mock(window.HTMLMediaElement.prototype, 'play')
|
||||
const broadcastMock = this.mock(socketService, 'broadcast')
|
||||
const emitMock = this.mock(eventBus, 'emit')
|
||||
|
@ -304,12 +304,13 @@ new class extends UnitTestCase {
|
|||
await playbackService.resume()
|
||||
|
||||
expect(queueStore.current?.playback_state).toEqual('Playing')
|
||||
expect(broadcastMock).toHaveBeenCalledWith('SOCKET_SONG', dataToBroadcast)
|
||||
expect(broadcastMock).toHaveBeenCalledWith('SOCKET_SONG', song)
|
||||
expect(playMock).toHaveBeenCalled()
|
||||
expect(emitMock).toHaveBeenCalledWith('SONG_STARTED', currentSong)
|
||||
expect(emitMock).toHaveBeenCalledWith('SONG_STARTED', song)
|
||||
})
|
||||
|
||||
it('plays first in queue if toggled when there is no current song', async () => {
|
||||
queueStore.clear()
|
||||
const playFirstInQueueMock = this.mock(playbackService, 'playFirstInQueue')
|
||||
|
||||
await playbackService.toggle()
|
||||
|
@ -318,10 +319,10 @@ new class extends UnitTestCase {
|
|||
})
|
||||
|
||||
it.each<[MethodOf<typeof playbackService>, PlaybackState]>([
|
||||
['resume', 'Stopped'],
|
||||
['resume', 'Paused'],
|
||||
['pause', 'Playing']
|
||||
])('%ss playback if toggled when current song playback state is %s', async (action, playbackState) => {
|
||||
this.setCurrentSong(factory<Song>('song', { playback_state: playbackState }))
|
||||
const actionMock = this.mock(playbackService, action)
|
||||
await playbackService.toggle()
|
||||
|
||||
|
@ -375,14 +376,5 @@ new class extends UnitTestCase {
|
|||
|
||||
expect(playMock).toHaveBeenCalledWith(songs[0])
|
||||
})
|
||||
|
||||
it('playFirstInQueue triggers queueAndPlay if queue is empty', async () => {
|
||||
queueStore.all = []
|
||||
const queueAndPlayMock = this.mock(playbackService, 'queueAndPlay')
|
||||
|
||||
await playbackService.playFirstInQueue()
|
||||
|
||||
expect(queueAndPlayMock).toHaveBeenCalled()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,20 +22,20 @@ import router from '@/router'
|
|||
const PRELOAD_BUFFER = 30
|
||||
const DEFAULT_VOLUME_VALUE = 7
|
||||
const VOLUME_INPUT_SELECTOR = '#volumeInput'
|
||||
const REPEAT_MODES: RepeatMode[] = ['NO_REPEAT', 'REPEAT_ALL', 'REPEAT_ONE']
|
||||
|
||||
export const playbackService = {
|
||||
player: null as Plyr | null,
|
||||
volumeInput: null as unknown as HTMLInputElement,
|
||||
repeatModes: REPEAT_MODES,
|
||||
initialized: false,
|
||||
class PlaybackService {
|
||||
public player: Plyr
|
||||
private volumeInput: HTMLInputElement
|
||||
private repeatModes: RepeatMode[] = ['NO_REPEAT', 'REPEAT_ALL', 'REPEAT_ONE']
|
||||
private initialized = false
|
||||
|
||||
init () {
|
||||
// We don't need to init this service twice, or the media events will be duplicated.
|
||||
public init () {
|
||||
if (this.initialized) {
|
||||
return
|
||||
}
|
||||
|
||||
this.initialized = true
|
||||
|
||||
this.player = plyr.setup('.plyr', {
|
||||
controls: []
|
||||
})[0]
|
||||
|
@ -54,10 +54,9 @@ export const playbackService = {
|
|||
}
|
||||
|
||||
this.setMediaSessionActionHandlers()
|
||||
this.initialized = true
|
||||
},
|
||||
}
|
||||
|
||||
setMediaSessionActionHandlers () {
|
||||
private setMediaSessionActionHandlers () {
|
||||
if (!navigator.mediaSession) {
|
||||
return
|
||||
}
|
||||
|
@ -66,9 +65,9 @@ export const playbackService = {
|
|||
navigator.mediaSession.setActionHandler('pause', () => this.pause())
|
||||
navigator.mediaSession.setActionHandler('previoustrack', () => this.playPrev())
|
||||
navigator.mediaSession.setActionHandler('nexttrack', () => this.playNext())
|
||||
},
|
||||
}
|
||||
|
||||
listenToMediaEvents (mediaElement: HTMLMediaElement) {
|
||||
private listenToMediaEvents (mediaElement: HTMLMediaElement) {
|
||||
mediaElement.addEventListener('error', () => this.playNext(), true)
|
||||
|
||||
mediaElement.addEventListener('ended', () => {
|
||||
|
@ -104,29 +103,25 @@ export const playbackService = {
|
|||
}
|
||||
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
timeUpdateHandler = throttle(timeUpdateHandler, 3000)
|
||||
timeUpdateHandler = throttle(timeUpdateHandler, 1000)
|
||||
}
|
||||
|
||||
mediaElement.addEventListener('timeupdate', timeUpdateHandler)
|
||||
},
|
||||
}
|
||||
|
||||
get isTranscoding () {
|
||||
return isMobile.any && preferences.transcodeOnMobile
|
||||
},
|
||||
|
||||
registerPlay (song: Song) {
|
||||
public registerPlay (song: Song) {
|
||||
recentlyPlayedStore.add(song)
|
||||
songStore.registerPlay(song)
|
||||
song.play_count_registered = true
|
||||
},
|
||||
}
|
||||
|
||||
preload (song: Song) {
|
||||
public preload (song: Song) {
|
||||
const audioElement = document.createElement('audio')
|
||||
audioElement.setAttribute('src', songStore.getSourceUrl(song))
|
||||
audioElement.setAttribute('preload', 'auto')
|
||||
audioElement.load()
|
||||
song.preloaded = true
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Play a song. Because
|
||||
|
@ -136,9 +131,9 @@ export const playbackService = {
|
|||
* So many dreams swinging out of the blue
|
||||
* We'll let them come true
|
||||
*/
|
||||
async play (song: Song) {
|
||||
public async play (song: Song) {
|
||||
document.title = `${song.title} ♫ Koel`
|
||||
this.player!.media.setAttribute('title', `${song.artist_name} - ${song.title}`)
|
||||
this.player.media.setAttribute('title', `${song.artist_name} - ${song.title}`)
|
||||
|
||||
if (queueStore.current) {
|
||||
queueStore.current.playback_state = 'Stopped'
|
||||
|
@ -148,7 +143,7 @@ export const playbackService = {
|
|||
|
||||
// Manually set the `src` attribute of the audio to prevent plyr from resetting
|
||||
// the audio media object and cause our equalizer to malfunction.
|
||||
this.getPlayer().media.src = songStore.getSourceUrl(song)
|
||||
this.player.media.src = songStore.getSourceUrl(song)
|
||||
|
||||
// We'll just "restart" playing the song, which will handle notification, scrobbling etc.
|
||||
// Fixes #898
|
||||
|
@ -157,9 +152,9 @@ export const playbackService = {
|
|||
}
|
||||
|
||||
await this.restart()
|
||||
},
|
||||
}
|
||||
|
||||
showNotification (song: Song) {
|
||||
public showNotification (song: Song) {
|
||||
if (!window.Notification || !preferences.notify) {
|
||||
return
|
||||
}
|
||||
|
@ -191,9 +186,9 @@ export const playbackService = {
|
|||
}
|
||||
]
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
async restart () {
|
||||
public async restart () {
|
||||
const song = queueStore.current!
|
||||
|
||||
this.showNotification(song)
|
||||
|
@ -205,21 +200,25 @@ export const playbackService = {
|
|||
eventBus.emit('SONG_STARTED', song)
|
||||
socketService.broadcast('SOCKET_SONG', song)
|
||||
|
||||
this.getPlayer().restart()
|
||||
this.player.restart()
|
||||
|
||||
try {
|
||||
await this.getPlayer().media.play()
|
||||
await this.player.media.play()
|
||||
} catch (error) {
|
||||
// convert this into a warning, as an error will cause Cypress to fail the tests entirely
|
||||
logger.warn(error)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
public get isTranscoding () {
|
||||
return isMobile.any && preferences.transcodeOnMobile
|
||||
}
|
||||
|
||||
/**
|
||||
* The next song in the queue.
|
||||
* If we're in REPEAT_ALL mode and there's no next song, just get the first song.
|
||||
*/
|
||||
get next () {
|
||||
public get next () {
|
||||
if (queueStore.next) {
|
||||
return queueStore.next
|
||||
}
|
||||
|
@ -227,13 +226,13 @@ export const playbackService = {
|
|||
if (preferences.repeatMode === 'REPEAT_ALL') {
|
||||
return queueStore.first
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* The previous song in the queue.
|
||||
* If we're in REPEAT_ALL mode and there's no prev song, get the last song.
|
||||
*/
|
||||
get previous () {
|
||||
public get previous () {
|
||||
if (queueStore.previous) {
|
||||
return queueStore.previous
|
||||
}
|
||||
|
@ -241,13 +240,13 @@ export const playbackService = {
|
|||
if (preferences.repeatMode === 'REPEAT_ALL') {
|
||||
return queueStore.last
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Circle through the repeat mode.
|
||||
* The selected mode will be stored into local storage as well.
|
||||
*/
|
||||
changeRepeatMode () {
|
||||
public changeRepeatMode () {
|
||||
let index = this.repeatModes.indexOf(preferences.repeatMode) + 1
|
||||
|
||||
if (index >= this.repeatModes.length) {
|
||||
|
@ -255,17 +254,17 @@ export const playbackService = {
|
|||
}
|
||||
|
||||
preferences.repeatMode = this.repeatModes[index]
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Play the prev song in the queue, if one is found.
|
||||
* If the prev song is not found and the current mode is NO_REPEAT, we stop completely.
|
||||
*/
|
||||
async playPrev () {
|
||||
public async playPrev () {
|
||||
// If the song's duration is greater than 5 seconds and we've passed 5 seconds into it,
|
||||
// restart playing instead.
|
||||
if (this.getPlayer().media.currentTime > 5 && queueStore.current!.length > 5) {
|
||||
this.getPlayer().restart()
|
||||
if (this.player.media.currentTime > 5 && queueStore.current!.length > 5) {
|
||||
this.player.restart()
|
||||
|
||||
return
|
||||
}
|
||||
|
@ -275,66 +274,64 @@ export const playbackService = {
|
|||
} else {
|
||||
this.previous && await this.play(this.previous)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Play the next song in the queue, if one is found.
|
||||
* If the next song is not found and the current mode is NO_REPEAT, we stop completely.
|
||||
*/
|
||||
async playNext () {
|
||||
public async playNext () {
|
||||
if (!this.next && preferences.repeatMode === 'NO_REPEAT') {
|
||||
await this.stop() // Nothing lasts forever, even cold November rain.
|
||||
} else {
|
||||
this.next && await this.play(this.next)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
getVolume: () => preferences.volume,
|
||||
public getVolume () {
|
||||
return preferences.volume
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Number} volume 0-10
|
||||
* @param {Boolean=true} persist Whether the volume should be saved into local storage
|
||||
*/
|
||||
setVolume (volume: number, persist = true) {
|
||||
this.getPlayer().setVolume(volume)
|
||||
|
||||
if (persist) {
|
||||
preferences.volume = volume
|
||||
}
|
||||
|
||||
public setVolume (volume: number, persist = true) {
|
||||
this.player.setVolume(volume)
|
||||
persist && (preferences.volume = volume)
|
||||
this.volumeInput.value = String(volume)
|
||||
},
|
||||
}
|
||||
|
||||
mute () {
|
||||
public mute () {
|
||||
this.setVolume(0, false)
|
||||
},
|
||||
}
|
||||
|
||||
unmute () {
|
||||
public unmute () {
|
||||
preferences.volume = preferences.volume || DEFAULT_VOLUME_VALUE
|
||||
this.setVolume(preferences.volume)
|
||||
},
|
||||
}
|
||||
|
||||
async stop () {
|
||||
public async stop () {
|
||||
document.title = 'Koel'
|
||||
this.getPlayer().pause()
|
||||
this.getPlayer().seek(0)
|
||||
this.player.pause()
|
||||
this.player.seek(0)
|
||||
|
||||
if (queueStore.current) {
|
||||
queueStore.current.playback_state = 'Stopped'
|
||||
}
|
||||
|
||||
socketService.broadcast('SOCKET_PLAYBACK_STOPPED')
|
||||
},
|
||||
}
|
||||
|
||||
pause () {
|
||||
this.getPlayer().pause()
|
||||
public pause () {
|
||||
this.player.pause()
|
||||
queueStore.current!.playback_state = 'Paused'
|
||||
socketService.broadcast('SOCKET_SONG', queueStore.current)
|
||||
},
|
||||
}
|
||||
|
||||
async resume () {
|
||||
public async resume () {
|
||||
try {
|
||||
await this.getPlayer().media.play()
|
||||
await this.player.media.play()
|
||||
} catch (error) {
|
||||
logger.error(error)
|
||||
}
|
||||
|
@ -342,9 +339,9 @@ export const playbackService = {
|
|||
queueStore.current!.playback_state = 'Playing'
|
||||
eventBus.emit('SONG_STARTED', queueStore.current)
|
||||
socketService.broadcast('SOCKET_SONG', queueStore.current)
|
||||
},
|
||||
}
|
||||
|
||||
async toggle () {
|
||||
public async toggle () {
|
||||
if (!queueStore.current) {
|
||||
await this.playFirstInQueue()
|
||||
return
|
||||
|
@ -356,7 +353,7 @@ export const playbackService = {
|
|||
}
|
||||
|
||||
this.pause()
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue up songs (replace them into the queue) and start playing right away.
|
||||
|
@ -364,7 +361,7 @@ export const playbackService = {
|
|||
* @param {?Song[]} songs An array of song objects. Defaults to all songs if null.
|
||||
* @param {Boolean=false} shuffled Whether to shuffle the songs before playing.
|
||||
*/
|
||||
async queueAndPlay (songs: Song[], shuffled = false) {
|
||||
public async queueAndPlay (songs: Song[], shuffled = false) {
|
||||
if (shuffled) {
|
||||
songs = shuffle(songs)
|
||||
}
|
||||
|
@ -376,13 +373,11 @@ export const playbackService = {
|
|||
await nextTick()
|
||||
router.go('queue')
|
||||
await this.play(queueStore.first)
|
||||
},
|
||||
}
|
||||
|
||||
getPlayer () {
|
||||
return this.player!
|
||||
},
|
||||
|
||||
async playFirstInQueue () {
|
||||
public async playFirstInQueue () {
|
||||
queueStore.all.length && await this.play(queueStore.first)
|
||||
}
|
||||
}
|
||||
|
||||
export const playbackService = new PlaybackService()
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import { reactive } from 'vue'
|
||||
import { differenceBy, shuffle, union, unionBy } from 'lodash'
|
||||
import { differenceBy, shuffle, unionBy } from 'lodash'
|
||||
import { arrayify } from '@/utils'
|
||||
import { httpService } from '@/services'
|
||||
import { songStore } from '@/stores'
|
||||
|
||||
export const queueStore = {
|
||||
state: reactive({
|
||||
songs: [] as Song[],
|
||||
current: null as Song
|
||||
songs: [] as Song[]
|
||||
}),
|
||||
|
||||
init () {
|
||||
|
@ -40,7 +39,7 @@ export const queueStore = {
|
|||
},
|
||||
|
||||
set all (songs: Song[]) {
|
||||
this.state.songs = songs
|
||||
this.state.songs = reactive(songs)
|
||||
},
|
||||
|
||||
get first () {
|
||||
|
@ -52,7 +51,7 @@ export const queueStore = {
|
|||
},
|
||||
|
||||
contains (song: Song) {
|
||||
return this.all.includes(song)
|
||||
return this.all.includes(reactive(song))
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -74,7 +73,7 @@ export const queueStore = {
|
|||
},
|
||||
|
||||
replaceQueueWith (songs: Song | Song[]) {
|
||||
this.state.songs = arrayify(songs)
|
||||
this.state.songs = reactive(arrayify(songs))
|
||||
},
|
||||
|
||||
queueAfterCurrent (songs: Song | Song[]) {
|
||||
|
@ -88,7 +87,7 @@ export const queueStore = {
|
|||
this.unqueue(songs)
|
||||
|
||||
const head = this.all.splice(0, this.indexOf(this.current) + 1)
|
||||
this.all = head.concat(songs, this.all)
|
||||
this.all = head.concat(reactive(songs), this.all)
|
||||
},
|
||||
|
||||
unqueue (songs: Song | Song[]) {
|
||||
|
@ -104,7 +103,7 @@ export const queueStore = {
|
|||
|
||||
movedSongs.forEach(song => {
|
||||
this.all.splice(this.indexOf(song), 1)
|
||||
this.all.splice(targetIndex, 0, song)
|
||||
this.all.splice(targetIndex, 0, reactive(song))
|
||||
})
|
||||
},
|
||||
|
||||
|
@ -113,7 +112,7 @@ export const queueStore = {
|
|||
},
|
||||
|
||||
indexOf (song: Song) {
|
||||
return this.all.indexOf(song)
|
||||
return this.all.indexOf(reactive(song))
|
||||
},
|
||||
|
||||
get next () {
|
||||
|
|
Loading…
Reference in a new issue