From 989f66315b985b69c0370bb1576732fba8d41b33 Mon Sep 17 00:00:00 2001 From: Agate Date: Sun, 21 Jun 2020 16:21:16 +0200 Subject: [PATCH 1/4] Fix #1165: Fixed broken mimetype detection during import --- api/funkwhale_api/music/models.py | 2 +- api/funkwhale_api/music/utils.py | 18 ++++++++++++------ changes/changelog.d/1165.bugfix | 1 + 3 files changed, 14 insertions(+), 7 deletions(-) create mode 100644 changes/changelog.d/1165.bugfix diff --git a/api/funkwhale_api/music/models.py b/api/funkwhale_api/music/models.py index 43371d5d8..b058f2e80 100644 --- a/api/funkwhale_api/music/models.py +++ b/api/funkwhale_api/music/models.py @@ -874,7 +874,7 @@ class Upload(models.Model): if self.audio_file: self.mimetype = utils.guess_mimetype(self.audio_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: self.size = self.audio_file.size if not self.checksum: diff --git a/api/funkwhale_api/music/utils.py b/api/funkwhale_api/music/utils.py index 64a7c24f8..b61c8223b 100644 --- a/api/funkwhale_api/music/utils.py +++ b/api/funkwhale_api/music/utils.py @@ -18,12 +18,18 @@ def guess_mimetype(f): b = min(1000000, f.size) t = magic.from_buffer(f.read(b), mime=True) if not t.startswith("audio/"): - # failure, we try guessing by extension - mt, _ = mimetypes.guess_type(f.name) - if mt: - t = mt - else: - t = EXTENSION_TO_MIMETYPE.get(f.name.split(".")[-1]) + t = guess_mimetype_from_name(f.name) + + return t + + +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 diff --git a/changes/changelog.d/1165.bugfix b/changes/changelog.d/1165.bugfix new file mode 100644 index 000000000..e1b1b10e2 --- /dev/null +++ b/changes/changelog.d/1165.bugfix @@ -0,0 +1 @@ +Fixed broken mimetype detection during import (#1165) From 0ebd7c610d01961b0b1425e002ab97675d1e3089 Mon Sep 17 00:00:00 2001 From: Agate Date: Sun, 21 Jun 2020 16:38:57 +0200 Subject: [PATCH 2/4] Linting --- api/funkwhale_api/music/models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/api/funkwhale_api/music/models.py b/api/funkwhale_api/music/models.py index b058f2e80..e020a619b 100644 --- a/api/funkwhale_api/music/models.py +++ b/api/funkwhale_api/music/models.py @@ -1,6 +1,5 @@ import datetime import logging -import mimetypes import os import tempfile import urllib.parse From e6344338f98d4a0209141207e4aedda9bbe8821d Mon Sep 17 00:00:00 2001 From: Agate Date: Mon, 22 Jun 2020 13:22:26 +0200 Subject: [PATCH 3/4] Fix #1145: Support ordering=random for artists, albums, tracks and channels endpoints --- api/funkwhale_api/audio/filters.py | 1 + api/funkwhale_api/music/filters.py | 35 ++++++++++++++++++++++++++++++ api/funkwhale_api/music/views.py | 18 --------------- 3 files changed, 36 insertions(+), 18 deletions(-) diff --git a/api/funkwhale_api/audio/filters.py b/api/funkwhale_api/audio/filters.py index 6b1a9e8d9..9b7088c1d 100644 --- a/api/funkwhale_api/audio/filters.py +++ b/api/funkwhale_api/audio/filters.py @@ -35,6 +35,7 @@ class ChannelFilter(moderation_filters.HiddenContentFilterSet): fields=( ("creation_date", "creation_date"), ("artist__modification_date", "modification_date"), + ("?", "random"), ) ) diff --git a/api/funkwhale_api/music/filters.py b/api/funkwhale_api/music/filters.py index d69dd13a3..7f09924d1 100644 --- a/api/funkwhale_api/music/filters.py +++ b/api/funkwhale_api/music/filters.py @@ -1,5 +1,6 @@ from django.db.models import Q +import django_filters from django_filters import rest_framework as filters from funkwhale_api.audio import filters as audio_filters @@ -80,6 +81,15 @@ class ArtistFilter( scope = common_filters.ActorScopeFilter( 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"), + ) + ) class Meta: model = models.Artist @@ -118,6 +128,21 @@ class TrackFilter( 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"), + ) + ) + class Meta: model = models.Track fields = { @@ -207,6 +232,16 @@ class AlbumFilter( 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"), + ) + ) + class Meta: model = models.Album fields = ["playable", "q", "artist", "scope", "mbid"] diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py index 6c9f7e41c..e38b15887 100644 --- a/api/funkwhale_api/music/views.py +++ b/api/funkwhale_api/music/views.py @@ -130,7 +130,6 @@ class ArtistViewSet( required_scope = "libraries" anonymous_policy = "setting" filterset_class = filters.ArtistFilter - ordering_fields = ("id", "name", "creation_date", "modification_date") fetches = federation_decorators.fetches_route() mutations = common_decorators.mutations_route(types=["update"]) @@ -187,12 +186,6 @@ class AlbumViewSet( permission_classes = [oauth_permissions.ScopePermission] required_scope = "libraries" anonymous_policy = "setting" - ordering_fields = ( - "creation_date", - "release_date", - "title", - "artist__modification_date", - ) filterset_class = filters.AlbumFilter fetches = federation_decorators.fetches_route() @@ -345,17 +338,6 @@ class TrackViewSet( required_scope = "libraries" anonymous_policy = "setting" 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() mutations = common_decorators.mutations_route(types=["update"]) From d50cce36e2537af3732d6908805e02dac193f63f Mon Sep 17 00:00:00 2001 From: Agate Date: Mon, 22 Jun 2020 14:39:50 +0200 Subject: [PATCH 4/4] Added a new ?related=obj_id filter for artists, albums and tracks, based on tags --- api/funkwhale_api/music/filters.py | 29 ++++++++++++++++ api/funkwhale_api/tags/filters.py | 18 ++++++++++ api/tests/music/test_filters.py | 38 +++++++++++++++++++++ changes/changelog.d/1145.enhancement | 1 + changes/changelog.d/api-related.enhancement | 1 + 5 files changed, 87 insertions(+) create mode 100644 changes/changelog.d/1145.enhancement create mode 100644 changes/changelog.d/api-related.enhancement diff --git a/api/funkwhale_api/music/filters.py b/api/funkwhale_api/music/filters.py index 7f09924d1..feebaa542 100644 --- a/api/funkwhale_api/music/filters.py +++ b/api/funkwhale_api/music/filters.py @@ -9,6 +9,7 @@ from funkwhale_api.common import fields from funkwhale_api.common import filters as common_filters from funkwhale_api.common import search from funkwhale_api.moderation import filters as moderation_filters +from funkwhale_api.tags import filters as tags_filters from . import models from . import utils @@ -24,6 +25,28 @@ def filter_tags(queryset, name, value): 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): channel = filters.CharFilter(field_name="_", method="filter_channel") @@ -70,6 +93,7 @@ class LibraryFilterSet(filters.FilterSet): class ArtistFilter( + RelatedFilterSet, LibraryFilterSet, audio_filters.IncludeChannelsFilterSet, moderation_filters.HiddenContentFilterSet, @@ -88,6 +112,7 @@ class ArtistFilter( ("creation_date", "creation_date"), ("modification_date", "modification_date"), ("?", "random"), + ("tag_matches", "related"), ) ) @@ -109,6 +134,7 @@ class ArtistFilter( class TrackFilter( + RelatedFilterSet, ChannelFilterSet, LibraryFilterSet, audio_filters.IncludeChannelsFilterSet, @@ -140,6 +166,7 @@ class TrackFilter( ("artist__name", "artist__name"), ("artist__modification_date", "artist__modification_date"), ("?", "random"), + ("tag_matches", "related"), ) ) @@ -217,6 +244,7 @@ class UploadFilter(audio_filters.IncludeChannelsFilterSet): class AlbumFilter( + RelatedFilterSet, ChannelFilterSet, LibraryFilterSet, audio_filters.IncludeChannelsFilterSet, @@ -239,6 +267,7 @@ class AlbumFilter( ("title", "title"), ("artist__modification_date", "artist__modification_date"), ("?", "random"), + ("tag_matches", "related"), ) ) diff --git a/api/funkwhale_api/tags/filters.py b/api/funkwhale_api/tags/filters.py index e0ac9675a..c41ace91b 100644 --- a/api/funkwhale_api/tags/filters.py +++ b/api/funkwhale_api/tags/filters.py @@ -1,3 +1,5 @@ +from django.db import models as dj_models + import django_filters from django_filters import rest_framework as filters @@ -19,3 +21,19 @@ class TagFilter(filters.FilterSet): class Meta: model = models.Tag 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() diff --git a/api/tests/music/test_filters.py b/api/tests/music/test_filters.py index 87d8c4816..f078932a8 100644 --- a/api/tests/music/test_filters.py +++ b/api/tests/music/test_filters.py @@ -203,3 +203,41 @@ def test_track_filter_artist_includes_album_artist( ) 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 diff --git a/changes/changelog.d/1145.enhancement b/changes/changelog.d/1145.enhancement new file mode 100644 index 000000000..eefc8d46d --- /dev/null +++ b/changes/changelog.d/1145.enhancement @@ -0,0 +1 @@ +Support ordering=random for artists, albums, tracks and channels endpoints (#1145) diff --git a/changes/changelog.d/api-related.enhancement b/changes/changelog.d/api-related.enhancement new file mode 100644 index 000000000..82be1c0f0 --- /dev/null +++ b/changes/changelog.d/api-related.enhancement @@ -0,0 +1 @@ +Added a new ?related=obj_id filter for artists, albums and tracks, based on tags