Fix #578: added embed.html page to power iframe widget

This commit is contained in:
Eliot Berriot 2018-12-19 14:03:21 +01:00
parent 10e630f373
commit 815d729367
No known key found for this signature in database
GPG Key ID: DD6965E2476E5C27
13 changed files with 797 additions and 79 deletions

View File

@ -0,0 +1,48 @@
Allow embedding of albums and tracks available in public libraries via an <iframe> (#578)
Iframe widget to embed public tracks and albums [manual action required]
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Funkwhale now support embedding a lightweight audio player on external websites
for album and tracks that are available in public libraries. Important pages,
such as artist, album and track pages also include OpenGraph tags that will
enable previews on compatible apps (like sharing a Funkwhale track link on Mastodon
or Twitter).
To achieve that, we had to tweak the way Funkwhale front-end is served. You'll have
to modify your nginx configuration when upgrading to keep your instance working.
**On docker setups**, edit your ``/srv/funkwhale/nginx/funkwhale.template`` and replace
the ``location /api/`` and `location /` blocks by the following snippets::
location / {
include /etc/nginx/funkwhale_proxy.conf;
# this is needed if you have file import via upload enabled
client_max_body_size ${NGINX_MAX_BODY_SIZE};
proxy_pass http://funkwhale-api/;
}
location /front/ {
alias /frontend;
}
The change of configuration will be picked when restarting your nginx container.
**On non-docker setups**, edit your ``/etc/nginx/sites-available/funkwhale.conf`` file,
and replace the ``location /api/`` and `location /` blocks by the following snippets::
location / {
include /etc/nginx/funkwhale_proxy.conf;
# this is needed if you have file import via upload enabled
client_max_body_size ${NGINX_MAX_BODY_SIZE};
proxy_pass http://funkwhale-api/;
}
location /front/ {
alias ${FUNKWHALE_FRONTEND_PATH};
}
Replace ``${FUNKWHALE_FRONTEND_PATH}`` by the corresponding variable from your .env file,
which should be ``/srv/funkwhale/front/dist`` by default, then reload your nginx process with
``sudo systemctl reload nginx``.

42
dev.yml
View File

@ -10,20 +10,13 @@ services:
- "HOST=0.0.0.0" - "HOST=0.0.0.0"
- "VUE_PORT=${VUE_PORT-8080}" - "VUE_PORT=${VUE_PORT-8080}"
ports: ports:
- "${VUE_PORT-8080}:${VUE_PORT-8080}" - "${VUE_PORT-8080}"
volumes: volumes:
- "./front:/app" - "./front:/app"
- "/app/node_modules" - "/app/node_modules"
- "./po:/po" - "./po:/po"
networks: networks:
- federation
- internal - internal
labels:
traefik.backend: "${COMPOSE_PROJECT_NAME-node1}"
traefik.frontend.rule: "Host:${COMPOSE_PROJECT_NAME-node1}.funkwhale.test,${NODE_IP-127.0.0.1}"
traefik.enable: "true"
traefik.federation.protocol: "http"
traefik.federation.port: "${VUE_PORT-8080}"
postgres: postgres:
env_file: env_file:
@ -66,7 +59,7 @@ services:
- "CACHE_URL=redis://redis:6379/0" - "CACHE_URL=redis://redis:6379/0"
volumes: volumes:
- ./api:/app - ./api:/app
- "${MUSIC_DIRECTORY-./data/music}:/music:ro" - "${MUSIC_DIRECTORY_PATH-./data/music}:/music:ro"
networks: networks:
- internal - internal
api: api:
@ -76,10 +69,10 @@ services:
build: build:
context: ./api context: ./api
dockerfile: docker/Dockerfile.test dockerfile: docker/Dockerfile.test
command: python /app/manage.py runserver 0.0.0.0:12081 command: python /app/manage.py runserver 0.0.0.0:${FUNKWHALE_API_PORT-5000}
volumes: volumes:
- ./api:/app - ./api:/app
- "${MUSIC_DIRECTORY-./data/music}:/music:ro" - "${MUSIC_DIRECTORY_PATH-./data/music}:/music:ro"
environment: environment:
- "FUNKWHALE_HOSTNAME=${FUNKWHALE_HOSTNAME-localhost}" - "FUNKWHALE_HOSTNAME=${FUNKWHALE_HOSTNAME-localhost}"
- "FUNKWHALE_HOSTNAME_SUFFIX=funkwhale.test" - "FUNKWHALE_HOSTNAME_SUFFIX=funkwhale.test"
@ -99,22 +92,35 @@ services:
- .env - .env
image: nginx image: nginx
environment: environment:
- "VUE_PORT=${VUE_PORT-8080}" - "NGINX_MAX_BODY_SIZE=${NGINX_MAX_BODY_SIZE-30M}"
- "FUNKWHALE_API_IP=${FUNKHALE_API_IP-api}"
- "FUNKWHALE_API_PORT=${FUNKWHALE_API_PORT-5000}"
- "FUNKWHALE_FRONT_IP=${FUNKHALE_FRONT_IP-front}"
- "FUNKWHALE_FRONT_PORT=${VUE_PORT-8080}"
- "COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_NAME- }" - "COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_NAME- }"
- "FUNKWHALE_HOSTNAME=${FUNKWHALE_HOSTNAME-localhost}" - "FUNKWHALE_HOSTNAME=${FUNKWHALE_HOSTNAME-localhost}"
links: links:
- api - api
- front - front
volumes: volumes:
- ./docker/nginx/conf.dev:/etc/nginx/nginx.conf - ./docker/nginx/conf.dev:/etc/nginx/nginx.conf.template:ro
- ./docker/nginx/entrypoint.sh:/entrypoint.sh:ro - ./docker/nginx/entrypoint.sh:/entrypoint.sh:ro
- "${MUSIC_DIRECTORY-./data/music}:/music:ro" - "${MUSIC_DIRECTORY_PATH-./data/music}:/music:ro"
- ./deploy/funkwhale_proxy.conf:/etc/nginx/funkwhale_proxy.conf.template:ro - ./deploy/funkwhale_proxy.conf:/etc/nginx/funkwhale_proxy.conf:ro
- ./api/funkwhale_api/media:/protected/media - "${MEDIA_ROOT-./api/funkwhale_api/media}:/protected/media:ro"
ports:
- "6001"
networks: networks:
- federation
- internal - internal
labels:
traefik.backend: "${COMPOSE_PROJECT_NAME-node1}"
traefik.frontend.rule: "Host:${COMPOSE_PROJECT_NAME-node1}.funkwhale.test,${NODE_IP-127.0.0.1}"
traefik.enable: "true"
traefik.federation.protocol: "http"
traefik.federation.port: "80"
traefik.frontend.passHostHeader: true
traefik.docker.network: federation
docs: docs:
build: docs build: docs
command: python serve.py command: python serve.py

View File

@ -32,26 +32,57 @@ http {
'' close; '' close;
} }
upstream funkwhale-api {
server ${FUNKWHALE_API_IP}:${FUNKWHALE_API_PORT};
}
upstream funkwhale-front {
server ${FUNKWHALE_FRONT_IP}:${FUNKWHALE_FRONT_PORT};
}
server { server {
listen 6001; listen 80;
charset utf-8; charset utf-8;
client_max_body_size 30M; client_max_body_size 30M;
include /etc/nginx/funkwhale_proxy.conf; include /etc/nginx/funkwhale_proxy.conf;
location /front/ {
proxy_pass http://funkwhale-front/front/;
}
location /front-server/ {
proxy_pass http://funkwhale-front/;
}
location / {
include /etc/nginx/funkwhale_proxy.conf;
# this is needed if you have file import via upload enabled
client_max_body_size ${NGINX_MAX_BODY_SIZE};
proxy_pass http://funkwhale-api/;
}
# You can comment this if you do not plan to use the Subsonic API
location /rest/ {
include /etc/nginx/funkwhale_proxy.conf;
proxy_pass http://funkwhale-api/api/subsonic/rest/;
}
location /media/ {
alias /protected/media/;
}
location /_protected/media { location /_protected/media {
# this is an internal location that is used to serve
# audio files once correct permission / authentication
# has been checked on API side
internal; internal;
alias /protected/media; alias /protected/media;
} }
location /_protected/music { location /_protected/music {
# this is an internal location that is used to serve
# audio files once correct permission / authentication
# has been checked on API side
# Set this to the same value as your MUSIC_DIRECTORY_PATH setting
internal; internal;
alias /music; alias /music;
} }
location / {
include /etc/nginx/funkwhale_proxy.conf;
proxy_pass http://api:12081/;
}
location /rest/ {
include /etc/nginx/funkwhale_proxy.conf;
proxy_pass http://api:12081/api/subsonic/rest/;
}
} }
} }

View File

@ -1,18 +1,8 @@
#!/bin/bash -eux #!/bin/bash -eux
FORWARDED_PORT="$VUE_PORT"
COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME// /}"
if [ -n "$COMPOSE_PROJECT_NAME" ]; then
echo
FUNKWHALE_HOSTNAME="$COMPOSE_PROJECT_NAME.funkwhale.test"
FORWARDED_PORT="443"
fi
echo "Copying template file..."
cp /etc/nginx/funkwhale_proxy.conf{.template,}
sed -i "s/X-Forwarded-Host \$host:\$server_port/X-Forwarded-Host ${FUNKWHALE_HOSTNAME}/" /etc/nginx/funkwhale_proxy.conf
sed -i "s/proxy_set_header Host \$host/proxy_set_header Host ${FUNKWHALE_HOSTNAME}/" /etc/nginx/funkwhale_proxy.conf
sed -i "s/proxy_set_header X-Forwarded-Port \$server_port/proxy_set_header X-Forwarded-Port ${FORWARDED_PORT}/" /etc/nginx/funkwhale_proxy.conf
sed -i "s/proxy_set_header X-Forwarded-Proto \$scheme/proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO}/" /etc/nginx/funkwhale_proxy.conf
cat /etc/nginx/funkwhale_proxy.conf envsubst "`env | awk -F = '{printf \" $$%s\", $$1}'`" \
nginx -g "daemon off;" < /etc/nginx/nginx.conf.template \
> /etc/nginx/nginx.conf \
&& cat /etc/nginx/nginx.conf \
&& nginx-debug -g 'daemon off;'

View File

@ -27,6 +27,7 @@
"vue-gettext": "^2.1.0", "vue-gettext": "^2.1.0",
"vue-lazyload": "^1.2.6", "vue-lazyload": "^1.2.6",
"vue-masonry": "^0.11.5", "vue-masonry": "^0.11.5",
"vue-plyr": "^5.0.4",
"vue-router": "^3.0.1", "vue-router": "^3.0.1",
"vue-upload-component": "^2.8.11", "vue-upload-component": "^2.8.11",
"vuedraggable": "^2.16.0", "vuedraggable": "^2.16.0",

20
front/public/embed.html Normal file
View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.png">
<title>Funkwhale Widget</title>
</head>
<body>
<noscript>
<strong>We're sorry but this widget doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

567
front/src/Embed.vue Normal file
View File

@ -0,0 +1,567 @@
<template>
<main :class="[theme]">
<!-- SVG from https://cdn.plyr.io/3.4.7/plyr.svg -->
<svg aria-hidden="true" style="display: none" xmlns="http://www.w3.org/2000/svg">
<symbol id="plyr-download"><path d="M9 13c.3 0 .5-.1.7-.3L15.4 7 14 5.6l-4 4V1H8v8.6l-4-4L2.6 7l5.7 5.7c.2.2.4.3.7.3zM2 15h14v2H2z"/></symbol>
<symbol id="plyr-enter-fullscreen"><path d="M10 3h3.6l-4 4L11 8.4l4-4V8h2V1h-7zM7 9.6l-4 4V10H1v7h7v-2H4.4l4-4z"/></symbol>
<symbol id="plyr-exit-fullscreen"><path d="M1 12h3.6l-4 4L2 17.4l4-4V17h2v-7H1zM16 .6l-4 4V1h-2v7h7V6h-3.6l4-4z"/></symbol>
<symbol id="plyr-fast-forward"><path d="M7.875 7.171L0 1v16l7.875-6.171V17L18 9 7.875 1z"/></symbol>
<symbol id="plyr-muted"><path d="M12.4 12.5l2.1-2.1 2.1 2.1 1.4-1.4L15.9 9 18 6.9l-1.4-1.4-2.1 2.1-2.1-2.1L11 6.9 13.1 9 11 11.1zM3.786 6.008H.714C.286 6.008 0 6.31 0 6.76v4.512c0 .452.286.752.714.752h3.072l4.071 3.858c.5.3 1.143 0 1.143-.602V2.752c0-.601-.643-.977-1.143-.601L3.786 6.008z"/></symbol>
<symbol id="plyr-pause"><path d="M6 1H3c-.6 0-1 .4-1 1v14c0 .6.4 1 1 1h3c.6 0 1-.4 1-1V2c0-.6-.4-1-1-1zM12 1c-.6 0-1 .4-1 1v14c0 .6.4 1 1 1h3c.6 0 1-.4 1-1V2c0-.6-.4-1-1-1h-3z"/></symbol>
<symbol id="plyr-pip"><path d="M13.293 3.293L7.022 9.564l1.414 1.414 6.271-6.271L17 7V1h-6z"/><path d="M13 15H3V5h5V3H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-6h-2v5z"/></symbol>
<symbol id="plyr-play"><path d="M15.562 8.1L3.87.225C3.052-.337 2 .225 2 1.125v15.75c0 .9 1.052 1.462 1.87.9L15.563 9.9c.584-.45.584-1.35 0-1.8z"/></symbol>
<symbol id="plyr-restart"><path d="M9.7 1.2l.7 6.4 2.1-2.1c1.9 1.9 1.9 5.1 0 7-.9 1-2.2 1.5-3.5 1.5-1.3 0-2.6-.5-3.5-1.5-1.9-1.9-1.9-5.1 0-7 .6-.6 1.4-1.1 2.3-1.3l-.6-1.9C6 2.6 4.9 3.2 4 4.1 1.3 6.8 1.3 11.2 4 14c1.3 1.3 3.1 2 4.9 2 1.9 0 3.6-.7 4.9-2 2.7-2.7 2.7-7.1 0-9.9L16 1.9l-6.3-.7z"/></symbol>
<symbol id="plyr-rewind"><path d="M10.125 1L0 9l10.125 8v-6.171L18 17V1l-7.875 6.171z"/></symbol>
<symbol id="plyr-settings"><path d="M16.135 7.784a2 2 0 0 1-1.23-2.969c.322-.536.225-.998-.094-1.316l-.31-.31c-.318-.318-.78-.415-1.316-.094a2 2 0 0 1-2.969-1.23C10.065 1.258 9.669 1 9.219 1h-.438c-.45 0-.845.258-.997.865a2 2 0 0 1-2.969 1.23c-.536-.322-.999-.225-1.317.093l-.31.31c-.318.318-.415.781-.093 1.317a2 2 0 0 1-1.23 2.969C1.26 7.935 1 8.33 1 8.781v.438c0 .45.258.845.865.997a2 2 0 0 1 1.23 2.969c-.322.536-.225.998.094 1.316l.31.31c.319.319.782.415 1.316.094a2 2 0 0 1 2.969 1.23c.151.607.547.865.997.865h.438c.45 0 .845-.258.997-.865a2 2 0 0 1 2.969-1.23c.535.321.997.225 1.316-.094l.31-.31c.318-.318.415-.781.094-1.316a2 2 0 0 1 1.23-2.969c.607-.151.865-.547.865-.997v-.438c0-.451-.26-.846-.865-.997zM9 12a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></symbol>
<symbol id="plyr-volume"><path d="M15.6 3.3c-.4-.4-1-.4-1.4 0-.4.4-.4 1 0 1.4C15.4 5.9 16 7.4 16 9c0 1.6-.6 3.1-1.8 4.3-.4.4-.4 1 0 1.4.2.2.5.3.7.3.3 0 .5-.1.7-.3C17.1 13.2 18 11.2 18 9s-.9-4.2-2.4-5.7z"/><path d="M11.282 5.282a.909.909 0 0 0 0 1.316c.735.735.995 1.458.995 2.402 0 .936-.425 1.917-.995 2.487a.909.909 0 0 0 0 1.316c.145.145.636.262 1.018.156a.725.725 0 0 0 .298-.156C13.773 11.733 14.13 10.16 14.13 9c0-.17-.002-.34-.011-.51-.053-.992-.319-2.005-1.522-3.208a.909.909 0 0 0-1.316 0zM3.786 6.008H.714C.286 6.008 0 6.31 0 6.76v4.512c0 .452.286.752.714.752h3.072l4.071 3.858c.5.3 1.143 0 1.143-.602V2.752c0-.601-.643-.977-1.143-.601L3.786 6.008z"/></symbol></svg>
<!-- those ones are from fork-awesome -->
<symbol id="plyr-step-backward"><path d="M979 141c25-25 45-16 45 19v1472c0 35-20 44-45 19L269 941c-6-6-10-12-13-19v678c0 35-29 64-64 64H64c-35 0-64-29-64-64V192c0-35 29-64 64-64h128c35 0 64 29 64 64v678c3-7 7-13 13-19z"/></symbol>
<symbol id="plyr-step-forward"><path d="M45 1651c-25 25-45 16-45-19V160c0-35 20-44 45-19l710 710c6 6 10 12 13 19V192c0-35 29-64 64-64h128c35 0 64 29 64 64v1408c0 35-29 64-64 64H832c-35 0-64-29-64-64V922c-3 7-7 13-13 19z"/></symbol>
</svg>
<article>
<aside class="cover main" v-if="currentTrack">
<img height="120" v-if="currentTrack.cover" :src="currentTrack.cover" alt="Cover" />
<img height="120" v-else src="./assets/embed/default-cover.jpeg" alt="Cover" />
</aside>
<div class="content" aria-label="Track information">
<header v-if="currentTrack">
<h3><a :href="fullUrl('/library/tracks/' + currentTrack.id)" target="_blank" rel="noopener noreferrer">{{ currentTrack.title }}</a></h3>
By <a :href="fullUrl('/library/artists/' + currentTrack.artist.id)" target="_blank" rel="noopener noreferrer">{{ currentTrack.artist.name }}</a>
</header>
<section v-if="!isLoading" class="controls" aria-label="Audio player">
<template v-if="currentTrack && currentTrack.sources.length > 0">
<div class="queue-controls plyr--audio" v-if="tracks.length > 1">
<div class="plyr__controls">
<button
@focus="setControlFocus($event, true)"
@blur="setControlFocus($event, false)"
@click="previous()"
type="button"
class="plyr__control"
aria-label="Play previous track">
<svg class="icon--not-pressed" role="presentation" focusable="false" viewBox="0 0 1100 1650" width="80" height="80">
<use xlink:href="#plyr-step-backward"></use>
</svg>
</button>
<button
@click="next()"
@focus="setControlFocus($event, true)"
@blur="setControlFocus($event, false)"
type="button"
class="plyr__control"
aria-label="Play next track">
<svg class="icon--not-pressed" role="presentation" focusable="false" viewBox="0 0 1100 1650" width="80" height="80">
<use xlink:href="#plyr-step-forward"></use>
</svg>
</button>
</div>
</div>
<vue-plyr
:key="currentIndex"
ref="player"
class="player"
:options="{loadSprite: false, controls: controls, duration: currentTrack.sources[0].duration}">
<audio preload="none">
<source v-for="source in currentTrack.sources" :src="source.src" :type="source.type"/>
</audio>
</vue-plyr>
</template>
<div v-else class="player">
<span v-if="error === 'invalid_type'" class="error">Widget improperly configured (bad resource type {{ type }}).</span>
<span v-else-if="error === 'invalid_id'" class="error">Widget improperly configured (missing resource id).</span>
<span v-else-if="error === 'server_not_found'" class="error">Track not found.</span>
<span v-else-if="error === 'server_requires_auth'" class="error">You need to login to access this resource.</span>
<span v-else-if="error === 'server_error'" class="error">A server error occured.</span>
<span v-else-if="error === 'server_error'" class="error">An unknown error occured while loading track data from server.</span>
<span v-else-if="currentTrack && currentTrack.sources.length === 0" class="error">This track is unavailable.</span>
<span v-else class="error">An unknown error occured while loading track data.</span>
</div>
<a title="Funkwhale" href="https://funkwhale.audio" target="_blank" rel="noopener noreferrer" class="logo-wrapper">
<logo :fill="currentTheme.textColor" class="logo"></logo>
</a>
</section>
</div>
</article>
<div v-if="tracks.length > 1" class="queue-wrapper" id="queue">
<table class="queue">
<tbody>
<tr
:id="'queue-item-' + index"
role="button"
tabindex="0"
v-if="track.sources.length > 0"
:key="index"
:class="[{active: index === currentIndex}]"
@click="play(index)"
@keyup.enter="play(index)"
v-for="(track, index) in tracks">
<td class="position-cell" width="40">
<span class="position">
{{ index + 1 }}
</span>
</td>
<td class="title" :title="track.title" ><div colspan="2" class="ellipsis">{{ track.title }}</div></td>
<td class="artist" :title="track.artist.name" ><div class="ellipsis">{{ track.artist.name }}</div></td>
<td class="album">
<div class="ellipsis " v-if="track.album" :title="track.album.title">{{ track.album.title }}</div>
</td>
<td width="50">{{ time.durationFormatted(track.sources[0].duration) }}</td>
</tr>
</tbody>
</table>
</div>
</main>
</template>
<script>
import axios from 'axios'
import Logo from "@/components/Logo"
import url from '@/utils/url'
import time from '@/utils/time'
function getURLParams () {
var urlParams
var match,
pl = /\+/g, // Regex for replacing addition symbol with a space
search = /([^&=]+)=?([^&]*)/g,
decode = function (s) { return decodeURIComponent(s.replace(pl, " ")); },
query = window.location.search.substring(1);
urlParams = {};
while (match = search.exec(query))
urlParams[decode(match[1])] = decode(match[2]);
return urlParams
}
export default {
name: 'app',
components: {Logo},
data () {
return {
time,
supportedTypes: ['track', 'album'],
baseUrl: '',
error: null,
type: null,
id: null,
tracks: [],
url: null,
isLoading: true,
theme: 'dark',
currentIndex: -1,
themes: {
dark: {
textColor: 'white',
}
}
}
},
created () {
let params = getURLParams()
this.type = params.type
if (this.supportedTypes.indexOf(this.type) === -1) {
this.error = 'invalid_type'
}
this.id = params.id
if (!this.id) {
this.error = 'invalid_id'
}
if (this.error) {
this.isLoading = false
return
}
if (!!params.instance) {
this.baseUrl = params.instance
}
this.fetch(this.type, this.id)
},
mounted () {
var parser = document.createElement('a')
parser.href = this.baseUrl
this.url = parser
},
computed: {
currentTrack () {
if (this.tracks.length === 0) {
return null
}
return this.tracks[this.currentIndex]
},
currentTheme () {
return this.themes[this.theme]
},
controls () {
return [
'play', // Play/pause playback
'progress', // The progress bar and scrubber for playback and buffering
'current-time', // The current time of playback
'mute', // Toggle mute
'volume', // Volume control
]
},
hasPrevious () {
return this.currentIndex > 0
},
hasNext () {
return this.currentIndex < this.tracks.length - 1
},
},
methods: {
next () {
if (this.hasNext) {
this.play(this.currentIndex + 1)
}
},
previous () {
if (this.hasPrevious) {
this.play(this.currentIndex - 1)
}
},
setControlFocus(event, enable) {
if (enable) {
event.target.classList.add("plyr__tab-focus");
} else {
event.target.classList.remove("plyr__tab-focus");
}
},
fetch (type, id) {
if (type === 'track') {
this.fetchTrack(id)
}
if (type === 'album') {
this.fetchTracks({album: id, playable: true})
}
},
play (index) {
this.currentIndex = index
let self = this
this.$nextTick(() => {
self.$refs.player.player.play()
})
},
fetchTrack (id) {
let self = this
let url = `${this.baseUrl}/api/v1/tracks/${id}/`
axios.get(url).then(response => {
self.tracks = self.parseTracks([response.data])
self.isLoading = false;
}).catch(error => {
if (error.response) {
console.log(error.response)
if (error.response.status === 404) {
self.error = 'server_not_found'
}
else if (error.response.status === 403) {
self.error = 'server_requires_auth'
}
else if (error.response.status === 500) {
self.error = 'server_error'
}
else {
self.error = 'server_unknown_error'
}
} else {
self.error = 'server_unknown_error'
}
self.isLoading = false;
})
},
fetchTracks (filters) {
let self = this
let url = `${this.baseUrl}/api/v1/tracks/`
axios.get(url, {params: filters}).then(response => {
self.tracks = self.parseTracks(response.data.results)
self.isLoading = false;
}).catch(error => {
if (error.response) {
console.log(error.response)
if (error.response.status === 404) {
self.error = 'server_not_found'
}
else if (error.response.status === 403) {
self.error = 'server_requires_auth'
}
else if (error.response.status === 500) {
self.error = 'server_error'
}
else {
self.error = 'server_unknown_error'
}
} else {
self.error = 'server_unknown_error'
}
self.isLoading = false;
})
},
parseTracks (tracks) {
let self = this
return tracks.map(t => {
return {
id: t.id,
title: t.title,
artist: t.artist,
album: t.album,
cover: self.getCover(t.album.cover),
sources: self.getSources(t.uploads)
}
})
},
bindEvents () {
let self = this
this.$refs.player.player.on('ended', () => {
self.next()
})
},
fullUrl (path) {
if (path.startsWith('/')) {
return this.baseUrl + path
}
return path
},
getCover(albumCover) {
if (albumCover) {
return albumCover.medium_square_crop
}
},
getSources (uploads) {
let self = this;
let sources = uploads.map(u => {
return {
type: u.mimetype,
src: self.fullUrl(u.listen_url),
duration: u.duration
}
})
if (sources.length > 0) {
// We always add a transcoded MP3 src at the end
// because transcoding is expensive, but we want browsers that do
// not support other codecs to be able to play it :)
sources.push({
type: 'audio/mpeg',
src: url.updateQueryString(
self.fullUrl(sources[0].src),
'to',
'mp3'
)
})
}
return sources
}
},
watch: {
currentIndex (v) {
// we bind player events
let self = this
this.$nextTick(() => {
self.bindEvents()
if (self.tracks.length > 0) {
var topPos = document.getElementById(`queue-item-${v}`).offsetTop;
document.getElementById('queue').scrollTop = topPos-10;
}
})
},
tracks () {
this.currentIndex = 0
}
}
}
</script>
<style lang="scss">
html,
body,
main {
height: 100%;
}
body {
margin: 0;
font-family: sans-serif;
}
main {
display: flex;
flex-direction: column;
}
article {
display: flex;
position: relative;
aside {
padding: 0.5em;
}
}
a {
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
section.controls {
display: flex;
}
.cover {
max-width: 120px;
max-height: 120px;
}
.player {
flex: 1;
align-self: flex-end;
}
article .content {
flex: 1;
display: flex;
flex-direction: column;
h3 {
margin: 0 0 0.5em;
}
header {
flex: 1;
padding: 1em;
}
}
.player,
.queue-controls {
padding: 0.25em 0;
margin-right: 0.25em;
align-self: center;
}
section .plyr--audio .plyr__controls {
padding: 0;
}
.error {
font-weight: bold;
display: block;
text-align: center;
}
.logo-wrapper {
height: 2em;
width: 2em;
padding: 0.25em;
margin-left: 0.5em;
display: block;
}
[role="button"] {
cursor: pointer;
}
.ellipsis {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.queue-wrapper {
flex: 1;
overflow-y: auto;
padding: 0.5em;
}
.queue {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
margin-bottom: 0.5em;
td {
padding: 0.5em;
font-size: 90%;
img {
vertical-align: middle;
margin-right: 1em;
}
}
td:last-child {
text-align: right;
}
.position {
padding: 0.1em 0.3em;
display: inline-block;
}
}
@media screen and (max-width: 640px) {
.queue .album {
display: none;
}
.plyr__controls .plyr__time {
display: none;
}
}
@media screen and (max-width: 460px) {
article,
article .content {
display: block;
}
.cover.main {
float: right;
img {
height: 60px;
width: 60px;
}
}
}
@media screen and (max-width: 320px) {
.logo-wrapper,
.position-cell {
display: none;
}
}
// themes
.dark {
$primary-color: rgb(242, 113, 28);
$dark: rgb(27, 28, 29);
$lighter: rgb(47, 48, 48);
$clear: rgb(242, 242, 242);
// $primary-color: rgb(255, 88, 78);
.logo-wrapper {
background-color: $primary-color;
}
.plyr--audio .plyr__control.plyr__tab-focus,
.plyr--audio .plyr__control:hover,
.plyr--audio .plyr__control[aria-expanded="true"] {
background-color: $primary-color;
}
.plyr--audio .plyr__control.plyr__tab-focus,
.plyr--audio .plyr__control:hover,
.plyr--audio .plyr__control[aria-expanded="true"] {
background-color: $primary-color;
}
.plyr--full-ui input[type="range"] {
color: $primary-color;
}
article,
.player,
.plyr--audio .plyr__controls {
background-color: $dark;
}
.queue-wrapper {
background-color: $lighter;
}
article,
article a,
.player,
.queue tr,
.plyr--audio .plyr__controls {
color: white;
}
.plyr__control.plyr__tab-focus {
-webkit-box-shadow: 0 0 0 2px rgba(26, 175, 255, 0.5);
box-shadow: 0 0 0 2px rgba(26, 175, 255, 0.5);
outline: 0;
}
tr:hover,
tr:focus {
background-color: $dark;
}
tr.active {
background-color: $clear;
color: $dark;
}
tr.active {
.position {
background-color: $primary-color;
color: $clear;
}
}
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

@ -3,16 +3,16 @@
viewBox="0 0 141.7 141.7" enable-background="new 0 0 141.7 141.7" xml:space="preserve"> viewBox="0 0 141.7 141.7" enable-background="new 0 0 141.7 141.7" xml:space="preserve">
<g> <g>
<g> <g>
<path fill="#4082B4" d="M70.9,86.1c11.7,0,21.2-9.5,21.2-21.2c0-0.6-0.5-1.1-1.1-1.1h-8c-0.6,0-1.1,0.5-1.1,1.1c0,6-4.9,11-11,11 <path :fill="fill" d="M70.9,86.1c11.7,0,21.2-9.5,21.2-21.2c0-0.6-0.5-1.1-1.1-1.1h-8c-0.6,0-1.1,0.5-1.1,1.1c0,6-4.9,11-11,11
c-6,0-11-4.9-11-11c0-0.6-0.5-1.1-1.1-1.1h-8c-0.6,0-1.1,0.5-1.1,1.1C49.7,76.6,59.2,86.1,70.9,86.1z"/> c-6,0-11-4.9-11-11c0-0.6-0.5-1.1-1.1-1.1h-8c-0.6,0-1.1,0.5-1.1,1.1C49.7,76.6,59.2,86.1,70.9,86.1z"/>
<path fill="#4082B4" d="M70.9,106.1c22.7,0,41.2-18.5,41.2-41.2c0-0.6-0.5-1.1-1.1-1.1h-8c-0.6,0-1.1,0.5-1.1,1.1 <path :fill="fill" d="M70.9,106.1c22.7,0,41.2-18.5,41.2-41.2c0-0.6-0.5-1.1-1.1-1.1h-8c-0.6,0-1.1,0.5-1.1,1.1
c0,17.1-13.9,31-31,31c-17.1,0-31-13.9-31-31c0-0.6-0.5-1.1-1.1-1.1h-8c-0.6,0-1.1,0.5-1.1,1.1C29.6,87.6,48.1,106.1,70.9,106.1z" c0,17.1-13.9,31-31,31c-17.1,0-31-13.9-31-31c0-0.6-0.5-1.1-1.1-1.1h-8c-0.6,0-1.1,0.5-1.1,1.1C29.6,87.6,48.1,106.1,70.9,106.1z"
/> />
<path fill="#4082B4" d="M131.1,63.8h-8c-0.6,0-1.1,0.5-1.1,1.1C122,93.1,99,116,70.9,116c-28.2,0-51.1-22.9-51.1-51.1 <path :fill="fill" d="M131.1,63.8h-8c-0.6,0-1.1,0.5-1.1,1.1C122,93.1,99,116,70.9,116c-28.2,0-51.1-22.9-51.1-51.1
c0-0.6-0.5-1.1-1.1-1.1h-8c-0.6,0-1.1,0.5-1.1,1.1c0,33.8,27.5,61.3,61.3,61.3c33.8,0,61.3-27.5,61.3-61.3 c0-0.6-0.5-1.1-1.1-1.1h-8c-0.6,0-1.1,0.5-1.1,1.1c0,33.8,27.5,61.3,61.3,61.3c33.8,0,61.3-27.5,61.3-61.3
C132.2,64.3,131.7,63.8,131.1,63.8z"/> C132.2,64.3,131.7,63.8,131.1,63.8z"/>
</g> </g>
<path fill="#222222" d="M43.3,37.3c4.1,2.1,8.5,2.5,12.5,4.8c2.6,1.5,4.2,3.2,5.8,5.7c2.5,3.8,2.4,8.5,2.4,8.5l0.3,5.2 <path :fill="fill" d="M43.3,37.3c4.1,2.1,8.5,2.5,12.5,4.8c2.6,1.5,4.2,3.2,5.8,5.7c2.5,3.8,2.4,8.5,2.4,8.5l0.3,5.2
c0,0,2,5.2,6.4,5.2c4.7,0,6.4-5.2,6.4-5.2l0.3-5.2c0,0-0.1-4.7,2.4-8.5c1.6-2.5,3.2-4.3,5.8-5.7c4-2.3,8.4-2.7,12.5-4.8 c0,0,2,5.2,6.4,5.2c4.7,0,6.4-5.2,6.4-5.2l0.3-5.2c0,0-0.1-4.7,2.4-8.5c1.6-2.5,3.2-4.3,5.8-5.7c4-2.3,8.4-2.7,12.5-4.8
c4.1-2.1,8.1-4.8,10.8-8.6c2.7-3.8,4-8.8,2.5-13.2c-7.8-0.4-16.8,0.5-23.7,4.2c-9.6,5.1-15.4,3.3-17.1,10.9h-0.1 c4.1-2.1,8.1-4.8,10.8-8.6c2.7-3.8,4-8.8,2.5-13.2c-7.8-0.4-16.8,0.5-23.7,4.2c-9.6,5.1-15.4,3.3-17.1,10.9h-0.1
c-1.7-7.7-7.5-5.8-17.1-10.9c-6.9-3.7-15.9-4.6-23.7-4.2c-1.5,4.4-0.2,9.4,2.5,13.2C35.2,32.5,39.2,35.2,43.3,37.3z"/> c-1.7-7.7-7.5-5.8-17.1-10.9c-6.9-3.7-15.9-4.6-23.7-4.2c-1.5,4.4-0.2,9.4,2.5,13.2C35.2,32.5,39.2,35.2,43.3,37.3z"/>
@ -24,10 +24,8 @@
<script> <script>
export default { export default {
props: {
fill: {type: String, default: '#222222'}
}
} }
</script> </script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
</style>

16
front/src/embed.js Normal file
View File

@ -0,0 +1,16 @@
import Vue from 'vue'
import Embed from './Embed'
import axios from 'axios'
import VuePlyr from 'vue-plyr'
Vue.use(VuePlyr)
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
el: '#app',
template: '<Embed/>',
components: { Embed }
})

View File

@ -12,5 +12,13 @@ export default {
min = Math.floor(sec / 60) min = Math.floor(sec / 60)
sec = sec - min * 60 sec = sec - min * 60
return pad(min) + ':' + pad(sec) return pad(min) + ':' + pad(sec)
},
durationFormatted (v) {
let duration = parseInt(v)
if (duration % 1 !== 0) {
return time.parse(0)
}
duration = Math.round(duration)
return this.parse(duration)
} }
} }

View File

@ -1,5 +1,21 @@
module.exports = { module.exports = {
baseUrl: '/front/',
pages: {
embed: {
entry: 'src/embed.js',
template: 'public/embed.html',
filename: 'embed.html',
},
index: {
entry: 'src/main.js',
template: 'public/index.html',
filename: 'index.html'
}
},
chainWebpack: config => {
config.optimization.delete('splitChunks')
},
configureWebpack: { configureWebpack: {
resolve: { resolve: {
alias: { alias: {
@ -9,33 +25,7 @@ module.exports = {
}, },
devServer: { devServer: {
disableHostCheck: true, disableHostCheck: true,
proxy: { // use https://node1.funkwhale.test/front-server/ if you use docker with federation
'^/rest': { public: process.env.FRONT_DEVSERVER_URL || ('http://localhost:' + (process.env.VUE_PORT || '8080'))
target: 'http://nginx:6001',
changeOrigin: true,
},
'^/staticfiles': {
target: 'http://nginx:6001',
changeOrigin: true,
},
'^/.well-known': {
target: 'http://nginx:6001',
changeOrigin: true,
},
'^/media': {
target: 'http://nginx:6001',
changeOrigin: true,
},
'^/federation': {
target: 'http://nginx:6001',
changeOrigin: true,
ws: true,
},
'^/api': {
target: 'http://nginx:6001',
changeOrigin: true,
ws: true,
},
}
} }
} }

View File

@ -2168,6 +2168,11 @@ core-js@^2.4.0, core-js@^2.5.3:
version "2.5.7" version "2.5.7"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e"
core-js@^2.5.7:
version "2.6.0"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.0.tgz#1e30793e9ee5782b307e37ffa22da0eacddd84d4"
integrity sha512-kLRC6ncVpuEW/1kwrOXYX6KQASCVtrh1gQr/UiaVgFlf9WE5Vp+lNe5+h3LuMr5PAucWnnEXwH0nQHRH/gpGtw==
core-util-is@1.0.2, core-util-is@~1.0.0: core-util-is@1.0.2, core-util-is@~1.0.0:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
@ -2430,6 +2435,11 @@ currently-unhandled@^0.4.1:
dependencies: dependencies:
array-find-index "^1.0.1" array-find-index "^1.0.1"
custom-event-polyfill@^1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/custom-event-polyfill/-/custom-event-polyfill-1.0.6.tgz#6b026e81cd9f7bc896bd6b016a427407bb068db1"
integrity sha512-3FxpFlzGcHrDykwWu+xWVXZ8PfykM/9/bI3zXb953sh+AjInZWcQmrnmvPoZgiqNjmbtTm10PWvYqvRW527x6g==
cyclist@~0.2.2: cyclist@~0.2.2:
version "0.2.2" version "0.2.2"
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640" resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640"
@ -4602,6 +4612,11 @@ loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0:
emojis-list "^2.0.0" emojis-list "^2.0.0"
json5 "^0.5.0" json5 "^0.5.0"
loadjs@^3.5.4:
version "3.5.5"
resolved "https://registry.yarnpkg.com/loadjs/-/loadjs-3.5.5.tgz#2fbaa981ffdd079e0f8786ea75aeed643483b368"
integrity sha512-qBuLnKt4C6+vctutozFqPHQ6s4SSa9tcE64NsvDJ92UZmUrFvqGI1oVOtnZz2xwpgOT+2niQtHtQIDP4e/wlTA==
locate-path@^2.0.0: locate-path@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
@ -5677,6 +5692,17 @@ pluralize@^7.0.0:
version "7.0.0" version "7.0.0"
resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777" resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777"
plyr@^3.4.5:
version "3.4.7"
resolved "https://registry.yarnpkg.com/plyr/-/plyr-3.4.7.tgz#7d92470fb27f8019422c6d4edfd3b172d902ef06"
integrity sha512-RxxT2WdC4/sEZQT7CBZqKx5ImVw96aWjT6kB6DM82jy9GcWDiBBnv04m/AeeaXg9S5ambPdiHhB6Pzfm2q84Gw==
dependencies:
core-js "^2.5.7"
custom-event-polyfill "^1.0.6"
loadjs "^3.5.4"
raven-js "^3.27.0"
url-polyfill "^1.1.0"
pn@^1.1.0: pn@^1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb" resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb"
@ -6246,6 +6272,11 @@ raven-js@^3.26.4:
version "3.26.4" version "3.26.4"
resolved "https://registry.yarnpkg.com/raven-js/-/raven-js-3.26.4.tgz#32aae3a63a9314467a453c94c89a364ea43707be" resolved "https://registry.yarnpkg.com/raven-js/-/raven-js-3.26.4.tgz#32aae3a63a9314467a453c94c89a364ea43707be"
raven-js@^3.27.0:
version "3.27.0"
resolved "https://registry.yarnpkg.com/raven-js/-/raven-js-3.27.0.tgz#9f47c03e17933ce756e189f3669d49c441c1ba6e"
integrity sha512-vChdOL+yzecfnGA+B5EhEZkJ3kY3KlMzxEhShKh6Vdtooyl0yZfYNFQfYzgMf2v4pyQa+OTZ5esTxxgOOZDHqw==
raw-body@2.3.2: raw-body@2.3.2:
version "2.3.2" version "2.3.2"
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.2.tgz#bcd60c77d3eb93cde0050295c3f379389bc88f89" resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.2.tgz#bcd60c77d3eb93cde0050295c3f379389bc88f89"
@ -7558,6 +7589,11 @@ url-parse@^1.1.8, url-parse@^1.4.3:
querystringify "^2.0.0" querystringify "^2.0.0"
requires-port "^1.0.0" requires-port "^1.0.0"
url-polyfill@^1.1.0:
version "1.1.3"
resolved "https://registry.yarnpkg.com/url-polyfill/-/url-polyfill-1.1.3.tgz#ce0bdf2e923aa6f66bc198ab776323dfc5a91e62"
integrity sha512-xIAXc0DyXJCd767sSeRu4eqisyYhR0z0sohWArCn+WPwIatD39xGrc09l+tluIUi6jGkpGa8Gz8TKwkKYxMQvQ==
url@^0.11.0: url@^0.11.0:
version "0.11.0" version "0.11.0"
resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
@ -7682,6 +7718,13 @@ vue-masonry@^0.11.5:
masonry-layout "4.2.0" masonry-layout "4.2.0"
vue "^2.0.0" vue "^2.0.0"
vue-plyr@^5.0.4:
version "5.0.4"
resolved "https://registry.yarnpkg.com/vue-plyr/-/vue-plyr-5.0.4.tgz#13083b71a876d01200a3c93ebfd11585b671afda"
integrity sha512-zOLD7SZiYR/8DPYkZZR9zGTV+04GAc+fhnBymAWSRryncAG4889cYxXJSbIvlsNVGpdGRIOSIZ4p6pIupfmZ5w==
dependencies:
plyr "^3.4.5"
vue-router@^3.0.1: vue-router@^3.0.1:
version "3.0.1" version "3.0.1"
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.0.1.tgz#d9b05ad9c7420ba0f626d6500d693e60092cc1e9" resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.0.1.tgz#d9b05ad9c7420ba0f626d6500d693e60092cc1e9"