Merge branch '432-subsonic-genres' into 'develop'
See #432: genre support in Subsonic API See merge request funkwhale/funkwhale!835
This commit is contained in:
commit
2c697ae2cc
|
@ -52,6 +52,9 @@ def dict_to_xml_tree(root_tag, d, parent=None):
|
||||||
elif isinstance(value, list):
|
elif isinstance(value, list):
|
||||||
for obj in value:
|
for obj in value:
|
||||||
root.append(dict_to_xml_tree(key, obj, parent=root))
|
root.append(dict_to_xml_tree(key, obj, parent=root))
|
||||||
|
else:
|
||||||
|
if key == "value":
|
||||||
|
root.text = str(value)
|
||||||
else:
|
else:
|
||||||
root.set(key, str(value))
|
root.set(key, str(value))
|
||||||
return root
|
return root
|
||||||
|
|
|
@ -263,3 +263,11 @@ class ScrobbleSerializer(serializers.Serializer):
|
||||||
return history_models.Listening.objects.create(
|
return history_models.Listening.objects.create(
|
||||||
user=self.context["user"], track=data["id"]
|
user=self.context["user"], track=data["id"]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_genre_data(tag):
|
||||||
|
return {
|
||||||
|
"songCount": getattr(tag, "_tracks_count", 0),
|
||||||
|
"albumCount": getattr(tag, "_albums_count", 0),
|
||||||
|
"value": tag.name,
|
||||||
|
}
|
||||||
|
|
|
@ -2,6 +2,8 @@ import datetime
|
||||||
import functools
|
import functools
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.db.models import Count, Q
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from rest_framework import exceptions
|
from rest_framework import exceptions
|
||||||
from rest_framework import permissions as rest_permissions
|
from rest_framework import permissions as rest_permissions
|
||||||
|
@ -18,6 +20,7 @@ from funkwhale_api.music import models as music_models
|
||||||
from funkwhale_api.music import utils
|
from funkwhale_api.music import utils
|
||||||
from funkwhale_api.music import views as music_views
|
from funkwhale_api.music import views as music_views
|
||||||
from funkwhale_api.playlists import models as playlists_models
|
from funkwhale_api.playlists import models as playlists_models
|
||||||
|
from funkwhale_api.tags import models as tags_models
|
||||||
from funkwhale_api.users import models as users_models
|
from funkwhale_api.users import models as users_models
|
||||||
|
|
||||||
from . import authentication, filters, negotiation, serializers
|
from . import authentication, filters, negotiation, serializers
|
||||||
|
@ -362,6 +365,26 @@ class SubsonicViewSet(viewsets.GenericViewSet):
|
||||||
queryset = filterset.qs
|
queryset = filterset.qs
|
||||||
actor = utils.get_actor_from_request(request)
|
actor = utils.get_actor_from_request(request)
|
||||||
queryset = queryset.playable_by(actor)
|
queryset = queryset.playable_by(actor)
|
||||||
|
type = data.get("type", "alphabeticalByArtist")
|
||||||
|
|
||||||
|
if type == "alphabeticalByArtist":
|
||||||
|
queryset = queryset.order_by("artist__name")
|
||||||
|
elif type == "random":
|
||||||
|
queryset = queryset.order_by("?")
|
||||||
|
elif type == "alphabeticalByName" or not type:
|
||||||
|
queryset = queryset.order_by("artist__title")
|
||||||
|
elif type == "recent" or not type:
|
||||||
|
queryset = queryset.exclude(release_date__in=["", None]).order_by(
|
||||||
|
"-release_date"
|
||||||
|
)
|
||||||
|
elif type == "newest" or not type:
|
||||||
|
queryset = queryset.order_by("-creation_date")
|
||||||
|
elif type == "byGenre" and data.get("genre"):
|
||||||
|
genre = data.get("genre")
|
||||||
|
queryset = queryset.filter(
|
||||||
|
Q(tagged_items__tag__name=genre)
|
||||||
|
| Q(artist__tagged_items__tag__name=genre)
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
offset = int(data["offset"])
|
offset = int(data["offset"])
|
||||||
|
@ -669,3 +692,29 @@ class SubsonicViewSet(viewsets.GenericViewSet):
|
||||||
listening = serializer.save()
|
listening = serializer.save()
|
||||||
record.send(listening)
|
record.send(listening)
|
||||||
return response.Response({})
|
return response.Response({})
|
||||||
|
|
||||||
|
@action(
|
||||||
|
detail=False,
|
||||||
|
methods=["get", "post"],
|
||||||
|
url_name="get_genres",
|
||||||
|
url_path="getGenres",
|
||||||
|
)
|
||||||
|
def get_genres(self, request, *args, **kwargs):
|
||||||
|
album_ct = ContentType.objects.get_for_model(music_models.Album)
|
||||||
|
track_ct = ContentType.objects.get_for_model(music_models.Track)
|
||||||
|
queryset = (
|
||||||
|
tags_models.Tag.objects.annotate(
|
||||||
|
_albums_count=Count(
|
||||||
|
"tagged_items", filter=Q(tagged_items__content_type=album_ct)
|
||||||
|
),
|
||||||
|
_tracks_count=Count(
|
||||||
|
"tagged_items", filter=Q(tagged_items__content_type=track_ct)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.exclude(_tracks_count=0, _albums_count=0)
|
||||||
|
.order_by("name")
|
||||||
|
)
|
||||||
|
data = {
|
||||||
|
"genres": {"genre": [serializers.get_genre_data(tag) for tag in queryset]}
|
||||||
|
}
|
||||||
|
return response.Response(data)
|
||||||
|
|
|
@ -33,14 +33,12 @@ class TaggedItemQuerySet(models.QuerySet):
|
||||||
|
|
||||||
class TaggedItem(models.Model):
|
class TaggedItem(models.Model):
|
||||||
creation_date = models.DateTimeField(default=timezone.now)
|
creation_date = models.DateTimeField(default=timezone.now)
|
||||||
tag = models.ForeignKey(
|
tag = models.ForeignKey(Tag, related_name="tagged_items", on_delete=models.CASCADE)
|
||||||
Tag, related_name="%(app_label)s_%(class)s_items", on_delete=models.CASCADE
|
|
||||||
)
|
|
||||||
content_type = models.ForeignKey(
|
content_type = models.ForeignKey(
|
||||||
ContentType,
|
ContentType,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
verbose_name=_("Content type"),
|
verbose_name=_("Content type"),
|
||||||
related_name="%(app_label)s_%(class)s_tagged_items",
|
related_name="tagged_items",
|
||||||
)
|
)
|
||||||
object_id = models.IntegerField(verbose_name=_("Object id"), db_index=True)
|
object_id = models.IntegerField(verbose_name=_("Object id"), db_index=True)
|
||||||
content_object = GenericForeignKey()
|
content_object = GenericForeignKey()
|
||||||
|
|
|
@ -67,9 +67,12 @@ def test_json_renderer():
|
||||||
|
|
||||||
|
|
||||||
def test_xml_renderer_dict_to_xml():
|
def test_xml_renderer_dict_to_xml():
|
||||||
payload = {"hello": "world", "item": [{"this": 1}, {"some": "node"}]}
|
payload = {
|
||||||
|
"hello": "world",
|
||||||
|
"item": [{"this": 1, "value": "text"}, {"some": "node"}],
|
||||||
|
}
|
||||||
expected = """<?xml version="1.0" encoding="UTF-8"?>
|
expected = """<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<key hello="world"><item this="1" /><item some="node" /></key>"""
|
<key hello="world"><item this="1">text</item><item some="node" /></key>"""
|
||||||
result = renderers.dict_to_xml_tree("key", payload)
|
result = renderers.dict_to_xml_tree("key", payload)
|
||||||
exp = ET.fromstring(expected)
|
exp = ET.fromstring(expected)
|
||||||
assert ET.tostring(result) == ET.tostring(exp)
|
assert ET.tostring(result) == ET.tostring(exp)
|
||||||
|
|
|
@ -375,6 +375,28 @@ def test_get_random_songs(f, db, logged_in_api_client, factories, mocker):
|
||||||
order_by.assert_called_once_with("?")
|
order_by.assert_called_once_with("?")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("f", ["json"])
|
||||||
|
def test_get_genres(f, db, logged_in_api_client, factories, mocker):
|
||||||
|
url = reverse("api:subsonic-get_genres")
|
||||||
|
assert url.endswith("getGenres") is True
|
||||||
|
tag1 = factories["tags.Tag"](name="Pop")
|
||||||
|
tag2 = factories["tags.Tag"](name="Rock")
|
||||||
|
|
||||||
|
factories["music.Album"](set_tags=[tag1.name, tag2.name])
|
||||||
|
factories["music.Track"](set_tags=[tag1.name])
|
||||||
|
response = logged_in_api_client.get(url)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.data == {
|
||||||
|
"genres": {
|
||||||
|
"genre": [
|
||||||
|
{"songCount": 1, "albumCount": 1, "value": tag1.name},
|
||||||
|
{"songCount": 0, "albumCount": 1, "value": tag2.name},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("f", ["json"])
|
@pytest.mark.parametrize("f", ["json"])
|
||||||
def test_get_starred(f, db, logged_in_api_client, factories):
|
def test_get_starred(f, db, logged_in_api_client, factories):
|
||||||
url = reverse("api:subsonic-get_starred")
|
url = reverse("api:subsonic-get_starred")
|
||||||
|
@ -426,6 +448,27 @@ def test_get_album_list2_pagination(f, db, logged_in_api_client, factories):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("f", ["json"])
|
||||||
|
def test_get_album_list2_by_genre(f, db, logged_in_api_client, factories):
|
||||||
|
url = reverse("api:subsonic-get_album_list2")
|
||||||
|
assert url.endswith("getAlbumList2") is True
|
||||||
|
album1 = factories["music.Album"](
|
||||||
|
artist__name="Artist1", playable=True, set_tags=["Rock"]
|
||||||
|
)
|
||||||
|
album2 = factories["music.Album"](
|
||||||
|
artist__name="Artist2", playable=True, artist__set_tags=["Rock"]
|
||||||
|
)
|
||||||
|
factories["music.Album"](playable=True, set_tags=["Pop"])
|
||||||
|
response = logged_in_api_client.get(
|
||||||
|
url, {"f": f, "type": "byGenre", "size": 5, "offset": 0, "genre": "rock"}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.data == {
|
||||||
|
"albumList2": {"album": serializers.get_album_list2_data([album1, album2])}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("f", ["json"])
|
@pytest.mark.parametrize("f", ["json"])
|
||||||
def test_search3(f, db, logged_in_api_client, factories):
|
def test_search3(f, db, logged_in_api_client, factories):
|
||||||
url = reverse("api:subsonic-search3")
|
url = reverse("api:subsonic-search3")
|
||||||
|
|
Loading…
Reference in New Issue