From 435133135502c5bd28b4b1c478c0dd3abcc3fce5 Mon Sep 17 00:00:00 2001 From: Agate Date: Tue, 21 Jul 2020 10:19:14 +0200 Subject: [PATCH 1/2] Fix #1175: Fix embedded player not working on channel serie/album --- .gitlab-ci.yml | 1 + api/funkwhale_api/music/serializers.py | 3 +++ api/funkwhale_api/music/views.py | 2 +- changes/changelog.d/1175.bugfix | 1 + docs/conf.py | 6 +++--- front/src/EmbedFrame.vue | 1 + front/src/components/library/AlbumDropdown.vue | 2 +- front/src/components/library/ArtistBase.vue | 2 +- front/src/components/library/TrackBase.vue | 2 +- front/src/views/channels/DetailBase.vue | 2 +- front/src/views/playlists/Detail.vue | 2 +- 11 files changed, 15 insertions(+), 9 deletions(-) create mode 100644 changes/changelog.d/1175.bugfix diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7a7e106e8..e10b8b981 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -136,6 +136,7 @@ test_api: - branches before_script: - apk add make git gcc python3-dev musl-dev + - apk add postgresql-dev py3-psycopg2 libldap libffi-dev make zlib-dev jpeg-dev openldap-dev - cd api - pip3 install -r requirements/base.txt - pip3 install -r requirements/local.txt diff --git a/api/funkwhale_api/music/serializers.py b/api/funkwhale_api/music/serializers.py index 6abd6e262..3f68fd9fc 100644 --- a/api/funkwhale_api/music/serializers.py +++ b/api/funkwhale_api/music/serializers.py @@ -177,6 +177,8 @@ def serialize_artist_simple(artist): if artist.attachment_cover else None ) + if "channel" in artist._state.fields_cache and artist.get_channel(): + data["channel"] = str(artist.channel.uuid) if getattr(artist, "_tracks_count", None) is not None: data["tracks_count"] = artist._tracks_count @@ -857,4 +859,5 @@ class AlbumCreateSerializer(serializers.Serializer): instance, "description", validated_data.get("description") ) tag_models.set_tags(instance, *(validated_data.get("tags", []) or [])) + instance.artist.get_channel() return instance diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py index e38b15887..6006ce9c3 100644 --- a/api/funkwhale_api/music/views.py +++ b/api/funkwhale_api/music/views.py @@ -180,7 +180,7 @@ class AlbumViewSet( queryset = ( models.Album.objects.all() .order_by("-creation_date") - .prefetch_related("artist", "attributed_to", "attachment_cover") + .prefetch_related("artist__channel", "attributed_to", "attachment_cover") ) serializer_class = serializers.AlbumSerializer permission_classes = [oauth_permissions.ScopePermission] diff --git a/changes/changelog.d/1175.bugfix b/changes/changelog.d/1175.bugfix new file mode 100644 index 000000000..29283b2d3 --- /dev/null +++ b/changes/changelog.d/1175.bugfix @@ -0,0 +1 @@ +Fix embedded player not working on channel serie/album (#1175) \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index ae278c5e2..9cbeb8198 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -30,9 +30,9 @@ FUNKWHALE_CONFIG = { "FUNKWHALE_URL": "mypod.funkwhale", "FUNKWHAL_PROTOCOL": "https", "DATABASE_URL": "postgres://localhost:5432/db", - "AWS_ACCESS_KEY_ID": 'my_access_key', - "AWS_SECRET_ACCESS_KEY": 'my_secret_key', - "AWS_STORAGE_BUCKET_NAME": 'my_bucket', + "AWS_ACCESS_KEY_ID": "my_access_key", + "AWS_SECRET_ACCESS_KEY": "my_secret_key", + "AWS_STORAGE_BUCKET_NAME": "my_bucket", } for key, value in FUNKWHALE_CONFIG.items(): os.environ[key] = value diff --git a/front/src/EmbedFrame.vue b/front/src/EmbedFrame.vue index dd830b486..517f2869d 100644 --- a/front/src/EmbedFrame.vue +++ b/front/src/EmbedFrame.vue @@ -280,6 +280,7 @@ export default { }, fetchTracks (filters, path) { path = path || "/api/v1/tracks/" + filters.include_channels = "true" let self = this let url = `${this.baseUrl}${path}` axios.get(url, {params: filters}).then(response => { diff --git a/front/src/components/library/AlbumDropdown.vue b/front/src/components/library/AlbumDropdown.vue index 8a065833e..3bb4a3b98 100644 --- a/front/src/components/library/AlbumDropdown.vue +++ b/front/src/components/library/AlbumDropdown.vue @@ -5,7 +5,7 @@
Embed this album on your website
-
+
diff --git a/front/src/components/library/ArtistBase.vue b/front/src/components/library/ArtistBase.vue index d7ff13a66..6cb631a75 100644 --- a/front/src/components/library/ArtistBase.vue +++ b/front/src/components/library/ArtistBase.vue @@ -38,7 +38,7 @@
Embed this artist work on your website
-
+
diff --git a/front/src/components/library/TrackBase.vue b/front/src/components/library/TrackBase.vue index 8af866daf..5263789dd 100644 --- a/front/src/components/library/TrackBase.vue +++ b/front/src/components/library/TrackBase.vue @@ -30,7 +30,7 @@
Embed this track on your website
-
+
diff --git a/front/src/views/channels/DetailBase.vue b/front/src/views/channels/DetailBase.vue index 06491313f..cedc36ab4 100644 --- a/front/src/views/channels/DetailBase.vue +++ b/front/src/views/channels/DetailBase.vue @@ -159,7 +159,7 @@
Embed this artist work on your website
-
+
diff --git a/front/src/views/playlists/Detail.vue b/front/src/views/playlists/Detail.vue index 99bf84761..6612d4af6 100644 --- a/front/src/views/playlists/Detail.vue +++ b/front/src/views/playlists/Detail.vue @@ -52,7 +52,7 @@
Embed this playlist on your website
-
+
From ca056f717e97a6aeff2fa57dd214c2894bbd77f0 Mon Sep 17 00:00:00 2001 From: Agate Date: Tue, 21 Jul 2020 14:32:59 +0200 Subject: [PATCH 2/2] Fix #1116: Can now filter subscribed content through API --- api/funkwhale_api/common/filters.py | 71 +++++++++++++++++++--------- api/funkwhale_api/music/filters.py | 4 +- api/tests/common/test_filters.py | 21 ++++++-- changes/changelog.d/1116.enhancement | 1 + docs/api/parameters.yml | 5 ++ 5 files changed, 75 insertions(+), 27 deletions(-) create mode 100644 changes/changelog.d/1116.enhancement diff --git a/api/funkwhale_api/common/filters.py b/api/funkwhale_api/common/filters.py index dec4a89ab..2548bcfb9 100644 --- a/api/funkwhale_api/common/filters.py +++ b/api/funkwhale_api/common/filters.py @@ -7,6 +7,7 @@ from django_filters import rest_framework as filters from . import fields from . import models from . import search +from . import utils class NoneObject(object): @@ -170,13 +171,17 @@ class MutationFilter(filters.FilterSet): fields = ["is_approved", "is_applied", "type"] +class EmptyQuerySet(ValueError): + pass + + class ActorScopeFilter(filters.CharFilter): def __init__(self, *args, **kwargs): self.actor_field = kwargs.pop("actor_field") + self.library_field = kwargs.pop("library_field", None) super().__init__(*args, **kwargs) def filter(self, queryset, value): - from funkwhale_api.federation import models as federation_models if not value: return queryset @@ -186,35 +191,57 @@ class ActorScopeFilter(filters.CharFilter): return queryset.none() user = getattr(request, "user", None) - qs = queryset - if value.lower() == "me": - qs = self.filter_me(user=user, queryset=queryset) - elif value.lower() == "all": - return queryset - elif value.lower().startswith("actor:"): - full_username = value.split("actor:", 1)[1] + actor = getattr(user, "actor", None) + scopes = [v.strip().lower() for v in value.split(",")] + query = None + for scope in scopes: + try: + right_query = self.get_query(scope, user, actor) + except ValueError: + return queryset.none() + query = utils.join_queries_or(query, right_query) + + return queryset.filter(query).distinct() + + def get_query(self, scope, user, actor): + from funkwhale_api.federation import models as federation_models + + if scope == "me": + return self.filter_me(actor) + elif scope == "all": + return Q(pk__gte=0) + + elif scope == "subscribed": + if not actor or self.library_field is None: + raise EmptyQuerySet() + followed_libraries = federation_models.LibraryFollow.objects.filter( + approved=True, actor=user.actor + ).values_list("target_id", flat=True) + if not self.library_field: + predicate = "pk__in" + else: + predicate = "{}__in".format(self.library_field) + return Q(**{predicate: followed_libraries}) + + elif scope.startswith("actor:"): + full_username = scope.split("actor:", 1)[1] username, domain = full_username.split("@") try: actor = federation_models.Actor.objects.get( preferred_username=username, domain_id=domain, ) except federation_models.Actor.DoesNotExist: - return queryset.none() + raise EmptyQuerySet() - return queryset.filter(**{self.actor_field: actor}) - elif value.lower().startswith("domain:"): - domain = value.split("domain:", 1)[1] - return queryset.filter(**{"{}__domain_id".format(self.actor_field): domain}) + return Q(**{self.actor_field: actor}) + elif scope.startswith("domain:"): + domain = scope.split("domain:", 1)[1] + return Q(**{"{}__domain_id".format(self.actor_field): domain}) else: - return queryset.none() + raise EmptyQuerySet() - if self.distinct: - qs = qs.distinct() - return qs - - def filter_me(self, user, queryset): - actor = getattr(user, "actor", None) + def filter_me(self, actor): if not actor: - return queryset.none() + raise EmptyQuerySet() - return queryset.filter(**{self.actor_field: actor}) + return Q(**{self.actor_field: actor}) diff --git a/api/funkwhale_api/music/filters.py b/api/funkwhale_api/music/filters.py index feebaa542..51ef1f0d7 100644 --- a/api/funkwhale_api/music/filters.py +++ b/api/funkwhale_api/music/filters.py @@ -148,7 +148,9 @@ class TrackFilter( tag = TAG_FILTER id = common_filters.MultipleQueryFilter(coerce=int) scope = common_filters.ActorScopeFilter( - actor_field="uploads__library__actor", distinct=True + actor_field="uploads__library__actor", + library_field="uploads__library", + distinct=True, ) artist = filters.ModelChoiceFilter( field_name="_", method="filter_artist", queryset=models.Artist.objects.all() diff --git a/api/tests/common/test_filters.py b/api/tests/common/test_filters.py index 6e8a7c354..bc155ac8c 100644 --- a/api/tests/common/test_filters.py +++ b/api/tests/common/test_filters.py @@ -44,15 +44,20 @@ def test_mutation_filter_is_approved(value, expected, factories): ("me", 0, [0]), ("me", 1, [1]), ("me", 2, []), - ("all", 0, [0, 1, 2]), - ("all", 1, [0, 1, 2]), - ("all", 2, [0, 1, 2]), + ("all", 0, [0, 1, 2, 3]), + ("all", 1, [0, 1, 2, 3]), + ("all", 2, [0, 1, 2, 3]), ("noop", 0, []), ("noop", 1, []), ("noop", 2, []), ("actor:actor1@domain.test", 0, [0]), ("actor:actor2@domain.test", 0, [1]), ("domain:domain.test", 0, [0, 1]), + ("subscribed", 0, [3]), + ("subscribed", 1, []), + ("subscribed", 2, []), + ("me,subscribed", 0, [0, 3]), + ("me,subscribed", 1, [1]), ], ) def test_actor_scope_filter( @@ -72,15 +77,23 @@ def test_actor_scope_filter( preferred_username="actor2", domain=domain ) users = [actor1.user, actor2.user, anonymous_user] + followed_library = factories["music.Library"]() tracks = [ factories["music.Upload"](library__actor=actor1, playable=True).track, factories["music.Upload"](library__actor=actor2, playable=True).track, factories["music.Upload"](playable=True).track, + factories["music.Upload"](playable=True, library=followed_library).track, ] + factories["federation.LibraryFollow"]( + actor=actor1, target=followed_library, approved=True + ) + class FS(filters.filters.FilterSet): scope = filters.ActorScopeFilter( - actor_field="uploads__library__actor", distinct=True + actor_field="uploads__library__actor", + library_field="uploads__library", + distinct=True, ) class Meta: diff --git a/changes/changelog.d/1116.enhancement b/changes/changelog.d/1116.enhancement new file mode 100644 index 000000000..d8c4cfbad --- /dev/null +++ b/changes/changelog.d/1116.enhancement @@ -0,0 +1 @@ +Can now filter subscribed content through API (#1116) \ No newline at end of file diff --git a/docs/api/parameters.yml b/docs/api/parameters.yml index e8fdd076b..18313b358 100644 --- a/docs/api/parameters.yml +++ b/docs/api/parameters.yml @@ -76,14 +76,19 @@ Scope: Limit the results to a given user or pod: - Use `all` (or do not specify the property to disable scope filtering) - Use `me` to retrieve content relative to the current user + - Use `subscribed` to retrieve content in libraries you follow - Use `actor:alice@example.com` to retrieve content relative to the account `alice@example.com - Use `domain:example.com` to retrieve content relative to the domain `example.com + + You can specify multiple coma separated scopes, e.g `scope=me,subscribed` to retrieve content matching either scopes. + schema: required: false type: "string" enum: - "me" - "all" + - "subscribed" - "actor:alice@example.com" - "domain:example.com"