...
 
Commits (1)
......@@ -108,7 +108,10 @@ def get_library_page(library, page_url, actor):
)
serializer = serializers.CollectionPageSerializer(
data=response.json(),
context={"library": library, "item_serializer": serializers.AudioSerializer},
context={
"library": library,
"item_serializer": serializers.TrackFileSerializer,
},
)
serializer.is_valid(raise_exception=True)
return serializer.validated_data
......@@ -4,6 +4,7 @@ import urllib.parse
from django.core.exceptions import ObjectDoesNotExist
from django.core.paginator import Paginator
from django.db.models import F, Q
from rest_framework import serializers
from funkwhale_api.common import utils as funkwhale_utils
......@@ -639,43 +640,141 @@ class CollectionPageSerializer(serializers.Serializer):
return d
class ArtistMetadataSerializer(serializers.Serializer):
musicbrainz_id = serializers.UUIDField(required=False, allow_null=True)
name = serializers.CharField()
class MusicEntitySerializer(serializers.Serializer):
id = serializers.URLField(max_length=500)
published = serializers.DateTimeField()
musicbrainzId = serializers.UUIDField(allow_null=True, required=False)
name = serializers.CharField(max_length=1000)
def create(self, validated_data):
mbid = validated_data.get('musicbrainzId')
candidates = self.model.objects.filter(
Q(mbid=mbid) | Q(fid=validated_data['id'])
).order_by(F('fid').desc(nulls_last=True))
existing = candidates.first()
if existing:
return existing
# nothing matching in our database, let's create a new object
return self.model.objects.create(
**self.get_create_data(validated_data)
)
def get_create_data(self, validated_data):
return {
'mbid': validated_data.get('musicbrainzId'),
'fid': validated_data['id'],
'name': validated_data['name'],
'creation_date': validated_data['published']
}
class ArtistSerializer(MusicEntitySerializer):
model = music_models.Artist
def to_representation(self, instance):
d = {
"type": "Artist",
"id": instance.fid,
"name": instance.name,
"published": instance.creation_date.isoformat(),
"musicbrainzId": str(instance.mbid) if instance.mbid else None,
}
if self.context.get("include_ap_context", self.parent is None):
d["@context"] = AP_CONTEXT
return d
class AlbumSerializer(MusicEntitySerializer):
model = music_models.Album
released = serializers.DateField(allow_null=True, required=False)
artists = serializers.ListField(child=ArtistSerializer(), min_length=1)
def to_representation(self, instance):
d = {
"type": "Album",
"id": instance.fid,
"name": instance.title,
"published": instance.creation_date.isoformat(),
"musicbrainzId": str(instance.mbid) if instance.mbid else None,
"released": instance.release_date.isoformat() if instance.release_date else None,
"artists": [
ArtistSerializer(instance.artist, context={'include_ap_context': False}).data
]
}
if self.context.get("include_ap_context", self.parent is None):
d["@context"] = AP_CONTEXT
return d
def get_create_data(self, validated_data):
artist_data = validated_data['artists'][0]
artist = ArtistSerializer().create(artist_data)
return {
'mbid': validated_data.get('musicbrainzId'),
'fid': validated_data['id'],
'title': validated_data['name'],
'creation_date': validated_data['published'],
'artist': artist,
'release_date': validated_data.get('released'),
}
class ReleaseMetadataSerializer(serializers.Serializer):
musicbrainz_id = serializers.UUIDField(required=False, allow_null=True)
title = serializers.CharField()
class TrackSerializer(MusicEntitySerializer):
model = music_models.Track
position = serializers.IntegerField(min_value=0, allow_null=True, required=False)
artists = serializers.ListField(child=ArtistSerializer(), min_length=1)
album = AlbumSerializer()
def to_representation(self, instance):
d = {
"type": "Track",
"id": instance.fid,
"name": instance.title,
"published": instance.creation_date.isoformat(),
"musicbrainzId": str(instance.mbid) if instance.mbid else None,
"position": instance.position,
"artists": [
ArtistSerializer(instance.artist, context={'include_ap_context': False}).data
],
"album": AlbumSerializer(instance.album, context={'include_ap_context': False}).data
}
class RecordingMetadataSerializer(serializers.Serializer):
musicbrainz_id = serializers.UUIDField(required=False, allow_null=True)
title = serializers.CharField()
if self.context.get("include_ap_context", self.parent is None):
d["@context"] = AP_CONTEXT
return d
def get_create_data(self, validated_data):
artist_data = validated_data['artists'][0]
artist = ArtistSerializer().create(artist_data)
album = AlbumSerializer().create(validated_data['album'])
class AudioMetadataSerializer(serializers.Serializer):
artist = ArtistMetadataSerializer()
release = ReleaseMetadataSerializer()
recording = RecordingMetadataSerializer()
bitrate = serializers.IntegerField(required=False, allow_null=True, min_value=0)
size = serializers.IntegerField(required=False, allow_null=True, min_value=0)
length = serializers.IntegerField(required=False, allow_null=True, min_value=0)
return {
'mbid': validated_data.get('musicbrainzId'),
'fid': validated_data['id'],
'title': validated_data['name'],
'position': validated_data.get('position'),
'creation_date': validated_data['published'],
'artist': artist,
'album': album,
}
class AudioSerializer(serializers.Serializer):
type = serializers.CharField()
class TrackFileSerializer(serializers.Serializer):
type = serializers.ChoiceField(choices=["Audio"])
id = serializers.URLField(max_length=500)
library = serializers.URLField(max_length=500)
url = serializers.JSONField()
published = serializers.DateTimeField()
updated = serializers.DateTimeField(required=False)
metadata = AudioMetadataSerializer()
updated = serializers.DateTimeField(required=False, allow_null=True)
bitrate = serializers.IntegerField(min_value=0)
size = serializers.IntegerField(min_value=0)
duration = serializers.IntegerField(min_value=0)
def validate_type(self, v):
if v != "Audio":
raise serializers.ValidationError("Invalid type for audio")
return v
track = TrackSerializer(required=True)
def validate_url(self, v):
try:
......@@ -705,55 +804,48 @@ class AudioSerializer(serializers.Serializer):
raise serializers.ValidationError("Invalid library")
def create(self, validated_data):
defaults = {
try:
return music_models.TrackFile.objects.get(fid=validated_data['id'])
except music_models.TrackFile.DoesNotExist:
pass
track = TrackSerializer().create(validated_data['track'])
data = {
"mimetype": validated_data["url"]["mediaType"],
"source": validated_data["url"]["href"],
"creation_date": validated_data["published"],
"modification_date": validated_data.get("updated"),
"metadata": self.initial_data,
"track": track,
"duration": validated_data['duration'],
"size": validated_data['size'],
"bitrate": validated_data['bitrate'],
"library": validated_data['library'],
}
tf, created = validated_data["library"].files.update_or_create(
fid=validated_data["id"], defaults=defaults
)
return tf
return music_models.TrackFile.objects.create(**data)
def to_representation(self, instance):
track = instance.track
album = instance.track.album
artist = instance.track.artist
d = {
"type": "Audio",
"id": instance.get_federation_id(),
"library": instance.library.get_federation_id(),
"name": instance.track.full_name,
"id": instance.fid,
"library": instance.library.fid,
"name": track.full_name,
"published": instance.creation_date.isoformat(),
"metadata": {
"artist": {
"musicbrainz_id": str(artist.mbid) if artist.mbid else None,
"name": artist.name,
},
"release": {
"musicbrainz_id": str(album.mbid) if album.mbid else None,
"title": album.title,
},
"recording": {
"musicbrainz_id": str(track.mbid) if track.mbid else None,
"title": track.title,
},
"bitrate": instance.bitrate,
"size": instance.size,
"length": instance.duration,
},
"bitrate": instance.bitrate,
"size": instance.size,
"duration": instance.duration,
"url": {
"href": utils.full_url(instance.listen_url),
"type": "Link",
"mediaType": instance.mimetype,
},
"track": TrackSerializer(track, context={'include_ap_context': False}).data
}
if instance.modification_date:
d["updated"] = instance.modification_date.isoformat()
if self.context.get("include_ap_context", True):
if self.context.get("include_ap_context", self.parent is None):
d["@context"] = AP_CONTEXT
return d
......
......@@ -176,7 +176,7 @@ class MusicLibraryViewSet(
"name": lb.name,
"summary": lb.description,
"items": lb.files.order_by("-creation_date"),
"item_serializer": serializers.AudioSerializer,
"item_serializer": serializers.TrackFileSerializer,
}
page = request.GET.get("page")
if page is None:
......
......@@ -17,6 +17,7 @@ SAMPLES_PATH = os.path.join(
class ArtistFactory(factory.django.DjangoModelFactory):
name = factory.Faker("name")
mbid = factory.Faker("uuid4")
fid = factory.Faker("url")
class Meta:
model = "music.Artist"
......@@ -30,6 +31,7 @@ class AlbumFactory(factory.django.DjangoModelFactory):
cover = factory.django.ImageField()
artist = factory.SubFactory(ArtistFactory)
release_group_id = factory.Faker("uuid4")
fid = factory.Faker("url")
class Meta:
model = "music.Album"
......@@ -37,6 +39,7 @@ class AlbumFactory(factory.django.DjangoModelFactory):
@registry.register
class TrackFactory(factory.django.DjangoModelFactory):
fid = factory.Faker("url")
title = factory.Faker("sentence", nb_words=3)
mbid = factory.Faker("uuid4")
album = factory.SubFactory(AlbumFactory)
......
......@@ -15,7 +15,7 @@ class Importer(object):
# let's validate data, just in case
instance = self.model(**cleaned_data)
exclude = EXCLUDE_VALIDATION.get(self.model.__name__, [])
instance.full_clean(exclude=["mbid", "uuid"] + exclude)
instance.full_clean(exclude=["mbid", "uuid", "fid"] + exclude)
m = self.model.objects.update_or_create(mbid=mbid, defaults=cleaned_data)[0]
for hook in import_hooks:
hook(m, cleaned_data, raw_data)
......
# Generated by Django 2.0.8 on 2018-09-13 16:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('music', '0030_auto_20180825_1411'),
]
operations = [
migrations.AddField(
model_name='album',
name='fid',
field=models.URLField(db_index=True, max_length=500, null=True, unique=True),
),
migrations.AddField(
model_name='artist',
name='fid',
field=models.URLField(db_index=True, max_length=500, null=True, unique=True),
),
migrations.AddField(
model_name='track',
name='fid',
field=models.URLField(db_index=True, max_length=500, null=True, unique=True),
),
migrations.AddField(
model_name='work',
name='fid',
field=models.URLField(db_index=True, max_length=500, null=True, unique=True),
),
]
......@@ -32,6 +32,7 @@ def empty_dict():
class APIModelMixin(models.Model):
fid = models.URLField(unique=True, max_length=500, db_index=True, null=True)
mbid = models.UUIDField(unique=True, db_index=True, null=True, blank=True)
uuid = models.UUIDField(unique=True, db_index=True, default=uuid.uuid4)
api_includes = []
......@@ -86,6 +87,18 @@ class APIModelMixin(models.Model):
self.musicbrainz_model, self.mbid
)
def get_federation_id(self):
if self.fid:
return self.fid
return federation_utils.full_url("/federation/music/{}/{}".format(self.federation_namespace, self.uuid))
def save(self, **kwargs):
if not self.pk and not self.fid:
self.fid = self.get_federation_id()
return super().save(**kwargs)
class ArtistQuerySet(models.QuerySet):
def with_albums_count(self):
......@@ -116,7 +129,7 @@ class ArtistQuerySet(models.QuerySet):
class Artist(APIModelMixin):
name = models.CharField(max_length=255)
federation_namespace = 'artist'
musicbrainz_model = "artist"
musicbrainz_mapping = {
"mbid": {"musicbrainz_field_name": "id"},
......@@ -195,6 +208,7 @@ class Album(APIModelMixin):
api_includes = ["artist-credits", "recordings", "media", "release-groups"]
api = musicbrainz.api.releases
federation_namespace = 'album'
musicbrainz_model = "release"
musicbrainz_mapping = {
"mbid": {"musicbrainz_field_name": "id"},
......@@ -290,6 +304,8 @@ class Work(APIModelMixin):
api = musicbrainz.api.works
api_includes = ["url-rels", "recording-rels"]
musicbrainz_model = "work"
federation_namespace = 'work'
musicbrainz_mapping = {
"mbid": {"musicbrainz_field_name": "id"},
"title": {"musicbrainz_field_name": "title"},
......@@ -364,7 +380,7 @@ class Track(APIModelMixin):
work = models.ForeignKey(
Work, related_name="tracks", null=True, blank=True, on_delete=models.CASCADE
)
federation_namespace = 'track'
musicbrainz_model = "recording"
api = musicbrainz.api.recordings
api_includes = ["artist-credits", "releases", "media", "tags", "work-rels"]
......@@ -537,7 +553,7 @@ class TrackFile(models.Model):
max_length=500,
)
creation_date = models.DateTimeField(default=timezone.now)
modification_date = models.DateTimeField(auto_now=True)
modification_date = models.DateTimeField(default=timezone.now, null=True)
accessed_date = models.DateTimeField(null=True, blank=True)
duration = models.IntegerField(null=True, blank=True)
size = models.IntegerField(null=True, blank=True)
......
This diff is collapsed.
......@@ -143,7 +143,7 @@ def test_music_library_retrieve_page_public(factories, api_client):
expected = serializers.CollectionPageSerializer(
{
"id": id,
"item_serializer": serializers.AudioSerializer,
"item_serializer": serializers.TrackFileSerializer,
"actor": library.actor,
"page": Paginator([tf], 1).page(1),
"name": library.name,
......
......@@ -2,6 +2,7 @@ import datetime
import pytest
from funkwhale_api.federation import utils as federation_utils
from funkwhale_api.music import models
......@@ -17,6 +18,9 @@ def test_can_create_artist_from_api(artists, mocker, db):
assert data["id"], "62c3befb-6366-4585-b256-809472333801"
assert artist.mbid, data["id"]
assert artist.name, "Adhesive Wombat"
assert artist.fid == federation_utils.full_url(
"/federation/music/artist/{}".format(artist.uuid)
)
def test_can_create_album_from_api(artists, albums, mocker, db):
......@@ -41,6 +45,9 @@ def test_can_create_album_from_api(artists, albums, mocker, db):
assert album.release_date, datetime.date(2005, 1, 1)
assert album.artist.name, "System of a Down"
assert album.artist.mbid, data["artist-credit"][0]["artist"]["id"]
assert album.fid == federation_utils.full_url(
"/federation/music/album/{}".format(album.uuid)
)
def test_can_create_track_from_api(artists, albums, tracks, mocker, db):
......@@ -66,6 +73,9 @@ def test_can_create_track_from_api(artists, albums, tracks, mocker, db):
assert track.artist.name == "Adhesive Wombat"
assert str(track.album.mbid) == "a50d2a81-2a50-484d-9cb4-b9f6833f583e"
assert track.album.title == "Marsupial Madness"
assert track.fid == federation_utils.full_url(
"/federation/music/track/{}".format(track.uuid)
)
def test_can_create_track_from_api_with_corresponding_tags(
......
......@@ -285,7 +285,7 @@ def test_scan_page_fetches_page_and_creates_tracks(now, mocker, factories, r_moc
"actor": scan.library.actor,
"id": scan.library.fid,
"page": Paginator(tfs, 3).page(1),
"item_serializer": federation_serializers.AudioSerializer,
"item_serializer": federation_serializers.TrackFileSerializer,
}
page = federation_serializers.CollectionPageSerializer(page_conf)
r_mock.get(page.data["id"], json=page.data)
......@@ -317,7 +317,7 @@ def test_scan_page_trigger_next_page_scan_skip_if_same(mocker, factories, r_mock
"actor": scan.library.actor,
"id": scan.library.fid,
"page": Paginator(tfs, 3).page(1),
"item_serializer": federation_serializers.AudioSerializer,
"item_serializer": federation_serializers.TrackFileSerializer,
}
page = federation_serializers.CollectionPageSerializer(page_conf)
data = page.data
......