mirror of
https://code.eliotberriot.com/funkwhale/funkwhale.git
synced 2025-10-06 03:09:55 +02:00
See #170: admin UI for channels, reporting channels
This commit is contained in:
parent
ae52969efe
commit
102c90d499
32 changed files with 1106 additions and 77 deletions
|
@ -69,6 +69,15 @@ class Channel(models.Model):
|
|||
|
||||
objects = ChannelQuerySet.as_manager()
|
||||
|
||||
@property
|
||||
def fid(self):
|
||||
if not self.is_external_rss:
|
||||
return self.actor.fid
|
||||
|
||||
@property
|
||||
def is_external_rss(self):
|
||||
return self.actor.preferred_username.startswith("rssfeed-")
|
||||
|
||||
def get_absolute_url(self):
|
||||
suffix = self.uuid
|
||||
if self.actor.is_local:
|
||||
|
@ -78,9 +87,7 @@ class Channel(models.Model):
|
|||
return federation_utils.full_url("/channels/{}".format(suffix))
|
||||
|
||||
def get_rss_url(self):
|
||||
if not self.artist.is_local or self.actor.preferred_username.startswith(
|
||||
"rssfeed-"
|
||||
):
|
||||
if not self.artist.is_local or self.is_external_rss:
|
||||
return self.rss_url
|
||||
|
||||
return federation_utils.full_url(
|
||||
|
@ -90,10 +97,6 @@ class Channel(models.Model):
|
|||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def fid(self):
|
||||
return self.actor.fid
|
||||
|
||||
|
||||
def generate_actor(username, **kwargs):
|
||||
actor_data = user_models.get_actor_data(username, **kwargs)
|
||||
|
|
|
@ -145,6 +145,7 @@ class Domain(models.Model):
|
|||
actors=models.Count("actors", distinct=True),
|
||||
outbox_activities=models.Count("actors__outbox_activities", distinct=True),
|
||||
libraries=models.Count("actors__libraries", distinct=True),
|
||||
channels=models.Count("actors__owned_channels", distinct=True),
|
||||
received_library_follows=models.Count(
|
||||
"actors__libraries__received_follows", distinct=True
|
||||
),
|
||||
|
@ -283,6 +284,7 @@ class Actor(models.Model):
|
|||
data = Actor.objects.filter(pk=self.pk).aggregate(
|
||||
outbox_activities=models.Count("outbox_activities", distinct=True),
|
||||
libraries=models.Count("libraries", distinct=True),
|
||||
channels=models.Count("owned_channels", distinct=True),
|
||||
received_library_follows=models.Count(
|
||||
"libraries__received_follows", distinct=True
|
||||
),
|
||||
|
|
|
@ -482,6 +482,8 @@ def inbox_flag(payload, context):
|
|||
@outbox.register({"type": "Flag"})
|
||||
def outbox_flag(context):
|
||||
report = context["report"]
|
||||
if not report.target or not report.target.fid:
|
||||
return
|
||||
actor = actors.get_service_actor()
|
||||
serializer = serializers.FlagSerializer(report)
|
||||
yield {
|
||||
|
|
|
@ -266,5 +266,11 @@ def get_object_by_fid(fid, local=None):
|
|||
|
||||
if not result:
|
||||
raise ObjectDoesNotExist()
|
||||
model = apps.get_model(*result["__type"].split("."))
|
||||
instance = model.objects.get(fid=fid)
|
||||
if model._meta.label == "federation.Actor":
|
||||
channel = instance.get_channel()
|
||||
if channel:
|
||||
return channel
|
||||
|
||||
return apps.get_model(*result["__type"].split(".")).objects.get(fid=fid)
|
||||
return instance
|
||||
|
|
|
@ -8,6 +8,7 @@ from funkwhale_api.common import fields
|
|||
from funkwhale_api.common import filters as common_filters
|
||||
from funkwhale_api.common import search
|
||||
|
||||
from funkwhale_api.audio import models as audio_models
|
||||
from funkwhale_api.federation import models as federation_models
|
||||
from funkwhale_api.federation import utils as federation_utils
|
||||
from funkwhale_api.moderation import models as moderation_models
|
||||
|
@ -34,6 +35,34 @@ def get_actor_filter(actor_field):
|
|||
return {"field": ActorField(), "handler": handler}
|
||||
|
||||
|
||||
class ManageChannelFilterSet(filters.FilterSet):
|
||||
q = fields.SmartSearchFilter(
|
||||
config=search.SearchConfig(
|
||||
search_fields={
|
||||
"name": {"to": "artist__name"},
|
||||
"username": {"to": "artist__name"},
|
||||
"fid": {"to": "artist__fid"},
|
||||
"rss": {"to": "rss_url"},
|
||||
},
|
||||
filter_fields={
|
||||
"uuid": {"to": "uuid"},
|
||||
"category": {"to": "artist__content_category"},
|
||||
"domain": {
|
||||
"handler": lambda v: federation_utils.get_domain_query_from_url(
|
||||
v, url_field="attributed_to__fid"
|
||||
)
|
||||
},
|
||||
"tag": {"to": "artist__tagged_items__tag__name", "distinct": True},
|
||||
"account": get_actor_filter("attributed_to"),
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = audio_models.Channel
|
||||
fields = ["q"]
|
||||
|
||||
|
||||
class ManageArtistFilterSet(filters.FilterSet):
|
||||
q = fields.SmartSearchFilter(
|
||||
config=search.SearchConfig(
|
||||
|
@ -52,6 +81,7 @@ class ManageArtistFilterSet(filters.FilterSet):
|
|||
"field": forms.IntegerField(),
|
||||
"distinct": True,
|
||||
},
|
||||
"category": {"to": "content_category"},
|
||||
"tag": {"to": "tagged_items__tag__name", "distinct": True},
|
||||
},
|
||||
)
|
||||
|
@ -59,7 +89,7 @@ class ManageArtistFilterSet(filters.FilterSet):
|
|||
|
||||
class Meta:
|
||||
model = music_models.Artist
|
||||
fields = ["q", "name", "mbid", "fid"]
|
||||
fields = ["q", "name", "mbid", "fid", "content_category"]
|
||||
|
||||
|
||||
class ManageAlbumFilterSet(filters.FilterSet):
|
||||
|
|
|
@ -3,6 +3,7 @@ from django.db import transaction
|
|||
|
||||
from rest_framework import serializers
|
||||
|
||||
from funkwhale_api.audio import models as audio_models
|
||||
from funkwhale_api.common import fields as common_fields
|
||||
from funkwhale_api.common import serializers as common_serializers
|
||||
from funkwhale_api.common import utils as common_utils
|
||||
|
@ -386,26 +387,39 @@ class ManageNestedAlbumSerializer(ManageBaseAlbumSerializer):
|
|||
class ManageArtistSerializer(
|
||||
music_serializers.OptionalDescriptionMixin, ManageBaseArtistSerializer
|
||||
):
|
||||
albums = ManageNestedAlbumSerializer(many=True)
|
||||
tracks = ManageNestedTrackSerializer(many=True)
|
||||
attributed_to = ManageBaseActorSerializer()
|
||||
tags = serializers.SerializerMethodField()
|
||||
tracks_count = serializers.SerializerMethodField()
|
||||
albums_count = serializers.SerializerMethodField()
|
||||
channel = serializers.SerializerMethodField()
|
||||
cover = music_serializers.cover_field
|
||||
|
||||
class Meta:
|
||||
model = music_models.Artist
|
||||
fields = ManageBaseArtistSerializer.Meta.fields + [
|
||||
"albums",
|
||||
"tracks",
|
||||
"tracks_count",
|
||||
"albums_count",
|
||||
"attributed_to",
|
||||
"tags",
|
||||
"cover",
|
||||
"channel",
|
||||
"content_category",
|
||||
]
|
||||
|
||||
def get_tracks_count(self, obj):
|
||||
return getattr(obj, "_tracks_count", None)
|
||||
|
||||
def get_albums_count(self, obj):
|
||||
return getattr(obj, "_albums_count", None)
|
||||
|
||||
def get_tags(self, obj):
|
||||
tagged_items = getattr(obj, "_prefetched_tagged_items", [])
|
||||
return [ti.tag.name for ti in tagged_items]
|
||||
|
||||
def get_channel(self, obj):
|
||||
if "channel" in obj._state.fields_cache and obj.get_channel():
|
||||
return str(obj.channel.uuid)
|
||||
|
||||
|
||||
class ManageNestedArtistSerializer(ManageBaseArtistSerializer):
|
||||
pass
|
||||
|
@ -743,3 +757,23 @@ class ManageUserRequestSerializer(serializers.ModelSerializer):
|
|||
def get_notes(self, o):
|
||||
notes = getattr(o, "_prefetched_notes", [])
|
||||
return ManageBaseNoteSerializer(notes, many=True).data
|
||||
|
||||
|
||||
class ManageChannelSerializer(serializers.ModelSerializer):
|
||||
attributed_to = ManageBaseActorSerializer()
|
||||
actor = ManageBaseActorSerializer()
|
||||
artist = ManageArtistSerializer()
|
||||
|
||||
class Meta:
|
||||
model = audio_models.Channel
|
||||
fields = [
|
||||
"id",
|
||||
"uuid",
|
||||
"creation_date",
|
||||
"artist",
|
||||
"attributed_to",
|
||||
"actor",
|
||||
"rss_url",
|
||||
"metadata",
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
|
|
@ -27,6 +27,7 @@ users_router.register(r"invitations", views.ManageInvitationViewSet, "invitation
|
|||
|
||||
other_router = routers.OptionalSlashRouter()
|
||||
other_router.register(r"accounts", views.ManageActorViewSet, "accounts")
|
||||
other_router.register(r"channels", views.ManageChannelViewSet, "channels")
|
||||
other_router.register(r"tags", views.ManageTagViewSet, "tags")
|
||||
|
||||
urlpatterns = [
|
||||
|
|
|
@ -6,12 +6,15 @@ from django.db.models import Count, Prefetch, Q, Sum, OuterRef, Subquery
|
|||
from django.db.models.functions import Coalesce, Length
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from funkwhale_api.audio import models as audio_models
|
||||
from funkwhale_api.common.mixins import MultipleLookupDetailMixin
|
||||
from funkwhale_api.common import models as common_models
|
||||
from funkwhale_api.common import preferences, decorators
|
||||
from funkwhale_api.common import utils as common_utils
|
||||
from funkwhale_api.favorites import models as favorites_models
|
||||
from funkwhale_api.federation import models as federation_models
|
||||
from funkwhale_api.federation import tasks as federation_tasks
|
||||
from funkwhale_api.federation import utils as federation_utils
|
||||
from funkwhale_api.history import models as history_models
|
||||
from funkwhale_api.music import models as music_models
|
||||
from funkwhale_api.music import views as music_views
|
||||
|
@ -25,37 +28,39 @@ from funkwhale_api.users import models as users_models
|
|||
from . import filters, serializers
|
||||
|
||||
|
||||
def get_stats(tracks, target):
|
||||
data = {}
|
||||
def get_stats(tracks, target, ignore_fields=[]):
|
||||
tracks = list(tracks.values_list("pk", flat=True))
|
||||
uploads = music_models.Upload.objects.filter(track__in=tracks)
|
||||
data["listenings"] = history_models.Listening.objects.filter(
|
||||
track__in=tracks
|
||||
).count()
|
||||
data["mutations"] = common_models.Mutation.objects.get_for_target(target).count()
|
||||
data["playlists"] = (
|
||||
playlists_models.PlaylistTrack.objects.filter(track__in=tracks)
|
||||
.values_list("playlist", flat=True)
|
||||
.distinct()
|
||||
.count()
|
||||
)
|
||||
data["track_favorites"] = favorites_models.TrackFavorite.objects.filter(
|
||||
track__in=tracks
|
||||
).count()
|
||||
data["libraries"] = (
|
||||
uploads.filter(library__channel=None)
|
||||
.values_list("library", flat=True)
|
||||
.distinct()
|
||||
.count()
|
||||
)
|
||||
data["channels"] = (
|
||||
uploads.exclude(library__channel=None)
|
||||
.values_list("library", flat=True)
|
||||
.distinct()
|
||||
.count()
|
||||
)
|
||||
data["uploads"] = uploads.count()
|
||||
data["reports"] = moderation_models.Report.objects.get_for_target(target).count()
|
||||
fields = {
|
||||
"listenings": history_models.Listening.objects.filter(track__in=tracks),
|
||||
"mutations": common_models.Mutation.objects.get_for_target(target),
|
||||
"playlists": (
|
||||
playlists_models.PlaylistTrack.objects.filter(track__in=tracks)
|
||||
.values_list("playlist", flat=True)
|
||||
.distinct()
|
||||
),
|
||||
"track_favorites": (
|
||||
favorites_models.TrackFavorite.objects.filter(track__in=tracks)
|
||||
),
|
||||
"libraries": (
|
||||
uploads.filter(library__channel=None)
|
||||
.values_list("library", flat=True)
|
||||
.distinct()
|
||||
),
|
||||
"channels": (
|
||||
uploads.exclude(library__channel=None)
|
||||
.values_list("library", flat=True)
|
||||
.distinct()
|
||||
),
|
||||
"uploads": uploads,
|
||||
"reports": moderation_models.Report.objects.get_for_target(target),
|
||||
}
|
||||
data = {}
|
||||
for key, qs in fields.items():
|
||||
if key in ignore_fields:
|
||||
continue
|
||||
data[key] = qs.count()
|
||||
|
||||
data.update(get_media_stats(uploads))
|
||||
return data
|
||||
|
||||
|
@ -78,17 +83,10 @@ class ManageArtistViewSet(
|
|||
queryset = (
|
||||
music_models.Artist.objects.all()
|
||||
.order_by("-id")
|
||||
.select_related("attributed_to", "attachment_cover",)
|
||||
.prefetch_related(
|
||||
"tracks",
|
||||
Prefetch(
|
||||
"albums",
|
||||
queryset=music_models.Album.objects.select_related(
|
||||
"attachment_cover"
|
||||
).annotate(tracks_count=Count("tracks")),
|
||||
),
|
||||
music_views.TAG_PREFETCH,
|
||||
)
|
||||
.select_related("attributed_to", "attachment_cover", "channel")
|
||||
.annotate(_tracks_count=Count("tracks"))
|
||||
.annotate(_albums_count=Count("albums"))
|
||||
.prefetch_related(music_views.TAG_PREFETCH)
|
||||
)
|
||||
serializer_class = serializers.ManageArtistSerializer
|
||||
filterset_class = filters.ManageArtistFilterSet
|
||||
|
@ -661,3 +659,64 @@ class ManageUserRequestViewSet(
|
|||
)
|
||||
else:
|
||||
serializer.save()
|
||||
|
||||
|
||||
class ManageChannelViewSet(
|
||||
MultipleLookupDetailMixin,
|
||||
mixins.ListModelMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
viewsets.GenericViewSet,
|
||||
):
|
||||
|
||||
url_lookups = [
|
||||
{
|
||||
"lookup_field": "uuid",
|
||||
"validator": serializers.serializers.UUIDField().to_internal_value,
|
||||
},
|
||||
{
|
||||
"lookup_field": "username",
|
||||
"validator": federation_utils.get_actor_data_from_username,
|
||||
"get_query": lambda v: Q(
|
||||
actor__domain=v["domain"],
|
||||
actor__preferred_username__iexact=v["username"],
|
||||
),
|
||||
},
|
||||
]
|
||||
queryset = (
|
||||
audio_models.Channel.objects.all()
|
||||
.order_by("-id")
|
||||
.select_related("attributed_to", "actor",)
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"artist",
|
||||
queryset=(
|
||||
music_models.Artist.objects.all()
|
||||
.order_by("-id")
|
||||
.select_related("attributed_to", "attachment_cover", "channel")
|
||||
.annotate(_tracks_count=Count("tracks"))
|
||||
.annotate(_albums_count=Count("albums"))
|
||||
.prefetch_related(music_views.TAG_PREFETCH)
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
serializer_class = serializers.ManageChannelSerializer
|
||||
filterset_class = filters.ManageChannelFilterSet
|
||||
required_scope = "instance:libraries"
|
||||
ordering_fields = ["creation_date", "name"]
|
||||
|
||||
@rest_decorators.action(methods=["get"], detail=True)
|
||||
def stats(self, request, *args, **kwargs):
|
||||
channel = self.get_object()
|
||||
tracks = music_models.Track.objects.filter(
|
||||
Q(artist=channel.artist) | Q(album__artist=channel.artist)
|
||||
)
|
||||
data = get_stats(tracks, channel, ignore_fields=["libraries", "channels"])
|
||||
data["follows"] = channel.actor.received_follows.count()
|
||||
return response.Response(data, status=200)
|
||||
|
||||
def get_serializer_context(self):
|
||||
context = super().get_serializer_context()
|
||||
context["description"] = self.action in ["retrieve", "create", "update"]
|
||||
return context
|
||||
|
|
|
@ -6,6 +6,7 @@ from django.core.serializers.json import DjangoJSONEncoder
|
|||
import persisting_theory
|
||||
from rest_framework import serializers
|
||||
|
||||
from funkwhale_api.audio import models as audio_models
|
||||
from funkwhale_api.common import fields as common_fields
|
||||
from funkwhale_api.common import preferences
|
||||
from funkwhale_api.federation import models as federation_models
|
||||
|
@ -61,20 +62,36 @@ class UserFilterSerializer(serializers.ModelSerializer):
|
|||
state_serializers = persisting_theory.Registry()
|
||||
|
||||
|
||||
class DescriptionStateMixin(object):
|
||||
def get_description(self, o):
|
||||
if o.description:
|
||||
return o.description.text
|
||||
|
||||
|
||||
TAGS_FIELD = serializers.ListField(source="get_tags")
|
||||
|
||||
|
||||
@state_serializers.register(name="music.Artist")
|
||||
class ArtistStateSerializer(serializers.ModelSerializer):
|
||||
class ArtistStateSerializer(DescriptionStateMixin, serializers.ModelSerializer):
|
||||
tags = TAGS_FIELD
|
||||
|
||||
class Meta:
|
||||
model = music_models.Artist
|
||||
fields = ["id", "name", "mbid", "fid", "creation_date", "uuid", "tags"]
|
||||
fields = [
|
||||
"id",
|
||||
"name",
|
||||
"mbid",
|
||||
"fid",
|
||||
"creation_date",
|
||||
"uuid",
|
||||
"tags",
|
||||
"content_category",
|
||||
"description",
|
||||
]
|
||||
|
||||
|
||||
@state_serializers.register(name="music.Album")
|
||||
class AlbumStateSerializer(serializers.ModelSerializer):
|
||||
class AlbumStateSerializer(DescriptionStateMixin, serializers.ModelSerializer):
|
||||
tags = TAGS_FIELD
|
||||
artist = ArtistStateSerializer()
|
||||
|
||||
|
@ -90,11 +107,12 @@ class AlbumStateSerializer(serializers.ModelSerializer):
|
|||
"artist",
|
||||
"release_date",
|
||||
"tags",
|
||||
"description",
|
||||
]
|
||||
|
||||
|
||||
@state_serializers.register(name="music.Track")
|
||||
class TrackStateSerializer(serializers.ModelSerializer):
|
||||
class TrackStateSerializer(DescriptionStateMixin, serializers.ModelSerializer):
|
||||
tags = TAGS_FIELD
|
||||
artist = ArtistStateSerializer()
|
||||
album = AlbumStateSerializer()
|
||||
|
@ -115,6 +133,7 @@ class TrackStateSerializer(serializers.ModelSerializer):
|
|||
"license",
|
||||
"copyright",
|
||||
"tags",
|
||||
"description",
|
||||
]
|
||||
|
||||
|
||||
|
@ -156,6 +175,36 @@ class ActorStateSerializer(serializers.ModelSerializer):
|
|||
]
|
||||
|
||||
|
||||
@state_serializers.register(name="audio.Channel")
|
||||
class ChannelStateSerializer(serializers.ModelSerializer):
|
||||
rss_url = serializers.CharField(source="get_rss_url")
|
||||
name = serializers.CharField(source="artist.name")
|
||||
full_username = serializers.CharField(source="actor.full_username")
|
||||
domain = serializers.CharField(source="actor.domain_id")
|
||||
description = serializers.SerializerMethodField()
|
||||
tags = serializers.ListField(source="artist.get_tags")
|
||||
content_category = serializers.CharField(source="artist.content_category")
|
||||
|
||||
class Meta:
|
||||
model = audio_models.Channel
|
||||
fields = [
|
||||
"uuid",
|
||||
"name",
|
||||
"rss_url",
|
||||
"metadata",
|
||||
"full_username",
|
||||
"description",
|
||||
"domain",
|
||||
"creation_date",
|
||||
"tags",
|
||||
"content_category",
|
||||
]
|
||||
|
||||
def get_description(self, o):
|
||||
if o.artist.description:
|
||||
return o.artist.description.text
|
||||
|
||||
|
||||
def get_actor_query(attr, value):
|
||||
data = federation_utils.get_actor_data_from_username(value)
|
||||
return federation_utils.get_actor_from_username_data_query(None, data)
|
||||
|
@ -163,6 +212,7 @@ def get_actor_query(attr, value):
|
|||
|
||||
def get_target_owner(target):
|
||||
mapping = {
|
||||
audio_models.Channel: lambda t: t.attributed_to,
|
||||
music_models.Artist: lambda t: t.attributed_to,
|
||||
music_models.Album: lambda t: t.attributed_to,
|
||||
music_models.Track: lambda t: t.attributed_to,
|
||||
|
@ -175,6 +225,11 @@ def get_target_owner(target):
|
|||
|
||||
|
||||
TARGET_CONFIG = {
|
||||
"channel": {
|
||||
"queryset": audio_models.Channel.objects.all(),
|
||||
"id_attr": "uuid",
|
||||
"id_field": serializers.UUIDField(),
|
||||
},
|
||||
"artist": {"queryset": music_models.Artist.objects.all()},
|
||||
"album": {"queryset": music_models.Album.objects.all()},
|
||||
"track": {"queryset": music_models.Track.objects.all()},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue