diff --git a/api/config/api_urls.py b/api/config/api_urls.py index e90601470105f7386255398c312991a23cc4fafc..04fbda87c7c958ee7dab011d52078e73f03db573 100644 --- a/api/config/api_urls.py +++ b/api/config/api_urls.py @@ -20,6 +20,7 @@ router.register(r"tracks", views.TrackViewSet, "tracks") router.register(r"uploads", views.UploadViewSet, "uploads") router.register(r"libraries", views.LibraryViewSet, "libraries") router.register(r"listen", views.ListenViewSet, "listen") +router.register(r"stream", views.StreamViewSet, "stream") router.register(r"artists", views.ArtistViewSet, "artists") router.register(r"channels", audio_views.ChannelViewSet, "channels") router.register(r"subscriptions", audio_views.SubscriptionsViewSet, "subscriptions") diff --git a/api/funkwhale_api/audio/serializers.py b/api/funkwhale_api/audio/serializers.py index be67539e0a5915718cd369d91dfff7b444f8f8e1..fd57ed374e1b547a2f70ee0f9a5ad4877580f7ee 100644 --- a/api/funkwhale_api/audio/serializers.py +++ b/api/funkwhale_api/audio/serializers.py @@ -830,7 +830,10 @@ def rss_serialize_item(upload): { # we enforce MP3, since it's the only format supported everywhere "url": federation_utils.full_url( - upload.get_listen_url(to="mp3", download=False) + reverse( + "api:v1:stream-detail", kwargs={"uuid": str(upload.track.uuid)} + ) + + ".mp3" ), "length": upload.size or 0, "type": "audio/mpeg", diff --git a/api/funkwhale_api/common/routers.py b/api/funkwhale_api/common/routers.py index 11e9dec615394f44d52d2b92d9dd84bb1eb2e647..af519b74236d603c6c162b7124ae69078aea2b0a 100644 --- a/api/funkwhale_api/common/routers.py +++ b/api/funkwhale_api/common/routers.py @@ -1,7 +1,7 @@ -from rest_framework.routers import SimpleRouter +from rest_framework.routers import DefaultRouter -class OptionalSlashRouter(SimpleRouter): +class OptionalSlashRouter(DefaultRouter): def __init__(self): super().__init__() self.trailing_slash = "/?" diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py index 0df0d46408ac53e6e56a8c4c444733cf70fa0519..314162420f9481990ab07bbabee1a65037d918b1 100644 --- a/api/funkwhale_api/music/views.py +++ b/api/funkwhale_api/music/views.py @@ -10,6 +10,7 @@ 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 @@ -614,7 +615,7 @@ def handle_serve( return response -class ListenViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet): +class ListenMixin(mixins.RetrieveModelMixin, viewsets.GenericViewSet): queryset = models.Track.objects.all() serializer_class = serializers.TrackSerializer authentication_classes = ( @@ -627,39 +628,66 @@ class ListenViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet): lookup_field = "uuid" 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() - actor = utils.get_actor_from_request(request) - queryset = track.uploads.prefetch_related( - "track__album__artist", "track__artist" - ) - explicit_file = request.GET.get("upload") - download = request.GET.get("download", "true").lower() == "true" - 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) - - 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=upload, - user=request.user, - format=format, - max_bitrate=max_bitrate, - proxy_media=settings.PROXY_MEDIA, - download=download, - wsgi_request=request._request, - ) + 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 ListenViewSet(ListenMixin): + pass + + +class MP3Renderer(renderers.JSONRenderer): + format = "mp3" + media_type = "audio/mpeg" + + +class StreamViewSet(ListenMixin): + renderer_classes = [MP3Renderer] + + 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( diff --git a/api/tests/audio/test_serializers.py b/api/tests/audio/test_serializers.py index 1667dfe2c38f52782d453874dbe82ebff04f5c6c..fbcd28a1a02625ca7366a37a0f7f300a17fe02ed 100644 --- a/api/tests/audio/test_serializers.py +++ b/api/tests/audio/test_serializers.py @@ -6,6 +6,7 @@ import pytest import pytz from django.templatetags.static import static +from django.urls import reverse from funkwhale_api.audio import serializers from funkwhale_api.common import serializers as common_serializers @@ -315,7 +316,10 @@ def test_rss_item_serializer(factories): "enclosure": [ { "url": federation_utils.full_url( - upload.get_listen_url("mp3", download=False) + reverse( + "api:v1:stream-detail", kwargs={"uuid": str(upload.track.uuid)} + ) + + ".mp3" ), "length": upload.size, "type": "audio/mpeg", diff --git a/api/tests/music/test_views.py b/api/tests/music/test_views.py index 81988a6b38d9b82d1f06172be32620e980bbba89..3ab5812f2f49072298c8ae278066772799f3efa8 100644 --- a/api/tests/music/test_views.py +++ b/api/tests/music/test_views.py @@ -445,6 +445,30 @@ def test_listen_explicit_file(factories, logged_in_api_client, mocker, settings) ) +def test_stream(factories, logged_in_api_client, mocker, settings): + mocked_serve = mocker.spy(views, "handle_serve") + upload = factories["music.Upload"]( + library__privacy_level="everyone", import_status="finished" + ) + url = ( + reverse("api:v1:stream-detail", kwargs={"uuid": str(upload.track.uuid)}) + + ".mp3" + ) + assert url.endswith("/{}.mp3".format(upload.track.uuid)) + response = logged_in_api_client.get(url) + + assert response.status_code == 200 + mocked_serve.assert_called_once_with( + upload=upload, + user=logged_in_api_client.user, + format="mp3", + download=False, + max_bitrate=None, + proxy_media=True, + wsgi_request=response.wsgi_request, + ) + + def test_listen_no_proxy(factories, logged_in_api_client, settings): settings.PROXY_MEDIA = False upload = factories["music.Upload"](