kopia lustrzana https://dev.funkwhale.audio/funkwhale/funkwhale
				
				
				
			Merge branch 'playlist-embed' into 'develop'
Playlist embed See merge request funkwhale/funkwhale!878environments/review-docs-890-d-9gdp99/deployments/2535
						commit
						05e36c745c
					
				| 
						 | 
				
			
			@ -15,4 +15,9 @@ urlpatterns = [
 | 
			
		|||
        spa_views.library_artist,
 | 
			
		||||
        name="library_artist",
 | 
			
		||||
    ),
 | 
			
		||||
    urls.re_path(
 | 
			
		||||
        r"^library/playlists/(?P<pk>\d+)/?$",
 | 
			
		||||
        spa_views.library_playlist,
 | 
			
		||||
        name="library_playlist",
 | 
			
		||||
    ),
 | 
			
		||||
]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,6 +11,7 @@ from funkwhale_api.common import serializers as common_serializers
 | 
			
		|||
from funkwhale_api.common import utils as common_utils
 | 
			
		||||
from funkwhale_api.federation import routes
 | 
			
		||||
from funkwhale_api.federation import utils as federation_utils
 | 
			
		||||
from funkwhale_api.playlists import models as playlists_models
 | 
			
		||||
from funkwhale_api.tags.models import Tag
 | 
			
		||||
 | 
			
		||||
from . import filters, models, tasks
 | 
			
		||||
| 
						 | 
				
			
			@ -552,6 +553,38 @@ 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 == "library_playlist":
 | 
			
		||||
            qs = playlists_models.Playlist.objects.filter(
 | 
			
		||||
                pk=int(match.kwargs["pk"]), privacy_level="everyone"
 | 
			
		||||
            )
 | 
			
		||||
            try:
 | 
			
		||||
                obj = qs.get()
 | 
			
		||||
            except playlists_models.Playlist.DoesNotExist:
 | 
			
		||||
                raise serializers.ValidationError(
 | 
			
		||||
                    "No artist matching id {}".format(match.kwargs["pk"])
 | 
			
		||||
                )
 | 
			
		||||
            embed_type = "playlist"
 | 
			
		||||
            embed_id = obj.pk
 | 
			
		||||
            playlist_tracks = obj.playlist_tracks.exclude(track__album__cover="")
 | 
			
		||||
            playlist_tracks = playlist_tracks.exclude(track__album__cover=None)
 | 
			
		||||
            playlist_tracks = playlist_tracks.select_related("track__album").order_by(
 | 
			
		||||
                "index"
 | 
			
		||||
            )
 | 
			
		||||
            first_playlist_track = playlist_tracks.first()
 | 
			
		||||
 | 
			
		||||
            if first_playlist_track:
 | 
			
		||||
                data["thumbnail_url"] = federation_utils.full_url(
 | 
			
		||||
                    first_playlist_track.track.album.cover.crop["400x400"].url
 | 
			
		||||
                )
 | 
			
		||||
                data["thumbnail_width"] = 400
 | 
			
		||||
                data["thumbnail_height"] = 400
 | 
			
		||||
            data["title"] = obj.name
 | 
			
		||||
            data["description"] = obj.name
 | 
			
		||||
            data["author_name"] = obj.name
 | 
			
		||||
            data["height"] = 400
 | 
			
		||||
            data["author_url"] = federation_utils.full_url(
 | 
			
		||||
                common_utils.spa_reverse("library_playlist", kwargs={"pk": obj.pk})
 | 
			
		||||
            )
 | 
			
		||||
        else:
 | 
			
		||||
            raise serializers.ValidationError(
 | 
			
		||||
                "Unsupported url: {}".format(validated_data["url"])
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,6 +5,7 @@ from django.urls import reverse
 | 
			
		|||
from django.db.models import Q
 | 
			
		||||
 | 
			
		||||
from funkwhale_api.common import utils
 | 
			
		||||
from funkwhale_api.playlists import models as playlists_models
 | 
			
		||||
 | 
			
		||||
from . import models
 | 
			
		||||
from . import serializers
 | 
			
		||||
| 
						 | 
				
			
			@ -203,3 +204,59 @@ def library_artist(request, pk):
 | 
			
		|||
        # twitter player is also supported in various software
 | 
			
		||||
        metas += get_twitter_card_metas(type="artist", id=obj.pk)
 | 
			
		||||
    return metas
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def library_playlist(request, pk):
 | 
			
		||||
    queryset = playlists_models.Playlist.objects.filter(pk=pk, privacy_level="everyone")
 | 
			
		||||
    try:
 | 
			
		||||
        obj = queryset.get()
 | 
			
		||||
    except playlists_models.Playlist.DoesNotExist:
 | 
			
		||||
        return []
 | 
			
		||||
    obj_url = utils.join_url(
 | 
			
		||||
        settings.FUNKWHALE_URL,
 | 
			
		||||
        utils.spa_reverse("library_playlist", kwargs={"pk": obj.pk}),
 | 
			
		||||
    )
 | 
			
		||||
    # we use the first playlist track's album's cover as image
 | 
			
		||||
    playlist_tracks = obj.playlist_tracks.exclude(track__album__cover="")
 | 
			
		||||
    playlist_tracks = playlist_tracks.exclude(track__album__cover=None)
 | 
			
		||||
    playlist_tracks = playlist_tracks.select_related("track__album").order_by("index")
 | 
			
		||||
    first_playlist_track = playlist_tracks.first()
 | 
			
		||||
    metas = [
 | 
			
		||||
        {"tag": "meta", "property": "og:url", "content": obj_url},
 | 
			
		||||
        {"tag": "meta", "property": "og:title", "content": obj.name},
 | 
			
		||||
        {"tag": "meta", "property": "og:type", "content": "music.playlist"},
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    if first_playlist_track:
 | 
			
		||||
        metas.append(
 | 
			
		||||
            {
 | 
			
		||||
                "tag": "meta",
 | 
			
		||||
                "property": "og:image",
 | 
			
		||||
                "content": utils.join_url(
 | 
			
		||||
                    settings.FUNKWHALE_URL,
 | 
			
		||||
                    first_playlist_track.track.album.cover.crop["400x400"].url,
 | 
			
		||||
                ),
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
        models.Upload.objects.filter(
 | 
			
		||||
            track__pk__in=[obj.playlist_tracks.values("track")]
 | 
			
		||||
        )
 | 
			
		||||
        .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 += get_twitter_card_metas(type="playlist", id=obj.pk)
 | 
			
		||||
    return metas
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -195,3 +195,77 @@ def test_library_artist(spa_html, no_api_auth, client, factories, settings):
 | 
			
		|||
 | 
			
		||||
    # we only test our custom metas, not the default ones
 | 
			
		||||
    assert metas[: len(expected_metas)] == expected_metas
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_library_playlist(spa_html, no_api_auth, client, factories, settings):
 | 
			
		||||
    playlist = factories["playlists.Playlist"](privacy_level="everyone")
 | 
			
		||||
    track = factories["music.Upload"](playable=True).track
 | 
			
		||||
    playlist.insert_many([track])
 | 
			
		||||
 | 
			
		||||
    url = "/library/playlists/{}".format(playlist.pk)
 | 
			
		||||
 | 
			
		||||
    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": playlist.name},
 | 
			
		||||
        {"tag": "meta", "property": "og:type", "content": "music.playlist"},
 | 
			
		||||
        {
 | 
			
		||||
            "tag": "meta",
 | 
			
		||||
            "property": "og:image",
 | 
			
		||||
            "content": utils.join_url(
 | 
			
		||||
                settings.FUNKWHALE_URL, track.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("playlist", id=playlist.id),
 | 
			
		||||
        },
 | 
			
		||||
        {"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_library_playlist_empty(spa_html, no_api_auth, client, factories, settings):
 | 
			
		||||
    playlist = factories["playlists.Playlist"](privacy_level="everyone")
 | 
			
		||||
 | 
			
		||||
    url = "/library/playlists/{}".format(playlist.pk)
 | 
			
		||||
 | 
			
		||||
    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": playlist.name},
 | 
			
		||||
        {"tag": "meta", "property": "og:type", "content": "music.playlist"},
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    metas = utils.parse_meta(response.content.decode())
 | 
			
		||||
 | 
			
		||||
    # we only test our custom metas, not the default ones
 | 
			
		||||
    assert metas[: len(expected_metas)] == expected_metas
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -832,6 +832,43 @@ def test_oembed_artist(factories, no_api_auth, api_client, settings):
 | 
			
		|||
    assert response.data == expected
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_oembed_playlist(factories, no_api_auth, api_client, settings):
 | 
			
		||||
    settings.FUNKWHALE_URL = "http://test"
 | 
			
		||||
    settings.FUNKWHALE_EMBED_URL = "http://embed"
 | 
			
		||||
    playlist = factories["playlists.Playlist"](privacy_level="everyone")
 | 
			
		||||
    track = factories["music.Upload"](playable=True).track
 | 
			
		||||
    playlist.insert_many([track])
 | 
			
		||||
    url = reverse("api:v1:oembed")
 | 
			
		||||
    playlist_url = "https://test.com/library/playlists/{}".format(playlist.pk)
 | 
			
		||||
    iframe_src = "http://embed?type=playlist&id={}".format(playlist.pk)
 | 
			
		||||
    expected = {
 | 
			
		||||
        "version": "1.0",
 | 
			
		||||
        "type": "rich",
 | 
			
		||||
        "provider_name": settings.APP_NAME,
 | 
			
		||||
        "provider_url": settings.FUNKWHALE_URL,
 | 
			
		||||
        "height": 400,
 | 
			
		||||
        "width": 600,
 | 
			
		||||
        "title": playlist.name,
 | 
			
		||||
        "description": playlist.name,
 | 
			
		||||
        "thumbnail_url": federation_utils.full_url(
 | 
			
		||||
            track.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": playlist.name,
 | 
			
		||||
        "author_url": federation_utils.full_url(
 | 
			
		||||
            utils.spa_reverse("library_playlist", kwargs={"pk": playlist.pk})
 | 
			
		||||
        ),
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    response = api_client.get(url, {"url": playlist_url, "format": "json"})
 | 
			
		||||
 | 
			
		||||
    assert response.data == expected
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.parametrize(
 | 
			
		||||
    "factory_name, url_name",
 | 
			
		||||
    [
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
Support embeds on public playlists
 | 
			
		||||
| 
						 | 
				
			
			@ -139,7 +139,7 @@ export default {
 | 
			
		|||
  data () {
 | 
			
		||||
    return {
 | 
			
		||||
      time,
 | 
			
		||||
      supportedTypes: ['track', 'album', 'artist'],
 | 
			
		||||
      supportedTypes: ['track', 'album', 'artist', 'playlist'],
 | 
			
		||||
      baseUrl: '',
 | 
			
		||||
      error: null,
 | 
			
		||||
      type: null,
 | 
			
		||||
| 
						 | 
				
			
			@ -235,6 +235,9 @@ export default {
 | 
			
		|||
      if (type === 'artist') {
 | 
			
		||||
        this.fetchTracks({artist: id, playable: true, ordering: "-release_date,disc_number,position"})
 | 
			
		||||
      }
 | 
			
		||||
      if (type === 'playlist') {
 | 
			
		||||
        this.fetchTracks({}, `/api/v1/playlists/${id}/tracks/`)
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    play (index) {
 | 
			
		||||
      this.currentIndex = index
 | 
			
		||||
| 
						 | 
				
			
			@ -269,9 +272,10 @@ export default {
 | 
			
		|||
        self.isLoading = false;
 | 
			
		||||
      })
 | 
			
		||||
    },
 | 
			
		||||
    fetchTracks (filters) {
 | 
			
		||||
    fetchTracks (filters, path) {
 | 
			
		||||
      path = path || "/api/v1/tracks/"
 | 
			
		||||
      let self = this
 | 
			
		||||
      let url = `${this.baseUrl}/api/v1/tracks/`
 | 
			
		||||
      let url = `${this.baseUrl}${path}`
 | 
			
		||||
      axios.get(url, {params: filters}).then(response => {
 | 
			
		||||
        self.tracks = self.parseTracks(response.data.results)
 | 
			
		||||
        self.isLoading = false;
 | 
			
		||||
| 
						 | 
				
			
			@ -297,6 +301,11 @@ export default {
 | 
			
		|||
    },
 | 
			
		||||
    parseTracks (tracks) {
 | 
			
		||||
      let self = this
 | 
			
		||||
      if (this.type === 'playlist') {
 | 
			
		||||
        tracks = tracks.map((t) => {
 | 
			
		||||
          return t.track
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
      return tracks.map(t => {
 | 
			
		||||
        return {
 | 
			
		||||
          id: t.id,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -50,7 +50,7 @@ export default {
 | 
			
		|||
      minHeight: 100,
 | 
			
		||||
      copied: false
 | 
			
		||||
    }
 | 
			
		||||
    if (this.type === 'album') {
 | 
			
		||||
    if (this.type === 'album' || this.type === 'artist' || this.type === 'playlist') {
 | 
			
		||||
      d.height = 330
 | 
			
		||||
      d.minHeight = 250
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -31,6 +31,14 @@
 | 
			
		|||
          <template v-if="edit"><translate translate-context="Content/Playlist/Button.Label/Verb">End edition</translate></template>
 | 
			
		||||
          <template v-else><translate translate-context="Content/*/Button.Label/Verb">Edit</translate></template>
 | 
			
		||||
        </button>
 | 
			
		||||
        <button
 | 
			
		||||
          class="ui icon labeled button"
 | 
			
		||||
          v-if="playlist.privacy_level === 'everyone' && playlist.is_playable"
 | 
			
		||||
          @click="showEmbedModal = !showEmbedModal">
 | 
			
		||||
          <i class="code icon"></i>
 | 
			
		||||
          <translate translate-context="Content/*/Button.Label/Verb">Embed</translate>
 | 
			
		||||
        </button>
 | 
			
		||||
 | 
			
		||||
        <dangerous-button v-if="$store.state.auth.profile && playlist.user.id === $store.state.auth.profile.id" class="labeled icon" :action="deletePlaylist">
 | 
			
		||||
          <i class="trash icon"></i> <translate translate-context="*/*/*/Verb">Delete</translate>
 | 
			
		||||
          <p slot="modal-header" v-translate="{playlist: playlist.name}" translate-context="Popup/Playlist/Title/Call to action" :translate-params="{playlist: playlist.name}">
 | 
			
		||||
| 
						 | 
				
			
			@ -40,6 +48,23 @@
 | 
			
		|||
          <div slot="modal-confirm"><translate translate-context="Popup/Playlist/Button.Label/Verb">Delete playlist</translate></div>
 | 
			
		||||
        </dangerous-button>
 | 
			
		||||
      </div>
 | 
			
		||||
      <modal v-if="playlist.privacy_level === 'everyone' && playlist.is_playable" :show.sync="showEmbedModal">
 | 
			
		||||
        <div class="header">
 | 
			
		||||
          <translate translate-context="Popup/Album/Title/Verb">Embed this playlist on your website</translate>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="content">
 | 
			
		||||
          <div class="description">
 | 
			
		||||
            <embed-wizard type="playlist" :id="playlist.id" />
 | 
			
		||||
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="actions">
 | 
			
		||||
          <div class="ui deny button">
 | 
			
		||||
            <translate translate-context="*/*/Button.Label/Verb">Cancel</translate>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </modal>
 | 
			
		||||
 | 
			
		||||
    </section>
 | 
			
		||||
    <section class="ui vertical stripe segment">
 | 
			
		||||
      <template v-if="edit">
 | 
			
		||||
| 
						 | 
				
			
			@ -61,6 +86,8 @@ import TrackTable from "@/components/audio/track/Table"
 | 
			
		|||
import RadioButton from "@/components/radios/Button"
 | 
			
		||||
import PlayButton from "@/components/audio/PlayButton"
 | 
			
		||||
import PlaylistEditor from "@/components/playlists/Editor"
 | 
			
		||||
import EmbedWizard from "@/components/audio/EmbedWizard"
 | 
			
		||||
import Modal from '@/components/semantic/Modal'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  props: {
 | 
			
		||||
| 
						 | 
				
			
			@ -71,7 +98,9 @@ export default {
 | 
			
		|||
    PlaylistEditor,
 | 
			
		||||
    TrackTable,
 | 
			
		||||
    PlayButton,
 | 
			
		||||
    RadioButton
 | 
			
		||||
    RadioButton,
 | 
			
		||||
    Modal,
 | 
			
		||||
    EmbedWizard,
 | 
			
		||||
  },
 | 
			
		||||
  data: function() {
 | 
			
		||||
    return {
 | 
			
		||||
| 
						 | 
				
			
			@ -79,7 +108,8 @@ export default {
 | 
			
		|||
      isLoading: false,
 | 
			
		||||
      playlist: null,
 | 
			
		||||
      tracks: [],
 | 
			
		||||
      playlistTracks: []
 | 
			
		||||
      playlistTracks: [],
 | 
			
		||||
      showEmbedModal: false,
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  created: function() {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Ładowanie…
	
		Reference in New Issue