diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 32dcab186..e10b8b981 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -136,7 +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 + - 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/common/filters.py b/api/funkwhale_api/common/filters.py index 6b199d531..20c3822b7 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): @@ -169,13 +170,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 @@ -185,35 +190,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 64c0cd5a6..d8d703407 100644 --- a/api/funkwhale_api/music/filters.py +++ b/api/funkwhale_api/music/filters.py @@ -146,7 +146,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/funkwhale_api/music/serializers.py b/api/funkwhale_api/music/serializers.py index 7d894b5ed..c546dc316 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 @@ -833,4 +835,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 2b4af7ae6..5dab86abd 100644 --- a/api/funkwhale_api/music/views.py +++ b/api/funkwhale_api/music/views.py @@ -181,10 +181,11 @@ class AlbumViewSet( viewsets.ReadOnlyModelViewSet, ): queryset = ( - models.Album.objects.all().order_by("-creation_date") - # we do a prefetech related on tracks instead of a count because it's more efficient - # db-wise - .prefetch_related("artist", "attributed_to", "attachment_cover", "tracks") + models.Album.objects.all() + .order_by("-creation_date") + .prefetch_related( + "artist__channel", "attributed_to", "attachment_cover", "tracks" + ) ) serializer_class = serializers.AlbumSerializer permission_classes = [oauth_permissions.ScopePermission] 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/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/api/parameters.yml b/docs/api/parameters.yml index 356284587..f8345ff86 100644 --- a/docs/api/parameters.yml +++ b/docs/api/parameters.yml @@ -94,14 +94,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" diff --git a/front/src/EmbedFrame.vue b/front/src/EmbedFrame.vue index 19ab1a3b8..8dcccf3e5 100644 --- a/front/src/EmbedFrame.vue +++ b/front/src/EmbedFrame.vue @@ -279,6 +279,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 6298ea57c..3c4fc00d5 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 725d91bbe..87769b4ff 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 4f33a3a25..93b64e17d 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 75621dfc7..c5b796195 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 82349b8bc..31f30e244 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

-
+