From 921317a217dc6193e5b6235bdc45fb5c0eebb583 Mon Sep 17 00:00:00 2001
From: Eliot Berriot <contact@eliotberriot.com>
Date: Thu, 19 Sep 2019 21:09:18 +0200
Subject: [PATCH] Implemented missing getSongsByGenre subsonic endpoint

---
 api/funkwhale_api/music/factories.py | 21 +++++++++++++-
 api/funkwhale_api/subsonic/views.py  | 42 ++++++++++++++++++++++++++++
 api/tests/subsonic/test_views.py     | 22 +++++++++++++++
 3 files changed, 84 insertions(+), 1 deletion(-)

diff --git a/api/funkwhale_api/music/factories.py b/api/funkwhale_api/music/factories.py
index 7551502e0..52e5020bb 100644
--- a/api/funkwhale_api/music/factories.py
+++ b/api/funkwhale_api/music/factories.py
@@ -108,7 +108,6 @@ class TrackFactory(
     title = factory.Faker("sentence", nb_words=3)
     mbid = factory.Faker("uuid4")
     album = factory.SubFactory(AlbumFactory)
-    artist = factory.SelfAttribute("album.artist")
     position = 1
     playable = playable_factory("track")
 
@@ -124,6 +123,26 @@ class TrackFactory(
             fid=factory.Faker("federation_url", local=True), album__local=True
         )
 
+    @factory.post_generation
+    def artist(self, created, extracted, **kwargs):
+        """
+        A bit intricated, because we want to be able to specify a different
+        track artist with a fallback on album artist if nothing is specified.
+
+        And handle cases where build or build_batch are used (so no db calls)
+        """
+        if extracted:
+            self.artist = extracted
+        elif kwargs:
+            if created:
+                self.artist = ArtistFactory(**kwargs)
+            else:
+                self.artist = ArtistFactory.build(**kwargs)
+        elif self.album:
+            self.artist = self.album.artist
+        if created:
+            self.save()
+
     @factory.post_generation
     def license(self, created, extracted, **kwargs):
         if not created:
diff --git a/api/funkwhale_api/subsonic/views.py b/api/funkwhale_api/subsonic/views.py
index e206555ab..6633224df 100644
--- a/api/funkwhale_api/subsonic/views.py
+++ b/api/funkwhale_api/subsonic/views.py
@@ -333,6 +333,48 @@ class SubsonicViewSet(viewsets.GenericViewSet):
         }
         return response.Response(data)
 
+    @action(
+        detail=False,
+        methods=["get", "post"],
+        url_name="get_songs_by_genre",
+        url_path="getSongsByGenre",
+    )
+    def get_songs_by_genre(self, request, *args, **kwargs):
+        data = request.GET or request.POST
+        actor = utils.get_actor_from_request(request)
+        queryset = music_models.Track.objects.all().exclude(
+            moderation_filters.get_filtered_content_query(
+                moderation_filters.USER_FILTER_CONFIG["TRACK"], request.user
+            )
+        )
+        queryset = queryset.playable_by(actor)
+        try:
+            size = int(
+                data["count"]
+            )  # yep. Some endpoints have size, other have count…
+        except (TypeError, KeyError, ValueError):
+            size = 50
+
+        genre = data.get("genre")
+        queryset = (
+            queryset.playable_by(actor)
+            .filter(
+                Q(tagged_items__tag__name=genre)
+                | Q(artist__tagged_items__tag__name=genre)
+                | Q(album__artist__tagged_items__tag__name=genre)
+                | Q(album__tagged_items__tag__name=genre)
+            )
+            .prefetch_related("uploads")
+            .distinct()
+            .order_by("-creation_date")[:size]
+        )
+        data = {
+            "songsByGenre": {
+                "song": serializers.GetSongSerializer(queryset, many=True).data
+            }
+        }
+        return response.Response(data)
+
     @action(
         detail=False,
         methods=["get", "post"],
diff --git a/api/tests/subsonic/test_views.py b/api/tests/subsonic/test_views.py
index 361a46a73..298ad34f7 100644
--- a/api/tests/subsonic/test_views.py
+++ b/api/tests/subsonic/test_views.py
@@ -469,6 +469,28 @@ def test_get_album_list2_by_genre(f, db, logged_in_api_client, factories):
     }
 
 
+@pytest.mark.parametrize("f", ["json"])
+@pytest.mark.parametrize(
+    "tags_field",
+    ["set_tags", "artist__set_tags", "album__set_tags", "album__artist__set_tags"],
+)
+def test_get_songs_by_genre(f, tags_field, db, logged_in_api_client, factories):
+    url = reverse("api:subsonic-get_songs_by_genre")
+    assert url.endswith("getSongsByGenre") is True
+    track1 = factories["music.Track"](playable=True, **{tags_field: ["Rock"]})
+    track2 = factories["music.Track"](playable=True, **{tags_field: ["Rock"]})
+    factories["music.Track"](playable=True, **{tags_field: ["Pop"]})
+    expected = {
+        "songsByGenre": {"song": serializers.get_song_list_data([track2, track1])}
+    }
+
+    response = logged_in_api_client.get(
+        url, {"f": f, "count": 5, "offset": 0, "genre": "rock"}
+    )
+    assert response.status_code == 200
+    assert response.data == expected
+
+
 @pytest.mark.parametrize("f", ["json"])
 def test_search3(f, db, logged_in_api_client, factories):
     url = reverse("api:subsonic-search3")
-- 
GitLab