From b757ca4616178c0192f5e866f54282339626be2a Mon Sep 17 00:00:00 2001
From: Eliot Berriot <contact@eliotberriot.com>
Date: Fri, 26 Oct 2018 15:21:35 +0200
Subject: [PATCH] See #272: updated front-end for transcoding and new API
 results, improved error handling in player

---
 front/src/components/audio/PlayButton.vue  | 10 +++++--
 front/src/components/audio/Player.vue      | 23 ++++++++++++++--
 front/src/components/audio/Track.vue       | 32 ++++++++++++++++------
 front/src/components/audio/track/Row.vue   |  4 +--
 front/src/components/library/Track.vue     | 27 ++++++++++--------
 front/src/store/index.js                   |  1 +
 front/src/store/player.js                  | 21 ++++++++++++--
 front/src/store/queue.js                   |  1 -
 front/tests/unit/specs/store/queue.spec.js |  3 --
 9 files changed, 87 insertions(+), 35 deletions(-)

diff --git a/front/src/components/audio/PlayButton.vue b/front/src/components/audio/PlayButton.vue
index dcb1c507e..521a66f87 100644
--- a/front/src/components/audio/PlayButton.vue
+++ b/front/src/components/audio/PlayButton.vue
@@ -79,10 +79,14 @@ export default {
         return true
       }
       if (this.track) {
-        return this.track.is_playable
+        return this.track.uploads && this.track.uploads.length > 0
+      } else if (this.artist) {
+        return this.albums.filter((a) => {
+          return a.is_playable === true
+        }).length > 0
       } else if (this.tracks) {
         return this.tracks.filter((t) => {
-          return t.is_playable
+          return t.uploads && t.uploads.length > 0
         }).length > 0
       }
       return false
@@ -139,7 +143,7 @@ export default {
           self.isLoading = false
         }, 250)
         return tracks.filter(e => {
-          return e.is_playable === true
+          return e.uploads && e.uploads.length > 0
         })
       })
     },
diff --git a/front/src/components/audio/Player.vue b/front/src/components/audio/Player.vue
index 1d622f330..80c54bbb2 100644
--- a/front/src/components/audio/Player.vue
+++ b/front/src/components/audio/Player.vue
@@ -4,6 +4,7 @@
       <audio-track
         ref="currentAudio"
         v-if="currentTrack"
+        @errored="handleError"
         :is-current="true"
         :start-time="$store.state.player.currentTime"
         :autoplay="$store.state.player.playing"
@@ -41,13 +42,13 @@
           </div>
         </div>
       </div>
-      <div class="progress-area" v-if="currentTrack">
+      <div class="progress-area" v-if="currentTrack && !errored">
         <div class="ui grid">
           <div class="left floated four wide column">
             <p class="timer start" @click="updateProgress(0)">{{currentTimeFormatted}}</p>
           </div>
 
-          <div class="right floated four wide column">
+          <div v-if="!isLoadingAudio" class="right floated four wide column">
             <p class="timer total">{{durationFormatted}}</p>
           </div>
         </div>
@@ -59,7 +60,18 @@
           <div class="bar" :data-percent="progress" :style="{ 'width': progress + '%' }"></div>
         </div>
       </div>
-
+      <div class="ui small warning message" v-if="currentTrack && errored">
+        <div class="header">
+          <translate>We cannot load this track</translate>
+        </div>
+        <p v-if="hasNext && playing && $store.state.player.errorCount < $store.state.player.maxConsecutiveErrors">
+          <translate>The next track will play automatically in a few seconds...</translate>
+          <i class="loading spinner icon"></i>
+        </p>
+        <p>
+          <translate>You may have a connectivity issue.</translate>
+        </p>
+      </div>
       <div class="two wide column controls ui grid">
         <a
           href
@@ -299,6 +311,10 @@ export default {
       }
       let image = this.$refs.cover
       this.ambiantColors = ColorThief.prototype.getPalette(image, 4).slice(0, 4)
+    },
+    handleError ({sound, error}) {
+      this.$store.commit('player/isLoadingAudio', false)
+      this.$store.dispatch('player/trackErrored')
     }
   },
   computed: {
@@ -310,6 +326,7 @@ export default {
       looping: state => state.player.looping,
       duration: state => state.player.duration,
       bufferProgress: state => state.player.bufferProgress,
+      errored: state => state.player.errored,
       queue: state => state.queue
     }),
     ...mapGetters({
diff --git a/front/src/components/audio/Track.vue b/front/src/components/audio/Track.vue
index 32eb62722..c847d4de1 100644
--- a/front/src/components/audio/Track.vue
+++ b/front/src/components/audio/Track.vue
@@ -46,12 +46,17 @@ export default {
       onload: function () {
         self.$store.commit('player/isLoadingAudio', false)
         self.$store.commit('player/resetErrorCount')
+        self.$store.commit('player/errored', false)
         self.$store.commit('player/duration', self.sound.duration())
         let node = self.sound._sounds[0]._node;
         node.addEventListener('progress', () => {
           self.updateBuffer(node)
         })
-      }
+      },
+      onloaderror: function (sound, error) {
+        console.log('Error while playing:', sound, error)
+        self.$emit('errored', {sound, error})
+      },
     })
     if (this.autoplay) {
       self.$store.commit('player/isLoadingAudio', true)
@@ -73,14 +78,23 @@ export default {
       looping: state => state.player.looping
     }),
     srcs: function () {
-      // let file = this.track.files[0]
-      // if (!file) {
-      //   this.$store.dispatch('player/trackErrored')
-      //   return []
-      // }
-      let sources = [
-        {type: 'mp3', url: this.$store.getters['instance/absoluteUrl'](this.track.listen_url)}
-      ]
+      let sources = this.track.uploads.map(u => {
+        return {
+          type: u.extension,
+          url: this.$store.getters['instance/absoluteUrl'](u.listen_url),
+        }
+      })
+      // We always add a transcoded MP3 src at the end
+      // because transcoding is expensive, but we want browsers that do
+      // not support other codecs to be able to play it :)
+      sources.push({
+        type: 'mp3',
+        url: url.updateQueryString(
+          this.$store.getters['instance/absoluteUrl'](this.track.listen_url),
+          'to',
+          'mp3'
+        )
+      })
       if (this.$store.state.auth.authenticated) {
         // we need to send the token directly in url
         // so authentication can be checked by the backend
diff --git a/front/src/components/audio/track/Row.vue b/front/src/components/audio/track/Row.vue
index b17cf1170..fd8b2daaf 100644
--- a/front/src/components/audio/track/Row.vue
+++ b/front/src/components/audio/track/Row.vue
@@ -34,8 +34,8 @@
         {{ track.album.title }}
       </router-link>
     </td>
-    <td colspan="4" v-if="track.duration">
-      {{ time.parse(track.duration) }}
+    <td colspan="4" v-if="track.uploads && track.uploads.length > 0">
+      {{ time.parse(track.uploads[0].duration) }}
     </td>
     <td colspan="4" v-else>
       <translate>N/A</translate>
diff --git a/front/src/components/library/Track.vue b/front/src/components/library/Track.vue
index 483ff6673..ddccda397 100644
--- a/front/src/components/library/Track.vue
+++ b/front/src/components/library/Track.vue
@@ -44,13 +44,13 @@
             <i class="external icon"></i>
             <translate>View on MusicBrainz</translate>
           </a>
-          <a v-if="track.is_playable" :href="downloadUrl" target="_blank" class="ui button">
+          <a v-if="upload" :href="downloadUrl" target="_blank" class="ui button">
             <i class="download icon"></i>
             <translate>Download</translate>
           </a>
         </div>
       </div>
-      <div class="ui vertical stripe center aligned segment">
+      <div class="ui vertical stripe center aligned segment" v-if="upload">
         <h2 class="ui header"><translate>Track information</translate></h2>
         <table class="ui very basic collapsing celled center aligned table">
           <tbody>
@@ -58,8 +58,8 @@
               <td>
                 <translate>Duration</translate>
               </td>
-              <td v-if="track.duration">
-                {{ time.parse(track.duration) }}
+              <td v-if="upload.duration">
+                {{ time.parse(upload.duration) }}
               </td>
               <td v-else>
                 <translate>N/A</translate>
@@ -69,8 +69,8 @@
               <td>
                 <translate>Size</translate>
               </td>
-              <td v-if="track.size">
-                {{ track.size | humanSize }}
+              <td v-if="upload.size">
+                {{ upload.size | humanSize }}
               </td>
               <td v-else>
                 <translate>N/A</translate>
@@ -80,8 +80,8 @@
               <td>
                 <translate>Bitrate</translate>
               </td>
-              <td v-if="track.bitrate">
-                {{ track.bitrate | humanSize }}/s
+              <td v-if="upload.bitrate">
+                {{ upload.bitrate | humanSize }}/s
               </td>
               <td v-else>
                 <translate>N/A</translate>
@@ -91,8 +91,8 @@
               <td>
                 <translate>Type</translate>
               </td>
-              <td v-if="track.mimetype">
-                {{ track.mimetype }}
+              <td v-if="upload.extension">
+                {{ upload.extension }}
               </td>
               <td v-else>
                 <translate>N/A</translate>
@@ -195,6 +195,11 @@ export default {
         title: this.$gettext('Track')
       }
     },
+    upload () {
+      if (this.track.uploads) {
+        return this.track.uploads[0]
+      }
+    },
     wikipediaUrl () {
       return 'https://en.wikipedia.org/w/index.php?search=' + encodeURI(this.track.title + ' ' + this.track.artist.name)
     },
@@ -204,7 +209,7 @@ export default {
       }
     },
     downloadUrl () {
-      let u = this.$store.getters['instance/absoluteUrl'](this.track.listen_url)
+      let u = this.$store.getters['instance/absoluteUrl'](this.upload.listen_url)
       if (this.$store.state.auth.authenticated) {
         u = url.updateQueryString(u, 'jwt', encodeURI(this.$store.state.auth.token))
       }
diff --git a/front/src/store/index.js b/front/src/store/index.js
index 051e89b39..0b8eb3321 100644
--- a/front/src/store/index.js
+++ b/front/src/store/index.js
@@ -79,6 +79,7 @@ export default new Vuex.Store({
                 id: track.id,
                 title: track.title,
                 mbid: track.mbid,
+                uploads: track.uploads,
                 listen_url: track.listen_url,
                 album: {
                   id: track.album.id,
diff --git a/front/src/store/player.js b/front/src/store/player.js
index 08492541e..fac17368d 100644
--- a/front/src/store/player.js
+++ b/front/src/store/player.js
@@ -95,10 +95,19 @@ export default {
     incrementVolume ({commit, state}, value) {
       commit('volume', state.volume + value)
     },
-    stop (context) {
+    stop ({commit}) {
+      commit('errored', false)
+      commit('resetErrorCount')
     },
-    togglePlay ({commit, state}) {
+    togglePlay ({commit, state, dispatch}) {
       commit('playing', !state.playing)
+      if (state.errored && state.errorCount < state.maxConsecutiveErrors) {
+        setTimeout(() => {
+          if (state.playing) {
+            dispatch('queue/next', null, {root: true})
+          }
+        }, 3000)
+      }
     },
     trackListened ({commit, rootState}, track) {
       if (!rootState.auth.authenticated) {
@@ -121,7 +130,13 @@ export default {
     trackErrored ({commit, dispatch, state}) {
       commit('errored', true)
       commit('incrementErrorCount')
-      dispatch('queue/next', null, {root: true})
+      if (state.errorCount < state.maxConsecutiveErrors) {
+        setTimeout(() => {
+          if (state.playing) {
+            dispatch('queue/next', null, {root: true})
+          }
+        }, 3000)
+      }
     },
     updateProgress ({commit}, t) {
       commit('currentTime', t)
diff --git a/front/src/store/queue.js b/front/src/store/queue.js
index b6edb2242..81403b11f 100644
--- a/front/src/store/queue.js
+++ b/front/src/store/queue.js
@@ -142,7 +142,6 @@ export default {
       commit('ended', false)
       commit('player/currentTime', 0, {root: true})
       commit('player/playing', true, {root: true})
-      commit('player/errored', false, {root: true})
       commit('currentIndex', index)
       if (state.tracks.length - index <= 2 && rootState.radios.running) {
         dispatch('radios/populateQueue', null, {root: true})
diff --git a/front/tests/unit/specs/store/queue.spec.js b/front/tests/unit/specs/store/queue.spec.js
index 373f4938e..282a4f026 100644
--- a/front/tests/unit/specs/store/queue.spec.js
+++ b/front/tests/unit/specs/store/queue.spec.js
@@ -267,7 +267,6 @@ describe('store/queue', () => {
           { type: 'ended', payload: false },
           { type: 'player/currentTime', payload: 0, options: {root: true} },
           { type: 'player/playing', payload: true, options: {root: true} },
-          { type: 'player/errored', payload: false, options: {root: true} },
           { type: 'currentIndex', payload: 1 }
         ]
       })
@@ -281,7 +280,6 @@ describe('store/queue', () => {
           { type: 'ended', payload: false },
           { type: 'player/currentTime', payload: 0, options: {root: true} },
           { type: 'player/playing', payload: true, options: {root: true} },
-          { type: 'player/errored', payload: false, options: {root: true} },
           { type: 'currentIndex', payload: 1 }
         ]
       })
@@ -295,7 +293,6 @@ describe('store/queue', () => {
           { type: 'ended', payload: false },
           { type: 'player/currentTime', payload: 0, options: {root: true} },
           { type: 'player/playing', payload: true, options: {root: true} },
-          { type: 'player/errored', payload: false, options: {root: true} },
           { type: 'currentIndex', payload: 1 }
         ],
         expectedActions: [
-- 
GitLab