wip: add v2 upload endpoint
This commit is contained in:
parent
d3879e4792
commit
b2866adfe3
|
@ -1456,3 +1456,7 @@ class UploadGroup(models.Model):
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def upload_url(self):
|
||||||
|
return f"{settings.FUNKWHALE_URL}/api/v2/upload-groups/{self.guid}/uploads"
|
||||||
|
|
|
@ -858,8 +858,99 @@ class UploadGroupSerializer(serializers.ModelSerializer):
|
||||||
fields = ["guid", "name", "createdAt", "uploadUrl"]
|
fields = ["guid", "name", "createdAt", "uploadUrl"]
|
||||||
|
|
||||||
name = serializers.CharField(required=False)
|
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")
|
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")
|
||||||
|
|
|
@ -15,6 +15,7 @@ from rest_framework import mixins, renderers
|
||||||
from rest_framework import settings as rest_settings
|
from rest_framework import settings as rest_settings
|
||||||
from rest_framework import views, viewsets
|
from rest_framework import views, viewsets
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.parsers import FormParser, MultiPartParser
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from funkwhale_api.common import decorators as common_decorators
|
from funkwhale_api.common import decorators as common_decorators
|
||||||
|
@ -946,3 +947,26 @@ class UploadGroupViewSet(viewsets.ModelViewSet):
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
serializer.save(owner=self.request.user.actor)
|
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)
|
||||||
|
|
|
@ -1,6 +1,18 @@
|
||||||
|
import pytest
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
|
|
||||||
def test_can_resolve_upload_urls():
|
@pytest.mark.parametrize(
|
||||||
url = reverse("api:v2:upload-groups-list")
|
"input,args,expected_url",
|
||||||
assert url == "/api/v2/upload-groups"
|
[
|
||||||
|
("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
|
||||||
|
|
|
@ -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(
|
assert "https://test.federation/api/v2/upload-groups/" in response.data.get(
|
||||||
"uploadUrl"
|
"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
|
||||||
|
|
Loading…
Reference in New Issue