funkwhale/api/funkwhale_api/music/views.py

556 wiersze
20 KiB
Python
Czysty Zwykły widok Historia

import logging
import urllib
from django.conf import settings
from django.db import transaction
from django.db.models import Count, Prefetch, Sum, F, Q
2018-06-10 08:55:16 +00:00
from django.db.models.functions import Length
from django.utils import timezone
2018-06-10 08:55:16 +00:00
from rest_framework import mixins
from rest_framework import settings as rest_settings
from rest_framework import views, viewsets
2019-01-11 12:33:35 +00:00
from rest_framework.decorators import action
from rest_framework.response import Response
from funkwhale_api.common import decorators as common_decorators
from funkwhale_api.common import permissions as common_permissions
from funkwhale_api.common import preferences
from funkwhale_api.common import utils as common_utils
2019-02-14 09:49:06 +00:00
from funkwhale_api.common import views as common_views
from funkwhale_api.federation.authentication import SignatureAuthentication
from funkwhale_api.federation import actors
2018-09-13 15:18:23 +00:00
from funkwhale_api.federation import api_serializers as federation_api_serializers
2019-04-18 12:37:17 +00:00
from funkwhale_api.federation import decorators as federation_decorators
2018-09-22 12:29:30 +00:00
from funkwhale_api.federation import routes
2019-07-08 13:26:14 +00:00
from funkwhale_api.tags.models import Tag
from funkwhale_api.users.oauth import permissions as oauth_permissions
2018-12-04 14:13:37 +00:00
from . import filters, licenses, models, serializers, tasks, utils
logger = logging.getLogger(__name__)
2017-12-12 21:04:39 +00:00
def get_libraries(filter_uploads):
2019-01-11 12:33:35 +00:00
def libraries(self, request, *args, **kwargs):
obj = self.get_object()
actor = utils.get_actor_from_request(request)
uploads = models.Upload.objects.all()
uploads = filter_uploads(obj, uploads)
uploads = uploads.playable_by(actor)
2019-01-11 12:33:35 +00:00
qs = models.Library.objects.filter(
pk__in=uploads.values_list("library", flat=True)
).annotate(_uploads_count=Count("uploads"))
2019-01-11 12:33:35 +00:00
qs = qs.select_related("actor")
page = self.paginate_queryset(qs)
if page is not None:
serializer = federation_api_serializers.LibrarySerializer(page, many=True)
return self.get_paginated_response(serializer.data)
2019-01-11 12:33:35 +00:00
serializer = federation_api_serializers.LibrarySerializer(qs, many=True)
return Response(serializer.data)
2019-01-11 12:33:35 +00:00
return libraries
2019-02-14 09:49:06 +00:00
class ArtistViewSet(common_views.SkipFilterForGetObject, viewsets.ReadOnlyModelViewSet):
queryset = models.Artist.objects.all()
serializer_class = serializers.ArtistWithAlbumsSerializer
permission_classes = [oauth_permissions.ScopePermission]
required_scope = "libraries"
anonymous_policy = "setting"
filterset_class = filters.ArtistFilter
2018-06-09 13:36:16 +00:00
ordering_fields = ("id", "name", "creation_date")
2019-04-18 12:37:17 +00:00
fetches = federation_decorators.fetches_route()
2019-04-17 14:11:24 +00:00
mutations = common_decorators.mutations_route(types=["update"])
def get_queryset(self):
queryset = super().get_queryset()
albums = models.Album.objects.with_tracks_count()
2018-09-22 12:29:30 +00:00
albums = albums.annotate_playable_by_actor(
utils.get_actor_from_request(self.request)
)
return queryset.prefetch_related(Prefetch("albums", queryset=albums))
2019-01-11 12:33:35 +00:00
libraries = action(methods=["get"], detail=True)(
get_libraries(
filter_uploads=lambda o, uploads: uploads.filter(
Q(track__artist=o) | Q(track__album__artist=o)
)
)
)
2019-02-14 09:49:06 +00:00
class AlbumViewSet(common_views.SkipFilterForGetObject, viewsets.ReadOnlyModelViewSet):
queryset = (
models.Album.objects.all().order_by("artist", "release_date").select_related()
2018-06-09 13:36:16 +00:00
)
serializer_class = serializers.AlbumSerializer
permission_classes = [oauth_permissions.ScopePermission]
required_scope = "libraries"
anonymous_policy = "setting"
2018-06-09 13:36:16 +00:00
ordering_fields = ("creation_date", "release_date", "title")
filterset_class = filters.AlbumFilter
2019-04-18 12:37:17 +00:00
fetches = federation_decorators.fetches_route()
2019-04-17 14:11:24 +00:00
mutations = common_decorators.mutations_route(types=["update"])
def get_queryset(self):
queryset = super().get_queryset()
tracks = (
models.Track.objects.select_related("artist")
.with_playable_uploads(utils.get_actor_from_request(self.request))
.order_for_album()
)
qs = queryset.prefetch_related(Prefetch("tracks", queryset=tracks))
return qs
2019-01-11 12:33:35 +00:00
libraries = action(methods=["get"], detail=True)(
get_libraries(filter_uploads=lambda o, uploads: uploads.filter(track__album=o))
)
class LibraryViewSet(
2018-06-09 13:36:16 +00:00
mixins.CreateModelMixin,
mixins.ListModelMixin,
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
2018-06-09 13:36:16 +00:00
viewsets.GenericViewSet,
):
lookup_field = "uuid"
queryset = (
models.Library.objects.all()
2018-06-09 13:36:16 +00:00
.order_by("-creation_date")
2018-09-22 12:29:30 +00:00
.annotate(_uploads_count=Count("uploads"))
.annotate(_size=Sum("uploads__size"))
)
serializer_class = serializers.LibraryForOwnerSerializer
permission_classes = [
oauth_permissions.ScopePermission,
common_permissions.OwnerPermission,
]
required_scope = "libraries"
anonymous_policy = "setting"
owner_field = "actor.user"
owner_checks = ["read", "write"]
2017-12-27 22:32:02 +00:00
def get_queryset(self):
qs = super().get_queryset()
return qs.filter(actor=self.request.user.actor)
2017-12-27 22:32:02 +00:00
def perform_create(self, serializer):
serializer.save(actor=self.request.user.actor)
2017-12-27 22:32:02 +00:00
2018-09-22 12:29:30 +00:00
@transaction.atomic
def perform_destroy(self, instance):
routes.outbox.dispatch(
{"type": "Delete", "object": {"type": "Library"}},
context={"library": instance},
)
instance.delete()
2019-01-11 12:33:35 +00:00
follows = action
@action(methods=["get"], detail=True)
2018-09-13 15:18:23 +00:00
@transaction.non_atomic_requests
def follows(self, request, *args, **kwargs):
library = self.get_object()
queryset = (
library.received_follows.filter(target__actor=self.request.user.actor)
.select_related("actor", "target__actor")
.order_by("-creation_date")
)
page = self.paginate_queryset(queryset)
if page is not None:
serializer = federation_api_serializers.LibraryFollowSerializer(
page, many=True
)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
2019-07-08 13:26:14 +00:00
class TrackViewSet(common_views.SkipFilterForGetObject, viewsets.ReadOnlyModelViewSet):
"""
A simple ViewSet for viewing and editing accounts.
"""
2018-06-09 13:36:16 +00:00
queryset = models.Track.objects.all().for_nested_serialization()
serializer_class = serializers.TrackSerializer
permission_classes = [oauth_permissions.ScopePermission]
required_scope = "libraries"
anonymous_policy = "setting"
filterset_class = filters.TrackFilter
2017-12-17 19:07:18 +00:00
ordering_fields = (
2018-06-09 13:36:16 +00:00
"creation_date",
"title",
"album__release_date",
"size",
"position",
"disc_number",
2018-06-09 13:36:16 +00:00
"artist__name",
2017-12-17 19:07:18 +00:00
)
2019-04-18 12:37:17 +00:00
fetches = federation_decorators.fetches_route()
mutations = common_decorators.mutations_route(types=["update"])
def get_queryset(self):
queryset = super().get_queryset()
2018-06-09 13:36:16 +00:00
filter_favorites = self.request.GET.get("favorites", None)
user = self.request.user
2018-06-09 13:36:16 +00:00
if user.is_authenticated and filter_favorites == "true":
queryset = queryset.filter(track_favorites__user=user)
queryset = queryset.with_playable_uploads(
utils.get_actor_from_request(self.request)
)
return queryset
2019-01-11 12:33:35 +00:00
libraries = action(methods=["get"], detail=True)(
get_libraries(filter_uploads=lambda o, uploads: uploads.filter(track=o))
)
def get_file_path(audio_file):
serve_path = settings.MUSIC_DIRECTORY_SERVE_PATH
prefix = settings.MUSIC_DIRECTORY_PATH
t = settings.REVERSE_PROXY_TYPE
2018-06-09 13:36:16 +00:00
if t == "nginx":
# we have to use the internal locations
try:
path = audio_file.url
except AttributeError:
# a path was given
if not serve_path or not prefix:
raise ValueError(
2018-06-09 13:36:16 +00:00
"You need to specify MUSIC_DIRECTORY_SERVE_PATH and "
"MUSIC_DIRECTORY_PATH to serve in-place imported files"
)
2018-06-09 13:36:16 +00:00
path = "/music" + audio_file.replace(prefix, "", 1)
if path.startswith("http://") or path.startswith("https://"):
return (settings.PROTECT_FILES_PATH + "/media/" + path).encode("utf-8")
2018-06-09 13:36:16 +00:00
return (settings.PROTECT_FILES_PATH + path).encode("utf-8")
if t == "apache2":
try:
path = audio_file.path
except AttributeError:
# a path was given
if not serve_path or not prefix:
raise ValueError(
2018-06-09 13:36:16 +00:00
"You need to specify MUSIC_DIRECTORY_SERVE_PATH and "
"MUSIC_DIRECTORY_PATH to serve in-place imported files"
)
path = audio_file.replace(prefix, serve_path, 1)
2018-06-09 13:36:16 +00:00
return path.encode("utf-8")
def should_transcode(upload, format, max_bitrate=None):
if not preferences.get("music__transcoding_enabled"):
return False
format_need_transcoding = True
bitrate_need_transcoding = True
if format is None:
format_need_transcoding = False
elif format not in utils.EXTENSION_TO_MIMETYPE:
# format should match supported formats
format_need_transcoding = False
elif upload.mimetype is None:
# upload should have a mimetype, otherwise we cannot transcode
format_need_transcoding = False
elif upload.mimetype == utils.EXTENSION_TO_MIMETYPE[format]:
# requested format sould be different than upload mimetype, otherwise
# there is no need to transcode
format_need_transcoding = False
if max_bitrate is None:
bitrate_need_transcoding = False
elif not upload.bitrate:
bitrate_need_transcoding = False
elif upload.bitrate <= max_bitrate:
bitrate_need_transcoding = False
return format_need_transcoding or bitrate_need_transcoding
def get_content_disposition(filename):
filename = "filename*=UTF-8''{}".format(urllib.parse.quote(filename))
return "attachment; {}".format(filename)
def handle_serve(upload, user, format=None, max_bitrate=None, proxy_media=True):
2018-09-22 12:29:30 +00:00
f = upload
# we update the accessed_date
now = timezone.now()
upload.accessed_date = now
upload.save(update_fields=["accessed_date"])
f = upload
if f.audio_file:
file_path = get_file_path(f.audio_file)
elif f.source and (
f.source.startswith("http://") or f.source.startswith("https://")
):
# we need to populate from cache
with transaction.atomic():
# why the transaction/select_for_update?
# this is because browsers may send multiple requests
# in a short time range, for partial content,
# thus resulting in multiple downloads from the remote
qs = f.__class__.objects.select_for_update()
f = qs.get(pk=f.pk)
if user.is_authenticated:
actor = user.actor
else:
actor = actors.get_service_actor()
f.download_audio_from_remote(actor=actor)
data = f.get_audio_data()
if data:
f.duration = data["duration"]
f.size = data["size"]
f.bitrate = data["bitrate"]
f.save(update_fields=["bitrate", "duration", "size"])
file_path = get_file_path(f.audio_file)
2018-06-09 13:36:16 +00:00
elif f.source and f.source.startswith("file://"):
file_path = get_file_path(f.source.replace("file://", "", 1))
mt = f.mimetype
if should_transcode(f, format, max_bitrate=max_bitrate):
transcoded_version = f.get_transcoded_version(format, max_bitrate=max_bitrate)
transcoded_version.accessed_date = now
transcoded_version.save(update_fields=["accessed_date"])
f = transcoded_version
file_path = get_file_path(f.audio_file)
mt = f.mimetype
if not proxy_media:
# we simply issue a 302 redirect to the real URL
response = Response(status=302)
response["Location"] = f.audio_file.url
return response
if mt:
response = Response(content_type=mt)
else:
response = Response()
filename = f.filename
2018-06-09 13:36:16 +00:00
mapping = {"nginx": "X-Accel-Redirect", "apache2": "X-Sendfile"}
file_header = mapping[settings.REVERSE_PROXY_TYPE]
response[file_header] = file_path
response["Content-Disposition"] = get_content_disposition(filename)
if mt:
response["Content-Type"] = mt
return response
class ListenViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet):
queryset = models.Track.objects.all()
serializer_class = serializers.TrackSerializer
2018-06-09 13:36:16 +00:00
authentication_classes = (
rest_settings.api_settings.DEFAULT_AUTHENTICATION_CLASSES
+ [SignatureAuthentication]
)
permission_classes = [oauth_permissions.ScopePermission]
required_scope = "libraries"
anonymous_policy = "setting"
lookup_field = "uuid"
def retrieve(self, request, *args, **kwargs):
track = self.get_object()
actor = utils.get_actor_from_request(request)
2018-09-22 12:29:30 +00:00
queryset = track.uploads.select_related("track__album__artist", "track__artist")
explicit_file = request.GET.get("upload")
if explicit_file:
queryset = queryset.filter(uuid=explicit_file)
queryset = queryset.playable_by(actor)
2018-09-22 12:29:30 +00:00
queryset = queryset.order_by(F("audio_file").desc(nulls_last=True))
upload = queryset.first()
if not upload:
return Response(status=404)
format = request.GET.get("to")
max_bitrate = request.GET.get("max_bitrate")
try:
max_bitrate = min(max(int(max_bitrate), 0), 320) or None
except (TypeError, ValueError):
max_bitrate = None
if max_bitrate:
max_bitrate = max_bitrate * 1000
return handle_serve(
upload,
user=request.user,
format=format,
max_bitrate=max_bitrate,
proxy_media=settings.PROXY_MEDIA,
)
2018-09-22 12:29:30 +00:00
class UploadViewSet(
mixins.ListModelMixin,
mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
viewsets.GenericViewSet,
):
lookup_field = "uuid"
queryset = (
2018-09-22 12:29:30 +00:00
models.Upload.objects.all()
.order_by("-creation_date")
.select_related("library", "track__artist", "track__album__artist")
)
2018-09-22 12:29:30 +00:00
serializer_class = serializers.UploadForOwnerSerializer
permission_classes = [
oauth_permissions.ScopePermission,
common_permissions.OwnerPermission,
]
required_scope = "libraries"
anonymous_policy = "setting"
owner_field = "library.actor.user"
owner_checks = ["read", "write"]
filterset_class = filters.UploadFilter
ordering_fields = (
"creation_date",
"import_date",
"bitrate",
"size",
"artist__name",
)
def get_queryset(self):
qs = super().get_queryset()
return qs.filter(library__actor=self.request.user.actor)
2019-01-11 12:33:35 +00:00
@action(methods=["post"], detail=False)
def action(self, request, *args, **kwargs):
queryset = self.get_queryset()
2018-09-22 12:29:30 +00:00
serializer = serializers.UploadActionSerializer(request.data, queryset=queryset)
serializer.is_valid(raise_exception=True)
result = serializer.save()
return Response(result, status=200)
def get_serializer_context(self):
context = super().get_serializer_context()
context["user"] = self.request.user
return context
def perform_create(self, serializer):
2018-09-22 12:29:30 +00:00
upload = serializer.save()
common_utils.on_commit(tasks.process_upload.delay, upload_id=upload.pk)
2018-09-22 12:29:30 +00:00
@transaction.atomic
def perform_destroy(self, instance):
routes.outbox.dispatch(
{"type": "Delete", "object": {"type": "Audio"}},
context={"uploads": [instance]},
)
instance.delete()
class Search(views.APIView):
max_results = 3
permission_classes = [oauth_permissions.ScopePermission]
required_scope = "libraries"
anonymous_policy = "setting"
2017-06-26 16:10:38 +00:00
def get(self, request, *args, **kwargs):
2018-06-09 13:36:16 +00:00
query = request.GET["query"]
results = {
# 'tags': serializers.TagSerializer(self.get_tags(query), many=True).data,
2018-06-09 13:36:16 +00:00
"artists": serializers.ArtistWithAlbumsSerializer(
self.get_artists(query), many=True
).data,
"tracks": serializers.TrackSerializer(
self.get_tracks(query), many=True
).data,
"albums": serializers.AlbumSerializer(
self.get_albums(query), many=True
).data,
}
return Response(results, status=200)
def get_tracks(self, query):
search_fields = [
2018-06-09 13:36:16 +00:00
"mbid",
"title__unaccent",
"album__title__unaccent",
"artist__name__unaccent",
]
query_obj = utils.get_query(query, search_fields)
qs = (
models.Track.objects.all()
2018-06-09 13:36:16 +00:00
.filter(query_obj)
.select_related("artist", "album__artist")
)
return common_utils.order_for_search(qs, "title")[: self.max_results]
def get_albums(self, query):
2018-06-09 13:36:16 +00:00
search_fields = ["mbid", "title__unaccent", "artist__name__unaccent"]
query_obj = utils.get_query(query, search_fields)
qs = (
models.Album.objects.all()
2018-06-09 13:36:16 +00:00
.filter(query_obj)
.select_related()
.prefetch_related("tracks__artist")
)
return common_utils.order_for_search(qs, "title")[: self.max_results]
def get_artists(self, query):
2018-06-09 13:36:16 +00:00
search_fields = ["mbid", "name__unaccent"]
query_obj = utils.get_query(query, search_fields)
qs = models.Artist.objects.all().filter(query_obj).with_albums()
return common_utils.order_for_search(qs, "name")[: self.max_results]
def get_tags(self, query):
2019-07-08 13:26:14 +00:00
search_fields = ["name__unaccent"]
query_obj = utils.get_query(query, search_fields)
# We want the shortest tag first
2018-06-09 13:36:16 +00:00
qs = (
Tag.objects.all()
2019-07-08 13:26:14 +00:00
.annotate(name_length=Length("name"))
.order_by("name_length")
2018-06-09 13:36:16 +00:00
)
2018-06-09 13:36:16 +00:00
return qs.filter(query_obj)[: self.max_results]
2018-12-04 14:13:37 +00:00
class LicenseViewSet(viewsets.ReadOnlyModelViewSet):
permission_classes = [oauth_permissions.ScopePermission]
required_scope = "libraries"
anonymous_policy = "setting"
2018-12-04 14:13:37 +00:00
serializer_class = serializers.LicenseSerializer
queryset = models.License.objects.all().order_by("code")
lookup_value_regex = ".*"
2019-04-09 08:30:26 +00:00
max_page_size = 1000
2018-12-04 14:13:37 +00:00
def get_queryset(self):
# ensure our licenses are up to date in DB
licenses.load(licenses.LICENSES)
return super().get_queryset()
def get_serializer(self, *args, **kwargs):
if len(args) == 0:
return super().get_serializer(*args, **kwargs)
# our serializer works with license dict, not License instances
# so we pass those instead
instance_or_qs = args[0]
try:
first_arg = instance_or_qs.conf
except AttributeError:
first_arg = [i.conf for i in instance_or_qs if i.conf]
return super().get_serializer(*((first_arg,) + args[1:]), **kwargs)
class OembedView(views.APIView):
permission_classes = [oauth_permissions.ScopePermission]
required_scope = "libraries"
anonymous_policy = "setting"
def get(self, request, *args, **kwargs):
serializer = serializers.OembedSerializer(data=request.GET)
serializer.is_valid(raise_exception=True)
embed_data = serializer.save()
return Response(embed_data)