From 1674ad919f6ec9f63ca7a70e66aa5d9da7f76881 Mon Sep 17 00:00:00 2001 From: Eliot Berriot <contact@eliotberriot.com> Date: Tue, 8 May 2018 23:06:47 +0200 Subject: [PATCH] See #75: implemented subsonic playlist API endpoints --- api/funkwhale_api/subsonic/serializers.py | 28 +++++ api/funkwhale_api/subsonic/views.py | 124 +++++++++++++++++++++- api/tests/subsonic/test_serializers.py | 40 +++++++ api/tests/subsonic/test_views.py | 91 ++++++++++++++++ 4 files changed, 279 insertions(+), 4 deletions(-) diff --git a/api/funkwhale_api/subsonic/serializers.py b/api/funkwhale_api/subsonic/serializers.py index 59fdb930..d098279b 100644 --- a/api/funkwhale_api/subsonic/serializers.py +++ b/api/funkwhale_api/subsonic/serializers.py @@ -150,3 +150,31 @@ def get_album_list2_data(albums): get_album2_data(a) for a in albums ] + + +def get_playlist_data(playlist): + return { + 'id': playlist.pk, + 'name': playlist.name, + 'owner': playlist.user.username, + 'public': 'false', + 'songCount': playlist._tracks_count, + 'duration': 0, + 'created': playlist.creation_date, + } + + +def get_playlist_detail_data(playlist): + data = get_playlist_data(playlist) + qs = playlist.playlist_tracks.select_related( + 'track__album__artist' + ).prefetch_related('track__files').order_by('index') + data['entry'] = [] + for plt in qs: + try: + tf = [tf for tf in plt.track.files.all()][0] + except IndexError: + continue + td = get_track_data(plt.track.album, plt.track, tf) + data['entry'].append(td) + return data diff --git a/api/funkwhale_api/subsonic/views.py b/api/funkwhale_api/subsonic/views.py index 7bb96179..3836019d 100644 --- a/api/funkwhale_api/subsonic/views.py +++ b/api/funkwhale_api/subsonic/views.py @@ -13,6 +13,7 @@ from funkwhale_api.favorites.models import TrackFavorite from funkwhale_api.music import models as music_models from funkwhale_api.music import utils from funkwhale_api.music import views as music_views +from funkwhale_api.playlists import models as playlists_models from . import authentication from . import filters @@ -38,13 +39,16 @@ def find_object(queryset, model_field='pk', field='id', cast=int): 'code': 0, 'message': 'For input string "{}"'.format(raw_value) }) + qs = queryset + if hasattr(qs, '__call__'): + qs = qs(request) try: - obj = queryset.get(**{model_field: value}) - except queryset.model.DoesNotExist: + obj = qs.get(**{model_field: value}) + except qs.model.DoesNotExist: return response.Response({ 'code': 70, 'message': '{} not found'.format( - queryset.model.__class__.__name__) + qs.model.__class__.__name__) }) kwargs['obj'] = obj return func(self, request, *args, **kwargs) @@ -241,7 +245,6 @@ class SubsonicViewSet(viewsets.GenericViewSet): } return response.Response(data) - @list_route( methods=['get', 'post'], url_name='search3', @@ -308,3 +311,116 @@ class SubsonicViewSet(viewsets.GenericViewSet): queryset = queryset[offset:size] payload['searchResult3'][c['subsonic']] = c['serializer'](queryset) return response.Response(payload) + + @list_route( + methods=['get', 'post'], + url_name='get_playlists', + url_path='getPlaylists') + def get_playlists(self, request, *args, **kwargs): + playlists = request.user.playlists.with_tracks_count().select_related( + 'user' + ) + data = { + 'playlists': { + 'playlist': [ + serializers.get_playlist_data(p) for p in playlists] + } + } + return response.Response(data) + + @list_route( + methods=['get', 'post'], + url_name='get_playlist', + url_path='getPlaylist') + @find_object( + playlists_models.Playlist.objects.with_tracks_count()) + def get_playlist(self, request, *args, **kwargs): + playlist = kwargs.pop('obj') + data = { + 'playlist': serializers.get_playlist_detail_data(playlist) + } + return response.Response(data) + + @list_route( + methods=['get', 'post'], + url_name='update_playlist', + url_path='updatePlaylist') + @find_object( + lambda request: request.user.playlists.all(), + field='playlistId') + def update_playlist(self, request, *args, **kwargs): + playlist = kwargs.pop('obj') + data = request.GET or request.POST + new_name = data.get('name', '') + if new_name: + playlist.name = new_name + playlist.save(update_fields=['name', 'modification_date']) + try: + to_remove = int(data['songIndexToRemove']) + plt = playlist.playlist_tracks.get(index=to_remove) + except (TypeError, ValueError, KeyError): + pass + except playlists_models.PlaylistTrack.DoesNotExist: + pass + else: + plt.delete(update_indexes=True) + + try: + to_add = int(data['songIdToAdd']) + track = music_models.Track.objects.get(pk=to_add) + except (TypeError, ValueError, KeyError): + pass + except music_models.Track.DoesNotExist: + pass + else: + playlist.insert_many([track]) + data = { + 'status': 'ok' + } + return response.Response(data) + + @list_route( + methods=['get', 'post'], + url_name='delete_playlist', + url_path='deletePlaylist') + @find_object( + lambda request: request.user.playlists.all()) + def delete_playlist(self, request, *args, **kwargs): + playlist = kwargs.pop('obj') + playlist.delete() + data = { + 'status': 'ok' + } + return response.Response(data) + + @list_route( + methods=['get', 'post'], + url_name='create_playlist', + url_path='createPlaylist') + def create_playlist(self, request, *args, **kwargs): + data = request.GET or request.POST + name = data.get('name', '') + if not name: + return response.Response({ + 'code': 10, + 'message': 'Playlist ID or name must be specified.' + }, data) + + playlist = request.user.playlists.create( + name=name + ) + try: + to_add = int(data['songId']) + track = music_models.Track.objects.get(pk=to_add) + except (TypeError, ValueError, KeyError): + pass + except music_models.Track.DoesNotExist: + pass + else: + playlist.insert_many([track]) + playlist = request.user.playlists.with_tracks_count().get( + pk=playlist.pk) + data = { + 'playlist': serializers.get_playlist_detail_data(playlist) + } + return response.Response(data) diff --git a/api/tests/subsonic/test_serializers.py b/api/tests/subsonic/test_serializers.py index 64ecc3b3..bb0e8407 100644 --- a/api/tests/subsonic/test_serializers.py +++ b/api/tests/subsonic/test_serializers.py @@ -133,3 +133,43 @@ def test_get_album_list2_serializer(factories): ] data = serializers.get_album_list2_data(qs) assert data == expected + + +def test_playlist_serializer(factories): + plt = factories['playlists.PlaylistTrack']() + playlist = plt.playlist + qs = music_models.Album.objects.with_tracks_count().order_by('pk') + expected = { + 'id': playlist.pk, + 'name': playlist.name, + 'owner': playlist.user.username, + 'public': 'false', + 'songCount': 1, + 'duration': 0, + 'created': playlist.creation_date, + } + qs = playlist.__class__.objects.with_tracks_count() + data = serializers.get_playlist_data(qs.first()) + assert data == expected + + +def test_playlist_detail_serializer(factories): + plt = factories['playlists.PlaylistTrack']() + tf = factories['music.TrackFile'](track=plt.track) + playlist = plt.playlist + qs = music_models.Album.objects.with_tracks_count().order_by('pk') + expected = { + 'id': playlist.pk, + 'name': playlist.name, + 'owner': playlist.user.username, + 'public': 'false', + 'songCount': 1, + 'duration': 0, + 'created': playlist.creation_date, + 'entry': [ + serializers.get_track_data(plt.track.album, plt.track, tf) + ] + } + qs = playlist.__class__.objects.with_tracks_count() + data = serializers.get_playlist_detail_data(qs.first()) + assert data == expected diff --git a/api/tests/subsonic/test_views.py b/api/tests/subsonic/test_views.py index 9fd9bfe3..93aa7285 100644 --- a/api/tests/subsonic/test_views.py +++ b/api/tests/subsonic/test_views.py @@ -240,3 +240,94 @@ def test_search3(f, db, logged_in_api_client, factories): 'song': serializers.get_song_list_data([track]), } } + + +@pytest.mark.parametrize('f', ['xml', 'json']) +def test_get_playlists(f, db, logged_in_api_client, factories): + url = reverse('api:subsonic-get-playlists') + assert url.endswith('getPlaylists') is True + playlist = factories['playlists.Playlist']( + user=logged_in_api_client.user + ) + response = logged_in_api_client.get(url, {'f': f}) + + qs = playlist.__class__.objects.with_tracks_count() + assert response.status_code == 200 + assert response.data == { + 'playlists': { + 'playlist': [serializers.get_playlist_data(qs.first())], + } + } + + +@pytest.mark.parametrize('f', ['xml', 'json']) +def test_get_playlist(f, db, logged_in_api_client, factories): + url = reverse('api:subsonic-get-playlist') + assert url.endswith('getPlaylist') is True + playlist = factories['playlists.Playlist']( + user=logged_in_api_client.user + ) + response = logged_in_api_client.get(url, {'f': f, 'id': playlist.pk}) + + qs = playlist.__class__.objects.with_tracks_count() + assert response.status_code == 200 + assert response.data == { + 'playlist': serializers.get_playlist_detail_data(qs.first()) + } + + +@pytest.mark.parametrize('f', ['xml', 'json']) +def test_update_playlist(f, db, logged_in_api_client, factories): + url = reverse('api:subsonic-update-playlist') + assert url.endswith('updatePlaylist') is True + playlist = factories['playlists.Playlist']( + user=logged_in_api_client.user + ) + plt = factories['playlists.PlaylistTrack']( + index=0, playlist=playlist) + new_track = factories['music.Track']() + response = logged_in_api_client.get( + url, { + 'f': f, + 'name': 'new_name', + 'playlistId': playlist.pk, + 'songIdToAdd': new_track.pk, + 'songIndexToRemove': 0}) + playlist.refresh_from_db() + assert response.status_code == 200 + assert playlist.name == 'new_name' + assert playlist.playlist_tracks.count() == 1 + assert playlist.playlist_tracks.first().track_id == new_track.pk + + +@pytest.mark.parametrize('f', ['xml', 'json']) +def test_delete_playlist(f, db, logged_in_api_client, factories): + url = reverse('api:subsonic-delete-playlist') + assert url.endswith('deletePlaylist') is True + playlist = factories['playlists.Playlist']( + user=logged_in_api_client.user + ) + response = logged_in_api_client.get( + url, {'f': f, 'id': playlist.pk}) + assert response.status_code == 200 + with pytest.raises(playlist.__class__.DoesNotExist): + playlist.refresh_from_db() + + +@pytest.mark.parametrize('f', ['xml', 'json']) +def test_create_playlist(f, db, logged_in_api_client, factories): + url = reverse('api:subsonic-create-playlist') + assert url.endswith('createPlaylist') is True + track = factories['music.Track']() + response = logged_in_api_client.get( + url, {'f': f, 'name': 'hello', 'songId': track.pk}) + assert response.status_code == 200 + playlist = logged_in_api_client.user.playlists.latest('id') + plt = playlist.playlist_tracks.latest('id') + assert playlist.name == 'hello' + assert plt.index == 0 + assert plt.track == track + qs = playlist.__class__.objects.with_tracks_count() + assert response.data == { + 'playlist': serializers.get_playlist_detail_data(qs.first()) + } -- GitLab