diff --git a/api/funkwhale_api/music/models.py b/api/funkwhale_api/music/models.py index d7a08b7a40f5f06d7501bb666e9b64323e5f1e6d..596f890b7e43b609d036c4a46040973544eaf2fd 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 6b839b9cfce30e2a650c95146a21f2fe4d6ed181..40fbb65ea4a0267ec031d1c53dfdb5e09adaa47d 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 4a4032c57f80695ba88fb92826941be3ede23605..98319255270bfdae84f70e3335fc4917f650885b 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 8dca90902efec2027fb22e7a8650e399d8bc8d9b..e9beaa05a5ac8f0bb6285597a9316eb8afab4e4f 100644 --- a/front/src/components/audio/track/Table.vue +++ b/front/src/components/audio/track/Table.vue @@ -40,26 +40,70 @@ <td><track-favorite-icon class="favorite-icon" :track="track"></track-favorite-icon></td> </tr> </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> </template> <script> import backend from '@/audio/backend' +import auth from '@/auth' import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon' import PlayButton from '@/components/audio/PlayButton' +import Modal from '@/components/semantic/Modal' + export default { props: { tracks: {type: Array, required: true}, displayPosition: {type: Boolean, default: false} }, components: { + Modal, TrackFavoriteIcon, PlayButton }, data () { return { - backend: backend + backend: backend, + auth: auth, + showDownloadModal: false } } } diff --git a/front/src/components/library/Album.vue b/front/src/components/library/Album.vue index 494f2396b9bc976d7a6c4593656517abc2cc84d5..cf3403400b8a07afc56cd0ca0edd7874ca8bacf6 100644 --- a/front/src/components/library/Album.vue +++ b/front/src/components/library/Album.vue @@ -34,7 +34,7 @@ </div> <div class="ui vertical stripe segment"> <h2>Tracks</h2> - <track-table v-if="album" display-position="true" :tracks="album.tracks"></track-table> + <track-table v-if="album" :display-position="true" :tracks="album.tracks"></track-table> </div> </template> </div> diff --git a/front/src/components/semantic/Modal.vue b/front/src/components/semantic/Modal.vue new file mode 100644 index 0000000000000000000000000000000000000000..ec7a5a0884262ac2437570cd6a360e231eaccaba --- /dev/null +++ b/front/src/components/semantic/Modal.vue @@ -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>