Merge branch '224-music-api' into 'develop'
Resolve "Better API for artists/album/tracks" Closes #224 and #226 See merge request funkwhale/funkwhale!205
This commit is contained in:
commit
54008aa37c
|
@ -3,7 +3,6 @@ from django.conf import settings
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from funkwhale_api.activity import serializers as activity_serializers
|
from funkwhale_api.activity import serializers as activity_serializers
|
||||||
from funkwhale_api.music.serializers import TrackSerializerNested
|
|
||||||
from funkwhale_api.music.serializers import TrackActivitySerializer
|
from funkwhale_api.music.serializers import TrackActivitySerializer
|
||||||
from funkwhale_api.users.serializers import UserActivitySerializer
|
from funkwhale_api.users.serializers import UserActivitySerializer
|
||||||
|
|
||||||
|
@ -35,7 +34,6 @@ class TrackFavoriteActivitySerializer(activity_serializers.ModelSerializer):
|
||||||
|
|
||||||
|
|
||||||
class UserTrackFavoriteSerializer(serializers.ModelSerializer):
|
class UserTrackFavoriteSerializer(serializers.ModelSerializer):
|
||||||
# track = TrackSerializerNested(read_only=True)
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.TrackFavorite
|
model = models.TrackFavorite
|
||||||
fields = ('id', 'track', 'creation_date')
|
fields = ('id', 'track', 'creation_date')
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from funkwhale_api.activity import serializers as activity_serializers
|
from funkwhale_api.activity import serializers as activity_serializers
|
||||||
from funkwhale_api.music.serializers import TrackSerializerNested
|
|
||||||
from funkwhale_api.music.serializers import TrackActivitySerializer
|
from funkwhale_api.music.serializers import TrackActivitySerializer
|
||||||
from funkwhale_api.users.serializers import UserActivitySerializer
|
from funkwhale_api.users.serializers import UserActivitySerializer
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,6 @@ from rest_framework.decorators import detail_route
|
||||||
|
|
||||||
from funkwhale_api.activity import record
|
from funkwhale_api.activity import record
|
||||||
from funkwhale_api.common.permissions import ConditionalAuthentication
|
from funkwhale_api.common.permissions import ConditionalAuthentication
|
||||||
from funkwhale_api.music.serializers import TrackSerializerNested
|
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
from . import serializers
|
from . import serializers
|
||||||
|
|
|
@ -32,6 +32,33 @@ class ArtistFilter(ListenableMixin):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TrackFilter(filters.FilterSet):
|
||||||
|
q = fields.SearchFilter(search_fields=[
|
||||||
|
'title',
|
||||||
|
'album__title',
|
||||||
|
'artist__name',
|
||||||
|
])
|
||||||
|
listenable = filters.BooleanFilter(name='_', method='filter_listenable')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Track
|
||||||
|
fields = {
|
||||||
|
'title': ['exact', 'iexact', 'startswith', 'icontains'],
|
||||||
|
'listenable': ['exact'],
|
||||||
|
'artist': ['exact'],
|
||||||
|
'album': ['exact'],
|
||||||
|
}
|
||||||
|
|
||||||
|
def filter_listenable(self, queryset, name, value):
|
||||||
|
queryset = queryset.annotate(
|
||||||
|
files_count=Count('files')
|
||||||
|
)
|
||||||
|
if value:
|
||||||
|
return queryset.filter(files_count__gt=0)
|
||||||
|
else:
|
||||||
|
return queryset.filter(files_count=0)
|
||||||
|
|
||||||
|
|
||||||
class ImportBatchFilter(filters.FilterSet):
|
class ImportBatchFilter(filters.FilterSet):
|
||||||
q = fields.SearchFilter(search_fields=[
|
q = fields.SearchFilter(search_fields=[
|
||||||
'submitted_by__username',
|
'submitted_by__username',
|
||||||
|
@ -67,7 +94,12 @@ class ImportJobFilter(filters.FilterSet):
|
||||||
|
|
||||||
class AlbumFilter(ListenableMixin):
|
class AlbumFilter(ListenableMixin):
|
||||||
listenable = filters.BooleanFilter(name='_', method='filter_listenable')
|
listenable = filters.BooleanFilter(name='_', method='filter_listenable')
|
||||||
|
q = fields.SearchFilter(search_fields=[
|
||||||
|
'title',
|
||||||
|
'artist__name'
|
||||||
|
'source',
|
||||||
|
])
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Album
|
model = models.Album
|
||||||
fields = ['listenable']
|
fields = ['listenable', 'q', 'artist']
|
||||||
|
|
|
@ -80,6 +80,12 @@ class ArtistQuerySet(models.QuerySet):
|
||||||
def with_albums_count(self):
|
def with_albums_count(self):
|
||||||
return self.annotate(_albums_count=models.Count('albums'))
|
return self.annotate(_albums_count=models.Count('albums'))
|
||||||
|
|
||||||
|
def with_albums(self):
|
||||||
|
return self.prefetch_related(
|
||||||
|
models.Prefetch(
|
||||||
|
'albums', queryset=Album.objects.with_tracks_count())
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Artist(APIModelMixin):
|
class Artist(APIModelMixin):
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
|
@ -313,11 +319,8 @@ class Lyrics(models.Model):
|
||||||
class TrackQuerySet(models.QuerySet):
|
class TrackQuerySet(models.QuerySet):
|
||||||
def for_nested_serialization(self):
|
def for_nested_serialization(self):
|
||||||
return (self.select_related()
|
return (self.select_related()
|
||||||
.select_related('album__artist')
|
.select_related('album__artist', 'artist')
|
||||||
.prefetch_related(
|
.prefetch_related('files'))
|
||||||
'tags',
|
|
||||||
'files',
|
|
||||||
'artist__albums__tracks__tags'))
|
|
||||||
|
|
||||||
|
|
||||||
class Track(APIModelMixin):
|
class Track(APIModelMixin):
|
||||||
|
|
|
@ -13,24 +13,38 @@ from . import models
|
||||||
from . import tasks
|
from . import tasks
|
||||||
|
|
||||||
|
|
||||||
class TagSerializer(serializers.ModelSerializer):
|
class ArtistAlbumSerializer(serializers.ModelSerializer):
|
||||||
|
tracks_count = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Tag
|
model = models.Album
|
||||||
fields = ('id', 'name', 'slug')
|
fields = (
|
||||||
|
'id',
|
||||||
|
'mbid',
|
||||||
|
'title',
|
||||||
|
'artist',
|
||||||
|
'release_date',
|
||||||
|
'cover',
|
||||||
|
'creation_date',
|
||||||
|
'tracks_count',
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_tracks_count(self, o):
|
||||||
|
return o._tracks_count
|
||||||
|
|
||||||
|
|
||||||
class SimpleArtistSerializer(serializers.ModelSerializer):
|
class ArtistWithAlbumsSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
albums = ArtistAlbumSerializer(many=True, read_only=True)
|
||||||
model = models.Artist
|
|
||||||
fields = ('id', 'mbid', 'name', 'creation_date')
|
|
||||||
|
|
||||||
|
|
||||||
class ArtistSerializer(serializers.ModelSerializer):
|
|
||||||
tags = TagSerializer(many=True, read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Artist
|
model = models.Artist
|
||||||
fields = ('id', 'mbid', 'name', 'tags', 'creation_date')
|
fields = (
|
||||||
|
'id',
|
||||||
|
'mbid',
|
||||||
|
'name',
|
||||||
|
'creation_date',
|
||||||
|
'albums',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TrackFileSerializer(serializers.ModelSerializer):
|
class TrackFileSerializer(serializers.ModelSerializer):
|
||||||
|
@ -62,71 +76,110 @@ class TrackFileSerializer(serializers.ModelSerializer):
|
||||||
return url
|
return url
|
||||||
|
|
||||||
|
|
||||||
class SimpleAlbumSerializer(serializers.ModelSerializer):
|
class AlbumTrackSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = models.Album
|
|
||||||
fields = ('id', 'mbid', 'title', 'release_date', 'cover')
|
|
||||||
|
|
||||||
|
|
||||||
class AlbumSerializer(serializers.ModelSerializer):
|
|
||||||
tags = TagSerializer(many=True, read_only=True)
|
|
||||||
class Meta:
|
|
||||||
model = models.Album
|
|
||||||
fields = ('id', 'mbid', 'title', 'cover', 'release_date', 'tags')
|
|
||||||
|
|
||||||
|
|
||||||
class LyricsMixin(serializers.ModelSerializer):
|
|
||||||
lyrics = serializers.SerializerMethodField()
|
|
||||||
|
|
||||||
def get_lyrics(self, obj):
|
|
||||||
return obj.get_lyrics_url()
|
|
||||||
|
|
||||||
|
|
||||||
class TrackSerializer(LyricsMixin):
|
|
||||||
files = TrackFileSerializer(many=True, read_only=True)
|
files = TrackFileSerializer(many=True, read_only=True)
|
||||||
tags = TagSerializer(many=True, read_only=True)
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Track
|
model = models.Track
|
||||||
fields = (
|
fields = (
|
||||||
'id',
|
'id',
|
||||||
'mbid',
|
'mbid',
|
||||||
'title',
|
'title',
|
||||||
|
'album',
|
||||||
'artist',
|
'artist',
|
||||||
|
'creation_date',
|
||||||
'files',
|
'files',
|
||||||
'tags',
|
|
||||||
'position',
|
'position',
|
||||||
'lyrics')
|
)
|
||||||
|
|
||||||
|
|
||||||
class TrackSerializerNested(LyricsMixin):
|
class ArtistSimpleSerializer(serializers.ModelSerializer):
|
||||||
artist = ArtistSerializer()
|
|
||||||
files = TrackFileSerializer(many=True, read_only=True)
|
|
||||||
album = SimpleAlbumSerializer(read_only=True)
|
|
||||||
tags = TagSerializer(many=True, read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Track
|
model = models.Artist
|
||||||
fields = ('id', 'mbid', 'title', 'artist', 'files', 'album', 'tags', 'lyrics')
|
fields = (
|
||||||
|
'id',
|
||||||
|
'mbid',
|
||||||
|
'name',
|
||||||
|
'creation_date',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AlbumSerializerNested(serializers.ModelSerializer):
|
class AlbumSerializer(serializers.ModelSerializer):
|
||||||
tracks = TrackSerializer(many=True, read_only=True)
|
tracks = serializers.SerializerMethodField()
|
||||||
artist = SimpleArtistSerializer()
|
artist = ArtistSimpleSerializer(read_only=True)
|
||||||
tags = TagSerializer(many=True, read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Album
|
model = models.Album
|
||||||
fields = ('id', 'mbid', 'title', 'cover', 'artist', 'release_date', 'tracks', 'tags')
|
fields = (
|
||||||
|
'id',
|
||||||
|
'mbid',
|
||||||
|
'title',
|
||||||
|
'artist',
|
||||||
|
'tracks',
|
||||||
|
'release_date',
|
||||||
|
'cover',
|
||||||
|
'creation_date',
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_tracks(self, o):
|
||||||
|
ordered_tracks = sorted(
|
||||||
|
o.tracks.all(),
|
||||||
|
key=lambda v: (v.position, v.title) if v.position else (99999, v.title)
|
||||||
|
)
|
||||||
|
return AlbumTrackSerializer(ordered_tracks, many=True).data
|
||||||
|
|
||||||
|
|
||||||
class ArtistSerializerNested(serializers.ModelSerializer):
|
class TrackAlbumSerializer(serializers.ModelSerializer):
|
||||||
albums = AlbumSerializerNested(many=True, read_only=True)
|
artist = ArtistSimpleSerializer(read_only=True)
|
||||||
tags = TagSerializer(many=True, read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Artist
|
model = models.Album
|
||||||
fields = ('id', 'mbid', 'name', 'albums', 'tags')
|
fields = (
|
||||||
|
'id',
|
||||||
|
'mbid',
|
||||||
|
'title',
|
||||||
|
'artist',
|
||||||
|
'release_date',
|
||||||
|
'cover',
|
||||||
|
'creation_date',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TrackSerializer(serializers.ModelSerializer):
|
||||||
|
files = TrackFileSerializer(many=True, read_only=True)
|
||||||
|
artist = ArtistSimpleSerializer(read_only=True)
|
||||||
|
album = TrackAlbumSerializer(read_only=True)
|
||||||
|
lyrics = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Track
|
||||||
|
fields = (
|
||||||
|
'id',
|
||||||
|
'mbid',
|
||||||
|
'title',
|
||||||
|
'album',
|
||||||
|
'artist',
|
||||||
|
'creation_date',
|
||||||
|
'files',
|
||||||
|
'position',
|
||||||
|
'lyrics',
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_lyrics(self, obj):
|
||||||
|
return obj.get_lyrics_url()
|
||||||
|
|
||||||
|
|
||||||
|
class TagSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Tag
|
||||||
|
fields = ('id', 'name', 'slug')
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleAlbumSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Album
|
||||||
|
fields = ('id', 'mbid', 'title', 'release_date', 'cover')
|
||||||
|
|
||||||
|
|
||||||
class LyricsSerializer(serializers.ModelSerializer):
|
class LyricsSerializer(serializers.ModelSerializer):
|
||||||
|
|
|
@ -46,17 +46,6 @@ from . import utils
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class SearchMixin(object):
|
|
||||||
search_fields = []
|
|
||||||
|
|
||||||
@list_route(methods=['get'])
|
|
||||||
def search(self, request, *args, **kwargs):
|
|
||||||
query = utils.get_query(request.GET['query'], self.search_fields)
|
|
||||||
queryset = self.get_queryset().filter(query)
|
|
||||||
serializer = self.serializer_class(queryset, many=True)
|
|
||||||
return Response(serializer.data)
|
|
||||||
|
|
||||||
|
|
||||||
class TagViewSetMixin(object):
|
class TagViewSetMixin(object):
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
@ -67,31 +56,25 @@ class TagViewSetMixin(object):
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
class ArtistViewSet(SearchMixin, viewsets.ReadOnlyModelViewSet):
|
class ArtistViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
queryset = (
|
queryset = models.Artist.objects.with_albums()
|
||||||
models.Artist.objects.all()
|
serializer_class = serializers.ArtistWithAlbumsSerializer
|
||||||
.prefetch_related(
|
|
||||||
'albums__tracks__files',
|
|
||||||
'albums__tracks__artist',
|
|
||||||
'albums__tracks__tags'))
|
|
||||||
serializer_class = serializers.ArtistSerializerNested
|
|
||||||
permission_classes = [ConditionalAuthentication]
|
permission_classes = [ConditionalAuthentication]
|
||||||
search_fields = ['name__unaccent']
|
|
||||||
filter_class = filters.ArtistFilter
|
filter_class = filters.ArtistFilter
|
||||||
ordering_fields = ('id', 'name', 'creation_date')
|
ordering_fields = ('id', 'name', 'creation_date')
|
||||||
|
|
||||||
|
|
||||||
class AlbumViewSet(SearchMixin, viewsets.ReadOnlyModelViewSet):
|
class AlbumViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
queryset = (
|
queryset = (
|
||||||
models.Album.objects.all()
|
models.Album.objects.all()
|
||||||
.order_by('-creation_date')
|
.order_by('artist', 'release_date')
|
||||||
.select_related()
|
.select_related()
|
||||||
.prefetch_related('tracks__tags',
|
.prefetch_related(
|
||||||
'tracks__files'))
|
'tracks__artist',
|
||||||
serializer_class = serializers.AlbumSerializerNested
|
'tracks__files'))
|
||||||
|
serializer_class = serializers.AlbumSerializer
|
||||||
permission_classes = [ConditionalAuthentication]
|
permission_classes = [ConditionalAuthentication]
|
||||||
search_fields = ['title__unaccent']
|
ordering_fields = ('creation_date', 'release_date', 'title')
|
||||||
ordering_fields = ('creation_date',)
|
|
||||||
filter_class = filters.AlbumFilter
|
filter_class = filters.AlbumFilter
|
||||||
|
|
||||||
|
|
||||||
|
@ -160,19 +143,20 @@ class ImportJobViewSet(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class TrackViewSet(
|
class TrackViewSet(TagViewSetMixin, viewsets.ReadOnlyModelViewSet):
|
||||||
TagViewSetMixin, SearchMixin, viewsets.ReadOnlyModelViewSet):
|
|
||||||
"""
|
"""
|
||||||
A simple ViewSet for viewing and editing accounts.
|
A simple ViewSet for viewing and editing accounts.
|
||||||
"""
|
"""
|
||||||
queryset = (models.Track.objects.all().for_nested_serialization())
|
queryset = (models.Track.objects.all().for_nested_serialization())
|
||||||
serializer_class = serializers.TrackSerializerNested
|
serializer_class = serializers.TrackSerializer
|
||||||
permission_classes = [ConditionalAuthentication]
|
permission_classes = [ConditionalAuthentication]
|
||||||
search_fields = ['title', 'artist__name']
|
filter_class = filters.TrackFilter
|
||||||
ordering_fields = (
|
ordering_fields = (
|
||||||
'creation_date',
|
'creation_date',
|
||||||
'title__unaccent',
|
'title__unaccent',
|
||||||
'album__title__unaccent',
|
'album__title__unaccent',
|
||||||
|
'album__release_date',
|
||||||
|
'position',
|
||||||
'artist__name__unaccent',
|
'artist__name__unaccent',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -370,10 +354,10 @@ class Search(views.APIView):
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
query = request.GET['query']
|
query = request.GET['query']
|
||||||
results = {
|
results = {
|
||||||
'tags': serializers.TagSerializer(self.get_tags(query), many=True).data,
|
# 'tags': serializers.TagSerializer(self.get_tags(query), many=True).data,
|
||||||
'artists': serializers.ArtistSerializerNested(self.get_artists(query), many=True).data,
|
'artists': serializers.ArtistWithAlbumsSerializer(self.get_artists(query), many=True).data,
|
||||||
'tracks': serializers.TrackSerializerNested(self.get_tracks(query), many=True).data,
|
'tracks': serializers.TrackSerializer(self.get_tracks(query), many=True).data,
|
||||||
'albums': serializers.AlbumSerializerNested(self.get_albums(query), many=True).data,
|
'albums': serializers.AlbumSerializer(self.get_albums(query), many=True).data,
|
||||||
}
|
}
|
||||||
return Response(results, status=200)
|
return Response(results, status=200)
|
||||||
|
|
||||||
|
@ -387,14 +371,10 @@ class Search(views.APIView):
|
||||||
return (
|
return (
|
||||||
models.Track.objects.all()
|
models.Track.objects.all()
|
||||||
.filter(query_obj)
|
.filter(query_obj)
|
||||||
.select_related('album__artist')
|
.select_related('artist', 'album__artist')
|
||||||
.prefetch_related(
|
.prefetch_related('files')
|
||||||
'tags',
|
|
||||||
'artist__albums__tracks__tags',
|
|
||||||
'files')
|
|
||||||
)[:self.max_results]
|
)[:self.max_results]
|
||||||
|
|
||||||
|
|
||||||
def get_albums(self, query):
|
def get_albums(self, query):
|
||||||
search_fields = [
|
search_fields = [
|
||||||
'mbid',
|
'mbid',
|
||||||
|
@ -406,27 +386,19 @@ class Search(views.APIView):
|
||||||
.filter(query_obj)
|
.filter(query_obj)
|
||||||
.select_related()
|
.select_related()
|
||||||
.prefetch_related(
|
.prefetch_related(
|
||||||
'tracks__tags',
|
|
||||||
'tracks__files',
|
'tracks__files',
|
||||||
)
|
)
|
||||||
)[:self.max_results]
|
)[:self.max_results]
|
||||||
|
|
||||||
|
|
||||||
def get_artists(self, query):
|
def get_artists(self, query):
|
||||||
search_fields = ['mbid', 'name__unaccent']
|
search_fields = ['mbid', 'name__unaccent']
|
||||||
query_obj = utils.get_query(query, search_fields)
|
query_obj = utils.get_query(query, search_fields)
|
||||||
return (
|
return (
|
||||||
models.Artist.objects.all()
|
models.Artist.objects.all()
|
||||||
.filter(query_obj)
|
.filter(query_obj)
|
||||||
.select_related()
|
.with_albums()
|
||||||
.prefetch_related(
|
|
||||||
'albums__tracks__tags',
|
|
||||||
'albums__tracks__files',
|
|
||||||
)
|
|
||||||
|
|
||||||
)[:self.max_results]
|
)[:self.max_results]
|
||||||
|
|
||||||
|
|
||||||
def get_tags(self, query):
|
def get_tags(self, query):
|
||||||
search_fields = ['slug', 'name__unaccent']
|
search_fields = ['slug', 'name__unaccent']
|
||||||
query_obj = utils.get_query(query, search_fields)
|
query_obj = utils.get_query(query, search_fields)
|
||||||
|
|
|
@ -5,13 +5,13 @@ from taggit.models import Tag
|
||||||
|
|
||||||
from funkwhale_api.common import preferences
|
from funkwhale_api.common import preferences
|
||||||
from funkwhale_api.music.models import Track
|
from funkwhale_api.music.models import Track
|
||||||
from funkwhale_api.music.serializers import TrackSerializerNested
|
from funkwhale_api.music.serializers import TrackSerializer
|
||||||
from funkwhale_api.users.serializers import UserBasicSerializer
|
from funkwhale_api.users.serializers import UserBasicSerializer
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
class PlaylistTrackSerializer(serializers.ModelSerializer):
|
class PlaylistTrackSerializer(serializers.ModelSerializer):
|
||||||
track = TrackSerializerNested()
|
track = TrackSerializer()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.PlaylistTrack
|
model = models.PlaylistTrack
|
||||||
|
|
|
@ -144,8 +144,8 @@ class ArtistFilter(RadioFilter):
|
||||||
'name': 'ids',
|
'name': 'ids',
|
||||||
'type': 'list',
|
'type': 'list',
|
||||||
'subtype': 'number',
|
'subtype': 'number',
|
||||||
'autocomplete': reverse_lazy('api:v1:artists-search'),
|
'autocomplete': reverse_lazy('api:v1:artists-list'),
|
||||||
'autocomplete_qs': 'query={query}',
|
'autocomplete_qs': 'q={query}',
|
||||||
'autocomplete_fields': {'name': 'name', 'value': 'id'},
|
'autocomplete_fields': {'name': 'name', 'value': 'id'},
|
||||||
'label': 'Artist',
|
'label': 'Artist',
|
||||||
'placeholder': 'Select artists'
|
'placeholder': 'Select artists'
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from funkwhale_api.music.serializers import TrackSerializerNested
|
from funkwhale_api.music.serializers import TrackSerializer
|
||||||
from funkwhale_api.users.serializers import UserBasicSerializer
|
from funkwhale_api.users.serializers import UserBasicSerializer
|
||||||
|
|
||||||
from . import filters
|
from . import filters
|
||||||
|
@ -46,7 +46,7 @@ class RadioSessionTrackSerializerCreate(serializers.ModelSerializer):
|
||||||
|
|
||||||
|
|
||||||
class RadioSessionTrackSerializer(serializers.ModelSerializer):
|
class RadioSessionTrackSerializer(serializers.ModelSerializer):
|
||||||
track = TrackSerializerNested()
|
track = TrackSerializer()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.RadioSessionTrack
|
model = models.RadioSessionTrack
|
||||||
|
|
|
@ -7,7 +7,7 @@ from rest_framework import status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.decorators import detail_route, list_route
|
from rest_framework.decorators import detail_route, list_route
|
||||||
|
|
||||||
from funkwhale_api.music.serializers import TrackSerializerNested
|
from funkwhale_api.music.serializers import TrackSerializer
|
||||||
from funkwhale_api.common.permissions import ConditionalAuthentication
|
from funkwhale_api.common.permissions import ConditionalAuthentication
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
@ -49,7 +49,7 @@ class RadioViewSet(
|
||||||
|
|
||||||
page = self.paginate_queryset(tracks)
|
page = self.paginate_queryset(tracks)
|
||||||
if page is not None:
|
if page is not None:
|
||||||
serializer = TrackSerializerNested(page, many=True)
|
serializer = TrackSerializer(page, many=True)
|
||||||
return self.get_paginated_response(serializer.data)
|
return self.get_paginated_response(serializer.data)
|
||||||
|
|
||||||
@list_route(methods=['get'])
|
@list_route(methods=['get'])
|
||||||
|
@ -72,7 +72,7 @@ class RadioViewSet(
|
||||||
results = filters.test(f)
|
results = filters.test(f)
|
||||||
if results['candidates']['sample']:
|
if results['candidates']['sample']:
|
||||||
qs = results['candidates']['sample'].for_nested_serialization()
|
qs = results['candidates']['sample'].for_nested_serialization()
|
||||||
results['candidates']['sample'] = TrackSerializerNested(
|
results['candidates']['sample'] = TrackSerializer(
|
||||||
qs, many=True).data
|
qs, many=True).data
|
||||||
data['filters'].append(results)
|
data['filters'].append(results)
|
||||||
|
|
||||||
|
|
|
@ -3,15 +3,12 @@ from rest_framework import status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.decorators import detail_route
|
from rest_framework.decorators import detail_route
|
||||||
|
|
||||||
from funkwhale_api.music.views import SearchMixin
|
|
||||||
|
|
||||||
from . import filters
|
from . import filters
|
||||||
from . import models
|
from . import models
|
||||||
from . import serializers
|
from . import serializers
|
||||||
|
|
||||||
|
|
||||||
class ImportRequestViewSet(
|
class ImportRequestViewSet(
|
||||||
SearchMixin,
|
|
||||||
mixins.CreateModelMixin,
|
mixins.CreateModelMixin,
|
||||||
mixins.RetrieveModelMixin,
|
mixins.RetrieveModelMixin,
|
||||||
mixins.ListModelMixin,
|
mixins.ListModelMixin,
|
||||||
|
@ -22,7 +19,6 @@ class ImportRequestViewSet(
|
||||||
models.ImportRequest.objects.all()
|
models.ImportRequest.objects.all()
|
||||||
.select_related()
|
.select_related()
|
||||||
.order_by('-creation_date'))
|
.order_by('-creation_date'))
|
||||||
search_fields = ['artist_name', 'album_name', 'comment']
|
|
||||||
filter_class = filters.ImportRequestFilter
|
filter_class = filters.ImportRequestFilter
|
||||||
ordering_fields = ('id', 'artist_name', 'creation_date', 'status')
|
ordering_fields = ('id', 'artist_name', 'creation_date', 'status')
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import datetime
|
||||||
import factory
|
import factory
|
||||||
import pytest
|
import pytest
|
||||||
import requests_mock
|
import requests_mock
|
||||||
|
@ -10,6 +11,7 @@ from django.test import client
|
||||||
|
|
||||||
from dynamic_preferences.registries import global_preferences_registry
|
from dynamic_preferences.registries import global_preferences_registry
|
||||||
|
|
||||||
|
from rest_framework import fields as rest_fields
|
||||||
from rest_framework.test import APIClient
|
from rest_framework.test import APIClient
|
||||||
from rest_framework.test import APIRequestFactory
|
from rest_framework.test import APIRequestFactory
|
||||||
|
|
||||||
|
@ -233,3 +235,16 @@ def assert_user_permission():
|
||||||
assert HasUserPermission in view.permission_classes
|
assert HasUserPermission in view.permission_classes
|
||||||
assert set(view.required_permissions) == set(permissions)
|
assert set(view.required_permissions) == set(permissions)
|
||||||
return inner
|
return inner
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def to_api_date():
|
||||||
|
def inner(value):
|
||||||
|
if isinstance(value, datetime.datetime):
|
||||||
|
f = rest_fields.DateTimeField()
|
||||||
|
return f.to_representation(value)
|
||||||
|
if isinstance(value, datetime.date):
|
||||||
|
f = rest_fields.DateField()
|
||||||
|
return f.to_representation(value)
|
||||||
|
raise ValueError('Invalid value: {}'.format(value))
|
||||||
|
return inner
|
||||||
|
|
|
@ -223,41 +223,6 @@ def test_user_can_create_import_job_with_file(
|
||||||
import_job_id=job.pk)
|
import_job_id=job.pk)
|
||||||
|
|
||||||
|
|
||||||
def test_can_search_artist(factories, logged_in_client):
|
|
||||||
artist1 = factories['music.Artist']()
|
|
||||||
artist2 = factories['music.Artist']()
|
|
||||||
expected = [serializers.ArtistSerializerNested(artist1).data]
|
|
||||||
url = reverse('api:v1:artists-search')
|
|
||||||
response = logged_in_client.get(url, {'query': artist1.name})
|
|
||||||
assert response.data == expected
|
|
||||||
|
|
||||||
|
|
||||||
def test_can_search_artist_by_name_start(factories, logged_in_client):
|
|
||||||
artist1 = factories['music.Artist'](name='alpha')
|
|
||||||
artist2 = factories['music.Artist'](name='beta')
|
|
||||||
expected = {
|
|
||||||
'next': None,
|
|
||||||
'previous': None,
|
|
||||||
'count': 1,
|
|
||||||
'results': [serializers.ArtistSerializerNested(artist1).data]
|
|
||||||
}
|
|
||||||
url = reverse('api:v1:artists-list')
|
|
||||||
response = logged_in_client.get(url, {'name__startswith': 'a'})
|
|
||||||
|
|
||||||
assert expected == response.data
|
|
||||||
|
|
||||||
|
|
||||||
def test_can_search_tracks(factories, logged_in_client):
|
|
||||||
track1 = factories['music.Track'](title="test track 1")
|
|
||||||
track2 = factories['music.Track']()
|
|
||||||
query = 'test track 1'
|
|
||||||
expected = [serializers.TrackSerializerNested(track1).data]
|
|
||||||
url = reverse('api:v1:tracks-search')
|
|
||||||
response = logged_in_client.get(url, {'query': query})
|
|
||||||
|
|
||||||
assert expected == response.data
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('route,method', [
|
@pytest.mark.parametrize('route,method', [
|
||||||
('api:v1:tags-list', 'get'),
|
('api:v1:tags-list', 'get'),
|
||||||
('api:v1:tracks-list', 'get'),
|
('api:v1:tracks-list', 'get'),
|
||||||
|
|
|
@ -0,0 +1,121 @@
|
||||||
|
from funkwhale_api.music import serializers
|
||||||
|
|
||||||
|
|
||||||
|
def test_artist_album_serializer(factories, to_api_date):
|
||||||
|
track = factories['music.Track']()
|
||||||
|
album = track.album
|
||||||
|
album = album.__class__.objects.with_tracks_count().get(pk=album.pk)
|
||||||
|
expected = {
|
||||||
|
'id': album.id,
|
||||||
|
'mbid': str(album.mbid),
|
||||||
|
'title': album.title,
|
||||||
|
'artist': album.artist.id,
|
||||||
|
'creation_date': to_api_date(album.creation_date),
|
||||||
|
'tracks_count': 1,
|
||||||
|
'cover': album.cover.url,
|
||||||
|
'release_date': to_api_date(album.release_date),
|
||||||
|
}
|
||||||
|
serializer = serializers.ArtistAlbumSerializer(album)
|
||||||
|
|
||||||
|
assert serializer.data == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_artist_with_albums_serializer(factories, to_api_date):
|
||||||
|
track = factories['music.Track']()
|
||||||
|
artist = track.artist
|
||||||
|
artist = artist.__class__.objects.with_albums().get(pk=artist.pk)
|
||||||
|
album = list(artist.albums.all())[0]
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
'id': artist.id,
|
||||||
|
'mbid': str(artist.mbid),
|
||||||
|
'name': artist.name,
|
||||||
|
'creation_date': to_api_date(artist.creation_date),
|
||||||
|
'albums': [
|
||||||
|
serializers.ArtistAlbumSerializer(album).data
|
||||||
|
]
|
||||||
|
}
|
||||||
|
serializer = serializers.ArtistWithAlbumsSerializer(artist)
|
||||||
|
assert serializer.data == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_album_track_serializer(factories, to_api_date):
|
||||||
|
tf = factories['music.TrackFile']()
|
||||||
|
track = tf.track
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
'id': track.id,
|
||||||
|
'artist': track.artist.id,
|
||||||
|
'album': track.album.id,
|
||||||
|
'mbid': str(track.mbid),
|
||||||
|
'title': track.title,
|
||||||
|
'position': track.position,
|
||||||
|
'creation_date': to_api_date(track.creation_date),
|
||||||
|
'files': [
|
||||||
|
serializers.TrackFileSerializer(tf).data
|
||||||
|
]
|
||||||
|
}
|
||||||
|
serializer = serializers.AlbumTrackSerializer(track)
|
||||||
|
assert serializer.data == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_track_file_serializer(factories, to_api_date):
|
||||||
|
tf = factories['music.TrackFile']()
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
'id': tf.id,
|
||||||
|
'path': tf.path,
|
||||||
|
'source': tf.source,
|
||||||
|
'filename': tf.filename,
|
||||||
|
'mimetype': tf.mimetype,
|
||||||
|
'track': tf.track.pk,
|
||||||
|
'duration': tf.duration,
|
||||||
|
'mimetype': tf.mimetype,
|
||||||
|
'bitrate': tf.bitrate,
|
||||||
|
'size': tf.size,
|
||||||
|
}
|
||||||
|
serializer = serializers.TrackFileSerializer(tf)
|
||||||
|
assert serializer.data == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_album_serializer(factories, to_api_date):
|
||||||
|
track1 = factories['music.Track'](position=2)
|
||||||
|
track2 = factories['music.Track'](position=1, album=track1.album)
|
||||||
|
album = track1.album
|
||||||
|
expected = {
|
||||||
|
'id': album.id,
|
||||||
|
'mbid': str(album.mbid),
|
||||||
|
'title': album.title,
|
||||||
|
'artist': serializers.ArtistSimpleSerializer(album.artist).data,
|
||||||
|
'creation_date': to_api_date(album.creation_date),
|
||||||
|
'cover': album.cover.url,
|
||||||
|
'release_date': to_api_date(album.release_date),
|
||||||
|
'tracks': serializers.AlbumTrackSerializer(
|
||||||
|
[track2, track1],
|
||||||
|
many=True
|
||||||
|
).data
|
||||||
|
}
|
||||||
|
serializer = serializers.AlbumSerializer(album)
|
||||||
|
|
||||||
|
assert serializer.data == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_track_serializer(factories, to_api_date):
|
||||||
|
tf = factories['music.TrackFile']()
|
||||||
|
track = tf.track
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
'id': track.id,
|
||||||
|
'artist': serializers.ArtistSimpleSerializer(track.artist).data,
|
||||||
|
'album': serializers.TrackAlbumSerializer(track.album).data,
|
||||||
|
'mbid': str(track.mbid),
|
||||||
|
'title': track.title,
|
||||||
|
'position': track.position,
|
||||||
|
'creation_date': to_api_date(track.creation_date),
|
||||||
|
'lyrics': track.get_lyrics_url(),
|
||||||
|
'files': [
|
||||||
|
serializers.TrackFileSerializer(tf).data
|
||||||
|
]
|
||||||
|
}
|
||||||
|
serializer = serializers.TrackSerializer(track)
|
||||||
|
assert serializer.data == expected
|
|
@ -4,6 +4,7 @@ import pytest
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from funkwhale_api.music import serializers
|
||||||
from funkwhale_api.music import views
|
from funkwhale_api.music import views
|
||||||
from funkwhale_api.federation import actors
|
from funkwhale_api.federation import actors
|
||||||
|
|
||||||
|
@ -16,6 +17,65 @@ def test_permissions(assert_user_permission, view, permissions):
|
||||||
assert_user_permission(view, permissions)
|
assert_user_permission(view, permissions)
|
||||||
|
|
||||||
|
|
||||||
|
def test_artist_list_serializer(api_request, factories, logged_in_api_client):
|
||||||
|
track = factories['music.Track']()
|
||||||
|
artist = track.artist
|
||||||
|
request = api_request.get('/')
|
||||||
|
qs = artist.__class__.objects.with_albums()
|
||||||
|
serializer = serializers.ArtistWithAlbumsSerializer(
|
||||||
|
qs, many=True, context={'request': request})
|
||||||
|
expected = {
|
||||||
|
'count': 1,
|
||||||
|
'next': None,
|
||||||
|
'previous': None,
|
||||||
|
'results': serializer.data
|
||||||
|
}
|
||||||
|
url = reverse('api:v1:artists-list')
|
||||||
|
response = logged_in_api_client.get(url)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.data == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_album_list_serializer(api_request, factories, logged_in_api_client):
|
||||||
|
track = factories['music.Track']()
|
||||||
|
album = track.album
|
||||||
|
request = api_request.get('/')
|
||||||
|
qs = album.__class__.objects.all()
|
||||||
|
serializer = serializers.AlbumSerializer(
|
||||||
|
qs, many=True, context={'request': request})
|
||||||
|
expected = {
|
||||||
|
'count': 1,
|
||||||
|
'next': None,
|
||||||
|
'previous': None,
|
||||||
|
'results': serializer.data
|
||||||
|
}
|
||||||
|
url = reverse('api:v1:albums-list')
|
||||||
|
response = logged_in_api_client.get(url)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.data == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_track_list_serializer(api_request, factories, logged_in_api_client):
|
||||||
|
track = factories['music.Track']()
|
||||||
|
request = api_request.get('/')
|
||||||
|
qs = track.__class__.objects.all()
|
||||||
|
serializer = serializers.TrackSerializer(
|
||||||
|
qs, many=True, context={'request': request})
|
||||||
|
expected = {
|
||||||
|
'count': 1,
|
||||||
|
'next': None,
|
||||||
|
'previous': None,
|
||||||
|
'results': serializer.data
|
||||||
|
}
|
||||||
|
url = reverse('api:v1:tracks-list')
|
||||||
|
response = logged_in_api_client.get(url)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.data == expected
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('param,expected', [
|
@pytest.mark.parametrize('param,expected', [
|
||||||
('true', 'full'),
|
('true', 'full'),
|
||||||
('false', 'empty'),
|
('false', 'empty'),
|
||||||
|
|
|
@ -3,7 +3,7 @@ import pytest
|
||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from funkwhale_api.music.serializers import TrackSerializerNested
|
from funkwhale_api.music.serializers import TrackSerializer
|
||||||
from funkwhale_api.radios import filters
|
from funkwhale_api.radios import filters
|
||||||
from funkwhale_api.radios import serializers
|
from funkwhale_api.radios import serializers
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ def test_can_validate_config(logged_in_client, factories):
|
||||||
|
|
||||||
expected = {
|
expected = {
|
||||||
'count': candidates.count(),
|
'count': candidates.count(),
|
||||||
'sample': TrackSerializerNested(candidates, many=True).data
|
'sample': TrackSerializer(candidates, many=True).data
|
||||||
}
|
}
|
||||||
assert payload['filters'][0]['candidates'] == expected
|
assert payload['filters'][0]['candidates'] == expected
|
||||||
assert payload['filters'][0]['errors'] == []
|
assert payload['filters'][0]['errors'] == []
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
Retructured music API to increase performance and remove useless endpoints (#224)
|
||||||
|
|
||||||
|
Music API changes
|
||||||
|
^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
This release includes an API break. Even though the API is advertised
|
||||||
|
as unstable, and not documented, here is a brief explanation of the change in
|
||||||
|
case you are using the API in a client or in a script. Summary of the changes:
|
||||||
|
|
||||||
|
- ``/api/v1/artists`` does not includes a list of tracks anymore. It was to heavy
|
||||||
|
to return all of this data all the time. You can get all tracks for an
|
||||||
|
artist using ``/api/v1/tracks?artist=artist_id``
|
||||||
|
- Additionally, ``/api/v1/tracks`` now support an ``album`` filter to filter
|
||||||
|
tracks matching an album
|
||||||
|
- ``/api/v1/artists/search``, ``/api/v1/albums/search`` and ``/api/v1/tracks/search``
|
||||||
|
endpoints are removed. Use ``/api/v1/{artists|albums|tracks}/?q=yourquery``
|
||||||
|
instead. It's also more powerful, since you can combine search with other
|
||||||
|
filters and ordering options.
|
||||||
|
- ``/api/v1/requests/import-requests/search`` endpoint is removed as well.
|
||||||
|
Use ``/api/v1/requests/import-requests/?q=yourquery``
|
||||||
|
instead. It's also more powerful, since you can combine search with other
|
||||||
|
filters and ordering options.
|
||||||
|
|
||||||
|
Of course, the front-end was updated to work with the new API, so this should
|
||||||
|
not impact end-users in any way, apart from slight performance gains.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
The API is still not stable and may evolve again in the future. API freeze
|
||||||
|
will come at a later point.
|
|
@ -0,0 +1 @@
|
||||||
|
Empty save button in radio builder (#226)
|
2
dev.yml
2
dev.yml
|
@ -130,7 +130,7 @@ services:
|
||||||
ports:
|
ports:
|
||||||
- '8002:8080'
|
- '8002:8080'
|
||||||
volumes:
|
volumes:
|
||||||
- "./api/docs/swagger.yml:/usr/share/nginx/html/swagger.yml"
|
- "./docs/swagger.yml:/usr/share/nginx/html/swagger.yml"
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
internal:
|
internal:
|
||||||
|
|
|
@ -78,7 +78,7 @@ paths:
|
||||||
results:
|
results:
|
||||||
type: "array"
|
type: "array"
|
||||||
items:
|
items:
|
||||||
$ref: "#/definitions/ArtistNested"
|
$ref: "#/definitions/ArtistWithAlbums"
|
||||||
|
|
||||||
properties:
|
properties:
|
||||||
resultsCount:
|
resultsCount:
|
||||||
|
@ -106,7 +106,7 @@ definitions:
|
||||||
creation_date:
|
creation_date:
|
||||||
type: "string"
|
type: "string"
|
||||||
format: "date-time"
|
format: "date-time"
|
||||||
ArtistNested:
|
ArtistWithAlbums:
|
||||||
type: "object"
|
type: "object"
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: "#/definitions/Artist"
|
- $ref: "#/definitions/Artist"
|
||||||
|
@ -115,7 +115,7 @@ definitions:
|
||||||
albums:
|
albums:
|
||||||
type: "array"
|
type: "array"
|
||||||
items:
|
items:
|
||||||
$ref: "#/definitions/AlbumNested"
|
$ref: "#/definitions/ArtistAlbum"
|
||||||
|
|
||||||
Album:
|
Album:
|
||||||
type: "object"
|
type: "object"
|
||||||
|
@ -143,16 +143,16 @@ definitions:
|
||||||
format: "date"
|
format: "date"
|
||||||
example: "2001-01-01"
|
example: "2001-01-01"
|
||||||
|
|
||||||
AlbumNested:
|
ArtistAlbum:
|
||||||
type: "object"
|
type: "object"
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: "#/definitions/Album"
|
- $ref: "#/definitions/Album"
|
||||||
- type: "object"
|
- type: "object"
|
||||||
properties:
|
properties:
|
||||||
tracks:
|
tracks_count:
|
||||||
type: "array"
|
type: "integer"
|
||||||
items:
|
format: "int64"
|
||||||
$ref: "#/definitions/Track"
|
example: 16
|
||||||
|
|
||||||
Track:
|
Track:
|
||||||
type: "object"
|
type: "object"
|
||||||
|
|
|
@ -21,7 +21,6 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import logger from '@/logging'
|
|
||||||
import jQuery from 'jquery'
|
import jQuery from 'jquery'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -30,18 +29,15 @@ export default {
|
||||||
tracks: {type: Array, required: false},
|
tracks: {type: Array, required: false},
|
||||||
track: {type: Object, required: false},
|
track: {type: Object, required: false},
|
||||||
playlist: {type: Object, required: false},
|
playlist: {type: Object, required: false},
|
||||||
discrete: {type: Boolean, default: false}
|
discrete: {type: Boolean, default: false},
|
||||||
|
artist: {type: Number, required: false},
|
||||||
|
album: {type: Number, required: false}
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isLoading: false
|
isLoading: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
|
||||||
if (!this.playlist && !this.track && !this.tracks) {
|
|
||||||
logger.default.error('You have to provide either a track playlist or tracks property')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted () {
|
mounted () {
|
||||||
jQuery(this.$el).find('.ui.dropdown').dropdown()
|
jQuery(this.$el).find('.ui.dropdown').dropdown()
|
||||||
},
|
},
|
||||||
|
@ -62,6 +58,10 @@ export default {
|
||||||
return this.tracks.length > 0
|
return this.tracks.length > 0
|
||||||
} else if (this.playlist) {
|
} else if (this.playlist) {
|
||||||
return true
|
return true
|
||||||
|
} else if (this.artist) {
|
||||||
|
return true
|
||||||
|
} else if (this.album) {
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -81,6 +81,20 @@ export default {
|
||||||
return plt.track
|
return plt.track
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
|
} else if (self.artist) {
|
||||||
|
let params = {
|
||||||
|
params: {'artist': self.artist, 'ordering': 'album__release_date,position'}
|
||||||
|
}
|
||||||
|
axios.get('tracks', params).then((response) => {
|
||||||
|
resolve(response.data.results)
|
||||||
|
})
|
||||||
|
} else if (self.album) {
|
||||||
|
let params = {
|
||||||
|
params: {'album': self.album, 'ordering': 'position'}
|
||||||
|
}
|
||||||
|
axios.get('tracks', params).then((response) => {
|
||||||
|
resolve(response.data.results)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return getTracks.then((tracks) => {
|
return getTracks.then((tracks) => {
|
||||||
|
|
|
@ -18,10 +18,10 @@
|
||||||
<router-link class="discrete link":to="{name: 'library.albums.detail', params: {id: album.id }}">
|
<router-link class="discrete link":to="{name: 'library.albums.detail', params: {id: album.id }}">
|
||||||
<strong>{{ album.title }}</strong>
|
<strong>{{ album.title }}</strong>
|
||||||
</router-link><br />
|
</router-link><br />
|
||||||
{{ album.tracks.length }} tracks
|
{{ album.tracks_count }} tracks
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<play-button class="right floated basic icon" :discrete="true" :tracks="album.tracks"></play-button>
|
<play-button class="right floated basic icon" :discrete="true" :album="album.id"></play-button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -45,7 +45,7 @@
|
||||||
{{ artist.albums.length }}
|
{{ artist.albums.length }}
|
||||||
</i18next>
|
</i18next>
|
||||||
</span>
|
</span>
|
||||||
<play-button class="mini basic orange right floated" :tracks="allTracks">
|
<play-button class="mini basic orange right floated" :artist="artist.id">
|
||||||
<i18next path="Play all"/>
|
<i18next path="Play all"/>
|
||||||
</play-button>
|
</play-button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -74,15 +74,6 @@ export default {
|
||||||
return this.artist.albums
|
return this.artist.albums
|
||||||
}
|
}
|
||||||
return this.artist.albums.slice(0, this.initialAlbums)
|
return this.artist.albums.slice(0, this.initialAlbums)
|
||||||
},
|
|
||||||
allTracks () {
|
|
||||||
let tracks = []
|
|
||||||
this.artist.albums.forEach(album => {
|
|
||||||
album.tracks.forEach(track => {
|
|
||||||
tracks.push(track)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
return tracks
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
<i class="circular inverted users violet icon"></i>
|
<i class="circular inverted users violet icon"></i>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
{{ artist.name }}
|
{{ artist.name }}
|
||||||
<div class="sub header">
|
<div class="sub header" v-if="albums">
|
||||||
{{ $t('{% track_count %} tracks in {% album_count %} albums', {track_count: totalTracks, album_count: albums.length})}}
|
{{ $t('{% track_count %} tracks in {% album_count %} albums', {track_count: totalTracks, album_count: albums.length})}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -18,7 +18,7 @@
|
||||||
<div class="ui hidden divider"></div>
|
<div class="ui hidden divider"></div>
|
||||||
<radio-button type="artist" :object-id="artist.id"></radio-button>
|
<radio-button type="artist" :object-id="artist.id"></radio-button>
|
||||||
</button>
|
</button>
|
||||||
<play-button class="orange" :tracks="allTracks"><i18next path="Play all albums"/></play-button>
|
<play-button class="orange" :artist="artist.id"><i18next path="Play all albums"/></play-button>
|
||||||
|
|
||||||
<a :href="wikipediaUrl" target="_blank" class="ui button">
|
<a :href="wikipediaUrl" target="_blank" class="ui button">
|
||||||
<i class="wikipedia icon"></i>
|
<i class="wikipedia icon"></i>
|
||||||
|
@ -30,10 +30,13 @@
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui vertical stripe segment">
|
<div v-if="isLoadingAlbums" class="ui vertical stripe segment">
|
||||||
|
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="albums" class="ui vertical stripe segment">
|
||||||
<h2><i18next path="Albums by this artist"/></h2>
|
<h2><i18next path="Albums by this artist"/></h2>
|
||||||
<div class="ui stackable doubling three column grid">
|
<div class="ui stackable doubling three column grid">
|
||||||
<div class="column" :key="album.id" v-for="album in sortedAlbums">
|
<div class="column" :key="album.id" v-for="album in albums">
|
||||||
<album-card :mode="'rich'" class="fluid" :album="album"></album-card>
|
<album-card :mode="'rich'" class="fluid" :album="album"></album-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -43,7 +46,6 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import _ from 'lodash'
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import logger from '@/logging'
|
import logger from '@/logging'
|
||||||
import backend from '@/audio/backend'
|
import backend from '@/audio/backend'
|
||||||
|
@ -63,6 +65,7 @@ export default {
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
|
isLoadingAlbums: true,
|
||||||
artist: null,
|
artist: null,
|
||||||
albums: null
|
albums: null
|
||||||
}
|
}
|
||||||
|
@ -78,18 +81,19 @@ export default {
|
||||||
logger.default.debug('Fetching artist "' + this.id + '"')
|
logger.default.debug('Fetching artist "' + this.id + '"')
|
||||||
axios.get(url).then((response) => {
|
axios.get(url).then((response) => {
|
||||||
self.artist = response.data
|
self.artist = response.data
|
||||||
self.albums = JSON.parse(JSON.stringify(self.artist.albums)).map((album) => {
|
|
||||||
return backend.Album.clean(album)
|
|
||||||
})
|
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
|
self.isLoadingAlbums = true
|
||||||
|
axios.get('albums/', {params: {artist: this.id, ordering: '-release_date'}}).then((response) => {
|
||||||
|
self.albums = JSON.parse(JSON.stringify(response.data.results)).map((album) => {
|
||||||
|
return backend.Album.clean(album)
|
||||||
|
})
|
||||||
|
|
||||||
|
self.isLoadingAlbums = false
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
sortedAlbums () {
|
|
||||||
let a = this.albums || []
|
|
||||||
return _.orderBy(a, ['release_date'], ['asc'])
|
|
||||||
},
|
|
||||||
totalTracks () {
|
totalTracks () {
|
||||||
return this.albums.map((album) => {
|
return this.albums.map((album) => {
|
||||||
return album.tracks.length
|
return album.tracks.length
|
||||||
|
|
|
@ -69,7 +69,6 @@ import axios from 'axios'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import $ from 'jquery'
|
import $ from 'jquery'
|
||||||
|
|
||||||
import backend from '@/audio/backend'
|
|
||||||
import logger from '@/logging'
|
import logger from '@/logging'
|
||||||
|
|
||||||
import OrderingMixin from '@/components/mixins/Ordering'
|
import OrderingMixin from '@/components/mixins/Ordering'
|
||||||
|
@ -135,13 +134,6 @@ export default {
|
||||||
logger.default.debug('Fetching artists')
|
logger.default.debug('Fetching artists')
|
||||||
axios.get(url, {params: params}).then((response) => {
|
axios.get(url, {params: params}).then((response) => {
|
||||||
self.result = response.data
|
self.result = response.data
|
||||||
self.result.results.map((artist) => {
|
|
||||||
var albums = JSON.parse(JSON.stringify(artist.albums)).map((album) => {
|
|
||||||
return backend.Album.clean(album)
|
|
||||||
})
|
|
||||||
artist.albums = albums
|
|
||||||
return artist
|
|
||||||
})
|
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
})
|
})
|
||||||
}, 500),
|
}, 500),
|
||||||
|
|
|
@ -30,7 +30,6 @@
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import Search from '@/components/audio/Search'
|
import Search from '@/components/audio/Search'
|
||||||
import backend from '@/audio/backend'
|
|
||||||
import logger from '@/logging'
|
import logger from '@/logging'
|
||||||
import ArtistCard from '@/components/audio/artist/Card'
|
import ArtistCard from '@/components/audio/artist/Card'
|
||||||
import RadioCard from '@/components/radios/Card'
|
import RadioCard from '@/components/radios/Card'
|
||||||
|
@ -66,13 +65,6 @@ export default {
|
||||||
logger.default.time('Loading latest artists')
|
logger.default.time('Loading latest artists')
|
||||||
axios.get(url, {params: params}).then((response) => {
|
axios.get(url, {params: params}).then((response) => {
|
||||||
self.artists = response.data.results
|
self.artists = response.data.results
|
||||||
self.artists.map((artist) => {
|
|
||||||
var albums = JSON.parse(JSON.stringify(artist.albums)).map((album) => {
|
|
||||||
return backend.Album.clean(album)
|
|
||||||
})
|
|
||||||
artist.albums = albums
|
|
||||||
return artist
|
|
||||||
})
|
|
||||||
logger.default.timeEnd('Loading latest artists')
|
logger.default.timeEnd('Loading latest artists')
|
||||||
self.isLoadingArtists = false
|
self.isLoadingArtists = false
|
||||||
})
|
})
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
<input id="public" type="checkbox" v-model="isPublic" />
|
<input id="public" type="checkbox" v-model="isPublic" />
|
||||||
<i18next tag="label" for="public" path="Display publicly"/>
|
<i18next tag="label" for="public" path="Display publicly"/>
|
||||||
</div>
|
</div>
|
||||||
<button :disabled="!canSave" @click="save" class="ui green button"><i18ext path="Save"/></button>
|
<button :disabled="!canSave" @click="save" class="ui green button"><i18next path="Save"/></button>
|
||||||
<radio-button v-if="id" type="custom" :custom-radio-id="id"></radio-button>
|
<radio-button v-if="id" type="custom" :custom-radio-id="id"></radio-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -123,7 +123,7 @@ export default {
|
||||||
if (settings.fields.remoteValues) {
|
if (settings.fields.remoteValues) {
|
||||||
return initialResponse
|
return initialResponse
|
||||||
}
|
}
|
||||||
return {results: initialResponse}
|
return {results: initialResponse.results}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue