From 8ae00b069889e164c4be624811cf280aedfd7df2 Mon Sep 17 00:00:00 2001
From: Eliot Berriot <contact@eliotberriot.com>
Date: Fri, 8 Mar 2019 10:30:04 +0100
Subject: [PATCH] Fix #747: Support embedding full artist discographies

---
 api/funkwhale_api/music/serializers.py     | 30 ++++++++++++++++
 api/funkwhale_api/music/spa_views.py       | 19 ++++++++++
 api/funkwhale_api/music/views.py           |  2 ++
 api/tests/music/test_spa_views.py          | 20 +++++++++++
 api/tests/music/test_views.py              | 35 ++++++++++++++++++
 changes/changelog.d/747.feature            |  1 +
 front/src/EmbedFrame.vue                   |  8 +++--
 front/src/components/audio/EmbedWizard.vue |  6 +++-
 front/src/components/library/Artist.vue    | 42 ++++++++++++++++++++--
 9 files changed, 157 insertions(+), 6 deletions(-)
 create mode 100644 changes/changelog.d/747.feature

diff --git a/api/funkwhale_api/music/serializers.py b/api/funkwhale_api/music/serializers.py
index 99e3dc40..8a8c4c5c 100644
--- a/api/funkwhale_api/music/serializers.py
+++ b/api/funkwhale_api/music/serializers.py
@@ -470,6 +470,36 @@ class OembedSerializer(serializers.Serializer):
                     "library_artist", kwargs={"pk": album.artist.pk}
                 )
             )
+        elif match.url_name == "library_artist":
+            qs = models.Artist.objects.filter(pk=int(match.kwargs["pk"]))
+            try:
+                artist = qs.get()
+            except models.Artist.DoesNotExist:
+                raise serializers.ValidationError(
+                    "No artist matching id {}".format(match.kwargs["pk"])
+                )
+            embed_type = "artist"
+            embed_id = artist.pk
+            album = (
+                artist.albums.filter(cover__isnull=False)
+                .exclude(cover="")
+                .order_by("-id")
+                .first()
+            )
+
+            if album and album.cover:
+                data["thumbnail_url"] = federation_utils.full_url(
+                    album.cover.crop["400x400"].url
+                )
+                data["thumbnail_width"] = 400
+                data["thumbnail_height"] = 400
+            data["title"] = artist.name
+            data["description"] = artist.name
+            data["author_name"] = artist.name
+            data["height"] = 400
+            data["author_url"] = federation_utils.full_url(
+                common_utils.spa_reverse("library_artist", kwargs={"pk": artist.pk})
+            )
         else:
             raise serializers.ValidationError(
                 "Unsupported url: {}".format(validated_data["url"])
diff --git a/api/funkwhale_api/music/spa_views.py b/api/funkwhale_api/music/spa_views.py
index 351431d0..7fafedf6 100644
--- a/api/funkwhale_api/music/spa_views.py
+++ b/api/funkwhale_api/music/spa_views.py
@@ -2,6 +2,7 @@ import urllib.parse
 
 from django.conf import settings
 from django.urls import reverse
+from django.db.models import Q
 
 from funkwhale_api.common import utils
 
@@ -183,4 +184,22 @@ def library_artist(request, pk):
             }
         )
 
+    if (
+        models.Upload.objects.filter(Q(track__artist=obj) | Q(track__album__artist=obj))
+        .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(artist_url))
+                ),
+            }
+        )
+        # twitter player is also supported in various software
+        metas += get_twitter_card_metas(type="artist", id=obj.pk)
     return metas
diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py
index f6bed500..e7897edb 100644
--- a/api/funkwhale_api/music/views.py
+++ b/api/funkwhale_api/music/views.py
@@ -184,6 +184,8 @@ class TrackViewSet(
         "title",
         "album__release_date",
         "size",
+        "position",
+        "disc_number",
         "artist__name",
     )
 
diff --git a/api/tests/music/test_spa_views.py b/api/tests/music/test_spa_views.py
index 901c6fe4..b9397009 100644
--- a/api/tests/music/test_spa_views.py
+++ b/api/tests/music/test_spa_views.py
@@ -149,6 +149,7 @@ def test_library_album(spa_html, no_api_auth, client, factories, settings):
 
 def test_library_artist(spa_html, no_api_auth, client, factories, settings):
     album = factories["music.Album"]()
+    factories["music.Upload"](playable=True, track__album=album)
     artist = album.artist
     url = "/library/artists/{}".format(artist.pk)
 
@@ -169,6 +170,25 @@ def test_library_artist(spa_html, no_api_auth, client, factories, settings):
                 settings.FUNKWHALE_URL, album.cover.crop["400x400"].url
             ),
         },
+        {
+            "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("artist", id=artist.id),
+        },
+        {"tag": "meta", "property": "twitter:player:width", "content": "600"},
+        {"tag": "meta", "property": "twitter:player:height", "content": "400"},
     ]
 
     metas = utils.parse_meta(response.content.decode())
diff --git a/api/tests/music/test_views.py b/api/tests/music/test_views.py
index b11f9b00..7b12c6c8 100644
--- a/api/tests/music/test_views.py
+++ b/api/tests/music/test_views.py
@@ -701,3 +701,38 @@ def test_oembed_album(factories, no_api_auth, api_client, settings):
     response = api_client.get(url, {"url": album_url, "format": "json"})
 
     assert response.data == expected
+
+
+def test_oembed_artist(factories, no_api_auth, api_client, settings):
+    settings.FUNKWHALE_URL = "http://test"
+    settings.FUNKWHALE_EMBED_URL = "http://embed"
+    track = factories["music.Track"]()
+    album = track.album
+    artist = track.artist
+    url = reverse("api:v1:oembed")
+    artist_url = "https://test.com/library/artists/{}".format(artist.pk)
+    iframe_src = "http://embed?type=artist&id={}".format(artist.pk)
+    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(album.cover.crop["400x400"].url),
+        "thumbnail_height": 400,
+        "thumbnail_width": 400,
+        "html": '<iframe width="600" height="400" scrolling="no" frameborder="no" src="{}"></iframe>'.format(
+            iframe_src
+        ),
+        "author_name": artist.name,
+        "author_url": federation_utils.full_url(
+            utils.spa_reverse("library_artist", kwargs={"pk": artist.pk})
+        ),
+    }
+
+    response = api_client.get(url, {"url": artist_url, "format": "json"})
+
+    assert response.data == expected
diff --git a/changes/changelog.d/747.feature b/changes/changelog.d/747.feature
new file mode 100644
index 00000000..a278f0a1
--- /dev/null
+++ b/changes/changelog.d/747.feature
@@ -0,0 +1 @@
+Support embedding full artist discographies (#747)
diff --git a/front/src/EmbedFrame.vue b/front/src/EmbedFrame.vue
index 50219204..7dcc371e 100644
--- a/front/src/EmbedFrame.vue
+++ b/front/src/EmbedFrame.vue
@@ -139,7 +139,7 @@ export default {
   data () {
     return {
       time,
-      supportedTypes: ['track', 'album'],
+      supportedTypes: ['track', 'album', 'artist'],
       baseUrl: '',
       error: null,
       type: null,
@@ -158,6 +158,7 @@ export default {
   },
   created () {
     let params = getURLParams()
+    this.baseUrl = params.b || ''
     this.type = params.type
     if (this.supportedTypes.indexOf(this.type) === -1) {
       this.error = 'invalid_type'
@@ -229,7 +230,10 @@ export default {
         this.fetchTrack(id)
       }
       if (type === 'album') {
-        this.fetchTracks({album: id, playable: true})
+        this.fetchTracks({album: id, playable: true, ordering: ",disc_number,position"})
+      }
+      if (type === 'artist') {
+        this.fetchTracks({artist: id, playable: true, ordering: "-release_date,disc_number,position"})
       }
     },
     play (index) {
diff --git a/front/src/components/audio/EmbedWizard.vue b/front/src/components/audio/EmbedWizard.vue
index 0022b60d..ebb65b36 100644
--- a/front/src/components/audio/EmbedWizard.vue
+++ b/front/src/components/audio/EmbedWizard.vue
@@ -29,7 +29,11 @@
       </div>
     </div>
     <div class="preview">
-      <h3><translate :translate-context="'Popup/Embed/Title/Noun'">Preview</translate></h3>
+      <h3>
+        <a :href="iframeSrc" target="_blank">
+          <translate :translate-context="'Popup/Embed/Title/Noun'">Preview</translate>
+        </a>
+      </h3>
       <iframe :width="frameWidth" :height="height" scrolling="no" frameborder="no" :src="iframeSrc"></iframe>
     </div>
   </div>
diff --git a/front/src/components/library/Artist.vue b/front/src/components/library/Artist.vue
index 084ef34c..b23153d2 100644
--- a/front/src/components/library/Artist.vue
+++ b/front/src/components/library/Artist.vue
@@ -35,6 +35,30 @@
             <i class="external icon"></i>
             <translate :translate-context="'Content/*/Button.Label/Verb'">View on MusicBrainz</translate>
           </a>
+          <template v-if="publicLibraries.length > 0">
+            <button
+              @click="showEmbedModal = !showEmbedModal"
+              class="ui button icon labeled">
+              <i class="code icon"></i>
+              <translate :translate-context="'Content/*/Button.Label/Verb'">Embed</translate>
+            </button>
+            <modal :show.sync="showEmbedModal">
+              <div class="header">
+                <translate :translate-context="'Popup/Artist/Title/Verb'">Embed this artist work on your website</translate>
+              </div>
+              <div class="content">
+                <div class="description">
+                  <embed-wizard type="artist" :id="artist.id" />
+
+                </div>
+              </div>
+              <div class="actions">
+                <div class="ui deny button">
+                  <translate :translate-context="'Popup/*/Button.Label/Verb'">Cancel</translate>
+                </div>
+              </div>
+            </modal>
+          </template>
         </div>
       </section>
       <div class="ui small text container" v-if="contentFilter">
@@ -72,7 +96,7 @@
         <h2>
           <translate :translate-context="'Content/Artist/Title'">User libraries</translate>
         </h2>
-        <library-widget :url="'artists/' + id + '/libraries/'">
+        <library-widget @loaded="libraries = $event" :url="'artists/' + id + '/libraries/'">
           <translate :translate-context="'Content/Artist/Paragraph'" slot="subtitle">This artist is present in the following libraries:</translate>
         </library-widget>
       </section>
@@ -90,6 +114,8 @@ import RadioButton from "@/components/radios/Button"
 import PlayButton from "@/components/audio/PlayButton"
 import TrackTable from "@/components/audio/track/Table"
 import LibraryWidget from "@/components/federation/LibraryWidget"
+import EmbedWizard from "@/components/audio/EmbedWizard"
+import Modal from '@/components/semantic/Modal'
 
 export default {
   props: ["id"],
@@ -98,7 +124,9 @@ export default {
     RadioButton,
     PlayButton,
     TrackTable,
-    LibraryWidget
+    LibraryWidget,
+    EmbedWizard,
+    Modal
   },
   data() {
     return {
@@ -108,7 +136,9 @@ export default {
       albums: null,
       totalTracks: 0,
       totalAlbums: 0,
-      tracks: []
+      tracks: [],
+      libraries: [],
+      showEmbedModal: false
     }
   },
   created() {
@@ -185,6 +215,12 @@ export default {
           return album.cover
         })[0]
     },
+
+    publicLibraries () {
+      return this.libraries.filter(l => {
+        return l.privacy_level === 'everyone'
+      })
+    },
     headerStyle() {
       if (!this.cover || !this.cover.original) {
         return ""
-- 
GitLab