funkwhale/api/funkwhale_api/music/views.py

941 wiersze
32 KiB
Python

import base64
import datetime
import logging
import urllib.parse
from django.conf import settings
from django.core.cache import cache
from django.db import transaction
from django.db.models import Count, Prefetch, Sum, F, Q
import django.db.utils
from django.utils import timezone
from rest_framework import mixins
from rest_framework import renderers
from rest_framework import settings as rest_settings
from rest_framework import views, viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from drf_spectacular.utils import extend_schema, OpenApiParameter
import requests.exceptions
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
from funkwhale_api.common import views as common_views
from funkwhale_api.federation.authentication import SignatureAuthentication
from funkwhale_api.federation import actors
from funkwhale_api.federation import api_serializers as federation_api_serializers
from funkwhale_api.federation import decorators as federation_decorators
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.tags.models import Tag, TaggedItem
from funkwhale_api.tags.serializers import TagSerializer
from funkwhale_api.users.oauth import permissions as oauth_permissions
from funkwhale_api.users.authentication import ScopedTokenAuthentication
from . import filters, licenses, models, serializers, tasks, utils
logger = logging.getLogger(__name__)
TAG_PREFETCH = Prefetch(
"tagged_items",
queryset=TaggedItem.objects.all().select_related().order_by("tag__name"),
to_attr="_prefetched_tagged_items",
)
def get_libraries(filter_uploads):
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)
qs = models.Library.objects.filter(
pk__in=uploads.values_list("library", flat=True),
channel=None,
).annotate(_uploads_count=Count("uploads"))
qs = qs.prefetch_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)
serializer = federation_api_serializers.LibrarySerializer(qs, many=True)
return Response(serializer.data)
return extend_schema(
responses=federation_api_serializers.LibrarySerializer(many=True),
parameters=[OpenApiParameter("id", location="query", exclude=True)],
)(action(methods=["get"], detail=True)(libraries))
def refetch_obj(obj, queryset):
"""
Given an Artist/Album/Track instance, if the instance is from a remote pod,
will attempt to update local data with the latest ActivityPub representation.
"""
if obj.is_local:
return obj
now = timezone.now()
limit = now - datetime.timedelta(minutes=settings.FEDERATION_OBJECT_FETCH_DELAY)
last_fetch = obj.fetches.order_by("-creation_date").first()
if last_fetch is not None and last_fetch.creation_date > limit:
# we fetched recently, no need to do it again
return obj
logger.info("Refetching %s:%s at %s", obj._meta.label, obj.pk, obj.fid)
actor = actors.get_service_actor()
fetch = federation_models.Fetch.objects.create(actor=actor, url=obj.fid, object=obj)
try:
federation_tasks.fetch(fetch_id=fetch.pk)
except Exception:
logger.exception(
"Error while refetching %s:%s at %s", obj._meta.label, obj.pk, obj.fid
)
else:
fetch.refresh_from_db()
if fetch.status == "finished":
obj = queryset.get(pk=obj.pk)
return obj
class HandleInvalidSearch(object):
def list(self, *args, **kwargs):
try:
return super().list(*args, **kwargs)
except django.db.utils.ProgrammingError as e:
if "in tsquery:" in str(e):
return Response({"detail": "Invalid query"}, status=400)
else:
raise
class ArtistViewSet(
HandleInvalidSearch,
common_views.SkipFilterForGetObject,
viewsets.ReadOnlyModelViewSet,
):
queryset = (
models.Artist.objects.all()
.prefetch_related("attributed_to", "attachment_cover")
.prefetch_related(
"channel__actor",
Prefetch(
"tracks",
queryset=models.Track.objects.all(),
to_attr="_prefetched_tracks",
),
)
.order_by("-id")
)
serializer_class = serializers.ArtistWithAlbumsSerializer
permission_classes = [oauth_permissions.ScopePermission]
required_scope = "libraries"
anonymous_policy = "setting"
filterset_class = filters.ArtistFilter
fetches = federation_decorators.fetches_route()
mutations = common_decorators.mutations_route(types=["update"])
def get_object(self):
obj = super().get_object()
if (
self.action == "retrieve"
and self.request.GET.get("refresh", "").lower() == "true"
):
obj = refetch_obj(obj, self.get_queryset())
return obj
def get_serializer_context(self):
context = super().get_serializer_context()
context["description"] = self.action in ["retrieve", "create", "update"]
return context
def get_queryset(self):
queryset = super().get_queryset()
albums = (
models.Album.objects.with_tracks_count()
.select_related("attachment_cover")
.prefetch_related("tracks")
)
albums = albums.annotate_playable_by_actor(
utils.get_actor_from_request(self.request)
)
return queryset.prefetch_related(
Prefetch("albums", queryset=albums), TAG_PREFETCH
)
libraries = get_libraries(
lambda o, uploads: uploads.filter(
Q(track__artist=o) | Q(track__album__artist=o)
)
)
class AlbumViewSet(
HandleInvalidSearch,
common_views.SkipFilterForGetObject,
mixins.CreateModelMixin,
mixins.DestroyModelMixin,
viewsets.ReadOnlyModelViewSet,
):
queryset = (
models.Album.objects.all()
.order_by("-creation_date")
.prefetch_related("artist__channel", "attributed_to", "attachment_cover")
)
serializer_class = serializers.AlbumSerializer
permission_classes = [oauth_permissions.ScopePermission]
required_scope = "libraries"
anonymous_policy = "setting"
filterset_class = filters.AlbumFilter
fetches = federation_decorators.fetches_route()
mutations = common_decorators.mutations_route(types=["update"])
def get_object(self):
obj = super().get_object()
if (
self.action == "retrieve"
and self.request.GET.get("refresh", "").lower() == "true"
):
obj = refetch_obj(obj, self.get_queryset())
return obj
def get_serializer_context(self):
context = super().get_serializer_context()
context["description"] = self.action in [
"retrieve",
"create",
]
context["user"] = self.request.user
return context
def get_queryset(self):
queryset = super().get_queryset()
if self.action in ["destroy"]:
queryset = queryset.exclude(artist__channel=None).filter(
artist__attributed_to=self.request.user.actor
)
tracks = models.Track.objects.all().prefetch_related("album")
tracks = tracks.annotate_playable_by_actor(
utils.get_actor_from_request(self.request)
)
return queryset.prefetch_related(
Prefetch("tracks", queryset=tracks), TAG_PREFETCH
)
libraries = get_libraries(lambda o, uploads: uploads.filter(track__album=o))
def get_serializer_class(self):
if self.action in ["create"]:
return serializers.AlbumCreateSerializer
return super().get_serializer_class()
@transaction.atomic
def perform_destroy(self, instance):
routes.outbox.dispatch(
{"type": "Delete", "object": {"type": "Album"}},
context={"album": instance},
)
models.Album.objects.filter(pk=instance.pk).delete()
class LibraryViewSet(
mixins.CreateModelMixin,
mixins.ListModelMixin,
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
viewsets.GenericViewSet,
):
lookup_field = "uuid"
queryset = (
models.Library.objects.all()
.filter(channel=None)
.select_related("actor")
.order_by("-creation_date")
.annotate(_uploads_count=Count("uploads"))
.annotate(_size=Sum("uploads__size"))
)
serializer_class = serializers.LibraryForOwnerSerializer
permission_classes = [
oauth_permissions.ScopePermission,
common_permissions.OwnerPermission,
]
filterset_class = filters.LibraryFilter
required_scope = "libraries"
anonymous_policy = "setting"
owner_field = "actor.user"
owner_checks = ["write"]
def get_queryset(self):
qs = super().get_queryset()
# allow retrieving a single library by uuid if request.user isn't
# the owner. Any other get should be from the owner only
if self.action not in ["retrieve", "list"]:
qs = qs.filter(actor=self.request.user.actor)
if self.action == "list":
actor = utils.get_actor_from_request(self.request)
qs = qs.viewable_by(actor)
return qs
def perform_create(self, serializer):
serializer.save(actor=self.request.user.actor)
@transaction.atomic
def perform_destroy(self, instance):
routes.outbox.dispatch(
{"type": "Delete", "object": {"type": "Library"}},
context={"library": instance},
)
instance.delete()
follows = action
@extend_schema(
responses=federation_api_serializers.LibraryFollowSerializer(many=True)
)
@action(
methods=["get"],
detail=True,
)
@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)
.prefetch_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, required=False
)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True, required=False)
return Response(serializer.data)
# TODO quickfix, basically specifying the response would be None
@extend_schema(responses=None)
@action(
methods=["get", "post", "delete"],
detail=False,
url_name="fs-import",
url_path="fs-import",
)
@transaction.non_atomic_requests
def fs_import(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return Response({}, status=403)
if not request.user.all_permissions["library"]:
return Response({}, status=403)
if request.method == "GET":
path = request.GET.get("path", "")
data = {
"root": settings.MUSIC_DIRECTORY_PATH,
"path": path,
"import": None,
}
status = cache.get("fs-import:status", default=None)
if status:
data["import"] = {
"status": status,
"reference": cache.get("fs-import:reference"),
"logs": cache.get("fs-import:logs", default=[]),
}
try:
data["content"] = utils.browse_dir(data["root"], data["path"])
except (NotADirectoryError, ValueError, FileNotFoundError) as e:
return Response({"detail": str(e)}, status=400)
return Response(data)
if request.method == "POST":
if cache.get("fs-import:status", default=None) in [
"pending",
"started",
]:
return Response({"detail": "An import is already running"}, status=400)
data = request.data
serializer = serializers.FSImportSerializer(
data=data, context={"user": request.user}
)
serializer.is_valid(raise_exception=True)
cache.set("fs-import:status", "pending")
cache.set(
"fs-import:reference", serializer.validated_data["import_reference"]
)
tasks.fs_import.delay(
library_id=serializer.validated_data["library"].pk,
path=serializer.validated_data["path"],
import_reference=serializer.validated_data["import_reference"],
)
return Response(status=201)
if request.method == "DELETE":
cache.set("fs-import:status", "canceled")
return Response(status=204)
class TrackViewSet(
HandleInvalidSearch,
common_views.SkipFilterForGetObject,
mixins.DestroyModelMixin,
viewsets.ReadOnlyModelViewSet,
):
"""
A simple ViewSet for viewing and editing accounts.
"""
queryset = (
models.Track.objects.all()
.for_nested_serialization()
.prefetch_related("attributed_to", "attachment_cover")
.order_by("-creation_date")
)
serializer_class = serializers.TrackSerializer
permission_classes = [oauth_permissions.ScopePermission]
required_scope = "libraries"
anonymous_policy = "setting"
filterset_class = filters.TrackFilter
fetches = federation_decorators.fetches_route()
mutations = common_decorators.mutations_route(types=["update"])
def get_object(self):
obj = super().get_object()
if (
self.action == "retrieve"
and self.request.GET.get("refresh", "").lower() == "true"
):
obj = refetch_obj(obj, self.get_queryset())
return obj
def get_queryset(self):
queryset = super().get_queryset()
if self.action in ["destroy"]:
queryset = queryset.exclude(artist__channel=None).filter(
artist__attributed_to=self.request.user.actor
)
filter_favorites = self.request.GET.get("favorites", None)
user = self.request.user
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.prefetch_related(TAG_PREFETCH)
libraries = get_libraries(lambda o, uploads: uploads.filter(track=o))
def get_serializer_context(self):
context = super().get_serializer_context()
context["description"] = self.action in ["retrieve", "create", "update"]
return context
@transaction.atomic
def perform_destroy(self, instance):
uploads = instance.uploads.order_by("id")
routes.outbox.dispatch(
{"type": "Delete", "object": {"type": "Audio"}},
context={"uploads": list(uploads)},
)
instance.delete()
def strip_absolute_media_url(path):
if (
settings.MEDIA_URL.startswith("http://")
or settings.MEDIA_URL.startswith("https://")
and path.startswith(settings.MEDIA_URL)
):
path = path.replace(settings.MEDIA_URL, "/media/", 1)
return path
def get_file_path(audio_file):
serve_path = settings.MUSIC_DIRECTORY_SERVE_PATH
prefix = settings.MUSIC_DIRECTORY_PATH
t = settings.REVERSE_PROXY_TYPE
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(
"You need to specify MUSIC_DIRECTORY_SERVE_PATH and "
"MUSIC_DIRECTORY_PATH to serve in-place imported files"
)
path = "/music" + audio_file.replace(prefix, "", 1)
path = strip_absolute_media_url(path)
if path.startswith("http://") or path.startswith("https://"):
protocol, remainder = path.split("://", 1)
hostname, r_path = remainder.split("/", 1)
r_path = urllib.parse.quote(r_path)
path = protocol + "://" + hostname + "/" + r_path
return (settings.PROTECT_FILES_PATH + "/media/" + path).encode("utf-8")
# needed to serve files with % or ? chars
path = urllib.parse.quote(path)
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(
"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)
path = strip_absolute_media_url(path)
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 record_downloads(f):
def inner(*args, **kwargs):
user = kwargs.get("user")
wsgi_request = kwargs.pop("wsgi_request")
upload = kwargs.get("upload")
response = f(*args, **kwargs)
if response.status_code >= 200 and response.status_code < 400:
utils.increment_downloads_count(
upload=upload, user=user, wsgi_request=wsgi_request
)
return response
return inner
@record_downloads
def handle_serve(
upload, user, format=None, max_bitrate=None, proxy_media=True, download=True
):
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()
try:
f.download_audio_from_remote(actor=actor)
except requests.exceptions.RequestException:
return Response({"detail": "Remove track is unavailable"}, status=503)
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)
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 and f.audio_file:
# 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
mapping = {"nginx": "X-Accel-Redirect", "apache2": "X-Sendfile"}
file_header = mapping[settings.REVERSE_PROXY_TYPE]
response[file_header] = file_path
if download:
response["Content-Disposition"] = get_content_disposition(filename)
if mt:
response["Content-Type"] = mt
return response
class ListenMixin(mixins.RetrieveModelMixin, viewsets.GenericViewSet):
queryset = models.Track.objects.all()
serializer_class = serializers.TrackSerializer
authentication_classes = (
rest_settings.api_settings.DEFAULT_AUTHENTICATION_CLASSES
+ [SignatureAuthentication, ScopedTokenAuthentication]
)
permission_classes = [oauth_permissions.ScopePermission]
required_scope = "libraries"
anonymous_policy = "setting"
lookup_field = "uuid"
@extend_schema(responses=bytes)
def retrieve(self, request, *args, **kwargs):
config = {
"explicit_file": request.GET.get("upload"),
"download": request.GET.get("download", "true").lower() == "true",
"format": request.GET.get("to"),
"max_bitrate": request.GET.get("max_bitrate"),
}
track = self.get_object()
return handle_stream(track, request, **config)
def handle_stream(track, request, download, explicit_file, format, max_bitrate):
actor = utils.get_actor_from_request(request)
queryset = track.uploads.prefetch_related("track__album__artist", "track__artist")
if explicit_file:
queryset = queryset.filter(uuid=explicit_file)
queryset = queryset.playable_by(actor)
queryset = queryset.order_by(F("audio_file").desc(nulls_last=True))
upload = queryset.first()
if not upload:
return Response(status=404)
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=upload,
user=request.user,
format=format,
max_bitrate=max_bitrate,
proxy_media=settings.PROXY_MEDIA,
download=download,
wsgi_request=request._request,
)
class AudioRenderer(renderers.JSONRenderer):
media_type = "audio/*"
@extend_schema(operation_id="get_track_file")
class ListenViewSet(ListenMixin):
renderer_classes = [AudioRenderer]
class MP3Renderer(renderers.JSONRenderer):
format = "mp3"
media_type = "audio/mpeg"
class StreamViewSet(ListenMixin):
renderer_classes = [MP3Renderer]
@extend_schema(operation_id="get_track_stream", responses=bytes)
def retrieve(self, request, *args, **kwargs):
config = {
"explicit_file": None,
"download": False,
"format": "mp3",
"max_bitrate": None,
}
track = self.get_object()
return handle_stream(track, request, **config)
class UploadViewSet(
mixins.ListModelMixin,
mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
viewsets.GenericViewSet,
):
lookup_field = "uuid"
queryset = (
models.Upload.objects.all()
.order_by("-creation_date")
.prefetch_related(
"library__actor",
"track__artist",
"track__album__artist",
"track__attachment_cover",
)
)
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 = ["write"]
filterset_class = filters.UploadFilter
ordering_fields = (
"creation_date",
"import_date",
"bitrate",
"size",
"artist__name",
)
def get_queryset(self):
qs = super().get_queryset()
if self.action in ["update", "partial_update"]:
# prevent updating an upload that is already processed
qs = qs.filter(import_status="draft")
if self.action != "retrieve":
qs = qs.filter(library__actor=self.request.user.actor)
else:
actor = utils.get_actor_from_request(self.request)
qs = qs.playable_by(actor)
return qs
@extend_schema(
responses=tasks.metadata.TrackMetadataSerializer(),
operation_id="get_upload_metadata",
)
@action(methods=["get"], detail=True, url_path="audio-file-metadata")
def audio_file_metadata(self, request, *args, **kwargs):
upload = self.get_object()
try:
m = tasks.metadata.Metadata(upload.get_audio_file())
except FileNotFoundError:
return Response({"detail": "File not found"}, status=500)
serializer = tasks.metadata.TrackMetadataSerializer(
data=m, context={"strict": False}
)
if not serializer.is_valid():
return Response(serializer.errors, status=500)
payload = serializer.validated_data
cover_data = payload.get(
"cover_data", payload.get("album", {}).get("cover_data", {})
)
if cover_data and "content" in cover_data:
cover_data["content"] = base64.b64encode(cover_data["content"])
return Response(payload, status=200)
@action(methods=["post"], detail=False)
def action(self, request, *args, **kwargs):
queryset = self.get_queryset()
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):
upload = serializer.save()
if upload.import_status == "pending":
common_utils.on_commit(tasks.process_upload.delay, upload_id=upload.pk)
def perform_update(self, serializer):
upload = serializer.save()
if upload.import_status == "pending":
common_utils.on_commit(tasks.process_upload.delay, upload_id=upload.pk)
@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"
@extend_schema(operation_id="get_search_results")
def get(self, request, *args, **kwargs):
query = request.GET.get("query", request.GET.get("q", "")) or ""
query = query.strip()
if not query:
return Response({"detail": "empty query"}, status=400)
try:
results = {
# 'tags': serializers.TagSerializer(self.get_tags(query), many=True).data,
"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,
"tags": TagSerializer(self.get_tags(query), many=True).data,
}
except django.db.utils.ProgrammingError as e:
if "in tsquery:" in str(e):
return Response({"detail": "Invalid query"}, status=400)
else:
raise
return Response(results, status=200)
def get_tracks(self, query):
query_obj = utils.get_fts_query(
query,
fts_fields=["body_text", "album__body_text", "artist__body_text"],
model=models.Track,
)
qs = (
models.Track.objects.all()
.filter(query_obj)
.prefetch_related(
"artist",
"attributed_to",
Prefetch(
"album",
queryset=models.Album.objects.select_related(
"artist", "attachment_cover", "attributed_to"
).prefetch_related("tracks"),
),
)
)
return common_utils.order_for_search(qs, "title")[: self.max_results]
def get_albums(self, query):
query_obj = utils.get_fts_query(
query, fts_fields=["body_text", "artist__body_text"], model=models.Album
)
qs = (
models.Album.objects.all()
.filter(query_obj)
.select_related("artist", "attachment_cover", "attributed_to")
.prefetch_related("tracks__artist")
)
return common_utils.order_for_search(qs, "title")[: self.max_results]
def get_artists(self, query):
query_obj = utils.get_fts_query(query, model=models.Artist)
qs = (
models.Artist.objects.all()
.filter(query_obj)
.with_albums()
.prefetch_related("channel__actor")
.select_related("attributed_to")
)
return common_utils.order_for_search(qs, "name")[: self.max_results]
def get_tags(self, query):
search_fields = ["name__unaccent"]
query_obj = utils.get_query(query, search_fields)
qs = Tag.objects.all().filter(query_obj)
return common_utils.order_for_search(qs, "name")[: self.max_results]
class LicenseViewSet(viewsets.ReadOnlyModelViewSet):
permission_classes = [oauth_permissions.ScopePermission]
required_scope = "libraries"
anonymous_policy = "setting"
serializer_class = serializers.LicenseSerializer
queryset = models.License.objects.all().order_by("code")
lookup_value_regex = ".*"
max_page_size = 1000
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)