diff --git a/api/funkwhale_api/subsonic/serializers.py b/api/funkwhale_api/subsonic/serializers.py
index 59fdb9308f58137ca7b2b5fa879b6319e62dad01..d098279b282df59099c2d7913c9f180bbacaf4d1 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 7bb9617953cfa986c7f388f4165b89bfac41fa9b..3836019d4b6e6f66999bf6a958305ea68afc43f4 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 64ecc3b3445d8721e38e80e6d3ae076776a6d21d..bb0e8407b3bd6cd560deb53bf2200d8262d5e8ee 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 9fd9bfe38126ae7650750ef5082c4cd46a979b57..93aa72855ef1c194c6952cce9762e6937af59c06 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())
+ }