diff --git a/api/funkwhale_api/common/fields.py b/api/funkwhale_api/common/fields.py
index 47e673cb5b567cfebfc8ad14ef6aae07acf44687..b8e217ba4eca9ef0954236124a577a88cf4e72d5 100644
--- a/api/funkwhale_api/common/fields.py
+++ b/api/funkwhale_api/common/fields.py
@@ -49,6 +49,6 @@ class SmartSearchFilter(django_filters.CharFilter):
             return qs
         try:
             cleaned = self.config.clean(value)
-        except forms.ValidationError:
+        except (forms.ValidationError):
             return qs.none()
         return search.apply(qs, cleaned)
diff --git a/api/funkwhale_api/common/filters.py b/api/funkwhale_api/common/filters.py
index 4825d3b5d8d4a934d458faead03baa693f178b94..364a1fba18a276cef05a34976a95749408132dc9 100644
--- a/api/funkwhale_api/common/filters.py
+++ b/api/funkwhale_api/common/filters.py
@@ -104,6 +104,31 @@ class MultipleQueryFilter(filters.TypedMultipleChoiceFilter):
         self.lookup_expr = "in"
 
 
+def filter_target(value):
+
+    config = {
+        "artist": ["artist", "target_id", int],
+        "album": ["album", "target_id", int],
+        "track": ["track", "target_id", int],
+    }
+    parts = value.lower().split(" ")
+    if parts[0].strip() not in config:
+        raise forms.ValidationError("Improper target")
+
+    conf = config[parts[0].strip()]
+
+    query = Q(target_content_type__model=conf[0])
+    if len(parts) > 1:
+        _, lookup_field, validator = conf
+        try:
+            lookup_value = validator(parts[1].strip())
+        except TypeError:
+            raise forms.ValidationError("Imparsable target id")
+        return query & Q(**{lookup_field: lookup_value})
+
+    return query
+
+
 class MutationFilter(filters.FilterSet):
     is_approved = NullBooleanFilter("is_approved")
     q = fields.SmartSearchFilter(
@@ -116,6 +141,7 @@ class MutationFilter(filters.FilterSet):
             filter_fields={
                 "domain": {"to": "created_by__domain__name__iexact"},
                 "is_approved": get_null_boolean_filter("is_approved"),
+                "target": {"handler": filter_target},
                 "is_applied": {"to": "is_applied"},
             },
         )
diff --git a/api/funkwhale_api/common/search.py b/api/funkwhale_api/common/search.py
index 622cb29dd174d1c7dbc3ece052b6e8ab5a5068a4..cc046f758d5d37ffc62d960517654f35dd099e27 100644
--- a/api/funkwhale_api/common/search.py
+++ b/api/funkwhale_api/common/search.py
@@ -77,12 +77,15 @@ class SearchConfig:
     def clean(self, query):
         tokens = parse_query(query)
         cleaned_data = {}
-
         cleaned_data["types"] = self.clean_types(filter_tokens(tokens, ["is"]))
         cleaned_data["search_query"] = self.clean_search_query(
-            filter_tokens(tokens, [None, "in"])
+            filter_tokens(tokens, [None, "in"] + list(self.search_fields.keys()))
         )
-        unhandled_tokens = [t for t in tokens if t["key"] not in [None, "is", "in"]]
+        unhandled_tokens = [
+            t
+            for t in tokens
+            if t["key"] not in [None, "is", "in"] + list(self.search_fields.keys())
+        ]
         cleaned_data["filter_query"] = self.clean_filter_query(unhandled_tokens)
         return cleaned_data
 
@@ -95,8 +98,33 @@ class SearchConfig:
         } or set(self.search_fields.keys())
         fields_subset = set(self.search_fields.keys()) & fields_subset
         to_fields = [self.search_fields[k]["to"] for k in fields_subset]
+
+        specific_field_query = None
+        for token in tokens:
+            if token["key"] not in self.search_fields:
+                continue
+            to = self.search_fields[token["key"]]["to"]
+            try:
+                field = token["field"]
+                value = field.clean(token["value"])
+            except KeyError:
+                # no cleaning to apply
+                value = token["value"]
+            q = Q(**{"{}__icontains".format(to): value})
+            if not specific_field_query:
+                specific_field_query = q
+            else:
+                specific_field_query &= q
         query_string = " ".join([t["value"] for t in filter_tokens(tokens, [None])])
-        return get_query(query_string, sorted(to_fields))
+        unhandled_tokens_query = get_query(query_string, sorted(to_fields))
+
+        if specific_field_query and unhandled_tokens_query:
+            return unhandled_tokens_query & specific_field_query
+        elif specific_field_query:
+            return specific_field_query
+        elif unhandled_tokens_query:
+            return unhandled_tokens_query
+        return None
 
     def clean_filter_query(self, tokens):
         if not self.filter_fields or not tokens:
diff --git a/api/funkwhale_api/common/views.py b/api/funkwhale_api/common/views.py
index 743c95095b1f1042b3e411fe6cb784a18e5d1dd6..db39c56d1afbb5eed7fa0ba7c89693fb32c57ab4 100644
--- a/api/funkwhale_api/common/views.py
+++ b/api/funkwhale_api/common/views.py
@@ -36,6 +36,7 @@ class MutationViewSet(
     lookup_field = "uuid"
     queryset = (
         models.Mutation.objects.all()
+        .exclude(target_id=None)
         .order_by("-creation_date")
         .select_related("created_by", "approved_by")
         .prefetch_related("target")
diff --git a/api/funkwhale_api/federation/fields.py b/api/funkwhale_api/federation/fields.py
index 3523396dbceb86a2aa8c84c1767cad9b6f767db1..8a8a1eb2de2059a2b6edeeec629c81147c84381b 100644
--- a/api/funkwhale_api/federation/fields.py
+++ b/api/funkwhale_api/federation/fields.py
@@ -1,6 +1,9 @@
+import django_filters
+
 from rest_framework import serializers
 
 from . import models
+from . import utils
 
 
 class ActorRelatedField(serializers.EmailField):
@@ -16,3 +19,15 @@ class ActorRelatedField(serializers.EmailField):
             )
         except models.Actor.DoesNotExist:
             raise serializers.ValidationError("Invalid actor name")
+
+
+class DomainFromURLFilter(django_filters.CharFilter):
+    def __init__(self, *args, **kwargs):
+        self.url_field = kwargs.pop("url_field", "fid")
+        super().__init__(*args, **kwargs)
+
+    def filter(self, qs, value):
+        if not value:
+            return qs
+        query = utils.get_domain_query_from_url(value, self.url_field)
+        return qs.filter(query)
diff --git a/api/funkwhale_api/federation/utils.py b/api/funkwhale_api/federation/utils.py
index a32256921a55f8eee1a5c406255d224b433eb5b0..2bbfdf7fadd023c3349961237f877677e29dc06a 100644
--- a/api/funkwhale_api/federation/utils.py
+++ b/api/funkwhale_api/federation/utils.py
@@ -1,6 +1,7 @@
 import unicodedata
 import re
 from django.conf import settings
+from django.db.models import Q
 
 from funkwhale_api.common import session
 from funkwhale_api.moderation import models as moderation_models
@@ -107,3 +108,16 @@ def retrieve_ap_object(
     serializer = serializer_class(data=data, context={"fetch_actor": actor})
     serializer.is_valid(raise_exception=True)
     return serializer.save()
+
+
+def get_domain_query_from_url(domain, url_field="fid"):
+    """
+    Given a domain name and a field, will return a Q() object
+    to match objects that have this domain in the given field.
+    """
+
+    query = Q(**{"{}__startswith".format(url_field): "http://{}/".format(domain)})
+    query = query | Q(
+        **{"{}__startswith".format(url_field): "https://{}/".format(domain)}
+    )
+    return query
diff --git a/api/funkwhale_api/manage/filters.py b/api/funkwhale_api/manage/filters.py
index edae49f991bcb9f2a415f8f829df1a1e7d7c5248..64a6473e0b93b471197511cb88ae82f2a5647c16 100644
--- a/api/funkwhale_api/manage/filters.py
+++ b/api/funkwhale_api/manage/filters.py
@@ -1,9 +1,11 @@
+from django import forms
 from django_filters import rest_framework as filters
 
 from funkwhale_api.common import fields
 from funkwhale_api.common import search
 
 from funkwhale_api.federation import models as federation_models
+from funkwhale_api.federation import utils as federation_utils
 from funkwhale_api.moderation import models as moderation_models
 from funkwhale_api.music import models as music_models
 from funkwhale_api.users import models as users_models
@@ -24,6 +26,82 @@ class ManageUploadFilterSet(filters.FilterSet):
         fields = ["q", "track__album", "track__artist", "track"]
 
 
+class ManageArtistFilterSet(filters.FilterSet):
+    q = fields.SmartSearchFilter(
+        config=search.SearchConfig(
+            search_fields={
+                "name": {"to": "name"},
+                "fid": {"to": "fid"},
+                "mbid": {"to": "mbid"},
+            },
+            filter_fields={
+                "domain": {
+                    "handler": lambda v: federation_utils.get_domain_query_from_url(v)
+                }
+            },
+        )
+    )
+
+    class Meta:
+        model = music_models.Artist
+        fields = ["q", "name", "mbid", "fid"]
+
+
+class ManageAlbumFilterSet(filters.FilterSet):
+    q = fields.SmartSearchFilter(
+        config=search.SearchConfig(
+            search_fields={
+                "title": {"to": "title"},
+                "fid": {"to": "fid"},
+                "artist": {"to": "artist__name"},
+                "mbid": {"to": "mbid"},
+            },
+            filter_fields={
+                "artist_id": {"to": "artist_id", "field": forms.IntegerField()},
+                "domain": {
+                    "handler": lambda v: federation_utils.get_domain_query_from_url(v)
+                },
+            },
+        )
+    )
+
+    class Meta:
+        model = music_models.Album
+        fields = ["q", "title", "mbid", "fid", "artist"]
+
+
+class ManageTrackFilterSet(filters.FilterSet):
+    q = fields.SmartSearchFilter(
+        config=search.SearchConfig(
+            search_fields={
+                "title": {"to": "title"},
+                "fid": {"to": "fid"},
+                "mbid": {"to": "mbid"},
+                "artist": {"to": "artist__name"},
+                "album": {"to": "album__title"},
+                "album_artist": {"to": "album__artist__name"},
+                "copyright": {"to": "copyright"},
+            },
+            filter_fields={
+                "album_id": {"to": "album_id", "field": forms.IntegerField()},
+                "album_artist_id": {
+                    "to": "album__artist_id",
+                    "field": forms.IntegerField(),
+                },
+                "artist_id": {"to": "artist_id", "field": forms.IntegerField()},
+                "license": {"to": "license"},
+                "domain": {
+                    "handler": lambda v: federation_utils.get_domain_query_from_url(v)
+                },
+            },
+        )
+    )
+
+    class Meta:
+        model = music_models.Track
+        fields = ["q", "title", "mbid", "fid", "artist", "album", "license"]
+
+
 class ManageDomainFilterSet(filters.FilterSet):
     q = fields.SearchFilter(search_fields=["name"])
 
@@ -60,7 +138,15 @@ class ManageActorFilterSet(filters.FilterSet):
 
 
 class ManageUserFilterSet(filters.FilterSet):
-    q = fields.SearchFilter(search_fields=["username", "email", "name"])
+    q = fields.SmartSearchFilter(
+        config=search.SearchConfig(
+            search_fields={
+                "name": {"to": "name"},
+                "username": {"to": "username"},
+                "email": {"to": "email"},
+            }
+        )
+    )
 
     class Meta:
         model = users_models.User
diff --git a/api/funkwhale_api/manage/serializers.py b/api/funkwhale_api/manage/serializers.py
index ed50d86777d30c7d8fc255944f5034f70a3f4bef..cf6a1eab4b698b4bed8e899b2f10470c0c5b5219 100644
--- a/api/funkwhale_api/manage/serializers.py
+++ b/api/funkwhale_api/manage/serializers.py
@@ -9,6 +9,7 @@ from funkwhale_api.federation import fields as federation_fields
 from funkwhale_api.federation import tasks as federation_tasks
 from funkwhale_api.moderation import models as moderation_models
 from funkwhale_api.music import models as music_models
+from funkwhale_api.music import serializers as music_serializers
 from funkwhale_api.users import models as users_models
 
 from . import filters
@@ -216,10 +217,7 @@ class ManageDomainActionSerializer(common_serializers.ActionSerializer):
         common_utils.on_commit(federation_tasks.purge_actors.delay, domains=list(ids))
 
 
-class ManageActorSerializer(serializers.ModelSerializer):
-    uploads_count = serializers.SerializerMethodField()
-    user = ManageUserSerializer()
-
+class ManageBaseActorSerializer(serializers.ModelSerializer):
     class Meta:
         model = federation_models.Actor
         fields = [
@@ -238,6 +236,17 @@ class ManageActorSerializer(serializers.ModelSerializer):
             "outbox_url",
             "shared_inbox_url",
             "manually_approves_followers",
+        ]
+        read_only_fields = ["creation_date", "instance_policy"]
+
+
+class ManageActorSerializer(ManageBaseActorSerializer):
+    uploads_count = serializers.SerializerMethodField()
+    user = ManageUserSerializer()
+
+    class Meta:
+        model = federation_models.Actor
+        fields = ManageBaseActorSerializer.Meta.fields + [
             "uploads_count",
             "user",
             "instance_policy",
@@ -339,3 +348,148 @@ class ManageInstancePolicySerializer(serializers.ModelSerializer):
                 )
 
         return instance
+
+
+class ManageBaseArtistSerializer(serializers.ModelSerializer):
+    domain = serializers.CharField(source="domain_name")
+
+    class Meta:
+        model = music_models.Artist
+        fields = ["id", "fid", "mbid", "name", "creation_date", "domain", "is_local"]
+
+
+class ManageBaseAlbumSerializer(serializers.ModelSerializer):
+    cover = music_serializers.cover_field
+    domain = serializers.CharField(source="domain_name")
+
+    class Meta:
+        model = music_models.Album
+        fields = [
+            "id",
+            "fid",
+            "mbid",
+            "title",
+            "creation_date",
+            "release_date",
+            "cover",
+            "domain",
+            "is_local",
+        ]
+
+
+class ManageNestedTrackSerializer(serializers.ModelSerializer):
+    domain = serializers.CharField(source="domain_name")
+
+    class Meta:
+        model = music_models.Track
+        fields = [
+            "id",
+            "fid",
+            "mbid",
+            "title",
+            "creation_date",
+            "position",
+            "disc_number",
+            "domain",
+            "is_local",
+            "copyright",
+            "license",
+        ]
+
+
+class ManageNestedAlbumSerializer(ManageBaseAlbumSerializer):
+
+    tracks_count = serializers.SerializerMethodField()
+
+    class Meta:
+        model = music_models.Album
+        fields = ManageBaseAlbumSerializer.Meta.fields + ["tracks_count"]
+
+    def get_tracks_count(self, obj):
+        return getattr(obj, "tracks_count", None)
+
+
+class ManageArtistSerializer(ManageBaseArtistSerializer):
+    albums = ManageNestedAlbumSerializer(many=True)
+    tracks = ManageNestedTrackSerializer(many=True)
+    attributed_to = ManageBaseActorSerializer()
+
+    class Meta:
+        model = music_models.Artist
+        fields = ManageBaseArtistSerializer.Meta.fields + [
+            "albums",
+            "tracks",
+            "attributed_to",
+        ]
+
+
+class ManageNestedArtistSerializer(ManageBaseArtistSerializer):
+    pass
+
+
+class ManageAlbumSerializer(ManageBaseAlbumSerializer):
+    tracks = ManageNestedTrackSerializer(many=True)
+    attributed_to = ManageBaseActorSerializer()
+    artist = ManageNestedArtistSerializer()
+
+    class Meta:
+        model = music_models.Album
+        fields = ManageBaseAlbumSerializer.Meta.fields + [
+            "artist",
+            "tracks",
+            "attributed_to",
+        ]
+
+
+class ManageTrackAlbumSerializer(ManageBaseAlbumSerializer):
+    artist = ManageNestedArtistSerializer()
+
+    class Meta:
+        model = music_models.Album
+        fields = ManageBaseAlbumSerializer.Meta.fields + ["artist"]
+
+
+class ManageTrackSerializer(ManageNestedTrackSerializer):
+    artist = ManageNestedArtistSerializer()
+    album = ManageTrackAlbumSerializer()
+    attributed_to = ManageBaseActorSerializer()
+    uploads_count = serializers.SerializerMethodField()
+
+    class Meta:
+        model = music_models.Track
+        fields = ManageNestedTrackSerializer.Meta.fields + [
+            "artist",
+            "album",
+            "attributed_to",
+            "uploads_count",
+        ]
+
+    def get_uploads_count(self, obj):
+        return getattr(obj, "uploads_count", None)
+
+
+class ManageTrackActionSerializer(common_serializers.ActionSerializer):
+    actions = [common_serializers.Action("delete", allow_all=False)]
+    filterset_class = filters.ManageTrackFilterSet
+
+    @transaction.atomic
+    def handle_delete(self, objects):
+        return objects.delete()
+
+
+class ManageAlbumActionSerializer(common_serializers.ActionSerializer):
+    actions = [common_serializers.Action("delete", allow_all=False)]
+    filterset_class = filters.ManageAlbumFilterSet
+
+    @transaction.atomic
+    def handle_delete(self, objects):
+        return objects.delete()
+
+
+class ManageArtistActionSerializer(common_serializers.ActionSerializer):
+    actions = [common_serializers.Action("delete", allow_all=False)]
+    filterset_class = filters.ManageArtistFilterSet
+
+    @transaction.atomic
+    def handle_delete(self, objects):
+        return objects.delete()
diff --git a/api/funkwhale_api/manage/urls.py b/api/funkwhale_api/manage/urls.py
index 4c220fe0ef28598eab3feec59c9fb19ecf33bd86..f93667725ae0e2ad4354b2527bcef3738b64c676 100644
--- a/api/funkwhale_api/manage/urls.py
+++ b/api/funkwhale_api/manage/urls.py
@@ -8,6 +8,9 @@ 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"tracks", views.ManageTrackViewSet, "tracks")
 
 moderation_router = routers.SimpleRouter()
 moderation_router.register(
diff --git a/api/funkwhale_api/manage/views.py b/api/funkwhale_api/manage/views.py
index 588e66c589ba2adafd229e894fdf64536960c70f..6fc1a2f1e63512574d1d20bed54f2abd0429690f 100644
--- a/api/funkwhale_api/manage/views.py
+++ b/api/funkwhale_api/manage/views.py
@@ -1,12 +1,18 @@
 from rest_framework import mixins, response, viewsets
 from rest_framework import decorators as rest_decorators
+
+from django.db.models import Count, Prefetch, Q, Sum
 from django.shortcuts import get_object_or_404
 
+from funkwhale_api.common import models as common_models
 from funkwhale_api.common import preferences, decorators
+from funkwhale_api.favorites import models as favorites_models
 from funkwhale_api.federation import models as federation_models
 from funkwhale_api.federation import tasks as federation_tasks
+from funkwhale_api.history import models as history_models
 from funkwhale_api.music import models as music_models
 from funkwhale_api.moderation import models as moderation_models
+from funkwhale_api.playlists import models as playlists_models
 from funkwhale_api.users import models as users_models
 
 
@@ -45,6 +51,151 @@ class ManageUploadViewSet(
         return response.Response(result, status=200)
 
 
+def get_stats(tracks, target):
+    data = {}
+    tracks = list(tracks.values_list("pk", flat=True))
+    uploads = music_models.Upload.objects.filter(track__in=tracks)
+    data["listenings"] = history_models.Listening.objects.filter(
+        track__in=tracks
+    ).count()
+    data["mutations"] = common_models.Mutation.objects.get_for_target(target).count()
+    data["playlists"] = (
+        playlists_models.PlaylistTrack.objects.filter(track__in=tracks)
+        .values_list("playlist", flat=True)
+        .distinct()
+        .count()
+    )
+    data["track_favorites"] = favorites_models.TrackFavorite.objects.filter(
+        track__in=tracks
+    ).count()
+    data["libraries"] = uploads.values_list("library", flat=True).distinct().count()
+    data["uploads"] = uploads.count()
+    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
+    )
+    return data
+
+
+class ManageArtistViewSet(
+    mixins.ListModelMixin,
+    mixins.RetrieveModelMixin,
+    mixins.DestroyModelMixin,
+    viewsets.GenericViewSet,
+):
+    queryset = (
+        music_models.Artist.objects.all()
+        .order_by("-id")
+        .select_related("attributed_to")
+        .prefetch_related(
+            "tracks",
+            Prefetch(
+                "albums",
+                queryset=music_models.Album.objects.annotate(
+                    tracks_count=Count("tracks")
+                ),
+            ),
+        )
+    )
+    serializer_class = serializers.ManageArtistSerializer
+    filterset_class = filters.ManageArtistFilterSet
+    required_scope = "instance:libraries"
+    ordering_fields = ["creation_date", "name"]
+
+    @rest_decorators.action(methods=["get"], detail=True)
+    def stats(self, request, *args, **kwargs):
+        artist = self.get_object()
+        tracks = music_models.Track.objects.filter(
+            Q(artist=artist) | Q(album__artist=artist)
+        )
+        data = get_stats(tracks, artist)
+        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.ManageArtistActionSerializer(
+            request.data, queryset=queryset
+        )
+        serializer.is_valid(raise_exception=True)
+        result = serializer.save()
+        return response.Response(result, status=200)
+
+
+class ManageAlbumViewSet(
+    mixins.ListModelMixin,
+    mixins.RetrieveModelMixin,
+    mixins.DestroyModelMixin,
+    viewsets.GenericViewSet,
+):
+    queryset = (
+        music_models.Album.objects.all()
+        .order_by("-id")
+        .select_related("attributed_to", "artist")
+        .prefetch_related("tracks")
+    )
+    serializer_class = serializers.ManageAlbumSerializer
+    filterset_class = filters.ManageAlbumFilterSet
+    required_scope = "instance:libraries"
+    ordering_fields = ["creation_date", "title", "release_date"]
+
+    @rest_decorators.action(methods=["get"], detail=True)
+    def stats(self, request, *args, **kwargs):
+        album = self.get_object()
+        data = get_stats(album.tracks.all(), album)
+        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.ManageAlbumActionSerializer(
+            request.data, queryset=queryset
+        )
+        serializer.is_valid(raise_exception=True)
+        result = serializer.save()
+        return response.Response(result, status=200)
+
+
+class ManageTrackViewSet(
+    mixins.ListModelMixin,
+    mixins.RetrieveModelMixin,
+    mixins.DestroyModelMixin,
+    viewsets.GenericViewSet,
+):
+    queryset = (
+        music_models.Track.objects.all()
+        .order_by("-id")
+        .select_related("attributed_to", "artist", "album__artist")
+        .annotate(uploads_count=Count("uploads"))
+    )
+    serializer_class = serializers.ManageTrackSerializer
+    filterset_class = filters.ManageTrackFilterSet
+    required_scope = "instance:libraries"
+    ordering_fields = [
+        "creation_date",
+        "title",
+        "album__release_date",
+        "position",
+        "disc_number",
+    ]
+
+    @rest_decorators.action(methods=["get"], detail=True)
+    def stats(self, request, *args, **kwargs):
+        track = self.get_object()
+        data = get_stats(track.__class__.objects.filter(pk=track.pk), track)
+        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 ManageUserViewSet(
     mixins.ListModelMixin,
     mixins.RetrieveModelMixin,
diff --git a/api/funkwhale_api/music/models.py b/api/funkwhale_api/music/models.py
index 9e0268504dc815bc24b37edb4d653a974d3af1e6..4b166be7c3e01b2909375997a5fe5322edd918a6 100644
--- a/api/funkwhale_api/music/models.py
+++ b/api/funkwhale_api/music/models.py
@@ -3,6 +3,7 @@ import logging
 import mimetypes
 import os
 import tempfile
+import urllib.parse
 import uuid
 
 import markdown
@@ -124,6 +125,14 @@ class APIModelMixin(models.Model):
             "https://{}/".format(d)
         )
 
+    @property
+    def domain_name(self):
+        if not self.fid:
+            return
+
+        parsed = urllib.parse.urlparse(self.fid)
+        return parsed.hostname
+
 
 class License(models.Model):
     code = models.CharField(primary_key=True, max_length=100)
diff --git a/api/tests/common/test_views.py b/api/tests/common/test_views.py
index 9a03fb429284f7955222a006deb7d055501d25e9..d2b53b41f620ac80caa5d5835ef3afdb02c1bd60 100644
--- a/api/tests/common/test_views.py
+++ b/api/tests/common/test_views.py
@@ -7,7 +7,9 @@ from funkwhale_api.common import tasks
 
 
 def test_can_detail_mutation(logged_in_api_client, factories):
-    mutation = factories["common.Mutation"](payload={})
+    mutation = factories["common.Mutation"](
+        payload={}, target=factories["music.Artist"]()
+    )
     url = reverse("api:v1:mutations-detail", kwargs={"uuid": mutation.uuid})
 
     response = logged_in_api_client.get(url)
@@ -19,7 +21,9 @@ def test_can_detail_mutation(logged_in_api_client, factories):
 
 
 def test_can_list_mutations(logged_in_api_client, factories):
-    mutation = factories["common.Mutation"](payload={})
+    mutation = factories["common.Mutation"](
+        payload={}, target=factories["music.Artist"]()
+    )
     url = reverse("api:v1:mutations-list")
 
     response = logged_in_api_client.get(url)
diff --git a/api/tests/federation/test_api_filters.py b/api/tests/federation/test_api_filters.py
index c6e70b6178cd35bf8ad3752f0c8aa1204de20e7f..4cbf4293a3c3a82bda4fb42d3936dc8b41ea0e8c 100644
--- a/api/tests/federation/test_api_filters.py
+++ b/api/tests/federation/test_api_filters.py
@@ -1,3 +1,4 @@
+from funkwhale_api.federation import fields
 from funkwhale_api.federation import filters
 from funkwhale_api.federation import models
 
@@ -7,3 +8,17 @@ def test_inbox_item_filter_before(factories):
     f = filters.InboxItemFilter({"before": 12}, queryset=models.InboxItem.objects.all())
 
     assert str(f.qs.query) == str(expected.query)
+
+
+def test_domain_from_url_filter(factories):
+    found = [
+        factories["music.Artist"](fid="http://domain/test1"),
+        factories["music.Artist"](fid="https://domain/test2"),
+    ]
+    factories["music.Artist"](fid="http://domain2/test1")
+    factories["music.Artist"](fid="https://otherdomain/test2")
+
+    queryset = found[0].__class__.objects.all().order_by("id")
+    field = fields.DomainFromURLFilter()
+    result = field.filter(queryset, "domain")
+    assert list(result) == found
diff --git a/api/tests/manage/test_serializers.py b/api/tests/manage/test_serializers.py
index aef8dc4ea9c837124401733ead8dcbb64ae06aec..64a26538f9d8cac0b87826e90cb08fef111825a5 100644
--- a/api/tests/manage/test_serializers.py
+++ b/api/tests/manage/test_serializers.py
@@ -257,3 +257,160 @@ def test_instance_policy_serializer_purges_target_actor(
 
     assert getattr(policy, param) is False
     assert on_commit.call_count == 0
+
+
+def test_manage_artist_serializer(factories, now):
+    artist = factories["music.Artist"](attributed=True)
+    track = factories["music.Track"](artist=artist)
+    album = factories["music.Album"](artist=artist)
+    expected = {
+        "id": artist.id,
+        "domain": artist.domain_name,
+        "is_local": artist.is_local,
+        "fid": artist.fid,
+        "name": artist.name,
+        "mbid": artist.mbid,
+        "creation_date": artist.creation_date.isoformat().split("+")[0] + "Z",
+        "albums": [serializers.ManageNestedAlbumSerializer(album).data],
+        "tracks": [serializers.ManageNestedTrackSerializer(track).data],
+        "attributed_to": serializers.ManageBaseActorSerializer(
+            artist.attributed_to
+        ).data,
+    }
+    s = serializers.ManageArtistSerializer(artist)
+
+    assert s.data == expected
+
+
+def test_manage_nested_track_serializer(factories, now):
+    track = factories["music.Track"]()
+    expected = {
+        "id": track.id,
+        "domain": track.domain_name,
+        "is_local": track.is_local,
+        "fid": track.fid,
+        "title": track.title,
+        "mbid": track.mbid,
+        "creation_date": track.creation_date.isoformat().split("+")[0] + "Z",
+        "position": track.position,
+        "disc_number": track.disc_number,
+        "copyright": track.copyright,
+        "license": track.license,
+    }
+    s = serializers.ManageNestedTrackSerializer(track)
+
+    assert s.data == expected
+
+
+def test_manage_nested_album_serializer(factories, now):
+    album = factories["music.Album"]()
+    setattr(album, "tracks_count", 44)
+    expected = {
+        "id": album.id,
+        "domain": album.domain_name,
+        "is_local": album.is_local,
+        "fid": album.fid,
+        "title": album.title,
+        "mbid": album.mbid,
+        "creation_date": album.creation_date.isoformat().split("+")[0] + "Z",
+        "release_date": album.release_date.isoformat(),
+        "cover": {
+            "original": album.cover.url,
+            "square_crop": album.cover.crop["400x400"].url,
+            "medium_square_crop": album.cover.crop["200x200"].url,
+            "small_square_crop": album.cover.crop["50x50"].url,
+        },
+        "tracks_count": 44,
+    }
+    s = serializers.ManageNestedAlbumSerializer(album)
+
+    assert s.data == expected
+
+
+def test_manage_nested_artist_serializer(factories, now):
+    artist = factories["music.Artist"]()
+    expected = {
+        "id": artist.id,
+        "domain": artist.domain_name,
+        "is_local": artist.is_local,
+        "fid": artist.fid,
+        "name": artist.name,
+        "mbid": artist.mbid,
+        "creation_date": artist.creation_date.isoformat().split("+")[0] + "Z",
+    }
+    s = serializers.ManageNestedArtistSerializer(artist)
+
+    assert s.data == expected
+
+
+def test_manage_album_serializer(factories, now):
+    album = factories["music.Album"](attributed=True)
+    track = factories["music.Track"](album=album)
+    expected = {
+        "id": album.id,
+        "domain": album.domain_name,
+        "is_local": album.is_local,
+        "fid": album.fid,
+        "title": album.title,
+        "mbid": album.mbid,
+        "creation_date": album.creation_date.isoformat().split("+")[0] + "Z",
+        "release_date": album.release_date.isoformat(),
+        "cover": {
+            "original": album.cover.url,
+            "square_crop": album.cover.crop["400x400"].url,
+            "medium_square_crop": album.cover.crop["200x200"].url,
+            "small_square_crop": album.cover.crop["50x50"].url,
+        },
+        "artist": serializers.ManageNestedArtistSerializer(album.artist).data,
+        "tracks": [serializers.ManageNestedTrackSerializer(track).data],
+        "attributed_to": serializers.ManageBaseActorSerializer(
+            album.attributed_to
+        ).data,
+    }
+    s = serializers.ManageAlbumSerializer(album)
+
+    assert s.data == expected
+
+
+def test_manage_track_serializer(factories, now):
+    track = factories["music.Track"](attributed=True)
+    setattr(track, "uploads_count", 44)
+    expected = {
+        "id": track.id,
+        "domain": track.domain_name,
+        "is_local": track.is_local,
+        "fid": track.fid,
+        "title": track.title,
+        "mbid": track.mbid,
+        "disc_number": track.disc_number,
+        "position": track.position,
+        "copyright": track.copyright,
+        "license": track.license,
+        "creation_date": track.creation_date.isoformat().split("+")[0] + "Z",
+        "artist": serializers.ManageNestedArtistSerializer(track.artist).data,
+        "album": serializers.ManageTrackAlbumSerializer(track.album).data,
+        "attributed_to": serializers.ManageBaseActorSerializer(
+            track.attributed_to
+        ).data,
+        "uploads_count": 44,
+    }
+    s = serializers.ManageTrackSerializer(track)
+
+    assert s.data == expected
+
+
+@pytest.mark.parametrize(
+    "factory, serializer_class",
+    [
+        ("music.Track", serializers.ManageTrackActionSerializer),
+        ("music.Album", serializers.ManageAlbumActionSerializer),
+        ("music.Artist", serializers.ManageArtistActionSerializer),
+    ],
+)
+def test_action_serializer_delete(factory, serializer_class, factories):
+    objects = factories[factory].create_batch(size=5)
+    s = serializer_class(queryset=None)
+
+    s.handle_delete(objects[0].__class__.objects.all())
+
+    assert objects[0].__class__.objects.count() == 0
diff --git a/api/tests/manage/test_views.py b/api/tests/manage/test_views.py
index 673c39cbc9458df3aaca10e1dea391ab3780524a..923d331d8ca722a38d31410be539097f1df87453 100644
--- a/api/tests/manage/test_views.py
+++ b/api/tests/manage/test_views.py
@@ -148,3 +148,144 @@ def test_instance_policy_create(superuser_api_client, factories):
 
     policy = domain.instance_policy
     assert policy.actor == actor
+
+
+def test_artist_list(factories, superuser_api_client, settings):
+    artist = factories["music.Artist"]()
+    url = reverse("api:v1:manage:library:artists-list")
+    response = superuser_api_client.get(url)
+
+    assert response.status_code == 200
+
+    assert response.data["count"] == 1
+    assert response.data["results"][0]["id"] == artist.id
+
+
+def test_artist_detail(factories, superuser_api_client):
+    artist = factories["music.Artist"]()
+    url = reverse("api:v1:manage:library:artists-detail", kwargs={"pk": artist.pk})
+    response = superuser_api_client.get(url)
+
+    assert response.status_code == 200
+    assert response.data["id"] == artist.id
+
+
+def test_artist_detail_stats(factories, superuser_api_client):
+    artist = factories["music.Artist"]()
+    url = reverse("api:v1:manage:library:artists-stats", kwargs={"pk": artist.pk})
+    response = superuser_api_client.get(url)
+    expected = {
+        "libraries": 0,
+        "uploads": 0,
+        "listenings": 0,
+        "playlists": 0,
+        "mutations": 0,
+        "track_favorites": 0,
+        "media_total_size": 0,
+        "media_downloaded_size": 0,
+    }
+    assert response.status_code == 200
+    assert response.data == expected
+
+
+def test_artist_delete(factories, superuser_api_client):
+    artist = factories["music.Artist"]()
+    url = reverse("api:v1:manage:library:artists-detail", kwargs={"pk": artist.pk})
+    response = superuser_api_client.delete(url)
+
+    assert response.status_code == 204
+
+
+def test_album_list(factories, superuser_api_client, settings):
+    album = factories["music.Album"]()
+    factories["music.Album"]()
+    url = reverse("api:v1:manage:library:albums-list")
+    response = superuser_api_client.get(
+        url, {"q": 'artist:"{}"'.format(album.artist.name)}
+    )
+
+    assert response.status_code == 200
+
+    assert response.data["count"] == 1
+    assert response.data["results"][0]["id"] == album.id
+
+
+def test_album_detail(factories, superuser_api_client):
+    album = factories["music.Album"]()
+    url = reverse("api:v1:manage:library:albums-detail", kwargs={"pk": album.pk})
+    response = superuser_api_client.get(url)
+
+    assert response.status_code == 200
+    assert response.data["id"] == album.id
+
+
+def test_album_detail_stats(factories, superuser_api_client):
+    album = factories["music.Album"]()
+    url = reverse("api:v1:manage:library:albums-stats", kwargs={"pk": album.pk})
+    response = superuser_api_client.get(url)
+    expected = {
+        "libraries": 0,
+        "uploads": 0,
+        "listenings": 0,
+        "playlists": 0,
+        "mutations": 0,
+        "track_favorites": 0,
+        "media_total_size": 0,
+        "media_downloaded_size": 0,
+    }
+    assert response.status_code == 200
+    assert response.data == expected
+
+
+def test_album_delete(factories, superuser_api_client):
+    album = factories["music.Album"]()
+    url = reverse("api:v1:manage:library:albums-detail", kwargs={"pk": album.pk})
+    response = superuser_api_client.delete(url)
+
+    assert response.status_code == 204
+
+
+def test_track_list(factories, superuser_api_client, settings):
+    track = factories["music.Track"]()
+    url = reverse("api:v1:manage:library:tracks-list")
+    response = superuser_api_client.get(url)
+
+    assert response.status_code == 200
+
+    assert response.data["count"] == 1
+    assert response.data["results"][0]["id"] == track.id
+
+
+def test_track_detail(factories, superuser_api_client):
+    track = factories["music.Track"]()
+    url = reverse("api:v1:manage:library:tracks-detail", kwargs={"pk": track.pk})
+    response = superuser_api_client.get(url)
+
+    assert response.status_code == 200
+    assert response.data["id"] == track.id
+
+
+def test_track_detail_stats(factories, superuser_api_client):
+    track = factories["music.Track"]()
+    url = reverse("api:v1:manage:library:tracks-stats", kwargs={"pk": track.pk})
+    response = superuser_api_client.get(url)
+    expected = {
+        "libraries": 0,
+        "uploads": 0,
+        "listenings": 0,
+        "playlists": 0,
+        "mutations": 0,
+        "track_favorites": 0,
+        "media_total_size": 0,
+        "media_downloaded_size": 0,
+    }
+    assert response.status_code == 200
+    assert response.data == expected
+
+
+def test_track_delete(factories, superuser_api_client):
+    track = factories["music.Track"]()
+    url = reverse("api:v1:manage:library:tracks-detail", kwargs={"pk": track.pk})
+    response = superuser_api_client.delete(url)
+
+    assert response.status_code == 204
diff --git a/api/tests/music/test_models.py b/api/tests/music/test_models.py
index 4446de7dd978a33686c699dae6e18f0248640437..5aa29b3cc4519d0c5158301d521a3550e2d56e62 100644
--- a/api/tests/music/test_models.py
+++ b/api/tests/music/test_models.py
@@ -548,3 +548,9 @@ def test_api_model_mixin_is_local(federation_hostname, fid, expected, settings):
     settings.FEDERATION_HOSTNAME = federation_hostname
     obj = models.Track(fid=fid)
     assert obj.is_local is expected
+
+
+def test_api_model_mixin_domain_name():
+    obj = models.Track(fid="https://test.domain:543/something")
+
+    assert obj.domain_name == "test.domain"
diff --git a/api/tests/users/oauth/test_api_permissions.py b/api/tests/users/oauth/test_api_permissions.py
index aaac8430b5ae704e2de4d945fe07537188c14b8c..e73d3a3f91c71b30c2444719ed035f5049d3d4a2 100644
--- a/api/tests/users/oauth/test_api_permissions.py
+++ b/api/tests/users/oauth/test_api_permissions.py
@@ -53,6 +53,7 @@ from funkwhale_api.users.oauth import scopes
             "read:instance:policies",
             "get",
         ),
+        ("api:v1:manage:library:artists-list", {}, "read:instance:libraries", "get"),
     ],
 )
 def test_views_permissions(
diff --git a/front/src/components/Sidebar.vue b/front/src/components/Sidebar.vue
index 213ab92ce9092cbe073422ce02386396fdd942ad..8b183b215875b532c40faeff53ed547d1eb766ef 100644
--- a/front/src/components/Sidebar.vue
+++ b/front/src/components/Sidebar.vue
@@ -80,16 +80,15 @@
           <header class="header"><translate translate-context="Sidebar/Admin/Title/Noun">Administration</translate></header>
           <div class="menu">
             <router-link
-              v-if="$store.state.auth.availablePermissions['settings']"
-              class="item"
-              :to="{path: '/manage/settings'}">
-              <i class="settings icon"></i><translate translate-context="*/*/*/Noun">Settings</translate>
-            </router-link>
-            <router-link
-              v-if="$store.state.auth.availablePermissions['settings']"
+              v-if="$store.state.auth.availablePermissions['library']"
               class="item"
-              :to="{name: 'manage.users.users.list'}">
-              <i class="users icon"></i><translate translate-context="*/*/*/Noun">Users</translate>
+              :to="{name: 'manage.library.edits', query: {q: 'is_approved:null'}}">
+              <i class="book icon"></i><translate translate-context="*/*/*">Library</translate>
+              <div
+                v-if="$store.state.ui.notifications.pendingReviewEdits > 0"
+                :title="labels.pendingReviewEdits"
+                :class="['ui', 'teal', 'label']">
+                {{ $store.state.ui.notifications.pendingReviewEdits }}</div>
             </router-link>
             <router-link
               v-if="$store.state.auth.availablePermissions['moderation']"
@@ -98,15 +97,16 @@
               <i class="shield icon"></i><translate translate-context="*/Moderation/*">Moderation</translate>
             </router-link>
             <router-link
-              v-if="$store.state.auth.availablePermissions['library']"
+              v-if="$store.state.auth.availablePermissions['settings']"
               class="item"
-              :to="{name: 'manage.library.edits', query: {q: 'is_approved:null'}}">
-              <i class="book icon"></i><translate translate-context="*/*/*">Library</translate>
-              <div
-                v-if="$store.state.ui.notifications.pendingReviewEdits > 0"
-                :title="labels.pendingReviewEdits"
-                :class="['ui', 'teal', 'label']">
-                {{ $store.state.ui.notifications.pendingReviewEdits }}</div>
+              :to="{name: 'manage.users.users.list'}">
+              <i class="users icon"></i><translate translate-context="*/*/*/Noun">Users</translate>
+            </router-link>
+            <router-link
+              v-if="$store.state.auth.availablePermissions['settings']"
+              class="item"
+              :to="{path: '/manage/settings'}">
+              <i class="settings icon"></i><translate translate-context="*/*/*/Noun">Settings</translate>
             </router-link>
           </div>
         </div>
diff --git a/front/src/components/common/ActionTable.vue b/front/src/components/common/ActionTable.vue
index 9bbf18f85fbdf83d3c946bcbe3b5579381cf6cb2..1a43a5f9ff1ba4ea1e87ff61992458e8c947f0b1 100644
--- a/front/src/components/common/ActionTable.vue
+++ b/front/src/components/common/ActionTable.vue
@@ -30,7 +30,7 @@
                 <div class="field">
                   <dangerous-button
                     v-if="selectAll || currentAction.isDangerous" :class="['ui', {disabled: checked.length === 0}, {'loading': actionLoading}, 'button']"
-                    confirm-color="green"
+                    :confirm-color="currentAction.confirmColor || 'green'"
                     color=""
                     @confirm="launchAction">
                     <translate translate-context="Content/*/Button.Label/Short, Verb">Go</translate>
@@ -44,7 +44,8 @@
                       </translate>
                     </p>
                     <p slot="modal-content">
-                      <translate translate-context="Modal/*/Paragraph">This may affect a lot of elements or have irreversible consequences, please double check this is really what you want.</translate>
+                      <template v-if="currentAction.confirmationMessage">{{ currentAction.confirmationMessage }}</template>
+                      <translate v-else translate-context="Modal/*/Paragraph">This may affect a lot of elements or have irreversible consequences, please double check this is really what you want.</translate>
                     </p>
                     <div slot="modal-confirm"><translate translate-context="Modal/*/Button.Label/Short, Verb">Launch</translate></div>
                   </dangerous-button>
diff --git a/front/src/components/common/EmptyState.vue b/front/src/components/common/EmptyState.vue
index cc9f32ca1ef413fba27c62646ac90dabf3756911..862700679e2a5e9db60d756d26f6081fdb7d9b7a 100644
--- a/front/src/components/common/EmptyState.vue
+++ b/front/src/components/common/EmptyState.vue
@@ -29,7 +29,7 @@ export default {
 }
 </script>
 
-<style>
+<style scoped>
 .ui.small.placeholder.segment {
   min-height: auto;
 }
diff --git a/front/src/components/library/Album.vue b/front/src/components/library/Album.vue
index 0031aeed8460801bccb77c3545a21abcee77d16b..1a5f6b50da9e05dc8e1d0b93ba5190909b492fb4 100644
--- a/front/src/components/library/Album.vue
+++ b/front/src/components/library/Album.vue
@@ -14,26 +14,15 @@
             </div>
           </h2>
           <div class="ui hidden divider"></div>
-          <play-button class="orange" :tracks="album.tracks">
-            <translate translate-context="Content/Queue/Button.Label/Short, Verb">Play all</translate>
-          </play-button>
+          <div class="header-buttons">
+
+            <div class="ui buttons">
+              <play-button class="orange" :tracks="album.tracks">
+                <translate translate-context="Content/Queue/Button.Label/Short, Verb">Play all</translate>
+              </play-button>
+            </div>
 
-          <a :href="wikipediaUrl" target="_blank" class="ui icon labeled button">
-            <i class="wikipedia w icon"></i>
-            <translate translate-context="Content/*/Button.Label/Verb">Search on Wikipedia</translate>
-          </a>
-          <a v-if="musicbrainzUrl" :href="musicbrainzUrl" target="_blank" class="ui icon labeled button">
-            <i class="external icon"></i>
-            <translate translate-context="Content/*/*/Clickable, Verb">View on MusicBrainz</translate>
-          </a>
-          <template v-if="publicLibraries.length > 0">
-            <button
-              @click="showEmbedModal = !showEmbedModal"
-              class="ui button icon labeled">
-              <i class="code icon"></i>
-              <translate translate-context="Content/*/Button.Label/Verb">Embed</translate>
-            </button>
-            <modal :show.sync="showEmbedModal">
+            <modal v-if="publicLibraries.length > 0" :show.sync="showEmbedModal">
               <div class="header">
                 <translate translate-context="Popup/Album/Title/Verb">Embed this album on your website</translate>
               </div>
@@ -49,7 +38,46 @@
                 </div>
               </div>
             </modal>
-          </template>
+            <div class="ui buttons">
+              <button class="ui button" @click="$refs.dropdown.click()">
+                <translate translate-context="*/*/Button.Label/Noun">More…</translate>
+              </button>
+              <div class="ui floating dropdown icon button" ref="dropdown" v-dropdown>
+                <i class="dropdown icon"></i>
+                <div class="menu">
+                  <div
+                    role="button"
+                    v-if="publicLibraries.length > 0"
+                    @click="showEmbedModal = !showEmbedModal"
+                    class="basic item">
+                    <i class="code icon"></i>
+                    <translate translate-context="Content/*/Button.Label/Verb">Embed</translate>
+                  </div>
+                  <a :href="wikipediaUrl" target="_blank" rel="noreferrer noopener" class="basic item">
+                    <i class="wikipedia w icon"></i>
+                    <translate translate-context="Content/*/Button.Label/Verb">Search on Wikipedia</translate>
+                  </a>
+                  <a v-if="musicbrainzUrl" :href="musicbrainzUrl" target="_blank" rel="noreferrer noopener" class="basic item">
+                    <i class="external icon"></i>
+                    <translate translate-context="Content/*/*/Clickable, Verb">View on MusicBrainz</translate>
+                  </a>
+                  <div class="divider"></div>
+                  <router-link class="basic item" v-if="$store.state.auth.availablePermissions['library']" :to="{name: 'manage.library.albums.detail', params: {id: album.id}}">
+                    <i class="wrench icon"></i>
+                    <translate translate-context="Content/Moderation/Link">Open in moderation interface</translate>
+                  </router-link>
+                  <a
+                    v-if="$store.state.auth.profile.is_superuser"
+                    class="basic item"
+                    :href="$store.getters['instance/absoluteUrl'](`/api/admin/music/album/${album.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>
+              </div>
+            </div>
+          </div>
         </div>
       </section>
       <template v-if="discs && discs.length > 1">
diff --git a/front/src/components/library/Artist.vue b/front/src/components/library/Artist.vue
index 34071e746dde4617308d26b3cb9e646287fa34b8..4d6e10e2d755a924b294a85f1bdb49c340d17e46 100644
--- a/front/src/components/library/Artist.vue
+++ b/front/src/components/library/Artist.vue
@@ -22,27 +22,18 @@
             </div>
           </h2>
           <div class="ui hidden divider"></div>
-          <radio-button type="artist" :object-id="artist.id"></radio-button>
-          <play-button :is-playable="isPlayable" class="orange" :artist="artist">
-            <translate translate-context="Content/Artist/Button.Label/Verb">Play all albums</translate>
-          </play-button>
+          <div class="header-buttons">
+            <div class="ui buttons">
+              <radio-button type="artist" :object-id="artist.id"></radio-button>
 
-          <a :href="wikipediaUrl" target="_blank" class="ui icon labeled button">
-            <i class="wikipedia w icon"></i>
-            <translate translate-context="Content/*/Button.Label/Verb">Search on Wikipedia</translate>
-          </a>
-          <a v-if="musicbrainzUrl" :href="musicbrainzUrl" target="_blank" class="ui button">
-            <i class="external icon"></i>
-            <translate translate-context="Content/*/*/Clickable, Verb">View on MusicBrainz</translate>
-          </a>
-          <template v-if="publicLibraries.length > 0">
-            <button
-              @click="showEmbedModal = !showEmbedModal"
-              class="ui button icon labeled">
-              <i class="code icon"></i>
-              <translate translate-context="Content/*/Button.Label/Verb">Embed</translate>
-            </button>
-            <modal :show.sync="showEmbedModal">
+            </div>
+            <div class="ui buttons">
+              <play-button :is-playable="isPlayable" class="orange" :artist="artist">
+                <translate translate-context="Content/Artist/Button.Label/Verb">Play all albums</translate>
+              </play-button>
+            </div>
+
+            <modal :show.sync="showEmbedModal" v-if="publicLibraries.length > 0">
               <div class="header">
                 <translate translate-context="Popup/Artist/Title/Verb">Embed this artist work on your website</translate>
               </div>
@@ -58,7 +49,46 @@
                 </div>
               </div>
             </modal>
-          </template>
+            <div class="ui buttons">
+              <button class="ui button" @click="$refs.dropdown.click()">
+                <translate translate-context="*/*/Button.Label/Noun">More…</translate>
+              </button>
+              <div class="ui floating dropdown icon button" ref="dropdown" v-dropdown>
+                <i class="dropdown icon"></i>
+                <div class="menu">
+                  <div
+                    role="button"
+                    v-if="publicLibraries.length > 0"
+                    @click="showEmbedModal = !showEmbedModal"
+                    class="basic item">
+                    <i class="code icon"></i>
+                    <translate translate-context="Content/*/Button.Label/Verb">Embed</translate>
+                  </div>
+                  <a :href="wikipediaUrl" target="_blank" rel="noreferrer noopener" class="basic item">
+                    <i class="wikipedia w icon"></i>
+                    <translate translate-context="Content/*/Button.Label/Verb">Search on Wikipedia</translate>
+                  </a>
+                  <a v-if="musicbrainzUrl" :href="musicbrainzUrl" target="_blank" rel="noreferrer noopener" class="basic item">
+                    <i class="external icon"></i>
+                    <translate translate-context="Content/*/*/Clickable, Verb">View on MusicBrainz</translate>
+                  </a>
+                  <div class="divider"></div>
+                  <router-link class="basic item" v-if="$store.state.auth.availablePermissions['library']" :to="{name: 'manage.library.artists.detail', params: {id: artist.id}}">
+                    <i class="wrench icon"></i>
+                    <translate translate-context="Content/Moderation/Link">Open in moderation interface</translate>
+                  </router-link>
+                  <a
+                    v-if="$store.state.auth.profile.is_superuser"
+                    class="basic item"
+                    :href="$store.getters['instance/absoluteUrl'](`/api/admin/music/artist/${artist.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>
+              </div>
+            </div>
+          </div>
         </div>
       </section>
       <div class="ui small text container" v-if="contentFilter">
diff --git a/front/src/components/library/TrackBase.vue b/front/src/components/library/TrackBase.vue
index cd4572f5b65990f8449f1881738eb9fd8b622786..6df2f7aa0caabde16847f7deb8f8aa2faedbc722 100644
--- a/front/src/components/library/TrackBase.vue
+++ b/front/src/components/library/TrackBase.vue
@@ -21,33 +21,27 @@
               </div>
             </div>
           </h2>
+          <div class="header-buttons">
+            <div class="ui buttons">
+              <play-button class="orange" :track="track">
+                <translate translate-context="*/Queue/Button.Label/Short, Verb">Play</translate>
+              </play-button>
+            </div>
+            <div class="ui buttons">
+              <track-favorite-icon :track="track" :button="true"></track-favorite-icon>
+            </div>
+            <div class="ui buttons">
+              <track-playlist-icon :button="true" v-if="$store.state.auth.authenticated" :track="track"></track-playlist-icon>
+            </div>
 
-          <play-button class="orange" :track="track">
-            <translate translate-context="*/Queue/Button.Label/Short, Verb">Play</translate>
-          </play-button>
-          <track-favorite-icon :track="track" :button="true"></track-favorite-icon>
-          <track-playlist-icon :button="true" v-if="$store.state.auth.authenticated" :track="track"></track-playlist-icon>
+            <div class="ui buttons">
+              <a v-if="upload" :href="downloadUrl" target="_blank" class="ui icon labeled button">
+                <i class="download icon"></i>
+                <translate translate-context="Content/Track/Link/Verb">Download</translate>
+              </a>
+            </div>
 
-          <a :href="wikipediaUrl" target="_blank" class="ui icon labeled button">
-            <i class="wikipedia w icon"></i>
-            <translate translate-context="Content/*/Button.Label/Verb">Search on Wikipedia</translate>
-          </a>
-          <a v-if="musicbrainzUrl" :href="musicbrainzUrl" target="_blank" class="ui icon labeled button">
-            <i class="external icon"></i>
-            <translate translate-context="Content/*/*/Clickable, Verb">View on MusicBrainz</translate>
-          </a>
-          <a v-if="upload" :href="downloadUrl" target="_blank" class="ui icon labeled button">
-            <i class="download icon"></i>
-            <translate translate-context="Content/Track/Link/Verb">Download</translate>
-          </a>
-          <template v-if="publicLibraries.length > 0">
-            <button
-              @click="showEmbedModal = !showEmbedModal"
-              class="ui icon labeled button">
-              <i class="code icon"></i>
-              <translate translate-context="Content/*/Button.Label/Verb">Embed</translate>
-            </button>
-            <modal :show.sync="showEmbedModal">
+            <modal v-if="publicLibraries.length > 0" :show.sync="showEmbedModal">
               <div class="header">
                 <translate translate-context="Popup/Track/Title">Embed this track on your website</translate>
               </div>
@@ -63,14 +57,53 @@
                 </div>
               </div>
             </modal>
-          </template>
-          <router-link
-            v-if="track.is_local"
-            :to="{name: 'library.tracks.edit', params: {id: track.id }}"
-            class="ui icon labeled button">
-            <i class="edit icon"></i>
-            <translate translate-context="Content/*/Button.Label/Verb">Edit</translate>
-          </router-link>
+            <div class="ui buttons">
+              <button class="ui button" @click="$refs.dropdown.click()">
+                <translate translate-context="*/*/Button.Label/Noun">More…</translate>
+              </button>
+              <div class="ui floating dropdown icon button" ref="dropdown" v-dropdown>
+                <i class="dropdown icon"></i>
+                <div class="menu">
+                  <div
+                    role="button"
+                    v-if="publicLibraries.length > 0"
+                    @click="showEmbedModal = !showEmbedModal"
+                    class="basic item">
+                    <i class="code icon"></i>
+                    <translate translate-context="Content/*/Button.Label/Verb">Embed</translate>
+                  </div>
+                  <a :href="wikipediaUrl" target="_blank" rel="noreferrer noopener" class="basic item">
+                    <i class="wikipedia w icon"></i>
+                    <translate translate-context="Content/*/Button.Label/Verb">Search on Wikipedia</translate>
+                  </a>
+                  <a v-if="musicbrainzUrl" :href="musicbrainzUrl" target="_blank" rel="noreferrer noopener" class="basic item">
+                    <i class="external icon"></i>
+                    <translate translate-context="Content/*/*/Clickable, Verb">View on MusicBrainz</translate>
+                  </a>
+                  <router-link
+                    v-if="track.is_local"
+                    :to="{name: 'library.tracks.edit', params: {id: track.id }}"
+                    class="basic item">
+                    <i class="edit icon"></i>
+                    <translate translate-context="Content/*/Button.Label/Verb">Edit</translate>
+                  </router-link>
+                  <div class="divider"></div>
+                  <router-link class="basic item" v-if="$store.state.auth.availablePermissions['library']" :to="{name: 'manage.library.tracks.detail', params: {id: track.id}}">
+                    <i class="wrench icon"></i>
+                    <translate translate-context="Content/Moderation/Link">Open in moderation interface</translate>
+                  </router-link>
+                  <a
+                    v-if="$store.state.auth.profile.is_superuser"
+                    class="basic item"
+                    :href="$store.getters['instance/absoluteUrl'](`/api/admin/music/track/${track.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>
+              </div>
+            </div>
+          </div>
         </div>
       </section>
       <router-view v-if="track" @libraries-loaded="libraries = $event" :track="track" :object="track" object-type="track" :key="$route.fullPath"></router-view>
diff --git a/front/src/components/manage/library/AlbumsTable.vue b/front/src/components/manage/library/AlbumsTable.vue
new file mode 100644
index 0000000000000000000000000000000000000000..3af2da4db9bccd1d5d691f2b0ee9152fe9d2b7e1
--- /dev/null
+++ b/front/src/components/manage/library/AlbumsTable.vue
@@ -0,0 +1,218 @@
+<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="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/albums/action/"
+        :filters="actionFilters">
+        <template slot="header-cells">
+          <th><translate translate-context="*/*/*">Title</translate></th>
+          <th><translate translate-context="*/*/*">Artist</translate></th>
+          <th><translate translate-context="Content/Moderation/*/Noun">Domain</translate></th>
+          <th><translate translate-context="*/*/*">Tracks</translate></th>
+          <th><translate translate-context="Content/*/*/Noun">Release date</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.albums.detail', params: {id: scope.obj.id }}">{{ scope.obj.title }}</router-link>
+          </td>
+          <td>
+            <router-link :to="{name: 'manage.library.artists.detail', params: {id: scope.obj.artist.id }}">
+              <i class="wrench icon"></i>
+            </router-link>
+            <span role="button" class="discrete link" @click="addSearchToken('artist', scope.obj.artist.name)" :title="scope.obj.artist.name">{{ scope.obj.artist.name }}</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>
+            {{ scope.obj.tracks.length }}
+          </td>
+          <td>
+            <human-date v-if="scope.obj.release_date" :date="scope.obj.release_date"></human-date>
+            <translate v-else translate-context="*/*/*">N/A</translate>
+
+          </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'],
+        ['release_date', 'release_date'],
+        ["name", "name"],
+      ]
+    }
+  },
+  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/albums/', {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, title, artist, MusicBrainz ID…')
+      }
+    },
+    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 albums will be removed, as well as associated tracks, uploads, favorites and listening history. 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/ArtistsTable.vue b/front/src/components/manage/library/ArtistsTable.vue
new file mode 100644
index 0000000000000000000000000000000000000000..84c873832d5f3429565b4fd55caec6e03c1501bf
--- /dev/null
+++ b/front/src/components/manage/library/ArtistsTable.vue
@@ -0,0 +1,208 @@
+<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="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/artists/action/"
+        :filters="actionFilters">
+        <template slot="header-cells">
+          <th><translate translate-context="*/*/*/Noun">Name</translate></th>
+          <th><translate translate-context="Content/Moderation/*/Noun">Domain</translate></th>
+          <th><translate translate-context="*/*/*">Albums</translate></th>
+          <th><translate translate-context="*/*/*">Tracks</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.artists.detail', params: {id: scope.obj.id }}">{{ scope.obj.name }}</router-link>
+          </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>
+            {{ scope.obj.albums.length }}
+          </td>
+          <td>
+            {{ scope.obj.tracks.length }}
+          </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'],
+        ["name", "name"],
+      ]
+    }
+  },
+  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/artists/', {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, name, MusicBrainz ID…')
+      }
+    },
+    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 artist will be removed, as well as associated uploads, tracks, albums, favorites and listening history. 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/TracksTable.vue b/front/src/components/manage/library/TracksTable.vue
new file mode 100644
index 0000000000000000000000000000000000000000..a702d8e25cb180827e1134aad530f99936bd7675
--- /dev/null
+++ b/front/src/components/manage/library/TracksTable.vue
@@ -0,0 +1,218 @@
+<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="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/tracks/action/"
+        :filters="actionFilters">
+        <template slot="header-cells">
+          <th><translate translate-context="*/*/*">Title</translate></th>
+          <th><translate translate-context="*/*/*">Album</translate></th>
+          <th><translate translate-context="*/*/*">Artist</translate></th>
+          <th><translate translate-context="Content/Moderation/*/Noun">Domain</translate></th>
+          <th><translate translate-context="Content/*/*/Noun">License</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.tracks.detail', params: {id: scope.obj.id }}">{{ scope.obj.title }}</router-link>
+          </td>
+          <td>
+            <router-link :to="{name: 'manage.library.albums.detail', params: {id: scope.obj.album.id }}">
+              <i class="wrench icon"></i>
+            </router-link>
+            <span role="button" class="discrete link" @click="addSearchToken('album_id', scope.obj.album.id)" :title="scope.obj.album.title">{{ scope.obj.album.title }}</span>
+          </td>
+          <td>
+            <router-link :to="{name: 'manage.library.artists.detail', params: {id: scope.obj.artist.id }}">
+              <i class="wrench icon"></i>
+            </router-link>
+            <span role="button" class="discrete link" @click="addSearchToken('artist_id', scope.obj.artist.id)" :title="scope.obj.artist.name">{{ scope.obj.artist.name }}</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" v-if="scope.obj.license" class="discrete link" @click="addSearchToken('license', scope.obj.license)" :title="scope.obj.license">{{ scope.obj.license }}</span>
+            <translate v-else translate-context="*/*/*">N/A</translate>
+          </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'],
+      ]
+    }
+  },
+  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/tracks/', {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, title, artist, album, MusicBrainz ID…')
+      }
+    },
+    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 tracks will be removed, as well as associated uploads, favorites and listening history. 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/SmartSearch.vue b/front/src/components/mixins/SmartSearch.vue
index 8b03becbbd8e7fb7b3bd38eaa673d9b01615a728..ef450603961a8cbdf61385d861698993a7ee5734 100644
--- a/front/src/components/mixins/SmartSearch.vue
+++ b/front/src/components/mixins/SmartSearch.vue
@@ -18,6 +18,7 @@ export default {
       return fallback
     },
     addSearchToken (key, value) {
+      value = String(value)
       if (!value) {
         // we remove existing matching tokens, if any
         this.search.tokens = this.search.tokens.filter(t => {
@@ -45,17 +46,19 @@ export default {
     },
     'search.tokens': {
       handler (newValue) {
-        this.search.query = compileTokens(newValue)
-        this.page = 1
-        this.fetchData()
+        let newQuery = compileTokens(newValue)
         if (this.updateUrl) {
           let params = {}
-          if (this.search.query) {
-            params.q = this.search.query
+          if (newQuery) {
+            params.q = newQuery
           }
           this.$router.replace({
             query: params
           })
+        } else {
+          this.search.query = newQuery
+          this.page = 1
+          this.fetchData()
         }
       },
       deep: true
diff --git a/front/src/components/mixins/Translations.vue b/front/src/components/mixins/Translations.vue
index 2bbc64c83f05314018ce9e0ea101920a228cfc28..b2bd455cc64e916a11f36a361a0573db3a88d5e1 100644
--- a/front/src/components/mixins/Translations.vue
+++ b/front/src/components/mixins/Translations.vue
@@ -16,6 +16,7 @@ export default {
         },
         filters: {
           creation_date: this.$pgettext('Content/*/*/Noun', 'Creation date'),
+          release_date: this.$pgettext('Content/*/*/Noun', 'Release 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/main.js b/front/src/main.js
index 31aa07d03193354d3bd04daf960023b69c651307..959b776b3df147f1fab98c48cc2e74c7bdb05f67 100644
--- a/front/src/main.js
+++ b/front/src/main.js
@@ -4,6 +4,7 @@ import logger from '@/logging'
 
 logger.default.info('Loading environment:', process.env.NODE_ENV)
 logger.default.debug('Environment variables:', process.env)
+import jQuery from "jquery"
 
 import Vue from 'vue'
 import App from './App'
@@ -60,6 +61,11 @@ Vue.config.productionTip = false
 Vue.directive('title', function (el, binding) {
   store.commit('ui/pageTitle', binding.value)
 })
+Vue.directive('dropdown', function (el, binding) {
+  jQuery(el).dropdown({
+    selectOnKeydown: false,
+  })
+})
 axios.interceptors.request.use(function (config) {
   // Do something before request is sent
   if (store.state.auth.token) {
diff --git a/front/src/router/index.js b/front/src/router/index.js
index 8703eedead7693332d242ea6a52ef65c06aa358a..bee09f20281da8c94c6543c07ba63820bfa4f67b 100644
--- a/front/src/router/index.js
+++ b/front/src/router/index.js
@@ -33,6 +33,12 @@ import Favorites from '@/components/favorites/List'
 import AdminSettings from '@/views/admin/Settings'
 import AdminLibraryBase from '@/views/admin/library/Base'
 import AdminLibraryEditsList from '@/views/admin/library/EditsList'
+import AdminLibraryArtistsList from '@/views/admin/library/ArtistsList'
+import AdminLibraryArtistsDetail from '@/views/admin/library/ArtistDetail'
+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 AdminUsersBase from '@/views/admin/users/Base'
 import AdminUsersList from '@/views/admin/users/UsersList'
 import AdminInvitationsList from '@/views/admin/users/InvitationsList'
@@ -244,7 +250,55 @@ export default new Router({
               defaultQuery: route.query.q,
             }
           }
-        }
+        },
+        {
+          path: 'artists',
+          name: 'manage.library.artists',
+          component: AdminLibraryArtistsList,
+          props: (route) => {
+            return {
+              defaultQuery: route.query.q,
+            }
+          }
+        },
+        {
+          path: 'artists/:id',
+          name: 'manage.library.artists.detail',
+          component: AdminLibraryArtistsDetail,
+          props: true
+        },
+        {
+          path: 'albums',
+          name: 'manage.library.albums',
+          component: AdminLibraryAlbumsList,
+          props: (route) => {
+            return {
+              defaultQuery: route.query.q,
+            }
+          }
+        },
+        {
+          path: 'albums/:id',
+          name: 'manage.library.albums.detail',
+          component: AdminLibraryAlbumDetail,
+          props: true
+        },
+        {
+          path: 'tracks',
+          name: 'manage.library.tracks',
+          component: AdminLibraryTracksList,
+          props: (route) => {
+            return {
+              defaultQuery: route.query.q,
+            }
+          }
+        },
+        {
+          path: 'tracks/:id',
+          name: 'manage.library.tracks.detail',
+          component: AdminLibraryTrackDetail,
+          props: true
+        },
       ]
     },
     {
diff --git a/front/src/style/_main.scss b/front/src/style/_main.scss
index cd12dda2f24465c1daabaa49b58379a623e79853..c1ddb3628d93e0111fcf19f99b35acb1eca70914 100644
--- a/front/src/style/_main.scss
+++ b/front/src/style/_main.scss
@@ -231,8 +231,15 @@ body {
   justify-content: center;
 }
 
-.segment-content .button {
-  margin: 0.5em;
+.header-buttons > .buttons {
+  display: inline-block;
+  padding: 0.2em;
+  margin: 0;
+  font-size: 1em;
+  .buttons {
+    margin: 0;
+  }
+
 }
 
 a {
diff --git a/front/src/views/admin/library/AlbumDetail.vue b/front/src/views/admin/library/AlbumDetail.vue
new file mode 100644
index 0000000000000000000000000000000000000000..3215da10113c8c3945e0ea132c111dea04c7ff99
--- /dev/null
+++ b/front/src/views/admin/library/AlbumDetail.vue
@@ -0,0 +1,322 @@
+<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">
+                <img v-if="object.cover.original" v-lazy="$store.getters['instance/absoluteUrl'](object.cover.square_crop)">
+                <img v-else src="../../../assets/audio/default-cover.png">
+                <div class="content">
+                  {{ object.title | 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">
+                  <router-link class="ui labeled icon button" :to="{name: 'library.albums.detail', params: {id: object.id }}">
+                    <i class="info icon"></i>
+                    <translate translate-context="Content/Moderation/Link/Verb">Open local profile</translate>&nbsp;
+                  </router-link>
+                  <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/album/${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" v-if="object.mbid" :href="`https://musicbrainz.org/release/${object.mbid}`" target="_blank" rel="noopener noreferrer">
+                        <i class="external icon"></i>
+                        <translate translate-context="Content/Moderation/Link/Verb">Open on MusicBrainz</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">
+                  <router-link
+                    v-if="object.is_local"
+                    :to="{name: 'library.albums.edit', params: {id: object.id }}"
+                    class="ui labeled icon button">
+                    <i class="edit icon"></i>
+                    <translate translate-context="Content/*/Button.Label/Verb">Edit</translate>
+                  </router-link>
+                </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 album?</translate></p>
+                    <div slot="modal-content">
+                      <p><translate translate-context="Content/Moderation/Paragraph">The album will be removed, as well as associated uploads, tracks, favorites and listening history. 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">Album data</translate>
+                </div>
+              </h3>
+              <table class="ui very basic table">
+                <tbody>
+                  <tr>
+                    <td>
+                      <translate translate-context="*/*/*/Noun">Title</translate>
+                    </td>
+                    <td>
+                      {{ object.title }}
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <router-link :to="{name: 'manage.library.artists.detail', params: {id: object.artist.id }}">
+                        <translate translate-context="*/*/*/Noun">Artist</translate>
+                      </router-link>
+                    </td>
+                    <td>
+                      {{ 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 }}
+                      </router-link>
+                    </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="*/*/*/Noun">Listenings</translate>
+                    </td>
+                    <td>
+                      {{ stats.listenings }}
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <translate translate-context="*/*/*">Favorited tracks</translate>
+                    </td>
+                    <td>
+                      {{ stats.track_favorites }}
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <translate translate-context="*/*/*">Playlists</translate>
+                    </td>
+                    <td>
+                      {{ stats.playlists }}
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <router-link :to="{name: 'manage.library.edits', query: {q: getQuery('target', 'album ' + object.id)}}">
+                        <translate translate-context="*/Admin/*/Noun">Edits</translate>
+                      </router-link>
+                    </td>
+                    <td>
+                      {{ stats.mutations }}
+                    </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>
+                      <translate translate-context="*/*/*/Noun">Libraries</translate>
+                    </td>
+                    <td>
+                      {{ stats.libraries }}
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <translate translate-context="Content/Moderation/Table.Label/Noun">Uploads</translate>
+                    </td>
+                    <td>
+                      {{ stats.uploads }}
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <router-link :to="{name: 'manage.library.tracks', query: {q: getQuery('album_id', object.id) }}">
+                        <translate translate-context="*/*/*">Tracks</translate>
+                      </router-link>
+                    </td>
+                    <td>
+                      {{ object.tracks.length }}
+                    </td>
+                  </tr>
+                </tbody>
+              </table>
+
+            </section>
+          </div>
+        </div>
+      </div>
+
+    </template>
+  </main>
+</template>
+
+<script>
+import axios from "axios"
+import logger from "@/logging"
+
+
+export default {
+  props: ["id"],
+  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/albums/${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/albums/${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/albums/${this.id}/`
+      axios.delete(url).then(response => {
+        self.$router.push({name: 'manage.library.albums'})
+      })
+    },
+    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/AlbumsList.vue b/front/src/views/admin/library/AlbumsList.vue
new file mode 100644
index 0000000000000000000000000000000000000000..650b4d69389aadd05ae8fd2e7a385ed02c2a5945
--- /dev/null
+++ b/front/src/views/admin/library/AlbumsList.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>
+      <albums-table :update-url="true" :default-query="defaultQuery"></albums-table>
+    </section>
+  </main>
+</template>
+
+<script>
+import AlbumsTable from "@/components/manage/library/AlbumsTable"
+
+export default {
+  components: {
+    AlbumsTable
+  },
+  props: {
+    defaultQuery: {type: String, required: false},
+  },
+  computed: {
+    labels() {
+      return {
+        title: this.$pgettext('*/*/*', 'Albums')
+      }
+    }
+  }
+}
+</script>
diff --git a/front/src/views/admin/library/ArtistDetail.vue b/front/src/views/admin/library/ArtistDetail.vue
new file mode 100644
index 0000000000000000000000000000000000000000..bfbd414a171900fe11a709f4a5624aaa1e5abd00
--- /dev/null
+++ b/front/src/views/admin/library/ArtistDetail.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 user 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">
+                  <router-link class="ui labeled icon button" :to="{name: 'library.artists.detail', params: {id: object.id }}">
+                    <i class="info icon"></i>
+                    <translate translate-context="Content/Moderation/Link/Verb">Open local profile</translate>&nbsp;
+                  </router-link>
+                  <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/artist/${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" v-if="object.mbid" :href="`https://musicbrainz.org/artist/${object.mbid}`" target="_blank" rel="noopener noreferrer">
+                        <i class="external icon"></i>
+                        <translate translate-context="Content/Moderation/Link/Verb">Open on MusicBrainz</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">
+                  <router-link
+                    v-if="object.is_local"
+                    :to="{name: 'library.artists.edit', params: {id: object.id }}"
+                    class="ui labeled icon button">
+                    <i class="edit icon"></i>
+                    <translate translate-context="Content/*/Button.Label/Verb">Edit</translate>
+                  </router-link>
+                </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 artist?</translate></p>
+                    <div slot="modal-content">
+                      <p><translate translate-context="Content/Moderation/Paragraph">The artist will be removed, as well as associated uploads, tracks, albums, favorites and listening history. 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">Artist 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>
+                      <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 }}
+                      </router-link>
+                    </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="*/*/*/Noun">Listenings</translate>
+                    </td>
+                    <td>
+                      {{ stats.listenings }}
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <translate translate-context="*/*/*">Favorited tracks</translate>
+                    </td>
+                    <td>
+                      {{ stats.track_favorites }}
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <translate translate-context="*/*/*">Playlists</translate>
+                    </td>
+                    <td>
+                      {{ stats.playlists }}
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <router-link :to="{name: 'manage.library.edits', query: {q: getQuery('target', 'artist ' + object.id)}}">
+                        <translate translate-context="*/Admin/*/Noun">Edits</translate>
+                      </router-link>
+                    </td>
+                    <td>
+                      {{ stats.mutations }}
+                    </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>
+                      <translate translate-context="*/*/*/Noun">Libraries</translate>
+                    </td>
+                    <td>
+                      {{ stats.libraries }}
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <translate translate-context="Content/Moderation/Table.Label/Noun">Uploads</translate>
+                    </td>
+                    <td>
+                      {{ stats.uploads }}
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <router-link :to="{name: 'manage.library.albums', query: {q: getQuery('artist_id', object.id) }}">
+                        <translate translate-context="*/*/*">Albums</translate>
+                      </router-link>
+                    </td>
+                    <td>
+                      {{ object.albums.length }}
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <router-link :to="{name: 'manage.library.tracks', query: {q: getQuery('artist_id', object.id) }}">
+                        <translate translate-context="*/*/*">Tracks</translate>
+                      </router-link>
+                    </td>
+                    <td>
+                      {{ object.tracks.length }}
+                    </td>
+                  </tr>
+                </tbody>
+              </table>
+
+            </section>
+          </div>
+        </div>
+      </div>
+
+    </template>
+  </main>
+</template>
+
+<script>
+import axios from "axios"
+import logger from "@/logging"
+
+
+export default {
+  props: ["id"],
+  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/artists/${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/artists/${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/artists/${this.id}/`
+      axios.delete(url).then(response => {
+        self.$router.push({name: 'manage.library.artists'})
+      })
+    },
+    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/ArtistsList.vue b/front/src/views/admin/library/ArtistsList.vue
new file mode 100644
index 0000000000000000000000000000000000000000..2a5932796ec6003703942ee1fe6e7e31f4d532c9
--- /dev/null
+++ b/front/src/views/admin/library/ArtistsList.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>
+      <artists-table :update-url="true" :default-query="defaultQuery"></artists-table>
+    </section>
+  </main>
+</template>
+
+<script>
+import ArtistsTable from "@/components/manage/library/ArtistsTable"
+
+export default {
+  components: {
+    ArtistsTable
+  },
+  props: {
+    defaultQuery: {type: String, required: false},
+  },
+  computed: {
+    labels() {
+      return {
+        title: this.$pgettext('*/*/*', 'Artists')
+      }
+    }
+  }
+}
+</script>
diff --git a/front/src/views/admin/library/Base.vue b/front/src/views/admin/library/Base.vue
index b521e8f6ece9232d9a8c92221de0e7d5af8ef2ac..4b7048a17bbe0cd6d187d49f1a6e0e606be05a9c 100644
--- a/front/src/views/admin/library/Base.vue
+++ b/front/src/views/admin/library/Base.vue
@@ -4,6 +4,15 @@
       <router-link
         class="ui item"
         :to="{name: 'manage.library.edits'}"><translate translate-context="*/Admin/*/Noun">Edits</translate></router-link>
+      <router-link
+        class="ui item"
+        :to="{name: 'manage.library.artists'}"><translate translate-context="*/*/*">Artists</translate></router-link>
+      <router-link
+        class="ui item"
+        :to="{name: 'manage.library.albums'}"><translate translate-context="*/*/*">Albums</translate></router-link>
+      <router-link
+        class="ui item"
+        :to="{name: 'manage.library.tracks'}"><translate translate-context="*/*/*">Tracks</translate></router-link>
     </nav>
     <router-view :key="$route.fullPath"></router-view>
   </div>
diff --git a/front/src/views/admin/library/TrackDetail.vue b/front/src/views/admin/library/TrackDetail.vue
new file mode 100644
index 0000000000000000000000000000000000000000..6e0c9f65e29b73ee89552a6b773324759a934a7c
--- /dev/null
+++ b/front/src/views/admin/library/TrackDetail.vue
@@ -0,0 +1,364 @@
+<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 user icon"></i>
+                <div class="content">
+                  {{ object.title | 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">
+                  <router-link class="ui icon labeled button" :to="{name: 'library.tracks.detail', params: {id: object.id }}">
+                    <i class="info icon"></i>
+                    <translate translate-context="Content/Moderation/Link/Verb">Open local profile</translate>&nbsp;
+                  </router-link>
+                  <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/track/${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" v-if="object.mbid" :href="`https://musicbrainz.org/recording/${object.mbid}`" target="_blank" rel="noopener noreferrer">
+                        <i class="external icon"></i>
+                        <translate translate-context="Content/Moderation/Link/Verb">Open on MusicBrainz</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">
+                  <router-link
+                    v-if="object.is_local"
+                    :to="{name: 'library.tracks.edit', params: {id: object.id }}"
+                    class="ui labeled icon button">
+                    <i class="edit icon"></i>
+                    <translate translate-context="Content/*/Button.Label/Verb">Edit</translate>
+                  </router-link>
+                </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 album?</translate></p>
+                    <div slot="modal-content">
+                      <p><translate translate-context="Content/Moderation/Paragraph">The track will be removed, as well as associated uploads, favorites and listening history. 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">Track data</translate>
+                </div>
+              </h3>
+              <table class="ui very basic table">
+                <tbody>
+                  <tr>
+                    <td>
+                      <translate translate-context="*/*/*/Noun">Title</translate>
+                    </td>
+                    <td>
+                      {{ object.title }}
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <router-link :to="{name: 'manage.library.albums.detail', params: {id: object.album.id }}">
+                        <translate translate-context="*/*/*/Noun">Album</translate>
+                      </router-link>
+                    </td>
+                    <td>
+                      {{ object.album.title }}
+                    </td>
+                  </tr>
+
+                  <tr>
+                    <td>
+                      <router-link :to="{name: 'manage.library.artists.detail', params: {id: object.artist.id }}">
+                        <translate translate-context="*/*/*/Noun">Artist</translate>
+                      </router-link>
+                    </td>
+                    <td>
+                      {{ object.artist.name }}
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <router-link :to="{name: 'manage.library.artists.detail', params: {id: object.album.artist.id }}">
+                        <translate translate-context="*/*/*/Noun">Album artist</translate>
+                      </router-link>
+                    </td>
+                    <td>
+                      {{ 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>
+                    </td>
+                    <td>
+                      {{ object.position }}
+                    </td>
+                  </tr>
+                  <tr v-if="object.disc_number">
+                    <td>
+                      <translate translate-context="*/*/*/Noun">Disc number</translate>
+                    </td>
+                    <td>
+                      {{ object.disc_number }}
+                    </td>
+                  </tr>
+                  <tr v-if="object.copyright">
+                    <td>
+                      <translate translate-context="Content/Track/Table.Label/Noun">Copyright</translate>
+                    </td>
+                    <td>{{ object.copyright }}</td>
+                  </tr>
+                  <tr v-if="object.license">
+                    <td>
+                      <translate translate-context="Content/*/*/Noun">License</translate>
+                    </td>
+                    <td>
+                      <router-link :to="{name: 'manage.library.tracks', query: {q: getQuery('license', object.license)}}">
+                        {{ object.license }}
+                      </router-link>
+                    </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 }}
+                      </router-link>
+                    </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="*/*/*/Noun">Listenings</translate>
+                    </td>
+                    <td>
+                      {{ stats.listenings }}
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <translate translate-context="*/*/*">Favorited tracks</translate>
+                    </td>
+                    <td>
+                      {{ stats.track_favorites }}
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <translate translate-context="*/*/*">Playlists</translate>
+                    </td>
+                    <td>
+                      {{ stats.playlists }}
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <router-link :to="{name: 'manage.library.edits', query: {q: getQuery('target', 'track ' + object.id)}}">
+                        <translate translate-context="*/Admin/*/Noun">Edits</translate>
+                      </router-link>
+                    </td>
+                    <td>
+                      {{ stats.mutations }}
+                    </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>
+                      <translate translate-context="*/*/*/Noun">Libraries</translate>
+                    </td>
+                    <td>
+                      {{ stats.libraries }}
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <translate translate-context="Content/Moderation/Table.Label/Noun">Uploads</translate>
+                    </td>
+                    <td>
+                      {{ stats.uploads }}
+                    </td>
+                  </tr>
+                </tbody>
+              </table>
+
+            </section>
+          </div>
+        </div>
+      </div>
+
+    </template>
+  </main>
+</template>
+
+<script>
+import axios from "axios"
+import logger from "@/logging"
+
+
+export default {
+  props: ["id"],
+  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/tracks/${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/tracks/${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/tracks/${this.id}/`
+      axios.delete(url).then(response => {
+        self.$router.push({name: 'manage.library.tracks'})
+      })
+    },
+    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/TracksList.vue b/front/src/views/admin/library/TracksList.vue
new file mode 100644
index 0000000000000000000000000000000000000000..3aefc86060af1a12aa006136f49c224f27b2b7af
--- /dev/null
+++ b/front/src/views/admin/library/TracksList.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>
+      <tracks-table :update-url="true" :default-query="defaultQuery"></tracks-table>
+    </section>
+  </main>
+</template>
+
+<script>
+import TracksTable from "@/components/manage/library/TracksTable"
+
+export default {
+  components: {
+    TracksTable
+  },
+  props: {
+    defaultQuery: {type: String, required: false},
+  },
+  computed: {
+    labels() {
+      return {
+        title: this.$pgettext('*/*/*', 'Tracks')
+      }
+    }
+  }
+}
+</script>
diff --git a/front/src/views/admin/moderation/AccountsDetail.vue b/front/src/views/admin/moderation/AccountsDetail.vue
index 2338d4812c6e6ae461a2faaf6d11656b6fe2d8e2..ec02f7e11ac0b87bbf302e3085d23cc0eec6fd79 100644
--- a/front/src/views/admin/moderation/AccountsDetail.vue
+++ b/front/src/views/admin/moderation/AccountsDetail.vue
@@ -14,7 +14,7 @@
                   {{ object.full_username }}
                   <div class="sub header">
                     <template v-if="object.user">
-                      <span class="ui tiny teal icon label">
+                      <span class="ui tiny teal label">
                         <i class="home icon"></i>
                         <translate translate-context="Content/Moderation/*/Short, Noun">Local account</translate>
                       </span>