diff --git a/api/requirements/base.txt b/api/requirements/base.txt
index aee1222591882c83062e190a2a7c8b36a10dc7d7..7e56b6cfda24f977288de6ed3b91a5df42f1fde0 100644
--- a/api/requirements/base.txt
+++ b/api/requirements/base.txt
@@ -49,7 +49,7 @@ mutagen>=1.39,<1.40
 
 # Until this is merged
 #django-taggit>=0.22,<0.23
-git+https://github.com/jdufresne/django-taggit.git@e8f7f216f04c9781bebc84363ab24d575f948ede
+git+https://github.com/alex/django-taggit.git@95776ac66948ed7ba7c12e35c1170551e3be66a5
 # Until this is merged
 git+https://github.com/EliotBerriot/PyMemoize.git@django
 # Until this is merged
diff --git a/dev.yml b/dev.yml
index c71298cfc8932c1b7874a906b582fe85a59ee4bd..44e38e32671073cbeb4d3c9d697a216c2b99b541 100644
--- a/dev.yml
+++ b/dev.yml
@@ -13,7 +13,6 @@ services:
       - "8080:8080"
     volumes:
       - './front:/app'
-      - /app/node_modules
 
   postgres:
     env_file: .env.dev
diff --git a/front/package.json b/front/package.json
index bad90430f4144be99d4b17ac11e82073a6705ff0..5bec01602d4beed1e1443af3fd690bdf86e06e00 100644
--- a/front/package.json
+++ b/front/package.json
@@ -22,7 +22,8 @@
     "vue-lazyload": "^1.1.4",
     "vue-resource": "^1.3.4",
     "vue-router": "^2.3.1",
-    "vuedraggable": "^2.14.1"
+    "vuedraggable": "^2.14.1",
+    "vuex": "^3.0.1"
   },
   "devDependencies": {
     "autoprefixer": "^6.7.2",
diff --git a/front/src/audio/index.js b/front/src/audio/index.js
deleted file mode 100644
index 4896b83b0f895c8bca9269aebf9141d16ab53087..0000000000000000000000000000000000000000
--- 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 4273fb9a630dd098458b532cd85f5bdb96399aba..0000000000000000000000000000000000000000
--- 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/auth/index.js b/front/src/auth/index.js
deleted file mode 100644
index 80236942858440d517d2fe80b7bb43c71b3ea7c0..0000000000000000000000000000000000000000
--- a/front/src/auth/index.js
+++ /dev/null
@@ -1,99 +0,0 @@
-import logger from '@/logging'
-import config from '@/config'
-import cache from '@/cache'
-import Vue from 'vue'
-
-import favoriteTracks from '@/favorites/tracks'
-
-// URL and endpoint constants
-const LOGIN_URL = config.API_URL + 'token/'
-const USER_PROFILE_URL = config.API_URL + 'users/users/me/'
-// const SIGNUP_URL = API_URL + 'users/'
-
-let userData = {
-  authenticated: false,
-  username: '',
-  availablePermissions: {},
-  profile: {}
-}
-let auth = {
-
-  // Send a request to the login URL and save the returned JWT
-  login (context, creds, redirect, onError) {
-    return context.$http.post(LOGIN_URL, creds).then(response => {
-      logger.default.info('Successfully logged in as', creds.username)
-      cache.set('token', response.data.token)
-      cache.set('username', creds.username)
-
-      this.user.authenticated = true
-      this.user.username = creds.username
-      this.connect()
-      // Redirect to a specified route
-      if (redirect) {
-        context.$router.push(redirect)
-      }
-    }, response => {
-      logger.default.error('Error while logging in', response.data)
-      if (onError) {
-        onError(response)
-      }
-    })
-  },
-
-  // To log out, we just need to remove the token
-  logout () {
-    cache.clear()
-    this.user.authenticated = false
-    logger.default.info('Log out, goodbye!')
-  },
-
-  checkAuth () {
-    logger.default.info('Checking authentication...')
-    var jwt = this.getAuthToken()
-    var username = cache.get('username')
-    if (jwt) {
-      this.user.authenticated = true
-      this.user.username = username
-      logger.default.info('Logged back in as ' + username)
-      this.connect()
-    } else {
-      logger.default.info('Anonymous user')
-      this.user.authenticated = false
-    }
-  },
-
-  getAuthToken () {
-    return cache.get('token')
-  },
-
-  // The object to be passed as a header for authenticated requests
-  getAuthHeader () {
-    return 'JWT ' + this.getAuthToken()
-  },
-
-  fetchProfile () {
-    let resource = Vue.resource(USER_PROFILE_URL)
-    return resource.get({}).then((response) => {
-      logger.default.info('Successfully fetched user profile')
-      return response.data
-    }, (response) => {
-      logger.default.info('Error while fetching user profile')
-    })
-  },
-  connect () {
-    // called once user has logged in successfully / reauthenticated
-    // e.g. after a page refresh
-    let self = this
-    this.fetchProfile().then(data => {
-      Vue.set(self.user, 'profile', data)
-      Object.keys(data.permissions).forEach(function (key) {
-        // this makes it easier to check for permissions in templates
-        Vue.set(self.user.availablePermissions, key, data.permissions[String(key)].status)
-      })
-    })
-    favoriteTracks.fetch()
-  }
-}
-
-Vue.set(auth, 'user', userData)
-export default auth
diff --git a/front/src/components/Sidebar.vue b/front/src/components/Sidebar.vue
index 68927a37b09a5e302a9dfb7ce9be44b9e7750b96..d6a2539224afb89412f5488cd1e7b02ade452cdb 100644
--- a/front/src/components/Sidebar.vue
+++ b/front/src/components/Sidebar.vue
@@ -28,8 +28,8 @@
   <div class="tabs">
     <div class="ui bottom attached active tab" data-tab="library">
       <div class="ui inverted vertical fluid menu">
-        <router-link class="item" v-if="auth.user.authenticated" :to="{name: 'profile', params: {username: auth.user.username}}"><i class="user icon"></i> Logged in as {{ auth.user.username }}</router-link>
-        <router-link class="item" v-if="auth.user.authenticated" :to="{name: 'logout'}"><i class="sign out icon"></i> Logout</router-link>
+        <router-link class="item" v-if="$store.state.auth.authenticated" :to="{name: 'profile', params: {username: $store.state.auth.username}}"><i class="user icon"></i> Logged in as {{ $store.state.auth.username }}</router-link>
+        <router-link class="item" v-if="$store.state.auth.authenticated" :to="{name: 'logout'}"><i class="sign out icon"></i> Logout</router-link>
         <router-link class="item" v-else :to="{name: 'login'}"><i class="sign in icon"></i> Login</router-link>
         <router-link class="item" :to="{path: '/library'}"><i class="sound icon"> </i>Browse library</router-link>
         <router-link class="item" :to="{path: '/favorites'}"><i class="heart icon"></i> Favorites</router-link>
@@ -51,7 +51,7 @@
     <div class="ui bottom attached tab" data-tab="queue">
       <table class="ui compact inverted very basic fixed single line table">
         <draggable v-model="queue.tracks" element="tbody" @update="reorder">
-          <tr @click="queue.play(index)" v-for="(track, index) in queue.tracks" :key="index" :class="[{'active': index === queue.currentIndex}]">
+          <tr @click="$store.dispatch('queue/currentIndex', index)" v-for="(track, index) in queue.tracks" :key="index" :class="[{'active': index === queue.currentIndex}]">
               <td class="right aligned">{{ index + 1}}</td>
               <td class="center aligned">
                   <img class="ui mini image" v-if="track.album.cover" :src="backend.absoluteUrl(track.album.cover)">
@@ -63,23 +63,23 @@
               </td>
               <td>
                 <template v-if="favoriteTracks.objects[track.id]">
-                  <i @click.stop="queue.cleanTrack(index)" class="pink heart icon"></i>
-                  </template
+                  <i class="pink heart icon"></i>
+                </template
               </td>
               <td>
-                  <i @click.stop="queue.cleanTrack(index)" class="circular trash icon"></i>
+                  <i @click.stop="cleanTrack(index)" class="circular trash icon"></i>
               </td>
             </tr>
           </draggable>
       </table>
-      <div v-if="radios.running" class="ui black message">
+      <div v-if="$store.state.radios.running" class="ui black message">
 
         <div class="content">
           <div class="header">
             <i class="feed icon"></i> You have a radio playing
           </div>
           <p>New tracks will be appended here automatically.</p>
-          <div @click="radios.stop()" class="ui basic inverted red button">Stop radio</div>
+          <div @click="$store.dispatch('radios/stop')" class="ui basic inverted red button">Stop radio</div>
         </div>
       </div>
     </div>
@@ -87,24 +87,18 @@
   <div class="ui inverted segment player-wrapper">
     <player></player>
   </div>
-  <GlobalEvents
-    @keydown.r.stop="queue.restore"
-    />
 </div>
 </template>
 
 <script>
-import GlobalEvents from '@/components/utils/global-events'
+import {mapState, mapActions} from 'vuex'
 
 import Player from '@/components/audio/Player'
 import favoriteTracks from '@/favorites/tracks'
 import Logo from '@/components/Logo'
 import SearchBar from '@/components/audio/SearchBar'
-import auth from '@/auth'
-import queue from '@/audio/queue'
 import backend from '@/audio/backend'
 import draggable from 'vuedraggable'
-import radios from '@/radios'
 
 import $ from 'jquery'
 
@@ -114,24 +108,28 @@ export default {
     Player,
     SearchBar,
     Logo,
-    draggable,
-    GlobalEvents
+    draggable
   },
   data () {
     return {
-      auth: auth,
       backend: backend,
-      queue: queue,
-      radios,
       favoriteTracks
     }
   },
   mounted () {
     $(this.$el).find('.menu .item').tab()
   },
+  computed: {
+    ...mapState({
+      queue: state => state.queue
+    })
+  },
   methods: {
-    reorder (e) {
-      this.queue.reorder(e.oldIndex, e.newIndex)
+    ...mapActions({
+      cleanTrack: 'queue/cleanTrack'
+    }),
+    reorder: function (oldValue, newValue) {
+      this.$store.commit('queue/reorder', {oldValue, newValue})
     }
   }
 }
diff --git a/front/src/components/audio/PlayButton.vue b/front/src/components/audio/PlayButton.vue
index 240fa498032c05c2823f0aa3d53cf2e8a7991b0f..4767255ecae8b6bb27a872614f9a4e029146ded9 100644
--- a/front/src/components/audio/PlayButton.vue
+++ b/front/src/components/audio/PlayButton.vue
@@ -17,7 +17,6 @@
 
 <script>
 import logger from '@/logging'
-import queue from '@/audio/queue'
 import jQuery from 'jquery'
 
 export default {
@@ -40,19 +39,19 @@ export default {
   methods: {
     add () {
       if (this.track) {
-        queue.append(this.track)
+        this.$store.dispatch('queue/append', {track: this.track})
       } else {
-        queue.appendMany(this.tracks)
+        this.$store.dispatch('queue/appendMany', {tracks: this.tracks})
       }
     },
     addNext (next) {
       if (this.track) {
-        queue.append(this.track, queue.currentIndex + 1)
+        this.$store.dispatch('queue/append', {track: this.track, index: this.$store.state.queue.currentIndex + 1})
       } else {
-        queue.appendMany(this.tracks, queue.currentIndex + 1)
+        this.$store.dispatch('queue/appendMany', {tracks: this.tracks, index: this.$store.state.queue.currentIndex + 1})
       }
       if (next) {
-        queue.next()
+        this.$store.dispatch('queue/next')
       }
     }
   }
diff --git a/front/src/components/audio/Player.vue b/front/src/components/audio/Player.vue
index c862660ad60ed0960cc5b0e66a2380810cdef9b7..fec74b3dcb26411d6cfedf4125c32d4a810ce574 100644
--- a/front/src/components/audio/Player.vue
+++ b/front/src/components/audio/Player.vue
@@ -1,104 +1,112 @@
 <template>
   <div class="player">
-    <div v-if="queue.currentTrack" class="track-area ui items">
+    <audio-track
+      ref="currentAudio"
+      v-if="currentTrack"
+      :key="(currentIndex, currentTrack.id)"
+      :is-current="true"
+      :track="currentTrack">
+    </audio-track>
+
+    <div v-if="currentTrack" class="track-area ui items">
       <div class="ui inverted item">
         <div class="ui tiny image">
-          <img v-if="queue.currentTrack.album.cover" :src="Track.getCover(queue.currentTrack)">
+          <img v-if="currentTrack.album.cover" :src="Track.getCover(currentTrack)">
           <img v-else src="../../assets/audio/default-cover.png">
         </div>
         <div class="middle aligned content">
-          <router-link class="small header discrete link track" :to="{name: 'library.tracks.detail', params: {id: queue.currentTrack.id }}">
-            {{ queue.currentTrack.title }}
+          <router-link class="small header discrete link track" :to="{name: 'library.tracks.detail', params: {id: currentTrack.id }}">
+            {{ currentTrack.title }}
           </router-link>
           <div class="meta">
-            <router-link class="artist" :to="{name: 'library.artists.detail', params: {id: queue.currentTrack.artist.id }}">
-              {{ queue.currentTrack.artist.name }}
+            <router-link class="artist" :to="{name: 'library.artists.detail', params: {id: currentTrack.artist.id }}">
+              {{ currentTrack.artist.name }}
             </router-link> /
-            <router-link class="album" :to="{name: 'library.albums.detail', params: {id: queue.currentTrack.album.id }}">
-              {{ queue.currentTrack.album.title }}
+            <router-link class="album" :to="{name: 'library.albums.detail', params: {id: currentTrack.album.id }}">
+              {{ currentTrack.album.title }}
             </router-link>
           </div>
           <div class="description">
-            <track-favorite-icon :track="queue.currentTrack"></track-favorite-icon>
+            <track-favorite-icon :track="currentTrack"></track-favorite-icon>
           </div>
         </div>
       </div>
     </div>
-    <div class="progress-area" v-if="queue.currentTrack">
+    <div class="progress-area" v-if="currentTrack">
       <div class="ui grid">
         <div class="left floated four wide column">
-          <p class="timer start" @click="queue.audio.setTime(0)">{{queue.audio.state.currentTimeFormat}}</p>
+          <p class="timer start" @click="updateProgress(0)">{{currentTimeFormatted}}</p>
         </div>
 
         <div class="right floated four wide column">
-          <p class="timer total">{{queue.audio.state.durationTimerFormat}}</p>
+          <p class="timer total">{{durationFormatted}}</p>
         </div>
       </div>
       <div ref="progress" class="ui small orange inverted progress" @click="touchProgress">
-        <div class="bar" :data-percent="queue.audio.state.progress" :style="{ 'width': queue.audio.state.progress + '%' }"></div>
+        <div class="bar" :data-percent="progress" :style="{ 'width': progress + '%' }"></div>
       </div>
     </div>
 
     <div class="two wide column controls ui grid">
       <div
-        @click="queue.previous()"
+        @click="previous"
         title="Previous track"
         class="two wide column control"
         :disabled="!hasPrevious">
           <i :class="['ui', {'disabled': !hasPrevious}, 'step', 'backward', 'big', 'icon']" ></i>
       </div>
       <div
-        v-if="!queue.audio.state.playing"
-        @click="pauseOrPlay"
+        v-if="!playing"
+        @click="togglePlay"
         title="Play track"
         class="two wide column control">
-          <i :class="['ui', 'play', {'disabled': !queue.currentTrack}, 'big', 'icon']"></i>
+          <i :class="['ui', 'play', {'disabled': !currentTrack}, 'big', 'icon']"></i>
       </div>
       <div
         v-else
-        @click="pauseOrPlay"
+        @click="togglePlay"
         title="Pause track"
         class="two wide column control">
-          <i :class="['ui', 'pause', {'disabled': !queue.currentTrack}, 'big', 'icon']"></i>
+          <i :class="['ui', 'pause', {'disabled': !currentTrack}, 'big', 'icon']"></i>
       </div>
       <div
-        @click="queue.next()"
+        @click="next"
         title="Next track"
         class="two wide column control"
         :disabled="!hasNext">
-          <i :class="['ui', {'disabled': !hasPrevious}, 'step', 'forward', 'big', 'icon']" ></i>
+          <i :class="['ui', {'disabled': !hasNext}, 'step', 'forward', 'big', 'icon']" ></i>
       </div>
       <div class="two wide column control volume-control">
-        <i title="Unmute" @click="queue.setVolume(1)" v-if="currentVolume === 0" class="volume off secondary icon"></i>
-        <i title="Mute" @click="queue.setVolume(0)" v-else-if="currentVolume < 0.5" class="volume down secondary icon"></i>
-        <i title="Mute" @click="queue.setVolume(0)" v-else class="volume up secondary icon"></i>
+        <i title="Unmute" @click="$store.commit('player/volume', 1)" v-if="volume === 0" class="volume off secondary icon"></i>
+        <i title="Mute" @click="$store.commit('player/volume', 0)" v-else-if="volume < 0.5" class="volume down secondary icon"></i>
+        <i title="Mute" @click="$store.commit('player/volume', 0)" v-else class="volume up secondary icon"></i>
         <input type="range" step="0.05" min="0" max="1" v-model="sliderVolume" />
       </div>
       <div class="two wide column control looping">
         <i
           title="Looping disabled. Click to switch to single-track looping."
-          v-if="queue.state.looping === 0"
-          @click="queue.state.looping = 1"
-          :disabled="!queue.currentTrack"
-          :class="['ui', {'disabled': !queue.currentTrack}, 'step', 'repeat', 'secondary', 'icon']"></i>
+          v-if="looping === 0"
+          @click="$store.commit('player/looping', 1)"
+          :disabled="!currentTrack"
+          :class="['ui', {'disabled': !currentTrack}, 'step', 'repeat', 'secondary', 'icon']"></i>
         <i
           title="Looping on a single track. Click to switch to whole queue looping."
-          v-if="queue.state.looping === 1"
-          @click="queue.state.looping = 2"
-          :disabled="!queue.currentTrack"
+          v-if="looping === 1"
+          @click="$store.commit('player/looping', 2)"
+          :disabled="!currentTrack"
           class="repeat secondary icon">
           <span class="ui circular tiny orange label">1</span>
         </i>
         <i
           title="Looping on whole queue. Click to disable looping."
-          v-if="queue.state.looping === 2"
-          @click="queue.state.looping = 0"
-          :disabled="!queue.currentTrack"
+          v-if="looping === 2"
+          @click="$store.commit('player/looping', 0)"
+          :disabled="!currentTrack"
           class="repeat orange secondary icon">
         </i>
       </div>
       <div
-        @click="queue.shuffle()"
+        @click="shuffle()"
         :disabled="queue.tracks.length === 0"
         title="Shuffle your queue"
         class="two wide column control">
@@ -106,7 +114,7 @@
       </div>
       <div class="one wide column"></div>
       <div
-        @click="queue.clean()"
+        @click="clean()"
         :disabled="queue.tracks.length === 0"
         title="Clear your queue"
         class="two wide column control">
@@ -114,79 +122,87 @@
       </div>
     </div>
     <GlobalEvents
-      @keydown.space.prevent.exact="pauseOrPlay"
-      @keydown.ctrl.left.prevent.exact="queue.previous"
-      @keydown.ctrl.right.prevent.exact="queue.next"
-      @keydown.ctrl.down.prevent.exact="queue.incrementVolume(-0.1)"
-      @keydown.ctrl.up.prevent.exact="queue.incrementVolume(0.1)"
-      @keydown.f.prevent.exact="favoriteTracks.toggle(queue.currentTrack.id)"
-      @keydown.l.prevent.exact="queue.toggleLooping"
-      @keydown.s.prevent.exact="queue.shuffle"
+      @keydown.space.prevent.exact="togglePlay"
+      @keydown.ctrl.left.prevent.exact="previous"
+      @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.l.prevent.exact="$store.commit('player/toggleLooping')"
+      @keydown.s.prevent.exact="shuffle"
       />
 
   </div>
 </template>
 
 <script>
+import {mapState, mapGetters, mapActions} from 'vuex'
 import GlobalEvents from '@/components/utils/global-events'
 
 import favoriteTracks from '@/favorites/tracks'
-import queue from '@/audio/queue'
-import radios from '@/radios'
 import Track from '@/audio/track'
+import AudioTrack from '@/components/audio/Track'
 import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon'
 
 export default {
   name: 'player',
   components: {
     TrackFavoriteIcon,
-    GlobalEvents
+    GlobalEvents,
+    AudioTrack
   },
   data () {
     return {
-      sliderVolume: this.currentVolume,
-      queue: queue,
+      sliderVolume: this.volume,
       Track: Track,
-      favoriteTracks,
-      radios
+      favoriteTracks
     }
   },
   mounted () {
     // we trigger the watcher explicitely it does not work otherwise
-    this.sliderVolume = this.currentVolume
+    this.sliderVolume = this.volume
   },
   methods: {
-    pauseOrPlay () {
-      if (this.queue.audio.state.playing) {
-        this.queue.audio.pause()
-      } else {
-        this.queue.audio.play()
-      }
-    },
+    ...mapActions({
+      pause: 'player/pause',
+      togglePlay: 'player/togglePlay',
+      clean: 'queue/clean',
+      next: 'queue/next',
+      previous: 'queue/previous',
+      shuffle: 'queue/shuffle',
+      updateProgress: 'player/updateProgress'
+    }),
     touchProgress (e) {
       let time
       let target = this.$refs.progress
-      time = e.layerX / target.offsetWidth * this.queue.audio.state.duration
-      this.queue.audio.setTime(time)
+      time = e.layerX / target.offsetWidth * this.duration
+      this.$refs.currentAudio.setCurrentTime(time)
     }
   },
   computed: {
-    hasPrevious () {
-      return this.queue.currentIndex > 0
-    },
-    hasNext () {
-      return this.queue.currentIndex < this.queue.tracks.length - 1
-    },
-    currentVolume () {
-      return this.queue.audio.state.volume
-    }
+    ...mapState({
+      currentIndex: state => state.queue.currentIndex,
+      playing: state => state.player.playing,
+      volume: state => state.player.volume,
+      looping: state => state.player.looping,
+      duration: state => state.player.duration,
+      queue: state => state.queue
+    }),
+    ...mapGetters({
+      currentTrack: 'queue/currentTrack',
+      hasNext: 'queue/hasNext',
+      hasPrevious: 'queue/hasPrevious',
+      durationFormatted: 'player/durationFormatted',
+      currentTimeFormatted: 'player/currentTimeFormatted',
+      progress: 'player/progress'
+    })
   },
   watch: {
-    currentVolume (newValue) {
+    volume (newValue) {
       this.sliderVolume = newValue
     },
     sliderVolume (newValue) {
-      this.queue.setVolume(parseFloat(newValue))
+      this.$store.commit('player/volume', newValue)
     }
   }
 }
diff --git a/front/src/components/audio/Search.vue b/front/src/components/audio/Search.vue
index 5c902e5e5abba8e1472feff9cfae3af269e1641e..2811c2b5c4f58ca71f8c5c55c239ffcf41c0e5d6 100644
--- a/front/src/components/audio/Search.vue
+++ b/front/src/components/audio/Search.vue
@@ -30,7 +30,6 @@
 
 <script>
 import logger from '@/logging'
-import queue from '@/audio/queue'
 import backend from '@/audio/backend'
 import AlbumCard from '@/components/audio/album/Card'
 import ArtistCard from '@/components/audio/artist/Card'
@@ -54,8 +53,7 @@ export default {
         artists: []
       },
       backend: backend,
-      isLoading: false,
-      queue: queue
+      isLoading: false
     }
   },
   mounted () {
diff --git a/front/src/components/audio/SearchBar.vue b/front/src/components/audio/SearchBar.vue
index 2324c88392f258f95b7915cce04ea1b9166d5e80..386e24a74f677d40fc60d1b536c15793288a5454 100644
--- a/front/src/components/audio/SearchBar.vue
+++ b/front/src/components/audio/SearchBar.vue
@@ -12,7 +12,6 @@
 <script>
 import jQuery from 'jquery'
 import config from '@/config'
-import auth from '@/auth'
 import router from '@/router'
 
 const SEARCH_URL = config.API_URL + 'search?query={query}'
@@ -27,7 +26,7 @@ export default {
       },
       apiSettings: {
         beforeXHR: function (xhrObject) {
-          xhrObject.setRequestHeader('Authorization', auth.getAuthHeader())
+          xhrObject.setRequestHeader('Authorization', this.$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
new file mode 100644
index 0000000000000000000000000000000000000000..f0e1f14fa70fbb52a85ba5d99ddc7ffdb4e11dbf
--- /dev/null
+++ b/front/src/components/audio/Track.vue
@@ -0,0 +1,103 @@
+<template>
+  <audio
+    ref="audio"
+    :src="url"
+    @error="errored"
+    @progress="updateLoad"
+    @loadeddata="loaded"
+    @timeupdate="updateProgress"
+    @ended="ended"
+    preload>
+
+  </audio>
+</template>
+
+<script>
+import {mapState} from 'vuex'
+import backend from '@/audio/backend'
+import url from '@/utils/url'
+
+// import logger from '@/logging'
+
+export default {
+  props: {
+    track: {type: Object},
+    isCurrent: {type: Boolean, default: false}
+  },
+  computed: {
+    ...mapState({
+      playing: state => state.player.playing,
+      currentTime: state => state.player.currentTime,
+      duration: state => state.player.duration,
+      volume: state => state.player.volume,
+      looping: state => state.player.looping
+    }),
+    url: function () {
+      let file = this.track.files[0]
+      if (!file) {
+        this.$store.dispatch('player/trackErrored')
+        return null
+      }
+      let path = backend.absoluteUrl(file.path)
+      if (this.$store.state.auth.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', this.$store.state.auth.token)
+      }
+      return path
+    }
+  },
+  methods: {
+    errored: function () {
+      this.$store.dispatch('player/trackErrored')
+    },
+    updateLoad: function () {
+
+    },
+    loaded: function () {
+      this.$store.commit('player/duration', this.$refs.audio.duration)
+      if (this.isCurrent) {
+        this.$store.commit('player/playing', true)
+        this.$refs.audio.play()
+      }
+    },
+    updateProgress: function () {
+      if (this.$refs.audio) {
+        this.$store.dispatch('player/updateProgress', this.$refs.audio.currentTime)
+      }
+    },
+    ended: function () {
+      if (this.looping === 1) {
+        this.setCurrentTime(0)
+        this.$refs.audio.play()
+      }
+      this.$store.dispatch('player/trackEnded', this.track)
+    },
+    setCurrentTime (t) {
+      if (t < 0 | t > this.duration) {
+        return
+      }
+      this.updateProgress(t)
+      this.$refs.audio.currentTime = t
+    }
+  },
+  watch: {
+    playing: function (newValue) {
+      if (newValue === true) {
+        this.$refs.audio.play()
+      } else {
+        this.$refs.audio.pause()
+      }
+    },
+    volume: function (newValue) {
+      this.$refs.audio.volume = newValue
+    }
+  }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+</style>
diff --git a/front/src/components/audio/album/Card.vue b/front/src/components/audio/album/Card.vue
index ce5e832e2794a4288a673460244434f30945615f..4c803b29cc704fbc7e05258bfe595ee9b6908d99 100644
--- a/front/src/components/audio/album/Card.vue
+++ b/front/src/components/audio/album/Card.vue
@@ -51,7 +51,6 @@
 </template>
 
 <script>
-import queue from '@/audio/queue'
 import backend from '@/audio/backend'
 import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon'
 import PlayButton from '@/components/audio/PlayButton'
@@ -68,7 +67,6 @@ export default {
   data () {
     return {
       backend: backend,
-      queue: queue,
       initialTracks: 4,
       showAllTracks: false
     }
diff --git a/front/src/components/audio/track/Table.vue b/front/src/components/audio/track/Table.vue
index efb98e382804ecb2623d4f96f5e6b9935559052b..8a591d3bd05e9542484a2b8bf8ef9b931363a837 100644
--- a/front/src/components/audio/track/Table.vue
+++ b/front/src/components/audio/track/Table.vue
@@ -58,9 +58,9 @@
                   Keep your PRIVATE_TOKEN secret as it gives access to your account.
                 </div>
                 <pre>
-export PRIVATE_TOKEN="{{ auth.getAuthToken ()}}"
+export PRIVATE_TOKEN="{{ $store.state.auth.token }}"
 <template v-for="track in tracks"><template v-if="track.files.length > 0">
-curl -G -o "{{ track.files[0].filename }}" <template v-if="auth.user.authenticated">--header "Authorization: JWT $PRIVATE_TOKEN"</template> "{{ backend.absoluteUrl(track.files[0].path) }}"</template></template>
+curl -G -o "{{ track.files[0].filename }}" <template v-if="$store.state.auth.authenticated">--header "Authorization: JWT $PRIVATE_TOKEN"</template> "{{ backend.absoluteUrl(track.files[0].path) }}"</template></template>
 </pre>
               </div>
             </div>
@@ -83,7 +83,6 @@ curl -G -o "{{ track.files[0].filename }}" <template v-if="auth.user.authenticat
 
 <script>
 import backend from '@/audio/backend'
-import auth from '@/auth'
 import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon'
 import PlayButton from '@/components/audio/PlayButton'
 
@@ -102,7 +101,6 @@ export default {
   data () {
     return {
       backend: backend,
-      auth: auth,
       showDownloadModal: false
     }
   }
diff --git a/front/src/components/auth/Login.vue b/front/src/components/auth/Login.vue
index 54e7b82e096433aacd18d02053e66c164e51ca60..99b439af8b3e25c821ddb1dbc0c86c6419961d2c 100644
--- a/front/src/components/auth/Login.vue
+++ b/front/src/components/auth/Login.vue
@@ -39,12 +39,11 @@
 </template>
 
 <script>
-import auth from '@/auth'
 
 export default {
   name: 'login',
   props: {
-    next: {type: String}
+    next: {type: String, default: '/'}
   },
   data () {
     return {
@@ -72,14 +71,17 @@ export default {
       }
       // We need to pass the component's this context
       // to properly make use of http in the auth service
-      auth.login(this, credentials, {path: this.next}, function (response) {
-        // error callback
-        if (response.status === 400) {
-          self.error = 'invalid_credentials'
-        } else {
-          self.error = 'unknown_error'
+      this.$store.dispatch('auth/login', {
+        credentials,
+        next: this.next,
+        onError: response => {
+          if (response.status === 400) {
+            self.error = 'invalid_credentials'
+          } else {
+            self.error = 'unknown_error'
+          }
         }
-      }).then((response) => {
+      }).then(e => {
         self.isLoading = false
       })
     }
diff --git a/front/src/components/auth/Logout.vue b/front/src/components/auth/Logout.vue
index f4b2979bc05394dc04f7644e88700a08b13989d5..fbacca70338ed295dc284664d443c9b239b25da0 100644
--- a/front/src/components/auth/Logout.vue
+++ b/front/src/components/auth/Logout.vue
@@ -3,8 +3,8 @@
     <div class="ui vertical stripe segment">
       <div class="ui small text container">
         <h2>Are you sure you want to log out?</h2>
-        <p>You are currently logged in as {{ auth.user.username }}</p>
-        <button class="ui button" @click="logout">Yes, log me out!</button>
+        <p>You are currently logged in as {{ $store.state.auth.username }}</p>
+        <button class="ui button" @click="$store.dispatch('auth/logout')">Yes, log me out!</button>
         </form>
       </div>
     </div>
@@ -12,23 +12,8 @@
 </template>
 
 <script>
-import auth from '@/auth'
-
 export default {
-  name: 'logout',
-  data () {
-    return {
-      // We need to initialize the component with any
-      // properties that will be used in it
-      auth: auth
-    }
-  },
-  methods: {
-    logout () {
-      auth.logout()
-      this.$router.push({name: 'index'})
-    }
-  }
+  name: 'logout'
 }
 </script>
 
diff --git a/front/src/components/auth/Profile.vue b/front/src/components/auth/Profile.vue
index 2aaae9e2df34d292f5cc66ae04dee5898bc9d335..607fa8ff2b84ffdde60ccbf3b514938dfeb5aba4 100644
--- a/front/src/components/auth/Profile.vue
+++ b/front/src/components/auth/Profile.vue
@@ -3,17 +3,17 @@
     <div v-if="isLoading" class="ui vertical segment">
       <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
     </div>
-    <template v-if="profile">
+    <template v-if="$store.state.auth.profile">
       <div :class="['ui', 'head', 'vertical', 'center', 'aligned', 'stripe', 'segment']">
         <h2 class="ui center aligned icon header">
           <i class="circular inverted user green icon"></i>
           <div class="content">
-            {{ profile.username }}
+            {{ $store.state.auth.profile.username }}
             <div class="sub header">Registered since {{ signupDate }}</div>
           </div>
         </h2>
         <div class="ui basic green label">this is you!</div>
-        <div v-if="profile.is_staff" class="ui yellow label">
+        <div v-if="$store.state.auth.profile.is_staff" class="ui yellow label">
           <i class="star icon"></i>
           Staff member
         </div>
@@ -23,35 +23,21 @@
 </template>
 
 <script>
-import auth from '@/auth'
-var dateFormat = require('dateformat')
+const dateFormat = require('dateformat')
 
 export default {
   name: 'login',
   props: ['username'],
-  data () {
-    return {
-      profile: null
-    }
-  },
   created () {
-    this.fetchProfile()
-  },
-  methods: {
-    fetchProfile () {
-      let self = this
-      auth.fetchProfile().then(data => {
-        self.profile = data
-      })
-    }
+    this.$store.dispatch('auth/fetchProfile')
   },
   computed: {
     signupDate () {
-      let d = new Date(this.profile.date_joined)
+      let d = new Date(this.$store.state.auth.profile.date_joined)
       return dateFormat(d, 'longDate')
     },
     isLoading () {
-      return !this.profile
+      return !this.$store.state.auth.profile
     }
   }
 }
diff --git a/front/src/components/favorites/List.vue b/front/src/components/favorites/List.vue
index 91efd72907e745244f5a74d7cdc3547e750611d2..aef4bea93c1a0a6bfdbd695098619f1d45d6af22 100644
--- a/front/src/components/favorites/List.vue
+++ b/front/src/components/favorites/List.vue
@@ -6,7 +6,7 @@
       </div>
       <h2 v-if="results" class="ui center aligned icon header">
         <i class="circular inverted heart pink icon"></i>
-        {{ favoriteTracks.count }} favorites
+        {{ $store.state.favorites.count }} favorites
       </h2>
       <radio-button type="favorites"></radio-button>
     </div>
@@ -55,10 +55,8 @@
 
 <script>
 import $ from 'jquery'
-import Vue from 'vue'
 import logger from '@/logging'
 import config from '@/config'
-import favoriteTracks from '@/favorites/tracks'
 import TrackTable from '@/components/audio/track/Table'
 import RadioButton from '@/components/radios/Button'
 import Pagination from '@/components/Pagination'
@@ -80,7 +78,6 @@ export default {
       isLoading: false,
       nextLink: null,
       previousLink: null,
-      favoriteTracks,
       page: parseInt(this.defaultPage),
       paginateBy: parseInt(this.defaultPaginateBy || 25),
       orderingDirection: defaultOrdering.direction,
@@ -122,10 +119,9 @@ export default {
         self.results = response.data
         self.nextLink = response.data.next
         self.previousLink = response.data.previous
-        Vue.set(favoriteTracks, 'count', response.data.count)
-        favoriteTracks.count = response.data.count
+        self.$store.commit('favorites/count', response.data.count)
         self.results.results.forEach((track) => {
-          Vue.set(favoriteTracks.objects, track.id, true)
+          self.$store.commit('favorites/track', {id: track.id, value: true})
         })
         logger.default.timeEnd('Loading user favorites')
         self.isLoading = false
diff --git a/front/src/components/favorites/TrackFavoriteIcon.vue b/front/src/components/favorites/TrackFavoriteIcon.vue
index 5e3e5b07e68bede9300a0b95fb68ac2927a4b936..5abc57a952446b6b9d34e366f09c8af7e617dcd0 100644
--- a/front/src/components/favorites/TrackFavoriteIcon.vue
+++ b/front/src/components/favorites/TrackFavoriteIcon.vue
@@ -1,5 +1,5 @@
  <template>
-  <button @click="favoriteTracks.set(track.id, !isFavorite)" v-if="button" :class="['ui', 'pink', {'inverted': isFavorite}, {'favorited': isFavorite}, 'button']">
+  <button @click="$store.dispatch('favorites/set', {id: track.id, value: !isFavorite})" 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,23 @@
       Add to favorites
     </template>
   </button>
-  <i v-else @click="favoriteTracks.set(track.id, !isFavorite)" :class="['favorite-icon', 'heart', {'pink': isFavorite}, {'favorited': isFavorite}, 'link', 'icon']" :title="title"></i>
+  <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>
 </template>
 
 <script>
-import favoriteTracks from '@/favorites/tracks'
+import {mapState} from 'vuex'
 
 export default {
   props: {
     track: {type: Object},
     button: {type: Boolean, default: false}
   },
-  data () {
-    return {
-      favoriteTracks
-    }
-  },
   computed: {
+    ...mapState({
+      favorites: state => {
+        return state.favorites.tracks
+      }
+    }),
     title () {
       if (this.isFavorite) {
         return 'Remove from favorites'
@@ -33,7 +33,7 @@ export default {
       }
     },
     isFavorite () {
-      return favoriteTracks.objects[this.track.id]
+      return this.$store.getters['favorites/isFavorite'](this.track.id)
     }
   }
 
diff --git a/front/src/components/library/Library.vue b/front/src/components/library/Library.vue
index e8b053b6d0124175fda02254e71a85a420401e5e..3e3de9c61044852d2ae1ba9cb46b5c32a512f3cb 100644
--- a/front/src/components/library/Library.vue
+++ b/front/src/components/library/Library.vue
@@ -4,8 +4,8 @@
       <router-link class="ui item" to="/library" exact>Browse</router-link>
       <router-link class="ui item" to="/library/artists" exact>Artists</router-link>
       <div class="ui secondary right menu">
-        <router-link v-if="auth.user.availablePermissions['import.launch']" class="ui item" to="/library/import/launch" exact>Import</router-link>
-        <router-link v-if="auth.user.availablePermissions['import.launch']" class="ui item" to="/library/import/batches">Import batches</router-link>
+        <router-link v-if="$store.state.auth.availablePermissions['import.launch']" class="ui item" to="/library/import/launch" exact>Import</router-link>
+        <router-link v-if="$store.state.auth.availablePermissions['import.launch']" class="ui item" to="/library/import/batches">Import batches</router-link>
       </div>
     </div>
     <router-view :key="$route.fullPath"></router-view>
@@ -14,15 +14,8 @@
 
 <script>
 
-import auth from '@/auth'
-
 export default {
-  name: 'library',
-  data: function () {
-    return {
-      auth
-    }
-  }
+  name: 'library'
 }
 </script>
 
diff --git a/front/src/components/library/Track.vue b/front/src/components/library/Track.vue
index 36a76e822c2ac0218d8c23a1fb2cca8f21db4f29..48cd801c3d8a96fc34b4904febbacb0d6243d66f 100644
--- a/front/src/components/library/Track.vue
+++ b/front/src/components/library/Track.vue
@@ -61,7 +61,6 @@
 
 <script>
 
-import auth from '@/auth'
 import url from '@/utils/url'
 import logger from '@/logging'
 import backend from '@/audio/backend'
@@ -124,8 +123,8 @@ export default {
     downloadUrl () {
       if (this.track.files.length > 0) {
         let u = backend.absoluteUrl(this.track.files[0].path)
-        if (auth.user.authenticated) {
-          u = url.updateQueryString(u, 'jwt', auth.getAuthToken())
+        if (this.$store.state.auth.authenticated) {
+          u = url.updateQueryString(u, 'jwt', this.$store.state.auth.token)
         }
         return u
       }
diff --git a/front/src/components/metadata/Search.vue b/front/src/components/metadata/Search.vue
index 8a400cf7b0e5b488e794afb62a0ddab8d38ce59a..c3dc7433c5e268baa4e848bcb9a22125b555bd93 100644
--- a/front/src/components/metadata/Search.vue
+++ b/front/src/components/metadata/Search.vue
@@ -23,7 +23,6 @@
 <script>
 import jQuery from 'jquery'
 import config from '@/config'
-import auth from '@/auth'
 
 export default {
   props: {
@@ -66,7 +65,7 @@ export default {
         },
         apiSettings: {
           beforeXHR: function (xhrObject, s) {
-            xhrObject.setRequestHeader('Authorization', auth.getAuthHeader())
+            xhrObject.setRequestHeader('Authorization', this.$store.getters['auth/header'])
             return xhrObject
           },
           onResponse: function (initialResponse) {
diff --git a/front/src/components/radios/Button.vue b/front/src/components/radios/Button.vue
index b334dce561183cb32a06ba8ce5b6955f202ef244..4bf4279890d05f39ac06a350164b9c2101747068 100644
--- a/front/src/components/radios/Button.vue
+++ b/front/src/components/radios/Button.vue
@@ -9,33 +9,28 @@
 
 <script>
 
-import radios from '@/radios'
-
 export default {
   props: {
     type: {type: String, required: true},
     objectId: {type: Number, default: null}
   },
-  data () {
-    return {
-      radios
-    }
-  },
   methods: {
     toggleRadio () {
       if (this.running) {
-        radios.stop()
+        this.$store.dispatch('radios/stop')
       } else {
-        radios.start(this.type, this.objectId)
+        this.$store.dispatch('radios/start', {type: this.type, objectId: this.objectId})
       }
     }
   },
   computed: {
     running () {
-      if (!radios.running) {
+      let state = this.$store.state.radios
+      let current = state.current
+      if (!state.running) {
         return false
       } else {
-        return radios.current.type === this.type & radios.current.objectId === this.objectId
+        return current.type === this.type & current.objectId === this.objectId
       }
     }
   }
diff --git a/front/src/components/radios/Card.vue b/front/src/components/radios/Card.vue
index 1e496324aa893518ca2728fcf6f9f19c168bc9c1..dc8a24ff3c2e31d901ee11fb27eceb976ca7e819 100644
--- a/front/src/components/radios/Card.vue
+++ b/front/src/components/radios/Card.vue
@@ -13,7 +13,6 @@
 </template>
 
 <script>
-import radios from '@/radios'
 import RadioButton from './Button'
 
 export default {
@@ -25,7 +24,7 @@ export default {
   },
   computed: {
     radio () {
-      return radios.types[this.type]
+      return this.$store.getters['radios/types'][this.type]
     }
   }
 }
diff --git a/front/src/favorites/tracks.js b/front/src/favorites/tracks.js
deleted file mode 100644
index 45d05c50d250f06466c3eb3bf610d9536329b28e..0000000000000000000000000000000000000000
--- a/front/src/favorites/tracks.js
+++ /dev/null
@@ -1,57 +0,0 @@
-import config from '@/config'
-import logger from '@/logging'
-import Vue from 'vue'
-
-const REMOVE_URL = config.API_URL + 'favorites/tracks/remove/'
-const FAVORITES_URL = config.API_URL + 'favorites/tracks/'
-
-export default {
-  objects: {},
-  count: 0,
-  set (id, newValue) {
-    let self = this
-    Vue.set(self.objects, id, newValue)
-    if (newValue) {
-      Vue.set(self, 'count', self.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')
-        Vue.set(self.objects, id, !newValue)
-        Vue.set(self, 'count', self.count - 1)
-      })
-    } else {
-      Vue.set(self, 'count', self.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')
-        Vue.set(self.objects, id, !newValue)
-        Vue.set(self, 'count', self.count + 1)
-      })
-    }
-  },
-  toggle (id) {
-    let isFavorite = this.objects[id]
-    this.set(id, !isFavorite)
-  },
-  fetch (url) {
-    // will fetch favorites by batches from API to have them locally
-    var self = this
-    url = url || FAVORITES_URL
-    let resource = Vue.resource(url)
-    resource.get().then((response) => {
-      logger.default.info('Fetched a batch of ' + response.data.results.length + ' favorites')
-      Vue.set(self, 'count', response.data.count)
-      response.data.results.forEach(result => {
-        Vue.set(self.objects, result.track, true)
-      })
-      if (response.data.next) {
-        self.fetch(response.data.next)
-      }
-    })
-  }
-
-}
diff --git a/front/src/main.js b/front/src/main.js
index f153635121ececa77e909defc6defccc8d00c938..0c9230e8e8acc3059bf7bf030f53e74e5a69e204 100644
--- a/front/src/main.js
+++ b/front/src/main.js
@@ -9,8 +9,8 @@ import Vue from 'vue'
 import App from './App'
 import router from './router'
 import VueResource from 'vue-resource'
-import auth from './auth'
 import VueLazyload from 'vue-lazyload'
+import store from './store'
 
 window.$ = window.jQuery = require('jquery')
 
@@ -25,8 +25,8 @@ Vue.config.productionTip = false
 
 Vue.http.interceptors.push(function (request, next) {
   // modify headers
-  if (auth.user.authenticated) {
-    request.headers.set('Authorization', auth.getAuthHeader())
+  if (store.state.auth.authenticated) {
+    request.headers.set('Authorization', store.getters['auth/header'])
   }
   next(function (response) {
     // redirect to login form when we get unauthorized response from server
@@ -37,11 +37,12 @@ Vue.http.interceptors.push(function (request, next) {
   })
 })
 
-auth.checkAuth()
+store.dispatch('auth/check')
 /* eslint-disable no-new */
 new Vue({
   el: '#app',
   router,
+  store,
   template: '<App/>',
   components: { App }
 })
diff --git a/front/src/radios/index.js b/front/src/radios/index.js
deleted file mode 100644
index b468830863f1a443a987149128495413b7b5f000..0000000000000000000000000000000000000000
--- a/front/src/radios/index.js
+++ /dev/null
@@ -1,64 +0,0 @@
-import Vue from 'vue'
-import config from '@/config'
-import logger from '@/logging'
-import queue from '@/audio/queue'
-
-const CREATE_RADIO_URL = config.API_URL + 'radios/sessions/'
-const GET_TRACK_URL = config.API_URL + 'radios/tracks/'
-
-var radios = {
-  types: {
-    random: {
-      name: 'Random',
-      description: "Totally random picks, maybe you'll discover new things?"
-    },
-    favorites: {
-      name: 'Favorites',
-      description: 'Play your favorites tunes in a never-ending happiness loop.'
-    },
-    'less-listened': {
-      name: 'Less listened',
-      description: "Listen to tracks you usually don't. It's time to restore some balance."
-    }
-  },
-  start (type, objectId) {
-    this.current.type = type
-    this.current.objectId = objectId
-    this.running = true
-    let resource = Vue.resource(CREATE_RADIO_URL)
-    var self = this
-    var params = {
-      radio_type: type,
-      related_object_id: objectId
-    }
-    resource.save({}, params).then((response) => {
-      logger.default.info('Successfully started radio ', type)
-      self.current.session = response.data.id
-      queue.populateFromRadio()
-    }, (response) => {
-      logger.default.error('Error while starting radio', type)
-    })
-  },
-  stop () {
-    this.current.type = null
-    this.current.objectId = null
-    this.running = false
-    this.session = null
-  },
-  fetch () {
-    let resource = Vue.resource(GET_TRACK_URL)
-    var self = this
-    var params = {
-      session: self.current.session
-    }
-    return resource.save({}, params)
-  }
-}
-
-Vue.set(radios, 'running', false)
-Vue.set(radios, 'current', {})
-Vue.set(radios.current, 'objectId', null)
-Vue.set(radios.current, 'type', null)
-Vue.set(radios.current, 'session', null)
-
-export default radios
diff --git a/front/src/store/auth.js b/front/src/store/auth.js
new file mode 100644
index 0000000000000000000000000000000000000000..d4b23adcb03401d75721310b22383460d64fe113
--- /dev/null
+++ b/front/src/store/auth.js
@@ -0,0 +1,100 @@
+import Vue from 'vue'
+import config from '@/config'
+import logger from '@/logging'
+import cache from '@/cache'
+import router from '@/router'
+// import favoriteTracks from '@/favorites/tracks'
+
+const LOGIN_URL = config.API_URL + 'token/'
+const USER_PROFILE_URL = config.API_URL + 'users/users/me/'
+
+export default {
+  namespaced: true,
+  state: {
+    authenticated: false,
+    username: '',
+    availablePermissions: {},
+    profile: null,
+    token: ''
+  },
+  getters: {
+    header: state => {
+      return 'JWT ' + state.token
+    }
+  },
+  mutations: {
+    profile: (state, value) => {
+      state.profile = value
+    },
+    authenticated: (state, value) => {
+      state.authenticated = value
+    },
+    username: (state, value) => {
+      state.username = value
+    },
+    token: (state, value) => {
+      state.token = value
+    },
+    permission: (state, {key, status}) => {
+      state.availablePermissions[key] = status
+    }
+  },
+  actions: {
+    // Send a request to the login URL and save the returned JWT
+    login ({commit, dispatch, state}, {next, credentials, onError}) {
+      let resource = Vue.resource(LOGIN_URL)
+      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
+        router.push(next)
+      }, response => {
+        logger.default.error('Error while logging in', response.data)
+        onError(response)
+      })
+    },
+    logout ({commit}) {
+      cache.clear()
+      commit('authenticated', false)
+      commit('profile', null)
+      logger.default.info('Log out, goodbye!')
+      router.push({name: 'index'})
+    },
+    check ({commit, dispatch, state}) {
+      logger.default.info('Checking authentication...')
+      var jwt = cache.get('token')
+      var username = cache.get('username')
+      if (jwt) {
+        commit('authenticated', true)
+        commit('username', username)
+        commit('token', jwt)
+        logger.default.info('Logged back in as ' + username)
+        dispatch('fetchProfile')
+      } else {
+        logger.default.info('Anonymous user')
+        commit('authenticated', false)
+      }
+    },
+    fetchProfile ({commit, dispatch, state}) {
+      let resource = Vue.resource(USER_PROFILE_URL)
+      return resource.get({}).then((response) => {
+        logger.default.info('Successfully fetched user profile')
+        let data = response.data
+        commit('profile', data)
+        dispatch('favorites/fetch', null, {root: true})
+        Object.keys(data.permissions).forEach(function (key) {
+          // this makes it easier to check for permissions in templates
+          commit('permission', {key, status: data.permissions[String(key)].status})
+        })
+        return response.data
+      }, (response) => {
+        logger.default.info('Error while fetching user profile')
+      })
+    }
+  }
+}
diff --git a/front/src/store/favorites.js b/front/src/store/favorites.js
new file mode 100644
index 0000000000000000000000000000000000000000..8bb4bb5afe6add992547b631a91c3683e00fbb6d
--- /dev/null
+++ b/front/src/store/favorites.js
@@ -0,0 +1,78 @@
+import Vue from 'vue'
+import config from '@/config'
+import logger from '@/logging'
+
+const REMOVE_URL = config.API_URL + 'favorites/tracks/remove/'
+const FAVORITES_URL = config.API_URL + 'favorites/tracks/'
+
+export default {
+  namespaced: true,
+  state: {
+    tracks: [],
+    count: 0
+  },
+  mutations: {
+    track: (state, {id, value}) => {
+      if (value) {
+        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
+    }
+  },
+  getters: {
+    isFavorite: (state) => (id) => {
+      return state.tracks.indexOf(id) > -1
+    }
+  },
+  actions: {
+    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)})
+    },
+    fetch ({dispatch, state, commit}, url) {
+      // will fetch favorites by batches from API to have them locally
+      url = url || FAVORITES_URL
+      let resource = Vue.resource(url)
+      resource.get().then((response) => {
+        logger.default.info('Fetched a batch of ' + response.data.results.length + ' favorites')
+        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
new file mode 100644
index 0000000000000000000000000000000000000000..99e466e510f0cad3de2abfdaad4847bd0083defd
--- /dev/null
+++ b/front/src/store/index.js
@@ -0,0 +1,20 @@
+import Vue from 'vue'
+import Vuex from 'vuex'
+
+import favorites from './favorites'
+import auth from './auth'
+import queue from './queue'
+import radios from './radios'
+import player from './player'
+
+Vue.use(Vuex)
+
+export default new Vuex.Store({
+  modules: {
+    auth,
+    favorites,
+    queue,
+    radios,
+    player
+  }
+})
diff --git a/front/src/store/player.js b/front/src/store/player.js
new file mode 100644
index 0000000000000000000000000000000000000000..74b0b9f9ea72dcbc38d0c3cefc6fa90d1647fc49
--- /dev/null
+++ b/front/src/store/player.js
@@ -0,0 +1,91 @@
+import Vue from 'vue'
+import config from '@/config'
+import logger from '@/logging'
+import time from '@/utils/time'
+
+export default {
+  namespaced: true,
+  state: {
+    playing: false,
+    volume: 0.5,
+    duration: 0,
+    currentTime: 0,
+    errored: false,
+    looping: 0 // 0 -> no, 1 -> on  track, 2 -> on queue
+  },
+  mutations: {
+    volume (state, value) {
+      value = parseFloat(value)
+      value = Math.min(value, 1)
+      value = Math.max(value, 0)
+      state.volume = value
+    },
+    incrementVolume (state, value) {
+      value = parseFloat(state.volume + value)
+      value = Math.min(value, 1)
+      value = Math.max(value, 0)
+      state.volume = value
+    },
+    duration (state, value) {
+      state.duration = value
+    },
+    errored (state, value) {
+      state.errored = value
+    },
+    currentTime (state, value) {
+      state.currentTime = value
+    },
+    looping (state, value) {
+      state.looping = value
+    },
+    playing (state, value) {
+      state.playing = value
+    },
+    toggleLooping (state) {
+      if (state.looping > 1) {
+        state.looping = 0
+      } else {
+        state.looping += 1
+      }
+    }
+  },
+  getters: {
+    durationFormatted: state => {
+      return time.parse(Math.round(state.duration))
+    },
+    currentTimeFormatted: state => {
+      return time.parse(Math.round(state.currentTime))
+    },
+    progress: state => {
+      return Math.round(state.currentTime / state.duration * 100)
+    }
+  },
+  actions: {
+    incrementVolume (context, value) {
+      context.commit('volume', context.state.volume + value)
+    },
+    stop (context) {
+    },
+    togglePlay ({commit, state}) {
+      commit('playing', !state.playing)
+    },
+    trackListened ({commit}, 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')
+      })
+    },
+    trackEnded ({dispatch}, track) {
+      dispatch('trackListened', track)
+      dispatch('queue/next', null, {root: true})
+    },
+    trackErrored ({commit, dispatch}) {
+      commit('errored', true)
+      dispatch('queue/next', null, {root: true})
+    },
+    updateProgress ({commit}, t) {
+      commit('currentTime', t)
+    }
+  }
+}
diff --git a/front/src/store/queue.js b/front/src/store/queue.js
new file mode 100644
index 0000000000000000000000000000000000000000..3a0b7dd7979471f647dd5743602fb1b8fd5ea0bd
--- /dev/null
+++ b/front/src/store/queue.js
@@ -0,0 +1,153 @@
+import logger from '@/logging'
+import _ from 'lodash'
+
+export default {
+  namespaced: true,
+  state: {
+    tracks: [],
+    currentIndex: -1,
+    ended: true,
+    previousQueue: null
+  },
+  mutations: {
+    currentIndex (state, value) {
+      state.currentIndex = value
+    },
+    ended (state, value) {
+      state.ended = value
+    },
+    splice (state, {start, size}) {
+      state.tracks.splice(start, size)
+    },
+    tracks (state, value) {
+      state.tracks = value
+    },
+    insert (state, {track, index}) {
+      state.tracks.splice(index, 0, track)
+    },
+    reorder (state, {oldIndex, newIndex}) {
+      // called when the user uses drag / drop to reorder
+      // tracks in queue
+      if (oldIndex === state.currentIndex) {
+        state.currentIndex = newIndex
+        return
+      }
+      if (oldIndex < state.currentIndex && newIndex >= state.currentIndex) {
+        // item before was moved after
+        state.currentIndex -= 1
+      }
+      if (oldIndex > state.currentIndex && newIndex <= state.currentIndex) {
+        // item after was moved before
+        state.currentIndex += 1
+      }
+    }
+
+  },
+  getters: {
+    currentTrack: state => {
+      return state.tracks[state.currentIndex]
+    },
+    hasNext: state => {
+      return state.currentIndex < state.tracks.length - 1
+    },
+    hasPrevious: state => {
+      return state.currentIndex > 0
+    }
+  },
+  actions: {
+    append (context, {track, index, skipPlay}) {
+      index = index || context.state.tracks.length
+      if (index > context.state.tracks.length - 1) {
+        // we simply push to the end
+        context.commit('insert', {track, index: context.state.tracks.length})
+      } else {
+        // we insert the track at given position
+        context.commit('insert', {track, index})
+      }
+      if (!skipPlay) {
+        context.dispatch('resume')
+      }
+      // this.cache()
+    },
+
+    appendMany (context, {tracks, index}) {
+      logger.default.info('Appending many tracks to the queue', tracks.map(e => { return e.title }))
+      if (context.state.tracks.length === 0) {
+        index = 0
+      } else {
+        index = index || context.state.tracks.length
+      }
+      tracks.forEach((t) => {
+        context.dispatch('append', {track: t, index: index, skipPlay: true})
+        index += 1
+      })
+      context.dispatch('resume')
+    },
+
+    cleanTrack ({state, dispatch, commit}, index) {
+      // are we removing current playin track
+      let current = index === state.currentIndex
+      if (current) {
+        dispatch('player/stop', null, {root: true})
+      }
+      if (index < state.currentIndex) {
+        dispatch('currentIndex', state.currentIndex - 1)
+      }
+      commit('splice', {start: index, size: 1})
+      if (current) {
+        // we play next track, which now have the same index
+        dispatch('currentIndex', index)
+      }
+    },
+
+    resume (context) {
+      if (context.state.ended | context.rootState.player.errored) {
+        context.dispatch('next')
+      }
+    },
+    previous (context) {
+      if (context.state.currentIndex > 0) {
+        context.dispatch('currentIndex', context.state.currentIndex - 1)
+      }
+    },
+    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)
+      } else {
+        if (state.currentIndex < state.tracks.length - 1) {
+          logger.default.debug('Playing next track')
+          return dispatch('currentIndex', state.currentIndex + 1)
+        } else {
+          commit('ended', true)
+        }
+      }
+    },
+    currentIndex ({commit, state, rootState, dispatch}, index) {
+      commit('ended', false)
+      commit('player/errored', false, {root: true})
+      commit('currentIndex', index)
+      if (state.tracks.length - index <= 2 && rootState.radios.running) {
+        dispatch('radios/populateQueue', null, {root: true})
+      }
+    },
+    clean ({dispatch, commit}) {
+      dispatch('player/stop', null, {root: true})
+      // radios.stop()
+      commit('tracks', [])
+      dispatch('currentIndex', -1)
+      // so we replay automatically on next track append
+      commit('ended', true)
+    },
+    shuffle ({dispatch, commit, state}) {
+      let shuffled = _.shuffle(state.tracks)
+      commit('tracks', [])
+      dispatch('appendMany', {tracks: shuffled})
+    }
+  }
+}
diff --git a/front/src/store/radios.js b/front/src/store/radios.js
new file mode 100644
index 0000000000000000000000000000000000000000..a9c429876a4ff974635de9f73062dd2597237209
--- /dev/null
+++ b/front/src/store/radios.js
@@ -0,0 +1,78 @@
+import Vue from 'vue'
+import config from '@/config'
+import logger from '@/logging'
+
+const CREATE_RADIO_URL = config.API_URL + 'radios/sessions/'
+const GET_TRACK_URL = config.API_URL + 'radios/tracks/'
+
+export default {
+  namespaced: true,
+  state: {
+    current: null,
+    running: false
+  },
+  getters: {
+    types: state => {
+      return {
+        random: {
+          name: 'Random',
+          description: "Totally random picks, maybe you'll discover new things?"
+        },
+        favorites: {
+          name: 'Favorites',
+          description: 'Play your favorites tunes in a never-ending happiness loop.'
+        },
+        'less-listened': {
+          name: 'Less listened',
+          description: "Listen to tracks you usually don't. It's time to restore some balance."
+        }
+      }
+    }
+  },
+  mutations: {
+    current: (state, value) => {
+      state.current = value
+    },
+    running: (state, value) => {
+      state.running = value
+    }
+  },
+  actions: {
+    start ({commit, dispatch}, {type, objectId}) {
+      let resource = Vue.resource(CREATE_RADIO_URL)
+      var params = {
+        radio_type: type,
+        related_object_id: objectId
+      }
+      resource.save({}, params).then((response) => {
+        logger.default.info('Successfully started radio ', type)
+        commit('current', {type, objectId, session: response.data.id})
+        commit('running', true)
+        dispatch('populateQueue')
+      }, (response) => {
+        logger.default.error('Error while starting radio', type)
+      })
+    },
+    stop ({commit}) {
+      commit('current', null)
+      commit('running', false)
+    },
+    populateQueue ({state, dispatch}) {
+      if (!state.running) {
+        return
+      }
+      let resource = Vue.resource(GET_TRACK_URL)
+      var params = {
+        session: state.current.session
+      }
+      let promise = resource.save({}, params)
+      promise.then((response) => {
+        logger.default.info('Adding track to queue from radio')
+        dispatch('queue/append', {track: response.data.track}, {root: true})
+      }, (response) => {
+        logger.default.error('Error while adding track to queue from radio')
+      })
+    }
+  }
+
+}