音楽で楽しみましょう!-Let's have fun with music!-
Signed-off-by: Shin'ya Minazuki <shinyoukai@laidback.moe>
This commit is contained in:
parent
7c3206bf83
commit
54c6d22102
517 changed files with 637 additions and 639 deletions
|
|
@ -1,344 +0,0 @@
|
|||
from django import http
|
||||
from django.db import transaction
|
||||
from django.db.models import Count, Prefetch, Q, Sum
|
||||
from django.utils import timezone
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_view
|
||||
from rest_framework import decorators, exceptions, mixins
|
||||
from rest_framework import permissions as rest_permissions
|
||||
from rest_framework import response, viewsets
|
||||
|
||||
from funkwhale_api.common import locales, permissions, preferences
|
||||
from funkwhale_api.common import utils as common_utils
|
||||
from funkwhale_api.common.mixins import MultipleLookupDetailMixin
|
||||
from funkwhale_api.federation import actors
|
||||
from funkwhale_api.federation import models as federation_models
|
||||
from funkwhale_api.federation import routes
|
||||
from funkwhale_api.federation import tasks as federation_tasks
|
||||
from funkwhale_api.federation import utils as federation_utils
|
||||
from funkwhale_api.music import models as music_models
|
||||
from funkwhale_api.music import views as music_views
|
||||
from funkwhale_api.users.oauth import permissions as oauth_permissions
|
||||
|
||||
from . import categories, filters, models, renderers, serializers
|
||||
|
||||
ARTIST_PREFETCH_QS = (
|
||||
music_models.Artist.objects.select_related(
|
||||
"description",
|
||||
"attachment_cover",
|
||||
)
|
||||
.prefetch_related(music_views.TAG_PREFETCH)
|
||||
.annotate(_tracks_count=Count("tracks"))
|
||||
)
|
||||
|
||||
|
||||
class ChannelsMixin:
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not preferences.get("audio__channels_enabled"):
|
||||
return http.HttpResponse(status=405)
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
metedata_choices=extend_schema(operation_id="get_channel_metadata_choices"),
|
||||
subscribe=extend_schema(operation_id="subscribe_channel"),
|
||||
unsubscribe=extend_schema(operation_id="unsubscribe_channel"),
|
||||
rss_subscribe=extend_schema(operation_id="subscribe_channel_rss"),
|
||||
)
|
||||
class ChannelViewSet(
|
||||
ChannelsMixin,
|
||||
MultipleLookupDetailMixin,
|
||||
mixins.CreateModelMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.UpdateModelMixin,
|
||||
mixins.ListModelMixin,
|
||||
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"],
|
||||
),
|
||||
},
|
||||
]
|
||||
filterset_class = filters.ChannelFilter
|
||||
serializer_class = serializers.ChannelSerializer
|
||||
queryset = (
|
||||
models.Channel.objects.all()
|
||||
.prefetch_related(
|
||||
"library",
|
||||
"attributed_to",
|
||||
"actor",
|
||||
Prefetch("artist", queryset=ARTIST_PREFETCH_QS),
|
||||
)
|
||||
.order_by("-creation_date")
|
||||
)
|
||||
permission_classes = [
|
||||
oauth_permissions.ScopePermission,
|
||||
permissions.OwnerPermission,
|
||||
]
|
||||
required_scope = "libraries"
|
||||
anonymous_policy = "setting"
|
||||
owner_checks = ["write"]
|
||||
owner_field = "attributed_to.user"
|
||||
owner_exception = exceptions.PermissionDenied
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.request.method.lower() in ["head", "get", "options"]:
|
||||
return serializers.ChannelSerializer
|
||||
elif self.action in ["update", "partial_update"]:
|
||||
return serializers.ChannelUpdateSerializer
|
||||
elif self.action == "create":
|
||||
return serializers.ChannelCreateSerializer
|
||||
return serializers.ChannelSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
if self.action == "retrieve":
|
||||
queryset = queryset.annotate(
|
||||
_downloads_count=Sum("artist__tracks__downloads_count")
|
||||
)
|
||||
return queryset
|
||||
|
||||
def perform_create(self, serializer):
|
||||
return serializer.save(attributed_to=self.request.user.actor)
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
if self.request.GET.get("output") == "opml":
|
||||
queryset = self.filter_queryset(self.get_queryset())[:500]
|
||||
opml = serializers.get_opml(
|
||||
channels=queryset,
|
||||
date=timezone.now(),
|
||||
title="Funkwhale channels OPML export",
|
||||
)
|
||||
xml_body = renderers.render_xml(renderers.dict_to_xml_tree("opml", opml))
|
||||
return http.HttpResponse(xml_body, content_type="application/xml")
|
||||
|
||||
else:
|
||||
return super().list(request, *args, **kwargs)
|
||||
|
||||
def get_object(self):
|
||||
obj = super().get_object()
|
||||
if (
|
||||
self.action == "retrieve"
|
||||
and self.request.GET.get("refresh", "").lower() == "true"
|
||||
):
|
||||
obj = music_views.refetch_obj(obj, self.get_queryset())
|
||||
return obj
|
||||
|
||||
@decorators.action(
|
||||
detail=True,
|
||||
methods=["post"],
|
||||
permission_classes=[rest_permissions.IsAuthenticated],
|
||||
serializer_class=serializers.SubscriptionSerializer,
|
||||
)
|
||||
def subscribe(self, request, *args, **kwargs):
|
||||
object = self.get_object()
|
||||
subscription = federation_models.Follow(actor=request.user.actor)
|
||||
subscription.fid = subscription.get_federation_id()
|
||||
subscription, created = SubscriptionsViewSet.queryset.get_or_create(
|
||||
target=object.actor,
|
||||
actor=request.user.actor,
|
||||
defaults={
|
||||
"approved": True,
|
||||
"fid": subscription.fid,
|
||||
"uuid": subscription.uuid,
|
||||
},
|
||||
)
|
||||
# prefetch stuff
|
||||
subscription = SubscriptionsViewSet.queryset.get(pk=subscription.pk)
|
||||
if not object.actor.is_local:
|
||||
routes.outbox.dispatch({"type": "Follow"}, context={"follow": subscription})
|
||||
|
||||
data = serializers.SubscriptionSerializer(subscription).data
|
||||
return response.Response(data, status=201)
|
||||
|
||||
@extend_schema(responses={204: None})
|
||||
@decorators.action(
|
||||
detail=True,
|
||||
methods=["post", "delete"],
|
||||
permission_classes=[rest_permissions.IsAuthenticated],
|
||||
)
|
||||
def unsubscribe(self, request, *args, **kwargs):
|
||||
object = self.get_object()
|
||||
follow_qs = request.user.actor.emitted_follows.filter(target=object.actor)
|
||||
follow = follow_qs.first()
|
||||
if follow:
|
||||
if not object.actor.is_local:
|
||||
routes.outbox.dispatch(
|
||||
{"type": "Undo", "object": {"type": "Follow"}},
|
||||
context={"follow": follow},
|
||||
)
|
||||
follow_qs.delete()
|
||||
return response.Response(status=204)
|
||||
|
||||
@decorators.action(
|
||||
detail=True,
|
||||
methods=["get"],
|
||||
content_negotiation_class=renderers.PodcastRSSContentNegociation,
|
||||
)
|
||||
def rss(self, request, *args, **kwargs):
|
||||
object = self.get_object()
|
||||
if not object.attributed_to.is_local:
|
||||
return response.Response({"detail": "Not found"}, status=404)
|
||||
|
||||
if object.attributed_to == actors.get_service_actor():
|
||||
# external feed, we redirect to the canonical one
|
||||
return http.HttpResponseRedirect(object.rss_url)
|
||||
|
||||
uploads = (
|
||||
object.library.uploads.playable_by(None)
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"track",
|
||||
queryset=music_models.Track.objects.select_related(
|
||||
"attachment_cover", "description"
|
||||
).prefetch_related(
|
||||
music_views.TAG_PREFETCH,
|
||||
),
|
||||
),
|
||||
)
|
||||
.select_related("track__attachment_cover", "track__description")
|
||||
.order_by("-creation_date")
|
||||
)[:50]
|
||||
data = serializers.rss_serialize_channel_full(channel=object, uploads=uploads)
|
||||
return response.Response(data, status=200)
|
||||
|
||||
@decorators.action(
|
||||
methods=["get"],
|
||||
detail=False,
|
||||
url_path="metadata-choices",
|
||||
url_name="metadata_choices",
|
||||
permission_classes=[],
|
||||
)
|
||||
def metedata_choices(self, request, *args, **kwargs):
|
||||
data = {
|
||||
"language": [
|
||||
{"value": code, "label": name} for code, name in locales.ISO_639_CHOICES
|
||||
],
|
||||
"itunes_category": [
|
||||
{"value": code, "label": code, "children": children}
|
||||
for code, children in categories.ITUNES_CATEGORIES.items()
|
||||
],
|
||||
}
|
||||
return response.Response(data)
|
||||
|
||||
@decorators.action(
|
||||
methods=["post"],
|
||||
detail=False,
|
||||
url_path="rss-subscribe",
|
||||
url_name="rss_subscribe",
|
||||
)
|
||||
@transaction.atomic
|
||||
def rss_subscribe(self, request, *args, **kwargs):
|
||||
serializer = serializers.RssSubscribeSerializer(data=request.data)
|
||||
if not serializer.is_valid():
|
||||
return response.Response(serializer.errors, status=400)
|
||||
channel = (
|
||||
models.Channel.objects.filter(
|
||||
rss_url=serializer.validated_data["url"],
|
||||
)
|
||||
.order_by("id")
|
||||
.first()
|
||||
)
|
||||
if not channel:
|
||||
# try to retrieve the channel via its URL and create it
|
||||
try:
|
||||
channel, uploads = serializers.get_channel_from_rss_url(
|
||||
serializer.validated_data["url"]
|
||||
)
|
||||
except serializers.FeedFetchException as e:
|
||||
return response.Response(
|
||||
{"detail": str(e)},
|
||||
status=400,
|
||||
)
|
||||
|
||||
subscription = federation_models.Follow(actor=request.user.actor)
|
||||
subscription.fid = subscription.get_federation_id()
|
||||
subscription, created = SubscriptionsViewSet.queryset.get_or_create(
|
||||
target=channel.actor,
|
||||
actor=request.user.actor,
|
||||
defaults={
|
||||
"approved": True,
|
||||
"fid": subscription.fid,
|
||||
"uuid": subscription.uuid,
|
||||
},
|
||||
)
|
||||
# prefetch stuff
|
||||
subscription = SubscriptionsViewSet.queryset.get(pk=subscription.pk)
|
||||
|
||||
return response.Response(
|
||||
serializers.SubscriptionSerializer(subscription).data, status=201
|
||||
)
|
||||
|
||||
def get_serializer_context(self):
|
||||
context = super().get_serializer_context()
|
||||
context["subscriptions_count"] = self.action in [
|
||||
"retrieve",
|
||||
"create",
|
||||
"update",
|
||||
"partial_update",
|
||||
]
|
||||
if self.request.user.is_authenticated:
|
||||
context["actor"] = self.request.user.actor
|
||||
return context
|
||||
|
||||
@transaction.atomic
|
||||
def perform_destroy(self, instance):
|
||||
instance.__class__.objects.filter(pk=instance.pk).delete()
|
||||
common_utils.on_commit(
|
||||
federation_tasks.remove_actor.delay, actor_id=instance.actor.pk
|
||||
)
|
||||
|
||||
|
||||
class SubscriptionsViewSet(
|
||||
ChannelsMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.ListModelMixin,
|
||||
viewsets.GenericViewSet,
|
||||
):
|
||||
lookup_field = "uuid"
|
||||
serializer_class = serializers.SubscriptionSerializer
|
||||
queryset = (
|
||||
federation_models.Follow.objects.exclude(target__channel__isnull=True)
|
||||
.prefetch_related(
|
||||
"target__channel__library",
|
||||
"target__channel__attributed_to",
|
||||
"actor",
|
||||
Prefetch("target__channel__artist", queryset=ARTIST_PREFETCH_QS),
|
||||
)
|
||||
.order_by("-creation_date")
|
||||
)
|
||||
permission_classes = [
|
||||
oauth_permissions.ScopePermission,
|
||||
rest_permissions.IsAuthenticated,
|
||||
]
|
||||
required_scope = "libraries"
|
||||
anonymous_policy = False
|
||||
|
||||
def get_queryset(self):
|
||||
qs = super().get_queryset()
|
||||
return qs.filter(actor=self.request.user.actor)
|
||||
|
||||
@extend_schema(
|
||||
responses=serializers.AllSubscriptionsSerializer(),
|
||||
operation_id="get_all_subscriptions",
|
||||
)
|
||||
@decorators.action(methods=["get"], detail=False)
|
||||
def all(self, request, *args, **kwargs):
|
||||
"""
|
||||
Return all the subscriptions of the current user, with only limited data
|
||||
to have a performant endpoint and avoid lots of queries just to display
|
||||
subscription status in the UI
|
||||
"""
|
||||
subscriptions = self.get_queryset().values("uuid", "target__channel__uuid")
|
||||
|
||||
payload = serializers.AllSubscriptionsSerializer(subscriptions).data
|
||||
return response.Response(payload, status=200)
|
||||
Loading…
Add table
Add a link
Reference in a new issue