diff --git a/api/config/api_urls.py b/api/config/api_urls.py
index c7ebc4ed3668196ccd11d6844ba14d704e3ba43e..ff6db0d069395c316d207a640e0187ccf92b12df 100644
--- a/api/config/api_urls.py
+++ b/api/config/api_urls.py
@@ -52,6 +52,10 @@ v1_patterns += [
         include(
             ('funkwhale_api.users.api_urls', 'users'),
             namespace='users')),
+    url(r'^requests/',
+        include(
+            ('funkwhale_api.requests.api_urls', 'requests'),
+            namespace='requests')),
     url(r'^token/$', jwt_views.obtain_jwt_token, name='token'),
     url(r'^token/refresh/$', jwt_views.refresh_jwt_token, name='token_refresh'),
 ]
diff --git a/api/config/settings/common.py b/api/config/settings/common.py
index 6d02cbbc1038999cc7a3de46448369aa00e030bf..5fe55e53a33fdc58384d858370b4e2a5ba646cb9 100644
--- a/api/config/settings/common.py
+++ b/api/config/settings/common.py
@@ -80,10 +80,12 @@ if RAVEN_ENABLED:
 
 # Apps specific for this project go here.
 LOCAL_APPS = (
+    'funkwhale_api.common',
     'funkwhale_api.users',  # custom users app
     # Your stuff: custom apps go here
     'funkwhale_api.instance',
     'funkwhale_api.music',
+    'funkwhale_api.requests',
     'funkwhale_api.favorites',
     'funkwhale_api.radios',
     'funkwhale_api.history',
diff --git a/api/funkwhale_api/history/views.py b/api/funkwhale_api/history/views.py
index 32bad6060273e53abad7de942c285b48b16c252e..59dcbd26b8449abef324273219a961d44f39f489 100644
--- a/api/funkwhale_api/history/views.py
+++ b/api/funkwhale_api/history/views.py
@@ -17,9 +17,6 @@ class ListeningViewSet(mixins.CreateModelMixin,
     queryset = models.Listening.objects.all()
     permission_classes = [ConditionalAuthentication]
 
-    def create(self, request, *args, **kwargs):
-        return super().create(request, *args, **kwargs)
-
     def get_queryset(self):
         queryset = super().get_queryset()
         if self.request.user.is_authenticated:
diff --git a/api/funkwhale_api/music/migrations/0020_importbatch_status.py b/api/funkwhale_api/music/migrations/0020_importbatch_status.py
new file mode 100644
index 0000000000000000000000000000000000000000..265d1ba5d5312d086f25d0861039b3a94a5df4e5
--- /dev/null
+++ b/api/funkwhale_api/music/migrations/0020_importbatch_status.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.0.2 on 2018-02-20 19:12
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('music', '0019_populate_mimetypes'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='importbatch',
+            name='status',
+            field=models.CharField(choices=[('pending', 'Pending'), ('finished', 'Finished'), ('errored', 'Errored'), ('skipped', 'Skipped')], default='pending', max_length=30),
+        ),
+    ]
diff --git a/api/funkwhale_api/music/migrations/0021_populate_batch_status.py b/api/funkwhale_api/music/migrations/0021_populate_batch_status.py
new file mode 100644
index 0000000000000000000000000000000000000000..061d649b06115d4871b6c6eb7ae57b25c27661e8
--- /dev/null
+++ b/api/funkwhale_api/music/migrations/0021_populate_batch_status.py
@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+import os
+
+from django.db import migrations, models
+
+
+def populate_status(apps, schema_editor):
+    from funkwhale_api.music.utils import compute_status
+    ImportBatch = apps.get_model("music", "ImportBatch")
+
+    for ib in ImportBatch.objects.prefetch_related('jobs'):
+        ib.status = compute_status(ib.jobs.all())
+        ib.save(update_fields=['status'])
+
+
+def rewind(apps, schema_editor):
+    pass
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('music', '0020_importbatch_status'),
+    ]
+
+    operations = [
+        migrations.RunPython(populate_status, rewind),
+    ]
diff --git a/api/funkwhale_api/music/migrations/0022_importbatch_import_request.py b/api/funkwhale_api/music/migrations/0022_importbatch_import_request.py
new file mode 100644
index 0000000000000000000000000000000000000000..d9f6f01d9121f1148e1f3d5a729b1fc476a89f42
--- /dev/null
+++ b/api/funkwhale_api/music/migrations/0022_importbatch_import_request.py
@@ -0,0 +1,20 @@
+# Generated by Django 2.0.2 on 2018-02-20 22:48
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('requests', '__first__'),
+        ('music', '0021_populate_batch_status'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='importbatch',
+            name='import_request',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='import_batches', to='requests.ImportRequest'),
+        ),
+    ]
diff --git a/api/funkwhale_api/music/models.py b/api/funkwhale_api/music/models.py
index 3ebd07419e6d0fc35608fd4e63f7c2b990e83657..97992fc8f12cafa7af7cb99c7cbfb1cee2b42192 100644
--- a/api/funkwhale_api/music/models.py
+++ b/api/funkwhale_api/music/models.py
@@ -10,8 +10,11 @@ from django.conf import settings
 from django.db import models
 from django.core.files.base import ContentFile
 from django.core.files import File
+from django.db.models.signals import post_save
+from django.dispatch import receiver
 from django.urls import reverse
 from django.utils import timezone
+
 from taggit.managers import TaggableManager
 from versatileimagefield.fields import VersatileImageField
 
@@ -400,6 +403,14 @@ class TrackFile(models.Model):
             self.mimetype = utils.guess_mimetype(self.audio_file)
         return super().save(**kwargs)
 
+
+IMPORT_STATUS_CHOICES = (
+    ('pending', 'Pending'),
+    ('finished', 'Finished'),
+    ('errored', 'Errored'),
+    ('skipped', 'Skipped'),
+)
+
 class ImportBatch(models.Model):
     IMPORT_BATCH_SOURCES = [
         ('api', 'api'),
@@ -412,22 +423,24 @@ class ImportBatch(models.Model):
         'users.User',
         related_name='imports',
         on_delete=models.CASCADE)
-
+    status = models.CharField(
+        choices=IMPORT_STATUS_CHOICES, default='pending', max_length=30)
+    import_request = models.ForeignKey(
+        'requests.ImportRequest',
+        related_name='import_batches',
+        null=True,
+        blank=True,
+        on_delete=models.CASCADE)
     class Meta:
         ordering = ['-creation_date']
 
     def __str__(self):
         return str(self.pk)
 
-    @property
-    def status(self):
-        pending = any([job.status == 'pending' for job in self.jobs.all()])
-        errored = any([job.status == 'errored' for job in self.jobs.all()])
-        if pending:
-            return 'pending'
-        if errored:
-            return 'errored'
-        return 'finished'
+    def update_status(self):
+        self.status = utils.compute_status(self.jobs.all())
+        self.save(update_fields=['status'])
+
 
 class ImportJob(models.Model):
     batch = models.ForeignKey(
@@ -440,15 +453,39 @@ class ImportJob(models.Model):
         on_delete=models.CASCADE)
     source = models.CharField(max_length=500)
     mbid = models.UUIDField(editable=False, null=True, blank=True)
-    STATUS_CHOICES = (
-        ('pending', 'Pending'),
-        ('finished', 'Finished'),
-        ('errored', 'Errored'),
-        ('skipped', 'Skipped'),
-    )
-    status = models.CharField(choices=STATUS_CHOICES, default='pending', max_length=30)
+
+    status = models.CharField(
+        choices=IMPORT_STATUS_CHOICES, default='pending', max_length=30)
     audio_file = models.FileField(
         upload_to='imports/%Y/%m/%d', max_length=255, null=True, blank=True)
 
     class Meta:
         ordering = ('id', )
+
+
+@receiver(post_save, sender=ImportJob)
+def update_batch_status(sender, instance, **kwargs):
+    instance.batch.update_status()
+
+
+@receiver(post_save, sender=ImportBatch)
+def update_request_status(sender, instance, created, **kwargs):
+    update_fields = kwargs.get('update_fields', []) or []
+    if not instance.import_request:
+        return
+
+    if not created and not 'status' in update_fields:
+        return
+
+    r_status = instance.import_request.status
+    status = instance.status
+
+    if status == 'pending' and r_status == 'pending':
+        # let's mark the request as accepted since we started an import
+        instance.import_request.status = 'accepted'
+        return instance.import_request.save(update_fields=['status'])
+
+    if status == 'finished' and r_status == 'accepted':
+        # let's mark the request as imported since the import is over
+        instance.import_request.status = 'imported'
+        return instance.import_request.save(update_fields=['status'])
diff --git a/api/funkwhale_api/music/serializers.py b/api/funkwhale_api/music/serializers.py
index 41de30f1026e54e0f98a50926934f23ce2fd0c84..db6298a9e446eddc64bc17f95e3a6b75bf126573 100644
--- a/api/funkwhale_api/music/serializers.py
+++ b/api/funkwhale_api/music/serializers.py
@@ -125,5 +125,5 @@ class ImportBatchSerializer(serializers.ModelSerializer):
     jobs = ImportJobSerializer(many=True, read_only=True)
     class Meta:
         model = models.ImportBatch
-        fields = ('id', 'jobs', 'status', 'creation_date')
+        fields = ('id', 'jobs', 'status', 'creation_date', 'import_request')
         read_only_fields = ('creation_date',)
diff --git a/api/funkwhale_api/music/utils.py b/api/funkwhale_api/music/utils.py
index 0e4318e563ee5341e0d8b9d0aa32291eea867e7a..a75cf5de8508efaa5df587eb17b01a2e87f1214f 100644
--- a/api/funkwhale_api/music/utils.py
+++ b/api/funkwhale_api/music/utils.py
@@ -43,3 +43,13 @@ def get_query(query_string, search_fields):
 def guess_mimetype(f):
     b = min(100000, f.size)
     return magic.from_buffer(f.read(b), mime=True)
+
+
+def compute_status(jobs):
+    errored = any([job.status == 'errored' for job in jobs])
+    if errored:
+        return 'errored'
+    pending = any([job.status == 'pending' for job in jobs])
+    if pending:
+        return 'pending'
+    return 'finished'
diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py
index 8e46cbd71612f6b6d65563d136408efd4e0401ac..bf9d39b1d507c7122cb5e6d2eeb70b45e8946fee 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.requests.models import ImportRequest
 from funkwhale_api.musicbrainz import api
 from funkwhale_api.common.permissions import (
     ConditionalAuthentication, HasModelPermission)
@@ -314,14 +315,28 @@ class SubmitViewSet(viewsets.ViewSet):
         serializer = serializers.ImportBatchSerializer(batch)
         return Response(serializer.data)
 
+    def get_import_request(self, data):
+        try:
+            raw = data['importRequest']
+        except KeyError:
+            return
+
+        pk = int(raw)
+        try:
+            return ImportRequest.objects.get(pk=pk)
+        except ImportRequest.DoesNotExist:
+            pass
+
     @list_route(methods=['post'])
     @transaction.non_atomic_requests
     def album(self, request, *args, **kwargs):
         data = json.loads(request.body.decode('utf-8'))
-        import_data, batch = self._import_album(data, request, batch=None)
+        import_request = self.get_import_request(data)
+        import_data, batch = self._import_album(
+            data, request, batch=None, import_request=import_request)
         return Response(import_data)
 
-    def _import_album(self, data, request, batch=None):
+    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
         album_data = api.releases.get(id=data['releaseId'], includes=models.Album.api_includes)['release']
@@ -332,7 +347,9 @@ class SubmitViewSet(viewsets.ViewSet):
         except ResponseError:
             pass
         if not batch:
-            batch = models.ImportBatch.objects.create(submitted_by=request.user)
+            batch = models.ImportBatch.objects.create(
+                submitted_by=request.user,
+                import_request=import_request)
         for row in data['tracks']:
             try:
                 models.TrackFile.objects.get(track__mbid=row['mbid'])
@@ -346,6 +363,7 @@ class SubmitViewSet(viewsets.ViewSet):
     @transaction.non_atomic_requests
     def artist(self, request, *args, **kwargs):
         data = json.loads(request.body.decode('utf-8'))
+        import_request = self.get_import_request(data)
         artist_data = api.artists.get(id=data['artistId'])['artist']
         cleaned_data = models.Artist.clean_musicbrainz_data(artist_data)
         artist = importers.load(models.Artist, cleaned_data, artist_data, import_hooks=[])
@@ -353,7 +371,8 @@ class SubmitViewSet(viewsets.ViewSet):
         import_data = []
         batch = None
         for row in data['albums']:
-            row_data, batch = self._import_album(row, request, batch=batch)
+            row_data, batch = self._import_album(
+                row, request, batch=batch, import_request=import_request)
             import_data.append(row_data)
 
         return Response(import_data[0])
diff --git a/api/funkwhale_api/requests/__init__.py b/api/funkwhale_api/requests/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/api/funkwhale_api/requests/api_urls.py b/api/funkwhale_api/requests/api_urls.py
new file mode 100644
index 0000000000000000000000000000000000000000..37459a664a4d7e1fd9bc229fbb678daa02adb012
--- /dev/null
+++ b/api/funkwhale_api/requests/api_urls.py
@@ -0,0 +1,11 @@
+from django.conf.urls import include, url
+from . import views
+
+from rest_framework import routers
+router = routers.SimpleRouter()
+router.register(
+    r'import-requests',
+    views.ImportRequestViewSet,
+    'import-requests')
+
+urlpatterns = router.urls
diff --git a/api/funkwhale_api/requests/factories.py b/api/funkwhale_api/requests/factories.py
new file mode 100644
index 0000000000000000000000000000000000000000..2bcdeb6a9699ee47a16cc4fb2e05ebe9b19286e0
--- /dev/null
+++ b/api/funkwhale_api/requests/factories.py
@@ -0,0 +1,15 @@
+import factory
+
+from funkwhale_api.factories import registry
+from funkwhale_api.users.factories import UserFactory
+
+
+@registry.register
+class ImportRequestFactory(factory.django.DjangoModelFactory):
+    artist_name = factory.Faker('name')
+    albums = factory.Faker('sentence')
+    user = factory.SubFactory(UserFactory)
+    comment = factory.Faker('paragraph')
+
+    class Meta:
+        model = 'requests.ImportRequest'
diff --git a/api/funkwhale_api/requests/filters.py b/api/funkwhale_api/requests/filters.py
new file mode 100644
index 0000000000000000000000000000000000000000..bf353e8ad075abb76f3a4fdc846d9d25b0a105f4
--- /dev/null
+++ b/api/funkwhale_api/requests/filters.py
@@ -0,0 +1,14 @@
+import django_filters
+
+from . import models
+
+
+class ImportRequestFilter(django_filters.FilterSet):
+
+    class Meta:
+        model = models.ImportRequest
+        fields = {
+            'artist_name': ['exact', 'iexact', 'startswith', 'icontains'],
+            'status': ['exact'],
+            'user__username': ['exact'],
+        }
diff --git a/api/funkwhale_api/requests/migrations/0001_initial.py b/api/funkwhale_api/requests/migrations/0001_initial.py
new file mode 100644
index 0000000000000000000000000000000000000000..7c239b3c079234ec130be85c300e3a9956a7ad41
--- /dev/null
+++ b/api/funkwhale_api/requests/migrations/0001_initial.py
@@ -0,0 +1,31 @@
+# Generated by Django 2.0.2 on 2018-02-20 22:49
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='ImportRequest',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
+                ('imported_date', models.DateTimeField(blank=True, null=True)),
+                ('artist_name', models.CharField(max_length=250)),
+                ('albums', models.CharField(blank=True, max_length=3000, null=True)),
+                ('status', models.CharField(choices=[('pending', 'pending'), ('accepted', 'accepted'), ('imported', 'imported'), ('closed', 'closed')], default='pending', max_length=50)),
+                ('comment', models.TextField(blank=True, max_length=3000, null=True)),
+                ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='import_requests', to=settings.AUTH_USER_MODEL)),
+            ],
+        ),
+    ]
diff --git a/api/funkwhale_api/requests/migrations/__init__.py b/api/funkwhale_api/requests/migrations/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/api/funkwhale_api/requests/models.py b/api/funkwhale_api/requests/models.py
new file mode 100644
index 0000000000000000000000000000000000000000..c298524306caa9bc7f4cd6e9eb1f4abbb2d4bde0
--- /dev/null
+++ b/api/funkwhale_api/requests/models.py
@@ -0,0 +1,29 @@
+from django.db import models
+
+from django.utils import timezone
+
+NATURE_CHOICES = [
+    ('artist', 'artist'),
+    ('album', 'album'),
+    ('track', 'track'),
+]
+
+STATUS_CHOICES = [
+    ('pending', 'pending'),
+    ('accepted', 'accepted'),
+    ('imported', 'imported'),
+    ('closed', 'closed'),
+]
+
+class ImportRequest(models.Model):
+    creation_date = models.DateTimeField(default=timezone.now)
+    imported_date = models.DateTimeField(null=True, blank=True)
+    user = models.ForeignKey(
+        'users.User',
+        related_name='import_requests',
+        on_delete=models.CASCADE)
+    artist_name = models.CharField(max_length=250)
+    albums = models.CharField(max_length=3000, null=True, blank=True)
+    status = models.CharField(
+        choices=STATUS_CHOICES, max_length=50, default='pending')
+    comment = models.TextField(null=True, blank=True, max_length=3000)
diff --git a/api/funkwhale_api/requests/serializers.py b/api/funkwhale_api/requests/serializers.py
new file mode 100644
index 0000000000000000000000000000000000000000..51a709514e0cb95a2f03289dd981df918734c795
--- /dev/null
+++ b/api/funkwhale_api/requests/serializers.py
@@ -0,0 +1,30 @@
+from rest_framework import serializers
+
+from funkwhale_api.users.serializers import UserBasicSerializer
+
+from . import models
+
+
+class ImportRequestSerializer(serializers.ModelSerializer):
+    user = UserBasicSerializer(read_only=True)
+
+    class Meta:
+        model = models.ImportRequest
+        fields = (
+            'id',
+            'status',
+            'albums',
+            'artist_name',
+            'user',
+            'creation_date',
+            'imported_date',
+            'comment')
+        read_only_fields = (
+            'creation_date',
+            'imported_date',
+            'user',
+            'status')
+
+    def create(self, validated_data):
+        validated_data['user'] = self.context['user']
+        return super().create(validated_data)
diff --git a/api/funkwhale_api/requests/views.py b/api/funkwhale_api/requests/views.py
new file mode 100644
index 0000000000000000000000000000000000000000..395fac66cff33e3a01fdcc33a56adfcea0aa0c7e
--- /dev/null
+++ b/api/funkwhale_api/requests/views.py
@@ -0,0 +1,36 @@
+from rest_framework import generics, mixins, viewsets
+from rest_framework import status
+from rest_framework.response import Response
+from rest_framework.decorators import detail_route
+
+from funkwhale_api.music.views import SearchMixin
+
+from . import filters
+from . import models
+from . import serializers
+
+
+class ImportRequestViewSet(
+        SearchMixin,
+        mixins.CreateModelMixin,
+        mixins.RetrieveModelMixin,
+        mixins.ListModelMixin,
+        viewsets.GenericViewSet):
+
+    serializer_class = serializers.ImportRequestSerializer
+    queryset = (
+        models.ImportRequest.objects.all()
+              .select_related()
+              .order_by('-creation_date'))
+    search_fields = ['artist_name', 'album_name', 'comment']
+    filter_class = filters.ImportRequestFilter
+    ordering_fields = ('id', 'artist_name', 'creation_date', 'status')
+
+    def perform_create(self, serializer):
+        return serializer.save(user=self.request.user)
+
+    def get_serializer_context(self):
+        context = super().get_serializer_context()
+        if self.request.user.is_authenticated:
+            context['user'] = self.request.user
+        return context
diff --git a/api/funkwhale_api/users/serializers.py b/api/funkwhale_api/users/serializers.py
index 261873bdbec1a34ee0c69e30a002161e69e423f7..8c218b1c28acd2811b208e0a3f4b4f0d788c1aea 100644
--- a/api/funkwhale_api/users/serializers.py
+++ b/api/funkwhale_api/users/serializers.py
@@ -3,6 +3,12 @@ from rest_framework import serializers
 from . import models
 
 
+class UserBasicSerializer(serializers.ModelSerializer):
+    class Meta:
+        model = models.User
+        fields = ['id', 'username', 'name', 'date_joined']
+
+
 class UserSerializer(serializers.ModelSerializer):
 
     permissions = serializers.SerializerMethodField()
diff --git a/api/tests/conftest.py b/api/tests/conftest.py
index 4d7a6fa981d1431cd01a97d4578d2a992b4cb749..10d7c323512c684ff495bda2a2a7a7ae581213f8 100644
--- a/api/tests/conftest.py
+++ b/api/tests/conftest.py
@@ -56,6 +56,24 @@ def api_client(client):
     return APIClient()
 
 
+@pytest.fixture
+def logged_in_api_client(db, factories, api_client):
+    user = factories['users.User']()
+    assert api_client.login(username=user.username, password='test')
+    setattr(api_client, 'user', user)
+    yield api_client
+    delattr(api_client, 'user')
+
+
+@pytest.fixture
+def superuser_api_client(db, factories, api_client):
+    user = factories['users.SuperUser']()
+    assert api_client.login(username=user.username, password='test')
+    setattr(api_client, 'user', user)
+    yield api_client
+    delattr(api_client, 'user')
+
+
 @pytest.fixture
 def superuser_client(db, factories, client):
     user = factories['users.SuperUser']()
diff --git a/api/tests/music/test_import.py b/api/tests/music/test_import.py
new file mode 100644
index 0000000000000000000000000000000000000000..f2ca1abbd04a562764194f09653e17c4724f3cc4
--- /dev/null
+++ b/api/tests/music/test_import.py
@@ -0,0 +1,37 @@
+import json
+
+from django.urls import reverse
+
+from . import data as api_data
+
+
+def test_create_import_can_bind_to_request(
+        mocker, factories, superuser_api_client):
+    request = factories['requests.ImportRequest']()
+
+    mocker.patch('funkwhale_api.music.tasks.import_job_run')
+    mocker.patch(
+        'funkwhale_api.musicbrainz.api.artists.get',
+        return_value=api_data.artists['get']['soad'])
+    mocker.patch(
+        'funkwhale_api.musicbrainz.api.images.get_front',
+        return_value=b'')
+    mocker.patch(
+        'funkwhale_api.musicbrainz.api.releases.get',
+        return_value=api_data.albums['get_with_includes']['hypnotize'])
+    payload = {
+        'releaseId': '47ae093f-1607-49a3-be11-a15d335ccc94',
+        'importRequest': request.pk,
+        'tracks': [
+            {
+                'mbid': '1968a9d6-8d92-4051-8f76-674e157b6eed',
+                'source': 'https://www.youtube.com/watch?v=1111111111',
+            }
+        ]
+    }
+    url = reverse('api:v1:submit-album')
+    response = superuser_api_client.post(
+        url, json.dumps(payload), content_type='application/json')
+    batch = request.import_batches.latest('id')
+
+    assert batch.import_request == request
diff --git a/api/tests/music/test_models.py b/api/tests/music/test_models.py
index 2eb1f276332fc32bcd01e947fac72c3774cfaea8..9f52ba8874e50c10ea0216cacc8363ab767d6396 100644
--- a/api/tests/music/test_models.py
+++ b/api/tests/music/test_models.py
@@ -52,6 +52,20 @@ def test_import_job_is_bound_to_track_file(factories, mocker):
     job.refresh_from_db()
     assert job.track_file.track == track
 
+
+@pytest.mark.parametrize('status', ['pending', 'errored', 'finished'])
+def test_saving_job_updates_batch_status(status,factories, mocker):
+    batch = factories['music.ImportBatch']()
+
+    assert batch.status == 'pending'
+
+    job = factories['music.ImportJob'](batch=batch, status=status)
+
+    batch.refresh_from_db()
+
+    assert batch.status == status
+
+
 @pytest.mark.parametrize('extention,mimetype', [
     ('ogg', 'audio/ogg'),
     ('mp3', 'audio/mpeg'),
diff --git a/api/tests/requests/test_models.py b/api/tests/requests/test_models.py
new file mode 100644
index 0000000000000000000000000000000000000000..797656bd70c5ab916c650c589bd70ea5fc41be87
--- /dev/null
+++ b/api/tests/requests/test_models.py
@@ -0,0 +1,23 @@
+import pytest
+
+from django.forms import ValidationError
+
+
+def test_can_bind_import_batch_to_request(factories):
+    request = factories['requests.ImportRequest']()
+
+    assert request.status == 'pending'
+
+    # when we create the import, we consider the request as accepted
+    batch = factories['music.ImportBatch'](import_request=request)
+    request.refresh_from_db()
+
+    assert request.status == 'accepted'
+
+    # now, the batch is finished, therefore the request status should be
+    # imported
+    batch.status = 'finished'
+    batch.save(update_fields=['status'])
+    request.refresh_from_db()
+
+    assert request.status == 'imported'
diff --git a/api/tests/requests/test_views.py b/api/tests/requests/test_views.py
new file mode 100644
index 0000000000000000000000000000000000000000..6c34f9ad19fcc8ae7ec10ba6563cb44d45cb3461
--- /dev/null
+++ b/api/tests/requests/test_views.py
@@ -0,0 +1,26 @@
+from django.urls import reverse
+
+
+def test_request_viewset_requires_auth(db, api_client):
+    url = reverse('api:v1:requests:import-requests-list')
+    response = api_client.get(url)
+    assert response.status_code == 401
+
+
+def test_user_can_create_request(logged_in_api_client):
+    url = reverse('api:v1:requests:import-requests-list')
+    user = logged_in_api_client.user
+    data = {
+        'artist_name': 'System of a Down',
+        'albums': 'All please!',
+        'comment': 'Please, they rock!',
+    }
+    response = logged_in_api_client.post(url, data)
+
+    assert response.status_code == 201
+
+    ir = user.import_requests.latest('id')
+    assert ir.status == 'pending'
+    assert ir.creation_date is not None
+    for field, value in data.items():
+        assert getattr(ir, field) == value
diff --git a/front/package.json b/front/package.json
index ac3895f6d65655fd198699c92cd55300df301b1e..042e332d0eb91d81df9c27fe13b82b0e16013d3a 100644
--- a/front/package.json
+++ b/front/package.json
@@ -20,9 +20,11 @@
     "js-logger": "^1.3.0",
     "jwt-decode": "^2.2.0",
     "lodash": "^4.17.4",
+    "moment": "^2.20.1",
     "moxios": "^0.4.0",
     "raven-js": "^3.22.3",
     "semantic-ui-css": "^2.2.10",
+    "showdown": "^1.8.6",
     "vue": "^2.3.3",
     "vue-lazyload": "^1.1.4",
     "vue-router": "^2.3.1",
diff --git a/front/src/components/Pagination.vue b/front/src/components/Pagination.vue
index 3ac7c59af5817413c13a97e569065ca48cb95b91..83b386fdeb57f72700b97abacf37c4512bcff5e0 100644
--- a/front/src/components/Pagination.vue
+++ b/front/src/components/Pagination.vue
@@ -49,4 +49,8 @@ export default {
 </script>
 
 <style scoped>
+.ui.menu {
+  border: none;
+  box-shadow: none;
+}
 </style>
diff --git a/front/src/components/common/HumanDate.vue b/front/src/components/common/HumanDate.vue
new file mode 100644
index 0000000000000000000000000000000000000000..ff6ff5c71efa2df05e1467751235ddb15dd67222
--- /dev/null
+++ b/front/src/components/common/HumanDate.vue
@@ -0,0 +1,8 @@
+<template>
+  <time :datetime="date" :title="date | moment">{{ date | ago }}</time>
+</template>
+<script>
+export default {
+  props: ['date']
+}
+</script>
diff --git a/front/src/components/discussion/Comment.vue b/front/src/components/discussion/Comment.vue
new file mode 100644
index 0000000000000000000000000000000000000000..a3c5176ecb9b908817b72f62f884c6f87d2b9660
--- /dev/null
+++ b/front/src/components/discussion/Comment.vue
@@ -0,0 +1,49 @@
+<template>
+  <div class="comment">
+    <div class="content">
+      <a class="author">{{ user.username }}</a>
+      <div class="metadata">
+        <div class="date"><human-date :date="date"></human-date></div>
+      </div>
+      <div class="text" v-html="comment"></div>
+      </div>
+      <div class="actions">
+        <span
+          @click="collapsed = false"
+          v-if="truncated && collapsed"
+          class="expand">Expand</span>
+        <span
+          @click="collapsed = true"
+          v-if="truncated && !collapsed"
+          class="collapse">Collapse</span>
+      </div>
+    </div>
+  </div>
+</template>
+<script>
+  export default {
+    props: {
+      user: {type: Object, required: true},
+      date: {required: true},
+      content: {type: String, required: true}
+    },
+    data () {
+      return {
+        collapsed: true,
+        length: 50
+      }
+    },
+    computed: {
+      comment () {
+        let text = this.content
+        if (this.collapsed) {
+          text = this.$options.filters.truncate(text, this.length)
+        }
+        return this.$options.filters.markdown(text)
+      },
+      truncated () {
+        return this.content.length > this.length
+      }
+    }
+  }
+</script>
diff --git a/front/src/components/globals.js b/front/src/components/globals.js
new file mode 100644
index 0000000000000000000000000000000000000000..40315bc47d8efa9731c7133354181e09bbd6cb59
--- /dev/null
+++ b/front/src/components/globals.js
@@ -0,0 +1,7 @@
+import Vue from 'vue'
+
+import HumanDate from '@/components/common/HumanDate'
+
+Vue.component('human-date', HumanDate)
+
+export default {}
diff --git a/front/src/components/library/Home.vue b/front/src/components/library/Home.vue
index cdcbe4b72ee7153d757da33eddb393d1d67a568e..e4e22fc09919f815038795762f408ac0a760aa58 100644
--- a/front/src/components/library/Home.vue
+++ b/front/src/components/library/Home.vue
@@ -4,7 +4,7 @@
       <search :autofocus="true"></search>
     </div>
     <div class="ui vertical stripe segment">
-      <div class="ui stackable two column grid">
+      <div class="ui stackable three column grid">
         <div class="column">
           <h2 class="ui header">Latest artists</h2>
           <div :class="['ui', {'active': isLoadingArtists}, 'inline', 'loader']"></div>
@@ -18,6 +18,10 @@
           <radio-card :type="'random'"></radio-card>
           <radio-card :type="'less-listened'"></radio-card>
         </div>
+        <div class="column">
+          <h2 class="ui header">Music requests</h2>
+          <request-form></request-form>
+        </div>
       </div>
     </div>
   </div>
@@ -30,6 +34,7 @@ import backend from '@/audio/backend'
 import logger from '@/logging'
 import ArtistCard from '@/components/audio/artist/Card'
 import RadioCard from '@/components/radios/Card'
+import RequestForm from '@/components/requests/Form'
 
 const ARTISTS_URL = 'artists/'
 
@@ -38,7 +43,8 @@ export default {
   components: {
     Search,
     ArtistCard,
-    RadioCard
+    RadioCard,
+    RequestForm
   },
   data () {
     return {
diff --git a/front/src/components/library/Library.vue b/front/src/components/library/Library.vue
index 5fe192022c8e34416259118f262fb87076e98458..6cd156493f4487683df91fd9314f31fb8807de1d 100644
--- a/front/src/components/library/Library.vue
+++ b/front/src/components/library/Library.vue
@@ -5,8 +5,13 @@
       <router-link class="ui item" to="/library/artists" exact>Artists</router-link>
       <router-link class="ui item" to="/library/radios" exact>Radios</router-link>
       <div class="ui secondary right menu">
+        <router-link class="ui item" to="/library/requests/" exact>
+          Requests
+          <div class="ui teal label">{{ requestsCount }}</div>
+        </router-link>
         <router-link v-if="$store.state.auth.availablePermissions['import.launch']" class="ui item" to="/library/import/launch" exact>Import</router-link>
-        <router-link v-if="$store.state.auth.availablePermissions['import.launch']" class="ui item" to="/library/import/batches">Import batches</router-link>
+        <router-link v-if="$store.state.auth.availablePermissions['import.launch']" class="ui item" to="/library/import/batches">Import batches
+        </router-link>
       </div>
     </div>
     <router-view :key="$route.fullPath"></router-view>
@@ -14,9 +19,25 @@
 </template>
 
 <script>
-
+import axios from 'axios'
 export default {
-  name: 'library'
+  name: 'library',
+  data () {
+    return {
+      requestsCount: 0
+    }
+  },
+  created () {
+    this.fetchRequestsCount()
+  },
+  methods: {
+    fetchRequestsCount () {
+      let self = this
+      axios.get('requests/import-requests', {params: {status: 'pending'}}).then(response => {
+        self.requestsCount = response.data.count
+      })
+    }
+  }
 }
 </script>
 
diff --git a/front/src/components/library/import/ImportMixin.vue b/front/src/components/library/import/ImportMixin.vue
index 33c6193bd7c0a95afa6e59847dc6056a0ca38145..8b0757dcc9256afa7d09fc4e61eb091668ed7582 100644
--- a/front/src/components/library/import/ImportMixin.vue
+++ b/front/src/components/library/import/ImportMixin.vue
@@ -13,7 +13,8 @@ export default {
     defaultEnabled: {type: Boolean, default: true},
     backends: {type: Array},
     defaultBackendId: {type: String},
-    queryTemplate: {type: String, default: '$artist $title'}
+    queryTemplate: {type: String, default: '$artist $title'},
+    request: {type: Object, required: false}
   },
   data () {
     return {
@@ -32,6 +33,9 @@ export default {
       this.isImporting = true
       let url = 'submit/' + self.importType + '/'
       let payload = self.importData
+      if (this.request) {
+        payload.importRequest = this.request.id
+      }
       axios.post(url, payload).then((response) => {
         logger.default.info('launched import for', self.type, self.metadata.id)
         self.isImporting = false
diff --git a/front/src/components/library/import/Main.vue b/front/src/components/library/import/Main.vue
index 66ba8c66144db35f4d4c17dfcada5e4c89fcc2e4..0a1cc6df9480366a7105166045e39ffbf91249de 100644
--- a/front/src/components/library/import/Main.vue
+++ b/front/src/components/library/import/Main.vue
@@ -92,6 +92,7 @@
           <component
             ref="import"
             v-if="currentSource == 'external'"
+            :request="currentRequest"
             :metadata="metadata"
             :is="importComponent"
             :backends="backends"
@@ -113,7 +114,10 @@
         </div>
       </div>
     </div>
-    <div class="ui vertical stripe segment">
+    <div class="ui vertical stripe segment" v-if="currentRequest">
+      <h3 class="ui header">Music request</h3>
+      <p>This import will be associated with the music request below. After the import is finished, the request will be marked as fulfilled.</p>
+      <request-card :request="currentRequest" :import-action="false"></request-card>
 
     </div>
   </div>
@@ -121,6 +125,7 @@
 
 <script>
 
+import RequestCard from '@/components/requests/Card'
 import MetadataSearch from '@/components/metadata/Search'
 import ReleaseCard from '@/components/metadata/ReleaseCard'
 import ArtistCard from '@/components/metadata/ArtistCard'
@@ -128,6 +133,7 @@ import ReleaseImport from './ReleaseImport'
 import FileUpload from './FileUpload'
 import ArtistImport from './ArtistImport'
 
+import axios from 'axios'
 import router from '@/router'
 import $ from 'jquery'
 
@@ -138,19 +144,22 @@ export default {
     ReleaseCard,
     ArtistImport,
     ReleaseImport,
-    FileUpload
+    FileUpload,
+    RequestCard
   },
   props: {
     mbType: {type: String, required: false},
+    request: {type: String, required: false},
     source: {type: String, required: false},
     mbId: {type: String, required: false}
   },
   data: function () {
     return {
+      currentRequest: null,
       currentType: this.mbType || 'artist',
       currentId: this.mbId,
       currentStep: 0,
-      currentSource: '',
+      currentSource: this.source,
       metadata: {},
       isImporting: false,
       importData: {
@@ -166,6 +175,9 @@ export default {
     }
   },
   created () {
+    if (this.request) {
+      this.fetchRequest(this.request)
+    }
     if (this.currentSource) {
       this.currentStep = 1
     }
@@ -179,7 +191,8 @@ export default {
         query: {
           source: this.currentSource,
           type: this.currentType,
-          id: this.currentId
+          id: this.currentId,
+          request: this.request
         }
       })
     },
@@ -197,6 +210,12 @@ export default {
     },
     updateId (newValue) {
       this.currentId = newValue
+    },
+    fetchRequest (id) {
+      let self = this
+      axios.get(`requests/import-requests/${id}`).then((response) => {
+        self.currentRequest = response.data
+      })
     }
   },
   computed: {
diff --git a/front/src/components/requests/Card.vue b/front/src/components/requests/Card.vue
new file mode 100644
index 0000000000000000000000000000000000000000..deb9c3fe093a58cc6913805ad4fcbe9bed4da58d
--- /dev/null
+++ b/front/src/components/requests/Card.vue
@@ -0,0 +1,61 @@
+<template>
+  <div :class="['ui', {collapsed: collapsed}, 'card']">
+    <div class="content">
+      <div class="header">{{ request.artist_name }}</div>
+      <div class="description">
+        <div
+          v-if="request.albums" v-html="$options.filters.markdown(request.albums)"></div>
+        <div v-if="request.comment" class="ui comments">
+          <comment
+            :user="request.user"
+            :content="request.comment"
+            :date="request.creation_date"></comment>
+        </div>
+      </div>
+    </div>
+    <div class="extra content">
+      <span >
+        <i v-if="request.status === 'pending'" class="hourglass start icon"></i>
+        <i v-if="request.status === 'accepted'" class="hourglass half icon"></i>
+        <i v-if="request.status === 'imported'" class="check icon"></i>
+        {{ request.status | capitalize }}
+      </span>
+      <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>
+      
+    </div>
+  </div>
+</template>
+
+<script>
+import Comment from '@/components/discussion/Comment'
+
+export default {
+  props: {
+    request: {type: Object, required: true},
+    importAction: {type: Boolean, default: true}
+  },
+  components: {
+    Comment
+  },
+  data () {
+    return {
+      collapsed: true
+    }
+  },
+  methods: {
+    createImport () {
+      this.$router.push({
+        name: 'library.import.launch',
+        query: {request: this.request.id}})
+    }
+  }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+
+</style>
diff --git a/front/src/components/requests/Form.vue b/front/src/components/requests/Form.vue
new file mode 100644
index 0000000000000000000000000000000000000000..68c725ba7adfe903c3ecdf971067b1abb2d5baef
--- /dev/null
+++ b/front/src/components/requests/Form.vue
@@ -0,0 +1,115 @@
+<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>
+      <div class="required field">
+        <label>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>
+        <input v-model="currentAlbums" placeholder="The White Album, Thriller…" maxlength="2000">
+      </div>
+      <div class="field">
+        <label>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>
+    </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>
+    </div>
+    <div v-if="requests.length > 0">
+      <div class="ui divider"></div>
+      <h3 class="ui header">Pending requests</h3>
+      <div class="ui list">
+        <div v-for="request in requests" class="item">
+          <div class="content">
+            <div class="header">{{ request.artist_name }}</div>
+            <div v-if="request.albums" class="description">
+              {{ request.albums|truncate }}</div>
+            <div v-if="request.comment" class="description">
+              {{ request.comment|truncate }}</div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import $ from 'jquery'
+import axios from 'axios'
+
+import logger from '@/logging'
+
+export default {
+  props: {
+    defaultArtistName: {type: String, default: ''},
+    defaultAlbums: {type: String, default: ''},
+    defaultComment: {type: String, default: ''}
+  },
+  created () {
+    this.fetchRequests()
+  },
+  mounted () {
+    $('.ui.radio.checkbox').checkbox()
+  },
+  data () {
+    return {
+      currentArtistName: this.defaultArtistName,
+      currentAlbums: this.defaultAlbums,
+      currentComment: this.defaultComment,
+      isLoading: false,
+      over: false,
+      requests: []
+    }
+  },
+  methods: {
+    fetchRequests () {
+      let self = this
+      let url = 'requests/import-requests/'
+      axios.get(url, {}).then((response) => {
+        self.requests = response.data.results
+      })
+    },
+    submit () {
+      let self = this
+      this.isLoading = true
+      let url = 'requests/import-requests/'
+      let payload = {
+        artist_name: this.currentArtistName,
+        albums: this.currentAlbums,
+        comment: this.currentComment
+      }
+      axios.post(url, payload).then((response) => {
+        logger.default.info('Submitted request!')
+        self.isLoading = false
+        self.over = true
+        self.requests.unshift(response.data)
+      }, (response) => {
+        logger.default.error('error while submitting request')
+        self.isLoading = false
+      })
+    },
+    reset () {
+      this.over = false
+      this.currentArtistName = ''
+      this.currentAlbums = ''
+      this.currentComment = ''
+    },
+    truncate (string, length) {
+      if (string.length > length) {
+        return string.substring(0, length) + '…'
+      }
+      return string
+    }
+  }
+}
+</script>
+
+<style scoped>
+</style>
diff --git a/front/src/components/requests/RequestsList.vue b/front/src/components/requests/RequestsList.vue
new file mode 100644
index 0000000000000000000000000000000000000000..cb3e9af00bc40d0301b23dcb446576a567e7f153
--- /dev/null
+++ b/front/src/components/requests/RequestsList.vue
@@ -0,0 +1,163 @@
+<template>
+  <div>
+    <div class="ui vertical stripe segment">
+      <h2 class="ui header">Music requests</h2>
+      <div :class="['ui', {'loading': isLoading}, 'form']">
+        <div class="fields">
+          <div class="field">
+            <label>Search</label>
+            <input type="text" v-model="query" placeholder="Enter an artist name, a username..."/>
+          </div>
+          <div class="field">
+            <label>Ordering</label>
+            <select class="ui dropdown" v-model="ordering">
+              <option v-for="option in orderingOptions" :value="option[0]">
+                {{ option[1] }}
+              </option>
+            </select>
+          </div>
+          <div class="field">
+            <label>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>
+            <select class="ui dropdown" v-model="paginateBy">
+              <option :value="parseInt(12)">12</option>
+              <option :value="parseInt(25)">25</option>
+              <option :value="parseInt(50)">50</option>
+            </select>
+          </div>
+        </div>
+      </div>
+      <div class="ui hidden divider"></div>
+      <div v-if="result" class="ui stackable three column grid">
+        <div
+          v-if="result.results.length > 0"
+          v-for="request in result.results"
+          :key="request.id"
+          class="column">
+          <request-card class="fluid" :request="request"></request-card>
+        </div>
+      </div>
+      <div class="ui center aligned basic segment">
+        <pagination
+          v-if="result && result.results.length > 0"
+          @page-changed="selectPage"
+          :current="page"
+          :paginate-by="paginateBy"
+          :total="result.count"
+          ></pagination>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import axios from 'axios'
+import _ from 'lodash'
+import $ from 'jquery'
+
+import logger from '@/logging'
+
+import OrderingMixin from '@/components/mixins/Ordering'
+import PaginationMixin from '@/components/mixins/Pagination'
+import RequestCard from '@/components/requests/Card'
+import Pagination from '@/components/Pagination'
+
+const FETCH_URL = 'requests/import-requests/'
+
+export default {
+  mixins: [OrderingMixin, PaginationMixin],
+  props: {
+    defaultQuery: {type: String, required: false, default: ''}
+  },
+  components: {
+    RequestCard,
+    Pagination
+  },
+  data () {
+    let defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date')
+    return {
+      isLoading: true,
+      result: null,
+      page: parseInt(this.defaultPage),
+      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']
+      ]
+    }
+  },
+  created () {
+    this.fetchData()
+  },
+  mounted () {
+    $('.ui.dropdown').dropdown()
+  },
+  methods: {
+    updateQueryString: _.debounce(function () {
+      this.$router.replace({
+        query: {
+          query: this.query,
+          page: this.page,
+          paginateBy: this.paginateBy,
+          ordering: this.getOrderingAsString()
+        }
+      })
+    }, 500),
+    fetchData: _.debounce(function () {
+      var self = this
+      this.isLoading = true
+      let url = FETCH_URL
+      let params = {
+        page: this.page,
+        page_size: this.paginateBy,
+        search: this.query,
+        ordering: this.getOrderingAsString()
+      }
+      logger.default.debug('Fetching request...')
+      axios.get(url, {params: params}).then((response) => {
+        self.result = response.data
+        self.isLoading = false
+      })
+    }, 500),
+    selectPage: function (page) {
+      this.page = page
+    }
+  },
+  watch: {
+    page () {
+      this.updateQueryString()
+      this.fetchData()
+    },
+    paginateBy () {
+      this.updateQueryString()
+      this.fetchData()
+    },
+    ordering () {
+      this.updateQueryString()
+      this.fetchData()
+    },
+    orderingDirection () {
+      this.updateQueryString()
+      this.fetchData()
+    },
+    query () {
+      this.updateQueryString()
+      this.fetchData()
+    }
+  }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+</style>
diff --git a/front/src/filters.js b/front/src/filters.js
new file mode 100644
index 0000000000000000000000000000000000000000..22d93149bb402657e56d37d9ef68cf2959d9c767
--- /dev/null
+++ b/front/src/filters.js
@@ -0,0 +1,44 @@
+import Vue from 'vue'
+
+import moment from 'moment'
+import showdown from 'showdown'
+
+export function truncate (str, max, ellipsis) {
+  max = max || 100
+  ellipsis = ellipsis || '…'
+  if (str.length <= max) {
+    return str
+  }
+  return str.slice(0, max) + ellipsis
+}
+
+Vue.filter('truncate', truncate)
+
+export function markdown (str) {
+  const converter = new showdown.Converter()
+  return converter.makeHtml(str)
+}
+
+Vue.filter('markdown', markdown)
+
+export function ago (date) {
+  const m = moment(date)
+  return m.fromNow()
+}
+
+Vue.filter('ago', ago)
+
+export function momentFormat (date, format) {
+  format = format || 'lll'
+  return moment(date).format(format)
+}
+
+Vue.filter('moment', momentFormat)
+
+export function capitalize (str) {
+  return str.charAt(0).toUpperCase() + str.slice(1)
+}
+
+Vue.filter('capitalize', capitalize)
+
+export default {}
diff --git a/front/src/main.js b/front/src/main.js
index d1ff90c3256846b65848021e1abf0f0b3fb19bc7..2e351310a15e218d1e9438c12d7c8e8ba1b37426 100644
--- a/front/src/main.js
+++ b/front/src/main.js
@@ -13,6 +13,8 @@ import VueLazyload from 'vue-lazyload'
 import store from './store'
 import config from './config'
 import { sync } from 'vuex-router-sync'
+import filters from '@/filters' // eslint-disable-line
+import globals from '@/components/globals' // eslint-disable-line
 
 sync(store, router)
 
diff --git a/front/src/router/index.js b/front/src/router/index.js
index 971ef05cd82f034b9513716a42f35b02a5291054..ea8854bbe48e4f06ec4369a2853b3e98271df733 100644
--- a/front/src/router/index.js
+++ b/front/src/router/index.js
@@ -17,6 +17,7 @@ import LibraryRadios from '@/components/library/Radios'
 import RadioBuilder from '@/components/library/radios/Builder'
 import BatchList from '@/components/library/import/BatchList'
 import BatchDetail from '@/components/library/import/BatchDetail'
+import RequestsList from '@/components/requests/RequestsList'
 
 import Favorites from '@/components/favorites/List'
 
@@ -98,7 +99,11 @@ export default new Router({
           path: 'import/launch',
           name: 'library.import.launch',
           component: LibraryImport,
-          props: (route) => ({ mbType: route.query.type, mbId: route.query.id })
+          props: (route) => ({
+            source: route.query.source,
+            request: route.query.request,
+            mbType: route.query.type,
+            mbId: route.query.id })
         },
         {
           path: 'import/batches',
@@ -107,7 +112,21 @@ export default new Router({
           children: [
           ]
         },
-        { path: 'import/batches/:id', name: 'library.import.batches.detail', component: BatchDetail, props: true }
+        { path: 'import/batches/:id', name: 'library.import.batches.detail', component: BatchDetail, props: true },
+        {
+          path: 'requests/',
+          name: 'library.requests',
+          component: RequestsList,
+          props: (route) => ({
+            defaultOrdering: route.query.ordering,
+            defaultQuery: route.query.query,
+            defaultPaginateBy: route.query.paginateBy,
+            defaultPage: route.query.page,
+            defaultStatus: route.query.status || 'pending'
+          }),
+          children: [
+          ]
+        }
       ]
     },
     { path: '*', component: PageNotFound }
diff --git a/front/test/unit/specs/filters/filters.spec.js b/front/test/unit/specs/filters/filters.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..c2b43da44a83eb0a983d981c7891853a634fb31a
--- /dev/null
+++ b/front/test/unit/specs/filters/filters.spec.js
@@ -0,0 +1,42 @@
+import {truncate, markdown, ago, capitalize} from '@/filters'
+
+describe('filters', () => {
+  describe('truncate', () => {
+    it('leave strings as it if correct size', () => {
+      const input = 'Hello world'
+      let output = truncate(input, 100)
+      expect(output).to.equal(input)
+    })
+    it('returns shorter string with character', () => {
+      const input = 'Hello world'
+      let output = truncate(input, 5)
+      expect(output).to.equal('Hello…')
+    })
+    it('custom ellipsis', () => {
+      const input = 'Hello world'
+      let output = truncate(input, 5, ' pouet')
+      expect(output).to.equal('Hello pouet')
+    })
+  })
+  describe('markdown', () => {
+    it('renders markdown', () => {
+      const input = 'Hello world'
+      let output = markdown(input)
+      expect(output).to.equal('<p>Hello world</p>')
+    })
+  })
+  describe('ago', () => {
+    it('works', () => {
+      const input = new Date()
+      let output = ago(input)
+      expect(output).to.equal('a few seconds ago')
+    })
+  })
+  describe('capitalize', () => {
+    it('works', () => {
+      const input = 'hello world'
+      let output = capitalize(input)
+      expect(output).to.equal('Hello world')
+    })
+  })
+})