diff --git a/api/funkwhale_api/audio/models.py b/api/funkwhale_api/audio/models.py
index f97442d74dfca517f35f0481d762b0cbc150102e..37800962c4509147ab06d97b3caefddb6e804e35 100644
--- a/api/funkwhale_api/audio/models.py
+++ b/api/funkwhale_api/audio/models.py
@@ -69,6 +69,15 @@ class Channel(models.Model):
 
     objects = ChannelQuerySet.as_manager()
 
+    @property
+    def fid(self):
+        if not self.is_external_rss:
+            return self.actor.fid
+
+    @property
+    def is_external_rss(self):
+        return self.actor.preferred_username.startswith("rssfeed-")
+
     def get_absolute_url(self):
         suffix = self.uuid
         if self.actor.is_local:
@@ -78,9 +87,7 @@ class Channel(models.Model):
         return federation_utils.full_url("/channels/{}".format(suffix))
 
     def get_rss_url(self):
-        if not self.artist.is_local or self.actor.preferred_username.startswith(
-            "rssfeed-"
-        ):
+        if not self.artist.is_local or self.is_external_rss:
             return self.rss_url
 
         return federation_utils.full_url(
@@ -90,10 +97,6 @@ class Channel(models.Model):
             )
         )
 
-    @property
-    def fid(self):
-        return self.actor.fid
-
 
 def generate_actor(username, **kwargs):
     actor_data = user_models.get_actor_data(username, **kwargs)
diff --git a/api/funkwhale_api/federation/models.py b/api/funkwhale_api/federation/models.py
index 50e8eef1a634c298f2b526df8c0feb311e7f8cbb..7f8121641c4b272c74cac7dd799cd1c609e36e76 100644
--- a/api/funkwhale_api/federation/models.py
+++ b/api/funkwhale_api/federation/models.py
@@ -145,6 +145,7 @@ class Domain(models.Model):
             actors=models.Count("actors", distinct=True),
             outbox_activities=models.Count("actors__outbox_activities", distinct=True),
             libraries=models.Count("actors__libraries", distinct=True),
+            channels=models.Count("actors__owned_channels", distinct=True),
             received_library_follows=models.Count(
                 "actors__libraries__received_follows", distinct=True
             ),
@@ -283,6 +284,7 @@ class Actor(models.Model):
         data = Actor.objects.filter(pk=self.pk).aggregate(
             outbox_activities=models.Count("outbox_activities", distinct=True),
             libraries=models.Count("libraries", distinct=True),
+            channels=models.Count("owned_channels", distinct=True),
             received_library_follows=models.Count(
                 "libraries__received_follows", distinct=True
             ),
diff --git a/api/funkwhale_api/federation/routes.py b/api/funkwhale_api/federation/routes.py
index 56eae7f125775f8dce2ae19f4f3a0ef04b89521d..4177f76d5592a8d156d490df0ed88a2644bb0f38 100644
--- a/api/funkwhale_api/federation/routes.py
+++ b/api/funkwhale_api/federation/routes.py
@@ -482,6 +482,8 @@ def inbox_flag(payload, context):
 @outbox.register({"type": "Flag"})
 def outbox_flag(context):
     report = context["report"]
+    if not report.target or not report.target.fid:
+        return
     actor = actors.get_service_actor()
     serializer = serializers.FlagSerializer(report)
     yield {
diff --git a/api/funkwhale_api/federation/utils.py b/api/funkwhale_api/federation/utils.py
index b1f3fdb069af5db5623bf087ff0dcf41d5f33809..7f2f346e12775bcfc7de19b897ca5a2aa2f1e5db 100644
--- a/api/funkwhale_api/federation/utils.py
+++ b/api/funkwhale_api/federation/utils.py
@@ -266,5 +266,11 @@ def get_object_by_fid(fid, local=None):
 
     if not result:
         raise ObjectDoesNotExist()
-
-    return apps.get_model(*result["__type"].split(".")).objects.get(fid=fid)
+    model = apps.get_model(*result["__type"].split("."))
+    instance = model.objects.get(fid=fid)
+    if model._meta.label == "federation.Actor":
+        channel = instance.get_channel()
+        if channel:
+            return channel
+
+    return instance
diff --git a/api/funkwhale_api/manage/filters.py b/api/funkwhale_api/manage/filters.py
index eb86a59f025cac52d6e18dc4703577827e8f8333..7279119956b0e10cf235b30ad9a76298dd9cf162 100644
--- a/api/funkwhale_api/manage/filters.py
+++ b/api/funkwhale_api/manage/filters.py
@@ -8,6 +8,7 @@ from funkwhale_api.common import fields
 from funkwhale_api.common import filters as common_filters
 from funkwhale_api.common import search
 
+from funkwhale_api.audio import models as audio_models
 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
@@ -34,6 +35,34 @@ def get_actor_filter(actor_field):
     return {"field": ActorField(), "handler": handler}
 
 
+class ManageChannelFilterSet(filters.FilterSet):
+    q = fields.SmartSearchFilter(
+        config=search.SearchConfig(
+            search_fields={
+                "name": {"to": "artist__name"},
+                "username": {"to": "artist__name"},
+                "fid": {"to": "artist__fid"},
+                "rss": {"to": "rss_url"},
+            },
+            filter_fields={
+                "uuid": {"to": "uuid"},
+                "category": {"to": "artist__content_category"},
+                "domain": {
+                    "handler": lambda v: federation_utils.get_domain_query_from_url(
+                        v, url_field="attributed_to__fid"
+                    )
+                },
+                "tag": {"to": "artist__tagged_items__tag__name", "distinct": True},
+                "account": get_actor_filter("attributed_to"),
+            },
+        )
+    )
+
+    class Meta:
+        model = audio_models.Channel
+        fields = ["q"]
+
+
 class ManageArtistFilterSet(filters.FilterSet):
     q = fields.SmartSearchFilter(
         config=search.SearchConfig(
@@ -52,6 +81,7 @@ class ManageArtistFilterSet(filters.FilterSet):
                     "field": forms.IntegerField(),
                     "distinct": True,
                 },
+                "category": {"to": "content_category"},
                 "tag": {"to": "tagged_items__tag__name", "distinct": True},
             },
         )
@@ -59,7 +89,7 @@ class ManageArtistFilterSet(filters.FilterSet):
 
     class Meta:
         model = music_models.Artist
-        fields = ["q", "name", "mbid", "fid"]
+        fields = ["q", "name", "mbid", "fid", "content_category"]
 
 
 class ManageAlbumFilterSet(filters.FilterSet):
diff --git a/api/funkwhale_api/manage/serializers.py b/api/funkwhale_api/manage/serializers.py
index c6966f108789e270168bb4058306525860922de2..d29433e5669d15e4168833cd58225e00c033b110 100644
--- a/api/funkwhale_api/manage/serializers.py
+++ b/api/funkwhale_api/manage/serializers.py
@@ -3,6 +3,7 @@ from django.db import transaction
 
 from rest_framework import serializers
 
+from funkwhale_api.audio import models as audio_models
 from funkwhale_api.common import fields as common_fields
 from funkwhale_api.common import serializers as common_serializers
 from funkwhale_api.common import utils as common_utils
@@ -386,26 +387,39 @@ class ManageNestedAlbumSerializer(ManageBaseAlbumSerializer):
 class ManageArtistSerializer(
     music_serializers.OptionalDescriptionMixin, ManageBaseArtistSerializer
 ):
-    albums = ManageNestedAlbumSerializer(many=True)
-    tracks = ManageNestedTrackSerializer(many=True)
     attributed_to = ManageBaseActorSerializer()
     tags = serializers.SerializerMethodField()
+    tracks_count = serializers.SerializerMethodField()
+    albums_count = serializers.SerializerMethodField()
+    channel = serializers.SerializerMethodField()
     cover = music_serializers.cover_field
 
     class Meta:
         model = music_models.Artist
         fields = ManageBaseArtistSerializer.Meta.fields + [
-            "albums",
-            "tracks",
+            "tracks_count",
+            "albums_count",
             "attributed_to",
             "tags",
             "cover",
+            "channel",
+            "content_category",
         ]
 
+    def get_tracks_count(self, obj):
+        return getattr(obj, "_tracks_count", None)
+
+    def get_albums_count(self, obj):
+        return getattr(obj, "_albums_count", None)
+
     def get_tags(self, obj):
         tagged_items = getattr(obj, "_prefetched_tagged_items", [])
         return [ti.tag.name for ti in tagged_items]
 
+    def get_channel(self, obj):
+        if "channel" in obj._state.fields_cache and obj.get_channel():
+            return str(obj.channel.uuid)
+
 
 class ManageNestedArtistSerializer(ManageBaseArtistSerializer):
     pass
@@ -743,3 +757,23 @@ class ManageUserRequestSerializer(serializers.ModelSerializer):
     def get_notes(self, o):
         notes = getattr(o, "_prefetched_notes", [])
         return ManageBaseNoteSerializer(notes, many=True).data
+
+
+class ManageChannelSerializer(serializers.ModelSerializer):
+    attributed_to = ManageBaseActorSerializer()
+    actor = ManageBaseActorSerializer()
+    artist = ManageArtistSerializer()
+
+    class Meta:
+        model = audio_models.Channel
+        fields = [
+            "id",
+            "uuid",
+            "creation_date",
+            "artist",
+            "attributed_to",
+            "actor",
+            "rss_url",
+            "metadata",
+        ]
+        read_only_fields = fields
diff --git a/api/funkwhale_api/manage/urls.py b/api/funkwhale_api/manage/urls.py
index 8af692d7a06c7d9ea50be3d7da50a65f85a66af7..760e24c8dc504d5681eec7510c50f9f6cf071e00 100644
--- a/api/funkwhale_api/manage/urls.py
+++ b/api/funkwhale_api/manage/urls.py
@@ -27,6 +27,7 @@ users_router.register(r"invitations", views.ManageInvitationViewSet, "invitation
 
 other_router = routers.OptionalSlashRouter()
 other_router.register(r"accounts", views.ManageActorViewSet, "accounts")
+other_router.register(r"channels", views.ManageChannelViewSet, "channels")
 other_router.register(r"tags", views.ManageTagViewSet, "tags")
 
 urlpatterns = [
diff --git a/api/funkwhale_api/manage/views.py b/api/funkwhale_api/manage/views.py
index 5a0f81a39cf77694852171d9d0eeadfd12428e3e..0f0f16ce04815f03b071623d78be8aca358c25c5 100644
--- a/api/funkwhale_api/manage/views.py
+++ b/api/funkwhale_api/manage/views.py
@@ -6,12 +6,15 @@ from django.db.models import Count, Prefetch, Q, Sum, OuterRef, Subquery
 from django.db.models.functions import Coalesce, Length
 from django.shortcuts import get_object_or_404
 
+from funkwhale_api.audio import models as audio_models
+from funkwhale_api.common.mixins import MultipleLookupDetailMixin
 from funkwhale_api.common import models as common_models
 from funkwhale_api.common import preferences, decorators
 from funkwhale_api.common import utils as common_utils
 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.federation import utils as federation_utils
 from funkwhale_api.history import models as history_models
 from funkwhale_api.music import models as music_models
 from funkwhale_api.music import views as music_views
@@ -25,37 +28,39 @@ from funkwhale_api.users import models as users_models
 from . import filters, serializers
 
 
-def get_stats(tracks, target):
-    data = {}
+def get_stats(tracks, target, ignore_fields=[]):
     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.filter(library__channel=None)
-        .values_list("library", flat=True)
-        .distinct()
-        .count()
-    )
-    data["channels"] = (
-        uploads.exclude(library__channel=None)
-        .values_list("library", flat=True)
-        .distinct()
-        .count()
-    )
-    data["uploads"] = uploads.count()
-    data["reports"] = moderation_models.Report.objects.get_for_target(target).count()
+    fields = {
+        "listenings": history_models.Listening.objects.filter(track__in=tracks),
+        "mutations": common_models.Mutation.objects.get_for_target(target),
+        "playlists": (
+            playlists_models.PlaylistTrack.objects.filter(track__in=tracks)
+            .values_list("playlist", flat=True)
+            .distinct()
+        ),
+        "track_favorites": (
+            favorites_models.TrackFavorite.objects.filter(track__in=tracks)
+        ),
+        "libraries": (
+            uploads.filter(library__channel=None)
+            .values_list("library", flat=True)
+            .distinct()
+        ),
+        "channels": (
+            uploads.exclude(library__channel=None)
+            .values_list("library", flat=True)
+            .distinct()
+        ),
+        "uploads": uploads,
+        "reports": moderation_models.Report.objects.get_for_target(target),
+    }
+    data = {}
+    for key, qs in fields.items():
+        if key in ignore_fields:
+            continue
+        data[key] = qs.count()
+
     data.update(get_media_stats(uploads))
     return data
 
@@ -78,17 +83,10 @@ class ManageArtistViewSet(
     queryset = (
         music_models.Artist.objects.all()
         .order_by("-id")
-        .select_related("attributed_to", "attachment_cover",)
-        .prefetch_related(
-            "tracks",
-            Prefetch(
-                "albums",
-                queryset=music_models.Album.objects.select_related(
-                    "attachment_cover"
-                ).annotate(tracks_count=Count("tracks")),
-            ),
-            music_views.TAG_PREFETCH,
-        )
+        .select_related("attributed_to", "attachment_cover", "channel")
+        .annotate(_tracks_count=Count("tracks"))
+        .annotate(_albums_count=Count("albums"))
+        .prefetch_related(music_views.TAG_PREFETCH)
     )
     serializer_class = serializers.ManageArtistSerializer
     filterset_class = filters.ManageArtistFilterSet
@@ -661,3 +659,64 @@ class ManageUserRequestViewSet(
             )
         else:
             serializer.save()
+
+
+class ManageChannelViewSet(
+    MultipleLookupDetailMixin,
+    mixins.ListModelMixin,
+    mixins.RetrieveModelMixin,
+    mixins.DestroyModelMixin,
+    viewsets.GenericViewSet,
+):
+
+    url_lookups = [
+        {
+            "lookup_field": "uuid",
+            "validator": serializers.serializers.UUIDField().to_internal_value,
+        },
+        {
+            "lookup_field": "username",
+            "validator": federation_utils.get_actor_data_from_username,
+            "get_query": lambda v: Q(
+                actor__domain=v["domain"],
+                actor__preferred_username__iexact=v["username"],
+            ),
+        },
+    ]
+    queryset = (
+        audio_models.Channel.objects.all()
+        .order_by("-id")
+        .select_related("attributed_to", "actor",)
+        .prefetch_related(
+            Prefetch(
+                "artist",
+                queryset=(
+                    music_models.Artist.objects.all()
+                    .order_by("-id")
+                    .select_related("attributed_to", "attachment_cover", "channel")
+                    .annotate(_tracks_count=Count("tracks"))
+                    .annotate(_albums_count=Count("albums"))
+                    .prefetch_related(music_views.TAG_PREFETCH)
+                ),
+            )
+        )
+    )
+    serializer_class = serializers.ManageChannelSerializer
+    filterset_class = filters.ManageChannelFilterSet
+    required_scope = "instance:libraries"
+    ordering_fields = ["creation_date", "name"]
+
+    @rest_decorators.action(methods=["get"], detail=True)
+    def stats(self, request, *args, **kwargs):
+        channel = self.get_object()
+        tracks = music_models.Track.objects.filter(
+            Q(artist=channel.artist) | Q(album__artist=channel.artist)
+        )
+        data = get_stats(tracks, channel, ignore_fields=["libraries", "channels"])
+        data["follows"] = channel.actor.received_follows.count()
+        return response.Response(data, status=200)
+
+    def get_serializer_context(self):
+        context = super().get_serializer_context()
+        context["description"] = self.action in ["retrieve", "create", "update"]
+        return context
diff --git a/api/funkwhale_api/moderation/serializers.py b/api/funkwhale_api/moderation/serializers.py
index 7d772d39e1d4a6de5318b94c2323a441249f6af3..4b099a1b7590c69c1869fab851cf5b851c4c5408 100644
--- a/api/funkwhale_api/moderation/serializers.py
+++ b/api/funkwhale_api/moderation/serializers.py
@@ -6,6 +6,7 @@ from django.core.serializers.json import DjangoJSONEncoder
 import persisting_theory
 from rest_framework import serializers
 
+from funkwhale_api.audio import models as audio_models
 from funkwhale_api.common import fields as common_fields
 from funkwhale_api.common import preferences
 from funkwhale_api.federation import models as federation_models
@@ -61,20 +62,36 @@ class UserFilterSerializer(serializers.ModelSerializer):
 state_serializers = persisting_theory.Registry()
 
 
+class DescriptionStateMixin(object):
+    def get_description(self, o):
+        if o.description:
+            return o.description.text
+
+
 TAGS_FIELD = serializers.ListField(source="get_tags")
 
 
 @state_serializers.register(name="music.Artist")
-class ArtistStateSerializer(serializers.ModelSerializer):
+class ArtistStateSerializer(DescriptionStateMixin, serializers.ModelSerializer):
     tags = TAGS_FIELD
 
     class Meta:
         model = music_models.Artist
-        fields = ["id", "name", "mbid", "fid", "creation_date", "uuid", "tags"]
+        fields = [
+            "id",
+            "name",
+            "mbid",
+            "fid",
+            "creation_date",
+            "uuid",
+            "tags",
+            "content_category",
+            "description",
+        ]
 
 
 @state_serializers.register(name="music.Album")
-class AlbumStateSerializer(serializers.ModelSerializer):
+class AlbumStateSerializer(DescriptionStateMixin, serializers.ModelSerializer):
     tags = TAGS_FIELD
     artist = ArtistStateSerializer()
 
@@ -90,11 +107,12 @@ class AlbumStateSerializer(serializers.ModelSerializer):
             "artist",
             "release_date",
             "tags",
+            "description",
         ]
 
 
 @state_serializers.register(name="music.Track")
-class TrackStateSerializer(serializers.ModelSerializer):
+class TrackStateSerializer(DescriptionStateMixin, serializers.ModelSerializer):
     tags = TAGS_FIELD
     artist = ArtistStateSerializer()
     album = AlbumStateSerializer()
@@ -115,6 +133,7 @@ class TrackStateSerializer(serializers.ModelSerializer):
             "license",
             "copyright",
             "tags",
+            "description",
         ]
 
 
@@ -156,6 +175,36 @@ class ActorStateSerializer(serializers.ModelSerializer):
         ]
 
 
+@state_serializers.register(name="audio.Channel")
+class ChannelStateSerializer(serializers.ModelSerializer):
+    rss_url = serializers.CharField(source="get_rss_url")
+    name = serializers.CharField(source="artist.name")
+    full_username = serializers.CharField(source="actor.full_username")
+    domain = serializers.CharField(source="actor.domain_id")
+    description = serializers.SerializerMethodField()
+    tags = serializers.ListField(source="artist.get_tags")
+    content_category = serializers.CharField(source="artist.content_category")
+
+    class Meta:
+        model = audio_models.Channel
+        fields = [
+            "uuid",
+            "name",
+            "rss_url",
+            "metadata",
+            "full_username",
+            "description",
+            "domain",
+            "creation_date",
+            "tags",
+            "content_category",
+        ]
+
+    def get_description(self, o):
+        if o.artist.description:
+            return o.artist.description.text
+
+
 def get_actor_query(attr, value):
     data = federation_utils.get_actor_data_from_username(value)
     return federation_utils.get_actor_from_username_data_query(None, data)
@@ -163,6 +212,7 @@ def get_actor_query(attr, value):
 
 def get_target_owner(target):
     mapping = {
+        audio_models.Channel: lambda t: t.attributed_to,
         music_models.Artist: lambda t: t.attributed_to,
         music_models.Album: lambda t: t.attributed_to,
         music_models.Track: lambda t: t.attributed_to,
@@ -175,6 +225,11 @@ def get_target_owner(target):
 
 
 TARGET_CONFIG = {
+    "channel": {
+        "queryset": audio_models.Channel.objects.all(),
+        "id_attr": "uuid",
+        "id_field": serializers.UUIDField(),
+    },
     "artist": {"queryset": music_models.Artist.objects.all()},
     "album": {"queryset": music_models.Album.objects.all()},
     "track": {"queryset": music_models.Track.objects.all()},
diff --git a/api/tests/federation/test_models.py b/api/tests/federation/test_models.py
index a0895d44a44d04fe6c9719ea18481cd4867af6e3..b6c8dc0ad83f7d47a23d8cdd0ef9a0eb8fdbf624 100644
--- a/api/tests/federation/test_models.py
+++ b/api/tests/federation/test_models.py
@@ -126,6 +126,7 @@ def test_domain_stats(factories):
         "libraries": 0,
         "tracks": 0,
         "albums": 0,
+        "channels": 0,
         "uploads": 0,
         "artists": 0,
         "outbox_activities": 0,
@@ -148,6 +149,7 @@ def test_actor_stats(factories):
         "uploads": 0,
         "artists": 0,
         "reports": 0,
+        "channels": 0,
         "requests": 0,
         "outbox_activities": 0,
         "received_library_follows": 0,
diff --git a/api/tests/federation/test_routes.py b/api/tests/federation/test_routes.py
index 0ae18bd15b9beab176cefd4ca96e70cfbe51d417..63d8905ca3f239abc9ab993f0779c3dbf2b83610 100644
--- a/api/tests/federation/test_routes.py
+++ b/api/tests/federation/test_routes.py
@@ -844,6 +844,7 @@ def test_inbox_delete_actor_doesnt_delete_local_actor(factories):
 @pytest.mark.parametrize(
     "factory_name, factory_kwargs",
     [
+        ("audio.Channel", {"local": True}),
         ("federation.Actor", {"local": True}),
         ("music.Artist", {"local": True}),
         ("music.Album", {"local": True}),
@@ -885,6 +886,7 @@ def test_inbox_flag(factory_name, factory_kwargs, factories, mocker):
 @pytest.mark.parametrize(
     "factory_name, factory_kwargs",
     [
+        ("audio.Channel", {"local": True}),
         ("federation.Actor", {"local": True}),
         ("music.Artist", {"local": True}),
         ("music.Album", {"local": True}),
diff --git a/api/tests/federation/test_utils.py b/api/tests/federation/test_utils.py
index 6ba9ccfaeceb747f3e6a18e14b717652d3c0d283..ea3b43f5129c8a413857e3b199a0928ab0ed41ed 100644
--- a/api/tests/federation/test_utils.py
+++ b/api/tests/federation/test_utils.py
@@ -207,3 +207,9 @@ def test_get_obj_by_fid(factory_name, factories):
     obj = factories[factory_name]()
     factories[factory_name]()
     assert utils.get_object_by_fid(obj.fid) == obj
+
+
+def test_get_channel_by_fid(factories):
+    obj = factories["audio.Channel"]()
+    factories["audio.Channel"]()
+    assert utils.get_object_by_fid(obj.actor.fid) == obj
diff --git a/api/tests/manage/test_serializers.py b/api/tests/manage/test_serializers.py
index d2d00b058f849ee2d7d3bbc9fd4357a6b9ffc837..c4dbaa45ed1772b5b6952668ef0492bd231c86e7 100644
--- a/api/tests/manage/test_serializers.py
+++ b/api/tests/manage/test_serializers.py
@@ -287,8 +287,11 @@ def test_instance_policy_serializer_purges_target_actor(
 
 def test_manage_artist_serializer(factories, now, to_api_date):
     artist = factories["music.Artist"](attributed=True, with_cover=True)
-    track = factories["music.Track"](artist=artist)
-    album = factories["music.Album"](artist=artist)
+    channel = factories["audio.Channel"](artist=artist)
+    # put channel in cache
+    artist.get_channel()
+    setattr(artist, "_tracks_count", 12)
+    setattr(artist, "_albums_count", 13)
     expected = {
         "id": artist.id,
         "domain": artist.domain_name,
@@ -297,12 +300,14 @@ def test_manage_artist_serializer(factories, now, to_api_date):
         "name": artist.name,
         "mbid": artist.mbid,
         "creation_date": to_api_date(artist.creation_date),
-        "albums": [serializers.ManageNestedAlbumSerializer(album).data],
-        "tracks": [serializers.ManageNestedTrackSerializer(track).data],
+        "tracks_count": 12,
+        "albums_count": 13,
         "attributed_to": serializers.ManageBaseActorSerializer(
             artist.attributed_to
         ).data,
         "tags": [],
+        "channel": str(channel.uuid),
+        "content_category": artist.content_category,
         "cover": common_serializers.AttachmentSerializer(artist.attachment_cover).data,
     }
     s = serializers.ManageArtistSerializer(artist)
@@ -585,3 +590,22 @@ def test_manage_user_request_serializer(factories, to_api_date):
     s = serializers.ManageUserRequestSerializer(user_request)
 
     assert s.data == expected
+
+
+def test_manage_channel_serializer(factories, now, to_api_date):
+    channel = factories["audio.Channel"]()
+    expected = {
+        "id": channel.id,
+        "uuid": channel.uuid,
+        "artist": serializers.ManageArtistSerializer(channel.artist).data,
+        "actor": serializers.ManageBaseActorSerializer(channel.actor).data,
+        "attributed_to": serializers.ManageBaseActorSerializer(
+            channel.attributed_to
+        ).data,
+        "creation_date": to_api_date(channel.creation_date),
+        "rss_url": channel.get_rss_url(),
+        "metadata": channel.metadata,
+    }
+    s = serializers.ManageChannelSerializer(channel)
+
+    assert s.data == expected
diff --git a/api/tests/manage/test_views.py b/api/tests/manage/test_views.py
index 2482e0d80b7a91ac808017f87acada96da904630..bf4f62bb9e6c1ac8468f7fc4edaea72920b32a3c 100644
--- a/api/tests/manage/test_views.py
+++ b/api/tests/manage/test_views.py
@@ -599,3 +599,50 @@ def test_user_request_update_status_assigns(factories, superuser_api_client, moc
         new_status="refused",
         old_status="pending",
     )
+
+
+def test_channel_list(factories, superuser_api_client, settings):
+    channel = factories["audio.Channel"]()
+    url = reverse("api:v1:manage:channels-list")
+    response = superuser_api_client.get(url)
+
+    assert response.status_code == 200
+
+    assert response.data["count"] == 1
+    assert response.data["results"][0]["id"] == channel.id
+
+
+def test_channel_detail(factories, superuser_api_client):
+    channel = factories["audio.Channel"]()
+    url = reverse("api:v1:manage:channels-detail", kwargs={"composite": channel.uuid})
+    response = superuser_api_client.get(url)
+
+    assert response.status_code == 200
+    assert response.data["id"] == channel.id
+
+
+def test_channel_delete(factories, superuser_api_client, mocker):
+    channel = factories["audio.Channel"]()
+    url = reverse("api:v1:manage:channels-detail", kwargs={"composite": channel.uuid})
+    response = superuser_api_client.delete(url)
+
+    assert response.status_code == 204
+
+
+def test_channel_detail_stats(factories, superuser_api_client):
+    channel = factories["audio.Channel"]()
+    url = reverse("api:v1:manage:channels-stats", kwargs={"composite": channel.uuid})
+    response = superuser_api_client.get(url)
+    expected = {
+        "uploads": 0,
+        "playlists": 0,
+        "listenings": 0,
+        "mutations": 0,
+        "reports": 0,
+        "follows": 0,
+        "track_favorites": 0,
+        "media_total_size": 0,
+        "media_downloaded_size": 0,
+    }
+    assert response.status_code == 200
+    assert response.data == expected
diff --git a/api/tests/moderation/test_serializers.py b/api/tests/moderation/test_serializers.py
index 01cb323eee220050e5afdcd3868972d94d14dea2..9089dc371547a60cfd3b46e3ac04fdd8939c9cb6 100644
--- a/api/tests/moderation/test_serializers.py
+++ b/api/tests/moderation/test_serializers.py
@@ -52,6 +52,7 @@ def test_user_filter_serializer_save(factories):
             "full_username",
             serializers.ActorStateSerializer,
         ),
+        ("audio.Channel", "channel", "uuid", serializers.ChannelStateSerializer),
     ],
 )
 def test_report_federated_entity_serializer_save(
@@ -161,6 +162,7 @@ def test_report_serializer_save_anonymous(factories, mocker):
         ("music.Library", {}, "actor"),
         ("playlists.Playlist", {"user__with_actor": True}, "user.actor"),
         ("federation.Actor", {}, "self"),
+        ("audio.Channel", {}, "attributed_to"),
     ],
 )
 def test_get_target_owner(factory_name, factory_kwargs, owner_field, factories):
diff --git a/front/src/App.vue b/front/src/App.vue
index 6e8f34dbb9c5aa1707a63b15ac506274e6d5d35c..f85d8e920ae4316c57019cf95d3cda0c537165ae 100644
--- a/front/src/App.vue
+++ b/front/src/App.vue
@@ -406,9 +406,9 @@ export default {
     },
     'serviceWorker.updateAvailable': {
       handler (v) {
-        // if (!v) {
-        //   return
-        // }
+        if (!v) {
+          return
+        }
         let self = this
         this.$store.commit('ui/addMessage', {
           content: this.$pgettext("App/Message/Paragraph", "A new version of the app is available."),
diff --git a/front/src/components/audio/ChannelCard.vue b/front/src/components/audio/ChannelCard.vue
index 37db4805e7e7d9fc28d19a5fce934239ba6f927d..c7490023e5be731c202e11ed1fb4b3e83cffa60c 100644
--- a/front/src/components/audio/ChannelCard.vue
+++ b/front/src/components/audio/ChannelCard.vue
@@ -30,7 +30,11 @@
         :title="updatedTitle">
         {{ object.artist.modification_date | fromNow }}
       </time>
-      <play-button class="right floated basic icon" :dropdown-only="true" :is-playable="true" :dropdown-icon-classes="['ellipsis', 'horizontal', 'large', 'grey']" :artist="object.artist"></play-button>
+      <play-button
+        class="right floated basic icon"
+        :dropdown-only="true"
+        :is-playable="true"
+        :dropdown-icon-classes="['ellipsis', 'horizontal', 'large', 'grey']" :artist="object.artist" :channel="object" :account="object.attributed_to"></play-button>
     </div>
   </div>
 </template>
diff --git a/front/src/components/audio/PlayButton.vue b/front/src/components/audio/PlayButton.vue
index 62f72bc95dd864ad9b222d9d02450669428e6d81..906dafee339f1ad4ecff4ba7b777b0d4e34fca84 100644
--- a/front/src/components/audio/PlayButton.vue
+++ b/front/src/components/audio/PlayButton.vue
@@ -35,7 +35,7 @@
           <i class="eye slash outline icon"></i><translate translate-context="*/Queue/Dropdown/Button/Label/Short">Hide content from this artist</translate>
         </button>
         <button
-          v-for="obj in getReportableObjs({track, album, artist, playlist, account})"
+          v-for="obj in getReportableObjs({track, album, artist, playlist, account, channel})"
           :key="obj.target.type + obj.target.id"
           class="item basic"
           @click.stop.prevent="$store.dispatch('moderation/report', obj.target)">
@@ -69,6 +69,7 @@ export default {
     artist: {type: Object, required: false},
     album: {type: Object, required: false},
     library: {type: Object, required: false},
+    channel: {type: Object, required: false},
     isPlayable: {type: Boolean, required: false, default: null}
   },
   data () {
diff --git a/front/src/components/manage/ChannelsTable.vue b/front/src/components/manage/ChannelsTable.vue
new file mode 100644
index 0000000000000000000000000000000000000000..c8f87bd7165be7813d67a047fb9d03b9f57ac066
--- /dev/null
+++ b/front/src/components/manage/ChannelsTable.vue
@@ -0,0 +1,224 @@
+<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="*/*/*">Category</translate></label>
+          <select class="ui dropdown" @change="addSearchToken('category', $event.target.value)" :value="getTokenValue('category', '')">
+            <option value=""><translate translate-context="Content/*/Dropdown">All</translate></option>
+            <option value="podcast">{{ sharedLabels.fields.content_category.choices.podcast }}</option>
+            <option value="music">{{ sharedLabels.fields.content_category.choices.music }}</option>
+            <option value="other">{{ sharedLabels.fields.content_category.choices.other }}</option>
+          </select>
+        </div>
+        <div class="field">
+          <label><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering</translate></label>
+          <select class="ui dropdown" v-model="ordering">
+            <option v-for="option in orderingOptions" :value="option[0]">
+              {{ sharedLabels.filters[option[1]] }}
+            </option>
+          </select>
+        </div>
+        <div class="field">
+          <label><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering direction</translate></label>
+          <select class="ui dropdown" v-model="orderingDirection">
+            <option value="+"><translate translate-context="Content/Search/Dropdown">Ascending</translate></option>
+            <option value="-"><translate translate-context="Content/Search/Dropdown">Descending</translate></option>
+          </select>
+        </div>
+      </div>
+      </div>
+    <div class="dimmable">
+      <div v-if="isLoading" class="ui active inverted dimmer">
+          <div class="ui loader"></div>
+      </div>
+      <action-table
+        v-if="result"
+        @action-launched="fetchData"
+        :objects-data="result"
+        :actions="actions"
+        action-url="manage/library/artists/action/"
+        :filters="actionFilters">
+        <template slot="header-cells">
+          <th><translate translate-context="*/*/*/Noun">Name</translate></th>
+          <th><translate translate-context="*/*/*/Noun">Account</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.channels.detail', params: {id: scope.obj.actor.full_username }}">{{ scope.obj.artist.name }}</router-link>
+          </td>
+          <td>
+            <router-link :to="{name: 'manage.moderation.accounts.detail', params: {id: scope.obj.attributed_to.full_username }}">
+              <i class="wrench icon"></i>
+            </router-link>
+            <span role="button" class="discrete link" @click="addSearchToken('account', scope.obj.attributed_to.full_username)" :title="scope.obj.attributed_to.full_username">{{ scope.obj.attributed_to.preferred_username }}</span>
+          </td>
+          <td>
+            <template v-if="!scope.obj.is_local">
+              <router-link :to="{name: 'manage.moderation.domains.detail', params: {id: scope.obj.attributed_to.domain }}">
+                <i class="wrench icon"></i>
+              </router-link>
+              <span role="button" class="discrete link" @click="addSearchToken('domain', scope.obj.attributed_to.domain)" :title="scope.obj.attributed_to.domain">{{ scope.obj.attributed_to.domain }}</span>
+            </template>
+            <span role="button" v-else class="ui tiny teal icon link label" @click="addSearchToken('domain', scope.obj.attributed_to.domain)">
+              <i class="home icon"></i>
+              <translate translate-context="Content/Moderation/*/Short, Noun">Local</translate>
+            </span>
+          </td>
+          <td>
+            {{ scope.obj.artist.albums_count }}
+          </td>
+          <td>
+            {{ scope.obj.artist.tracks_count }}
+          </td>
+          <td>
+            <human-date :date="scope.obj.creation_date"></human-date>
+          </td>
+        </template>
+      </action-table>
+    </div>
+    <div>
+      <pagination
+        v-if="result && result.count > paginateBy"
+        @page-changed="selectPage"
+        :compact="true"
+        :current="page"
+        :paginate-by="paginateBy"
+        :total="result.count"
+        ></pagination>
+
+      <span v-if="result && result.results.length > 0">
+        <translate translate-context="Content/*/Paragraph"
+          :translate-params="{start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}">
+          Showing results %{ start }-%{ end } on %{ total }
+        </translate>
+      </span>
+    </div>
+  </div>
+</template>
+
+<script>
+import axios from 'axios'
+import _ from '@/lodash'
+import time from '@/utils/time'
+import {normalizeQuery, parseTokens} from '@/search'
+import Pagination from '@/components/Pagination'
+import ActionTable from '@/components/common/ActionTable'
+import OrderingMixin from '@/components/mixins/Ordering'
+import TranslationsMixin from '@/components/mixins/Translations'
+import SmartSearchMixin from '@/components/mixins/SmartSearch'
+
+
+export default {
+  mixins: [OrderingMixin, TranslationsMixin, SmartSearchMixin],
+  props: {
+    filters: {type: Object, required: false},
+  },
+  components: {
+    Pagination,
+    ActionTable
+  },
+  data () {
+    let defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date')
+    return {
+      time,
+      isLoading: false,
+      result: null,
+      page: 1,
+      paginateBy: 50,
+      search: {
+        query: this.defaultQuery,
+        tokens: parseTokens(normalizeQuery(this.defaultQuery))
+      },
+      orderingDirection: defaultOrdering.direction || '+',
+      ordering: defaultOrdering.field,
+      orderingOptions: [
+        ['creation_date', 'creation_date'],
+        ["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/channels/', {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, account…')
+      }
+    },
+    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/ArtistsTable.vue b/front/src/components/manage/library/ArtistsTable.vue
index 84c873832d5f3429565b4fd55caec6e03c1501bf..1bc69e02a128f4889edd061ac4a2ddefd3748794 100644
--- a/front/src/components/manage/library/ArtistsTable.vue
+++ b/front/src/components/manage/library/ArtistsTable.vue
@@ -8,6 +8,15 @@
             <input name="search" ref="search" type="text" :value="search.query" :placeholder="labels.searchPlaceholder" />
           </form>
         </div>
+        <div class="field">
+          <label><translate translate-context="*/*/*">Category</translate></label>
+          <select class="ui dropdown" @change="addSearchToken('category', $event.target.value)" :value="getTokenValue('category', '')">
+            <option value=""><translate translate-context="Content/*/Dropdown">All</translate></option>
+            <option value="podcast">{{ sharedLabels.fields.content_category.choices.podcast }}</option>
+            <option value="music">{{ sharedLabels.fields.content_category.choices.music }}</option>
+            <option value="other">{{ sharedLabels.fields.content_category.choices.other }}</option>
+          </select>
+        </div>
         <div class="field">
           <label><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering</translate></label>
           <select class="ui dropdown" v-model="ordering">
@@ -45,7 +54,9 @@
         </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>
+            <router-link :to="getUrl(scope.obj)">
+              {{ scope.obj.name }}
+            </router-link>
           </td>
           <td>
             <template v-if="!scope.obj.is_local">
@@ -60,10 +71,10 @@
             </span>
           </td>
           <td>
-            {{ scope.obj.albums.length }}
+            {{ scope.obj.albums_count }}
           </td>
           <td>
-            {{ scope.obj.tracks.length }}
+            {{ scope.obj.tracks_count }}
           </td>
           <td>
             <human-date :date="scope.obj.creation_date"></human-date>
@@ -136,6 +147,12 @@ export default {
     this.fetchData()
   },
   methods: {
+    getUrl (artist) {
+      if (artist.channel) {
+        return {name: 'manage.channels.detail', params: {id: artist.channel }}
+      }
+      return {name: 'manage.library.artists.detail', params: {id: artist.id }}
+    },
     fetchData () {
       let params = _.merge({
         'page': this.page,
diff --git a/front/src/components/mixins/Report.vue b/front/src/components/mixins/Report.vue
index 058c4b5cbf651022fa8d288c5133ba6268e07af2..403b89f243c2696d866f0da1f6404f2ab17dc9f0 100644
--- a/front/src/components/mixins/Report.vue
+++ b/front/src/components/mixins/Report.vue
@@ -49,7 +49,20 @@ export default {
           artist = album.artist
         }
       }
-      if (artist) {
+
+      if (channel) {
+        reportableObjs.push({
+          label: this.$pgettext('*/Moderation/*/Verb', "Report this channel…"),
+          target: {
+            type: 'channel',
+            uuid: channel.uuid,
+            label: channel.artist.name,
+            _obj: channel,
+            typeLabel: this.$pgettext("*/*/*", 'Channel'),
+          }
+        })
+      }
+      else if (artist) {
         reportableObjs.push({
           label: this.$pgettext('*/Moderation/*/Verb', "Report this artist…"),
           target: {
diff --git a/front/src/components/mixins/Translations.vue b/front/src/components/mixins/Translations.vue
index 47eae2b66b33956728631094a57ff44fe0eaca80..1648a830a3906d6e4943faa58f52b1a4b7de1637 100644
--- a/front/src/components/mixins/Translations.vue
+++ b/front/src/components/mixins/Translations.vue
@@ -56,6 +56,14 @@ export default {
           summary: {
             label: this.$pgettext('Content/Account/*', 'Bio'),
           },
+          content_category: {
+            label: this.$pgettext('Content/*/Dropdown.Label/Noun', 'Content category'),
+            choices: {
+              podcast: this.$pgettext('Content/*/Dropdown', 'Podcast'),
+              music: this.$pgettext('*/*/*', 'Music'),
+              other: this.$pgettext('*/*/*', 'Other'),
+            },
+          }
         },
         filters: {
           creation_date: this.$pgettext('Content/*/*/Noun', 'Creation date'),
diff --git a/front/src/components/moderation/ReportModal.vue b/front/src/components/moderation/ReportModal.vue
index f53f6e99820a89a043556d0fd712ec62fba63299..103cb203dadd6d99020cd65758680692c98b5e6c 100644
--- a/front/src/components/moderation/ReportModal.vue
+++ b/front/src/components/moderation/ReportModal.vue
@@ -140,6 +140,9 @@ export default {
         return
       }
       let fid = this.target._obj.fid
+      if (this.target.type === 'channel' && this.target._obj.actor ) {
+        fid = this.target._obj.actor.fid
+      }
       if (!fid) {
         return this.$store.getters['instance/domain']
       }
diff --git a/front/src/entities.js b/front/src/entities.js
index 83ed3dba2082e3aed998f20008323c55897a4512..c13d7c6e2bc86d6f2f7953f8bb3953153af936e9 100644
--- a/front/src/entities.js
+++ b/front/src/entities.js
@@ -179,6 +179,7 @@ export default {
         label: this.$pgettext('*/*/*/Noun', 'Account'),
         icon: 'user',
         urls: {
+          getDetail: (obj) => { return {name: 'profile.full.overview', params: {username: obj.preferred_username, domain: obj.domain}}},
           getAdminDetail: (obj) => { return {name: 'manage.moderation.accounts.detail', params: {id: `${obj.preferred_username}@${obj.domain}`}}}
         },
         moderatedFields: [
@@ -194,6 +195,33 @@ export default {
           },
         ]
       },
+      channel: {
+        label: this.$pgettext('*/*/*', 'Channel'),
+        icon: 'stream',
+        urls: {
+          getDetail: (obj) => { return {name: 'channels.detail', params: {id: obj.uuid}}},
+          getAdminDetail: (obj) => { return {name: 'manage.channels.detail', params: {id: obj.uuid}}}
+        },
+        moderatedFields: [
+          {
+            id: 'name',
+            label: this.$pgettext('*/*/*/Noun', 'Name'),
+            getValue: (obj) => { return obj.name }
+          },
+          {
+            id: 'creation_date',
+            label: this.$pgettext('Content/*/*/Noun', 'Creation date'),
+            getValue: (obj) => { return obj.creation_date }
+          },
+          {
+            id: 'tags',
+            type: 'tags',
+            label: this.$pgettext('*/*/*/Noun', 'Tags'),
+            getValue: (obj) => { return obj.tags },
+            getValueRepr: getTagsValueRepr
+          },
+        ]
+      },
     }
   },
 
diff --git a/front/src/router/index.js b/front/src/router/index.js
index cc5e166dbc39bb358789dc66b10bc8523a408cb2..4581dd0d7ee7f35f52613fad6940076336554750 100644
--- a/front/src/router/index.js
+++ b/front/src/router/index.js
@@ -298,6 +298,28 @@ export default new Router({
         },
         {
           path: "channels",
+          name: "manage.channels",
+          component: () =>
+            import(
+              /* webpackChunkName: "admin" */ "@/views/admin/ChannelsList"
+            ),
+          props: route => {
+            return {
+              defaultQuery: route.query.q
+            }
+          }
+        },
+        {
+          path: "channels/:id",
+          name: "manage.channels.detail",
+          component: () =>
+            import(
+              /* webpackChunkName: "admin" */ "@/views/admin/ChannelDetail"
+            ),
+          props: true
+        },
+        {
+          path: "albums",
           name: "manage.library.albums",
           component: () =>
             import(
diff --git a/front/src/views/admin/ChannelDetail.vue b/front/src/views/admin/ChannelDetail.vue
new file mode 100644
index 0000000000000000000000000000000000000000..d5de3a7cb5c7cab970aa902e455ca753e8ae4e26
--- /dev/null
+++ b/front/src/views/admin/ChannelDetail.vue
@@ -0,0 +1,369 @@
+<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.artist.name">
+        <div class="ui stackable one column grid">
+          <div class="ui column">
+            <div class="segment-content">
+              <h2 class="ui header">
+                <img v-if="object.artist.cover && object.artist.cover.square_crop" v-lazy="$store.getters['instance/absoluteUrl'](object.artist.cover.square_crop)">
+                <img v-else src="../../assets/audio/default-cover.png">
+                <div class="content">
+                  {{ object.artist.name | truncate(100) }}
+                  <div class="sub header">
+                    <template v-if="object.artist.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>
+              <template v-if="object.artist.tags && object.artist.tags.length > 0">
+                <tags-list :limit="5" detail-route="manage.library.tags.detail" :tags="object.artist.tags"></tags-list>
+                <div class="ui hidden divider"></div>
+              </template>
+
+              <div class="header-buttons">
+
+                <div class="ui icon buttons">
+                  <router-link class="ui labeled icon button" :to="{name: 'channels.detail', params: {id: object.uuid }}">
+                    <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 && $store.state.auth.profile.is_superuser"
+                        class="basic item"
+                        :href="$store.getters['instance/absoluteUrl'](`/api/admin/audio/channel/${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>
+                      <fetch-button @refresh="fetchData" v-if="!object.actor.is_local" class="basic item" :url="`channels/${object.uuid}/fetches/`">
+                        <i class="refresh icon"></i>&nbsp;
+                        <translate translate-context="Content/Moderation/Button/Verb">Refresh from remote server</translate>&nbsp;
+                      </fetch-button>
+                      <a class="basic item" :href="object.actor.url || object.actor.fid" target="_blank" rel="noopener noreferrer">
+                        <i class="external icon"></i>
+                        <translate translate-context="Content/Moderation/Link/Verb">Open remote profile</translate>&nbsp;
+                      </a>
+                    </div>
+                  </div>
+                </div>
+                <div class="ui buttons">
+                  <dangerous-button
+                    :class="['ui', {loading: isLoading}, 'basic red button']"
+                    :action="remove">
+                    <translate translate-context="*/*/*/Verb">Delete</translate>
+                    <p slot="modal-header"><translate translate-context="Popup/Library/Title">Delete this channel?</translate></p>
+                    <div slot="modal-content">
+                      <p><translate translate-context="Content/Moderation/Paragraph">The channel will be removed, as well as associated uploads, tracks, and albums. 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">Channel data</translate>
+                </div>
+              </h3>
+              <table class="ui very basic table">
+                <tbody>
+                  <tr>
+                    <td>
+                      <translate translate-context="*/*/*/Noun">Name</translate>
+                    </td>
+                    <td>
+                      {{ object.artist.name }}
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <router-link :to="{name: 'manage.channels', query: {q: getQuery('category', object.artist.content_category) }}">
+                        <translate translate-context="*/*/*">Category</translate>
+                      </router-link>
+                    </td>
+                    <td>
+                      {{ object.artist.content_category }}
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <router-link :to="{name: 'manage.moderation.accounts.detail', params: {id: object.attributed_to.full_username }}">
+                        <translate translate-context="*/*/*/Noun">Account</translate>
+                      </router-link>
+                    </td>
+                    <td>
+                      {{ object.attributed_to.preferred_username }}
+                    </td>
+                  </tr>
+                  <tr v-if="!object.actor.is_local">
+                    <td>
+                      <router-link :to="{name: 'manage.moderation.domains.detail', params: {id: object.actor.domain }}">
+                        <translate translate-context="Content/Moderation/*/Noun">Domain</translate>
+                      </router-link>
+                    </td>
+                    <td>
+                      {{ object.actor.domain }}
+                    </td>
+                  </tr>
+                  <tr v-if="object.artist.description">
+                    <td>
+                      <translate translate-context="'*/*/*/Noun">Description</translate>
+                    </td>
+                    <td v-html="object.artist.description.html"></td>
+                  </tr>
+                  <tr v-if="object.actor.url">
+                    <td>
+                      <translate translate-context="'Content/*/*/Noun">URL</translate>
+                    </td>
+                    <td>
+                      <a :href="object.actor.url" rel="noreferrer noopener" target="_blank">{{ object.actor.url }}</a>
+                    </td>
+                  </tr>
+                  <tr v-if="object.rss_url">
+                    <td>
+                      <translate translate-context="'*/*/*">RSS Feed</translate>
+                    </td>
+                    <td>
+                      <a :href="object.rss_url" rel="noreferrer noopener" target="_blank">{{ object.rss_url }}</a>
+                    </td>
+                  </tr>
+                </tbody>
+              </table>
+            </section>
+          </div>
+          <div class="column">
+            <section>
+              <h3 class="ui header">
+                <i class="feed icon"></i>
+                <div class="content">
+                  <translate translate-context="Content/Moderation/Title">Activity</translate>&nbsp;
+                  <span :data-tooltip="labels.statsWarning"><i class="question circle icon"></i></span>
+
+                </div>
+              </h3>
+              <div v-if="isLoadingStats" class="ui placeholder">
+                <div class="full line"></div>
+                <div class="short line"></div>
+                <div class="medium line"></div>
+                <div class="long line"></div>
+              </div>
+              <table v-else class="ui very basic table">
+                <tbody>
+                  <tr>
+                    <td>
+                      <translate translate-context="Content/Moderation/Table.Label/Short (Value is a date)">First seen</translate>
+                    </td>
+                    <td>
+                      <human-date :date="object.creation_date"></human-date>
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <translate translate-context="*/*/*/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.moderation.reports.list', query: {q: getQuery('target', `channel:${object.uuid}`) }}">
+                        <translate translate-context="Content/Moderation/Table.Label/Noun">Linked reports</translate>
+                      </router-link>
+                    </td>
+                    <td>
+                      {{ stats.reports }}
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <router-link :to="{name: 'manage.library.edits', query: {q: getQuery('target', 'artist ' + object.artist.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>
+                      <router-link :to="{name: 'manage.library.uploads', query: {q: getQuery('channel_id', object.uuid) }}">
+                        <translate translate-context="*/*/*">Uploads</translate>
+                      </router-link>
+                    </td>
+                    <td>
+                      {{ stats.uploads }}
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <router-link :to="{name: 'manage.library.albums', query: {q: getQuery('channel_id', object.uuid) }}">
+                        <translate translate-context="*/*/*">Albums</translate>
+                      </router-link>
+                    </td>
+                    <td>
+                      {{ object.artist.albums_count }}
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <router-link :to="{name: 'manage.library.tracks', query: {q: getQuery('channel_id', object.uuid) }}">
+                        <translate translate-context="*/*/*">Tracks</translate>
+                      </router-link>
+                    </td>
+                    <td>
+                      {{ object.artist.tracks_count }}
+                    </td>
+                  </tr>
+                </tbody>
+              </table>
+
+            </section>
+          </div>
+        </div>
+      </div>
+
+    </template>
+  </main>
+</template>
+
+<script>
+import axios from "axios"
+import logger from "@/logging"
+
+import TagsList from "@/components/tags/List"
+import FetchButton from "@/components/federation/FetchButton"
+
+export default {
+  props: ["id"],
+  components: {
+    FetchButton,
+    TagsList
+  },
+  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/channels/${this.id}/`
+      axios.get(url).then(response => {
+        self.object = response.data
+        self.isLoading = false
+      })
+    },
+    fetchStats() {
+      var self = this
+      this.isLoadingStats = true
+      let url = `manage/channels/${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/channels/${this.id}/`
+      axios.delete(url).then(response => {
+        self.$router.push({name: 'manage.channels'})
+      })
+    },
+    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/ChannelsList.vue b/front/src/views/admin/ChannelsList.vue
new file mode 100644
index 0000000000000000000000000000000000000000..74cfb56ccfd912f25d7108faed22d5049b910f8d
--- /dev/null
+++ b/front/src/views/admin/ChannelsList.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>
+      <channels-table :update-url="true" :default-query="defaultQuery"></channels-table>
+    </section>
+  </main>
+</template>
+
+<script>
+import ChannelsTable from "@/components/manage/ChannelsTable"
+
+export default {
+  components: {
+    ChannelsTable
+  },
+  props: {
+    defaultQuery: {type: String, required: false},
+  },
+  computed: {
+    labels() {
+      return {
+        title: this.$pgettext('*/*/*', 'Channels')
+      }
+    }
+  }
+}
+</script>
diff --git a/front/src/views/admin/library/ArtistDetail.vue b/front/src/views/admin/library/ArtistDetail.vue
index 5544445bb19767c8cd35fc907eac1ab3b8e3bceb..f3db10de20055ab87e63e03fef8fd4d772d1dd95 100644
--- a/front/src/views/admin/library/ArtistDetail.vue
+++ b/front/src/views/admin/library/ArtistDetail.vue
@@ -108,6 +108,16 @@
                       {{ object.name }}
                     </td>
                   </tr>
+                  <tr>
+                    <td>
+                      <router-link :to="{name: 'manage.library.artists', query: {q: getQuery('category', object.content_category) }}">
+                        <translate translate-context="*/*/*">Category</translate>
+                      </router-link>
+                    </td>
+                    <td>
+                      {{ object.content_category }}
+                    </td>
+                  </tr>
                   <tr v-if="!object.is_local">
                     <td>
                       <router-link :to="{name: 'manage.moderation.domains.detail', params: {id: object.domain }}">
@@ -265,7 +275,7 @@
                       </router-link>
                     </td>
                     <td>
-                      {{ object.albums.length }}
+                      {{ object.albums_count }}
                     </td>
                   </tr>
                   <tr>
@@ -275,7 +285,7 @@
                       </router-link>
                     </td>
                     <td>
-                      {{ object.tracks.length }}
+                      {{ object.tracks_count }}
                     </td>
                   </tr>
                 </tbody>
@@ -321,8 +331,12 @@ export default {
       this.isLoading = true
       let url = `manage/library/artists/${this.id}/`
       axios.get(url).then(response => {
-        self.object = response.data
-        self.isLoading = false
+        if (response.data.channel) {
+          self.$router.push({name: "manage.channels.detail", params: {id: response.data.channel}})
+        } else {
+          self.object = response.data
+          self.isLoading = false
+        }
       })
     },
     fetchStats() {
diff --git a/front/src/views/admin/library/Base.vue b/front/src/views/admin/library/Base.vue
index 8b99b273b371ad27c187a171ee100f37174f9270..311fe9e77284f9a2b65f6489c5e138b6352f6832 100644
--- a/front/src/views/admin/library/Base.vue
+++ b/front/src/views/admin/library/Base.vue
@@ -4,6 +4,9 @@
       <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.channels'}"><translate translate-context="*/*/*">Channels</translate></router-link>
       <router-link
         class="ui item"
         :to="{name: 'manage.library.artists'}"><translate translate-context="*/*/*/Noun">Artists</translate></router-link>
diff --git a/front/src/views/admin/moderation/AccountsDetail.vue b/front/src/views/admin/moderation/AccountsDetail.vue
index e8f65dca818dcefe32f11fb6565d7c019ffd70e4..5dbacb18ef9a79a9d31e2902d5a8ae916749bf9b 100644
--- a/front/src/views/admin/moderation/AccountsDetail.vue
+++ b/front/src/views/admin/moderation/AccountsDetail.vue
@@ -343,7 +343,16 @@
                       {{ stats.media_total_size | humanSize }}
                     </td>
                   </tr>
-
+                  <tr>
+                    <td>
+                      <router-link :to="{name: 'manage.channels', query: {q: getQuery('account', object.full_username) }}">
+                        <translate translate-context="*/*/*">Channels</translate>
+                      </router-link>
+                    </td>
+                    <td>
+                      {{ stats.channels }}
+                    </td>
+                  </tr>
                   <tr>
                     <td>
                       <router-link :to="{name: 'manage.library.libraries', query: {q: getQuery('account', object.full_username) }}">
diff --git a/front/src/views/admin/moderation/DomainsDetail.vue b/front/src/views/admin/moderation/DomainsDetail.vue
index 65434f2eca1eaa81b35fa78f590f5845cd639fc7..651a4063ee133f5c295c351e4462327ecac8c61b 100644
--- a/front/src/views/admin/moderation/DomainsDetail.vue
+++ b/front/src/views/admin/moderation/DomainsDetail.vue
@@ -266,6 +266,16 @@
                       {{ stats.media_total_size | humanSize }}
                     </td>
                   </tr>
+                  <tr>
+                    <td>
+                      <router-link :to="{name: 'manage.channels', query: {q: getQuery('domain', object.name) }}">
+                        <translate translate-context="*/*/*">Channels</translate>
+                      </router-link>
+                    </td>
+                    <td>
+                      {{ stats.channels }}
+                    </td>
+                  </tr>
                   <tr>
                     <td>
                       <router-link :to="{name: 'manage.library.libraries', query: {q: getQuery('domain', object.name) }}">
diff --git a/front/src/views/channels/DetailBase.vue b/front/src/views/channels/DetailBase.vue
index 0fa345a2dc594e0e768059d239d9f16db1f8bee4..4e5dabc23a1b8db47dfe1ba252882cdae07ce435 100644
--- a/front/src/views/channels/DetailBase.vue
+++ b/front/src/views/channels/DetailBase.vue
@@ -84,7 +84,7 @@
                     <div
                       role="button"
                       class="basic item"
-                      v-for="obj in getReportableObjs({channel: object})"
+                      v-for="obj in getReportableObjs({account: object.attributed_to, channel: object})"
                       :key="obj.target.type + obj.target.id"
                       @click.stop.prevent="$store.dispatch('moderation/report', obj.target)">
                       <i class="share icon" /> {{ obj.label }}
@@ -112,7 +112,7 @@
                     </template>
                     <template v-if="$store.state.auth.availablePermissions['library']" >
                       <div class="divider"></div>
-                      <router-link class="basic item" :to="{name: 'manage.library.channels.detail', params: {id: object.uuid}}">
+                      <router-link class="basic item" :to="{name: 'manage.channels.detail', params: {id: object.uuid}}">
                         <i class="wrench icon"></i>
                         <translate translate-context="Content/Moderation/Link">Open in moderation interface</translate>
                       </router-link>