From 6172c0beacdb927b2409dc54a5c7e2b2ad56bd6b Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sun, 9 Jul 2017 13:27:15 +0200 Subject: [PATCH 1/9] upgrade to latest dynamic_preferences and use redis as cache even locally --- api/config/settings/common.py | 12 ++++++++++++ api/config/settings/local.py | 8 -------- api/config/settings/production.py | 12 +----------- api/requirements/base.txt | 2 +- 4 files changed, 14 insertions(+), 20 deletions(-) diff --git a/api/config/settings/common.py b/api/config/settings/common.py index 70804c3c9..d71992be1 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 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/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 From f1c05d4f427ee8db23cb9493cc7a04e71b94c556 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Mon, 10 Jul 2017 23:04:32 +0200 Subject: [PATCH 2/9] More robust audio player and queue in various situations: - Ensure clearing the queue also stop current radio - Will also repopulate a track from radio on track deletion in queue - Clearing the queue then appending tracks would play automatically instead of getting stuck --- front/src/audio/index.js | 2 +- front/src/audio/queue.js | 47 +++++++++++++++++++++++---- front/src/components/audio/Player.vue | 2 +- 3 files changed, 42 insertions(+), 9 deletions(-) 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}}

From 0786c58d3d417b3fecf45d0a90ac7058bb4181da Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Mon, 10 Jul 2017 23:24:04 +0200 Subject: [PATCH 3/9] Fixed #33: sort by track position in album in API vy default, also reuse that information on frontend side --- api/funkwhale_api/music/models.py | 6 ++++++ api/funkwhale_api/music/serializers.py | 10 +++++++++- front/src/components/audio/album/Card.vue | 3 +++ front/src/components/audio/track/Table.vue | 14 ++++++++++---- front/src/components/library/Album.vue | 2 +- 5 files changed, 29 insertions(+), 6 deletions(-) diff --git a/api/funkwhale_api/music/models.py b/api/funkwhale_api/music/models.py index 6a55dfc00..d7a08b7a4 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 @@ -386,6 +390,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..6b839b9cf 100644 --- a/api/funkwhale_api/music/serializers.py +++ b/api/funkwhale_api/music/serializers.py @@ -62,7 +62,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/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.title }} diff --git a/front/src/components/audio/track/Table.vue b/front/src/components/audio/track/Table.vue index 6898353d8..8dca90902 100644 --- a/front/src/components/audio/track/Table.vue +++ b/front/src/components/audio/track/Table.vue @@ -20,9 +20,12 @@ - - {{ track.title }} - + + + {{ track.title }} + @@ -46,7 +49,10 @@ import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon' import PlayButton from '@/components/audio/PlayButton' export default { - props: ['tracks'], + props: { + tracks: {type: Array, required: true}, + displayPosition: {type: Boolean, default: false} + }, components: { TrackFavoriteIcon, PlayButton diff --git a/front/src/components/library/Album.vue b/front/src/components/library/Album.vue index 5cc4d0271..494f2396b 100644 --- a/front/src/components/library/Album.vue +++ b/front/src/components/library/Album.vue @@ -34,7 +34,7 @@

Tracks

- +
From c34ea44687b99dfee80296c32a17f2ff57b2822d Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Mon, 10 Jul 2017 23:43:14 +0200 Subject: [PATCH 4/9] Fixed media path mismatch in sample docker-compose.yml --- deploy/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From e8eaf6db942b01623cd5b3a7298b5f4daac70850 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Tue, 11 Jul 2017 08:40:54 +0200 Subject: [PATCH 5/9] Now display CLI instructions to download a set of tracks --- api/funkwhale_api/music/models.py | 6 +++ api/funkwhale_api/music/serializers.py | 2 +- api/funkwhale_api/music/views.py | 5 +- front/src/components/audio/track/Table.vue | 46 ++++++++++++++++++- front/src/components/library/Album.vue | 2 +- front/src/components/semantic/Modal.vue | 53 ++++++++++++++++++++++ 6 files changed, 108 insertions(+), 6 deletions(-) create mode 100644 front/src/components/semantic/Modal.vue diff --git a/api/funkwhale_api/music/models.py b/api/funkwhale_api/music/models.py index d7a08b7a4..596f890b7 100644 --- a/api/funkwhale_api/music/models.py +++ b/api/funkwhale_api/music/models.py @@ -362,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) diff --git a/api/funkwhale_api/music/serializers.py b/api/funkwhale_api/music/serializers.py index 6b839b9cf..40fbb65ea 100644 --- a/api/funkwhale_api/music/serializers.py +++ b/api/funkwhale_api/music/serializers.py @@ -34,7 +34,7 @@ class ImportBatchSerializer(serializers.ModelSerializer): class TrackFileSerializer(serializers.ModelSerializer): class Meta: model = models.TrackFile - fields = ('id', 'path', 'duration', 'source') + fields = ('id', 'path', 'duration', 'source', 'filename') class SimpleAlbumSerializer(serializers.ModelSerializer): 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/front/src/components/audio/track/Table.vue b/front/src/components/audio/track/Table.vue index 8dca90902..e9beaa05a 100644 --- a/front/src/components/audio/track/Table.vue +++ b/front/src/components/audio/track/Table.vue @@ -40,26 +40,70 @@ + + + + + +
+ Download tracks +
+
+
+

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 ()}}"
+
+
+
+
+
+
+ Cancel +
+
+
+ + + + + + + + + + + From da4c76ace84e42ae0502fa470335b601164cc58c Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Tue, 11 Jul 2017 09:14:54 +0200 Subject: [PATCH 6/9] Now return media files with absolute URL --- api/config/settings/common.py | 3 ++- api/funkwhale_api/music/serializers.py | 9 +++++++++ deploy/nginx.conf | 2 ++ dev.yml | 2 +- docker/nginx/conf.dev | 4 +++- 5 files changed, 17 insertions(+), 3 deletions(-) diff --git a/api/config/settings/common.py b/api/config/settings/common.py index d71992be1..3f7cc7503 100644 --- a/api/config/settings/common.py +++ b/api/config/settings/common.py @@ -313,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/funkwhale_api/music/serializers.py b/api/funkwhale_api/music/serializers.py index 40fbb65ea..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', '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): 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/; } From abdafd68cdf1442de2a7e59b3ad4c6befb6da910 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Mon, 17 Jul 2017 22:01:02 +0200 Subject: [PATCH 7/9] Now also zip API code for easier release and installation --- .gitlab-ci.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 58602d296..d10b17a3d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -80,6 +80,20 @@ 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 + only: + - tags + - master + - develop + + docker_release: stage: deploy before_script: From 53e02cecb2bed37277b14c64a35829a0b9c0b902 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Mon, 17 Jul 2017 22:01:57 +0200 Subject: [PATCH 8/9] Added missing script --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d10b17a3d..24b23b84b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -88,6 +88,7 @@ build_api: name: "api_${CI_COMMIT_REF_NAME}" paths: - api + script: echo Done! only: - tags - master From 05ce2ba7218dec0f32f9454b6647e7c8336dc255 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Mon, 17 Jul 2017 22:08:44 +0200 Subject: [PATCH 9/9] Changelog && version bump --- api/funkwhale_api/__init__.py | 2 +- docs/changelog.rst | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) 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/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 -------