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