diff --git a/api/config/settings/local.py b/api/config/settings/local.py
index 9f0119cee54f51ec10377e662bdc8e8fa2bf32b2..b8df4bdb7c894fc0b0f3a85dcbfaca2676f16885 100644
--- a/api/config/settings/local.py
+++ b/api/config/settings/local.py
@@ -39,6 +39,7 @@ DEBUG_TOOLBAR_CONFIG = {
     "DISABLE_PANELS": ["debug_toolbar.panels.redirects.RedirectsPanel"],
     "SHOW_TEMPLATE_CONTEXT": True,
     "SHOW_TOOLBAR_CALLBACK": lambda request: True,
+    "JQUERY_URL": "",
 }
 
 # django-extensions
diff --git a/api/funkwhale_api/favorites/serializers.py b/api/funkwhale_api/favorites/serializers.py
index 3cafb80f021488cf0c64244a7dc1b5607d185497..16171aa34bd05eea4991969de09d46f5b76a0508 100644
--- a/api/funkwhale_api/favorites/serializers.py
+++ b/api/funkwhale_api/favorites/serializers.py
@@ -2,8 +2,8 @@
 from rest_framework import serializers
 
 from funkwhale_api.activity import serializers as activity_serializers
-from funkwhale_api.music.serializers import TrackActivitySerializer
-from funkwhale_api.users.serializers import UserActivitySerializer
+from funkwhale_api.music.serializers import TrackActivitySerializer, TrackSerializer
+from funkwhale_api.users.serializers import UserActivitySerializer, UserBasicSerializer
 
 from . import models
 
@@ -26,6 +26,15 @@ class TrackFavoriteActivitySerializer(activity_serializers.ModelSerializer):
 
 
 class UserTrackFavoriteSerializer(serializers.ModelSerializer):
+    track = TrackSerializer(read_only=True)
+    user = UserBasicSerializer(read_only=True)
+
+    class Meta:
+        model = models.TrackFavorite
+        fields = ("id", "user", "track", "creation_date")
+
+
+class UserTrackFavoriteWriteSerializer(serializers.ModelSerializer):
     class Meta:
         model = models.TrackFavorite
         fields = ("id", "track", "creation_date")
diff --git a/api/funkwhale_api/favorites/views.py b/api/funkwhale_api/favorites/views.py
index 4d1c1e756af1bf5ca01345ad0fba172c316ac152..61b5bee6ce2e132d6bc85d810497b8dfdf3250a5 100644
--- a/api/funkwhale_api/favorites/views.py
+++ b/api/funkwhale_api/favorites/views.py
@@ -1,9 +1,10 @@
 from rest_framework import mixins, status, viewsets
 from rest_framework.decorators import list_route
+from rest_framework.permissions import IsAuthenticatedOrReadOnly
 from rest_framework.response import Response
 
 from funkwhale_api.activity import record
-from funkwhale_api.common.permissions import ConditionalAuthentication
+from funkwhale_api.common import fields, permissions
 from funkwhale_api.music.models import Track
 
 from . import models, serializers
@@ -18,7 +19,17 @@ class TrackFavoriteViewSet(
 
     serializer_class = serializers.UserTrackFavoriteSerializer
     queryset = models.TrackFavorite.objects.all()
-    permission_classes = [ConditionalAuthentication]
+    permission_classes = [
+        permissions.ConditionalAuthentication,
+        permissions.OwnerPermission,
+        IsAuthenticatedOrReadOnly,
+    ]
+    owner_checks = ["write"]
+
+    def get_serializer_class(self):
+        if self.request.method.lower() in ["head", "get", "options"]:
+            return serializers.UserTrackFavoriteSerializer
+        return serializers.UserTrackFavoriteWriteSerializer
 
     def create(self, request, *args, **kwargs):
         serializer = self.get_serializer(data=request.data)
@@ -32,7 +43,10 @@ class TrackFavoriteViewSet(
         )
 
     def get_queryset(self):
-        return self.queryset.filter(user=self.request.user)
+        queryset = super().get_queryset()
+        return queryset.filter(
+            fields.privacy_level_query(self.request.user, "user__privacy_level")
+        )
 
     def perform_create(self, serializer):
         track = Track.objects.get(pk=serializer.data["track"])
diff --git a/api/funkwhale_api/history/serializers.py b/api/funkwhale_api/history/serializers.py
index e493227988a7d608910db46d2ebd01b0a95a96db..2254aee8cee370f4124c82acf59ad4c2c0d3148f 100644
--- a/api/funkwhale_api/history/serializers.py
+++ b/api/funkwhale_api/history/serializers.py
@@ -1,8 +1,8 @@
 from rest_framework import serializers
 
 from funkwhale_api.activity import serializers as activity_serializers
-from funkwhale_api.music.serializers import TrackActivitySerializer
-from funkwhale_api.users.serializers import UserActivitySerializer
+from funkwhale_api.music.serializers import TrackActivitySerializer, TrackSerializer
+from funkwhale_api.users.serializers import UserActivitySerializer, UserBasicSerializer
 
 from . import models
 
@@ -25,6 +25,20 @@ class ListeningActivitySerializer(activity_serializers.ModelSerializer):
 
 
 class ListeningSerializer(serializers.ModelSerializer):
+    track = TrackSerializer(read_only=True)
+    user = UserBasicSerializer(read_only=True)
+
+    class Meta:
+        model = models.Listening
+        fields = ("id", "user", "track", "creation_date")
+
+    def create(self, validated_data):
+        validated_data["user"] = self.context["user"]
+
+        return super().create(validated_data)
+
+
+class ListeningWriteSerializer(serializers.ModelSerializer):
     class Meta:
         model = models.Listening
         fields = ("id", "user", "track", "creation_date")
diff --git a/api/funkwhale_api/history/views.py b/api/funkwhale_api/history/views.py
index e104a2aa3dc44f3c538bc747df63a6e5d630f10d..6c7ef39914bf9fdbdda9a896d59347ac27eb3666 100644
--- a/api/funkwhale_api/history/views.py
+++ b/api/funkwhale_api/history/views.py
@@ -1,17 +1,36 @@
-from rest_framework import mixins, permissions, viewsets
+from rest_framework import mixins, viewsets
+from rest_framework.permissions import IsAuthenticatedOrReadOnly
 
 from funkwhale_api.activity import record
+from funkwhale_api.common import fields, permissions
 
 from . import models, serializers
 
 
 class ListeningViewSet(
-    mixins.CreateModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
+    mixins.CreateModelMixin,
+    mixins.ListModelMixin,
+    mixins.RetrieveModelMixin,
+    viewsets.GenericViewSet,
 ):
 
     serializer_class = serializers.ListeningSerializer
-    queryset = models.Listening.objects.all()
-    permission_classes = [permissions.IsAuthenticated]
+    queryset = (
+        models.Listening.objects.all()
+        .select_related("track__artist", "track__album__artist", "user")
+        .prefetch_related("track__files")
+    )
+    permission_classes = [
+        permissions.ConditionalAuthentication,
+        permissions.OwnerPermission,
+        IsAuthenticatedOrReadOnly,
+    ]
+    owner_checks = ["write"]
+
+    def get_serializer_class(self):
+        if self.request.method.lower() in ["head", "get", "options"]:
+            return serializers.ListeningSerializer
+        return serializers.ListeningWriteSerializer
 
     def perform_create(self, serializer):
         r = super().perform_create(serializer)
@@ -20,7 +39,9 @@ class ListeningViewSet(
 
     def get_queryset(self):
         queryset = super().get_queryset()
-        return queryset.filter(user=self.request.user)
+        return queryset.filter(
+            fields.privacy_level_query(self.request.user, "user__privacy_level")
+        )
 
     def get_serializer_context(self):
         context = super().get_serializer_context()
diff --git a/api/funkwhale_api/playlists/filters.py b/api/funkwhale_api/playlists/filters.py
index ae9f0226f265c41e1db2c185aa9c609fed635478..144b0f049b23e78002fc3f79a87d395c87b141d1 100644
--- a/api/funkwhale_api/playlists/filters.py
+++ b/api/funkwhale_api/playlists/filters.py
@@ -1,3 +1,4 @@
+from django.db.models import Count
 from django_filters import rest_framework as filters
 
 from funkwhale_api.music import utils
@@ -7,10 +8,23 @@ from . import models
 
 class PlaylistFilter(filters.FilterSet):
     q = filters.CharFilter(name="_", method="filter_q")
+    listenable = filters.BooleanFilter(name="_", method="filter_listenable")
 
     class Meta:
         model = models.Playlist
-        fields = {"user": ["exact"], "name": ["exact", "icontains"], "q": "exact"}
+        fields = {
+            "user": ["exact"],
+            "name": ["exact", "icontains"],
+            "q": "exact",
+            "listenable": "exact",
+        }
+
+    def filter_listenable(self, queryset, name, value):
+        queryset = queryset.annotate(plts_count=Count("playlist_tracks"))
+        if value:
+            return queryset.filter(plts_count__gt=0)
+        else:
+            return queryset.filter(plts_count=0)
 
     def filter_q(self, queryset, name, value):
         query = utils.get_query(value, ["name", "user__username"])
diff --git a/api/funkwhale_api/playlists/models.py b/api/funkwhale_api/playlists/models.py
index e9df4624de9b32d9ef45a1599f545f3cccf72e98..d2504d84846509198f20054844a844b9c0670917 100644
--- a/api/funkwhale_api/playlists/models.py
+++ b/api/funkwhale_api/playlists/models.py
@@ -3,12 +3,41 @@ from django.utils import timezone
 from rest_framework import exceptions
 
 from funkwhale_api.common import fields, preferences
+from funkwhale_api.music import models as music_models
 
 
 class PlaylistQuerySet(models.QuerySet):
     def with_tracks_count(self):
         return self.annotate(_tracks_count=models.Count("playlist_tracks"))
 
+    def with_duration(self):
+        return self.annotate(
+            duration=models.Sum("playlist_tracks__track__files__duration")
+        )
+
+    def with_covers(self):
+        album_prefetch = models.Prefetch(
+            "album", queryset=music_models.Album.objects.only("cover")
+        )
+        track_prefetch = models.Prefetch(
+            "track",
+            queryset=music_models.Track.objects.prefetch_related(album_prefetch).only(
+                "id", "album_id"
+            ),
+        )
+
+        plt_prefetch = models.Prefetch(
+            "playlist_tracks",
+            queryset=PlaylistTrack.objects.all()
+            .exclude(track__album__cover=None)
+            .exclude(track__album__cover="")
+            .order_by("index")
+            .only("id", "playlist_id", "track_id")
+            .prefetch_related(track_prefetch),
+            to_attr="plts_for_cover",
+        )
+        return self.prefetch_related(plt_prefetch)
+
 
 class Playlist(models.Model):
     name = models.CharField(max_length=50)
diff --git a/api/funkwhale_api/playlists/serializers.py b/api/funkwhale_api/playlists/serializers.py
index 17cc06b10bdab8f17fb4f3cc246a5e5e26d12fab..71b8f315ade121cbf0d4ce270b5263973eab11b0 100644
--- a/api/funkwhale_api/playlists/serializers.py
+++ b/api/funkwhale_api/playlists/serializers.py
@@ -65,6 +65,8 @@ class PlaylistTrackWriteSerializer(serializers.ModelSerializer):
 
 class PlaylistSerializer(serializers.ModelSerializer):
     tracks_count = serializers.SerializerMethodField(read_only=True)
+    duration = serializers.SerializerMethodField(read_only=True)
+    album_covers = serializers.SerializerMethodField(read_only=True)
     user = UserBasicSerializer(read_only=True)
 
     class Meta:
@@ -72,11 +74,13 @@ class PlaylistSerializer(serializers.ModelSerializer):
         fields = (
             "id",
             "name",
-            "tracks_count",
             "user",
             "modification_date",
             "creation_date",
             "privacy_level",
+            "tracks_count",
+            "album_covers",
+            "duration",
         )
         read_only_fields = ["id", "modification_date", "creation_date"]
 
@@ -87,6 +91,36 @@ class PlaylistSerializer(serializers.ModelSerializer):
             # no annotation?
             return obj.playlist_tracks.count()
 
+    def get_duration(self, obj):
+        try:
+            return obj.duration
+        except AttributeError:
+            # no annotation?
+            return 0
+
+    def get_album_covers(self, obj):
+        try:
+            plts = obj.plts_for_cover
+        except AttributeError:
+            return []
+
+        covers = []
+        max_covers = 5
+        for plt in plts:
+            url = plt.track.album.cover.url
+            if url in covers:
+                continue
+            covers.append(url)
+            if len(covers) >= max_covers:
+                break
+
+        full_urls = []
+        for url in covers:
+            if "request" in self.context:
+                url = self.context["request"].build_absolute_uri(url)
+            full_urls.append(url)
+        return full_urls
+
 
 class PlaylistAddManySerializer(serializers.Serializer):
     tracks = serializers.PrimaryKeyRelatedField(
diff --git a/api/funkwhale_api/playlists/views.py b/api/funkwhale_api/playlists/views.py
index 21e35f50a8c711fa2a7cdee401d8eba7b9986023..8db076a8608fa388d8a8fedda646836c2df20ef5 100644
--- a/api/funkwhale_api/playlists/views.py
+++ b/api/funkwhale_api/playlists/views.py
@@ -24,6 +24,8 @@ class PlaylistViewSet(
         models.Playlist.objects.all()
         .select_related("user")
         .annotate(tracks_count=Count("playlist_tracks"))
+        .with_covers()
+        .with_duration()
     )
     permission_classes = [
         permissions.ConditionalAuthentication,
diff --git a/api/funkwhale_api/users/serializers.py b/api/funkwhale_api/users/serializers.py
index fd007e234747ec0eaa4591231c0f97b0b2863732..a13a44c81a8fdeabb6ba9b4287d7ae8a84bb1449 100644
--- a/api/funkwhale_api/users/serializers.py
+++ b/api/funkwhale_api/users/serializers.py
@@ -45,12 +45,6 @@ class UserActivitySerializer(activity_serializers.ModelSerializer):
         return "Person"
 
 
-class UserBasicSerializer(serializers.ModelSerializer):
-    class Meta:
-        model = models.User
-        fields = ["id", "username", "name", "date_joined"]
-
-
 avatar_field = VersatileImageFieldSerializer(
     allow_null=True,
     sizes=[
@@ -62,6 +56,14 @@ avatar_field = VersatileImageFieldSerializer(
 )
 
 
+class UserBasicSerializer(serializers.ModelSerializer):
+    avatar = avatar_field
+
+    class Meta:
+        model = models.User
+        fields = ["id", "username", "name", "date_joined", "avatar"]
+
+
 class UserWriteSerializer(serializers.ModelSerializer):
     avatar = avatar_field
 
diff --git a/api/tests/favorites/test_favorites.py b/api/tests/favorites/test_favorites.py
index cd75b0d26e3bf77a866eea745105a41642bcf828..6ef323db50b3eeb4edaaf5c7d97b49a5c3b3ad19 100644
--- a/api/tests/favorites/test_favorites.py
+++ b/api/tests/favorites/test_favorites.py
@@ -4,6 +4,8 @@ import pytest
 from django.urls import reverse
 
 from funkwhale_api.favorites.models import TrackFavorite
+from funkwhale_api.music import serializers as music_serializers
+from funkwhale_api.users import serializers as users_serializers
 
 
 def test_user_can_add_favorite(factories):
@@ -15,21 +17,25 @@ def test_user_can_add_favorite(factories):
     assert f.user == user
 
 
-def test_user_can_get_his_favorites(factories, logged_in_client, client):
+def test_user_can_get_his_favorites(api_request, factories, logged_in_client, client):
+    r = api_request.get("/")
     favorite = factories["favorites.TrackFavorite"](user=logged_in_client.user)
     url = reverse("api:v1:favorites:tracks-list")
     response = logged_in_client.get(url)
-
     expected = [
         {
-            "track": favorite.track.pk,
+            "user": users_serializers.UserBasicSerializer(
+                favorite.user, context={"request": r}
+            ).data,
+            "track": music_serializers.TrackSerializer(
+                favorite.track, context={"request": r}
+            ).data,
             "id": favorite.id,
             "creation_date": favorite.creation_date.isoformat().replace("+00:00", "Z"),
         }
     ]
-    parsed_json = json.loads(response.content.decode("utf-8"))
-
-    assert expected == parsed_json["results"]
+    assert response.status_code == 200
+    assert response.data["results"] == expected
 
 
 def test_user_can_add_favorite_via_api(factories, logged_in_client, activity_muted):
diff --git a/api/tests/favorites/test_views.py b/api/tests/favorites/test_views.py
new file mode 100644
index 0000000000000000000000000000000000000000..7c3aed402a29125bfccfb773a0508fd65a45e070
--- /dev/null
+++ b/api/tests/favorites/test_views.py
@@ -0,0 +1,13 @@
+import pytest
+
+from django.urls import reverse
+
+
+@pytest.mark.parametrize("level", ["instance", "me", "followers"])
+def test_privacy_filter(preferences, level, factories, api_client):
+    preferences["common__api_authentication_required"] = False
+    factories["favorites.TrackFavorite"](user__privacy_level=level)
+    url = reverse("api:v1:favorites:tracks-list")
+    response = api_client.get(url)
+    assert response.status_code == 200
+    assert response.data["count"] == 0
diff --git a/api/tests/history/test_views.py b/api/tests/history/test_views.py
new file mode 100644
index 0000000000000000000000000000000000000000..8ec9277103825fcc863478e05ad44bc8ae344f23
--- /dev/null
+++ b/api/tests/history/test_views.py
@@ -0,0 +1,13 @@
+import pytest
+
+from django.urls import reverse
+
+
+@pytest.mark.parametrize("level", ["instance", "me", "followers"])
+def test_privacy_filter(preferences, level, factories, api_client):
+    preferences["common__api_authentication_required"] = False
+    factories["history.Listening"](user__privacy_level=level)
+    url = reverse("api:v1:history:listenings-list")
+    response = api_client.get(url)
+    assert response.status_code == 200
+    assert response.data["count"] == 0
diff --git a/api/tests/playlists/test_serializers.py b/api/tests/playlists/test_serializers.py
index 677288070082872453325d2bf7ad8a8fd5072cca..42569f7a3283a365103b1aa315a6d318db376c2f 100644
--- a/api/tests/playlists/test_serializers.py
+++ b/api/tests/playlists/test_serializers.py
@@ -63,3 +63,40 @@ def test_update_insert_is_called_when_index_is_provided(factories, mocker):
     insert.assert_called_once_with(playlist, plt, 0)
     assert plt.index == 0
     assert first.index == 1
+
+
+def test_playlist_serializer_include_covers(factories, api_request):
+    playlist = factories["playlists.Playlist"]()
+    t1 = factories["music.Track"]()
+    t2 = factories["music.Track"]()
+    t3 = factories["music.Track"](album__cover=None)
+    t4 = factories["music.Track"]()
+    t5 = factories["music.Track"]()
+    t6 = factories["music.Track"]()
+    t7 = factories["music.Track"]()
+
+    playlist.insert_many([t1, t2, t3, t4, t5, t6, t7])
+    request = api_request.get("/")
+    qs = playlist.__class__.objects.with_covers().with_tracks_count()
+
+    expected = [
+        request.build_absolute_uri(t1.album.cover.url),
+        request.build_absolute_uri(t2.album.cover.url),
+        request.build_absolute_uri(t4.album.cover.url),
+        request.build_absolute_uri(t5.album.cover.url),
+        request.build_absolute_uri(t6.album.cover.url),
+    ]
+
+    serializer = serializers.PlaylistSerializer(qs.get(), context={"request": request})
+    assert serializer.data["album_covers"] == expected
+
+
+def test_playlist_serializer_include_duration(factories, api_request):
+    playlist = factories["playlists.Playlist"]()
+    tf1 = factories["music.TrackFile"](duration=15)
+    tf2 = factories["music.TrackFile"](duration=30)
+    playlist.insert_many([tf1.track, tf2.track])
+    qs = playlist.__class__.objects.with_duration().with_tracks_count()
+
+    serializer = serializers.PlaylistSerializer(qs.get())
+    assert serializer.data["duration"] == 45
diff --git a/front/src/App.vue b/front/src/App.vue
index 11ef9f13df004d45073234850ef4d49e40b9c823..b53b36f1614dedde3fee34fbbfc664d2b69a387b 100644
--- a/front/src/App.vue
+++ b/front/src/App.vue
@@ -32,6 +32,9 @@
                 <router-link class="item" to="/about">
                   <translate>About this instance</translate>
                 </router-link>
+                <router-link class="item" :to="{name: 'library.request'}">
+                  <translate>Request music</translate>
+                </router-link>
                 <a href="https://funkwhale.audio" class="item" target="_blank"><translate>Official website</translate></a>
                 <a href="https://docs.funkwhale.audio" class="item" target="_blank"><translate>Documentation</translate></a>
                 <a href="https://code.eliotberriot.com/funkwhale/funkwhale" class="item" target="_blank">
diff --git a/front/src/components/About.vue b/front/src/components/About.vue
index c9e1e23c84825a8731f90947f4b09b800f6fdf41..438fed67db518c4b99a29b8cf7b3a870d8e5c412 100644
--- a/front/src/components/About.vue
+++ b/front/src/components/About.vue
@@ -3,10 +3,10 @@
     <div class="ui vertical center aligned stripe segment">
       <div class="ui text container">
         <h1 class="ui huge header">
-            <template v-if="instance.name.value" :template-params="{instance: instance.name}">
+            <translate v-if="instance.name.value" :translate-params="{instance: instance.name.value}">
              About %{ instance }
-            </template>
-            <template v-else="instance.name.value"><translate>About this instance</translate></template>
+            </translate>
+            <translate v-else>About this instance</translate>
         </h1>
         <stats></stats>
       </div>
diff --git a/front/src/components/Sidebar.vue b/front/src/components/Sidebar.vue
index 938a48070078c42364c96150292c9467784265a3..6f744d74f3be33315a6d1dd04fa40f5f41d6116d 100644
--- a/front/src/components/Sidebar.vue
+++ b/front/src/components/Sidebar.vue
@@ -2,7 +2,7 @@
 <div :class="['ui', 'vertical', 'left', 'visible', 'wide', {'collapsed': isCollapsed}, 'sidebar',]">
   <div class="ui inverted segment header-wrapper">
     <search-bar @search="isCollapsed = false">
-      <router-link :title="'Funkwhale'" :to="{name: 'index'}">
+      <router-link :title="'Funkwhale'" :to="{name: logoUrl}">
         <i class="logo bordered inverted orange big icon">
           <logo class="logo"></logo>
         </i>
@@ -39,7 +39,7 @@
               <translate :translate-params="{username: $store.state.auth.username}">
                 Logged in as %{ username }
               </translate>
-              <img class="ui avatar right floated circular mini image" v-if="$store.state.auth.profile.avatar.square_crop" :src="$store.getters['instance/absoluteUrl']($store.state.auth.profile.avatar.square_crop)" />
+              <img class="ui right floated circular tiny avatar image" v-if="$store.state.auth.profile.avatar.square_crop" :src="$store.getters['instance/absoluteUrl']($store.state.auth.profile.avatar.square_crop)" />
             </router-link>
             <router-link class="item" v-if="$store.state.auth.authenticated" :to="{name: 'logout'}"><i class="sign out icon"></i><translate>Logout</translate></router-link>
             <router-link class="item" v-else :to="{name: 'login'}"><i class="sign in icon"></i><translate>Login</translate></router-link>
@@ -237,6 +237,13 @@ export default {
       set (value) {
         this.tracksChangeBuffer = value
       }
+    },
+    logoUrl () {
+      if (this.$store.state.auth.authenticated) {
+        return 'library.index'
+      } else {
+        return 'index'
+      }
     }
   },
   methods: {
@@ -433,8 +440,9 @@ $sidebar-color: #3d3e3f;
     }
   }
 }
-.avatar {
+.ui.tiny.avatar.image {
   position: relative;
   top: -0.5em;
+  width: 3em;
 }
 </style>
diff --git a/front/src/components/audio/PlayButton.vue b/front/src/components/audio/PlayButton.vue
index 6c5ebbc2d6181b6bb19c23ecd15cdcccf3ca705e..ad85e72ce82191d88edf18ccd3eb3fbb462bfb63 100644
--- a/front/src/components/audio/PlayButton.vue
+++ b/front/src/components/audio/PlayButton.vue
@@ -1,22 +1,23 @@
 <template>
-  <div :title="title" :class="['ui', {'tiny': discrete}, 'buttons']">
+  <span :title="title" :class="['ui', {'tiny': discrete}, {'buttons': !dropdownOnly && !iconOnly}]">
     <button
+      v-if="!dropdownOnly"
       :title="labels.addToQueue"
       @click="addNext(true)"
       :disabled="!playable"
-      :class="['ui', {loading: isLoading}, {'mini': discrete}, {disabled: !playable}, 'button']">
-      <i class="ui play icon"></i>
-      <template v-if="!discrete"><slot><translate>Play</translate></slot></template>
+      :class="buttonClasses.concat(['ui', {loading: isLoading}, {'mini': discrete}, {disabled: !playable}])">
+      <i :class="[playIconClass, 'icon']"></i>
+      <template v-if="!discrete && !iconOnly"><slot><translate>Play</translate></slot></template>
     </button>
-    <div v-if="!discrete" :class="['ui', {disabled: !playable}, 'floating', 'dropdown', 'icon', 'button']">
-      <i class="dropdown icon"></i>
+    <div v-if="!discrete && !iconOnly" :class="['ui', {disabled: !playable}, 'floating', 'dropdown', {'icon': !dropdownOnly}, {'button': !dropdownOnly}]">
+      <i :class="dropdownIconClasses.concat(['icon'])"></i>
       <div class="menu">
         <div class="item" :disabled="!playable" @click="add"><i class="plus icon"></i><translate>Add to queue</translate></div>
         <div class="item" :disabled="!playable" @click="addNext()"><i class="step forward icon"></i><translate>Play next</translate></div>
         <div class="item" :disabled="!playable" @click="addNext(true)"><i class="arrow down icon"></i><translate>Play now</translate></div>
       </div>
     </div>
-  </div>
+  </span>
 </template>
 
 <script>
@@ -28,8 +29,13 @@ export default {
     // we can either have a single or multiple tracks to play when clicked
     tracks: {type: Array, required: false},
     track: {type: Object, required: false},
+    dropdownIconClasses: {type: Array, required: false, default: () => { return ['dropdown'] }},
+    playIconClass: {type: String, required: false, default: 'play icon'},
+    buttonClasses: {type: Array, required: false, default: () => { return ['button'] }},
     playlist: {type: Object, required: false},
     discrete: {type: Boolean, default: false},
+    dropdownOnly: {type: Boolean, default: false},
+    iconOnly: {type: Boolean, default: false},
     artist: {type: Number, required: false},
     album: {type: Number, required: false}
   },
diff --git a/front/src/components/audio/album/Widget.vue b/front/src/components/audio/album/Widget.vue
new file mode 100644
index 0000000000000000000000000000000000000000..37cddd50015c78d03199dcd963e3f2568e119fea
--- /dev/null
+++ b/front/src/components/audio/album/Widget.vue
@@ -0,0 +1,134 @@
+<template>
+  <div>
+    <h3 class="ui header">
+      <slot name="title"></slot>
+    </h3>
+    <i @click="fetchData(previousPage)" :disabled="!previousPage" :class="['ui', {disabled: !previousPage}, 'circular', 'large', 'angle left', 'icon']">
+    </i>
+    <i @click="fetchData(nextPage)" :disabled="!nextPage" :class="['ui', {disabled: !nextPage}, 'circular', 'large', 'angle right', 'icon']">
+    </i>
+    <div class="ui hidden divider"></div>
+    <div class="ui five cards">
+      <div v-if="isLoading" class="ui inverted active dimmer">
+        <div class="ui loader"></div>
+      </div>
+      <div class="card" v-for="album in albums" :key="album.id">
+        <div :class="['ui', 'image', 'with-overlay', {'default-cover': !album.cover}]" :style="getImageStyle(album)">
+          <play-button class="play-overlay" :icon-only="true" :button-classes="['ui', 'circular', 'large', 'orange', 'icon', 'button']" :album="album.id"></play-button>
+        </div>
+        <div class="content">
+          <router-link :title="album.title" :to="{name: 'library.albums.detail', params: {id: album.id}}">
+            {{ album.title|truncate(25) }}
+          </router-link>
+          <div class="description">
+            <span>
+              <router-link :title="album.artist.name" class="discrete link" :to="{name: 'library.artists.detail', params: {id: album.artist.id}}">
+                {{ album.artist.name|truncate(23) }}
+              </router-link>
+            </span>
+          </div>
+        </div>
+        <div class="extra content">
+          <human-date class="left floated" :date="album.creation_date"></human-date>
+          <play-button class="right floated basic icon" :dropdown-only="true" :dropdown-icon-classes="['ellipsis', 'horizontal', 'large', 'grey']" :album="album.id"></play-button>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import _ from 'lodash'
+import axios from 'axios'
+import PlayButton from '@/components/audio/PlayButton'
+
+export default {
+  props: {
+    filters: {type: Object, required: true}
+  },
+  components: {
+    PlayButton
+  },
+  data () {
+    return {
+      albums: [],
+      limit: 12,
+      isLoading: false,
+      errors: null,
+      previousPage: null,
+      nextPage: null
+    }
+  },
+  created () {
+    this.fetchData('albums/')
+  },
+  methods: {
+    fetchData (url) {
+      if (!url) {
+        return
+      }
+      this.isLoading = true
+      let self = this
+      let params = _.clone(this.filters)
+      params.page_size = this.limit
+      params.offset = this.offset
+      axios.get(url, {params: params}).then((response) => {
+        self.previousPage = response.data.previous
+        self.nextPage = response.data.next
+        self.isLoading = false
+        self.albums = response.data.results
+      }, error => {
+        self.isLoading = false
+        self.errors = error.backendErrors
+      })
+    },
+    updateOffset (increment) {
+      if (increment) {
+        this.offset += this.limit
+      } else {
+        this.offset = Math.max(this.offset - this.limit, 0)
+      }
+    },
+    getImageStyle (album) {
+      let url = '../../../assets/audio/default-cover.png'
+
+      if (album.cover) {
+        url = this.$store.getters['instance/absoluteUrl'](album.cover)
+      } else {
+        return {}
+      }
+      return {
+        'background-image': `url("${url}")`
+      }
+    }
+  },
+  watch: {
+    offset () {
+      this.fetchData()
+    }
+  }
+}
+</script>
+<style scoped lang="scss">
+@import '../../../style/vendor/media';
+
+.default-cover {
+  background-image: url('../../../assets/audio/default-cover.png') !important;
+}
+
+.ui.cards {
+  justify-content: center;
+}
+.ui.cards > .card {
+  width: 15em;
+}
+.with-overlay {
+  background-size: cover !important;
+  background-position: center !important;
+  height: 15em;
+  width: 15em;
+  display: flex !important;
+  justify-content: center !important;
+  align-items: center !important;
+}
+</style>
diff --git a/front/src/components/audio/track/Row.vue b/front/src/components/audio/track/Row.vue
index 5870ac799cf2808baf03cf029a8ff166ed767ac5..ef3660ee2dc34e485fd604c09cfd09c281b070cc 100644
--- a/front/src/components/audio/track/Row.vue
+++ b/front/src/components/audio/track/Row.vue
@@ -5,7 +5,7 @@
     </td>
     <td>
       <img class="ui mini image" v-if="track.album.cover" v-lazy="$store.getters['instance/absoluteUrl'](track.album.cover)">
-      <img class="ui mini image" v-else src="../../..//assets/audio/default-cover.png">
+      <img class="ui mini image" v-else src="../../../assets/audio/default-cover.png">
     </td>
     <td colspan="6">
       <router-link class="track" :to="{name: 'library.tracks.detail', params: {id: track.id }}">
diff --git a/front/src/components/audio/track/Widget.vue b/front/src/components/audio/track/Widget.vue
new file mode 100644
index 0000000000000000000000000000000000000000..7c727b4027aa48823ba9ad9ddb838ae6a78089cf
--- /dev/null
+++ b/front/src/components/audio/track/Widget.vue
@@ -0,0 +1,124 @@
+<template>
+  <div>
+    <h3 class="ui header">
+      <slot name="title"></slot>
+    </h3>
+    <i @click="fetchData(previousPage)" :disabled="!previousPage" :class="['ui', {disabled: !previousPage}, 'circular', 'large', 'angle up', 'icon']">
+    </i>
+    <i @click="fetchData(nextPage)" :disabled="!nextPage" :class="['ui', {disabled: !nextPage}, 'circular', 'large', 'angle down', 'icon']">
+    </i>
+    <div class="ui divided unstackable items">
+      <div v-if="isLoading" class="ui inverted active dimmer">
+        <div class="ui loader"></div>
+      </div>
+      <div class="item" v-for="object in objects" :key="object.id">
+        <div class="ui tiny image">
+          <img v-if="object.track.album.cover" v-lazy="$store.getters['instance/absoluteUrl'](object.track.album.cover)">
+          <img v-else src="../../../assets/audio/default-cover.png">
+          <play-button class="play-overlay" :icon-only="true" :button-classes="['ui', 'circular', 'tiny', 'orange', 'icon', 'button']" :track="object.track"></play-button>
+        </div>
+        <div class="middle aligned content">
+          <div class="ui unstackable grid">
+            <div class="thirteen wide stretched column">
+              <div>
+                <router-link :title="object.track.title" :to="{name: 'library.tracks.detail', params: {id: object.track.id}}">
+                  {{ object.track.title|truncate(25) }}
+                </router-link>
+              </div>
+              <div class="meta">
+                <span>
+                  <router-link :title="object.track.artist.name" class="discrete link" :to="{name: 'library.artists.detail', params: {id: object.track.artist.id}}">
+                    {{ object.track.artist.name|truncate(25) }}
+                  </router-link>
+                </span>
+              </div>
+              <div class="extra">
+                <span class="left floated">@{{ object.user.username }}</span>
+                <span class="right floated"><human-date :date="object.creation_date" /></span>
+              </div>
+            </div>
+            <div class="one wide stretched column">
+              <play-button class="basic icon" :dropdown-only="true" :dropdown-icon-classes="['ellipsis', 'vertical', 'large', 'grey']" :track="object.track"></play-button>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import _ from 'lodash'
+import axios from 'axios'
+import PlayButton from '@/components/audio/PlayButton'
+
+export default {
+  props: {
+    filters: {type: Object, required: true},
+    url: {type: String, required: true}
+  },
+  components: {
+    PlayButton
+  },
+  data () {
+    return {
+      objects: [],
+      limit: 5,
+      isLoading: false,
+      errors: null,
+      previousPage: null,
+      nextPage: null
+    }
+  },
+  created () {
+    this.fetchData(this.url)
+  },
+  methods: {
+    fetchData (url) {
+      if (!url) {
+        return
+      }
+      this.isLoading = true
+      let self = this
+      let params = _.clone(this.filters)
+      params.page_size = this.limit
+      params.offset = this.offset
+      axios.get(url, {params: params}).then((response) => {
+        self.previousPage = response.data.previous
+        self.nextPage = response.data.next
+        self.isLoading = false
+        self.objects = response.data.results
+      }, error => {
+        self.isLoading = false
+        self.errors = error.backendErrors
+      })
+    },
+    updateOffset (increment) {
+      if (increment) {
+        this.offset += this.limit
+      } else {
+        this.offset = Math.max(this.offset - this.limit, 0)
+      }
+    }
+  },
+  watch: {
+    offset () {
+      this.fetchData()
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+@import '../../../style/vendor/media';
+
+.play-overlay {
+  position: absolute;
+  top: 4em;
+  left: 4em;
+  @include media(">tablet") {
+    top: 2.5em;
+    left: 2.5em;
+  }
+}
+</style>
diff --git a/front/src/components/auth/Signup.vue b/front/src/components/auth/Signup.vue
index ae3c47e5cb2a089630b04c2fd0df505ab69916b1..8d2e80470b63d1a651e383bcf546825a1f01cacb 100644
--- a/front/src/components/auth/Signup.vue
+++ b/front/src/components/auth/Signup.vue
@@ -65,7 +65,7 @@ import PasswordInput from '@/components/forms/PasswordInput'
 
 export default {
   props: {
-    invitation: {type: String, required: false, default: null},
+    defaultInvitation: {type: String, required: false, default: null},
     next: {type: String, default: '/'}
   },
   components: {
@@ -78,7 +78,8 @@ export default {
       password: '',
       isLoadingInstanceSetting: true,
       errors: [],
-      isLoading: false
+      isLoading: false,
+      invitation: this.defaultInvitation
     }
   },
   created () {
diff --git a/front/src/components/common/Duration.vue b/front/src/components/common/Duration.vue
new file mode 100644
index 0000000000000000000000000000000000000000..85b070fcd4f18a121e6ce76bdc52bf6fd9762e51
--- /dev/null
+++ b/front/src/components/common/Duration.vue
@@ -0,0 +1,22 @@
+<template>
+  <span>
+    <translate
+      v-if="durationData.hours > 0"
+      :translate-params="{minutes: durationData.minutes, hours: durationData.hours}">%{ hours } h %{ minutes } min</translate>
+    <translate
+      v-else
+      :translate-params="{minutes: durationData.minutes}">%{ minutes } min</translate>
+  </span>
+</template>
+<script>
+import {secondsToObject} from '@/filters'
+
+export default {
+  props: ['seconds'],
+  computed: {
+    durationData () {
+      return secondsToObject(this.seconds)
+    }
+  }
+}
+</script>
diff --git a/front/src/components/common/UserLink.vue b/front/src/components/common/UserLink.vue
new file mode 100644
index 0000000000000000000000000000000000000000..0ae4d4ec8f2f544506bfd0adab65457ecfd5d6a1
--- /dev/null
+++ b/front/src/components/common/UserLink.vue
@@ -0,0 +1,34 @@
+<template>
+  <span>
+    <img
+      class="ui tiny circular avatar"
+      v-if="user.avatar && user.avatar.small_square_crop"
+      :src="$store.getters['instance/absoluteUrl'](user.avatar.small_square_crop)" />
+    <span v-else :style="defaultAvatarStyle" class="ui circular label">{{ user.username[0]}}</span>
+    &nbsp;@{{ user.username }}
+  </span>
+</template>
+
+<script>
+import {hashCode, intToRGB} from '@/utils/color'
+
+export default {
+  props: ['user'],
+  computed: {
+    userColor () {
+      return intToRGB(hashCode(this.user.username + String(this.user.id)))
+    },
+    defaultAvatarStyle () {
+      return {
+        'background-color': `#${this.userColor}`
+      }
+    }
+  }
+}
+</script>
+<style scoped>
+.tiny.circular.avatar {
+  width: 1.7em;
+  height: 1.7em;
+}
+</style>
diff --git a/front/src/components/globals.js b/front/src/components/globals.js
index 4ad09f70425a987fd99e86f6e4277cd07d797605..6865ac1bc55f74c20914169d48560b96759ec852 100644
--- a/front/src/components/globals.js
+++ b/front/src/components/globals.js
@@ -8,6 +8,14 @@ import Username from '@/components/common/Username'
 
 Vue.component('username', Username)
 
+import UserLink from '@/components/common/UserLink'
+
+Vue.component('user-link', UserLink)
+
+import Duration from '@/components/common/Duration'
+
+Vue.component('duration', Duration)
+
 import DangerousButton from '@/components/common/DangerousButton'
 
 Vue.component('dangerous-button', DangerousButton)
diff --git a/front/src/components/library/Home.vue b/front/src/components/library/Home.vue
index ce6627e18bc66d50950a53b007c84d442c13d9ff..0bb16e1dd9e01d316169c45151a45da5a1114cb6 100644
--- a/front/src/components/library/Home.vue
+++ b/front/src/components/library/Home.vue
@@ -1,32 +1,29 @@
 <template>
   <div v-title="labels.title">
-    <div class="ui vertical stripe segment">
-      <search :autofocus="true"></search>
-    </div>
     <div class="ui vertical stripe segment">
       <div class="ui stackable three column grid">
         <div class="column">
-          <h2 class="ui header">
-            <translate>Latest artists</translate>
-          </h2>
-          <div :class="['ui', {'active': isLoadingArtists}, 'inline', 'loader']"></div>
-          <div v-if="artists.length > 0" v-for="artist in artists.slice(0, 3)" :key="artist.id" class="ui cards">
-            <artist-card :artist="artist"></artist-card>
-          </div>
+          <track-widget :url="'history/listenings/'" :filters="{scope: 'user', ordering: '-creation_date'}">
+            <template slot="title"><translate>Recently listened</translate></template>
+          </track-widget>
         </div>
         <div class="column">
-          <h2 class="ui header">
-            <translate>Radios</translate>
-          </h2>
-          <radio-card :type="'favorites'"></radio-card>
-          <radio-card :type="'random'"></radio-card>
-          <radio-card :type="'less-listened'"></radio-card>
+          <track-widget :url="'favorites/tracks/'" :filters="{scope: 'user', ordering: '-creation_date'}">
+            <template slot="title"><translate>Recently favorited</translate></template>
+          </track-widget>
         </div>
         <div class="column">
-          <h2 class="ui header">
-            <translate>Music requests</translate>
-          </h2>
-          <request-form v-if="$store.state.auth.authenticated"></request-form>
+          <playlist-widget :url="'playlists/'" :filters="{scope: 'user', listenable: true, ordering: '-creation_date'}">
+            <template slot="title"><translate>Playlists</translate></template>
+          </playlist-widget>
+        </div>
+      </div>
+      <div class="ui section hidden divider"></div>
+      <div class="ui grid">
+        <div class="ui row">
+          <album-widget :filters="{ordering: '-creation_date'}">
+            <template slot="title"><translate>Recently added</translate></template>
+          </album-widget>
         </div>
       </div>
     </div>
@@ -38,8 +35,9 @@ import axios from 'axios'
 import Search from '@/components/audio/Search'
 import logger from '@/logging'
 import ArtistCard from '@/components/audio/artist/Card'
-import RadioCard from '@/components/radios/Card'
-import RequestForm from '@/components/requests/Form'
+import TrackWidget from '@/components/audio/track/Widget'
+import AlbumWidget from '@/components/audio/album/Widget'
+import PlaylistWidget from '@/components/playlists/Widget'
 
 const ARTISTS_URL = 'artists/'
 
@@ -48,8 +46,9 @@ export default {
   components: {
     Search,
     ArtistCard,
-    RadioCard,
-    RequestForm
+    TrackWidget,
+    AlbumWidget,
+    PlaylistWidget
   },
   data () {
     return {
diff --git a/front/src/components/library/Radios.vue b/front/src/components/library/Radios.vue
index 3c3ac95b33dda065f5ae8c7704f2ece6dd847786..2b542a71bef62ed5bd9f958bf4d535a297b7eae3 100644
--- a/front/src/components/library/Radios.vue
+++ b/front/src/components/library/Radios.vue
@@ -4,6 +4,22 @@
       <h2 class="ui header">
         <translate>Browsing radios</translate>
       </h2>
+      <div class="ui hidden divider"></div>
+      <div class="ui row">
+        <h3 class="ui header">
+          <translate>Instance radios</translate>
+        </h3>
+        <div class="ui cards">
+          <radio-card :type="'favorites'"></radio-card>
+          <radio-card :type="'random'"></radio-card>
+          <radio-card :type="'less-listened'"></radio-card>
+        </div>
+      </div>
+
+      <div class="ui hidden divider"></div>
+      <h3 class="ui header">
+        <translate>User radios</translate>
+      </h3>
       <router-link class="ui green basic button" to="/library/radios/build" exact>
         <translate>Create your own radio</translate>
       </router-link>
diff --git a/front/src/components/playlists/Card.vue b/front/src/components/playlists/Card.vue
index a480975ef63234d733073b66800b65cfe85d0cf6..956a543a6cf66165c6808f3a4d2ffbf5b2c87959 100644
--- a/front/src/components/playlists/Card.vue
+++ b/front/src/components/playlists/Card.vue
@@ -1,30 +1,35 @@
 <template>
-  <div class="ui card">
+  <div class="ui playlist card">
+    <div class="ui top attached icon button" :style="coversStyle">
+    </div>
     <div class="content">
       <div class="header">
-        <router-link class="discrete link" :to="{name: 'library.playlists.detail', params: {id: playlist.id }}">
-          {{ playlist.name }}
+        <div class="right floated">
+          <play-button :icon-only="true" class="ui inline" :button-classes="['ui', 'circular', 'large', {orange: playlist.tracks_count > 0}, 'icon', 'button', {disabled: playlist.tracks_count === 0}]" :playlist="playlist"></play-button>
+          <play-button class="basic inline icon" :dropdown-only="true" :dropdown-icon-classes="['ellipsis', 'vertical', 'large', {disabled: playlist.tracks_count === 0}, 'grey']" :playlist="playlist"></play-button>
+        </div>
+        <router-link :title="playlist.name" class="discrete link" :to="{name: 'library.playlists.detail', params: {id: playlist.id }}">
+          {{ playlist.name | truncate(30) }}
         </router-link>
       </div>
       <div class="meta">
-        <i class="user icon"></i> {{ playlist.user.username }}
-      </div>
-      <div class="meta">
-        <i class="clock icon"></i>
-        <human-date :date="playlist.modification_date" />
-      </div>
-    </div>
-    <div class="extra content">
-      <span>
+        <duration :seconds="playlist.duration" />
+         |
         <i class="sound icon"></i>
         <translate
           translate-plural="%{ count } tracks"
           :translate-n="playlist.tracks_count"
           :translate-params="{count: playlist.tracks_count}">
           %{ count} track
-        </translate>
+        </translate>&nbsp;
+      </div>
+    </div>
+    <div class="extra content">
+      <user-link :user="playlist.user" class="left floated" />
+      <span class="right floated">
+        <i class="clock outline icon" />
+        <human-date :date="playlist.creation_date" />
       </span>
-      <play-button class="mini basic orange right floated" :playlist="playlist"><translate>Play all</translate></play-button>
     </div>
   </div>
 </template>
@@ -36,11 +41,46 @@ export default {
   props: ['playlist'],
   components: {
     PlayButton
+  },
+  computed: {
+    coversStyle () {
+      let self = this
+      let urls = this.playlist.album_covers.map((url) => {
+        url = self.$store.getters['instance/absoluteUrl'](url)
+        return `url("${url}")`
+      }).slice(0, 4)
+      return {
+        'background-image': urls.join(', ')
+      }
+    }
   }
 }
 </script>
 
 <!-- Add "scoped" attribute to limit CSS to this component only -->
+<style>
+
+.playlist.card .header .ellipsis.vertical.large.grey {
+  font-size: 1.2em;
+  margin-right: 0;
+}
+
+</style>
 <style scoped>
+.card .header {
+  margin-bottom: 0.25em;
+}
+
+.attached.button {
+  background-color: rgb(243, 244, 245);
+  background-size: 25% ;
+  background-repeat: no-repeat;
+  background-origin: border-box;
+  background-position: 0 0, 33.33% 0, 66.67% 0, 100% 0;
+  /* background-position: 0 0, 50% 0, 100% 0; */
+  /* background-position: 0 0, 25% 0, 50% 0, 75% 0, 100% 0; */
+  font-size: 4em;
+  box-shadow: 0px 0px 0px 1px rgba(34, 36, 38, 0.15) inset !important;
+}
 
 </style>
diff --git a/front/src/components/playlists/Widget.vue b/front/src/components/playlists/Widget.vue
new file mode 100644
index 0000000000000000000000000000000000000000..72c544205fa5e4261eeff2032c2f9a766205053c
--- /dev/null
+++ b/front/src/components/playlists/Widget.vue
@@ -0,0 +1,77 @@
+<template>
+  <div>
+    <h3 class="ui header">
+      <slot name="title"></slot>
+    </h3>
+    <i @click="fetchData(previousPage)" :disabled="!previousPage" :class="['ui', {disabled: !previousPage}, 'circular', 'large', 'angle up', 'icon']">
+    </i>
+    <i @click="fetchData(nextPage)" :disabled="!nextPage" :class="['ui', {disabled: !nextPage}, 'circular', 'large', 'angle down', 'icon']">
+    </i>
+    <div v-if="isLoading" class="ui inverted active dimmer">
+      <div class="ui loader"></div>
+    </div>
+    <playlist-card class="fluid" v-for="playlist in objects" :key="playlist.id" :playlist="playlist"></playlist-card>
+  </div>
+</template>
+
+<script>
+import _ from 'lodash'
+import axios from 'axios'
+import PlaylistCard from '@/components/playlists/Card'
+
+export default {
+  props: {
+    filters: {type: Object, required: true},
+    url: {type: String, required: true}
+  },
+  components: {
+    PlaylistCard
+  },
+  data () {
+    return {
+      objects: [],
+      limit: 3,
+      isLoading: false,
+      errors: null,
+      previousPage: null,
+      nextPage: null
+    }
+  },
+  created () {
+    this.fetchData(this.url)
+  },
+  methods: {
+    fetchData (url) {
+      if (!url) {
+        return
+      }
+      this.isLoading = true
+      let self = this
+      let params = _.clone(this.filters)
+      params.page_size = this.limit
+      params.offset = this.offset
+      axios.get(url, {params: params}).then((response) => {
+        self.previousPage = response.data.previous
+        self.nextPage = response.data.next
+        self.isLoading = false
+        self.objects = response.data.results
+      }, error => {
+        self.isLoading = false
+        self.errors = error.backendErrors
+      })
+    },
+    updateOffset (increment) {
+      if (increment) {
+        this.offset += this.limit
+      } else {
+        this.offset = Math.max(this.offset - this.limit, 0)
+      }
+    }
+  },
+  watch: {
+    offset () {
+      this.fetchData()
+    }
+  }
+}
+</script>
diff --git a/front/src/filters.js b/front/src/filters.js
index 11751559961c393b9d5bb3369aba199f8badf34a..878b3c9f2813756914afdbe62c8f4459768b61ac 100644
--- a/front/src/filters.js
+++ b/front/src/filters.js
@@ -28,6 +28,16 @@ export function ago (date) {
 
 Vue.filter('ago', ago)
 
+export function secondsToObject (seconds) {
+  let m = moment.duration(seconds, 'seconds')
+  return {
+    minutes: m.minutes(),
+    hours: parseInt(m.asHours())
+  }
+}
+
+Vue.filter('secondsToObject', secondsToObject)
+
 export function momentFormat (date, format) {
   format = format || 'lll'
   return moment(date).format(format)
diff --git a/front/src/router/index.js b/front/src/router/index.js
index bb59b5348b5cdbc36178c91b81249d5f7f7e19ec..1747c88581f9ef45254a07fc79a0a27d41e68d4f 100644
--- a/front/src/router/index.js
+++ b/front/src/router/index.js
@@ -35,6 +35,7 @@ import AdminUsersBase from '@/views/admin/users/Base'
 import AdminUsersDetail from '@/views/admin/users/UsersDetail'
 import AdminUsersList from '@/views/admin/users/UsersList'
 import AdminInvitationsList from '@/views/admin/users/InvitationsList'
+import MusicRequest from '@/views/library/MusicRequest'
 import FederationBase from '@/views/federation/Base'
 import FederationScan from '@/views/federation/Scan'
 import FederationLibraryDetail from '@/views/federation/LibraryDetail'
@@ -218,7 +219,12 @@ export default new Router({
       path: '/library',
       component: Library,
       children: [
-        { path: '', component: LibraryHome },
+        { path: '', component: LibraryHome, name: 'library.index' },
+        {
+          path: 'requests/',
+          name: 'library.request',
+          component: MusicRequest
+        },
         {
           path: 'artists/',
           name: 'library.artists.browse',
diff --git a/front/src/utils/color.js b/front/src/utils/color.js
new file mode 100644
index 0000000000000000000000000000000000000000..8066abd3c2d4a2c86fd5d51608626a96849556a1
--- /dev/null
+++ b/front/src/utils/color.js
@@ -0,0 +1,12 @@
+export function hashCode (str) { // java String#hashCode
+  var hash = 0
+  for (var i = 0; i < str.length; i++) {
+    hash = str.charCodeAt(i) + ((hash << 5) - hash)
+  }
+  return hash
+}
+
+export function intToRGB (i) {
+  var c = (i & 0x00FFFFFF).toString(16).toUpperCase()
+  return '00000'.substring(0, 6 - c.length) + c
+}
diff --git a/front/src/views/library/MusicRequest.vue b/front/src/views/library/MusicRequest.vue
new file mode 100644
index 0000000000000000000000000000000000000000..ac2aeafb2c6cb4732c29cdaad275a1bbf69a3363
--- /dev/null
+++ b/front/src/views/library/MusicRequest.vue
@@ -0,0 +1,32 @@
+<template>
+  <div class="ui vertical stripe segment" v-title="labels.title">
+    <div class="ui small text container">
+      <h2 class="ui header">
+        <translate>Request some music</translate>
+      </h2>
+      <request-form v-if="$store.state.auth.authenticated"></request-form>
+    </div>
+  </div>
+</template>
+
+<script>
+import RequestForm from '@/components/requests/Form'
+
+export default {
+  components: {
+    RequestForm
+  },
+  computed: {
+    labels () {
+      let title = this.$gettext('Request some music')
+      return {
+        title
+      }
+    }
+  }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+</style>
diff --git a/front/src/views/playlists/Detail.vue b/front/src/views/playlists/Detail.vue
index 3fd4730bc58cf26c5197b19dd9e258ced8eb657e..f9d2327759f814ff582769534cf9edcabffc5b04 100644
--- a/front/src/views/playlists/Detail.vue
+++ b/front/src/views/playlists/Detail.vue
@@ -9,14 +9,15 @@
           <i class="circular inverted list yellow icon"></i>
           <div class="content">
             {{ playlist.name }}
-            <translate
-              tag="div"
-              class="sub header"
-              translate-plural="Playlist containing %{ count } tracks, by %{ username }"
-              :translate-n="playlistTracks.length"
-              :translate-params="{count: playlistTracks.length, username: playlist.user.username}">
-              Playlist containing %{ count } track, by %{ username }
-            </translate>
+            <div class="sub header">
+              <translate
+                translate-plural="Playlist containing %{ count } tracks, by %{ username }"
+                :translate-n="playlistTracks.length"
+                :translate-params="{count: playlistTracks.length, username: playlist.user.username}">
+                Playlist containing %{ count } track, by %{ username }
+              </translate><br>
+              <duration :seconds="playlist.duration" />
+            </div>
           </div>
         </h2>
         <div class="ui hidden divider"></div>
diff --git a/front/src/views/playlists/List.vue b/front/src/views/playlists/List.vue
index c28f99561131e402d7b0ecac0e1c40902f31ee08..a831dfe7035ca033fd24a20e807ab289d011c5dd 100644
--- a/front/src/views/playlists/List.vue
+++ b/front/src/views/playlists/List.vue
@@ -118,7 +118,7 @@ export default {
           ordering: this.getOrderingAsString()
         }
       })
-    }, 500),
+    }, 250),
     fetchData: _.debounce(function () {
       var self = this
       this.isLoading = true