diff --git a/api/funkwhale_api/common/models.py b/api/funkwhale_api/common/models.py
index 87f7dc8e3a4031b23ee6111c2ebcfb449234857b..52a02cad9433f4e79972b6cfe858534c830cb513 100644
--- a/api/funkwhale_api/common/models.py
+++ b/api/funkwhale_api/common/models.py
@@ -5,9 +5,10 @@ from django.contrib.contenttypes.fields import GenericForeignKey
 from django.contrib.contenttypes.models import ContentType
 from django.conf import settings
 from django.core.serializers.json import DjangoJSONEncoder
-from django.db import models, transaction
+from django.db import connections, models, transaction
 from django.db.models import Lookup
 from django.db.models.fields import Field
+from django.db.models.sql.compiler import SQLCompiler
 from django.utils import timezone
 from django.urls import reverse
 
@@ -25,6 +26,41 @@ class NotEqual(Lookup):
         return "%s <> %s" % (lhs, rhs), params
 
 
+class NullsLastSQLCompiler(SQLCompiler):
+    def get_order_by(self):
+        result = super().get_order_by()
+        if result and self.connection.vendor == "postgresql":
+            return [
+                (
+                    expr,
+                    (
+                        sql + " NULLS LAST" if not sql.endswith(" NULLS LAST") else sql,
+                        params,
+                        is_ref,
+                    ),
+                )
+                for (expr, (sql, params, is_ref)) in result
+            ]
+        return result
+
+
+class NullsLastQuery(models.sql.query.Query):
+    """Use a custom compiler to inject 'NULLS LAST' (for PostgreSQL)."""
+
+    def get_compiler(self, using=None, connection=None):
+        if using is None and connection is None:
+            raise ValueError("Need either using or connection")
+        if using:
+            connection = connections[using]
+        return NullsLastSQLCompiler(self, connection, using)
+
+
+class NullsLastQuerySet(models.QuerySet):
+    def __init__(self, model=None, query=None, using=None, hints=None):
+        super().__init__(model, query, using, hints)
+        self.query = query or NullsLastQuery(self.model)
+
+
 class LocalFromFidQuerySet:
     def local(self, include=True):
         host = settings.FEDERATION_HOSTNAME
diff --git a/api/funkwhale_api/federation/models.py b/api/funkwhale_api/federation/models.py
index 7d3d5639de3520a2647b773885868c6ff24fa66a..3e3cb0e52b8c19909fca4914bdf5c80aedcb97b2 100644
--- a/api/funkwhale_api/federation/models.py
+++ b/api/funkwhale_api/federation/models.py
@@ -1,4 +1,5 @@
 import tempfile
+import urllib.parse
 import uuid
 
 from django.conf import settings
@@ -43,6 +44,18 @@ class FederationMixin(models.Model):
     class Meta:
         abstract = True
 
+    @property
+    def is_local(self):
+        return federation_utils.is_local(self.fid)
+
+    @property
+    def domain_name(self):
+        if not self.fid:
+            return
+
+        parsed = urllib.parse.urlparse(self.fid)
+        return parsed.hostname
+
 
 class ActorQuerySet(models.QuerySet):
     def local(self, include=True):
diff --git a/api/funkwhale_api/manage/filters.py b/api/funkwhale_api/manage/filters.py
index 64a6473e0b93b471197511cb88ae82f2a5647c16..c6f5db53ade2a163955807bd904a07ac1fb0962c 100644
--- a/api/funkwhale_api/manage/filters.py
+++ b/api/funkwhale_api/manage/filters.py
@@ -1,4 +1,8 @@
 from django import forms
+from django.db.models import Q
+from django.conf import settings
+
+import django_filters
 from django_filters import rest_framework as filters
 
 from funkwhale_api.common import fields
@@ -11,19 +15,32 @@ from funkwhale_api.music import models as music_models
 from funkwhale_api.users import models as users_models
 
 
-class ManageUploadFilterSet(filters.FilterSet):
-    q = fields.SearchFilter(
-        search_fields=[
-            "track__title",
-            "track__album__title",
-            "track__artist__name",
-            "source",
-        ]
-    )
+class ActorField(forms.CharField):
+    def clean(self, value):
+        value = super().clean(value)
+        if not value:
+            return value
 
-    class Meta:
-        model = music_models.Upload
-        fields = ["q", "track__album", "track__artist", "track"]
+        parts = value.split("@")
+
+        return {
+            "username": parts[0],
+            "domain": parts[1] if len(parts) > 1 else settings.FEDERATION_HOSTNAME,
+        }
+
+
+def get_actor_filter(actor_field):
+    def handler(v):
+        if not v:
+            return Q(**{actor_field: None})
+        return Q(
+            **{
+                "{}__preferred_username__iexact".format(actor_field): v["username"],
+                "{}__domain__name__iexact".format(actor_field): v["domain"],
+            }
+        )
+
+    return {"field": ActorField(), "handler": handler}
 
 
 class ManageArtistFilterSet(filters.FilterSet):
@@ -37,7 +54,11 @@ class ManageArtistFilterSet(filters.FilterSet):
             filter_fields={
                 "domain": {
                     "handler": lambda v: federation_utils.get_domain_query_from_url(v)
-                }
+                },
+                "library_id": {
+                    "to": "tracks__uploads__library_id",
+                    "field": forms.IntegerField(),
+                },
             },
         )
     )
@@ -61,6 +82,10 @@ class ManageAlbumFilterSet(filters.FilterSet):
                 "domain": {
                     "handler": lambda v: federation_utils.get_domain_query_from_url(v)
                 },
+                "library_id": {
+                    "to": "tracks__uploads__library_id",
+                    "field": forms.IntegerField(),
+                },
             },
         )
     )
@@ -93,6 +118,10 @@ class ManageTrackFilterSet(filters.FilterSet):
                 "domain": {
                     "handler": lambda v: federation_utils.get_domain_query_from_url(v)
                 },
+                "library_id": {
+                    "to": "uploads__library_id",
+                    "field": forms.IntegerField(),
+                },
             },
         )
     )
@@ -102,6 +131,96 @@ class ManageTrackFilterSet(filters.FilterSet):
         fields = ["q", "title", "mbid", "fid", "artist", "album", "license"]
 
 
+class ManageLibraryFilterSet(filters.FilterSet):
+    ordering = django_filters.OrderingFilter(
+        # tuple-mapping retains order
+        fields=(
+            ("creation_date", "creation_date"),
+            ("_uploads_count", "uploads_count"),
+            ("followers_count", "followers_count"),
+        )
+    )
+    q = fields.SmartSearchFilter(
+        config=search.SearchConfig(
+            search_fields={
+                "name": {"to": "name"},
+                "description": {"to": "description"},
+                "fid": {"to": "fid"},
+            },
+            filter_fields={
+                "artist_id": {
+                    "to": "uploads__track__artist_id",
+                    "field": forms.IntegerField(),
+                },
+                "album_id": {
+                    "to": "uploads__track__album_id",
+                    "field": forms.IntegerField(),
+                },
+                "track_id": {"to": "uploads__track__id", "field": forms.IntegerField()},
+                "domain": {"to": "actor__domain_id"},
+                "account": get_actor_filter("actor"),
+                "privacy_level": {"to": "privacy_level"},
+            },
+        )
+    )
+    domain = filters.CharFilter("actor__domain_id")
+
+    class Meta:
+        model = music_models.Library
+        fields = ["q", "name", "fid", "privacy_level", "domain"]
+
+
+class ManageUploadFilterSet(filters.FilterSet):
+    ordering = django_filters.OrderingFilter(
+        # tuple-mapping retains order
+        fields=(
+            ("creation_date", "creation_date"),
+            ("modification_date", "modification_date"),
+            ("accessed_date", "accessed_date"),
+            ("size", "size"),
+            ("bitrate", "bitrate"),
+            ("duration", "duration"),
+        )
+    )
+    q = fields.SmartSearchFilter(
+        config=search.SearchConfig(
+            search_fields={
+                "source": {"to": "source"},
+                "fid": {"to": "fid"},
+                "track": {"to": "track__title"},
+                "album": {"to": "track__album__title"},
+                "artist": {"to": "track__artist__name"},
+            },
+            filter_fields={
+                "library_id": {"to": "library_id", "field": forms.IntegerField()},
+                "artist_id": {"to": "track__artist_id", "field": forms.IntegerField()},
+                "album_id": {"to": "track__album_id", "field": forms.IntegerField()},
+                "track_id": {"to": "track__id", "field": forms.IntegerField()},
+                "domain": {"to": "library__actor__domain_id"},
+                "import_reference": {"to": "import_reference"},
+                "type": {"to": "mimetype"},
+                "status": {"to": "import_status"},
+                "account": get_actor_filter("library__actor"),
+                "privacy_level": {"to": "library__privacy_level"},
+            },
+        )
+    )
+    domain = filters.CharFilter("library__actor__domain_id")
+    privacy_level = filters.CharFilter("library__privacy_level")
+
+    class Meta:
+        model = music_models.Upload
+        fields = [
+            "q",
+            "fid",
+            "privacy_level",
+            "domain",
+            "mimetype",
+            "import_reference",
+            "import_status",
+        ]
+
+
 class ManageDomainFilterSet(filters.FilterSet):
     q = fields.SearchFilter(search_fields=["name"])
 
diff --git a/api/funkwhale_api/manage/serializers.py b/api/funkwhale_api/manage/serializers.py
index cf6a1eab4b698b4bed8e899b2f10470c0c5b5219..add9364e86a3fc9e7f82873fbbea15949a59fde4 100644
--- a/api/funkwhale_api/manage/serializers.py
+++ b/api/funkwhale_api/manage/serializers.py
@@ -15,67 +15,6 @@ from funkwhale_api.users import models as users_models
 from . import filters
 
 
-class ManageUploadArtistSerializer(serializers.ModelSerializer):
-    class Meta:
-        model = music_models.Artist
-        fields = ["id", "mbid", "creation_date", "name"]
-
-
-class ManageUploadAlbumSerializer(serializers.ModelSerializer):
-    artist = ManageUploadArtistSerializer()
-
-    class Meta:
-        model = music_models.Album
-        fields = (
-            "id",
-            "mbid",
-            "title",
-            "artist",
-            "release_date",
-            "cover",
-            "creation_date",
-        )
-
-
-class ManageUploadTrackSerializer(serializers.ModelSerializer):
-    artist = ManageUploadArtistSerializer()
-    album = ManageUploadAlbumSerializer()
-
-    class Meta:
-        model = music_models.Track
-        fields = ("id", "mbid", "title", "album", "artist", "creation_date", "position")
-
-
-class ManageUploadSerializer(serializers.ModelSerializer):
-    track = ManageUploadTrackSerializer()
-
-    class Meta:
-        model = music_models.Upload
-        fields = (
-            "id",
-            "path",
-            "source",
-            "filename",
-            "mimetype",
-            "track",
-            "duration",
-            "mimetype",
-            "creation_date",
-            "bitrate",
-            "size",
-            "path",
-        )
-
-
-class ManageUploadActionSerializer(common_serializers.ActionSerializer):
-    actions = [common_serializers.Action("delete", allow_all=False)]
-    filterset_class = filters.ManageUploadFilterSet
-
-    @transaction.atomic
-    def handle_delete(self, objects):
-        return objects.delete()
-
-
 class PermissionsSerializer(serializers.Serializer):
     def to_representation(self, o):
         return o.get_permissions(defaults=self.context.get("default_permissions"))
@@ -493,3 +432,111 @@ class ManageArtistActionSerializer(common_serializers.ActionSerializer):
     @transaction.atomic
     def handle_delete(self, objects):
         return objects.delete()
+
+
+class ManageLibraryActionSerializer(common_serializers.ActionSerializer):
+    actions = [common_serializers.Action("delete", allow_all=False)]
+    filterset_class = filters.ManageLibraryFilterSet
+
+    @transaction.atomic
+    def handle_delete(self, objects):
+        return objects.delete()
+
+
+class ManageUploadActionSerializer(common_serializers.ActionSerializer):
+    actions = [common_serializers.Action("delete", allow_all=False)]
+    filterset_class = filters.ManageUploadFilterSet
+
+    @transaction.atomic
+    def handle_delete(self, objects):
+        return objects.delete()
+
+
+class ManageLibrarySerializer(serializers.ModelSerializer):
+    domain = serializers.CharField(source="domain_name")
+    actor = ManageBaseActorSerializer()
+    uploads_count = serializers.SerializerMethodField()
+    followers_count = serializers.SerializerMethodField()
+
+    class Meta:
+        model = music_models.Library
+        fields = [
+            "id",
+            "uuid",
+            "fid",
+            "url",
+            "name",
+            "description",
+            "domain",
+            "is_local",
+            "creation_date",
+            "privacy_level",
+            "uploads_count",
+            "followers_count",
+            "followers_url",
+            "actor",
+        ]
+
+    def get_uploads_count(self, obj):
+        return getattr(obj, "_uploads_count", obj.uploads_count)
+
+    def get_followers_count(self, obj):
+        return getattr(obj, "followers_count", None)
+
+
+class ManageNestedLibrarySerializer(serializers.ModelSerializer):
+    domain = serializers.CharField(source="domain_name")
+    actor = ManageBaseActorSerializer()
+
+    class Meta:
+        model = music_models.Library
+        fields = [
+            "id",
+            "uuid",
+            "fid",
+            "url",
+            "name",
+            "description",
+            "domain",
+            "is_local",
+            "creation_date",
+            "privacy_level",
+            "followers_url",
+            "actor",
+        ]
+
+
+class ManageUploadSerializer(serializers.ModelSerializer):
+    track = ManageNestedTrackSerializer()
+    library = ManageNestedLibrarySerializer()
+    domain = serializers.CharField(source="domain_name")
+
+    class Meta:
+        model = music_models.Upload
+        fields = (
+            "id",
+            "uuid",
+            "fid",
+            "domain",
+            "is_local",
+            "audio_file",
+            "listen_url",
+            "source",
+            "filename",
+            "mimetype",
+            "duration",
+            "mimetype",
+            "bitrate",
+            "size",
+            "creation_date",
+            "accessed_date",
+            "modification_date",
+            "metadata",
+            "import_date",
+            "import_details",
+            "import_status",
+            "import_metadata",
+            "import_reference",
+            "track",
+            "library",
+        )
diff --git a/api/funkwhale_api/manage/urls.py b/api/funkwhale_api/manage/urls.py
index f93667725ae0e2ad4354b2527bcef3738b64c676..2d5da59e3ed6b8656c3438fb9121819e18079d47 100644
--- a/api/funkwhale_api/manage/urls.py
+++ b/api/funkwhale_api/manage/urls.py
@@ -7,10 +7,11 @@ federation_router = routers.SimpleRouter()
 federation_router.register(r"domains", views.ManageDomainViewSet, "domains")
 
 library_router = routers.SimpleRouter()
-library_router.register(r"uploads", views.ManageUploadViewSet, "uploads")
-library_router.register(r"artists", views.ManageArtistViewSet, "artists")
 library_router.register(r"albums", views.ManageAlbumViewSet, "albums")
+library_router.register(r"artists", views.ManageArtistViewSet, "artists")
+library_router.register(r"libraries", views.ManageLibraryViewSet, "libraries")
 library_router.register(r"tracks", views.ManageTrackViewSet, "tracks")
+library_router.register(r"uploads", views.ManageUploadViewSet, "uploads")
 
 moderation_router = routers.SimpleRouter()
 moderation_router.register(
diff --git a/api/funkwhale_api/manage/views.py b/api/funkwhale_api/manage/views.py
index 6fc1a2f1e63512574d1d20bed54f2abd0429690f..48ed62a022ab367dc88d8db9ba51c7086fa5a46c 100644
--- a/api/funkwhale_api/manage/views.py
+++ b/api/funkwhale_api/manage/views.py
@@ -19,38 +19,6 @@ from funkwhale_api.users import models as users_models
 from . import filters, serializers
 
 
-class ManageUploadViewSet(
-    mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
-):
-    queryset = (
-        music_models.Upload.objects.all()
-        .select_related("track__artist", "track__album__artist")
-        .order_by("-id")
-    )
-    serializer_class = serializers.ManageUploadSerializer
-    filterset_class = filters.ManageUploadFilterSet
-    required_scope = "instance:libraries"
-    ordering_fields = [
-        "accessed_date",
-        "modification_date",
-        "creation_date",
-        "track__artist__name",
-        "bitrate",
-        "size",
-        "duration",
-    ]
-
-    @rest_decorators.action(methods=["post"], detail=False)
-    def action(self, request, *args, **kwargs):
-        queryset = self.get_queryset()
-        serializer = serializers.ManageUploadActionSerializer(
-            request.data, queryset=queryset
-        )
-        serializer.is_valid(raise_exception=True)
-        result = serializer.save()
-        return response.Response(result, status=200)
-
-
 def get_stats(tracks, target):
     data = {}
     tracks = list(tracks.values_list("pk", flat=True))
@@ -70,6 +38,12 @@ def get_stats(tracks, target):
     ).count()
     data["libraries"] = uploads.values_list("library", flat=True).distinct().count()
     data["uploads"] = uploads.count()
+    data.update(get_media_stats(uploads))
+    return data
+
+
+def get_media_stats(uploads):
+    data = {}
     data["media_total_size"] = uploads.aggregate(v=Sum("size"))["v"] or 0
     data["media_downloaded_size"] = (
         uploads.with_file().aggregate(v=Sum("size"))["v"] or 0
@@ -85,6 +59,7 @@ class ManageArtistViewSet(
 ):
     queryset = (
         music_models.Artist.objects.all()
+        .distinct()
         .order_by("-id")
         .select_related("attributed_to")
         .prefetch_related(
@@ -130,6 +105,7 @@ class ManageAlbumViewSet(
 ):
     queryset = (
         music_models.Album.objects.all()
+        .distinct()
         .order_by("-id")
         .select_related("attributed_to", "artist")
         .prefetch_related("tracks")
@@ -164,6 +140,7 @@ class ManageTrackViewSet(
 ):
     queryset = (
         music_models.Track.objects.all()
+        .distinct()
         .order_by("-id")
         .select_related("attributed_to", "artist", "album__artist")
         .annotate(uploads_count=Count("uploads"))
@@ -196,6 +173,96 @@ class ManageTrackViewSet(
         return response.Response(result, status=200)
 
 
+class ManageLibraryViewSet(
+    mixins.ListModelMixin,
+    mixins.RetrieveModelMixin,
+    mixins.DestroyModelMixin,
+    viewsets.GenericViewSet,
+):
+    lookup_field = "uuid"
+    queryset = (
+        music_models.Library.objects.all()
+        .distinct()
+        .order_by("-id")
+        .select_related("actor")
+        .annotate(
+            followers_count=Count("received_follows", distinct=True),
+            _uploads_count=Count("uploads", distinct=True),
+        )
+    )
+    serializer_class = serializers.ManageLibrarySerializer
+    filterset_class = filters.ManageLibraryFilterSet
+    required_scope = "instance:libraries"
+
+    @rest_decorators.action(methods=["get"], detail=True)
+    def stats(self, request, *args, **kwargs):
+        library = self.get_object()
+        uploads = library.uploads.all()
+        tracks = uploads.values_list("track", flat=True).distinct()
+        albums = (
+            music_models.Track.objects.filter(pk__in=tracks)
+            .values_list("album", flat=True)
+            .distinct()
+        )
+        artists = set(
+            music_models.Album.objects.filter(pk__in=albums).values_list(
+                "artist", flat=True
+            )
+        ) | set(
+            music_models.Track.objects.filter(pk__in=tracks).values_list(
+                "artist", flat=True
+            )
+        )
+
+        data = {
+            "uploads": uploads.count(),
+            "followers": library.received_follows.count(),
+            "tracks": tracks.count(),
+            "albums": albums.count(),
+            "artists": len(artists),
+        }
+        data.update(get_media_stats(uploads.all()))
+        return response.Response(data, status=200)
+
+    @rest_decorators.action(methods=["post"], detail=False)
+    def action(self, request, *args, **kwargs):
+        queryset = self.get_queryset()
+        serializer = serializers.ManageTrackActionSerializer(
+            request.data, queryset=queryset
+        )
+        serializer.is_valid(raise_exception=True)
+        result = serializer.save()
+        return response.Response(result, status=200)
+
+
+class ManageUploadViewSet(
+    mixins.ListModelMixin,
+    mixins.RetrieveModelMixin,
+    mixins.DestroyModelMixin,
+    viewsets.GenericViewSet,
+):
+    lookup_field = "uuid"
+    queryset = (
+        music_models.Upload.objects.all()
+        .distinct()
+        .order_by("-id")
+        .select_related("library__actor", "track__artist", "track__album__artist")
+    )
+    serializer_class = serializers.ManageUploadSerializer
+    filterset_class = filters.ManageUploadFilterSet
+    required_scope = "instance:libraries"
+
+    @rest_decorators.action(methods=["post"], detail=False)
+    def action(self, request, *args, **kwargs):
+        queryset = self.get_queryset()
+        serializer = serializers.ManageTrackActionSerializer(
+            request.data, queryset=queryset
+        )
+        serializer.is_valid(raise_exception=True)
+        result = serializer.save()
+        return response.Response(result, status=200)
+
+
 class ManageUserViewSet(
     mixins.ListModelMixin,
     mixins.RetrieveModelMixin,
diff --git a/api/funkwhale_api/music/models.py b/api/funkwhale_api/music/models.py
index 7ad88d45f442ca3570c4ceaec266086821a1ed96..4b004bf15876026bebcf99bc9a67c1ab217eafd5 100644
--- a/api/funkwhale_api/music/models.py
+++ b/api/funkwhale_api/music/models.py
@@ -649,7 +649,7 @@ class Track(APIModelMixin):
         return licenses.LICENSES_BY_ID.get(self.license_id)
 
 
-class UploadQuerySet(models.QuerySet):
+class UploadQuerySet(common_models.NullsLastQuerySet):
     def playable_by(self, actor, include=True):
         libraries = Library.objects.viewable_by(actor)
 
@@ -746,6 +746,18 @@ class Upload(models.Model):
 
     objects = UploadQuerySet.as_manager()
 
+    @property
+    def is_local(self):
+        return federation_utils.is_local(self.fid)
+
+    @property
+    def domain_name(self):
+        if not self.fid:
+            return
+
+        parsed = urllib.parse.urlparse(self.fid)
+        return parsed.hostname
+
     def download_audio_from_remote(self, actor):
         from funkwhale_api.common import session
         from funkwhale_api.federation import signing
diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py
index 336a87ce0cc46af663c1be350fcb0e7f86cb3df5..86ea5d406293b8686004e024e3edab29137a1817 100644
--- a/api/funkwhale_api/music/views.py
+++ b/api/funkwhale_api/music/views.py
@@ -440,8 +440,6 @@ class UploadViewSet(
         "artist__name",
     )
 
-    fetches = federation_decorators.fetches_route()
-
     def get_queryset(self):
         qs = super().get_queryset()
         return qs.filter(library__actor=self.request.user.actor)
diff --git a/api/tests/manage/test_serializers.py b/api/tests/manage/test_serializers.py
index 64a26538f9d8cac0b87826e90cb08fef111825a5..65c75c2c331cc013761d3ee00c22a985e21c2b21 100644
--- a/api/tests/manage/test_serializers.py
+++ b/api/tests/manage/test_serializers.py
@@ -399,12 +399,73 @@ def test_manage_track_serializer(factories, now):
     assert s.data == expected
 
 
+def test_manage_library_serializer(factories, now):
+    library = factories["music.Library"]()
+    setattr(library, "followers_count", 42)
+    setattr(library, "_uploads_count", 44)
+    expected = {
+        "id": library.id,
+        "fid": library.fid,
+        "url": library.url,
+        "uuid": str(library.uuid),
+        "followers_url": library.followers_url,
+        "domain": library.domain_name,
+        "is_local": library.is_local,
+        "name": library.name,
+        "description": library.description,
+        "privacy_level": library.privacy_level,
+        "creation_date": library.creation_date.isoformat().split("+")[0] + "Z",
+        "actor": serializers.ManageBaseActorSerializer(library.actor).data,
+        "uploads_count": 44,
+        "followers_count": 42,
+    }
+    s = serializers.ManageLibrarySerializer(library)
+
+    assert s.data == expected
+
+
+def test_manage_upload_serializer(factories, now):
+    upload = factories["music.Upload"]()
+
+    expected = {
+        "id": upload.id,
+        "fid": upload.fid,
+        "audio_file": upload.audio_file.url,
+        "listen_url": upload.listen_url,
+        "uuid": str(upload.uuid),
+        "domain": upload.domain_name,
+        "is_local": upload.is_local,
+        "duration": upload.duration,
+        "size": upload.size,
+        "bitrate": upload.bitrate,
+        "mimetype": upload.mimetype,
+        "source": upload.source,
+        "filename": upload.filename,
+        "metadata": upload.metadata,
+        "creation_date": upload.creation_date.isoformat().split("+")[0] + "Z",
+        "modification_date": upload.modification_date.isoformat().split("+")[0] + "Z",
+        "accessed_date": None,
+        "import_date": None,
+        "import_metadata": upload.import_metadata,
+        "import_status": upload.import_status,
+        "import_reference": upload.import_reference,
+        "import_details": upload.import_details,
+        "library": serializers.ManageNestedLibrarySerializer(upload.library).data,
+        "track": serializers.ManageNestedTrackSerializer(upload.track).data,
+    }
+    s = serializers.ManageUploadSerializer(upload)
+
+    assert s.data == expected
+
+
 @pytest.mark.parametrize(
     "factory, serializer_class",
     [
         ("music.Track", serializers.ManageTrackActionSerializer),
         ("music.Album", serializers.ManageAlbumActionSerializer),
         ("music.Artist", serializers.ManageArtistActionSerializer),
+        ("music.Library", serializers.ManageLibraryActionSerializer),
+        ("music.Upload", serializers.ManageUploadActionSerializer),
     ],
 )
 def test_action_serializer_delete(factory, serializer_class, factories):
diff --git a/api/tests/manage/test_views.py b/api/tests/manage/test_views.py
index 923d331d8ca722a38d31410be539097f1df87453..e3d136a0e841ab5e49c9dd782083d5f3b9c5cf2a 100644
--- a/api/tests/manage/test_views.py
+++ b/api/tests/manage/test_views.py
@@ -1,4 +1,3 @@
-import pytest
 from django.urls import reverse
 
 from funkwhale_api.federation import models as federation_models
@@ -6,21 +5,6 @@ from funkwhale_api.federation import tasks as federation_tasks
 from funkwhale_api.manage import serializers
 
 
-@pytest.mark.skip(reason="Refactoring in progress")
-def test_upload_view(factories, superuser_api_client):
-    uploads = factories["music.Upload"].create_batch(size=5)
-    qs = uploads[0].__class__.objects.order_by("-creation_date")
-    url = reverse("api:v1:manage:library:uploads-list")
-
-    response = superuser_api_client.get(url, {"sort": "-creation_date"})
-    expected = serializers.ManageUploadSerializer(
-        qs, many=True, context={"request": response.wsgi_request}
-    ).data
-
-    assert response.data["count"] == len(uploads)
-    assert response.data["results"] == expected
-
-
 def test_user_view(factories, superuser_api_client, mocker):
     mocker.patch("funkwhale_api.users.models.User.record_activity")
     users = factories["users.User"].create_batch(size=5) + [superuser_api_client.user]
@@ -289,3 +273,82 @@ def test_track_delete(factories, superuser_api_client):
     response = superuser_api_client.delete(url)
 
     assert response.status_code == 204
+
+
+def test_library_list(factories, superuser_api_client, settings):
+    library = factories["music.Library"]()
+    url = reverse("api:v1:manage:library:libraries-list")
+    response = superuser_api_client.get(url)
+
+    assert response.status_code == 200
+
+    assert response.data["count"] == 1
+    assert response.data["results"][0]["id"] == library.id
+
+
+def test_library_detail(factories, superuser_api_client):
+    library = factories["music.Library"]()
+    url = reverse(
+        "api:v1:manage:library:libraries-detail", kwargs={"uuid": library.uuid}
+    )
+    response = superuser_api_client.get(url)
+
+    assert response.status_code == 200
+    assert response.data["id"] == library.id
+
+
+def test_library_detail_stats(factories, superuser_api_client):
+    library = factories["music.Library"]()
+    url = reverse(
+        "api:v1:manage:library:libraries-stats", kwargs={"uuid": library.uuid}
+    )
+    response = superuser_api_client.get(url)
+    expected = {
+        "uploads": 0,
+        "followers": 0,
+        "tracks": 0,
+        "albums": 0,
+        "artists": 0,
+        "media_total_size": 0,
+        "media_downloaded_size": 0,
+    }
+    assert response.status_code == 200
+    assert response.data == expected
+
+
+def test_library_delete(factories, superuser_api_client):
+    library = factories["music.Library"]()
+    url = reverse(
+        "api:v1:manage:library:libraries-detail", kwargs={"uuid": library.uuid}
+    )
+    response = superuser_api_client.delete(url)
+
+    assert response.status_code == 204
+
+
+def test_upload_list(factories, superuser_api_client, settings):
+    upload = factories["music.Upload"]()
+    url = reverse("api:v1:manage:library:uploads-list")
+    response = superuser_api_client.get(url)
+
+    assert response.status_code == 200
+
+    assert response.data["count"] == 1
+    assert response.data["results"][0]["id"] == upload.id
+
+
+def test_upload_detail(factories, superuser_api_client):
+    upload = factories["music.Upload"]()
+    url = reverse("api:v1:manage:library:uploads-detail", kwargs={"uuid": upload.uuid})
+    response = superuser_api_client.get(url)
+
+    assert response.status_code == 200
+    assert response.data["id"] == upload.id
+
+
+def test_upload_delete(factories, superuser_api_client):
+    upload = factories["music.Upload"]()
+    url = reverse("api:v1:manage:library:uploads-detail", kwargs={"uuid": upload.uuid})
+    response = superuser_api_client.delete(url)
+
+    assert response.status_code == 204
diff --git a/front/src/components/library/ImportStatusModal.vue b/front/src/components/library/ImportStatusModal.vue
new file mode 100644
index 0000000000000000000000000000000000000000..5632e95943cb78571ed55af3a458ab788e7bceca
--- /dev/null
+++ b/front/src/components/library/ImportStatusModal.vue
@@ -0,0 +1,164 @@
+<template>
+
+  <modal :show.sync="showModal">
+    <div class="header">
+      <translate translate-context="Popup/Import/Title">Import detail</translate>
+    </div>
+    <div class="content" v-if="upload">
+      <div class="description">
+        <div class="ui message" v-if="upload.import_status === 'pending'">
+          <translate translate-context="Popup/Import/Message">Upload is still pending and will soon be processed by the server.</translate>
+        </div>
+        <div class="ui success message" v-if="upload.import_status === 'finished'">
+          <translate translate-context="Popup/Import/Message">Upload was successfully processed by the server.</translate>
+        </div>
+        <div class="ui warning message" v-if="upload.import_status === 'skipped'">
+          <translate translate-context="Popup/Import/Message">Upload was skipped because a similar one is already available in one of your libraries.</translate>
+        </div>
+        <div class="ui error message" v-if="upload.import_status === 'errored'">
+          <translate translate-context="Popup/Import/Message">An error occured during upload processing. You will find more information below.</translate>
+        </div>
+        <template v-if="upload.import_status === 'errored'">
+          <table class="ui very basic collapsing celled table">
+            <tbody>
+              <tr>
+                <td>
+                  <translate translate-context="Popup/Import/Table.Label/Noun">Error type</translate>
+                </td>
+                <td>
+                  {{ getErrorData(upload).label }}
+                </td>
+              </tr>
+              <tr>
+                <td>
+                  <translate translate-context="Popup/Import/Table.Label/Noun">Error detail</translate>
+                </td>
+                <td>
+                  {{ getErrorData(upload).detail }}
+                  <ul v-if="getErrorData(upload).errorRows.length > 0">
+                    <li v-for="row in getErrorData(upload).errorRows">
+                      {{ row.key}}: {{ row.value}}
+                    </li>
+                  </ul>
+                </td>
+              </tr>
+              <tr>
+                <td>
+                  <translate translate-context="Popup/Import/Table.Label/Noun">Getting help</translate>
+                </td>
+                <td>
+                  <ul>
+                    <li>
+                      <a :href="getErrorData(upload).documentationUrl" target="_blank">
+                        <translate translate-context="Popup/Import/Table.Label/Value">Read our documentation for this error</translate>
+                      </a>
+                    </li>
+                    <li>
+                      <a :href="getErrorData(upload).supportUrl" target="_blank">
+                        <translate translate-context="Popup/Import/Table.Label/Value">Open a support thread (include the debug information below in your message)</translate>
+                      </a>
+                    </li>
+                  </ul>
+                </td>
+              </tr>
+              <tr>
+                <td>
+                  <translate translate-context="Popup/Import/Table.Label/Noun">Debug information</translate>
+                </td>
+                <td>
+                  <div class="ui form">
+                    <textarea class="ui textarea" rows="10" :value="getErrorData(upload).debugInfo"></textarea>
+                  </div>
+                </td>
+              </tr>
+            </tbody>
+          </table>
+        </template>
+      </div>
+    </div>
+    <div class="actions">
+      <div class="ui deny button">
+        <translate translate-context="*/*/Button.Label/Verb">Close</translate>
+      </div>
+    </div>
+  </modal>
+</template>
+<script>
+import Modal from '@/components/semantic/Modal'
+
+function getErrors(payload) {
+  let errors = []
+  for (var k in payload) {
+    if (payload.hasOwnProperty(k)) {
+      let value = payload[k]
+      if (Array.isArray(value)) {
+        errors.push({
+          key: k,
+          value: value.join(', ')
+        })
+      } else {
+        // possibly artists, so nested errors
+        if (typeof value === 'object') {
+          getErrors(value).forEach((e) => {
+            errors.push({
+              key: `${k} / ${e.key}`,
+              value: e.value
+            })
+          })
+        }
+      }
+    }
+  }
+  return errors
+}
+
+export default {
+  props: ['upload', "show"],
+  components: {
+    Modal
+  },
+  data () {
+    return {
+      showModal: this.show
+    }
+  },
+  methods: {
+    getErrorData (upload) {
+      let payload = upload.import_details || {}
+      let d = {
+        supportUrl: 'https://governance.funkwhale.audio/g/246YOJ1m/funkwhale-support',
+        errorRows: []
+      }
+      if (!payload.error_code) {
+        d.errorCode = 'unknown_error'
+      } else {
+        d.errorCode = payload.error_code
+      }
+      d.documentationUrl = `https://docs.funkwhale.audio/users/upload.html#${d.errorCode}`
+      if (d.errorCode === 'invalid_metadata') {
+        d.label = this.$pgettext('Popup/Import/Error.Label', 'Invalid metadata')
+        d.detail = this.$pgettext('Popup/Import/Error.Label', 'The metadata included in the file is invalid or some mandatory fields are missing.')
+        let detail = payload.detail || {}
+        d.errorRows = getErrors(detail)
+      } else {
+        d.label = this.$pgettext('Popup/Import/Error.Label', 'Unkwown error')
+        d.detail = this.$pgettext('Popup/Import/Error.Label', 'An unkwown error occured')
+      }
+      let debugInfo = {
+        source: upload.source,
+        ...payload,
+      }
+      d.debugInfo = JSON.stringify(debugInfo, null, 4)
+      return d
+    }
+  },
+  watch: {
+    showModal (v) {
+      this.$emit('update:show', v)
+    },
+    show (v) {
+      this.showModal = v
+    }
+  }
+}
+</script>
diff --git a/front/src/components/manage/library/LibrariesTable.vue b/front/src/components/manage/library/LibrariesTable.vue
new file mode 100644
index 0000000000000000000000000000000000000000..88c58f3110ff068b9905a81d9bd09deffc1e7bda
--- /dev/null
+++ b/front/src/components/manage/library/LibrariesTable.vue
@@ -0,0 +1,235 @@
+<template>
+  <div>
+    <div class="ui inline form">
+      <div class="fields">
+        <div class="ui six wide field">
+          <label><translate translate-context="Content/Search/Input.Label/Noun">Search</translate></label>
+          <form @submit.prevent="search.query = $refs.search.value">
+            <input name="search" ref="search" type="text" :value="search.query" :placeholder="labels.searchPlaceholder" />
+          </form>
+        </div>
+        <div class="field">
+          <label><translate translate-context="*/*/*">Visibility</translate></label>
+          <select class="ui dropdown" @change="addSearchToken('privacy_level', $event.target.value)" :value="getTokenValue('privacy_level', '')">
+            <option value=""><translate translate-context="Content/*/Dropdown">All</translate></option>
+            <option value="me">{{ sharedLabels.fields.privacy_level.shortChoices.me }}</option>
+            <option value="instance">{{ sharedLabels.fields.privacy_level.shortChoices.instance }}</option>
+            <option value="everyone">{{ sharedLabels.fields.privacy_level.shortChoices.everyone }}</option>
+          </select>
+        </div>
+        <div class="field">
+          <label><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering</translate></label>
+          <select class="ui dropdown" v-model="ordering">
+            <option v-for="option in orderingOptions" :value="option[0]">
+              {{ sharedLabels.filters[option[1]] }}
+            </option>
+          </select>
+        </div>
+        <div class="field">
+          <label><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering direction</translate></label>
+          <select class="ui dropdown" v-model="orderingDirection">
+            <option value="+"><translate translate-context="Content/Search/Dropdown">Ascending</translate></option>
+            <option value="-"><translate translate-context="Content/Search/Dropdown">Descending</translate></option>
+          </select>
+        </div>
+      </div>
+      </div>
+    <div class="dimmable">
+      <div v-if="isLoading" class="ui active inverted dimmer">
+          <div class="ui loader"></div>
+      </div>
+      <action-table
+        v-if="result"
+        @action-launched="fetchData"
+        :objects-data="result"
+        :actions="actions"
+        action-url="manage/library/libraries/action/"
+        :filters="actionFilters">
+        <template slot="header-cells">
+          <th><translate translate-context="*/*/*">Name</translate></th>
+          <th><translate translate-context="*/*/*">Account</translate></th>
+          <th><translate translate-context="Content/Moderation/*/Noun">Domain</translate></th>
+          <th><translate translate-context="*/*/*">Visibility</translate></th>
+          <th><translate translate-context="Content/*/*/Noun">Uploads</translate></th>
+          <th><translate translate-context="Content/*/*/Noun">Followers</translate></th>
+          <th><translate translate-context="Content/*/*/Noun">Creation date</translate></th>
+        </template>
+        <template slot="row-cells" slot-scope="scope">
+          <td>
+            <router-link :to="{name: 'manage.library.libraries.detail', params: {id: scope.obj.uuid }}">{{ scope.obj.name }}</router-link>
+          </td>
+          <td>
+            <router-link :to="{name: 'manage.moderation.accounts.detail', params: {id: scope.obj.actor.full_username }}">
+              <i class="wrench icon"></i>
+            </router-link>
+            <span role="button" class="discrete link" @click="addSearchToken('account', scope.obj.actor.full_username)" :title="scope.obj.actor.full_username">{{ scope.obj.actor.preferred_username }}</span>
+          </td>
+          <td>
+            <template v-if="!scope.obj.is_local">
+              <router-link :to="{name: 'manage.moderation.domains.detail', params: {id: scope.obj.domain }}">
+                <i class="wrench icon"></i>
+              </router-link>
+              <span role="button" class="discrete link" @click="addSearchToken('domain', scope.obj.domain)" :title="scope.obj.domain">{{ scope.obj.domain }}</span>
+            </template>
+            <span role="button" v-else class="ui tiny teal icon link label" @click="addSearchToken('domain', scope.obj.domain)">
+              <i class="home icon"></i>
+              <translate translate-context="Content/Moderation/*/Short, Noun">Local</translate>
+            </span>
+          </td>
+          <td>
+            <span
+              role="button"
+              class="discrete link"
+              @click="addSearchToken('privacy_level', scope.obj.privacy_level)"
+              :title="sharedLabels.fields.privacy_level.shortChoices[scope.obj.privacy_level]">
+              {{ sharedLabels.fields.privacy_level.shortChoices[scope.obj.privacy_level] }}
+            </span>
+          </td>
+          <td>
+            {{ scope.obj.uploads_count }}
+          </td>
+          <td>
+            {{ scope.obj.followers_count }}
+          </td>
+          <td>
+            <human-date :date="scope.obj.creation_date"></human-date>
+          </td>
+        </template>
+      </action-table>
+    </div>
+    <div>
+      <pagination
+        v-if="result && result.count > paginateBy"
+        @page-changed="selectPage"
+        :compact="true"
+        :current="page"
+        :paginate-by="paginateBy"
+        :total="result.count"
+        ></pagination>
+
+      <span v-if="result && result.results.length > 0">
+        <translate translate-context="Content/*/Paragraph"
+          :translate-params="{start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}">
+          Showing results %{ start }-%{ end } on %{ total }
+        </translate>
+      </span>
+    </div>
+  </div>
+</template>
+
+<script>
+import axios from 'axios'
+import _ from '@/lodash'
+import time from '@/utils/time'
+import {normalizeQuery, parseTokens} from '@/search'
+import Pagination from '@/components/Pagination'
+import ActionTable from '@/components/common/ActionTable'
+import OrderingMixin from '@/components/mixins/Ordering'
+import TranslationsMixin from '@/components/mixins/Translations'
+import SmartSearchMixin from '@/components/mixins/SmartSearch'
+
+
+export default {
+  mixins: [OrderingMixin, TranslationsMixin, SmartSearchMixin],
+  props: {
+    filters: {type: Object, required: false},
+  },
+  components: {
+    Pagination,
+    ActionTable
+  },
+  data () {
+    let defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date')
+    return {
+      time,
+      isLoading: false,
+      result: null,
+      page: 1,
+      paginateBy: 50,
+      search: {
+        query: this.defaultQuery,
+        tokens: parseTokens(normalizeQuery(this.defaultQuery))
+      },
+      orderingDirection: defaultOrdering.direction || '+',
+      ordering: defaultOrdering.field,
+      orderingOptions: [
+        ['creation_date', 'creation_date'],
+        ['followers_count', 'followers'],
+        ['uploads_count', 'uploads'],
+      ]
+    }
+  },
+  created () {
+    this.fetchData()
+  },
+  methods: {
+    fetchData () {
+      let params = _.merge({
+        'page': this.page,
+        'page_size': this.paginateBy,
+        'q': this.search.query,
+        'ordering': this.getOrderingAsString()
+      }, this.filters)
+      let self = this
+      self.isLoading = true
+      self.checked = []
+      axios.get('/manage/library/libraries/', {params: params}).then((response) => {
+        self.result = response.data
+        self.isLoading = false
+      }, error => {
+        self.isLoading = false
+        self.errors = error.backendErrors
+      })
+    },
+    selectPage: function (page) {
+      this.page = page
+    },
+  },
+  computed: {
+    labels () {
+      return {
+        searchPlaceholder: this.$pgettext('Content/Search/Input.Placeholder', 'Search by domain, actor, name, description…')
+      }
+    },
+    actionFilters () {
+      var currentFilters = {
+        q: this.search.query
+      }
+      if (this.filters) {
+        return _.merge(currentFilters, this.filters)
+      } else {
+        return currentFilters
+      }
+    },
+    actions () {
+      let deleteLabel = this.$pgettext('*/*/*/Verb', 'Delete')
+      let confirmationMessage = this.$pgettext('Popup/*/Paragraph', 'The selected library will be removed, as well as associated uploads and follows. This action is irreversible.')
+      return [
+        {
+          name: 'delete',
+          label: deleteLabel,
+          confirmationMessage: confirmationMessage,
+          isDangerous: true,
+          allowAll: false,
+          confirmColor: 'red',
+        },
+      ]
+    }
+  },
+  watch: {
+    search (newValue) {
+      this.page = 1
+      this.fetchData()
+    },
+    page () {
+      this.fetchData()
+    },
+    ordering () {
+      this.fetchData()
+    },
+    orderingDirection () {
+      this.fetchData()
+    }
+  }
+}
+</script>
diff --git a/front/src/components/manage/library/UploadsTable.vue b/front/src/components/manage/library/UploadsTable.vue
new file mode 100644
index 0000000000000000000000000000000000000000..efc4e23944387bf84ae8330452a561d341e90dfe
--- /dev/null
+++ b/front/src/components/manage/library/UploadsTable.vue
@@ -0,0 +1,285 @@
+<template>
+  <div>
+    <div class="ui inline form">
+      <div class="fields">
+        <div class="ui six wide field">
+          <label><translate translate-context="Content/Search/Input.Label/Noun">Search</translate></label>
+          <form @submit.prevent="search.query = $refs.search.value">
+            <input name="search" ref="search" type="text" :value="search.query" :placeholder="labels.searchPlaceholder" />
+          </form>
+        </div>
+        <div class="field">
+          <label><translate translate-context="*/*/*">Visibility</translate></label>
+          <select class="ui dropdown" @change="addSearchToken('privacy_level', $event.target.value)" :value="getTokenValue('privacy_level', '')">
+            <option value=""><translate translate-context="Content/*/Dropdown">All</translate></option>
+            <option value="me">{{ sharedLabels.fields.privacy_level.shortChoices.me }}</option>
+            <option value="instance">{{ sharedLabels.fields.privacy_level.shortChoices.instance }}</option>
+            <option value="everyone">{{ sharedLabels.fields.privacy_level.shortChoices.everyone }}</option>
+          </select>
+        </div>
+        <div class="field">
+          <label><translate translate-context="Content/Library/*/Noun">Import status</translate></label>
+          <select class="ui dropdown" @change="addSearchToken('status', $event.target.value)" :value="getTokenValue('status', '')">
+            <option value=""><translate translate-context="Content/*/Dropdown">All</translate></option>
+            <option value="pending"><translate translate-context="Content/Library/*/Short">Pending</translate></option>
+            <option value="skipped"><translate translate-context="Content/Library/*">Skipped</translate></option>
+            <option value="errored"><translate translate-context="Content/Library/Dropdown">Failed</translate></option>
+            <option value="finished"><translate translate-context="Content/Library/*">Finished</translate></option>
+          </select>
+        </div>
+        <div class="field">
+          <label><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering</translate></label>
+          <select class="ui dropdown" v-model="ordering">
+            <option v-for="option in orderingOptions" :value="option[0]">
+              {{ sharedLabels.filters[option[1]] }}
+            </option>
+          </select>
+        </div>
+        <div class="field">
+          <label><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering direction</translate></label>
+          <select class="ui dropdown" v-model="orderingDirection">
+            <option value="+"><translate translate-context="Content/Search/Dropdown">Ascending</translate></option>
+            <option value="-"><translate translate-context="Content/Search/Dropdown">Descending</translate></option>
+          </select>
+        </div>
+      </div>
+    </div>
+    <import-status-modal :upload="detailedUpload" :show.sync="showUploadDetailModal" />
+    <div class="dimmable">
+      <div v-if="isLoading" class="ui active inverted dimmer">
+          <div class="ui loader"></div>
+      </div>
+      <action-table
+        v-if="result"
+        @action-launched="fetchData"
+        :objects-data="result"
+        :actions="actions"
+        action-url="manage/library/uploads/action/"
+        :filters="actionFilters">
+        <template slot="header-cells">
+          <th><translate translate-context="*/*/*">Name</translate></th>
+          <th><translate translate-context="*/*/*">Library</translate></th>
+          <th><translate translate-context="*/*/*">Account</translate></th>
+          <th><translate translate-context="Content/Moderation/*/Noun">Domain</translate></th>
+          <th><translate translate-context="*/*/*">Visibility</translate></th>
+          <th><translate translate-context="Content/*/*/Noun">Import status</translate></th>
+          <th><translate translate-context="Content/*/*/Noun">Size</translate></th>
+          <th><translate translate-context="Content/*/*/Noun">Creation date</translate></th>
+          <th><translate translate-context="Content/*/*/Noun">Accessed date</translate></th>
+        </template>
+        <template slot="row-cells" slot-scope="scope">
+          <td>
+            <router-link :to="{name: 'manage.library.uploads.detail', params: {id: scope.obj.uuid }}" :title="displayName(scope.obj)">
+              {{ displayName(scope.obj)|truncate(30, "…", true) }}
+            </router-link>
+          </td>
+          <td>
+            <router-link :to="{name: 'manage.library.libraries.detail', params: {id: scope.obj.library.uuid }}">
+              <i class="wrench icon"></i>
+            </router-link>
+            <span role="button" class="discrete link"
+              @click="addSearchToken('library_id', scope.obj.library.id)"
+              :title="scope.obj.library.name">
+              {{ scope.obj.library.name | truncate(20) }}
+            </span>
+          </td>
+          <td>
+            <router-link :to="{name: 'manage.moderation.accounts.detail', params: {id: scope.obj.library.actor.full_username }}">
+            </router-link>
+            <span role="button" class="discrete link" @click="addSearchToken('account', scope.obj.library.actor.full_username)" :title="scope.obj.library.actor.full_username">{{ scope.obj.library.actor.preferred_username }}</span>
+          </td>
+          <td>
+            <template v-if="!scope.obj.is_local">
+              <router-link :to="{name: 'manage.moderation.domains.detail', params: {id: scope.obj.domain }}">
+                <i class="wrench icon"></i>
+              </router-link>
+              <span role="button" class="discrete link" @click="addSearchToken('domain', scope.obj.domain)" :title="scope.obj.domain">{{ scope.obj.domain }}</span>
+            </template>
+            <span role="button" v-else class="ui tiny teal icon link label" @click="addSearchToken('domain', scope.obj.domain)">
+              <i class="home icon"></i>
+              <translate translate-context="Content/Moderation/*/Short, Noun">Local</translate>
+            </span>
+          </td>
+          <td>
+            <span
+              role="button"
+              class="discrete link"
+              @click="addSearchToken('privacy_level', scope.obj.library.privacy_level)"
+              :title="sharedLabels.fields.privacy_level.shortChoices[scope.obj.library.privacy_level]">
+              {{ sharedLabels.fields.privacy_level.shortChoices[scope.obj.library.privacy_level] }}
+            </span>
+          </td>
+          <td>
+            <span class="discrete link" @click="addSearchToken('status', scope.obj.import_status)" :title="sharedLabels.fields.import_status.choices[scope.obj.import_status].help">
+              {{ sharedLabels.fields.import_status.choices[scope.obj.import_status].label }}
+            </span>
+            <button class="ui tiny basic icon button" :title="sharedLabels.fields.import_status.detailTitle" @click="detailedUpload = scope.obj; showUploadDetailModal = true">
+              <i class="question circle outline icon"></i>
+            </button>
+          </td>
+          <td>
+            <span v-if="scope.obj.size">{{ scope.obj.size | humanSize }}</span>
+            <translate v-else translate-context="*/*/*">N/A</translate>
+          </td>
+          <td>
+            <human-date :date="scope.obj.creation_date"></human-date>
+          </td>
+          <td>
+            <human-date v-if="scope.obj.accessed_date" :date="scope.obj.accessed_date"></human-date>
+            <translate v-else translate-context="*/*/*">N/A</translate>
+          </td>
+        </template>
+      </action-table>
+    </div>
+    <div>
+      <pagination
+        v-if="result && result.count > paginateBy"
+        @page-changed="selectPage"
+        :compact="true"
+        :current="page"
+        :paginate-by="paginateBy"
+        :total="result.count"
+        ></pagination>
+
+      <span v-if="result && result.results.length > 0">
+        <translate translate-context="Content/*/Paragraph"
+          :translate-params="{start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}">
+          Showing results %{ start }-%{ end } on %{ total }
+        </translate>
+      </span>
+    </div>
+  </div>
+</template>
+
+<script>
+import axios from 'axios'
+import _ from '@/lodash'
+import time from '@/utils/time'
+import {normalizeQuery, parseTokens} from '@/search'
+import Pagination from '@/components/Pagination'
+import ActionTable from '@/components/common/ActionTable'
+import OrderingMixin from '@/components/mixins/Ordering'
+import TranslationsMixin from '@/components/mixins/Translations'
+import SmartSearchMixin from '@/components/mixins/SmartSearch'
+import ImportStatusModal from '@/components/library/ImportStatusModal'
+
+
+export default {
+  mixins: [OrderingMixin, TranslationsMixin, SmartSearchMixin],
+  props: {
+    filters: {type: Object, required: false},
+  },
+  components: {
+    Pagination,
+    ActionTable,
+    ImportStatusModal
+  },
+  data () {
+    let defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date')
+    return {
+      detailedUpload: null,
+      showUploadDetailModal: false,
+      time,
+      isLoading: false,
+      result: null,
+      page: 1,
+      paginateBy: 50,
+      search: {
+        query: this.defaultQuery,
+        tokens: parseTokens(normalizeQuery(this.defaultQuery))
+      },
+      orderingDirection: defaultOrdering.direction || '+',
+      ordering: defaultOrdering.field,
+      orderingOptions: [
+        ['creation_date', 'creation_date'],
+        ['modification_date', 'modification_date'],
+        ['accessed_date', 'accessed_date'],
+        ['size', 'size'],
+        ['bitrate', 'bitrate'],
+        ['duration', 'duration'],
+      ]
+    }
+  },
+  created () {
+    this.fetchData()
+  },
+  methods: {
+    fetchData () {
+      let params = _.merge({
+        'page': this.page,
+        'page_size': this.paginateBy,
+        'q': this.search.query,
+        'ordering': this.getOrderingAsString()
+      }, this.filters)
+      let self = this
+      self.isLoading = true
+      self.checked = []
+      axios.get('/manage/library/uploads/', {params: params}).then((response) => {
+        self.result = response.data
+        self.isLoading = false
+      }, error => {
+        self.isLoading = false
+        self.errors = error.backendErrors
+      })
+    },
+    selectPage: function (page) {
+      this.page = page
+    },
+    displayName (upload) {
+      if (upload.filename) {
+        return upload.filename
+      }
+      if (upload.source) {
+        return upload.source
+      }
+      return upload.uuid
+    }
+  },
+  computed: {
+    labels () {
+      return {
+        searchPlaceholder: this.$pgettext('Content/Search/Input.Placeholder', 'Search by domain, actor, name, reference, source…')
+      }
+    },
+    actionFilters () {
+      var currentFilters = {
+        q: this.search.query
+      }
+      if (this.filters) {
+        return _.merge(currentFilters, this.filters)
+      } else {
+        return currentFilters
+      }
+    },
+    actions () {
+      let deleteLabel = this.$pgettext('*/*/*/Verb', 'Delete')
+      let confirmationMessage = this.$pgettext('Popup/*/Paragraph', 'The selected upload will be removed. This action is irreversible.')
+      return [
+        {
+          name: 'delete',
+          label: deleteLabel,
+          confirmationMessage: confirmationMessage,
+          isDangerous: true,
+          allowAll: false,
+          confirmColor: 'red',
+        },
+      ]
+    }
+  },
+  watch: {
+    search (newValue) {
+      this.page = 1
+      this.fetchData()
+    },
+    page () {
+      this.fetchData()
+    },
+    ordering () {
+      this.fetchData()
+    },
+    orderingDirection () {
+      this.fetchData()
+    }
+  }
+}
+</script>
diff --git a/front/src/components/mixins/Translations.vue b/front/src/components/mixins/Translations.vue
index b2bd455cc64e916a11f36a361a0573db3a88d5e1..56ea3ed152642d6b5fa99e415d42625262571224 100644
--- a/front/src/components/mixins/Translations.vue
+++ b/front/src/components/mixins/Translations.vue
@@ -11,12 +11,39 @@ export default {
               me: this.$pgettext('Content/Settings/Dropdown', 'Nobody except me'),
               instance: this.$pgettext('Content/Settings/Dropdown', 'Everyone on this instance'),
               everyone: this.$pgettext('Content/Settings/Dropdown', 'Everyone, across all instances'),
+            },
+            shortChoices: {
+              me: this.$pgettext('Content/Settings/Dropdown/Short', 'Private'),
+              instance: this.$pgettext('Content/Settings/Dropdown/Short', 'Instance'),
+              everyone: this.$pgettext('Content/Settings/Dropdown/Short', 'Everyone'),
             }
-          }
+          },
+          import_status: {
+            detailTitle: this.$pgettext('Content/Library/Link.Title', 'Click to display more information about the import process for this upload'),
+            choices: {
+              skipped: {
+                label: this.$pgettext('Content/Library/*', 'Skipped'),
+                help: this.$pgettext('Content/Library/Help text', 'This track is already present in one of your libraries'),
+              },
+              pending: {
+                label: this.$pgettext('Content/Library/*/Short', 'Pending'),
+                help: this.$pgettext('Content/Library/Help text', 'This track has been uploaded, but hasn\'t been processed by the server yet'),
+              },
+              errored: {
+                label: this.$pgettext('Content/Library/Table/Short', 'Errored'),
+                help: this.$pgettext('Content/Library/Help text', 'This track could not be processed, please it is tagged correctly'),
+              },
+              finished: {
+                label: this.$pgettext('Content/Library/*', 'Finished'),
+                help: this.$pgettext('Content/Library/Help text', 'Imported'),
+              },
+            }
+          },
         },
         filters: {
           creation_date: this.$pgettext('Content/*/*/Noun', 'Creation date'),
           release_date: this.$pgettext('Content/*/*/Noun', 'Release date'),
+          accessed_date: this.$pgettext('Content/*/*/Noun', 'Accessed date'),
           first_seen: this.$pgettext('Content/Moderation/Dropdown/Noun', 'First seen date'),
           last_seen: this.$pgettext('Content/Moderation/Dropdown/Noun', 'Last seen date'),
           modification_date: this.$pgettext('Content/Playlist/Dropdown/Noun', 'Modification date'),
diff --git a/front/src/filters.js b/front/src/filters.js
index 1edea76f6afb6b8f929b7ab0566b15d4f1813e75..9667426191e6fae0aa76ba9eba47dcad023730fe 100644
--- a/front/src/filters.js
+++ b/front/src/filters.js
@@ -2,13 +2,24 @@ import Vue from 'vue'
 
 import moment from 'moment'
 
-export function truncate (str, max, ellipsis) {
+export function truncate (str, max, ellipsis, middle) {
   max = max || 100
   ellipsis = ellipsis || '…'
   if (str.length <= max) {
     return str
   }
-  return str.slice(0, max) + ellipsis
+  if (middle) {
+    var sepLen = 1,
+        charsToShow = max - sepLen,
+        frontChars = Math.ceil(charsToShow/2),
+        backChars = Math.floor(charsToShow/2);
+
+    return str.substr(0, frontChars) +
+           ellipsis +
+           str.substr(str.length - backChars);
+  } else {
+    return str.slice(0, max) + ellipsis
+  }
 }
 
 Vue.filter('truncate', truncate)
diff --git a/front/src/router/index.js b/front/src/router/index.js
index f9332f5f556683067ae0fe03b187a2826d6e0e41..4b59deacc2ab731045b2eacb5dfef49095bd4bf9 100644
--- a/front/src/router/index.js
+++ b/front/src/router/index.js
@@ -43,6 +43,10 @@ import AdminLibraryAlbumsList from '@/views/admin/library/AlbumsList'
 import AdminLibraryAlbumDetail from '@/views/admin/library/AlbumDetail'
 import AdminLibraryTracksList from '@/views/admin/library/TracksList'
 import AdminLibraryTrackDetail from '@/views/admin/library/TrackDetail'
+import AdminLibraryLibrariesList from '@/views/admin/library/LibrariesList'
+import AdminLibraryLibraryDetail from '@/views/admin/library/LibraryDetail'
+import AdminLibraryUploadsList from '@/views/admin/library/UploadsList'
+import AdminLibraryUploadDetail from '@/views/admin/library/UploadDetail'
 import AdminUsersBase from '@/views/admin/users/Base'
 import AdminUsersList from '@/views/admin/users/UsersList'
 import AdminInvitationsList from '@/views/admin/users/InvitationsList'
@@ -303,6 +307,38 @@ export default new Router({
           component: AdminLibraryTrackDetail,
           props: true
         },
+        {
+          path: 'libraries',
+          name: 'manage.library.libraries',
+          component: AdminLibraryLibrariesList,
+          props: (route) => {
+            return {
+              defaultQuery: route.query.q,
+            }
+          }
+        },
+        {
+          path: 'libraries/:id',
+          name: 'manage.library.libraries.detail',
+          component: AdminLibraryLibraryDetail,
+          props: true
+        },
+        {
+          path: 'uploads',
+          name: 'manage.library.uploads',
+          component: AdminLibraryUploadsList,
+          props: (route) => {
+            return {
+              defaultQuery: route.query.q,
+            }
+          }
+        },
+        {
+          path: 'uploads/:id',
+          name: 'manage.library.uploads.detail',
+          component: AdminLibraryUploadDetail,
+          props: true
+        },
       ]
     },
     {
diff --git a/front/src/views/admin/library/AlbumDetail.vue b/front/src/views/admin/library/AlbumDetail.vue
index b5d802d98591e32dbde7148271535286000470c1..8de907a72bcb8d07671156e27939cc2195028c6c 100644
--- a/front/src/views/admin/library/AlbumDetail.vue
+++ b/front/src/views/admin/library/AlbumDetail.vue
@@ -4,7 +4,7 @@
       <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
     </div>
     <template v-if="object">
-      <section :class="['ui', 'head', 'vertical', 'stripe', 'segment']" v-title="object.name">
+      <section :class="['ui', 'head', 'vertical', 'stripe', 'segment']" v-title="object.title">
         <div class="ui stackable one column grid">
           <div class="ui column">
             <div class="segment-content">
@@ -113,23 +113,15 @@
                       {{ object.artist.name }}
                     </td>
                   </tr>
-                  <tr>
-                    <td>
-                      <translate translate-context="Content/Moderation/Table.Label/Short (Value is a date)">First seen</translate>
-                    </td>
-                    <td>
-                      <human-date :date="object.creation_date"></human-date>
-                    </td>
-                  </tr>
                   <tr v-if="!object.is_local">
-                    <td>
-                      <translate translate-context="Content/Moderation/*/Noun">Domain</translate>
-                    </td>
                     <td>
                       <router-link :to="{name: 'manage.moderation.domains.detail', params: {id: object.domain }}">
-                        {{ object.domain }}
+                        <translate translate-context="Content/Moderation/*/Noun">Domain</translate>
                       </router-link>
                     </td>
+                    <td>
+                      {{ object.domain }}
+                    </td>
                   </tr>
                 </tbody>
               </table>
@@ -153,6 +145,14 @@
               </div>
               <table v-else class="ui very basic table">
                 <tbody>
+                  <tr>
+                    <td>
+                      <translate translate-context="Content/Moderation/Table.Label/Short (Value is a date)">First seen</translate>
+                    </td>
+                    <td>
+                      <human-date :date="object.creation_date"></human-date>
+                    </td>
+                  </tr>
                   <tr>
                     <td>
                       <translate translate-context="*/*/*/Noun">Listenings</translate>
@@ -229,7 +229,9 @@
 
                   <tr>
                     <td>
-                      <translate translate-context="*/*/*/Noun">Libraries</translate>
+                      <router-link :to="{name: 'manage.library.libraries', query: {q: getQuery('album_id', object.id) }}">
+                        <translate translate-context="*/*/*/Noun">Libraries</translate>
+                      </router-link>
                     </td>
                     <td>
                       {{ stats.libraries }}
@@ -237,7 +239,9 @@
                   </tr>
                   <tr>
                     <td>
-                      <translate translate-context="Content/Moderation/Table.Label/Noun">Uploads</translate>
+                      <router-link :to="{name: 'manage.library.uploads', query: {q: getQuery('album_id', object.id) }}">
+                        <translate translate-context="Content/Moderation/Table.Label/Noun">Uploads</translate>
+                      </router-link>
                     </td>
                     <td>
                       {{ stats.uploads }}
diff --git a/front/src/views/admin/library/ArtistDetail.vue b/front/src/views/admin/library/ArtistDetail.vue
index d509f7394ad516492e17f4bd625b5e78866b5001..91b3542b2dc1c93721f4036699f6f84aa90643cb 100644
--- a/front/src/views/admin/library/ArtistDetail.vue
+++ b/front/src/views/admin/library/ArtistDetail.vue
@@ -102,23 +102,15 @@
                       {{ object.name }}
                     </td>
                   </tr>
-                  <tr>
-                    <td>
-                      <translate translate-context="Content/Moderation/Table.Label/Short (Value is a date)">First seen</translate>
-                    </td>
-                    <td>
-                      <human-date :date="object.creation_date"></human-date>
-                    </td>
-                  </tr>
                   <tr v-if="!object.is_local">
-                    <td>
-                      <translate translate-context="Content/Moderation/*/Noun">Domain</translate>
-                    </td>
                     <td>
                       <router-link :to="{name: 'manage.moderation.domains.detail', params: {id: object.domain }}">
-                        {{ object.domain }}
+                        <translate translate-context="Content/Moderation/*/Noun">Domain</translate>
                       </router-link>
                     </td>
+                    <td>
+                      {{ object.domain }}
+                    </td>
                   </tr>
                 </tbody>
               </table>
@@ -142,6 +134,14 @@
               </div>
               <table v-else class="ui very basic table">
                 <tbody>
+                  <tr>
+                    <td>
+                      <translate translate-context="Content/Moderation/Table.Label/Short (Value is a date)">First seen</translate>
+                    </td>
+                    <td>
+                      <human-date :date="object.creation_date"></human-date>
+                    </td>
+                  </tr>
                   <tr>
                     <td>
                       <translate translate-context="*/*/*/Noun">Listenings</translate>
@@ -218,7 +218,9 @@
 
                   <tr>
                     <td>
-                      <translate translate-context="*/*/*/Noun">Libraries</translate>
+                      <router-link :to="{name: 'manage.library.libraries', query: {q: getQuery('artist_id', object.id) }}">
+                        <translate translate-context="*/*/*/Noun">Libraries</translate>
+                      </router-link>
                     </td>
                     <td>
                       {{ stats.libraries }}
@@ -226,7 +228,9 @@
                   </tr>
                   <tr>
                     <td>
-                      <translate translate-context="Content/Moderation/Table.Label/Noun">Uploads</translate>
+                      <router-link :to="{name: 'manage.library.uploads', query: {q: getQuery('artist_id', object.id) }}">
+                        <translate translate-context="Content/Moderation/Table.Label/Noun">Uploads</translate>
+                      </router-link>
                     </td>
                     <td>
                       {{ stats.uploads }}
diff --git a/front/src/views/admin/library/Base.vue b/front/src/views/admin/library/Base.vue
index 4b7048a17bbe0cd6d187d49f1a6e0e606be05a9c..009e1ca95c9f8bc615c73c37f808887b14949711 100644
--- a/front/src/views/admin/library/Base.vue
+++ b/front/src/views/admin/library/Base.vue
@@ -13,6 +13,12 @@
       <router-link
         class="ui item"
         :to="{name: 'manage.library.tracks'}"><translate translate-context="*/*/*">Tracks</translate></router-link>
+      <router-link
+        class="ui item"
+        :to="{name: 'manage.library.libraries'}"><translate translate-context="*/*/*">Libraries</translate></router-link>
+      <router-link
+        class="ui item"
+        :to="{name: 'manage.library.uploads'}"><translate translate-context="*/*/*">Uploads</translate></router-link>
     </nav>
     <router-view :key="$route.fullPath"></router-view>
   </div>
diff --git a/front/src/views/admin/library/LibrariesList.vue b/front/src/views/admin/library/LibrariesList.vue
new file mode 100644
index 0000000000000000000000000000000000000000..495a660c19609fde101fff87b2774f6ca7890c3c
--- /dev/null
+++ b/front/src/views/admin/library/LibrariesList.vue
@@ -0,0 +1,29 @@
+<template>
+  <main v-title="labels.title">
+    <section class="ui vertical stripe segment">
+      <h2 class="ui header">{{ labels.title }}</h2>
+      <div class="ui hidden divider"></div>
+      <libraries-table :update-url="true" :default-query="defaultQuery"></libraries-table>
+    </section>
+  </main>
+</template>
+
+<script>
+import LibrariesTable from "@/components/manage/library/LibrariesTable"
+
+export default {
+  components: {
+    LibrariesTable
+  },
+  props: {
+    defaultQuery: {type: String, required: false},
+  },
+  computed: {
+    labels() {
+      return {
+        title: this.$pgettext('*/*/*', 'Libraries')
+      }
+    }
+  }
+}
+</script>
diff --git a/front/src/views/admin/library/LibraryDetail.vue b/front/src/views/admin/library/LibraryDetail.vue
new file mode 100644
index 0000000000000000000000000000000000000000..a4df048da668db2f77ef304052204d09d69f4bf1
--- /dev/null
+++ b/front/src/views/admin/library/LibraryDetail.vue
@@ -0,0 +1,321 @@
+<template>
+  <main>
+    <div v-if="isLoading" class="ui vertical segment">
+      <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
+    </div>
+    <template v-if="object">
+      <section :class="['ui', 'head', 'vertical', 'stripe', 'segment']" v-title="object.name">
+        <div class="ui stackable one column grid">
+          <div class="ui column">
+            <div class="segment-content">
+              <h2 class="ui header">
+                <i class="circular inverted book icon"></i>
+                <div class="content">
+                  {{ object.name | truncate(100) }}
+                  <div class="sub header">
+                    <template v-if="object.is_local">
+                      <span class="ui tiny teal label">
+                        <i class="home icon"></i>
+                        <translate translate-context="Content/Moderation/*/Short, Noun">Local</translate>
+                      </span>
+                      &nbsp;
+                    </template>
+                  </div>
+                </div>
+              </h2>
+              <div class="header-buttons">
+
+                <div class="ui icon buttons">
+                  <a
+                    v-if="$store.state.auth.profile.is_superuser"
+                    class="ui labeled icon button"
+                    :href="$store.getters['instance/absoluteUrl'](`/api/admin/music/library/${object.id}`)"
+                    target="_blank" rel="noopener noreferrer">
+                    <i class="wrench icon"></i>
+                    <translate translate-context="Content/Moderation/Link/Verb">View in Django's admin</translate>&nbsp;
+                  </a>
+                  <div class="ui floating dropdown icon button" v-dropdown>
+                    <i class="dropdown icon"></i>
+                    <div class="menu">
+                      <a
+                        v-if="$store.state.auth.profile.is_superuser"
+                        class="basic item"
+                        :href="$store.getters['instance/absoluteUrl'](`/api/admin/music/library/${object.id}`)"
+                        target="_blank" rel="noopener noreferrer">
+                        <i class="wrench icon"></i>
+                        <translate translate-context="Content/Moderation/Link/Verb">View in Django's admin</translate>&nbsp;
+                      </a>
+                      <a class="basic item" :href="object.url || object.fid" target="_blank" rel="noopener noreferrer">
+                        <i class="external icon"></i>
+                        <translate translate-context="Content/Moderation/Link/Verb">Open remote profile</translate>&nbsp;
+                      </a>
+                    </div>
+                  </div>
+                </div>
+                <div class="ui buttons">
+                  <dangerous-button
+                    :class="['ui', {loading: isLoading}, 'basic button']"
+                    :action="remove">
+                    <translate translate-context="*/*/*/Verb">Delete</translate>
+                    <p slot="modal-header"><translate translate-context="Popup/Library/Title">Delete this library?</translate></p>
+                    <div slot="modal-content">
+                      <p><translate translate-context="Content/Moderation/Paragraph">The library will be removed, as well as associated uploads, and follows. This action is irreversible.</translate></p>
+                    </div>
+                    <p slot="modal-confirm"><translate translate-context="*/*/*/Verb">Delete</translate></p>
+                  </dangerous-button>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </section>
+      <div class="ui vertical stripe segment">
+        <div class="ui stackable three column grid">
+          <div class="column">
+            <section>
+              <h3 class="ui header">
+                <i class="info icon"></i>
+                <div class="content">
+                  <translate translate-context="Content/Moderation/Title">Library data</translate>
+                </div>
+              </h3>
+              <table class="ui very basic table">
+                <tbody>
+                  <tr>
+                    <td>
+                      <translate translate-context="*/*/*/Noun">Name</translate>
+                    </td>
+                    <td>
+                      {{ object.name }}
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <router-link :to="{name: 'manage.library.libraries', query: {q: getQuery('privacy_level', object.privacy_level) }}">
+                        <translate translate-context="*/*/*">Visibility</translate>
+                      </router-link>
+                    </td>
+                    <td>
+                      {{ sharedLabels.fields.privacy_level.shortChoices[object.privacy_level] }}
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <router-link :to="{name: 'manage.moderation.accounts.detail', params: {id: object.actor.full_username }}">
+                        <translate translate-context="*/*/*/Noun">Account</translate>
+                      </router-link>
+                    </td>
+                    <td>
+                      {{ object.actor.preferred_username }}
+                    </td>
+                  </tr>
+                  <tr v-if="!object.is_local">
+                    <td>
+                      <router-link :to="{name: 'manage.moderation.domains.detail', params: {id: object.domain }}">
+                        <translate translate-context="Content/Moderation/*/Noun">Domain</translate>
+                      </router-link>
+                    </td>
+                    <td>
+                      {{ object.domain }}
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <translate translate-context="*/*/*/Noun">Description</translate>
+                    </td>
+                    <td>
+                      {{ object.description }}
+                    </td>
+                  </tr>
+                </tbody>
+              </table>
+            </section>
+          </div>
+          <div class="column">
+            <section>
+              <h3 class="ui header">
+                <i class="feed icon"></i>
+                <div class="content">
+                  <translate translate-context="Content/Moderation/Title">Activity</translate>&nbsp;
+                  <span :data-tooltip="labels.statsWarning"><i class="question circle icon"></i></span>
+
+                </div>
+              </h3>
+              <div v-if="isLoadingStats" class="ui placeholder">
+                <div class="full line"></div>
+                <div class="short line"></div>
+                <div class="medium line"></div>
+                <div class="long line"></div>
+              </div>
+              <table v-else class="ui very basic table">
+                <tbody>
+                  <tr>
+                    <td>
+                      <translate translate-context="Content/Moderation/Table.Label/Short (Value is a date)">First seen</translate>
+                    </td>
+                    <td>
+                      <human-date :date="object.creation_date"></human-date>
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <translate translate-context="Content/Federation/*/Noun">Followers</translate>
+                    </td>
+                    <td>
+                      {{ stats.followers }}
+                    </td>
+                  </tr>
+                </tbody>
+              </table>
+            </section>
+          </div>
+          <div class="column">
+            <section>
+              <h3 class="ui header">
+                <i class="music icon"></i>
+                <div class="content">
+                  <translate translate-context="Content/Moderation/Title">Audio content</translate>&nbsp;
+                  <span :data-tooltip="labels.statsWarning"><i class="question circle icon"></i></span>
+
+                </div>
+              </h3>
+              <div v-if="isLoadingStats" class="ui placeholder">
+                <div class="full line"></div>
+                <div class="short line"></div>
+                <div class="medium line"></div>
+                <div class="long line"></div>
+              </div>
+              <table v-else class="ui very basic table">
+                <tbody>
+
+                  <tr>
+                    <td>
+                      <translate translate-context="Content/Moderation/Table.Label/Noun">Cached size</translate>
+                    </td>
+                    <td>
+                      {{ stats.media_downloaded_size | humanSize }}
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <translate translate-context="Content/Moderation/Table.Label">Total size</translate>
+                    </td>
+                    <td>
+                      {{ stats.media_total_size | humanSize }}
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <router-link :to="{name: 'manage.library.artists', query: {q: getQuery('library_id', object.id) }}">
+                        <translate translate-context="*/*/*">Artists</translate>
+                      </router-link>
+                    </td>
+                    <td>
+                      {{ stats.artists }}
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <router-link :to="{name: 'manage.library.albums', query: {q: getQuery('library_id', object.id) }}">
+                        <translate translate-context="*/*/*">Albums</translate>
+                      </router-link>
+                    </td>
+                    <td>
+                      {{ stats.albums }}
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <router-link :to="{name: 'manage.library.tracks', query: {q: getQuery('library_id', object.id) }}">
+                        <translate translate-context="*/*/*">Tracks</translate>
+                      </router-link>
+                    </td>
+                    <td>
+                      {{ stats.tracks }}
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <router-link :to="{name: 'manage.library.uploads', query: {q: getQuery('library_id', object.id) }}">
+                        <translate translate-context="Content/Moderation/Table.Label/Noun">Uploads</translate>
+                      </router-link>
+                    </td>
+                    <td>
+                      {{ stats.uploads }}
+                    </td>
+                  </tr>
+                </tbody>
+              </table>
+
+            </section>
+          </div>
+        </div>
+      </div>
+
+    </template>
+  </main>
+</template>
+
+<script>
+import axios from "axios"
+import logger from "@/logging"
+import TranslationsMixin from "@/components/mixins/Translations"
+
+
+export default {
+  props: ["id"],
+  mixins: [
+    TranslationsMixin
+  ],
+  data() {
+    return {
+      isLoading: true,
+      isLoadingStats: false,
+      object: null,
+      stats: null,
+    }
+  },
+  created() {
+    this.fetchData()
+    this.fetchStats()
+  },
+  methods: {
+    fetchData() {
+      var self = this
+      this.isLoading = true
+      let url = `manage/library/libraries/${this.id}/`
+      axios.get(url).then(response => {
+        self.object = response.data
+        self.isLoading = false
+      })
+    },
+    fetchStats() {
+      var self = this
+      this.isLoadingStats = true
+      let url = `manage/library/libraries/${this.id}/stats/`
+      axios.get(url).then(response => {
+        self.stats = response.data
+        self.isLoadingStats = false
+      })
+    },
+    remove () {
+      var self = this
+      this.isLoading = true
+      let url = `manage/library/libraries/${this.id}/`
+      axios.delete(url).then(response => {
+        self.$router.push({name: 'manage.library.libraries'})
+      })
+    },
+    getQuery (field, value) {
+      return `${field}:"${value}"`
+    }
+  },
+  computed: {
+    labels() {
+      return {
+        statsWarning: this.$pgettext('Content/Moderation/Help text', 'Statistics are computed from known activity and content on your instance, and do not reflect general activity for this object'),
+      }
+    },
+  }
+}
+</script>
diff --git a/front/src/views/admin/library/TrackDetail.vue b/front/src/views/admin/library/TrackDetail.vue
index 15e08f7a8f169537e9eb373446e66f251385d420..3256d0d639a88a65839186c627533c5c8bd5d499 100644
--- a/front/src/views/admin/library/TrackDetail.vue
+++ b/front/src/views/admin/library/TrackDetail.vue
@@ -4,7 +4,7 @@
       <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
     </div>
     <template v-if="object">
-      <section :class="['ui', 'head', 'vertical', 'stripe', 'segment']" v-title="object.name">
+      <section :class="['ui', 'head', 'vertical', 'stripe', 'segment']" v-title="object.title">
         <div class="ui stackable one column grid">
           <div class="ui column">
             <div class="segment-content">
@@ -133,14 +133,6 @@
                       {{ object.album.artist.name }}
                     </td>
                   </tr>
-                  <tr>
-                    <td>
-                      <translate translate-context="Content/Moderation/Table.Label/Short (Value is a date)">First seen</translate>
-                    </td>
-                    <td>
-                      <human-date :date="object.creation_date"></human-date>
-                    </td>
-                  </tr>
                   <tr>
                     <td>
                       <translate translate-context="*/*/*/Noun">Position</translate>
@@ -174,14 +166,14 @@
                     </td>
                   </tr>
                   <tr v-if="!object.is_local">
-                    <td>
-                      <translate translate-context="Content/Moderation/*/Noun">Domain</translate>
-                    </td>
                     <td>
                       <router-link :to="{name: 'manage.moderation.domains.detail', params: {id: object.domain }}">
-                        {{ object.domain }}
+                        <translate translate-context="Content/Moderation/*/Noun">Domain</translate>
                       </router-link>
                     </td>
+                    <td>
+                      {{ object.domain }}
+                    </td>
                   </tr>
                 </tbody>
               </table>
@@ -205,6 +197,14 @@
               </div>
               <table v-else class="ui very basic table">
                 <tbody>
+                  <tr>
+                    <td>
+                      <translate translate-context="Content/Moderation/Table.Label/Short (Value is a date)">First seen</translate>
+                    </td>
+                    <td>
+                      <human-date :date="object.creation_date"></human-date>
+                    </td>
+                  </tr>
                   <tr>
                     <td>
                       <translate translate-context="*/*/*/Noun">Listenings</translate>
@@ -281,7 +281,9 @@
 
                   <tr>
                     <td>
-                      <translate translate-context="*/*/*/Noun">Libraries</translate>
+                      <router-link :to="{name: 'manage.library.libraries', query: {q: getQuery('track_id', object.id) }}">
+                        <translate translate-context="*/*/*/Noun">Libraries</translate>
+                      </router-link>
                     </td>
                     <td>
                       {{ stats.libraries }}
@@ -289,7 +291,9 @@
                   </tr>
                   <tr>
                     <td>
-                      <translate translate-context="Content/Moderation/Table.Label/Noun">Uploads</translate>
+                      <router-link :to="{name: 'manage.library.uploads', query: {q: getQuery('track_id', object.id) }}">
+                        <translate translate-context="Content/Moderation/Table.Label/Noun">Uploads</translate>
+                      </router-link>
                     </td>
                     <td>
                       {{ stats.uploads }}
diff --git a/front/src/views/admin/library/UploadDetail.vue b/front/src/views/admin/library/UploadDetail.vue
new file mode 100644
index 0000000000000000000000000000000000000000..604d0af3046312605a06e5bf0e1e0b6b400d5954
--- /dev/null
+++ b/front/src/views/admin/library/UploadDetail.vue
@@ -0,0 +1,340 @@
+<template>
+  <main>
+    <div v-if="isLoading" class="ui vertical segment">
+      <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
+    </div>
+    <template v-if="object">
+      <import-status-modal :upload="object" :show.sync="showUploadDetailModal" />
+      <section :class="['ui', 'head', 'vertical', 'stripe', 'segment']" v-title="displayName(object)">
+        <div class="ui stackable one column grid">
+          <div class="ui column">
+            <div class="segment-content">
+              <h2 class="ui header">
+                <i class="circular inverted file icon"></i>
+                <div class="content">
+                  {{ displayName(object) | truncate(100) }}
+                  <div class="sub header">
+                    <template v-if="object.is_local">
+                      <span class="ui tiny teal label">
+                        <i class="home icon"></i>
+                        <translate translate-context="Content/Moderation/*/Short, Noun">Local</translate>
+                      </span>
+                      &nbsp;
+                    </template>
+                  </div>
+                </div>
+              </h2>
+              <div class="header-buttons">
+
+                <div class="ui icon buttons">
+                  <a
+                    v-if="$store.state.auth.profile.is_superuser"
+                    class="ui labeled icon button"
+                    :href="$store.getters['instance/absoluteUrl'](`/api/admin/music/upload/${object.id}`)"
+                    target="_blank" rel="noopener noreferrer">
+                    <i class="wrench icon"></i>
+                    <translate translate-context="Content/Moderation/Link/Verb">View in Django's admin</translate>&nbsp;
+                  </a>
+                  <div class="ui floating dropdown icon button" v-dropdown>
+                    <i class="dropdown icon"></i>
+                    <div class="menu">
+                      <a
+                        v-if="$store.state.auth.profile.is_superuser"
+                        class="basic item"
+                        :href="$store.getters['instance/absoluteUrl'](`/api/admin/music/upload/${object.id}`)"
+                        target="_blank" rel="noopener noreferrer">
+                        <i class="wrench icon"></i>
+                        <translate translate-context="Content/Moderation/Link/Verb">View in Django's admin</translate>&nbsp;
+                      </a>
+                      <a class="basic item" :href="object.url || object.fid" target="_blank" rel="noopener noreferrer">
+                        <i class="external icon"></i>
+                        <translate translate-context="Content/Moderation/Link/Verb">Open remote profile</translate>&nbsp;
+                      </a>
+                    </div>
+                  </div>
+                </div>
+                <div class="ui buttons">
+                  <a class="ui labeled icon button" v-if="object.audio_file" :href="$store.getters['instance/absoluteUrl'](object.audio_file)" target="_blank" rel="noopener noreferrer">
+                    <i class="download icon"></i>
+                    <translate translate-context="Content/Track/Link/Verb">Download</translate>
+                  </a>
+                </div>
+                <div class="ui buttons">
+                  <dangerous-button
+                    :class="['ui', {loading: isLoading}, 'basic button']"
+                    :action="remove">
+                    <translate translate-context="*/*/*/Verb">Delete</translate>
+                    <p slot="modal-header"><translate translate-context="Popup/Library/Title">Delete this upload?</translate></p>
+                    <div slot="modal-content">
+                      <p><translate translate-context="Content/Moderation/Paragraph">The upload will be removed. This action is irreversible.</translate></p>
+                    </div>
+                    <p slot="modal-confirm"><translate translate-context="*/*/*/Verb">Delete</translate></p>
+                  </dangerous-button>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </section>
+      <div class="ui vertical stripe segment">
+        <div class="ui stackable three column grid">
+          <div class="column">
+            <section>
+              <h3 class="ui header">
+                <i class="info icon"></i>
+                <div class="content">
+                  <translate translate-context="Content/Moderation/Title">Upload data</translate>
+                </div>
+              </h3>
+              <table class="ui very basic table">
+                <tbody>
+                  <tr>
+                    <td>
+                      <translate translate-context="*/*/*/Noun">Name</translate>
+                    </td>
+                    <td>
+                      {{ displayName(object) }}
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <router-link :to="{name: 'manage.library.uploads', query: {q: getQuery('privacy_level', object.library.privacy_level) }}">
+                        <translate translate-context="*/*/*">Visibility</translate>
+                      </router-link>
+                    </td>
+                    <td>
+                      {{ sharedLabels.fields.privacy_level.shortChoices[object.library.privacy_level] }}
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <router-link :to="{name: 'manage.moderation.accounts.detail', params: {id: object.library.actor.full_username }}">
+                        <translate translate-context="*/*/*/Noun">Account</translate>
+                      </router-link>
+                    </td>
+                    <td>
+                      {{ object.library.actor.preferred_username }}
+                    </td>
+                  </tr>
+                  <tr v-if="!object.is_local">
+                    <td>
+                      <router-link :to="{name: 'manage.moderation.domains.detail', params: {id: object.domain }}">
+                        <translate translate-context="Content/Moderation/*/Noun">Domain</translate>
+                      </router-link>
+                    </td>
+                    <td>
+                      {{ object.domain }}
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <router-link :to="{name: 'manage.library.uploads', query: {q: getQuery('status', object.import_status) }}">
+                        <translate translate-context="Content/*/*/Noun">Import status</translate>
+                      </router-link>
+                    </td>
+                    <td>
+                      {{ sharedLabels.fields.import_status.choices[object.import_status].label }}
+                      <button class="ui tiny basic icon button" :title="sharedLabels.fields.import_status.detailTitle" @click="detailedUpload = object; showUploadDetailModal = true">
+                        <i class="question circle outline icon"></i>
+                      </button>
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <router-link :to="{name: 'manage.library.libraries.detail', params: {id: object.library.uuid }}">
+                        <translate translate-context="*/*/*">Library</translate>
+                      </router-link>
+                    </td>
+                    <td>
+                      {{ object.library.name }}
+                    </td>
+                  </tr>
+                </tbody>
+              </table>
+            </section>
+          </div>
+          <div class="column">
+            <section>
+              <h3 class="ui header">
+                <i class="feed icon"></i>
+                <div class="content">
+                  <translate translate-context="Content/Moderation/Title">Activity</translate>&nbsp;
+                </div>
+              </h3>
+              <table class="ui very basic table">
+                <tbody>
+                  <tr>
+                    <td>
+                      <translate translate-context="Content/Moderation/Table.Label/Short (Value is a date)">First seen</translate>
+                    </td>
+                    <td>
+                      <human-date :date="object.creation_date"></human-date>
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <translate translate-context="Content/*/*/Noun">Accessed date</translate>
+                    </td>
+                    <td>
+                      <human-date v-if="object.accessed_date" :date="object.accessed_date"></human-date>
+                      <translate v-else translate-context="*/*/*">N/A</translate>
+                    </td>
+                  </tr>
+                </tbody>
+              </table>
+            </section>
+          </div>
+          <div class="column">
+            <section>
+              <h3 class="ui header">
+                <i class="music icon"></i>
+                <div class="content">
+                  <translate translate-context="Content/Moderation/Title">Audio content</translate>&nbsp;
+                </div>
+              </h3>
+              <table class="ui very basic table">
+                <tbody>
+                  <tr v-if="object.track">
+                    <td>
+                      <router-link :to="{name: 'manage.library.tracks.detail', params: {id: object.track.id }}">
+                        <translate translate-context="*/*/*">Track</translate>
+                      </router-link>
+                    </td>
+                    <td>
+                      {{ object.track.title }}
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <translate translate-context="Content/Moderation/Table.Label/Noun">Cached size</translate>
+                    </td>
+                    <td>
+                      <template v-if="object.audio_file">
+                        {{ object.size | humanSize }}
+                      </template>
+                      <translate v-else translate-context="*/*/*">N/A</translate>
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <translate translate-context="Content/*/*/Noun">Size</translate>
+                    </td>
+                    <td>
+                      {{ object.size | humanSize }}
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <translate translate-context="Content/Track/*/Noun">Bitrate</translate>
+                    </td>
+                    <td>
+                      <template v-if="object.bitrate">
+                        {{ object.bitrate | humanSize }}/s
+                      </template>
+                      <translate v-else translate-context="*/*/*">N/A</translate>
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <translate translate-context="Content/*/*">Duration</translate>
+                    </td>
+                    <td>
+                      <template v-if="object.duration">
+                        {{ time.parse(object.duration) }}
+                      </template>
+                      <translate v-else translate-context="*/*/*">N/A</translate>
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <router-link :to="{name: 'manage.library.uploads', query: {q: getQuery('type', object.mimetype) }}">
+                        <translate translate-context="Content/Track/Table.Label/Noun">Type</translate>
+                      </router-link>
+                    </td>
+                    <td>
+                      <template v-if="object.mimetype">
+                        {{ object.mimetype }}
+                      </template>
+                      <translate v-else translate-context="*/*/*">N/A</translate>
+                    </td>
+                  </tr>
+                </tbody>
+              </table>
+            </section>
+          </div>
+        </div>
+      </div>
+
+    </template>
+  </main>
+</template>
+
+<script>
+import axios from "axios"
+import logger from "@/logging"
+import TranslationsMixin from "@/components/mixins/Translations"
+import ImportStatusModal from '@/components/library/ImportStatusModal'
+import time from '@/utils/time'
+
+
+export default {
+  props: ["id"],
+  mixins: [
+    TranslationsMixin,
+  ],
+  components: {
+    ImportStatusModal
+  },
+  data() {
+    return {
+      time,
+      detailedUpload: null,
+      showUploadDetailModal: false,
+      isLoading: true,
+      object: null,
+      stats: null,
+    }
+  },
+  created() {
+    this.fetchData()
+  },
+  methods: {
+    fetchData() {
+      var self = this
+      this.isLoading = true
+      let url = `manage/library/uploads/${this.id}/`
+      axios.get(url).then(response => {
+        self.object = response.data
+        self.isLoading = false
+      })
+    },
+    remove () {
+      var self = this
+      this.isLoading = true
+      let url = `manage/library/uploads/${this.id}/`
+      axios.delete(url).then(response => {
+        self.$router.push({name: 'manage.library.uploads'})
+      })
+    },
+    getQuery (field, value) {
+      return `${field}:"${value}"`
+    },
+    displayName (upload) {
+      if (upload.filename) {
+        return upload.filename
+      }
+      if (upload.source) {
+        return upload.source
+      }
+      return upload.uuid
+    }
+  },
+  computed: {
+    labels() {
+      return {
+        statsWarning: this.$pgettext('Content/Moderation/Help text', 'Statistics are computed from known activity and content on your instance, and do not reflect general activity for this object'),
+      }
+    },
+  }
+}
+</script>
diff --git a/front/src/views/admin/library/UploadsList.vue b/front/src/views/admin/library/UploadsList.vue
new file mode 100644
index 0000000000000000000000000000000000000000..0d4d7b5e3c2975914ebbb05f3a5d3e26e044cd6a
--- /dev/null
+++ b/front/src/views/admin/library/UploadsList.vue
@@ -0,0 +1,29 @@
+<template>
+  <main v-title="labels.title">
+    <section class="ui vertical stripe segment">
+      <h2 class="ui header">{{ labels.title }}</h2>
+      <div class="ui hidden divider"></div>
+      <uploads-table :update-url="true" :default-query="defaultQuery"></uploads-table>
+    </section>
+  </main>
+</template>
+
+<script>
+import UploadsTable from "@/components/manage/library/UploadsTable"
+
+export default {
+  components: {
+    UploadsTable
+  },
+  props: {
+    defaultQuery: {type: String, required: false},
+  },
+  computed: {
+    labels() {
+      return {
+        title: this.$pgettext('*/*/*', 'Uploads')
+      }
+    }
+  }
+}
+</script>
diff --git a/front/src/views/admin/moderation/AccountsDetail.vue b/front/src/views/admin/moderation/AccountsDetail.vue
index ec02f7e11ac0b87bbf302e3085d23cc0eec6fd79..d863af52d7432ea82e34d157fb23c90b6d03bb9b 100644
--- a/front/src/views/admin/moderation/AccountsDetail.vue
+++ b/front/src/views/admin/moderation/AccountsDetail.vue
@@ -90,14 +90,14 @@
                     </td>
                   </tr>
                   <tr v-if="!object.user">
-                    <td>
-                      <translate translate-context="Content/Moderation/*/Noun">Domain</translate>
-                    </td>
                     <td>
                       <router-link :to="{name: 'manage.moderation.domains.detail', params: {id: object.domain }}">
-                        {{ object.domain }}
+                        <translate translate-context="Content/Moderation/*/Noun">Domain</translate>
                       </router-link>
                     </td>
+                    <td>
+                      {{ object.domain }}
+                    </td>
                   </tr>
                   <tr>
                     <td>
@@ -155,14 +155,6 @@
                       {{ object.type }}
                     </td>
                   </tr>
-                  <tr v-if="!object.user">
-                    <td>
-                      <translate translate-context="Content/Moderation/Table.Label/Short (Value is a date)">First seen</translate>
-                    </td>
-                    <td>
-                      <human-date :date="object.creation_date"></human-date>
-                    </td>
-                  </tr>
                   <tr v-if="!object.user">
                     <td>
                       <translate translate-context="Content/*/Table.Label">Last checked</translate>
@@ -210,6 +202,14 @@
               </div>
               <table v-else class="ui very basic table">
                 <tbody>
+                  <tr v-if="!object.user">
+                    <td>
+                      <translate translate-context="Content/Moderation/Table.Label/Short (Value is a date)">First seen</translate>
+                    </td>
+                    <td>
+                      <human-date :date="object.creation_date"></human-date>
+                    </td>
+                  </tr>
                   <tr>
                     <td>
                       <translate translate-context="Content/Moderation/Table.Label/Noun">Emitted messages</translate>
@@ -295,7 +295,9 @@
 
                   <tr>
                     <td>
-                      <translate translate-context="*/*/*/Noun">Libraries</translate>
+                      <router-link :to="{name: 'manage.library.libraries', query: {q: getQuery('account', object.full_username) }}">
+                        <translate translate-context="*/*/*/Noun">Libraries</translate>
+                      </router-link>
                     </td>
                     <td>
                       {{ stats.libraries }}
@@ -303,7 +305,9 @@
                   </tr>
                   <tr>
                     <td>
-                      <translate translate-context="Content/Moderation/Table.Label/Noun">Uploads</translate>
+                      <router-link :to="{name: 'manage.library.uploads', query: {q: getQuery('account', object.full_username) }}">
+                        <translate translate-context="Content/Moderation/Table.Label/Noun">Uploads</translate>
+                      </router-link>
                     </td>
                     <td>
                       {{ stats.uploads }}
@@ -446,6 +450,9 @@ export default {
           )
         }
       )
+    },
+    getQuery (field, value) {
+      return `${field}:"${value}"`
     }
   },
   computed: {
diff --git a/front/src/views/admin/moderation/DomainsDetail.vue b/front/src/views/admin/moderation/DomainsDetail.vue
index e0e2b5680c2e0e7fa62218873bd2f3f38ad93ce1..575b15f93ca2bbf696b9672cbb38e7535c8b09ab 100644
--- a/front/src/views/admin/moderation/DomainsDetail.vue
+++ b/front/src/views/admin/moderation/DomainsDetail.vue
@@ -74,14 +74,6 @@
               </h3>
               <table class="ui very basic table">
                 <tbody>
-                  <tr>
-                    <td>
-                      <translate translate-context="Content/Moderation/Table.Label/Short (Value is a date)">First seen</translate>
-                    </td>
-                    <td>
-                      <human-date :date="object.creation_date"></human-date>
-                    </td>
-                  </tr>
                   <tr>
                     <td>
                       <translate translate-context="Content/*/Table.Label">Last checked</translate>
@@ -155,6 +147,14 @@
               </div>
               <table v-else class="ui very basic table">
                 <tbody>
+                  <tr>
+                    <td>
+                      <translate translate-context="Content/Moderation/Table.Label/Short (Value is a date)">First seen</translate>
+                    </td>
+                    <td>
+                      <human-date :date="object.creation_date"></human-date>
+                    </td>
+                  </tr>
                   <tr>
                     <td>
                       <router-link
@@ -231,7 +231,9 @@
                   </tr>
                   <tr>
                     <td>
-                      <translate translate-context="*/*/*/Noun">Libraries</translate>
+                      <router-link :to="{name: 'manage.library.libraries', query: {q: getQuery('domain', object.name) }}">
+                        <translate translate-context="*/*/*/Noun">Libraries</translate>
+                      </router-link>
                     </td>
                     <td>
                       {{ stats.libraries }}
@@ -239,7 +241,9 @@
                   </tr>
                   <tr>
                     <td>
-                      <translate translate-context="Content/Moderation/Table.Label/Noun">Uploads</translate>
+                      <router-link :to="{name: 'manage.library.uploads', query: {q: getQuery('domain', object.name) }}">
+                        <translate translate-context="Content/Moderation/Table.Label/Noun">Uploads</translate>
+                      </router-link>
                     </td>
                     <td>
                       {{ stats.uploads }}
@@ -247,7 +251,9 @@
                   </tr>
                   <tr>
                     <td>
-                      <translate translate-context="*/*/*/Noun">Artists</translate>
+                      <router-link :to="{name: 'manage.library.artists', query: {q: getQuery('domain', object.name) }}">
+                        <translate translate-context="*/*/*/Noun">Artists</translate>
+                      </router-link>
                     </td>
                     <td>
                       {{ stats.artists }}
@@ -255,7 +261,9 @@
                   </tr>
                   <tr>
                     <td>
-                      <translate translate-context="*/*/*">Albums</translate>
+                      <router-link :to="{name: 'manage.library.albums', query: {q: getQuery('domain', object.name) }}">
+                        <translate translate-context="*/*/*">Albums</translate>
+                      </router-link>
                     </td>
                     <td>
                       {{ stats.albums}}
@@ -263,7 +271,9 @@
                   </tr>
                   <tr>
                     <td>
-                      <translate translate-context="*/*/*/Noun">Tracks</translate>
+                      <router-link :to="{name: 'manage.library.tracks', query: {q: getQuery('domain', object.name) }}">
+                        <translate translate-context="*/*/*/Noun">Tracks</translate>
+                      </router-link>
                     </td>
                     <td>
                       {{ stats.tracks }}
@@ -350,6 +360,9 @@ export default {
     updatePolicy (policy) {
       this.policy = policy
       this.showPolicyForm = false
+    },
+    getQuery (field, value) {
+      return `${field}:"${value}"`
     }
   },
   computed: {
diff --git a/front/src/views/content/libraries/FilesTable.vue b/front/src/views/content/libraries/FilesTable.vue
index f6c4e69efaa9a90c96db8aa4d82bbd3bfe3d9c9a..9ea4634adcb6360dbc5e8c454405b6deeac11527 100644
--- a/front/src/views/content/libraries/FilesTable.vue
+++ b/front/src/views/content/libraries/FilesTable.vue
@@ -35,88 +35,7 @@
         </div>
       </div>
     </div>
-    <modal :show.sync="showUploadDetailModal">
-      <div class="header">
-        <translate translate-context="Popup/Import/Title">Import detail</translate>
-      </div>
-      <div class="content" v-if="detailedUpload">
-        <div class="description">
-          <div class="ui message" v-if="detailedUpload.import_status === 'pending'">
-            <translate translate-context="Popup/Import/Message">Upload is still pending and will soon be processed by the server.</translate>
-          </div>
-          <div class="ui success message" v-if="detailedUpload.import_status === 'finished'">
-            <translate translate-context="Popup/Import/Message">Upload was successfully processed by the server.</translate>
-          </div>
-          <div class="ui warning message" v-if="detailedUpload.import_status === 'skipped'">
-            <translate translate-context="Popup/Import/Message">Upload was skipped because a similar one is already available in one of your libraries.</translate>
-          </div>
-          <div class="ui error message" v-if="detailedUpload.import_status === 'errored'">
-            <translate translate-context="Popup/Import/Message">An error occured during upload processing. You will find more information below.</translate>
-          </div>
-          <template v-if="detailedUpload.import_status === 'errored'">
-            <table class="ui very basic collapsing celled table">
-              <tbody>
-                <tr>
-                  <td>
-                    <translate translate-context="Popup/Import/Table.Label/Noun">Error type</translate>
-                  </td>
-                  <td>
-                    {{ getErrorData(detailedUpload).label }}
-                  </td>
-                </tr>
-                <tr>
-                  <td>
-                    <translate translate-context="Popup/Import/Table.Label/Noun">Error detail</translate>
-                  </td>
-                  <td>
-                    {{ getErrorData(detailedUpload).detail }}
-                    <ul v-if="getErrorData(detailedUpload).errorRows.length > 0">
-                      <li v-for="row in getErrorData(detailedUpload).errorRows">
-                        {{ row.key}}: {{ row.value}}
-                      </li>
-                    </ul>
-                  </td>
-                </tr>
-                <tr>
-                  <td>
-                    <translate translate-context="Popup/Import/Table.Label/Noun">Getting help</translate>
-                  </td>
-                  <td>
-                    <ul>
-                      <li>
-                        <a :href="getErrorData(detailedUpload).documentationUrl" target="_blank">
-                          <translate translate-context="Popup/Import/Table.Label/Value">Read our documentation for this error</translate>
-                        </a>
-                      </li>
-                      <li>
-                        <a :href="getErrorData(detailedUpload).supportUrl" target="_blank">
-                          <translate translate-context="Popup/Import/Table.Label/Value">Open a support thread (include the debug information below in your message)</translate>
-                        </a>
-                      </li>
-                    </ul>
-                  </td>
-                </tr>
-                <tr>
-                  <td>
-                    <translate translate-context="Popup/Import/Table.Label/Noun">Debug information</translate>
-                  </td>
-                  <td>
-                    <div class="ui form">
-                      <textarea class="ui textarea" rows="10" :value="getErrorData(detailedUpload).debugInfo"></textarea>
-                    </div>
-                  </td>
-                </tr>
-              </tbody>
-            </table>
-          </template>
-        </div>
-      </div>
-      <div class="actions">
-        <div class="ui deny button">
-          <translate translate-context="*/*/Button.Label/Verb">Close</translate>
-        </div>
-      </div>
-    </modal>
+    <import-status-modal :upload="detailedUpload" :show.sync="showUploadDetailModal" />
     <div class="dimmable">
       <div v-if="isLoading" class="ui active inverted dimmer">
           <div class="ui loader"></div>
@@ -163,10 +82,10 @@
             <human-date :date="scope.obj.creation_date"></human-date>
           </td>
           <td>
-            <span class="discrete link" @click="addSearchToken('status', scope.obj.import_status)" :title="labels.importStatuses[scope.obj.import_status].help">
-              {{ labels.importStatuses[scope.obj.import_status].label }}
+            <span class="discrete link" @click="addSearchToken('status', scope.obj.import_status)" :title="sharedLabels.fields.import_status.choices[scope.obj.import_status].help">
+              {{ sharedLabels.fields.import_status.choices[scope.obj.import_status].label }}
             </span>
-            <button class="ui tiny basic icon button" :title="labels.statusDetailTitle" @click="detailedUpload = scope.obj; showUploadDetailModal = true">
+            <button class="ui tiny basic icon button" :title="sharedLabels.fields.import_status.detailTitle" @click="detailedUpload = scope.obj; showUploadDetailModal = true">
               <i class="question circle outline icon"></i>
             </button>
           </td>
@@ -216,33 +135,8 @@ import ActionTable from '@/components/common/ActionTable'
 import OrderingMixin from '@/components/mixins/Ordering'
 import TranslationsMixin from '@/components/mixins/Translations'
 import SmartSearchMixin from '@/components/mixins/SmartSearch'
-import Modal from '@/components/semantic/Modal'
+import ImportStatusModal from '@/components/library/ImportStatusModal'
 
-function getErrors(payload) {
-  let errors = []
-  for (var k in payload) {
-    if (payload.hasOwnProperty(k)) {
-      let value = payload[k]
-      if (Array.isArray(value)) {
-        errors.push({
-          key: k,
-          value: value.join(', ')
-        })
-      } else {
-        // possibly artists, so nested errors
-        if (typeof value === 'object') {
-          getErrors(value).forEach((e) => {
-            errors.push({
-              key: `${k} / ${e.key}`,
-              value: e.value
-            })
-          })
-        }
-      }
-    }
-  }
-  return errors
-}
 export default {
   mixins: [OrderingMixin, TranslationsMixin, SmartSearchMixin],
   props: {
@@ -253,7 +147,7 @@ export default {
   components: {
     Pagination,
     ActionTable,
-    Modal
+    ImportStatusModal
   },
   data () {
     return {
@@ -307,58 +201,11 @@ export default {
     selectPage: function (page) {
       this.page = page
     },
-    getErrorData (upload) {
-      let payload = upload.import_details || {}
-      let d = {
-        supportUrl: 'https://governance.funkwhale.audio/g/246YOJ1m/funkwhale-support',
-        errorRows: []
-      }
-      if (!payload.error_code) {
-        d.errorCode = 'unknown_error'
-      } else {
-        d.errorCode = payload.error_code
-      }
-      d.documentationUrl = `https://docs.funkwhale.audio/users/upload.html#${d.errorCode}`
-      if (d.errorCode === 'invalid_metadata') {
-        d.label = this.$pgettext('Popup/Import/Error.Label', 'Invalid metadata')
-        d.detail = this.$pgettext('Popup/Import/Error.Label', 'The metadata included in the file is invalid or some mandatory fields are missing.')
-        let detail = payload.detail || {}
-        d.errorRows = getErrors(detail)
-      } else {
-        d.label = this.$pgettext('Popup/Import/Error.Label', 'Unkwown error')
-        d.detail = this.$pgettext('Popup/Import/Error.Label', 'An unkwown error occured')
-      }
-      let debugInfo = {
-        source: upload.source,
-        ...payload,
-      }
-      d.debugInfo = JSON.stringify(debugInfo, null, 4)
-      return d
-    }
   },
   computed: {
     labels () {
       return {
         searchPlaceholder: this.$pgettext('Content/Library/Input.Placeholder', 'Search by title, artist, album…'),
-        statusDetailTitle: this.$pgettext('Content/Library/Link.Title', 'Click to display more information about the import process for this upload'),
-        importStatuses: {
-          skipped: {
-            label: this.$pgettext('Content/Library/*', 'Skipped'),
-            help: this.$pgettext('Content/Library/Help text', 'This track is already present in one of your libraries'),
-          },
-          pending: {
-            label: this.$pgettext('Content/Library/*/Short', 'Pending'),
-            help: this.$pgettext('Content/Library/Help text', 'This track has been uploaded, but hasn\'t been processed by the server yet'),
-          },
-          errored: {
-            label: this.$pgettext('Content/Library/Table/Short', 'Errored'),
-            help: this.$pgettext('Content/Library/Help text', 'This track could not be processed, please it is tagged correctly'),
-          },
-          finished: {
-            label: this.$pgettext('Content/Library/*', 'Finished'),
-            help: this.$pgettext('Content/Library/Help text', 'Imported'),
-          },
-        }
       }
     },
     actionFilters () {