kopia lustrzana https://dev.funkwhale.audio/funkwhale/funkwhale
See #224: updated front-end with new music API
rodzic
857fab526d
commit
a8baf8fa67
|
@ -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):
|
||||
q = fields.SearchFilter(search_fields=[
|
||||
'submitted_by__username',
|
||||
|
@ -67,7 +94,12 @@ class ImportJobFilter(filters.FilterSet):
|
|||
|
||||
class AlbumFilter(ListenableMixin):
|
||||
listenable = filters.BooleanFilter(name='_', method='filter_listenable')
|
||||
q = fields.SearchFilter(search_fields=[
|
||||
'title',
|
||||
'artist__name'
|
||||
'source',
|
||||
])
|
||||
|
||||
class Meta:
|
||||
model = models.Album
|
||||
fields = ['listenable']
|
||||
fields = ['listenable', 'q', 'artist']
|
||||
|
|
|
@ -319,11 +319,8 @@ class Lyrics(models.Model):
|
|||
class TrackQuerySet(models.QuerySet):
|
||||
def for_nested_serialization(self):
|
||||
return (self.select_related()
|
||||
.select_related('album__artist')
|
||||
.prefetch_related(
|
||||
'tags',
|
||||
'files',
|
||||
'artist__albums__tracks__tags'))
|
||||
.select_related('album__artist', 'artist')
|
||||
.prefetch_related('files'))
|
||||
|
||||
|
||||
class Track(APIModelMixin):
|
||||
|
|
|
@ -122,7 +122,10 @@ class AlbumSerializer(serializers.ModelSerializer):
|
|||
)
|
||||
|
||||
def get_tracks(self, o):
|
||||
ordered_tracks = sorted(o.tracks.all(), key=lambda v: v.position)
|
||||
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
|
||||
|
||||
|
||||
|
|
|
@ -46,17 +46,6 @@ from . import utils
|
|||
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):
|
||||
|
||||
def get_queryset(self):
|
||||
|
@ -67,26 +56,25 @@ class TagViewSetMixin(object):
|
|||
return queryset
|
||||
|
||||
|
||||
class ArtistViewSet(SearchMixin, viewsets.ReadOnlyModelViewSet):
|
||||
class ArtistViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
queryset = models.Artist.objects.with_albums()
|
||||
serializer_class = serializers.ArtistWithAlbumsSerializer
|
||||
permission_classes = [ConditionalAuthentication]
|
||||
search_fields = ['name__unaccent']
|
||||
filter_class = filters.ArtistFilter
|
||||
ordering_fields = ('id', 'name', 'creation_date')
|
||||
|
||||
|
||||
class AlbumViewSet(SearchMixin, viewsets.ReadOnlyModelViewSet):
|
||||
class AlbumViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
queryset = (
|
||||
models.Album.objects.all()
|
||||
.order_by('-creation_date')
|
||||
.order_by('artist', 'release_date')
|
||||
.select_related()
|
||||
.prefetch_related('tracks__tags',
|
||||
'tracks__files'))
|
||||
.prefetch_related(
|
||||
'tracks__artist',
|
||||
'tracks__files'))
|
||||
serializer_class = serializers.AlbumSerializer
|
||||
permission_classes = [ConditionalAuthentication]
|
||||
search_fields = ['title__unaccent']
|
||||
ordering_fields = ('creation_date',)
|
||||
ordering_fields = ('creation_date', 'release_date', 'title')
|
||||
filter_class = filters.AlbumFilter
|
||||
|
||||
|
||||
|
@ -155,19 +143,20 @@ class ImportJobViewSet(
|
|||
)
|
||||
|
||||
|
||||
class TrackViewSet(
|
||||
TagViewSetMixin, SearchMixin, viewsets.ReadOnlyModelViewSet):
|
||||
class TrackViewSet(TagViewSetMixin, viewsets.ReadOnlyModelViewSet):
|
||||
"""
|
||||
A simple ViewSet for viewing and editing accounts.
|
||||
"""
|
||||
queryset = (models.Track.objects.all().for_nested_serialization())
|
||||
serializer_class = serializers.TrackSerializer
|
||||
permission_classes = [ConditionalAuthentication]
|
||||
search_fields = ['title', 'artist__name']
|
||||
filter_class = filters.TrackFilter
|
||||
ordering_fields = (
|
||||
'creation_date',
|
||||
'title__unaccent',
|
||||
'album__title__unaccent',
|
||||
'album__release_date',
|
||||
'position',
|
||||
'artist__name__unaccent',
|
||||
)
|
||||
|
||||
|
|
|
@ -144,8 +144,8 @@ class ArtistFilter(RadioFilter):
|
|||
'name': 'ids',
|
||||
'type': 'list',
|
||||
'subtype': 'number',
|
||||
'autocomplete': reverse_lazy('api:v1:artists-search'),
|
||||
'autocomplete_qs': 'query={query}',
|
||||
'autocomplete': reverse_lazy('api:v1:artists-list'),
|
||||
'autocomplete_qs': 'q={query}',
|
||||
'autocomplete_fields': {'name': 'name', 'value': 'id'},
|
||||
'label': 'Artist',
|
||||
'placeholder': 'Select artists'
|
||||
|
|
|
@ -3,15 +3,12 @@ from rest_framework import status
|
|||
from rest_framework.response import Response
|
||||
from rest_framework.decorators import detail_route
|
||||
|
||||
from funkwhale_api.music.views import SearchMixin
|
||||
|
||||
from . import filters
|
||||
from . import models
|
||||
from . import serializers
|
||||
|
||||
|
||||
class ImportRequestViewSet(
|
||||
SearchMixin,
|
||||
mixins.CreateModelMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.ListModelMixin,
|
||||
|
@ -22,7 +19,6 @@ class ImportRequestViewSet(
|
|||
models.ImportRequest.objects.all()
|
||||
.select_related()
|
||||
.order_by('-creation_date'))
|
||||
search_fields = ['artist_name', 'album_name', 'comment']
|
||||
filter_class = filters.ImportRequestFilter
|
||||
ordering_fields = ('id', 'artist_name', 'creation_date', 'status')
|
||||
|
||||
|
|
|
@ -21,7 +21,6 @@
|
|||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import logger from '@/logging'
|
||||
import jQuery from 'jquery'
|
||||
|
||||
export default {
|
||||
|
@ -30,18 +29,15 @@ export default {
|
|||
tracks: {type: Array, required: false},
|
||||
track: {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 () {
|
||||
return {
|
||||
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 () {
|
||||
jQuery(this.$el).find('.ui.dropdown').dropdown()
|
||||
},
|
||||
|
@ -62,6 +58,10 @@ export default {
|
|||
return this.tracks.length > 0
|
||||
} else if (this.playlist) {
|
||||
return true
|
||||
} else if (this.artist) {
|
||||
return true
|
||||
} else if (this.album) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
@ -81,6 +81,20 @@ export default {
|
|||
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) => {
|
||||
|
|
|
@ -18,10 +18,10 @@
|
|||
<router-link class="discrete link":to="{name: 'library.albums.detail', params: {id: album.id }}">
|
||||
<strong>{{ album.title }}</strong>
|
||||
</router-link><br />
|
||||
{{ album.tracks.length }} tracks
|
||||
{{ album.tracks_count }} tracks
|
||||
</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>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
@ -45,7 +45,7 @@
|
|||
{{ artist.albums.length }}
|
||||
</i18next>
|
||||
</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"/>
|
||||
</play-button>
|
||||
</div>
|
||||
|
@ -74,15 +74,6 @@ export default {
|
|||
return this.artist.albums
|
||||
}
|
||||
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>
|
||||
<div class="content">
|
||||
{{ 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})}}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -18,7 +18,7 @@
|
|||
<div class="ui hidden divider"></div>
|
||||
<radio-button type="artist" :object-id="artist.id"></radio-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">
|
||||
<i class="wikipedia icon"></i>
|
||||
|
@ -30,10 +30,13 @@
|
|||
</a>
|
||||
</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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -43,7 +46,6 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import axios from 'axios'
|
||||
import logger from '@/logging'
|
||||
import backend from '@/audio/backend'
|
||||
|
@ -63,6 +65,7 @@ export default {
|
|||
data () {
|
||||
return {
|
||||
isLoading: true,
|
||||
isLoadingAlbums: true,
|
||||
artist: null,
|
||||
albums: null
|
||||
}
|
||||
|
@ -78,18 +81,19 @@ export default {
|
|||
logger.default.debug('Fetching artist "' + this.id + '"')
|
||||
axios.get(url).then((response) => {
|
||||
self.artist = response.data
|
||||
self.albums = JSON.parse(JSON.stringify(self.artist.albums)).map((album) => {
|
||||
return backend.Album.clean(album)
|
||||
})
|
||||
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: {
|
||||
sortedAlbums () {
|
||||
let a = this.albums || []
|
||||
return _.orderBy(a, ['release_date'], ['asc'])
|
||||
},
|
||||
totalTracks () {
|
||||
return this.albums.map((album) => {
|
||||
return album.tracks.length
|
||||
|
|
|
@ -69,7 +69,6 @@ import axios from 'axios'
|
|||
import _ from 'lodash'
|
||||
import $ from 'jquery'
|
||||
|
||||
import backend from '@/audio/backend'
|
||||
import logger from '@/logging'
|
||||
|
||||
import OrderingMixin from '@/components/mixins/Ordering'
|
||||
|
@ -135,13 +134,6 @@ export default {
|
|||
logger.default.debug('Fetching artists')
|
||||
axios.get(url, {params: params}).then((response) => {
|
||||
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
|
||||
})
|
||||
}, 500),
|
||||
|
|
|
@ -30,7 +30,6 @@
|
|||
<script>
|
||||
import axios from 'axios'
|
||||
import Search from '@/components/audio/Search'
|
||||
import backend from '@/audio/backend'
|
||||
import logger from '@/logging'
|
||||
import ArtistCard from '@/components/audio/artist/Card'
|
||||
import RadioCard from '@/components/radios/Card'
|
||||
|
@ -66,13 +65,6 @@ export default {
|
|||
logger.default.time('Loading latest artists')
|
||||
axios.get(url, {params: params}).then((response) => {
|
||||
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')
|
||||
self.isLoadingArtists = false
|
||||
})
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
<input id="public" type="checkbox" v-model="isPublic" />
|
||||
<i18next tag="label" for="public" path="Display publicly"/>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -123,7 +123,7 @@ export default {
|
|||
if (settings.fields.remoteValues) {
|
||||
return initialResponse
|
||||
}
|
||||
return {results: initialResponse}
|
||||
return {results: initialResponse.results}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue