mirror of
https://github.com/koel/koel
synced 2024-11-10 14:44:13 +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) {
|
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
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 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()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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 () {
|
||||||
|
|
Loading…
Reference in a new issue