koel/resources/assets/js/services/playback.js

396 lines
9.3 KiB
JavaScript
Raw Normal View History

2016-11-26 03:25:35 +00:00
import { shuffle, orderBy } from 'lodash'
import $ from 'jquery'
import plyr from 'plyr'
import Vue from 'vue'
2015-12-13 04:42:28 +00:00
2016-11-26 03:25:35 +00:00
import { event } from '../utils'
import { queueStore, sharedStore, userStore, songStore, preferenceStore as preferences } from '../stores'
import config from '../config'
import router from '../router'
2015-12-13 04:42:28 +00:00
2016-06-25 10:15:57 +00:00
export const playback = {
2016-06-25 16:05:24 +00:00
player: null,
$volumeInput: null,
repeatModes: ['NO_REPEAT', 'REPEAT_ALL', 'REPEAT_ONE'],
initialized: false,
/**
* Initialize the playback service for this whole Koel app.
*/
2016-11-26 03:25:35 +00:00
init () {
2016-06-25 16:05:24 +00:00
// We don't need to init this service twice, or the media events will be duplicated.
if (this.initialized) {
2016-11-26 03:25:35 +00:00
return
2016-06-25 16:05:24 +00:00
}
this.player = plyr.setup({
2016-11-26 03:25:35 +00:00
controls: []
})[0]
2016-06-25 16:05:24 +00:00
2016-11-26 03:25:35 +00:00
this.audio = $('audio')
2016-06-25 16:05:24 +00:00
2016-11-26 03:25:35 +00:00
this.$volumeInput = $('#volumeRange')
2015-12-13 04:42:28 +00:00
/**
2016-06-25 16:05:24 +00:00
* Listen to 'error' event on the audio player and play the next song if any.
2015-12-13 04:42:28 +00:00
*/
2016-06-25 16:05:24 +00:00
document.querySelector('.plyr').addEventListener('error', e => {
2016-11-26 03:25:35 +00:00
this.playNext()
}, true)
2015-12-13 04:42:28 +00:00
/**
2016-06-25 16:05:24 +00:00
* Listen to 'ended' event on the audio player and play the next song in the queue.
2015-12-13 04:42:28 +00:00
*/
2016-06-25 16:05:24 +00:00
document.querySelector('.plyr').addEventListener('ended', e => {
2016-06-27 06:11:35 +00:00
if (sharedStore.state.useLastfm && userStore.current.preferences.lastfm_session_key) {
2016-11-26 03:25:35 +00:00
songStore.scrobble(queueStore.current)
2016-06-27 06:11:35 +00:00
}
2015-12-13 04:42:28 +00:00
2016-06-25 16:05:24 +00:00
if (preferences.repeatMode === 'REPEAT_ONE') {
2016-11-26 03:25:35 +00:00
this.restart()
2015-12-13 04:42:28 +00:00
2016-11-26 03:25:35 +00:00
return
2016-06-25 16:05:24 +00:00
}
2015-12-13 04:42:28 +00:00
2016-11-26 03:25:35 +00:00
this.playNext()
})
2015-12-13 04:42:28 +00:00
/**
2016-06-25 16:05:24 +00:00
* Attempt to preload the next song if the current song is about to end.
2015-12-13 04:42:28 +00:00
*/
2016-06-25 16:05:24 +00:00
document.querySelector('.plyr').addEventListener('timeupdate', e => {
if (!this.player.media.duration || this.player.media.currentTime + 10 < this.player.media.duration) {
2016-11-26 03:25:35 +00:00
return
2016-06-25 16:05:24 +00:00
}
2015-12-13 04:42:28 +00:00
2016-06-25 16:05:24 +00:00
// The current song has only 10 seconds left to play.
2016-11-26 03:25:35 +00:00
const nextSong = queueStore.next
2016-06-25 16:05:24 +00:00
if (!nextSong || nextSong.preloaded) {
2016-11-26 03:25:35 +00:00
return
2016-06-25 16:05:24 +00:00
}
2016-11-26 03:25:35 +00:00
const $preloader = $('<audio>')
$preloader.attr('src', songStore.getSourceUrl(nextSong))
2015-12-13 04:42:28 +00:00
2016-11-26 03:25:35 +00:00
nextSong.preloaded = true
})
2015-12-13 04:42:28 +00:00
/**
2016-06-25 16:05:24 +00:00
* Listen to 'input' event on the volume range control.
* When user drags the volume control, this event will be triggered, and we
* update the volume on the plyr object.
2015-12-13 04:42:28 +00:00
*/
2016-06-25 16:05:24 +00:00
this.$volumeInput.on('input', e => {
2016-11-26 03:25:35 +00:00
this.setVolume($(e.target).val())
})
2016-06-25 16:05:24 +00:00
// On init, set the volume to the value found in the local storage.
2016-11-26 03:25:35 +00:00
this.setVolume(preferences.volume)
2016-06-25 16:05:24 +00:00
// Init the equalizer if supported.
2016-11-26 03:25:35 +00:00
event.emit('equalizer:init', this.player.media)
2016-06-25 16:05:24 +00:00
2016-11-26 03:25:35 +00:00
this.initialized = true
2016-06-25 16:05:24 +00:00
},
/**
* Play a song. Because
*
* So many adventures couldn't happen today,
* So many songs we forgot to play
* So many dreams swinging out of the blue
* We'll let them come true
*
* @param {Object} song The song to play
*/
2016-11-26 03:25:35 +00:00
play (song) {
2016-06-25 16:05:24 +00:00
if (!song) {
2016-11-26 03:25:35 +00:00
return
2016-06-25 16:05:24 +00:00
}
if (queueStore.current) {
2016-11-26 03:25:35 +00:00
queueStore.current.playbackState = 'stopped'
2016-06-25 16:05:24 +00:00
}
2016-11-26 03:25:35 +00:00
song.playbackState = 'playing'
2016-06-25 16:05:24 +00:00
// Set the song as the current song
2016-11-26 03:25:35 +00:00
queueStore.current = song
2016-06-25 16:05:24 +00:00
// Add it into the "recent" list
2016-11-26 03:25:35 +00:00
songStore.addRecent(song)
2016-06-25 16:05:24 +00:00
// Manually set the `src` attribute of the audio to prevent plyr from resetting
// the audio media object and cause our equalizer to malfunction.
2016-11-26 03:25:35 +00:00
this.player.media.src = songStore.getSourceUrl(song)
2016-06-25 16:05:24 +00:00
2016-11-26 03:25:35 +00:00
$('title').text(`${song.title}${config.appTitle}`)
$('.plyr audio').attr('title', `${song.artist.name} - ${song.title}`)
2016-06-25 16:05:24 +00:00
// We'll just "restart" playing the song, which will handle notification, scrobbling etc.
2016-11-26 03:25:35 +00:00
this.restart()
2016-06-25 16:05:24 +00:00
},
/**
* Restart playing a song.
*/
2016-11-26 03:25:35 +00:00
restart () {
const song = queueStore.current
2016-06-25 16:05:24 +00:00
// Record the UNIX timestamp the song start playing, for scrobbling purpose
2016-11-26 03:25:35 +00:00
song.playStartTime = Math.floor(Date.now() / 1000)
2016-06-25 16:05:24 +00:00
2016-11-26 03:25:35 +00:00
event.emit('song:played', song)
2016-06-25 16:05:24 +00:00
2016-11-26 03:25:35 +00:00
this.player.restart()
this.player.play()
2016-06-25 16:05:24 +00:00
// Register the play to the server
2016-11-26 03:25:35 +00:00
songStore.registerPlay(song)
2016-06-25 16:05:24 +00:00
// Show the notification if we're allowed to
if (!window.Notification || !preferences.notify) {
2016-11-26 03:25:35 +00:00
return
2016-06-25 16:05:24 +00:00
}
try {
2016-11-26 03:25:35 +00:00
const notif = new window.Notification(`${song.title}`, {
2016-06-25 16:05:24 +00:00
icon: song.album.cover,
body: `${song.album.name} ${song.artist.name}`
2016-11-26 03:25:35 +00:00
})
2016-06-25 16:05:24 +00:00
2016-11-26 03:25:35 +00:00
notif.onclick = () => window.focus()
2016-06-25 16:05:24 +00:00
// Close the notif after 5 secs.
2016-11-26 03:25:35 +00:00
window.setTimeout(() => notif.close(), 5000)
2016-06-25 16:05:24 +00:00
} catch (e) {
// Notification fails.
// @link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification
}
},
/**
* The next song in the queue.
* If we're in REPEAT_ALL mode and there's no next song, just get the first song.
*
* @return {Object} The song
*/
2016-11-26 03:25:35 +00:00
get next () {
const next = queueStore.next
2016-06-25 16:05:24 +00:00
if (next) {
2016-11-26 03:25:35 +00:00
return next
2016-06-25 16:05:24 +00:00
}
if (preferences.repeatMode === 'REPEAT_ALL') {
2016-11-26 03:25:35 +00:00
return queueStore.first
2016-06-25 16:05:24 +00:00
}
},
/**
* The previous song in the queue.
* If we're in REPEAT_ALL mode and there's no prev song, get the last song.
*
* @return {Object} The song
*/
2016-11-26 03:25:35 +00:00
get previous () {
const prev = queueStore.previous
2016-06-25 16:05:24 +00:00
if (prev) {
2016-11-26 03:25:35 +00:00
return prev
2016-06-25 16:05:24 +00:00
}
if (preferences.repeatMode === 'REPEAT_ALL') {
2016-11-26 03:25:35 +00:00
return queueStore.last
2016-06-25 16:05:24 +00:00
}
},
/**
* Circle through the repeat mode.
* The selected mode will be stored into local storage as well.
*/
2016-11-26 03:25:35 +00:00
changeRepeatMode () {
let idx = this.repeatModes.indexOf(preferences.repeatMode) + 1
2016-06-25 16:05:24 +00:00
if (idx >= this.repeatModes.length) {
2016-11-26 03:25:35 +00:00
idx = 0
2016-06-25 16:05:24 +00:00
}
2016-11-26 03:25:35 +00:00
preferences.repeatMode = this.repeatModes[idx]
2016-06-25 16:05:24 +00:00
},
/**
* 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.
*/
2016-11-26 03:25:35 +00:00
playPrev () {
2016-06-25 16:05:24 +00:00
// If the song's duration is greater than 5 seconds and we've passed 5 seconds into it,
// restart playing instead.
if (this.player.media.currentTime > 5 && queueStore.current.length > 5) {
2016-11-26 03:25:35 +00:00
this.player.restart()
2016-06-25 16:05:24 +00:00
2016-11-26 03:25:35 +00:00
return
2016-06-25 16:05:24 +00:00
}
2016-11-26 03:25:35 +00:00
const prev = this.previous
2016-06-25 16:05:24 +00:00
if (!prev && preferences.repeatMode === 'NO_REPEAT') {
2016-11-26 03:25:35 +00:00
this.stop()
2016-06-25 16:05:24 +00:00
2016-11-26 03:25:35 +00:00
return
2016-06-25 16:05:24 +00:00
}
2016-11-26 03:25:35 +00:00
this.play(prev)
2016-06-25 16:05:24 +00:00
},
/**
* 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.
*/
2016-11-26 03:25:35 +00:00
playNext () {
const next = this.next
2016-06-25 16:05:24 +00:00
if (!next && preferences.repeatMode === 'NO_REPEAT') {
// Nothing lasts forever, even cold November rain.
2016-11-26 03:25:35 +00:00
this.stop()
2016-06-25 16:05:24 +00:00
2016-11-26 03:25:35 +00:00
return
2016-06-25 16:05:24 +00:00
}
2016-11-26 03:25:35 +00:00
this.play(next)
2016-06-25 16:05:24 +00:00
},
/**
* Set the volume level.
*
* @param {Number} volume 0-10
* @param {Boolean=true} persist Whether the volume should be saved into local storage
*/
2016-11-26 03:25:35 +00:00
setVolume (volume, persist = true) {
this.player.setVolume(volume)
2016-06-25 16:05:24 +00:00
if (persist) {
2016-11-26 03:25:35 +00:00
preferences.volume = volume
2016-06-25 16:05:24 +00:00
}
2016-11-26 03:25:35 +00:00
this.$volumeInput.val(volume)
2016-06-25 16:05:24 +00:00
},
/**
* Mute playback.
*/
2016-11-26 03:25:35 +00:00
mute () {
this.setVolume(0, false)
2016-06-25 16:05:24 +00:00
},
/**
* Unmute playback.
*/
2016-11-26 03:25:35 +00:00
unmute () {
2016-06-25 16:05:24 +00:00
// If the saved volume is 0, we unmute to the default level (7).
if (preferences.volume === '0' || preferences.volume === 0) {
2016-11-26 03:25:35 +00:00
preferences.volume = 7
2016-06-25 16:05:24 +00:00
}
2016-11-26 03:25:35 +00:00
this.setVolume(preferences.volume)
2016-06-25 16:05:24 +00:00
},
/**
* Completely stop playback.
*/
2016-11-26 03:25:35 +00:00
stop () {
$('title').text(config.appTitle)
this.player.pause()
this.player.seek(0)
2016-06-25 16:05:24 +00:00
if (queueStore.current) {
2016-11-26 03:25:35 +00:00
queueStore.current.playbackState = 'stopped'
2016-06-25 16:05:24 +00:00
}
},
/**
* Pause playback.
*/
2016-11-26 03:25:35 +00:00
pause () {
this.player.pause()
queueStore.current.playbackState = 'paused'
2016-06-25 16:05:24 +00:00
},
/**
* Resume playback.
*/
2016-11-26 03:25:35 +00:00
resume () {
this.player.play()
queueStore.current.playbackState = 'playing'
event.emit('song:played', queueStore.current)
2016-06-25 16:05:24 +00:00
},
/**
* Queue up songs (replace them into the queue) and start playing right away.
*
* @param {?Array.<Object>} songs An array of song objects. Defaults to all songs if null.
* @param {Boolean=false} shuffled Whether to shuffle the songs before playing.
*/
2016-11-26 03:25:35 +00:00
queueAndPlay (songs = null, shuffled = false) {
2016-06-25 16:05:24 +00:00
if (!songs) {
2016-11-26 03:25:35 +00:00
songs = songStore.all
2016-06-25 16:05:24 +00:00
}
if (!songs.length) {
2016-11-26 03:25:35 +00:00
return
2016-06-25 16:05:24 +00:00
}
if (shuffled) {
2016-11-26 03:25:35 +00:00
songs = shuffle(songs)
2016-06-25 16:05:24 +00:00
}
2016-11-26 03:25:35 +00:00
queueStore.queue(songs, true)
2016-06-25 16:05:24 +00:00
// Wrap this inside a nextTick() to wait for the DOM to complete updating
// and then play the first song in the queue.
2016-07-10 17:55:20 +00:00
Vue.nextTick(() => {
2016-11-26 03:25:35 +00:00
router.go('queue')
this.play(queueStore.first)
})
2016-06-25 16:05:24 +00:00
},
/**
* Play the first song in the queue.
* If the current queue is empty, try creating it by shuffling all songs.
*/
2016-11-26 03:25:35 +00:00
playFirstInQueue () {
2016-06-25 16:05:24 +00:00
if (!queueStore.all.length) {
2016-11-26 03:25:35 +00:00
this.queueAndPlay()
2016-06-25 16:05:24 +00:00
2016-11-26 03:25:35 +00:00
return
2016-06-25 16:05:24 +00:00
}
2016-11-26 03:25:35 +00:00
this.play(queueStore.first)
2016-06-25 16:05:24 +00:00
},
/**
* Play all songs by an artist.
*
* @param {Object} artist The artist object
* @param {Boolean=true} shuffled Whether to shuffle the songs
*/
2016-11-26 03:25:35 +00:00
playAllByArtist (artist, shuffled = true) {
this.queueAndPlay(artist.songs, shuffled)
2016-06-25 16:05:24 +00:00
},
/**
* Play all songs in an album.
*
* @param {Object} album The album object
* @param {Boolean=true} shuffled Whether to shuffle the songs
*/
2016-11-26 03:25:35 +00:00
playAllInAlbum (album, shuffled = true) {
2016-06-25 16:05:24 +00:00
if (!shuffled) {
2016-11-26 03:25:35 +00:00
this.queueAndPlay(orderBy(album.songs, 'track'))
return
2016-06-25 16:05:24 +00:00
}
2016-11-26 03:25:35 +00:00
this.queueAndPlay(album.songs, true)
}
}