From b2866adfe37c55791c0fe2ce40408f6802214df8 Mon Sep 17 00:00:00 2001 From: Georg Krause Date: Mon, 29 Jan 2024 13:48:22 +0000 Subject: [PATCH] wip: add v2 upload endpoint --- api/funkwhale_api/music/models.py | 4 ++ api/funkwhale_api/music/serializers.py | 97 +++++++++++++++++++++++++- api/funkwhale_api/music/views.py | 24 +++++++ api/tests/music/test_urls.py | 18 ++++- api/tests/music/test_views.py | 55 +++++++++++++++ 5 files changed, 192 insertions(+), 6 deletions(-) diff --git a/api/funkwhale_api/music/models.py b/api/funkwhale_api/music/models.py index 5ea65bbea..63f2375bb 100644 --- a/api/funkwhale_api/music/models.py +++ b/api/funkwhale_api/music/models.py @@ -1456,3 +1456,7 @@ class UploadGroup(models.Model): def __str__(self): return self.name + + @property + def upload_url(self): + return f"{settings.FUNKWHALE_URL}/api/v2/upload-groups/{self.guid}/uploads" diff --git a/api/funkwhale_api/music/serializers.py b/api/funkwhale_api/music/serializers.py index a250033d5..544b89751 100644 --- a/api/funkwhale_api/music/serializers.py +++ b/api/funkwhale_api/music/serializers.py @@ -858,8 +858,99 @@ class UploadGroupSerializer(serializers.ModelSerializer): fields = ["guid", "name", "createdAt", "uploadUrl"] name = serializers.CharField(required=False) - uploadUrl = serializers.SerializerMethodField(read_only=True) + uploadUrl = serializers.URLField(read_only=True, source="upload_url") createdAt = serializers.DateTimeField(read_only=True, source="created_at") - def get_uploadUrl(self, value): - return f"{settings.FUNKWHALE_URL}/api/v2/upload-groups/{value.guid}/uploads" + +class UploadGroupUploadMetadataReleaseSerializer(serializers.Serializer): + title = serializers.CharField() + artist = serializers.CharField() + mbid = serializers.UUIDField(required=False) + + +class UploadGroupUploadMetadataArtistSerializer(serializers.Serializer): + name = serializers.CharField() + mbid = serializers.UUIDField(required=False) + + +class UploadGroupUploadMetadataSerializer(serializers.Serializer): + title = serializers.CharField() + mbid = serializers.UUIDField(required=False) + tags = serializers.ListField(child=serializers.CharField(), required=False) + position = serializers.IntegerField(required=False) + entryNumber = serializers.IntegerField(required=False) + releaseDate = serializers.DateField(required=False) + license = serializers.URLField(required=False) + release = UploadGroupUploadMetadataReleaseSerializer(required=False) + artist = UploadGroupUploadMetadataArtistSerializer(required=False) + + +class TargetSerializer(serializers.Serializer): + library = serializers.UUIDField(required=False) + collections = serializers.ListField(child=serializers.UUIDField(), required=False) + channels = serializers.ListField(child=serializers.UUIDField(), required=False) + + def validate(self, data): + # At the moment we allow to set exactly one target, it can be either a library or a channel. + # The structure already allows setting multiple targets in the future, however this is disabled for now. + if "channels" in data and "library" in data: + raise serializers.ValidationError + if "channels" not in data and "library" not in data: + raise serializers.ValidationError + if "collections" in data: + raise serializers.ValidationError("Not yet implemented") + try: + if len(data.channels) > 1: + raise serializers.ValidationError + except AttributeError: + pass + return data + + +class UploadGroupUploadSerializer(serializers.ModelSerializer): + class Meta: + model = models.Upload + fields = [ + "audioFile", + "target", + "metadata", + ] # , "cover"] TODO we need to process the cover + + metadata = serializers.JSONField(source="import_metadata") + target = serializers.JSONField() + audioFile = serializers.FileField(source="audio_file") + # cover = serializers.FileField(required=False) + + def validate_target(self, value): + serializer = TargetSerializer(data=value) + if serializer.is_valid(): + return serializer.validated_data + else: + print(serializer.errors) + raise serializers.ValidationError + + def validate_metadata(self, value): + serializer = UploadGroupUploadMetadataSerializer(data=value) + if serializer.is_valid(): + return serializer.validated_data + else: + print(serializer.errors) + raise serializers.ValidationError + + def create(self, validated_data): + library = models.Library.objects.get(uuid=validated_data["target"]["library"]) + del validated_data["target"] + return models.Upload.objects.create( + library=library, source="upload://test", **validated_data + ) + + +class BaseUploadSerializer(serializers.ModelSerializer): + class Meta: + model = models.Upload + fields = ["guid", "createdDate", "uploadGroup", "status"] + + guid = serializers.UUIDField(source="uuid") + createdDate = serializers.DateTimeField(source="creation_date") + uploadGroup = serializers.UUIDField(source="upload_group.guid") + status = serializers.CharField(source="import_status") diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py index ab7c56bec..a5b79914d 100644 --- a/api/funkwhale_api/music/views.py +++ b/api/funkwhale_api/music/views.py @@ -15,6 +15,7 @@ from rest_framework import mixins, renderers from rest_framework import settings as rest_settings from rest_framework import views, viewsets from rest_framework.decorators import action +from rest_framework.parsers import FormParser, MultiPartParser from rest_framework.response import Response from funkwhale_api.common import decorators as common_decorators @@ -946,3 +947,26 @@ class UploadGroupViewSet(viewsets.ModelViewSet): def perform_create(self, serializer): serializer.save(owner=self.request.user.actor) + + @action( + detail=True, + methods=["post"], + parser_classes=(MultiPartParser, FormParser), + serializer_class=serializers.UploadGroupUploadSerializer, + ) + def uploads(self, request, pk=None): + print(request.data) + serializer = self.get_serializer(data=request.data) + if serializer.is_valid(): + upload_group = models.UploadGroup.objects.get(guid=pk) + if upload_group.owner == request.user.actor: + upload = serializer.save(upload_group=upload_group) + common_utils.on_commit(tasks.process_upload.delay, upload_id=upload.pk) + response = serializers.BaseUploadSerializer(upload).data + return Response(response, status=200) + else: + return Response("You don't own this Upload Group", status=403) + else: + print(serializer.errors) + + return Response("Fehler", status=202) diff --git a/api/tests/music/test_urls.py b/api/tests/music/test_urls.py index 103531653..4d0fd1897 100644 --- a/api/tests/music/test_urls.py +++ b/api/tests/music/test_urls.py @@ -1,6 +1,18 @@ +import pytest from django.urls import reverse -def test_can_resolve_upload_urls(): - url = reverse("api:v2:upload-groups-list") - assert url == "/api/v2/upload-groups" +@pytest.mark.parametrize( + "input,args,expected_url", + [ + ("api:v2:upload-groups-list", None, "/api/v2/upload-groups"), + ( + "api:v2:upload-groups-uploads", + ["1234-1234-1234"], + "/api/v2/upload-groups/1234-1234-1234/uploads", + ), + ], +) +def test_can_resolve_upload_urls(input, args, expected_url): + url = reverse(input, args=args) + assert url == expected_url diff --git a/api/tests/music/test_views.py b/api/tests/music/test_views.py index f607ec442..6f48e5cab 100644 --- a/api/tests/music/test_views.py +++ b/api/tests/music/test_views.py @@ -1672,3 +1672,58 @@ def test_can_create_upload_group_with_name(logged_in_api_client): assert "https://test.federation/api/v2/upload-groups/" in response.data.get( "uploadUrl" ) + + +def test_user_can_create_upload_v2(logged_in_api_client, factories, mocker, audio_file): + library = factories["music.Library"](actor__user=logged_in_api_client.user) + logged_in_api_client.user.create_actor() + + upload_group = factories["music.UploadGroup"](owner=logged_in_api_client.user.actor) + upload_url = upload_group.upload_url + + m = mocker.patch("funkwhale_api.common.utils.on_commit") + + response = logged_in_api_client.post( + upload_url, + { + "audioFile": audio_file, + "metadata": '{"title": "foo"}', + "target": f'{{"library": "{ library.uuid }"}}', + }, + ) + + print(response.data) + + assert response.status_code == 200 + + upload = library.uploads.latest("id") + + audio_file.seek(0) + assert upload.audio_file.read() == audio_file.read() + assert upload.source == "upload://test" + assert upload.import_status == "pending" + assert upload.import_metadata == {"title": "foo"} + assert upload.track is None + assert upload.upload_group == upload_group + m.assert_called_once_with(tasks.process_upload.delay, upload_id=upload.pk) + + +def test_user_cannot_create_upload_for_foreign_group( + logged_in_api_client, factories, mocker, audio_file +): + library = factories["music.Library"](actor__user=logged_in_api_client.user) + logged_in_api_client.user.create_actor() + + upload_group = factories["music.UploadGroup"]() + upload_url = upload_group.upload_url + + response = logged_in_api_client.post( + upload_url, + { + "audioFile": audio_file, + "metadata": '{"title": "foo"}', + "target": f'{{"library": "{ library.uuid }"}}', + }, + ) + + assert response.status_code == 403