Merge branch 'release/0.2.1'
This commit is contained in:
commit
f4a8d0f80b
|
@ -80,6 +80,21 @@ docker_develop:
|
||||||
tags:
|
tags:
|
||||||
- dind
|
- dind
|
||||||
|
|
||||||
|
build_api:
|
||||||
|
# Simply publish a zip containing api/ directory
|
||||||
|
stage: deploy
|
||||||
|
image: busybox
|
||||||
|
artifacts:
|
||||||
|
name: "api_${CI_COMMIT_REF_NAME}"
|
||||||
|
paths:
|
||||||
|
- api
|
||||||
|
script: echo Done!
|
||||||
|
only:
|
||||||
|
- tags
|
||||||
|
- master
|
||||||
|
- develop
|
||||||
|
|
||||||
|
|
||||||
docker_release:
|
docker_release:
|
||||||
stage: deploy
|
stage: deploy
|
||||||
before_script:
|
before_script:
|
||||||
|
|
|
@ -260,6 +260,18 @@ BROKER_URL = env("CELERY_BROKER_URL", default='django://')
|
||||||
########## END CELERY
|
########## END CELERY
|
||||||
|
|
||||||
|
|
||||||
|
CACHES = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "django_redis.cache.RedisCache",
|
||||||
|
"LOCATION": "{0}/{1}".format(env.cache_url('REDIS_URL', default="redis://127.0.0.1:6379"), 0),
|
||||||
|
"OPTIONS": {
|
||||||
|
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||||
|
"IGNORE_EXCEPTIONS": True, # mimics memcache behavior.
|
||||||
|
# http://niwinz.github.io/django-redis/latest/#_memcached_exceptions_behavior
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# Location of root django.contrib.admin URL, use {% url 'admin:index' %}
|
# Location of root django.contrib.admin URL, use {% url 'admin:index' %}
|
||||||
ADMIN_URL = r'^admin/'
|
ADMIN_URL = r'^admin/'
|
||||||
# Your common stuff: Below this line define 3rd party library settings
|
# Your common stuff: Below this line define 3rd party library settings
|
||||||
|
@ -301,7 +313,8 @@ REST_FRAMEWORK = {
|
||||||
}
|
}
|
||||||
|
|
||||||
ATOMIC_REQUESTS = False
|
ATOMIC_REQUESTS = False
|
||||||
|
USE_X_FORWARDED_HOST = True
|
||||||
|
USE_X_FORWARDED_PORT = True
|
||||||
# Wether we should check user permission before serving audio files (meaning
|
# Wether we should check user permission before serving audio files (meaning
|
||||||
# return an obfuscated url)
|
# return an obfuscated url)
|
||||||
# This require a special configuration on the reverse proxy side
|
# This require a special configuration on the reverse proxy side
|
||||||
|
|
|
@ -28,14 +28,6 @@ EMAIL_PORT = 1025
|
||||||
EMAIL_BACKEND = env('DJANGO_EMAIL_BACKEND',
|
EMAIL_BACKEND = env('DJANGO_EMAIL_BACKEND',
|
||||||
default='django.core.mail.backends.console.EmailBackend')
|
default='django.core.mail.backends.console.EmailBackend')
|
||||||
|
|
||||||
# CACHING
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
CACHES = {
|
|
||||||
'default': {
|
|
||||||
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
|
||||||
'LOCATION': ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# django-debug-toolbar
|
# django-debug-toolbar
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
|
@ -100,17 +100,7 @@ DATABASES['default'] = env.db("DATABASE_URL")
|
||||||
# CACHING
|
# CACHING
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# Heroku URL does not pass the DB number, so we parse it in
|
# Heroku URL does not pass the DB number, so we parse it in
|
||||||
CACHES = {
|
|
||||||
"default": {
|
|
||||||
"BACKEND": "django_redis.cache.RedisCache",
|
|
||||||
"LOCATION": "{0}/{1}".format(env.cache_url('REDIS_URL', default="redis://127.0.0.1:6379"), 0),
|
|
||||||
"OPTIONS": {
|
|
||||||
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
|
||||||
"IGNORE_EXCEPTIONS": True, # mimics memcache behavior.
|
|
||||||
# http://niwinz.github.io/django-redis/latest/#_memcached_exceptions_behavior
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# LOGGING CONFIGURATION
|
# LOGGING CONFIGURATION
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
__version__ = '0.2.0'
|
__version__ = '0.2.1'
|
||||||
__version_info__ = tuple([int(num) if num.isdigit() else num for num in __version__.replace('-', '.', 1).split('.')])
|
__version_info__ = tuple([int(num) if num.isdigit() else num for num in __version__.replace('-', '.', 1).split('.')])
|
||||||
|
|
|
@ -27,6 +27,7 @@ class APIModelMixin(models.Model):
|
||||||
api_includes = []
|
api_includes = []
|
||||||
creation_date = models.DateTimeField(default=timezone.now)
|
creation_date = models.DateTimeField(default=timezone.now)
|
||||||
import_hooks = []
|
import_hooks = []
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
ordering = ['-creation_date']
|
ordering = ['-creation_date']
|
||||||
|
@ -291,6 +292,9 @@ class Track(APIModelMixin):
|
||||||
]
|
]
|
||||||
tags = TaggableManager()
|
tags = TaggableManager()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['album', 'position']
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.title
|
return self.title
|
||||||
|
|
||||||
|
@ -358,6 +362,12 @@ class TrackFile(models.Model):
|
||||||
'api:v1:trackfiles-serve', kwargs={'pk': self.pk})
|
'api:v1:trackfiles-serve', kwargs={'pk': self.pk})
|
||||||
return self.audio_file.url
|
return self.audio_file.url
|
||||||
|
|
||||||
|
@property
|
||||||
|
def filename(self):
|
||||||
|
return '{}{}'.format(
|
||||||
|
self.track.full_name,
|
||||||
|
os.path.splitext(self.audio_file.name)[-1])
|
||||||
|
|
||||||
|
|
||||||
class ImportBatch(models.Model):
|
class ImportBatch(models.Model):
|
||||||
creation_date = models.DateTimeField(default=timezone.now)
|
creation_date = models.DateTimeField(default=timezone.now)
|
||||||
|
@ -386,6 +396,8 @@ class ImportJob(models.Model):
|
||||||
)
|
)
|
||||||
status = models.CharField(choices=STATUS_CHOICES, default='pending', max_length=30)
|
status = models.CharField(choices=STATUS_CHOICES, default='pending', max_length=30)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ('id', )
|
||||||
@celery.app.task(name='ImportJob.run', filter=celery.task_method)
|
@celery.app.task(name='ImportJob.run', filter=celery.task_method)
|
||||||
def run(self, replace=False):
|
def run(self, replace=False):
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -31,11 +31,20 @@ class ImportBatchSerializer(serializers.ModelSerializer):
|
||||||
model = models.ImportBatch
|
model = models.ImportBatch
|
||||||
fields = ('id', 'jobs', 'status', 'creation_date')
|
fields = ('id', 'jobs', 'status', 'creation_date')
|
||||||
|
|
||||||
|
|
||||||
class TrackFileSerializer(serializers.ModelSerializer):
|
class TrackFileSerializer(serializers.ModelSerializer):
|
||||||
|
path = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.TrackFile
|
model = models.TrackFile
|
||||||
fields = ('id', 'path', 'duration', 'source')
|
fields = ('id', 'path', 'duration', 'source', 'filename')
|
||||||
|
|
||||||
|
def get_path(self, o):
|
||||||
|
request = self.context.get('request')
|
||||||
|
url = o.path
|
||||||
|
if request:
|
||||||
|
url = request.build_absolute_uri(url)
|
||||||
|
return url
|
||||||
|
|
||||||
class SimpleAlbumSerializer(serializers.ModelSerializer):
|
class SimpleAlbumSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
|
@ -62,7 +71,15 @@ class TrackSerializer(LyricsMixin):
|
||||||
tags = TagSerializer(many=True, read_only=True)
|
tags = TagSerializer(many=True, read_only=True)
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Track
|
model = models.Track
|
||||||
fields = ('id', 'mbid', 'title', 'artist', 'files', 'tags', 'lyrics')
|
fields = (
|
||||||
|
'id',
|
||||||
|
'mbid',
|
||||||
|
'title',
|
||||||
|
'artist',
|
||||||
|
'files',
|
||||||
|
'tags',
|
||||||
|
'position',
|
||||||
|
'lyrics')
|
||||||
|
|
||||||
class TrackSerializerNested(LyricsMixin):
|
class TrackSerializerNested(LyricsMixin):
|
||||||
artist = ArtistSerializer()
|
artist = ArtistSerializer()
|
||||||
|
|
|
@ -139,9 +139,8 @@ class TrackFileViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
return Response(status=404)
|
return Response(status=404)
|
||||||
|
|
||||||
response = Response()
|
response = Response()
|
||||||
filename = "filename*=UTF-8''{}{}".format(
|
filename = "filename*=UTF-8''{}".format(
|
||||||
urllib.parse.quote(f.track.full_name),
|
urllib.parse.quote(f.filename))
|
||||||
os.path.splitext(f.audio_file.name)[-1])
|
|
||||||
response["Content-Disposition"] = "attachment; {}".format(filename)
|
response["Content-Disposition"] = "attachment; {}".format(filename)
|
||||||
response['X-Accel-Redirect'] = "{}{}".format(
|
response['X-Accel-Redirect'] = "{}{}".format(
|
||||||
settings.PROTECT_FILES_PATH,
|
settings.PROTECT_FILES_PATH,
|
||||||
|
|
|
@ -55,4 +55,4 @@ mutagen==1.38
|
||||||
# Until this is merged
|
# Until this is merged
|
||||||
git+https://github.com/EliotBerriot/PyMemoize.git@django
|
git+https://github.com/EliotBerriot/PyMemoize.git@django
|
||||||
|
|
||||||
django-dynamic-preferences>=1.2,<1.3
|
django-dynamic-preferences>=1.3,<1.4
|
||||||
|
|
|
@ -28,7 +28,7 @@ services:
|
||||||
- C_FORCE_ROOT=true
|
- C_FORCE_ROOT=true
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/music:/music:ro
|
- ./data/music:/music:ro
|
||||||
- ./api/media:/app/funkwhale_api/media
|
- ./data/media:/app/funkwhale_api/media
|
||||||
|
|
||||||
celerybeat:
|
celerybeat:
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
|
@ -41,6 +41,8 @@ server {
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Host $host:$server_port;
|
||||||
|
proxy_set_header X-Forwarded-Port $server_port;
|
||||||
proxy_redirect off;
|
proxy_redirect off;
|
||||||
proxy_pass http://funkwhale-api/api/;
|
proxy_pass http://funkwhale-api/api/;
|
||||||
}
|
}
|
||||||
|
|
2
dev.yml
2
dev.yml
|
@ -63,4 +63,4 @@ services:
|
||||||
- ./docker/nginx/conf.dev:/etc/nginx/nginx.conf
|
- ./docker/nginx/conf.dev:/etc/nginx/nginx.conf
|
||||||
- ./api/funkwhale_api/media:/protected/media
|
- ./api/funkwhale_api/media:/protected/media
|
||||||
ports:
|
ports:
|
||||||
- "0.0.0.0:6001:80"
|
- "0.0.0.0:6001:6001"
|
||||||
|
|
|
@ -28,7 +28,7 @@ http {
|
||||||
#gzip on;
|
#gzip on;
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 6001;
|
||||||
charset utf-8;
|
charset utf-8;
|
||||||
|
|
||||||
location /_protected/media {
|
location /_protected/media {
|
||||||
|
@ -40,6 +40,8 @@ http {
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Host $host:$server_port;
|
||||||
|
proxy_set_header X-Forwarded-Port $server_port;
|
||||||
proxy_redirect off;
|
proxy_redirect off;
|
||||||
proxy_pass http://api:12081/;
|
proxy_pass http://api:12081/;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,18 @@
|
||||||
Changelog
|
Changelog
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
0.2.1
|
||||||
|
-----
|
||||||
|
|
||||||
|
2017-07-17
|
||||||
|
|
||||||
|
* Now return media files with absolute URL
|
||||||
|
* Now display CLI instructions to download a set of tracks
|
||||||
|
* Fixed #33: sort by track position in album in API by default, also reuse that information on frontend side
|
||||||
|
* More robust audio player and queue in various situations:
|
||||||
|
* upgrade to latest dynamic_preferences and use redis as cache even locally
|
||||||
|
|
||||||
|
|
||||||
0.2
|
0.2
|
||||||
-------
|
-------
|
||||||
|
|
||||||
|
|
|
@ -124,9 +124,9 @@ class Audio {
|
||||||
}
|
}
|
||||||
|
|
||||||
play () {
|
play () {
|
||||||
logger.default.info('Playing track')
|
|
||||||
if (this.state.startLoad) {
|
if (this.state.startLoad) {
|
||||||
if (!this.state.playing && this.$Audio.readyState >= 2) {
|
if (!this.state.playing && this.$Audio.readyState >= 2) {
|
||||||
|
logger.default.info('Playing track')
|
||||||
this.$Audio.play()
|
this.$Audio.play()
|
||||||
this.state.paused = false
|
this.state.paused = false
|
||||||
this.state.playing = true
|
this.state.playing = true
|
||||||
|
|
|
@ -123,6 +123,7 @@ class Queue {
|
||||||
this.tracks.splice(index, 0, track)
|
this.tracks.splice(index, 0, track)
|
||||||
}
|
}
|
||||||
if (this.ended) {
|
if (this.ended) {
|
||||||
|
logger.default.debug('Playing appended track')
|
||||||
this.play(this.currentIndex + 1)
|
this.play(this.currentIndex + 1)
|
||||||
}
|
}
|
||||||
this.cache()
|
this.cache()
|
||||||
|
@ -152,19 +153,31 @@ class Queue {
|
||||||
|
|
||||||
clean () {
|
clean () {
|
||||||
this.stop()
|
this.stop()
|
||||||
|
radios.stop()
|
||||||
this.tracks = []
|
this.tracks = []
|
||||||
this.currentIndex = -1
|
this.currentIndex = -1
|
||||||
this.currentTrack = null
|
this.currentTrack = null
|
||||||
|
// so we replay automatically on next track append
|
||||||
|
this.ended = true
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanTrack (index) {
|
cleanTrack (index) {
|
||||||
if (index === this.currentIndex) {
|
// are we removing current playin track
|
||||||
|
let current = index === this.currentIndex
|
||||||
|
if (current) {
|
||||||
this.stop()
|
this.stop()
|
||||||
}
|
}
|
||||||
if (index < this.currentIndex) {
|
if (index < this.currentIndex) {
|
||||||
this.currentIndex -= 1
|
this.currentIndex -= 1
|
||||||
}
|
}
|
||||||
this.tracks.splice(index, 1)
|
this.tracks.splice(index, 1)
|
||||||
|
if (current) {
|
||||||
|
// we play next track, which now have the same index
|
||||||
|
this.play(index)
|
||||||
|
}
|
||||||
|
if (this.currentIndex === this.tracks.length - 1) {
|
||||||
|
this.populateFromRadio()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stop () {
|
stop () {
|
||||||
|
@ -172,12 +185,17 @@ class Queue {
|
||||||
this.audio.destroyed()
|
this.audio.destroyed()
|
||||||
}
|
}
|
||||||
play (index) {
|
play (index) {
|
||||||
if (this.audio.destroyed) {
|
let self = this
|
||||||
logger.default.debug('Destroying previous audio...')
|
let currentIndex = index
|
||||||
this.audio.destroyed()
|
let currentTrack = this.tracks[index]
|
||||||
|
if (!currentTrack) {
|
||||||
|
logger.default.debug('No track at index', index)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
this.currentIndex = index
|
|
||||||
this.currentTrack = this.tracks[index]
|
this.currentIndex = currentIndex
|
||||||
|
this.currentTrack = currentTrack
|
||||||
|
|
||||||
this.ended = false
|
this.ended = false
|
||||||
let file = this.currentTrack.files[0]
|
let file = this.currentTrack.files[0]
|
||||||
if (!file) {
|
if (!file) {
|
||||||
|
@ -193,7 +211,11 @@ class Queue {
|
||||||
path = url.updateQueryString(path, 'jwt', auth.getAuthToken())
|
path = url.updateQueryString(path, 'jwt', auth.getAuthToken())
|
||||||
}
|
}
|
||||||
|
|
||||||
this.audio = new Audio(path, {
|
if (this.audio.destroyed) {
|
||||||
|
logger.default.debug('Destroying previous audio...', index - 1)
|
||||||
|
this.audio.destroyed()
|
||||||
|
}
|
||||||
|
let audio = new Audio(path, {
|
||||||
preload: true,
|
preload: true,
|
||||||
autoplay: true,
|
autoplay: true,
|
||||||
rate: 1,
|
rate: 1,
|
||||||
|
@ -201,6 +223,17 @@ class Queue {
|
||||||
volume: this.state.volume,
|
volume: this.state.volume,
|
||||||
onEnded: this.handleAudioEnded.bind(this)
|
onEnded: this.handleAudioEnded.bind(this)
|
||||||
})
|
})
|
||||||
|
this.audio = audio
|
||||||
|
audio.updateHook('playState', function (e) {
|
||||||
|
// in some situations, we may have a race condition, for example
|
||||||
|
// if the user spams the next / previous buttons, with multiple audios
|
||||||
|
// playing at the same time. To avoid that, we ensure the audio
|
||||||
|
// still matches de queue current audio
|
||||||
|
if (audio !== self.audio) {
|
||||||
|
logger.default.debug('Destroying duplicate audio')
|
||||||
|
audio.destroyed()
|
||||||
|
}
|
||||||
|
})
|
||||||
if (this.currentIndex === this.tracks.length - 1) {
|
if (this.currentIndex === this.tracks.length - 1) {
|
||||||
this.populateFromRadio()
|
this.populateFromRadio()
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="progress-area">
|
<div class="progress-area" v-if="queue.currentTrack">
|
||||||
<div class="ui grid">
|
<div class="ui grid">
|
||||||
<div class="left floated four wide column">
|
<div class="left floated four wide column">
|
||||||
<p class="timer start" @click="queue.audio.setTime(0)">{{queue.audio.state.currentTimeFormat}}</p>
|
<p class="timer start" @click="queue.audio.setTime(0)">{{queue.audio.state.currentTimeFormat}}</p>
|
||||||
|
|
|
@ -22,6 +22,9 @@
|
||||||
</td>
|
</td>
|
||||||
<td colspan="6">
|
<td colspan="6">
|
||||||
<router-link class="track discrete link" :to="{name: 'library.track', params: {id: track.id }}">
|
<router-link class="track discrete link" :to="{name: 'library.track', params: {id: track.id }}">
|
||||||
|
<template v-if="track.position">
|
||||||
|
{{ track.position }}.
|
||||||
|
</template>
|
||||||
{{ track.title }}
|
{{ track.title }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -20,9 +20,12 @@
|
||||||
<img class="ui mini image" v-else src="../../..//assets/audio/default-cover.png">
|
<img class="ui mini image" v-else src="../../..//assets/audio/default-cover.png">
|
||||||
</td>
|
</td>
|
||||||
<td colspan="6">
|
<td colspan="6">
|
||||||
<router-link class="track" :to="{name: 'library.track', params: {id: track.id }}">
|
<router-link class="track" :to="{name: 'library.track', params: {id: track.id }}">
|
||||||
{{ track.title }}
|
<template v-if="displayPosition && track.position">
|
||||||
</router-link>
|
{{ track.position }}.
|
||||||
|
</template>
|
||||||
|
{{ track.title }}
|
||||||
|
</router-link>
|
||||||
</td>
|
</td>
|
||||||
<td colspan="6">
|
<td colspan="6">
|
||||||
<router-link class="artist discrete link" :to="{name: 'library.artist', params: {id: track.artist.id }}">
|
<router-link class="artist discrete link" :to="{name: 'library.artist', params: {id: track.artist.id }}">
|
||||||
|
@ -37,23 +40,70 @@
|
||||||
<td><track-favorite-icon class="favorite-icon" :track="track"></track-favorite-icon></td>
|
<td><track-favorite-icon class="favorite-icon" :track="track"></track-favorite-icon></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
<tfoot class="full-width">
|
||||||
|
<tr>
|
||||||
|
<th colspan="3">
|
||||||
|
<button @click="showDownloadModal = !showDownloadModal" class="ui basic button">Download...</button>
|
||||||
|
<modal :show.sync="showDownloadModal">
|
||||||
|
<div class="header">
|
||||||
|
Download tracks
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<div class="description">
|
||||||
|
<p>There is currently no way to download directly multiple tracks from funkwhale as a ZIP archive.
|
||||||
|
However, you can use a command line tools such as <a href="https://curl.haxx.se/" target="_blank">cURL</a> to easily download a list of tracks.
|
||||||
|
</p>
|
||||||
|
<p>Simply copy paste the snippet below into a terminal to launch the download.</p>
|
||||||
|
<div class="ui warning message">
|
||||||
|
Keep your PRIVATE_TOKEN secret as it gives access to your account.
|
||||||
|
</div>
|
||||||
|
<pre>
|
||||||
|
export PRIVATE_TOKEN="{{ auth.getAuthToken ()}}"
|
||||||
|
<template v-for="track in tracks">
|
||||||
|
curl -G -o "{{ track.files[0].filename }}" <template v-if="auth.user.authenticated">--header "Authorization: JWT $PRIVATE_TOKEN"</template> "{{ backend.absoluteUrl(track.files[0].path) }}"</template>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<div class="ui black deny button">
|
||||||
|
Cancel
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</modal>
|
||||||
|
</th>
|
||||||
|
<th></th>
|
||||||
|
<th colspan="4"></th>
|
||||||
|
<th colspan="6"></th>
|
||||||
|
<th colspan="6"></th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
</table>
|
</table>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import backend from '@/audio/backend'
|
import backend from '@/audio/backend'
|
||||||
|
import auth from '@/auth'
|
||||||
import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon'
|
import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon'
|
||||||
import PlayButton from '@/components/audio/PlayButton'
|
import PlayButton from '@/components/audio/PlayButton'
|
||||||
|
|
||||||
|
import Modal from '@/components/semantic/Modal'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['tracks'],
|
props: {
|
||||||
|
tracks: {type: Array, required: true},
|
||||||
|
displayPosition: {type: Boolean, default: false}
|
||||||
|
},
|
||||||
components: {
|
components: {
|
||||||
|
Modal,
|
||||||
TrackFavoriteIcon,
|
TrackFavoriteIcon,
|
||||||
PlayButton
|
PlayButton
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
backend: backend
|
backend: backend,
|
||||||
|
auth: auth,
|
||||||
|
showDownloadModal: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="ui vertical stripe segment">
|
<div class="ui vertical stripe segment">
|
||||||
<h2>Tracks</h2>
|
<h2>Tracks</h2>
|
||||||
<track-table v-if="album" :tracks="album.tracks"></track-table>
|
<track-table v-if="album" :display-position="true" :tracks="album.tracks"></track-table>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
<template>
|
||||||
|
<div :class="['ui', {'active': show}, 'modal']">
|
||||||
|
<i class="close icon"></i>
|
||||||
|
<slot>
|
||||||
|
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import $ from 'jquery'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
show: {type: Boolean, required: true}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
control: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.control = $(this.$el).modal({
|
||||||
|
onApprove: function () {
|
||||||
|
this.$emit('approved')
|
||||||
|
}.bind(this),
|
||||||
|
onDeny: function () {
|
||||||
|
this.$emit('deny')
|
||||||
|
}.bind(this),
|
||||||
|
onHidden: function () {
|
||||||
|
this.$emit('update:show', false)
|
||||||
|
}.bind(this)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
show: {
|
||||||
|
handler (newValue) {
|
||||||
|
if (newValue) {
|
||||||
|
this.control.modal('show')
|
||||||
|
} else {
|
||||||
|
this.control.modal('hide')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||||
|
<style scoped lang="scss">
|
||||||
|
|
||||||
|
</style>
|
Loading…
Reference in New Issue