diff --git a/CHANGELOG b/CHANGELOG
index 1b98df96e32ad596bff153a67d670a4eefa27f78..f2705739c095e2246ebb93d9c982998eb7a57138 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,8 +1,31 @@
 Changelog
 =========
 
-0.6 (Unreleased)
-----------------
+.. towncrier
+
+0.5.3 (2018-02-27)
+------------------
+
+Features:
+
+- Added admin interface for radios, track files, favorites and import requests (#80)
+- Added basic instance stats on /about (#82)
+- Search now unaccent letters for queries like "The Dø" or "Björk" yielding more results (#81)
+
+
+Bugfixes:
+
+- Always use username in sidebar (#89)
+- Click event outside of player icons (#83)
+- Fixed broken import because of missing transaction
+- Now always load next radio track on last queue track ended (#87)
+- Now exclude tracks without file from radio candidates (#88)
+- skip to next track properly on 40X errors (#86)
+
+
+Other:
+
+- Switched to towncrier for changelog management and compilation
 
 
 0.5.2 (2018-02-26)
diff --git a/api/config/settings/common.py b/api/config/settings/common.py
index 491babdd15f8d4c017230f097af4b274c880a000..f5ddec00b1da2f591f1a651e1b9c60427d1f4da1 100644
--- a/api/config/settings/common.py
+++ b/api/config/settings/common.py
@@ -37,6 +37,7 @@ DJANGO_APPS = (
     'django.contrib.sites',
     'django.contrib.messages',
     'django.contrib.staticfiles',
+    'django.contrib.postgres',
 
     # Useful template tags:
     # 'django.contrib.humanize',
diff --git a/api/funkwhale_api/__init__.py b/api/funkwhale_api/__init__.py
index 2df7e2034219375fac6f0dffd0865f54d6c4f589..03e434591bb102570ca35cf6602834153901412d 100644
--- a/api/funkwhale_api/__init__.py
+++ b/api/funkwhale_api/__init__.py
@@ -1,3 +1,3 @@
 # -*- coding: utf-8 -*-
-__version__ = '0.5.2'
+__version__ = '0.5.3'
 __version_info__ = tuple([int(num) if num.isdigit() else num for num in __version__.replace('-', '.', 1).split('.')])
diff --git a/api/funkwhale_api/common/migrations/0001_initial.py b/api/funkwhale_api/common/migrations/0001_initial.py
new file mode 100644
index 0000000000000000000000000000000000000000..e95cc11e9a464eaa2a72de047262f78af0888038
--- /dev/null
+++ b/api/funkwhale_api/common/migrations/0001_initial.py
@@ -0,0 +1,12 @@
+# Generated by Django 2.0.2 on 2018-02-27 18:43
+from django.db import migrations
+from django.contrib.postgres.operations import UnaccentExtension
+
+
+class Migration(migrations.Migration):
+
+    dependencies = []
+
+    operations = [
+        UnaccentExtension()
+    ]
diff --git a/api/funkwhale_api/common/migrations/__init__.py b/api/funkwhale_api/common/migrations/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/api/funkwhale_api/common/utils.py b/api/funkwhale_api/common/utils.py
index 838c15c0031fba88e52d5bc895d1c80aa049ac63..c9d450e6ad7c9c80c9682be95a487d84263625c4 100644
--- a/api/funkwhale_api/common/utils.py
+++ b/api/funkwhale_api/common/utils.py
@@ -1,6 +1,8 @@
 import os
 import shutil
 
+from django.db import transaction
+
 
 def rename_file(instance, field_name, new_name, allow_missing_file=False):
     field = getattr(instance, field_name)
@@ -17,3 +19,9 @@ def rename_file(instance, field_name, new_name, allow_missing_file=False):
     field.name = os.path.join(initial_path, new_name_with_extension)
     instance.save()
     return new_name_with_extension
+
+
+def on_commit(f, *args, **kwargs):
+    return transaction.on_commit(
+        lambda: f(*args, **kwargs)
+    )
diff --git a/api/funkwhale_api/favorites/admin.py b/api/funkwhale_api/favorites/admin.py
new file mode 100644
index 0000000000000000000000000000000000000000..e8f29fac452f23f5ba544395dca248a41afe3cfe
--- /dev/null
+++ b/api/funkwhale_api/favorites/admin.py
@@ -0,0 +1,12 @@
+from django.contrib import admin
+
+from . import models
+
+
+@admin.register(models.TrackFavorite)
+class TrackFavoriteAdmin(admin.ModelAdmin):
+    list_display = ['user', 'track', 'creation_date']
+    list_select_related = [
+        'user',
+        'track'
+    ]
diff --git a/api/funkwhale_api/history/admin.py b/api/funkwhale_api/history/admin.py
index f8f587a017d8a5e65f8537b286a312367d3837ae..6d0480e73b4209629499c7ae26b8c5efa8348999 100644
--- a/api/funkwhale_api/history/admin.py
+++ b/api/funkwhale_api/history/admin.py
@@ -6,3 +6,7 @@ from . import models
 class ListeningAdmin(admin.ModelAdmin):
     list_display = ['track', 'end_date', 'user', 'session_key']
     search_fields = ['track__name', 'user__username']
+    list_select_related = [
+        'user',
+        'track'
+    ]
diff --git a/api/funkwhale_api/instance/stats.py b/api/funkwhale_api/instance/stats.py
new file mode 100644
index 0000000000000000000000000000000000000000..167b333d6d2c7f64f33055caa83cbea170569751
--- /dev/null
+++ b/api/funkwhale_api/instance/stats.py
@@ -0,0 +1,51 @@
+from django.db.models import Sum
+
+from funkwhale_api.favorites.models import TrackFavorite
+from funkwhale_api.history.models import Listening
+from funkwhale_api.music import models
+from funkwhale_api.users.models import User
+
+
+def get():
+    return {
+        'users': get_users(),
+        'tracks': get_tracks(),
+        'albums': get_albums(),
+        'artists': get_artists(),
+        'track_favorites': get_track_favorites(),
+        'listenings': get_listenings(),
+        'music_duration': get_music_duration(),
+    }
+
+
+def get_users():
+    return User.objects.count()
+
+
+def get_listenings():
+    return Listening.objects.count()
+
+
+def get_track_favorites():
+    return TrackFavorite.objects.count()
+
+
+def get_tracks():
+    return models.Track.objects.count()
+
+
+def get_albums():
+    return models.Album.objects.count()
+
+
+def get_artists():
+    return models.Artist.objects.count()
+
+
+def get_music_duration():
+    seconds = models.TrackFile.objects.aggregate(
+        d=Sum('duration'),
+    )['d']
+    if seconds:
+        return seconds / 3600
+    return 0
diff --git a/api/funkwhale_api/instance/urls.py b/api/funkwhale_api/instance/urls.py
index 2f2b46b87a4fe301387c5e134cc7a6fcdf6291b2..af23e7e08433b97c6c10f07e1b929892e5dbe32c 100644
--- a/api/funkwhale_api/instance/urls.py
+++ b/api/funkwhale_api/instance/urls.py
@@ -1,7 +1,11 @@
 from django.conf.urls import url
+from django.views.decorators.cache import cache_page
+
 from . import views
 
 
 urlpatterns = [
     url(r'^settings/$', views.InstanceSettings.as_view(), name='settings'),
+    url(r'^stats/$',
+        cache_page(60 * 5)(views.InstanceStats.as_view()), name='stats'),
 ]
diff --git a/api/funkwhale_api/instance/views.py b/api/funkwhale_api/instance/views.py
index 44ee228735d0ad7297dcf31d513ecc087d8c9d59..7f8f393c964e24bfc7a5ee29bf6be2e30f337188 100644
--- a/api/funkwhale_api/instance/views.py
+++ b/api/funkwhale_api/instance/views.py
@@ -4,6 +4,8 @@ from rest_framework.response import Response
 from dynamic_preferences.api import serializers
 from dynamic_preferences.registries import global_preferences_registry
 
+from . import stats
+
 
 class InstanceSettings(views.APIView):
     permission_classes = []
@@ -23,3 +25,12 @@ class InstanceSettings(views.APIView):
         data = serializers.GlobalPreferenceSerializer(
             api_preferences, many=True).data
         return Response(data, status=200)
+
+
+class InstanceStats(views.APIView):
+    permission_classes = []
+    authentication_classes = []
+
+    def get(self, request, *args, **kwargs):
+        data = stats.get()
+        return Response(data, status=200)
diff --git a/api/funkwhale_api/music/admin.py b/api/funkwhale_api/music/admin.py
index 524b85386a6c4b41482d2733719757b2fdb18dbf..219b40a91deeeb9d06dd55d3292aa5ca22ccc654 100644
--- a/api/funkwhale_api/music/admin.py
+++ b/api/funkwhale_api/music/admin.py
@@ -25,13 +25,26 @@ class TrackAdmin(admin.ModelAdmin):
 
 @admin.register(models.ImportBatch)
 class ImportBatchAdmin(admin.ModelAdmin):
-    list_display = ['creation_date', 'status']
-
+    list_display = [
+        'submitted_by',
+        'creation_date',
+        'import_request',
+        'status']
+    list_select_related = [
+        'submitted_by',
+        'import_request',
+    ]
+    list_filter = ['status']
+    search_fields = [
+        'import_request__name', 'source', 'batch__pk', 'mbid']
 
 @admin.register(models.ImportJob)
 class ImportJobAdmin(admin.ModelAdmin):
     list_display = ['source', 'batch', 'track_file', 'status', 'mbid']
-    list_select_related = True
+    list_select_related = [
+        'track_file',
+        'batch',
+    ]
     search_fields = ['source', 'batch__pk', 'mbid']
     list_filter = ['status']
 
@@ -50,3 +63,19 @@ class LyricsAdmin(admin.ModelAdmin):
     list_select_related = True
     search_fields = ['url', 'work__title']
     list_filter = ['work__language']
+
+
+@admin.register(models.TrackFile)
+class TrackFileAdmin(admin.ModelAdmin):
+    list_display = [
+        'track',
+        'audio_file',
+        'source',
+        'duration',
+        'mimetype',
+    ]
+    list_select_related = [
+        'track'
+    ]
+    search_fields = ['source', 'acoustid_track_id']
+    list_filter = ['mimetype']
diff --git a/api/funkwhale_api/music/tasks.py b/api/funkwhale_api/music/tasks.py
index fc706c812fcd97145540747cd2fc4a3b11db56b6..cb4a737c9dcff48f7e69d3e1b9aa4417c3d9bc95 100644
--- a/api/funkwhale_api/music/tasks.py
+++ b/api/funkwhale_api/music/tasks.py
@@ -73,7 +73,10 @@ def _do_import(import_job, replace):
 
 
 @celery.app.task(name='ImportJob.run', bind=True)
-@celery.require_instance(models.ImportJob, 'import_job')
+@celery.require_instance(
+    models.ImportJob.objects.filter(
+        status__in=['pending', 'errored']),
+    'import_job')
 def import_job_run(self, import_job, replace=False):
     def mark_errored():
         import_job.status = 'errored'
diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py
index bf9d39b1d507c7122cb5e6d2eeb70b45e8946fee..d026c9847ca3db7f9920469a225399b0d6c0d3dc 100644
--- a/api/funkwhale_api/music/views.py
+++ b/api/funkwhale_api/music/views.py
@@ -19,6 +19,7 @@ from musicbrainzngs import ResponseError
 from django.contrib.auth.decorators import login_required
 from django.utils.decorators import method_decorator
 
+from funkwhale_api.common import utils as funkwhale_utils
 from funkwhale_api.requests.models import ImportRequest
 from funkwhale_api.musicbrainz import api
 from funkwhale_api.common.permissions import (
@@ -62,7 +63,7 @@ class ArtistViewSet(SearchMixin, viewsets.ReadOnlyModelViewSet):
                                 'albums__tracks__tags'))
     serializer_class = serializers.ArtistSerializerNested
     permission_classes = [ConditionalAuthentication]
-    search_fields = ['name']
+    search_fields = ['name__unaccent']
     filter_class = filters.ArtistFilter
     ordering_fields = ('id', 'name', 'creation_date')
 
@@ -75,7 +76,7 @@ class AlbumViewSet(SearchMixin, viewsets.ReadOnlyModelViewSet):
                                               'tracks__files'))
     serializer_class = serializers.AlbumSerializerNested
     permission_classes = [ConditionalAuthentication]
-    search_fields = ['title']
+    search_fields = ['title__unaccent']
     ordering_fields = ('creation_date',)
 
 
@@ -116,7 +117,10 @@ class ImportJobViewSet(
     def perform_create(self, serializer):
         source = 'file://' + serializer.validated_data['audio_file'].name
         serializer.save(source=source)
-        tasks.import_job_run.delay(import_job_id=serializer.instance.pk)
+        funkwhale_utils.on_commit(
+            tasks.import_job_run.delay,
+            import_job_id=serializer.instance.pk
+        )
 
 
 class TrackViewSet(TagViewSetMixin, SearchMixin, viewsets.ReadOnlyModelViewSet):
@@ -129,9 +133,9 @@ class TrackViewSet(TagViewSetMixin, SearchMixin, viewsets.ReadOnlyModelViewSet):
     search_fields = ['title', 'artist__name']
     ordering_fields = (
         'creation_date',
-        'title',
-        'album__title',
-        'artist__name',
+        'title__unaccent',
+        'album__title__unaccent',
+        'artist__name__unaccent',
     )
 
     def get_queryset(self):
@@ -245,7 +249,11 @@ class Search(views.APIView):
         return Response(results, status=200)
 
     def get_tracks(self, query):
-        search_fields = ['mbid', 'title', 'album__title', 'artist__name']
+        search_fields = [
+            'mbid',
+            'title__unaccent',
+            'album__title__unaccent',
+            'artist__name__unaccent']
         query_obj = utils.get_query(query, search_fields)
         return (
             models.Track.objects.all()
@@ -259,7 +267,10 @@ class Search(views.APIView):
 
 
     def get_albums(self, query):
-        search_fields = ['mbid', 'title', 'artist__name']
+        search_fields = [
+            'mbid',
+            'title__unaccent',
+            'artist__name__unaccent']
         query_obj = utils.get_query(query, search_fields)
         return (
             models.Album.objects.all()
@@ -273,7 +284,7 @@ class Search(views.APIView):
 
 
     def get_artists(self, query):
-        search_fields = ['mbid', 'name']
+        search_fields = ['mbid', 'name__unaccent']
         query_obj = utils.get_query(query, search_fields)
         return (
             models.Artist.objects.all()
@@ -288,7 +299,7 @@ class Search(views.APIView):
 
 
     def get_tags(self, query):
-        search_fields = ['slug', 'name']
+        search_fields = ['slug', 'name__unaccent']
         query_obj = utils.get_query(query, search_fields)
 
         # We want the shortest tag first
@@ -336,6 +347,7 @@ class SubmitViewSet(viewsets.ViewSet):
             data, request, batch=None, import_request=import_request)
         return Response(import_data)
 
+    @transaction.atomic
     def _import_album(self, data, request, batch=None, import_request=None):
         # we import the whole album here to prevent race conditions that occurs
         # when using get_or_create_from_api in tasks
@@ -355,7 +367,11 @@ class SubmitViewSet(viewsets.ViewSet):
                 models.TrackFile.objects.get(track__mbid=row['mbid'])
             except models.TrackFile.DoesNotExist:
                 job = models.ImportJob.objects.create(mbid=row['mbid'], batch=batch, source=row['source'])
-                tasks.import_job_run.delay(import_job_id=job.pk)
+                funkwhale_utils.on_commit(
+                    tasks.import_job_run.delay,
+                    import_job_id=job.pk
+                )
+
         serializer = serializers.ImportBatchSerializer(batch)
         return serializer.data, batch
 
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 4130e93f3805c8e644c542af699c4ad8e7d65146..17a199473de9a8f0ec82da1d44b5ae74a481f31c 100644
--- a/api/funkwhale_api/providers/audiofile/management/commands/import_files.py
+++ b/api/funkwhale_api/providers/audiofile/management/commands/import_files.py
@@ -3,6 +3,9 @@ import os
 
 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 tasks
 from funkwhale_api.users.models import User
 
@@ -86,6 +89,7 @@ class Command(BaseCommand):
         self.stdout.write(
             "For details, please refer to import batch #".format(batch.pk))
 
+    @transaction.atomic
     def do_import(self, matching, user, options):
         message = 'Importing {}...'
         if options['async']:
@@ -94,7 +98,7 @@ class Command(BaseCommand):
         # we create an import batch binded to the user
         batch = user.imports.create(source='shell')
         async = options['async']
-        handler = tasks.import_job_run.delay if async else tasks.import_job_run
+        import_handler = tasks.import_job_run.delay if async else tasks.import_job_run
         for path in matching:
             job = batch.jobs.create(
                 source='file://' + path,
@@ -105,7 +109,8 @@ class Command(BaseCommand):
 
             job.save()
             try:
-                handler(import_job_id=job.pk)
+                utils.on_commit(import_handler, import_job_id=job.pk)
             except Exception as e:
                 self.stdout.write('Error: {}'.format(e))
+
         return batch
diff --git a/api/funkwhale_api/radios/admin.py b/api/funkwhale_api/radios/admin.py
new file mode 100644
index 0000000000000000000000000000000000000000..6d5abadaff79fcbff2a32e6713800146b6edf05f
--- /dev/null
+++ b/api/funkwhale_api/radios/admin.py
@@ -0,0 +1,48 @@
+from django.contrib import admin
+
+from . import models
+
+
+@admin.register(models.Radio)
+class RadioAdmin(admin.ModelAdmin):
+    list_display = [
+        'user', 'name', 'is_public', 'creation_date', 'config']
+    list_select_related = [
+        'user',
+    ]
+    list_filter = [
+        'is_public',
+    ]
+    search_fields = ['name', 'description']
+
+
+@admin.register(models.RadioSession)
+class RadioSessionAdmin(admin.ModelAdmin):
+    list_display = [
+        'user',
+        'custom_radio',
+        'radio_type',
+        'creation_date',
+        'related_object']
+
+    list_select_related = [
+        'user',
+        'custom_radio'
+    ]
+    list_filter = [
+        'radio_type',
+    ]
+
+
+@admin.register(models.RadioSessionTrack)
+class RadioSessionTrackAdmin(admin.ModelAdmin):
+    list_display = [
+        'id',
+        'session',
+        'position',
+        'track',]
+
+    list_select_related = [
+        'track',
+        'session'
+    ]
diff --git a/api/funkwhale_api/radios/radios.py b/api/funkwhale_api/radios/radios.py
index 585bbbe334f0c3ca297d9ada8883b48bb848354c..0d045ea4dc89ba2d545364e2a3123f3f625a001e 100644
--- a/api/funkwhale_api/radios/radios.py
+++ b/api/funkwhale_api/radios/radios.py
@@ -1,5 +1,6 @@
 import random
 from rest_framework import serializers
+from django.db.models import Count
 from django.core.exceptions import ValidationError
 from taggit.models import Tag
 from funkwhale_api.users.models import User
@@ -39,8 +40,11 @@ class SessionRadio(SimpleRadio):
         self.session = models.RadioSession.objects.create(user=user, radio_type=self.radio_type, **kwargs)
         return self.session
 
-    def get_queryset(self):
-        raise NotImplementedError
+    def get_queryset(self, **kwargs):
+        qs = Track.objects.annotate(
+            files_count=Count('files')
+        )
+        return qs.filter(files_count__gt=0)
 
     def get_queryset_kwargs(self):
         return {}
@@ -75,7 +79,9 @@ class SessionRadio(SimpleRadio):
 @registry.register(name='random')
 class RandomRadio(SessionRadio):
     def get_queryset(self, **kwargs):
-        return Track.objects.all()
+        qs = super().get_queryset(**kwargs)
+        return qs.order_by('?')
+
 
 @registry.register(name='favorites')
 class FavoritesRadio(SessionRadio):
@@ -87,8 +93,9 @@ class FavoritesRadio(SessionRadio):
         return kwargs
 
     def get_queryset(self, **kwargs):
+        qs = super().get_queryset(**kwargs)
         track_ids = kwargs['user'].track_favorites.all().values_list('track', flat=True)
-        return Track.objects.filter(pk__in=track_ids)
+        return qs.filter(pk__in=track_ids)
 
 
 @registry.register(name='custom')
@@ -101,7 +108,11 @@ class CustomRadio(SessionRadio):
         return kwargs
 
     def get_queryset(self, **kwargs):
-        return filters.run(kwargs['custom_radio'].config)
+        qs = super().get_queryset(**kwargs)
+        return filters.run(
+            kwargs['custom_radio'].config,
+            candidates=qs,
+        )
 
     def validate_session(self, data, **context):
         data = super().validate_session(data, **context)
@@ -141,6 +152,7 @@ class TagRadio(RelatedObjectRadio):
     model = Tag
 
     def get_queryset(self, **kwargs):
+        qs = super().get_queryset(**kwargs)
         return Track.objects.filter(tags__in=[self.session.related_object])
 
 @registry.register(name='artist')
@@ -148,7 +160,8 @@ class ArtistRadio(RelatedObjectRadio):
     model = Artist
 
     def get_queryset(self, **kwargs):
-        return self.session.related_object.tracks.all()
+        qs = super().get_queryset(**kwargs)
+        return qs.filter(artist=self.session.related_object)
 
 
 @registry.register(name='less-listened')
@@ -160,5 +173,6 @@ class LessListenedRadio(RelatedObjectRadio):
         super().clean(instance)
 
     def get_queryset(self, **kwargs):
+        qs = super().get_queryset(**kwargs)
         listened = self.session.user.listenings.all().values_list('track', flat=True)
-        return Track.objects.exclude(pk__in=listened).order_by('?')
+        return qs.exclude(pk__in=listened).order_by('?')
diff --git a/api/funkwhale_api/requests/admin.py b/api/funkwhale_api/requests/admin.py
new file mode 100644
index 0000000000000000000000000000000000000000..71933eaa940d09c8482e6d7b2a2b73cf22afc5b5
--- /dev/null
+++ b/api/funkwhale_api/requests/admin.py
@@ -0,0 +1,16 @@
+from django.contrib import admin
+
+from . import models
+
+
+@admin.register(models.ImportRequest)
+class ImportRequestAdmin(admin.ModelAdmin):
+    list_display = ['artist_name', 'user', 'status', 'creation_date']
+    list_select_related = [
+        'user',
+        'track'
+    ]
+    list_filter = [
+        'status',
+    ]
+    search_fields = ['artist_name', 'comment', 'albums']
diff --git a/api/tests/instance/test_stats.py b/api/tests/instance/test_stats.py
new file mode 100644
index 0000000000000000000000000000000000000000..6eaad76f7f9d8292211965a462f473c8bb41745a
--- /dev/null
+++ b/api/tests/instance/test_stats.py
@@ -0,0 +1,84 @@
+from django.urls import reverse
+
+from funkwhale_api.instance import stats
+
+
+def test_can_get_stats_via_api(db, api_client, mocker):
+    stats = {
+        'foo': 'bar'
+    }
+    mocker.patch('funkwhale_api.instance.stats.get', return_value=stats)
+    url = reverse('api:v1:instance:stats')
+    response = api_client.get(url)
+    assert response.data == stats
+
+
+def test_get_users(mocker):
+    mocker.patch(
+        'funkwhale_api.users.models.User.objects.count', return_value=42)
+
+    assert stats.get_users() == 42
+
+
+def test_get_music_duration(factories):
+    factories['music.TrackFile'].create_batch(size=5, duration=360)
+
+    # duration is in hours
+    assert stats.get_music_duration() == 0.5
+
+
+def test_get_listenings(mocker):
+    mocker.patch(
+        'funkwhale_api.history.models.Listening.objects.count',
+         return_value=42)
+    assert stats.get_listenings() == 42
+
+
+def test_get_track_favorites(mocker):
+    mocker.patch(
+        'funkwhale_api.favorites.models.TrackFavorite.objects.count',
+         return_value=42)
+    assert stats.get_track_favorites() == 42
+
+
+def test_get_tracks(mocker):
+    mocker.patch(
+        'funkwhale_api.music.models.Track.objects.count',
+         return_value=42)
+    assert stats.get_tracks() == 42
+
+
+def test_get_albums(mocker):
+    mocker.patch(
+        'funkwhale_api.music.models.Album.objects.count',
+         return_value=42)
+    assert stats.get_albums() == 42
+
+
+def test_get_artists(mocker):
+    mocker.patch(
+        'funkwhale_api.music.models.Artist.objects.count',
+         return_value=42)
+    assert stats.get_artists() == 42
+
+
+def test_get(mocker):
+    keys = [
+        'users',
+        'tracks',
+        'albums',
+        'artists',
+        'track_favorites',
+        'listenings',
+        'music_duration',
+    ]
+    mocks = [
+        mocker.patch.object(stats, 'get_{}'.format(k), return_value=i)
+        for i, k in enumerate(keys)
+    ]
+
+    expected = {
+        k: i for i, k in enumerate(keys)
+    }
+
+    assert stats.get() == expected
diff --git a/api/tests/music/test_api.py b/api/tests/music/test_api.py
index 7a856efd1f21971ec7657417f462d007f053c80a..8196d3c092e62b4d85f5da5c1b7490edc6f2def2 100644
--- a/api/tests/music/test_api.py
+++ b/api/tests/music/test_api.py
@@ -6,6 +6,7 @@ from django.urls import reverse
 from funkwhale_api.music import models
 from funkwhale_api.musicbrainz import api
 from funkwhale_api.music import serializers
+from funkwhale_api.music import tasks
 
 from . import data as api_data
 
@@ -208,7 +209,7 @@ def test_user_can_create_an_empty_batch(client, factories):
 
 def test_user_can_create_import_job_with_file(client, factories, mocker):
     path = os.path.join(DATA_DIR, 'test.ogg')
-    m = mocker.patch('funkwhale_api.music.tasks.import_job_run.delay')
+    m = mocker.patch('funkwhale_api.common.utils.on_commit')
     user = factories['users.SuperUser']()
     batch = factories['music.ImportBatch'](submitted_by=user)
     url = reverse('api:v1:import-jobs-list')
@@ -231,7 +232,9 @@ def test_user_can_create_import_job_with_file(client, factories, mocker):
     assert 'test.ogg' in job.source
     assert job.audio_file.read() == content
 
-    m.assert_called_once_with(import_job_id=job.pk)
+    m.assert_called_once_with(
+        tasks.import_job_run.delay,
+        import_job_id=job.pk)
 
 
 def test_can_search_artist(factories, client):
diff --git a/api/tests/radios/test_radios.py b/api/tests/radios/test_radios.py
index b00bfcd79ce3b917a26698ac4d868dea39521428..b731e3024b039bd5006023bb80276405565edd8e 100644
--- a/api/tests/radios/test_radios.py
+++ b/api/tests/radios/test_radios.py
@@ -51,7 +51,8 @@ def test_can_pick_by_weight():
 
 
 def test_can_get_choices_for_favorites_radio(factories):
-    tracks = factories['music.Track'].create_batch(10)
+    files = factories['music.TrackFile'].create_batch(10)
+    tracks = [f.track for f in files]
     user = factories['users.User']()
     for i in range(5):
         TrackFavorite.add(track=random.choice(tracks), user=user)
@@ -71,8 +72,12 @@ def test_can_get_choices_for_favorites_radio(factories):
 
 def test_can_get_choices_for_custom_radio(factories):
     artist = factories['music.Artist']()
-    tracks = factories['music.Track'].create_batch(5, artist=artist)
-    wrong_tracks = factories['music.Track'].create_batch(5)
+    files = factories['music.TrackFile'].create_batch(
+        5, track__artist=artist)
+    tracks = [f.track for f in files]
+    wrong_files = factories['music.TrackFile'].create_batch(5)
+    wrong_tracks = [f.track for f in wrong_files]
+
     session = factories['radios.CustomRadioSession'](
         custom_radio__config=[{'type': 'artist', 'ids': [artist.pk]}]
     )
@@ -113,7 +118,8 @@ def test_can_start_custom_radio_from_api(logged_in_client, factories):
 
 
 def test_can_use_radio_session_to_filter_choices(factories):
-    tracks = factories['music.Track'].create_batch(30)
+    files = factories['music.TrackFile'].create_batch(30)
+    tracks = [f.track for f in files]
     user = factories['users.User']()
     radio = radios.RandomRadio()
     session = radio.start_session(user)
@@ -156,8 +162,8 @@ def test_can_start_radio_for_anonymous_user(client, db):
 
 
 def test_can_get_track_for_session_from_api(factories, logged_in_client):
-    tracks = factories['music.Track'].create_batch(size=1)
-
+    files = factories['music.TrackFile'].create_batch(1)
+    tracks = [f.track for f in files]
     url = reverse('api:v1:radios:sessions-list')
     response = logged_in_client.post(url, {'radio_type': 'random'})
     session = models.RadioSession.objects.latest('id')
@@ -169,7 +175,7 @@ def test_can_get_track_for_session_from_api(factories, logged_in_client):
     assert data['track']['id'] == tracks[0].id
     assert data['position'] == 1
 
-    next_track = factories['music.Track']()
+    next_track = factories['music.TrackFile']().track
     response = logged_in_client.post(url, {'session': session.pk})
     data = json.loads(response.content.decode('utf-8'))
 
@@ -193,8 +199,11 @@ def test_related_object_radio_validate_related_object(factories):
 def test_can_start_artist_radio(factories):
     user = factories['users.User']()
     artist = factories['music.Artist']()
-    wrong_tracks = factories['music.Track'].create_batch(5)
-    good_tracks = factories['music.Track'].create_batch(5, artist=artist)
+    wrong_files = factories['music.TrackFile'].create_batch(5)
+    wrong_tracks = [f.track for f in wrong_files]
+    good_files = factories['music.TrackFile'].create_batch(
+        5, track__artist=artist)
+    good_tracks = [f.track for f in good_files]
 
     radio = radios.ArtistRadio()
     session = radio.start_session(user, related_object=artist)
@@ -206,8 +215,11 @@ def test_can_start_artist_radio(factories):
 def test_can_start_tag_radio(factories):
     user = factories['users.User']()
     tag = factories['taggit.Tag']()
-    wrong_tracks = factories['music.Track'].create_batch(5)
-    good_tracks = factories['music.Track'].create_batch(5, tags=[tag])
+    wrong_files = factories['music.TrackFile'].create_batch(5)
+    wrong_tracks = [f.track for f in wrong_files]
+    good_files = factories['music.TrackFile'].create_batch(
+        5, track__tags=[tag])
+    good_tracks = [f.track for f in good_files]
 
     radio = radios.TagRadio()
     session = radio.start_session(user, related_object=tag)
@@ -229,9 +241,11 @@ def test_can_start_artist_radio_from_api(client, factories):
 
 def test_can_start_less_listened_radio(factories):
     user = factories['users.User']()
-    history = factories['history.Listening'].create_batch(5, user=user)
-    wrong_tracks = [h.track for h in history]
-    good_tracks = factories['music.Track'].create_batch(size=5)
+    wrong_files = factories['music.TrackFile'].create_batch(5)
+    for f in wrong_files:
+        factories['history.Listening'](track=f.track, user=user)
+    good_files = factories['music.TrackFile'].create_batch(5)
+    good_tracks = [f.track for f in good_files]
     radio = radios.LessListenedRadio()
     session = radio.start_session(user)
     assert session.related_object == user
diff --git a/api/tests/test_import_audio_file.py b/api/tests/test_import_audio_file.py
index e81e9b8141455ffe218dc7be469dcdb73462c718..2c254afd959414d7ae3dbccdf5840cc42ad50956 100644
--- a/api/tests/test_import_audio_file.py
+++ b/api/tests/test_import_audio_file.py
@@ -6,6 +6,7 @@ from django.core.management import call_command
 from django.core.management.base import CommandError
 
 from funkwhale_api.providers.audiofile import tasks
+from funkwhale_api.music import tasks as music_tasks
 
 DATA_DIR = os.path.join(
     os.path.dirname(os.path.abspath(__file__)),
@@ -53,7 +54,7 @@ def test_management_command_requires_a_valid_username(factories, mocker):
 
 
 def test_import_files_creates_a_batch_and_job(factories, mocker):
-    m = mocker.patch('funkwhale_api.music.tasks.import_job_run.delay')
+    m = m = mocker.patch('funkwhale_api.common.utils.on_commit')
     user = factories['users.User'](username='me')
     path = os.path.join(DATA_DIR, 'dummy_file.ogg')
     call_command(
@@ -74,4 +75,6 @@ def test_import_files_creates_a_batch_and_job(factories, mocker):
         assert job.audio_file.read() == f.read()
 
     assert job.source == 'file://' + path
-    m.assert_called_once_with(import_job_id=job.pk)
+    m.assert_called_once_with(
+        music_tasks.import_job_run.delay,
+        import_job_id=job.pk)
diff --git a/changes/__init__.py b/changes/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/changes/changelog.d/.gitkeep b/changes/changelog.d/.gitkeep
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/changes/template.rst b/changes/template.rst
new file mode 100644
index 0000000000000000000000000000000000000000..f4d94dee8e3d9b0fd548b30c8649c8c1856a1bb3
--- /dev/null
+++ b/changes/template.rst
@@ -0,0 +1,27 @@
+{% for section, _ in sections.items() %}
+{% if sections[section] %}
+{% for category, val in definitions.items() if category in sections[section]%}
+{{ definitions[category]['name'] }}:
+
+{% if definitions[category]['showcontent'] %}
+{% for text in sections[section][category].keys()|sort() %}
+- {{ text }}
+{% endfor %}
+
+{% else %}
+- {{ sections[section][category]['']|join(', ') }}
+
+{% endif %}
+{% if sections[section][category]|length == 0 %}
+No significant changes.
+
+{% else %}
+{% endif %}
+
+{% endfor %}
+{% else %}
+No significant changes.
+
+
+{% endif %}
+{% endfor %}
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 0e6872f4b67b1b77fa07dcf4c23f2553172b01f5..491ea7340e87986932a13970959e1affba82f41b 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -1,38 +1 @@
-Changelog
-=========
-
-0.2.1
------
-
-2017-07-17
-
-* Now return media files with absolute URL
-* Now display CLI instructions to download a set of tracks
-* Fixed #33: sort by track position in album in API by default, also reuse that information on frontend side
-* More robust audio player and queue in various situations:
-* upgrade to latest dynamic_preferences and use redis as cache even locally
-
-
-0.2
--------
-
-2017-07-09
-
-* [feature] can now import artist and releases from youtube and musicbrainz.
-  This requires a YouTube API key for the search
-* [breaking] we now check for user permission before serving audio files, which requires
-  a specific configuration block in your reverse proxy configuration::
-
-    location /_protected/media {
-        internal;
-        alias   /srv/funkwhale/data/media;
-    }
-
-
-
-0.1
--------
-
-2017-06-26
-
-Initial release
+.. include:: ../CHANGELOG
diff --git a/docs/conf.py b/docs/conf.py
index 3a0c8f6f19d8a31d5e1f9ccb48bfa98d9c00fbdb..01da9bc05b473b280e53c6d29101a01b6027d6ab 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -57,9 +57,7 @@ author = 'Eliot Berriot'
 # built documents.
 #
 # The short X.Y version.
-# version = funkwhale_api.__version__
-# @TODO use real version here
-version = 'feature/22-debian-installation'
+version = funkwhale_api.__version__
 # The full version, including alpha/beta/rc tags.
 release = version
 
diff --git a/front/src/components/About.vue b/front/src/components/About.vue
index 01ce6a294fdeb632d03e8eb23a4a4bbf83766223..92bafd7afddde359aa7ffef0b7a2fb1e4b531e29 100644
--- a/front/src/components/About.vue
+++ b/front/src/components/About.vue
@@ -6,6 +6,7 @@
             <template v-if="instance.name.value">About {{ instance.name.value }}</template>
             <template v-else="instance.name.value">About this instance</template>
         </h1>
+        <stats></stats>
       </div>
     </div>
     <div class="ui vertical stripe segment">
@@ -27,8 +28,12 @@
 
 <script>
 import {mapState} from 'vuex'
+import Stats from '@/components/instance/Stats'
 
 export default {
+  components: {
+    Stats
+  },
   created () {
     this.$store.dispatch('instance/fetchSettings')
   },
diff --git a/front/src/components/audio/Player.vue b/front/src/components/audio/Player.vue
index a6cd80523a7ca28a91bb4d0ccf8ba8ea24cc77e9..b5cbd8f81e0bcd1c9bca8cdc7dd192044f94d968 100644
--- a/front/src/components/audio/Player.vue
+++ b/front/src/components/audio/Player.vue
@@ -52,32 +52,28 @@
 
       <div class="two wide column controls ui grid">
         <div
-          @click="previous"
           title="Previous track"
           class="two wide column control"
           :disabled="!hasPrevious">
-            <i :class="['ui', {'disabled': !hasPrevious}, 'step', 'backward', 'big', 'icon']" ></i>
+            <i @click="previous" :class="['ui', {'disabled': !hasPrevious}, 'step', 'backward', 'big', 'icon']" ></i>
         </div>
         <div
           v-if="!playing"
-          @click="togglePlay"
           title="Play track"
           class="two wide column control">
-            <i :class="['ui', 'play', {'disabled': !currentTrack}, 'big', 'icon']"></i>
+            <i @click="togglePlay" :class="['ui', 'play', {'disabled': !currentTrack}, 'big', 'icon']"></i>
         </div>
         <div
           v-else
-          @click="togglePlay"
           title="Pause track"
           class="two wide column control">
-            <i :class="['ui', 'pause', {'disabled': !currentTrack}, 'big', 'icon']"></i>
+            <i @click="togglePlay" :class="['ui', 'pause', {'disabled': !currentTrack}, 'big', 'icon']"></i>
         </div>
         <div
-          @click="next"
           title="Next track"
           class="two wide column control"
           :disabled="!hasNext">
-            <i :class="['ui', {'disabled': !hasNext}, 'step', 'forward', 'big', 'icon']" ></i>
+            <i @click="next" :class="['ui', {'disabled': !hasNext}, 'step', 'forward', 'big', 'icon']" ></i>
         </div>
         <div class="two wide column control volume-control">
           <i title="Unmute" @click="$store.commit('player/volume', 1)" v-if="volume === 0" class="volume off secondary icon"></i>
@@ -109,19 +105,17 @@
           </i>
         </div>
         <div
-          @click="shuffle()"
           :disabled="queue.tracks.length === 0"
           title="Shuffle your queue"
           class="two wide column control">
-          <i :class="['ui', 'random', 'secondary', {'disabled': queue.tracks.length === 0}, 'icon']" ></i>
+          <i @click="shuffle()" :class="['ui', 'random', 'secondary', {'disabled': queue.tracks.length === 0}, 'icon']" ></i>
         </div>
         <div class="one wide column"></div>
         <div
-          @click="clean()"
           :disabled="queue.tracks.length === 0"
           title="Clear your queue"
           class="two wide column control">
-          <i :class="['ui', 'trash', 'secondary', {'disabled': queue.tracks.length === 0}, 'icon']" ></i>
+          <i @click="clean()" :class="['ui', 'trash', 'secondary', {'disabled': queue.tracks.length === 0}, 'icon']" ></i>
         </div>
       </div>
       <GlobalEvents
diff --git a/front/src/components/audio/Track.vue b/front/src/components/audio/Track.vue
index d8dcaff9b393013be60b2cb7e0b82ac0b6329e31..e3f1c18b33871f94671c391d16df51ad0b20cb50 100644
--- a/front/src/components/audio/Track.vue
+++ b/front/src/components/audio/Track.vue
@@ -7,7 +7,11 @@
     @timeupdate="updateProgress"
     @ended="ended"
     preload>
-    <source v-for="src in srcs" :src="src.url" :type="src.type">
+    <source
+      @error="sourceErrored"
+      v-for="src in srcs"
+      src="src.url"
+      :type="src.type">
   </audio>
 </template>
 
@@ -25,6 +29,11 @@ export default {
     startTime: {type: Number, default: 0},
     autoplay: {type: Boolean, default: false}
   },
+  data () {
+    return {
+      sourceErrors: 0
+    }
+  },
   computed: {
     ...mapState({
       playing: state => state.player.playing,
@@ -65,11 +74,19 @@ export default {
     errored: function () {
       this.$store.dispatch('player/trackErrored')
     },
+    sourceErrored: function () {
+      this.sourceErrors += 1
+      if (this.sourceErrors >= this.srcs.length) {
+        // all sources failed
+        this.errored()
+      }
+    },
     updateDuration: function (e) {
       this.$store.commit('player/duration', this.$refs.audio.duration)
     },
     loaded: function () {
       this.$refs.audio.volume = this.volume
+      this.$store.commit('player/resetErrorCount')
       if (this.isCurrent) {
         this.$store.commit('player/duration', this.$refs.audio.duration)
         if (this.startTime) {
diff --git a/front/src/components/instance/Stats.vue b/front/src/components/instance/Stats.vue
new file mode 100644
index 0000000000000000000000000000000000000000..884809f3a247515d5f2aebcc5d40ab6b14646d15
--- /dev/null
+++ b/front/src/components/instance/Stats.vue
@@ -0,0 +1,104 @@
+<template>
+  <div>
+    <div v-if="stats" class="ui stackable two column grid">
+      <div class="column">
+        <h3 class="ui left aligned header">User activity</h3>
+        <div class="ui mini horizontal statistics">
+          <div class="statistic">
+            <div class="value">
+              <i class="green user icon"></i>
+              {{ stats.users }}
+            </div>
+            <div class="label">
+              Users
+            </div>
+          </div>
+          <div class="statistic">
+            <div class="value">
+              <i class="orange sound icon"></i> {{ stats.listenings }}
+            </div>
+            <div class="label">
+              tracks listened
+            </div>
+          </div>
+          <div class="statistic">
+            <div class="value">
+              <i class="pink heart icon"></i> {{ stats.track_favorites }}
+            </div>
+            <div class="label">
+              Tracks favorited
+            </div>
+          </div>
+        </div>
+      </div>
+      <div class="column">
+        <h3 class="ui left aligned header">Library</h3>
+        <div class="ui mini horizontal statistics">
+          <div class="statistic">
+            <div class="value">
+              {{ parseInt(stats.music_duration) }}
+            </div>
+            <div class="label">
+              hours of music
+            </div>
+          </div>
+          <div class="statistic">
+            <div class="value">
+              {{ stats.artists }}
+            </div>
+            <div class="label">
+              Artists
+            </div>
+          </div>
+          <div class="statistic">
+            <div class="value">
+              {{ stats.albums }}
+            </div>
+            <div class="label">
+              Albums
+            </div>
+          </div>
+          <div class="statistic">
+            <div class="value">
+              {{ stats.tracks }}
+            </div>
+            <div class="label">
+              tracks
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import axios from 'axios'
+import logger from '@/logging'
+
+export default {
+  data () {
+    return {
+      stats: null
+    }
+  },
+  created () {
+    this.fetchData()
+  },
+  methods: {
+    fetchData () {
+      var self = this
+      this.isLoading = true
+      logger.default.debug('Fetching instance stats...')
+      axios.get('instance/stats/').then((response) => {
+        self.stats = response.data
+        self.isLoading = false
+      })
+    }
+  }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+</style>
diff --git a/front/src/store/auth.js b/front/src/store/auth.js
index 24dafcd7266d7fe23f2e0c60807ea29fb440db98..7944cae0836f58a6eed111b2134fb9ab702123ef 100644
--- a/front/src/store/auth.js
+++ b/front/src/store/auth.js
@@ -89,6 +89,7 @@ export default {
         logger.default.info('Successfully fetched user profile')
         let data = response.data
         commit('profile', data)
+        commit('username', data.username)
         dispatch('favorites/fetch', null, {root: true})
         Object.keys(data.permissions).forEach(function (key) {
           // this makes it easier to check for permissions in templates
diff --git a/front/src/store/player.js b/front/src/store/player.js
index df8d159f40b4fe3287f9f6252df0ecfdb9023830..2dc3a7402f29c0f72ed46a36f2b02591330099dc 100644
--- a/front/src/store/player.js
+++ b/front/src/store/player.js
@@ -5,6 +5,8 @@ import time from '@/utils/time'
 export default {
   namespaced: true,
   state: {
+    maxConsecutiveErrors: 5,
+    errorCount: 0,
     playing: false,
     volume: 0.5,
     duration: 0,
@@ -25,6 +27,12 @@ export default {
       value = Math.max(value, 0)
       state.volume = value
     },
+    incrementErrorCount (state) {
+      state.errorCount += 1
+    },
+    resetErrorCount (state) {
+      state.errorCount = 0
+    },
     duration (state, value) {
       state.duration = value
     },
@@ -78,12 +86,20 @@ export default {
         logger.default.error('Could not record track in history')
       })
     },
-    trackEnded ({dispatch}, track) {
+    trackEnded ({dispatch, rootState}, track) {
       dispatch('trackListened', track)
+      let queueState = rootState.queue
+      if (queueState.currentIndex === queueState.tracks.length - 1) {
+        // we've reached last track of queue, trigger a reload
+        // from radio if any
+        dispatch('radios/populateQueue', null, {root: true})
+      }
+      dispatch('queue/next', null, {root: true})
       dispatch('queue/next', null, {root: true})
     },
-    trackErrored ({commit, dispatch}) {
+    trackErrored ({commit, dispatch, state}) {
       commit('errored', true)
+      commit('incrementErrorCount')
       dispatch('queue/next', null, {root: true})
     },
     updateProgress ({commit}, t) {
diff --git a/front/src/store/radios.js b/front/src/store/radios.js
index 922083d8841083afbd2ddb29848c62a0e240d6f7..e95db512643d297fa08ecca16df30e8004739e7c 100644
--- a/front/src/store/radios.js
+++ b/front/src/store/radios.js
@@ -53,10 +53,13 @@ export default {
       commit('current', null)
       commit('running', false)
     },
-    populateQueue ({state, dispatch}) {
+    populateQueue ({rootState, state, dispatch}) {
       if (!state.running) {
         return
       }
+      if (rootState.player.errorCount >= rootState.player.maxConsecutiveErrors - 1) {
+        return
+      }
       var params = {
         session: state.current.session
       }
diff --git a/front/test/unit/specs/store/auth.spec.js b/front/test/unit/specs/store/auth.spec.js
index aa07f9f8bfc03c5f2c766f4a333ff233058ace39..3271f5168f335a95f5e2799007bd3ec64cf77155 100644
--- a/front/test/unit/specs/store/auth.spec.js
+++ b/front/test/unit/specs/store/auth.spec.js
@@ -176,6 +176,7 @@ describe('store/auth', () => {
         action: store.actions.fetchProfile,
         expectedMutations: [
           { type: 'profile', payload: profile },
+          { type: 'username', payload: profile.username },
           { type: 'permission', payload: {key: 'admin', status: true} }
         ],
         expectedActions: [
diff --git a/front/test/unit/specs/store/player.spec.js b/front/test/unit/specs/store/player.spec.js
index af0b6b4354394dbc9c1c62186796e62653ca52fb..b55fb010d08576bfe9e029cd6eb8e130af81570f 100644
--- a/front/test/unit/specs/store/player.spec.js
+++ b/front/test/unit/specs/store/player.spec.js
@@ -74,6 +74,16 @@ describe('store/player', () => {
       store.mutations.toggleLooping(state)
       expect(state.looping).to.equal(0)
     })
+    it('increment error count', () => {
+      const state = { errorCount: 0 }
+      store.mutations.incrementErrorCount(state)
+      expect(state.errorCount).to.equal(1)
+    })
+    it('reset error count', () => {
+      const state = { errorCount: 10 }
+      store.mutations.resetErrorCount(state)
+      expect(state.errorCount).to.equal(0)
+    })
   })
   describe('getters', () => {
     it('durationFormatted', () => {
@@ -122,8 +132,21 @@ describe('store/player', () => {
       testAction({
         action: store.actions.trackEnded,
         payload: {test: 'track'},
+        params: {rootState: {queue: {currentIndex:0, tracks: [1, 2]}}},
+        expectedActions: [
+          { type: 'trackListened', payload: {test: 'track'} },
+          { type: 'queue/next', payload: null, options: {root: true} }
+        ]
+      }, done)
+    })
+    it('trackEnded calls populateQueue if last', (done) => {
+      testAction({
+        action: store.actions.trackEnded,
+        payload: {test: 'track'},
+        params: {rootState: {queue: {currentIndex:1, tracks: [1, 2]}}},
         expectedActions: [
           { type: 'trackListened', payload: {test: 'track'} },
+          { type: 'radios/populateQueue', payload: null, options: {root: true} },
           { type: 'queue/next', payload: null, options: {root: true} }
         ]
       }, done)
@@ -132,8 +155,10 @@ describe('store/player', () => {
       testAction({
         action: store.actions.trackErrored,
         payload: {test: 'track'},
+        params: {state: {errorCount: 0, maxConsecutiveErrors: 5}},
         expectedMutations: [
-          { type: 'errored', payload: true }
+          { type: 'errored', payload: true },
+          { type: 'incrementErrorCount' }
         ],
         expectedActions: [
           { type: 'queue/next', payload: null, options: {root: true} }
diff --git a/front/test/unit/specs/store/radios.spec.js b/front/test/unit/specs/store/radios.spec.js
index 3ff8a05ed49df400cf71c588732794c1f907b144..6de6b8dd94858f34ee07d522d1e3d2ab36185cc0 100644
--- a/front/test/unit/specs/store/radios.spec.js
+++ b/front/test/unit/specs/store/radios.spec.js
@@ -69,7 +69,11 @@ describe('store/radios', () => {
       })
       testAction({
         action: store.actions.populateQueue,
-        params: {state: {running: true, current: {session: 1}}},
+        params: {
+          state: {running: true, current: {session: 1}},
+          rootState: {player: {errorCount: 0, maxConsecutiveErrors: 5}}
+
+        },
         expectedActions: [
           { type: 'queue/append', payload: {track: {id: 1}}, options: {root: true} }
         ]
@@ -82,5 +86,17 @@ describe('store/radios', () => {
         expectedActions: []
       }, done)
     })
+    it('populateQueue does nothing when too much errors', (done) => {
+      testAction({
+        action: store.actions.populateQueue,
+        payload: {test: 'track'},
+        params: {
+          rootState: {player: {errorCount: 5, maxConsecutiveErrors: 5}},
+          state: {running: true}
+        },
+        expectedActions: []
+      }, done)
+    })
+
   })
 })
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000000000000000000000000000000000000..baea16861cd0cb3b81afa8c6561921148f8b1d68
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,33 @@
+[tool.towncrier]
+    package = "changes"
+    package_dir = ""
+    filename = "CHANGELOG"
+    directory = "changes/changelog.d/"
+    start_string = ".. towncrier\n"
+    template = "changes/template.rst"
+    issue_format = ""
+    title_format = "{version} (unreleased)"
+    underlines = "-"
+
+    [[tool.towncrier.section]]
+        path = ""
+
+    [[tool.towncrier.type]]
+        directory = "feature"
+        name = "Features"
+        showcontent = true
+
+    [[tool.towncrier.type]]
+        directory = "bugfix"
+        name = "Bugfixes"
+        showcontent = true
+
+    [[tool.towncrier.type]]
+        directory = "doc"
+        name = "Documentation"
+        showcontent = true
+
+    [[tool.towncrier.type]]
+        directory = "misc"
+        name = "Other"
+        showcontent = true