Skip to content
Snippets Groups Projects
Verified Commit d3f8fb6c authored by Eliot Berriot's avatar Eliot Berriot
Browse files

See #272: updated API to return upload data on tracks

parent 8489c79c
No related branches found
No related tags found
No related merge requests found
Showing
with 97 additions and 142 deletions
......@@ -51,7 +51,7 @@ class TrackFavoriteViewSet(
queryset = queryset.filter(
fields.privacy_level_query(self.request.user, "user__privacy_level")
)
tracks = Track.objects.annotate_playable_by_actor(
tracks = Track.objects.with_playable_uploads(
music_utils.get_actor_from_request(self.request)
).select_related("artist", "album__artist")
queryset = queryset.prefetch_related(Prefetch("track", queryset=tracks))
......
......@@ -41,7 +41,7 @@ class ListeningViewSet(
queryset = queryset.filter(
fields.privacy_level_query(self.request.user, "user__privacy_level")
)
tracks = Track.objects.annotate_playable_by_actor(
tracks = Track.objects.with_playable_uploads(
music_utils.get_actor_from_request(self.request)
).select_related("artist", "album__artist")
return queryset.prefetch_related(Prefetch("track", queryset=tracks))
......
......@@ -124,8 +124,8 @@ class ArtistQuerySet(models.QuerySet):
def annotate_playable_by_actor(self, actor):
tracks = (
Track.objects.playable_by(actor)
.filter(artist=models.OuterRef("id"))
Upload.objects.playable_by(actor)
.filter(track__artist=models.OuterRef("id"))
.order_by("id")
.values("id")[:1]
)
......@@ -192,8 +192,8 @@ class AlbumQuerySet(models.QuerySet):
def annotate_playable_by_actor(self, actor):
tracks = (
Track.objects.playable_by(actor)
.filter(album=models.OuterRef("id"))
Upload.objects.playable_by(actor)
.filter(track__album=models.OuterRef("id"))
.order_by("id")
.values("id")[:1]
)
......@@ -207,6 +207,15 @@ class AlbumQuerySet(models.QuerySet):
else:
return self.exclude(tracks__in=tracks).distinct()
def with_prefetched_tracks_and_playable_uploads(self, actor):
tracks = Track.objects.with_playable_uploads(actor)
return self.prefetch_related(
models.Prefetch(
'tracks',
queryset=tracks,
)
)
class Album(APIModelMixin):
title = models.CharField(max_length=255)
......@@ -403,18 +412,14 @@ class TrackQuerySet(models.QuerySet):
else:
return self.exclude(uploads__in=files).distinct()
def annotate_duration(self):
first_upload = Upload.objects.filter(track=models.OuterRef("pk")).order_by("pk")
return self.annotate(
duration=models.Subquery(first_upload.values("duration")[:1])
def with_playable_uploads(self, actor):
uploads = Upload.objects.playable_by(actor).select_related('track')
return self.prefetch_related(
models.Prefetch(
'uploads',
queryset=uploads,
to_attr='playable_uploads'
)
def annotate_file_data(self):
first_upload = Upload.objects.filter(track=models.OuterRef("pk")).order_by("pk")
return self.annotate(
bitrate=models.Subquery(first_upload.values("bitrate")[:1]),
size=models.Subquery(first_upload.values("size")[:1]),
mimetype=models.Subquery(first_upload.values("mimetype")[:1]),
)
......
......@@ -59,7 +59,7 @@ class ArtistSimpleSerializer(serializers.ModelSerializer):
class AlbumTrackSerializer(serializers.ModelSerializer):
artist = ArtistSimpleSerializer(read_only=True)
is_playable = serializers.SerializerMethodField()
uploads = serializers.SerializerMethodField()
listen_url = serializers.SerializerMethodField()
duration = serializers.SerializerMethodField()
......@@ -73,16 +73,14 @@ class AlbumTrackSerializer(serializers.ModelSerializer):
"artist",
"creation_date",
"position",
"is_playable",
"uploads",
"listen_url",
"duration",
)
def get_is_playable(self, obj):
try:
return bool(obj.is_playable_by_actor)
except AttributeError:
return None
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
......@@ -123,7 +121,9 @@ class AlbumSerializer(serializers.ModelSerializer):
def get_is_playable(self, obj):
try:
return any([bool(t.is_playable_by_actor) for t in obj.tracks.all()])
return any(
[bool(getattr(t, "playable_uploads", [])) for t in obj.tracks.all()]
)
except AttributeError:
return None
......@@ -145,16 +145,26 @@ class TrackAlbumSerializer(serializers.ModelSerializer):
)
class TrackUploadSerializer(serializers.ModelSerializer):
class Meta:
model = models.Upload
fields = (
"uuid",
"listen_url",
"size",
"duration",
"bitrate",
"mimetype",
"extension",
)
class TrackSerializer(serializers.ModelSerializer):
artist = ArtistSimpleSerializer(read_only=True)
album = TrackAlbumSerializer(read_only=True)
lyrics = serializers.SerializerMethodField()
is_playable = serializers.SerializerMethodField()
uploads = serializers.SerializerMethodField()
listen_url = serializers.SerializerMethodField()
duration = serializers.SerializerMethodField()
bitrate = serializers.SerializerMethodField()
size = serializers.SerializerMethodField()
mimetype = serializers.SerializerMethodField()
class Meta:
model = models.Track
......@@ -167,12 +177,8 @@ class TrackSerializer(serializers.ModelSerializer):
"creation_date",
"position",
"lyrics",
"is_playable",
"uploads",
"listen_url",
"duration",
"bitrate",
"size",
"mimetype",
)
def get_lyrics(self, obj):
......@@ -181,35 +187,9 @@ class TrackSerializer(serializers.ModelSerializer):
def get_listen_url(self, obj):
return obj.listen_url
def get_is_playable(self, obj):
try:
return bool(obj.is_playable_by_actor)
except AttributeError:
return None
def get_duration(self, obj):
try:
return obj.duration
except AttributeError:
return None
def get_bitrate(self, obj):
try:
return obj.bitrate
except AttributeError:
return None
def get_size(self, obj):
try:
return obj.size
except AttributeError:
return None
def get_mimetype(self, obj):
try:
return obj.mimetype
except AttributeError:
return None
def get_uploads(self, obj):
uploads = getattr(obj, "playable_uploads", [])
return TrackUploadSerializer(uploads, many=True).data
class LibraryForOwnerSerializer(serializers.ModelSerializer):
......
......@@ -5,7 +5,6 @@ import mutagen
import pydub
from funkwhale_api.common.search import normalize_query, get_query # noqa
from funkwhale_api.common import utils
def guess_mimetype(f):
......
......@@ -93,17 +93,9 @@ class AlbumViewSet(viewsets.ReadOnlyModelViewSet):
def get_queryset(self):
queryset = super().get_queryset()
tracks = models.Track.objects.annotate_playable_by_actor(
tracks = models.Track.objects.select_related("artist").with_playable_uploads(
utils.get_actor_from_request(self.request)
).select_related("artist")
if (
hasattr(self, "kwargs")
and self.kwargs
and self.request.method.lower() == "get"
):
# we are detailing a single album, so we can add the overhead
# to fetch additional data
tracks = tracks.annotate_duration()
)
qs = queryset.prefetch_related(Prefetch("tracks", queryset=tracks))
return qs.distinct()
......@@ -194,18 +186,10 @@ class TrackViewSet(TagViewSetMixin, viewsets.ReadOnlyModelViewSet):
if user.is_authenticated and filter_favorites == "true":
queryset = queryset.filter(track_favorites__user=user)
queryset = queryset.annotate_playable_by_actor(
queryset = queryset.with_playable_uploads(
utils.get_actor_from_request(self.request)
).annotate_duration()
if (
hasattr(self, "kwargs")
and self.kwargs
and self.request.method.lower() == "get"
):
# we are detailing a single track, so we can add the overhead
# to fetch additional data
queryset = queryset.annotate_file_data()
return queryset.distinct()
)
return queryset
@detail_route(methods=["get"])
@transaction.non_atomic_requests
......
......@@ -38,15 +38,14 @@ class PlaylistQuerySet(models.QuerySet):
)
return self.prefetch_related(plt_prefetch)
def annotate_playable_by_actor(self, actor):
plts = (
PlaylistTrack.objects.playable_by(actor)
.filter(playlist=models.OuterRef("id"))
.order_by("id")
.values("id")[:1]
def with_playable_plts(self, actor):
return self.prefetch_related(
models.Prefetch(
"playlist_tracks",
queryset=PlaylistTrack.objects.playable_by(actor),
to_attr="playable_plts",
)
)
subquery = models.Subquery(plts)
return self.annotate(is_playable_by_actor=subquery)
def playable_by(self, actor, include=True):
plts = PlaylistTrack.objects.playable_by(actor, include)
......@@ -148,7 +147,7 @@ class Playlist(models.Model):
class PlaylistTrackQuerySet(models.QuerySet):
def for_nested_serialization(self, actor=None):
tracks = music_models.Track.objects.annotate_playable_by_actor(actor)
tracks = music_models.Track.objects.with_playable_uploads(actor)
tracks = tracks.select_related("artist", "album__artist")
return self.prefetch_related(
models.Prefetch("track", queryset=tracks, to_attr="_prefetched_track")
......@@ -156,8 +155,8 @@ class PlaylistTrackQuerySet(models.QuerySet):
def annotate_playable_by_actor(self, actor):
tracks = (
music_models.Track.objects.playable_by(actor)
.filter(pk=models.OuterRef("track"))
music_models.Upload.objects.playable_by(actor)
.filter(track__pk=models.OuterRef("track"))
.order_by("id")
.values("id")[:1]
)
......
......@@ -93,7 +93,7 @@ class PlaylistSerializer(serializers.ModelSerializer):
def get_is_playable(self, obj):
try:
return bool(obj.is_playable_by_actor)
return bool(obj.playable_plts)
except AttributeError:
return None
......
......@@ -78,7 +78,7 @@ class PlaylistViewSet(
def get_queryset(self):
return self.queryset.filter(
fields.privacy_level_query(self.request.user)
).annotate_playable_by_actor(music_utils.get_actor_from_request(self.request))
).with_playable_plts(music_utils.get_actor_from_request(self.request))
def perform_create(self, serializer):
return serializer.save(
......
......@@ -35,7 +35,6 @@ def test_user_can_get_his_favorites(api_request, factories, logged_in_client, cl
"creation_date": favorite.creation_date.isoformat().replace("+00:00", "Z"),
}
]
expected[0]["track"]["is_playable"] = False
assert response.status_code == 200
assert response.data["results"] == expected
......
......@@ -464,24 +464,6 @@ def test_library_queryset_with_follows(factories):
assert l2._follows == [follow]
def test_annotate_duration(factories):
tf = factories["music.Upload"](duration=32)
track = models.Track.objects.annotate_duration().get(pk=tf.track.pk)
assert track.duration == 32
def test_annotate_file_data(factories):
tf = factories["music.Upload"](size=42, bitrate=55, mimetype="audio/ogg")
track = models.Track.objects.annotate_file_data().get(pk=tf.track.pk)
assert track.size == 42
assert track.bitrate == 55
assert track.mimetype == "audio/ogg"
@pytest.mark.parametrize(
"model,factory_args,namespace",
[
......
......@@ -48,6 +48,7 @@ def test_artist_with_albums_serializer(factories, to_api_date):
def test_album_track_serializer(factories, to_api_date):
upload = factories["music.Upload"]()
track = upload.track
setattr(track, "playable_uploads", [upload])
expected = {
"id": track.id,
......@@ -56,7 +57,7 @@ def test_album_track_serializer(factories, to_api_date):
"mbid": str(track.mbid),
"title": track.title,
"position": track.position,
"is_playable": None,
"uploads": [serializers.TrackUploadSerializer(upload).data],
"creation_date": to_api_date(track.creation_date),
"listen_url": track.listen_url,
"duration": None,
......@@ -127,7 +128,7 @@ def test_album_serializer(factories, to_api_date):
"title": album.title,
"artist": serializers.ArtistSimpleSerializer(album.artist).data,
"creation_date": to_api_date(album.creation_date),
"is_playable": None,
"is_playable": False,
"cover": {
"original": album.cover.url,
"square_crop": album.cover.crop["400x400"].url,
......@@ -145,7 +146,7 @@ def test_album_serializer(factories, to_api_date):
def test_track_serializer(factories, to_api_date):
upload = factories["music.Upload"]()
track = upload.track
setattr(track, "playable_uploads", [upload])
expected = {
"id": track.id,
"artist": serializers.ArtistSimpleSerializer(track.artist).data,
......@@ -153,14 +154,10 @@ def test_track_serializer(factories, to_api_date):
"mbid": str(track.mbid),
"title": track.title,
"position": track.position,
"is_playable": None,
"uploads": [serializers.TrackUploadSerializer(upload).data],
"creation_date": to_api_date(track.creation_date),
"lyrics": track.get_lyrics_url(),
"listen_url": track.listen_url,
"duration": None,
"size": None,
"bitrate": None,
"mimetype": None,
}
serializer = serializers.TrackSerializer(track)
assert serializer.data == expected
......@@ -260,3 +257,20 @@ def test_manage_upload_action_relaunch_import(factories, mocker):
finished.refresh_from_db()
assert finished.import_status == "finished"
assert m.call_count == 3
def test_track_upload_serializer(factories):
upload = factories["music.Upload"]()
expected = {
"listen_url": upload.listen_url,
"uuid": str(upload.uuid),
"size": upload.size,
"bitrate": upload.bitrate,
"mimetype": upload.mimetype,
"extension": upload.extension,
"duration": upload.duration,
}
serializer = serializers.TrackUploadSerializer(upload)
assert serializer.data == expected
......@@ -549,11 +549,11 @@ def test_scan_page_trigger_next_page_scan_skip_if_same(mocker, factories, r_mock
def test_clean_transcoding_cache(preferences, now, factories):
preferences['music__transcoding_cache_duration'] = 60
u1 = factories['music.UploadVersion'](
preferences["music__transcoding_cache_duration"] = 60
u1 = factories["music.UploadVersion"](
accessed_date=now - datetime.timedelta(minutes=61)
)
u2 = factories['music.UploadVersion'](
u2 = factories["music.UploadVersion"](
accessed_date=now - datetime.timedelta(minutes=59)
)
......
......@@ -39,13 +39,11 @@ def test_album_list_serializer(api_request, factories, logged_in_api_client):
).track
album = track.album
request = api_request.get("/")
qs = album.__class__.objects.all()
qs = album.__class__.objects.with_prefetched_tracks_and_playable_uploads(None)
serializer = serializers.AlbumSerializer(
qs, many=True, context={"request": request}
)
expected = {"count": 1, "next": None, "previous": None, "results": serializer.data}
expected["results"][0]["is_playable"] = True
expected["results"][0]["tracks"][0]["is_playable"] = True
url = reverse("api:v1:albums-list")
response = logged_in_api_client.get(url)
......@@ -58,12 +56,11 @@ def test_track_list_serializer(api_request, factories, logged_in_api_client):
library__privacy_level="everyone", import_status="finished"
).track
request = api_request.get("/")
qs = track.__class__.objects.all()
qs = track.__class__.objects.with_playable_uploads(None)
serializer = serializers.TrackSerializer(
qs, many=True, context={"request": request}
)
expected = {"count": 1, "next": None, "previous": None, "results": serializer.data}
expected["results"][0]["is_playable"] = True
url = reverse("api:v1:tracks-list")
response = logged_in_api_client.get(url)
......
......@@ -150,10 +150,6 @@ def test_playlist_playable_by_anonymous(privacy_level, expected, factories):
factories["music.Upload"](
track=track, library__privacy_level=privacy_level, import_status="finished"
)
queryset = playlist.__class__.objects.playable_by(None).annotate_playable_by_actor(
None
)
queryset = playlist.__class__.objects.playable_by(None).with_playable_plts(None)
match = playlist in list(queryset)
assert match is expected
if expected:
assert bool(queryset.first().is_playable_by_actor) is expected
......@@ -145,7 +145,7 @@ def test_can_list_tracks_from_playlist(level, factories, logged_in_api_client):
url = reverse("api:v1:playlists-tracks", kwargs={"pk": plt.playlist.pk})
response = logged_in_api_client.get(url)
serialized_plt = serializers.PlaylistTrackSerializer(plt).data
serialized_plt["track"]["is_playable"] = False
assert response.data["count"] == 1
assert response.data["results"][0] == serialized_plt
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment