Merge branch 'master' into develop
This commit is contained in:
commit
e730ed45d5
|
@ -35,6 +35,7 @@ class ChannelFilter(moderation_filters.HiddenContentFilterSet):
|
||||||
fields=(
|
fields=(
|
||||||
("creation_date", "creation_date"),
|
("creation_date", "creation_date"),
|
||||||
("artist__modification_date", "modification_date"),
|
("artist__modification_date", "modification_date"),
|
||||||
|
("?", "random"),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
|
import django_filters
|
||||||
from django_filters import rest_framework as filters
|
from django_filters import rest_framework as filters
|
||||||
|
|
||||||
from funkwhale_api.audio import filters as audio_filters
|
from funkwhale_api.audio import filters as audio_filters
|
||||||
|
@ -8,6 +9,7 @@ from funkwhale_api.common import fields
|
||||||
from funkwhale_api.common import filters as common_filters
|
from funkwhale_api.common import filters as common_filters
|
||||||
from funkwhale_api.common import search
|
from funkwhale_api.common import search
|
||||||
from funkwhale_api.moderation import filters as moderation_filters
|
from funkwhale_api.moderation import filters as moderation_filters
|
||||||
|
from funkwhale_api.tags import filters as tags_filters
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
from . import utils
|
from . import utils
|
||||||
|
@ -23,6 +25,28 @@ def filter_tags(queryset, name, value):
|
||||||
TAG_FILTER = common_filters.MultipleQueryFilter(method=filter_tags)
|
TAG_FILTER = common_filters.MultipleQueryFilter(method=filter_tags)
|
||||||
|
|
||||||
|
|
||||||
|
class RelatedFilterSet(filters.FilterSet):
|
||||||
|
related_type = int
|
||||||
|
related_field = "pk"
|
||||||
|
related = filters.CharFilter(field_name="_", method="filter_related")
|
||||||
|
|
||||||
|
def filter_related(self, queryset, name, value):
|
||||||
|
if not value:
|
||||||
|
return queryset.none()
|
||||||
|
try:
|
||||||
|
pk = self.related_type(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return queryset.none()
|
||||||
|
|
||||||
|
try:
|
||||||
|
obj = queryset.model.objects.get(**{self.related_field: pk})
|
||||||
|
except queryset.model.DoesNotExist:
|
||||||
|
return queryset.none()
|
||||||
|
|
||||||
|
queryset = queryset.exclude(pk=obj.pk)
|
||||||
|
return tags_filters.get_by_similar_tags(queryset, obj.get_tags())
|
||||||
|
|
||||||
|
|
||||||
class ChannelFilterSet(filters.FilterSet):
|
class ChannelFilterSet(filters.FilterSet):
|
||||||
|
|
||||||
channel = filters.CharFilter(field_name="_", method="filter_channel")
|
channel = filters.CharFilter(field_name="_", method="filter_channel")
|
||||||
|
@ -69,6 +93,7 @@ class LibraryFilterSet(filters.FilterSet):
|
||||||
|
|
||||||
|
|
||||||
class ArtistFilter(
|
class ArtistFilter(
|
||||||
|
RelatedFilterSet,
|
||||||
LibraryFilterSet,
|
LibraryFilterSet,
|
||||||
audio_filters.IncludeChannelsFilterSet,
|
audio_filters.IncludeChannelsFilterSet,
|
||||||
moderation_filters.HiddenContentFilterSet,
|
moderation_filters.HiddenContentFilterSet,
|
||||||
|
@ -80,6 +105,16 @@ class ArtistFilter(
|
||||||
scope = common_filters.ActorScopeFilter(
|
scope = common_filters.ActorScopeFilter(
|
||||||
actor_field="tracks__uploads__library__actor", distinct=True
|
actor_field="tracks__uploads__library__actor", distinct=True
|
||||||
)
|
)
|
||||||
|
ordering = django_filters.OrderingFilter(
|
||||||
|
fields=(
|
||||||
|
("id", "id"),
|
||||||
|
("name", "name"),
|
||||||
|
("creation_date", "creation_date"),
|
||||||
|
("modification_date", "modification_date"),
|
||||||
|
("?", "random"),
|
||||||
|
("tag_matches", "related"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Artist
|
model = models.Artist
|
||||||
|
@ -99,6 +134,7 @@ class ArtistFilter(
|
||||||
|
|
||||||
|
|
||||||
class TrackFilter(
|
class TrackFilter(
|
||||||
|
RelatedFilterSet,
|
||||||
ChannelFilterSet,
|
ChannelFilterSet,
|
||||||
LibraryFilterSet,
|
LibraryFilterSet,
|
||||||
audio_filters.IncludeChannelsFilterSet,
|
audio_filters.IncludeChannelsFilterSet,
|
||||||
|
@ -118,6 +154,22 @@ class TrackFilter(
|
||||||
field_name="_", method="filter_artist", queryset=models.Artist.objects.all()
|
field_name="_", method="filter_artist", queryset=models.Artist.objects.all()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ordering = django_filters.OrderingFilter(
|
||||||
|
fields=(
|
||||||
|
("creation_date", "creation_date"),
|
||||||
|
("title", "title"),
|
||||||
|
("album__title", "album__title"),
|
||||||
|
("album__release_date", "album__release_date"),
|
||||||
|
("size", "size"),
|
||||||
|
("position", "position"),
|
||||||
|
("disc_number", "disc_number"),
|
||||||
|
("artist__name", "artist__name"),
|
||||||
|
("artist__modification_date", "artist__modification_date"),
|
||||||
|
("?", "random"),
|
||||||
|
("tag_matches", "related"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Track
|
model = models.Track
|
||||||
fields = {
|
fields = {
|
||||||
|
@ -192,6 +244,7 @@ class UploadFilter(audio_filters.IncludeChannelsFilterSet):
|
||||||
|
|
||||||
|
|
||||||
class AlbumFilter(
|
class AlbumFilter(
|
||||||
|
RelatedFilterSet,
|
||||||
ChannelFilterSet,
|
ChannelFilterSet,
|
||||||
LibraryFilterSet,
|
LibraryFilterSet,
|
||||||
audio_filters.IncludeChannelsFilterSet,
|
audio_filters.IncludeChannelsFilterSet,
|
||||||
|
@ -207,6 +260,17 @@ class AlbumFilter(
|
||||||
actor_field="tracks__uploads__library__actor", distinct=True
|
actor_field="tracks__uploads__library__actor", distinct=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ordering = django_filters.OrderingFilter(
|
||||||
|
fields=(
|
||||||
|
("creation_date", "creation_date"),
|
||||||
|
("release_date", "release_date"),
|
||||||
|
("title", "title"),
|
||||||
|
("artist__modification_date", "artist__modification_date"),
|
||||||
|
("?", "random"),
|
||||||
|
("tag_matches", "related"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Album
|
model = models.Album
|
||||||
fields = ["playable", "q", "artist", "scope", "mbid"]
|
fields = ["playable", "q", "artist", "scope", "mbid"]
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
import mimetypes
|
|
||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
@ -874,7 +873,7 @@ class Upload(models.Model):
|
||||||
if self.audio_file:
|
if self.audio_file:
|
||||||
self.mimetype = utils.guess_mimetype(self.audio_file)
|
self.mimetype = utils.guess_mimetype(self.audio_file)
|
||||||
elif self.source and self.source.startswith("file://"):
|
elif self.source and self.source.startswith("file://"):
|
||||||
self.mimetype = mimetypes.guess_type(self.source)[0]
|
self.mimetype = utils.guess_mimetype_from_name(self.source)
|
||||||
if not self.size and self.audio_file:
|
if not self.size and self.audio_file:
|
||||||
self.size = self.audio_file.size
|
self.size = self.audio_file.size
|
||||||
if not self.checksum:
|
if not self.checksum:
|
||||||
|
|
|
@ -18,12 +18,18 @@ def guess_mimetype(f):
|
||||||
b = min(1000000, f.size)
|
b = min(1000000, f.size)
|
||||||
t = magic.from_buffer(f.read(b), mime=True)
|
t = magic.from_buffer(f.read(b), mime=True)
|
||||||
if not t.startswith("audio/"):
|
if not t.startswith("audio/"):
|
||||||
# failure, we try guessing by extension
|
t = guess_mimetype_from_name(f.name)
|
||||||
mt, _ = mimetypes.guess_type(f.name)
|
|
||||||
if mt:
|
return t
|
||||||
t = mt
|
|
||||||
else:
|
|
||||||
t = EXTENSION_TO_MIMETYPE.get(f.name.split(".")[-1])
|
def guess_mimetype_from_name(name):
|
||||||
|
# failure, we try guessing by extension
|
||||||
|
mt, _ = mimetypes.guess_type(name)
|
||||||
|
if mt:
|
||||||
|
t = mt
|
||||||
|
else:
|
||||||
|
t = EXTENSION_TO_MIMETYPE.get(name.split(".")[-1])
|
||||||
return t
|
return t
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -131,7 +131,6 @@ class ArtistViewSet(
|
||||||
required_scope = "libraries"
|
required_scope = "libraries"
|
||||||
anonymous_policy = "setting"
|
anonymous_policy = "setting"
|
||||||
filterset_class = filters.ArtistFilter
|
filterset_class = filters.ArtistFilter
|
||||||
ordering_fields = ("id", "name", "creation_date", "modification_date")
|
|
||||||
|
|
||||||
fetches = federation_decorators.fetches_route()
|
fetches = federation_decorators.fetches_route()
|
||||||
mutations = common_decorators.mutations_route(types=["update"])
|
mutations = common_decorators.mutations_route(types=["update"])
|
||||||
|
@ -188,12 +187,6 @@ class AlbumViewSet(
|
||||||
permission_classes = [oauth_permissions.ScopePermission]
|
permission_classes = [oauth_permissions.ScopePermission]
|
||||||
required_scope = "libraries"
|
required_scope = "libraries"
|
||||||
anonymous_policy = "setting"
|
anonymous_policy = "setting"
|
||||||
ordering_fields = (
|
|
||||||
"creation_date",
|
|
||||||
"release_date",
|
|
||||||
"title",
|
|
||||||
"artist__modification_date",
|
|
||||||
)
|
|
||||||
filterset_class = filters.AlbumFilter
|
filterset_class = filters.AlbumFilter
|
||||||
|
|
||||||
fetches = federation_decorators.fetches_route()
|
fetches = federation_decorators.fetches_route()
|
||||||
|
@ -346,17 +339,6 @@ class TrackViewSet(
|
||||||
required_scope = "libraries"
|
required_scope = "libraries"
|
||||||
anonymous_policy = "setting"
|
anonymous_policy = "setting"
|
||||||
filterset_class = filters.TrackFilter
|
filterset_class = filters.TrackFilter
|
||||||
ordering_fields = (
|
|
||||||
"creation_date",
|
|
||||||
"title",
|
|
||||||
"album__title",
|
|
||||||
"album__release_date",
|
|
||||||
"size",
|
|
||||||
"position",
|
|
||||||
"disc_number",
|
|
||||||
"artist__name",
|
|
||||||
"artist__modification_date",
|
|
||||||
)
|
|
||||||
fetches = federation_decorators.fetches_route()
|
fetches = federation_decorators.fetches_route()
|
||||||
mutations = common_decorators.mutations_route(types=["update"])
|
mutations = common_decorators.mutations_route(types=["update"])
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
from django.db import models as dj_models
|
||||||
|
|
||||||
import django_filters
|
import django_filters
|
||||||
from django_filters import rest_framework as filters
|
from django_filters import rest_framework as filters
|
||||||
|
|
||||||
|
@ -19,3 +21,19 @@ class TagFilter(filters.FilterSet):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Tag
|
model = models.Tag
|
||||||
fields = {"q": ["exact"], "name": ["exact", "startswith"]}
|
fields = {"q": ["exact"], "name": ["exact", "startswith"]}
|
||||||
|
|
||||||
|
|
||||||
|
def get_by_similar_tags(qs, tags):
|
||||||
|
"""
|
||||||
|
Return a queryset of obects with at least one matching tag.
|
||||||
|
Annotate the queryset so you can order later by number of matches.
|
||||||
|
"""
|
||||||
|
qs = qs.filter(tagged_items__tag__name__in=tags).annotate(
|
||||||
|
tag_matches=dj_models.Count(
|
||||||
|
dj_models.Case(
|
||||||
|
dj_models.When(tagged_items__tag__name__in=tags, then=1),
|
||||||
|
output_field=dj_models.IntegerField(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return qs.distinct()
|
||||||
|
|
|
@ -203,3 +203,41 @@ def test_track_filter_artist_includes_album_artist(
|
||||||
)
|
)
|
||||||
|
|
||||||
assert filterset.qs == [track2, track1]
|
assert filterset.qs == [track2, track1]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"factory_name, filterset_class",
|
||||||
|
[
|
||||||
|
("music.Track", filters.TrackFilter),
|
||||||
|
("music.Artist", filters.ArtistFilter),
|
||||||
|
("music.Album", filters.AlbumFilter),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_filter_tag_related(
|
||||||
|
factory_name,
|
||||||
|
filterset_class,
|
||||||
|
factories,
|
||||||
|
anonymous_user,
|
||||||
|
queryset_equal_list,
|
||||||
|
mocker,
|
||||||
|
):
|
||||||
|
factories["tags.Tag"](name="foo")
|
||||||
|
factories["tags.Tag"](name="bar")
|
||||||
|
factories["tags.Tag"](name="baz")
|
||||||
|
factories["tags.Tag"]()
|
||||||
|
factories["tags.Tag"]()
|
||||||
|
|
||||||
|
matches = [
|
||||||
|
factories[factory_name](set_tags=["foo", "bar", "baz", "noop"]),
|
||||||
|
factories[factory_name](set_tags=["foo", "baz", "noop"]),
|
||||||
|
factories[factory_name](set_tags=["baz", "noop"]),
|
||||||
|
]
|
||||||
|
factories[factory_name](set_tags=["noop"]),
|
||||||
|
obj = factories[factory_name](set_tags=["foo", "bar", "baz"])
|
||||||
|
|
||||||
|
filterset = filterset_class(
|
||||||
|
{"related": obj.pk, "ordering": "-related"},
|
||||||
|
request=mocker.Mock(user=anonymous_user, actor=None),
|
||||||
|
queryset=obj.__class__.objects.all(),
|
||||||
|
)
|
||||||
|
assert filterset.qs == matches
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Support ordering=random for artists, albums, tracks and channels endpoints (#1145)
|
|
@ -0,0 +1 @@
|
||||||
|
Fixed broken mimetype detection during import (#1165)
|
|
@ -0,0 +1 @@
|
||||||
|
Added a new ?related=obj_id filter for artists, albums and tracks, based on tags
|
Loading…
Reference in New Issue