From df94ae37bf2c3f6f5fe090cf9a06a6746529d024 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sat, 23 Dec 2017 16:41:19 +0100 Subject: [PATCH] Now use vuex to manage state for player/queue/radios --- front/src/audio/index.js | 184 ------------ front/src/audio/queue.js | 332 ---------------------- front/src/components/Sidebar.vue | 36 +-- front/src/components/audio/PlayButton.vue | 11 +- front/src/components/audio/Player.vue | 160 ++++++----- front/src/components/audio/Search.vue | 4 +- front/src/components/audio/Track.vue | 104 +++++++ front/src/components/audio/album/Card.vue | 2 - front/src/components/radios/Button.vue | 17 +- front/src/components/radios/Card.vue | 3 +- front/src/main.js | 2 + front/src/radios/index.js | 64 ----- front/src/store/index.js | 16 ++ front/src/store/player.js | 91 ++++++ front/src/store/queue.js | 153 ++++++++++ front/src/store/radios.js | 78 +++++ 16 files changed, 563 insertions(+), 694 deletions(-) delete mode 100644 front/src/audio/index.js delete mode 100644 front/src/audio/queue.js create mode 100644 front/src/components/audio/Track.vue delete mode 100644 front/src/radios/index.js create mode 100644 front/src/store/index.js create mode 100644 front/src/store/player.js create mode 100644 front/src/store/queue.js create mode 100644 front/src/store/radios.js diff --git a/front/src/audio/index.js b/front/src/audio/index.js deleted file mode 100644 index 4896b83b0..000000000 --- a/front/src/audio/index.js +++ /dev/null @@ -1,184 +0,0 @@ -import logger from '@/logging' -import time from '@/utils/time' - -const Cov = { - on (el, type, func) { - el.addEventListener(type, func) - }, - off (el, type, func) { - el.removeEventListener(type, func) - } -} - -class Audio { - constructor (src, options = {}) { - let preload = true - if (options.preload !== undefined && options.preload === false) { - preload = false - } - this.tmp = { - src: src, - options: options - } - this.onEnded = function (e) { - logger.default.info('track ended') - } - if (options.onEnded) { - this.onEnded = options.onEnded - } - this.onError = options.onError - - this.state = { - preload: preload, - startLoad: false, - failed: false, - try: 3, - tried: 0, - playing: false, - paused: false, - playbackRate: 1.0, - progress: 0, - currentTime: 0, - volume: 0.5, - duration: 0, - loaded: '0', - durationTimerFormat: '00:00', - currentTimeFormat: '00:00', - lastTimeFormat: '00:00' - } - if (options.volume !== undefined) { - this.state.volume = options.volume - } - this.hook = { - playState: [], - loadState: [] - } - if (preload) { - this.init(src, options) - } - } - - init (src, options = {}) { - if (!src) throw Error('src must be required') - this.state.startLoad = true - if (this.state.tried >= this.state.try) { - this.state.failed = true - logger.default.error('Cannot fetch audio', src) - if (this.onError) { - this.onError(src) - } - return - } - this.$Audio = new window.Audio(src) - Cov.on(this.$Audio, 'error', () => { - this.state.tried++ - this.init(src, options) - }) - if (options.autoplay) { - this.play() - } - if (options.rate) { - this.$Audio.playbackRate = options.rate - } - if (options.loop) { - this.$Audio.loop = true - } - if (options.volume) { - this.setVolume(options.volume) - } - this.loadState() - } - - loadState () { - if (this.$Audio.readyState >= 2) { - Cov.on(this.$Audio, 'progress', this.updateLoadState.bind(this)) - } else { - Cov.on(this.$Audio, 'loadeddata', () => { - this.loadState() - }) - } - } - - updateLoadState (e) { - if (!this.$Audio) return - this.hook.loadState.forEach(func => { - func(this.state) - }) - this.state.duration = Math.round(this.$Audio.duration * 100) / 100 - this.state.loaded = Math.round(10000 * this.$Audio.buffered.end(0) / this.$Audio.duration) / 100 - this.state.durationTimerFormat = time.parse(this.state.duration) - } - - updatePlayState (e) { - this.state.currentTime = Math.round(this.$Audio.currentTime * 100) / 100 - this.state.duration = Math.round(this.$Audio.duration * 100) / 100 - this.state.progress = Math.round(10000 * this.state.currentTime / this.state.duration) / 100 - - this.state.durationTimerFormat = time.parse(this.state.duration) - this.state.currentTimeFormat = time.parse(this.state.currentTime) - this.state.lastTimeFormat = time.parse(this.state.duration - this.state.currentTime) - - this.hook.playState.forEach(func => { - func(this.state) - }) - } - - updateHook (type, func) { - if (!(type in this.hook)) throw Error('updateHook: type should be playState or loadState') - this.hook[type].push(func) - } - - play () { - if (this.state.startLoad) { - if (!this.state.playing && this.$Audio.readyState >= 2) { - logger.default.info('Playing track') - this.$Audio.play() - this.state.paused = false - this.state.playing = true - Cov.on(this.$Audio, 'timeupdate', this.updatePlayState.bind(this)) - Cov.on(this.$Audio, 'ended', this.onEnded) - } else { - Cov.on(this.$Audio, 'loadeddata', () => { - this.play() - }) - } - } else { - this.init(this.tmp.src, this.tmp.options) - Cov.on(this.$Audio, 'loadeddata', () => { - this.play() - }) - } - } - - destroyed () { - this.$Audio.pause() - Cov.off(this.$Audio, 'timeupdate', this.updatePlayState) - Cov.off(this.$Audio, 'progress', this.updateLoadState) - Cov.off(this.$Audio, 'ended', this.onEnded) - this.$Audio.remove() - } - - pause () { - logger.default.info('Pausing track') - this.$Audio.pause() - this.state.paused = true - this.state.playing = false - this.$Audio.removeEventListener('timeupdate', this.updatePlayState) - } - - setVolume (number) { - if (number > -0.01 && number <= 1) { - this.state.volume = Math.round(number * 100) / 100 - this.$Audio.volume = this.state.volume - } - } - - setTime (time) { - if (time < 0 && time > this.state.duration) { - return false - } - this.$Audio.currentTime = time - } -} - -export default Audio diff --git a/front/src/audio/queue.js b/front/src/audio/queue.js deleted file mode 100644 index 4273fb9a6..000000000 --- a/front/src/audio/queue.js +++ /dev/null @@ -1,332 +0,0 @@ -import Vue from 'vue' -import _ from 'lodash' - -import logger from '@/logging' -import cache from '@/cache' -import config from '@/config' -import Audio from '@/audio' -import backend from '@/audio/backend' -import radios from '@/radios' -import url from '@/utils/url' -import auth from '@/auth' - -class Queue { - constructor (options = {}) { - logger.default.info('Instanciating queue') - this.previousQueue = cache.get('queue') - this.tracks = [] - this.currentIndex = -1 - this.currentTrack = null - this.ended = true - this.state = { - looping: 0, // 0 -> no, 1 -> on track, 2 -> on queue - volume: cache.get('volume', 0.5) - } - this.audio = { - state: { - startLoad: false, - failed: false, - try: 3, - tried: 0, - playing: false, - paused: false, - playbackRate: 1.0, - progress: 0, - currentTime: 0, - duration: 0, - volume: this.state.volume, - loaded: '0', - durationTimerFormat: '00:00', - currentTimeFormat: '00:00', - lastTimeFormat: '00:00' - } - } - } - - cache () { - let cached = { - tracks: this.tracks.map(track => { - // we keep only valuable fields to make the cache lighter and avoid - // cyclic value serialization errors - let artist = { - id: track.artist.id, - mbid: track.artist.mbid, - name: track.artist.name - } - return { - id: track.id, - title: track.title, - mbid: track.mbid, - album: { - id: track.album.id, - title: track.album.title, - mbid: track.album.mbid, - cover: track.album.cover, - artist: artist - }, - artist: artist, - files: track.files - } - }), - currentIndex: this.currentIndex - } - cache.set('queue', cached) - } - - restore () { - let cached = cache.get('queue') - if (!cached) { - return false - } - logger.default.info('Restoring previous queue...') - this.tracks = cached.tracks - this.play(cached.currentIndex) - this.previousQueue = null - return true - } - removePrevious () { - this.previousQueue = undefined - cache.remove('queue') - } - setVolume (newValue) { - newValue = Math.min(newValue, 1) - newValue = Math.max(newValue, 0) - this.state.volume = newValue - if (this.audio.setVolume) { - this.audio.setVolume(newValue) - } else { - this.audio.state.volume = newValue - } - cache.set('volume', newValue) - } - incrementVolume (value) { - this.setVolume(this.state.volume + value) - } - reorder (oldIndex, newIndex) { - // called when the user uses drag / drop to reorder - // tracks in queue - if (oldIndex === this.currentIndex) { - this.currentIndex = newIndex - return - } - if (oldIndex < this.currentIndex && newIndex >= this.currentIndex) { - // item before was moved after - this.currentIndex -= 1 - } - if (oldIndex > this.currentIndex && newIndex <= this.currentIndex) { - // item after was moved before - this.currentIndex += 1 - } - } - - append (track, index, skipPlay) { - this.previousQueue = null - index = index || this.tracks.length - if (index > this.tracks.length - 1) { - // we simply push to the end - this.tracks.push(track) - } else { - // we insert the track at given position - this.tracks.splice(index, 0, track) - } - if (!skipPlay) { - this.resumeQueue() - } - this.cache() - } - - appendMany (tracks, index) { - logger.default.info('Appending many tracks to the queue', tracks.map(e => { return e.title })) - let self = this - if (this.tracks.length === 0) { - index = 0 - } else { - index = index || this.tracks.length - } - tracks.forEach((t) => { - self.append(t, index, true) - index += 1 - }) - this.resumeQueue() - } - - resumeQueue () { - if (this.ended | this.errored) { - this.next() - } - } - - populateFromRadio () { - if (!radios.running) { - return - } - var self = this - radios.fetch().then((response) => { - logger.default.info('Adding track to queue from radio') - self.append(response.data.track) - }, (response) => { - logger.default.error('Error while adding track to queue from radio') - }) - } - - clean () { - this.stop() - radios.stop() - this.tracks = [] - this.currentIndex = -1 - this.currentTrack = null - // so we replay automatically on next track append - this.ended = true - } - - cleanTrack (index) { - // are we removing current playin track - let current = index === this.currentIndex - if (current) { - this.stop() - } - if (index < this.currentIndex) { - this.currentIndex -= 1 - } - this.tracks.splice(index, 1) - if (current) { - // we play next track, which now have the same index - this.play(index) - } - if (this.currentIndex === this.tracks.length - 1) { - this.populateFromRadio() - } - } - - stop () { - if (this.audio.pause) { - this.audio.pause() - } - if (this.audio.destroyed) { - this.audio.destroyed() - } - } - play (index) { - let self = this - let currentIndex = index - let currentTrack = this.tracks[index] - - if (this.audio.destroyed) { - logger.default.debug('Destroying previous audio...', index - 1) - this.audio.destroyed() - } - - if (!currentTrack) { - return - } - - this.currentIndex = currentIndex - this.currentTrack = currentTrack - - this.ended = false - this.errored = false - let file = this.currentTrack.files[0] - if (!file) { - this.errored = true - return this.next() - } - let path = backend.absoluteUrl(file.path) - if (auth.user.authenticated) { - // we need to send the token directly in url - // so authentication can be checked by the backend - // because for audio files we cannot use the regular Authentication - // header - path = url.updateQueryString(path, 'jwt', auth.getAuthToken()) - } - - let audio = new Audio(path, { - preload: true, - autoplay: true, - rate: 1, - loop: false, - volume: this.state.volume, - onEnded: this.handleAudioEnded.bind(this), - onError: function (src) { - self.errored = true - self.next() - } - }) - this.audio = audio - audio.updateHook('playState', function (e) { - // in some situations, we may have a race condition, for example - // if the user spams the next / previous buttons, with multiple audios - // playing at the same time. To avoid that, we ensure the audio - // still matches de queue current audio - if (audio !== self.audio) { - logger.default.debug('Destroying duplicate audio') - audio.destroyed() - } - }) - if (this.currentIndex === this.tracks.length - 1) { - this.populateFromRadio() - } - this.cache() - } - - handleAudioEnded (e) { - this.recordListen(this.currentTrack) - if (this.state.looping === 1) { - // we loop on the same track - logger.default.info('Looping on the same track') - return this.play(this.currentIndex) - } - if (this.currentIndex < this.tracks.length - 1) { - logger.default.info('Audio track ended, playing next one') - return this.next() - } else { - logger.default.info('We reached the end of the queue') - if (this.state.looping === 2) { - logger.default.info('Going back to the beginning of the queue') - return this.play(0) - } else { - this.ended = true - } - } - } - - recordListen (track) { - let url = config.API_URL + 'history/listenings/' - let resource = Vue.resource(url) - resource.save({}, {'track': track.id}).then((response) => {}, (response) => { - logger.default.error('Could not record track in history') - }) - } - - previous () { - if (this.currentIndex > 0) { - this.play(this.currentIndex - 1) - } - } - - next () { - if (this.currentIndex < this.tracks.length - 1) { - logger.default.debug('Playing next track') - this.play(this.currentIndex + 1) - } - } - - toggleLooping () { - if (this.state.looping > 1) { - this.state.looping = 0 - } else { - this.state.looping += 1 - } - } - - shuffle () { - let tracks = this.tracks - let shuffled = _.shuffle(tracks) - this.clean() - this.appendMany(shuffled) - } - -} - -let queue = new Queue() - -export default queue diff --git a/front/src/components/Sidebar.vue b/front/src/components/Sidebar.vue index 68927a37b..9112c2588 100644 --- a/front/src/components/Sidebar.vue +++ b/front/src/components/Sidebar.vue @@ -51,7 +51,7 @@
- +
{{ index + 1}} @@ -63,23 +63,23 @@ + - +
-
+
You have a radio playing

New tracks will be appended here automatically.

-
Stop radio
+
Stop radio
@@ -87,24 +87,19 @@
-
+ + + diff --git a/front/src/components/audio/album/Card.vue b/front/src/components/audio/album/Card.vue index ce5e832e2..4c803b29c 100644 --- a/front/src/components/audio/album/Card.vue +++ b/front/src/components/audio/album/Card.vue @@ -51,7 +51,6 @@