fix(test): fix playbackService tests

This commit is contained in:
Phan An 2022-07-25 20:23:30 +02:00
parent 235362ba30
commit be68021723
No known key found for this signature in database
GPG key ID: A81E4477F0BB6FDC
5 changed files with 127 additions and 359 deletions

View file

@ -103,8 +103,11 @@ export default abstract class UnitTestCase {
} }
protected setReadOnlyProperty<T> (obj: T, prop: keyof T, value: any) { protected setReadOnlyProperty<T> (obj: T, prop: keyof T, value: any) {
return Object.defineProperty(obj, prop, { return Object.defineProperties(obj, {
get: () => value [prop]: {
value,
configurable: true
}
}) })
} }

View file

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

View file

@ -5,7 +5,7 @@ import { eventBus, noop } from '@/utils'
import router from '@/router' import router from '@/router'
import factory from '@/__tests__/factory' import factory from '@/__tests__/factory'
import UnitTestCase from '@/__tests__/UnitTestCase' import UnitTestCase from '@/__tests__/UnitTestCase'
import { nextTick } from 'vue' import { nextTick, reactive } from 'vue'
import { socketService } from '@/services' import { socketService } from '@/services'
import { playbackService } from './playbackService' import { playbackService } from './playbackService'
@ -43,11 +43,22 @@ new class extends UnitTestCase {
super.beforeEach(() => this.setupEnvironment()) 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 () { protected test () {
it('only initializes once', () => { it('only initializes once', () => {
const spy = vi.spyOn(plyr, 'setup') const spy = vi.spyOn(plyr, 'setup')
playbackService.init() playbackService.init()
expect(spy).toHaveBeenCalled() expect(spy).toHaveBeenCalled()
playbackService.init() playbackService.init()
expect(spy).toHaveBeenCalledTimes(1) 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', '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) => { (playCountRegistered, isTranscoding, currentTime, duration, numberOfCalls) => {
this.setCurrentSong(factory<Song>('song', {
play_count_registered: playCountRegistered,
playback_state: 'Playing'
}))
this.setReadOnlyProperty(playbackService, 'isTranscoding', isTranscoding) this.setReadOnlyProperty(playbackService, 'isTranscoding', isTranscoding)
playbackService.init() 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 // we can't set mediaElement.currentTime|duration directly because they're read-only
Object.defineProperties(mediaElement, { this.setReadOnlyProperty(mediaElement, 'currentTime', currentTime)
currentTime: { this.setReadOnlyProperty(mediaElement, 'duration', duration)
value: currentTime,
configurable: true
},
duration: {
value: duration,
configurable: true
}
})
const registerPlayMock = this.mock(playbackService, 'registerPlay') const registerPlayMock = this.mock(playbackService, 'registerPlay')
mediaElement.dispatchEvent(new Event('timeupdate')) mediaElement.dispatchEvent(new Event('timeupdate'))
expect(registerPlayMock).toHaveBeenCalledTimes(numberOfCalls) expect(registerPlayMock).toHaveBeenCalledTimes(numberOfCalls)
}) })
@ -89,11 +100,11 @@ new class extends UnitTestCase {
it('scrobbles if current song ends', () => { it('scrobbles if current song ends', () => {
commonStore.state.use_last_fm = true commonStore.state.use_last_fm = true
userStore.current = factory<User>('user', { userStore.state.current = reactive(factory<User>('user', {
preferences: { preferences: {
lastfm_session_key: 'foo' lastfm_session_key: 'foo'
} }
}) }))
playbackService.init() playbackService.init()
const scrobbleMock = this.mock(songStore, 'scrobble') const scrobbleMock = this.mock(songStore, 'scrobble')
@ -109,7 +120,9 @@ new class extends UnitTestCase {
playbackService.init() playbackService.init()
const restartMock = this.mock(playbackService, 'restart') const restartMock = this.mock(playbackService, 'restart')
const playNextMock = this.mock(playbackService, 'playNext') const playNextMock = this.mock(playbackService, 'playNext')
playbackService.player!.media.dispatchEvent(new Event('ended')) playbackService.player!.media.dispatchEvent(new Event('ended'))
expect(restartMock).toHaveBeenCalledTimes(restartCalls) expect(restartMock).toHaveBeenCalledTimes(restartCalls)
expect(playNextMock).toHaveBeenCalledTimes(playNextCalls) expect(playNextMock).toHaveBeenCalledTimes(playNextCalls)
}) })
@ -128,16 +141,8 @@ new class extends UnitTestCase {
const mediaElement = playbackService.player!.media const mediaElement = playbackService.player!.media
Object.defineProperties(mediaElement, { this.setReadOnlyProperty(mediaElement, 'currentTime', currentTime)
currentTime: { this.setReadOnlyProperty(mediaElement, 'duration', duration)
value: currentTime,
configurable: true
},
duration: {
value: duration,
configurable: true
}
})
const preloadMock = this.mock(playbackService, 'preload') const preloadMock = this.mock(playbackService, 'preload')
mediaElement.dispatchEvent(new Event('timeupdate')) mediaElement.dispatchEvent(new Event('timeupdate'))
@ -148,14 +153,12 @@ new class extends UnitTestCase {
it('registers play', () => { it('registers play', () => {
const recentlyPlayedStoreAddMock = this.mock(recentlyPlayedStore, 'add') const recentlyPlayedStoreAddMock = this.mock(recentlyPlayedStore, 'add')
const recentlyPlayedStoreFetchAllMock = this.mock(recentlyPlayedStore, 'fetch')
const registerPlayMock = this.mock(songStore, 'registerPlay') const registerPlayMock = this.mock(songStore, 'registerPlay')
const song = factory<Song>('song') const song = factory<Song>('song')
playbackService.registerPlay(song) playbackService.registerPlay(song)
expect(recentlyPlayedStoreAddMock).toHaveBeenCalledWith(song) expect(recentlyPlayedStoreAddMock).toHaveBeenCalledWith(song)
expect(recentlyPlayedStoreFetchAllMock).toHaveBeenCalled()
expect(registerPlayMock).toHaveBeenCalledWith(song) expect(registerPlayMock).toHaveBeenCalledWith(song)
expect(song.play_count_registered).toBe(true) expect(song.play_count_registered).toBe(true)
}) })
@ -180,13 +183,11 @@ new class extends UnitTestCase {
}) })
it('restarts a song', async () => { it('restarts a song', async () => {
const song = factory<Song>('song') const song = this.setCurrentSong()
this.mock(Math, 'floor', 1000) this.mock(Math, 'floor', 1000)
const emitMock = this.mock(eventBus, 'emit') const emitMock = this.mock(eventBus, 'emit')
const broadcastMock = this.mock(socketService, 'broadcast') const broadcastMock = this.mock(socketService, 'broadcast')
const showNotificationMock = this.mock(playbackService, 'showNotification') const showNotificationMock = this.mock(playbackService, 'showNotification')
const dataToBroadcast = {}
this.mock(songStore, 'generateDataToBroadcast', dataToBroadcast)
const restartMock = this.mock(playbackService.player!, 'restart') const restartMock = this.mock(playbackService.player!, 'restart')
const playMock = this.mock(window.HTMLMediaElement.prototype, 'play') 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_start_time).toEqual(1000)
expect(song.play_count_registered).toBe(false) expect(song.play_count_registered).toBe(false)
expect(emitMock).toHaveBeenCalledWith('SONG_STARTED', song) expect(emitMock).toHaveBeenCalledWith('SONG_STARTED', song)
expect(broadcastMock).toHaveBeenCalledWith('SOCKET_SONG', dataToBroadcast) expect(broadcastMock).toHaveBeenCalledWith('SOCKET_SONG', song)
expect(showNotificationMock).toHaveBeenCalled() expect(showNotificationMock).toHaveBeenCalled()
expect(restartMock).toHaveBeenCalled() expect(restartMock).toHaveBeenCalled()
expect(playMock).toHaveBeenCalled() expect(playMock).toHaveBeenCalled()
@ -279,23 +280,22 @@ new class extends UnitTestCase {
}) })
it('pauses playback', () => { it('pauses playback', () => {
const currentSong = factory<Song>('song') const song = this.setCurrentSong()
const dataToBroadcast = {}
this.mock(songStore, 'generateDataToBroadcast', dataToBroadcast)
const pauseMock = this.mock(playbackService.player!, 'pause') const pauseMock = this.mock(playbackService.player!, 'pause')
const broadcastMock = this.mock(socketService, 'broadcast') const broadcastMock = this.mock(socketService, 'broadcast')
playbackService.pause() playbackService.pause()
expect(currentSong.playback_state).toEqual('Paused') expect(song.playback_state).toEqual('Paused')
expect(broadcastMock).toHaveBeenCalledWith('SOCKET_SONG', dataToBroadcast) expect(broadcastMock).toHaveBeenCalledWith('SOCKET_SONG', song)
expect(pauseMock).toHaveBeenCalled() expect(pauseMock).toHaveBeenCalled()
}) })
it('resumes playback', async () => { it('resumes playback', async () => {
const currentSong = factory<Song>('song') const song = this.setCurrentSong(factory<Song>('song', {
const dataToBroadcast = {} playback_state: 'Paused'
this.mock(songStore, 'generateDataToBroadcast', dataToBroadcast) }))
const playMock = this.mock(window.HTMLMediaElement.prototype, 'play') const playMock = this.mock(window.HTMLMediaElement.prototype, 'play')
const broadcastMock = this.mock(socketService, 'broadcast') const broadcastMock = this.mock(socketService, 'broadcast')
const emitMock = this.mock(eventBus, 'emit') const emitMock = this.mock(eventBus, 'emit')
@ -304,12 +304,13 @@ new class extends UnitTestCase {
await playbackService.resume() await playbackService.resume()
expect(queueStore.current?.playback_state).toEqual('Playing') expect(queueStore.current?.playback_state).toEqual('Playing')
expect(broadcastMock).toHaveBeenCalledWith('SOCKET_SONG', dataToBroadcast) expect(broadcastMock).toHaveBeenCalledWith('SOCKET_SONG', song)
expect(playMock).toHaveBeenCalled() 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 () => { it('plays first in queue if toggled when there is no current song', async () => {
queueStore.clear()
const playFirstInQueueMock = this.mock(playbackService, 'playFirstInQueue') const playFirstInQueueMock = this.mock(playbackService, 'playFirstInQueue')
await playbackService.toggle() await playbackService.toggle()
@ -318,10 +319,10 @@ new class extends UnitTestCase {
}) })
it.each<[MethodOf<typeof playbackService>, PlaybackState]>([ it.each<[MethodOf<typeof playbackService>, PlaybackState]>([
['resume', 'Stopped'],
['resume', 'Paused'], ['resume', 'Paused'],
['pause', 'Playing'] ['pause', 'Playing']
])('%ss playback if toggled when current song playback state is %s', async (action, playbackState) => { ])('%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) const actionMock = this.mock(playbackService, action)
await playbackService.toggle() await playbackService.toggle()
@ -375,14 +376,5 @@ new class extends UnitTestCase {
expect(playMock).toHaveBeenCalledWith(songs[0]) 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()
})
} }
} }

View file

@ -22,20 +22,20 @@ import router from '@/router'
const PRELOAD_BUFFER = 30 const PRELOAD_BUFFER = 30
const DEFAULT_VOLUME_VALUE = 7 const DEFAULT_VOLUME_VALUE = 7
const VOLUME_INPUT_SELECTOR = '#volumeInput' const VOLUME_INPUT_SELECTOR = '#volumeInput'
const REPEAT_MODES: RepeatMode[] = ['NO_REPEAT', 'REPEAT_ALL', 'REPEAT_ONE']
export const playbackService = { class PlaybackService {
player: null as Plyr | null, public player: Plyr
volumeInput: null as unknown as HTMLInputElement, private volumeInput: HTMLInputElement
repeatModes: REPEAT_MODES, private repeatModes: RepeatMode[] = ['NO_REPEAT', 'REPEAT_ALL', 'REPEAT_ONE']
initialized: false, private initialized = false
init () { public init () {
// We don't need to init this service twice, or the media events will be duplicated.
if (this.initialized) { if (this.initialized) {
return return
} }
this.initialized = true
this.player = plyr.setup('.plyr', { this.player = plyr.setup('.plyr', {
controls: [] controls: []
})[0] })[0]
@ -54,10 +54,9 @@ export const playbackService = {
} }
this.setMediaSessionActionHandlers() this.setMediaSessionActionHandlers()
this.initialized = true }
},
setMediaSessionActionHandlers () { private setMediaSessionActionHandlers () {
if (!navigator.mediaSession) { if (!navigator.mediaSession) {
return return
} }
@ -66,9 +65,9 @@ export const playbackService = {
navigator.mediaSession.setActionHandler('pause', () => this.pause()) navigator.mediaSession.setActionHandler('pause', () => this.pause())
navigator.mediaSession.setActionHandler('previoustrack', () => this.playPrev()) navigator.mediaSession.setActionHandler('previoustrack', () => this.playPrev())
navigator.mediaSession.setActionHandler('nexttrack', () => this.playNext()) navigator.mediaSession.setActionHandler('nexttrack', () => this.playNext())
}, }
listenToMediaEvents (mediaElement: HTMLMediaElement) { private listenToMediaEvents (mediaElement: HTMLMediaElement) {
mediaElement.addEventListener('error', () => this.playNext(), true) mediaElement.addEventListener('error', () => this.playNext(), true)
mediaElement.addEventListener('ended', () => { mediaElement.addEventListener('ended', () => {
@ -104,29 +103,25 @@ export const playbackService = {
} }
if (process.env.NODE_ENV !== 'test') { if (process.env.NODE_ENV !== 'test') {
timeUpdateHandler = throttle(timeUpdateHandler, 3000) timeUpdateHandler = throttle(timeUpdateHandler, 1000)
} }
mediaElement.addEventListener('timeupdate', timeUpdateHandler) mediaElement.addEventListener('timeupdate', timeUpdateHandler)
}, }
get isTranscoding () { public registerPlay (song: Song) {
return isMobile.any && preferences.transcodeOnMobile
},
registerPlay (song: Song) {
recentlyPlayedStore.add(song) recentlyPlayedStore.add(song)
songStore.registerPlay(song) songStore.registerPlay(song)
song.play_count_registered = true song.play_count_registered = true
}, }
preload (song: Song) { public preload (song: Song) {
const audioElement = document.createElement('audio') const audioElement = document.createElement('audio')
audioElement.setAttribute('src', songStore.getSourceUrl(song)) audioElement.setAttribute('src', songStore.getSourceUrl(song))
audioElement.setAttribute('preload', 'auto') audioElement.setAttribute('preload', 'auto')
audioElement.load() audioElement.load()
song.preloaded = true song.preloaded = true
}, }
/** /**
* Play a song. Because * Play a song. Because
@ -136,9 +131,9 @@ export const playbackService = {
* So many dreams swinging out of the blue * So many dreams swinging out of the blue
* We'll let them come true * We'll let them come true
*/ */
async play (song: Song) { public async play (song: Song) {
document.title = `${song.title} ♫ Koel` 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) { if (queueStore.current) {
queueStore.current.playback_state = 'Stopped' 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 // Manually set the `src` attribute of the audio to prevent plyr from resetting
// the audio media object and cause our equalizer to malfunction. // 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. // We'll just "restart" playing the song, which will handle notification, scrobbling etc.
// Fixes #898 // Fixes #898
@ -157,9 +152,9 @@ export const playbackService = {
} }
await this.restart() await this.restart()
}, }
showNotification (song: Song) { public showNotification (song: Song) {
if (!window.Notification || !preferences.notify) { if (!window.Notification || !preferences.notify) {
return return
} }
@ -191,9 +186,9 @@ export const playbackService = {
} }
] ]
}) })
}, }
async restart () { public async restart () {
const song = queueStore.current! const song = queueStore.current!
this.showNotification(song) this.showNotification(song)
@ -205,21 +200,25 @@ export const playbackService = {
eventBus.emit('SONG_STARTED', song) eventBus.emit('SONG_STARTED', song)
socketService.broadcast('SOCKET_SONG', song) socketService.broadcast('SOCKET_SONG', song)
this.getPlayer().restart() this.player.restart()
try { try {
await this.getPlayer().media.play() await this.player.media.play()
} catch (error) { } catch (error) {
// convert this into a warning, as an error will cause Cypress to fail the tests entirely // convert this into a warning, as an error will cause Cypress to fail the tests entirely
logger.warn(error) logger.warn(error)
} }
}, }
public get isTranscoding () {
return isMobile.any && preferences.transcodeOnMobile
}
/** /**
* The next song in the queue. * The next song in the queue.
* If we're in REPEAT_ALL mode and there's no next song, just get the first song. * 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) { if (queueStore.next) {
return queueStore.next return queueStore.next
} }
@ -227,13 +226,13 @@ export const playbackService = {
if (preferences.repeatMode === 'REPEAT_ALL') { if (preferences.repeatMode === 'REPEAT_ALL') {
return queueStore.first return queueStore.first
} }
}, }
/** /**
* The previous song in the queue. * The previous song in the queue.
* If we're in REPEAT_ALL mode and there's no prev song, get the last song. * 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) { if (queueStore.previous) {
return queueStore.previous return queueStore.previous
} }
@ -241,13 +240,13 @@ export const playbackService = {
if (preferences.repeatMode === 'REPEAT_ALL') { if (preferences.repeatMode === 'REPEAT_ALL') {
return queueStore.last return queueStore.last
} }
}, }
/** /**
* Circle through the repeat mode. * Circle through the repeat mode.
* The selected mode will be stored into local storage as well. * The selected mode will be stored into local storage as well.
*/ */
changeRepeatMode () { public changeRepeatMode () {
let index = this.repeatModes.indexOf(preferences.repeatMode) + 1 let index = this.repeatModes.indexOf(preferences.repeatMode) + 1
if (index >= this.repeatModes.length) { if (index >= this.repeatModes.length) {
@ -255,17 +254,17 @@ export const playbackService = {
} }
preferences.repeatMode = this.repeatModes[index] preferences.repeatMode = this.repeatModes[index]
}, }
/** /**
* Play the prev song in the queue, if one is found. * 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. * 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, // If the song's duration is greater than 5 seconds and we've passed 5 seconds into it,
// restart playing instead. // restart playing instead.
if (this.getPlayer().media.currentTime > 5 && queueStore.current!.length > 5) { if (this.player.media.currentTime > 5 && queueStore.current!.length > 5) {
this.getPlayer().restart() this.player.restart()
return return
} }
@ -275,66 +274,64 @@ export const playbackService = {
} else { } else {
this.previous && await this.play(this.previous) this.previous && await this.play(this.previous)
} }
}, }
/** /**
* Play the next song in the queue, if one is found. * 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. * 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') { if (!this.next && preferences.repeatMode === 'NO_REPEAT') {
await this.stop() // Nothing lasts forever, even cold November rain. await this.stop() // Nothing lasts forever, even cold November rain.
} else { } else {
this.next && await this.play(this.next) this.next && await this.play(this.next)
} }
}, }
getVolume: () => preferences.volume, public getVolume () {
return preferences.volume
}
/** /**
* @param {Number} volume 0-10 * @param {Number} volume 0-10
* @param {Boolean=true} persist Whether the volume should be saved into local storage * @param {Boolean=true} persist Whether the volume should be saved into local storage
*/ */
setVolume (volume: number, persist = true) { public setVolume (volume: number, persist = true) {
this.getPlayer().setVolume(volume) this.player.setVolume(volume)
persist && (preferences.volume = volume)
if (persist) {
preferences.volume = volume
}
this.volumeInput.value = String(volume) this.volumeInput.value = String(volume)
}, }
mute () { public mute () {
this.setVolume(0, false) this.setVolume(0, false)
}, }
unmute () { public unmute () {
preferences.volume = preferences.volume || DEFAULT_VOLUME_VALUE preferences.volume = preferences.volume || DEFAULT_VOLUME_VALUE
this.setVolume(preferences.volume) this.setVolume(preferences.volume)
}, }
async stop () { public async stop () {
document.title = 'Koel' document.title = 'Koel'
this.getPlayer().pause() this.player.pause()
this.getPlayer().seek(0) this.player.seek(0)
if (queueStore.current) { if (queueStore.current) {
queueStore.current.playback_state = 'Stopped' queueStore.current.playback_state = 'Stopped'
} }
socketService.broadcast('SOCKET_PLAYBACK_STOPPED') socketService.broadcast('SOCKET_PLAYBACK_STOPPED')
}, }
pause () { public pause () {
this.getPlayer().pause() this.player.pause()
queueStore.current!.playback_state = 'Paused' queueStore.current!.playback_state = 'Paused'
socketService.broadcast('SOCKET_SONG', queueStore.current) socketService.broadcast('SOCKET_SONG', queueStore.current)
}, }
async resume () { public async resume () {
try { try {
await this.getPlayer().media.play() await this.player.media.play()
} catch (error) { } catch (error) {
logger.error(error) logger.error(error)
} }
@ -342,9 +339,9 @@ export const playbackService = {
queueStore.current!.playback_state = 'Playing' queueStore.current!.playback_state = 'Playing'
eventBus.emit('SONG_STARTED', queueStore.current) eventBus.emit('SONG_STARTED', queueStore.current)
socketService.broadcast('SOCKET_SONG', queueStore.current) socketService.broadcast('SOCKET_SONG', queueStore.current)
}, }
async toggle () { public async toggle () {
if (!queueStore.current) { if (!queueStore.current) {
await this.playFirstInQueue() await this.playFirstInQueue()
return return
@ -356,7 +353,7 @@ export const playbackService = {
} }
this.pause() this.pause()
}, }
/** /**
* Queue up songs (replace them into the queue) and start playing right away. * 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 {?Song[]} songs An array of song objects. Defaults to all songs if null.
* @param {Boolean=false} shuffled Whether to shuffle the songs before playing. * @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) { if (shuffled) {
songs = shuffle(songs) songs = shuffle(songs)
} }
@ -376,13 +373,11 @@ export const playbackService = {
await nextTick() await nextTick()
router.go('queue') router.go('queue')
await this.play(queueStore.first) await this.play(queueStore.first)
}, }
getPlayer () { public async playFirstInQueue () {
return this.player!
},
async playFirstInQueue () {
queueStore.all.length && await this.play(queueStore.first) queueStore.all.length && await this.play(queueStore.first)
} }
} }
export const playbackService = new PlaybackService()

View file

@ -1,13 +1,12 @@
import { reactive } from 'vue' import { reactive } from 'vue'
import { differenceBy, shuffle, union, unionBy } from 'lodash' import { differenceBy, shuffle, unionBy } from 'lodash'
import { arrayify } from '@/utils' import { arrayify } from '@/utils'
import { httpService } from '@/services' import { httpService } from '@/services'
import { songStore } from '@/stores' import { songStore } from '@/stores'
export const queueStore = { export const queueStore = {
state: reactive({ state: reactive({
songs: [] as Song[], songs: [] as Song[]
current: null as Song
}), }),
init () { init () {
@ -40,7 +39,7 @@ export const queueStore = {
}, },
set all (songs: Song[]) { set all (songs: Song[]) {
this.state.songs = songs this.state.songs = reactive(songs)
}, },
get first () { get first () {
@ -52,7 +51,7 @@ export const queueStore = {
}, },
contains (song: Song) { 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[]) { replaceQueueWith (songs: Song | Song[]) {
this.state.songs = arrayify(songs) this.state.songs = reactive(arrayify(songs))
}, },
queueAfterCurrent (songs: Song | Song[]) { queueAfterCurrent (songs: Song | Song[]) {
@ -88,7 +87,7 @@ export const queueStore = {
this.unqueue(songs) this.unqueue(songs)
const head = this.all.splice(0, this.indexOf(this.current) + 1) 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[]) { unqueue (songs: Song | Song[]) {
@ -104,7 +103,7 @@ export const queueStore = {
movedSongs.forEach(song => { movedSongs.forEach(song => {
this.all.splice(this.indexOf(song), 1) 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) { indexOf (song: Song) {
return this.all.indexOf(song) return this.all.indexOf(reactive(song))
}, },
get next () { get next () {