feat(subsonic):Subsonic getAlbumInfo, getAlbumInfo2 and getTopSongs endpoints (#2392)

merge-requests/2870/merge
petitminion 2025-02-13 11:32:06 +00:00
rodzic 994765d952
commit 4db233b0c8
4 zmienionych plików z 206 dodań i 15 usunięć

Wyświetl plik

@ -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(),

Wyświetl plik

@ -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):

Wyświetl plik

@ -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

Wyświetl plik

@ -0,0 +1 @@
Subsonic getAlbumInfo, getAlbumInfo2 and getTopSongs endpoints (#2392)