Newer
Older
import urllib.parse
from django.db import transaction
from django import urls
from django.conf import settings
Eliot Berriot
committed
from rest_framework import serializers
from funkwhale_api.activity import serializers as activity_serializers
from funkwhale_api.audio import serializers as audio_serializers
from funkwhale_api.common import serializers as common_serializers
from funkwhale_api.common import utils as common_utils
from funkwhale_api.federation import utils as federation_utils
from funkwhale_api.playlists import models as playlists_models
from funkwhale_api.tags import serializers as tags_serializers
from . import filters, models, tasks
Eliot Berriot
committed
class CoverField(
common_serializers.NullToEmptDict, common_serializers.AttachmentSerializer
):
# XXX: BACKWARD COMPATIBILITY
pass
cover_field = CoverField()
def serialize_attributed_to(self, obj):
# Import at runtime to avoid a circular import issue
from funkwhale_api.federation import serializers as federation_serializers
if not obj.attributed_to_id:
return
return federation_serializers.APIActorSerializer(obj.attributed_to).data
class OptionalDescriptionMixin(object):
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):
return obj["identifiers"][0]
class Meta:
model = models.License
class ArtistAlbumSerializer(serializers.Serializer):
tracks_count = serializers.SerializerMethodField()
is_playable = serializers.SerializerMethodField()
is_local = serializers.BooleanField()
id = serializers.IntegerField()
fid = serializers.URLField()
mbid = serializers.UUIDField()
title = serializers.CharField()
artist = serializers.SerializerMethodField()
release_date = serializers.DateField()
creation_date = serializers.DateTimeField()
def get_artist(self, o):
return o.artist_id
Eliot Berriot
committed
def get_tracks_count(self, o):
return o._tracks_count
def get_is_playable(self, obj):
try:
return bool(obj.is_playable_by_actor)
except AttributeError:
return None
DATETIME_FIELD = serializers.DateTimeField()
class ArtistWithAlbumsSerializer(OptionalDescriptionMixin, serializers.Serializer):
albums = ArtistAlbumSerializer(many=True)
tags = serializers.SerializerMethodField()
attributed_to = serializers.SerializerMethodField()
channel = serializers.SerializerMethodField()
Ciarán Ainsworth
committed
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()
def get_tags(self, obj):
tagged_items = getattr(obj, "_prefetched_tagged_items", [])
return [ti.tag.name for ti in tagged_items]
Eliot Berriot
committed
get_attributed_to = serialize_attributed_to
Ciarán Ainsworth
committed
def get_tracks_count(self, o):
Eliot Berriot
committed
tracks = getattr(o, "_prefetched_tracks", None)
return len(tracks) if tracks else None
def get_channel(self, o):
channel = o.get_channel()
if not channel:
return
return {
"uuid": str(channel.uuid),
"actor": {
"full_username": channel.actor.full_username,
"preferred_username": channel.actor.preferred_username,
"domain": channel.actor.domain_id,
},
}
Eliot Berriot
committed
def serialize_artist_simple(artist):
Eliot Berriot
committed
"id": artist.id,
"fid": artist.fid,
"mbid": str(artist.mbid),
"name": artist.name,
"creation_date": DATETIME_FIELD.to_representation(artist.creation_date),
Eliot Berriot
committed
"is_local": artist.is_local,
"content_category": artist.content_category,
Eliot Berriot
committed
}
if "description" in artist._state.fields_cache:
data["description"] = (
common_serializers.ContentSerializer(artist.description).data
if artist.description
else None
)
return data
Eliot Berriot
committed
def serialize_album_track(track):
return {
"id": track.id,
"fid": track.fid,
"mbid": str(track.mbid),
"title": track.title,
"artist": serialize_artist_simple(track.artist),
"album": track.album_id,
"creation_date": DATETIME_FIELD.to_representation(track.creation_date),
Eliot Berriot
committed
"position": track.position,
"disc_number": track.disc_number,
"uploads": [
serialize_upload(u) for u in getattr(track, "playable_uploads", [])
],
"listen_url": track.listen_url,
"duration": getattr(track, "duration", None),
"copyright": track.copyright,
"license": track.license_id,
"is_local": track.is_local,
}
class AlbumSerializer(OptionalDescriptionMixin, serializers.Serializer):
tracks = serializers.SerializerMethodField()
Eliot Berriot
committed
artist = serializers.SerializerMethodField()
is_playable = serializers.SerializerMethodField()
tags = serializers.SerializerMethodField()
attributed_to = serializers.SerializerMethodField()
id = serializers.IntegerField()
fid = serializers.URLField()
mbid = serializers.UUIDField()
title = serializers.CharField()
artist = serializers.SerializerMethodField()
release_date = serializers.DateField()
creation_date = serializers.DateTimeField()
is_local = serializers.BooleanField()
is_playable = serializers.SerializerMethodField()
Eliot Berriot
committed
get_attributed_to = serialize_attributed_to
Eliot Berriot
committed
def get_artist(self, o):
return serialize_artist_simple(o.artist)
def get_tracks(self, o):
ordered_tracks = o.tracks.all()
Eliot Berriot
committed
return [serialize_album_track(track) for track in ordered_tracks]
Eliot Berriot
committed
def get_is_playable(self, obj):
try:
return any(
[bool(getattr(t, "playable_uploads", [])) for t in obj.tracks.all()]
)
except AttributeError:
return None
def get_tags(self, obj):
tagged_items = getattr(obj, "_prefetched_tagged_items", [])
return [ti.tag.name for ti in tagged_items]
Eliot Berriot
committed
class TrackAlbumSerializer(serializers.ModelSerializer):
Eliot Berriot
committed
artist = serializers.SerializerMethodField()
Eliot Berriot
committed
class Meta:
model = models.Album
Eliot Berriot
committed
fields = (
"mbid",
"title",
"artist",
"release_date",
"cover",
"creation_date",
Eliot Berriot
committed
Eliot Berriot
committed
def get_artist(self, o):
return serialize_artist_simple(o.artist)
Eliot Berriot
committed
def serialize_upload(upload):
return {
"uuid": str(upload.uuid),
"listen_url": upload.listen_url,
"size": upload.size,
"duration": upload.duration,
"bitrate": upload.bitrate,
"mimetype": upload.mimetype,
"extension": upload.extension,
}
class TrackSerializer(OptionalDescriptionMixin, serializers.Serializer):
Eliot Berriot
committed
artist = serializers.SerializerMethodField()
album = TrackAlbumSerializer(read_only=True)
uploads = serializers.SerializerMethodField()
listen_url = serializers.SerializerMethodField()
tags = serializers.SerializerMethodField()
attributed_to = serializers.SerializerMethodField()
id = serializers.IntegerField()
fid = serializers.URLField()
mbid = serializers.UUIDField()
title = serializers.CharField()
artist = serializers.SerializerMethodField()
creation_date = serializers.DateTimeField()
is_local = serializers.BooleanField()
position = serializers.IntegerField()
disc_number = serializers.IntegerField()
copyright = serializers.CharField()
license = serializers.SerializerMethodField()
get_attributed_to = serialize_attributed_to
Eliot Berriot
committed
def get_artist(self, o):
return serialize_artist_simple(o.artist)
def get_listen_url(self, obj):
return obj.listen_url
def get_uploads(self, obj):
Eliot Berriot
committed
return [serialize_upload(u) for u in getattr(obj, "playable_uploads", [])]
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):
return o.license_id
@common_serializers.track_fields_for_update("name", "description", "privacy_level")
class LibraryForOwnerSerializer(serializers.ModelSerializer):
size = serializers.SerializerMethodField()
class Meta:
model = models.Library
fields = [
"uuid",
"fid",
"name",
"description",
"privacy_level",
"size",
"creation_date",
]
read_only_fields = ["fid", "uuid", "creation_date", "actor"]
def get_uploads_count(self, o):
return getattr(o, "_uploads_count", o.uploads_count)
def get_size(self, o):
return getattr(o, "_size", 0)
def on_updated_fields(self, obj, before, after):
routes.outbox.dispatch(
{"type": "Update", "object": {"type": "Library"}}, context={"library": obj}
)
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",
audio_serializers.ChannelSerializer(),
required=False,
filters=lambda context: {"attributed_to": context["user"].actor},
)
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",
]
class ImportMetadataSerializer(serializers.Serializer):
title = serializers.CharField(max_length=500, required=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
)
class ImportMetadataField(serializers.JSONField):
def to_internal_value(self, v):
v = super().to_internal_value(v)
s = ImportMetadataSerializer(data=v)
s.is_valid(raise_exception=True)
return v
import_status = serializers.ChoiceField(
choices=["draft", "pending"], default="pending"
)
import_metadata = ImportMetadataField(required=False)
class Meta(UploadSerializer.Meta):
fields = UploadSerializer.Meta.fields + [
"import_details",
"import_metadata",
"import_reference",
"metadata",
"source",
"audio_file",
]
write_only_fields = ["audio_file"]
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
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 UploadActionSerializer(common_serializers.ActionSerializer):
actions = [
common_serializers.Action("delete", allow_all=True),
common_serializers.Action("relaunch_import", allow_all=True),
]
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)
Eliot Berriot
committed
class TagSerializer(serializers.ModelSerializer):
Eliot Berriot
committed
class Meta:
Eliot Berriot
committed
class SimpleAlbumSerializer(serializers.ModelSerializer):
cover = cover_field
Eliot Berriot
committed
class Meta:
model = models.Album
fields = ("id", "mbid", "title", "release_date", "cover")
Eliot Berriot
committed
class TrackActivitySerializer(activity_serializers.ModelSerializer):
type = serializers.SerializerMethodField()
name = serializers.CharField(source="title")
artist = serializers.CharField(source="artist.name")
album = serializers.CharField(source="album.title")
class Meta:
model = models.Track
fields = ["id", "local_id", "name", "type", "artist", "album"]
Eliot Berriot
committed
def get_embed_url(type, id):
return settings.FUNKWHALE_EMBED_URL + "?type={}&id={}".format(type, 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 = {
"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.select_related("artist", "album__artist").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"] = "{} by {}".format(track.title, track.artist.name)
if 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.artist.name
data["height"] = 150
data["author_url"] = federation_utils.full_url(
common_utils.spa_reverse(
"library_artist", kwargs={"pk": track.artist.pk}
)
)
elif match.url_name == "library_album":
qs = models.Album.objects.select_related("artist").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"] = "{} by {}".format(album.title, album.artist.name)
data["description"] = "{} by {}".format(album.title, album.artist.name)
data["author_name"] = album.artist.name
data["height"] = 400
data["author_url"] = federation_utils.full_url(
common_utils.spa_reverse(
"library_artist", kwargs={"pk": album.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 = artist.albums.exclude(attachment_cover=None).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})
)
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
elif match.url_name == "channel_detail":
from funkwhale_api.audio.models import Channel
qs = Channel.objects.filter(uuid=match.kwargs["uuid"]).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(
Eliot Berriot
committed
data["width"], data["height"], get_embed_url(embed_type, embed_id)
)
return data
def create(self, data):
return data