diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 58602d296056e4c8fe131baf29252df9fa4c0d1f..24b23b84b104ccc2bc9e5fa5860ed1c31ab152c2 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -80,6 +80,21 @@ docker_develop:
   tags:
     - dind
 
+build_api:
+  # Simply publish a zip containing api/ directory
+  stage: deploy
+  image: busybox
+  artifacts:
+    name: "api_${CI_COMMIT_REF_NAME}"
+    paths:
+      - api
+  script: echo Done!
+  only:
+    - tags
+    - master
+    - develop
+
+
 docker_release:
   stage: deploy
   before_script:
diff --git a/api/config/settings/common.py b/api/config/settings/common.py
index 70804c3c9dd2144783746c7a69596edb75be8350..3f7cc7503385fae3cbf5b030386c510844ed74e0 100644
--- a/api/config/settings/common.py
+++ b/api/config/settings/common.py
@@ -260,6 +260,18 @@ BROKER_URL = env("CELERY_BROKER_URL", default='django://')
 ########## END CELERY
 
 
+CACHES = {
+    "default": {
+        "BACKEND": "django_redis.cache.RedisCache",
+        "LOCATION": "{0}/{1}".format(env.cache_url('REDIS_URL', default="redis://127.0.0.1:6379"), 0),
+        "OPTIONS": {
+            "CLIENT_CLASS": "django_redis.client.DefaultClient",
+            "IGNORE_EXCEPTIONS": True,  # mimics memcache behavior.
+                                        # http://niwinz.github.io/django-redis/latest/#_memcached_exceptions_behavior
+        }
+    }
+}
+
 # Location of root django.contrib.admin URL, use {% url 'admin:index' %}
 ADMIN_URL = r'^admin/'
 # Your common stuff: Below this line define 3rd party library settings
@@ -301,7 +313,8 @@ REST_FRAMEWORK = {
 }
 
 ATOMIC_REQUESTS = False
-
+USE_X_FORWARDED_HOST = True
+USE_X_FORWARDED_PORT = True
 # Wether we should check user permission before serving audio files (meaning
 # return an obfuscated url)
 # This require a special configuration on the reverse proxy side
diff --git a/api/config/settings/local.py b/api/config/settings/local.py
index 762ffe7aaf3ccc90a9ac27c6f9db1f35da50ff77..e8108e98bd63bc2fc3b51c111e268b260f258dc6 100644
--- a/api/config/settings/local.py
+++ b/api/config/settings/local.py
@@ -28,14 +28,6 @@ EMAIL_PORT = 1025
 EMAIL_BACKEND = env('DJANGO_EMAIL_BACKEND',
                     default='django.core.mail.backends.console.EmailBackend')
 
-# CACHING
-# ------------------------------------------------------------------------------
-CACHES = {
-    'default': {
-        'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
-        'LOCATION': ''
-    }
-}
 
 # django-debug-toolbar
 # ------------------------------------------------------------------------------
diff --git a/api/config/settings/production.py b/api/config/settings/production.py
index 937328d1f9994817ea598281298214b99d08da2d..e8a05bd3b6c5a34e442480539e39716a3f1baad4 100644
--- a/api/config/settings/production.py
+++ b/api/config/settings/production.py
@@ -100,17 +100,7 @@ DATABASES['default'] = env.db("DATABASE_URL")
 # CACHING
 # ------------------------------------------------------------------------------
 # Heroku URL does not pass the DB number, so we parse it in
-CACHES = {
-    "default": {
-        "BACKEND": "django_redis.cache.RedisCache",
-        "LOCATION": "{0}/{1}".format(env.cache_url('REDIS_URL', default="redis://127.0.0.1:6379"), 0),
-        "OPTIONS": {
-            "CLIENT_CLASS": "django_redis.client.DefaultClient",
-            "IGNORE_EXCEPTIONS": True,  # mimics memcache behavior.
-                                        # http://niwinz.github.io/django-redis/latest/#_memcached_exceptions_behavior
-        }
-    }
-}
+
 
 
 # LOGGING CONFIGURATION
diff --git a/api/funkwhale_api/__init__.py b/api/funkwhale_api/__init__.py
index 70d6b5ac1643438d12f9f87edc04735728a0dcfe..c46b89cc90dbd429220a9792dfd84b2d9654d530 100644
--- a/api/funkwhale_api/__init__.py
+++ b/api/funkwhale_api/__init__.py
@@ -1,3 +1,3 @@
 # -*- coding: utf-8 -*-
-__version__ = '0.2.0'
+__version__ = '0.2.1'
 __version_info__ = tuple([int(num) if num.isdigit() else num for num in __version__.replace('-', '.', 1).split('.')])
diff --git a/api/funkwhale_api/music/models.py b/api/funkwhale_api/music/models.py
index 6a55dfc00c385f18da92c7f6c0da49458c16402e..596f890b7e43b609d036c4a46040973544eaf2fd 100644
--- a/api/funkwhale_api/music/models.py
+++ b/api/funkwhale_api/music/models.py
@@ -27,6 +27,7 @@ class APIModelMixin(models.Model):
     api_includes = []
     creation_date = models.DateTimeField(default=timezone.now)
     import_hooks = []
+
     class Meta:
         abstract = True
         ordering = ['-creation_date']
@@ -291,6 +292,9 @@ class Track(APIModelMixin):
     ]
     tags = TaggableManager()
 
+    class Meta:
+        ordering = ['album', 'position']
+
     def __str__(self):
         return self.title
 
@@ -358,6 +362,12 @@ class TrackFile(models.Model):
                 'api:v1:trackfiles-serve', kwargs={'pk': self.pk})
         return self.audio_file.url
 
+    @property
+    def filename(self):
+        return '{}{}'.format(
+            self.track.full_name,
+            os.path.splitext(self.audio_file.name)[-1])
+
 
 class ImportBatch(models.Model):
     creation_date = models.DateTimeField(default=timezone.now)
@@ -386,6 +396,8 @@ class ImportJob(models.Model):
     )
     status = models.CharField(choices=STATUS_CHOICES, default='pending', max_length=30)
 
+    class Meta:
+        ordering = ('id', )
     @celery.app.task(name='ImportJob.run', filter=celery.task_method)
     def run(self, replace=False):
         try:
diff --git a/api/funkwhale_api/music/serializers.py b/api/funkwhale_api/music/serializers.py
index e7d7399ad01ffeddc5e9bec5d452c05b4c56f3d9..744115f866ff43137be1b61cf47671fec4f414ae 100644
--- a/api/funkwhale_api/music/serializers.py
+++ b/api/funkwhale_api/music/serializers.py
@@ -31,11 +31,20 @@ class ImportBatchSerializer(serializers.ModelSerializer):
         model = models.ImportBatch
         fields = ('id', 'jobs', 'status', 'creation_date')
 
+
 class TrackFileSerializer(serializers.ModelSerializer):
+    path = serializers.SerializerMethodField()
+
     class Meta:
         model = models.TrackFile
-        fields = ('id', 'path', 'duration', 'source')
+        fields = ('id', 'path', 'duration', 'source', 'filename')
 
+    def get_path(self, o):
+        request = self.context.get('request')
+        url = o.path
+        if request:
+            url = request.build_absolute_uri(url)
+        return url
 
 class SimpleAlbumSerializer(serializers.ModelSerializer):
 
@@ -62,7 +71,15 @@ class TrackSerializer(LyricsMixin):
     tags = TagSerializer(many=True, read_only=True)
     class Meta:
         model = models.Track
-        fields = ('id', 'mbid', 'title', 'artist', 'files', 'tags', 'lyrics')
+        fields = (
+            'id',
+            'mbid',
+            'title',
+            'artist',
+            'files',
+            'tags',
+            'position',
+            'lyrics')
 
 class TrackSerializerNested(LyricsMixin):
     artist = ArtistSerializer()
diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py
index 4a4032c57f80695ba88fb92826941be3ede23605..98319255270bfdae84f70e3335fc4917f650885b 100644
--- a/api/funkwhale_api/music/views.py
+++ b/api/funkwhale_api/music/views.py
@@ -139,9 +139,8 @@ class TrackFileViewSet(viewsets.ReadOnlyModelViewSet):
             return Response(status=404)
 
         response = Response()
-        filename = "filename*=UTF-8''{}{}".format(
-            urllib.parse.quote(f.track.full_name),
-            os.path.splitext(f.audio_file.name)[-1])
+        filename = "filename*=UTF-8''{}".format(
+            urllib.parse.quote(f.filename))
         response["Content-Disposition"] = "attachment; {}".format(filename)
         response['X-Accel-Redirect'] = "{}{}".format(
             settings.PROTECT_FILES_PATH,
diff --git a/api/requirements/base.txt b/api/requirements/base.txt
index bdf17cf9a6aaa2473fe66bf9a4b93b7ea90697cc..e7bc870cfd85efeff22033e71f583536592b7ab9 100644
--- a/api/requirements/base.txt
+++ b/api/requirements/base.txt
@@ -55,4 +55,4 @@ mutagen==1.38
 # Until this is merged
 git+https://github.com/EliotBerriot/PyMemoize.git@django
 
-django-dynamic-preferences>=1.2,<1.3
+django-dynamic-preferences>=1.3,<1.4
diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml
index 4ffede783e55cc2b07ec010260b436f278ca1700..0c3b565474355632257694bfe84d729a376301da 100644
--- a/deploy/docker-compose.yml
+++ b/deploy/docker-compose.yml
@@ -28,7 +28,7 @@ services:
       - C_FORCE_ROOT=true
     volumes:
       - ./data/music:/music:ro
-      - ./api/media:/app/funkwhale_api/media
+      - ./data/media:/app/funkwhale_api/media
 
   celerybeat:
     restart: unless-stopped
diff --git a/deploy/nginx.conf b/deploy/nginx.conf
index 6a0a9f50936f32dfc9c29cbf29ac58ed1658e473..a85230ae8d6b9b629760154117584aef08d949fe 100644
--- a/deploy/nginx.conf
+++ b/deploy/nginx.conf
@@ -41,6 +41,8 @@ server {
         proxy_set_header X-Real-IP $remote_addr;
         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
         proxy_set_header X-Forwarded-Proto $scheme;
+        proxy_set_header X-Forwarded-Host   $host:$server_port;
+        proxy_set_header X-Forwarded-Port   $server_port;
         proxy_redirect off;
         proxy_pass   http://funkwhale-api/api/;
     }
diff --git a/dev.yml b/dev.yml
index f0fc8845a085e6ed27251b3a7cd24e3006b0db91..78bf76fcd30f479ffbd552dc127a5262f48cdf58 100644
--- a/dev.yml
+++ b/dev.yml
@@ -63,4 +63,4 @@ services:
       - ./docker/nginx/conf.dev:/etc/nginx/nginx.conf
       - ./api/funkwhale_api/media:/protected/media
     ports:
-      - "0.0.0.0:6001:80"
+      - "0.0.0.0:6001:6001"
diff --git a/docker/nginx/conf.dev b/docker/nginx/conf.dev
index 6ca395fb1aaf47fbfb1a09bfd491daddc5f28875..9c00fd76fc577c417e489c33a731baba1a2f9980 100644
--- a/docker/nginx/conf.dev
+++ b/docker/nginx/conf.dev
@@ -28,7 +28,7 @@ http {
     #gzip  on;
 
     server {
-        listen 80;
+        listen 6001;
         charset     utf-8;
 
         location /_protected/media {
@@ -40,6 +40,8 @@ http {
             proxy_set_header X-Real-IP $remote_addr;
             proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
             proxy_set_header X-Forwarded-Proto $scheme;
+            proxy_set_header X-Forwarded-Host   $host:$server_port;
+            proxy_set_header X-Forwarded-Port   $server_port;
             proxy_redirect off;
             proxy_pass   http://api:12081/;
         }
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 6e609aac973e5aabf6501a879bc7d98502450eb4..47bf0ed9cd4688db5806606d19555c7cfe7ab94a 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -1,6 +1,18 @@
 Changelog
 =========
 
+0.2.1
+-----
+
+2017-07-17
+
+* Now return media files with absolute URL
+* Now display CLI instructions to download a set of tracks
+* Fixed #33: sort by track position in album in API by default, also reuse that information on frontend side
+* More robust audio player and queue in various situations:
+* upgrade to latest dynamic_preferences and use redis as cache even locally
+
+
 0.2
 -------
 
diff --git a/front/src/audio/index.js b/front/src/audio/index.js
index 48f61044399795977b90f0f4a8b767f07c235ff6..7750ee500a47eb9d4074645642a3e121f8885b12 100644
--- a/front/src/audio/index.js
+++ b/front/src/audio/index.js
@@ -124,9 +124,9 @@ class Audio {
   }
 
   play () {
-    logger.default.info('Playing track')
     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
diff --git a/front/src/audio/queue.js b/front/src/audio/queue.js
index efa3dcdf7d6ba5ee22694b1e5d0ac160924d8c7c..25b27f00ea7e677ee9d4973ae876413488cbb17a 100644
--- a/front/src/audio/queue.js
+++ b/front/src/audio/queue.js
@@ -123,6 +123,7 @@ class Queue {
       this.tracks.splice(index, 0, track)
     }
     if (this.ended) {
+      logger.default.debug('Playing appended track')
       this.play(this.currentIndex + 1)
     }
     this.cache()
@@ -152,19 +153,31 @@ class Queue {
 
   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) {
-    if (index === this.currentIndex) {
+    // 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 () {
@@ -172,12 +185,17 @@ class Queue {
     this.audio.destroyed()
   }
   play (index) {
-    if (this.audio.destroyed) {
-      logger.default.debug('Destroying previous audio...')
-      this.audio.destroyed()
+    let self = this
+    let currentIndex = index
+    let currentTrack = this.tracks[index]
+    if (!currentTrack) {
+      logger.default.debug('No track at index', index)
+      return
     }
-    this.currentIndex = index
-    this.currentTrack = this.tracks[index]
+
+    this.currentIndex = currentIndex
+    this.currentTrack = currentTrack
+
     this.ended = false
     let file = this.currentTrack.files[0]
     if (!file) {
@@ -193,7 +211,11 @@ class Queue {
       path = url.updateQueryString(path, 'jwt', auth.getAuthToken())
     }
 
-    this.audio = new Audio(path, {
+    if (this.audio.destroyed) {
+      logger.default.debug('Destroying previous audio...', index - 1)
+      this.audio.destroyed()
+    }
+    let audio = new Audio(path, {
       preload: true,
       autoplay: true,
       rate: 1,
@@ -201,6 +223,17 @@ class Queue {
       volume: this.state.volume,
       onEnded: this.handleAudioEnded.bind(this)
     })
+    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()
     }
diff --git a/front/src/components/audio/Player.vue b/front/src/components/audio/Player.vue
index 466ead0e8369a04716d6d6ba6380be09e899e6fd..b72f37c5c83ff741ba336d2be1d51dca595845eb 100644
--- a/front/src/components/audio/Player.vue
+++ b/front/src/components/audio/Player.vue
@@ -24,7 +24,7 @@
         </div>
       </div>
     </div>
-    <div class="progress-area">
+    <div class="progress-area" v-if="queue.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>
diff --git a/front/src/components/audio/album/Card.vue b/front/src/components/audio/album/Card.vue
index fcdf1622d039fe2ae0cf0f5420dd70efaf1a638e..7fd60d963b512ae637faa7c7dc8acbb2f9021d80 100644
--- a/front/src/components/audio/album/Card.vue
+++ b/front/src/components/audio/album/Card.vue
@@ -22,6 +22,9 @@
                 </td>
                 <td colspan="6">
                   <router-link class="track discrete link" :to="{name: 'library.track', params: {id: track.id }}">
+                    <template v-if="track.position">
+                      {{ track.position }}.
+                    </template>
                     {{ track.title }}
                   </router-link>
                 </td>
diff --git a/front/src/components/audio/track/Table.vue b/front/src/components/audio/track/Table.vue
index 6898353d89810ee4952b161c5701c700733df13d..e9beaa05a5ac8f0bb6285597a9316eb8afab4e4f 100644
--- a/front/src/components/audio/track/Table.vue
+++ b/front/src/components/audio/track/Table.vue
@@ -20,9 +20,12 @@
           <img class="ui mini image" v-else src="../../..//assets/audio/default-cover.png">
         </td>
         <td colspan="6">
-            <router-link class="track" :to="{name: 'library.track', params: {id: track.id }}">
-              {{ track.title }}
-            </router-link>
+          <router-link class="track" :to="{name: 'library.track', params: {id: track.id }}">
+            <template v-if="displayPosition && track.position">
+              {{ track.position }}.
+            </template>
+            {{ track.title }}
+          </router-link>
         </td>
         <td colspan="6">
           <router-link class="artist discrete link" :to="{name: 'library.artist', params: {id: track.artist.id }}">
@@ -37,23 +40,70 @@
         <td><track-favorite-icon class="favorite-icon" :track="track"></track-favorite-icon></td>
       </tr>
     </tbody>
+    <tfoot class="full-width">
+      <tr>
+        <th colspan="3">
+          <button @click="showDownloadModal = !showDownloadModal" class="ui basic button">Download...</button>
+          <modal :show.sync="showDownloadModal">
+            <div class="header">
+              Download tracks
+            </div>
+            <div class="content">
+              <div class="description">
+                <p>There is currently no way to download directly multiple tracks from funkwhale as a ZIP archive.
+                  However, you can use a command line tools such as <a href="https://curl.haxx.se/" target="_blank">cURL</a> to easily download a list of tracks.
+                </p>
+                <p>Simply copy paste the snippet below into a terminal to launch the download.</p>
+                <div class="ui warning message">
+                  Keep your PRIVATE_TOKEN secret as it gives access to your account.
+                </div>
+                <pre>
+export PRIVATE_TOKEN="{{ auth.getAuthToken ()}}"
+<template v-for="track in tracks">
+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>
+</pre>
+              </div>
+            </div>
+            <div class="actions">
+              <div class="ui black deny button">
+                Cancel
+              </div>
+            </div>
+          </modal>
+        </th>
+        <th></th>
+        <th colspan="4"></th>
+        <th colspan="6"></th>
+        <th colspan="6"></th>
+        <th></th>
+      </tr>
+    </tfoot>
   </table>
 </template>
 
 <script>
 import backend from '@/audio/backend'
+import auth from '@/auth'
 import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon'
 import PlayButton from '@/components/audio/PlayButton'
 
+import Modal from '@/components/semantic/Modal'
+
 export default {
-  props: ['tracks'],
+  props: {
+    tracks: {type: Array, required: true},
+    displayPosition: {type: Boolean, default: false}
+  },
   components: {
+    Modal,
     TrackFavoriteIcon,
     PlayButton
   },
   data () {
     return {
-      backend: backend
+      backend: backend,
+      auth: auth,
+      showDownloadModal: false
     }
   }
 }
diff --git a/front/src/components/library/Album.vue b/front/src/components/library/Album.vue
index 5cc4d027159625f7807b01b73840886ee2285b4e..cf3403400b8a07afc56cd0ca0edd7874ca8bacf6 100644
--- a/front/src/components/library/Album.vue
+++ b/front/src/components/library/Album.vue
@@ -34,7 +34,7 @@
       </div>
       <div class="ui vertical stripe segment">
         <h2>Tracks</h2>
-        <track-table v-if="album" :tracks="album.tracks"></track-table>
+        <track-table v-if="album" :display-position="true" :tracks="album.tracks"></track-table>
       </div>
     </template>
   </div>
diff --git a/front/src/components/semantic/Modal.vue b/front/src/components/semantic/Modal.vue
new file mode 100644
index 0000000000000000000000000000000000000000..ec7a5a0884262ac2437570cd6a360e231eaccaba
--- /dev/null
+++ b/front/src/components/semantic/Modal.vue
@@ -0,0 +1,53 @@
+<template>
+  <div :class="['ui', {'active': show}, 'modal']">
+    <i class="close icon"></i>
+    <slot>
+      
+    </slot>
+  </div>
+</template>
+
+<script>
+import $ from 'jquery'
+
+export default {
+  props: {
+    show: {type: Boolean, required: true}
+  },
+  data () {
+    return {
+      control: null
+    }
+  },
+  mounted () {
+    this.control = $(this.$el).modal({
+      onApprove: function () {
+        this.$emit('approved')
+      }.bind(this),
+      onDeny: function () {
+        this.$emit('deny')
+      }.bind(this),
+      onHidden: function () {
+        this.$emit('update:show', false)
+      }.bind(this)
+    })
+  },
+  watch: {
+    show: {
+      handler (newValue) {
+        if (newValue) {
+          this.control.modal('show')
+        } else {
+          this.control.modal('hide')
+        }
+      }
+    }
+  }
+
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped lang="scss">
+
+</style>