diff --git a/api/config/api_urls.py b/api/config/api_urls.py
index 3cb7ec36daf2b7b9f564c7c3d33df1dd58e7303a..ab01e623c3e5bd298ee17fe79d5fdecd604a1aee 100644
--- a/api/config/api_urls.py
+++ b/api/config/api_urls.py
@@ -40,6 +40,12 @@ v1_patterns += [
         r"^manage/",
         include(("funkwhale_api.manage.urls", "manage"), namespace="manage"),
     ),
+    url(
+        r"^moderation/",
+        include(
+            ("funkwhale_api.moderation.urls", "moderation"), namespace="moderation"
+        ),
+    ),
     url(
         r"^federation/",
         include(
diff --git a/api/funkwhale_api/common/views.py b/api/funkwhale_api/common/views.py
new file mode 100644
index 0000000000000000000000000000000000000000..fe7d6733aba97fa481adc85b5ce4c19a1beee9ce
--- /dev/null
+++ b/api/funkwhale_api/common/views.py
@@ -0,0 +1,9 @@
+class SkipFilterForGetObject:
+    def get_object(self, *args, **kwargs):
+        setattr(self.request, "_skip_filters", True)
+        return super().get_object(*args, **kwargs)
+
+    def filter_queryset(self, queryset):
+        if getattr(self.request, "_skip_filters", False):
+            return queryset
+        return super().filter_queryset(queryset)
diff --git a/api/funkwhale_api/favorites/filters.py b/api/funkwhale_api/favorites/filters.py
index a355593d91bea643277086fe98d7e81a1653e998..cf8048b8d712d5e8da5dbab80df2093b29fbf794 100644
--- a/api/funkwhale_api/favorites/filters.py
+++ b/api/funkwhale_api/favorites/filters.py
@@ -1,11 +1,10 @@
-from django_filters import rest_framework as filters
-
 from funkwhale_api.common import fields
+from funkwhale_api.moderation import filters as moderation_filters
 
 from . import models
 
 
-class TrackFavoriteFilter(filters.FilterSet):
+class TrackFavoriteFilter(moderation_filters.HiddenContentFilterSet):
     q = fields.SearchFilter(
         search_fields=["track__title", "track__artist__name", "track__album__title"]
     )
@@ -13,3 +12,6 @@ class TrackFavoriteFilter(filters.FilterSet):
     class Meta:
         model = models.TrackFavorite
         fields = ["user", "q"]
+        hidden_content_fields_mapping = moderation_filters.USER_FILTER_CONFIG[
+            "TRACK_FAVORITE"
+        ]
diff --git a/api/funkwhale_api/history/filters.py b/api/funkwhale_api/history/filters.py
new file mode 100644
index 0000000000000000000000000000000000000000..30bc78f6a9d0a10394a1d76bf2e5ea4de0c6e8aa
--- /dev/null
+++ b/api/funkwhale_api/history/filters.py
@@ -0,0 +1,12 @@
+from funkwhale_api.moderation import filters as moderation_filters
+
+from . import models
+
+
+class ListeningFilter(moderation_filters.HiddenContentFilterSet):
+    class Meta:
+        model = models.Listening
+        hidden_content_fields_mapping = moderation_filters.USER_FILTER_CONFIG[
+            "LISTENING"
+        ]
+        fields = ["hidden"]
diff --git a/api/funkwhale_api/history/views.py b/api/funkwhale_api/history/views.py
index 56c30af36511e8cf124645dfcd00f835e3a04287..b03c85a8ef1a3834f934a299998ec7230d8631e1 100644
--- a/api/funkwhale_api/history/views.py
+++ b/api/funkwhale_api/history/views.py
@@ -7,7 +7,7 @@ from funkwhale_api.activity import record
 from funkwhale_api.common import fields, permissions
 from funkwhale_api.music.models import Track
 from funkwhale_api.music import utils as music_utils
-from . import models, serializers
+from . import filters, models, serializers
 
 
 class ListeningViewSet(
@@ -25,6 +25,7 @@ class ListeningViewSet(
         IsAuthenticatedOrReadOnly,
     ]
     owner_checks = ["write"]
+    filterset_class = filters.ListeningFilter
 
     def get_serializer_class(self):
         if self.request.method.lower() in ["head", "get", "options"]:
diff --git a/api/funkwhale_api/moderation/admin.py b/api/funkwhale_api/moderation/admin.py
index 5e421255ed344d61335edbb588f7792d07ac21ec..9f8340030e3aa4108ee7f63a87b0c2f8a50e85d9 100644
--- a/api/funkwhale_api/moderation/admin.py
+++ b/api/funkwhale_api/moderation/admin.py
@@ -28,3 +28,10 @@ class InstancePolicyAdmin(admin.ModelAdmin):
         "summary",
     ]
     list_select_related = True
+
+
+@admin.register(models.UserFilter)
+class UserFilterAdmin(admin.ModelAdmin):
+    list_display = ["uuid", "user", "target_artist", "creation_date"]
+    search_fields = ["target_artist__name", "user__username", "user__email"]
+    list_select_related = True
diff --git a/api/funkwhale_api/moderation/factories.py b/api/funkwhale_api/moderation/factories.py
index aba5256c9cba399cf2e345101eb0d9958f1b13f4..8829caa2bacf60b444b18984c71b24b5f8b788d0 100644
--- a/api/funkwhale_api/moderation/factories.py
+++ b/api/funkwhale_api/moderation/factories.py
@@ -2,6 +2,8 @@ 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 funkwhale_api.users import factories as users_factories
 
 
 @registry.register
@@ -21,3 +23,17 @@ class InstancePolicyFactory(NoUpdateOnCreate, factory.DjangoModelFactory):
         for_actor = factory.Trait(
             target_actor=factory.SubFactory(federation_factories.ActorFactory)
         )
+
+
+@registry.register
+class UserFilterFactory(NoUpdateOnCreate, factory.DjangoModelFactory):
+    user = factory.SubFactory(users_factories.UserFactory)
+    target_artist = None
+
+    class Meta:
+        model = "moderation.UserFilter"
+
+    class Params:
+        for_artist = factory.Trait(
+            target_artist=factory.SubFactory(music_factories.ArtistFactory)
+        )
diff --git a/api/funkwhale_api/moderation/filters.py b/api/funkwhale_api/moderation/filters.py
new file mode 100644
index 0000000000000000000000000000000000000000..ddf183045869bd01eeb71e2f3cfe80341e57e244
--- /dev/null
+++ b/api/funkwhale_api/moderation/filters.py
@@ -0,0 +1,69 @@
+from django.db.models import Q
+
+from django_filters import rest_framework as filters
+
+
+USER_FILTER_CONFIG = {
+    "ARTIST": {"target_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"]},
+    "TRACK_FAVORITE": {
+        "target_artist": ["track__album__artist__pk", "track__artist__pk"]
+    },
+}
+
+
+def get_filtered_content_query(config, user):
+    final_query = None
+    for filter_field, model_fields in config.items():
+        query = None
+        ids = user.content_filters.values_list(filter_field, flat=True)
+        for model_field in model_fields:
+            q = Q(**{"{}__in".format(model_field): ids})
+            if query:
+                query |= q
+            else:
+                query = q
+
+        final_query = query
+    return final_query
+
+
+class HiddenContentFilterSet(filters.FilterSet):
+    """
+    A filterset that include a "hidden" param:
+    - hidden=true : list user hidden/filtered objects
+    - hidden=false : list all objects user hidden/filtered objects
+    - not specified: hidden=false
+
+    Usage:
+
+    class MyFilterSet(HiddenContentFilterSet):
+        class Meta:
+            hidden_content_fields_mapping = {'target_artist': ['pk']}
+
+    Will map UserContentFilter.artist values to the pk field of the filtered model.
+
+    """
+
+    hidden = filters.BooleanFilter(field_name="_", method="filter_hidden_content")
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.data = self.data.copy()
+        self.data.setdefault("hidden", False)
+
+    def filter_hidden_content(self, queryset, name, value):
+        user = self.request.user
+        if not user.is_authenticated:
+            # no filter to apply
+            return queryset
+
+        config = self.__class__.Meta.hidden_content_fields_mapping
+        final_query = get_filtered_content_query(config, user)
+
+        if value is True:
+            return queryset.filter(final_query)
+        else:
+            return queryset.exclude(final_query)
diff --git a/api/funkwhale_api/moderation/migrations/0002_auto_20190213_0927.py b/api/funkwhale_api/moderation/migrations/0002_auto_20190213_0927.py
new file mode 100644
index 0000000000000000000000000000000000000000..2832a34ef669b2d10aec088d6bae4347bb88091b
--- /dev/null
+++ b/api/funkwhale_api/moderation/migrations/0002_auto_20190213_0927.py
@@ -0,0 +1,57 @@
+# Generated by Django 2.1.5 on 2019-02-13 09:27
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+import uuid
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ("music", "0037_auto_20190103_1757"),
+        ("moderation", "0001_initial"),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name="UserFilter",
+            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),
+                ),
+                (
+                    "target_artist",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="user_filters",
+                        to="music.Artist",
+                    ),
+                ),
+                (
+                    "user",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="content_filters",
+                        to=settings.AUTH_USER_MODEL,
+                    ),
+                ),
+            ],
+        ),
+        migrations.AlterUniqueTogether(
+            name="userfilter", unique_together={("user", "target_artist")}
+        ),
+    ]
diff --git a/api/funkwhale_api/moderation/models.py b/api/funkwhale_api/moderation/models.py
index c184bbda8117929da26940a1197b36c6cb75f6f4..7ade5d05a1e5a1ff0bfb0e259b5770b100a77489 100644
--- a/api/funkwhale_api/moderation/models.py
+++ b/api/funkwhale_api/moderation/models.py
@@ -73,3 +73,22 @@ class InstancePolicy(models.Model):
             return {"type": "actor", "obj": self.target_actor}
         if self.target_domain_id:
             return {"type": "domain", "obj": self.target_domain}
+
+
+class UserFilter(models.Model):
+    uuid = models.UUIDField(default=uuid.uuid4, unique=True)
+    creation_date = models.DateTimeField(default=timezone.now)
+    target_artist = models.ForeignKey(
+        "music.Artist", on_delete=models.CASCADE, related_name="user_filters"
+    )
+    user = models.ForeignKey(
+        "users.User", on_delete=models.CASCADE, related_name="content_filters"
+    )
+
+    class Meta:
+        unique_together = ("user", "target_artist")
+
+    @property
+    def target(self):
+        if self.target_artist:
+            return {"type": "artist", "obj": self.target_artist}
diff --git a/api/funkwhale_api/moderation/serializers.py b/api/funkwhale_api/moderation/serializers.py
new file mode 100644
index 0000000000000000000000000000000000000000..20c34242102d4f302ac29a9781a059c08fef2fb9
--- /dev/null
+++ b/api/funkwhale_api/moderation/serializers.py
@@ -0,0 +1,45 @@
+from rest_framework import serializers
+
+from funkwhale_api.music import models as music_models
+from . import models
+
+
+class FilteredArtistSerializer(serializers.ModelSerializer):
+    class Meta:
+        model = music_models.Artist
+        fields = ["id", "name"]
+
+
+class TargetSerializer(serializers.Serializer):
+    type = serializers.ChoiceField(choices=["artist"])
+    id = serializers.CharField()
+
+    def to_representation(self, value):
+        if value["type"] == "artist":
+            data = FilteredArtistSerializer(value["obj"]).data
+            data.update({"type": "artist"})
+            return data
+
+    def to_internal_value(self, value):
+        if value["type"] == "artist":
+            field = serializers.PrimaryKeyRelatedField(
+                queryset=music_models.Artist.objects.all()
+            )
+        value["obj"] = field.to_internal_value(value["id"])
+        return value
+
+
+class UserFilterSerializer(serializers.ModelSerializer):
+    target = TargetSerializer()
+
+    class Meta:
+        model = models.UserFilter
+        fields = ["uuid", "target", "creation_date"]
+        read_only_fields = ["uuid", "creation_date"]
+
+    def validate(self, data):
+        target = data.pop("target")
+        if target["type"] == "artist":
+            data["target_artist"] = target["obj"]
+
+        return data
diff --git a/api/funkwhale_api/moderation/urls.py b/api/funkwhale_api/moderation/urls.py
new file mode 100644
index 0000000000000000000000000000000000000000..05d2e7a9223774db0329f66ae295255cb88d9b7a
--- /dev/null
+++ b/api/funkwhale_api/moderation/urls.py
@@ -0,0 +1,8 @@
+from rest_framework import routers
+
+from . import views
+
+router = routers.SimpleRouter()
+router.register(r"content-filters", views.UserFilterViewSet, "content-filters")
+
+urlpatterns = router.urls
diff --git a/api/funkwhale_api/moderation/views.py b/api/funkwhale_api/moderation/views.py
new file mode 100644
index 0000000000000000000000000000000000000000..feeeccf016686bbe754f492a43cb998756ce76f8
--- /dev/null
+++ b/api/funkwhale_api/moderation/views.py
@@ -0,0 +1,42 @@
+from django.db import IntegrityError
+
+from rest_framework import mixins
+from rest_framework import permissions
+from rest_framework import response
+from rest_framework import status
+from rest_framework import viewsets
+
+from . import models
+from . import serializers
+
+
+class UserFilterViewSet(
+    mixins.ListModelMixin,
+    mixins.CreateModelMixin,
+    mixins.RetrieveModelMixin,
+    mixins.DestroyModelMixin,
+    viewsets.GenericViewSet,
+):
+    lookup_field = "uuid"
+    queryset = (
+        models.UserFilter.objects.all()
+        .order_by("-creation_date")
+        .select_related("target_artist")
+    )
+    serializer_class = serializers.UserFilterSerializer
+    permission_classes = [permissions.IsAuthenticated]
+    ordering_fields = ("creation_date",)
+
+    def create(self, request, *args, **kwargs):
+        try:
+            return super().create(request, *args, **kwargs)
+        except IntegrityError:
+            content = {"detail": "A content filter already exists for this object"}
+            return response.Response(content, status=status.HTTP_400_BAD_REQUEST)
+
+    def get_queryset(self):
+        qs = super().get_queryset()
+        return qs.filter(user=self.request.user)
+
+    def perform_create(self, serializer):
+        serializer.save(user=self.request.user)
diff --git a/api/funkwhale_api/music/filters.py b/api/funkwhale_api/music/filters.py
index 009f0088d74999e72c9ac57200f416857849a8a6..3134ae19c987f086d3b4d80ce1ceb4abb903e976 100644
--- a/api/funkwhale_api/music/filters.py
+++ b/api/funkwhale_api/music/filters.py
@@ -2,12 +2,13 @@ from django_filters import rest_framework as filters
 
 from funkwhale_api.common import fields
 from funkwhale_api.common import search
+from funkwhale_api.moderation import filters as moderation_filters
 
 from . import models
 from . import utils
 
 
-class ArtistFilter(filters.FilterSet):
+class ArtistFilter(moderation_filters.HiddenContentFilterSet):
     q = fields.SearchFilter(search_fields=["name"])
     playable = filters.BooleanFilter(field_name="_", method="filter_playable")
 
@@ -17,13 +18,14 @@ class ArtistFilter(filters.FilterSet):
             "name": ["exact", "iexact", "startswith", "icontains"],
             "playable": "exact",
         }
+        hidden_content_fields_mapping = moderation_filters.USER_FILTER_CONFIG["ARTIST"]
 
     def filter_playable(self, queryset, name, value):
         actor = utils.get_actor_from_request(self.request)
         return queryset.playable_by(actor, value)
 
 
-class TrackFilter(filters.FilterSet):
+class TrackFilter(moderation_filters.HiddenContentFilterSet):
     q = fields.SearchFilter(search_fields=["title", "album__title", "artist__name"])
     playable = filters.BooleanFilter(field_name="_", method="filter_playable")
 
@@ -36,6 +38,7 @@ class TrackFilter(filters.FilterSet):
             "album": ["exact"],
             "license": ["exact"],
         }
+        hidden_content_fields_mapping = moderation_filters.USER_FILTER_CONFIG["TRACK"]
 
     def filter_playable(self, queryset, name, value):
         actor = utils.get_actor_from_request(self.request)
@@ -85,13 +88,14 @@ class UploadFilter(filters.FilterSet):
         return queryset.playable_by(actor, value)
 
 
-class AlbumFilter(filters.FilterSet):
+class AlbumFilter(moderation_filters.HiddenContentFilterSet):
     playable = filters.BooleanFilter(field_name="_", method="filter_playable")
     q = fields.SearchFilter(search_fields=["title", "artist__name"])
 
     class Meta:
         model = models.Album
         fields = ["playable", "q", "artist"]
+        hidden_content_fields_mapping = moderation_filters.USER_FILTER_CONFIG["ALBUM"]
 
     def filter_playable(self, queryset, name, value):
         actor = utils.get_actor_from_request(self.request)
diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py
index 5de07ca947edb23ac3a7334c6f94c294e1b64a15..d07fd27ec40f9a7173e563c9c50f132238adb13d 100644
--- a/api/funkwhale_api/music/views.py
+++ b/api/funkwhale_api/music/views.py
@@ -18,6 +18,7 @@ from taggit.models import Tag
 from funkwhale_api.common import permissions as common_permissions
 from funkwhale_api.common import preferences
 from funkwhale_api.common import utils as common_utils
+from funkwhale_api.common import views as common_views
 from funkwhale_api.federation.authentication import SignatureAuthentication
 from funkwhale_api.federation import api_serializers as federation_api_serializers
 from funkwhale_api.federation import routes
@@ -58,7 +59,7 @@ class TagViewSetMixin(object):
         return queryset
 
 
-class ArtistViewSet(viewsets.ReadOnlyModelViewSet):
+class ArtistViewSet(common_views.SkipFilterForGetObject, viewsets.ReadOnlyModelViewSet):
     queryset = models.Artist.objects.all()
     serializer_class = serializers.ArtistWithAlbumsSerializer
     permission_classes = [common_permissions.ConditionalAuthentication]
@@ -82,7 +83,7 @@ class ArtistViewSet(viewsets.ReadOnlyModelViewSet):
     )
 
 
-class AlbumViewSet(viewsets.ReadOnlyModelViewSet):
+class AlbumViewSet(common_views.SkipFilterForGetObject, viewsets.ReadOnlyModelViewSet):
     queryset = (
         models.Album.objects.all().order_by("artist", "release_date").select_related()
     )
@@ -166,7 +167,9 @@ class LibraryViewSet(
         return Response(serializer.data)
 
 
-class TrackViewSet(TagViewSetMixin, viewsets.ReadOnlyModelViewSet):
+class TrackViewSet(
+    common_views.SkipFilterForGetObject, TagViewSetMixin, viewsets.ReadOnlyModelViewSet
+):
     """
     A simple ViewSet for viewing and editing accounts.
     """
diff --git a/api/funkwhale_api/playlists/models.py b/api/funkwhale_api/playlists/models.py
index 1d33388015a80c618628b4923311cab49f15298c..9112dc49cd5f8c255cf50880752d469c24d383d2 100644
--- a/api/funkwhale_api/playlists/models.py
+++ b/api/funkwhale_api/playlists/models.py
@@ -17,7 +17,7 @@ class PlaylistQuerySet(models.QuerySet):
 
     def with_covers(self):
         album_prefetch = models.Prefetch(
-            "album", queryset=music_models.Album.objects.only("cover")
+            "album", queryset=music_models.Album.objects.only("cover", "artist_id")
         )
         track_prefetch = models.Prefetch(
             "track",
diff --git a/api/funkwhale_api/playlists/serializers.py b/api/funkwhale_api/playlists/serializers.py
index b64996640259c03a6394c1353bea641afb31dcf5..87350cd04a3bec09e04cf19bd186fefeb7dfbe7d 100644
--- a/api/funkwhale_api/playlists/serializers.py
+++ b/api/funkwhale_api/playlists/serializers.py
@@ -117,9 +117,21 @@ class PlaylistSerializer(serializers.ModelSerializer):
         except AttributeError:
             return []
 
+        try:
+            user = self.context["request"].user
+        except (KeyError, AttributeError):
+            excluded_artists = []
+            user = None
+        if user and user.is_authenticated:
+            excluded_artists = list(
+                user.content_filters.values_list("target_artist", flat=True)
+            )
+
         covers = []
         max_covers = 5
         for plt in plts:
+            if plt.track.album.artist_id in excluded_artists:
+                continue
             url = plt.track.album.cover.crop["200x200"].url
             if url in covers:
                 continue
diff --git a/api/funkwhale_api/radios/radios.py b/api/funkwhale_api/radios/radios.py
index 8ca15a026fd523feae3ecf094e9ec1bf66996005..f19c6b884d299fd53c17b472cc8324f7beb69d97 100644
--- a/api/funkwhale_api/radios/radios.py
+++ b/api/funkwhale_api/radios/radios.py
@@ -5,6 +5,7 @@ from django.db import connection
 from rest_framework import serializers
 from taggit.models import Tag
 
+from funkwhale_api.moderation import filters as moderation_filters
 from funkwhale_api.music.models import Artist, Track
 from funkwhale_api.users.models import User
 
@@ -43,7 +44,14 @@ class SessionRadio(SimpleRadio):
         return self.session
 
     def get_queryset(self, **kwargs):
-        return Track.objects.all()
+        qs = Track.objects.all()
+        if not self.session:
+            return qs
+        query = moderation_filters.get_filtered_content_query(
+            config=moderation_filters.USER_FILTER_CONFIG["TRACK"],
+            user=self.session.user,
+        )
+        return qs.exclude(query)
 
     def get_queryset_kwargs(self):
         return {}
diff --git a/api/funkwhale_api/subsonic/views.py b/api/funkwhale_api/subsonic/views.py
index f7926d1fdc09250f049045ea8f72ac9542e1a0f8..c0b26bf4634c0cdf4cbd0418f763c71d494543f3 100644
--- a/api/funkwhale_api/subsonic/views.py
+++ b/api/funkwhale_api/subsonic/views.py
@@ -13,6 +13,7 @@ import funkwhale_api
 from funkwhale_api.activity import record
 from funkwhale_api.common import fields, preferences, utils as common_utils
 from funkwhale_api.favorites.models import TrackFavorite
+from funkwhale_api.moderation import filters as moderation_filters
 from funkwhale_api.music import models as music_models
 from funkwhale_api.music import utils
 from funkwhale_api.music import views as music_views
@@ -152,8 +153,14 @@ class SubsonicViewSet(viewsets.GenericViewSet):
         url_path="getArtists",
     )
     def get_artists(self, request, *args, **kwargs):
-        artists = music_models.Artist.objects.all().playable_by(
-            utils.get_actor_from_request(request)
+        artists = (
+            music_models.Artist.objects.all()
+            .exclude(
+                moderation_filters.get_filtered_content_query(
+                    moderation_filters.USER_FILTER_CONFIG["ARTIST"], request.user
+                )
+            )
+            .playable_by(utils.get_actor_from_request(request))
         )
         data = serializers.GetArtistsSerializer(artists).data
         payload = {"artists": data}
@@ -167,8 +174,14 @@ class SubsonicViewSet(viewsets.GenericViewSet):
         url_path="getIndexes",
     )
     def get_indexes(self, request, *args, **kwargs):
-        artists = music_models.Artist.objects.all().playable_by(
-            utils.get_actor_from_request(request)
+        artists = (
+            music_models.Artist.objects.all()
+            .exclude(
+                moderation_filters.get_filtered_content_query(
+                    moderation_filters.USER_FILTER_CONFIG["ARTIST"], request.user
+                )
+            )
+            .playable_by(utils.get_actor_from_request(request))
         )
         data = serializers.GetArtistsSerializer(artists).data
         payload = {"indexes": data}
@@ -273,7 +286,11 @@ class SubsonicViewSet(viewsets.GenericViewSet):
     def get_random_songs(self, request, *args, **kwargs):
         data = request.GET or request.POST
         actor = utils.get_actor_from_request(request)
-        queryset = music_models.Track.objects.all()
+        queryset = music_models.Track.objects.all().exclude(
+            moderation_filters.get_filtered_content_query(
+                moderation_filters.USER_FILTER_CONFIG["TRACK"], request.user
+            )
+        )
         queryset = queryset.playable_by(actor)
         try:
             size = int(data["size"])
@@ -308,8 +325,14 @@ class SubsonicViewSet(viewsets.GenericViewSet):
         url_path="getAlbumList2",
     )
     def get_album_list2(self, request, *args, **kwargs):
-        queryset = music_models.Album.objects.with_tracks_count().order_by(
-            "artist__name"
+        queryset = (
+            music_models.Album.objects.exclude(
+                moderation_filters.get_filtered_content_query(
+                    moderation_filters.USER_FILTER_CONFIG["ALBUM"], request.user
+                )
+            )
+            .with_tracks_count()
+            .order_by("artist__name")
         )
         data = request.GET or request.POST
         filterset = filters.AlbumList2FilterSet(data, queryset=queryset)
diff --git a/api/tests/favorites/test_filters.py b/api/tests/favorites/test_filters.py
new file mode 100644
index 0000000000000000000000000000000000000000..e5eaca39ebcf9c265cff93dbed9466c5e93b4ca5
--- /dev/null
+++ b/api/tests/favorites/test_filters.py
@@ -0,0 +1,30 @@
+from funkwhale_api.favorites import filters
+from funkwhale_api.favorites import models
+
+
+def test_track_favorite_filter_track_artist(factories, mocker, queryset_equal_list):
+    factories["favorites.TrackFavorite"]()
+    cf = factories["moderation.UserFilter"](for_artist=True)
+    hidden_fav = factories["favorites.TrackFavorite"](track__artist=cf.target_artist)
+    qs = models.TrackFavorite.objects.all()
+    filterset = filters.TrackFavoriteFilter(
+        {"hidden": "true"}, request=mocker.Mock(user=cf.user), queryset=qs
+    )
+
+    assert filterset.qs == [hidden_fav]
+
+
+def test_track_favorite_filter_track_album_artist(
+    factories, mocker, queryset_equal_list
+):
+    factories["favorites.TrackFavorite"]()
+    cf = factories["moderation.UserFilter"](for_artist=True)
+    hidden_fav = factories["favorites.TrackFavorite"](
+        track__album__artist=cf.target_artist
+    )
+    qs = models.TrackFavorite.objects.all()
+    filterset = filters.TrackFavoriteFilter(
+        {"hidden": "true"}, request=mocker.Mock(user=cf.user), queryset=qs
+    )
+
+    assert filterset.qs == [hidden_fav]
diff --git a/api/tests/history/test_filters.py b/api/tests/history/test_filters.py
new file mode 100644
index 0000000000000000000000000000000000000000..257d46bf0c0b311ff57c0b71f90d32b0beea38c1
--- /dev/null
+++ b/api/tests/history/test_filters.py
@@ -0,0 +1,28 @@
+from funkwhale_api.history import filters
+from funkwhale_api.history import models
+
+
+def test_listening_filter_track_artist(factories, mocker, queryset_equal_list):
+    factories["history.Listening"]()
+    cf = factories["moderation.UserFilter"](for_artist=True)
+    hidden_listening = factories["history.Listening"](track__artist=cf.target_artist)
+    qs = models.Listening.objects.all()
+    filterset = filters.ListeningFilter(
+        {"hidden": "true"}, request=mocker.Mock(user=cf.user), queryset=qs
+    )
+
+    assert filterset.qs == [hidden_listening]
+
+
+def test_listening_filter_track_album_artist(factories, mocker, queryset_equal_list):
+    factories["history.Listening"]()
+    cf = factories["moderation.UserFilter"](for_artist=True)
+    hidden_listening = factories["history.Listening"](
+        track__album__artist=cf.target_artist
+    )
+    qs = models.Listening.objects.all()
+    filterset = filters.ListeningFilter(
+        {"hidden": "true"}, request=mocker.Mock(user=cf.user), queryset=qs
+    )
+
+    assert filterset.qs == [hidden_listening]
diff --git a/api/tests/moderation/__init__.py b/api/tests/moderation/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/api/tests/moderation/test_filters.py b/api/tests/moderation/test_filters.py
new file mode 100644
index 0000000000000000000000000000000000000000..cb1dab95a309cf5a8c001989e1a164ed30d9c2a2
--- /dev/null
+++ b/api/tests/moderation/test_filters.py
@@ -0,0 +1,68 @@
+from funkwhale_api.moderation import filters
+from funkwhale_api.music import models as music_models
+
+
+def test_hidden_defaults_to_true(factories, queryset_equal_list, mocker):
+    user = factories["users.User"]()
+    artist = factories["music.Artist"]()
+    hidden_artist = factories["music.Artist"]()
+    factories["moderation.UserFilter"](target_artist=hidden_artist, user=user)
+
+    class FS(filters.HiddenContentFilterSet):
+        class Meta:
+            hidden_content_fields_mapping = {"target_artist": ["pk"]}
+
+    filterset = FS(
+        data={},
+        queryset=music_models.Artist.objects.all(),
+        request=mocker.Mock(user=user),
+    )
+    assert filterset.data["hidden"] is False
+    queryset = filterset.filter_hidden_content(
+        music_models.Artist.objects.all(), "", False
+    )
+
+    assert queryset == [artist]
+
+
+def test_hidden_false(factories, queryset_equal_list, mocker):
+    user = factories["users.User"]()
+    factories["music.Artist"]()
+    hidden_artist = factories["music.Artist"]()
+    factories["moderation.UserFilter"](target_artist=hidden_artist, user=user)
+
+    class FS(filters.HiddenContentFilterSet):
+        class Meta:
+            hidden_content_fields_mapping = {"target_artist": ["pk"]}
+
+    filterset = FS(
+        data={},
+        queryset=music_models.Artist.objects.all(),
+        request=mocker.Mock(user=user),
+    )
+
+    queryset = filterset.filter_hidden_content(
+        music_models.Artist.objects.all(), "", True
+    )
+
+    assert queryset == [hidden_artist]
+
+
+def test_hidden_anonymous(factories, queryset_equal_list, mocker, anonymous_user):
+    artist = factories["music.Artist"]()
+
+    class FS(filters.HiddenContentFilterSet):
+        class Meta:
+            hidden_content_fields_mapping = {"target_artist": ["pk"]}
+
+    filterset = FS(
+        data={},
+        queryset=music_models.Artist.objects.all(),
+        request=mocker.Mock(user=anonymous_user),
+    )
+
+    queryset = filterset.filter_hidden_content(
+        music_models.Artist.objects.all(), "", True
+    )
+
+    assert queryset == [artist]
diff --git a/api/tests/moderation/test_serializers.py b/api/tests/moderation/test_serializers.py
new file mode 100644
index 0000000000000000000000000000000000000000..a38214143db36f97aa349f712681a3dab2fcbf1b
--- /dev/null
+++ b/api/tests/moderation/test_serializers.py
@@ -0,0 +1,30 @@
+from funkwhale_api.moderation import serializers
+
+
+def test_user_filter_serializer_repr(factories):
+    artist = factories["music.Artist"]()
+    content_filter = factories["moderation.UserFilter"](target_artist=artist)
+
+    expected = {
+        "uuid": str(content_filter.uuid),
+        "target": {"type": "artist", "id": artist.pk, "name": artist.name},
+        "creation_date": content_filter.creation_date.isoformat().replace(
+            "+00:00", "Z"
+        ),
+    }
+
+    serializer = serializers.UserFilterSerializer(content_filter)
+
+    assert serializer.data == expected
+
+
+def test_user_filter_serializer_save(factories):
+    artist = factories["music.Artist"]()
+    user = factories["users.User"]()
+    data = {"target": {"type": "artist", "id": artist.pk}}
+
+    serializer = serializers.UserFilterSerializer(data=data)
+    serializer.is_valid(raise_exception=True)
+    content_filter = serializer.save(user=user)
+
+    assert content_filter.target_artist == artist
diff --git a/api/tests/moderation/test_views.py b/api/tests/moderation/test_views.py
new file mode 100644
index 0000000000000000000000000000000000000000..3d53f4565a315b84c40fccd7d8b6c226a6883a24
--- /dev/null
+++ b/api/tests/moderation/test_views.py
@@ -0,0 +1,24 @@
+from django.urls import reverse
+
+
+def test_restrict_to_own_filters(factories, logged_in_api_client):
+    cf = factories["moderation.UserFilter"](
+        for_artist=True, user=logged_in_api_client.user
+    )
+    factories["moderation.UserFilter"](for_artist=True)
+    url = reverse("api:v1:moderation:content-filters-list")
+    response = logged_in_api_client.get(url)
+    assert response.status_code == 200
+    assert response.data["count"] == 1
+    assert response.data["results"][0]["uuid"] == str(cf.uuid)
+
+
+def test_create_filter(factories, logged_in_api_client):
+    artist = factories["music.Artist"]()
+    url = reverse("api:v1:moderation:content-filters-list")
+    data = {"target": {"type": "artist", "id": artist.pk}}
+    response = logged_in_api_client.post(url, data, format="json")
+
+    cf = logged_in_api_client.user.content_filters.latest("id")
+    assert cf.target_artist == artist
+    assert response.status_code == 201
diff --git a/api/tests/music/test_filters.py b/api/tests/music/test_filters.py
new file mode 100644
index 0000000000000000000000000000000000000000..f9abc4b2156b6a537471bb83ec583ecfb477faee
--- /dev/null
+++ b/api/tests/music/test_filters.py
@@ -0,0 +1,54 @@
+from funkwhale_api.music import filters
+from funkwhale_api.music import models
+
+
+def test_album_filter_hidden(factories, mocker, queryset_equal_list):
+    factories["music.Album"]()
+    cf = factories["moderation.UserFilter"](for_artist=True)
+    hidden_album = factories["music.Album"](artist=cf.target_artist)
+
+    qs = models.Album.objects.all()
+    filterset = filters.AlbumFilter(
+        {"hidden": "true"}, request=mocker.Mock(user=cf.user), queryset=qs
+    )
+
+    assert filterset.qs == [hidden_album]
+
+
+def test_artist_filter_hidden(factories, mocker, queryset_equal_list):
+    factories["music.Artist"]()
+    cf = factories["moderation.UserFilter"](for_artist=True)
+    hidden_artist = cf.target_artist
+
+    qs = models.Artist.objects.all()
+    filterset = filters.ArtistFilter(
+        {"hidden": "true"}, request=mocker.Mock(user=cf.user), queryset=qs
+    )
+
+    assert filterset.qs == [hidden_artist]
+
+
+def test_artist_filter_track_artist(factories, mocker, queryset_equal_list):
+    factories["music.Track"]()
+    cf = factories["moderation.UserFilter"](for_artist=True)
+    hidden_track = factories["music.Track"](artist=cf.target_artist)
+
+    qs = models.Track.objects.all()
+    filterset = filters.TrackFilter(
+        {"hidden": "true"}, request=mocker.Mock(user=cf.user), queryset=qs
+    )
+
+    assert filterset.qs == [hidden_track]
+
+
+def test_artist_filter_track_album_artist(factories, mocker, queryset_equal_list):
+    factories["music.Track"]()
+    cf = factories["moderation.UserFilter"](for_artist=True)
+    hidden_track = factories["music.Track"](album__artist=cf.target_artist)
+
+    qs = models.Track.objects.all()
+    filterset = filters.TrackFilter(
+        {"hidden": "true"}, request=mocker.Mock(user=cf.user), queryset=qs
+    )
+
+    assert filterset.qs == [hidden_track]
diff --git a/api/tests/radios/test_radios.py b/api/tests/radios/test_radios.py
index cedb6bd7f856afefe6c9de617a32e70a3b0b83b1..640e712117bfb2ffc641af43daaa0eb5268ad0fd 100644
--- a/api/tests/radios/test_radios.py
+++ b/api/tests/radios/test_radios.py
@@ -254,3 +254,27 @@ def test_similar_radio_track(factories):
     factories["history.Listening"](track=expected_next, user=l1.user)
 
     assert radio.pick(filter_playable=False) == expected_next
+
+
+def test_session_radio_get_queryset_ignore_filtered_track_artist(
+    factories, queryset_equal_list
+):
+    cf = factories["moderation.UserFilter"](for_artist=True)
+    factories["music.Track"](artist=cf.target_artist)
+    valid_track = factories["music.Track"]()
+    radio = radios.RandomRadio()
+    radio.start_session(user=cf.user)
+
+    assert radio.get_queryset() == [valid_track]
+
+
+def test_session_radio_get_queryset_ignore_filtered_track_album_artist(
+    factories, queryset_equal_list
+):
+    cf = factories["moderation.UserFilter"](for_artist=True)
+    factories["music.Track"](album__artist=cf.target_artist)
+    valid_track = factories["music.Track"]()
+    radio = radios.RandomRadio()
+    radio.start_session(user=cf.user)
+
+    assert radio.get_queryset() == [valid_track]
diff --git a/api/tests/subsonic/test_views.py b/api/tests/subsonic/test_views.py
index 0dbbaf39a2b6018017351dbf51400b3b62ad5d35..e75bfc2a0de554cc9e6940ea12c240469ec27221 100644
--- a/api/tests/subsonic/test_views.py
+++ b/api/tests/subsonic/test_views.py
@@ -7,6 +7,7 @@ from django.utils import timezone
 from rest_framework.response import Response
 
 import funkwhale_api
+from funkwhale_api.moderation import filters as moderation_filters
 from funkwhale_api.music import models as music_models
 from funkwhale_api.music import views as music_views
 from funkwhale_api.subsonic import renderers, serializers
@@ -100,20 +101,31 @@ def test_ping(f, db, api_client):
 def test_get_artists(
     f, db, logged_in_api_client, factories, mocker, queryset_equal_queries
 ):
+    factories["moderation.UserFilter"](
+        user=logged_in_api_client.user,
+        target_artist=factories["music.Artist"](playable=True),
+    )
     url = reverse("api:subsonic-get_artists")
     assert url.endswith("getArtists") is True
     factories["music.Artist"].create_batch(size=3, playable=True)
     playable_by = mocker.spy(music_models.ArtistQuerySet, "playable_by")
+    exclude_query = moderation_filters.get_filtered_content_query(
+        moderation_filters.USER_FILTER_CONFIG["ARTIST"], logged_in_api_client.user
+    )
+    assert exclude_query is not None
     expected = {
         "artists": serializers.GetArtistsSerializer(
-            music_models.Artist.objects.all()
+            music_models.Artist.objects.all().exclude(exclude_query)
         ).data
     }
     response = logged_in_api_client.get(url, {"f": f})
 
     assert response.status_code == 200
     assert response.data == expected
-    playable_by.assert_called_once_with(music_models.Artist.objects.all(), None)
+    playable_by.assert_called_once_with(
+        music_models.Artist.objects.all().exclude(exclude_query),
+        logged_in_api_client.user.actor,
+    )
 
 
 @pytest.mark.parametrize("f", ["json"])
@@ -502,12 +514,20 @@ def test_get_music_folders(f, db, logged_in_api_client, factories):
 def test_get_indexes(
     f, db, logged_in_api_client, factories, mocker, queryset_equal_queries
 ):
+    factories["moderation.UserFilter"](
+        user=logged_in_api_client.user,
+        target_artist=factories["music.Artist"](playable=True),
+    )
+    exclude_query = moderation_filters.get_filtered_content_query(
+        moderation_filters.USER_FILTER_CONFIG["ARTIST"], logged_in_api_client.user
+    )
+
     url = reverse("api:subsonic-get_indexes")
     assert url.endswith("getIndexes") is True
     factories["music.Artist"].create_batch(size=3, playable=True)
     expected = {
         "indexes": serializers.GetArtistsSerializer(
-            music_models.Artist.objects.all()
+            music_models.Artist.objects.all().exclude(exclude_query)
         ).data
     }
     playable_by = mocker.spy(music_models.ArtistQuerySet, "playable_by")
@@ -516,7 +536,10 @@ def test_get_indexes(
     assert response.status_code == 200
     assert response.data == expected
 
-    playable_by.assert_called_once_with(music_models.Artist.objects.all(), None)
+    playable_by.assert_called_once_with(
+        music_models.Artist.objects.all().exclude(exclude_query),
+        logged_in_api_client.user.actor,
+    )
 
 
 def test_get_cover_art_album(factories, logged_in_api_client):
diff --git a/changes/changelog.d/701.feature b/changes/changelog.d/701.feature
new file mode 100644
index 0000000000000000000000000000000000000000..d2a9500d6b1fd40f6d4ec33dd9f034bc307cf37c
--- /dev/null
+++ b/changes/changelog.d/701.feature
@@ -0,0 +1 @@
+Allow artists hiding (#701)
diff --git a/changes/notes.rst b/changes/notes.rst
index 96ac3d7651f92166072a2fb200c0dd57606851e3..19aa5a077c5e7425bb4b732470c8340f87e8b72c 100644
--- a/changes/notes.rst
+++ b/changes/notes.rst
@@ -5,3 +5,18 @@ Next release notes
 
     Those release notes refer to the current development branch and are reset
     after each release.
+
+Artist hiding in the interface
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+It's now possible for users to hide artists they don't want to see.
+
+Content linked to hidden artists will not show up in the interface anymore. Especially:
+
+- Hidden artists tracks are removed from the current queue
+- Starting a playlist will skip tracks from hidden artists
+- Recently favorited, recently listened and recently added widgets on the homepage won't include content from hidden artists
+- Radio suggestions will exclude tracks from hidden artists
+- Hidden artists won't appear in Subsonic apps
+
+Results linked to hidden artists will continue to show up in search results and their profile page remains accessible.
diff --git a/front/src/App.vue b/front/src/App.vue
index 02383e6cfbf41e17e45c5382f57d75e764eadc7f..e06156c182a1f3bbb860d17dab886d6ea2567f0f 100644
--- a/front/src/App.vue
+++ b/front/src/App.vue
@@ -44,6 +44,7 @@
         @show:shortcuts-modal="showShortcutsModal = !showShortcutsModal"
       ></app-footer>
       <playlist-modal v-if="$store.state.auth.authenticated"></playlist-modal>
+      <filter-modal v-if="$store.state.auth.authenticated"></filter-modal>
       <shortcuts-modal @update:show="showShortcutsModal = $event" :show="showShortcutsModal"></shortcuts-modal>
       <GlobalEvents @keydown.h.exact="showShortcutsModal = !showShortcutsModal"/>
     </template>
@@ -63,6 +64,7 @@ import ServiceMessages from '@/components/ServiceMessages'
 import moment from  'moment'
 import locales from './locales'
 import PlaylistModal from '@/components/playlists/PlaylistModal'
+import FilterModal from '@/components/moderation/FilterModal'
 import ShortcutsModal from '@/components/ShortcutsModal'
 
 export default {
@@ -70,6 +72,7 @@ export default {
   components: {
     Sidebar,
     AppFooter,
+    FilterModal,
     PlaylistModal,
     ShortcutsModal,
     GlobalEvents,
diff --git a/front/src/components/Sidebar.vue b/front/src/components/Sidebar.vue
index 3a5bf2db8a3ddbee831bbbdf5d88c7fc1d018b3f..865618b12cc8ffd58dec9f50e35b1f15d82e4a3c 100644
--- a/front/src/components/Sidebar.vue
+++ b/front/src/components/Sidebar.vue
@@ -259,6 +259,29 @@ export default {
           ? 0
           : container.clientHeight / 2
       container.scrollTop = container.scrollTop - scrollBack
+    },
+    applyContentFilters () {
+      let artistIds = this.$store.getters['moderation/artistFilters']().map((f) => {
+        return f.target.id
+      })
+
+      if (artistIds.length === 0) {
+        return
+      }
+      let self = this
+      let tracks = this.tracks.slice().reverse()
+      tracks.forEach(async (t, i) => {
+        // we loop from the end because removing index from the start can lead to removing the wrong tracks
+        let realIndex = tracks.length - i - 1
+        let matchArtist = artistIds.indexOf(t.artist.id) > -1
+        if (matchArtist) {
+          return await self.cleanTrack(realIndex)
+        }
+        if (t.album && artistIds.indexOf(t.album.artist.id) > -1) {
+          return await self.cleanTrack(realIndex)
+        }
+      })
+
     }
   },
   watch: {
@@ -274,6 +297,9 @@ export default {
       if (this.selectedTab !== "queue") {
         this.scrollToCurrent()
       }
+    },
+    "$store.state.moderation.lastUpdate": function () {
+      this.applyContentFilters()
     }
   }
 }
diff --git a/front/src/components/audio/PlayButton.vue b/front/src/components/audio/PlayButton.vue
index a84ee67d5d25cd809cd08c5d34b0dfe0d5b465a2..9e16a556fdf36be448cda10a48a5e63a0d465a18 100644
--- a/front/src/components/audio/PlayButton.vue
+++ b/front/src/components/audio/PlayButton.vue
@@ -1,5 +1,5 @@
 <template>
-  <span :title="title" :class="['ui', {'tiny': discrete}, {'buttons': !dropdownOnly && !iconOnly}]">
+  <span :class="['ui', {'tiny': discrete}, {'buttons': !dropdownOnly && !iconOnly}]">
     <button
       v-if="!dropdownOnly"
       :title="labels.playNow"
@@ -9,8 +9,8 @@
       <i :class="[playIconClass, 'icon']"></i>
       <template v-if="!discrete && !iconOnly"><slot><translate :translate-context="'*/Queue/Button/Label/Short, Verb'">Play</translate></slot></template>
     </button>
-    <div v-if="!discrete && !iconOnly" :class="['ui', {disabled: !playable}, 'floating', 'dropdown', {'icon': !dropdownOnly}, {'button': !dropdownOnly}]">
-      <i :class="dropdownIconClasses.concat(['icon'])"></i>
+    <div v-if="!discrete && !iconOnly" :class="['ui', {disabled: !playable && !filterableArtist}, 'floating', 'dropdown', {'icon': !dropdownOnly}, {'button': !dropdownOnly}]">
+      <i :class="dropdownIconClasses.concat(['icon'])" :title="title" ></i>
       <div class="menu">
         <button class="item basic" ref="add" data-ref="add" :disabled="!playable" @click.stop.prevent="add" :title="labels.addToQueue">
           <i class="plus icon"></i><translate :translate-context="'*/Queue/Dropdown/Button/Label/Short'">Add to queue</translate>
@@ -24,6 +24,9 @@
         <button v-if="track" class="item basic" :disabled="!playable" @click.stop.prevent="$store.dispatch('radios/start', {type: 'similar', objectId: track.id})" :title="labels.startRadio">
           <i class="feed icon"></i><translate :translate-context="'*/Queue/Dropdown/Button/Label/Short'">Start radio</translate>
         </button>
+        <button v-if="filterableArtist" class="item basic" :disabled="!filterableArtist" @click.stop.prevent="filterArtist" :title="labels.hideArtist">
+          <i class="eye slash outline icon"></i><translate :translate-context="'*/Queue/Dropdown/Button/Label/Short'">Hide content from this artist</translate>
+        </button>
       </div>
     </div>
   </span>
@@ -45,13 +48,13 @@ export default {
     discrete: {type: Boolean, default: false},
     dropdownOnly: {type: Boolean, default: false},
     iconOnly: {type: Boolean, default: false},
-    artist: {type: Number, required: false},
-    album: {type: Number, required: false},
+    artist: {type: Object, required: false},
+    album: {type: Object, required: false},
     isPlayable: {type: Boolean, required: false, default: null}
   },
   data () {
     return {
-      isLoading: false
+      isLoading: false,
     }
   },
   mounted () {
@@ -91,7 +94,7 @@ export default {
       if (this.track) {
         return this.track.uploads && this.track.uploads.length > 0
       } else if (this.artist) {
-        return this.albums.filter((a) => {
+        return this.artist.albums.filter((a) => {
           return a.is_playable === true
         }).length > 0
       } else if (this.tracks) {
@@ -100,9 +103,24 @@ export default {
         }).length > 0
       }
       return false
+    },
+    filterableArtist () {
+      if (this.track) {
+        return this.track.artist
+      }
+      if (this.album) {
+        return this.album.artist
+      }
+      if (this.artist) {
+        return this.artist
+      }
     }
   },
   methods: {
+
+    filterArtist () {
+      this.$store.dispatch('moderation/hide', {type: 'artist', target: this.filterableArtist})
+    },
     getTracksPage (page, params, resolve, tracks) {
       if (page > 10) {
         // it's 10 * 100 tracks already, let's stop here
@@ -113,6 +131,7 @@ export default {
       let self = this
       params['page_size'] = 100
       params['page'] = page
+      params['hidden'] = ''
       tracks = tracks || []
       axios.get('tracks/', {params: params}).then((response) => {
         response.data.results.forEach(t => {
@@ -143,15 +162,27 @@ export default {
         } else if (self.playlist) {
           let url = 'playlists/' + self.playlist.id + '/'
           axios.get(url + 'tracks/').then((response) => {
-            resolve(response.data.results.map(plt => {
+            let artistIds = self.$store.getters['moderation/artistFilters']().map((f) => {
+              return f.target.id
+            })
+            let tracks = response.data.results.map(plt => {
               return plt.track
-            }))
+            })
+            if (artistIds.length > 0) {
+              // skip tracks from hidden artists
+              tracks = tracks.filter((t) => {
+                let matchArtist = artistIds.indexOf(t.artist.id) > -1
+                return !(matchArtist || t.album && artistIds.indexOf(t.album.artist.id) > -1)
+              })
+            }
+
+            resolve(tracks)
           })
         } else if (self.artist) {
-          let params = {'artist': self.artist, 'ordering': 'album__release_date,position'}
+          let params = {'artist': self.artist.id, 'ordering': 'album__release_date,position'}
           self.getTracksPage(1, params, resolve)
         } else if (self.album) {
-          let params = {'album': self.album, 'ordering': 'position'}
+          let params = {'album': self.album.id, 'ordering': 'position'}
           self.getTracksPage(1, params, resolve)
         }
       })
@@ -192,7 +223,7 @@ export default {
         content: this.$gettextInterpolate(msg, {count: tracks.length}),
         date: new Date()
       })
-    }
+    },
   }
 }
 </script>
diff --git a/front/src/components/audio/Player.vue b/front/src/components/audio/Player.vue
index 5df3248bf36da2a461ecb89a5f9e1b252282d10f..6d506b9660416d8e5782a49f5b764c0ba4ef97c8 100644
--- a/front/src/components/audio/Player.vue
+++ b/front/src/components/audio/Player.vue
@@ -38,6 +38,14 @@
                 v-if="$store.state.auth.authenticated"
                 :class="['inverted']"
                 :track="currentTrack"></track-playlist-icon>
+              <button
+                v-if="$store.state.auth.authenticated"
+                @click="$store.dispatch('moderation/hide', {type: 'artist', target: currentTrack.artist})"
+                :class="['ui', 'really', 'basic', 'circular', 'inverted', 'icon', 'button']"
+                :aria-label="labels.addArtistContentFilter"
+                :title="labels.addArtistContentFilter">
+                <i :class="['eye slash outline', 'basic', 'icon']"></i>
+              </button>
             </div>
           </div>
         </div>
@@ -353,13 +361,13 @@ export default {
       let next = this.$pgettext('Sidebar/Player/Icon.Tooltip', "Next track")
       let unmute = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', "Unmute")
       let mute = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', "Mute")
-      let loopingDisabled = this.$pgettext('Sidebar/Player/Icon.Tooltip', 
+      let loopingDisabled = this.$pgettext('Sidebar/Player/Icon.Tooltip',
         "Looping disabled. Click to switch to single-track looping."
       )
-      let loopingSingle = this.$pgettext('Sidebar/Player/Icon.Tooltip', 
+      let loopingSingle = this.$pgettext('Sidebar/Player/Icon.Tooltip',
         "Looping on a single track. Click to switch to whole queue looping."
       )
-      let loopingWhole = this.$pgettext('Sidebar/Player/Icon.Tooltip', 
+      let loopingWhole = this.$pgettext('Sidebar/Player/Icon.Tooltip',
         "Looping on whole queue. Click to disable looping."
       )
       let shuffle = this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', "Shuffle your queue")
diff --git a/front/src/components/audio/album/Card.vue b/front/src/components/audio/album/Card.vue
index 75505d6f922b7130b3fe26d1be20c27ff553b399..1f4021ef5db7c41f43e05aab768d8f27947c1c73 100644
--- a/front/src/components/audio/album/Card.vue
+++ b/front/src/components/audio/album/Card.vue
@@ -45,7 +45,7 @@
         </div>
       </div>
       <div class="extra content">
-        <play-button class="mini basic orange right floated" :tracks="album.tracks">
+        <play-button class="mini basic orange right floated" :tracks="album.tracks" :album="album">
           <translate :translate-context="'Content/Queue/Card.Button.Label/Short, Verb'">Play all</translate>
         </play-button>
         <span>
diff --git a/front/src/components/audio/album/Widget.vue b/front/src/components/audio/album/Widget.vue
index f7100a9b86c3f33ea5ac3d0bef26f84d3f45f8ec..7d8d82cc766865d9b9057795d6628fca566f8513 100644
--- a/front/src/components/audio/album/Widget.vue
+++ b/front/src/components/audio/album/Widget.vue
@@ -12,7 +12,7 @@
       </div>
       <div class="card" v-for="album in albums" :key="album.id">
         <div :class="['ui', 'image', 'with-overlay', {'default-cover': !album.cover.original}]" v-lazy:background-image="getImageUrl(album)">
-          <play-button class="play-overlay" :icon-only="true" :is-playable="album.is_playable" :button-classes="['ui', 'circular', 'large', 'orange', 'icon', 'button']" :album="album.id"></play-button>
+          <play-button class="play-overlay" :icon-only="true" :is-playable="album.is_playable" :button-classes="['ui', 'circular', 'large', 'orange', 'icon', 'button']" :album="album"></play-button>
         </div>
         <div class="content">
           <router-link :title="album.title" :to="{name: 'library.albums.detail', params: {id: album.id}}">
@@ -28,7 +28,7 @@
         </div>
         <div class="extra content">
           <human-date class="left floated" :date="album.creation_date"></human-date>
-          <play-button class="right floated basic icon" :dropdown-only="true" :is-playable="album.is_playable" :dropdown-icon-classes="['ellipsis', 'horizontal', 'large', 'grey']" :album="album.id"></play-button>
+          <play-button class="right floated basic icon" :dropdown-only="true" :is-playable="album.is_playable" :dropdown-icon-classes="['ellipsis', 'horizontal', 'large', 'grey']" :album="album"></play-button>
         </div>
       </div>
     </div>
@@ -101,6 +101,9 @@ export default {
   watch: {
     offset () {
       this.fetchData()
+    },
+    "$store.state.moderation.lastUpdate": function () {
+      this.fetchData('albums/')
     }
   }
 }
diff --git a/front/src/components/audio/artist/Card.vue b/front/src/components/audio/artist/Card.vue
index 82af12524c20b24f7f8c80f894dfb46f605676fb..d0223ac2d87d96d6dbbe89df7da229f6a671f013 100644
--- a/front/src/components/audio/artist/Card.vue
+++ b/front/src/components/audio/artist/Card.vue
@@ -21,7 +21,7 @@
                   {{ album.tracks_count }} tracks
                 </td>
                 <td>
-                  <play-button class="right floated basic icon" :is-playable="album.is_playable" :discrete="true" :album="album.id"></play-button>
+                  <play-button class="right floated basic icon" :is-playable="album.is_playable" :discrete="true" :album="album"></play-button>
                 </td>
               </tr>
             </tbody>
@@ -41,7 +41,7 @@
           <i class="sound icon"></i>
             <translate :translate-context="'Content/Artist/Card'" :translate-params="{count: artist.albums.length}" :translate-n="artist.albums.length" translate-plural="%{ count } albums">1 album</translate>
         </span>
-        <play-button :is-playable="isPlayable" class="mini basic orange right floated" :artist="artist.id">
+        <play-button :is-playable="isPlayable" class="mini basic orange right floated" :artist="artist">
           <translate :translate-context="'Content/Queue/Button.Label/Short, Verb'">Play all</translate>
         </play-button>
       </div>
diff --git a/front/src/components/audio/track/Widget.vue b/front/src/components/audio/track/Widget.vue
index f909945f0f97119fcb3ab3a36d8943bfe9513a3b..b8ad3c639c66eb756ca7f3d50148d08aa5e7c5df 100644
--- a/front/src/components/audio/track/Widget.vue
+++ b/front/src/components/audio/track/Widget.vue
@@ -103,6 +103,9 @@ export default {
   watch: {
     offset () {
       this.fetchData()
+    },
+    "$store.state.moderation.lastUpdate": function () {
+      this.fetchData(this.url)
     }
   }
 }
diff --git a/front/src/components/auth/Settings.vue b/front/src/components/auth/Settings.vue
index 7081e0298df9536d4c9ef457acedc285d40d816d..73515a1ba3a901c049ea0cf7d3205fc17d3f281a 100644
--- a/front/src/components/auth/Settings.vue
+++ b/front/src/components/auth/Settings.vue
@@ -29,8 +29,8 @@
           </button>
         </form>
       </section>
-      <div class="ui hidden divider"></div>
       <section class="ui small text container">
+        <div class="ui divider"></div>
         <h2 class="ui header">
           <translate :translate-context="'Content/Settings/Title'">Avatar</translate>
         </h2>
@@ -62,8 +62,9 @@
           </div>
         </div>
       </section>
-      <div class="ui hidden divider"></div>
+
       <section class="ui small text container">
+        <div class="ui divider"></div>
         <h2 class="ui header">
           <translate :translate-context="'Content/Settings/Title/Verb'">Change my password</translate>
         </h2>
@@ -107,6 +108,53 @@
         <div class="ui hidden divider" />
         <subsonic-token-form />
       </section>
+
+      <section class="ui small text container" id="content-filters">
+        <div class="ui divider"></div>
+        <h2 class="ui header">
+          <i class="eye slash outline icon"></i>
+          <div class="content">
+            <translate>Content filters</translate>
+          </div>
+        </h2>
+        <p><translate>Content filters help you hide content you don't want to see on the service.</translate></p>
+
+        <button
+          @click="$store.dispatch('moderation/fetchContentFilters')"
+          class="ui basic icon button">
+          <i class="refresh icon"></i>&nbsp;
+          <translate :translate-context="'Content/*/Button.Label'">Refresh</translate>
+        </button>
+        <h3 class="ui header">
+          <translate>Hidden artists</translate>
+        </h3>
+        <table class="ui compact very basic fixed single line unstackable table">
+          <thead>
+            <tr>
+              <th><translate :translate-context="'Content/*/Table.Label'">Name</translate></th>
+              <th><translate :translate-context="'Content/*/Table.Label'">Creation date</translate></th>
+              <th></th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr v-for="filter in $store.getters['moderation/artistFilters']()" :key='filter.uuid'>
+              <td>
+                <router-link :to="{name: 'library.artists.detail', params: {id: filter.target.id }}">
+                  {{ filter.target.name }}
+                </router-link>
+              </td>
+              <td>
+                <human-date :date="filter.creation_date"></human-date>
+              </td>
+              <td>
+                <button @click="$store.dispatch('moderation/deleteContentFilter', filter.uuid)" class="ui basic tiny button">
+                  <translate :translate-context="'Content/*/Button.Label'">Delete</translate>
+                </button>
+              </td>
+            </tr>
+          </tbody>
+        </table>
+      </section>
     </div>
   </main>
 </template>
diff --git a/front/src/components/library/Albums.vue b/front/src/components/library/Albums.vue
index 5934bbe3bf5e10a11f0f29a5f5726813e36d86a0..f35d5236053078fe669ac6e94f847d3835c2f530 100644
--- a/front/src/components/library/Albums.vue
+++ b/front/src/components/library/Albums.vue
@@ -176,6 +176,9 @@ export default {
     query() {
       this.updateQueryString()
       this.fetchData()
+    },
+    "$store.state.moderation.lastUpdate": function () {
+      this.fetchData()
     }
   }
 }
diff --git a/front/src/components/library/Artist.vue b/front/src/components/library/Artist.vue
index c5f6acfb71a601a07f2cfab86be5fee474f5f35d..a5c0a0f45e575aecedfa080a599d6aa338d9969a 100644
--- a/front/src/components/library/Artist.vue
+++ b/front/src/components/library/Artist.vue
@@ -23,7 +23,7 @@
           </h2>
           <div class="ui hidden divider"></div>
           <radio-button type="artist" :object-id="artist.id"></radio-button>
-          <play-button :is-playable="isPlayable" class="orange" :artist="artist.id">
+          <play-button :is-playable="isPlayable" class="orange" :artist="artist">
             <translate :translate-context="'Content/Artist/Button.Label/Verb'">Play all albums</translate>
           </play-button>
 
@@ -37,6 +37,20 @@
           </a>
         </div>
       </section>
+      <div class="ui small text container" v-if="contentFilter">
+        <div class="ui hidden divider"></div>
+        <div class="ui message">
+          <p>
+            <translate>You are currently hiding content related to this artist.</translate>
+          </p>
+          <router-link class="right floated" :to="{name: 'settings'}">
+            <translate :translate-context="'Content/Moderation/Link'">Review my filters</translate>
+          </router-link>
+          <button @click="$store.dispatch('moderation/deleteContentFilter', contentFilter.uuid)" class="ui basic tiny button">
+            <translate :translate-context="'Content/Moderation/Button.Label'">Remove filter</translate>
+          </button>
+        </div>
+      </div>
       <section v-if="isLoadingAlbums" class="ui vertical stripe segment">
         <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
       </section>
@@ -105,7 +119,7 @@ export default {
       var self = this
       this.isLoading = true
       logger.default.debug('Fetching artist "' + this.id + '"')
-      axios.get("tracks/", { params: { artist: this.id } }).then(response => {
+      axios.get("tracks/", { params: { artist: this.id, hidden: '' } }).then(response => {
         self.tracks = response.data.results
         self.totalTracks = response.data.count
       })
@@ -115,7 +129,7 @@ export default {
         self.isLoadingAlbums = true
         axios
           .get("albums/", {
-            params: { artist: self.id, ordering: "-release_date" }
+            params: { artist: self.id, ordering: "-release_date", hidden: '' }
           })
           .then(response => {
             self.totalAlbums = response.data.count
@@ -180,6 +194,12 @@ export default {
         this.$store.getters["instance/absoluteUrl"](this.cover.original) +
         ")"
       )
+    },
+    contentFilter () {
+      let self = this
+      return this.$store.getters['moderation/artistFilters']().filter((e) => {
+        return e.target.id === this.artist.id
+      })[0]
     }
   },
   watch: {
diff --git a/front/src/components/library/Artists.vue b/front/src/components/library/Artists.vue
index 4fc30977e9edad811b90f92d7648505547ee8d37..64e5ab6c944e45cde389b081a2fa3102470a3ed0 100644
--- a/front/src/components/library/Artists.vue
+++ b/front/src/components/library/Artists.vue
@@ -173,6 +173,9 @@ export default {
     query() {
       this.updateQueryString()
       this.fetchData()
+    },
+    "$store.state.moderation.lastUpdate": function () {
+      this.fetchData()
     }
   }
 }
diff --git a/front/src/components/moderation/FilterModal.vue b/front/src/components/moderation/FilterModal.vue
new file mode 100644
index 0000000000000000000000000000000000000000..39a876546686935b31688db2e9a5083afe1c8c50
--- /dev/null
+++ b/front/src/components/moderation/FilterModal.vue
@@ -0,0 +1,109 @@
+<template>
+  <modal @update:show="update" :show="$store.state.moderation.showFilterModal">
+    <div class="header">
+      <translate
+        v-if="type === 'artist'"
+        key="1"
+        :translate-context="'Popup/Moderation/Title/Verb'"
+        :translate-params="{name: target.name}">Do you want to hide content from artist "%{ name }"?</translate>
+    </div>
+    <div class="scrolling content">
+      <div class="description">
+
+        <div v-if="errors.length > 0" class="ui negative message">
+          <div class="header"><translate :translate-context="'Popup/Moderation/Error message'">Error while creating filter</translate></div>
+          <ul class="list">
+            <li v-for="error in errors">{{ error }}</li>
+          </ul>
+        </div>
+        <template v-if="type === 'artist'">
+          <p>
+            <translate :translate-context="'Popup/Moderation/Paragraph'">
+              You will not see tracks, albums and user activity linked to this artist anymore:
+            </translate>
+          </p>
+          <ul>
+            <li><translate :translate-context="'Popup/Moderation/List item'">In other users favorites and listening history</translate></li>
+            <li><translate :translate-context="'Popup/Moderation/List item'">In "Recently added" widget</translate></li>
+            <li><translate :translate-context="'Popup/Moderation/List item'">In artists and album listings</translate></li>
+            <li><translate :translate-context="'Popup/Moderation/List item'">In radio suggestions</translate></li>
+          </ul>
+          <p>
+            <translate :translate-context="'Popup/Moderation/Paragraph'">
+              You can manage and update your filters anytime from your account settings.
+            </translate>
+          </p>
+        </template>
+      </div>
+    </div>
+    <div class="actions">
+      <div class="ui cancel button"><translate :translate-context="'Popup/*/Button.Label'">Cancel</translate></div>
+      <div :class="['ui', 'green', {loading: isLoading}, 'button']" @click="hide"><translate :translate-context="'Popup/*/Button.Label'">Hide content</translate></div>
+    </div>
+  </modal>
+</template>
+
+<script>
+import _ from '@/lodash'
+import axios from 'axios'
+import {mapState} from 'vuex'
+
+import logger from '@/logging'
+import Modal from '@/components/semantic/Modal'
+
+export default {
+  components: {
+    Modal,
+  },
+  data () {
+    return {
+      formKey: String(new Date()),
+      errors: [],
+      isLoading: false
+    }
+  },
+  computed: {
+    ...mapState({
+      type: state => state.moderation.filterModalTarget.type,
+      target: state => state.moderation.filterModalTarget.target,
+    })
+  },
+  methods: {
+    update (v) {
+      this.$store.commit('moderation/showFilterModal', v)
+      this.errors = []
+    },
+    hide () {
+      let self = this
+      self.isLoading = true
+      let payload = {
+        target: {
+          type: this.type,
+          id: this.target.id,
+        }
+      }
+      return axios.post('moderation/content-filters/', payload).then(response => {
+        logger.default.info('Successfully added track to playlist')
+        self.update(false)
+        self.$store.commit('moderation/lastUpdate', new Date())
+        self.isLoading = false
+        let msg = this.$pgettext('*/Moderation/Message', 'Content filter successfully added')
+        self.$store.commit('moderation/contentFilter', response.data)
+        self.$store.commit('ui/addMessage', {
+          content: msg,
+          date: new Date()
+        })
+      }, error => {
+        console.log('error', error)
+        logger.default.error(`Error while hiding ${self.type} ${self.target.id}`)
+        self.errors = error.backendErrors
+        self.isLoading = false
+      })
+    }
+  }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+</style>
diff --git a/front/src/components/playlists/Widget.vue b/front/src/components/playlists/Widget.vue
index 7329c502ec27f0f1f55cf41abea6636c9cf53ea7..b018f9731e97b307df3ded23a5d1660c6eba9ea9 100644
--- a/front/src/components/playlists/Widget.vue
+++ b/front/src/components/playlists/Widget.vue
@@ -6,7 +6,6 @@
     <button :disabled="!previousPage" @click="fetchData(previousPage)" :class="['ui', {disabled: !previousPage}, 'circular', 'icon', 'basic', 'button']"><i :class="['ui', 'angle up', 'icon']"></i></button>
     <button :disabled="!nextPage" @click="fetchData(nextPage)" :class="['ui', {disabled: !nextPage}, 'circular', 'icon', 'basic', 'button']"><i :class="['ui', 'angle down', 'icon']"></i></button>
     <button @click="fetchData(url)" :class="['ui', 'circular', 'icon', 'basic', 'button']"><i :class="['ui', 'refresh', 'icon']"></i></button>
-
     <div v-if="isLoading" class="ui inverted active dimmer">
       <div class="ui loader"></div>
     </div>
@@ -71,6 +70,9 @@ export default {
   watch: {
     offset () {
       this.fetchData()
+    },
+    "$store.state.moderation.lastUpdate": function () {
+      this.fetchData(this.url)
     }
   }
 }
diff --git a/front/src/lodash.js b/front/src/lodash.js
index 91e1a0eac30bdf5104a18913578bafcb35298fa6..afc53da2686668bfe4f28fed3f5a657f45786885 100644
--- a/front/src/lodash.js
+++ b/front/src/lodash.js
@@ -10,4 +10,5 @@ export default {
   sortBy: require('lodash/sortBy'),
   throttle: require('lodash/throttle'),
   uniq: require('lodash/uniq'),
+  remove: require('lodash/remove'),
 }
diff --git a/front/src/store/auth.js b/front/src/store/auth.js
index 90cd27e9e094b62534ec1ed94c325e4a4ae8ae7e..8893bcb495d1645584ad04da06d72091b6de2e6a 100644
--- a/front/src/store/auth.js
+++ b/front/src/store/auth.js
@@ -125,6 +125,7 @@ export default {
           })
           dispatch('ui/fetchUnreadNotifications', null, { root: true })
           dispatch('favorites/fetch', null, { root: true })
+          dispatch('moderation/fetchContentFilters', null, { root: true })
           dispatch('playlists/fetchOwn', null, { root: true })
         }, (response) => {
           logger.default.info('Error while fetching user profile')
diff --git a/front/src/store/index.js b/front/src/store/index.js
index 2454dd20a1497333d5a7b3416859952fdd323d32..e46aea86de226a678eaa27c0666249a44a526941 100644
--- a/front/src/store/index.js
+++ b/front/src/store/index.js
@@ -5,6 +5,7 @@ import createPersistedState from 'vuex-persistedstate'
 import favorites from './favorites'
 import auth from './auth'
 import instance from './instance'
+import moderation from './moderation'
 import queue from './queue'
 import radios from './radios'
 import player from './player'
@@ -19,6 +20,7 @@ export default new Vuex.Store({
     auth,
     favorites,
     instance,
+    moderation,
     queue,
     radios,
     playlists,
diff --git a/front/src/store/instance.js b/front/src/store/instance.js
index d0bff7dcb217007ff662838f5f96c2fb26b009aa..6af36e8090e3ba276c7fabaf870ebd251f086a15 100644
--- a/front/src/store/instance.js
+++ b/front/src/store/instance.js
@@ -104,6 +104,7 @@ export default {
       let modules = [
         'auth',
         'favorites',
+        'moderation',
         'player',
         'playlists',
         'queue',
diff --git a/front/src/store/moderation.js b/front/src/store/moderation.js
new file mode 100644
index 0000000000000000000000000000000000000000..153f3cd5959d307d0edbf7760a4ff34d6a8eced3
--- /dev/null
+++ b/front/src/store/moderation.js
@@ -0,0 +1,93 @@
+import axios from 'axios'
+import logger from '@/logging'
+import _ from '@/lodash'
+
+export default {
+  namespaced: true,
+  state: {
+    filters: [],
+    showFilterModal: false,
+    lastUpdate: new Date(),
+    filterModalTarget: {
+      type: null,
+      target: null,
+    }
+  },
+  mutations: {
+    filterModalTarget (state, value) {
+      state.filterModalTarget = value
+    },
+    empty (state) {
+      state.filters = []
+    },
+    lastUpdate (state, value) {
+      state.lastUpdate = value
+    },
+    contentFilter (state, value) {
+      state.filters.push(value)
+    },
+    showFilterModal (state, value) {
+      state.showFilterModal = value
+      if (!value) {
+        state.filterModalTarget = {
+          type: null,
+          target: null,
+        }
+      }
+    },
+    reset (state) {
+      state.filters = []
+      state.filterModalTarget = null
+      state.showFilterModal = false
+    },
+    deleteContentFilter (state, uuid) {
+      state.filters = state.filters.filter((e) => {
+        return e.uuid != uuid
+      })
+    }
+  },
+  getters: {
+    artistFilters: (state) => () => {
+      let f = state.filters.filter((f) => {
+        return f.target.type === 'artist'
+      })
+      let p = _.sortBy(f, [(e) => { return e.creation_date }])
+      p.reverse()
+      return p
+    },
+  },
+  actions: {
+    hide ({commit}, payload) {
+      commit('filterModalTarget', payload)
+      commit('showFilterModal', true)
+    },
+    fetchContentFilters ({dispatch, state, commit, rootState}, url) {
+      let params = {}
+      let promise
+      if (url) {
+        promise = axios.get(url)
+      } else {
+          commit('empty')
+          params = {
+            page_size: 100,
+            ordering: '-creation_date'
+          }
+          promise = axios.get('moderation/content-filters/', {params: params})
+      }
+      return promise.then((response) => {
+        logger.default.info('Fetched a batch of ' + response.data.results.length + ' filters')
+        if (response.data.next) {
+          dispatch('fetchContentFilters', response.data.next)
+        }
+        response.data.results.forEach(result => {
+          commit('contentFilter', result)
+        })
+      })
+    },
+    deleteContentFilter ({commit}, uuid) {
+      return axios.delete(`moderation/content-filters/${ uuid }/`).then((response) => {
+        commit('deleteContentFilter', uuid)
+      })
+    }
+  }
+}
diff --git a/front/src/style/_main.scss b/front/src/style/_main.scss
index 311e0e9dcb5a114ce3133bdbba81993a5c34f110..152881e303447df70732b339a92a4e499b1bb0d3 100644
--- a/front/src/style/_main.scss
+++ b/front/src/style/_main.scss
@@ -271,3 +271,8 @@ canvas.color-thief {
 .ui.list .list.icon {
   padding-left: 0;
 }
+
+
+.ui.dropdown .item[disabled] {
+  display: none;
+}