diff --git a/.env.dev b/.env.dev
index 3f904078c36d0a5fa1392356d83a5e1e0d493204..c09262509296ad91911a4bf75989e119117c9315 100644
--- a/.env.dev
+++ b/.env.dev
@@ -9,3 +9,4 @@ FUNKWHALE_HOSTNAME=localhost
 FUNKWHALE_PROTOCOL=http
 PYTHONDONTWRITEBYTECODE=true
 WEBPACK_DEVSERVER_PORT=8080
+MUSIC_DIRECTORY_PATH=/music
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 217047794ba8c2974dd1fa69396674cd11aea726..6a0f4b9d89ef23ee59872c3f0ea7d19225e16182 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -70,7 +70,9 @@ build_front:
     - yarn install
     - yarn run i18n-extract
     - yarn run i18n-compile
-    - yarn run build
+    # this is to ensure we don't have any errors in the output,
+    # cf https://code.eliotberriot.com/funkwhale/funkwhale/issues/169
+    - yarn run build | tee /dev/stderr | (! grep -i 'ERROR in')
   cache:
     key: "$CI_PROJECT_ID__front_dependencies"
     paths:
@@ -81,9 +83,9 @@ build_front:
     paths:
       - front/dist/
   only:
-    - tags
-    - master
-    - develop
+    - tags@funkwhale/funkwhale
+    - master@funkwhale/funkwhale
+    - develop@funkwhale/funkwhale
   tags:
     - docker
 
@@ -100,7 +102,7 @@ pages:
     paths:
       - public
   only:
-    - develop
+    - develop@funkwhale/funkwhale
   tags:
     - docker
 
@@ -114,7 +116,7 @@ docker_develop:
     - docker build -t $IMAGE .
     - docker push $IMAGE
   only:
-    - develop
+    - develop@funkwhale/funkwhale
   tags:
     - dind
 
@@ -128,9 +130,9 @@ build_api:
       - api
   script: echo Done!
   only:
-    - tags
-    - master
-    - develop
+    - tags@funkwhale/funkwhale
+    - master@funkwhale/funkwhale
+    - develop@funkwhale/funkwhale
 
 
 docker_release:
@@ -144,6 +146,6 @@ docker_release:
     - docker push $IMAGE
     - docker push $IMAGE_LATEST
   only:
-    - tags
+    - tags@funkwhale/funkwhale
   tags:
     - dind
diff --git a/CHANGELOG b/CHANGELOG
index b230b1556bf7e35ceb38e4529d360a94a5d835bd..c56d58836179a84d0af30ad333764a55d800a136 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -3,6 +3,88 @@ Changelog
 
 .. towncrier
 
+0.10 (2018-04-23)
+-----------------
+
+Features:
+
+- Can now import files in-place from the CLI importer (#155)
+
+
+Enhancements:
+
+- Avoid downloading audio files multiple times from remote libraries (#163)
+- Better file import performance and error handling (#144)
+- Import job and batch API and front-end have been improved with better
+  performance, pagination and additional filters (#171)
+- Increased max_length on TrackFile.source, this will help when importing files
+  with a really long path (#142)
+- Player is back in Queue tab (#150)
+
+
+Bugfixes:
+
+- Fail graciously when AP representation includes a null_value for mediaType
+- Fix sidebar tabs not showing under small resolution under Chrome (#173)
+- Fixed broken login due to badly configured Axios (#172)
+- Fixed broken playlist modal after login (#155)
+- Fixed queue reorder or track deletion restarting currently playing track
+  (#151)
+- Radio will now append new track if you delete the last track in queue (#145)
+- Reset all sensitive front-end data on logout (#124)
+- Typos/not showing text due to i18n work (#175)
+
+
+Documentation:
+
+- Better documentation for hardware requirements and memory usage (#165)
+
+
+In-place import
+^^^^^^^^^^^^^^^
+
+This release includes in-place imports for the CLI import. This means you can
+load gigabytes of music into funkwhale without worrying about about Funkwhale
+copying those music files in its internal storage and eating your disk space.
+
+`This new feature is documented here <https://docs.funkwhale.audio/importing-music.html#in-place-import>`_
+and require additional configuration to ensure funkwhale and your webserver can
+serve those files properly.
+
+**Non-docker users:**
+
+Assuming your music is stored in ``/srv/funkwhale/data/music``, add the following
+block to your nginx configuration::
+
+    location /_protected/music {
+        internal;
+        alias   /srv/funkwhale/data/music;
+    }
+
+And the following to your .env file::
+
+    MUSIC_DIRECTORY_PATH=/srv/funkwhale/data/music
+
+**Docker users:**
+
+Assuming your music is stored in ``/srv/funkwhale/data/music``, add the following
+block to your nginx configuration::
+
+    location /_protected/music {
+        internal;
+        alias   /srv/funkwhale/data/music;
+    }
+
+Assuming you have the following volume directive in your ``docker-compose.yml``
+(it's the default): ``/srv/funkwhale/data/music:/music:ro``, then add
+the following to your .env file::
+
+    # this is the path in the container
+    MUSIC_DIRECTORY_PATH=/music
+    # this is the path on the host
+    MUSIC_DIRECTORY_SERVE_PATH=/srv/funkwhale/data/music
+
+
 0.9.1 (2018-04-17)
 ------------------
 
diff --git a/api/config/settings/common.py b/api/config/settings/common.py
index a972ec7effc9e5696f94833c19c52096ff307f14..de1d653cb91fa3518273b8b103ed74fdee4b9259 100644
--- a/api/config/settings/common.py
+++ b/api/config/settings/common.py
@@ -390,6 +390,12 @@ REST_FRAMEWORK = {
 ATOMIC_REQUESTS = False
 USE_X_FORWARDED_HOST = True
 USE_X_FORWARDED_PORT = True
+
+# Wether we should use Apache, Nginx (or other) headers when serving audio files
+# Default to Nginx
+REVERSE_PROXY_TYPE = env('REVERSE_PROXY_TYPE', default='nginx')
+assert REVERSE_PROXY_TYPE in ['apache2', 'nginx'], 'Unsupported REVERSE_PROXY_TYPE'
+
 # 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
@@ -441,3 +447,9 @@ EXTERNAL_REQUESTS_VERIFY_SSL = env.bool(
     'EXTERNAL_REQUESTS_VERIFY_SSL',
     default=True
 )
+
+MUSIC_DIRECTORY_PATH = env('MUSIC_DIRECTORY_PATH', default=None)
+# on Docker setup, the music directory may not match the host path,
+# and we need to know it for it to serve stuff properly
+MUSIC_DIRECTORY_SERVE_PATH = env(
+    'MUSIC_DIRECTORY_SERVE_PATH', default=MUSIC_DIRECTORY_PATH)
diff --git a/api/funkwhale_api/__init__.py b/api/funkwhale_api/__init__.py
index f3a544e460a55bd0399a00d7ec62f70aed20f12c..596926919170b9efbf5914a51010f9da8f67751d 100644
--- a/api/funkwhale_api/__init__.py
+++ b/api/funkwhale_api/__init__.py
@@ -1,3 +1,3 @@
 # -*- coding: utf-8 -*-
-__version__ = '0.9.1'
+__version__ = '0.10'
 __version_info__ = tuple([int(num) if num.isdigit() else num for num in __version__.replace('-', '.', 1).split('.')])
diff --git a/api/funkwhale_api/federation/serializers.py b/api/funkwhale_api/federation/serializers.py
index 735a101b40e3e615602c2586ef3bcb8cded638cf..00bb7d45b0b1b98176d74f58075866392a53898c 100644
--- a/api/funkwhale_api/federation/serializers.py
+++ b/api/funkwhale_api/federation/serializers.py
@@ -708,23 +708,7 @@ class AudioSerializer(serializers.Serializer):
         except (KeyError, TypeError):
             raise serializers.ValidationError('Missing mediaType')
 
-        if not media_type.startswith('audio/'):
-            raise serializers.ValidationError('Invalid mediaType')
-
-        return url
-
-    def validate_url(self, v):
-        try:
-            url = v['href']
-        except (KeyError, TypeError):
-            raise serializers.ValidationError('Missing href')
-
-        try:
-            media_type = v['mediaType']
-        except (KeyError, TypeError):
-            raise serializers.ValidationError('Missing mediaType')
-
-        if not media_type.startswith('audio/'):
+        if not media_type or not media_type.startswith('audio/'):
             raise serializers.ValidationError('Invalid mediaType')
 
         return v
diff --git a/api/funkwhale_api/music/factories.py b/api/funkwhale_api/music/factories.py
index ea7ff64dfa0cb154c871089e75c70cf9c19a6e1a..bc0c74a2d0c108330e274d71522bfed9cccc8554 100644
--- a/api/funkwhale_api/music/factories.py
+++ b/api/funkwhale_api/music/factories.py
@@ -43,6 +43,7 @@ class TrackFactory(factory.django.DjangoModelFactory):
     artist = factory.SelfAttribute('album.artist')
     position = 1
     tags = ManyToManyFromList('tags')
+
     class Meta:
         model = 'music.Track'
 
@@ -57,6 +58,9 @@ class TrackFileFactory(factory.django.DjangoModelFactory):
         model = 'music.TrackFile'
 
     class Params:
+        in_place = factory.Trait(
+            audio_file=None,
+        )
         federation = factory.Trait(
             audio_file=None,
             library_track=factory.SubFactory(LibraryTrackFactory),
@@ -105,6 +109,10 @@ class ImportJobFactory(factory.django.DjangoModelFactory):
             status='finished',
             track_file=factory.SubFactory(TrackFileFactory),
         )
+        in_place = factory.Trait(
+            status='finished',
+            audio_file=None,
+        )
 
 
 @registry.register(name='music.FileImportJob')
diff --git a/api/funkwhale_api/music/filters.py b/api/funkwhale_api/music/filters.py
index fbea3735a267188485bfe236ecbd39ae56ae2e39..752422e75e64aae20f19bc46ab00cd4678c220d6 100644
--- a/api/funkwhale_api/music/filters.py
+++ b/api/funkwhale_api/music/filters.py
@@ -2,6 +2,7 @@ from django.db.models import Count
 
 from django_filters import rest_framework as filters
 
+from funkwhale_api.common import fields
 from . import models
 
 
@@ -28,6 +29,39 @@ class ArtistFilter(ListenableMixin):
         }
 
 
+class ImportBatchFilter(filters.FilterSet):
+    q = fields.SearchFilter(search_fields=[
+        'submitted_by__username',
+        'source',
+    ])
+
+    class Meta:
+        model = models.ImportBatch
+        fields = {
+            'status': ['exact'],
+            'source': ['exact'],
+            'submitted_by': ['exact'],
+        }
+
+
+class ImportJobFilter(filters.FilterSet):
+    q = fields.SearchFilter(search_fields=[
+        'batch__submitted_by__username',
+        'source',
+    ])
+
+    class Meta:
+        model = models.ImportJob
+        fields = {
+            'batch': ['exact'],
+            'batch__status': ['exact'],
+            'batch__source': ['exact'],
+            'batch__submitted_by': ['exact'],
+            'status': ['exact'],
+            'source': ['exact'],
+        }
+
+
 class AlbumFilter(ListenableMixin):
     listenable = filters.BooleanFilter(name='_', method='filter_listenable')
 
diff --git a/api/funkwhale_api/music/metadata.py b/api/funkwhale_api/music/metadata.py
index 3748d55730faf8188d56828bda3afd67dd219393..a200697834d1f68aff4291c5fcf9857ad401a1cd 100644
--- a/api/funkwhale_api/music/metadata.py
+++ b/api/funkwhale_api/music/metadata.py
@@ -1,5 +1,6 @@
-import mutagen
+from django import forms
 import arrow
+import mutagen
 
 NODEFAULT = object()
 
@@ -50,6 +51,13 @@ def convert_track_number(v):
     except (ValueError, AttributeError, IndexError):
         pass
 
+
+VALIDATION = {
+    'musicbrainz_artistid': forms.UUIDField(),
+    'musicbrainz_albumid': forms.UUIDField(),
+    'musicbrainz_recordingid': forms.UUIDField(),
+}
+
 CONF = {
     'OggVorbis': {
         'getter': lambda f, k: f[k][0],
@@ -146,4 +154,7 @@ class Metadata(object):
         converter = field_conf.get('to_application')
         if converter:
             v = converter(v)
+        field = VALIDATION.get(key)
+        if field:
+            v = field.to_python(v)
         return v
diff --git a/api/funkwhale_api/music/migrations/0025_auto_20180419_2023.py b/api/funkwhale_api/music/migrations/0025_auto_20180419_2023.py
new file mode 100644
index 0000000000000000000000000000000000000000..6b0230d505235f0f920fc4b84e7adf735601f990
--- /dev/null
+++ b/api/funkwhale_api/music/migrations/0025_auto_20180419_2023.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.0.3 on 2018-04-19 20:23
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('music', '0024_populate_uuid'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='trackfile',
+            name='source',
+            field=models.URLField(blank=True, max_length=500, null=True),
+        ),
+    ]
diff --git a/api/funkwhale_api/music/models.py b/api/funkwhale_api/music/models.py
index 4ec3ff4274efca8c6ad5f2e370ce90eda6b10141..98fc1965b51dff90a9aaae1717e5dc7ca5032538 100644
--- a/api/funkwhale_api/music/models.py
+++ b/api/funkwhale_api/music/models.py
@@ -412,7 +412,7 @@ class TrackFile(models.Model):
     track = models.ForeignKey(
         Track, related_name='files', on_delete=models.CASCADE)
     audio_file = models.FileField(upload_to='tracks/%Y/%m/%d', max_length=255)
-    source = models.URLField(null=True, blank=True)
+    source = models.URLField(null=True, blank=True, max_length=500)
     creation_date = models.DateTimeField(default=timezone.now)
     modification_date = models.DateTimeField(auto_now=True)
     duration = models.IntegerField(null=True, blank=True)
@@ -463,6 +463,26 @@ class TrackFile(models.Model):
             self.mimetype = utils.guess_mimetype(self.audio_file)
         return super().save(**kwargs)
 
+    @property
+    def serve_from_source_path(self):
+        if not self.source or not self.source.startswith('file://'):
+            raise ValueError('Cannot serve this file from source')
+        serve_path = settings.MUSIC_DIRECTORY_SERVE_PATH
+        prefix = settings.MUSIC_DIRECTORY_PATH
+        if not serve_path or not prefix:
+            raise ValueError(
+                'You need to specify MUSIC_DIRECTORY_SERVE_PATH and '
+                'MUSIC_DIRECTORY_PATH to serve in-place imported files'
+            )
+        file_path = self.source.replace('file://', '', 1)
+        parts = os.path.split(file_path.replace(prefix, '', 1))
+        if parts[0] == '/':
+            parts = parts[1:]
+        return os.path.join(
+            serve_path,
+            *parts
+        )
+
 
 IMPORT_STATUS_CHOICES = (
     ('pending', 'Pending'),
@@ -507,6 +527,8 @@ class ImportBatch(models.Model):
     def update_status(self):
         old_status = self.status
         self.status = utils.compute_status(self.jobs.all())
+        if self.status == old_status:
+            return
         self.save(update_fields=['status'])
         if self.status != old_status and self.status == 'finished':
             from . import tasks
diff --git a/api/funkwhale_api/music/serializers.py b/api/funkwhale_api/music/serializers.py
index b5f69eb1db866225fca4cc147e71a08901194dd8..b9ecfc50dcd982e109e369cc611656beba08c4b6 100644
--- a/api/funkwhale_api/music/serializers.py
+++ b/api/funkwhale_api/music/serializers.py
@@ -6,6 +6,7 @@ from funkwhale_api.activity import serializers as activity_serializers
 from funkwhale_api.federation import utils as federation_utils
 from funkwhale_api.federation.models import LibraryTrack
 from funkwhale_api.federation.serializers import AP_CONTEXT
+from funkwhale_api.users.serializers import UserBasicSerializer
 
 from . import models
 
@@ -90,6 +91,7 @@ class TrackSerializerNested(LyricsMixin):
     files = TrackFileSerializer(many=True, read_only=True)
     album = SimpleAlbumSerializer(read_only=True)
     tags = TagSerializer(many=True, read_only=True)
+
     class Meta:
         model = models.Track
         fields = ('id', 'mbid', 'title', 'artist', 'files', 'album', 'tags', 'lyrics')
@@ -108,6 +110,7 @@ class AlbumSerializerNested(serializers.ModelSerializer):
 class ArtistSerializerNested(serializers.ModelSerializer):
     albums = AlbumSerializerNested(many=True, read_only=True)
     tags = TagSerializer(many=True, read_only=True)
+
     class Meta:
         model = models.Artist
         fields = ('id', 'mbid', 'name', 'albums', 'tags')
@@ -121,18 +124,43 @@ class LyricsSerializer(serializers.ModelSerializer):
 
 class ImportJobSerializer(serializers.ModelSerializer):
     track_file = TrackFileSerializer(read_only=True)
+
     class Meta:
         model = models.ImportJob
-        fields = ('id', 'mbid', 'batch', 'source', 'status', 'track_file', 'audio_file')
+        fields = (
+            'id',
+            'mbid',
+            'batch',
+            'source',
+            'status',
+            'track_file',
+            'audio_file')
         read_only_fields = ('status', 'track_file')
 
 
 class ImportBatchSerializer(serializers.ModelSerializer):
-    jobs = ImportJobSerializer(many=True, read_only=True)
+    submitted_by = UserBasicSerializer(read_only=True)
+
     class Meta:
         model = models.ImportBatch
-        fields = ('id', 'jobs', 'status', 'creation_date', 'import_request')
-        read_only_fields = ('creation_date',)
+        fields = (
+            'id',
+            'submitted_by',
+            'source',
+            'status',
+            'creation_date',
+            'import_request')
+        read_only_fields = (
+            'creation_date', 'submitted_by', 'source')
+
+    def to_representation(self, instance):
+        repr = super().to_representation(instance)
+        try:
+            repr['job_count'] = instance.job_count
+        except AttributeError:
+            # Queryset was not annotated
+            pass
+        return repr
 
 
 class TrackActivitySerializer(activity_serializers.ModelSerializer):
diff --git a/api/funkwhale_api/music/tasks.py b/api/funkwhale_api/music/tasks.py
index bc5ab94f0ae7a56470f42e3c705c40ff11d86053..f2244d78527c5feff7248b9a40c083ea71498891 100644
--- a/api/funkwhale_api/music/tasks.py
+++ b/api/funkwhale_api/music/tasks.py
@@ -71,7 +71,7 @@ def import_track_from_remote(library_track):
         library_track.title, artist=artist, album=album)
 
 
-def _do_import(import_job, replace, use_acoustid=True):
+def _do_import(import_job, replace=False, use_acoustid=True):
     from_file = bool(import_job.audio_file)
     mbid = import_job.mbid
     acoustid_track_id = None
@@ -93,6 +93,9 @@ def _do_import(import_job, replace, use_acoustid=True):
         track = import_track_data_from_path(import_job.audio_file.path)
     elif import_job.library_track:
         track = import_track_from_remote(import_job.library_track)
+    elif import_job.source.startswith('file://'):
+        track = import_track_data_from_path(
+            import_job.source.replace('file://', '', 1))
     else:
         raise ValueError(
             'Not enough data to process import, '
@@ -123,7 +126,8 @@ def _do_import(import_job, replace, use_acoustid=True):
         else:
             # no downloading, we hotlink
             pass
-    else:
+    elif not import_job.audio_file and not import_job.source.startswith('file://'):
+        # not an implace import, and we have a source, so let's download it
         track_file.download_file()
     track_file.save()
     import_job.status = 'finished'
@@ -133,7 +137,7 @@ def _do_import(import_job, replace, use_acoustid=True):
         import_job.audio_file.delete()
     import_job.save()
 
-    return track.pk
+    return track_file
 
 
 @celery.app.task(name='ImportJob.run', bind=True)
@@ -147,7 +151,8 @@ def import_job_run(self, import_job, replace=False, use_acoustid=True):
         import_job.save(update_fields=['status'])
 
     try:
-        return _do_import(import_job, replace, use_acoustid=use_acoustid)
+        tf = _do_import(import_job, replace, use_acoustid=use_acoustid)
+        return tf.pk if tf else None
     except Exception as exc:
         if not settings.DEBUG:
             try:
diff --git a/api/funkwhale_api/music/utils.py b/api/funkwhale_api/music/utils.py
index af0e59ab497e63ea06be2a50ecf9df0651072587..7a851f7cc35e17681293a9e3a1c24d6cc1e64998 100644
--- a/api/funkwhale_api/music/utils.py
+++ b/api/funkwhale_api/music/utils.py
@@ -53,10 +53,11 @@ def guess_mimetype(f):
 
 
 def compute_status(jobs):
-    errored = any([job.status == 'errored' for job in jobs])
+    statuses = jobs.order_by().values_list('status', flat=True).distinct()
+    errored = any([status == 'errored' for status in statuses])
     if errored:
         return 'errored'
-    pending = any([job.status == 'pending' for job in jobs])
+    pending = any([status == 'pending' for status in statuses])
     if pending:
         return 'pending'
     return 'finished'
diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py
index e8ace1b3ab676c2cc6b76f134dbae6668ac2d655..af063da4679e7df8c4e295dc19dbd03812a1c810 100644
--- a/api/funkwhale_api/music/views.py
+++ b/api/funkwhale_api/music/views.py
@@ -11,6 +11,7 @@ from django.core.exceptions import ObjectDoesNotExist
 from django.conf import settings
 from django.db import models, transaction
 from django.db.models.functions import Length
+from django.db.models import Count
 from django.http import StreamingHttpResponse
 from django.urls import reverse
 from django.utils.decorators import method_decorator
@@ -23,13 +24,14 @@ from rest_framework import permissions
 from musicbrainzngs import ResponseError
 
 from funkwhale_api.common import utils as funkwhale_utils
-from funkwhale_api.federation import actors
-from funkwhale_api.requests.models import ImportRequest
-from funkwhale_api.musicbrainz import api
 from funkwhale_api.common.permissions import (
     ConditionalAuthentication, HasModelPermission)
 from taggit.models import Tag
+from funkwhale_api.federation import actors
 from funkwhale_api.federation.authentication import SignatureAuthentication
+from funkwhale_api.federation.models import LibraryTrack
+from funkwhale_api.musicbrainz import api
+from funkwhale_api.requests.models import ImportRequest
 
 from . import filters
 from . import forms
@@ -98,14 +100,14 @@ class ImportBatchViewSet(
         mixins.RetrieveModelMixin,
         viewsets.GenericViewSet):
     queryset = (
-        models.ImportBatch.objects.all()
-                          .prefetch_related('jobs__track_file')
-                          .order_by('-creation_date'))
+        models.ImportBatch.objects
+              .select_related()
+              .order_by('-creation_date')
+              .annotate(job_count=Count('jobs'))
+    )
     serializer_class = serializers.ImportBatchSerializer
     permission_classes = (permissions.DjangoModelPermissions, )
-
-    def get_queryset(self):
-        return super().get_queryset().filter(submitted_by=self.request.user)
+    filter_class = filters.ImportBatchFilter
 
     def perform_create(self, serializer):
         serializer.save(submitted_by=self.request.user)
@@ -118,13 +120,30 @@ class ImportJobPermission(HasModelPermission):
 
 class ImportJobViewSet(
         mixins.CreateModelMixin,
+        mixins.ListModelMixin,
         viewsets.GenericViewSet):
-    queryset = (models.ImportJob.objects.all())
+    queryset = (models.ImportJob.objects.all().select_related())
     serializer_class = serializers.ImportJobSerializer
     permission_classes = (ImportJobPermission, )
+    filter_class = filters.ImportJobFilter
 
-    def get_queryset(self):
-        return super().get_queryset().filter(batch__submitted_by=self.request.user)
+    @list_route(methods=['get'])
+    def stats(self, request, *args, **kwargs):
+        qs = models.ImportJob.objects.all()
+        filterset = filters.ImportJobFilter(request.GET, queryset=qs)
+        qs = filterset.qs
+        qs = qs.values('status').order_by('status')
+        qs = qs.annotate(status_count=Count('status'))
+
+        data = {}
+        for row in qs:
+            data[row['status']] = row['status_count']
+
+        for s, _ in models.IMPORT_STATUS_CHOICES:
+            data.setdefault(s, 0)
+
+        data['count'] = sum([v for v in data.values()])
+        return Response(data)
 
     def perform_create(self, serializer):
         source = 'file://' + serializer.validated_data['audio_file'].name
@@ -135,7 +154,8 @@ class ImportJobViewSet(
         )
 
 
-class TrackViewSet(TagViewSetMixin, SearchMixin, viewsets.ReadOnlyModelViewSet):
+class TrackViewSet(
+        TagViewSetMixin, SearchMixin, viewsets.ReadOnlyModelViewSet):
     """
     A simple ViewSet for viewing and editing accounts.
     """
@@ -185,6 +205,25 @@ class TrackViewSet(TagViewSetMixin, SearchMixin, viewsets.ReadOnlyModelViewSet):
         return Response(serializer.data)
 
 
+def get_file_path(audio_file):
+    t = settings.REVERSE_PROXY_TYPE
+    if t == 'nginx':
+        # we have to use the internal locations
+        try:
+            path = audio_file.url
+        except AttributeError:
+            # a path was given
+            path = '/music' + audio_file
+        return settings.PROTECT_FILES_PATH + path
+    if t == 'apache2':
+        try:
+            path = audio_file.path
+        except AttributeError:
+            # a path was given
+            path = audio_file
+        return path
+
+
 class TrackFileViewSet(viewsets.ReadOnlyModelViewSet):
     queryset = (models.TrackFile.objects.all().order_by('-id'))
     serializer_class = serializers.TrackFileSerializer
@@ -195,12 +234,13 @@ class TrackFileViewSet(viewsets.ReadOnlyModelViewSet):
 
     @detail_route(methods=['get'])
     def serve(self, request, *args, **kwargs):
+        queryset = models.TrackFile.objects.select_related(
+            'library_track',
+            'track__album__artist',
+            'track__artist',
+        )
         try:
-            f = models.TrackFile.objects.select_related(
-                'library_track',
-                'track__album__artist',
-                'track__artist',
-            ).get(pk=kwargs['pk'])
+            f = queryset.get(pk=kwargs['pk'])
         except models.TrackFile.DoesNotExist:
             return Response(status=404)
 
@@ -213,14 +253,29 @@ class TrackFileViewSet(viewsets.ReadOnlyModelViewSet):
         if library_track and not audio_file:
             if not library_track.audio_file:
                 # we need to populate from cache
-                library_track.download_audio()
+                with transaction.atomic():
+                    # why the transaction/select_for_update?
+                    # this is because browsers may send multiple requests
+                    # in a short time range, for partial content,
+                    # thus resulting in multiple downloads from the remote
+                    qs = LibraryTrack.objects.select_for_update()
+                    library_track = qs.get(pk=library_track.pk)
+                    library_track.download_audio()
             audio_file = library_track.audio_file
+            file_path = get_file_path(audio_file)
             mt = library_track.audio_mimetype
+        elif audio_file:
+            file_path = get_file_path(audio_file)
+        elif f.source and f.source.startswith('file://'):
+            file_path = get_file_path(f.serve_from_source_path)
         response = Response()
         filename = f.filename
-        response['X-Accel-Redirect'] = "{}{}".format(
-            settings.PROTECT_FILES_PATH,
-            audio_file.url)
+        mapping = {
+            'nginx': 'X-Accel-Redirect',
+            'apache2': 'X-Sendfile',
+        }
+        file_header = mapping[settings.REVERSE_PROXY_TYPE]
+        response[file_header] = file_path
         filename = "filename*=UTF-8''{}".format(
             urllib.parse.quote(filename))
         response["Content-Disposition"] = "attachment; {}".format(filename)
diff --git a/api/funkwhale_api/providers/audiofile/management/commands/import_files.py b/api/funkwhale_api/providers/audiofile/management/commands/import_files.py
index dbc01289f1206c18a15a691d2fb8025695155b17..a2757c692bd4607b0779ba112e4768a5881d8770 100644
--- a/api/funkwhale_api/providers/audiofile/management/commands/import_files.py
+++ b/api/funkwhale_api/providers/audiofile/management/commands/import_files.py
@@ -1,11 +1,11 @@
 import glob
 import os
 
+from django.conf import settings
 from django.core.files import File
 from django.core.management.base import BaseCommand, CommandError
-from django.db import transaction
 
-from funkwhale_api.common import utils
+from funkwhale_api.music import models
 from funkwhale_api.music import tasks
 from funkwhale_api.users.models import User
 
@@ -39,7 +39,20 @@ class Command(BaseCommand):
             action='store_true',
             dest='exit_on_failure',
             default=False,
-            help='use this flag to disable error catching',
+            help='Use this flag to disable error catching',
+        )
+        parser.add_argument(
+            '--in-place', '-i',
+            action='store_true',
+            dest='in_place',
+            default=False,
+            help=(
+                'Import files without duplicating them into the media directory.'
+                'For in-place import to work, the music files must be readable'
+                'by the web-server and funkwhale api and celeryworker processes.'
+                'You may want to use this if you have a big music library to '
+                'import and not much disk space available.'
+            )
         )
         parser.add_argument(
             '--no-acoustid',
@@ -54,21 +67,29 @@ class Command(BaseCommand):
         )
 
     def handle(self, *args, **options):
-        # self.stdout.write(self.style.SUCCESS('Successfully closed poll "%s"' % poll_id))
-
-        # Recursive is supported only on Python 3.5+, so we pass the option
-        # only if it's True to avoid breaking on older versions of Python
         glob_kwargs = {}
         if options['recursive']:
             glob_kwargs['recursive'] = True
         try:
-            matching = glob.glob(options['path'], **glob_kwargs)
+            matching = sorted(glob.glob(options['path'], **glob_kwargs))
         except TypeError:
             raise Exception('You need Python 3.5 to use the --recursive flag')
 
-        self.stdout.write('This will import {} files matching this pattern: {}'.format(
-            len(matching), options['path']))
-
+        if options['in_place']:
+            self.stdout.write(
+                'Checking imported paths against settings.MUSIC_DIRECTORY_PATH')
+            p = settings.MUSIC_DIRECTORY_PATH
+            if not p:
+                raise CommandError(
+                    'Importing in-place requires setting the '
+                    'MUSIC_DIRECTORY_PATH variable')
+            for m in matching:
+                if not m.startswith(p):
+                    raise CommandError(
+                        'Importing in-place only works if importing'
+                        'from {} (MUSIC_DIRECTORY_PATH), as this directory'
+                        'needs to be accessible by the webserver.'
+                        'Culprit: {}'.format(p, m))
         if not matching:
             raise CommandError('No file matching pattern, aborting')
 
@@ -86,6 +107,24 @@ class Command(BaseCommand):
             except AssertionError:
                 raise CommandError(
                     'No superuser available, please provide a --username')
+
+        filtered = self.filter_matching(matching, options)
+        self.stdout.write('Import summary:')
+        self.stdout.write('- {} files found matching this pattern: {}'.format(
+            len(matching), options['path']))
+        self.stdout.write('- {} files already found in database'.format(
+            len(filtered['skipped'])))
+        self.stdout.write('- {} new files'.format(
+            len(filtered['new'])))
+
+        self.stdout.write('Selected options: {}'.format(', '.join([
+            'no acoustid' if options['no_acoustid'] else 'use acoustid',
+            'in place' if options['in_place'] else 'copy music files',
+        ])))
+        if len(filtered['new']) == 0:
+            self.stdout.write('Nothing new to import, exiting')
+            return
+
         if options['interactive']:
             message = (
                 'Are you sure you want to do this?\n\n'
@@ -94,27 +133,52 @@ class Command(BaseCommand):
             if input(''.join(message)) != 'yes':
                 raise CommandError("Import cancelled.")
 
-        batch = self.do_import(matching, user=user, options=options)
+        batch, errors = self.do_import(
+            filtered['new'], user=user, options=options)
         message = 'Successfully imported {} tracks'
         if options['async']:
             message = 'Successfully launched import for {} tracks'
-        self.stdout.write(message.format(len(matching)))
+
+        self.stdout.write(message.format(len(filtered['new'])))
+        if len(errors) > 0:
+            self.stderr.write(
+                '{} tracks could not be imported:'.format(len(errors)))
+
+            for path, error in errors:
+                self.stderr.write('- {}: {}'.format(path, error))
         self.stdout.write(
             "For details, please refer to import batch #{}".format(batch.pk))
 
-    @transaction.atomic
-    def do_import(self, matching, user, options):
-        message = 'Importing {}...'
+    def filter_matching(self, matching, options):
+        sources = ['file://{}'.format(p) for p in matching]
+        # we skip reimport for path that are already found
+        # as a TrackFile.source
+        existing = models.TrackFile.objects.filter(source__in=sources)
+        existing = existing.values_list('source', flat=True)
+        existing = set([p.replace('file://', '', 1) for p in existing])
+        skipped = set(matching) & existing
+        result = {
+            'initial': matching,
+            'skipped': list(sorted(skipped)),
+            'new': list(sorted(set(matching) - skipped)),
+        }
+        return result
+
+    def do_import(self, paths, user, options):
+        message = '{i}/{total} Importing {path}...'
         if options['async']:
-            message = 'Launching import for {}...'
+            message = '{i}/{total} Launching import for {path}...'
 
         # we create an import batch binded to the user
-        batch = user.imports.create(source='shell')
         async = options['async']
         import_handler = tasks.import_job_run.delay if async else tasks.import_job_run
-        for path in matching:
+        batch = user.imports.create(source='shell')
+        total = len(paths)
+        errors = []
+        for i, path in list(enumerate(paths)):
             try:
-                self.stdout.write(message.format(path))
+                self.stdout.write(
+                    message.format(path=path, i=i+1, total=len(paths)))
                 self.import_file(path, batch, import_handler, options)
             except Exception as e:
                 if options['exit_on_failure']:
@@ -122,18 +186,19 @@ class Command(BaseCommand):
                 m = 'Error while importing {}: {} {}'.format(
                     path, e.__class__.__name__, e)
                 self.stderr.write(m)
-        return batch
+                errors.append((path, '{} {}'.format(e.__class__.__name__, e)))
+        return batch, errors
 
     def import_file(self, path, batch, import_handler, options):
         job = batch.jobs.create(
             source='file://' + path,
         )
-        name = os.path.basename(path)
-        with open(path, 'rb') as f:
-            job.audio_file.save(name, File(f))
+        if not options['in_place']:
+            name = os.path.basename(path)
+            with open(path, 'rb') as f:
+                job.audio_file.save(name, File(f))
 
-        job.save()
-        utils.on_commit(
-            import_handler,
+            job.save()
+        import_handler(
             import_job_id=job.pk,
             use_acoustid=not options['no_acoustid'])
diff --git a/api/funkwhale_api/providers/audiofile/tasks.py b/api/funkwhale_api/providers/audiofile/tasks.py
index bc18456c306731eee04e596b53b86c29d1cf87e5..40114c8774abfa2f18e520dd15b19be9f382cb58 100644
--- a/api/funkwhale_api/providers/audiofile/tasks.py
+++ b/api/funkwhale_api/providers/audiofile/tasks.py
@@ -2,12 +2,14 @@ import acoustid
 import os
 import datetime
 from django.core.files import File
+from django.db import transaction
 
 from funkwhale_api.taskapp import celery
 from funkwhale_api.providers.acoustid import get_acoustid_client
 from funkwhale_api.music import models, metadata
 
 
+@transaction.atomic
 def import_track_data_from_path(path):
     data = metadata.Metadata(path)
     artist = models.Artist.objects.get_or_create(
@@ -45,6 +47,7 @@ def import_track_data_from_path(path):
 def import_metadata_with_musicbrainz(path):
     pass
 
+
 @celery.app.task(name='audiofile.from_path')
 def from_path(path):
     acoustid_track_id = None
diff --git a/api/requirements/base.txt b/api/requirements/base.txt
index b66e297a9942524b02df71fdcc67c81225e62c1e..ac058656639aa810e334721e75a548c34704e178 100644
--- a/api/requirements/base.txt
+++ b/api/requirements/base.txt
@@ -33,6 +33,7 @@ musicbrainzngs==0.6
 youtube_dl>=2017.12.14
 djangorestframework>=3.7,<3.8
 djangorestframework-jwt>=1.11,<1.12
+oauth2client<4
 google-api-python-client>=1.6,<1.7
 arrow>=0.12,<0.13
 persisting-theory>=0.2,<0.3
diff --git a/api/tests/music/test_api.py b/api/tests/music/test_api.py
index 606720e133928ecda0d2bf22e9440e6b1c9eeb07..cc6fe644b6a4acb7a654453332021dfdc3803013 100644
--- a/api/tests/music/test_api.py
+++ b/api/tests/music/test_api.py
@@ -181,30 +181,6 @@ def test_can_import_whole_artist(
         assert job.source == row['source']
 
 
-def test_user_can_query_api_for_his_own_batches(
-        superuser_api_client, factories):
-    factories['music.ImportJob']()
-    job = factories['music.ImportJob'](
-        batch__submitted_by=superuser_api_client.user)
-    url = reverse('api:v1:import-batches-list')
-
-    response = superuser_api_client.get(url)
-    results = response.data
-    assert results['count'] == 1
-    assert results['results'][0]['jobs'][0]['mbid'] == job.mbid
-
-
-def test_user_cannnot_access_other_batches(
-        superuser_api_client, factories):
-    factories['music.ImportJob']()
-    job = factories['music.ImportJob']()
-    url = reverse('api:v1:import-batches-list')
-
-    response = superuser_api_client.get(url)
-    results = response.data
-    assert results['count'] == 0
-
-
 def test_user_can_create_an_empty_batch(superuser_api_client, factories):
     url = reverse('api:v1:import-batches-list')
     response = superuser_api_client.post(url)
diff --git a/api/tests/music/test_import.py b/api/tests/music/test_import.py
index 2f22ed69ad8db3ffb6ec098d606b194fbdb8ab3e..65e0242fb013785b0eb21e29ac9259512b281392 100644
--- a/api/tests/music/test_import.py
+++ b/api/tests/music/test_import.py
@@ -231,3 +231,15 @@ def test_import_batch_notifies_followers(
         on_behalf_of=library_actor,
         to=[f1.actor.url]
     )
+
+
+def test__do_import_in_place_mbid(factories, tmpfile):
+    path = '/test.ogg'
+    job = factories['music.ImportJob'](
+        in_place=True, source='file:///test.ogg')
+
+    track = factories['music.Track'](mbid=job.mbid)
+    tf = tasks._do_import(job, use_acoustid=False)
+
+    assert bool(tf.audio_file) is False
+    assert tf.source == 'file:///test.ogg'
diff --git a/api/tests/music/test_metadata.py b/api/tests/music/test_metadata.py
index 5df2dbcf117965cf41563a6d028cbe804c3e0fd8..342bc99b8115cf0646da9c9e61c10fe645738ec3 100644
--- a/api/tests/music/test_metadata.py
+++ b/api/tests/music/test_metadata.py
@@ -1,6 +1,7 @@
 import datetime
 import os
 import pytest
+import uuid
 
 from funkwhale_api.music import metadata
 
@@ -13,9 +14,9 @@ DATA_DIR = os.path.dirname(os.path.abspath(__file__))
     ('album', 'Peer Gynt Suite no. 1, op. 46'),
     ('date', datetime.date(2012, 8, 15)),
     ('track_number', 1),
-    ('musicbrainz_albumid', 'a766da8b-8336-47aa-a3ee-371cc41ccc75'),
-    ('musicbrainz_recordingid', 'bd21ac48-46d8-4e78-925f-d9cc2a294656'),
-    ('musicbrainz_artistid', '013c8e5b-d72a-4cd3-8dee-6c64d6125823'),
+    ('musicbrainz_albumid', uuid.UUID('a766da8b-8336-47aa-a3ee-371cc41ccc75')),
+    ('musicbrainz_recordingid', uuid.UUID('bd21ac48-46d8-4e78-925f-d9cc2a294656')),
+    ('musicbrainz_artistid', uuid.UUID('013c8e5b-d72a-4cd3-8dee-6c64d6125823')),
 ])
 def test_can_get_metadata_from_ogg_file(field, value):
     path = os.path.join(DATA_DIR, 'test.ogg')
@@ -30,9 +31,9 @@ def test_can_get_metadata_from_ogg_file(field, value):
     ('album', 'You Can\'t Stop Da Funk'),
     ('date', datetime.date(2006, 2, 7)),
     ('track_number', 1),
-    ('musicbrainz_albumid', 'ce40cdb1-a562-4fd8-a269-9269f98d4124'),
-    ('musicbrainz_recordingid', 'f269d497-1cc0-4ae4-a0c4-157ec7d73fcb'),
-    ('musicbrainz_artistid', '9c6bddde-6228-4d9f-ad0d-03f6fcb19e13'),
+    ('musicbrainz_albumid', uuid.UUID('ce40cdb1-a562-4fd8-a269-9269f98d4124')),
+    ('musicbrainz_recordingid', uuid.UUID('f269d497-1cc0-4ae4-a0c4-157ec7d73fcb')),
+    ('musicbrainz_artistid', uuid.UUID('9c6bddde-6228-4d9f-ad0d-03f6fcb19e13')),
 ])
 def test_can_get_metadata_from_id3_mp3_file(field, value):
     path = os.path.join(DATA_DIR, 'test.mp3')
diff --git a/api/tests/music/test_views.py b/api/tests/music/test_views.py
index 81f34fbe9d95d41e198ef783746df65dcdbb0561..5d7589af08c16efe1b69b72d48b37e4c77500633 100644
--- a/api/tests/music/test_views.py
+++ b/api/tests/music/test_views.py
@@ -76,6 +76,31 @@ def test_can_serve_track_file_as_remote_library_deny_not_following(
     assert response.status_code == 403
 
 
+def test_serve_file_apache(factories, api_client, settings):
+    settings.PROTECT_AUDIO_FILES = False
+    settings.REVERSE_PROXY_TYPE = 'apache2'
+    tf = factories['music.TrackFile']()
+    response = api_client.get(tf.path)
+
+    assert response.status_code == 200
+    assert response['X-Sendfile'] == tf.audio_file.path
+
+
+def test_serve_file_apache_in_place(factories, api_client, settings):
+    settings.PROTECT_AUDIO_FILES = False
+    settings.REVERSE_PROXY_TYPE = 'apache2'
+    settings.MUSIC_DIRECTORY_PATH = '/music'
+    settings.MUSIC_DIRECTORY_SERVE_PATH = '/host/music'
+    track_file = factories['music.TrackFile'](
+        in_place=True,
+        source='file:///music/test.ogg')
+
+    response = api_client.get(track_file.path)
+
+    assert response.status_code == 200
+    assert response['X-Sendfile'] == '/host/music/test.ogg'
+
+
 def test_can_proxy_remote_track(
         factories, settings, api_client, r_mock):
     settings.PROTECT_AUDIO_FILES = False
@@ -93,6 +118,25 @@ def test_can_proxy_remote_track(
     assert library_track.audio_file.read() == b'test'
 
 
+def test_can_serve_in_place_imported_file(
+        factories, settings, api_client, r_mock):
+    settings.PROTECT_AUDIO_FILES = False
+    settings.MUSIC_DIRECTORY_SERVE_PATH = '/host/music'
+    settings.MUSIC_DIRECTORY_PATH = '/music'
+    settings.MUSIC_DIRECTORY_PATH = '/music'
+    track_file = factories['music.TrackFile'](
+        in_place=True,
+        source='file:///music/test.ogg')
+
+    response = api_client.get(track_file.path)
+
+    assert response.status_code == 200
+    assert response['X-Accel-Redirect'] == '{}{}'.format(
+        settings.PROTECT_FILES_PATH,
+        '/music/host/music/test.ogg'
+    )
+
+
 def test_can_create_import_from_federation_tracks(
         factories, superuser_api_client, mocker):
     lts = factories['federation.LibraryTrack'].create_batch(size=5)
@@ -109,3 +153,46 @@ def test_can_create_import_from_federation_tracks(
     assert batch.jobs.count() == 5
     for i, job in enumerate(batch.jobs.all()):
         assert job.library_track == lts[i]
+
+
+def test_can_list_import_jobs(factories, superuser_api_client):
+    job = factories['music.ImportJob']()
+    url = reverse('api:v1:import-jobs-list')
+    response = superuser_api_client.get(url)
+
+    assert response.status_code == 200
+    assert response.data['results'][0]['id'] == job.pk
+
+
+def test_import_job_stats(factories, superuser_api_client):
+    job1 = factories['music.ImportJob'](status='pending')
+    job2 = factories['music.ImportJob'](status='errored')
+
+    url = reverse('api:v1:import-jobs-stats')
+    response = superuser_api_client.get(url)
+    expected = {
+        'errored': 1,
+        'pending': 1,
+        'finished': 0,
+        'skipped': 0,
+        'count': 2,
+    }
+    assert response.status_code == 200
+    assert response.data == expected
+
+
+def test_import_job_stats_filter(factories, superuser_api_client):
+    job1 = factories['music.ImportJob'](status='pending')
+    job2 = factories['music.ImportJob'](status='errored')
+
+    url = reverse('api:v1:import-jobs-stats')
+    response = superuser_api_client.get(url, {'batch': job1.batch.pk})
+    expected = {
+        'errored': 0,
+        'pending': 1,
+        'finished': 0,
+        'skipped': 0,
+        'count': 1,
+    }
+    assert response.status_code == 200
+    assert response.data == expected
diff --git a/api/tests/test_import_audio_file.py b/api/tests/test_import_audio_file.py
index 67263e66d0c56309bb21f344274277b7d4464165..8217ffa0b27a26ac99183314f87d69feced35e7d 100644
--- a/api/tests/test_import_audio_file.py
+++ b/api/tests/test_import_audio_file.py
@@ -2,6 +2,8 @@ import pytest
 import acoustid
 import datetime
 import os
+import uuid
+
 from django.core.management import call_command
 from django.core.management.base import CommandError
 
@@ -15,7 +17,8 @@ DATA_DIR = os.path.join(
 
 
 def test_can_create_track_from_file_metadata(db, mocker):
-    mocker.patch('acoustid.match', side_effect=acoustid.WebServiceError('test'))
+    mocker.patch(
+        'acoustid.match', side_effect=acoustid.WebServiceError('test'))
     metadata = {
         'artist': ['Test artist'],
         'album': ['Test album'],
@@ -35,33 +38,49 @@ def test_can_create_track_from_file_metadata(db, mocker):
         os.path.join(DATA_DIR, 'dummy_file.ogg'))
 
     assert track.title == metadata['title'][0]
-    assert track.mbid == metadata['musicbrainz_trackid'][0]
+    assert track.mbid == uuid.UUID(metadata['musicbrainz_trackid'][0])
     assert track.position == 4
     assert track.album.title == metadata['album'][0]
-    assert track.album.mbid == metadata['musicbrainz_albumid'][0]
+    assert track.album.mbid == uuid.UUID(metadata['musicbrainz_albumid'][0])
     assert track.album.release_date == datetime.date(2012, 8, 15)
     assert track.artist.name == metadata['artist'][0]
-    assert track.artist.mbid == metadata['musicbrainz_artistid'][0]
+    assert track.artist.mbid == uuid.UUID(metadata['musicbrainz_artistid'][0])
 
 
 def test_management_command_requires_a_valid_username(factories, mocker):
     path = os.path.join(DATA_DIR, 'dummy_file.ogg')
     user = factories['users.User'](username='me')
-    mocker.patch('funkwhale_api.providers.audiofile.management.commands.import_files.Command.do_import')  # NOQA
+    mocker.patch(
+        'funkwhale_api.providers.audiofile.management.commands.import_files.Command.do_import',  # noqa
+        return_value=(mocker.MagicMock(), []))
     with pytest.raises(CommandError):
         call_command('import_files', path, username='not_me', interactive=False)
     call_command('import_files', path, username='me', interactive=False)
 
 
+def test_in_place_import_only_from_music_dir(factories, settings):
+    user = factories['users.User'](username='me')
+    settings.MUSIC_DIRECTORY_PATH = '/nope'
+    path = os.path.join(DATA_DIR, 'dummy_file.ogg')
+    with pytest.raises(CommandError):
+        call_command(
+            'import_files',
+            path,
+            in_place=True,
+            username='me',
+            interactive=False
+        )
+
+
 def test_import_files_creates_a_batch_and_job(factories, mocker):
-    m = mocker.patch('funkwhale_api.common.utils.on_commit')
+    m = mocker.patch('funkwhale_api.music.tasks.import_job_run')
     user = factories['users.User'](username='me')
     path = os.path.join(DATA_DIR, 'dummy_file.ogg')
     call_command(
         'import_files',
         path,
         username='me',
-        async=True,
+        async=False,
         interactive=False)
 
     batch = user.imports.latest('id')
@@ -76,45 +95,79 @@ def test_import_files_creates_a_batch_and_job(factories, mocker):
 
     assert job.source == 'file://' + path
     m.assert_called_once_with(
-        music_tasks.import_job_run.delay,
         import_job_id=job.pk,
         use_acoustid=True)
 
 
 def test_import_files_skip_acoustid(factories, mocker):
-    m = mocker.patch('funkwhale_api.common.utils.on_commit')
+    m = mocker.patch('funkwhale_api.music.tasks.import_job_run')
     user = factories['users.User'](username='me')
     path = os.path.join(DATA_DIR, 'dummy_file.ogg')
     call_command(
         'import_files',
         path,
         username='me',
-        async=True,
+        async=False,
         no_acoustid=True,
         interactive=False)
     batch = user.imports.latest('id')
     job = batch.jobs.first()
     m.assert_called_once_with(
-        music_tasks.import_job_run.delay,
         import_job_id=job.pk,
         use_acoustid=False)
 
 
+def test_import_files_skip_if_path_already_imported(factories, mocker):
+    user = factories['users.User'](username='me')
+    path = os.path.join(DATA_DIR, 'dummy_file.ogg')
+    existing = factories['music.TrackFile'](
+        source='file://{}'.format(path))
+
+    call_command(
+        'import_files',
+        path,
+        username='me',
+        async=False,
+        no_acoustid=True,
+        interactive=False)
+    assert user.imports.count() == 0
+
+
 def test_import_files_works_with_utf8_file_name(factories, mocker):
-    m = mocker.patch('funkwhale_api.common.utils.on_commit')
+    m = mocker.patch('funkwhale_api.music.tasks.import_job_run')
+    user = factories['users.User'](username='me')
+    path = os.path.join(DATA_DIR, 'utf8-éà◌.ogg')
+    call_command(
+        'import_files',
+        path,
+        username='me',
+        async=False,
+        no_acoustid=True,
+        interactive=False)
+    batch = user.imports.latest('id')
+    job = batch.jobs.first()
+    m.assert_called_once_with(
+        import_job_id=job.pk,
+        use_acoustid=False)
+
+
+def test_import_files_in_place(factories, mocker, settings):
+    settings.MUSIC_DIRECTORY_PATH = DATA_DIR
+    m = mocker.patch('funkwhale_api.music.tasks.import_job_run')
     user = factories['users.User'](username='me')
     path = os.path.join(DATA_DIR, 'utf8-éà◌.ogg')
     call_command(
         'import_files',
         path,
         username='me',
-        async=True,
+        async=False,
+        in_place=True,
         no_acoustid=True,
         interactive=False)
     batch = user.imports.latest('id')
     job = batch.jobs.first()
+    assert bool(job.audio_file) is False
     m.assert_called_once_with(
-        music_tasks.import_job_run.delay,
         import_job_id=job.pk,
         use_acoustid=False)
 
diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml
index cc4f357cad229768d1b98d8b104c7aa8f2c691d5..3bcd2f6d508a7b41747ba10ee85306adf9aa6619 100644
--- a/deploy/docker-compose.yml
+++ b/deploy/docker-compose.yml
@@ -20,6 +20,14 @@ services:
     restart: unless-stopped
     image: funkwhale/funkwhale:${FUNKWHALE_VERSION:-latest}
     env_file: .env
+    # Celery workers handle background tasks (such file imports or federation
+    # messaging). The more processes a worker gets, the more tasks
+    # can be processed in parallel. However, more processes also means
+    # a bigger memory footprint.
+    # By default, a worker will span a number of process equal to your number
+    # of CPUs. You can adjust this, by explicitly setting the --concurrency
+    # flag:
+    #   celery -A funkwhale_api.taskapp worker -l INFO --concurrency=4
     command: celery -A funkwhale_api.taskapp worker -l INFO
     links:
       - postgres
diff --git a/deploy/env.prod.sample b/deploy/env.prod.sample
index 9e9938500823e538cced62b88280f7ef67824ccb..54f2e1ef08192d5a181d04bb69ad69b0848f2faa 100644
--- a/deploy/env.prod.sample
+++ b/deploy/env.prod.sample
@@ -1,17 +1,22 @@
+# If you have any doubts about what a setting does,
+# check https://docs.funkwhale.audio/configuration.html#configuration-reference
+
 # If you're tweaking this file from the template, ensure you edit at least the
 # following variables:
 # - DJANGO_SECRET_KEY
 # - DJANGO_ALLOWED_HOSTS
 # - FUNKWHALE_URL
-
-# Additionaly, on non-docker setup **only**, you'll also have to tweak/uncomment those variables:
+# On non-docker setup **only**, you'll also have to tweak/uncomment those variables:
 # - DATABASE_URL
 # - CACHE_URL
 # - STATIC_ROOT
 # - MEDIA_ROOT
 #
 # You **don't** need to update those variables on pure docker setups.
-
+#
+# Additional options you may want to check:
+# - MUSIC_DIRECTORY_PATH and MUSIC_DIRECTORY_SERVE_PATH if you plan to use
+#   in-place import
 # Docker only
 # -----------
 
@@ -19,7 +24,9 @@
 # (it will be interpolated in docker-compose file)
 # You can comment or ignore this if you're not using docker
 FUNKWHALE_VERSION=latest
+MUSIC_DIRECTORY_PATH=/music
 
+# End of Docker-only configuration
 
 # General configuration
 # ---------------------
@@ -34,6 +41,11 @@ FUNKWHALE_API_PORT=5000
 # your instance
 FUNKWHALE_URL=https://yourdomain.funwhale
 
+# Depending on the reverse proxy used in front of your funkwhale instance,
+# the API will use different kind of headers to serve audio files
+# Allowed values: nginx, apache2
+REVERSE_PROXY_TYPE=nginx
+
 # API/Django configuration
 
 # Database configuration
@@ -94,3 +106,9 @@ FEDERATION_ENABLED=True
 # means anyone can subscribe to your library and import your file,
 # use with caution.
 FEDERATION_MUSIC_NEEDS_APPROVAL=True
+
+# In-place import settings
+# You can safely leave those settings uncommented if you don't plan to use
+# in place imports.
+# MUSIC_DIRECTORY_PATH=
+# MUSIC_DIRECTORY_SERVE_PATH=
diff --git a/deploy/funkwhale-worker.service b/deploy/funkwhale-worker.service
index cb3c883070f13ec0052694a27d34719f908791e3..4df60b5e98ea5b997f882984624a3c79ebe6f01b 100644
--- a/deploy/funkwhale-worker.service
+++ b/deploy/funkwhale-worker.service
@@ -8,6 +8,14 @@ User=funkwhale
 # adapt this depending on the path of your funkwhale installation
 WorkingDirectory=/srv/funkwhale/api
 EnvironmentFile=/srv/funkwhale/config/.env
+# Celery workers handle background tasks (such file imports or federation
+# messaging). The more processes a worker gets, the more tasks
+# can be processed in parallel. However, more processes also means
+# a bigger memory footprint.
+# By default, a worker will span a number of process equal to your number
+# of CPUs. You can adjust this, by explicitly setting the --concurrency
+# flag:
+#   celery -A funkwhale_api.taskapp worker -l INFO --concurrency=4
 ExecStart=/srv/funkwhale/virtualenv/bin/celery -A funkwhale_api.taskapp worker -l INFO
 
 [Install]
diff --git a/deploy/nginx.conf b/deploy/nginx.conf
index 1c304b4938892bf87ba50ab05f2394784b020248..b3a4c6aaf762830b533774689cb26266ca6b65e8 100644
--- a/deploy/nginx.conf
+++ b/deploy/nginx.conf
@@ -84,6 +84,14 @@ server {
         alias   /srv/funkwhale/data/media;
     }
 
+    location /_protected/music {
+        # this is an internal location that is used to serve
+        # audio files once correct permission / authentication
+        # has been checked on API side
+        internal;
+        alias   /srv/funkwhale/data/music;
+    }
+
     # Transcoding logic and caching
     location = /transcode-auth {
         include /etc/nginx/funkwhale_proxy.conf;
diff --git a/dev.yml b/dev.yml
index 2df7b44e60100b4812db940f45fb8baf14da2544..264fc953483d1dec5b19b30d3ade754efb47691d 100644
--- a/dev.yml
+++ b/dev.yml
@@ -65,7 +65,7 @@ services:
       - "CACHE_URL=redis://redis:6379/0"
     volumes:
       - ./api:/app
-      - ./data/music:/music
+      - "${MUSIC_DIRECTORY-./data/music}:/music:ro"
     networks:
       - internal
   api:
@@ -78,7 +78,7 @@ services:
     command: python /app/manage.py runserver 0.0.0.0:12081
     volumes:
       - ./api:/app
-      - ./data/music:/music
+      - "${MUSIC_DIRECTORY-./data/music}:/music:ro"
     environment:
       - "FUNKWHALE_HOSTNAME=${FUNKWHALE_HOSTNAME-localhost}"
       - "FUNKWHALE_HOSTNAME_SUFFIX=funkwhale.test"
@@ -107,6 +107,7 @@ services:
     volumes:
       - ./docker/nginx/conf.dev:/etc/nginx/nginx.conf
       - ./docker/nginx/entrypoint.sh:/entrypoint.sh:ro
+      - "${MUSIC_DIRECTORY-./data/music}:/music:ro"
       - ./deploy/funkwhale_proxy.conf:/etc/nginx/funkwhale_proxy.conf.template:ro
       - ./api/funkwhale_api/media:/protected/media
     ports:
diff --git a/docker/nginx/conf.dev b/docker/nginx/conf.dev
index e832a5ae3ee5b4d96833bd1cf5bf23fee6d5033f..38c3de6c7e41369aa98bf6e3f21ad84a8d0c0a4d 100644
--- a/docker/nginx/conf.dev
+++ b/docker/nginx/conf.dev
@@ -42,6 +42,10 @@ http {
             internal;
             alias   /protected/media;
         }
+        location /_protected/music {
+            internal;
+            alias   /music;
+        }
         location = /transcode-auth {
             # needed so we can authenticate transcode requests, but still
             # cache the result
diff --git a/docs/configuration.rst b/docs/configuration.rst
index 5883a2d17e6d99ba5c34e0a6693e7a8537b34942..c0de76f56a1e9d7b1d8eaeb0d9da8dea0c3257a3 100644
--- a/docs/configuration.rst
+++ b/docs/configuration.rst
@@ -33,3 +33,44 @@ The URL should be ``/api/admin/dynamic_preferences/globalpreferencemodel/`` (pre
 If you plan to use acoustid and external imports
 (e.g. with the youtube backends), you should edit the corresponding
 settings in this interface.
+
+Configuration reference
+-----------------------
+
+.. _setting-MUSIC_DIRECTORY_PATH:
+
+``MUSIC_DIRECTORY_PATH``
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+Default: ``None``
+
+The path on your server where Funwkhale can import files using :ref:`in-place import
+<in-place-import>`. It must be readable by the webserver and funkwhale
+api and worker processes.
+
+On docker installations, we recommend you use the default of ``/music``
+for this value. For non-docker installation, you can use any absolute path.
+``/srv/funkwhale/data/music`` is a safe choice if you don't know what to use.
+
+.. note:: This path should not include any trailing slash
+
+.. _setting-MUSIC_DIRECTORY_SERVE_PATH:
+
+``MUSIC_DIRECTORY_SERVE_PATH``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Default: :ref:`setting-MUSIC_DIRECTORY_PATH`
+
+When using Docker, the value of :ref:`MUSIC_DIRECTORY_PATH` in your containers
+may differ from the real path on your host. Assuming you have the following directive
+in your :file:`docker-compose.yml` file::
+
+    volumes:
+      - /srv/funkwhale/data/music:/music:ro
+
+Then, the value of :ref:`setting-MUSIC_DIRECTORY_SERVE_PATH` should be
+``/srv/funkwhale/data``. This must be readable by the webserver.
+
+On non-docker setup, you don't need to configure this setting.
+
+.. note:: This path should not include any trailing slash
diff --git a/docs/importing-music.rst b/docs/importing-music.rst
index f09eea7b184d86bd465f5ba40c4c49343066da31..97dd1385485c18eab10fe805e49ff829140d3654 100644
--- a/docs/importing-music.rst
+++ b/docs/importing-music.rst
@@ -22,8 +22,15 @@ to the ``/music`` directory on the container:
 
     docker-compose run --rm api python manage.py import_files "/music/**/*.ogg" --recursive --noinput
 
-For the best results, we recommand tagging your music collection through
-`Picard <http://picard.musicbrainz.org/>`_ in order to have the best quality metadata.
+The import command supports several options, and you can check the help to
+get details::
+
+    docker-compose run --rm api python manage.py import_files --help
+
+.. note::
+
+    For the best results, we recommand tagging your music collection through
+    `Picard <http://picard.musicbrainz.org/>`_ in order to have the best quality metadata.
 
 .. note::
 
@@ -39,18 +46,39 @@ For the best results, we recommand tagging your music collection through
 
     At the moment, only OGG/Vorbis and MP3 files with ID3 tags are supported
 
-.. note::
 
-    The --recursive flag will work only on Python 3.5+, which is the default
-    version When using Docker or Debian 9. If you use an older version of Python,
-    remove the --recursive flag and use more explicit import patterns instead::
+.. _in-place-import:
+
+In-place import
+^^^^^^^^^^^^^^^
+
+By default, the CLI-importer will copy imported files to Funkwhale's internal
+storage. This means importing a 1Gb library will result in the same amount
+of space being used by Funkwhale.
+
+While this behaviour has some benefits (easier backups and configuration),
+it's not always the best choice, especially if you have a huge library
+to import and don't want to double your disk usage.
+
+The CLI importer supports an additional ``--in-place`` option that triggers the
+following behaviour during import:
+
+1. Imported files are not store in funkwhale anymore
+2. Instead, Funkwhale will store the file path and use it to serve the music
+
+Because those files are not managed by Funkwhale, we offer additional
+configuration options to ensure the webserver can serve them properly:
+
+- :ref:`setting-MUSIC_DIRECTORY_PATH`
+- :ref:`setting-MUSIC_DIRECTORY_SERVE_PATH`
 
-        # this will only import ogg files at the second level
-        "/srv/funkwhale/data/music/*/*.ogg"
-        # this will only import ogg files in the fiven directory
-        "/srv/funkwhale/data/music/System-of-a-down/*.ogg"
+.. warning::
 
+    While in-place import is faster and less disk-space-hungry, it's also
+    more fragile: if, for some reason, you move or rename the source files,
+    Funkwhale will not be able to serve those files anymore.
 
+    Thus, be especially careful when you manipulate the source files.
 
 Getting demo tracks
 ^^^^^^^^^^^^^^^^^^^
diff --git a/docs/installation/index.rst b/docs/installation/index.rst
index 2e62c71ec5b547da9a9fcd4db7a4566ff9814b57..776c22424f15929324520e1d57076d4bd2a5656c 100644
--- a/docs/installation/index.rst
+++ b/docs/installation/index.rst
@@ -12,9 +12,48 @@ The project relies on the following components and services to work:
 - A celery worker to run asynchronouse tasks (such as music import)
 - A celery scheduler to run recurrent tasks
 
+
+Hardware requirements
+---------------------
+
+Funkwhale is not especially CPU hungry, unless you're relying heavily
+on the transcoding feature (which is basic and unoptimized at the moment).
+
+On a dockerized instance with 2 CPUs and a few active users, the memory footprint is around ~500Mb::
+
+   CONTAINER                   MEM USAGE
+   funkwhale_api_1             202.1 MiB
+   funkwhale_celerybeat_1      96.52 MiB
+   funkwhale_celeryworker_1    168.7 MiB
+   funkwhale_postgres_1        22.73 MiB
+   funkwhale_redis_1           1.496 MiB
+
+Thus, Funkwhale should run fine on commodity hardware, small hosting boxes and
+Raspberry Pi. We lack real-world exemples of such deployments, so don't hesitate
+do give us your feedback (either positive or negative).
+
+Software requirements
+---------------------
+
+Software requirements will vary depending of your installation method. For
+Docker-based installations, the only requirement will be an Nginx reverse-proxy
+that will expose your instance to the outside world.
+
+If you plan to install your Funkwhale instance without Docker, most of the
+dependencies should be available in your distribution's repositories.
+
+.. note::
+
+   Funkwhale works only with Pyhon >= 3.5, as we need support for async/await.
+   Older versions of Python are not supported.
+
+
 Available installation methods
 -------------------------------
 
+Docker is the recommended and easiest way to setup your Funkwhale instance.
+We also maintain an installation guide for Debian 9.
+
 .. toctree::
    :maxdepth: 1
 
@@ -67,3 +106,24 @@ Then, download our sample virtualhost file and proxy conf:
     curl -L -o /etc/nginx/sites-enabled/funkwhale.conf "https://code.eliotberriot.com/funkwhale/funkwhale/raw/|version|/deploy/nginx.conf"
 
 Ensure static assets and proxy pass match your configuration, and check the configuration is valid with ``nginx -t``. If everything is fine, you can restart your nginx server with ``service nginx restart``.
+
+About internal locations
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+Music (and other static) files are never served by the app itself, but by the reverse
+proxy. This is needed because a webserver is way more efficient at serving
+files than a Python process.
+
+However, we do want to ensure users have the right to access music files, and
+it can't be done at the proxy's level. To tackle this issue, `we use
+nginx's internal directive <http://nginx.org/en/docs/http/ngx_http_core_module.html#internal>`_.
+
+When the API receives a request on its music serving endpoint, it will check
+that the user making the request can access the file. Then, it will return an empty
+response with a ``X-Accel-Redirect`` header. This header will contain the path
+to the file to serve to the user, and will be picked by nginx, but never sent
+back to the client.
+
+Using this technique, we can ensure music files are covered by the authentication
+and permission policy of your instance, while keeping as much as performance
+as possible.
diff --git a/front/src/components/About.vue b/front/src/components/About.vue
index 09a5ee24c523f8f8f5db77b47b7c6694ae74387b..524191250cf920fdbe4991381f827c1e50dd5ecd 100644
--- a/front/src/components/About.vue
+++ b/front/src/components/About.vue
@@ -3,15 +3,16 @@
     <div class="ui vertical center aligned stripe segment">
       <div class="ui text container">
         <h1 class="ui huge header">
-            <template v-if="instance.name.value">About {{ instance.name.value }}</template>
-            <template v-else="instance.name.value">About this instance</template>
+            <template v-if="instance.name.value">{{ $t('About {%instance%}', { instance: instance.name.value }) }}</template>
+            <template v-else="instance.name.value">{{ $t('About this instance') }}</template>
         </h1>
         <stats></stats>
       </div>
     </div>
     <div class="ui vertical stripe segment">
       <p v-if="!instance.short_description.value && !instance.long_description.value">
-        Unfortunately, owners of this instance did not yet take the time to complete this page.</p>
+        {{ $t('Unfortunately, owners of this instance did not yet take the time to complete this page.') }}
+      </p>
       <div
         v-if="instance.short_description.value"
         class="ui middle aligned stackable text container">
diff --git a/front/src/components/Home.vue b/front/src/components/Home.vue
index ce1307ff0c8a84b934f027c7a27509644edc32a5..03f4513e6cb645ab17dc79c75dcee32c202ef0b8 100644
--- a/front/src/components/Home.vue
+++ b/front/src/components/Home.vue
@@ -3,15 +3,15 @@
     <div class="ui vertical center aligned stripe segment">
       <div class="ui text container">
         <h1 class="ui huge header">
-          Welcome on Funkwhale
+          {{ $t('Welcome on Funkwhale') }}
         </h1>
-        <p>We think listening music should be simple.</p>
+        <p>{{ $t('We think listening music should be simple.') }}</p>
         <router-link class="ui icon button" to="/about">
           <i class="info icon"></i>
-          Learn more about this instance
+          {{ $t('Learn more about this instance') }}
         </router-link>
         <router-link class="ui icon teal button" to="/library">
-          Get me to the library
+          {{ $t('Get me to the library') }}
           <i class="right arrow icon"></i>
         </router-link>
       </div>
@@ -22,9 +22,9 @@
           <div class="row">
             <div class="eight wide left floated column">
               <h2 class="ui header">
-                Why funkwhale?
+                {{ $t('Why funkwhale?') }}
               </h2>
-              <p>That's simple: we loved Grooveshark and we want to build something even better.</p>
+              <p>{{ $t('That\'s simple: we loved Grooveshark and we want to build something even better.') }}</p>
             </div>
             <div class="four wide left floated column">
               <img class="ui medium image" src="../assets/logo/logo.png" />
@@ -35,26 +35,26 @@
       <div class="ui middle aligned stackable text container">
         <div class="ui hidden divider"></div>
         <h2 class="ui header">
-          Unlimited music
+          {{ $t('Unlimited music') }}
         </h2>
-        <p>Funkwhale is designed to make it easy to listen to music you like, or to discover new artists.</p>
+        <p>{{ $t('Funkwhale is designed to make it easy to listen to music you like, or to discover new artists.') }}</p>
         <div class="ui list">
           <div class="item">
             <i class="sound icon"></i>
             <div class="content">
-              Click once, listen for hours using built-in radios
+              {{ $t('Click once, listen for hours using built-in radios') }}
             </div>
           </div>
           <div class="item">
             <i class="heart icon"></i>
             <div class="content">
-              Keep a track of your favorite songs
+              {{ $t('Keep a track of your favorite songs') }}
             </div>
           </div>
           <div class="item">
             <i class="list icon"></i>
             <div class="content">
-              Playlists? We got them
+              {{ $t('Playlists? We got them') }}
             </div>
           </div>
         </div>
@@ -62,26 +62,28 @@
       <div class="ui middle aligned stackable text container">
         <div class="ui hidden divider"></div>
         <h2 class="ui header">
-          Clean library
+          {{ $t('Clean library') }}
         </h2>
-        <p>Funkwhale takes care of handling your music.</p>
+        <p>{{ $t('Funkwhale takes care of handling your music') }}.</p>
         <div class="ui list">
           <div class="item">
             <i class="download icon"></i>
             <div class="content">
-              Import music from various platforms, such as YouTube or SoundCloud
+              {{ $t('Import music from various platforms, such as YouTube or SoundCloud') }}
             </div>
           </div>
           <div class="item">
             <i class="tag icon"></i>
             <div class="content">
-              Get quality metadata about your music thanks to <a href="https://musicbrainz.org" target="_blank">MusicBrainz</a>
+              <i18next path="Get quality metadata about your music thanks to {%0%}">
+                <a href="https://musicbrainz.org" target="_blank">{{ $t('MusicBrainz') }}</a>
+              </i18next>
             </div>
           </div>
           <div class="item">
             <i class="plus icon"></i>
             <div class="content">
-              Covers, lyrics, our goal is to have them all ;)
+              {{ $t('Covers, lyrics, our goal is to have them all ;)') }}
             </div>
           </div>
         </div>
@@ -89,20 +91,20 @@
       <div class="ui middle aligned stackable text container">
         <div class="ui hidden divider"></div>
         <h2 class="ui header">
-          Easy to use
+          {{ $t('Easy to use') }}
         </h2>
-        <p>Funkwhale is dead simple to use.</p>
+        <p>{{ $t('Funkwhale is dead simple to use.') }}</p>
         <div class="ui list">
           <div class="item">
             <i class="book icon"></i>
             <div class="content">
-              No add-ons, no plugins : you only need a web library
+              {{ $t('No add-ons, no plugins : you only need a web library') }}
             </div>
           </div>
           <div class="item">
             <i class="wizard icon"></i>
             <div class="content">
-              Access your music from a clean interface that focus on what really matters
+              {{ $t('Access your music from a clean interface that focus on what really matters') }}
             </div>
           </div>
         </div>
@@ -110,26 +112,26 @@
       <div class="ui middle aligned stackable text container">
         <div class="ui hidden divider"></div>
         <h2 class="ui header">
-          Your music, your way
+          {{ $t('Your music, your way') }}
         </h2>
-        <p>Funkwhale is free and gives you control on your music.</p>
+        <p>{{ $t('Funkwhale is free and gives you control on your music.') }}</p>
         <div class="ui list">
           <div class="item">
             <i class="smile icon"></i>
             <div class="content">
-              The plaform is free and open-source, you can install it and modify it without worries
+              {{ $t('The plaform is free and open-source, you can install it and modify it without worries') }}
             </div>
           </div>
           <div class="item">
             <i class="protect icon"></i>
             <div class="content">
-              We do not track you or bother you with ads
+              {{ $t('We do not track you or bother you with ads') }}
             </div>
           </div>
           <div class="item">
             <i class="users icon"></i>
             <div class="content">
-              You can invite friends and family to your instance so they can enjoy your music
+              {{ $t('You can invite friends and family to your instance so they can enjoy your music') }}
             </div>
           </div>
         </div>
diff --git a/front/src/components/PageNotFound.vue b/front/src/components/PageNotFound.vue
index 25e6f86fd209a7cb6132c9c36812e918c1f116f9..b4d2250ca061db4b36c729b42dc22901d684cd7d 100644
--- a/front/src/components/PageNotFound.vue
+++ b/front/src/components/PageNotFound.vue
@@ -5,13 +5,13 @@
         <h1 class="ui huge header">
           <i class="warning icon"></i>
           <div class="content">
-            <strike>Whale</strike> Page not found!
+            <strike>{{ $t('Whale') }}</strike> {{ $t('Page not found!') }}
           </div>
         </h1>
-        <p>We're sorry, the page you asked for does not exists.</p>
-        <p>Requested URL: <a :href="path">{{ path }}</a></p>
+        <p>{{ $t('We\'re sorry, the page you asked for does not exists.') }}</p>
+        <i18next path="Requested URL: {%0%}"><a :href="path">{{ path }}</a></i18next>
         <router-link class="ui icon button" to="/">
-          Go to home page
+          {{ $t('Go to home page') }}
           <i class="right arrow icon"></i>
         </router-link>
       </div>
diff --git a/front/src/components/Sidebar.vue b/front/src/components/Sidebar.vue
index 96047ab9848621c55fe5b75971bfca47e6c9ba62..fb4074d80703ff8c13685eb7ff1298551e3ff250 100644
--- a/front/src/components/Sidebar.vue
+++ b/front/src/components/Sidebar.vue
@@ -18,12 +18,12 @@
     <div class="ui compact fluid two item inverted menu">
       <a class="active item" @click="selectedTab = 'library'" data-tab="library">Browse</a>
       <a class="item" @click="selectedTab = 'queue'" data-tab="queue">
-        Queue &nbsp;
+        {{ $t('Queue') }}
          <template v-if="queue.tracks.length === 0">
-           (empty)
+           {{ $t('(empty)') }}
          </template>
          <template v-else>
-           ({{ queue.currentIndex + 1}} of {{ queue.tracks.length }})
+           {{ $t('({%index%} of {%length%})', { index: queue.currentIndex + 1, length: queue.tracks.length }) }}
          </template>
       </a>
     </div>
@@ -31,37 +31,35 @@
   <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="$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>
+        <router-link class="item" v-if="$store.state.auth.authenticated" :to="{name: 'profile', params: {username: $store.state.auth.username}}"><i class="user icon"></i>{{ $t('Logged in as {%name%}', { name: $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> {{ $t('Logout') }}</router-link>
+        <router-link class="item" v-else :to="{name: 'login'}"><i class="sign in icon"></i> {{ $t('Login') }}</router-link>
+        <router-link class="item" :to="{path: '/library'}"><i class="sound icon"> </i>{{ $t('Browse library') }}</router-link>
+        <router-link class="item" v-if="$store.state.auth.authenticated" :to="{path: '/favorites'}"><i class="heart icon"></i> {{ $t('Favorites') }}</router-link>
         <a
           @click="$store.commit('playlists/chooseTrack', null)"
           v-if="$store.state.auth.authenticated"
           class="item">
-          <i class="list icon"></i> Playlists
+          <i class="list icon"></i> {{ $t('Playlists') }}
         </a>
         <router-link
           v-if="$store.state.auth.authenticated"
-          class="item" :to="{path: '/activity'}"><i class="bell icon"></i> Activity</router-link>
+          class="item" :to="{path: '/activity'}"><i class="bell icon"></i> {{ $t('Activity') }}</router-link>
         <router-link
           class="item" v-if="$store.state.auth.availablePermissions['federation.manage']"
-          :to="{path: '/manage/federation/libraries'}"><i class="sitemap icon"></i> Federation</router-link>
+          :to="{path: '/manage/federation/libraries'}"><i class="sitemap icon"></i> {{ $t('Federation') }}</router-link>
       </div>
-
-      <player></player>
     </div>
     <div v-if="queue.previousQueue " class="ui black icon message">
       <i class="history icon"></i>
       <div class="content">
         <div class="header">
-          Do you want to restore your previous queue?
+          {{ $t('Do you want to restore your previous queue?') }}
         </div>
-        <p>{{ queue.previousQueue.tracks.length }} tracks</p>
+        <p>{{ $t('{%count%} tracks', { count: queue.previousQueue.tracks.length }) }}</p>
         <div class="ui two buttons">
-          <div @click="queue.restore()" class="ui basic inverted green button">Yes</div>
-          <div @click="queue.removePrevious()" class="ui basic inverted red button">No</div>
+          <div @click="queue.restore()" class="ui basic inverted green button">{{ $t('Yes') }}</div>
+          <div @click="queue.removePrevious()" class="ui basic inverted red button">{{ $t('No') }}</div>
         </div>
       </div>
     </div>
@@ -90,17 +88,17 @@
           </draggable>
       </table>
       <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
+            <i class="feed icon"></i> {{ $t('You have a radio playing') }}
           </div>
-          <p>New tracks will be appended here automatically.</p>
-          <div @click="$store.dispatch('radios/stop')" class="ui basic inverted red button">Stop radio</div>
+          <p>{{ $t('New tracks will be appended here automatically.') }}</p>
+          <div @click="$store.dispatch('radios/stop')" class="ui basic inverted red button">{{ $t('Stop radio') }}</div>
         </div>
       </div>
     </div>
   </div>
+  <player @next="scrollToCurrent" @previous="scrollToCurrent"></player>
 </div>
 </template>
 
@@ -143,8 +141,9 @@ export default {
     ...mapActions({
       cleanTrack: 'queue/cleanTrack'
     }),
-    reorder: function (oldValue, newValue) {
-      this.$store.commit('queue/reorder', {oldValue, newValue})
+    reorder: function (event) {
+      this.$store.commit('queue/reorder', {
+        oldIndex: event.oldIndex, newIndex: event.newIndex})
     },
     scrollToCurrent () {
       let current = $(this.$el).find('[data-tab="queue"] .active')[0]
@@ -159,7 +158,6 @@ export default {
       // for half the height of the containers display area
       var scrollBack = (container.scrollHeight - container.scrollTop <= container.clientHeight) ? 0 : container.clientHeight / 2
       container.scrollTop = container.scrollTop - scrollBack
-      console.log(container.scrollHeight - container.scrollTop, container.clientHeight)
     }
   },
   watch: {
@@ -239,9 +237,6 @@ $sidebar-color: #3D3E3F;
   flex-direction: column;
   overflow-y: auto;
   justify-content: space-between;
-  @include media(">tablet") {
-    height: 0px;
-  }
   @include media("<desktop") {
     max-height: 500px;
   }
diff --git a/front/src/components/activity/Like.vue b/front/src/components/activity/Like.vue
index deda121cc9f8def680eeebd08159fc92c5ee97db..5396accc233033222eca551364cc4da740f59a47 100644
--- a/front/src/components/activity/Like.vue
+++ b/front/src/components/activity/Like.vue
@@ -5,10 +5,10 @@
    </div>
    <div class="content">
      <div class="summary">
-       <i18next path="{%0%} favorited a track {%1%}">
-        <slot name="user"></slot>
-        <slot name="date"></slot>
+       <i18next path="{%0%} favorited a track">
+         <username class="user" :username="event.actor.local_id" />
        </i18next>
+       <human-date class="date" :date="event.published" />
      </div>
      <div class="extra text">
        <router-link :to="{name: 'library.tracks.detail', params: {id: event.object.local_id }}">{{ event.object.name }}</router-link>
diff --git a/front/src/components/activity/Listen.vue b/front/src/components/activity/Listen.vue
index d207c280deef14f44cada9342a2732eabfdc1c48..bfa3ca16450a69adfa086e1189ab0446e81e0733 100644
--- a/front/src/components/activity/Listen.vue
+++ b/front/src/components/activity/Listen.vue
@@ -5,16 +5,16 @@
    </div>
    <div class="content">
      <div class="summary">
-       <i18next path="{%0%} listened to a track {%1%}">
-        <slot name="user"></slot>
-        <slot name="date"></slot>
+       <i18next path="{%0%} listened to a track">
+         <username class="user" :username="event.actor.local_id" />
        </i18next>
+       <human-date class="date" :date="event.published" />
+
      </div>
      <div class="extra text">
        <router-link :to="{name: 'library.tracks.detail', params: {id: event.object.local_id }}">{{ event.object.name }}</router-link>
         <i18next path="from album {%0%}, by {%1%}" v-if="event.object.album">
-          {{ event.object.album }}
-          <em>{{ event.object.artist }}</em>
+          {{ event.object.album }}<em>{{ event.object.artist }}</em>
         </i18next>
         <i18next path=", by {%0%}" v-else>
           <em>{{ event.object.artist }}</em>
diff --git a/front/src/components/audio/Player.vue b/front/src/components/audio/Player.vue
index ad90a599528a09cfd287389ddfe22a7585f9fa69..c475ec684a008ec46392361523eefc77ab38b0e6 100644
--- a/front/src/components/audio/Player.vue
+++ b/front/src/components/audio/Player.vue
@@ -4,7 +4,7 @@
       <audio-track
         ref="currentAudio"
         v-if="renderAudio && currentTrack"
-        :key="(currentIndex, currentTrack.id)"
+        :key="currentTrack.id"
         :is-current="true"
         :start-time="$store.state.player.currentTime"
         :autoplay="$store.state.player.playing"
@@ -173,11 +173,21 @@ export default {
     ...mapActions({
       togglePlay: 'player/togglePlay',
       clean: 'queue/clean',
-      next: 'queue/next',
-      previous: 'queue/previous',
       shuffle: 'queue/shuffle',
       updateProgress: 'player/updateProgress'
     }),
+    next () {
+      let self = this
+      this.$store.dispatch('queue/next').then(() => {
+        self.$emit('next')
+      })
+    },
+    previous () {
+      let self = this
+      this.$store.dispatch('queue/previous').then(() => {
+        self.$emit('previous')
+      })
+    },
     touchProgress (e) {
       let time
       let target = this.$refs.progress
diff --git a/front/src/components/audio/Track.vue b/front/src/components/audio/Track.vue
index 68dd34459870f432d7ca5c9e3b35a6d7966b225d..08a055f5ca97186d51130e419176338473f9390e 100644
--- a/front/src/components/audio/Track.vue
+++ b/front/src/components/audio/Track.vue
@@ -73,7 +73,10 @@ export default {
   },
   methods: {
     errored: function () {
-      this.$store.dispatch('player/trackErrored')
+      let self = this
+      setTimeout(
+        () => { self.$store.dispatch('player/trackErrored') }
+      , 1000)
     },
     sourceErrored: function () {
       this.sourceErrors += 1
@@ -83,9 +86,15 @@ export default {
       }
     },
     updateDuration: function (e) {
+      if (!this.$refs.audio) {
+        return
+      }
       this.$store.commit('player/duration', this.$refs.audio.duration)
     },
     loaded: function () {
+      if (!this.$refs.audio) {
+        return
+      }
       this.$refs.audio.volume = this.volume
       this.$store.commit('player/resetErrorCount')
       if (this.isCurrent) {
diff --git a/front/src/components/library/Artist.vue b/front/src/components/library/Artist.vue
index 5c17ac6af3d338fbdafff69511d7cc35c42877de..e16cb6587431d0d7494cae1ef0e92bf5c96fe189 100644
--- a/front/src/components/library/Artist.vue
+++ b/front/src/components/library/Artist.vue
@@ -11,10 +11,7 @@
             <div class="content">
               {{ artist.name }}
               <div class="sub header">
-                <i18next path="{%0%} tracks in {%1%} albums">
-                  {{ totalTracks }}
-                  {{ albums.length }}
-                </i18next>
+                {{ $t('{% track_count %} tracks in {% album_count %} albums', {track_count: totalTracks, album_count: albums.length})}}
               </div>
             </div>
           </h2>
diff --git a/front/src/components/library/import/BatchDetail.vue b/front/src/components/library/import/BatchDetail.vue
index c7894fcc0249c4f27f58e6c6f29327b52f3929f5..b73c8cf8257599e87cc21aea05ce669f58aaac45 100644
--- a/front/src/components/library/import/BatchDetail.vue
+++ b/front/src/components/library/import/BatchDetail.vue
@@ -4,31 +4,80 @@
       <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
     </div>
     <div v-if="batch" class="ui vertical stripe segment">
-      <div :class="
-        ['ui',
-        {'active': batch.status === 'pending'},
-        {'warning': batch.status === 'pending'},
-        {'error': batch.status === 'errored'},
-        {'success': batch.status === 'finished'},
-        'progress']">
-        <div class="bar" :style="progressBarStyle">
-          <div class="progress"></div>
+      <table class="ui very basic table">
+        <tbody>
+          <tr>
+            <td>
+              <strong>{{ $t('Import batch') }}</strong>
+            </td>
+            <td>
+              #{{ batch.id }}
+            </td>
+          </tr>
+          <tr>
+            <td>
+              <strong>{{ $t('Launch date') }}</strong>
+            </td>
+            <td>
+              <human-date :date="batch.creation_date"></human-date>
+            </td>
+          </tr>
+          <tr v-if="batch.user">
+            <td>
+              <strong>{{ $t('Submitted by') }}</strong>
+            </td>
+            <td>
+              <username :username="batch.user.username" />
+            </td>
+          </tr>
+          <tr v-if="stats">
+            <td><strong>{{ $t('Pending') }}</strong></td>
+            <td>{{ stats.pending }}</td>
+          </tr>
+          <tr v-if="stats">
+            <td><strong>{{ $t('Skipped') }}</strong></td>
+            <td>{{ stats.skipped }}</td>
+          </tr>
+          <tr v-if="stats">
+            <td><strong>{{ $t('Errored') }}</strong></td>
+            <td>{{ stats.errored }}</td>
+          </tr>
+          <tr v-if="stats">
+            <td><strong>{{ $t('Finished') }}</strong></td>
+            <td>{{ stats.finished }}/{{ stats.count}}</td>
+          </tr>
+        </tbody>
+      </table>
+      <div class="ui inline form">
+        <div class="fields">
+          <div class="ui field">
+            <label>{{ $t('Search') }}</label>
+            <input type="text" v-model="jobFilters.search" placeholder="Search by source..." />
+          </div>
+          <div class="ui field">
+            <label>{{ $t('Status') }}</label>
+            <select class="ui dropdown" v-model="jobFilters.status">
+              <option :value="null">{{ $t('Any') }}</option>
+              <option :value="'pending'">{{ $t('Pending') }}</option>
+              <option :value="'errored'">{{ $t('Errored') }}</option>
+              <option :value="'finished'">{{ $t('Success') }}</option>
+              <option :value="'skipped'">{{ $t('Skipped') }}</option>
+            </select>
+          </div>
         </div>
-        <div v-if="batch.status === 'pending'" class="label">Importing {{ batch.jobs.length }} tracks...</div>
-        <div v-if="batch.status === 'finished'" class="label">Imported {{ batch.jobs.length }} tracks!</div>
       </div>
-      <table class="ui unstackable table">
+      <table v-if="jobResult" class="ui unstackable table">
         <thead>
           <tr>
-            <i18next tag="th" path="Job ID"/>
-            <i18next tag="th" path="Recording MusicBrainz ID"/>
-            <i18next tag="th" path="Source"/>
-            <i18next tag="th" path="Status"/>
-            <i18next tag="th" path="Track"/>
+            <th>{{ $t('Job ID') }}</th>
+            <th>{{ $t('Recording MusicBrainz ID') }}</th>
+            <th>{{ $t('Source') }}</th>
+            <th>{{ $t('Status') }}</th>
+            <th>{{ $t('Track') }}</th>
           </tr>
         </thead>
         <tbody>
-          <tr v-for="job in batch.jobs">
+          <tr v-for="job in jobResult.results">
             <td>{{ job.id }}</th>
             <td>
               <a :href="'https://www.musicbrainz.org/recording/' + job.mbid" target="_blank">{{ job.mbid }}</a>
@@ -45,29 +94,64 @@
             </td>
           </tr>
         </tbody>
+        <tfoot class="full-width">
+          <tr>
+            <th>
+              <pagination
+              v-if="jobResult && jobResult.results.length > 0"
+              @page-changed="selectPage"
+              :compact="true"
+              :current="jobFilters.page"
+              :paginate-by="jobFilters.paginateBy"
+              :total="jobResult.count"
+              ></pagination>
+            </th>
+            <th v-if="jobResult && jobResult.results.length > 0">
+              {{ $t('Showing results {%start%}-{%end%} on {%total%}', {start: ((jobFilters.page-1) * jobFilters.paginateBy) + 1 , end: ((jobFilters.page-1) * jobFilters.paginateBy) + jobResult.results.length, total: jobResult.count})}}
+            <th>
+            <th></th>
+            <th></th>
+            <th></th>
+          </tr>
+        </tfoot>
       </table>
-
     </div>
   </div>
 </template>
 
 <script>
+import _ from 'lodash'
 import axios from 'axios'
 import logger from '@/logging'
-
-const FETCH_URL = 'import-batches/'
+import Pagination from '@/components/Pagination'
 
 export default {
   props: ['id'],
+  components: {
+    Pagination
+  },
   data () {
     return {
       isLoading: true,
       batch: null,
-      timeout: null
+      stats: null,
+      jobResult: null,
+      timeout: null,
+      jobFilters: {
+        status: null,
+        source: null,
+        search: '',
+        paginateBy: 25,
+        page: 1
+      }
     }
   },
   created () {
-    this.fetchData()
+    let self = this
+    this.fetchData().then(() => {
+      self.fetchJobs()
+      self.fetchStats()
+    })
   },
   destroyed () {
     if (this.timeout) {
@@ -78,9 +162,9 @@ export default {
     fetchData () {
       var self = this
       this.isLoading = true
-      let url = FETCH_URL + this.id + '/'
+      let url = 'import-batches/' + this.id + '/'
       logger.default.debug('Fetching batch "' + this.id + '"')
-      axios.get(url).then((response) => {
+      return axios.get(url).then((response) => {
         self.batch = response.data
         self.isLoading = false
         if (self.batch.status === 'pending') {
@@ -90,21 +174,58 @@ export default {
           )
         }
       })
-    }
-  },
-  computed: {
-    progress () {
-      return this.batch.jobs.filter(j => {
-        return j.status !== 'pending'
-      }).length * 100 / this.batch.jobs.length
     },
-    progressBarStyle () {
-      return 'width: ' + parseInt(this.progress) + '%'
+    fetchStats () {
+      var self = this
+      let url = 'import-jobs/stats/'
+      axios.get(url, {params: {batch: self.id}}).then((response) => {
+        let old = self.stats
+        self.stats = response.data
+        self.isLoading = false
+        if (!_.isEqual(old, self.stats)) {
+          self.fetchJobs()
+          self.fetchData()
+        }
+        if (self.batch.status === 'pending') {
+          self.timeout = setTimeout(
+            self.fetchStats,
+            5000
+          )
+        }
+      })
+    },
+    fetchJobs () {
+      let params = {
+        batch: this.id,
+        page_size: this.jobFilters.paginateBy,
+        page: this.jobFilters.page,
+        q: this.jobFilters.search
+      }
+      if (this.jobFilters.status) {
+        params.status = this.jobFilters.status
+      }
+      if (this.jobFilters.source) {
+        params.source = this.jobFilters.source
+      }
+      let self = this
+      axios.get('import-jobs/', {params}).then((response) => {
+        self.jobResult = response.data
+      })
+    },
+    selectPage: function (page) {
+      this.jobFilters.page = page
     }
+
   },
   watch: {
     id () {
       this.fetchData()
+    },
+    jobFilters: {
+      handler () {
+        this.fetchJobs()
+      },
+      deep: true
     }
   }
 }
diff --git a/front/src/components/library/import/BatchList.vue b/front/src/components/library/import/BatchList.vue
index 324c3990a1494892435162bb52aa78fb516478c1..bf5a0ca47599e79b56559563aca8291fb3aff0e4 100644
--- a/front/src/components/library/import/BatchList.vue
+++ b/front/src/components/library/import/BatchList.vue
@@ -2,76 +2,144 @@
   <div v-title="'Import Batches'">
     <div class="ui vertical stripe segment">
       <div v-if="isLoading" :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
-      <button
-        class="ui left floated labeled icon button"
-        @click="fetchData(previousLink)"
-        :disabled="!previousLink"><i class="left arrow icon"></i><i18next path="Previous"/></button>
-      <button
-        class="ui right floated right labeled icon button"
-        @click="fetchData(nextLink)"
-        :disabled="!nextLink"><i18next path="Next"/><i class="right arrow icon"></i></button>
-      <div class="ui hidden clearing divider"></div>
+      <div class="ui inline form">
+        <div class="fields">
+          <div class="ui field">
+            <label>{{ $t('Search') }}</label>
+            <input type="text" v-model="filters.search" placeholder="Search by submitter, source..." />
+          </div>
+          <div class="ui field">
+            <label>{{ $t('Status') }}</label>
+            <select class="ui dropdown" v-model="filters.status">
+              <option :value="null">{{ $t('Any') }}</option>
+              <option :value="'pending'">{{ $t('Pending') }}</option>
+              <option :value="'errored'">{{ $t('Errored') }}</option>
+              <option :value="'finished'">{{ $t('Success') }}</option>
+            </select>
+          </div>
+          <div class="ui field">
+            <label>{{ $t('Import source') }}</label>
+            <select class="ui dropdown" v-model="filters.source">
+              <option :value="null">{{ $t('Any') }}</option>
+              <option :value="'shell'">{{ $t('CLI') }}</option>
+              <option :value="'api'">{{ $t('API') }}</option>
+              <option :value="'federation'">{{ $t('Federation') }}</option>
+            </select>
+          </div>
+        </div>
+      </div>
       <div class="ui hidden clearing divider"></div>
-      <table v-if="results.length > 0" class="ui unstackable table">
+      <table v-if="result && result.results.length > 0" class="ui unstackable table">
         <thead>
           <tr>
-            <i18next tag="th" path="ID"/>
-            <i18next tag="th" path="Launch date"/>
-            <i18next tag="th" path="Jobs"/>
-            <i18next tag="th" path="Status"/>
+            <th>{{ $t('ID') }}</th>
+            <th>{{ $t('Launch date') }}</th>
+            <th>{{ $t('Jobs') }}</th>
+            <th>{{ $t('Status') }}</th>
+            <th>{{ $t('Source') }}</th>
+            <th>{{ $t('Submitted by') }}</th>
           </tr>
         </thead>
         <tbody>
-          <tr v-for="result in results">
-            <td>{{ result.id }}</th>
+          <tr v-for="obj in result.results">
+            <td>{{ obj.id }}</th>
             <td>
-              <router-link :to="{name: 'library.import.batches.detail', params: {id: result.id }}">
-                {{ result.creation_date }}
+              <router-link :to="{name: 'library.import.batches.detail', params: {id: obj.id }}">
+                <human-date :date="obj.creation_date"></human-date>
               </router-link>
             </td>
-            <td>{{ result.jobs.length }}</td>
+            <td>{{ obj.job_count }}</td>
             <td>
               <span
-                :class="['ui', {'yellow': result.status === 'pending'}, {'red': result.status === 'errored'}, {'green': result.status === 'finished'}, 'label']">{{ result.status }}</span>
-              </td>
-            </tr>
-          </tbody>
-        </table>
-      </div>
+                :class="['ui', {'yellow': obj.status === 'pending'}, {'red': obj.status === 'errored'}, {'green': obj.status === 'finished'}, 'label']">{{ obj.status }}
+              </span>
+            </td>
+            <td>{{ obj.source }}</td>
+            <td><template v-if="obj.submitted_by">{{ obj.submitted_by.username }}</template></td>
+          </tr>
+        </tbody>
+        <tfoot class="full-width">
+          <tr>
+            <th>
+              <pagination
+              v-if="result && result.results.length > 0"
+              @page-changed="selectPage"
+              :compact="true"
+              :current="filters.page"
+              :paginate-by="filters.paginateBy"
+              :total="result.count"
+              ></pagination>
+            </th>
+            <th v-if="result && result.results.length > 0">
+              {{ $t('Showing results {%start%}-{%end%} on {%total%}', {start: ((filters.page-1) * filters.paginateBy) + 1 , end: ((filters.page-1) * filters.paginateBy) + result.results.length, total: result.count})}}
+            <th>
+            <th></th>
+            <th></th>
+            <th></th>
+          </tr>
+        </tfoot>
+      </table>
+    </div>
   </div>
 </template>
 
 <script>
 import axios from 'axios'
 import logger from '@/logging'
-
-const BATCHES_URL = 'import-batches/'
+import Pagination from '@/components/Pagination'
 
 export default {
-  components: {},
+  components: {
+    Pagination
+  },
   data () {
     return {
-      results: [],
+      result: null,
       isLoading: false,
-      nextLink: null,
-      previousLink: null
+      filters: {
+        status: null,
+        source: null,
+        search: '',
+        paginateBy: 25,
+        page: 1
+      }
     }
   },
   created () {
-    this.fetchData(BATCHES_URL)
+    this.fetchData()
   },
   methods: {
-    fetchData (url) {
+    fetchData () {
+      let params = {
+        page_size: this.filters.paginateBy,
+        page: this.filters.page,
+        q: this.filters.search
+      }
+      if (this.filters.status) {
+        params.status = this.filters.status
+      }
+      if (this.filters.source) {
+        params.source = this.filters.source
+      }
       var self = this
       this.isLoading = true
       logger.default.time('Loading import batches')
-      axios.get(url, {}).then((response) => {
-        self.results = response.data.results
-        self.nextLink = response.data.next
-        self.previousLink = response.data.previous
+      axios.get('import-batches/', {params}).then((response) => {
+        self.result = response.data
         logger.default.timeEnd('Loading import batches')
         self.isLoading = false
       })
+    },
+    selectPage: function (page) {
+      this.filters.page = page
+    }
+  },
+  watch: {
+    filters: {
+      handler () {
+        this.fetchData()
+      },
+      deep: true
     }
   }
 }
diff --git a/front/src/components/metadata/ArtistCard.vue b/front/src/components/metadata/ArtistCard.vue
index 3a50a315522aed317437e203080f4a7061e4fc3c..c88438c0c0917e0394450affa04e1c1e5ec1958f 100644
--- a/front/src/components/metadata/ArtistCard.vue
+++ b/front/src/components/metadata/ArtistCard.vue
@@ -16,7 +16,7 @@
                   {{ group['first-release-date'] }}
                 </td>
                 <td colspan="3">
-                  <a :href="getMusicbrainzUrl('release-group', group.id)" class="discrete link" target="_blank" title="View on MusicBrainz">
+                  <a :href="getMusicbrainzUrl('release-group', group.id)" class="discrete link" target="_blank" :title="$t('View on MusicBrainz')">
                     {{ group.title }}
                   </a>
                 </td>
diff --git a/front/src/components/metadata/ReleaseCard.vue b/front/src/components/metadata/ReleaseCard.vue
index 201c3ab0c527440acb56f4320ff2e505a6f538c3..68bcd1284c06e84d0152d6e38b4803c32368236b 100644
--- a/front/src/components/metadata/ReleaseCard.vue
+++ b/front/src/components/metadata/ReleaseCard.vue
@@ -19,7 +19,7 @@
                   {{ track.position }}
                 </td>
                 <td colspan="3">
-                  <a :href="getMusicbrainzUrl('recording', track.id)" class="discrete link" target="_blank" title="View on MusicBrainz">
+                  <a :href="getMusicbrainzUrl('recording', track.id)" class="discrete link" target="_blank" :title="$t('View on MusicBrainz')">
                     {{ track.recording.title }}
                   </a>
                 </td>
diff --git a/front/src/components/metadata/Search.vue b/front/src/components/metadata/Search.vue
index f2dea6cab9dfa45c0edc8fb3ff995961ca1b3dea..305aa7a3d6b3aa7e695c9613f566946c3e7be805 100644
--- a/front/src/components/metadata/Search.vue
+++ b/front/src/components/metadata/Search.vue
@@ -12,7 +12,7 @@
     </div>
     <div class="ui fluid search">
       <div class="ui icon input">
-        <input class="prompt" placeholder="Enter your search query..." type="text">
+        <input class="prompt" :placeholder="$t('Enter your search query...')" type="text">
         <i class="search icon"></i>
       </div>
       <div class="results"></div>
@@ -32,21 +32,7 @@ export default {
   data: function () {
     return {
       currentType: this.mbType || 'artist',
-      currentId: this.mbId || '',
-      types: [
-        {
-          value: 'artist',
-          label: 'Artist'
-        },
-        {
-          value: 'release',
-          label: 'Album'
-        },
-        {
-          value: 'recording',
-          label: 'Track'
-        }
-      ]
+      currentId: this.mbId || ''
     }
   },
 
@@ -132,6 +118,22 @@ export default {
     },
     searchUrl: function () {
       return config.API_URL + 'providers/musicbrainz/search/' + this.currentTypeObject.value + 's/?query={query}'
+    },
+    types: function () {
+      return [
+        {
+          value: 'artist',
+          label: this.$t('Artist')
+        },
+        {
+          value: 'release',
+          label: this.$t('Album')
+        },
+        {
+          value: 'recording',
+          label: this.$t('Track')
+        }
+      ]
     }
   },
   watch: {
diff --git a/front/src/components/playlists/Card.vue b/front/src/components/playlists/Card.vue
index 6dd1b0a0ce477713af1e9dd9e7b33cff40906730..670b43194b5d78aaf485d37f785107d78a5b13d9 100644
--- a/front/src/components/playlists/Card.vue
+++ b/front/src/components/playlists/Card.vue
@@ -10,13 +10,16 @@
         <i class="user icon"></i> {{ playlist.user.username }}
       </div>
       <div class="meta">
-        <i class="clock icon"></i> Updated <human-date :date="playlist.modification_date"></human-date>
+        <i class="clock icon"></i>
+        <i18next path="Updated {%date%}">
+          <human-date :date="playlist.modification_date" />
+        </i18next>
       </div>
     </div>
     <div class="extra content">
       <span>
         <i class="sound icon"></i>
-        {{ playlist.tracks_count }} tracks
+        {{ $t('{%count%} tracks', { count: playlist.tracks_count }) }}
       </span>
       <play-button class="mini basic orange right floated" :playlist="playlist">Play all</play-button>
     </div>
diff --git a/front/src/components/playlists/Editor.vue b/front/src/components/playlists/Editor.vue
index c668857ea1f88557b22d77ecb81acdfd5fe52162..c036737ce032461e8b02cd00d12593e27c960504 100644
--- a/front/src/components/playlists/Editor.vue
+++ b/front/src/components/playlists/Editor.vue
@@ -2,16 +2,16 @@
   <div class="ui text container">
     <playlist-form @updated="$emit('playlist-updated', $event)" :title="false" :playlist="playlist"></playlist-form>
     <h3 class="ui top attached header">
-      Playlist editor
+      {{ $t('Playlist editor') }}
     </h3>
     <div class="ui attached segment">
       <template v-if="status === 'loading'">
         <div class="ui active tiny inline loader"></div>
-        Syncing changes to server...
+        {{ $t('Syncing changes to server...') }}
       </template>
       <template v-else-if="status === 'errored'">
         <i class="red close icon"></i>
-        An error occured while saving your changes
+        {{ $t('An error occured while saving your changes') }}
         <div v-if="errors.length > 0" class="ui negative message">
           <ul class="list">
             <li v-for="error in errors">{{ error }}</li>
@@ -19,7 +19,7 @@
         </div>
       </template>
       <template v-else-if="status === 'saved'">
-        <i class="green check icon"></i> Changes synced with server
+        <i class="green check icon"></i> {{ $t('Changes synced with server') }}
       </template>
     </div>
     <div class="ui bottom attached segment">
@@ -28,13 +28,15 @@
         :disabled="queueTracks.length === 0"
         :class="['ui', {disabled: queueTracks.length === 0}, 'labeled', 'icon', 'button']"
         title="Copy tracks from current queue to playlist">
-          <i class="plus icon"></i> Insert from queue ({{ queueTracks.length }} tracks)</div>
+          <i class="plus icon"></i>
+          {{ $t('Insert from queue ({%count%} tracks)', { count: queueTracks.length }) }}
+        </div>
 
       <dangerous-button :disabled="plts.length === 0" class="labeled right floated icon" color='yellow' :action="clearPlaylist">
-        <i class="eraser icon"></i> Clear playlist
-        <p slot="modal-header">Do you want to clear the playlist "{{ playlist.name }}"?</p>
-        <p slot="modal-content">This will remove all tracks from this playlist and cannot be undone.</p>
-        <p slot="modal-confirm">Clear playlist</p>
+        <i class="eraser icon"></i> {{ $t('Clear playlist') }}
+        <p slot="modal-header">{{ $t('Do you want to clear the playlist "{%name%}"?', { name: playlist.name }) }}</p>
+        <p slot="modal-content">{{ $t('This will remove all tracks from this playlist and cannot be undone.') }}</p>
+        <p slot="modal-confirm">{{ $t('Clear playlist') }}</p>
       </dangerous-button>
       <div class="ui hidden divider"></div>
       <template v-if="plts.length > 0">
diff --git a/front/src/components/playlists/Form.vue b/front/src/components/playlists/Form.vue
index 634e310bcdc2bb57484a8e048f65cfd8834da0d0..d19cd687668dca71fae2fabd8926426ae24986b4 100644
--- a/front/src/components/playlists/Form.vue
+++ b/front/src/components/playlists/Form.vue
@@ -1,29 +1,29 @@
 <template>
   <form class="ui form" @submit.prevent="submit()">
-    <h4 v-if="title" class="ui header">Create a new playlist</h4>
+    <h4 v-if="title" class="ui header">{{ $t('Create a new playlist') }}</h4>
     <div v-if="success" class="ui positive message">
       <div class="header">
         <template v-if="playlist">
-          Playlist updated
+          {{ $t('Playlist updated') }}
         </template>
         <template v-else>
-          Playlist created
+          {{ $t('Playlist created') }}
         </template>
       </div>
     </div>
     <div v-if="errors.length > 0" class="ui negative message">
-      <div class="header">We cannot create the playlist</div>
+      <div class="header">{{ $t('We cannot create the playlist') }}</div>
       <ul class="list">
         <li v-for="error in errors">{{ error }}</li>
       </ul>
     </div>
     <div class="three fields">
       <div class="field">
-        <label>Playlist name</label>
+        <label>{{ $t('Playlist name') }}</label>
         <input v-model="name" required type="text" placeholder="My awesome playlist" />
       </div>
       <div class="field">
-        <label>Playlist visibility</label>
+        <label>{{ $t('Playlist visibility') }}</label>
         <select class="ui dropdown" v-model="privacyLevel">
           <option :value="c.value" v-for="c in privacyLevelChoices">{{ c.label }}</option>
         </select>
@@ -31,8 +31,8 @@
       <div class="field">
         <label>&nbsp;</label>
         <button :class="['ui', 'fluid', {'loading': isLoading}, 'button']" type="submit">
-          <template v-if="playlist">Update playlist</template>
-          <template v-else>Create playlist</template>
+          <template v-if="playlist">{{ $t('Update playlist') }}</template>
+          <template v-else>{{ $t('Create playlist') }}</template>
         </button>
       </div>
     </div>
@@ -57,30 +57,34 @@ export default {
     let d = {
       errors: [],
       success: false,
-      isLoading: false,
-      privacyLevelChoices: [
+      isLoading: false
+    }
+    if (this.playlist) {
+      d.name = this.playlist.name
+      d.privacyLevel = this.playlist.privacy_level
+    } else {
+      d.privacyLevel = this.$store.state.auth.profile.privacy_level
+      d.name = ''
+    }
+    return d
+  },
+  computed: {
+    privacyLevelChoices: function () {
+      return [
         {
           value: 'me',
-          label: 'Nobody except me'
+          label: this.$t('Nobody except me')
         },
         {
           value: 'instance',
-          label: 'Everyone on this instance'
+          label: this.$t('Everyone on this instance')
         },
         {
           value: 'everyone',
-          label: 'Everyone'
+          label: this.$t('Everyone')
         }
       ]
     }
-    if (this.playlist) {
-      d.name = this.playlist.name
-      d.privacyLevel = this.playlist.privacy_level
-    } else {
-      d.privacyLevel = this.$store.state.auth.profile.privacy_level
-      d.name = ''
-    }
-    return d
   },
   methods: {
     submit () {
diff --git a/front/src/components/playlists/PlaylistModal.vue b/front/src/components/playlists/PlaylistModal.vue
index 5fdf585dfbd7ce44ee9fcb4a3a9f6610e9d00d6c..404948dc0793cfce4a9515bfde6b76c95afa17a9 100644
--- a/front/src/components/playlists/PlaylistModal.vue
+++ b/front/src/components/playlists/PlaylistModal.vue
@@ -1,14 +1,14 @@
 <template>
   <modal @update:show="update" :show="$store.state.playlists.showModal">
     <div class="header">
-      Manage playlists
+      {{ $t('Manage playlists') }}
     </div>
     <div class="scrolling content">
       <div class="description">
         <template v-if="track">
-          <h4 class="ui header">Current track</h4>
+          <h4 class="ui header">{{ $t('Current track') }}</h4>
           <div>
-            "{{ track.title }}" by {{ track.artist.name }}
+            {{ $t('"{%title%}" by {%artist%}', { title: track.title, artist: track.artist.name }) }}
           </div>
           <div class="ui divider"></div>
         </template>
@@ -16,20 +16,20 @@
         <playlist-form></playlist-form>
         <div class="ui divider"></div>
         <div v-if="errors.length > 0" class="ui negative message">
-          <div class="header">We cannot add the track to a playlist</div>
+          <div class="header">{{ $t('We cannot add the track to a playlist') }}</div>
           <ul class="list">
             <li v-for="error in errors">{{ error }}</li>
           </ul>
         </div>
         </div>
-        <h4 class="ui header">Available playlists</h4>
+        <h4 class="ui header">{{ $t('Available playlists') }}</h4>
         <table class="ui unstackable very basic table">
           <thead>
             <tr>
               <th></th>
-              <th>Name</th>
-              <th class="sorted descending">Last modification</th>
-              <th>Tracks</th>
+              <th>{{ $t('Name') }}</th>
+              <th class="sorted descending">{{ $t('Last modification') }}</th>
+              <th>{{ $t('Tracks') }}</th>
               <th></th>
             </tr>
           </thead>
@@ -48,9 +48,9 @@
                 <div
                   v-if="track"
                   class="ui green icon basic small right floated button"
-                  title="Add to this playlist"
+                  :title="$t('Add to this playlist')"
                   @click="addToPlaylist(playlist.id)">
-                  <i class="plus icon"></i> Add track
+                  <i class="plus icon"></i> {{ $t('Add track') }}
                 </div>
               </td>
             </tr>
@@ -59,7 +59,7 @@
       </div>
     </div>
     <div class="actions">
-      <div class="ui cancel button">Cancel</div>
+      <div class="ui cancel button">{{ $t('Cancel') }}</div>
     </div>
   </modal>
 </template>
diff --git a/front/src/components/playlists/TrackPlaylistIcon.vue b/front/src/components/playlists/TrackPlaylistIcon.vue
index bba4c515b0c90644182016f786c06e60b233b552..cd74b4d275996380122b2a134e7519f2e9f3a343 100644
--- a/front/src/components/playlists/TrackPlaylistIcon.vue
+++ b/front/src/components/playlists/TrackPlaylistIcon.vue
@@ -4,13 +4,13 @@
     v-if="button"
     :class="['ui', 'button']">
     <i class="list icon"></i>
-    Add to playlist...
+    {{ $t('Add to playlist...') }}
   </button>
   <i
     v-else
     @click="$store.commit('playlists/chooseTrack', track)"
     :class="['playlist-icon', 'list', 'link', 'icon']"
-    title="Add to playlist...">
+    :title="$t('Add to playlist...')">
   </i>
 </template>
 
diff --git a/front/src/components/radios/Button.vue b/front/src/components/radios/Button.vue
index 0869313a875eb153812a5b9fce0cf1ea1a932d81..abdc660fcedf463ad4abfb4abbd9fff90317bb80 100644
--- a/front/src/components/radios/Button.vue
+++ b/front/src/components/radios/Button.vue
@@ -1,8 +1,8 @@
 <template>
   <button @click="toggleRadio" :class="['ui', 'blue', {'inverted': running}, 'button']">
     <i class="ui feed icon"></i>
-    <template v-if="running">Stop</template>
-    <template v-else>Start</template>
+    <template v-if="running">{{ $t('Stop') }}</template>
+    <template v-else>{{ $t('Start') }}</template>
     radio
   </button>
 </template>
diff --git a/front/src/components/radios/Card.vue b/front/src/components/radios/Card.vue
index 17de3c85fe3c67d5a124d046aa9e5fa233503396..62de6ec65dcd0c9c0a8aa2bd70f3a31392b21e15 100644
--- a/front/src/components/radios/Card.vue
+++ b/front/src/components/radios/Card.vue
@@ -15,7 +15,7 @@
           class="ui basic yellow button"
           v-if="$store.state.auth.authenticated && type === 'custom' && customRadio.user === $store.state.auth.profile.id"
           :to="{name: 'library.radios.edit', params: {id: customRadioId }}">
-          Edit...
+          {{ $t('Edit...') }}
         </router-link>
         <radio-button class="right floated button" :type="type" :custom-radio-id="customRadioId"></radio-button>
       </div>
diff --git a/front/src/components/requests/Card.vue b/front/src/components/requests/Card.vue
index 17fecde5294a1e26d78467d8efc780b1b023b19f..9d0bf0b771bdaa398c882e84be4f07afb642c6ce 100644
--- a/front/src/components/requests/Card.vue
+++ b/front/src/components/requests/Card.vue
@@ -23,7 +23,7 @@
       <button
         @click="createImport"
         v-if="request.status === 'pending' && importAction && $store.state.auth.availablePermissions['import.launch']"
-        class="ui mini basic green right floated button">Create import</button>
+        class="ui mini basic green right floated button">{{ $t('Create import') }}</button>
 
     </div>
   </div>
diff --git a/front/src/components/requests/Form.vue b/front/src/components/requests/Form.vue
index 68c725ba7adfe903c3ecdf971067b1abb2d5baef..b03e1545ba4d7861fc6b02c71e752f630127fb98 100644
--- a/front/src/components/requests/Form.vue
+++ b/front/src/components/requests/Form.vue
@@ -1,30 +1,30 @@
 <template>
   <div>
     <form v-if="!over" class="ui form" @submit.prevent="submit">
-      <p>Something's missing in the library? Let us know what you would like to listen!</p>
+      <p>{{ $t('Something\'s missing in the library? Let us know what you would like to listen!') }}</p>
       <div class="required field">
-        <label>Artist name</label>
+        <label>{{ $t('Artist name') }}</label>
         <input v-model="currentArtistName" placeholder="The Beatles, Mickael Jackson…" required maxlength="200">
       </div>
       <div class="field">
-        <label>Albums</label>
-        <p>Leave this field empty if you're requesting the whole discography.</p>
+        <label>{{ $t('Albums') }}</label>
+        <p>{{ $t('Leave this field empty if you\'re requesting the whole discography.') }}</p>
         <input v-model="currentAlbums" placeholder="The White Album, Thriller…" maxlength="2000">
       </div>
       <div class="field">
-        <label>Comment</label>
+        <label>{{ $t('Comment') }}</label>
         <textarea v-model="currentComment" rows="3" placeholder="Use this comment box to add details to your request if needed" maxlength="2000"></textarea>
       </div>
-      <button class="ui submit button" type="submit">Submit</button>
+      <button class="ui submit button" type="submit">{{ $t('Submit') }}</button>
     </form>
     <div v-else class="ui success message">
       <div class="header">Request submitted!</div>
-      <p>We've received your request, you'll get some groove soon ;)</p>
-      <button @click="reset" class="ui button">Submit another request</button>
+      <p>{{ $t('We\'ve received your request, you\'ll get some groove soon ;)') }}</p>
+      <button @click="reset" class="ui button">{{ $t('Submit another request') }}</button>
     </div>
     <div v-if="requests.length > 0">
       <div class="ui divider"></div>
-      <h3 class="ui header">Pending requests</h3>
+      <h3 class="ui header">{{ $t('Pending requests') }}</h3>
       <div class="ui list">
         <div v-for="request in requests" class="item">
           <div class="content">
diff --git a/front/src/components/requests/RequestsList.vue b/front/src/components/requests/RequestsList.vue
index 5d4db243acce765d72e64c61e3771b54bf17e452..4464031c5e6918d0b9222922687fdc8e2fc59abc 100644
--- a/front/src/components/requests/RequestsList.vue
+++ b/front/src/components/requests/RequestsList.vue
@@ -1,15 +1,15 @@
 <template>
   <div v-title="'Import Requests'">
     <div class="ui vertical stripe segment">
-      <h2 class="ui header">Music requests</h2>
+      <h2 class="ui header">{{ $t('Music requests') }}</h2>
       <div :class="['ui', {'loading': isLoading}, 'form']">
         <div class="fields">
           <div class="field">
-            <label>Search</label>
+            <label>{{ $t('Search') }}</label>
             <input type="text" v-model="query" placeholder="Enter an artist name, a username..."/>
           </div>
           <div class="field">
-            <label>Ordering</label>
+            <label>{{ $t('Ordering') }}</label>
             <select class="ui dropdown" v-model="ordering">
               <option v-for="option in orderingOptions" :value="option[0]">
                 {{ option[1] }}
@@ -17,14 +17,14 @@
             </select>
           </div>
           <div class="field">
-            <label>Ordering direction</label>
+            <label>{{ $t('Ordering direction') }}</label>
             <select class="ui dropdown" v-model="orderingDirection">
               <option value="">Ascending</option>
               <option value="-">Descending</option>
             </select>
           </div>
           <div class="field">
-            <label>Results per page</label>
+            <label>{{ $t('Results per page') }}</label>
             <select class="ui dropdown" v-model="paginateBy">
               <option :value="parseInt(12)">12</option>
               <option :value="parseInt(25)">25</option>
@@ -96,12 +96,7 @@ export default {
       query: this.defaultQuery,
       paginateBy: parseInt(this.defaultPaginateBy || 12),
       orderingDirection: defaultOrdering.direction,
-      ordering: defaultOrdering.field,
-      orderingOptions: [
-        ['creation_date', 'Creation date'],
-        ['artist_name', 'Artist name'],
-        ['user__username', 'User']
-      ]
+      ordering: defaultOrdering.field
     }
   },
   created () {
@@ -141,6 +136,15 @@ export default {
       this.page = page
     }
   },
+  computed: {
+    orderingOptions: function () {
+      return [
+        ['creation_date', this.$t('Creation date')],
+        ['artist_name', this.$t('Artist name')],
+        ['user__username', this.$t('User')]
+      ]
+    }
+  },
   watch: {
     page () {
       this.updateQueryString()
diff --git a/front/src/main.js b/front/src/main.js
index 0c41294113929d4421a6ee36808be445de7ada5d..5481615f2006025cea7e009dc9ddd8a49b97623f 100644
--- a/front/src/main.js
+++ b/front/src/main.js
@@ -35,14 +35,14 @@ Vue.use(VueMasonryPlugin)
 Vue.use(VueLazyload)
 Vue.config.productionTip = false
 Vue.directive('title', {
-  inserted: (el, binding) => { console.log(binding.value); document.title = binding.value + ' - Funkwhale' },
+  inserted: (el, binding) => { document.title = binding.value + ' - Funkwhale' },
   updated: (el, binding) => { document.title = binding.value + ' - Funkwhale' }
 })
 
 axios.defaults.baseURL = config.API_URL
 axios.interceptors.request.use(function (config) {
   // Do something before request is sent
-  if (store.state.auth.authenticated) {
+  if (store.state.auth.token) {
     config.headers['Authorization'] = store.getters['auth/header']
   }
   return config
diff --git a/front/src/store/auth.js b/front/src/store/auth.js
index e72e1968f52f1ca3593abfbbdabf4e06fda7b1d5..b1753404f9be65c2d5fe2a067607d83ef45d4d6a 100644
--- a/front/src/store/auth.js
+++ b/front/src/store/auth.js
@@ -19,6 +19,14 @@ export default {
     }
   },
   mutations: {
+    reset (state) {
+      state.authenticated = false
+      state.profile = null
+      state.username = ''
+      state.token = ''
+      state.tokenData = {}
+      state.availablePermissions = {}
+    },
     profile: (state, value) => {
       state.profile = value
     },
@@ -53,8 +61,6 @@ export default {
       return axios.post('token/', credentials).then(response => {
         logger.default.info('Successfully logged in as', credentials.username)
         commit('token', response.data.token)
-        commit('username', credentials.username)
-        commit('authenticated', true)
         dispatch('fetchProfile')
         // Redirect to a specified route
         router.push(next)
@@ -64,19 +70,25 @@ export default {
       })
     },
     logout ({commit}) {
-      commit('authenticated', false)
+      let modules = [
+        'auth',
+        'favorites',
+        'player',
+        'playlists',
+        'queue',
+        'radios'
+      ]
+      modules.forEach(m => {
+        commit(`${m}/reset`, null, {root: true})
+      })
       logger.default.info('Log out, goodbye!')
       router.push({name: 'index'})
     },
     check ({commit, dispatch, state}) {
       logger.default.info('Checking authentication...')
       var jwt = state.token
-      var username = state.username
       if (jwt) {
-        commit('authenticated', true)
-        commit('username', username)
         commit('token', jwt)
-        logger.default.info('Logged back in as ' + username)
         dispatch('fetchProfile')
         dispatch('refreshToken')
       } else {
@@ -88,6 +100,7 @@ export default {
       return axios.get('users/users/me/').then((response) => {
         logger.default.info('Successfully fetched user profile')
         let data = response.data
+        commit('authenticated', true)
         commit('profile', data)
         commit('username', data.username)
         dispatch('favorites/fetch', null, {root: true})
diff --git a/front/src/store/favorites.js b/front/src/store/favorites.js
index a4f85b235daa36567f16528d8396d259e63b1db0..b7e789511217726e74412591d18233e6d25ed762 100644
--- a/front/src/store/favorites.js
+++ b/front/src/store/favorites.js
@@ -20,6 +20,10 @@ export default {
         }
       }
       state.count = state.tracks.length
+    },
+    reset (state) {
+      state.tracks = []
+      state.count = 0
     }
   },
   getters: {
diff --git a/front/src/store/player.js b/front/src/store/player.js
index d849b7b56ceaa80fa93ab30cd2b5654fbcc33a9f..ed437c3f0220d84ed26693c94a8036c38c756b64 100644
--- a/front/src/store/player.js
+++ b/front/src/store/player.js
@@ -15,6 +15,10 @@ export default {
     looping: 0 // 0 -> no, 1 -> on  track, 2 -> on queue
   },
   mutations: {
+    reset (state) {
+      state.errorCount = 0
+      state.playing = false
+    },
     volume (state, value) {
       value = parseFloat(value)
       value = Math.min(value, 1)
diff --git a/front/src/store/playlists.js b/front/src/store/playlists.js
index b3ed3ab235bf09456a4b7cc9d7129963443488ca..d0e144d803eec7917ea67ebc486f1f95c29e8e83 100644
--- a/front/src/store/playlists.js
+++ b/front/src/store/playlists.js
@@ -17,6 +17,11 @@ export default {
     },
     showModal (state, value) {
       state.showModal = value
+    },
+    reset (state) {
+      state.playlists = []
+      state.modalTrack = null
+      state.showModal = false
     }
   },
   actions: {
diff --git a/front/src/store/queue.js b/front/src/store/queue.js
index 6a26fa1e9a0dc70609b0ea6fcba6e5b11745f277..23e074a80c36fb849eb306ea59da68756a05282b 100644
--- a/front/src/store/queue.js
+++ b/front/src/store/queue.js
@@ -10,6 +10,12 @@ export default {
     previousQueue: null
   },
   mutations: {
+    reset (state) {
+      state.tracks = []
+      state.currentIndex = -1
+      state.ended = true
+      state.previousQueue = null
+    },
     currentIndex (state, value) {
       state.currentIndex = value
     },
@@ -86,14 +92,17 @@ export default {
       if (current) {
         dispatch('player/stop', null, {root: true})
       }
+      commit('splice', {start: index, size: 1})
       if (index < state.currentIndex) {
-        dispatch('currentIndex', state.currentIndex - 1)
+        commit('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)
       }
+      if (state.currentIndex + 1 === state.tracks.length) {
+        dispatch('radios/populateQueue', null, {root: true})
+      }
     },
 
     resume ({state, dispatch, rootState}) {
diff --git a/front/src/store/radios.js b/front/src/store/radios.js
index e95db512643d297fa08ecca16df30e8004739e7c..49bbd4f9410dff015721f57073c4d95c58d7146a 100644
--- a/front/src/store/radios.js
+++ b/front/src/store/radios.js
@@ -26,6 +26,10 @@ export default {
     }
   },
   mutations: {
+    reset (state) {
+      state.running = false
+      state.current = false
+    },
     current: (state, value) => {
       state.current = value
     },
diff --git a/front/src/views/federation/Base.vue b/front/src/views/federation/Base.vue
index 7958bb36b65332b840111cba5ab37c5e2a7962b0..951fe9f0f1c49abe2d43274151008f9d97113439 100644
--- a/front/src/views/federation/Base.vue
+++ b/front/src/views/federation/Base.vue
@@ -3,16 +3,16 @@
     <div class="ui secondary pointing menu">
       <router-link
         class="ui item"
-        :to="{name: 'federation.libraries.list'}">Libraries</router-link>
+        :to="{name: 'federation.libraries.list'}">{{ $t('Libraries') }}</router-link>
       <router-link
         class="ui item"
-        :to="{name: 'federation.tracks.list'}">Tracks</router-link>
+        :to="{name: 'federation.tracks.list'}">{{ $t('Tracks') }}</router-link>
         <div class="ui secondary right menu">
           <router-link
             class="ui item"
             :to="{name: 'federation.followers.list'}">
-            Followers
-            <div class="ui teal label" title="Pending requests">{{ requestsCount }}</div>
+            {{ $t('Followers') }}
+            <div class="ui teal label" :title="$t('Pending requests')">{{ requestsCount }}</div>
           </router-link>
         </div>
     </div>
diff --git a/front/src/views/federation/LibraryDetail.vue b/front/src/views/federation/LibraryDetail.vue
index 20250e333d866941e7605667b1a3541b7095e716..bd2e63c4d9c4644c66d625bd8218dcdbf864ef85 100644
--- a/front/src/views/federation/LibraryDetail.vue
+++ b/front/src/views/federation/LibraryDetail.vue
@@ -19,18 +19,18 @@
             <tbody>
               <tr>
                 <td >
-                  Follow status
+                  {{ $t('Follow status') }}
                   <span :data-tooltip="$t('This indicate if the remote library granted you access')"><i class="question circle icon"></i></span>
                 </td>
                 <td>
                   <template v-if="object.follow.approved === null">
-                    <i class="loading icon"></i> Pending approval
+                    <i class="loading icon"></i> {{ $t('Pending approval') }}
                   </template>
                   <template v-else-if="object.follow.approved === true">
-                    <i class="check icon"></i> Following
+                    <i class="check icon"></i> {{ $t('Following') }}
                   </template>
                   <template v-else-if="object.follow.approved === false">
-                    <i class="x icon"></i> Not following
+                    <i class="x icon"></i> {{ $t('Not following') }}
                   </template>
                 </td>
                 <td>
@@ -38,7 +38,7 @@
               </tr>
               <tr>
                 <td>
-                  Federation
+                  {{ $t('Federation') }}
                   <span :data-tooltip="$t('Use this flag to enable/disable federation with this library')"><i class="question circle icon"></i></span>
                 </td>
                 <td>
@@ -54,7 +54,7 @@
               </tr>
               <tr>
                 <td>
-                  Auto importing
+                  {{ $t('Auto importing') }}
                   <span :data-tooltip="$t('When enabled, auto importing will automatically import new tracks published in this library')"><i class="question circle icon"></i></span>
                 </td>
                 <td>
@@ -82,14 +82,14 @@
               </tr>
               -->
               <tr>
-                <td>Library size</td>
+                <td>{{ $t('Library size') }}</td>
                 <td>
-                  {{ object.tracks_count }} tracks
+                  {{ $t('{%count%} tracks', { count: object.tracks_count }) }}
                 </td>
                 <td></td>
               </tr>
               <tr>
-                <td>Last fetched</td>
+                <td>{{ $t('Last fetched') }}</td>
                 <td>
                   <human-date v-if="object.fetched_date" :date="object.fetched_date"></human-date>
                   <template v-else>Never</template>
@@ -97,10 +97,10 @@
                     @click="scan"
                     v-if="!scanTrigerred"
                     :class="['ui', 'basic', {loading: isScanLoading}, 'button']">
-                    <i class="sync icon"></i> Trigger scan
+                    <i class="sync icon"></i> {{ $t('Trigger scan') }}
                   </button>
                   <button v-else class="ui success button">
-                    <i class="check icon"></i> Scan triggered!
+                    <i class="check icon"></i> {{ $t('Scan triggered!') }}
                   </button>
 
                 </td>
@@ -110,10 +110,10 @@
           </table>
         </div>
         <div class="ui hidden divider"></div>
-        <button @click="fetchData" class="ui basic button">Refresh</button>
+        <button @click="fetchData" class="ui basic button">{{ $t('Refresh') }}</button>
       </div>
       <div class="ui vertical stripe segment">
-        <h2>Tracks available in this library</h2>
+        <h2>{{ $t('Tracks available in this library') }}</h2>
         <library-track-table v-if="!isLoading" :filters="{library: id}"></library-track-table>
       </div>
     </template>
diff --git a/front/src/views/federation/LibraryFollowersList.vue b/front/src/views/federation/LibraryFollowersList.vue
index 8ca120e8b54e3178d16dfcd7cd6f4af9468ddf1d..ce79e478747267637a418cb31f979a0421d1f98a 100644
--- a/front/src/views/federation/LibraryFollowersList.vue
+++ b/front/src/views/federation/LibraryFollowersList.vue
@@ -1,10 +1,9 @@
 <template>
   <div v-title="'Followers'">
     <div class="ui vertical stripe segment">
-      <h2 class="ui header">Browsing followers</h2>
+      <h2 class="ui header">{{ $t('Browsing followers') }}</h2>
       <p>
-        Be careful when accepting follow requests, as it means the follower
-        will have access to your entire library.
+        {{ $t('Be careful when accepting follow requests, as it means the follower will have access to your entire library.') }}
       </p>
       <div class="ui hidden divider"></div>
       <library-follow-table></library-follow-table>
diff --git a/front/src/views/federation/LibraryList.vue b/front/src/views/federation/LibraryList.vue
index 43800a72e4afd182b3b925997f842f11198bf416..7b0b259412a3665bb4c4c04f1ce62444872439dc 100644
--- a/front/src/views/federation/LibraryList.vue
+++ b/front/src/views/federation/LibraryList.vue
@@ -1,22 +1,22 @@
 <template>
   <div v-title="'Libraries'">
     <div class="ui vertical stripe segment">
-      <h2 class="ui header">Browsing libraries</h2>
+      <h2 class="ui header">{{ $t('Browsing libraries') }}</h2>
       <router-link
         class="ui basic green button"
         :to="{name: 'federation.libraries.scan'}">
         <i class="plus icon"></i>
-        Add a new library
+        {{ $t('Add a new library') }}
       </router-link>
       <div class="ui hidden divider"></div>
       <div :class="['ui', {'loading': isLoading}, 'form']">
         <div class="fields">
           <div class="field">
-            <label>Search</label>
+            <label>{{ $t('Search') }}</label>
             <input type="text" v-model="query" placeholder="Enter an library domain name..."/>
           </div>
           <div class="field">
-            <label>Ordering</label>
+            <label>{{ $t('Ordering') }}</label>
             <select class="ui dropdown" v-model="ordering">
               <option v-for="option in orderingOptions" :value="option[0]">
                 {{ option[1] }}
@@ -24,14 +24,14 @@
             </select>
           </div>
           <div class="field">
-            <label>Ordering direction</label>
+            <label>{{ $t('Ordering direction') }}</label>
             <select class="ui dropdown" v-model="orderingDirection">
-              <option value="">Ascending</option>
-              <option value="-">Descending</option>
+              <option value="">{{ $t('Ascending') }}</option>
+              <option value="-">{{ $t('Descending') }}</option>
             </select>
           </div>
           <div class="field">
-            <label>Results per page</label>
+            <label>{{ $t('Results per page') }}</label>
             <select class="ui dropdown" v-model="paginateBy">
               <option :value="parseInt(12)">12</option>
               <option :value="parseInt(25)">25</option>
diff --git a/front/src/views/federation/LibraryTrackList.vue b/front/src/views/federation/LibraryTrackList.vue
index 42526b6e422d2325c480024e035d9907a28e9fe0..b9a78527f16eac6eae0fb6365ede85cd35f0248f 100644
--- a/front/src/views/federation/LibraryTrackList.vue
+++ b/front/src/views/federation/LibraryTrackList.vue
@@ -1,7 +1,7 @@
 <template>
   <div v-title="'Federated tracks'">
     <div class="ui vertical stripe segment">
-      <h2 class="ui header">Browsing federated tracks</h2>
+      <h2 class="ui header">{{ $t('Browsing federated tracks') }}</h2>
       <div class="ui hidden divider"></div>
       <library-track-table :show-library="true"></library-track-table>
     </div>
diff --git a/front/src/views/instance/Timeline.vue b/front/src/views/instance/Timeline.vue
index f4ddedfb72e287235081cc8d86c5f5a388197c79..2ab8b708cd1ae140d31c7ca3c5e1cc3f56631f51 100644
--- a/front/src/views/instance/Timeline.vue
+++ b/front/src/views/instance/Timeline.vue
@@ -2,10 +2,10 @@
   <div class="main pusher" v-title="'Instance Timeline'">
     <div class="ui vertical center aligned stripe segment">
       <div v-if="isLoading" :class="['ui', {'active': isLoading}, 'inverted', 'dimmer']">
-        <div class="ui text loader">Loading timeline...</div>
+        <div class="ui text loader">{{ $t('Loading timeline...') }}</div>
       </div>
       <div v-else class="ui text container">
-        <h1 class="ui header">Recent activity on this instance</h1>
+        <h1 class="ui header">{{ $t('Recent activity on this instance') }}</h1>
         <div class="ui feed">
           <component
             class="event"
@@ -14,12 +14,6 @@
             v-if="components[event.type]"
             :is="components[event.type]"
             :event="event">
-            <username
-              class="user"
-              :username="event.actor.local_id"
-              slot="user"></username>
-              {{ event.published }}
-            <human-date class="date" :date="event.published" slot="date"></human-date>
           </component>
         </div>
       </div>
diff --git a/front/src/views/playlists/Detail.vue b/front/src/views/playlists/Detail.vue
index 2769317e6440f24c034f21d5db68bbd7fd52524b..61968c2e7e34683b03f9fa3bea2b2af437df3d18 100644
--- a/front/src/views/playlists/Detail.vue
+++ b/front/src/views/playlists/Detail.vue
@@ -1,6 +1,6 @@
 <template>
   <div>
-    <div v-if="isLoading" class="ui vertical segment" v-title="'Playlist'">
+    <div v-if="isLoading" class="ui vertical segment" v-title="$t('Playlist')">
       <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
     </div>
     <div v-if="!isLoading && playlist" class="ui head vertical center aligned stripe segment" v-title="playlist.name">
@@ -9,28 +9,28 @@
           <i class="circular inverted list yellow icon"></i>
           <div class="content">
             {{ playlist.name }}
-            <div class="sub header">
-              Playlist containing {{ playlistTracks.length }} tracks,
-              by <username :username="playlist.user.username"></username>
-            </div>
+            <i18next tag="div" class="sub header" path="Playlist containing {%0%} tracks, by {%1%}">
+              {{ playlistTracks.length }}
+              <username :username="playlist.user.username"></username>
+            </i18next>
           </div>
         </h2>
         <div class="ui hidden divider"></div>
         </button>
-        <play-button class="orange" :tracks="tracks">Play all</play-button>
+        <play-button class="orange" :tracks="tracks">{{ $t('Play all') }}</play-button>
         <button
           class="ui icon button"
           v-if="playlist.user.id === $store.state.auth.profile.id"
           @click="edit = !edit">
           <i class="pencil icon"></i>
-          <template v-if="edit">End edition</template>
-          <template v-else>Edit...</template>
+          <template v-if="edit">{{ $t('End edition') }}</template>
+          <template v-else>{{ $t('Edit...') }}</template>
         </button>
         <dangerous-button class="labeled icon" :action="deletePlaylist">
-          <i class="trash icon"></i> Delete
-          <p slot="modal-header">Do you want to delete the playlist "{{ playlist.name }}"?</p>
-          <p slot="modal-content">This will completely delete this playlist and cannot be undone.</p>
-          <p slot="modal-confirm">Delete playlist</p>
+          <i class="trash icon"></i> {{ $t('Delete') }}
+          <p slot="modal-header">{{ $t('Do you want to delete the playlist "{% playlist %}"?', {playlist: playlist.name}) }}</p>
+          <p slot="modal-content">{{ $t('This will completely delete this playlist and cannot be undone.') }}</p>
+          <p slot="modal-confirm">{{ $t('Delete playlist') }}</p>
         </dangerous-button>
       </div>
     </div>
diff --git a/front/src/views/playlists/List.vue b/front/src/views/playlists/List.vue
index 96aa36c4779bc11a6050208cf497ac861a7dabcd..32ee5aafaa9d28e34d1c824f479c6b0efea0447f 100644
--- a/front/src/views/playlists/List.vue
+++ b/front/src/views/playlists/List.vue
@@ -1,21 +1,21 @@
 <template>
-  <div v-title="'Playlists'">
+  <div v-title="$t('Playlists')">
     <div class="ui vertical stripe segment">
-      <h2 class="ui header">Browsing playlists</h2>
+      <h2 class="ui header">{{ $t('Browsing playlists') }}</h2>
       <div :class="['ui', {'loading': isLoading}, 'form']">
         <template v-if="$store.state.auth.authenticated">
           <button
             @click="$store.commit('playlists/chooseTrack', null)"
-            class="ui basic green button">Manage your playlists</button>
+            class="ui basic green button">{{ $t('Manage your playlists') }}</button>
           <div class="ui hidden divider"></div>
         </template>
         <div class="fields">
           <div class="field">
-            <label>Search</label>
-            <input type="text" v-model="query" placeholder="Enter an playlist name..."/>
+            <label>{{ $t('Search') }}</label>
+            <input type="text" v-model="query" :placeholder="$t('Enter an playlist name...')"/>
           </div>
           <div class="field">
-            <label>Ordering</label>
+            <label>{{ $t('Ordering') }}</label>
             <select class="ui dropdown" v-model="ordering">
               <option v-for="option in orderingOptions" :value="option[0]">
                 {{ option[1] }}
@@ -23,14 +23,14 @@
             </select>
           </div>
           <div class="field">
-            <label>Ordering direction</label>
+            <label>{{ $t('Ordering direction') }}</label>
             <select class="ui dropdown" v-model="orderingDirection">
-              <option value="">Ascending</option>
-              <option value="-">Descending</option>
+              <option value="">{{ $t('Ascending') }}</option>
+              <option value="-">{{ $t('Descending') }}</option>
             </select>
           </div>
           <div class="field">
-            <label>Results per page</label>
+            <label>{{ $t('Results per page') }}</label>
             <select class="ui dropdown" v-model="paginateBy">
               <option :value="parseInt(12)">12</option>
               <option :value="parseInt(25)">25</option>
@@ -76,6 +76,7 @@ export default {
     Pagination
   },
   data () {
+    console.log('YOLO', this.$t)
     let defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date')
     return {
       isLoading: true,
diff --git a/front/src/views/radios/Detail.vue b/front/src/views/radios/Detail.vue
index 397dcb49ec3fbd7e05fa45007f38418712a2d3b7..b3b500cf619d8b1809014609d9c77a94d5a0b6e3 100644
--- a/front/src/views/radios/Detail.vue
+++ b/front/src/views/radios/Detail.vue
@@ -83,7 +83,6 @@ export default {
       axios.get(url).then((response) => {
         self.radio = response.data
         axios.get(url + 'tracks', {params: {page: this.page}}).then((response) => {
-          console.log(response.data.count)
           this.totalTracks = response.data.count
           this.tracks = response.data.results
         }).then(() => {
diff --git a/front/test/unit/specs/components/common.spec.js b/front/test/unit/specs/components/common.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..1af4144ca858bb1c264d43cc602994569ce8fc8b
--- /dev/null
+++ b/front/test/unit/specs/components/common.spec.js
@@ -0,0 +1,10 @@
+import Username from '@/components/common/Username.vue'
+
+import { render } from '../../utils'
+
+describe('Username', () => {
+  it('displays username', () => {
+    const vm = render(Username, {username: 'Hello'})
+    expect(vm.$el.textContent).to.equal('Hello')
+  })
+})
diff --git a/front/test/unit/specs/store/auth.spec.js b/front/test/unit/specs/store/auth.spec.js
index 518dc10d4db29f680567d6c996853086cd6b4516..3d175e9f9fa0e31b3b51b79314fe182e0516549c 100644
--- a/front/test/unit/specs/store/auth.spec.js
+++ b/front/test/unit/specs/store/auth.spec.js
@@ -89,7 +89,12 @@ describe('store/auth', () => {
         action: store.actions.logout,
         params: {state: {}},
         expectedMutations: [
-          { type: 'authenticated', payload: false }
+          { type: 'auth/reset', payload: null, options: {root: true} },
+          { type: 'favorites/reset', payload: null, options: {root: true} },
+          { type: 'player/reset', payload: null, options: {root: true} },
+          { type: 'playlists/reset', payload: null, options: {root: true} },
+          { type: 'queue/reset', payload: null, options: {root: true} },
+          { type: 'radios/reset', payload: null, options: {root: true} }
         ]
       }, done)
     })
@@ -107,8 +112,6 @@ describe('store/auth', () => {
         action: store.actions.check,
         params: {state: {token: 'test', username: 'user'}},
         expectedMutations: [
-          { type: 'authenticated', payload: true },
-          { type: 'username', payload: 'user' },
           { type: 'token', payload: 'test' }
         ],
         expectedActions: [
@@ -131,9 +134,7 @@ describe('store/auth', () => {
         action: store.actions.login,
         payload: {credentials: credentials},
         expectedMutations: [
-          { type: 'token', payload: 'test' },
-          { type: 'username', payload: 'bob' },
-          { type: 'authenticated', payload: true }
+          { type: 'token', payload: 'test' }
         ],
         expectedActions: [
           { type: 'fetchProfile' }
@@ -175,13 +176,14 @@ describe('store/auth', () => {
       testAction({
         action: store.actions.fetchProfile,
         expectedMutations: [
+          { type: 'authenticated', payload: true },
           { type: 'profile', payload: profile },
           { type: 'username', payload: profile.username },
           { type: 'permission', payload: {key: 'admin', status: true} }
         ],
         expectedActions: [
           { type: 'favorites/fetch', payload: null, options: {root: true} },
-          { type: 'playlists/fetchOwn', payload: null, options: {root: true} },
+          { type: 'playlists/fetchOwn', payload: null, options: {root: true} }
         ]
       }, done)
     })
diff --git a/front/test/unit/specs/store/player.spec.js b/front/test/unit/specs/store/player.spec.js
index b55fb010d08576bfe9e029cd6eb8e130af81570f..1e6c9b33a5fd3c2c43911e4c8ae26e78bd239558 100644
--- a/front/test/unit/specs/store/player.spec.js
+++ b/front/test/unit/specs/store/player.spec.js
@@ -132,7 +132,7 @@ describe('store/player', () => {
       testAction({
         action: store.actions.trackEnded,
         payload: {test: 'track'},
-        params: {rootState: {queue: {currentIndex:0, tracks: [1, 2]}}},
+        params: {rootState: {queue: {currentIndex: 0, tracks: [1, 2]}}},
         expectedActions: [
           { type: 'trackListened', payload: {test: 'track'} },
           { type: 'queue/next', payload: null, options: {root: true} }
@@ -143,7 +143,7 @@ describe('store/player', () => {
       testAction({
         action: store.actions.trackEnded,
         payload: {test: 'track'},
-        params: {rootState: {queue: {currentIndex:1, tracks: [1, 2]}}},
+        params: {rootState: {queue: {currentIndex: 1, tracks: [1, 2]}}},
         expectedActions: [
           { type: 'trackListened', payload: {test: 'track'} },
           { type: 'radios/populateQueue', payload: null, options: {root: true} },
diff --git a/front/test/unit/specs/store/queue.spec.js b/front/test/unit/specs/store/queue.spec.js
index 2bc5cde4efec16a0cfdcf40ce36b1385780b93a1..3a59117d54f8d833f0a35ffe63884981849030fd 100644
--- a/front/test/unit/specs/store/queue.spec.js
+++ b/front/test/unit/specs/store/queue.spec.js
@@ -158,9 +158,7 @@ describe('store/queue', () => {
         payload: 1,
         params: {state: {currentIndex: 2}},
         expectedMutations: [
-          { type: 'splice', payload: {start: 1, size: 1} }
-        ],
-        expectedActions: [
+          { type: 'splice', payload: {start: 1, size: 1} },
           { type: 'currentIndex', payload: 1 }
         ]
       }, done)
@@ -326,7 +324,7 @@ describe('store/queue', () => {
         action: store.actions.shuffle,
         params: {state: {currentIndex: 1, tracks: tracks}},
         expectedMutations: [
-          { type: 'player/currentTime', payload: 0 , options: {root: true}},
+          { type: 'player/currentTime', payload: 0, options: {root: true} },
           { type: 'tracks', payload: [] }
         ],
         expectedActions: [
diff --git a/front/test/unit/specs/store/radios.spec.js b/front/test/unit/specs/store/radios.spec.js
index 6de6b8dd94858f34ee07d522d1e3d2ab36185cc0..3a8c48f04ec942ee8ecf8d71c8a64a4e009987ab 100644
--- a/front/test/unit/specs/store/radios.spec.js
+++ b/front/test/unit/specs/store/radios.spec.js
@@ -97,6 +97,5 @@ describe('store/radios', () => {
         expectedActions: []
       }, done)
     })
-
   })
 })
diff --git a/front/test/unit/utils.js b/front/test/unit/utils.js
index 233ee982e5221e0997da8bd29d33cfac17a113e3..6471fc97f710385ff20dcfe2fdd4ee9ac7b9ec36 100644
--- a/front/test/unit/utils.js
+++ b/front/test/unit/utils.js
@@ -1,4 +1,11 @@
 // helper for testing action with expected mutations
+import Vue from 'vue'
+
+export const render = (Component, propsData) => {
+  const Constructor = Vue.extend(Component)
+  return new Constructor({ propsData: propsData }).$mount()
+}
+
 export const testAction = ({action, payload, params, expectedMutations, expectedActions}, done) => {
   let mutationsCount = 0
   let actionsCount = 0