Skip to content
Snippets Groups Projects
serializers.py 27.2 KiB
Newer Older
  • Learn to ignore specific revisions
  • import urllib.parse
    
    
    from django.core.paginator import Paginator
    
    from rest_framework import serializers
    
    from funkwhale_api.common import serializers as common_serializers
    
    Eliot Berriot's avatar
    Eliot Berriot committed
    from funkwhale_api.common import utils as funkwhale_utils
    
    from funkwhale_api.music import models as music_models
    from funkwhale_api.music import tasks as music_tasks
    
    Eliot Berriot's avatar
    Eliot Berriot committed
    from . import activity, filters, models, utils
    
    Eliot Berriot's avatar
    Eliot Berriot committed
    AP_CONTEXT = [
    
    Eliot Berriot's avatar
    Eliot Berriot committed
        "https://www.w3.org/ns/activitystreams",
        "https://w3id.org/security/v1",
    
    logger = logging.getLogger(__name__)
    
    
    class ActorSerializer(serializers.Serializer):
    
        id = serializers.URLField(max_length=500)
        outbox = serializers.URLField(max_length=500)
        inbox = serializers.URLField(max_length=500)
    
        type = serializers.ChoiceField(choices=models.TYPE_CHOICES)
        preferredUsername = serializers.CharField()
        manuallyApprovesFollowers = serializers.NullBooleanField(required=False)
        name = serializers.CharField(required=False, max_length=200)
    
        summary = serializers.CharField(max_length=None, required=False)
    
        followers = serializers.URLField(max_length=500, required=False, allow_null=True)
        following = serializers.URLField(max_length=500, required=False, allow_null=True)
    
        publicKey = serializers.JSONField(required=False)
    
        def to_representation(self, instance):
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                "id": instance.url,
                "outbox": instance.outbox_url,
                "inbox": instance.inbox_url,
                "preferredUsername": instance.preferred_username,
                "type": instance.type,
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                ret["name"] = instance.name
    
            if instance.followers_url:
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                ret["followers"] = instance.followers_url
    
            if instance.following_url:
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                ret["following"] = instance.following_url
    
            if instance.summary:
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                ret["summary"] = instance.summary
    
            if instance.manually_approves_followers is not None:
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                ret["manuallyApprovesFollowers"] = instance.manually_approves_followers
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            ret["@context"] = AP_CONTEXT
    
            if instance.public_key:
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                ret["publicKey"] = {
                    "owner": instance.url,
                    "publicKeyPem": instance.public_key,
                    "id": "{}#main-key".format(instance.url),
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            ret["endpoints"] = {}
    
            if instance.shared_inbox_url:
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                ret["endpoints"]["sharedInbox"] = instance.shared_inbox_url
    
            return ret
    
        def prepare_missing_fields(self):
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                "url": self.validated_data["id"],
                "outbox_url": self.validated_data["outbox"],
                "inbox_url": self.validated_data["inbox"],
                "following_url": self.validated_data.get("following"),
                "followers_url": self.validated_data.get("followers"),
                "summary": self.validated_data.get("summary"),
                "type": self.validated_data["type"],
                "name": self.validated_data.get("name"),
                "preferred_username": self.validated_data["preferredUsername"],
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            maf = self.validated_data.get("manuallyApprovesFollowers")
    
            if maf is not None:
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                kwargs["manually_approves_followers"] = maf
            domain = urllib.parse.urlparse(kwargs["url"]).netloc
            kwargs["domain"] = domain
            for endpoint, url in self.initial_data.get("endpoints", {}).items():
                if endpoint == "sharedInbox":
                    kwargs["shared_inbox_url"] = url
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                kwargs["public_key"] = self.initial_data["publicKey"]["publicKeyPem"]
    
            except KeyError:
                pass
            return kwargs
    
        def build(self):
    
            d = self.prepare_missing_fields()
            return models.Actor(**d)
    
    
        def save(self, **kwargs):
    
            d = self.prepare_missing_fields()
            d.update(kwargs)
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            return models.Actor.objects.update_or_create(url=d["url"], defaults=d)[0]
    
        def validate_summary(self, value):
            if value:
                return value[:500]
    
    
    
    class APIActorSerializer(serializers.ModelSerializer):
        class Meta:
            model = models.Actor
            fields = [
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                "id",
                "url",
                "creation_date",
                "summary",
                "preferred_username",
                "name",
                "last_fetch_date",
                "domain",
                "type",
                "manually_approves_followers",
    
    class LibraryActorSerializer(ActorSerializer):
    
    Eliot Berriot's avatar
    Eliot Berriot committed
        url = serializers.ListField(child=serializers.JSONField())
    
    
        def validate(self, validated_data):
            try:
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                urls = validated_data["url"]
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                raise serializers.ValidationError("Missing URL field")
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                    if u["name"] != "library":
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                    validated_data["library_url"] = u["href"]
    
                    break
                except KeyError:
                    continue
    
            return validated_data
    
    
    
    class APIFollowSerializer(serializers.ModelSerializer):
        class Meta:
            model = models.Follow
            fields = [
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                "uuid",
                "actor",
                "target",
                "approved",
                "creation_date",
                "modification_date",
    
    Eliot Berriot's avatar
    Eliot Berriot committed
    class APILibrarySerializer(serializers.ModelSerializer):
    
        actor = APIActorSerializer()
        follow = APIFollowSerializer()
    
    Eliot Berriot's avatar
    Eliot Berriot committed
    
        class Meta:
            model = models.Library
    
    
            read_only_fields = [
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                "actor",
                "uuid",
                "url",
                "tracks_count",
                "follow",
                "fetched_date",
                "modification_date",
                "creation_date",
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                "autoimport",
                "federation_enabled",
                "download_files",
    
            ] + read_only_fields
    
    class APILibraryScanSerializer(serializers.Serializer):
        until = serializers.DateTimeField(required=False)
    
    
    
    class APILibraryFollowUpdateSerializer(serializers.Serializer):
        follow = serializers.IntegerField()
        approved = serializers.BooleanField()
    
        def validate_follow(self, value):
            from . import actors
    
    Eliot Berriot's avatar
    Eliot Berriot committed
    
            library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
            qs = models.Follow.objects.filter(pk=value, target=library_actor)
    
            try:
                return qs.get()
            except models.Follow.DoesNotExist:
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                raise serializers.ValidationError("Invalid follow")
    
    
        def save(self):
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            new_status = self.validated_data["approved"]
            follow = self.validated_data["follow"]
    
            if new_status == follow.approved:
                return follow
    
            follow.approved = new_status
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            follow.save(update_fields=["approved", "modification_date"])
    
            if new_status:
                activity.accept_follow(follow)
            return follow
    
    
    
    class APILibraryCreateSerializer(serializers.ModelSerializer):
    
        actor = serializers.URLField(max_length=500)
    
        federation_enabled = serializers.BooleanField()
    
        uuid = serializers.UUIDField(read_only=True)
    
    
        class Meta:
            model = models.Library
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            fields = ["uuid", "actor", "autoimport", "federation_enabled", "download_files"]
    
    
        def validate(self, validated_data):
            from . import actors
            from . import library
    
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            actor_url = validated_data["actor"]
    
            actor_data = actors.get_actor_data(actor_url)
            acs = LibraryActorSerializer(data=actor_data)
            acs.is_valid(raise_exception=True)
            try:
                actor = models.Actor.objects.get(url=actor_url)
            except models.Actor.DoesNotExist:
                actor = acs.save()
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
            validated_data["follow"] = models.Follow.objects.get_or_create(
                actor=library_actor, target=actor
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            if validated_data["follow"].approved is None:
    
                funkwhale_utils.on_commit(
                    activity.deliver,
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                    FollowSerializer(validated_data["follow"]).data,
                    on_behalf_of=validated_data["follow"].actor,
                    to=[validated_data["follow"].target.url],
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            library_data = library.get_library_data(acs.validated_data["library_url"])
            if "errors" in library_data:
    
                # we pass silently because it may means we require permission
                # before scanning
                pass
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            validated_data["library"] = library_data
            validated_data["library"].setdefault("id", acs.validated_data["library_url"])
            validated_data["actor"] = actor
    
            return validated_data
    
        def create(self, validated_data):
    
            library = models.Library.objects.update_or_create(
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                url=validated_data["library"]["id"],
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                    "actor": validated_data["actor"],
                    "follow": validated_data["follow"],
                    "tracks_count": validated_data["library"].get("totalItems"),
                    "federation_enabled": validated_data["federation_enabled"],
                    "autoimport": validated_data["autoimport"],
                    "download_files": validated_data["download_files"],
                },
    
    class APILibraryTrackSerializer(serializers.ModelSerializer):
        library = APILibrarySerializer()
    
        status = serializers.SerializerMethodField()
    
    
        class Meta:
            model = models.LibraryTrack
            fields = [
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                "id",
                "url",
                "audio_url",
                "audio_mimetype",
                "creation_date",
                "modification_date",
                "fetched_date",
                "published_date",
                "metadata",
                "artist_name",
                "album_title",
                "title",
                "library",
                "local_track_file",
                "status",
    
        def get_status(self, o):
            try:
                if o.local_track_file is not None:
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                    return "imported"
    
            except music_models.TrackFile.DoesNotExist:
                pass
            for job in o.import_jobs.all():
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                if job.status == "pending":
                    return "import_pending"
            return "not_imported"
    
    class FollowSerializer(serializers.Serializer):
    
        id = serializers.URLField(max_length=500)
        object = serializers.URLField(max_length=500)
        actor = serializers.URLField(max_length=500)
    
    Eliot Berriot's avatar
    Eliot Berriot committed
        type = serializers.ChoiceField(choices=["Follow"])
    
    Eliot Berriot's avatar
    Eliot Berriot committed
    
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            expected = self.context.get("follow_target")
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                raise serializers.ValidationError("Invalid target")
    
            try:
                return models.Actor.objects.get(url=v)
            except models.Actor.DoesNotExist:
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                raise serializers.ValidationError("Target not found")
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            expected = self.context.get("follow_actor")
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                raise serializers.ValidationError("Invalid actor")
    
            try:
                return models.Actor.objects.get(url=v)
            except models.Actor.DoesNotExist:
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                raise serializers.ValidationError("Actor not found")
    
    
        def save(self, **kwargs):
            return models.Follow.objects.get_or_create(
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                actor=self.validated_data["actor"],
                target=self.validated_data["object"],
    
    Eliot Berriot's avatar
    Eliot Berriot committed
    
        def to_representation(self, instance):
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                "@context": AP_CONTEXT,
                "actor": instance.actor.url,
                "id": instance.get_federation_url(),
                "object": instance.target.url,
                "type": "Follow",
    
    class APIFollowSerializer(serializers.ModelSerializer):
    
        actor = APIActorSerializer()
        target = APIActorSerializer()
    
    
        class Meta:
            model = models.Follow
            fields = [
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                "uuid",
                "id",
                "approved",
                "creation_date",
                "modification_date",
                "actor",
                "target",
    
    class AcceptFollowSerializer(serializers.Serializer):
    
        id = serializers.URLField(max_length=500)
        actor = serializers.URLField(max_length=500)
    
    Eliot Berriot's avatar
    Eliot Berriot committed
        type = serializers.ChoiceField(choices=["Accept"])
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            expected = self.context.get("follow_target")
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                raise serializers.ValidationError("Invalid actor")
    
            try:
                return models.Actor.objects.get(url=v)
            except models.Actor.DoesNotExist:
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                raise serializers.ValidationError("Actor not found")
    
    
        def validate(self, validated_data):
            # we ensure the accept actor actually match the follow target
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            if validated_data["actor"] != validated_data["object"]["object"]:
                raise serializers.ValidationError("Actor mismatch")
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                validated_data["follow"] = (
                    models.Follow.objects.filter(
                        target=validated_data["actor"],
                        actor=validated_data["object"]["actor"],
                    )
                    .exclude(approved=True)
                    .get()
                )
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                raise serializers.ValidationError("No follow to accept")
    
            return validated_data
    
        def to_representation(self, instance):
            return {
                "@context": AP_CONTEXT,
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                "id": instance.get_federation_url() + "/accept",
    
                "type": "Accept",
                "actor": instance.target.url,
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                "object": FollowSerializer(instance).data,
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            self.validated_data["follow"].approved = True
            self.validated_data["follow"].save()
            return self.validated_data["follow"]
    
    
    
    class UndoFollowSerializer(serializers.Serializer):
    
        id = serializers.URLField(max_length=500)
        actor = serializers.URLField(max_length=500)
    
    Eliot Berriot's avatar
    Eliot Berriot committed
        type = serializers.ChoiceField(choices=["Undo"])
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            expected = self.context.get("follow_target")
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                raise serializers.ValidationError("Invalid actor")
    
            try:
                return models.Actor.objects.get(url=v)
            except models.Actor.DoesNotExist:
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                raise serializers.ValidationError("Actor not found")
    
    
        def validate(self, validated_data):
            # we ensure the accept actor actually match the follow actor
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            if validated_data["actor"] != validated_data["object"]["actor"]:
                raise serializers.ValidationError("Actor mismatch")
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                validated_data["follow"] = models.Follow.objects.filter(
                    actor=validated_data["actor"], target=validated_data["object"]["object"]
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                raise serializers.ValidationError("No follow to remove")
    
            return validated_data
    
        def to_representation(self, instance):
            return {
                "@context": AP_CONTEXT,
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                "id": instance.get_federation_url() + "/undo",
    
                "type": "Undo",
                "actor": instance.actor.url,
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                "object": FollowSerializer(instance).data,
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            return self.validated_data["follow"].delete()
    
    class ActorWebfingerSerializer(serializers.Serializer):
        subject = serializers.CharField()
    
        aliases = serializers.ListField(child=serializers.URLField(max_length=500))
    
        links = serializers.ListField()
    
        actor_url = serializers.URLField(max_length=500, required=False)
    
    
        def validate(self, validated_data):
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            validated_data["actor_url"] = None
            for l in validated_data["links"]:
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                    if not l["rel"] == "self":
    
                        continue
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                    if not l["type"] == "application/activity+json":
    
                        continue
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                    validated_data["actor_url"] = l["href"]
    
                    break
                except KeyError:
                    pass
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            if validated_data["actor_url"] is None:
                raise serializers.ValidationError("No valid actor url found")
    
            return validated_data
    
    
        def to_representation(self, instance):
            data = {}
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            data["subject"] = "acct:{}".format(instance.webfinger_subject)
            data["links"] = [
                {"rel": "self", "href": instance.url, "type": "application/activity+json"}
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            data["aliases"] = [instance.url]
    
            return data
    
    
    
    class ActivitySerializer(serializers.Serializer):
    
        actor = serializers.URLField(max_length=500)
        id = serializers.URLField(max_length=500, required=False)
    
    Eliot Berriot's avatar
    Eliot Berriot committed
        type = serializers.ChoiceField(choices=[(c, c) for c in activity.ACTIVITY_TYPES])
    
        object = serializers.JSONField()
    
        def validate_object(self, value):
            try:
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                type = value["type"]
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                raise serializers.ValidationError("Missing object type")
    
            except TypeError:
                # probably a URL
                return value
    
            try:
                object_serializer = OBJECT_SERIALIZERS[type]
            except KeyError:
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                raise serializers.ValidationError("Unsupported type {}".format(type))
    
    
            serializer = object_serializer(data=value)
            serializer.is_valid(raise_exception=True)
            return serializer.data
    
        def validate_actor(self, value):
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            request_actor = self.context.get("actor")
    
            if request_actor and request_actor.url != value:
                raise serializers.ValidationError(
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                    "The actor making the request do not match" " the activity actor"
    
        def to_representation(self, conf):
            d = {}
            d.update(conf)
    
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            if self.context.get("include_ap_context", True):
                d["@context"] = AP_CONTEXT
    
    
    class ObjectSerializer(serializers.Serializer):
    
        id = serializers.URLField(max_length=500)
        url = serializers.URLField(max_length=500, required=False, allow_null=True)
    
    Eliot Berriot's avatar
    Eliot Berriot committed
        type = serializers.ChoiceField(choices=[(c, c) for c in activity.OBJECT_TYPES])
        content = serializers.CharField(required=False, allow_null=True)
        summary = serializers.CharField(required=False, allow_null=True)
        name = serializers.CharField(required=False, allow_null=True)
        published = serializers.DateTimeField(required=False, allow_null=True)
        updated = serializers.DateTimeField(required=False, allow_null=True)
    
        to = serializers.ListField(
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            child=serializers.URLField(max_length=500), required=False, allow_null=True
        )
    
        cc = serializers.ListField(
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            child=serializers.URLField(max_length=500), required=False, allow_null=True
        )
    
        bto = serializers.ListField(
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            child=serializers.URLField(max_length=500), required=False, allow_null=True
        )
    
        bcc = serializers.ListField(
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            child=serializers.URLField(max_length=500), required=False, allow_null=True
        )
    
    
    Eliot Berriot's avatar
    Eliot Berriot committed
    OBJECT_SERIALIZERS = {t: ObjectSerializer for t in activity.OBJECT_TYPES}
    
    
    
    class PaginatedCollectionSerializer(serializers.Serializer):
    
    Eliot Berriot's avatar
    Eliot Berriot committed
        type = serializers.ChoiceField(choices=["Collection"])
    
        totalItems = serializers.IntegerField(min_value=0)
    
        actor = serializers.URLField(max_length=500)
        id = serializers.URLField(max_length=500)
        first = serializers.URLField(max_length=500)
        last = serializers.URLField(max_length=500)
    
    
        def to_representation(self, conf):
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            paginator = Paginator(conf["items"], conf.get("page_size", 20))
            first = funkwhale_utils.set_query_parameter(conf["id"], page=1)
    
            current = first
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            last = funkwhale_utils.set_query_parameter(conf["id"], page=paginator.num_pages)
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                "id": conf["id"],
                "actor": conf["actor"].url,
                "totalItems": paginator.count,
                "type": "Collection",
                "current": current,
                "first": first,
                "last": last,
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            if self.context.get("include_ap_context", True):
                d["@context"] = AP_CONTEXT
    
            return d
    
    
    class CollectionPageSerializer(serializers.Serializer):
    
    Eliot Berriot's avatar
    Eliot Berriot committed
        type = serializers.ChoiceField(choices=["CollectionPage"])
    
        totalItems = serializers.IntegerField(min_value=0)
        items = serializers.ListField()
    
        actor = serializers.URLField(max_length=500)
        id = serializers.URLField(max_length=500)
        first = serializers.URLField(max_length=500)
        last = serializers.URLField(max_length=500)
        next = serializers.URLField(max_length=500, required=False)
        prev = serializers.URLField(max_length=500, required=False)
        partOf = serializers.URLField(max_length=500)
    
        def validate_items(self, v):
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            item_serializer = self.context.get("item_serializer")
    
            if not item_serializer:
                return v
            raw_items = [item_serializer(data=i, context=self.context) for i in v]
    
            for i in raw_items:
    
                if i.is_valid():
                    valid_items.append(i)
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                    logger.debug("Invalid item %s: %s", i.data, i.errors)
    
        def to_representation(self, conf):
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            page = conf["page"]
            first = funkwhale_utils.set_query_parameter(conf["id"], page=1)
    
            last = funkwhale_utils.set_query_parameter(
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                conf["id"], page=page.paginator.num_pages
            )
            id = funkwhale_utils.set_query_parameter(conf["id"], page=page.number)
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                "id": id,
                "partOf": conf["id"],
                "actor": conf["actor"].url,
                "totalItems": page.paginator.count,
                "type": "CollectionPage",
                "first": first,
                "last": last,
                "items": [
                    conf["item_serializer"](
                        i, context={"actor": conf["actor"], "include_ap_context": False}
    
                    ).data
                    for i in page.object_list
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                ],
    
            }
    
            if page.has_previous():
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                d["prev"] = funkwhale_utils.set_query_parameter(
                    conf["id"], page=page.previous_page_number()
                )
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            if page.has_next():
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                d["next"] = funkwhale_utils.set_query_parameter(
                    conf["id"], page=page.next_page_number()
                )
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            if self.context.get("include_ap_context", True):
                d["@context"] = AP_CONTEXT
    
    
    
    class ArtistMetadataSerializer(serializers.Serializer):
    
        musicbrainz_id = serializers.UUIDField(required=False, allow_null=True)
    
        name = serializers.CharField()
    
    
    class ReleaseMetadataSerializer(serializers.Serializer):
    
        musicbrainz_id = serializers.UUIDField(required=False, allow_null=True)
    
        title = serializers.CharField()
    
    
    class RecordingMetadataSerializer(serializers.Serializer):
    
        musicbrainz_id = serializers.UUIDField(required=False, allow_null=True)
    
        title = serializers.CharField()
    
    
    class AudioMetadataSerializer(serializers.Serializer):
        artist = ArtistMetadataSerializer()
        release = ReleaseMetadataSerializer()
        recording = RecordingMetadataSerializer()
    
    Eliot Berriot's avatar
    Eliot Berriot committed
        bitrate = serializers.IntegerField(required=False, allow_null=True, min_value=0)
        size = serializers.IntegerField(required=False, allow_null=True, min_value=0)
        length = serializers.IntegerField(required=False, allow_null=True, min_value=0)
    
    
    
    class AudioSerializer(serializers.Serializer):
        type = serializers.CharField()
    
        id = serializers.URLField(max_length=500)
    
        url = serializers.JSONField()
        published = serializers.DateTimeField()
        updated = serializers.DateTimeField(required=False)
        metadata = AudioMetadataSerializer()
    
        def validate_type(self, v):
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            if v != "Audio":
                raise serializers.ValidationError("Invalid type for audio")
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                raise serializers.ValidationError("Missing href")
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                media_type = v["mediaType"]
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                raise serializers.ValidationError("Missing mediaType")
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            if not media_type or not media_type.startswith("audio/"):
                raise serializers.ValidationError("Invalid mediaType")
    
    
            return v
    
        def create(self, validated_data):
            defaults = {
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                "audio_mimetype": validated_data["url"]["mediaType"],
                "audio_url": validated_data["url"]["href"],
                "metadata": validated_data["metadata"],
                "artist_name": validated_data["metadata"]["artist"]["name"],
                "album_title": validated_data["metadata"]["release"]["title"],
                "title": validated_data["metadata"]["recording"]["title"],
                "published_date": validated_data["published"],
                "modification_date": validated_data.get("updated"),
    
            }
            return models.LibraryTrack.objects.get_or_create(
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                library=self.context["library"], url=validated_data["id"], defaults=defaults
    
            )[0]
    
        def to_representation(self, instance):
            track = instance.track
            album = instance.track.album
            artist = instance.track.artist
    
            d = {
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                "type": "Audio",
                "id": instance.get_federation_url(),
                "name": instance.track.full_name,
                "published": instance.creation_date.isoformat(),
                "updated": instance.modification_date.isoformat(),
                "metadata": {
                    "artist": {
                        "musicbrainz_id": str(artist.mbid) if artist.mbid else None,
                        "name": artist.name,
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                    "release": {
                        "musicbrainz_id": str(album.mbid) if album.mbid else None,
                        "title": album.title,
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                    "recording": {
                        "musicbrainz_id": str(track.mbid) if track.mbid else None,
                        "title": track.title,
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                    "bitrate": instance.bitrate,
                    "size": instance.size,
                    "length": instance.duration,
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                "url": {
                    "href": utils.full_url(instance.path),
                    "type": "Link",
                    "mediaType": instance.mimetype,
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                "attributedTo": [self.context["actor"].url],
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            if self.context.get("include_ap_context", True):
                d["@context"] = AP_CONTEXT
    
            return d
    
    
    class CollectionSerializer(serializers.Serializer):
        def to_representation(self, conf):
            d = {
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                "id": conf["id"],
                "actor": conf["actor"].url,
                "totalItems": len(conf["items"]),
                "type": "Collection",
                "items": [
                    conf["item_serializer"](
                        i, context={"actor": conf["actor"], "include_ap_context": False}
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                    for i in conf["items"]
                ],
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            if self.context.get("include_ap_context", True):
                d["@context"] = AP_CONTEXT
    
    
    
    class LibraryTrackActionSerializer(common_serializers.ActionSerializer):
    
    Eliot Berriot's avatar
    Eliot Berriot committed
        actions = [common_serializers.Action("import", allow_all=True)]
    
        filterset_class = filters.LibraryTrackFilter
    
        @transaction.atomic
        def handle_import(self, objects):
            batch = music_models.ImportBatch.objects.create(
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                source="federation", submitted_by=self.context["submitted_by"]
    
                job = music_models.ImportJob(
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                    batch=batch, library_track=lt, mbid=lt.mbid, source=lt.url
    
                jobs.append(job)
    
            music_models.ImportJob.objects.bulk_create(jobs)
            music_tasks.import_batch_run.delay(import_batch_id=batch.pk)
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            return {"batch": {"id": batch.pk}}