diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 58602d296..24b23b84b 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -80,6 +80,21 @@ docker_develop:
tags:
- 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:
stage: deploy
before_script:
diff --git a/api/config/settings/common.py b/api/config/settings/common.py
index 70804c3c9..3f7cc7503 100644
--- a/api/config/settings/common.py
+++ b/api/config/settings/common.py
@@ -260,6 +260,18 @@ BROKER_URL = env("CELERY_BROKER_URL", default='django://')
########## 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' %}
ADMIN_URL = r'^admin/'
# Your common stuff: Below this line define 3rd party library settings
@@ -301,7 +313,8 @@ REST_FRAMEWORK = {
}
ATOMIC_REQUESTS = False
-
+USE_X_FORWARDED_HOST = True
+USE_X_FORWARDED_PORT = True
# Wether we should check user permission before serving audio files (meaning
# return an obfuscated url)
# This require a special configuration on the reverse proxy side
diff --git a/api/config/settings/local.py b/api/config/settings/local.py
index 762ffe7aa..e8108e98b 100644
--- a/api/config/settings/local.py
+++ b/api/config/settings/local.py
@@ -28,14 +28,6 @@ EMAIL_PORT = 1025
EMAIL_BACKEND = env('DJANGO_EMAIL_BACKEND',
default='django.core.mail.backends.console.EmailBackend')
-# CACHING
-# ------------------------------------------------------------------------------
-CACHES = {
- 'default': {
- 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
- 'LOCATION': ''
- }
-}
# django-debug-toolbar
# ------------------------------------------------------------------------------
diff --git a/api/config/settings/production.py b/api/config/settings/production.py
index 937328d1f..e8a05bd3b 100644
--- a/api/config/settings/production.py
+++ b/api/config/settings/production.py
@@ -100,17 +100,7 @@ DATABASES['default'] = env.db("DATABASE_URL")
# CACHING
# ------------------------------------------------------------------------------
# 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
diff --git a/api/funkwhale_api/__init__.py b/api/funkwhale_api/__init__.py
index 70d6b5ac1..c46b89cc9 100644
--- a/api/funkwhale_api/__init__.py
+++ b/api/funkwhale_api/__init__.py
@@ -1,3 +1,3 @@
# -*- 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('.')])
diff --git a/api/funkwhale_api/music/models.py b/api/funkwhale_api/music/models.py
index 6a55dfc00..596f890b7 100644
--- a/api/funkwhale_api/music/models.py
+++ b/api/funkwhale_api/music/models.py
@@ -27,6 +27,7 @@ class APIModelMixin(models.Model):
api_includes = []
creation_date = models.DateTimeField(default=timezone.now)
import_hooks = []
+
class Meta:
abstract = True
ordering = ['-creation_date']
@@ -291,6 +292,9 @@ class Track(APIModelMixin):
]
tags = TaggableManager()
+ class Meta:
+ ordering = ['album', 'position']
+
def __str__(self):
return self.title
@@ -358,6 +362,12 @@ class TrackFile(models.Model):
'api:v1:trackfiles-serve', kwargs={'pk': self.pk})
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):
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)
+ class Meta:
+ ordering = ('id', )
@celery.app.task(name='ImportJob.run', filter=celery.task_method)
def run(self, replace=False):
try:
diff --git a/api/funkwhale_api/music/serializers.py b/api/funkwhale_api/music/serializers.py
index e7d7399ad..744115f86 100644
--- a/api/funkwhale_api/music/serializers.py
+++ b/api/funkwhale_api/music/serializers.py
@@ -31,11 +31,20 @@ class ImportBatchSerializer(serializers.ModelSerializer):
model = models.ImportBatch
fields = ('id', 'jobs', 'status', 'creation_date')
+
class TrackFileSerializer(serializers.ModelSerializer):
+ path = serializers.SerializerMethodField()
+
class Meta:
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):
@@ -62,7 +71,15 @@ class TrackSerializer(LyricsMixin):
tags = TagSerializer(many=True, read_only=True)
class Meta:
model = models.Track
- fields = ('id', 'mbid', 'title', 'artist', 'files', 'tags', 'lyrics')
+ fields = (
+ 'id',
+ 'mbid',
+ 'title',
+ 'artist',
+ 'files',
+ 'tags',
+ 'position',
+ 'lyrics')
class TrackSerializerNested(LyricsMixin):
artist = ArtistSerializer()
diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py
index 4a4032c57..983192552 100644
--- a/api/funkwhale_api/music/views.py
+++ b/api/funkwhale_api/music/views.py
@@ -139,9 +139,8 @@ class TrackFileViewSet(viewsets.ReadOnlyModelViewSet):
return Response(status=404)
response = Response()
- filename = "filename*=UTF-8''{}{}".format(
- urllib.parse.quote(f.track.full_name),
- os.path.splitext(f.audio_file.name)[-1])
+ filename = "filename*=UTF-8''{}".format(
+ urllib.parse.quote(f.filename))
response["Content-Disposition"] = "attachment; {}".format(filename)
response['X-Accel-Redirect'] = "{}{}".format(
settings.PROTECT_FILES_PATH,
diff --git a/api/requirements/base.txt b/api/requirements/base.txt
index bdf17cf9a..e7bc870cf 100644
--- a/api/requirements/base.txt
+++ b/api/requirements/base.txt
@@ -55,4 +55,4 @@ mutagen==1.38
# Until this is merged
git+https://github.com/EliotBerriot/PyMemoize.git@django
-django-dynamic-preferences>=1.2,<1.3
+django-dynamic-preferences>=1.3,<1.4
diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml
index 4ffede783..0c3b56547 100644
--- a/deploy/docker-compose.yml
+++ b/deploy/docker-compose.yml
@@ -28,7 +28,7 @@ services:
- C_FORCE_ROOT=true
volumes:
- ./data/music:/music:ro
- - ./api/media:/app/funkwhale_api/media
+ - ./data/media:/app/funkwhale_api/media
celerybeat:
restart: unless-stopped
diff --git a/deploy/nginx.conf b/deploy/nginx.conf
index 6a0a9f509..a85230ae8 100644
--- a/deploy/nginx.conf
+++ b/deploy/nginx.conf
@@ -41,6 +41,8 @@ server {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
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_pass http://funkwhale-api/api/;
}
diff --git a/dev.yml b/dev.yml
index f0fc8845a..78bf76fcd 100644
--- a/dev.yml
+++ b/dev.yml
@@ -63,4 +63,4 @@ services:
- ./docker/nginx/conf.dev:/etc/nginx/nginx.conf
- ./api/funkwhale_api/media:/protected/media
ports:
- - "0.0.0.0:6001:80"
+ - "0.0.0.0:6001:6001"
diff --git a/docker/nginx/conf.dev b/docker/nginx/conf.dev
index 6ca395fb1..9c00fd76f 100644
--- a/docker/nginx/conf.dev
+++ b/docker/nginx/conf.dev
@@ -28,7 +28,7 @@ http {
#gzip on;
server {
- listen 80;
+ listen 6001;
charset utf-8;
location /_protected/media {
@@ -40,6 +40,8 @@ http {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
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_pass http://api:12081/;
}
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 6e609aac9..47bf0ed9c 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -1,6 +1,18 @@
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
-------
diff --git a/front/src/audio/index.js b/front/src/audio/index.js
index 48f610443..7750ee500 100644
--- a/front/src/audio/index.js
+++ b/front/src/audio/index.js
@@ -124,9 +124,9 @@ class Audio {
}
play () {
- logger.default.info('Playing track')
if (this.state.startLoad) {
if (!this.state.playing && this.$Audio.readyState >= 2) {
+ logger.default.info('Playing track')
this.$Audio.play()
this.state.paused = false
this.state.playing = true
diff --git a/front/src/audio/queue.js b/front/src/audio/queue.js
index efa3dcdf7..25b27f00e 100644
--- a/front/src/audio/queue.js
+++ b/front/src/audio/queue.js
@@ -123,6 +123,7 @@ class Queue {
this.tracks.splice(index, 0, track)
}
if (this.ended) {
+ logger.default.debug('Playing appended track')
this.play(this.currentIndex + 1)
}
this.cache()
@@ -152,19 +153,31 @@ class Queue {
clean () {
this.stop()
+ radios.stop()
this.tracks = []
this.currentIndex = -1
this.currentTrack = null
+ // so we replay automatically on next track append
+ this.ended = true
}
cleanTrack (index) {
- if (index === this.currentIndex) {
+ // are we removing current playin track
+ let current = index === this.currentIndex
+ if (current) {
this.stop()
}
if (index < this.currentIndex) {
this.currentIndex -= 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 () {
@@ -172,12 +185,17 @@ class Queue {
this.audio.destroyed()
}
play (index) {
- if (this.audio.destroyed) {
- logger.default.debug('Destroying previous audio...')
- this.audio.destroyed()
+ let self = this
+ let currentIndex = index
+ 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
let file = this.currentTrack.files[0]
if (!file) {
@@ -193,7 +211,11 @@ class Queue {
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,
autoplay: true,
rate: 1,
@@ -201,6 +223,17 @@ class Queue {
volume: this.state.volume,
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) {
this.populateFromRadio()
}
diff --git a/front/src/components/audio/Player.vue b/front/src/components/audio/Player.vue
index 466ead0e8..b72f37c5c 100644
--- a/front/src/components/audio/Player.vue
+++ b/front/src/components/audio/Player.vue
@@ -24,7 +24,7 @@
-
+
{{queue.audio.state.currentTimeFormat}}
diff --git a/front/src/components/audio/album/Card.vue b/front/src/components/audio/album/Card.vue
index fcdf1622d..7fd60d963 100644
--- a/front/src/components/audio/album/Card.vue
+++ b/front/src/components/audio/album/Card.vue
@@ -22,6 +22,9 @@
+
+ {{ track.position }}.
+
{{ track.title }}
diff --git a/front/src/components/audio/track/Table.vue b/front/src/components/audio/track/Table.vue
index 6898353d8..e9beaa05a 100644
--- a/front/src/components/audio/track/Table.vue
+++ b/front/src/components/audio/track/Table.vue
@@ -20,9 +20,12 @@
-
- {{ track.title }}
-
+
+
+ {{ track.position }}.
+
+ {{ track.title }}
+
@@ -37,23 +40,70 @@
+
+
+
+ Download...
+
+
+
+
+
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 cURL to easily download a list of tracks.
+
+
Simply copy paste the snippet below into a terminal to launch the download.
+
+ Keep your PRIVATE_TOKEN secret as it gives access to your account.
+
+
+export PRIVATE_TOKEN="{{ auth.getAuthToken ()}}"
+
+curl -G -o "{{ track.files[0].filename }}" --header "Authorization: JWT $PRIVATE_TOKEN" "{{ backend.absoluteUrl(track.files[0].path) }}"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+