diff --git a/api/config/api_urls.py b/api/config/api_urls.py
index dc5ef22a3031e733a3044bae6d027199ad11fd0d..b0d7eaaffefbf542eb4ef9b51dfe4d86692b37a7 100644
--- a/api/config/api_urls.py
+++ b/api/config/api_urls.py
@@ -4,6 +4,7 @@ from rest_framework import routers
 from rest_framework.urlpatterns import format_suffix_patterns
 
 from funkwhale_api.activity import views as activity_views
+from funkwhale_api.audio import views as audio_views
 from funkwhale_api.common import views as common_views
 from funkwhale_api.common import routers as common_routers
 from funkwhale_api.music import views
@@ -21,6 +22,7 @@ router.register(r"uploads", views.UploadViewSet, "uploads")
 router.register(r"libraries", views.LibraryViewSet, "libraries")
 router.register(r"listen", views.ListenViewSet, "listen")
 router.register(r"artists", views.ArtistViewSet, "artists")
+router.register(r"channels", audio_views.ChannelViewSet, "channels")
 router.register(r"albums", views.AlbumViewSet, "albums")
 router.register(r"licenses", views.LicenseViewSet, "licenses")
 router.register(r"playlists", playlists_views.PlaylistViewSet, "playlists")
diff --git a/api/config/settings/common.py b/api/config/settings/common.py
index 1ec11e7ff5c2f1b73b6427a19199032e9e097d9d..7c03b89dc049a5b0f26c7884b2b3eb616ff83cd4 100644
--- a/api/config/settings/common.py
+++ b/api/config/settings/common.py
@@ -191,6 +191,7 @@ LOCAL_APPS = (
     "funkwhale_api.users.oauth",
     # Your stuff: custom apps go here
     "funkwhale_api.instance",
+    "funkwhale_api.audio",
     "funkwhale_api.music",
     "funkwhale_api.requests",
     "funkwhale_api.favorites",
diff --git a/api/funkwhale_api/audio/__init__.py b/api/funkwhale_api/audio/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/api/funkwhale_api/audio/admin.py b/api/funkwhale_api/audio/admin.py
new file mode 100644
index 0000000000000000000000000000000000000000..5cde8cc870819130f7a7d5b22212b84154b68842
--- /dev/null
+++ b/api/funkwhale_api/audio/admin.py
@@ -0,0 +1,15 @@
+from funkwhale_api.common import admin
+
+from . import models
+
+
+@admin.register(models.Channel)
+class ChannelAdmin(admin.ModelAdmin):
+    list_display = [
+        "uuid",
+        "artist",
+        "attributed_to",
+        "actor",
+        "library",
+        "creation_date",
+    ]
diff --git a/api/funkwhale_api/audio/dynamic_preferences_registry.py b/api/funkwhale_api/audio/dynamic_preferences_registry.py
new file mode 100644
index 0000000000000000000000000000000000000000..8f9b096b03aa5904ee473ff5ade3360074b0cd4a
--- /dev/null
+++ b/api/funkwhale_api/audio/dynamic_preferences_registry.py
@@ -0,0 +1,16 @@
+from dynamic_preferences import types
+from dynamic_preferences.registries import global_preferences_registry
+
+audio = types.Section("audio")
+
+
+@global_preferences_registry.register
+class ChannelsEnabled(types.BooleanPreference):
+    section = audio
+    name = "channels_enabled"
+    default = True
+    verbose_name = "Enable channels"
+    help_text = (
+        "If disabled, the channels feature will be completely switched off, "
+        "and users won't be able to create channels or subscribe to them."
+    )
diff --git a/api/funkwhale_api/audio/factories.py b/api/funkwhale_api/audio/factories.py
new file mode 100644
index 0000000000000000000000000000000000000000..0c57eeb2e24018b329f76f0da28662b449f1c23f
--- /dev/null
+++ b/api/funkwhale_api/audio/factories.py
@@ -0,0 +1,32 @@
+import factory
+
+from funkwhale_api.factories import registry, NoUpdateOnCreate
+from funkwhale_api.federation import factories as federation_factories
+from funkwhale_api.music import factories as music_factories
+
+from . import models
+
+
+def set_actor(o):
+    return models.generate_actor(str(o.uuid))
+
+
+@registry.register
+class ChannelFactory(NoUpdateOnCreate, factory.django.DjangoModelFactory):
+    uuid = factory.Faker("uuid4")
+    attributed_to = factory.SubFactory(federation_factories.ActorFactory)
+    library = factory.SubFactory(
+        federation_factories.MusicLibraryFactory,
+        actor=factory.SelfAttribute("..attributed_to"),
+    )
+    actor = factory.LazyAttribute(set_actor)
+    artist = factory.SubFactory(music_factories.ArtistFactory)
+
+    class Meta:
+        model = "audio.Channel"
+
+    class Params:
+        local = factory.Trait(
+            attributed_to__fid=factory.Faker("federation_url", local=True),
+            artist__local=True,
+        )
diff --git a/api/funkwhale_api/audio/filters.py b/api/funkwhale_api/audio/filters.py
new file mode 100644
index 0000000000000000000000000000000000000000..02776e0321d0bd9d84f16ed5c5f2f7a3baab3fe7
--- /dev/null
+++ b/api/funkwhale_api/audio/filters.py
@@ -0,0 +1,65 @@
+import django_filters
+
+from funkwhale_api.common import fields
+from funkwhale_api.common import filters as common_filters
+from funkwhale_api.moderation import filters as moderation_filters
+
+from . import models
+
+
+def filter_tags(queryset, name, value):
+    non_empty_tags = [v.lower() for v in value if v]
+    for tag in non_empty_tags:
+        queryset = queryset.filter(artist__tagged_items__tag__name=tag).distinct()
+    return queryset
+
+
+TAG_FILTER = common_filters.MultipleQueryFilter(method=filter_tags)
+
+
+class ChannelFilter(moderation_filters.HiddenContentFilterSet):
+    q = fields.SearchFilter(
+        search_fields=["artist__name", "actor__summary", "actor__preferred_username"]
+    )
+    tag = TAG_FILTER
+    scope = common_filters.ActorScopeFilter(actor_field="attributed_to", distinct=True)
+
+    class Meta:
+        model = models.Channel
+        fields = ["q", "scope", "tag"]
+        hidden_content_fields_mapping = moderation_filters.USER_FILTER_CONFIG["CHANNEL"]
+
+
+class IncludeChannelsFilterSet(django_filters.FilterSet):
+    """
+
+    A filterset that include a "include_channels" param. Meant for compatibility
+    with clients that don't support channels yet:
+
+    - include_channels=false : exclude objects associated with a channel
+    - include_channels=true : don't exclude objects associated with a channel
+    - not specified: include_channels=false
+
+    Usage:
+
+    class MyFilterSet(IncludeChannelsFilterSet):
+        class Meta:
+            include_channels_field = "album__artist__channel"
+
+    """
+
+    include_channels = django_filters.BooleanFilter(
+        field_name="_", method="filter_include_channels"
+    )
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.data = self.data.copy()
+        self.data.setdefault("include_channels", False)
+
+    def filter_include_channels(self, queryset, name, value):
+        if value is True:
+            return queryset
+        else:
+            params = {self.__class__.Meta.include_channels_field: None}
+            return queryset.filter(**params)
diff --git a/api/funkwhale_api/audio/migrations/0001_initial.py b/api/funkwhale_api/audio/migrations/0001_initial.py
new file mode 100644
index 0000000000000000000000000000000000000000..62c271b26d3fabe65e9d9691527d71b249b9b174
--- /dev/null
+++ b/api/funkwhale_api/audio/migrations/0001_initial.py
@@ -0,0 +1,31 @@
+# Generated by Django 2.2.6 on 2019-10-29 12:57
+
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+import uuid
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+        ('federation', '0021_auto_20191029_1257'),
+        ('music', '0041_auto_20191021_1705'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Channel',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('uuid', models.UUIDField(default=uuid.uuid4, unique=True)),
+                ('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
+                ('actor', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='channel', to='federation.Actor')),
+                ('artist', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='channel', to='music.Artist')),
+                ('attributed_to', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='owned_channels', to='federation.Actor')),
+                ('library', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='channel', to='music.Library')),
+            ],
+        ),
+    ]
diff --git a/api/funkwhale_api/audio/migrations/__init__.py b/api/funkwhale_api/audio/migrations/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/api/funkwhale_api/audio/models.py b/api/funkwhale_api/audio/models.py
new file mode 100644
index 0000000000000000000000000000000000000000..f3f9db8960489d455dcc5a1396da881fe13c2c7b
--- /dev/null
+++ b/api/funkwhale_api/audio/models.py
@@ -0,0 +1,39 @@
+import uuid
+
+
+from django.db import models
+from django.utils import timezone
+
+from funkwhale_api.federation import keys
+from funkwhale_api.federation import models as federation_models
+from funkwhale_api.users import models as user_models
+
+
+class Channel(models.Model):
+    uuid = models.UUIDField(default=uuid.uuid4, unique=True)
+    artist = models.OneToOneField(
+        "music.Artist", on_delete=models.CASCADE, related_name="channel"
+    )
+    # the owner of the channel
+    attributed_to = models.ForeignKey(
+        "federation.Actor", on_delete=models.CASCADE, related_name="owned_channels"
+    )
+    # the federation actor created for the channel
+    # (the one people can follow to receive updates)
+    actor = models.OneToOneField(
+        "federation.Actor", on_delete=models.CASCADE, related_name="channel"
+    )
+
+    library = models.OneToOneField(
+        "music.Library", on_delete=models.CASCADE, related_name="channel"
+    )
+    creation_date = models.DateTimeField(default=timezone.now)
+
+
+def generate_actor(username, **kwargs):
+    actor_data = user_models.get_actor_data(username, **kwargs)
+    private, public = keys.get_key_pair()
+    actor_data["private_key"] = private.decode("utf-8")
+    actor_data["public_key"] = public.decode("utf-8")
+
+    return federation_models.Actor.objects.create(**actor_data)
diff --git a/api/funkwhale_api/audio/serializers.py b/api/funkwhale_api/audio/serializers.py
new file mode 100644
index 0000000000000000000000000000000000000000..e2e469b7e63bacef9aad186da4fbde1a702ab6c8
--- /dev/null
+++ b/api/funkwhale_api/audio/serializers.py
@@ -0,0 +1,88 @@
+from django.db import transaction
+
+from rest_framework import serializers
+
+from funkwhale_api.federation import serializers as federation_serializers
+from funkwhale_api.music import models as music_models
+from funkwhale_api.music import serializers as music_serializers
+from funkwhale_api.tags import models as tags_models
+from funkwhale_api.tags import serializers as tags_serializers
+
+from . import models
+
+
+class ChannelCreateSerializer(serializers.Serializer):
+    name = serializers.CharField(max_length=music_models.MAX_LENGTHS["ARTIST_NAME"])
+    username = serializers.CharField(max_length=music_models.MAX_LENGTHS["ARTIST_NAME"])
+    summary = serializers.CharField(max_length=500, allow_blank=True, allow_null=True)
+    tags = tags_serializers.TagsListField()
+
+    @transaction.atomic
+    def create(self, validated_data):
+        artist = music_models.Artist.objects.create(
+            attributed_to=validated_data["attributed_to"], name=validated_data["name"]
+        )
+        if validated_data.get("tags", []):
+            tags_models.set_tags(artist, *validated_data["tags"])
+
+        channel = models.Channel(
+            artist=artist, attributed_to=validated_data["attributed_to"]
+        )
+
+        channel.actor = models.generate_actor(
+            validated_data["username"],
+            summary=validated_data["summary"],
+            name=validated_data["name"],
+        )
+
+        channel.library = music_models.Library.objects.create(
+            name=channel.actor.preferred_username,
+            privacy_level="public",
+            actor=validated_data["attributed_to"],
+        )
+        channel.save()
+        return channel
+
+    def to_representation(self, obj):
+        return ChannelSerializer(obj).data
+
+
+class ChannelUpdateSerializer(serializers.Serializer):
+    name = serializers.CharField(max_length=music_models.MAX_LENGTHS["ARTIST_NAME"])
+    summary = serializers.CharField(max_length=500, allow_blank=True, allow_null=True)
+    tags = tags_serializers.TagsListField()
+
+    @transaction.atomic
+    def update(self, obj, validated_data):
+        if validated_data.get("tags") is not None:
+            tags_models.set_tags(obj.artist, *validated_data["tags"])
+        actor_update_fields = []
+
+        if "summary" in validated_data:
+            actor_update_fields.append(("summary", validated_data["summary"]))
+        if "name" in validated_data:
+            obj.artist.name = validated_data["name"]
+            obj.artist.save(update_fields=["name"])
+            actor_update_fields.append(("name", validated_data["name"]))
+
+        if actor_update_fields:
+            for field, value in actor_update_fields:
+                setattr(obj.actor, field, value)
+            obj.actor.save(update_fields=[f for f, _ in actor_update_fields])
+        return obj
+
+    def to_representation(self, obj):
+        return ChannelSerializer(obj).data
+
+
+class ChannelSerializer(serializers.ModelSerializer):
+    artist = serializers.SerializerMethodField()
+    actor = federation_serializers.APIActorSerializer()
+    attributed_to = federation_serializers.APIActorSerializer()
+
+    class Meta:
+        model = models.Channel
+        fields = ["uuid", "artist", "attributed_to", "actor", "creation_date"]
+
+    def get_artist(self, obj):
+        return music_serializers.serialize_artist_simple(obj.artist)
diff --git a/api/funkwhale_api/audio/views.py b/api/funkwhale_api/audio/views.py
new file mode 100644
index 0000000000000000000000000000000000000000..856c6b0506976e0c9babc3f4149b7e16ca0a531e
--- /dev/null
+++ b/api/funkwhale_api/audio/views.py
@@ -0,0 +1,54 @@
+from rest_framework import exceptions, mixins, viewsets
+
+from django import http
+
+from funkwhale_api.common import permissions
+from funkwhale_api.common import preferences
+from funkwhale_api.users.oauth import permissions as oauth_permissions
+
+from . import filters, models, serializers
+
+
+class ChannelsMixin(object):
+    def dispatch(self, request, *args, **kwargs):
+        if not preferences.get("audio__channels_enabled"):
+            return http.HttpResponse(status=405)
+        return super().dispatch(request, *args, **kwargs)
+
+
+class ChannelViewSet(
+    ChannelsMixin,
+    mixins.CreateModelMixin,
+    mixins.RetrieveModelMixin,
+    mixins.UpdateModelMixin,
+    mixins.ListModelMixin,
+    mixins.DestroyModelMixin,
+    viewsets.GenericViewSet,
+):
+    lookup_field = "uuid"
+    filterset_class = filters.ChannelFilter
+    serializer_class = serializers.ChannelSerializer
+    queryset = (
+        models.Channel.objects.all()
+        .prefetch_related("library", "attributed_to", "artist", "actor")
+        .order_by("-creation_date")
+    )
+    permission_classes = [
+        oauth_permissions.ScopePermission,
+        permissions.OwnerPermission,
+    ]
+    required_scope = "libraries"
+    anonymous_policy = "setting"
+    owner_checks = ["write"]
+    owner_field = "attributed_to.user"
+    owner_exception = exceptions.PermissionDenied
+
+    def get_serializer_class(self):
+        if self.request.method.lower() in ["head", "get", "options"]:
+            return serializers.ChannelSerializer
+        elif self.action in ["update", "partial_update"]:
+            return serializers.ChannelUpdateSerializer
+        return serializers.ChannelCreateSerializer
+
+    def perform_create(self, serializer):
+        return serializer.save(attributed_to=self.request.user.actor)
diff --git a/api/funkwhale_api/common/permissions.py b/api/funkwhale_api/common/permissions.py
index 237fc4ae4141813e505c98df76d2b0ea165ea7cb..76d8a7ff31586ec48b4a92b59c16b05ecbf9fea3 100644
--- a/api/funkwhale_api/common/permissions.py
+++ b/api/funkwhale_api/common/permissions.py
@@ -1,6 +1,8 @@
 import operator
 
+from django.core.exceptions import ObjectDoesNotExist
 from django.http import Http404
+
 from rest_framework.permissions import BasePermission
 
 from funkwhale_api.common import preferences
@@ -46,7 +48,12 @@ class OwnerPermission(BasePermission):
             return True
 
         owner_field = getattr(view, "owner_field", "user")
-        owner = operator.attrgetter(owner_field)(obj)
+        owner_exception = getattr(view, "owner_exception", Http404)
+        try:
+            owner = operator.attrgetter(owner_field)(obj)
+        except ObjectDoesNotExist:
+            raise owner_exception
+
         if not owner or not request.user.is_authenticated or owner != request.user:
-            raise Http404
+            raise owner_exception
         return True
diff --git a/api/funkwhale_api/federation/migrations/0021_auto_20191029_1257.py b/api/funkwhale_api/federation/migrations/0021_auto_20191029_1257.py
new file mode 100644
index 0000000000000000000000000000000000000000..2aad0df28d520747626fa1124ac9359c4fd1ab54
--- /dev/null
+++ b/api/funkwhale_api/federation/migrations/0021_auto_20191029_1257.py
@@ -0,0 +1,22 @@
+# Generated by Django 2.2.6 on 2019-10-29 12:57
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('federation', '0020_auto_20190730_0846'),
+    ]
+
+    operations = [
+        migrations.AlterModelOptions(
+            name='actor',
+            options={'verbose_name': 'Account'},
+        ),
+        migrations.AlterField(
+            model_name='actor',
+            name='type',
+            field=models.CharField(choices=[('Person', 'Person'), ('Tombstone', 'Tombstone'), ('Application', 'Application'), ('Group', 'Group'), ('Organization', 'Organization'), ('Service', 'Service')], default='Person', max_length=25),
+        ),
+    ]
diff --git a/api/funkwhale_api/moderation/filters.py b/api/funkwhale_api/moderation/filters.py
index ddf183045869bd01eeb71e2f3cfe80341e57e244..629ae685f95f36b75abb9a5e172f2f0de37620c4 100644
--- a/api/funkwhale_api/moderation/filters.py
+++ b/api/funkwhale_api/moderation/filters.py
@@ -5,6 +5,7 @@ from django_filters import rest_framework as filters
 
 USER_FILTER_CONFIG = {
     "ARTIST": {"target_artist": ["pk"]},
+    "CHANNEL": {"target_artist": ["artist__pk"]},
     "ALBUM": {"target_artist": ["artist__pk"]},
     "TRACK": {"target_artist": ["artist__pk", "album__artist__pk"]},
     "LISTENING": {"target_artist": ["track__album__artist__pk", "track__artist__pk"]},
diff --git a/api/funkwhale_api/music/filters.py b/api/funkwhale_api/music/filters.py
index f5bd17e67851827dd3cf9489faf73988908d4ec1..75ce9ec850d6a00dfd64457207835d98d4bb423c 100644
--- a/api/funkwhale_api/music/filters.py
+++ b/api/funkwhale_api/music/filters.py
@@ -1,5 +1,6 @@
 from django_filters import rest_framework as filters
 
+from funkwhale_api.audio import filters as audio_filters
 from funkwhale_api.common import fields
 from funkwhale_api.common import filters as common_filters
 from funkwhale_api.common import search
@@ -19,7 +20,9 @@ def filter_tags(queryset, name, value):
 TAG_FILTER = common_filters.MultipleQueryFilter(method=filter_tags)
 
 
-class ArtistFilter(moderation_filters.HiddenContentFilterSet):
+class ArtistFilter(
+    audio_filters.IncludeChannelsFilterSet, moderation_filters.HiddenContentFilterSet
+):
     q = fields.SearchFilter(search_fields=["name"])
     playable = filters.BooleanFilter(field_name="_", method="filter_playable")
     tag = TAG_FILTER
@@ -36,13 +39,16 @@ class ArtistFilter(moderation_filters.HiddenContentFilterSet):
             "mbid": ["exact"],
         }
         hidden_content_fields_mapping = moderation_filters.USER_FILTER_CONFIG["ARTIST"]
+        include_channels_field = "channel"
 
     def filter_playable(self, queryset, name, value):
         actor = utils.get_actor_from_request(self.request)
         return queryset.playable_by(actor, value)
 
 
-class TrackFilter(moderation_filters.HiddenContentFilterSet):
+class TrackFilter(
+    audio_filters.IncludeChannelsFilterSet, moderation_filters.HiddenContentFilterSet
+):
     q = fields.SearchFilter(search_fields=["title", "album__title", "artist__name"])
     playable = filters.BooleanFilter(field_name="_", method="filter_playable")
     tag = TAG_FILTER
@@ -64,13 +70,14 @@ class TrackFilter(moderation_filters.HiddenContentFilterSet):
             "mbid": ["exact"],
         }
         hidden_content_fields_mapping = moderation_filters.USER_FILTER_CONFIG["TRACK"]
+        include_channels_field = "artist__channel"
 
     def filter_playable(self, queryset, name, value):
         actor = utils.get_actor_from_request(self.request)
         return queryset.playable_by(actor, value)
 
 
-class UploadFilter(filters.FilterSet):
+class UploadFilter(audio_filters.IncludeChannelsFilterSet):
     library = filters.CharFilter("library__uuid")
     track = filters.UUIDFilter("track__uuid")
     track_artist = filters.UUIDFilter("track__artist__uuid")
@@ -109,13 +116,16 @@ class UploadFilter(filters.FilterSet):
             "import_reference",
             "scope",
         ]
+        include_channels_field = "track__artist__channel"
 
     def filter_playable(self, queryset, name, value):
         actor = utils.get_actor_from_request(self.request)
         return queryset.playable_by(actor, value)
 
 
-class AlbumFilter(moderation_filters.HiddenContentFilterSet):
+class AlbumFilter(
+    audio_filters.IncludeChannelsFilterSet, moderation_filters.HiddenContentFilterSet
+):
     playable = filters.BooleanFilter(field_name="_", method="filter_playable")
     q = fields.SearchFilter(search_fields=["title", "artist__name"])
     tag = TAG_FILTER
@@ -127,6 +137,7 @@ class AlbumFilter(moderation_filters.HiddenContentFilterSet):
         model = models.Album
         fields = ["playable", "q", "artist", "scope", "mbid"]
         hidden_content_fields_mapping = moderation_filters.USER_FILTER_CONFIG["ALBUM"]
+        include_channels_field = "artist__channel"
 
     def filter_playable(self, queryset, name, value):
         actor = utils.get_actor_from_request(self.request)
diff --git a/api/funkwhale_api/users/models.py b/api/funkwhale_api/users/models.py
index 2a2f875a60350a66b3a5d1cfe9d8472349cd3f4b..ca50c047fccb24a02723f0337633c4e738fac77e 100644
--- a/api/funkwhale_api/users/models.py
+++ b/api/funkwhale_api/users/models.py
@@ -362,7 +362,8 @@ def get_actor_data(username, **kwargs):
         "preferred_username": slugified_username,
         "domain": domain,
         "type": "Person",
-        "name": username,
+        "name": kwargs.get("name", username),
+        "summary": kwargs.get("summary"),
         "manually_approves_followers": False,
         "fid": federation_utils.full_url(
             reverse(
diff --git a/api/tests/audio/__init__.py b/api/tests/audio/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/api/tests/audio/test_models.py b/api/tests/audio/test_models.py
new file mode 100644
index 0000000000000000000000000000000000000000..5992e41ed78809bc268abb8a215cad8f0226ca5b
--- /dev/null
+++ b/api/tests/audio/test_models.py
@@ -0,0 +1,7 @@
+def test_channel(factories, now):
+    channel = factories["audio.Channel"]()
+    assert channel.artist is not None
+    assert channel.actor is not None
+    assert channel.attributed_to is not None
+    assert channel.library is not None
+    assert channel.creation_date >= now
diff --git a/api/tests/audio/test_serializers.py b/api/tests/audio/test_serializers.py
new file mode 100644
index 0000000000000000000000000000000000000000..02737a852951759062321704248c8bcb644877b3
--- /dev/null
+++ b/api/tests/audio/test_serializers.py
@@ -0,0 +1,74 @@
+from funkwhale_api.audio import serializers
+from funkwhale_api.federation import serializers as federation_serializers
+from funkwhale_api.music import serializers as music_serializers
+
+
+def test_channel_serializer_create(factories):
+    attributed_to = factories["federation.Actor"](local=True)
+
+    data = {
+        # TODO: cover
+        "name": "My channel",
+        "username": "mychannel",
+        "summary": "This is my channel",
+        "tags": ["hello", "world"],
+    }
+
+    serializer = serializers.ChannelCreateSerializer(data=data)
+    assert serializer.is_valid(raise_exception=True) is True
+
+    channel = serializer.save(attributed_to=attributed_to)
+
+    assert channel.artist.name == data["name"]
+    assert channel.artist.attributed_to == attributed_to
+    assert (
+        sorted(channel.artist.tagged_items.values_list("tag__name", flat=True))
+        == data["tags"]
+    )
+    assert channel.attributed_to == attributed_to
+    assert channel.actor.summary == data["summary"]
+    assert channel.actor.preferred_username == data["username"]
+    assert channel.actor.name == data["name"]
+    assert channel.library.privacy_level == "public"
+    assert channel.library.actor == attributed_to
+
+
+def test_channel_serializer_update(factories):
+    channel = factories["audio.Channel"](artist__set_tags=["rock"])
+
+    data = {
+        # TODO: cover
+        "name": "My channel",
+        "summary": "This is my channel",
+        "tags": ["hello", "world"],
+    }
+
+    serializer = serializers.ChannelUpdateSerializer(channel, data=data)
+    assert serializer.is_valid(raise_exception=True) is True
+
+    serializer.save()
+    channel.refresh_from_db()
+
+    assert channel.artist.name == data["name"]
+    assert (
+        sorted(channel.artist.tagged_items.values_list("tag__name", flat=True))
+        == data["tags"]
+    )
+    assert channel.actor.summary == data["summary"]
+    assert channel.actor.name == data["name"]
+
+
+def test_channel_serializer_representation(factories, to_api_date):
+    channel = factories["audio.Channel"]()
+
+    expected = {
+        "artist": music_serializers.serialize_artist_simple(channel.artist),
+        "uuid": str(channel.uuid),
+        "creation_date": to_api_date(channel.creation_date),
+        "actor": federation_serializers.APIActorSerializer(channel.actor).data,
+        "attributed_to": federation_serializers.APIActorSerializer(
+            channel.attributed_to
+        ).data,
+    }
+
+    assert serializers.ChannelSerializer(channel).data == expected
diff --git a/api/tests/audio/test_views.py b/api/tests/audio/test_views.py
new file mode 100644
index 0000000000000000000000000000000000000000..d12caa3dd9af2df8c78c1592a3912d45faa74388
--- /dev/null
+++ b/api/tests/audio/test_views.py
@@ -0,0 +1,128 @@
+import pytest
+
+from django.urls import reverse
+
+from funkwhale_api.audio import serializers
+
+
+def test_channel_create(logged_in_api_client):
+    actor = logged_in_api_client.user.create_actor()
+
+    data = {
+        # TODO: cover
+        "name": "My channel",
+        "username": "mychannel",
+        "summary": "This is my channel",
+        "tags": ["hello", "world"],
+    }
+
+    url = reverse("api:v1:channels-list")
+    response = logged_in_api_client.post(url, data)
+
+    assert response.status_code == 201
+
+    channel = actor.owned_channels.latest("id")
+    expected = serializers.ChannelSerializer(channel).data
+
+    assert response.data == expected
+    assert channel.artist.name == data["name"]
+    assert channel.artist.attributed_to == actor
+    assert (
+        sorted(channel.artist.tagged_items.values_list("tag__name", flat=True))
+        == data["tags"]
+    )
+    assert channel.attributed_to == actor
+    assert channel.actor.summary == data["summary"]
+    assert channel.actor.preferred_username == data["username"]
+    assert channel.library.privacy_level == "public"
+    assert channel.library.actor == actor
+
+
+def test_channel_detail(factories, logged_in_api_client):
+    channel = factories["audio.Channel"]()
+    url = reverse("api:v1:channels-detail", kwargs={"uuid": channel.uuid})
+    expected = serializers.ChannelSerializer(channel).data
+    response = logged_in_api_client.get(url)
+
+    assert response.status_code == 200
+    assert response.data == expected
+
+
+def test_channel_list(factories, logged_in_api_client):
+    channel = factories["audio.Channel"]()
+    url = reverse("api:v1:channels-list")
+    expected = serializers.ChannelSerializer(channel).data
+    response = logged_in_api_client.get(url)
+
+    assert response.status_code == 200
+    assert response.data == {
+        "results": [expected],
+        "count": 1,
+        "next": None,
+        "previous": None,
+    }
+
+
+def test_channel_update(logged_in_api_client, factories):
+    actor = logged_in_api_client.user.create_actor()
+    channel = factories["audio.Channel"](attributed_to=actor)
+
+    data = {
+        # TODO: cover
+        "name": "new name"
+    }
+
+    url = reverse("api:v1:channels-detail", kwargs={"uuid": channel.uuid})
+    response = logged_in_api_client.patch(url, data)
+
+    assert response.status_code == 200
+
+    channel.refresh_from_db()
+
+    assert channel.artist.name == data["name"]
+
+
+def test_channel_update_permission(logged_in_api_client, factories):
+    logged_in_api_client.user.create_actor()
+    channel = factories["audio.Channel"]()
+
+    data = {"name": "new name"}
+
+    url = reverse("api:v1:channels-detail", kwargs={"uuid": channel.uuid})
+    response = logged_in_api_client.patch(url, data)
+
+    assert response.status_code == 403
+
+
+def test_channel_delete(logged_in_api_client, factories):
+    actor = logged_in_api_client.user.create_actor()
+    channel = factories["audio.Channel"](attributed_to=actor)
+
+    url = reverse("api:v1:channels-detail", kwargs={"uuid": channel.uuid})
+    response = logged_in_api_client.delete(url)
+
+    assert response.status_code == 204
+
+    with pytest.raises(channel.DoesNotExist):
+        channel.refresh_from_db()
+
+
+def test_channel_delete_permission(logged_in_api_client, factories):
+    logged_in_api_client.user.create_actor()
+    channel = factories["audio.Channel"]()
+
+    url = reverse("api:v1:channels-detail", kwargs={"uuid": channel.uuid})
+    response = logged_in_api_client.patch(url)
+
+    assert response.status_code == 403
+    channel.refresh_from_db()
+
+
+@pytest.mark.parametrize("url_name", ["api:v1:channels-list"])
+def test_channel_views_disabled_via_feature_flag(
+    url_name, logged_in_api_client, preferences
+):
+    preferences["audio__channels_enabled"] = False
+    url = reverse(url_name)
+    response = logged_in_api_client.get(url)
+    assert response.status_code == 405
diff --git a/api/tests/music/test_filters.py b/api/tests/music/test_filters.py
index f3ff13e777f044f90f3a4e9e851b8f9dd36333cf..7ce7e16a038ec81bc6d7f7fd9c0dc8e5ca0b3eb0 100644
--- a/api/tests/music/test_filters.py
+++ b/api/tests/music/test_filters.py
@@ -60,8 +60,8 @@ def test_artist_filter_track_album_artist(factories, mocker, queryset_equal_list
     "factory_name, filterset_class",
     [
         ("music.Track", filters.TrackFilter),
-        ("music.Artist", filters.TrackFilter),
-        ("music.Album", filters.TrackFilter),
+        ("music.Artist", filters.ArtistFilter),
+        ("music.Album", filters.AlbumFilter),
     ],
 )
 def test_track_filter_tag_single(
diff --git a/api/tests/music/test_views.py b/api/tests/music/test_views.py
index ad3bf0413336adf8fb41399d78ea595563c9fb66..f0cc68f41b0a4871d2fe707a9d3ba097bfc4754f 100644
--- a/api/tests/music/test_views.py
+++ b/api/tests/music/test_views.py
@@ -997,3 +997,49 @@ def test_refetch_obj(mocker, factories, settings, service_actor):
     views.refetch_obj(obj, obj.__class__.objects.all())
     fetch = obj.fetches.filter(actor=service_actor).order_by("-creation_date").first()
     fetch_task.assert_called_once_with(fetch_id=fetch.pk)
+
+
+@pytest.mark.parametrize(
+    "params, expected",
+    [({}, 0), ({"include_channels": "false"}, 0), ({"include_channels": "true"}, 1)],
+)
+def test_artist_list_exclude_channels(
+    params, expected, factories, logged_in_api_client
+):
+    factories["audio.Channel"]()
+
+    url = reverse("api:v1:artists-list")
+    response = logged_in_api_client.get(url, params)
+
+    assert response.status_code == 200
+    assert response.data["count"] == expected
+
+
+@pytest.mark.parametrize(
+    "params, expected",
+    [({}, 0), ({"include_channels": "false"}, 0), ({"include_channels": "true"}, 1)],
+)
+def test_album_list_exclude_channels(params, expected, factories, logged_in_api_client):
+    channel_artist = factories["audio.Channel"]().artist
+    factories["music.Album"](artist=channel_artist)
+
+    url = reverse("api:v1:albums-list")
+    response = logged_in_api_client.get(url, params)
+
+    assert response.status_code == 200
+    assert response.data["count"] == expected
+
+
+@pytest.mark.parametrize(
+    "params, expected",
+    [({}, 0), ({"include_channels": "false"}, 0), ({"include_channels": "true"}, 1)],
+)
+def test_track_list_exclude_channels(params, expected, factories, logged_in_api_client):
+    channel_artist = factories["audio.Channel"]().artist
+    factories["music.Track"](artist=channel_artist)
+
+    url = reverse("api:v1:tracks-list")
+    response = logged_in_api_client.get(url, params)
+
+    assert response.status_code == 200
+    assert response.data["count"] == expected