Fix #865: Performance optimization on /artists, /albums and /tracks endpoints
This commit is contained in:
parent
8ed6f830b5
commit
81349e2b57
|
@ -104,58 +104,50 @@ class ArtistWithAlbumsSerializer(serializers.ModelSerializer):
|
||||||
get_attributed_to = serialize_attributed_to
|
get_attributed_to = serialize_attributed_to
|
||||||
|
|
||||||
def get_tracks_count(self, o):
|
def get_tracks_count(self, o):
|
||||||
return getattr(o, "_tracks_count", None)
|
tracks = getattr(o, "_prefetched_tracks", None)
|
||||||
|
return len(tracks) if tracks else None
|
||||||
|
|
||||||
|
|
||||||
class ArtistSimpleSerializer(serializers.ModelSerializer):
|
def serialize_artist_simple(artist):
|
||||||
class Meta:
|
return {
|
||||||
model = models.Artist
|
"id": artist.id,
|
||||||
fields = ("id", "fid", "mbid", "name", "creation_date", "is_local")
|
"fid": artist.fid,
|
||||||
|
"mbid": str(artist.mbid),
|
||||||
|
"name": artist.name,
|
||||||
|
"creation_date": serializers.DateTimeField().to_representation(
|
||||||
|
artist.creation_date
|
||||||
|
),
|
||||||
|
"is_local": artist.is_local,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class AlbumTrackSerializer(serializers.ModelSerializer):
|
def serialize_album_track(track):
|
||||||
artist = ArtistSimpleSerializer(read_only=True)
|
return {
|
||||||
uploads = serializers.SerializerMethodField()
|
"id": track.id,
|
||||||
listen_url = serializers.SerializerMethodField()
|
"fid": track.fid,
|
||||||
duration = serializers.SerializerMethodField()
|
"mbid": str(track.mbid),
|
||||||
|
"title": track.title,
|
||||||
class Meta:
|
"artist": serialize_artist_simple(track.artist),
|
||||||
model = models.Track
|
"album": track.album_id,
|
||||||
fields = (
|
"creation_date": serializers.DateTimeField().to_representation(
|
||||||
"id",
|
track.creation_date
|
||||||
"fid",
|
),
|
||||||
"mbid",
|
"position": track.position,
|
||||||
"title",
|
"disc_number": track.disc_number,
|
||||||
"album",
|
"uploads": [
|
||||||
"artist",
|
serialize_upload(u) for u in getattr(track, "playable_uploads", [])
|
||||||
"creation_date",
|
],
|
||||||
"position",
|
"listen_url": track.listen_url,
|
||||||
"disc_number",
|
"duration": getattr(track, "duration", None),
|
||||||
"uploads",
|
"copyright": track.copyright,
|
||||||
"listen_url",
|
"license": track.license_id,
|
||||||
"duration",
|
"is_local": track.is_local,
|
||||||
"copyright",
|
}
|
||||||
"license",
|
|
||||||
"is_local",
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_uploads(self, obj):
|
|
||||||
uploads = getattr(obj, "playable_uploads", [])
|
|
||||||
return TrackUploadSerializer(uploads, many=True).data
|
|
||||||
|
|
||||||
def get_listen_url(self, obj):
|
|
||||||
return obj.listen_url
|
|
||||||
|
|
||||||
def get_duration(self, obj):
|
|
||||||
try:
|
|
||||||
return obj.duration
|
|
||||||
except AttributeError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class AlbumSerializer(serializers.ModelSerializer):
|
class AlbumSerializer(serializers.ModelSerializer):
|
||||||
tracks = serializers.SerializerMethodField()
|
tracks = serializers.SerializerMethodField()
|
||||||
artist = ArtistSimpleSerializer(read_only=True)
|
artist = serializers.SerializerMethodField()
|
||||||
cover = cover_field
|
cover = cover_field
|
||||||
is_playable = serializers.SerializerMethodField()
|
is_playable = serializers.SerializerMethodField()
|
||||||
tags = serializers.SerializerMethodField()
|
tags = serializers.SerializerMethodField()
|
||||||
|
@ -181,9 +173,12 @@ class AlbumSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
get_attributed_to = serialize_attributed_to
|
get_attributed_to = serialize_attributed_to
|
||||||
|
|
||||||
|
def get_artist(self, o):
|
||||||
|
return serialize_artist_simple(o.artist)
|
||||||
|
|
||||||
def get_tracks(self, o):
|
def get_tracks(self, o):
|
||||||
ordered_tracks = o.tracks.all()
|
ordered_tracks = o.tracks.all()
|
||||||
return AlbumTrackSerializer(ordered_tracks, many=True).data
|
return [serialize_album_track(track) for track in ordered_tracks]
|
||||||
|
|
||||||
def get_is_playable(self, obj):
|
def get_is_playable(self, obj):
|
||||||
try:
|
try:
|
||||||
|
@ -199,7 +194,7 @@ class AlbumSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
|
|
||||||
class TrackAlbumSerializer(serializers.ModelSerializer):
|
class TrackAlbumSerializer(serializers.ModelSerializer):
|
||||||
artist = ArtistSimpleSerializer(read_only=True)
|
artist = serializers.SerializerMethodField()
|
||||||
cover = cover_field
|
cover = cover_field
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -216,23 +211,24 @@ class TrackAlbumSerializer(serializers.ModelSerializer):
|
||||||
"is_local",
|
"is_local",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_artist(self, o):
|
||||||
|
return serialize_artist_simple(o.artist)
|
||||||
|
|
||||||
class TrackUploadSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
def serialize_upload(upload):
|
||||||
model = models.Upload
|
return {
|
||||||
fields = (
|
"uuid": str(upload.uuid),
|
||||||
"uuid",
|
"listen_url": upload.listen_url,
|
||||||
"listen_url",
|
"size": upload.size,
|
||||||
"size",
|
"duration": upload.duration,
|
||||||
"duration",
|
"bitrate": upload.bitrate,
|
||||||
"bitrate",
|
"mimetype": upload.mimetype,
|
||||||
"mimetype",
|
"extension": upload.extension,
|
||||||
"extension",
|
}
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TrackSerializer(serializers.ModelSerializer):
|
class TrackSerializer(serializers.ModelSerializer):
|
||||||
artist = ArtistSimpleSerializer(read_only=True)
|
artist = serializers.SerializerMethodField()
|
||||||
album = TrackAlbumSerializer(read_only=True)
|
album = TrackAlbumSerializer(read_only=True)
|
||||||
uploads = serializers.SerializerMethodField()
|
uploads = serializers.SerializerMethodField()
|
||||||
listen_url = serializers.SerializerMethodField()
|
listen_url = serializers.SerializerMethodField()
|
||||||
|
@ -262,12 +258,14 @@ class TrackSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
get_attributed_to = serialize_attributed_to
|
get_attributed_to = serialize_attributed_to
|
||||||
|
|
||||||
|
def get_artist(self, o):
|
||||||
|
return serialize_artist_simple(o.artist)
|
||||||
|
|
||||||
def get_listen_url(self, obj):
|
def get_listen_url(self, obj):
|
||||||
return obj.listen_url
|
return obj.listen_url
|
||||||
|
|
||||||
def get_uploads(self, obj):
|
def get_uploads(self, obj):
|
||||||
uploads = getattr(obj, "playable_uploads", [])
|
return [serialize_upload(u) for u in getattr(obj, "playable_uploads", [])]
|
||||||
return TrackUploadSerializer(uploads, many=True).data
|
|
||||||
|
|
||||||
def get_tags(self, obj):
|
def get_tags(self, obj):
|
||||||
tagged_items = getattr(obj, "_prefetched_tagged_items", [])
|
tagged_items = getattr(obj, "_prefetched_tagged_items", [])
|
||||||
|
|
|
@ -96,8 +96,14 @@ def refetch_obj(obj, queryset):
|
||||||
class ArtistViewSet(common_views.SkipFilterForGetObject, viewsets.ReadOnlyModelViewSet):
|
class ArtistViewSet(common_views.SkipFilterForGetObject, viewsets.ReadOnlyModelViewSet):
|
||||||
queryset = (
|
queryset = (
|
||||||
models.Artist.objects.all()
|
models.Artist.objects.all()
|
||||||
.select_related("attributed_to")
|
.prefetch_related("attributed_to")
|
||||||
.annotate(_tracks_count=Count("tracks"))
|
.prefetch_related(
|
||||||
|
Prefetch(
|
||||||
|
"tracks",
|
||||||
|
queryset=models.Track.objects.all(),
|
||||||
|
to_attr="_prefetched_tracks",
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
serializer_class = serializers.ArtistWithAlbumsSerializer
|
serializer_class = serializers.ArtistWithAlbumsSerializer
|
||||||
permission_classes = [oauth_permissions.ScopePermission]
|
permission_classes = [oauth_permissions.ScopePermission]
|
||||||
|
|
|
@ -62,7 +62,7 @@ def test_artist_with_albums_serializer(factories, to_api_date):
|
||||||
artist = track.artist
|
artist = track.artist
|
||||||
artist = artist.__class__.objects.with_albums().get(pk=artist.pk)
|
artist = artist.__class__.objects.with_albums().get(pk=artist.pk)
|
||||||
album = list(artist.albums.all())[0]
|
album = list(artist.albums.all())[0]
|
||||||
setattr(artist, "_tracks_count", 42)
|
setattr(artist, "_prefetched_tracks", range(42))
|
||||||
expected = {
|
expected = {
|
||||||
"id": artist.id,
|
"id": artist.id,
|
||||||
"fid": artist.fid,
|
"fid": artist.fid,
|
||||||
|
@ -89,13 +89,13 @@ def test_album_track_serializer(factories, to_api_date):
|
||||||
expected = {
|
expected = {
|
||||||
"id": track.id,
|
"id": track.id,
|
||||||
"fid": track.fid,
|
"fid": track.fid,
|
||||||
"artist": serializers.ArtistSimpleSerializer(track.artist).data,
|
"artist": serializers.serialize_artist_simple(track.artist),
|
||||||
"album": track.album.id,
|
"album": track.album.id,
|
||||||
"mbid": str(track.mbid),
|
"mbid": str(track.mbid),
|
||||||
"title": track.title,
|
"title": track.title,
|
||||||
"position": track.position,
|
"position": track.position,
|
||||||
"disc_number": track.disc_number,
|
"disc_number": track.disc_number,
|
||||||
"uploads": [serializers.TrackUploadSerializer(upload).data],
|
"uploads": [serializers.serialize_upload(upload)],
|
||||||
"creation_date": to_api_date(track.creation_date),
|
"creation_date": to_api_date(track.creation_date),
|
||||||
"listen_url": track.listen_url,
|
"listen_url": track.listen_url,
|
||||||
"duration": None,
|
"duration": None,
|
||||||
|
@ -103,8 +103,8 @@ def test_album_track_serializer(factories, to_api_date):
|
||||||
"copyright": track.copyright,
|
"copyright": track.copyright,
|
||||||
"is_local": track.is_local,
|
"is_local": track.is_local,
|
||||||
}
|
}
|
||||||
serializer = serializers.AlbumTrackSerializer(track)
|
data = serializers.serialize_album_track(track)
|
||||||
assert serializer.data == expected
|
assert data == expected
|
||||||
|
|
||||||
|
|
||||||
def test_upload_serializer(factories, to_api_date):
|
def test_upload_serializer(factories, to_api_date):
|
||||||
|
@ -169,7 +169,7 @@ def test_album_serializer(factories, to_api_date):
|
||||||
"fid": album.fid,
|
"fid": album.fid,
|
||||||
"mbid": str(album.mbid),
|
"mbid": str(album.mbid),
|
||||||
"title": album.title,
|
"title": album.title,
|
||||||
"artist": serializers.ArtistSimpleSerializer(album.artist).data,
|
"artist": serializers.serialize_artist_simple(album.artist),
|
||||||
"creation_date": to_api_date(album.creation_date),
|
"creation_date": to_api_date(album.creation_date),
|
||||||
"is_playable": False,
|
"is_playable": False,
|
||||||
"cover": {
|
"cover": {
|
||||||
|
@ -179,7 +179,7 @@ def test_album_serializer(factories, to_api_date):
|
||||||
"small_square_crop": album.cover.crop["50x50"].url,
|
"small_square_crop": album.cover.crop["50x50"].url,
|
||||||
},
|
},
|
||||||
"release_date": to_api_date(album.release_date),
|
"release_date": to_api_date(album.release_date),
|
||||||
"tracks": serializers.AlbumTrackSerializer([track2, track1], many=True).data,
|
"tracks": [serializers.serialize_album_track(t) for t in [track2, track1]],
|
||||||
"is_local": album.is_local,
|
"is_local": album.is_local,
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"attributed_to": federation_serializers.APIActorSerializer(actor).data,
|
"attributed_to": federation_serializers.APIActorSerializer(actor).data,
|
||||||
|
@ -202,13 +202,13 @@ def test_track_serializer(factories, to_api_date):
|
||||||
expected = {
|
expected = {
|
||||||
"id": track.id,
|
"id": track.id,
|
||||||
"fid": track.fid,
|
"fid": track.fid,
|
||||||
"artist": serializers.ArtistSimpleSerializer(track.artist).data,
|
"artist": serializers.serialize_artist_simple(track.artist),
|
||||||
"album": serializers.TrackAlbumSerializer(track.album).data,
|
"album": serializers.TrackAlbumSerializer(track.album).data,
|
||||||
"mbid": str(track.mbid),
|
"mbid": str(track.mbid),
|
||||||
"title": track.title,
|
"title": track.title,
|
||||||
"position": track.position,
|
"position": track.position,
|
||||||
"disc_number": track.disc_number,
|
"disc_number": track.disc_number,
|
||||||
"uploads": [serializers.TrackUploadSerializer(upload).data],
|
"uploads": [serializers.serialize_upload(upload)],
|
||||||
"creation_date": to_api_date(track.creation_date),
|
"creation_date": to_api_date(track.creation_date),
|
||||||
"listen_url": track.listen_url,
|
"listen_url": track.listen_url,
|
||||||
"license": upload.track.license.code,
|
"license": upload.track.license.code,
|
||||||
|
@ -317,7 +317,7 @@ def test_manage_upload_action_relaunch_import(factories, mocker):
|
||||||
assert m.call_count == 3
|
assert m.call_count == 3
|
||||||
|
|
||||||
|
|
||||||
def test_track_upload_serializer(factories):
|
def test_serialize_upload(factories):
|
||||||
upload = factories["music.Upload"]()
|
upload = factories["music.Upload"]()
|
||||||
|
|
||||||
expected = {
|
expected = {
|
||||||
|
@ -330,8 +330,8 @@ def test_track_upload_serializer(factories):
|
||||||
"duration": upload.duration,
|
"duration": upload.duration,
|
||||||
}
|
}
|
||||||
|
|
||||||
serializer = serializers.TrackUploadSerializer(upload)
|
data = serializers.serialize_upload(upload)
|
||||||
assert serializer.data == expected
|
assert data == expected
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
|
|
@ -6,7 +6,7 @@ import urllib.parse
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from django.db.models import Count
|
from django.db.models import Prefetch
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
|
@ -28,7 +28,9 @@ def test_artist_list_serializer(api_request, factories, logged_in_api_client):
|
||||||
).track
|
).track
|
||||||
artist = track.artist
|
artist = track.artist
|
||||||
request = api_request.get("/")
|
request = api_request.get("/")
|
||||||
qs = artist.__class__.objects.with_albums().annotate(_tracks_count=Count("tracks"))
|
qs = artist.__class__.objects.with_albums().prefetch_related(
|
||||||
|
Prefetch("tracks", to_attr="_prefetched_tracks")
|
||||||
|
)
|
||||||
serializer = serializers.ArtistWithAlbumsSerializer(
|
serializer = serializers.ArtistWithAlbumsSerializer(
|
||||||
qs, many=True, context={"request": request}
|
qs, many=True, context={"request": request}
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Improved performance of /artists, /albums and /tracks API endpoints by a factor 2 (#865)
|
Loading…
Reference in New Issue