diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py index 5c92ad2b1..871dfc920 100644 --- a/api/funkwhale_api/music/views.py +++ b/api/funkwhale_api/music/views.py @@ -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 diff --git a/api/tests/music/test_views.py b/api/tests/music/test_views.py index 691fa049e..2f9d66e5b 100644 --- a/api/tests/music/test_views.py +++ b/api/tests/music/test_views.py @@ -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], + } diff --git a/changes/changelog.d/551.enhancement b/changes/changelog.d/551.enhancement new file mode 100644 index 000000000..267dba9f7 --- /dev/null +++ b/changes/changelog.d/551.enhancement @@ -0,0 +1 @@ +Added a library widget to display libraries associated with a track, album and artist (#551) diff --git a/front/src/components/audio/album/Card.vue b/front/src/components/audio/album/Card.vue index 0c5a7c803..a439a16b6 100644 --- a/front/src/components/audio/album/Card.vue +++ b/front/src/components/audio/album/Card.vue @@ -38,7 +38,7 @@
- Show 1 more track + Show %{ count } more track Collapse @@ -52,7 +52,7 @@ - 1 track + %{ count } track
diff --git a/front/src/components/common/CopyInput.vue b/front/src/components/common/CopyInput.vue index c2db315bd..af82f2c66 100644 --- a/front/src/components/common/CopyInput.vue +++ b/front/src/components/common/CopyInput.vue @@ -4,7 +4,7 @@ Text copied to clipboard!

- @@ -12,7 +12,10 @@ diff --git a/front/src/components/federation/LibraryFollowTable.vue b/front/src/components/federation/LibraryFollowTable.vue deleted file mode 100644 index a5dd08ced..000000000 --- a/front/src/components/federation/LibraryFollowTable.vue +++ /dev/null @@ -1,196 +0,0 @@ - - - diff --git a/front/src/components/federation/LibraryForm.vue b/front/src/components/federation/LibraryForm.vue deleted file mode 100644 index 7039cb524..000000000 --- a/front/src/components/federation/LibraryForm.vue +++ /dev/null @@ -1,123 +0,0 @@ - - - diff --git a/front/src/components/federation/LibraryTrackTable.vue b/front/src/components/federation/LibraryTrackTable.vue deleted file mode 100644 index b24e11099..000000000 --- a/front/src/components/federation/LibraryTrackTable.vue +++ /dev/null @@ -1,228 +0,0 @@ - - - diff --git a/front/src/components/federation/LibraryWidget.vue b/front/src/components/federation/LibraryWidget.vue new file mode 100644 index 000000000..ff73bb7a8 --- /dev/null +++ b/front/src/components/federation/LibraryWidget.vue @@ -0,0 +1,84 @@ + + + diff --git a/front/src/components/library/Album.vue b/front/src/components/library/Album.vue index 7ee5ae068..03d83e064 100644 --- a/front/src/components/library/Album.vue +++ b/front/src/components/library/Album.vue @@ -45,6 +45,14 @@ +
+

+ User libraries +

+ + This album is present in the following libraries: + +
@@ -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 { diff --git a/front/src/components/library/Artist.vue b/front/src/components/library/Artist.vue index ee9c625e1..fe74f6f17 100644 --- a/front/src/components/library/Artist.vue +++ b/front/src/components/library/Artist.vue @@ -56,6 +56,14 @@ +
+

+ User libraries +

+ + This artist is present in the following libraries: + +
@@ -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 { diff --git a/front/src/components/library/Track.vue b/front/src/components/library/Track.vue index 1ede2218b..75d5f650d 100644 --- a/front/src/components/library/Track.vue +++ b/front/src/components/library/Track.vue @@ -118,6 +118,14 @@ +
+

+ User libraries +

+ + This track is present in the following libraries: + +
@@ -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 { diff --git a/front/src/router/index.js b/front/src/router/index.js index 489ca2242..148e45b8a 100644 --- a/front/src/router/index.js +++ b/front/src/router/index.js @@ -31,12 +31,6 @@ import AdminUsersBase from '@/views/admin/users/Base' import AdminUsersDetail from '@/views/admin/users/UsersDetail' import AdminUsersList from '@/views/admin/users/UsersList' import AdminInvitationsList from '@/views/admin/users/InvitationsList' -import FederationBase from '@/views/federation/Base' -import FederationScan from '@/views/federation/Scan' -import FederationLibraryDetail from '@/views/federation/LibraryDetail' -import FederationLibraryList from '@/views/federation/LibraryList' -import FederationTrackList from '@/views/federation/LibraryTrackList' -import FederationFollowersList from '@/views/federation/LibraryFollowersList' import ContentBase from '@/views/content/Base' import ContentHome from '@/views/content/Home' import LibrariesHome from '@/views/content/libraries/Home' @@ -203,50 +197,6 @@ export default new Router({ name: 'manage.settings', component: AdminSettings }, - { - path: '/manage/federation', - component: FederationBase, - children: [ - { - path: 'scan', - name: 'federation.libraries.scan', - component: FederationScan }, - { - path: 'libraries', - name: 'federation.libraries.list', - component: FederationLibraryList, - props: (route) => ({ - defaultOrdering: route.query.ordering, - defaultQuery: route.query.query, - defaultPaginateBy: route.query.paginateBy, - defaultPage: route.query.page - }) - }, - { - path: 'tracks', - name: 'federation.tracks.list', - component: FederationTrackList, - props: (route) => ({ - defaultOrdering: route.query.ordering, - defaultQuery: route.query.query, - defaultPaginateBy: route.query.paginateBy, - defaultPage: route.query.page - }) - }, - { - path: 'followers', - name: 'federation.followers.list', - component: FederationFollowersList, - props: (route) => ({ - defaultOrdering: route.query.ordering, - defaultQuery: route.query.query, - defaultPaginateBy: route.query.paginateBy, - defaultPage: route.query.page - }) - }, - { path: 'libraries/:id', name: 'federation.libraries.detail', component: FederationLibraryDetail, props: true } - ] - }, { path: '/manage/library', component: AdminLibraryBase, diff --git a/front/src/views/content/libraries/Card.vue b/front/src/views/content/libraries/Card.vue index 12d6d7d5f..6b327f370 100644 --- a/front/src/views/content/libraries/Card.vue +++ b/front/src/views/content/libraries/Card.vue @@ -36,8 +36,8 @@ {{ library.size | humanSize }} - {{ library.uploads_count }} - 1 track + + %{ count } track
diff --git a/front/src/views/content/remote/Card.vue b/front/src/views/content/remote/Card.vue index 48f5896ad..9cbaf857f 100644 --- a/front/src/views/content/remote/Card.vue +++ b/front/src/views/content/remote/Card.vue @@ -24,9 +24,9 @@
- 1 tracks + %{ count } tracks
-
+
- - - diff --git a/front/src/views/federation/LibraryDetail.vue b/front/src/views/federation/LibraryDetail.vue deleted file mode 100644 index 708156c19..000000000 --- a/front/src/views/federation/LibraryDetail.vue +++ /dev/null @@ -1,220 +0,0 @@ - - - - - - diff --git a/front/src/views/federation/LibraryFollowersList.vue b/front/src/views/federation/LibraryFollowersList.vue deleted file mode 100644 index 0a9267a85..000000000 --- a/front/src/views/federation/LibraryFollowersList.vue +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - diff --git a/front/src/views/federation/LibraryList.vue b/front/src/views/federation/LibraryList.vue deleted file mode 100644 index d627d65f4..000000000 --- a/front/src/views/federation/LibraryList.vue +++ /dev/null @@ -1,183 +0,0 @@ - - - - - - diff --git a/front/src/views/federation/LibraryTrackList.vue b/front/src/views/federation/LibraryTrackList.vue deleted file mode 100644 index 55f9e46af..000000000 --- a/front/src/views/federation/LibraryTrackList.vue +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - diff --git a/front/src/views/federation/Scan.vue b/front/src/views/federation/Scan.vue deleted file mode 100644 index 5caa2f540..000000000 --- a/front/src/views/federation/Scan.vue +++ /dev/null @@ -1,39 +0,0 @@ - - -