diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py
index 90b833a2b..34ec02632 100644
--- a/api/funkwhale_api/music/views.py
+++ b/api/funkwhale_api/music/views.py
@@ -4,7 +4,6 @@ import urllib
from django.conf import settings
from django.db import transaction
from django.db.models import Count, Prefetch, Sum, F, Q
-from django.db.models.functions import Length
from django.utils import timezone
from rest_framework import mixins
@@ -24,6 +23,7 @@ from funkwhale_api.federation import api_serializers as federation_api_serialize
from funkwhale_api.federation import decorators as federation_decorators
from funkwhale_api.federation import routes
from funkwhale_api.tags.models import Tag, TaggedItem
+from funkwhale_api.tags.serializers import TagSerializer
from funkwhale_api.users.oauth import permissions as oauth_permissions
from . import filters, licenses, models, serializers, tasks, utils
@@ -339,7 +339,7 @@ def handle_serve(upload, user, format=None, max_bitrate=None, proxy_media=True):
f = transcoded_version
file_path = get_file_path(f.audio_file)
mt = f.mimetype
- if not proxy_media:
+ if not proxy_media and f.audio_file:
# we simply issue a 302 redirect to the real URL
response = Response(status=302)
response["Location"] = f.audio_file.url
@@ -482,6 +482,7 @@ class Search(views.APIView):
"albums": serializers.AlbumSerializer(
self.get_albums(query), many=True
).data,
+ "tags": TagSerializer(self.get_tags(query), many=True).data,
}
return Response(results, status=200)
@@ -520,15 +521,8 @@ class Search(views.APIView):
def get_tags(self, query):
search_fields = ["name__unaccent"]
query_obj = utils.get_query(query, search_fields)
-
- # We want the shortest tag first
- qs = (
- Tag.objects.all()
- .annotate(name_length=Length("name"))
- .order_by("name_length")
- )
-
- return qs.filter(query_obj)[: self.max_results]
+ qs = Tag.objects.all().filter(query_obj)
+ return common_utils.order_for_search(qs, "name")[: self.max_results]
class LicenseViewSet(viewsets.ReadOnlyModelViewSet):
diff --git a/api/funkwhale_api/radios/filters.py b/api/funkwhale_api/radios/filters.py
index 810673bd6..a92dbae88 100644
--- a/api/funkwhale_api/radios/filters.py
+++ b/api/funkwhale_api/radios/filters.py
@@ -178,9 +178,9 @@ class TagFilter(RadioFilter):
"autocomplete_fields": {
"remoteValues": "results",
"name": "name",
- "value": "slug",
+ "value": "name",
},
- "autocomplete_qs": "query={query}",
+ "autocomplete_qs": "q={query}&ordering=length",
"label": "Tags",
"placeholder": "Select tags",
}
@@ -189,4 +189,8 @@ class TagFilter(RadioFilter):
label = "Tag"
def get_query(self, candidates, names, **kwargs):
- return Q(tags__slug__in=names)
+ return (
+ Q(tagged_items__tag__name__in=names)
+ | Q(artist__tagged_items__tag__name__in=names)
+ | Q(album__tagged_items__tag__name__in=names)
+ )
diff --git a/api/funkwhale_api/radios/radios.py b/api/funkwhale_api/radios/radios.py
index 8940cdc15..86c84ad13 100644
--- a/api/funkwhale_api/radios/radios.py
+++ b/api/funkwhale_api/radios/radios.py
@@ -2,6 +2,7 @@ import random
from django.core.exceptions import ValidationError
from django.db import connection
+from django.db.models import Q
from rest_framework import serializers
from funkwhale_api.moderation import filters as moderation_filters
@@ -14,6 +15,8 @@ from .registries import registry
class SimpleRadio(object):
+ related_object_field = None
+
def clean(self, instance):
return
@@ -146,6 +149,8 @@ class CustomRadio(SessionRadio):
class RelatedObjectRadio(SessionRadio):
"""Abstract radio related to an object (tag, artist, user...)"""
+ related_object_field = serializers.IntegerField(required=True)
+
def clean(self, instance):
super().clean(instance)
if not instance.related_object:
@@ -162,10 +167,22 @@ class RelatedObjectRadio(SessionRadio):
@registry.register(name="tag")
class TagRadio(RelatedObjectRadio):
model = Tag
+ related_object_field = serializers.CharField(required=True)
+
+ def get_related_object(self, name):
+ return self.model.objects.get(name=name)
def get_queryset(self, **kwargs):
qs = super().get_queryset(**kwargs)
- return qs.filter(tagged_items__tag=self.session.related_object)
+ query = (
+ Q(tagged_items__tag=self.session.related_object)
+ | Q(artist__tagged_items__tag=self.session.related_object)
+ | Q(album__tagged_items__tag=self.session.related_object)
+ )
+ return qs.filter(query)
+
+ def get_related_object_id_repr(self, obj):
+ return obj.name
def weighted_choice(choices):
diff --git a/api/funkwhale_api/radios/serializers.py b/api/funkwhale_api/radios/serializers.py
index 397452ecc..65e48449a 100644
--- a/api/funkwhale_api/radios/serializers.py
+++ b/api/funkwhale_api/radios/serializers.py
@@ -54,6 +54,9 @@ class RadioSessionTrackSerializer(serializers.ModelSerializer):
class RadioSessionSerializer(serializers.ModelSerializer):
+
+ related_object_id = serializers.CharField(required=False, allow_null=True)
+
class Meta:
model = models.RadioSession
fields = (
@@ -66,7 +69,17 @@ class RadioSessionSerializer(serializers.ModelSerializer):
)
def validate(self, data):
- registry[data["radio_type"]]().validate_session(data, **self.context)
+ radio_conf = registry[data["radio_type"]]()
+ if radio_conf.related_object_field:
+ try:
+ data[
+ "related_object_id"
+ ] = radio_conf.related_object_field.to_internal_value(
+ data["related_object_id"]
+ )
+ except KeyError:
+ raise serializers.ValidationError("Radio requires a related object")
+ radio_conf.validate_session(data, **self.context)
return data
def create(self, validated_data):
@@ -77,3 +90,11 @@ class RadioSessionSerializer(serializers.ModelSerializer):
validated_data["related_object_id"]
)
return super().create(validated_data)
+
+ def to_representation(self, instance):
+ repr = super().to_representation(instance)
+ radio_conf = registry[repr["radio_type"]]()
+ handler = getattr(radio_conf, "get_related_object_id_repr", None)
+ if handler and instance.related_object:
+ repr["related_object_id"] = handler(instance.related_object)
+ return repr
diff --git a/api/tests/radios/test_radios.py b/api/tests/radios/test_radios.py
index 36f76e1c1..040217aac 100644
--- a/api/tests/radios/test_radios.py
+++ b/api/tests/radios/test_radios.py
@@ -197,16 +197,19 @@ def test_can_start_artist_radio(factories):
def test_can_start_tag_radio(factories):
user = factories["users.User"]()
- factories["music.Upload"].create_batch(5)
tag = factories["tags.Tag"]()
- good_files = factories["music.Upload"].create_batch(5, track__set_tags=[tag])
- good_tracks = [f.track for f in good_files]
+ good_tracks = [
+ factories["music.Track"](set_tags=[tag.name]),
+ factories["music.Track"](album__set_tags=[tag.name]),
+ factories["music.Track"](album__artist__set_tags=[tag.name]),
+ ]
+ factories["music.Track"].create_batch(3, set_tags=["notrock"])
radio = radios.TagRadio()
session = radio.start_session(user, related_object=tag)
assert session.radio_type == "tag"
- for i in range(5):
+ for i in range(3):
assert radio.pick(filter_playable=False) in good_tracks
diff --git a/api/tests/radios/test_serializers.py b/api/tests/radios/test_serializers.py
new file mode 100644
index 000000000..54019d5e1
--- /dev/null
+++ b/api/tests/radios/test_serializers.py
@@ -0,0 +1,44 @@
+from funkwhale_api.radios import serializers
+
+
+def test_create_tag_radio(factories):
+ tag = factories["tags.Tag"]()
+
+ data = {"radio_type": "tag", "related_object_id": tag.name}
+
+ serializer = serializers.RadioSessionSerializer(data=data)
+ assert serializer.is_valid(raise_exception=True) is True
+
+ session = serializer.save()
+
+ assert session.related_object_id == tag.pk
+ assert session.related_object == tag
+
+
+def test_create_artist_radio(factories):
+ artist = factories["music.Artist"]()
+
+ data = {"radio_type": "artist", "related_object_id": artist.pk}
+
+ serializer = serializers.RadioSessionSerializer(data=data)
+ assert serializer.is_valid(raise_exception=True) is True
+
+ session = serializer.save()
+
+ assert session.related_object_id == artist.pk
+ assert session.related_object == artist
+
+
+def test_tag_radio_repr(factories):
+ tag = factories["tags.Tag"]()
+ session = factories["radios.RadioSession"](related_object=tag, radio_type="tag")
+
+ expected = {
+ "id": session.pk,
+ "radio_type": "tag",
+ "custom_radio": None,
+ "user": session.user.pk,
+ "related_object_id": tag.name,
+ "creation_date": session.creation_date.isoformat().split("+")[0] + "Z",
+ }
+ assert serializers.RadioSessionSerializer(session).data == expected
diff --git a/front/src/components/audio/SearchBar.vue b/front/src/components/audio/SearchBar.vue
index e0c343fed..d7445d13b 100644
--- a/front/src/components/audio/SearchBar.vue
+++ b/front/src/components/audio/SearchBar.vue
@@ -32,6 +32,7 @@ export default {
let artistLabel = this.$pgettext('*/*/*/Noun', 'Artist')
let albumLabel = this.$pgettext('*/*/*', 'Album')
let trackLabel = this.$pgettext('*/*/*/Noun', 'Track')
+ let tagLabel = this.$pgettext('*/*/*/Noun', 'Tag')
let self = this
var searchQuery;
@@ -75,6 +76,9 @@ export default {
},
getDescription (r) {
return ''
+ },
+ getId (t) {
+ return t.id
}
},
{
@@ -86,6 +90,9 @@ export default {
},
getDescription (r) {
return ''
+ },
+ getId (t) {
+ return t.id
}
},
{
@@ -97,6 +104,23 @@ export default {
},
getDescription (r) {
return ''
+ },
+ getId (t) {
+ return t.id
+ }
+ },
+ {
+ code: 'tags',
+ route: 'library.tags.detail',
+ name: tagLabel,
+ getTitle (r) {
+ return r.name
+ },
+ getDescription (r) {
+ return ''
+ },
+ getId (t) {
+ return t.name
}
}
]
@@ -106,13 +130,14 @@ export default {
results: []
}
initialResponse[category.code].forEach(result => {
+ let id = category.getId(result)
results[category.code].results.push({
title: category.getTitle(result),
- id: result.id,
+ id,
routerUrl: {
name: category.route,
params: {
- id: result.id
+ id
}
},
description: category.getDescription(result)
diff --git a/front/src/components/library/TagDetail.vue b/front/src/components/library/TagDetail.vue
index 86ad1562a..904bce7ae 100644
--- a/front/src/components/library/TagDetail.vue
+++ b/front/src/components/library/TagDetail.vue
@@ -6,6 +6,7 @@
{{ labels.title }}
+