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