diff --git a/api/funkwhale_api/federation/models.py b/api/funkwhale_api/federation/models.py index 982619c9e..fbec8d601 100644 --- a/api/funkwhale_api/federation/models.py +++ b/api/funkwhale_api/federation/models.py @@ -414,6 +414,7 @@ class Fetch(models.Model): contexts.AS.Application: [serializers.ActorSerializer], # for mb the key must be the api namespace "recordings": [musicbrainz_serializers.RecordingSerializer], + "releases": [musicbrainz_serializers.ReleaseSerializer], } @property diff --git a/api/funkwhale_api/federation/tasks.py b/api/funkwhale_api/federation/tasks.py index e481530f5..d9d73607e 100644 --- a/api/funkwhale_api/federation/tasks.py +++ b/api/funkwhale_api/federation/tasks.py @@ -484,10 +484,13 @@ def musicbrainz_metadata_handler(type_, id): else: return obj + if type_ == "recordings": + includes = ["tags", "artists", "releases"] + elif type_ == "releases": + includes = ["tags", "artists", "recordings"] + result = replace_hyphens_in_keys( - getattr(musicbrainz.api, type_).get( - id=id, includes=["tags", "artists", "releases"] - ) + getattr(musicbrainz.api, type_).get(id=id, includes=includes) ) existing = ( @@ -537,7 +540,7 @@ def third_party_fetch(fetch_obj): type_, id = type_and_id_from_third_party[service](fetch_obj) logger.debug("Parsed URL %s into type %s and id %s", url, type_, id) except ValueError as e: - return error("url_parse_error", message=e.message) + return error("url_parse_error", message=str(e)) try: result, existing = metadata_from_third_party_[service](type_, id) @@ -587,6 +590,7 @@ def third_party_fetch(fetch_obj): fetch_obj.object = obj fetch_obj.status = "finished" fetch_obj.fetch_date = timezone.now() + # to do : trigger third party download ? return fetch_obj.save( update_fields=["fetch_date", "status", "object_id", "object_content_type"] ) diff --git a/api/funkwhale_api/musicbrainz/serializers.py b/api/funkwhale_api/musicbrainz/serializers.py index 2c68ff6d3..85e1851da 100644 --- a/api/funkwhale_api/musicbrainz/serializers.py +++ b/api/funkwhale_api/musicbrainz/serializers.py @@ -1,7 +1,12 @@ +import logging + from rest_framework import serializers +from funkwhale_api import musicbrainz from funkwhale_api.tags import models as tags_models +logger = logging.getLogger(__name__) + class ArtistSerializer(serializers.Serializer): """ @@ -18,7 +23,7 @@ class ArtistSerializer(serializers.Serializer): "name": validated_data["name"], "mbid": validated_data["id"], } - artist = Artist.objects.create(**data) + artist, created = Artist.objects.get_or_create(**data) return artist @@ -39,13 +44,13 @@ class ArtistCreditSerializer(serializers.Serializer): "joinphrase": validated_data.get("joinphrase", ""), "artist": ArtistSerializer().create(validated_data["artist"]), } - artist_credit = ArtistCredit.objects.create(**data) + artist_credit, created = ArtistCredit.objects.get_or_create(**data) return artist_credit -class ReleaseSerializer(serializers.Serializer): +class ReleaseForTrackSerializer(serializers.Serializer): """ - Serializer for Musicbrainz release data. + Serializer for Musicbrainz release data when returned in a recording object. """ id = serializers.CharField() @@ -60,9 +65,9 @@ class ReleaseSerializer(serializers.Serializer): data = { "title": validated_data["title"], "mbid": validated_data["id"], - "release_date": validated_data.get("date"), + "release_date": validated_data.get("date", None), } - album = Album.objects.create(**data) + album, created = Album.objects.get_or_create(**data) artist_credit = ArtistCreditSerializer(many=True).create( validated_data["artist_credit"] ) @@ -70,6 +75,9 @@ class ReleaseSerializer(serializers.Serializer): album.save() tags_models.add_tags(album, *validated_data.get("tags", [])) + if validated_data["media"]: + # an album can have various media/physical representation, we take the first one + validated_data["media"][0] return album def update(self, instance, validated_data): @@ -86,25 +94,29 @@ class RecordingSerializer(serializers.Serializer): Serializer for Musicbrainz track data. """ - # class Meta: - # model = Track - id = serializers.CharField() title = serializers.CharField() artist_credit = ArtistCreditSerializer(many=True) - releases = ReleaseSerializer(many=True) + releases = ReleaseForTrackSerializer(many=True, required=False) tags = serializers.ListField(child=serializers.CharField(), allow_empty=True) def create(self, validated_data): from funkwhale_api.music.models import Track - data = { + data = {"mbid": validated_data["id"]} + defaults = { "title": validated_data["title"], "mbid": validated_data["id"], # In mb a recording can have various releases, we take the fist one - "album": ReleaseSerializer(many=True).create(validated_data["releases"])[0], + "album": ( + ReleaseForTrackSerializer(many=True).create(validated_data["releases"])[ + 0 + ] + if validated_data.get("releases") + else None + ), } - track = Track.objects.create(**data) + track, created = Track.objects.get_or_create(**data, defaults=defaults) artist_credit = ArtistCreditSerializer(many=True).create( validated_data["artist_credit"] ) @@ -121,3 +133,98 @@ class RecordingSerializer(serializers.Serializer): tags_models.add_tags(instance, *validated_data.get("tags", [])) return instance + + +class RecordingForReleaseSerializer(serializers.Serializer): + id = serializers.CharField() + title = serializers.CharField() + + def create(self, validated_data): + def replace_hyphens_in_keys(obj): + if isinstance(obj, dict): + return { + k.replace("-", "_"): replace_hyphens_in_keys(v) + for k, v in obj.items() + } + elif isinstance(obj, list): + return [replace_hyphens_in_keys(item) for item in obj] + else: + return obj + + recordings_data = musicbrainz.api.recordings.get( + id=validated_data["id"], includes=["tags", "artists"] + ) + recordings_data = replace_hyphens_in_keys(recordings_data) + serializer = RecordingSerializer(data=recordings_data) + serializer.is_valid(raise_exception=True) + track = serializer.save() + track.album = validated_data["album"] + track.save() + return track + + def update(self, instance, validated_data): + instance.title = validated_data["title"] + tags_models.add_tags(instance, *validated_data.get("tags", [])) + instance.album = validated_data["album"] + instance.save() + return instance + + +class TrackSerializer(serializers.Serializer): + recording = RecordingForReleaseSerializer() + + +class MediaSerializer(serializers.Serializer): + tracks = TrackSerializer(many=True) + + +class ReleaseSerializer(serializers.Serializer): + """ + Serializer for Musicbrainz release data. + """ + + id = serializers.CharField() + title = serializers.CharField() + artist_credit = ArtistCreditSerializer(many=True) + tags = serializers.ListField(child=serializers.CharField(), allow_empty=True) + date = serializers.DateField(input_formats=["%Y", "%Y/%m/%d", "%Y-%m-%d"]) + media = serializers.ListField(child=MediaSerializer()) + + def create(self, validated_data): + from funkwhale_api.music.models import Album + + data = { + "title": validated_data["title"], + "mbid": validated_data["id"], + "release_date": validated_data.get("date"), + } + album, created = Album.objects.get_or_create(**data) + artist_credit = ArtistCreditSerializer(many=True).create( + validated_data["artist_credit"] + ) + album.artist_credit.set(artist_credit) + album.save() + tags_models.add_tags(album, *validated_data.get("tags", [])) + + # an album can have various media/physical representation, we take the first one + recordings = [t["recording"] for t in validated_data["media"][0]["tracks"]] + for r in recordings: + r["album"] = album + RecordingForReleaseSerializer().create(r) + + return album + + # this will never be used while FetchViewSet and third_party_fetch filter out finished fetch + # Would be nice to have a way to manually update releases from Musicbrainz + def update(self, instance, validated_data): + logger.info(f"Updating release {instance} with data: {validated_data}") + instance.title = validated_data["title"] + instance.release_date = validated_data.get("date") + instance.save() + tags_models.add_tags(instance, *validated_data.get("tags", [])) + + recordings = [t["recording"] for t in validated_data["media"][0]["tracks"]] + for r in recordings: + r["album"] = instance + RecordingForReleaseSerializer().update(r) + return instance