diff --git a/api/funkwhale_api/favorites/serializers.py b/api/funkwhale_api/favorites/serializers.py
index 66e10a1b49275781fc5b68d785b613b6b46a2079..dd28dcd07cbaff216d9988f9641d890f069b0d50 100644
--- a/api/funkwhale_api/favorites/serializers.py
+++ b/api/funkwhale_api/favorites/serializers.py
@@ -1,6 +1,7 @@
 from rest_framework import serializers
 
 from funkwhale_api.activity import serializers as activity_serializers
+from funkwhale_api.federation import serializers as federation_serializers
 from funkwhale_api.music.serializers import TrackActivitySerializer, TrackSerializer
 from funkwhale_api.users.serializers import UserActivitySerializer, UserBasicSerializer
 
@@ -27,10 +28,17 @@ class TrackFavoriteActivitySerializer(activity_serializers.ModelSerializer):
 class UserTrackFavoriteSerializer(serializers.ModelSerializer):
     track = TrackSerializer(read_only=True)
     user = UserBasicSerializer(read_only=True)
+    actor = serializers.SerializerMethodField()
 
     class Meta:
         model = models.TrackFavorite
-        fields = ("id", "user", "track", "creation_date")
+        fields = ("id", "user", "track", "creation_date", "actor")
+        actor = serializers.SerializerMethodField()
+
+    def get_actor(self, obj):
+        actor = obj.user.actor
+        if actor:
+            return federation_serializers.APIActorSerializer(actor).data
 
 
 class UserTrackFavoriteWriteSerializer(serializers.ModelSerializer):
diff --git a/api/funkwhale_api/favorites/views.py b/api/funkwhale_api/favorites/views.py
index dce285d85c65061fe81a86ceaa1886a6efe35ee7..7d1991bc67ebbba7f22932f91301265308ff9f32 100644
--- a/api/funkwhale_api/favorites/views.py
+++ b/api/funkwhale_api/favorites/views.py
@@ -22,7 +22,7 @@ class TrackFavoriteViewSet(
 
     filterset_class = filters.TrackFavoriteFilter
     serializer_class = serializers.UserTrackFavoriteSerializer
-    queryset = models.TrackFavorite.objects.all().select_related("user")
+    queryset = models.TrackFavorite.objects.all().select_related("user__actor")
     permission_classes = [
         oauth_permissions.ScopePermission,
         permissions.OwnerPermission,
@@ -54,7 +54,7 @@ class TrackFavoriteViewSet(
         )
         tracks = Track.objects.with_playable_uploads(
             music_utils.get_actor_from_request(self.request)
-        ).select_related("artist", "album__artist")
+        ).select_related("artist", "album__artist", "attributed_to")
         queryset = queryset.prefetch_related(Prefetch("track", queryset=tracks))
         return queryset
 
diff --git a/api/funkwhale_api/history/serializers.py b/api/funkwhale_api/history/serializers.py
index 2254aee8cee370f4124c82acf59ad4c2c0d3148f..c894ec59ab093026f014b467dadf2f2f8db1af79 100644
--- a/api/funkwhale_api/history/serializers.py
+++ b/api/funkwhale_api/history/serializers.py
@@ -1,6 +1,7 @@
 from rest_framework import serializers
 
 from funkwhale_api.activity import serializers as activity_serializers
+from funkwhale_api.federation import serializers as federation_serializers
 from funkwhale_api.music.serializers import TrackActivitySerializer, TrackSerializer
 from funkwhale_api.users.serializers import UserActivitySerializer, UserBasicSerializer
 
@@ -27,16 +28,22 @@ class ListeningActivitySerializer(activity_serializers.ModelSerializer):
 class ListeningSerializer(serializers.ModelSerializer):
     track = TrackSerializer(read_only=True)
     user = UserBasicSerializer(read_only=True)
+    actor = serializers.SerializerMethodField()
 
     class Meta:
         model = models.Listening
-        fields = ("id", "user", "track", "creation_date")
+        fields = ("id", "user", "track", "creation_date", "actor")
 
     def create(self, validated_data):
         validated_data["user"] = self.context["user"]
 
         return super().create(validated_data)
 
+    def get_actor(self, obj):
+        actor = obj.user.actor
+        if actor:
+            return federation_serializers.APIActorSerializer(actor).data
+
 
 class ListeningWriteSerializer(serializers.ModelSerializer):
     class Meta:
diff --git a/api/funkwhale_api/history/views.py b/api/funkwhale_api/history/views.py
index 30219629a4b0c5a2852d0cff1f9f04605604a654..6cdbc8a80f848c074adde2dee2f04f4ff5eefec1 100644
--- a/api/funkwhale_api/history/views.py
+++ b/api/funkwhale_api/history/views.py
@@ -19,7 +19,7 @@ class ListeningViewSet(
 ):
 
     serializer_class = serializers.ListeningSerializer
-    queryset = models.Listening.objects.all().select_related("user")
+    queryset = models.Listening.objects.all().select_related("user__actor")
 
     permission_classes = [
         oauth_permissions.ScopePermission,
@@ -47,7 +47,7 @@ class ListeningViewSet(
         )
         tracks = Track.objects.with_playable_uploads(
             music_utils.get_actor_from_request(self.request)
-        ).select_related("artist", "album__artist")
+        ).select_related("artist", "album__artist", "attributed_to")
         return queryset.prefetch_related(Prefetch("track", queryset=tracks))
 
     def get_serializer_context(self):
diff --git a/api/funkwhale_api/instance/nodeinfo.py b/api/funkwhale_api/instance/nodeinfo.py
index 178a8c1ab0c5a91be4077f695942e5d363b97a8e..ecdca9e4078e3eaecc4e0031b1e866304c02741d 100644
--- a/api/funkwhale_api/instance/nodeinfo.py
+++ b/api/funkwhale_api/instance/nodeinfo.py
@@ -3,6 +3,7 @@ import memoize.djangocache
 import funkwhale_api
 from funkwhale_api.common import preferences
 from funkwhale_api.federation import actors, models as federation_models
+from funkwhale_api.moderation import models as moderation_models
 from funkwhale_api.music import utils as music_utils
 
 from . import stats
@@ -15,6 +16,9 @@ def get():
     share_stats = preferences.get("instance__nodeinfo_stats_enabled")
     allow_list_enabled = preferences.get("moderation__allow_list_enabled")
     allow_list_public = preferences.get("moderation__allow_list_public")
+    unauthenticated_report_types = preferences.get(
+        "moderation__unauthenticated_report_types"
+    )
     if allow_list_enabled and allow_list_public:
         allowed_domains = list(
             federation_models.Domain.objects.filter(allowed=True)
@@ -47,6 +51,10 @@ def get():
             },
             "supportedUploadExtensions": music_utils.SUPPORTED_EXTENSIONS,
             "allowList": {"enabled": allow_list_enabled, "domains": allowed_domains},
+            "reportTypes": [
+                {"type": t, "label": l, "anonymous": t in unauthenticated_report_types}
+                for t, l in moderation_models.REPORT_TYPES
+            ],
         },
     }
     if share_stats:
diff --git a/api/funkwhale_api/moderation/models.py b/api/funkwhale_api/moderation/models.py
index 5a4081b7b6ea4528aae1bbd564a62504d4d970b1..c2b91760d916dd62f4eb59dc4e21ac6a3f2b89cc 100644
--- a/api/funkwhale_api/moderation/models.py
+++ b/api/funkwhale_api/moderation/models.py
@@ -115,7 +115,7 @@ REPORT_TYPES = [
 class Report(federation_models.FederationMixin):
     uuid = models.UUIDField(default=uuid.uuid4, unique=True)
     creation_date = models.DateTimeField(default=timezone.now)
-    summary = models.TextField(null=True, max_length=50000)
+    summary = models.TextField(null=True, blank=True, max_length=50000)
     handled_date = models.DateTimeField(null=True)
     is_handled = models.BooleanField(default=False)
     type = models.CharField(max_length=40, choices=REPORT_TYPES)
diff --git a/api/funkwhale_api/playlists/serializers.py b/api/funkwhale_api/playlists/serializers.py
index ccdf82f4b94fcb11ce5b3d16f1aa96db47a4e7db..dc61950dde7cd2f6d5268e8400284740a2d18449 100644
--- a/api/funkwhale_api/playlists/serializers.py
+++ b/api/funkwhale_api/playlists/serializers.py
@@ -2,6 +2,7 @@ from django.db import transaction
 from rest_framework import serializers
 
 from funkwhale_api.common import preferences
+from funkwhale_api.federation import serializers as federation_serializers
 from funkwhale_api.music.models import Track
 from funkwhale_api.music.serializers import TrackSerializer
 from funkwhale_api.users.serializers import UserBasicSerializer
@@ -79,6 +80,7 @@ class PlaylistSerializer(serializers.ModelSerializer):
     album_covers = serializers.SerializerMethodField(read_only=True)
     user = UserBasicSerializer(read_only=True)
     is_playable = serializers.SerializerMethodField()
+    actor = serializers.SerializerMethodField()
 
     class Meta:
         model = models.Playlist
@@ -93,9 +95,15 @@ class PlaylistSerializer(serializers.ModelSerializer):
             "album_covers",
             "duration",
             "is_playable",
+            "actor",
         )
         read_only_fields = ["id", "modification_date", "creation_date"]
 
+    def get_actor(self, obj):
+        actor = obj.user.actor
+        if actor:
+            return federation_serializers.APIActorSerializer(actor).data
+
     def get_is_playable(self, obj):
         try:
             return bool(obj.playable_plts)
diff --git a/api/funkwhale_api/playlists/views.py b/api/funkwhale_api/playlists/views.py
index 861dc81755f6007ade9f47df442c92d110646363..6f9ea23cecac6a997decfdf701b2cfada3fec731 100644
--- a/api/funkwhale_api/playlists/views.py
+++ b/api/funkwhale_api/playlists/views.py
@@ -23,7 +23,7 @@ class PlaylistViewSet(
     serializer_class = serializers.PlaylistSerializer
     queryset = (
         models.Playlist.objects.all()
-        .select_related("user")
+        .select_related("user__actor")
         .annotate(tracks_count=Count("playlist_tracks"))
         .with_covers()
         .with_duration()
diff --git a/api/tests/favorites/test_favorites.py b/api/tests/favorites/test_favorites.py
index 190c7918439f4acd73dce5b4e9d599ad3d979673..b81006386ac86b79a3ac34f37e19ffb89c153c40 100644
--- a/api/tests/favorites/test_favorites.py
+++ b/api/tests/favorites/test_favorites.py
@@ -4,8 +4,7 @@ import pytest
 from django.urls import reverse
 
 from funkwhale_api.favorites.models import TrackFavorite
-from funkwhale_api.music import serializers as music_serializers
-from funkwhale_api.users import serializers as users_serializers
+from funkwhale_api.favorites import serializers
 
 
 def test_user_can_add_favorite(factories):
@@ -20,22 +19,15 @@ def test_user_can_add_favorite(factories):
 def test_user_can_get_his_favorites(
     api_request, factories, logged_in_api_client, client
 ):
-    r = api_request.get("/")
+    request = api_request.get("/")
     favorite = factories["favorites.TrackFavorite"](user=logged_in_api_client.user)
     factories["favorites.TrackFavorite"]()
     url = reverse("api:v1:favorites:tracks-list")
     response = logged_in_api_client.get(url, {"user": logged_in_api_client.user.pk})
     expected = [
-        {
-            "user": users_serializers.UserBasicSerializer(
-                favorite.user, context={"request": r}
-            ).data,
-            "track": music_serializers.TrackSerializer(
-                favorite.track, context={"request": r}
-            ).data,
-            "id": favorite.id,
-            "creation_date": favorite.creation_date.isoformat().replace("+00:00", "Z"),
-        }
+        serializers.UserTrackFavoriteSerializer(
+            favorite, context={"request": request}
+        ).data
     ]
     assert response.status_code == 200
     assert response.data["results"] == expected
diff --git a/api/tests/favorites/test_serializers.py b/api/tests/favorites/test_serializers.py
new file mode 100644
index 0000000000000000000000000000000000000000..16823caa34441d30f05f329f271199b689e21104
--- /dev/null
+++ b/api/tests/favorites/test_serializers.py
@@ -0,0 +1,20 @@
+from funkwhale_api.federation import serializers as federation_serializers
+from funkwhale_api.favorites import serializers
+from funkwhale_api.music import serializers as music_serializers
+from funkwhale_api.users import serializers as users_serializers
+
+
+def test_track_favorite_serializer(factories, to_api_date):
+    favorite = factories["favorites.TrackFavorite"]()
+    actor = favorite.user.create_actor()
+
+    expected = {
+        "id": favorite.pk,
+        "creation_date": to_api_date(favorite.creation_date),
+        "track": music_serializers.TrackSerializer(favorite.track).data,
+        "actor": federation_serializers.APIActorSerializer(actor).data,
+        "user": users_serializers.UserBasicSerializer(favorite.user).data,
+    }
+    serializer = serializers.UserTrackFavoriteSerializer(favorite)
+
+    assert serializer.data == expected
diff --git a/api/tests/history/test_serializers.py b/api/tests/history/test_serializers.py
new file mode 100644
index 0000000000000000000000000000000000000000..170b44d6b11fa62b650f6f4a932db26b68873b6a
--- /dev/null
+++ b/api/tests/history/test_serializers.py
@@ -0,0 +1,20 @@
+from funkwhale_api.federation import serializers as federation_serializers
+from funkwhale_api.history import serializers
+from funkwhale_api.music import serializers as music_serializers
+from funkwhale_api.users import serializers as users_serializers
+
+
+def test_listening_serializer(factories, to_api_date):
+    listening = factories["history.Listening"]()
+    actor = listening.user.create_actor()
+
+    expected = {
+        "id": listening.pk,
+        "creation_date": to_api_date(listening.creation_date),
+        "track": music_serializers.TrackSerializer(listening.track).data,
+        "actor": federation_serializers.APIActorSerializer(actor).data,
+        "user": users_serializers.UserBasicSerializer(listening.user).data,
+    }
+    serializer = serializers.ListeningSerializer(listening)
+
+    assert serializer.data == expected
diff --git a/api/tests/instance/test_nodeinfo.py b/api/tests/instance/test_nodeinfo.py
index 211dbaa54dbeb832244a0dbacade8b615d64cfa9..cdb9ad93a651388c4d8eba00e37daad3f2bc5217 100644
--- a/api/tests/instance/test_nodeinfo.py
+++ b/api/tests/instance/test_nodeinfo.py
@@ -8,6 +8,12 @@ from funkwhale_api.music import utils as music_utils
 
 def test_nodeinfo_dump(preferences, mocker):
     preferences["instance__nodeinfo_stats_enabled"] = True
+    preferences["moderation__unauthenticated_report_types"] = [
+        "takedown_request",
+        "other",
+        "other_category_that_doesnt_exist",
+    ]
+
     stats = {
         "users": {"total": 1, "active_halfyear": 12, "active_month": 13},
         "tracks": 2,
@@ -51,6 +57,29 @@ def test_nodeinfo_dump(preferences, mocker):
             },
             "supportedUploadExtensions": music_utils.SUPPORTED_EXTENSIONS,
             "allowList": {"enabled": False, "domains": None},
+            "reportTypes": [
+                {
+                    "type": "takedown_request",
+                    "label": "Takedown request",
+                    "anonymous": True,
+                },
+                {
+                    "type": "invalid_metadata",
+                    "label": "Invalid metadata",
+                    "anonymous": False,
+                },
+                {
+                    "type": "illegal_content",
+                    "label": "Illegal content",
+                    "anonymous": False,
+                },
+                {
+                    "type": "offensive_content",
+                    "label": "Offensive content",
+                    "anonymous": False,
+                },
+                {"type": "other", "label": "Other", "anonymous": True},
+            ],
         },
     }
     assert nodeinfo.get() == expected
@@ -58,6 +87,10 @@ def test_nodeinfo_dump(preferences, mocker):
 
 def test_nodeinfo_dump_stats_disabled(preferences, mocker):
     preferences["instance__nodeinfo_stats_enabled"] = False
+    preferences["moderation__unauthenticated_report_types"] = [
+        "takedown_request",
+        "other",
+    ]
 
     expected = {
         "version": "2.0",
@@ -83,6 +116,29 @@ def test_nodeinfo_dump_stats_disabled(preferences, mocker):
             },
             "supportedUploadExtensions": music_utils.SUPPORTED_EXTENSIONS,
             "allowList": {"enabled": False, "domains": None},
+            "reportTypes": [
+                {
+                    "type": "takedown_request",
+                    "label": "Takedown request",
+                    "anonymous": True,
+                },
+                {
+                    "type": "invalid_metadata",
+                    "label": "Invalid metadata",
+                    "anonymous": False,
+                },
+                {
+                    "type": "illegal_content",
+                    "label": "Illegal content",
+                    "anonymous": False,
+                },
+                {
+                    "type": "offensive_content",
+                    "label": "Offensive content",
+                    "anonymous": False,
+                },
+                {"type": "other", "label": "Other", "anonymous": True},
+            ],
         },
     }
     assert nodeinfo.get() == expected
diff --git a/api/tests/playlists/test_serializers.py b/api/tests/playlists/test_serializers.py
index 2500947292142b653e99886144c8020788ad5762..f84df4bb23fbc7662eb7ea27b644ae3970ef1298 100644
--- a/api/tests/playlists/test_serializers.py
+++ b/api/tests/playlists/test_serializers.py
@@ -1,4 +1,6 @@
+from funkwhale_api.federation import serializers as federation_serializers
 from funkwhale_api.playlists import models, serializers
+from funkwhale_api.users import serializers as users_serializers
 
 
 def test_cannot_max_500_tracks_per_playlist(factories, preferences):
@@ -124,3 +126,25 @@ def test_playlist_serializer_include_duration(factories, api_request):
 
     serializer = serializers.PlaylistSerializer(qs.get())
     assert serializer.data["duration"] == 45
+
+
+def test_playlist_serializer(factories, to_api_date):
+    playlist = factories["playlists.Playlist"]()
+    actor = playlist.user.create_actor()
+
+    expected = {
+        "id": playlist.pk,
+        "name": playlist.name,
+        "privacy_level": playlist.privacy_level,
+        "is_playable": None,
+        "creation_date": to_api_date(playlist.creation_date),
+        "modification_date": to_api_date(playlist.modification_date),
+        "actor": federation_serializers.APIActorSerializer(actor).data,
+        "user": users_serializers.UserBasicSerializer(playlist.user).data,
+        "duration": 0,
+        "tracks_count": 0,
+        "album_covers": [],
+    }
+    serializer = serializers.PlaylistSerializer(playlist)
+
+    assert serializer.data == expected
diff --git a/front/src/App.vue b/front/src/App.vue
index e401d475e05955a42eb24c0625325306b1d9e956..ad64cb0b5467523b03f987b6355a9e061ba72212 100644
--- a/front/src/App.vue
+++ b/front/src/App.vue
@@ -21,6 +21,7 @@
       ></app-footer>
       <playlist-modal v-if="$store.state.auth.authenticated"></playlist-modal>
       <filter-modal v-if="$store.state.auth.authenticated"></filter-modal>
+      <report-modal></report-modal>
       <shortcuts-modal @update:show="showShortcutsModal = $event" :show="showShortcutsModal"></shortcuts-modal>
       <GlobalEvents @keydown.h.exact="showShortcutsModal = !showShortcutsModal"/>
     </template>
@@ -41,6 +42,7 @@ import moment from  'moment'
 import locales from './locales'
 import PlaylistModal from '@/components/playlists/PlaylistModal'
 import FilterModal from '@/components/moderation/FilterModal'
+import ReportModal from '@/components/moderation/ReportModal'
 import ShortcutsModal from '@/components/ShortcutsModal'
 import SetInstanceModal from '@/components/SetInstanceModal'
 
@@ -50,6 +52,7 @@ export default {
     Sidebar,
     AppFooter,
     FilterModal,
+    ReportModal,
     PlaylistModal,
     ShortcutsModal,
     GlobalEvents,
diff --git a/front/src/components/audio/PlayButton.vue b/front/src/components/audio/PlayButton.vue
index 8e490b4f4d8cce0e68ea1fa6d1ed3fabe42903b1..cbc1da119bf286307e8d79391de0c1adf34ba865 100644
--- a/front/src/components/audio/PlayButton.vue
+++ b/front/src/components/audio/PlayButton.vue
@@ -27,9 +27,17 @@
         <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/Button.Label/Short, Verb">Start radio</translate>
         </button>
+        <div class="divider"></div>
         <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>
+        <button
+          v-for="obj in getReportableObjs({track, album, artist, playlist, account})"
+          :key="obj.target.type + obj.target.id"
+          class="item basic"
+          @click.stop.prevent="$store.dispatch('moderation/report', obj.target)">
+          <i class="share icon" /> {{ obj.label }}
+        </button>
       </div>
     </div>
   </span>
@@ -39,11 +47,15 @@
 import axios from 'axios'
 import jQuery from 'jquery'
 
+import ReportMixin from '@/components/mixins/Report'
+
 export default {
+  mixins: [ReportMixin],
   props: {
     // we can either have a single or multiple tracks to play when clicked
     tracks: {type: Array, required: false},
     track: {type: Object, required: false},
+    account: {type: Object, required: false},
     dropdownIconClasses: {type: Array, required: false, default: () => { return ['dropdown'] }},
     playIconClass: {type: String, required: false, default: 'play icon'},
     buttonClasses: {type: Array, required: false, default: () => { return ['button'] }},
@@ -79,7 +91,8 @@ export default {
         addToQueue: this.$pgettext('*/Queue/Dropdown/Button/Title', 'Add to current queue'),
         playNext: this.$pgettext('*/Queue/Dropdown/Button/Title', 'Play next'),
         startRadio: this.$pgettext('*/Queue/Dropdown/Button/Title', 'Play similar songs'),
-        replacePlay: this.$pgettext('*/Queue/Dropdown/Button/Title', 'Replace current queue')
+        replacePlay: this.$pgettext('*/Queue/Dropdown/Button/Title', 'Replace current queue'),
+        report: this.$pgettext('*/Moderation/*/Button/Label,Verb', 'Report…'),
       }
     },
     title () {
@@ -118,7 +131,7 @@ export default {
       if (this.artist) {
         return this.artist
       }
-    }
+    },
   },
   methods: {
 
diff --git a/front/src/components/audio/track/Widget.vue b/front/src/components/audio/track/Widget.vue
index ecb967ab82a1cca1db1de7304a8992e750a71956..788d279d0f893a236a3d08d336fb27a8e77fceaf 100644
--- a/front/src/components/audio/track/Widget.vue
+++ b/front/src/components/audio/track/Widget.vue
@@ -37,7 +37,12 @@
               </div>
             </div>
             <div class="one wide stretched column">
-              <play-button class="basic icon" :dropdown-only="true" :dropdown-icon-classes="['ellipsis', 'vertical', 'large', 'grey']" :track="object.track"></play-button>
+              <play-button
+                class="basic icon"
+                :account="object.actor"
+                :dropdown-only="true"
+                :dropdown-icon-classes="['ellipsis', 'vertical', 'large', 'grey']"
+                :track="object.track"></play-button>
             </div>
           </div>
         </div>
diff --git a/front/src/components/library/AlbumBase.vue b/front/src/components/library/AlbumBase.vue
index 9f050e286d0be68f3dacbe0e5d6bc95d8b4cd294..e42f3e826be6cec0707577b85203af0d2e6c71c6 100644
--- a/front/src/components/library/AlbumBase.vue
+++ b/front/src/components/library/AlbumBase.vue
@@ -74,6 +74,15 @@
                     <translate translate-context="Content/*/Button.Label/Verb">Edit</translate>
                   </router-link>
                   <div class="divider"></div>
+                  <div
+                    role="button"
+                    class="basic item"
+                    v-for="obj in getReportableObjs({album: object})"
+                    :key="obj.target.type + obj.target.id"
+                    @click.stop.prevent="$store.dispatch('moderation/report', obj.target)">
+                    <i class="share icon" /> {{ obj.label }}
+                  </div>
+                  <div class="divider"></div>
                   <router-link class="basic item" v-if="$store.state.auth.availablePermissions['library']" :to="{name: 'manage.library.albums.detail', params: {id: object.id}}">
                     <i class="wrench icon"></i>
                     <translate translate-context="Content/Moderation/Link">Open in moderation interface</translate>
@@ -105,6 +114,7 @@ import PlayButton from "@/components/audio/PlayButton"
 import EmbedWizard from "@/components/audio/EmbedWizard"
 import Modal from '@/components/semantic/Modal'
 import TagsList from "@/components/tags/List"
+import ReportMixin from '@/components/mixins/Report'
 
 const FETCH_URL = "albums/"
 
@@ -121,6 +131,7 @@ function groupByDisc(acc, track) {
 }
 
 export default {
+  mixins: [ReportMixin],
   props: ["id"],
   components: {
     PlayButton,
diff --git a/front/src/components/library/ArtistBase.vue b/front/src/components/library/ArtistBase.vue
index 602423f70f16f413fcedfd24022d39d9affc0b67..2c5d5284aac8b1c39016f854830795a9de785e9b 100644
--- a/front/src/components/library/ArtistBase.vue
+++ b/front/src/components/library/ArtistBase.vue
@@ -84,6 +84,16 @@
                     <i class="edit icon"></i>
                     <translate translate-context="Content/*/Button.Label/Verb">Edit</translate>
                   </router-link>
+                  <div class="divider"></div>
+                  <div
+                    role="button"
+                    class="basic item"
+                    v-for="obj in getReportableObjs({artist: object})"
+                    :key="obj.target.type + obj.target.id"
+                    @click.stop.prevent="$store.dispatch('moderation/report', obj.target)">
+                    <i class="share icon" /> {{ obj.label }}
+                  </div>
+
                   <div class="divider"></div>
                   <router-link class="basic item" v-if="$store.state.auth.availablePermissions['library']" :to="{name: 'manage.library.artists.detail', params: {id: object.id}}">
                     <i class="wrench icon"></i>
@@ -125,12 +135,12 @@ import EmbedWizard from "@/components/audio/EmbedWizard"
 import Modal from '@/components/semantic/Modal'
 import RadioButton from "@/components/radios/Button"
 import TagsList from "@/components/tags/List"
+import ReportMixin from '@/components/mixins/Report'
 
 const FETCH_URL = "albums/"
 
-
-
 export default {
+  mixins: [ReportMixin],
   props: ["id"],
   components: {
     PlayButton,
diff --git a/front/src/components/library/TrackBase.vue b/front/src/components/library/TrackBase.vue
index 17a739d4290a017c7b88e881a13f5ea1d1762432..82f3aa8a7b4c548b9cc7a38ac43f3f1f0f5684e3 100644
--- a/front/src/components/library/TrackBase.vue
+++ b/front/src/components/library/TrackBase.vue
@@ -90,6 +90,15 @@
                     <translate translate-context="Content/*/Button.Label/Verb">Edit</translate>
                   </router-link>
                   <div class="divider"></div>
+                  <div
+                    role="button"
+                    class="basic item"
+                    v-for="obj in getReportableObjs({track})"
+                    :key="obj.target.type + obj.target.id"
+                    @click.stop.prevent="$store.dispatch('moderation/report', obj.target)">
+                    <i class="share icon" /> {{ obj.label }}
+                  </div>
+                  <div class="divider"></div>
                   <router-link class="basic item" v-if="$store.state.auth.availablePermissions['library']" :to="{name: 'manage.library.tracks.detail', params: {id: track.id}}">
                     <i class="wrench icon"></i>
                     <translate translate-context="Content/Moderation/Link">Open in moderation interface</translate>
@@ -124,11 +133,13 @@ import TrackPlaylistIcon from "@/components/playlists/TrackPlaylistIcon"
 import Modal from '@/components/semantic/Modal'
 import EmbedWizard from "@/components/audio/EmbedWizard"
 import TagsList from "@/components/tags/List"
+import ReportMixin from '@/components/mixins/Report'
 
 const FETCH_URL = "tracks/"
 
 export default {
   props: ["id"],
+  mixins: [ReportMixin],
   components: {
     PlayButton,
     TrackPlaylistIcon,
diff --git a/front/src/components/mixins/Report.vue b/front/src/components/mixins/Report.vue
new file mode 100644
index 0000000000000000000000000000000000000000..94e72a96c4238fc79507ccdecde64f378e68d423
--- /dev/null
+++ b/front/src/components/mixins/Report.vue
@@ -0,0 +1,75 @@
+<script>
+export default {
+  methods: {
+    getReportableObjs ({track, album, artist, playlist, account}) {
+      let reportableObjs = []
+      if (account) {
+        let accountLabel = this.$pgettext('*/Moderation/*/Verb', "Report @%{ username }…")
+        reportableObjs.push({
+          label: this.$gettextInterpolate(accountLabel, {username: account.preferred_username}),
+          target: {
+            type: 'account',
+            full_username: account.full_username,
+            label: account.full_username,
+            typeLabel: this.$pgettext("*/*/*", 'Account'),
+          }
+        })
+        if (track) {
+          album = track.album
+          artist = track.artist
+        }
+      }
+      if (track) {
+        reportableObjs.push({
+          label: this.$pgettext('*/Moderation/*/Verb', "Report this track…"),
+          target: {
+            type: 'track',
+            id: track.id,
+            label: track.title,
+            typeLabel: this.$pgettext("*/*/*", 'Track'),
+          }
+        })
+        album = track.album
+        artist = track.artist
+      }
+      if (album) {
+        reportableObjs.push({
+          label: this.$pgettext('*/Moderation/*/Verb', "Report this album…"),
+          target: {
+            type: 'album',
+            id: album.id,
+            label: album.title,
+            typeLabel: this.$pgettext("*/*/*", 'Album'),
+          }
+        })
+        if (!artist) {
+          artist = album.artist
+        }
+      }
+      if (artist) {
+        reportableObjs.push({
+          label: this.$pgettext('*/Moderation/*/Verb', "Report this artist…"),
+          target: {
+            type: 'artist',
+            id: artist.id,
+            label: artist.name,
+            typeLabel: this.$pgettext("*/*/*", 'Artist'),
+          }
+        })
+      }
+      if (this.playlist) {
+        reportableObjs.push({
+          label: this.$pgettext('*/Moderation/*/Verb', "Report this playlist…"),
+          target: {
+            type: 'playlist',
+            id: this.playlist.id,
+            label: this.playlist.name,
+            typeLabel: this.$pgettext("*/*/*", 'Playlist'),
+          }
+        })
+      }
+      return reportableObjs
+    },
+  }
+}
+</script>
diff --git a/front/src/components/moderation/ReportCategoryDropdown.vue b/front/src/components/moderation/ReportCategoryDropdown.vue
index 473fe0adbcc05c1bf9270af5fbd45400deffc669..617f89b182a293aca78e96a270b0bbd0032530e1 100644
--- a/front/src/components/moderation/ReportCategoryDropdown.vue
+++ b/front/src/components/moderation/ReportCategoryDropdown.vue
@@ -1,7 +1,8 @@
 <template>
   <div>
     <label v-if="label"><translate translate-context="*/*/*">Category</translate></label>
-    <select class="ui dropdown" :value="value" @change="$emit('input', $event.target.value)">
+    <select class="ui dropdown" :value="value" @change="$emit('input', $event.target.value)" :required="required">
+      <option v-if="empty" disabled value=''></option>
       <option :value="option.value" v-for="option in allCategories">{{ option.label }}</option>
     </select>
     <slot></slot>
@@ -13,7 +14,14 @@ import TranslationsMixin from '@/components/mixins/Translations'
 import lodash from '@/lodash'
 export default {
   mixins: [TranslationsMixin],
-  props: ['value', 'all', 'label'],
+  props: {
+    value: {},
+    all: {},
+    label: {},
+    empty: {},
+    required: {},
+    restrictTo: {default: () => { return [] }}
+  },
   computed: {
     allCategories () {
       let c = []
@@ -25,11 +33,17 @@ export default {
           },
         )
       }
+      let choices
+      if (this.restrictTo.length > 0)  {
+        choices = this.restrictTo
+      } else {
+        choices = lodash.keys(this.sharedLabels.fields.report_type.choices)
+      }
       return c.concat(
-        lodash.keys(this.sharedLabels.fields.report_type.choices).sort().map((v) => {
+        choices.sort().map((v) => {
           return {
             value: v,
-            label: this.sharedLabels.fields.report_type.choices[v]
+            label: this.sharedLabels.fields.report_type.choices[v] || v
           }
         })
       )
diff --git a/front/src/components/moderation/ReportModal.vue b/front/src/components/moderation/ReportModal.vue
new file mode 100644
index 0000000000000000000000000000000000000000..e8a30c512ef7d97a7f38fd63d22f86cf02c88e89
--- /dev/null
+++ b/front/src/components/moderation/ReportModal.vue
@@ -0,0 +1,169 @@
+<template>
+  <modal @update:show="update" :show="$store.state.moderation.showReportModal">
+    <h2 class="ui header" v-if="target">
+      <translate translate-context="Popup/Moderation/Title/Verb">Do you want to report this object?</translate>
+      <div class="ui sub header">
+        {{ target.typeLabel }} - {{ target.label }}
+      </div>
+    </h2>
+    <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 submitting report</translate></div>
+          <ul class="list">
+            <li v-for="error in errors">{{ error }}</li>
+          </ul>
+        </div>
+      </div>
+      <p>
+        <translate translate-context="*/Moderation/Popup,Paragraph">Use this form to submit a report to our moderation team.</translate>
+      </p>
+      <form v-if="canSubmit" id="report-form" class="ui form" @submit.prevent="submit">
+        <div v-if="!$store.state.auth.authenticated" class="ui inline required field">
+          <label for="report-submitter-email">
+            <translate translate-context="Content/*/*/Noun">Email</translate>
+          </label>
+          <input type="email" v-model="submitterEmail" name="report-submitter-email" id="report-submitter-email" required>
+        </div>
+        <report-category-dropdown
+          class="ui inline required field"
+          v-model="category"
+          :required="true"
+          :empty="true"
+          :restrict-to="allowedCategories"
+          :label="true"></report-category-dropdown>
+        <div class="ui field">
+          <label for="report-summary">
+            <translate translate-context="*/*/Field.Label/Noun">Message</translate>
+          </label>
+          <p>
+            <translate translate-context="*/*/Field,Help">Use this field to provide additional context to the moderator that will handle your report.</translate>
+          </p>
+          <textarea name="report-summary" id="report-summary" rows="8" v-model="summary"></textarea>
+        </div>
+      </form>
+      <div v-else-if="isLoadingReportTypes" class="ui inline active loader">
+
+      </div>
+      <div v-else class="ui warning message">
+        <div class="header">
+          <translate translate-context="Popup/Moderation/Error message">Anonymous reports are disabled, please sign-in to submit a report.</translate>
+        </div>
+      </div>
+    </div>
+    <div class="actions">
+      <div class="ui cancel button"><translate translate-context="*/*/Button.Label/Verb">Cancel</translate></div>
+      <button
+        v-if="canSubmit"
+        :class="['ui', 'green', {loading: isLoading}, 'button']"
+        type="submit" form="report-form">
+        <translate translate-context="Popup/*/Button.Label">Submit report</translate>
+      </button>
+    </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'
+import ReportCategoryDropdown from '@/components/moderation/ReportCategoryDropdown'
+
+export default {
+  components: {
+    Modal,
+    ReportCategoryDropdown,
+  },
+  data () {
+    return {
+      formKey: String(new Date()),
+      errors: [],
+      isLoading: false,
+      isLoadingReportTypes: false,
+      summary: '',
+      submitterEmail: '',
+      category: null,
+      reportTypes: [],
+    }
+  },
+  computed: {
+    ...mapState({
+      target: state => state.moderation.reportModalTarget,
+    }),
+    allowedCategories () {
+      if (this.$store.state.auth.authenticated) {
+        return []
+      }
+      return this.reportTypes.filter((t) => {
+        return t.anonymous === true
+      }).map((c) => {
+        return c.type
+      })
+
+    },
+    canSubmit () {
+      if (this.$store.state.auth.authenticated) {
+        return true
+      }
+
+      return this.allowedCategories.length > 0
+    }
+  },
+  methods: {
+    update (v) {
+      this.$store.commit('moderation/showReportModal', v)
+      this.errors = []
+    },
+    submit () {
+      let self = this
+      self.isLoading = true
+      let payload = {
+        target: this.target,
+        summary: this.summary,
+        type: this.category,
+      }
+      if (!this.$store.state.auth.authenticated) {
+        payload.submitter_email = this.submitterEmail
+      }
+      return axios.post('moderation/reports/', payload).then(response => {
+        self.update(false)
+        self.isLoading = false
+        let msg = this.$pgettext('*/Moderation/Message', 'Report successfully submitted, thank you')
+        self.$store.commit('moderation/contentFilter', response.data)
+        self.$store.commit('ui/addMessage', {
+          content: msg,
+          date: new Date()
+        })
+        self.summary = ''
+        self.category = ''
+      }, error => {
+        self.errors = error.backendErrors
+        self.isLoading = false
+      })
+    }
+  },
+  watch: {
+    '$store.state.moderation.showReportModal': function (v) {
+      if (!v || this.$store.state.auth.authenticated) {
+        return
+      }
+
+      let self = this
+      self.isLoadingReportTypes = true
+      axios.get('instance/nodeinfo/2.0/').then(response => {
+        self.isLoadingReportTypes = false
+        self.reportTypes = response.data.metadata.reportTypes || []
+      }, error => {
+        self.isLoadingReportTypes = false
+      })
+    }
+  }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+</style>
diff --git a/front/src/components/playlists/Card.vue b/front/src/components/playlists/Card.vue
index 6efc2350e00b990d0410c0e8c644340a5b7c3f4f..a8e5c149a5efb37aa8ba5c2ba76279ce444ad512 100644
--- a/front/src/components/playlists/Card.vue
+++ b/front/src/components/playlists/Card.vue
@@ -5,8 +5,18 @@
     <div class="content">
       <div class="header">
         <div class="right floated">
-          <play-button :is-playable="playlist.is_playable" :icon-only="true" class="ui inline" :button-classes="['ui', 'circular', 'large', {orange: playlist.tracks_count > 0}, 'icon', 'button', {disabled: playlist.tracks_count === 0}]" :playlist="playlist"></play-button>
-          <play-button :is-playable="playlist.is_playable" class="basic inline icon" :dropdown-only="true" :dropdown-icon-classes="['ellipsis', 'vertical', 'large', {disabled: playlist.tracks_count === 0}, 'grey']" :playlist="playlist"></play-button>
+          <play-button
+            :is-playable="playlist.is_playable"
+            :icon-only="true" class="ui inline"
+            :button-classes="['ui', 'circular', 'large', {orange: playlist.tracks_count > 0}, 'icon', 'button', {disabled: playlist.tracks_count === 0}]"
+            :playlist="playlist"></play-button>
+          <play-button
+            :is-playable="playlist.is_playable"
+            class="basic inline icon"
+            :dropdown-only="true"
+            :dropdown-icon-classes="['ellipsis', 'vertical', 'large', {disabled: playlist.tracks_count === 0}, 'grey']"
+            :account="playlist.actor"
+            :playlist="playlist"></play-button>
         </div>
         <router-link :title="playlist.name" class="discrete link" :to="{name: 'library.playlists.detail', params: {id: playlist.id }}">
           {{ playlist.name | truncate(30) }}
diff --git a/front/src/store/moderation.js b/front/src/store/moderation.js
index 153f3cd5959d307d0edbf7760a4ff34d6a8eced3..16caf60aa4a5abca53b6fd06f4f4e7ff61f37c36 100644
--- a/front/src/store/moderation.js
+++ b/front/src/store/moderation.js
@@ -7,16 +7,24 @@ export default {
   state: {
     filters: [],
     showFilterModal: false,
+    showReportModal: false,
     lastUpdate: new Date(),
     filterModalTarget: {
       type: null,
       target: null,
+    },
+    reportModalTarget: {
+      type: null,
+      target: null,
     }
   },
   mutations: {
     filterModalTarget (state, value) {
       state.filterModalTarget = value
     },
+    reportModalTarget (state, value) {
+      state.reportModalTarget = value
+    },
     empty (state) {
       state.filters = []
     },
@@ -35,10 +43,21 @@ export default {
         }
       }
     },
+    showReportModal (state, value) {
+      state.showReportModal = value
+      if (!value) {
+        state.reportModalTarget = {
+          type: null,
+          target: null,
+        }
+      }
+    },
     reset (state) {
       state.filters = []
       state.filterModalTarget = null
       state.showFilterModal = false
+      state.showReportModal = false
+      state.reportModalTarget = {}
     },
     deleteContentFilter (state, uuid) {
       state.filters = state.filters.filter((e) => {
@@ -61,6 +80,10 @@ export default {
       commit('filterModalTarget', payload)
       commit('showFilterModal', true)
     },
+    report ({commit}, payload) {
+      commit('reportModalTarget', payload)
+      commit('showReportModal', true)
+    },
     fetchContentFilters ({dispatch, state, commit, rootState}, url) {
       let params = {}
       let promise