See #75: implemented subsonic playlist API endpoints
This commit is contained in:
parent
7e9320fc1c
commit
1674ad919f
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue