See #432: expose and reuse tags over federation
This commit is contained in:
parent
58081a82a7
commit
57e0eea181
|
@ -214,6 +214,7 @@ CONTEXTS = [
|
||||||
"shares": {"@id": "as:shares", "@type": "@id"},
|
"shares": {"@id": "as:shares", "@type": "@id"},
|
||||||
# Added manually
|
# Added manually
|
||||||
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
|
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
|
||||||
|
"Hashtag": "as:Hashtag",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -11,6 +11,7 @@ from funkwhale_api.common import utils as funkwhale_utils
|
||||||
from funkwhale_api.music import licenses
|
from funkwhale_api.music import licenses
|
||||||
from funkwhale_api.music import models as music_models
|
from funkwhale_api.music import models as music_models
|
||||||
from funkwhale_api.music import tasks as music_tasks
|
from funkwhale_api.music import tasks as music_tasks
|
||||||
|
from funkwhale_api.tags import models as tags_models
|
||||||
|
|
||||||
from . import activity, actors, contexts, jsonld, models, tasks, utils
|
from . import activity, actors, contexts, jsonld, models, tasks, utils
|
||||||
|
|
||||||
|
@ -781,6 +782,20 @@ MUSIC_ENTITY_JSONLD_MAPPING = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TagSerializer(jsonld.JsonLdSerializer):
|
||||||
|
type = serializers.ChoiceField(choices=[contexts.AS.Hashtag])
|
||||||
|
name = serializers.CharField(max_length=100)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
jsonld_mapping = {"name": jsonld.first_val(contexts.AS.name)}
|
||||||
|
|
||||||
|
def validate_name(self, value):
|
||||||
|
if value.startswith("#"):
|
||||||
|
# remove trailing #
|
||||||
|
value = value[1:]
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
class MusicEntitySerializer(jsonld.JsonLdSerializer):
|
class MusicEntitySerializer(jsonld.JsonLdSerializer):
|
||||||
id = serializers.URLField(max_length=500)
|
id = serializers.URLField(max_length=500)
|
||||||
published = serializers.DateTimeField()
|
published = serializers.DateTimeField()
|
||||||
|
@ -797,8 +812,10 @@ class MusicEntitySerializer(jsonld.JsonLdSerializer):
|
||||||
self.updateable_fields, validated_data, instance
|
self.updateable_fields, validated_data, instance
|
||||||
)
|
)
|
||||||
if updated_fields:
|
if updated_fields:
|
||||||
return music_tasks.update_library_entity(instance, updated_fields)
|
music_tasks.update_library_entity(instance, updated_fields)
|
||||||
|
|
||||||
|
tags = [t["name"] for t in validated_data.get("tags", []) or []]
|
||||||
|
tags_models.set_tags(instance, *tags)
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
@ -892,6 +909,9 @@ class TrackSerializer(MusicEntitySerializer):
|
||||||
album = AlbumSerializer()
|
album = AlbumSerializer()
|
||||||
license = serializers.URLField(allow_null=True, required=False)
|
license = serializers.URLField(allow_null=True, required=False)
|
||||||
copyright = serializers.CharField(allow_null=True, required=False)
|
copyright = serializers.CharField(allow_null=True, required=False)
|
||||||
|
tags = serializers.ListField(
|
||||||
|
child=TagSerializer(), min_length=0, required=False, allow_null=True
|
||||||
|
)
|
||||||
|
|
||||||
updateable_fields = [
|
updateable_fields = [
|
||||||
("name", "title"),
|
("name", "title"),
|
||||||
|
@ -914,6 +934,7 @@ class TrackSerializer(MusicEntitySerializer):
|
||||||
"disc": jsonld.first_val(contexts.FW.disc),
|
"disc": jsonld.first_val(contexts.FW.disc),
|
||||||
"license": jsonld.first_id(contexts.FW.license),
|
"license": jsonld.first_id(contexts.FW.license),
|
||||||
"position": jsonld.first_val(contexts.FW.position),
|
"position": jsonld.first_val(contexts.FW.position),
|
||||||
|
"tags": jsonld.raw(contexts.AS.tag),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -941,6 +962,12 @@ class TrackSerializer(MusicEntitySerializer):
|
||||||
"attributedTo": instance.attributed_to.fid
|
"attributedTo": instance.attributed_to.fid
|
||||||
if instance.attributed_to
|
if instance.attributed_to
|
||||||
else None,
|
else None,
|
||||||
|
"tag": [
|
||||||
|
{"type": "Hashtag", "name": "#{}".format(tag)}
|
||||||
|
for tag in sorted(
|
||||||
|
instance.tagged_items.values_list("tag__name", flat=True)
|
||||||
|
)
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.context.get("include_ap_context", self.parent is None):
|
if self.context.get("include_ap_context", self.parent is None):
|
||||||
|
@ -950,6 +977,7 @@ class TrackSerializer(MusicEntitySerializer):
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
from funkwhale_api.music import tasks as music_tasks
|
from funkwhale_api.music import tasks as music_tasks
|
||||||
|
|
||||||
|
tags = [t["name"] for t in validated_data.get("tags", []) or []]
|
||||||
references = {}
|
references = {}
|
||||||
actors_to_fetch = set()
|
actors_to_fetch = set()
|
||||||
actors_to_fetch.add(
|
actors_to_fetch.add(
|
||||||
|
@ -981,7 +1009,6 @@ class TrackSerializer(MusicEntitySerializer):
|
||||||
if not url:
|
if not url:
|
||||||
continue
|
continue
|
||||||
references[url] = actors.get_actor(url)
|
references[url] = actors.get_actor(url)
|
||||||
|
|
||||||
metadata = music_tasks.federation_audio_track_to_metadata(
|
metadata = music_tasks.federation_audio_track_to_metadata(
|
||||||
validated_data, references
|
validated_data, references
|
||||||
)
|
)
|
||||||
|
@ -990,6 +1017,7 @@ class TrackSerializer(MusicEntitySerializer):
|
||||||
if from_activity:
|
if from_activity:
|
||||||
metadata["from_activity_id"] = from_activity.pk
|
metadata["from_activity_id"] = from_activity.pk
|
||||||
track = music_tasks.get_track_from_import_metadata(metadata, update_cover=True)
|
track = music_tasks.get_track_from_import_metadata(metadata, update_cover=True)
|
||||||
|
tags_models.add_tags(track, *tags)
|
||||||
return track
|
return track
|
||||||
|
|
||||||
def update(self, obj, validated_data):
|
def update(self, obj, validated_data):
|
||||||
|
|
|
@ -604,7 +604,11 @@ def test_activity_pub_album_serializer_to_ap(factories):
|
||||||
|
|
||||||
def test_activity_pub_track_serializer_to_ap(factories):
|
def test_activity_pub_track_serializer_to_ap(factories):
|
||||||
track = factories["music.Track"](
|
track = factories["music.Track"](
|
||||||
license="cc-by-4.0", copyright="test", disc_number=3, attributed=True
|
license="cc-by-4.0",
|
||||||
|
copyright="test",
|
||||||
|
disc_number=3,
|
||||||
|
attributed=True,
|
||||||
|
set_tags=["Punk", "Rock"],
|
||||||
)
|
)
|
||||||
expected = {
|
expected = {
|
||||||
"@context": jsonld.get_default_context(),
|
"@context": jsonld.get_default_context(),
|
||||||
|
@ -626,6 +630,10 @@ def test_activity_pub_track_serializer_to_ap(factories):
|
||||||
track.album, context={"include_ap_context": False}
|
track.album, context={"include_ap_context": False}
|
||||||
).data,
|
).data,
|
||||||
"attributedTo": track.attributed_to.fid,
|
"attributedTo": track.attributed_to.fid,
|
||||||
|
"tag": [
|
||||||
|
{"type": "Hashtag", "name": "#Punk"},
|
||||||
|
{"type": "Hashtag", "name": "#Rock"},
|
||||||
|
],
|
||||||
}
|
}
|
||||||
serializer = serializers.TrackSerializer(track)
|
serializer = serializers.TrackSerializer(track)
|
||||||
|
|
||||||
|
@ -633,6 +641,7 @@ def test_activity_pub_track_serializer_to_ap(factories):
|
||||||
|
|
||||||
|
|
||||||
def test_activity_pub_track_serializer_from_ap(factories, r_mock, mocker):
|
def test_activity_pub_track_serializer_from_ap(factories, r_mock, mocker):
|
||||||
|
add_tags = mocker.patch("funkwhale_api.tags.models.add_tags")
|
||||||
track_attributed_to = factories["federation.Actor"]()
|
track_attributed_to = factories["federation.Actor"]()
|
||||||
album_attributed_to = factories["federation.Actor"]()
|
album_attributed_to = factories["federation.Actor"]()
|
||||||
album_artist_attributed_to = factories["federation.Actor"]()
|
album_artist_attributed_to = factories["federation.Actor"]()
|
||||||
|
@ -685,6 +694,10 @@ def test_activity_pub_track_serializer_from_ap(factories, r_mock, mocker):
|
||||||
"published": published.isoformat(),
|
"published": published.isoformat(),
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"tag": [
|
||||||
|
{"type": "Hashtag", "name": "#Hello"},
|
||||||
|
{"type": "Hashtag", "name": "World"},
|
||||||
|
],
|
||||||
}
|
}
|
||||||
r_mock.get(data["album"]["cover"]["href"], body=io.BytesIO(b"coucou"))
|
r_mock.get(data["album"]["cover"]["href"], body=io.BytesIO(b"coucou"))
|
||||||
serializer = serializers.TrackSerializer(data=data, context={"activity": activity})
|
serializer = serializers.TrackSerializer(data=data, context={"activity": activity})
|
||||||
|
@ -728,6 +741,48 @@ def test_activity_pub_track_serializer_from_ap(factories, r_mock, mocker):
|
||||||
assert album_artist.creation_date == published
|
assert album_artist.creation_date == published
|
||||||
assert album_artist.attributed_to == album_artist_attributed_to
|
assert album_artist.attributed_to == album_artist_attributed_to
|
||||||
|
|
||||||
|
add_tags.assert_called_once_with(track, *["Hello", "World"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_activity_pub_track_serializer_from_ap_update(factories, r_mock, mocker):
|
||||||
|
set_tags = mocker.patch("funkwhale_api.tags.models.set_tags")
|
||||||
|
track_attributed_to = factories["federation.Actor"]()
|
||||||
|
track = factories["music.Track"]()
|
||||||
|
|
||||||
|
published = timezone.now()
|
||||||
|
data = {
|
||||||
|
"@context": jsonld.get_default_context(),
|
||||||
|
"type": "Track",
|
||||||
|
"id": track.fid,
|
||||||
|
"published": published.isoformat(),
|
||||||
|
"musicbrainzId": str(uuid.uuid4()),
|
||||||
|
"name": "Black in back",
|
||||||
|
"position": 5,
|
||||||
|
"disc": 2,
|
||||||
|
"attributedTo": track_attributed_to.fid,
|
||||||
|
"album": serializers.AlbumSerializer(track.album).data,
|
||||||
|
"artists": [serializers.ArtistSerializer(track.artist).data],
|
||||||
|
"tag": [
|
||||||
|
{"type": "Hashtag", "name": "#Hello"},
|
||||||
|
# Ensure we can handle tags without a leading #
|
||||||
|
{"type": "Hashtag", "name": "World"},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
serializer = serializers.TrackSerializer(track, data=data)
|
||||||
|
assert serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
serializer.save()
|
||||||
|
track.refresh_from_db()
|
||||||
|
|
||||||
|
assert track.fid == data["id"]
|
||||||
|
assert track.title == data["name"]
|
||||||
|
assert track.position == data["position"]
|
||||||
|
assert track.disc_number == data["disc"]
|
||||||
|
assert track.attributed_to == track_attributed_to
|
||||||
|
assert str(track.mbid) == data["musicbrainzId"]
|
||||||
|
|
||||||
|
set_tags.assert_called_once_with(track, *["Hello", "World"])
|
||||||
|
|
||||||
|
|
||||||
def test_activity_pub_upload_serializer_from_ap(factories, mocker, r_mock):
|
def test_activity_pub_upload_serializer_from_ap(factories, mocker, r_mock):
|
||||||
activity = factories["federation.Activity"]()
|
activity = factories["federation.Activity"]()
|
||||||
|
|
Loading…
Reference in New Issue