Verified Commit e31099ef authored by Agate's avatar Agate 💬

See #75 more subsonic api endpoints (star, unstar, search...)

parent 40cde0cd
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
]
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)
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
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]),
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment