Fix #551: Added a library widget to display libraries associated with a track, album and artist

This commit is contained in:
Eliot Berriot 2018-10-02 19:30:13 +02:00
parent f2812c67ce
commit a865fcdcf1
No known key found for this signature in database
GPG Key ID: DD6965E2476E5C27
9 changed files with 210 additions and 10 deletions

View File

@ -3,7 +3,7 @@ import urllib
from django.conf import settings
from django.db import transaction
from django.db.models import Count, Prefetch, Sum, F
from django.db.models import Count, Prefetch, Sum, F, Q
from django.db.models.functions import Length
from django.utils import timezone
@ -26,6 +26,28 @@ from . import filters, models, serializers, tasks, utils
logger = logging.getLogger(__name__)
def get_libraries(filter_uploads):
def view(self, request, *args, **kwargs):
obj = self.get_object()
actor = utils.get_actor_from_request(request)
uploads = models.Upload.objects.all()
uploads = filter_uploads(obj, uploads)
uploads = uploads.playable_by(actor)
libraries = models.Library.objects.filter(
pk__in=uploads.values_list("library", flat=True)
)
libraries = libraries.select_related("actor")
page = self.paginate_queryset(libraries)
if page is not None:
serializer = federation_api_serializers.LibrarySerializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = federation_api_serializers.LibrarySerializer(libraries, many=True)
return Response(serializer.data)
return view
class TagViewSetMixin(object):
def get_queryset(self):
queryset = super().get_queryset()
@ -50,6 +72,14 @@ class ArtistViewSet(viewsets.ReadOnlyModelViewSet):
)
return queryset.prefetch_related(Prefetch("albums", queryset=albums)).distinct()
libraries = detail_route(methods=["get"])(
get_libraries(
filter_uploads=lambda o, uploads: uploads.filter(
Q(track__artist=o) | Q(track__album__artist=o)
)
)
)
class AlbumViewSet(viewsets.ReadOnlyModelViewSet):
queryset = (
@ -76,6 +106,10 @@ class AlbumViewSet(viewsets.ReadOnlyModelViewSet):
qs = queryset.prefetch_related(Prefetch("tracks", queryset=tracks))
return qs.distinct()
libraries = detail_route(methods=["get"])(
get_libraries(filter_uploads=lambda o, uploads: uploads.filter(track__album=o))
)
class LibraryViewSet(
mixins.CreateModelMixin,
@ -197,6 +231,10 @@ class TrackViewSet(TagViewSetMixin, viewsets.ReadOnlyModelViewSet):
serializer = serializers.LyricsSerializer(lyrics)
return Response(serializer.data)
libraries = detail_route(methods=["get"])(
get_libraries(filter_uploads=lambda o, uploads: uploads.filter(track=o))
)
def get_file_path(audio_file):
serve_path = settings.MUSIC_DIRECTORY_SERVE_PATH

View File

@ -449,3 +449,34 @@ def test_user_can_list_own_library_follows(factories, logged_in_api_client):
"previous": None,
"results": [federation_api_serializers.LibraryFollowSerializer(follow).data],
}
@pytest.mark.parametrize("entity", ["artist", "album", "track"])
def test_can_get_libraries_for_music_entities(
factories, api_client, entity, preferences
):
preferences["common__api_authentication_required"] = False
upload = factories["music.Upload"](playable=True)
# another private library that should not appear
factories["music.Upload"](
import_status="finished", library__privacy_level="me", track=upload.track
).library
library = upload.library
data = {
"artist": upload.track.artist,
"album": upload.track.album,
"track": upload.track,
}
url = reverse("api:v1:{}s-libraries".format(entity), kwargs={"pk": data[entity].pk})
response = api_client.get(url)
expected = federation_api_serializers.LibrarySerializer(library).data
assert response.status_code == 200
assert response.data == {
"count": 1,
"next": None,
"previous": None,
"results": [expected],
}

View File

@ -0,0 +1 @@
Added a library widget to display libraries associated with a track, album and artist (#551)

View File

@ -4,7 +4,7 @@
<translate>Text copied to clipboard!</translate>
</p>
<input ref="input" :value="value" type="text">
<button @click="copy" class="ui teal right labeled icon button">
<button @click="copy" :class="['ui', buttonClasses, 'right', 'labeled', 'icon', 'button']">
<i class="copy icon"></i>
<translate>Copy</translate>
</button>
@ -12,7 +12,10 @@
</template>
<script>
export default {
props: ['value'],
props: {
value: {type: String},
buttonClasses: {type: String, default: 'teal'}
},
data () {
return {
copied: false,

View File

@ -0,0 +1,84 @@
<template>
<div class="wrapper">
<h3 class="ui header">
<slot name="title"></slot>
</h3>
<p v-if="!isLoading && libraries.length > 0" class="ui subtitle"><slot name="subtitle"></slot></p>
<p v-if="!isLoading && libraries.length === 0" class="ui subtitle"><translate>No matching library.</translate></p>
<i @click="fetchData(previousPage)" :disabled="!previousPage" :class="['ui', {disabled: !previousPage}, 'circular', 'medium', 'angle left', 'icon']">
</i>
<i @click="fetchData(nextPage)" :disabled="!nextPage" :class="['ui', {disabled: !nextPage}, 'circular', 'medium', 'angle right', 'icon']">
</i>
<div class="ui hidden divider"></div>
<div class="ui three cards">
<div v-if="isLoading" class="ui inverted active dimmer">
<div class="ui loader"></div>
</div>
<library-card
:display-scan="false"
:display-follow="$store.state.auth.authenticated"
:library="library"
:display-copy-fid="true"
v-for="library in libraries"
:key="library.uuid"></library-card>
</div>
</div>
</template>
<script>
import _ from 'lodash'
import axios from 'axios'
import LibraryCard from '@/views/content/remote/Card'
export default {
props: {
url: {type: String, required: true}
},
components: {
LibraryCard
},
data () {
return {
libraries: [],
limit: 6,
isLoading: false,
errors: null,
previousPage: null,
nextPage: null
}
},
created () {
this.fetchData()
},
methods: {
fetchData () {
this.isLoading = true
let self = this
let params = _.clone({})
params.page_size = this.limit
params.offset = this.offset
axios.get(this.url, {params: params}).then((response) => {
self.previousPage = response.data.previous
self.nextPage = response.data.next
self.isLoading = false
self.libraries = response.data.results
}, error => {
self.isLoading = false
self.errors = error.backendErrors
})
},
updateOffset (increment) {
if (increment) {
this.offset += this.limit
} else {
this.offset = Math.max(this.offset - this.limit, 0)
}
}
},
watch: {
offset () {
this.fetchData()
}
}
}
</script>

View File

@ -45,6 +45,14 @@
</h2>
<track-table v-if="album" :artist="album.artist" :display-position="true" :tracks="album.tracks"></track-table>
</div>
<div class="ui vertical stripe segment">
<h2>
<translate>User libraries</translate>
</h2>
<library-widget :url="'albums/' + id + '/libraries/'">
<translate slot="subtitle">This album is present in the following libraries:</translate>
</library-widget>
</div>
</template>
</div>
</template>
@ -55,6 +63,7 @@ import logger from '@/logging'
import backend from '@/audio/backend'
import PlayButton from '@/components/audio/PlayButton'
import TrackTable from '@/components/audio/track/Table'
import LibraryWidget from '@/components/federation/LibraryWidget'
const FETCH_URL = 'albums/'
@ -62,7 +71,8 @@ export default {
props: ['id'],
components: {
PlayButton,
TrackTable
TrackTable,
LibraryWidget
},
data () {
return {

View File

@ -56,6 +56,14 @@
</h2>
<track-table :display-position="true" :tracks="tracks"></track-table>
</div>
<div class="ui vertical stripe segment">
<h2>
<translate>User libraries</translate>
</h2>
<library-widget :url="'artists/' + id + '/libraries/'">
<translate slot="subtitle">This artist is present in the following libraries:</translate>
</library-widget>
</div>
</template>
</div>
</template>
@ -69,6 +77,7 @@ import AlbumCard from '@/components/audio/album/Card'
import RadioButton from '@/components/radios/Button'
import PlayButton from '@/components/audio/PlayButton'
import TrackTable from '@/components/audio/track/Table'
import LibraryWidget from '@/components/federation/LibraryWidget'
export default {
props: ['id'],
@ -76,7 +85,8 @@ export default {
AlbumCard,
RadioButton,
PlayButton,
TrackTable
TrackTable,
LibraryWidget
},
data () {
return {

View File

@ -118,6 +118,14 @@
</a>
</template>
</div>
<div class="ui vertical stripe segment">
<h2>
<translate>User libraries</translate>
</h2>
<library-widget :url="'tracks/' + id + '/libraries/'">
<translate slot="subtitle">This track is present in the following libraries:</translate>
</library-widget>
</div>
</template>
</div>
</template>
@ -131,6 +139,7 @@ import logger from '@/logging'
import PlayButton from '@/components/audio/PlayButton'
import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon'
import TrackPlaylistIcon from '@/components/playlists/TrackPlaylistIcon'
import LibraryWidget from '@/components/federation/LibraryWidget'
const FETCH_URL = 'tracks/'
@ -139,7 +148,8 @@ export default {
components: {
PlayButton,
TrackPlaylistIcon,
TrackFavoriteIcon
TrackFavoriteIcon,
LibraryWidget
},
data () {
return {

View File

@ -26,7 +26,7 @@
<i class="music icon"></i>
<translate :translate-params="{count: library.uploads_count}" :translate-n="library.uploads_count" translate-plural="%{ count } tracks">%{ count } tracks</translate>
</div>
<div v-if="latestScan" class="meta">
<div v-if="displayScan && latestScan" class="meta">
<template v-if="latestScan.status === 'pending'">
<i class="hourglass icon"></i>
<translate>Scan pending</translate>
@ -59,7 +59,7 @@
<translate>Errored tracks:</translate> {{ latestScan.errored_files }}
</div>
</div>
<div v-if="canLaunchScan" class="clearfix">
<div v-if="displayScan && canLaunchScan" class="clearfix">
<span class="right floated link" @click="launchScan">
<translate>Launch scan</translate> <i class="paper plane icon" />
</span>
@ -68,7 +68,15 @@
<div class="extra content">
<actor-link :actor="library.actor" />
</div>
<div class="ui bottom attached buttons">
<div v-if="displayCopyFid" class="extra content">
<div class="ui form">
<div class="field">
<label><translate>Sharing link</translate></label>
<copy-input :button-classes="'basic'" :value="library.fid" />
</div>
</div>
</div>
<div v-if="displayFollow" class="ui bottom attached buttons">
<button
v-if="!library.follow"
@click="follow()"
@ -104,7 +112,12 @@
import axios from 'axios'
export default {
props: ['library'],
props: {
library: {type: Object, required: true},
displayFollow: {type: Boolean, default: true},
displayScan: {type: Boolean, default: true},
displayCopyFid: {type: Boolean, default: false},
},
data () {
return {
isLoadingFollow: false,