Skip to content
Snippets Groups Projects
Commit e1331301 authored by Eliot Berriot's avatar Eliot Berriot
Browse files

Playlist embed

parent 39f6f51e
No related branches found
No related tags found
No related merge requests found
......@@ -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",
[
......
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() {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment