kopia lustrzana https://dev.funkwhale.audio/funkwhale/funkwhale
feat(subsonic):Subsonic getAlbumInfo, getAlbumInfo2 and getTopSongs endpoints (#2392)
rodzic
994765d952
commit
4db233b0c8
|
@ -226,6 +226,28 @@ class GetSongSerializer(serializers.Serializer):
|
|||
return get_track_data(track.album, track, uploads[0])
|
||||
|
||||
|
||||
class GetTopSongsSerializer(serializers.Serializer):
|
||||
def to_representation(self, artist):
|
||||
top_tracks = (
|
||||
history_models.Listening.objects.filter(track__artist_credit__artist=artist)
|
||||
.values("track")
|
||||
.annotate(listen_count=Count("id"))
|
||||
.order_by("-listen_count")[: self.context["count"]]
|
||||
)
|
||||
if not len(top_tracks):
|
||||
return {}
|
||||
|
||||
top_tracks_instances = []
|
||||
for track in top_tracks:
|
||||
track = music_models.Track.objects.get(id=track["track"])
|
||||
top_tracks_instances.append(track)
|
||||
|
||||
return [
|
||||
get_track_data(track.album, track, track.uploads.all()[0])
|
||||
for track in top_tracks_instances
|
||||
]
|
||||
|
||||
|
||||
def get_starred_tracks_data(favorites):
|
||||
by_track_id = {f.track_id: f for f in favorites}
|
||||
tracks = (
|
||||
|
@ -335,15 +357,21 @@ def get_channel_data(channel, uploads):
|
|||
"id": str(channel.uuid),
|
||||
"url": channel.get_rss_url(),
|
||||
"title": channel.artist.name,
|
||||
"description": channel.artist.description.as_plain_text
|
||||
if channel.artist.description
|
||||
else "",
|
||||
"coverArt": f"at-{channel.artist.attachment_cover.uuid}"
|
||||
if channel.artist.attachment_cover
|
||||
else "",
|
||||
"originalImageUrl": channel.artist.attachment_cover.url
|
||||
if channel.artist.attachment_cover
|
||||
else "",
|
||||
"description": (
|
||||
channel.artist.description.as_plain_text
|
||||
if channel.artist.description
|
||||
else ""
|
||||
),
|
||||
"coverArt": (
|
||||
f"at-{channel.artist.attachment_cover.uuid}"
|
||||
if channel.artist.attachment_cover
|
||||
else ""
|
||||
),
|
||||
"originalImageUrl": (
|
||||
channel.artist.attachment_cover.url
|
||||
if channel.artist.attachment_cover
|
||||
else ""
|
||||
),
|
||||
"status": "completed",
|
||||
}
|
||||
if uploads:
|
||||
|
@ -360,12 +388,14 @@ def get_channel_episode_data(upload, channel_id):
|
|||
"channelId": str(channel_id),
|
||||
"streamId": upload.track.id,
|
||||
"title": upload.track.title,
|
||||
"description": upload.track.description.as_plain_text
|
||||
if upload.track.description
|
||||
else "",
|
||||
"coverArt": f"at-{upload.track.attachment_cover.uuid}"
|
||||
if upload.track.attachment_cover
|
||||
else "",
|
||||
"description": (
|
||||
upload.track.description.as_plain_text if upload.track.description else ""
|
||||
),
|
||||
"coverArt": (
|
||||
f"at-{upload.track.attachment_cover.uuid}"
|
||||
if upload.track.attachment_cover
|
||||
else ""
|
||||
),
|
||||
"isDir": "false",
|
||||
"year": upload.track.creation_date.year,
|
||||
"publishDate": upload.track.creation_date.isoformat(),
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
"""
|
||||
Documentation of Subsonic API can be found at http://www.subsonic.org/pages/api.jsp
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import functools
|
||||
|
||||
|
@ -90,6 +91,8 @@ def find_object(
|
|||
}
|
||||
}
|
||||
)
|
||||
except qs.model.MultipleObjectsReturned:
|
||||
obj = qs.filter(**{model_field: value})[0]
|
||||
kwargs["obj"] = obj
|
||||
return func(self, request, *args, **kwargs)
|
||||
|
||||
|
@ -260,6 +263,43 @@ class SubsonicViewSet(viewsets.GenericViewSet):
|
|||
|
||||
return response.Response(payload, status=200)
|
||||
|
||||
# This should return last.fm data but we choose to return the pod top song
|
||||
@action(
|
||||
detail=False,
|
||||
methods=["get", "post"],
|
||||
url_name="get_top_songs",
|
||||
url_path="getTopSongs",
|
||||
)
|
||||
@find_object(
|
||||
music_models.Artist.objects.all(),
|
||||
model_field="artist_credit__artist__name",
|
||||
field="artist",
|
||||
filter_playable=True,
|
||||
cast=str,
|
||||
)
|
||||
def get_top_songs(self, request, *args, **kwargs):
|
||||
artist = kwargs.pop("obj")
|
||||
data = request.GET or request.POST
|
||||
try:
|
||||
count = int(data["count"])
|
||||
except KeyError:
|
||||
return response.Response(
|
||||
{
|
||||
"error": {
|
||||
"code": 10,
|
||||
"message": "required parameter 'count' not present",
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
# passing with many=true to make the serializer accept the returned list
|
||||
data = serializers.GetTopSongsSerializer(
|
||||
[artist], context={"count": count}, many=True
|
||||
).data
|
||||
payload = {"topSongs": data[0]}
|
||||
|
||||
return response.Response(payload, status=200)
|
||||
|
||||
@action(
|
||||
detail=False,
|
||||
methods=["get", "post"],
|
||||
|
@ -289,6 +329,44 @@ class SubsonicViewSet(viewsets.GenericViewSet):
|
|||
payload = {"album": data}
|
||||
return response.Response(payload, status=200)
|
||||
|
||||
# A clone of get_album (this should return last.fm data but we prefer to send our own metadata)
|
||||
@action(
|
||||
detail=False,
|
||||
methods=["get", "post"],
|
||||
url_name="get_album_info_2",
|
||||
url_path="getAlbumInfo2",
|
||||
)
|
||||
@find_object(
|
||||
music_models.Album.objects.with_duration().prefetch_related(
|
||||
"artist_credit__artist"
|
||||
),
|
||||
filter_playable=True,
|
||||
)
|
||||
def get_album_info_2(self, request, *args, **kwargs):
|
||||
album = kwargs.pop("obj")
|
||||
data = serializers.GetAlbumSerializer(album).data
|
||||
payload = {"albumInfo": data}
|
||||
return response.Response(payload, status=200)
|
||||
|
||||
# A clone of get_album (this should return last.fm data but we prefer to send our own metadata)
|
||||
@action(
|
||||
detail=False,
|
||||
methods=["get", "post"],
|
||||
url_name="get_album_info",
|
||||
url_path="getAlbumInfo",
|
||||
)
|
||||
@find_object(
|
||||
music_models.Album.objects.with_duration().prefetch_related(
|
||||
"artist_credit__artist"
|
||||
),
|
||||
filter_playable=True,
|
||||
)
|
||||
def get_album_info(self, request, *args, **kwargs):
|
||||
album = kwargs.pop("obj")
|
||||
data = serializers.GetAlbumSerializer(album).data
|
||||
payload = {"albumInfo": data}
|
||||
return response.Response(payload, status=200)
|
||||
|
||||
@action(detail=False, methods=["get", "post"], url_name="stream", url_path="stream")
|
||||
@find_object(music_models.Track.objects.all(), filter_playable=True)
|
||||
def stream(self, request, *args, **kwargs):
|
||||
|
|
|
@ -227,6 +227,62 @@ def test_get_album(
|
|||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("f", ["json"])
|
||||
def test_get_album_info_2(
|
||||
f, db, logged_in_api_client, factories, mocker, queryset_equal_queries
|
||||
):
|
||||
url = reverse("api:subsonic:subsonic-get_album_info_2")
|
||||
assert url.endswith("getAlbumInfo2") is True
|
||||
artist_credit = factories["music.ArtistCredit"]()
|
||||
album = (
|
||||
factories["music.Album"](artist_credit=artist_credit)
|
||||
.__class__.objects.with_duration()
|
||||
.first()
|
||||
)
|
||||
factories["music.Track"].create_batch(size=3, album=album, playable=True)
|
||||
playable_by = mocker.spy(music_models.AlbumQuerySet, "playable_by")
|
||||
expected = {"albumInfo": serializers.GetAlbumSerializer(album).data}
|
||||
response = logged_in_api_client.get(url, {"f": f, "id": album.pk})
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.data == expected
|
||||
|
||||
playable_by.assert_called_once_with(
|
||||
music_models.Album.objects.with_duration().prefetch_related(
|
||||
"artist_credit__artist"
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("f", ["json"])
|
||||
def test_get_album_info(
|
||||
f, db, logged_in_api_client, factories, mocker, queryset_equal_queries
|
||||
):
|
||||
url = reverse("api:subsonic:subsonic-get_album_info")
|
||||
assert url.endswith("getAlbumInfo") is True
|
||||
artist_credit = factories["music.ArtistCredit"]()
|
||||
album = (
|
||||
factories["music.Album"](artist_credit=artist_credit)
|
||||
.__class__.objects.with_duration()
|
||||
.first()
|
||||
)
|
||||
factories["music.Track"].create_batch(size=3, album=album, playable=True)
|
||||
playable_by = mocker.spy(music_models.AlbumQuerySet, "playable_by")
|
||||
expected = {"albumInfo": serializers.GetAlbumSerializer(album).data}
|
||||
response = logged_in_api_client.get(url, {"f": f, "id": album.pk})
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.data == expected
|
||||
|
||||
playable_by.assert_called_once_with(
|
||||
music_models.Album.objects.with_duration().prefetch_related(
|
||||
"artist_credit__artist"
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("f", ["json"])
|
||||
def test_get_song(
|
||||
f, db, logged_in_api_client, factories, mocker, queryset_equal_queries
|
||||
|
@ -247,6 +303,32 @@ def test_get_song(
|
|||
playable_by.assert_called_once_with(music_models.Track.objects.all(), None)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("f", ["json"])
|
||||
def test_get_top_songs(
|
||||
f, db, logged_in_api_client, factories, mocker, queryset_equal_queries
|
||||
):
|
||||
url = reverse("api:subsonic:subsonic-get_top_songs")
|
||||
assert url.endswith("getTopSongs") is True
|
||||
artist_credit = factories["music.ArtistCredit"]()
|
||||
album = factories["music.Album"](artist_credit=artist_credit)
|
||||
track = factories["music.Track"](album=album, playable=True)
|
||||
tracks = factories["music.Track"].create_batch(20, album=album, playable=True)
|
||||
factories["music.Upload"](track=track)
|
||||
factories["history.Listening"].create_batch(20, track=track)
|
||||
factories["history.Listening"].create_batch(2, track=tracks[2])
|
||||
|
||||
playable_by = mocker.spy(music_models.TrackQuerySet, "playable_by")
|
||||
response = logged_in_api_client.get(
|
||||
url, {"f": f, "artist": artist_credit.artist.name, "count": 2}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.data["topSongs"][0] == serializers.get_track_data(
|
||||
track.album, track, track.uploads.all()[0]
|
||||
)
|
||||
playable_by.assert_called_once_with(music_models.Track.objects.all(), None)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("f", ["json"])
|
||||
def test_stream(
|
||||
f, db, logged_in_api_client, factories, mocker, queryset_equal_queries, settings
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Subsonic getAlbumInfo, getAlbumInfo2 and getTopSongs endpoints (#2392)
|
Ładowanie…
Reference in New Issue