Fix #551: Added a library widget to display libraries associated with a track, album and artist
This commit is contained in:
parent
f2812c67ce
commit
a865fcdcf1
|
@ -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
|
||||
|
|
|
@ -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],
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Added a library widget to display libraries associated with a track, album and artist (#551)
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue