diff --git a/api/funkwhale_api/music/factories.py b/api/funkwhale_api/music/factories.py index fd905f675..9571f9785 100644 --- a/api/funkwhale_api/music/factories.py +++ b/api/funkwhale_api/music/factories.py @@ -14,11 +14,28 @@ SAMPLES_PATH = os.path.join( ) +def playable_factory(field): + @factory.post_generation + def inner(self, create, extracted, **kwargs): + if not create: + return + + if extracted: + UploadFactory( + library__privacy_level="everyone", + import_status="finished", + **{field: self} + ) + + return inner + + @registry.register class ArtistFactory(factory.django.DjangoModelFactory): name = factory.Faker("name") mbid = factory.Faker("uuid4") fid = factory.Faker("federation_url") + playable = playable_factory("track__album__artist") class Meta: model = "music.Artist" @@ -33,6 +50,7 @@ class AlbumFactory(factory.django.DjangoModelFactory): artist = factory.SubFactory(ArtistFactory) release_group_id = factory.Faker("uuid4") fid = factory.Faker("federation_url") + playable = playable_factory("track__album") class Meta: model = "music.Album" @@ -47,6 +65,7 @@ class TrackFactory(factory.django.DjangoModelFactory): artist = factory.SelfAttribute("album.artist") position = 1 tags = ManyToManyFromList("tags") + playable = playable_factory("track") class Meta: model = "music.Track" @@ -71,6 +90,9 @@ class UploadFactory(factory.django.DjangoModelFactory): class Params: in_place = factory.Trait(audio_file=None) + playable = factory.Trait( + import_status="finished", library__privacy_level="everyone" + ) @registry.register diff --git a/api/funkwhale_api/subsonic/views.py b/api/funkwhale_api/subsonic/views.py index 48810d02b..7ca9b13a8 100644 --- a/api/funkwhale_api/subsonic/views.py +++ b/api/funkwhale_api/subsonic/views.py @@ -19,7 +19,9 @@ from funkwhale_api.playlists import models as playlists_models from . import authentication, filters, negotiation, serializers -def find_object(queryset, model_field="pk", field="id", cast=int): +def find_object( + queryset, model_field="pk", field="id", cast=int, filter_playable=False +): def decorator(func): def inner(self, request, *args, **kwargs): data = request.GET or request.POST @@ -50,6 +52,11 @@ def find_object(queryset, model_field="pk", field="id", cast=int): qs = queryset if hasattr(qs, "__call__"): qs = qs(request) + + if filter_playable: + actor = utils.get_actor_from_request(request) + qs = qs.playable_by(actor).distinct() + try: obj = qs.get(**{model_field: value}) except qs.model.DoesNotExist: @@ -124,7 +131,9 @@ class SubsonicViewSet(viewsets.GenericViewSet): @list_route(methods=["get", "post"], url_name="get_artists", url_path="getArtists") def get_artists(self, request, *args, **kwargs): - artists = music_models.Artist.objects.all() + artists = music_models.Artist.objects.all().playable_by( + utils.get_actor_from_request(request) + ) data = serializers.GetArtistsSerializer(artists).data payload = {"artists": data} @@ -132,14 +141,16 @@ class SubsonicViewSet(viewsets.GenericViewSet): @list_route(methods=["get", "post"], url_name="get_indexes", url_path="getIndexes") def get_indexes(self, request, *args, **kwargs): - artists = music_models.Artist.objects.all() + artists = music_models.Artist.objects.all().playable_by( + utils.get_actor_from_request(request) + ) data = serializers.GetArtistsSerializer(artists).data payload = {"indexes": data} return response.Response(payload, status=200) @list_route(methods=["get", "post"], url_name="get_artist", url_path="getArtist") - @find_object(music_models.Artist.objects.all()) + @find_object(music_models.Artist.objects.all(), filter_playable=True) def get_artist(self, request, *args, **kwargs): artist = kwargs.pop("obj") data = serializers.GetArtistSerializer(artist).data @@ -148,7 +159,7 @@ class SubsonicViewSet(viewsets.GenericViewSet): return response.Response(payload, status=200) @list_route(methods=["get", "post"], url_name="get_song", url_path="getSong") - @find_object(music_models.Track.objects.all()) + @find_object(music_models.Track.objects.all(), filter_playable=True) def get_song(self, request, *args, **kwargs): track = kwargs.pop("obj") data = serializers.GetSongSerializer(track).data @@ -159,14 +170,16 @@ class SubsonicViewSet(viewsets.GenericViewSet): @list_route( methods=["get", "post"], url_name="get_artist_info2", url_path="getArtistInfo2" ) - @find_object(music_models.Artist.objects.all()) + @find_object(music_models.Artist.objects.all(), filter_playable=True) def get_artist_info2(self, request, *args, **kwargs): payload = {"artist-info2": {}} return response.Response(payload, status=200) @list_route(methods=["get", "post"], url_name="get_album", url_path="getAlbum") - @find_object(music_models.Album.objects.select_related("artist")) + @find_object( + music_models.Album.objects.select_related("artist"), filter_playable=True + ) def get_album(self, request, *args, **kwargs): album = kwargs.pop("obj") data = serializers.GetAlbumSerializer(album).data @@ -174,7 +187,7 @@ class SubsonicViewSet(viewsets.GenericViewSet): return response.Response(payload, status=200) @list_route(methods=["get", "post"], url_name="stream", url_path="stream") - @find_object(music_models.Track.objects.all()) + @find_object(music_models.Track.objects.all(), filter_playable=True) def stream(self, request, *args, **kwargs): track = kwargs.pop("obj") queryset = track.uploads.select_related("track__album__artist", "track__artist") @@ -221,6 +234,9 @@ class SubsonicViewSet(viewsets.GenericViewSet): data = request.GET or request.POST filterset = filters.AlbumList2FilterSet(data, queryset=queryset) queryset = filterset.qs + actor = utils.get_actor_from_request(request) + queryset = queryset.playable_by(actor) + try: offset = int(data["offset"]) except (TypeError, KeyError, ValueError): @@ -240,6 +256,7 @@ class SubsonicViewSet(viewsets.GenericViewSet): def search3(self, request, *args, **kwargs): data = request.GET or request.POST query = str(data.get("query", "")).replace("*", "") + actor = utils.get_actor_from_request(request) conf = [ { "subsonic": "artist", @@ -292,6 +309,7 @@ class SubsonicViewSet(viewsets.GenericViewSet): queryset = c["queryset"].filter( utils.get_query(query, c["search_fields"]) ) + queryset = queryset.playable_by(actor) queryset = queryset[offset : offset + size] payload["searchResult3"][c["subsonic"]] = c["serializer"](queryset) return response.Response(payload) diff --git a/api/tests/subsonic/test_views.py b/api/tests/subsonic/test_views.py index 1331c281e..94cbd8c16 100644 --- a/api/tests/subsonic/test_views.py +++ b/api/tests/subsonic/test_views.py @@ -74,10 +74,13 @@ def test_ping(f, db, api_client): @pytest.mark.parametrize("f", ["xml", "json"]) -def test_get_artists(f, db, logged_in_api_client, factories): +def test_get_artists( + f, db, logged_in_api_client, factories, mocker, queryset_equal_queries +): url = reverse("api:subsonic-get-artists") assert url.endswith("getArtists") is True - factories["music.Artist"].create_batch(size=10) + factories["music.Artist"].create_batch(size=3, playable=True) + playable_by = mocker.spy(music_models.ArtistQuerySet, "playable_by") expected = { "artists": serializers.GetArtistsSerializer( music_models.Artist.objects.all() @@ -87,19 +90,25 @@ def test_get_artists(f, db, logged_in_api_client, factories): assert response.status_code == 200 assert response.data == expected + playable_by.assert_called_once_with(music_models.Artist.objects.all(), None) @pytest.mark.parametrize("f", ["xml", "json"]) -def test_get_artist(f, db, logged_in_api_client, factories): +def test_get_artist( + f, db, logged_in_api_client, factories, mocker, queryset_equal_queries +): url = reverse("api:subsonic-get-artist") assert url.endswith("getArtist") is True - artist = factories["music.Artist"]() - factories["music.Album"].create_batch(size=3, artist=artist) + artist = factories["music.Artist"](playable=True) + factories["music.Album"].create_batch(size=3, artist=artist, playable=True) + playable_by = mocker.spy(music_models.ArtistQuerySet, "playable_by") + expected = {"artist": serializers.GetArtistSerializer(artist).data} response = logged_in_api_client.get(url, {"id": artist.pk}) assert response.status_code == 200 assert response.data == expected + playable_by.assert_called_once_with(music_models.Artist.objects.all(), None) @pytest.mark.parametrize("f", ["xml", "json"]) @@ -114,10 +123,13 @@ def test_get_invalid_artist(f, db, logged_in_api_client, factories): @pytest.mark.parametrize("f", ["xml", "json"]) -def test_get_artist_info2(f, db, logged_in_api_client, factories): +def test_get_artist_info2( + f, db, logged_in_api_client, factories, mocker, queryset_equal_queries +): url = reverse("api:subsonic-get-artist-info2") assert url.endswith("getArtistInfo2") is True - artist = factories["music.Artist"]() + artist = factories["music.Artist"](playable=True) + playable_by = mocker.spy(music_models.ArtistQuerySet, "playable_by") expected = {"artist-info2": {}} response = logged_in_api_client.get(url, {"id": artist.pk}) @@ -125,50 +137,62 @@ def test_get_artist_info2(f, db, logged_in_api_client, factories): assert response.status_code == 200 assert response.data == expected + playable_by.assert_called_once_with(music_models.Artist.objects.all(), None) + @pytest.mark.parametrize("f", ["xml", "json"]) -def test_get_album(f, db, logged_in_api_client, factories): +def test_get_album( + f, db, logged_in_api_client, factories, mocker, queryset_equal_queries +): url = reverse("api:subsonic-get-album") assert url.endswith("getAlbum") is True artist = factories["music.Artist"]() album = factories["music.Album"](artist=artist) - factories["music.Track"].create_batch(size=3, album=album) + factories["music.Track"].create_batch(size=3, album=album, playable=True) + playable_by = mocker.spy(music_models.AlbumQuerySet, "playable_by") expected = {"album": serializers.GetAlbumSerializer(album).data} response = logged_in_api_client.get(url, {"f": f, "id": album.pk}) assert response.status_code == 200 assert response.data == expected + playable_by.assert_called_once_with( + music_models.Album.objects.select_related("artist"), None + ) + @pytest.mark.parametrize("f", ["xml", "json"]) -def test_get_song(f, db, logged_in_api_client, factories): +def test_get_song( + f, db, logged_in_api_client, factories, mocker, queryset_equal_queries +): url = reverse("api:subsonic-get-song") assert url.endswith("getSong") is True artist = factories["music.Artist"]() album = factories["music.Album"](artist=artist) - track = factories["music.Track"](album=album) + track = factories["music.Track"](album=album, playable=True) upload = factories["music.Upload"](track=track) + playable_by = mocker.spy(music_models.TrackQuerySet, "playable_by") response = logged_in_api_client.get(url, {"f": f, "id": track.pk}) assert response.status_code == 200 assert response.data == { "song": serializers.get_track_data(track.album, track, upload) } + playable_by.assert_called_once_with(music_models.Track.objects.all(), None) @pytest.mark.parametrize("f", ["xml", "json"]) -def test_stream(f, db, logged_in_api_client, factories, mocker): +def test_stream(f, db, logged_in_api_client, factories, mocker, queryset_equal_queries): url = reverse("api:subsonic-stream") mocked_serve = mocker.spy(music_views, "handle_serve") assert url.endswith("stream") is True - artist = factories["music.Artist"]() - album = factories["music.Album"](artist=artist) - track = factories["music.Track"](album=album) - upload = factories["music.Upload"](track=track) - response = logged_in_api_client.get(url, {"f": f, "id": track.pk}) + upload = factories["music.Upload"](playable=True) + playable_by = mocker.spy(music_models.TrackQuerySet, "playable_by") + response = logged_in_api_client.get(url, {"f": f, "id": upload.track.pk}) mocked_serve.assert_called_once_with(upload=upload, user=logged_in_api_client.user) assert response.status_code == 200 + playable_by.assert_called_once_with(music_models.Track.objects.all(), None) @pytest.mark.parametrize("f", ["xml", "json"]) @@ -231,25 +255,30 @@ def test_get_starred(f, db, logged_in_api_client, factories): @pytest.mark.parametrize("f", ["xml", "json"]) -def test_get_album_list2(f, db, logged_in_api_client, factories): +def test_get_album_list2( + f, db, logged_in_api_client, factories, mocker, queryset_equal_queries +): url = reverse("api:subsonic-get-album-list2") assert url.endswith("getAlbumList2") is True - album1 = factories["music.Album"]() - album2 = factories["music.Album"]() + album1 = factories["music.Album"](playable=True) + album2 = factories["music.Album"](playable=True) + factories["music.Album"]() + playable_by = mocker.spy(music_models.AlbumQuerySet, "playable_by") response = logged_in_api_client.get(url, {"f": f, "type": "newest"}) assert response.status_code == 200 assert response.data == { "albumList2": {"album": serializers.get_album_list2_data([album2, album1])} } + playable_by.assert_called_once() @pytest.mark.parametrize("f", ["xml", "json"]) def test_get_album_list2_pagination(f, db, logged_in_api_client, factories): url = reverse("api:subsonic-get-album-list2") assert url.endswith("getAlbumList2") is True - album1 = factories["music.Album"]() - factories["music.Album"]() + album1 = factories["music.Album"](playable=True) + factories["music.Album"](playable=True) response = logged_in_api_client.get( url, {"f": f, "type": "newest", "size": 1, "offset": 1} ) @@ -264,12 +293,15 @@ def test_get_album_list2_pagination(f, db, logged_in_api_client, factories): def test_search3(f, db, logged_in_api_client, factories): url = reverse("api:subsonic-search3") assert url.endswith("search3") is True - artist = factories["music.Artist"](name="testvalue") + artist = factories["music.Artist"](name="testvalue", playable=True) factories["music.Artist"](name="nope") - album = factories["music.Album"](title="testvalue") + factories["music.Artist"](name="nope2", playable=True) + album = factories["music.Album"](title="testvalue", playable=True) factories["music.Album"](title="nope") - track = factories["music.Track"](title="testvalue") + factories["music.Album"](title="nope2", playable=True) + track = factories["music.Track"](title="testvalue", playable=True) factories["music.Track"](title="nope") + factories["music.Track"](title="nope2", playable=True) response = logged_in_api_client.get(url, {"f": f, "query": "testval"}) @@ -385,20 +417,25 @@ def test_get_music_folders(f, db, logged_in_api_client, factories): @pytest.mark.parametrize("f", ["xml", "json"]) -def test_get_indexes(f, db, logged_in_api_client, factories): +def test_get_indexes( + f, db, logged_in_api_client, factories, mocker, queryset_equal_queries +): url = reverse("api:subsonic-get-indexes") assert url.endswith("getIndexes") is True - factories["music.Artist"].create_batch(size=10) + factories["music.Artist"].create_batch(size=3, playable=True) expected = { "indexes": serializers.GetArtistsSerializer( music_models.Artist.objects.all() ).data } + playable_by = mocker.spy(music_models.ArtistQuerySet, "playable_by") response = logged_in_api_client.get(url) assert response.status_code == 200 assert response.data == expected + playable_by.assert_called_once_with(music_models.Artist.objects.all(), None) + def test_get_cover_art_album(factories, logged_in_api_client): url = reverse("api:subsonic-get-cover-art")