serializers.py 29.5 KB
Newer Older
1
2
import urllib.parse

3
from django.db import transaction
4
5
from django import urls
from django.conf import settings
6
7
from rest_framework import serializers

8
from funkwhale_api.activity import serializers as activity_serializers
9
from funkwhale_api.common import models as common_models
10
11
from funkwhale_api.common import serializers as common_serializers
from funkwhale_api.common import utils as common_utils
Eliot Berriot's avatar
Eliot Berriot committed
12
from funkwhale_api.federation import routes
13
from funkwhale_api.federation import utils as federation_utils
Eliot Berriot's avatar
Eliot Berriot committed
14
from funkwhale_api.playlists import models as playlists_models
15
from funkwhale_api.tags import models as tag_models
16
from funkwhale_api.tags import serializers as tags_serializers
17

18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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,
)

from funkwhale_api.audio import serializers as audio_serializers  # NOQA
33
34


35
class CoverField(common_serializers.AttachmentSerializer):
Eliot Berriot's avatar
Eliot Berriot committed
36
37
38
39
    pass


cover_field = CoverField()
40
41


42
43
44
45
46
47
48
49
50
51
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


52
53
54
55
56
57
58
59
60
61
62
63
64
65
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


Eliot Berriot's avatar
Eliot Berriot committed
66
67
68
69
70
71
72
73
74
75
76
77
78
79
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]

80
81
82
    class Meta:
        model = models.License

Eliot Berriot's avatar
Eliot Berriot committed
83

Eliot Berriot's avatar
Eliot Berriot committed
84
class ArtistAlbumSerializer(serializers.Serializer):
85
    tracks_count = serializers.SerializerMethodField()
86
    cover = cover_field
87
    is_playable = serializers.SerializerMethodField()
Eliot Berriot's avatar
Eliot Berriot committed
88
89
90
91
92
93
94
95
    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()
96

Eliot Berriot's avatar
Eliot Berriot committed
97
98
    def get_artist(self, o):
        return o.artist_id
99

100
    def get_tracks_count(self, o):
Agate's avatar
Agate committed
101
        return len(o.tracks.all())
102

103
104
105
106
107
108
    def get_is_playable(self, obj):
        try:
            return bool(obj.is_playable_by_actor)
        except AttributeError:
            return None

109

Eliot Berriot's avatar
Eliot Berriot committed
110
111
112
DATETIME_FIELD = serializers.DateTimeField()


113
class ArtistWithAlbumsSerializer(OptionalDescriptionMixin, serializers.Serializer):
Eliot Berriot's avatar
Eliot Berriot committed
114
    albums = ArtistAlbumSerializer(many=True)
115
    tags = serializers.SerializerMethodField()
116
    attributed_to = serializers.SerializerMethodField()
117
    channel = serializers.SerializerMethodField()
118
    tracks_count = serializers.SerializerMethodField()
Eliot Berriot's avatar
Eliot Berriot committed
119
120
121
122
    id = serializers.IntegerField()
    fid = serializers.URLField()
    mbid = serializers.UUIDField()
    name = serializers.CharField()
123
    content_category = serializers.CharField()
Eliot Berriot's avatar
Eliot Berriot committed
124
125
    creation_date = serializers.DateTimeField()
    is_local = serializers.BooleanField()
126
    cover = cover_field
127
128
129
130

    def get_tags(self, obj):
        tagged_items = getattr(obj, "_prefetched_tagged_items", [])
        return [ti.tag.name for ti in tagged_items]
131

132
133
    get_attributed_to = serialize_attributed_to

134
    def get_tracks_count(self, o):
135
136
137
        tracks = getattr(o, "_prefetched_tracks", None)
        return len(tracks) if tracks else None

138
139
140
141
142
143
144
145
146
147
148
149
150
151
    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,
            },
        }

152
153

def serialize_artist_simple(artist):
154
    data = {
155
156
157
158
        "id": artist.id,
        "fid": artist.fid,
        "mbid": str(artist.mbid),
        "name": artist.name,
Eliot Berriot's avatar
Eliot Berriot committed
159
        "creation_date": DATETIME_FIELD.to_representation(artist.creation_date),
160
        "modification_date": DATETIME_FIELD.to_representation(artist.modification_date),
161
        "is_local": artist.is_local,
162
        "content_category": artist.content_category,
163
    }
164
165
166
167
168
169
170
    if "description" in artist._state.fields_cache:
        data["description"] = (
            common_serializers.ContentSerializer(artist.description).data
            if artist.description
            else None
        )

171
172
173
174
175
176
    if "attachment_cover" in artist._state.fields_cache:
        data["cover"] = (
            cover_field.to_representation(artist.attachment_cover)
            if artist.attachment_cover
            else None
        )
177
178
    if "channel" in artist._state.fields_cache and artist.get_channel():
        data["channel"] = str(artist.channel.uuid)
179
180
181
182
183
184
185

    if getattr(artist, "_tracks_count", None) is not None:
        data["tracks_count"] = artist._tracks_count

    if getattr(artist, "_prefetched_tagged_items", None) is not None:
        data["tags"] = [ti.tag.name for ti in artist._prefetched_tagged_items]

186
    return data
187
188


189
class AlbumSerializer(OptionalDescriptionMixin, serializers.Serializer):
190
    artist = serializers.SerializerMethodField()
191
    cover = cover_field
192
    is_playable = serializers.SerializerMethodField()
193
    tags = serializers.SerializerMethodField()
194
    tracks_count = serializers.SerializerMethodField()
195
    attributed_to = serializers.SerializerMethodField()
Eliot Berriot's avatar
Eliot Berriot committed
196
197
198
199
200
201
202
203
204
    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()
205

206
207
    get_attributed_to = serialize_attributed_to

208
209
210
    def get_artist(self, o):
        return serialize_artist_simple(o.artist)

211
    def get_tracks_count(self, o):
Agate's avatar
Agate committed
212
        return len(o.tracks.all())
213

214
215
    def get_is_playable(self, obj):
        try:
216
217
218
            return any(
                [bool(getattr(t, "playable_uploads", [])) for t in obj.tracks.all()]
            )
219
220
221
        except AttributeError:
            return None

222
223
224
225
    def get_tags(self, obj):
        tagged_items = getattr(obj, "_prefetched_tagged_items", [])
        return [ti.tag.name for ti in tagged_items]

226

227
class TrackAlbumSerializer(serializers.ModelSerializer):
228
    artist = serializers.SerializerMethodField()
229
    cover = cover_field
230
231
232
233
    tracks_count = serializers.SerializerMethodField()

    def get_tracks_count(self, o):
        return getattr(o, "_prefetched_tracks_count", len(o.tracks.all()))
234
235

    class Meta:
236
        model = models.Album
237
        fields = (
Eliot Berriot's avatar
Eliot Berriot committed
238
            "id",
Eliot Berriot's avatar
Eliot Berriot committed
239
            "fid",
Eliot Berriot's avatar
Eliot Berriot committed
240
241
242
243
244
245
            "mbid",
            "title",
            "artist",
            "release_date",
            "cover",
            "creation_date",
Eliot Berriot's avatar
Eliot Berriot committed
246
            "is_local",
247
            "tracks_count",
248
        )
249

250
251
    def get_artist(self, o):
        return serialize_artist_simple(o.artist)
252

253
254
255
256
257
258
259
260
261
262

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,
263
        "is_local": federation_utils.is_local(upload.fid),
264
    }
265
266


267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
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]


283
class TrackSerializer(OptionalDescriptionMixin, serializers.Serializer):
284
    artist = serializers.SerializerMethodField()
285
    album = TrackAlbumSerializer(read_only=True)
286
    uploads = serializers.SerializerMethodField()
287
    listen_url = serializers.SerializerMethodField()
288
    tags = serializers.SerializerMethodField()
289
    attributed_to = serializers.SerializerMethodField()
290

Eliot Berriot's avatar
Eliot Berriot committed
291
292
293
294
295
296
297
298
299
    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()
300
    downloads_count = serializers.IntegerField()
Eliot Berriot's avatar
Eliot Berriot committed
301
302
    copyright = serializers.CharField()
    license = serializers.SerializerMethodField()
303
    cover = cover_field
304
305
    get_attributed_to = serialize_attributed_to

306
307
308
    def get_artist(self, o):
        return serialize_artist_simple(o.artist)

309
310
311
    def get_listen_url(self, obj):
        return obj.listen_url

312
    def get_uploads(self, obj):
313
        uploads = getattr(obj, "playable_uploads", [])
314
315
316
317
        # we put local uploads first
        uploads = [serialize_upload(u) for u in sort_uploads_for_listen(uploads)]
        uploads = sorted(uploads, key=lambda u: u["is_local"], reverse=True)
        return list(uploads)
Eliot Berriot's avatar
Eliot Berriot committed
318

319
320
321
322
    def get_tags(self, obj):
        tagged_items = getattr(obj, "_prefetched_tagged_items", [])
        return [ti.tag.name for ti in tagged_items]

Eliot Berriot's avatar
Eliot Berriot committed
323
324
325
    def get_license(self, o):
        return o.license_id

326

327
@common_serializers.track_fields_for_update("name", "description", "privacy_level")
328
class LibraryForOwnerSerializer(serializers.ModelSerializer):
Eliot Berriot's avatar
Eliot Berriot committed
329
    uploads_count = serializers.SerializerMethodField()
330
    size = serializers.SerializerMethodField()
331
    actor = serializers.SerializerMethodField()
332
333
334
335
336
337
338
339
340

    class Meta:
        model = models.Library
        fields = [
            "uuid",
            "fid",
            "name",
            "description",
            "privacy_level",
Eliot Berriot's avatar
Eliot Berriot committed
341
            "uploads_count",
342
343
            "size",
            "creation_date",
344
            "actor",
345
346
347
        ]
        read_only_fields = ["fid", "uuid", "creation_date", "actor"]

Eliot Berriot's avatar
Eliot Berriot committed
348
349
    def get_uploads_count(self, o):
        return getattr(o, "_uploads_count", o.uploads_count)
350
351
352
353

    def get_size(self, o):
        return getattr(o, "_size", 0)

354
355
356
357
358
    def on_updated_fields(self, obj, before, after):
        routes.outbox.dispatch(
            {"type": "Update", "object": {"type": "Library"}}, context={"library": obj}
        )

359
360
361
362
363
364
    def get_actor(self, o):
        # Import at runtime to avoid a circular import issue
        from funkwhale_api.federation import serializers as federation_serializers

        return federation_serializers.APIActorSerializer(o.actor).data

365

Eliot Berriot's avatar
Eliot Berriot committed
366
class UploadSerializer(serializers.ModelSerializer):
367
368
369
370
    track = TrackSerializer(required=False, allow_null=True)
    library = common_serializers.RelatedField(
        "uuid",
        LibraryForOwnerSerializer(),
371
        required=False,
372
373
        filters=lambda context: {"actor": context["user"].actor},
    )
374
375
376
377
378
379
    channel = common_serializers.RelatedField(
        "uuid",
        audio_serializers.ChannelSerializer(),
        required=False,
        filters=lambda context: {"attributed_to": context["user"].actor},
    )
380
381

    class Meta:
Eliot Berriot's avatar
Eliot Berriot committed
382
        model = models.Upload
383
384
385
386
387
388
389
        fields = [
            "uuid",
            "filename",
            "creation_date",
            "mimetype",
            "track",
            "library",
390
            "channel",
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
            "duration",
            "mimetype",
            "bitrate",
            "size",
            "import_date",
            "import_status",
        ]

        read_only_fields = [
            "uuid",
            "creation_date",
            "duration",
            "mimetype",
            "bitrate",
            "size",
            "track",
            "import_date",
        ]

410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
    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__channel=context["channel"])
    if "actor" in context:
        return qs.filter(artist__attributed_to=context["actor"])

    return qs.none()

428

429
430
class ImportMetadataSerializer(serializers.Serializer):
    title = serializers.CharField(max_length=500, required=True)
431
432
433
    description = serializers.CharField(
        max_length=5000, required=False, allow_null=True
    )
434
435
436
437
438
439
440
    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
    )
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
    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,
    )
459
460
461
462
463


class ImportMetadataField(serializers.JSONField):
    def to_internal_value(self, v):
        v = super().to_internal_value(v)
464
465
466
        s = ImportMetadataSerializer(
            data=v, context={"actor": self.context["user"].actor}
        )
467
468
469
470
        s.is_valid(raise_exception=True)
        return v


Eliot Berriot's avatar
Eliot Berriot committed
471
class UploadForOwnerSerializer(UploadSerializer):
472
473
474
475
476
    import_status = serializers.ChoiceField(
        choices=["draft", "pending"], default="pending"
    )
    import_metadata = ImportMetadataField(required=False)

Eliot Berriot's avatar
Eliot Berriot committed
477
478
    class Meta(UploadSerializer.Meta):
        fields = UploadSerializer.Meta.fields + [
479
480
481
482
483
484
485
486
            "import_details",
            "import_metadata",
            "import_reference",
            "metadata",
            "source",
            "audio_file",
        ]
        write_only_fields = ["audio_file"]
Eliot Berriot's avatar
Eliot Berriot committed
487
        read_only_fields = UploadSerializer.Meta.read_only_fields + [
488
489
490
491
492
493
494
495
496
497
498
            "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):
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
        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"
            )
515
516
517
        if "audio_file" in validated_data:
            self.validate_upload_quota(validated_data["audio_file"])

518
519
        if "channel" in validated_data:
            validated_data["library"] = validated_data.pop("channel").library
520
521
522
523
524
525
526
527
528
529
        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


Eliot Berriot's avatar
Eliot Berriot committed
530
class UploadActionSerializer(common_serializers.ActionSerializer):
531
532
533
    actions = [
        common_serializers.Action("delete", allow_all=True),
        common_serializers.Action("relaunch_import", allow_all=True),
534
        common_serializers.Action("publish", allow_all=False),
535
    ]
Eliot Berriot's avatar
Eliot Berriot committed
536
    filterset_class = filters.UploadFilter
537
538
539
540
    pk_field = "uuid"

    @transaction.atomic
    def handle_delete(self, objects):
Eliot Berriot's avatar
Eliot Berriot committed
541
542
543
544
545
546
547
548
549
550
        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},
                )

551
552
553
554
        return objects.delete()

    @transaction.atomic
    def handle_relaunch_import(self, objects):
555
        qs = objects.filter(import_status__in=["pending", "skipped", "errored"])
556
557
558
        pks = list(qs.values_list("id", flat=True))
        qs.update(import_status="pending")
        for pk in pks:
559
            common_utils.on_commit(tasks.process_upload.delay, upload_id=pk)
560

561
562
563
564
565
566
567
568
    @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)

569

570
class TagSerializer(serializers.ModelSerializer):
571
    class Meta:
572
        model = tag_models.Tag
Eliot Berriot's avatar
Eliot Berriot committed
573
        fields = ("id", "name", "creation_date")
574

575

576
class SimpleAlbumSerializer(serializers.ModelSerializer):
577
578
    cover = cover_field

579
    class Meta:
580
        model = models.Album
Eliot Berriot's avatar
Eliot Berriot committed
581
        fields = ("id", "mbid", "title", "release_date", "cover")
582
583


584
585
class TrackActivitySerializer(activity_serializers.ModelSerializer):
    type = serializers.SerializerMethodField()
Eliot Berriot's avatar
Eliot Berriot committed
586
587
    name = serializers.CharField(source="title")
    artist = serializers.CharField(source="artist.name")
588
    album = serializers.SerializerMethodField()
589
590
591

    class Meta:
        model = models.Track
Eliot Berriot's avatar
Eliot Berriot committed
592
        fields = ["id", "local_id", "name", "type", "artist", "album"]
593
594

    def get_type(self, obj):
Eliot Berriot's avatar
Eliot Berriot committed
595
        return "Audio"
596

597
598
599
600
    def get_album(self, o):
        if o.album:
            return o.album.title

601

602
603
604
605
def get_embed_url(type, id):
    return settings.FUNKWHALE_EMBED_URL + "?type={}&id={}".format(type, id)


606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
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 = {
622
            "version": "1.0",
623
            "type": "rich",
624
            "provider_name": settings.APP_NAME,
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
            "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)
644
645
646
            if track.attachment_cover:
                data[
                    "thumbnail_url"
Eliot Berriot's avatar
Eliot Berriot committed
647
                ] = track.attachment_cover.download_url_medium_square_crop
648
649
650
                data["thumbnail_width"] = 200
                data["thumbnail_height"] = 200
            elif track.album and track.album.attachment_cover:
Eliot Berriot's avatar
Eliot Berriot committed
651
652
653
654
655
                data[
                    "thumbnail_url"
                ] = track.album.attachment_cover.download_url_medium_square_crop
                data["thumbnail_width"] = 200
                data["thumbnail_height"] = 200
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
            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
Eliot Berriot's avatar
Eliot Berriot committed
676
677
678
679
680
681
            if album.attachment_cover:
                data[
                    "thumbnail_url"
                ] = album.attachment_cover.download_url_medium_square_crop
                data["thumbnail_width"] = 200
                data["thumbnail_height"] = 200
682
683
684
685
686
687
688
689
690
            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}
                )
            )
691
692
693
694
695
696
697
698
699
700
        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
Eliot Berriot's avatar
Eliot Berriot committed
701
702
703
704
705
706
707
708
            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
709
710
711
712
713
714
715
            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})
            )
716
717
718
        elif match.url_name == "channel_detail":
            from funkwhale_api.audio.models import Channel

719
720
721
722
723
724
725
726
727
728
            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(
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
                "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}
                )
            )
Eliot Berriot's avatar
Eliot Berriot committed
755
756
757
758
759
760
761
762
763
764
765
766
        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
Eliot Berriot's avatar
Eliot Berriot committed
767
768
            playlist_tracks = obj.playlist_tracks.exclude(
                track__album__attachment_cover=None
Eliot Berriot's avatar
Eliot Berriot committed
769
            )
Eliot Berriot's avatar
Eliot Berriot committed
770
771
772
            playlist_tracks = playlist_tracks.select_related(
                "track__album__attachment_cover"
            ).order_by("index")
Eliot Berriot's avatar
Eliot Berriot committed
773
774
775
            first_playlist_track = playlist_tracks.first()

            if first_playlist_track:
Eliot Berriot's avatar
Eliot Berriot committed
776
777
778
779
                data[
                    "thumbnail_url"
                ] = (
                    first_playlist_track.track.album.attachment_cover.download_url_medium_square_crop
Eliot Berriot's avatar
Eliot Berriot committed
780
                )
Eliot Berriot's avatar
Eliot Berriot committed
781
782
                data["thumbnail_width"] = 200
                data["thumbnail_height"] = 200
Eliot Berriot's avatar
Eliot Berriot committed
783
784
785
786
787
788
789
            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})
            )
790
791
792
793
794
795
796
        else:
            raise serializers.ValidationError(
                "Unsupported url: {}".format(validated_data["url"])
            )
        data[
            "html"
        ] = '<iframe width="{}" height="{}" scrolling="no" frameborder="no" src="{}"></iframe>'.format(
797
            data["width"], data["height"], get_embed_url(embed_type, embed_id)
798
799
800
801
802
        )
        return data

    def create(self, data):
        return data
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844


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)

    artist = common_serializers.RelatedField(
        "id",
        queryset=models.Artist.objects.exclude(channel__isnull=True),
        required=True,
        serializer=None,
        filters=lambda context: {"attributed_to": context["user"].actor},
    )

    def validate(self, validated_data):
        duplicates = validated_data["artist"].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):
        obj.artist.attachment_cover
        return AlbumSerializer(obj, context=self.context).data

    def create(self, validated_data):
        instance = models.Album.objects.create(
            attributed_to=self.context["user"].actor,
            artist=validated_data["artist"],
            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 []))
845
        instance.artist.get_channel()
846
        return instance
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866


class FSImportSerializer(serializers.Serializer):
    path = serializers.CharField(allow_blank=True)
    library = serializers.UUIDField()
    import_reference = serializers.CharField()

    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")