1014 lines
		
	
	
		
			36 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			1014 lines
		
	
	
		
			36 KiB
		
	
	
	
		
			Python
		
	
	
	
| import urllib.parse
 | |
| 
 | |
| from django import urls
 | |
| from django.conf import settings
 | |
| from django.db import transaction
 | |
| from drf_spectacular.types import OpenApiTypes
 | |
| from drf_spectacular.utils import extend_schema_field
 | |
| from rest_framework import serializers
 | |
| 
 | |
| from funkwhale_api.activity import serializers as activity_serializers
 | |
| from funkwhale_api.common import models as common_models
 | |
| from funkwhale_api.common import serializers as common_serializers
 | |
| from funkwhale_api.common import utils as common_utils
 | |
| from funkwhale_api.federation import routes
 | |
| from funkwhale_api.federation import utils as federation_utils
 | |
| from funkwhale_api.federation.serializers import APIActorSerializer
 | |
| from funkwhale_api.playlists import models as playlists_models
 | |
| from funkwhale_api.tags import models as tag_models
 | |
| from funkwhale_api.tags import serializers as tags_serializers
 | |
| 
 | |
| from . import filters, models, tasks, utils
 | |
| 
 | |
| NOOP = object()
 | |
| 
 | |
| COVER_WRITE_FIELD = common_serializers.RelatedField(
 | |
|     "uuid",
 | |
|     queryset=common_models.Attachment.objects.all().local(),
 | |
|     serializer=None,
 | |
|     allow_null=True,
 | |
|     required=False,
 | |
|     queryset_filter=lambda qs, context: qs.filter(actor=context["request"].user.actor),
 | |
|     write_only=True,
 | |
| )
 | |
| 
 | |
| 
 | |
| class CoverField(common_serializers.AttachmentSerializer):
 | |
|     pass
 | |
| 
 | |
| 
 | |
| cover_field = CoverField()
 | |
| 
 | |
| 
 | |
| class OptionalDescriptionMixin:
 | |
|     def to_representation(self, obj):
 | |
|         repr = super().to_representation(obj)
 | |
|         if self.context.get("description", False):
 | |
|             description = obj.description
 | |
|             repr["description"] = (
 | |
|                 common_serializers.ContentSerializer(description).data
 | |
|                 if description
 | |
|                 else None
 | |
|             )
 | |
| 
 | |
|         return repr
 | |
| 
 | |
| 
 | |
| class LicenseSerializer(serializers.Serializer):
 | |
|     id = serializers.SerializerMethodField()
 | |
|     url = serializers.URLField()
 | |
|     code = serializers.CharField()
 | |
|     name = serializers.CharField()
 | |
|     redistribute = serializers.BooleanField()
 | |
|     derivative = serializers.BooleanField()
 | |
|     commercial = serializers.BooleanField()
 | |
|     attribution = serializers.BooleanField()
 | |
|     copyleft = serializers.BooleanField()
 | |
| 
 | |
|     def get_id(self, obj) -> str:
 | |
|         return obj["identifiers"][0]
 | |
| 
 | |
|     class Meta:
 | |
|         model = models.License
 | |
| 
 | |
| 
 | |
| class ArtistAlbumSerializer(serializers.Serializer):
 | |
|     tracks_count = serializers.SerializerMethodField()
 | |
|     cover = CoverField(allow_null=True)
 | |
|     is_playable = serializers.SerializerMethodField()
 | |
|     is_local = serializers.BooleanField()
 | |
|     id = serializers.IntegerField()
 | |
|     fid = serializers.URLField()
 | |
|     mbid = serializers.UUIDField()
 | |
|     title = serializers.CharField()
 | |
|     artist_credit = serializers.SerializerMethodField()
 | |
|     release_date = serializers.DateField()
 | |
|     creation_date = serializers.DateTimeField()
 | |
| 
 | |
|     def get_artist_credit(self, o) -> int:
 | |
|         return [ac.id for ac in o.artist_credit.all()]
 | |
| 
 | |
|     def get_tracks_count(self, o) -> int:
 | |
|         return len(o.tracks.all())
 | |
| 
 | |
|     def get_is_playable(self, obj) -> bool:
 | |
|         try:
 | |
|             return bool(obj.is_playable_by_actor)
 | |
|         except AttributeError:
 | |
|             return None
 | |
| 
 | |
| 
 | |
| DATETIME_FIELD = serializers.DateTimeField()
 | |
| 
 | |
| 
 | |
| class InlineActorSerializer(serializers.Serializer):
 | |
|     full_username = serializers.CharField()
 | |
|     preferred_username = serializers.CharField()
 | |
|     domain = serializers.CharField(source="domain_id")
 | |
| 
 | |
| 
 | |
| class ArtistWithAlbumsInlineChannelSerializer(serializers.Serializer):
 | |
|     uuid = serializers.CharField()
 | |
|     actor = InlineActorSerializer()
 | |
| 
 | |
| 
 | |
| class ArtistWithAlbumsSerializer(OptionalDescriptionMixin, serializers.Serializer):
 | |
|     albums = serializers.SerializerMethodField()
 | |
|     tags = serializers.SerializerMethodField()
 | |
|     attributed_to = APIActorSerializer(allow_null=True)
 | |
|     channel = ArtistWithAlbumsInlineChannelSerializer(allow_null=True)
 | |
|     tracks_count = serializers.SerializerMethodField()
 | |
|     id = serializers.IntegerField()
 | |
|     fid = serializers.URLField()
 | |
|     mbid = serializers.UUIDField()
 | |
|     name = serializers.CharField()
 | |
|     content_category = serializers.CharField()
 | |
|     creation_date = serializers.DateTimeField()
 | |
|     is_local = serializers.BooleanField()
 | |
|     cover = CoverField(allow_null=True)
 | |
| 
 | |
|     def get_albums(self, artist):
 | |
|         albums = artist.artist_credit.albums()
 | |
|         return ArtistAlbumSerializer(albums, many=True).data
 | |
| 
 | |
|     @extend_schema_field({"type": "array", "items": {"type": "string"}})
 | |
|     def get_tags(self, obj):
 | |
|         tagged_items = getattr(obj, "_prefetched_tagged_items", [])
 | |
|         return [ti.tag.name for ti in tagged_items]
 | |
| 
 | |
|     def get_tracks_count(self, o) -> int:
 | |
|         return getattr(o, "_tracks_count", 0)
 | |
| 
 | |
| 
 | |
| class SimpleArtistSerializer(serializers.ModelSerializer):
 | |
|     attachment_cover = CoverField(allow_null=True, required=False)
 | |
|     description = common_serializers.ContentSerializer(allow_null=True, required=False)
 | |
|     channel = serializers.UUIDField(allow_null=True, required=False)
 | |
| 
 | |
|     class Meta:
 | |
|         model = models.Artist
 | |
|         fields = (
 | |
|             "id",
 | |
|             "fid",
 | |
|             "mbid",
 | |
|             "name",
 | |
|             "creation_date",
 | |
|             "modification_date",
 | |
|             "is_local",
 | |
|             "content_category",
 | |
|             "description",
 | |
|             "attachment_cover",
 | |
|             "channel",
 | |
|             "attributed_to",
 | |
|         )
 | |
| 
 | |
| 
 | |
| class ArtistCreditSerializer(serializers.ModelSerializer):
 | |
|     artist = SimpleArtistSerializer()
 | |
| 
 | |
|     class Meta:
 | |
|         model = models.ArtistCredit
 | |
|         fields = ["artist", "credit", "joinphrase", "index"]
 | |
| 
 | |
| 
 | |
| class AlbumSerializer(OptionalDescriptionMixin, serializers.Serializer):
 | |
|     artist_credit = ArtistCreditSerializer(many=True)
 | |
|     cover = CoverField(allow_null=True)
 | |
|     is_playable = serializers.SerializerMethodField()
 | |
|     tags = serializers.SerializerMethodField()
 | |
|     tracks_count = serializers.SerializerMethodField()
 | |
|     attributed_to = APIActorSerializer()
 | |
|     id = serializers.IntegerField()
 | |
|     fid = serializers.URLField()
 | |
|     mbid = serializers.UUIDField()
 | |
|     title = serializers.CharField()
 | |
|     release_date = serializers.DateField()
 | |
|     creation_date = serializers.DateTimeField()
 | |
|     is_local = serializers.BooleanField()
 | |
|     duration = serializers.SerializerMethodField(read_only=True)
 | |
| 
 | |
|     def get_tracks_count(self, o) -> int:
 | |
|         return len(o.tracks.all())
 | |
| 
 | |
|     def get_is_playable(self, obj) -> bool:
 | |
|         try:
 | |
|             return any(
 | |
|                 [
 | |
|                     bool(getattr(t, "is_playable_by_actor", None))
 | |
|                     for t in obj.tracks.all()
 | |
|                 ]
 | |
|             )
 | |
|         except AttributeError:
 | |
|             return None
 | |
| 
 | |
|     @extend_schema_field({"type": "array", "items": {"type": "string"}})
 | |
|     def get_tags(self, obj):
 | |
|         tagged_items = getattr(obj, "_prefetched_tagged_items", [])
 | |
|         return [ti.tag.name for ti in tagged_items]
 | |
| 
 | |
|     def get_duration(self, obj) -> int:
 | |
|         try:
 | |
|             return obj.duration
 | |
|         except AttributeError:
 | |
|             # no annotation?
 | |
|             return 0
 | |
| 
 | |
| 
 | |
| class TrackAlbumSerializer(serializers.ModelSerializer):
 | |
|     artist_credit = ArtistCreditSerializer(many=True)
 | |
|     cover = CoverField(allow_null=True)
 | |
|     tracks_count = serializers.SerializerMethodField()
 | |
| 
 | |
|     def get_tracks_count(self, o) -> int:
 | |
|         return getattr(o, "_prefetched_tracks_count", len(o.tracks.all()))
 | |
| 
 | |
|     class Meta:
 | |
|         model = models.Album
 | |
|         fields = (
 | |
|             "id",
 | |
|             "fid",
 | |
|             "mbid",
 | |
|             "title",
 | |
|             "artist_credit",
 | |
|             "release_date",
 | |
|             "cover",
 | |
|             "creation_date",
 | |
|             "is_local",
 | |
|             "tracks_count",
 | |
|         )
 | |
| 
 | |
| 
 | |
| class TrackUploadSerializer(serializers.Serializer):
 | |
|     uuid = serializers.UUIDField()
 | |
|     listen_url = serializers.URLField()
 | |
|     size = serializers.IntegerField()
 | |
|     duration = serializers.IntegerField()
 | |
|     bitrate = serializers.IntegerField()
 | |
|     mimetype = serializers.CharField()
 | |
|     extension = serializers.CharField()
 | |
|     is_local = serializers.SerializerMethodField()
 | |
| 
 | |
|     def get_is_local(self, upload) -> bool:
 | |
|         return federation_utils.is_local(upload.fid)
 | |
| 
 | |
| 
 | |
| def sort_uploads_for_listen(uploads):
 | |
|     """
 | |
|     Given a list of uploads, return a sorted list of uploads, with local or locally
 | |
|     cached ones first, and older first
 | |
|     """
 | |
|     score = {upload: 0 for upload in uploads}
 | |
|     for upload in uploads:
 | |
|         if upload.is_local:
 | |
|             score[upload] = 3
 | |
|         elif upload.audio_file:
 | |
|             score[upload] = 2
 | |
| 
 | |
|     sorted_tuples = sorted(score.items(), key=lambda t: (t[1], -t[0].pk), reverse=True)
 | |
|     return [t[0] for t in sorted_tuples]
 | |
| 
 | |
| 
 | |
| class TrackSerializer(OptionalDescriptionMixin, serializers.Serializer):
 | |
|     artist_credit = ArtistCreditSerializer(many=True)
 | |
|     album = TrackAlbumSerializer(read_only=True)
 | |
|     uploads = serializers.SerializerMethodField()
 | |
|     listen_url = serializers.SerializerMethodField()
 | |
|     tags = serializers.SerializerMethodField()
 | |
|     attributed_to = APIActorSerializer(allow_null=True)
 | |
| 
 | |
|     id = serializers.IntegerField()
 | |
|     fid = serializers.URLField()
 | |
|     mbid = serializers.UUIDField()
 | |
|     title = serializers.CharField()
 | |
|     creation_date = serializers.DateTimeField()
 | |
|     is_local = serializers.BooleanField()
 | |
|     position = serializers.IntegerField()
 | |
|     disc_number = serializers.IntegerField()
 | |
|     downloads_count = serializers.IntegerField()
 | |
|     copyright = serializers.CharField()
 | |
|     license = serializers.SerializerMethodField()
 | |
|     cover = CoverField(allow_null=True)
 | |
|     is_playable = serializers.SerializerMethodField()
 | |
| 
 | |
|     @extend_schema_field(OpenApiTypes.URI)
 | |
|     def get_listen_url(self, obj):
 | |
|         return obj.listen_url
 | |
| 
 | |
|     #  @extend_schema_field({"type": "array", "items": {"type": "object"}})
 | |
|     @extend_schema_field(TrackUploadSerializer(many=True))
 | |
|     def get_uploads(self, obj):
 | |
|         uploads = getattr(obj, "playable_uploads", [])
 | |
|         # we put local uploads first
 | |
|         uploads = [
 | |
|             TrackUploadSerializer(u).data for u in sort_uploads_for_listen(uploads)
 | |
|         ]
 | |
|         uploads = sorted(uploads, key=lambda u: u["is_local"], reverse=True)
 | |
|         return list(uploads)
 | |
| 
 | |
|     @extend_schema_field({"type": "array", "items": {"type": "string"}})
 | |
|     def get_tags(self, obj):
 | |
|         tagged_items = getattr(obj, "_prefetched_tagged_items", [])
 | |
|         return [ti.tag.name for ti in tagged_items]
 | |
| 
 | |
|     def get_license(self, o) -> str:
 | |
|         return o.license_id
 | |
| 
 | |
|     def get_is_playable(self, obj) -> bool:
 | |
|         return bool(getattr(obj, "playable_uploads", []))
 | |
| 
 | |
| 
 | |
| @common_serializers.track_fields_for_update("name", "description", "privacy_level")
 | |
| class LibraryForOwnerSerializer(serializers.ModelSerializer):
 | |
|     uploads_count = serializers.SerializerMethodField()
 | |
|     size = serializers.SerializerMethodField()
 | |
|     actor = serializers.SerializerMethodField()
 | |
| 
 | |
|     class Meta:
 | |
|         model = models.Library
 | |
|         fields = [
 | |
|             "uuid",
 | |
|             "fid",
 | |
|             "name",
 | |
|             "privacy_level",
 | |
|             "uploads_count",
 | |
|             "size",
 | |
|             "creation_date",
 | |
|             "actor",
 | |
|         ]
 | |
|         read_only_fields = ["fid", "uuid", "creation_date", "actor"]
 | |
| 
 | |
|     def get_uploads_count(self, o) -> int:
 | |
|         return getattr(o, "_uploads_count", int(o.uploads_count))
 | |
| 
 | |
|     def get_size(self, o) -> int:
 | |
|         return getattr(o, "_size", 0)
 | |
| 
 | |
|     def on_updated_fields(self, obj, before, after):
 | |
|         routes.outbox.dispatch(
 | |
|             {"type": "Update", "object": {"type": "Library"}}, context={"library": obj}
 | |
|         )
 | |
| 
 | |
|     @extend_schema_field(APIActorSerializer)
 | |
|     def get_actor(self, o):
 | |
|         return APIActorSerializer(o.actor).data
 | |
| 
 | |
| 
 | |
| class UploadSerializer(serializers.ModelSerializer):
 | |
|     from funkwhale_api.audio.serializers import ChannelSerializer
 | |
| 
 | |
|     track = TrackSerializer(required=False, allow_null=True)
 | |
|     library = common_serializers.RelatedField(
 | |
|         "uuid",
 | |
|         LibraryForOwnerSerializer(),
 | |
|         required=False,
 | |
|         filters=lambda context: {"actor": context["user"].actor},
 | |
|     )
 | |
|     channel = common_serializers.RelatedField(
 | |
|         "uuid",
 | |
|         ChannelSerializer(),
 | |
|         required=False,
 | |
|         filters=lambda context: {"attributed_to": context["user"].actor},
 | |
|     )
 | |
| 
 | |
|     class Meta:
 | |
|         model = models.Upload
 | |
|         fields = [
 | |
|             "uuid",
 | |
|             "filename",
 | |
|             "creation_date",
 | |
|             "mimetype",
 | |
|             "track",
 | |
|             "library",
 | |
|             "channel",
 | |
|             "duration",
 | |
|             "mimetype",
 | |
|             "bitrate",
 | |
|             "size",
 | |
|             "import_date",
 | |
|             "import_status",
 | |
|         ]
 | |
| 
 | |
|         read_only_fields = [
 | |
|             "uuid",
 | |
|             "creation_date",
 | |
|             "duration",
 | |
|             "mimetype",
 | |
|             "bitrate",
 | |
|             "size",
 | |
|             "track",
 | |
|             "import_date",
 | |
|         ]
 | |
| 
 | |
|     def validate(self, data):
 | |
|         validated_data = super().validate(data)
 | |
|         if "audio_file" in validated_data:
 | |
|             audio_data = utils.get_audio_file_data(validated_data["audio_file"])
 | |
|             if audio_data:
 | |
|                 validated_data["duration"] = audio_data["length"]
 | |
|                 validated_data["bitrate"] = audio_data["bitrate"]
 | |
|         return validated_data
 | |
| 
 | |
| 
 | |
| def filter_album(qs, context):
 | |
|     if "channel" in context:
 | |
|         return qs.filter(artist_credit__artist__channel=context["channel"])
 | |
|     if "actor" in context:
 | |
|         return qs.filter(artist_credit__artist__attributed_to=context["actor"])
 | |
| 
 | |
|     return qs.none()
 | |
| 
 | |
| 
 | |
| class ImportMetadataSerializer(serializers.Serializer):
 | |
|     title = serializers.CharField(max_length=500, required=True)
 | |
|     description = serializers.CharField(
 | |
|         max_length=5000, required=False, allow_null=True
 | |
|     )
 | |
|     mbid = serializers.UUIDField(required=False, allow_null=True)
 | |
|     copyright = serializers.CharField(max_length=500, required=False, allow_null=True)
 | |
|     position = serializers.IntegerField(min_value=1, required=False, allow_null=True)
 | |
|     tags = tags_serializers.TagsListField(required=False)
 | |
|     license = common_serializers.RelatedField(
 | |
|         "code", LicenseSerializer(), required=False, allow_null=True
 | |
|     )
 | |
|     cover = common_serializers.RelatedField(
 | |
|         "uuid",
 | |
|         queryset=common_models.Attachment.objects.all().local(),
 | |
|         serializer=None,
 | |
|         queryset_filter=lambda qs, context: qs.filter(actor=context["actor"]),
 | |
|         write_only=True,
 | |
|         required=False,
 | |
|         allow_null=True,
 | |
|     )
 | |
|     album = common_serializers.RelatedField(
 | |
|         "id",
 | |
|         queryset=models.Album.objects.all(),
 | |
|         serializer=None,
 | |
|         queryset_filter=filter_album,
 | |
|         write_only=True,
 | |
|         required=False,
 | |
|         allow_null=True,
 | |
|     )
 | |
| 
 | |
| 
 | |
| class ImportMetadataField(serializers.JSONField):
 | |
|     def to_internal_value(self, v):
 | |
|         v = super().to_internal_value(v)
 | |
|         s = ImportMetadataSerializer(
 | |
|             data=v, context={"actor": self.context["user"].actor}
 | |
|         )
 | |
|         s.is_valid(raise_exception=True)
 | |
|         return v
 | |
| 
 | |
| 
 | |
| class UploadForOwnerSerializer(UploadSerializer):
 | |
|     import_status = serializers.ChoiceField(
 | |
|         choices=models.TRACK_FILE_IMPORT_STATUS_CHOICES, default="pending"
 | |
|     )
 | |
|     import_metadata = ImportMetadataField(required=False)
 | |
|     filename = serializers.CharField(required=False)
 | |
| 
 | |
|     class Meta(UploadSerializer.Meta):
 | |
|         fields = UploadSerializer.Meta.fields + [
 | |
|             "import_details",
 | |
|             "import_metadata",
 | |
|             "import_reference",
 | |
|             "metadata",
 | |
|             "source",
 | |
|             "audio_file",
 | |
|         ]
 | |
|         extra_kwargs = {"audio_file": {"write_only": True}}
 | |
|         read_only_fields = UploadSerializer.Meta.read_only_fields + [
 | |
|             "import_details",
 | |
|             "metadata",
 | |
|         ]
 | |
| 
 | |
|     def to_representation(self, obj):
 | |
|         r = super().to_representation(obj)
 | |
|         if "audio_file" in r:
 | |
|             del r["audio_file"]
 | |
|         return r
 | |
| 
 | |
|     def validate(self, validated_data):
 | |
|         if (
 | |
|             not self.instance
 | |
|             and "library" not in validated_data
 | |
|             and "channel" not in validated_data
 | |
|         ):
 | |
|             raise serializers.ValidationError(
 | |
|                 "You need to specify a channel or a library"
 | |
|             )
 | |
|         if (
 | |
|             not self.instance
 | |
|             and "library" in validated_data
 | |
|             and "channel" in validated_data
 | |
|         ):
 | |
|             raise serializers.ValidationError(
 | |
|                 "You may specify a channel or a library, not both"
 | |
|             )
 | |
|         if "audio_file" in validated_data:
 | |
|             self.validate_upload_quota(validated_data["audio_file"])
 | |
| 
 | |
|         if "channel" in validated_data:
 | |
|             validated_data["library"] = validated_data.pop("channel").library
 | |
| 
 | |
|         if "import_status" in validated_data and validated_data[
 | |
|             "import_status"
 | |
|         ] not in ["draft", "pending"]:
 | |
|             raise serializers.ValidationError(
 | |
|                 "Newly created Uploads need to have import_status of draft or pending"
 | |
|             )
 | |
|         return super().validate(validated_data)
 | |
| 
 | |
|     def validate_upload_quota(self, f):
 | |
|         quota_status = self.context["user"].get_quota_status()
 | |
|         if (f.size / 1000 / 1000) > quota_status["remaining"]:
 | |
|             raise serializers.ValidationError("upload_quota_reached")
 | |
| 
 | |
|         return f
 | |
| 
 | |
| 
 | |
| class UploadBulkUpdateSerializer(serializers.Serializer):
 | |
|     uuid = serializers.UUIDField()
 | |
|     privacy_level = serializers.ChoiceField(
 | |
|         choices=models.LIBRARY_PRIVACY_LEVEL_CHOICES
 | |
|     )
 | |
| 
 | |
|     def validate(self, data):
 | |
|         try:
 | |
|             upload = models.Upload.objects.get(uuid=data["uuid"])
 | |
|         except models.Upload.DoesNotExist:
 | |
|             raise serializers.ValidationError(
 | |
|                 f"Upload with uuid {data['uuid']} does not exist"
 | |
|             )
 | |
| 
 | |
|         upload.library = upload.library.actor.libraries.get(
 | |
|             privacy_level=data["privacy_level"]
 | |
|         )
 | |
|         return upload
 | |
| 
 | |
| 
 | |
| class UploadActionSerializer(common_serializers.ActionSerializer):
 | |
|     actions = [
 | |
|         common_serializers.Action("delete", allow_all=True),
 | |
|         common_serializers.Action("relaunch_import", allow_all=True),
 | |
|         common_serializers.Action("publish", allow_all=False),
 | |
|     ]
 | |
|     filterset_class = filters.UploadFilter
 | |
|     pk_field = "uuid"
 | |
| 
 | |
|     @transaction.atomic
 | |
|     def handle_delete(self, objects):
 | |
|         libraries = sorted(set(objects.values_list("library", flat=True)))
 | |
|         for id in libraries:
 | |
|             # we group deletes by library for easier federation
 | |
|             uploads = objects.filter(library__pk=id).select_related("library__actor")
 | |
|             for chunk in common_utils.chunk_queryset(uploads, 100):
 | |
|                 routes.outbox.dispatch(
 | |
|                     {"type": "Delete", "object": {"type": "Audio"}},
 | |
|                     context={"uploads": chunk},
 | |
|                 )
 | |
| 
 | |
|         return objects.delete()
 | |
| 
 | |
|     @transaction.atomic
 | |
|     def handle_relaunch_import(self, objects):
 | |
|         qs = objects.filter(import_status__in=["pending", "skipped", "errored"])
 | |
|         pks = list(qs.values_list("id", flat=True))
 | |
|         qs.update(import_status="pending")
 | |
|         for pk in pks:
 | |
|             common_utils.on_commit(tasks.process_upload.delay, upload_id=pk)
 | |
| 
 | |
|     @transaction.atomic
 | |
|     def handle_publish(self, objects):
 | |
|         qs = objects.filter(import_status="draft")
 | |
|         pks = list(qs.values_list("id", flat=True))
 | |
|         qs.update(import_status="pending")
 | |
|         for pk in pks:
 | |
|             common_utils.on_commit(tasks.process_upload.delay, upload_id=pk)
 | |
| 
 | |
| 
 | |
| class SimpleAlbumSerializer(serializers.ModelSerializer):
 | |
|     cover = CoverField(allow_null=True)
 | |
| 
 | |
|     class Meta:
 | |
|         model = models.Album
 | |
|         fields = ("id", "mbid", "title", "release_date", "cover")
 | |
| 
 | |
| 
 | |
| class TrackActivitySerializer(activity_serializers.ModelSerializer):
 | |
|     type = serializers.SerializerMethodField()
 | |
|     name = serializers.CharField(source="title")
 | |
|     artist_credit = serializers.CharField(source="get_artist_credit_string")
 | |
|     album = serializers.SerializerMethodField()
 | |
| 
 | |
|     class Meta:
 | |
|         model = models.Track
 | |
|         fields = ["id", "local_id", "name", "type", "artist_credit", "album"]
 | |
| 
 | |
|     def get_type(self, obj):
 | |
|         return "Audio"
 | |
| 
 | |
|     def get_album(self, o):
 | |
|         if o.album:
 | |
|             return o.album.title
 | |
| 
 | |
| 
 | |
| def get_embed_url(type, id):
 | |
|     return settings.FUNKWHALE_EMBED_URL + f"?type={type}&id={id}"
 | |
| 
 | |
| 
 | |
| class OembedSerializer(serializers.Serializer):
 | |
|     format = serializers.ChoiceField(choices=["json"])
 | |
|     url = serializers.URLField()
 | |
|     maxheight = serializers.IntegerField(required=False)
 | |
|     maxwidth = serializers.IntegerField(required=False)
 | |
| 
 | |
|     def validate(self, validated_data):
 | |
|         try:
 | |
|             match = common_utils.spa_resolve(
 | |
|                 urllib.parse.urlparse(validated_data["url"]).path
 | |
|             )
 | |
|         except urls.exceptions.Resolver404:
 | |
|             raise serializers.ValidationError(
 | |
|                 "Invalid URL {}".format(validated_data["url"])
 | |
|             )
 | |
|         data = {
 | |
|             "version": "1.0",
 | |
|             "type": "rich",
 | |
|             "provider_name": settings.APP_NAME,
 | |
|             "provider_url": settings.FUNKWHALE_URL,
 | |
|             "height": validated_data.get("maxheight") or 400,
 | |
|             "width": validated_data.get("maxwidth") or 600,
 | |
|         }
 | |
|         embed_id = None
 | |
|         embed_type = None
 | |
|         if match.url_name == "library_track":
 | |
|             qs = models.Track.objects.prefetch_related(
 | |
|                 "artist_credit", "album__artist_credit"
 | |
|             ).filter(pk=int(match.kwargs["pk"]))
 | |
|             try:
 | |
|                 track = qs.get()
 | |
|             except models.Track.DoesNotExist:
 | |
|                 raise serializers.ValidationError(
 | |
|                     "No track matching id {}".format(match.kwargs["pk"])
 | |
|                 )
 | |
|             embed_type = "track"
 | |
|             embed_id = track.pk
 | |
|             data["title"] = f"{track.title} by {track.get_artist_credit_string}"
 | |
|             if track.attachment_cover:
 | |
|                 data[
 | |
|                     "thumbnail_url"
 | |
|                 ] = track.attachment_cover.download_url_medium_square_crop
 | |
|                 data["thumbnail_width"] = 200
 | |
|                 data["thumbnail_height"] = 200
 | |
|             elif track.album and track.album.attachment_cover:
 | |
|                 data[
 | |
|                     "thumbnail_url"
 | |
|                 ] = track.album.attachment_cover.download_url_medium_square_crop
 | |
|                 data["thumbnail_width"] = 200
 | |
|                 data["thumbnail_height"] = 200
 | |
|             data["description"] = track.full_name
 | |
|             data["author_name"] = track.get_artist_credit_string
 | |
|             data["height"] = 150
 | |
|             # here we take the first artist since oembed standard do not allow a list of url
 | |
|             data["author_url"] = federation_utils.full_url(
 | |
|                 common_utils.spa_reverse(
 | |
|                     "library_artist",
 | |
|                     kwargs={"pk": track.artist_credit.all()[0].artist.pk},
 | |
|                 )
 | |
|             )
 | |
|         elif match.url_name == "library_album":
 | |
|             qs = models.Album.objects.prefetch_related("artist_credit").filter(
 | |
|                 pk=int(match.kwargs["pk"])
 | |
|             )
 | |
|             try:
 | |
|                 album = qs.get()
 | |
|             except models.Album.DoesNotExist:
 | |
|                 raise serializers.ValidationError(
 | |
|                     "No album matching id {}".format(match.kwargs["pk"])
 | |
|                 )
 | |
|             embed_type = "album"
 | |
|             embed_id = album.pk
 | |
|             if album.attachment_cover:
 | |
|                 data[
 | |
|                     "thumbnail_url"
 | |
|                 ] = album.attachment_cover.download_url_medium_square_crop
 | |
|                 data["thumbnail_width"] = 200
 | |
|                 data["thumbnail_height"] = 200
 | |
|             data["title"] = f"{album.title} by {album.get_artist_credit_string}"
 | |
|             data["description"] = f"{album.title} by {album.get_artist_credit_string}"
 | |
|             data["author_name"] = album.get_artist_credit_string
 | |
|             data["height"] = 400
 | |
|             data["author_url"] = federation_utils.full_url(
 | |
|                 common_utils.spa_reverse(
 | |
|                     "library_artist",
 | |
|                     kwargs={"pk": album.artist_credit.all()[0].artist.pk},
 | |
|                 )
 | |
|             )
 | |
| 
 | |
|         elif match.url_name == "library_artist":
 | |
|             qs = models.Artist.objects.filter(pk=int(match.kwargs["pk"]))
 | |
|             try:
 | |
|                 artist = qs.get()
 | |
|             except models.Artist.DoesNotExist:
 | |
|                 raise serializers.ValidationError(
 | |
|                     "No artist matching id {}".format(match.kwargs["pk"])
 | |
|                 )
 | |
|             embed_type = "artist"
 | |
|             embed_id = artist.pk
 | |
|             album_ids = (
 | |
|                 artist.artist_credit.all()
 | |
|                 .prefetch_related("albums")
 | |
|                 .values_list("albums", flat=True)
 | |
|             )
 | |
|             album = (
 | |
|                 models.Album.objects.exclude(attachment_cover=None)
 | |
|                 .filter(pk__in=album_ids)
 | |
|                 .order_by("-id")
 | |
|                 .first()
 | |
|             )
 | |
| 
 | |
|             if album and album.attachment_cover:
 | |
|                 data[
 | |
|                     "thumbnail_url"
 | |
|                 ] = album.attachment_cover.download_url_medium_square_crop
 | |
|                 data["thumbnail_width"] = 200
 | |
|                 data["thumbnail_height"] = 200
 | |
|             data["title"] = artist.name
 | |
|             data["description"] = artist.name
 | |
|             data["author_name"] = artist.name
 | |
|             data["height"] = 400
 | |
|             data["author_url"] = federation_utils.full_url(
 | |
|                 common_utils.spa_reverse("library_artist", kwargs={"pk": artist.pk})
 | |
|             )
 | |
|         elif match.url_name == "channel_detail":
 | |
|             from funkwhale_api.audio.models import Channel
 | |
| 
 | |
|             kwargs = {}
 | |
|             if "uuid" in match.kwargs:
 | |
|                 kwargs["uuid"] = match.kwargs["uuid"]
 | |
|             else:
 | |
|                 username_data = federation_utils.get_actor_data_from_username(
 | |
|                     match.kwargs["username"]
 | |
|                 )
 | |
|                 kwargs["actor__domain"] = username_data["domain"]
 | |
|                 kwargs["actor__preferred_username__iexact"] = username_data["username"]
 | |
|             qs = Channel.objects.filter(**kwargs).select_related(
 | |
|                 "artist__attachment_cover"
 | |
|             )
 | |
|             try:
 | |
|                 channel = qs.get()
 | |
|             except models.Artist.DoesNotExist:
 | |
|                 raise serializers.ValidationError(
 | |
|                     "No channel matching id {}".format(match.kwargs["uuid"])
 | |
|                 )
 | |
|             embed_type = "channel"
 | |
|             embed_id = channel.uuid
 | |
| 
 | |
|             if channel.artist.attachment_cover:
 | |
|                 data[
 | |
|                     "thumbnail_url"
 | |
|                 ] = channel.artist.attachment_cover.download_url_medium_square_crop
 | |
|                 data["thumbnail_width"] = 200
 | |
|                 data["thumbnail_height"] = 200
 | |
|             data["title"] = channel.artist.name
 | |
|             data["description"] = channel.artist.name
 | |
|             data["author_name"] = channel.artist.name
 | |
|             data["height"] = 400
 | |
|             data["author_url"] = federation_utils.full_url(
 | |
|                 common_utils.spa_reverse(
 | |
|                     "channel_detail", kwargs={"uuid": channel.uuid}
 | |
|                 )
 | |
|             )
 | |
|         elif match.url_name == "library_playlist":
 | |
|             qs = playlists_models.Playlist.objects.filter(
 | |
|                 pk=int(match.kwargs["pk"]), privacy_level="everyone"
 | |
|             )
 | |
|             try:
 | |
|                 obj = qs.get()
 | |
|             except playlists_models.Playlist.DoesNotExist:
 | |
|                 raise serializers.ValidationError(
 | |
|                     "No artist matching id {}".format(match.kwargs["pk"])
 | |
|                 )
 | |
|             embed_type = "playlist"
 | |
|             embed_id = obj.pk
 | |
|             playlist_tracks = obj.playlist_tracks.exclude(
 | |
|                 track__album__attachment_cover=None
 | |
|             )
 | |
|             playlist_tracks = playlist_tracks.select_related(
 | |
|                 "track__album__attachment_cover"
 | |
|             ).order_by("index")
 | |
|             first_playlist_track = playlist_tracks.first()
 | |
| 
 | |
|             if first_playlist_track:
 | |
|                 data[
 | |
|                     "thumbnail_url"
 | |
|                 ] = (
 | |
|                     first_playlist_track.track.album.attachment_cover.download_url_medium_square_crop
 | |
|                 )
 | |
|                 data["thumbnail_width"] = 200
 | |
|                 data["thumbnail_height"] = 200
 | |
|             data["title"] = obj.name
 | |
|             data["description"] = obj.name
 | |
|             data["author_name"] = obj.name
 | |
|             data["height"] = 400
 | |
|             data["author_url"] = federation_utils.full_url(
 | |
|                 common_utils.spa_reverse("library_playlist", kwargs={"pk": obj.pk})
 | |
|             )
 | |
|         else:
 | |
|             raise serializers.ValidationError(
 | |
|                 "Unsupported url: {}".format(validated_data["url"])
 | |
|             )
 | |
|         data[
 | |
|             "html"
 | |
|         ] = '<iframe width="{}" height="{}" scrolling="no" frameborder="no" src="{}"></iframe>'.format(
 | |
|             data["width"], data["height"], get_embed_url(embed_type, embed_id)
 | |
|         )
 | |
|         return data
 | |
| 
 | |
|     def create(self, data):
 | |
|         return data
 | |
| 
 | |
| 
 | |
| class AlbumCreateSerializer(serializers.Serializer):
 | |
|     title = serializers.CharField(required=True, max_length=255)
 | |
|     cover = COVER_WRITE_FIELD
 | |
|     release_date = serializers.DateField(required=False, allow_null=True)
 | |
|     tags = tags_serializers.TagsListField(required=False)
 | |
|     description = common_serializers.ContentSerializer(allow_null=True, required=False)
 | |
|     # only used in album channel creation, so this is not a list
 | |
|     artist_credit = common_serializers.RelatedField(
 | |
|         "id",
 | |
|         queryset=models.ArtistCredit.objects.exclude(artist__channel__isnull=True),
 | |
|         required=True,
 | |
|         serializer=None,
 | |
|         many=True,
 | |
|         filters=lambda context: {"artist__attributed_to": context["user"].actor},
 | |
|     )
 | |
| 
 | |
|     def validate(self, validated_data):
 | |
|         duplicates = models.Album.objects.none()
 | |
|         for ac in validated_data["artist_credit"]:
 | |
|             duplicates = duplicates | ac.albums.filter(
 | |
|                 title__iexact=validated_data["title"]
 | |
|             )
 | |
|         if duplicates.exists():
 | |
|             raise serializers.ValidationError("An album with this title already exist")
 | |
| 
 | |
|         return super().validate(validated_data)
 | |
| 
 | |
|     def to_representation(self, obj):
 | |
|         return AlbumSerializer(obj, context=self.context).data
 | |
| 
 | |
|     @transaction.atomic
 | |
|     def create(self, validated_data):
 | |
|         instance = models.Album.objects.create(
 | |
|             attributed_to=self.context["user"].actor,
 | |
|             release_date=validated_data.get("release_date"),
 | |
|             title=validated_data["title"],
 | |
|             attachment_cover=validated_data.get("cover"),
 | |
|         )
 | |
|         common_utils.attach_content(
 | |
|             instance, "description", validated_data.get("description")
 | |
|         )
 | |
|         tag_models.set_tags(instance, *(validated_data.get("tags", []) or []))
 | |
| 
 | |
|         instance.artist_credit.set(validated_data["artist_credit"])
 | |
|         return instance
 | |
| 
 | |
| 
 | |
| class FSImportSerializer(serializers.Serializer):
 | |
|     path = serializers.CharField(allow_blank=True)
 | |
|     library = serializers.UUIDField()
 | |
|     import_reference = serializers.CharField()
 | |
|     prune = serializers.BooleanField(required=False, default=True)
 | |
|     outbox = serializers.BooleanField(required=False, default=False)
 | |
|     broadcast = serializers.BooleanField(required=False, default=False)
 | |
|     batch_size = serializers.IntegerField(required=False, default=1000)
 | |
|     verbosity = serializers.IntegerField(required=False, default=1)
 | |
| 
 | |
|     def validate_path(self, value):
 | |
|         try:
 | |
|             utils.browse_dir(settings.MUSIC_DIRECTORY_PATH, value)
 | |
|         except (NotADirectoryError, FileNotFoundError, ValueError):
 | |
|             raise serializers.ValidationError("Invalid path")
 | |
| 
 | |
|         return value
 | |
| 
 | |
|     def validate_library(self, value):
 | |
|         try:
 | |
|             return self.context["user"].actor.libraries.get(uuid=value)
 | |
|         except models.Library.DoesNotExist:
 | |
|             raise serializers.ValidationError("Invalid library")
 | |
| 
 | |
| 
 | |
| class SearchResultSerializer(serializers.Serializer):
 | |
|     artists = ArtistWithAlbumsSerializer(many=True)
 | |
|     tracks = TrackSerializer(many=True)
 | |
|     albums = AlbumSerializer(many=True)
 | |
|     tags = tags_serializers.TagSerializer(many=True)
 | |
| 
 | |
| 
 | |
| class UploadGroupSerializer(serializers.ModelSerializer):
 | |
|     class Meta:
 | |
|         model = models.UploadGroup
 | |
|         fields = ["guid", "name", "createdAt", "uploadUrl"]
 | |
| 
 | |
|     name = serializers.CharField(required=False)
 | |
|     uploadUrl = serializers.URLField(read_only=True, source="upload_url")
 | |
|     createdAt = serializers.DateTimeField(read_only=True, source="created_at")
 | |
| 
 | |
| 
 | |
| 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")
 |