See #75 more subsonic api endpoints (star, unstar, search...)
This commit is contained in:
parent
40cde0cd92
commit
e31099ef33
|
@ -0,0 +1,23 @@
|
|||
from django_filters import rest_framework as filters
|
||||
|
||||
from funkwhale_api.music import models as music_models
|
||||
|
||||
|
||||
class AlbumList2FilterSet(filters.FilterSet):
|
||||
type = filters.CharFilter(name='_', method='filter_type')
|
||||
|
||||
class Meta:
|
||||
model = music_models.Album
|
||||
fields = ['type']
|
||||
|
||||
def filter_type(self, queryset, name, value):
|
||||
ORDERING = {
|
||||
'random': '?',
|
||||
'newest': '-creation_date',
|
||||
'alphabeticalByArtist': 'artist__name',
|
||||
'alphabeticalByName': 'title',
|
||||
}
|
||||
if value not in ORDERING:
|
||||
return queryset
|
||||
|
||||
return queryset.order_by(ORDERING[value])
|
|
@ -4,6 +4,16 @@ from django.db.models import functions, Count
|
|||
|
||||
from rest_framework import serializers
|
||||
|
||||
from funkwhale_api.music import models as music_models
|
||||
|
||||
|
||||
def get_artist_data(artist_values):
|
||||
return {
|
||||
'id': artist_values['id'],
|
||||
'name': artist_values['name'],
|
||||
'albumCount': artist_values['_albums_count']
|
||||
}
|
||||
|
||||
|
||||
class GetArtistsSerializer(serializers.Serializer):
|
||||
def to_representation(self, queryset):
|
||||
|
@ -11,7 +21,7 @@ class GetArtistsSerializer(serializers.Serializer):
|
|||
'ignoredArticles': '',
|
||||
'index': []
|
||||
}
|
||||
queryset = queryset.annotate(_albums_count=Count('albums'))
|
||||
queryset = queryset.with_albums_count()
|
||||
queryset = queryset.order_by(functions.Lower('name'))
|
||||
values = queryset.values('id', '_albums_count', 'name')
|
||||
|
||||
|
@ -23,11 +33,7 @@ class GetArtistsSerializer(serializers.Serializer):
|
|||
letter_data = {
|
||||
'name': letter,
|
||||
'artist': [
|
||||
{
|
||||
'id': v['id'],
|
||||
'name': v['name'],
|
||||
'albumCount': v['_albums_count']
|
||||
}
|
||||
get_artist_data(v)
|
||||
for v in artists
|
||||
]
|
||||
}
|
||||
|
@ -59,42 +65,88 @@ class GetArtistSerializer(serializers.Serializer):
|
|||
return payload
|
||||
|
||||
|
||||
def get_track_data(album, track, tf):
|
||||
data = {
|
||||
'id': track.pk,
|
||||
'isDir': 'false',
|
||||
'title': track.title,
|
||||
'album': album.title,
|
||||
'artist': album.artist.name,
|
||||
'track': track.position,
|
||||
'contentType': tf.mimetype,
|
||||
'suffix': tf.extension or '',
|
||||
'duration': tf.duration or 0,
|
||||
'created': track.creation_date,
|
||||
'albumId': album.pk,
|
||||
'artistId': album.artist.pk,
|
||||
'type': 'music',
|
||||
}
|
||||
if album.release_date:
|
||||
data['year'] = album.release_date.year
|
||||
return data
|
||||
|
||||
|
||||
def get_album2_data(album):
|
||||
payload = {
|
||||
'id': album.id,
|
||||
'artistId': album.artist.id,
|
||||
'name': album.title,
|
||||
'artist': album.artist.name,
|
||||
'created': album.creation_date,
|
||||
}
|
||||
try:
|
||||
payload['songCount'] = album._tracks_count
|
||||
except AttributeError:
|
||||
payload['songCount'] = len(album.tracks.prefetch_related('files'))
|
||||
return payload
|
||||
|
||||
|
||||
def get_song_list_data(tracks):
|
||||
songs = []
|
||||
for track in tracks:
|
||||
try:
|
||||
tf = [tf for tf in track.files.all()][0]
|
||||
except IndexError:
|
||||
continue
|
||||
track_data = get_track_data(track.album, track, tf)
|
||||
songs.append(track_data)
|
||||
return songs
|
||||
|
||||
|
||||
class GetAlbumSerializer(serializers.Serializer):
|
||||
def to_representation(self, album):
|
||||
tracks = album.tracks.prefetch_related('files')
|
||||
payload = {
|
||||
'id': album.id,
|
||||
'artistId': album.artist.id,
|
||||
'name': album.title,
|
||||
'artist': album.artist.name,
|
||||
'created': album.creation_date,
|
||||
'songCount': len(tracks),
|
||||
'song': [],
|
||||
}
|
||||
tracks = album.tracks.prefetch_related('files').select_related('album')
|
||||
payload = get_album2_data(album)
|
||||
if album.release_date:
|
||||
payload['year'] = album.release_date.year
|
||||
|
||||
for track in tracks:
|
||||
try:
|
||||
tf = [tf for tf in track.files.all()][0]
|
||||
except IndexError:
|
||||
continue
|
||||
track_data = {
|
||||
'id': track.pk,
|
||||
'isDir': False,
|
||||
'title': track.title,
|
||||
'album': album.title,
|
||||
'artist': album.artist.name,
|
||||
'track': track.position,
|
||||
'contentType': tf.mimetype,
|
||||
'suffix': tf.extension,
|
||||
'duration': tf.duration,
|
||||
'created': track.creation_date,
|
||||
'albumId': album.pk,
|
||||
'artistId': album.artist.pk,
|
||||
'type': 'music',
|
||||
}
|
||||
if album.release_date:
|
||||
track_data['year'] = album.release_date.year
|
||||
payload['song'].append(track_data)
|
||||
payload['song'] = get_song_list_data(tracks)
|
||||
return payload
|
||||
|
||||
|
||||
def get_starred_tracks_data(favorites):
|
||||
by_track_id = {
|
||||
f.track_id: f
|
||||
for f in favorites
|
||||
}
|
||||
tracks = music_models.Track.objects.filter(
|
||||
pk__in=by_track_id.keys()
|
||||
).select_related('album__artist').prefetch_related('files')
|
||||
tracks = tracks.order_by('-creation_date')
|
||||
data = []
|
||||
for t in tracks:
|
||||
try:
|
||||
tf = [tf for tf in t.files.all()][0]
|
||||
except IndexError:
|
||||
continue
|
||||
td = get_track_data(t.album, t, tf)
|
||||
td['starred'] = by_track_id[t.pk].creation_date
|
||||
data.append(td)
|
||||
return data
|
||||
|
||||
|
||||
def get_album_list2_data(albums):
|
||||
return [
|
||||
get_album2_data(a)
|
||||
for a in albums
|
||||
]
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
import datetime
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
from rest_framework import exceptions
|
||||
from rest_framework import permissions as rest_permissions
|
||||
from rest_framework import response
|
||||
|
@ -5,10 +9,13 @@ from rest_framework import viewsets
|
|||
from rest_framework.decorators import list_route
|
||||
from rest_framework.serializers import ValidationError
|
||||
|
||||
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 . import authentication
|
||||
from . import filters
|
||||
from . import negotiation
|
||||
from . import serializers
|
||||
|
||||
|
@ -83,6 +90,24 @@ class SubsonicViewSet(viewsets.GenericViewSet):
|
|||
}
|
||||
return response.Response(data, status=200)
|
||||
|
||||
@list_route(
|
||||
methods=['get', 'post'],
|
||||
url_name='get_license',
|
||||
permissions_classes=[],
|
||||
url_path='getLicense')
|
||||
def get_license(self, request, *args, **kwargs):
|
||||
now = timezone.now()
|
||||
data = {
|
||||
'status': 'ok',
|
||||
'version': '1.16.0',
|
||||
'license': {
|
||||
'valid': 'true',
|
||||
'email': 'valid@valid.license',
|
||||
'licenseExpires': now + datetime.timedelta(days=365)
|
||||
}
|
||||
}
|
||||
return response.Response(data, status=200)
|
||||
|
||||
@list_route(
|
||||
methods=['get', 'post'],
|
||||
url_name='get_artists',
|
||||
|
@ -110,6 +135,19 @@ class SubsonicViewSet(viewsets.GenericViewSet):
|
|||
|
||||
return response.Response(payload, status=200)
|
||||
|
||||
@list_route(
|
||||
methods=['get', 'post'],
|
||||
url_name='get_artist_info2',
|
||||
url_path='getArtistInfo2')
|
||||
@find_object(music_models.Artist.objects.all())
|
||||
def get_artist_info2(self, request, *args, **kwargs):
|
||||
artist = kwargs.pop('obj')
|
||||
payload = {
|
||||
'artist-info2': {}
|
||||
}
|
||||
|
||||
return response.Response(payload, status=200)
|
||||
|
||||
@list_route(
|
||||
methods=['get', 'post'],
|
||||
url_name='get_album',
|
||||
|
@ -139,5 +177,134 @@ class SubsonicViewSet(viewsets.GenericViewSet):
|
|||
)
|
||||
track_file = queryset.first()
|
||||
if not track_file:
|
||||
return Response(status=404)
|
||||
return response.Response(status=404)
|
||||
return music_views.handle_serve(track_file)
|
||||
|
||||
@list_route(
|
||||
methods=['get', 'post'],
|
||||
url_name='star',
|
||||
url_path='star')
|
||||
@find_object(
|
||||
music_models.Track.objects.all())
|
||||
def star(self, request, *args, **kwargs):
|
||||
track = kwargs.pop('obj')
|
||||
TrackFavorite.add(user=request.user, track=track)
|
||||
return response.Response({'status': 'ok'})
|
||||
|
||||
@list_route(
|
||||
methods=['get', 'post'],
|
||||
url_name='unstar',
|
||||
url_path='unstar')
|
||||
@find_object(
|
||||
music_models.Track.objects.all())
|
||||
def unstar(self, request, *args, **kwargs):
|
||||
track = kwargs.pop('obj')
|
||||
request.user.track_favorites.filter(track=track).delete()
|
||||
return response.Response({'status': 'ok'})
|
||||
|
||||
@list_route(
|
||||
methods=['get', 'post'],
|
||||
url_name='get_starred2',
|
||||
url_path='getStarred2')
|
||||
def get_starred2(self, request, *args, **kwargs):
|
||||
favorites = request.user.track_favorites.all()
|
||||
data = {
|
||||
'song': serializers.get_starred_tracks_data(favorites)
|
||||
}
|
||||
return response.Response(data)
|
||||
|
||||
@list_route(
|
||||
methods=['get', 'post'],
|
||||
url_name='get_album_list2',
|
||||
url_path='getAlbumList2')
|
||||
def get_album_list2(self, request, *args, **kwargs):
|
||||
queryset = music_models.Album.objects.with_tracks_count()
|
||||
data = request.GET or request.POST
|
||||
filterset = filters.AlbumList2FilterSet(data, queryset=queryset)
|
||||
queryset = filterset.qs
|
||||
try:
|
||||
offset = int(data['offset'])
|
||||
except (TypeError, KeyError, ValueError):
|
||||
offset = 0
|
||||
|
||||
try:
|
||||
size = int(data['size'])
|
||||
except (TypeError, KeyError, ValueError):
|
||||
size = 50
|
||||
|
||||
size = min(size, 500)
|
||||
queryset = queryset[offset:size]
|
||||
data = {
|
||||
'albumList2': {
|
||||
'album': serializers.get_album_list2_data(queryset)
|
||||
}
|
||||
}
|
||||
return response.Response(data)
|
||||
|
||||
|
||||
@list_route(
|
||||
methods=['get', 'post'],
|
||||
url_name='search3',
|
||||
url_path='search3')
|
||||
def search3(self, request, *args, **kwargs):
|
||||
data = request.GET or request.POST
|
||||
query = str(data.get('query', '')).replace('*', '')
|
||||
conf = [
|
||||
{
|
||||
'subsonic': 'artist',
|
||||
'search_fields': ['name'],
|
||||
'queryset': (
|
||||
music_models.Artist.objects
|
||||
.with_albums_count()
|
||||
.values('id', '_albums_count', 'name')
|
||||
),
|
||||
'serializer': lambda qs: [
|
||||
serializers.get_artist_data(a) for a in qs
|
||||
]
|
||||
},
|
||||
{
|
||||
'subsonic': 'album',
|
||||
'search_fields': ['title'],
|
||||
'queryset': (
|
||||
music_models.Album.objects
|
||||
.with_tracks_count()
|
||||
.select_related('artist')
|
||||
),
|
||||
'serializer': serializers.get_album_list2_data,
|
||||
},
|
||||
{
|
||||
'subsonic': 'song',
|
||||
'search_fields': ['title'],
|
||||
'queryset': (
|
||||
music_models.Track.objects
|
||||
.prefetch_related('files')
|
||||
.select_related('album__artist')
|
||||
),
|
||||
'serializer': serializers.get_song_list_data,
|
||||
},
|
||||
]
|
||||
payload = {
|
||||
'searchResult3': {}
|
||||
}
|
||||
for c in conf:
|
||||
offsetKey = '{}Offset'.format(c['subsonic'])
|
||||
countKey = '{}Count'.format(c['subsonic'])
|
||||
try:
|
||||
offset = int(data[offsetKey])
|
||||
except (TypeError, KeyError, ValueError):
|
||||
offset = 0
|
||||
|
||||
try:
|
||||
size = int(data[countKey])
|
||||
except (TypeError, KeyError, ValueError):
|
||||
size = 20
|
||||
|
||||
size = min(size, 100)
|
||||
queryset = c['queryset']
|
||||
if query:
|
||||
queryset = c['queryset'].filter(
|
||||
utils.get_query(query, c['search_fields'])
|
||||
)
|
||||
queryset = queryset[offset:size]
|
||||
payload['searchResult3'][c['subsonic']] = c['serializer'](queryset)
|
||||
return response.Response(payload)
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
from funkwhale_api.music import models as music_models
|
||||
from funkwhale_api.subsonic import serializers
|
||||
|
||||
|
||||
|
@ -89,15 +90,15 @@ def test_get_album_serializer(factories):
|
|||
'song': [
|
||||
{
|
||||
'id': track.pk,
|
||||
'isDir': False,
|
||||
'isDir': 'false',
|
||||
'title': track.title,
|
||||
'album': album.title,
|
||||
'artist': artist.name,
|
||||
'track': track.position,
|
||||
'year': track.album.release_date.year,
|
||||
'contentType': tf.mimetype,
|
||||
'suffix': tf.extension,
|
||||
'duration': tf.duration,
|
||||
'suffix': tf.extension or '',
|
||||
'duration': tf.duration or 0,
|
||||
'created': track.creation_date,
|
||||
'albumId': album.pk,
|
||||
'artistId': artist.pk,
|
||||
|
@ -107,3 +108,28 @@ def test_get_album_serializer(factories):
|
|||
}
|
||||
|
||||
assert serializers.GetAlbumSerializer(album).data == expected
|
||||
|
||||
|
||||
def test_starred_tracks2_serializer(factories):
|
||||
artist = factories['music.Artist']()
|
||||
album = factories['music.Album'](artist=artist)
|
||||
track = factories['music.Track'](album=album)
|
||||
tf = factories['music.TrackFile'](track=track)
|
||||
favorite = factories['favorites.TrackFavorite'](track=track)
|
||||
expected = [serializers.get_track_data(album, track, tf)]
|
||||
expected[0]['starred'] = favorite.creation_date
|
||||
data = serializers.get_starred_tracks_data([favorite])
|
||||
assert data == expected
|
||||
|
||||
|
||||
def test_get_album_list2_serializer(factories):
|
||||
album1 = factories['music.Album']()
|
||||
album2 = factories['music.Album']()
|
||||
|
||||
qs = music_models.Album.objects.with_tracks_count().order_by('pk')
|
||||
expected = [
|
||||
serializers.get_album2_data(album1),
|
||||
serializers.get_album2_data(album2),
|
||||
]
|
||||
data = serializers.get_album_list2_data(qs)
|
||||
assert data == expected
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import datetime
|
||||
import json
|
||||
import pytest
|
||||
|
||||
from django.utils import timezone
|
||||
from django.urls import reverse
|
||||
|
||||
from rest_framework.response import Response
|
||||
|
||||
from funkwhale_api.music import models as music_models
|
||||
|
@ -42,6 +45,26 @@ def test_exception_wrong_credentials(f, db, api_client):
|
|||
assert response.data == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize('f', ['xml', 'json'])
|
||||
def test_get_license(f, db, logged_in_api_client, mocker):
|
||||
url = reverse('api:subsonic-get-license')
|
||||
assert url.endswith('getLicense') is True
|
||||
now = timezone.now()
|
||||
mocker.patch('django.utils.timezone.now', return_value=now)
|
||||
response = logged_in_api_client.get(url, {'f': f})
|
||||
expected = {
|
||||
'status': 'ok',
|
||||
'version': '1.16.0',
|
||||
'license': {
|
||||
'valid': 'true',
|
||||
'email': 'valid@valid.license',
|
||||
'licenseExpires': now + datetime.timedelta(days=365)
|
||||
}
|
||||
}
|
||||
assert response.status_code == 200
|
||||
assert response.data == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize('f', ['xml', 'json'])
|
||||
def test_ping(f, db, api_client):
|
||||
url = reverse('api:subsonic-ping')
|
||||
|
@ -86,6 +109,21 @@ def test_get_artist(f, db, logged_in_api_client, factories):
|
|||
assert response.data == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize('f', ['xml', 'json'])
|
||||
def test_get_artist_info2(f, db, logged_in_api_client, factories):
|
||||
url = reverse('api:subsonic-get-artist-info2')
|
||||
assert url.endswith('getArtistInfo2') is True
|
||||
artist = factories['music.Artist']()
|
||||
|
||||
expected = {
|
||||
'artist-info2': {}
|
||||
}
|
||||
response = logged_in_api_client.get(url, {'id': artist.pk})
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.data == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize('f', ['xml', 'json'])
|
||||
def test_get_album(f, db, logged_in_api_client, factories):
|
||||
url = reverse('api:subsonic-get-album')
|
||||
|
@ -118,3 +156,87 @@ def test_stream(f, db, logged_in_api_client, factories, mocker):
|
|||
track_file=tf
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.parametrize('f', ['xml', 'json'])
|
||||
def test_star(f, db, logged_in_api_client, factories):
|
||||
url = reverse('api:subsonic-star')
|
||||
assert url.endswith('star') is True
|
||||
track = factories['music.Track']()
|
||||
response = logged_in_api_client.get(url, {'f': f, 'id': track.pk})
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.data == {'status': 'ok'}
|
||||
|
||||
favorite = logged_in_api_client.user.track_favorites.latest('id')
|
||||
assert favorite.track == track
|
||||
|
||||
|
||||
@pytest.mark.parametrize('f', ['xml', 'json'])
|
||||
def test_unstar(f, db, logged_in_api_client, factories):
|
||||
url = reverse('api:subsonic-unstar')
|
||||
assert url.endswith('unstar') is True
|
||||
track = factories['music.Track']()
|
||||
favorite = factories['favorites.TrackFavorite'](
|
||||
track=track, user=logged_in_api_client.user)
|
||||
response = logged_in_api_client.get(url, {'f': f, 'id': track.pk})
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.data == {'status': 'ok'}
|
||||
assert logged_in_api_client.user.track_favorites.count() == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize('f', ['xml', 'json'])
|
||||
def test_get_starred2(f, db, logged_in_api_client, factories):
|
||||
url = reverse('api:subsonic-get-starred2')
|
||||
assert url.endswith('getStarred2') is True
|
||||
track = factories['music.Track']()
|
||||
favorite = factories['favorites.TrackFavorite'](
|
||||
track=track, user=logged_in_api_client.user)
|
||||
response = logged_in_api_client.get(url, {'f': f, 'id': track.pk})
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.data == {
|
||||
'song': serializers.get_starred_tracks_data([favorite])
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize('f', ['xml', 'json'])
|
||||
def test_get_album_list2(f, db, logged_in_api_client, factories):
|
||||
url = reverse('api:subsonic-get-album-list2')
|
||||
assert url.endswith('getAlbumList2') is True
|
||||
album1 = factories['music.Album']()
|
||||
album2 = factories['music.Album']()
|
||||
response = logged_in_api_client.get(url, {'f': f, 'type': 'newest'})
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.data == {
|
||||
'albumList2': {
|
||||
'album': serializers.get_album_list2_data([album2, album1])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize('f', ['xml', 'json'])
|
||||
def test_search3(f, db, logged_in_api_client, factories):
|
||||
url = reverse('api:subsonic-search3')
|
||||
assert url.endswith('search3') is True
|
||||
artist = factories['music.Artist'](name='testvalue')
|
||||
factories['music.Artist'](name='nope')
|
||||
album = factories['music.Album'](title='testvalue')
|
||||
factories['music.Album'](title='nope')
|
||||
track = factories['music.Track'](title='testvalue')
|
||||
factories['music.Track'](title='nope')
|
||||
|
||||
response = logged_in_api_client.get(url, {'f': f, 'query': 'testval'})
|
||||
|
||||
artist_qs = music_models.Artist.objects.with_albums_count().filter(
|
||||
pk=artist.pk).values('_albums_count', 'id', 'name')
|
||||
assert response.status_code == 200
|
||||
assert response.data == {
|
||||
'searchResult3': {
|
||||
'artist': [serializers.get_artist_data(a) for a in artist_qs],
|
||||
'album': serializers.get_album_list2_data([album]),
|
||||
'song': serializers.get_song_list_data([track]),
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue