mirror of
https://code.eliotberriot.com/funkwhale/funkwhale.git
synced 2025-10-04 02:29:17 +02:00
See #170: add a description field on tracks, albums, tracks
This commit is contained in:
parent
424b9f133a
commit
2bc71eecfd
38 changed files with 653 additions and 59 deletions
|
@ -26,3 +26,12 @@ class AttachmentFactory(NoUpdateOnCreate, factory.django.DjangoModelFactory):
|
|||
|
||||
class Meta:
|
||||
model = "common.Attachment"
|
||||
|
||||
|
||||
@registry.register
|
||||
class CommonFactory(NoUpdateOnCreate, factory.django.DjangoModelFactory):
|
||||
text = factory.Faker("paragraph")
|
||||
content_type = "text/plain"
|
||||
|
||||
class Meta:
|
||||
model = "common.Content"
|
||||
|
|
21
api/funkwhale_api/common/migrations/0006_content.py
Normal file
21
api/funkwhale_api/common/migrations/0006_content.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
# Generated by Django 2.2.7 on 2020-01-13 10:14
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('common', '0005_auto_20191125_1421'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Content',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('text', models.CharField(blank=True, max_length=5000, null=True)),
|
||||
('content_type', models.CharField(max_length=100)),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -24,6 +24,14 @@ from . import utils
|
|||
from . import validators
|
||||
|
||||
|
||||
CONTENT_TEXT_MAX_LENGTH = 5000
|
||||
CONTENT_TEXT_SUPPORTED_TYPES = [
|
||||
"text/html",
|
||||
"text/markdown",
|
||||
"text/plain",
|
||||
]
|
||||
|
||||
|
||||
@Field.register_lookup
|
||||
class NotEqual(Lookup):
|
||||
lookup_name = "ne"
|
||||
|
@ -273,6 +281,15 @@ class MutationAttachment(models.Model):
|
|||
unique_together = ("attachment", "mutation")
|
||||
|
||||
|
||||
class Content(models.Model):
|
||||
"""
|
||||
A text content that can be associated to other models, like a description, a summary, etc.
|
||||
"""
|
||||
|
||||
text = models.CharField(max_length=CONTENT_TEXT_MAX_LENGTH, blank=True, null=True)
|
||||
content_type = models.CharField(max_length=100)
|
||||
|
||||
|
||||
@receiver(models.signals.post_save, sender=Attachment)
|
||||
def warm_attachment_thumbnails(sender, instance, **kwargs):
|
||||
if not instance.file or not settings.CREATE_IMAGE_THUMBNAILS:
|
||||
|
@ -302,3 +319,18 @@ def trigger_mutation_post_init(sender, instance, created, **kwargs):
|
|||
except AttributeError:
|
||||
return
|
||||
handler(instance)
|
||||
|
||||
|
||||
CONTENT_FKS = {
|
||||
"music.Track": ["description"],
|
||||
"music.Album": ["description"],
|
||||
"music.Artist": ["description"],
|
||||
}
|
||||
|
||||
|
||||
@receiver(models.signals.post_delete, sender=None)
|
||||
def remove_attached_content(sender, instance, **kwargs):
|
||||
fk_fields = CONTENT_FKS.get(instance._meta.label, [])
|
||||
for field in fk_fields:
|
||||
if getattr(instance, "{}_id".format(field)):
|
||||
getattr(instance, field).delete()
|
||||
|
|
|
@ -86,7 +86,6 @@ class MutationSerializer(serializers.Serializer):
|
|||
|
||||
class UpdateMutationSerializer(serializers.ModelSerializer, MutationSerializer):
|
||||
serialized_relations = {}
|
||||
previous_state_handlers = {}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# we force partial mode, because update mutations are partial
|
||||
|
@ -141,9 +140,12 @@ class UpdateMutationSerializer(serializers.ModelSerializer, MutationSerializer):
|
|||
obj,
|
||||
*list(validated_data.keys()),
|
||||
serialized_relations=self.serialized_relations,
|
||||
handlers=self.previous_state_handlers,
|
||||
handlers=self.get_previous_state_handlers(),
|
||||
)
|
||||
|
||||
def get_previous_state_handlers(self):
|
||||
return {}
|
||||
|
||||
|
||||
def get_update_previous_state(obj, *fields, serialized_relations={}, handlers={}):
|
||||
if not fields:
|
||||
|
|
|
@ -11,6 +11,7 @@ from django.utils.encoding import smart_text
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from . import models
|
||||
from . import utils
|
||||
|
||||
|
||||
class RelatedField(serializers.RelatedField):
|
||||
|
@ -308,3 +309,12 @@ class AttachmentSerializer(serializers.Serializer):
|
|||
return models.Attachment.objects.create(
|
||||
file=validated_data["file"], actor=validated_data["actor"]
|
||||
)
|
||||
|
||||
|
||||
class ContentSerializer(serializers.Serializer):
|
||||
text = serializers.CharField(max_length=models.CONTENT_TEXT_MAX_LENGTH)
|
||||
content_type = serializers.ChoiceField(choices=models.CONTENT_TEXT_SUPPORTED_TYPES,)
|
||||
html = serializers.SerializerMethodField()
|
||||
|
||||
def get_html(self, o):
|
||||
return utils.render_html(o.text, o.content_type)
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
from django.utils.deconstruct import deconstructible
|
||||
|
||||
import bleach.sanitizer
|
||||
import markdown
|
||||
import os
|
||||
import shutil
|
||||
import uuid
|
||||
|
@ -241,3 +243,65 @@ def join_queries_or(left, right):
|
|||
return left | right
|
||||
else:
|
||||
return right
|
||||
|
||||
|
||||
def render_markdown(text):
|
||||
return markdown.markdown(text, extensions=["nl2br"])
|
||||
|
||||
|
||||
HTMl_CLEANER = bleach.sanitizer.Cleaner(
|
||||
strip=True,
|
||||
tags=[
|
||||
"p",
|
||||
"a",
|
||||
"abbr",
|
||||
"acronym",
|
||||
"b",
|
||||
"blockquote",
|
||||
"code",
|
||||
"em",
|
||||
"i",
|
||||
"li",
|
||||
"ol",
|
||||
"strong",
|
||||
"ul",
|
||||
],
|
||||
)
|
||||
|
||||
HTML_LINKER = bleach.linkifier.Linker()
|
||||
|
||||
|
||||
def clean_html(html):
|
||||
return HTMl_CLEANER.clean(html)
|
||||
|
||||
|
||||
def render_html(text, content_type):
|
||||
rendered = render_markdown(text)
|
||||
if content_type == "text/html":
|
||||
rendered = text
|
||||
elif content_type == "text/markdown":
|
||||
rendered = render_markdown(text)
|
||||
else:
|
||||
rendered = render_markdown(text)
|
||||
rendered = HTML_LINKER.linkify(rendered)
|
||||
return clean_html(rendered).strip().replace("\n", "")
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def attach_content(obj, field, content_data):
|
||||
from . import models
|
||||
|
||||
existing = getattr(obj, "{}_id".format(field))
|
||||
|
||||
if existing:
|
||||
getattr(obj, field).delete()
|
||||
|
||||
if not content_data:
|
||||
return
|
||||
|
||||
content_obj = models.Content.objects.create(
|
||||
text=content_data["text"][: models.CONTENT_TEXT_MAX_LENGTH],
|
||||
content_type=content_data["content_type"],
|
||||
)
|
||||
setattr(obj, field, content_obj)
|
||||
obj.save(update_fields=[field])
|
||||
|
|
|
@ -9,7 +9,7 @@ from django.db import transaction
|
|||
|
||||
from rest_framework import serializers
|
||||
|
||||
from funkwhale_api.common import utils as funkwhale_utils
|
||||
from funkwhale_api.common import utils as common_utils
|
||||
from funkwhale_api.common import models as common_models
|
||||
from funkwhale_api.music import licenses
|
||||
from funkwhale_api.music import models as music_models
|
||||
|
@ -611,9 +611,9 @@ class PaginatedCollectionSerializer(jsonld.JsonLdSerializer):
|
|||
|
||||
def to_representation(self, conf):
|
||||
paginator = Paginator(conf["items"], conf.get("page_size", 20))
|
||||
first = funkwhale_utils.set_query_parameter(conf["id"], page=1)
|
||||
first = common_utils.set_query_parameter(conf["id"], page=1)
|
||||
current = first
|
||||
last = funkwhale_utils.set_query_parameter(conf["id"], page=paginator.num_pages)
|
||||
last = common_utils.set_query_parameter(conf["id"], page=paginator.num_pages)
|
||||
d = {
|
||||
"id": conf["id"],
|
||||
# XXX Stable release: remove the obsolete actor field
|
||||
|
@ -646,7 +646,7 @@ class LibrarySerializer(PaginatedCollectionSerializer):
|
|||
)
|
||||
|
||||
class Meta:
|
||||
jsonld_mapping = funkwhale_utils.concat_dicts(
|
||||
jsonld_mapping = common_utils.concat_dicts(
|
||||
PAGINATED_COLLECTION_JSONLD_MAPPING,
|
||||
{
|
||||
"name": jsonld.first_val(contexts.AS.name),
|
||||
|
@ -740,11 +740,11 @@ class CollectionPageSerializer(jsonld.JsonLdSerializer):
|
|||
|
||||
def to_representation(self, conf):
|
||||
page = conf["page"]
|
||||
first = funkwhale_utils.set_query_parameter(conf["id"], page=1)
|
||||
last = funkwhale_utils.set_query_parameter(
|
||||
first = common_utils.set_query_parameter(conf["id"], page=1)
|
||||
last = common_utils.set_query_parameter(
|
||||
conf["id"], page=page.paginator.num_pages
|
||||
)
|
||||
id = funkwhale_utils.set_query_parameter(conf["id"], page=page.number)
|
||||
id = common_utils.set_query_parameter(conf["id"], page=page.number)
|
||||
d = {
|
||||
"id": id,
|
||||
"partOf": conf["id"],
|
||||
|
@ -764,12 +764,12 @@ class CollectionPageSerializer(jsonld.JsonLdSerializer):
|
|||
}
|
||||
|
||||
if page.has_previous():
|
||||
d["prev"] = funkwhale_utils.set_query_parameter(
|
||||
d["prev"] = common_utils.set_query_parameter(
|
||||
conf["id"], page=page.previous_page_number()
|
||||
)
|
||||
|
||||
if page.has_next():
|
||||
d["next"] = funkwhale_utils.set_query_parameter(
|
||||
d["next"] = common_utils.set_query_parameter(
|
||||
conf["id"], page=page.next_page_number()
|
||||
)
|
||||
d.update(get_additional_fields(conf))
|
||||
|
@ -784,6 +784,8 @@ MUSIC_ENTITY_JSONLD_MAPPING = {
|
|||
"musicbrainzId": jsonld.first_val(contexts.FW.musicbrainzId),
|
||||
"attributedTo": jsonld.first_id(contexts.AS.attributedTo),
|
||||
"tags": jsonld.raw(contexts.AS.tag),
|
||||
"mediaType": jsonld.first_val(contexts.AS.mediaType),
|
||||
"content": jsonld.first_val(contexts.AS.content),
|
||||
}
|
||||
|
||||
|
||||
|
@ -805,6 +807,28 @@ def repr_tag(tag_name):
|
|||
return {"type": "Hashtag", "name": "#{}".format(tag_name)}
|
||||
|
||||
|
||||
def include_content(repr, content_obj):
|
||||
if not content_obj:
|
||||
return
|
||||
|
||||
repr["content"] = common_utils.render_html(
|
||||
content_obj.text, content_obj.content_type
|
||||
)
|
||||
repr["mediaType"] = "text/html"
|
||||
|
||||
|
||||
class TruncatedCharField(serializers.CharField):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.truncate_length = kwargs.pop("truncate_length")
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def to_internal_value(self, v):
|
||||
v = super().to_internal_value(v)
|
||||
if v:
|
||||
v = v[: self.truncate_length]
|
||||
return v
|
||||
|
||||
|
||||
class MusicEntitySerializer(jsonld.JsonLdSerializer):
|
||||
id = serializers.URLField(max_length=500)
|
||||
published = serializers.DateTimeField()
|
||||
|
@ -815,13 +839,23 @@ class MusicEntitySerializer(jsonld.JsonLdSerializer):
|
|||
tags = serializers.ListField(
|
||||
child=TagSerializer(), min_length=0, required=False, allow_null=True
|
||||
)
|
||||
mediaType = serializers.ChoiceField(
|
||||
choices=common_models.CONTENT_TEXT_SUPPORTED_TYPES,
|
||||
default="text/html",
|
||||
required=False,
|
||||
)
|
||||
content = TruncatedCharField(
|
||||
truncate_length=common_models.CONTENT_TEXT_MAX_LENGTH,
|
||||
required=False,
|
||||
allow_null=True,
|
||||
)
|
||||
|
||||
@transaction.atomic
|
||||
def update(self, instance, validated_data):
|
||||
attributed_to_fid = validated_data.get("attributedTo")
|
||||
if attributed_to_fid:
|
||||
validated_data["attributedTo"] = actors.get_actor(attributed_to_fid)
|
||||
updated_fields = funkwhale_utils.get_updated_fields(
|
||||
updated_fields = common_utils.get_updated_fields(
|
||||
self.updateable_fields, validated_data, instance
|
||||
)
|
||||
updated_fields = self.validate_updated_data(instance, updated_fields)
|
||||
|
@ -831,6 +865,9 @@ class MusicEntitySerializer(jsonld.JsonLdSerializer):
|
|||
|
||||
tags = [t["name"] for t in validated_data.get("tags", []) or []]
|
||||
tags_models.set_tags(instance, *tags)
|
||||
common_utils.attach_content(
|
||||
instance, "description", validated_data.get("description")
|
||||
)
|
||||
return instance
|
||||
|
||||
def get_tags_repr(self, instance):
|
||||
|
@ -842,6 +879,15 @@ class MusicEntitySerializer(jsonld.JsonLdSerializer):
|
|||
def validate_updated_data(self, instance, validated_data):
|
||||
return validated_data
|
||||
|
||||
def validate(self, data):
|
||||
validated_data = super().validate(data)
|
||||
if data.get("content"):
|
||||
validated_data["description"] = {
|
||||
"content_type": data["mediaType"],
|
||||
"text": data["content"],
|
||||
}
|
||||
return validated_data
|
||||
|
||||
|
||||
class ArtistSerializer(MusicEntitySerializer):
|
||||
updateable_fields = [
|
||||
|
@ -866,7 +912,7 @@ class ArtistSerializer(MusicEntitySerializer):
|
|||
else None,
|
||||
"tag": self.get_tags_repr(instance),
|
||||
}
|
||||
|
||||
include_content(d, instance.description)
|
||||
if self.context.get("include_ap_context", self.parent is None):
|
||||
d["@context"] = jsonld.get_default_context()
|
||||
return d
|
||||
|
@ -888,7 +934,7 @@ class AlbumSerializer(MusicEntitySerializer):
|
|||
|
||||
class Meta:
|
||||
model = music_models.Album
|
||||
jsonld_mapping = funkwhale_utils.concat_dicts(
|
||||
jsonld_mapping = common_utils.concat_dicts(
|
||||
MUSIC_ENTITY_JSONLD_MAPPING,
|
||||
{
|
||||
"released": jsonld.first_val(contexts.FW.released),
|
||||
|
@ -917,6 +963,7 @@ class AlbumSerializer(MusicEntitySerializer):
|
|||
else None,
|
||||
"tag": self.get_tags_repr(instance),
|
||||
}
|
||||
include_content(d, instance.description)
|
||||
if instance.attachment_cover:
|
||||
d["cover"] = {
|
||||
"type": "Link",
|
||||
|
@ -968,7 +1015,7 @@ class TrackSerializer(MusicEntitySerializer):
|
|||
|
||||
class Meta:
|
||||
model = music_models.Track
|
||||
jsonld_mapping = funkwhale_utils.concat_dicts(
|
||||
jsonld_mapping = common_utils.concat_dicts(
|
||||
MUSIC_ENTITY_JSONLD_MAPPING,
|
||||
{
|
||||
"album": jsonld.first_obj(contexts.FW.album),
|
||||
|
@ -1006,7 +1053,7 @@ class TrackSerializer(MusicEntitySerializer):
|
|||
else None,
|
||||
"tag": self.get_tags_repr(instance),
|
||||
}
|
||||
|
||||
include_content(d, instance.description)
|
||||
if self.context.get("include_ap_context", self.parent is None):
|
||||
d["@context"] = jsonld.get_default_context()
|
||||
return d
|
||||
|
@ -1017,23 +1064,21 @@ class TrackSerializer(MusicEntitySerializer):
|
|||
references = {}
|
||||
actors_to_fetch = set()
|
||||
actors_to_fetch.add(
|
||||
funkwhale_utils.recursive_getattr(
|
||||
common_utils.recursive_getattr(
|
||||
validated_data, "attributedTo", permissive=True
|
||||
)
|
||||
)
|
||||
actors_to_fetch.add(
|
||||
funkwhale_utils.recursive_getattr(
|
||||
common_utils.recursive_getattr(
|
||||
validated_data, "album.attributedTo", permissive=True
|
||||
)
|
||||
)
|
||||
artists = (
|
||||
funkwhale_utils.recursive_getattr(
|
||||
validated_data, "artists", permissive=True
|
||||
)
|
||||
common_utils.recursive_getattr(validated_data, "artists", permissive=True)
|
||||
or []
|
||||
)
|
||||
album_artists = (
|
||||
funkwhale_utils.recursive_getattr(
|
||||
common_utils.recursive_getattr(
|
||||
validated_data, "album.artists", permissive=True
|
||||
)
|
||||
or []
|
||||
|
@ -1244,6 +1289,7 @@ class ChannelUploadSerializer(serializers.Serializer):
|
|||
},
|
||||
],
|
||||
}
|
||||
include_content(data, upload.track.description)
|
||||
tags = [item.tag.name for item in upload.get_all_tagged_items()]
|
||||
if tags:
|
||||
data["tag"] = [repr_tag(name) for name in tags]
|
||||
|
|
|
@ -225,11 +225,14 @@ class MusicLibraryViewSet(
|
|||
"album__attributed_to",
|
||||
"attributed_to",
|
||||
"album__attachment_cover",
|
||||
"description",
|
||||
).prefetch_related(
|
||||
"tagged_items__tag",
|
||||
"album__tagged_items__tag",
|
||||
"album__artist__tagged_items__tag",
|
||||
"artist__tagged_items__tag",
|
||||
"artist__description",
|
||||
"album__description",
|
||||
),
|
||||
)
|
||||
),
|
||||
|
@ -278,6 +281,7 @@ class MusicUploadViewSet(
|
|||
"library__actor",
|
||||
"track__artist",
|
||||
"track__album__artist",
|
||||
"track__description",
|
||||
"track__album__attachment_cover",
|
||||
)
|
||||
serializer_class = serializers.UploadSerializer
|
||||
|
@ -299,7 +303,7 @@ class MusicArtistViewSet(
|
|||
):
|
||||
authentication_classes = [authentication.SignatureAuthentication]
|
||||
renderer_classes = renderers.get_ap_renderers()
|
||||
queryset = music_models.Artist.objects.local()
|
||||
queryset = music_models.Artist.objects.local().select_related("description")
|
||||
serializer_class = serializers.ArtistSerializer
|
||||
lookup_field = "uuid"
|
||||
|
||||
|
@ -309,7 +313,9 @@ class MusicAlbumViewSet(
|
|||
):
|
||||
authentication_classes = [authentication.SignatureAuthentication]
|
||||
renderer_classes = renderers.get_ap_renderers()
|
||||
queryset = music_models.Album.objects.local().select_related("artist")
|
||||
queryset = music_models.Album.objects.local().select_related(
|
||||
"artist__description", "description"
|
||||
)
|
||||
serializer_class = serializers.AlbumSerializer
|
||||
lookup_field = "uuid"
|
||||
|
||||
|
@ -320,7 +326,7 @@ class MusicTrackViewSet(
|
|||
authentication_classes = [authentication.SignatureAuthentication]
|
||||
renderer_classes = renderers.get_ap_renderers()
|
||||
queryset = music_models.Track.objects.local().select_related(
|
||||
"album__artist", "artist"
|
||||
"album__artist", "album__description", "artist__description", "description"
|
||||
)
|
||||
serializer_class = serializers.TrackSerializer
|
||||
lookup_field = "uuid"
|
||||
|
|
|
@ -383,7 +383,9 @@ class ManageNestedAlbumSerializer(ManageBaseAlbumSerializer):
|
|||
return getattr(obj, "tracks_count", None)
|
||||
|
||||
|
||||
class ManageArtistSerializer(ManageBaseArtistSerializer):
|
||||
class ManageArtistSerializer(
|
||||
music_serializers.OptionalDescriptionMixin, ManageBaseArtistSerializer
|
||||
):
|
||||
albums = ManageNestedAlbumSerializer(many=True)
|
||||
tracks = ManageNestedTrackSerializer(many=True)
|
||||
attributed_to = ManageBaseActorSerializer()
|
||||
|
@ -407,7 +409,9 @@ class ManageNestedArtistSerializer(ManageBaseArtistSerializer):
|
|||
pass
|
||||
|
||||
|
||||
class ManageAlbumSerializer(ManageBaseAlbumSerializer):
|
||||
class ManageAlbumSerializer(
|
||||
music_serializers.OptionalDescriptionMixin, ManageBaseAlbumSerializer
|
||||
):
|
||||
tracks = ManageNestedTrackSerializer(many=True)
|
||||
attributed_to = ManageBaseActorSerializer()
|
||||
artist = ManageNestedArtistSerializer()
|
||||
|
@ -435,7 +439,9 @@ class ManageTrackAlbumSerializer(ManageBaseAlbumSerializer):
|
|||
fields = ManageBaseAlbumSerializer.Meta.fields + ["artist"]
|
||||
|
||||
|
||||
class ManageTrackSerializer(ManageNestedTrackSerializer):
|
||||
class ManageTrackSerializer(
|
||||
music_serializers.OptionalDescriptionMixin, ManageNestedTrackSerializer
|
||||
):
|
||||
artist = ManageNestedArtistSerializer()
|
||||
album = ManageTrackAlbumSerializer()
|
||||
attributed_to = ManageBaseActorSerializer()
|
||||
|
|
|
@ -100,6 +100,11 @@ class ManageArtistViewSet(
|
|||
result = serializer.save()
|
||||
return response.Response(result, status=200)
|
||||
|
||||
def get_serializer_context(self):
|
||||
context = super().get_serializer_context()
|
||||
context["description"] = self.action in ["retrieve", "create", "update"]
|
||||
return context
|
||||
|
||||
|
||||
class ManageAlbumViewSet(
|
||||
mixins.ListModelMixin,
|
||||
|
@ -134,6 +139,11 @@ class ManageAlbumViewSet(
|
|||
result = serializer.save()
|
||||
return response.Response(result, status=200)
|
||||
|
||||
def get_serializer_context(self):
|
||||
context = super().get_serializer_context()
|
||||
context["description"] = self.action in ["retrieve", "create", "update"]
|
||||
return context
|
||||
|
||||
|
||||
uploads_subquery = (
|
||||
music_models.Upload.objects.filter(track_id=OuterRef("pk"))
|
||||
|
@ -186,6 +196,11 @@ class ManageTrackViewSet(
|
|||
result = serializer.save()
|
||||
return response.Response(result, status=200)
|
||||
|
||||
def get_serializer_context(self):
|
||||
context = super().get_serializer_context()
|
||||
context["description"] = self.action in ["retrieve", "create", "update"]
|
||||
return context
|
||||
|
||||
|
||||
uploads_subquery = (
|
||||
music_models.Upload.objects.filter(library_id=OuterRef("pk"))
|
||||
|
|
|
@ -168,6 +168,17 @@ def get_mp3_recording_id(f, k):
|
|||
raise TagNotFound(k)
|
||||
|
||||
|
||||
def get_mp3_comment(f, k):
|
||||
keys_to_try = ["COMM", "COMM::eng"]
|
||||
for key in keys_to_try:
|
||||
try:
|
||||
return get_id3_tag(f, key)
|
||||
except TagNotFound:
|
||||
pass
|
||||
|
||||
raise TagNotFound("COMM")
|
||||
|
||||
|
||||
VALIDATION = {}
|
||||
|
||||
CONF = {
|
||||
|
@ -192,6 +203,7 @@ CONF = {
|
|||
"field": "metadata_block_picture",
|
||||
"to_application": clean_ogg_pictures,
|
||||
},
|
||||
"comment": {"field": "comment"},
|
||||
},
|
||||
},
|
||||
"OggVorbis": {
|
||||
|
@ -215,6 +227,7 @@ CONF = {
|
|||
"field": "metadata_block_picture",
|
||||
"to_application": clean_ogg_pictures,
|
||||
},
|
||||
"comment": {"field": "comment"},
|
||||
},
|
||||
},
|
||||
"OggTheora": {
|
||||
|
@ -234,6 +247,7 @@ CONF = {
|
|||
"license": {},
|
||||
"copyright": {},
|
||||
"genre": {},
|
||||
"comment": {"field": "comment"},
|
||||
},
|
||||
},
|
||||
"MP3": {
|
||||
|
@ -255,6 +269,7 @@ CONF = {
|
|||
"pictures": {},
|
||||
"license": {"field": "WCOP"},
|
||||
"copyright": {"field": "TCOP"},
|
||||
"comment": {"field": "COMM", "getter": get_mp3_comment},
|
||||
},
|
||||
},
|
||||
"MP4": {
|
||||
|
@ -282,6 +297,7 @@ CONF = {
|
|||
"pictures": {},
|
||||
"license": {"field": "----:com.apple.iTunes:LICENSE"},
|
||||
"copyright": {"field": "cprt"},
|
||||
"comment": {"field": "©cmt"},
|
||||
},
|
||||
},
|
||||
"FLAC": {
|
||||
|
@ -304,6 +320,7 @@ CONF = {
|
|||
"pictures": {},
|
||||
"license": {},
|
||||
"copyright": {},
|
||||
"comment": {},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -322,6 +339,7 @@ ALL_FIELDS = [
|
|||
"mbid",
|
||||
"license",
|
||||
"copyright",
|
||||
"comment",
|
||||
]
|
||||
|
||||
|
||||
|
@ -657,6 +675,21 @@ class PositionField(serializers.CharField):
|
|||
pass
|
||||
|
||||
|
||||
class DescriptionField(serializers.CharField):
|
||||
def get_value(self, data):
|
||||
return data
|
||||
|
||||
def to_internal_value(self, data):
|
||||
try:
|
||||
value = data.get("comment") or None
|
||||
except TagNotFound:
|
||||
return None
|
||||
if not value:
|
||||
return None
|
||||
value = super().to_internal_value(value)
|
||||
return {"text": value, "content_type": "text/plain"}
|
||||
|
||||
|
||||
class TrackMetadataSerializer(serializers.Serializer):
|
||||
title = serializers.CharField()
|
||||
position = PositionField(allow_blank=True, allow_null=True, required=False)
|
||||
|
@ -665,6 +698,7 @@ class TrackMetadataSerializer(serializers.Serializer):
|
|||
license = serializers.CharField(allow_blank=True, allow_null=True, required=False)
|
||||
mbid = MBIDField()
|
||||
tags = TagsField(allow_blank=True, allow_null=True, required=False)
|
||||
description = DescriptionField(allow_null=True, allow_blank=True, required=False)
|
||||
|
||||
album = AlbumField()
|
||||
artists = ArtistField()
|
||||
|
@ -672,6 +706,7 @@ class TrackMetadataSerializer(serializers.Serializer):
|
|||
|
||||
remove_blank_null_fields = [
|
||||
"copyright",
|
||||
"description",
|
||||
"license",
|
||||
"position",
|
||||
"disc_number",
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
# Generated by Django 2.2.7 on 2020-01-13 10:18
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('common', '0006_content'),
|
||||
('music', '0045_full_text_search_stop_words'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='album',
|
||||
name='description',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='common.Content'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='artist',
|
||||
name='description',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='common.Content'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='track',
|
||||
name='description',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='common.Content'),
|
||||
),
|
||||
]
|
|
@ -227,6 +227,9 @@ class Artist(APIModelMixin):
|
|||
content_type_field="object_content_type",
|
||||
object_id_field="object_id",
|
||||
)
|
||||
description = models.ForeignKey(
|
||||
"common.Content", null=True, blank=True, on_delete=models.SET_NULL
|
||||
)
|
||||
|
||||
api = musicbrainz.api.artists
|
||||
objects = ArtistQuerySet.as_manager()
|
||||
|
@ -327,6 +330,10 @@ class Album(APIModelMixin):
|
|||
object_id_field="object_id",
|
||||
)
|
||||
|
||||
description = models.ForeignKey(
|
||||
"common.Content", null=True, blank=True, on_delete=models.SET_NULL
|
||||
)
|
||||
|
||||
api_includes = ["artist-credits", "recordings", "media", "release-groups"]
|
||||
api = musicbrainz.api.releases
|
||||
federation_namespace = "albums"
|
||||
|
@ -508,6 +515,10 @@ class Track(APIModelMixin):
|
|||
copyright = models.CharField(
|
||||
max_length=MAX_LENGTHS["COPYRIGHT"], null=True, blank=True
|
||||
)
|
||||
description = models.ForeignKey(
|
||||
"common.Content", null=True, blank=True, on_delete=models.SET_NULL
|
||||
)
|
||||
|
||||
federation_namespace = "tracks"
|
||||
musicbrainz_model = "recording"
|
||||
api = musicbrainz.api.recordings
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
from funkwhale_api.common import models as common_models
|
||||
from funkwhale_api.common import mutations
|
||||
from funkwhale_api.common import serializers as common_serializers
|
||||
from funkwhale_api.common import utils as common_utils
|
||||
|
||||
from funkwhale_api.federation import routes
|
||||
from funkwhale_api.tags import models as tags_models
|
||||
from funkwhale_api.tags import serializers as tags_serializers
|
||||
|
@ -23,11 +25,13 @@ def can_approve(obj, actor):
|
|||
|
||||
class TagMutation(mutations.UpdateMutationSerializer):
|
||||
tags = tags_serializers.TagsListField()
|
||||
previous_state_handlers = {
|
||||
"tags": lambda obj: list(
|
||||
|
||||
def get_previous_state_handlers(self):
|
||||
handlers = super().get_previous_state_handlers()
|
||||
handlers["tags"] = lambda obj: list(
|
||||
sorted(obj.tagged_items.values_list("tag__name", flat=True))
|
||||
)
|
||||
}
|
||||
return handlers
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
tags = validated_data.pop("tags", [])
|
||||
|
@ -36,17 +40,36 @@ class TagMutation(mutations.UpdateMutationSerializer):
|
|||
return r
|
||||
|
||||
|
||||
class DescriptionMutation(mutations.UpdateMutationSerializer):
|
||||
description = common_serializers.ContentSerializer()
|
||||
|
||||
def get_previous_state_handlers(self):
|
||||
handlers = super().get_previous_state_handlers()
|
||||
handlers["description"] = (
|
||||
lambda obj: common_serializers.ContentSerializer(obj.description).data
|
||||
if obj.description_id
|
||||
else None
|
||||
)
|
||||
return handlers
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
description = validated_data.pop("description", None)
|
||||
r = super().update(instance, validated_data)
|
||||
common_utils.attach_content(instance, "description", description)
|
||||
return r
|
||||
|
||||
|
||||
@mutations.registry.connect(
|
||||
"update",
|
||||
models.Track,
|
||||
perm_checkers={"suggest": can_suggest, "approve": can_approve},
|
||||
)
|
||||
class TrackMutationSerializer(TagMutation):
|
||||
class TrackMutationSerializer(TagMutation, DescriptionMutation):
|
||||
serialized_relations = {"license": "code"}
|
||||
|
||||
class Meta:
|
||||
model = models.Track
|
||||
fields = ["license", "title", "position", "copyright", "tags"]
|
||||
fields = ["license", "title", "position", "copyright", "tags", "description"]
|
||||
|
||||
def post_apply(self, obj, validated_data):
|
||||
routes.outbox.dispatch(
|
||||
|
@ -59,10 +82,10 @@ class TrackMutationSerializer(TagMutation):
|
|||
models.Artist,
|
||||
perm_checkers={"suggest": can_suggest, "approve": can_approve},
|
||||
)
|
||||
class ArtistMutationSerializer(TagMutation):
|
||||
class ArtistMutationSerializer(TagMutation, DescriptionMutation):
|
||||
class Meta:
|
||||
model = models.Artist
|
||||
fields = ["name", "tags"]
|
||||
fields = ["name", "tags", "description"]
|
||||
|
||||
def post_apply(self, obj, validated_data):
|
||||
routes.outbox.dispatch(
|
||||
|
@ -75,27 +98,23 @@ class ArtistMutationSerializer(TagMutation):
|
|||
models.Album,
|
||||
perm_checkers={"suggest": can_suggest, "approve": can_approve},
|
||||
)
|
||||
class AlbumMutationSerializer(TagMutation):
|
||||
class AlbumMutationSerializer(TagMutation, DescriptionMutation):
|
||||
cover = common_serializers.RelatedField(
|
||||
"uuid", queryset=common_models.Attachment.objects.all().local(), serializer=None
|
||||
)
|
||||
|
||||
serialized_relations = {"cover": "uuid"}
|
||||
previous_state_handlers = dict(
|
||||
list(TagMutation.previous_state_handlers.items())
|
||||
+ [
|
||||
(
|
||||
"cover",
|
||||
lambda obj: str(obj.attachment_cover.uuid)
|
||||
if obj.attachment_cover
|
||||
else None,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = models.Album
|
||||
fields = ["title", "release_date", "tags", "cover"]
|
||||
fields = ["title", "release_date", "tags", "cover", "description"]
|
||||
|
||||
def get_previous_state_handlers(self):
|
||||
handlers = super().get_previous_state_handlers()
|
||||
handlers["cover"] = (
|
||||
lambda obj: str(obj.attachment_cover.uuid) if obj.attachment_cover else None
|
||||
)
|
||||
return handlers
|
||||
|
||||
def post_apply(self, obj, validated_data):
|
||||
routes.outbox.dispatch(
|
||||
|
|
|
@ -49,6 +49,20 @@ def serialize_attributed_to(self, obj):
|
|||
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()
|
||||
|
@ -96,7 +110,7 @@ class ArtistAlbumSerializer(serializers.Serializer):
|
|||
DATETIME_FIELD = serializers.DateTimeField()
|
||||
|
||||
|
||||
class ArtistWithAlbumsSerializer(serializers.Serializer):
|
||||
class ArtistWithAlbumsSerializer(OptionalDescriptionMixin, serializers.Serializer):
|
||||
albums = ArtistAlbumSerializer(many=True)
|
||||
tags = serializers.SerializerMethodField()
|
||||
attributed_to = serializers.SerializerMethodField()
|
||||
|
@ -152,7 +166,7 @@ def serialize_album_track(track):
|
|||
}
|
||||
|
||||
|
||||
class AlbumSerializer(serializers.Serializer):
|
||||
class AlbumSerializer(OptionalDescriptionMixin, serializers.Serializer):
|
||||
tracks = serializers.SerializerMethodField()
|
||||
artist = serializers.SerializerMethodField()
|
||||
cover = cover_field
|
||||
|
@ -225,7 +239,7 @@ def serialize_upload(upload):
|
|||
}
|
||||
|
||||
|
||||
class TrackSerializer(serializers.Serializer):
|
||||
class TrackSerializer(OptionalDescriptionMixin, serializers.Serializer):
|
||||
artist = serializers.SerializerMethodField()
|
||||
album = TrackAlbumSerializer(read_only=True)
|
||||
uploads = serializers.SerializerMethodField()
|
||||
|
|
|
@ -12,6 +12,7 @@ from musicbrainzngs import ResponseError
|
|||
from requests.exceptions import RequestException
|
||||
|
||||
from funkwhale_api.common import channels, preferences
|
||||
from funkwhale_api.common import utils as common_utils
|
||||
from funkwhale_api.federation import routes
|
||||
from funkwhale_api.federation import library as lb
|
||||
from funkwhale_api.federation import utils as federation_utils
|
||||
|
@ -309,6 +310,7 @@ def federation_audio_track_to_metadata(payload, references):
|
|||
"disc_number": payload.get("disc"),
|
||||
"license": payload.get("license"),
|
||||
"copyright": payload.get("copyright"),
|
||||
"description": payload.get("description"),
|
||||
"attributed_to": references.get(payload.get("attributedTo")),
|
||||
"mbid": str(payload.get("musicbrainzId"))
|
||||
if payload.get("musicbrainzId")
|
||||
|
@ -317,6 +319,7 @@ def federation_audio_track_to_metadata(payload, references):
|
|||
"title": payload["album"]["name"],
|
||||
"fdate": payload["album"]["published"],
|
||||
"fid": payload["album"]["id"],
|
||||
"description": payload["album"].get("description"),
|
||||
"attributed_to": references.get(payload["album"].get("attributedTo")),
|
||||
"mbid": str(payload["album"]["musicbrainzId"])
|
||||
if payload["album"].get("musicbrainzId")
|
||||
|
@ -328,6 +331,7 @@ def federation_audio_track_to_metadata(payload, references):
|
|||
"fid": a["id"],
|
||||
"name": a["name"],
|
||||
"fdate": a["published"],
|
||||
"description": a.get("description"),
|
||||
"attributed_to": references.get(a.get("attributedTo")),
|
||||
"mbid": str(a["musicbrainzId"]) if a.get("musicbrainzId") else None,
|
||||
"tags": [t["name"] for t in a.get("tags", []) or []],
|
||||
|
@ -340,6 +344,7 @@ def federation_audio_track_to_metadata(payload, references):
|
|||
"fid": a["id"],
|
||||
"name": a["name"],
|
||||
"fdate": a["published"],
|
||||
"description": a.get("description"),
|
||||
"attributed_to": references.get(a.get("attributedTo")),
|
||||
"mbid": str(a["musicbrainzId"]) if a.get("musicbrainzId") else None,
|
||||
"tags": [t["name"] for t in a.get("tags", []) or []],
|
||||
|
@ -505,6 +510,9 @@ def _get_track(data, attributed_to=None, **forced_values):
|
|||
)
|
||||
if created:
|
||||
tags_models.add_tags(artist, *artist_data.get("tags", []))
|
||||
common_utils.attach_content(
|
||||
artist, "description", artist_data.get("description")
|
||||
)
|
||||
|
||||
if "album" in forced_values:
|
||||
album = forced_values["album"]
|
||||
|
@ -539,6 +547,9 @@ def _get_track(data, attributed_to=None, **forced_values):
|
|||
)
|
||||
if created:
|
||||
tags_models.add_tags(album_artist, *album_artist_data.get("tags", []))
|
||||
common_utils.attach_content(
|
||||
album_artist, "description", album_artist_data.get("description")
|
||||
)
|
||||
|
||||
# get / create album
|
||||
album_data = data["album"]
|
||||
|
@ -569,6 +580,9 @@ def _get_track(data, attributed_to=None, **forced_values):
|
|||
)
|
||||
if created:
|
||||
tags_models.add_tags(album, *album_data.get("tags", []))
|
||||
common_utils.attach_content(
|
||||
album, "description", album_data.get("description")
|
||||
)
|
||||
|
||||
# get / create track
|
||||
track_title = (
|
||||
|
@ -602,6 +616,7 @@ def _get_track(data, attributed_to=None, **forced_values):
|
|||
query |= Q(mbid=track_mbid)
|
||||
if track_fid:
|
||||
query |= Q(fid=track_fid)
|
||||
|
||||
defaults = {
|
||||
"title": track_title,
|
||||
"album": album,
|
||||
|
@ -627,6 +642,8 @@ def _get_track(data, attributed_to=None, **forced_values):
|
|||
forced_values["tags"] if "tags" in forced_values else data.get("tags", [])
|
||||
)
|
||||
tags_models.add_tags(track, *tags)
|
||||
common_utils.attach_content(track, "description", data.get("description"))
|
||||
|
||||
return track
|
||||
|
||||
|
||||
|
|
|
@ -143,6 +143,11 @@ class ArtistViewSet(
|
|||
obj = refetch_obj(obj, self.get_queryset())
|
||||
return obj
|
||||
|
||||
def get_serializer_context(self):
|
||||
context = super().get_serializer_context()
|
||||
context["description"] = self.action in ["retrieve", "create", "update"]
|
||||
return context
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
albums = models.Album.objects.with_tracks_count().select_related(
|
||||
|
@ -194,6 +199,11 @@ class AlbumViewSet(
|
|||
obj = refetch_obj(obj, self.get_queryset())
|
||||
return obj
|
||||
|
||||
def get_serializer_context(self):
|
||||
context = super().get_serializer_context()
|
||||
context["description"] = self.action in ["retrieve", "create", "update"]
|
||||
return context
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
tracks = (
|
||||
|
@ -332,6 +342,11 @@ class TrackViewSet(
|
|||
get_libraries(filter_uploads=lambda o, uploads: uploads.filter(track=o))
|
||||
)
|
||||
|
||||
def get_serializer_context(self):
|
||||
context = super().get_serializer_context()
|
||||
context["description"] = self.action in ["retrieve", "create", "update"]
|
||||
return context
|
||||
|
||||
|
||||
def strip_absolute_media_url(path):
|
||||
if (
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue