diff --git a/api/config/spa_urls.py b/api/config/spa_urls.py index 246338d2d..19aefa2ed 100644 --- a/api/config/spa_urls.py +++ b/api/config/spa_urls.py @@ -1,5 +1,6 @@ from django import urls +from funkwhale_api.audio import spa_views as audio_spa_views from funkwhale_api.music import spa_views @@ -20,4 +21,9 @@ urlpatterns = [ spa_views.library_playlist, name="library_playlist", ), + urls.re_path( + r"^channels/(?P[0-9a-f-]+)/?$", + audio_spa_views.channel_detail, + name="channel_detail", + ), ] diff --git a/api/funkwhale_api/audio/spa_views.py b/api/funkwhale_api/audio/spa_views.py new file mode 100644 index 000000000..34404812d --- /dev/null +++ b/api/funkwhale_api/audio/spa_views.py @@ -0,0 +1,64 @@ +import urllib.parse + +from django.conf import settings +from django.urls import reverse + +from funkwhale_api.common import preferences +from funkwhale_api.common import utils +from funkwhale_api.music import spa_views + +from . import models + + +def channel_detail(request, uuid): + queryset = models.Channel.objects.filter(uuid=uuid).select_related( + "artist__attachment_cover", "actor", "library" + ) + try: + obj = queryset.get() + except models.Channel.DoesNotExist: + return [] + obj_url = utils.join_url( + settings.FUNKWHALE_URL, + utils.spa_reverse("channel_detail", kwargs={"uuid": obj.uuid}), + ) + metas = [ + {"tag": "meta", "property": "og:url", "content": obj_url}, + {"tag": "meta", "property": "og:title", "content": obj.artist.name}, + {"tag": "meta", "property": "og:type", "content": "profile"}, + ] + + if obj.artist.attachment_cover: + metas.append( + { + "tag": "meta", + "property": "og:image", + "content": obj.artist.attachment_cover.download_url_medium_square_crop, + } + ) + + if preferences.get("federation__enabled"): + metas.append( + { + "tag": "link", + "rel": "alternate", + "type": "application/activity+json", + "href": obj.actor.fid, + } + ) + + if obj.library.uploads.all().playable_by(None).exists(): + metas.append( + { + "tag": "link", + "rel": "alternate", + "type": "application/json+oembed", + "href": ( + utils.join_url(settings.FUNKWHALE_URL, reverse("api:v1:oembed")) + + "?format=json&url={}".format(urllib.parse.quote_plus(obj_url)) + ), + } + ) + # twitter player is also supported in various software + metas += spa_views.get_twitter_card_metas(type="channel", id=obj.uuid) + return metas diff --git a/api/funkwhale_api/music/serializers.py b/api/funkwhale_api/music/serializers.py index 281b7d792..7b367857e 100644 --- a/api/funkwhale_api/music/serializers.py +++ b/api/funkwhale_api/music/serializers.py @@ -614,6 +614,36 @@ class OembedSerializer(serializers.Serializer): data["author_url"] = federation_utils.full_url( common_utils.spa_reverse("library_artist", kwargs={"pk": artist.pk}) ) + elif match.url_name == "channel_detail": + from funkwhale_api.audio.models import Channel + + qs = Channel.objects.filter(uuid=match.kwargs["uuid"]).select_related( + "artist__attachment_cover" + ) + try: + channel = qs.get() + except models.Artist.DoesNotExist: + raise serializers.ValidationError( + "No channel matching id {}".format(match.kwargs["uuid"]) + ) + embed_type = "channel" + embed_id = channel.uuid + + if channel.artist.attachment_cover: + data[ + "thumbnail_url" + ] = channel.artist.attachment_cover.download_url_medium_square_crop + data["thumbnail_width"] = 200 + data["thumbnail_height"] = 200 + data["title"] = channel.artist.name + data["description"] = channel.artist.name + data["author_name"] = channel.artist.name + data["height"] = 400 + data["author_url"] = federation_utils.full_url( + common_utils.spa_reverse( + "channel_detail", kwargs={"uuid": channel.uuid} + ) + ) elif match.url_name == "library_playlist": qs = playlists_models.Playlist.objects.filter( pk=int(match.kwargs["pk"]), privacy_level="everyone" diff --git a/api/tests/audio/test_spa_views.py b/api/tests/audio/test_spa_views.py new file mode 100644 index 000000000..bae96e711 --- /dev/null +++ b/api/tests/audio/test_spa_views.py @@ -0,0 +1,96 @@ +import urllib.parse + +from django.urls import reverse + +from funkwhale_api.common import utils +from funkwhale_api.federation import utils as federation_utils +from funkwhale_api.music import serializers + + +def test_library_artist(spa_html, no_api_auth, client, factories, settings): + channel = factories["audio.Channel"]() + factories["music.Upload"](playable=True, library=channel.library) + url = "/channels/{}".format(channel.uuid) + + response = client.get(url) + + expected_metas = [ + { + "tag": "meta", + "property": "og:url", + "content": utils.join_url(settings.FUNKWHALE_URL, url), + }, + {"tag": "meta", "property": "og:title", "content": channel.artist.name}, + {"tag": "meta", "property": "og:type", "content": "profile"}, + { + "tag": "meta", + "property": "og:image", + "content": channel.artist.attachment_cover.download_url_medium_square_crop, + }, + { + "tag": "link", + "rel": "alternate", + "type": "application/activity+json", + "href": channel.actor.fid, + }, + { + "tag": "link", + "rel": "alternate", + "type": "application/json+oembed", + "href": ( + utils.join_url(settings.FUNKWHALE_URL, reverse("api:v1:oembed")) + + "?format=json&url={}".format( + urllib.parse.quote_plus(utils.join_url(settings.FUNKWHALE_URL, url)) + ) + ), + }, + {"tag": "meta", "property": "twitter:card", "content": "player"}, + { + "tag": "meta", + "property": "twitter:player", + "content": serializers.get_embed_url("channel", id=channel.uuid), + }, + {"tag": "meta", "property": "twitter:player:width", "content": "600"}, + {"tag": "meta", "property": "twitter:player:height", "content": "400"}, + ] + + metas = utils.parse_meta(response.content.decode()) + + # we only test our custom metas, not the default ones + assert metas[: len(expected_metas)] == expected_metas + + +def test_oembed_channel(factories, no_api_auth, api_client, settings): + settings.FUNKWHALE_URL = "http://test" + settings.FUNKWHALE_EMBED_URL = "http://embed" + channel = factories["audio.Channel"]() + artist = channel.artist + url = reverse("api:v1:oembed") + obj_url = "https://test.com/channels/{}".format(channel.uuid) + iframe_src = "http://embed?type=channel&id={}".format(channel.uuid) + expected = { + "version": "1.0", + "type": "rich", + "provider_name": settings.APP_NAME, + "provider_url": settings.FUNKWHALE_URL, + "height": 400, + "width": 600, + "title": artist.name, + "description": artist.name, + "thumbnail_url": federation_utils.full_url( + artist.attachment_cover.file.crop["200x200"].url + ), + "thumbnail_height": 200, + "thumbnail_width": 200, + "html": ''.format( + iframe_src + ), + "author_name": artist.name, + "author_url": federation_utils.full_url( + utils.spa_reverse("channel_detail", kwargs={"uuid": channel.uuid}) + ), + } + + response = api_client.get(url, {"url": obj_url, "format": "json"}) + + assert response.data == expected diff --git a/front/src/EmbedFrame.vue b/front/src/EmbedFrame.vue index f44f1ba4d..8c684d922 100644 --- a/front/src/EmbedFrame.vue +++ b/front/src/EmbedFrame.vue @@ -139,7 +139,7 @@ export default { data () { return { time, - supportedTypes: ['track', 'album', 'artist', 'playlist'], + supportedTypes: ['track', 'album', 'artist', 'playlist', 'channel'], baseUrl: '', error: null, type: null, @@ -230,7 +230,10 @@ export default { this.fetchTrack(id) } if (type === 'album') { - this.fetchTracks({album: id, playable: true, ordering: ",disc_number,position"}) + this.fetchTracks({album: id, playable: true, ordering: "disc_number,position"}) + } + if (type === 'channel') { + this.fetchTracks({channel: id, playable: true, include_channels: 'true', ordering: "-creation_date"}) } if (type === 'artist') { this.fetchTracks({artist: id, playable: true, ordering: "-release_date,disc_number,position"})