funquail/api/funkwhale_api/music/views.py

397 lines
13 KiB
Python
Raw Normal View History

import logging
import urllib
from django.conf import settings
from django.db import transaction
from django.db.models import Count, Prefetch, Sum
2018-06-10 10:55:16 +02:00
from django.db.models.functions import Length
from django.utils import timezone
2018-06-10 10:55:16 +02:00
from rest_framework import mixins
from rest_framework import permissions
2018-06-10 10:55:16 +02:00
from rest_framework import settings as rest_settings
from rest_framework import views, viewsets
from rest_framework.decorators import detail_route, list_route
from rest_framework.response import Response
2018-06-10 10:55:16 +02:00
from taggit.models import Tag
from funkwhale_api.common import utils as common_utils
from funkwhale_api.common import permissions as common_permissions
from funkwhale_api.federation.authentication import SignatureAuthentication
2018-09-13 15:18:23 +00:00
from funkwhale_api.federation import api_serializers as federation_api_serializers
from . import filters, models, serializers, tasks, utils
logger = logging.getLogger(__name__)
2017-12-12 22:04:39 +01:00
class TagViewSetMixin(object):
def get_queryset(self):
queryset = super().get_queryset()
2018-06-09 15:36:16 +02:00
tag = self.request.query_params.get("tag")
if tag:
queryset = queryset.filter(tags__pk=tag)
return queryset
class ArtistViewSet(viewsets.ReadOnlyModelViewSet):
queryset = models.Artist.objects.all()
serializer_class = serializers.ArtistWithAlbumsSerializer
permission_classes = [common_permissions.ConditionalAuthentication]
2017-12-12 22:04:39 +01:00
filter_class = filters.ArtistFilter
2018-06-09 15:36:16 +02:00
ordering_fields = ("id", "name", "creation_date")
def get_queryset(self):
queryset = super().get_queryset()
albums = models.Album.objects.with_tracks_count()
return queryset.prefetch_related(Prefetch("albums", queryset=albums)).distinct()
class AlbumViewSet(viewsets.ReadOnlyModelViewSet):
queryset = (
models.Album.objects.all().order_by("artist", "release_date").select_related()
2018-06-09 15:36:16 +02:00
)
serializer_class = serializers.AlbumSerializer
permission_classes = [common_permissions.ConditionalAuthentication]
2018-06-09 15:36:16 +02:00
ordering_fields = ("creation_date", "release_date", "title")
filter_class = filters.AlbumFilter
def get_queryset(self):
queryset = super().get_queryset()
tracks = models.Track.objects.annotate_playable_by_actor(
utils.get_actor_from_request(self.request)
).select_related("artist")
qs = queryset.prefetch_related(Prefetch("tracks", queryset=tracks))
return qs.distinct()
class LibraryViewSet(
2018-06-09 15:36:16 +02:00
mixins.CreateModelMixin,
mixins.ListModelMixin,
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
2018-06-09 15:36:16 +02:00
viewsets.GenericViewSet,
):
lookup_field = "uuid"
queryset = (
models.Library.objects.all()
2018-06-09 15:36:16 +02:00
.order_by("-creation_date")
.annotate(_files_count=Count("files"))
.annotate(_size=Sum("files__size"))
)
serializer_class = serializers.LibraryForOwnerSerializer
permission_classes = [
permissions.IsAuthenticated,
common_permissions.OwnerPermission,
]
owner_field = "actor.user"
owner_checks = ["read", "write"]
2017-12-27 23:32:02 +01:00
def get_queryset(self):
qs = super().get_queryset()
return qs.filter(actor=self.request.user.actor)
2017-12-27 23:32:02 +01:00
def perform_create(self, serializer):
serializer.save(actor=self.request.user.actor)
2017-12-27 23:32:02 +01:00
2018-09-13 15:18:23 +00:00
@detail_route(methods=["get"])
@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)
class TrackViewSet(TagViewSetMixin, viewsets.ReadOnlyModelViewSet):
"""
A simple ViewSet for viewing and editing accounts.
"""
2018-06-09 15:36:16 +02:00
queryset = models.Track.objects.all().for_nested_serialization()
serializer_class = serializers.TrackSerializer
permission_classes = [common_permissions.ConditionalAuthentication]
filter_class = filters.TrackFilter
2017-12-17 20:07:18 +01:00
ordering_fields = (
2018-06-09 15:36:16 +02:00
"creation_date",
"title",
"album__release_date",
"size",
2018-06-09 15:36:16 +02:00
"artist__name",
2017-12-17 20:07:18 +01:00
)
def get_queryset(self):
queryset = super().get_queryset()
2018-06-09 15:36:16 +02:00
filter_favorites = self.request.GET.get("favorites", None)
user = self.request.user
2018-06-09 15:36:16 +02:00
if user.is_authenticated and filter_favorites == "true":
queryset = queryset.filter(track_favorites__user=user)
queryset = queryset.annotate_playable_by_actor(
utils.get_actor_from_request(self.request)
)
return queryset.distinct()
2018-06-09 15:36:16 +02:00
@detail_route(methods=["get"])
@transaction.non_atomic_requests
def lyrics(self, request, *args, **kwargs):
try:
2018-06-09 15:36:16 +02:00
track = models.Track.objects.get(pk=kwargs["pk"])
except models.Track.DoesNotExist:
return Response(status=404)
work = track.work
if not work:
work = track.get_work()
if not work:
2018-06-09 15:36:16 +02:00
return Response({"error": "unavailable work "}, status=404)
lyrics = work.fetch_lyrics()
try:
if not lyrics.content:
tasks.fetch_content(lyrics_id=lyrics.pk)
lyrics.refresh_from_db()
except AttributeError:
2018-06-09 15:36:16 +02:00
return Response({"error": "unavailable lyrics"}, status=404)
serializer = serializers.LyricsSerializer(lyrics)
return Response(serializer.data)
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 15:36:16 +02: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 15:36:16 +02:00
"You need to specify MUSIC_DIRECTORY_SERVE_PATH and "
"MUSIC_DIRECTORY_PATH to serve in-place imported files"
)
2018-06-09 15:36:16 +02:00
path = "/music" + audio_file.replace(prefix, "", 1)
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 15:36:16 +02: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 15:36:16 +02:00
return path.encode("utf-8")
def handle_serve(track_file, user):
f = track_file
# we update the accessed_date
f.accessed_date = timezone.now()
2018-06-09 15:36:16 +02:00
f.save(update_fields=["accessed_date"])
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)
f.download_audio_from_remote(user=user)
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 15:36:16 +02:00
elif f.source and f.source.startswith("file://"):
file_path = get_file_path(f.source.replace("file://", "", 1))
mt = f.mimetype
if mt:
response = Response(content_type=mt)
else:
response = Response()
filename = f.filename
2018-06-09 15:36:16 +02:00
mapping = {"nginx": "X-Accel-Redirect", "apache2": "X-Sendfile"}
file_header = mapping[settings.REVERSE_PROXY_TYPE]
response[file_header] = file_path
2018-06-09 15:36:16 +02:00
filename = "filename*=UTF-8''{}".format(urllib.parse.quote(filename))
response["Content-Disposition"] = "attachment; {}".format(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 15:36:16 +02:00
authentication_classes = (
rest_settings.api_settings.DEFAULT_AUTHENTICATION_CLASSES
+ [SignatureAuthentication]
)
permission_classes = [common_permissions.ConditionalAuthentication]
lookup_field = "uuid"
def retrieve(self, request, *args, **kwargs):
track = self.get_object()
actor = utils.get_actor_from_request(request)
queryset = track.files.select_related("track__album__artist", "track__artist")
explicit_file = request.GET.get("file")
if explicit_file:
queryset = queryset.filter(uuid=explicit_file)
queryset = queryset.playable_by(actor)
tf = queryset.first()
if not tf:
return Response(status=404)
return handle_serve(tf, user=request.user)
class TrackFileViewSet(
mixins.ListModelMixin,
mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
viewsets.GenericViewSet,
):
lookup_field = "uuid"
queryset = (
models.TrackFile.objects.all()
.order_by("-creation_date")
.select_related("library", "track__artist", "track__album__artist")
)
serializer_class = serializers.TrackFileForOwnerSerializer
permission_classes = [
permissions.IsAuthenticated,
common_permissions.OwnerPermission,
]
owner_field = "library.actor.user"
owner_checks = ["read", "write"]
filter_class = filters.TrackFileFilter
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)
@list_route(methods=["post"])
def action(self, request, *args, **kwargs):
queryset = self.get_queryset()
serializer = serializers.TrackFileActionSerializer(
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):
tf = serializer.save()
common_utils.on_commit(tasks.import_track_file.delay, track_file_id=tf.pk)
class TagViewSet(viewsets.ReadOnlyModelViewSet):
2018-06-09 15:36:16 +02:00
queryset = Tag.objects.all().order_by("name")
serializer_class = serializers.TagSerializer
permission_classes = [common_permissions.ConditionalAuthentication]
class Search(views.APIView):
max_results = 3
permission_classes = [common_permissions.ConditionalAuthentication]
2017-06-26 18:10:38 +02:00
def get(self, request, *args, **kwargs):
2018-06-09 15:36:16 +02:00
query = request.GET["query"]
results = {
# 'tags': serializers.TagSerializer(self.get_tags(query), many=True).data,
2018-06-09 15:36:16 +02: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 15:36:16 +02:00
"mbid",
"title__unaccent",
"album__title__unaccent",
"artist__name__unaccent",
]
query_obj = utils.get_query(query, search_fields)
return (
models.Track.objects.all()
2018-06-09 15:36:16 +02:00
.filter(query_obj)
.select_related("artist", "album__artist")
)[: self.max_results]
def get_albums(self, query):
2018-06-09 15:36:16 +02:00
search_fields = ["mbid", "title__unaccent", "artist__name__unaccent"]
query_obj = utils.get_query(query, search_fields)
return (
models.Album.objects.all()
2018-06-09 15:36:16 +02:00
.filter(query_obj)
.select_related()
.prefetch_related("tracks")
2018-06-09 15:36:16 +02:00
)[: self.max_results]
def get_artists(self, query):
2018-06-09 15:36:16 +02:00
search_fields = ["mbid", "name__unaccent"]
query_obj = utils.get_query(query, search_fields)
2018-06-09 15:36:16 +02:00
return (models.Artist.objects.all().filter(query_obj).with_albums())[
: self.max_results
]
def get_tags(self, query):
2018-06-09 15:36:16 +02:00
search_fields = ["slug", "name__unaccent"]
query_obj = utils.get_query(query, search_fields)
# We want the shortest tag first
2018-06-09 15:36:16 +02:00
qs = (
Tag.objects.all()
.annotate(slug_length=Length("slug"))
.order_by("slug_length")
)
2018-06-09 15:36:16 +02:00
return qs.filter(query_obj)[: self.max_results]