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

359 lines
9.6 KiB
JavaScript
Raw Normal View History

2015-12-13 04:42:28 +00:00
import _ from 'lodash';
import $ from 'jquery';
2016-01-25 10:55:00 +00:00
import sharedStore from '../stores/shared';
2015-12-13 04:42:28 +00:00
import queueStore from '../stores/queue';
import songStore from '../stores/song';
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-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;
plyr.setup({
controls: [],
});
this.player = $('.player')[0].plyr;
this.$volumeInput = $('#volumeRange');
/**
* Listen to 'error' event on the audio player and play the next song if any.
* We don't care about network error capturing here, since every play() call
* comes with a POST to api/interaction/play. If the user is not logged in anymore,
* this call will result in a 401, following by a redirection to our login page.
*/
this.player.media.addEventListener('error', e => {
this.playNext();
});
/**
* 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());
});
// Listen to 'ended' event on the audio player and play the next song in the queue.
this.player.media.addEventListener('ended', e => {
2015-12-20 12:17:35 +00:00
songStore.scrobble(queueStore.current());
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();
});
// 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
*
* 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
*
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;
}
// Set the song as the current song
queueStore.current(song);
2016-01-25 10:55:00 +00:00
this.player.source(`${sharedStore.state.cdnUrl}/api/${song.id}/play?jwt-token=${ls.get('jwt-token')}`);
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
$('title').text(`${song.title} ♫ Koel`);
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) {
// Notification fails.
// @link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification
}
2015-12-13 04:42:28 +00:00
},
/**
* Get the next song in the queue.
* 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;
if (i >= this.repeatModes.length) {
i = 0;
}
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() {
// 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 && this.player.media.duration > 5) {
this.player.seek(0);
return;
}
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-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-01-07 09:03:38 +00:00
this.app.$broadcast('song:stopped');
2015-12-13 04:42:28 +00:00
},
/**
* Pause playback.
*/
pause() {
this.player.pause();
2016-01-07 09:03:38 +00:00
this.app.$broadcast('song:paused', queueStore.current());
2015-12-13 04:42:28 +00:00
},
/**
* Resume playback.
*/
resume() {
this.player.play();
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.
2015-12-14 13:13:12 +00:00
Vue.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());
},
/**
* Play all songs by an artist.
*
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
*/
playAllByArtist(artist, shuffle = true) {
this.queueAndPlay(artistStore.getSongsByArtist(artist), true);
},
/**
* Play all songs in an album.
*
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
*/
playAllInAlbum(album, shuffle = true) {
this.queueAndPlay(album.songs, true);
},
2015-12-13 04:42:28 +00:00
};