feat(subsonic):Subsonic getAlbumInfo, getAlbumInfo2 and getTopSongs endpoints (#2392)
This commit is contained in:
parent
994765d952
commit
4db233b0c8
|
@ -226,6 +226,28 @@ class GetSongSerializer(serializers.Serializer):
|
||||||
return get_track_data(track.album, track, uploads[0])
|
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):
|
def get_starred_tracks_data(favorites):
|
||||||
by_track_id = {f.track_id: f for f in favorites}
|
by_track_id = {f.track_id: f for f in favorites}
|
||||||
tracks = (
|
tracks = (
|
||||||
|
@ -335,15 +357,21 @@ def get_channel_data(channel, uploads):
|
||||||
"id": str(channel.uuid),
|
"id": str(channel.uuid),
|
||||||
"url": channel.get_rss_url(),
|
"url": channel.get_rss_url(),
|
||||||
"title": channel.artist.name,
|
"title": channel.artist.name,
|
||||||
"description": channel.artist.description.as_plain_text
|
"description": (
|
||||||
|
channel.artist.description.as_plain_text
|
||||||
if channel.artist.description
|
if channel.artist.description
|
||||||
else "",
|
else ""
|
||||||
"coverArt": f"at-{channel.artist.attachment_cover.uuid}"
|
),
|
||||||
|
"coverArt": (
|
||||||
|
f"at-{channel.artist.attachment_cover.uuid}"
|
||||||
if channel.artist.attachment_cover
|
if channel.artist.attachment_cover
|
||||||
else "",
|
else ""
|
||||||
"originalImageUrl": channel.artist.attachment_cover.url
|
),
|
||||||
|
"originalImageUrl": (
|
||||||
|
channel.artist.attachment_cover.url
|
||||||
if channel.artist.attachment_cover
|
if channel.artist.attachment_cover
|
||||||
else "",
|
else ""
|
||||||
|
),
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
}
|
}
|
||||||
if uploads:
|
if uploads:
|
||||||
|
@ -360,12 +388,14 @@ def get_channel_episode_data(upload, channel_id):
|
||||||
"channelId": str(channel_id),
|
"channelId": str(channel_id),
|
||||||
"streamId": upload.track.id,
|
"streamId": upload.track.id,
|
||||||
"title": upload.track.title,
|
"title": upload.track.title,
|
||||||
"description": upload.track.description.as_plain_text
|
"description": (
|
||||||
if upload.track.description
|
upload.track.description.as_plain_text if upload.track.description else ""
|
||||||
else "",
|
),
|
||||||
"coverArt": f"at-{upload.track.attachment_cover.uuid}"
|
"coverArt": (
|
||||||
|
f"at-{upload.track.attachment_cover.uuid}"
|
||||||
if upload.track.attachment_cover
|
if upload.track.attachment_cover
|
||||||
else "",
|
else ""
|
||||||
|
),
|
||||||
"isDir": "false",
|
"isDir": "false",
|
||||||
"year": upload.track.creation_date.year,
|
"year": upload.track.creation_date.year,
|
||||||
"publishDate": upload.track.creation_date.isoformat(),
|
"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
|
Documentation of Subsonic API can be found at http://www.subsonic.org/pages/api.jsp
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import functools
|
import functools
|
||||||
|
|
||||||
|
@ -90,6 +91,8 @@ def find_object(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
except qs.model.MultipleObjectsReturned:
|
||||||
|
obj = qs.filter(**{model_field: value})[0]
|
||||||
kwargs["obj"] = obj
|
kwargs["obj"] = obj
|
||||||
return func(self, request, *args, **kwargs)
|
return func(self, request, *args, **kwargs)
|
||||||
|
|
||||||
|
@ -260,6 +263,43 @@ class SubsonicViewSet(viewsets.GenericViewSet):
|
||||||
|
|
||||||
return response.Response(payload, status=200)
|
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(
|
@action(
|
||||||
detail=False,
|
detail=False,
|
||||||
methods=["get", "post"],
|
methods=["get", "post"],
|
||||||
|
@ -289,6 +329,44 @@ class SubsonicViewSet(viewsets.GenericViewSet):
|
||||||
payload = {"album": data}
|
payload = {"album": data}
|
||||||
return response.Response(payload, status=200)
|
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")
|
@action(detail=False, methods=["get", "post"], url_name="stream", url_path="stream")
|
||||||
@find_object(music_models.Track.objects.all(), filter_playable=True)
|
@find_object(music_models.Track.objects.all(), filter_playable=True)
|
||||||
def stream(self, request, *args, **kwargs):
|
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"])
|
@pytest.mark.parametrize("f", ["json"])
|
||||||
def test_get_song(
|
def test_get_song(
|
||||||
f, db, logged_in_api_client, factories, mocker, queryset_equal_queries
|
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)
|
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"])
|
@pytest.mark.parametrize("f", ["json"])
|
||||||
def test_stream(
|
def test_stream(
|
||||||
f, db, logged_in_api_client, factories, mocker, queryset_equal_queries, settings
|
f, db, logged_in_api_client, factories, mocker, queryset_equal_queries, settings
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Subsonic getAlbumInfo, getAlbumInfo2 and getTopSongs endpoints (#2392)
|
Loading…
Reference in New Issue