From 62a7d9091e92dc88dd5cb46447a07bf49c00aad9 Mon Sep 17 00:00:00 2001
From: Eliot Berriot <contact@eliotberriot.com>
Date: Sun, 24 Dec 2017 22:48:29 +0100
Subject: [PATCH] Now persist/restore queue/radio/player state automatically

---
 front/package.json                            |  3 +-
 front/src/cache/index.js                      | 29 -------
 front/src/components/Sidebar.vue              |  2 +-
 front/src/components/audio/Player.vue         |  4 +-
 front/src/components/audio/SearchBar.vue      |  3 +-
 front/src/components/audio/Track.vue          | 14 +++-
 front/src/components/favorites/List.vue       |  1 -
 .../favorites/TrackFavoriteIcon.vue           | 11 +--
 front/src/components/metadata/Search.vue      |  2 +-
 front/src/store/auth.js                       |  8 +-
 front/src/store/favorites.js                  | 15 ++--
 front/src/store/index.js                      | 82 ++++++++++++++++++-
 front/src/store/queue.js                      |  7 +-
 13 files changed, 111 insertions(+), 70 deletions(-)
 delete mode 100644 front/src/cache/index.js

diff --git a/front/package.json b/front/package.json
index 5bec01602d..3eb5201b29 100644
--- a/front/package.json
+++ b/front/package.json
@@ -23,7 +23,8 @@
     "vue-resource": "^1.3.4",
     "vue-router": "^2.3.1",
     "vuedraggable": "^2.14.1",
-    "vuex": "^3.0.1"
+    "vuex": "^3.0.1",
+    "vuex-persistedstate": "^2.4.2"
   },
   "devDependencies": {
     "autoprefixer": "^6.7.2",
diff --git a/front/src/cache/index.js b/front/src/cache/index.js
deleted file mode 100644
index e039ee7880..0000000000
--- a/front/src/cache/index.js
+++ /dev/null
@@ -1,29 +0,0 @@
-import logger from '@/logging'
-export default {
-  get (key, d) {
-    let v = localStorage.getItem(key)
-    if (v === null) {
-      return d
-    } else {
-      try {
-        return JSON.parse(v).value
-      } catch (e) {
-        logger.default.error('Removing unparsable cached value for key ' + key)
-        this.remove(key)
-        return d
-      }
-    }
-  },
-  set (key, value) {
-    return localStorage.setItem(key, JSON.stringify({value: value}))
-  },
-
-  remove (key) {
-    return localStorage.removeItem(key)
-  },
-
-  clear () {
-    localStorage.clear()
-  }
-
-}
diff --git a/front/src/components/Sidebar.vue b/front/src/components/Sidebar.vue
index b5c4f00462..a315aab199 100644
--- a/front/src/components/Sidebar.vue
+++ b/front/src/components/Sidebar.vue
@@ -62,7 +62,7 @@
                   {{ track.artist.name }}
               </td>
               <td>
-                <template v-if="favoriteTracks.objects[track.id]">
+                <template v-if="$store.getters['favorites/isFavorite'](track.id)">
                   <i class="pink heart icon"></i>
                 </template
               </td>
diff --git a/front/src/components/audio/Player.vue b/front/src/components/audio/Player.vue
index 9701b44614..500f4dc1d2 100644
--- a/front/src/components/audio/Player.vue
+++ b/front/src/components/audio/Player.vue
@@ -5,6 +5,8 @@
       v-if="currentTrack"
       :key="(currentIndex, currentTrack.id)"
       :is-current="true"
+      :start-time="$store.state.player.currentTime"
+      :autoplay="$store.state.player.playing"
       :track="currentTrack">
     </audio-track>
 
@@ -127,7 +129,7 @@
       @keydown.ctrl.right.prevent.exact="next"
       @keydown.ctrl.down.prevent.exact="$store.commit('player/incrementVolume', -0.1)"
       @keydown.ctrl.up.prevent.exact="$store.commit('player/incrementVolume', 0.1)"
-      @keydown.f.prevent.exact="favoriteTracks.toggle(currentTrack.id)"
+      @keydown.f.prevent.exact="$store.dispatch('favorites/toggle', currentTrack.id)"
       @keydown.l.prevent.exact="$store.commit('player/toggleLooping')"
       @keydown.s.prevent.exact="shuffle"
       />
diff --git a/front/src/components/audio/SearchBar.vue b/front/src/components/audio/SearchBar.vue
index 386e24a74f..9d8b39f870 100644
--- a/front/src/components/audio/SearchBar.vue
+++ b/front/src/components/audio/SearchBar.vue
@@ -18,6 +18,7 @@ const SEARCH_URL = config.API_URL + 'search?query={query}'
 
 export default {
   mounted () {
+    let self = this
     jQuery(this.$el).search({
       type: 'category',
       minCharacters: 3,
@@ -26,7 +27,7 @@ export default {
       },
       apiSettings: {
         beforeXHR: function (xhrObject) {
-          xhrObject.setRequestHeader('Authorization', this.$store.getters['auth/header'])
+          xhrObject.setRequestHeader('Authorization', self.$store.getters['auth/header'])
           return xhrObject
         },
         onResponse: function (initialResponse) {
diff --git a/front/src/components/audio/Track.vue b/front/src/components/audio/Track.vue
index f0e1f14fa7..c8627925eb 100644
--- a/front/src/components/audio/Track.vue
+++ b/front/src/components/audio/Track.vue
@@ -22,7 +22,9 @@ import url from '@/utils/url'
 export default {
   props: {
     track: {type: Object},
-    isCurrent: {type: Boolean, default: false}
+    isCurrent: {type: Boolean, default: false},
+    startTime: {type: Number, default: 0},
+    autoplay: {type: Boolean, default: false}
   },
   computed: {
     ...mapState({
@@ -57,8 +59,11 @@ export default {
 
     },
     loaded: function () {
-      this.$store.commit('player/duration', this.$refs.audio.duration)
-      if (this.isCurrent) {
+      if (this.isCurrent && this.autoplay) {
+        this.$store.commit('player/duration', this.$refs.audio.duration)
+        if (this.startTime) {
+          this.setCurrentTime(this.startTime)
+        }
         this.$store.commit('player/playing', true)
         this.$refs.audio.play()
       }
@@ -72,8 +77,9 @@ export default {
       if (this.looping === 1) {
         this.setCurrentTime(0)
         this.$refs.audio.play()
+      } else {
+        this.$store.dispatch('player/trackEnded', this.track)
       }
-      this.$store.dispatch('player/trackEnded', this.track)
     },
     setCurrentTime (t) {
       if (t < 0 | t > this.duration) {
diff --git a/front/src/components/favorites/List.vue b/front/src/components/favorites/List.vue
index aef4bea93c..8577e84ca5 100644
--- a/front/src/components/favorites/List.vue
+++ b/front/src/components/favorites/List.vue
@@ -119,7 +119,6 @@ export default {
         self.results = response.data
         self.nextLink = response.data.next
         self.previousLink = response.data.previous
-        self.$store.commit('favorites/count', response.data.count)
         self.results.results.forEach((track) => {
           self.$store.commit('favorites/track', {id: track.id, value: true})
         })
diff --git a/front/src/components/favorites/TrackFavoriteIcon.vue b/front/src/components/favorites/TrackFavoriteIcon.vue
index 5abc57a952..d4838ba5f3 100644
--- a/front/src/components/favorites/TrackFavoriteIcon.vue
+++ b/front/src/components/favorites/TrackFavoriteIcon.vue
@@ -1,5 +1,5 @@
  <template>
-  <button @click="$store.dispatch('favorites/set', {id: track.id, value: !isFavorite})" v-if="button" :class="['ui', 'pink', {'inverted': isFavorite}, {'favorited': isFavorite}, 'button']">
+  <button @click="$store.dispatch('favorites/toggle', track.id)" v-if="button" :class="['ui', 'pink', {'inverted': isFavorite}, {'favorited': isFavorite}, 'button']">
     <i class="heart icon"></i>
     <template v-if="isFavorite">
       In favorites
@@ -8,23 +8,16 @@
       Add to favorites
     </template>
   </button>
-  <i v-else @click="$store.dispatch('favorites/set', {id: track.id, value: !isFavorite})" :class="['favorite-icon', 'heart', {'pink': isFavorite}, {'favorited': isFavorite}, 'link', 'icon']" :title="title"></i>
+  <i v-else @click="$store.dispatch('favorites/toggle', track.id)" :class="['favorite-icon', 'heart', {'pink': isFavorite}, {'favorited': isFavorite}, 'link', 'icon']" :title="title"></i>
 </template>
 
 <script>
-import {mapState} from 'vuex'
-
 export default {
   props: {
     track: {type: Object},
     button: {type: Boolean, default: false}
   },
   computed: {
-    ...mapState({
-      favorites: state => {
-        return state.favorites.tracks
-      }
-    }),
     title () {
       if (this.isFavorite) {
         return 'Remove from favorites'
diff --git a/front/src/components/metadata/Search.vue b/front/src/components/metadata/Search.vue
index c3dc7433c5..f2dea6cab9 100644
--- a/front/src/components/metadata/Search.vue
+++ b/front/src/components/metadata/Search.vue
@@ -65,7 +65,7 @@ export default {
         },
         apiSettings: {
           beforeXHR: function (xhrObject, s) {
-            xhrObject.setRequestHeader('Authorization', this.$store.getters['auth/header'])
+            xhrObject.setRequestHeader('Authorization', self.$store.getters['auth/header'])
             return xhrObject
           },
           onResponse: function (initialResponse) {
diff --git a/front/src/store/auth.js b/front/src/store/auth.js
index 23b0b628dc..eea508df95 100644
--- a/front/src/store/auth.js
+++ b/front/src/store/auth.js
@@ -1,7 +1,6 @@
 import Vue from 'vue'
 import config from '@/config'
 import logger from '@/logging'
-import cache from '@/cache'
 import router from '@/router'
 
 const LOGIN_URL = config.API_URL + 'token/'
@@ -45,9 +44,7 @@ export default {
       return resource.save({}, credentials).then(response => {
         logger.default.info('Successfully logged in as', credentials.username)
         commit('token', response.data.token)
-        cache.set('token', response.data.token)
         commit('username', credentials.username)
-        cache.set('username', credentials.username)
         commit('authenticated', true)
         dispatch('fetchProfile')
         // Redirect to a specified route
@@ -58,7 +55,6 @@ export default {
       })
     },
     logout ({commit}) {
-      cache.clear()
       commit('authenticated', false)
       commit('profile', null)
       logger.default.info('Log out, goodbye!')
@@ -66,8 +62,8 @@ export default {
     },
     check ({commit, dispatch, state}) {
       logger.default.info('Checking authentication...')
-      var jwt = cache.get('token')
-      var username = cache.get('username')
+      var jwt = state.token
+      var username = state.username
       if (jwt) {
         commit('authenticated', true)
         commit('username', username)
diff --git a/front/src/store/favorites.js b/front/src/store/favorites.js
index 8bb4bb5afe..9337966fdf 100644
--- a/front/src/store/favorites.js
+++ b/front/src/store/favorites.js
@@ -14,16 +14,16 @@ export default {
   mutations: {
     track: (state, {id, value}) => {
       if (value) {
-        state.tracks.push(id)
+        if (state.tracks.indexOf(id) === -1) {
+          state.tracks.push(id)
+        }
       } else {
         let i = state.tracks.indexOf(id)
         if (i > -1) {
           state.tracks.splice(i, 1)
         }
       }
-    },
-    count: (state, value) => {
-      state.count = value
+      state.count = state.tracks.length
     }
   },
   getters: {
@@ -35,29 +35,25 @@ export default {
     set ({commit, state}, {id, value}) {
       commit('track', {id, value})
       if (value) {
-        commit('count', state.count + 1)
         let resource = Vue.resource(FAVORITES_URL)
         resource.save({}, {'track': id}).then((response) => {
           logger.default.info('Successfully added track to favorites')
         }, (response) => {
           logger.default.info('Error while adding track to favorites')
           commit('track', {id, value: !value})
-          commit('count', state.count - 1)
         })
       } else {
-        commit('count', state.count - 1)
         let resource = Vue.resource(REMOVE_URL)
         resource.delete({}, {'track': id}).then((response) => {
           logger.default.info('Successfully removed track from favorites')
         }, (response) => {
           logger.default.info('Error while removing track from favorites')
           commit('track', {id, value: !value})
-          commit('count', state.count + 1)
         })
       }
     },
     toggle ({getters, dispatch}, id) {
-      dispatch('set', {id, value: getters['isFavorite'](id)})
+      dispatch('set', {id, value: !getters['isFavorite'](id)})
     },
     fetch ({dispatch, state, commit}, url) {
       // will fetch favorites by batches from API to have them locally
@@ -68,7 +64,6 @@ export default {
         response.data.results.forEach(result => {
           commit('track', {id: result.track, value: true})
         })
-        commit('count', state.tracks.length)
         if (response.data.next) {
           dispatch('fetch', response.data.next)
         }
diff --git a/front/src/store/index.js b/front/src/store/index.js
index 99e466e510..507f0b5876 100644
--- a/front/src/store/index.js
+++ b/front/src/store/index.js
@@ -1,5 +1,6 @@
 import Vue from 'vue'
 import Vuex from 'vuex'
+import createPersistedState from 'vuex-persistedstate'
 
 import favorites from './favorites'
 import auth from './auth'
@@ -16,5 +17,84 @@ export default new Vuex.Store({
     queue,
     radios,
     player
-  }
+  },
+  plugins: [
+    createPersistedState({
+      key: 'auth',
+      paths: ['auth'],
+      filter: (mutation) => {
+        return mutation.type.startsWith('auth/')
+      }
+    }),
+    createPersistedState({
+      key: 'radios',
+      paths: ['radios'],
+      filter: (mutation) => {
+        return mutation.type.startsWith('radios/')
+      }
+    }),
+    createPersistedState({
+      key: 'player',
+      paths: [
+        'player.looping',
+        'player.playing',
+        'player.volume',
+        'player.duration',
+        'player.errored'],
+      filter: (mutation) => {
+        return mutation.type.startsWith('player/') && mutation.type !== 'player/currentTime'
+      }
+    }),
+    createPersistedState({
+      key: 'progress',
+      paths: ['player.currentTime'],
+      filter: (mutation) => {
+        let delay = 10
+        return mutation.type === 'player/currentTime' && parseInt(mutation.payload) % delay === 0
+      },
+      reducer: (state) => {
+        return {
+          player: {
+            currentTime: state.player.currentTime
+          }
+        }
+      }
+    }),
+    createPersistedState({
+      key: 'queue',
+      filter: (mutation) => {
+        return mutation.type.startsWith('queue/')
+      },
+      reducer: (state) => {
+        return {
+          queue: {
+            currentIndex: state.queue.currentIndex,
+            tracks: state.queue.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
+              }
+            })
+          }
+        }
+      }
+    })
+  ]
 })
diff --git a/front/src/store/queue.js b/front/src/store/queue.js
index 3a0b7dd797..5dde19bd8e 100644
--- a/front/src/store/queue.js
+++ b/front/src/store/queue.js
@@ -111,11 +111,6 @@ export default {
       }
     },
     next ({state, dispatch, commit, rootState}) {
-      if (rootState.player.looping === 1) {
-        // we loop on the same track, this is handled directly on the track
-        // component, so we do nothing.
-        return logger.default.info('Looping on the same track')
-      }
       if (rootState.player.looping === 2 && state.currentIndex >= state.tracks.length - 1) {
         logger.default.info('Going back to the beginning of the queue')
         return dispatch('currentIndex', 0)
@@ -130,6 +125,8 @@ export default {
     },
     currentIndex ({commit, state, rootState, dispatch}, index) {
       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) {
-- 
GitLab