Commit 6678349f authored by jackie / Andrea Ida Malkah Klaura's avatar jackie / Andrea Ida Malkah Klaura
Browse files

move file listing and show selector to own components

parent 9296781e
<template>
<b-container>
<show-selector title="Files &amp; playlists" />
<show-selector
title="Files &amp; playlists"
:callback="showHasSwitched"
/>
<hr>
<jumbotron />
......@@ -8,277 +11,7 @@
<!-- All the UI for uploading and editing files is only shown if the user
choose to edit files in the jumbotron above -->
<div v-if="mode === 'files'">
<!-- Only display a spinner if the files are not loaded yet -->
<div v-if="!loaded.files">
<b-row>
<b-col align="center">
<img
src="../assets/radio.gif"
alt="loading data"
>
</b-col>
</b-row>
</div>
<!-- If all file data is loaded we can present an upload button and
a table of files (if there are already any or if there are any already
being uploaded/imported )-->
<div v-else>
<!-- Modal: Add new file
This is the modal that is used when the user clicks on the
"Upload or add a file" button. When the user hits the OK button,
the addFile method will be called. -->
<b-modal
id="modal-add-file"
title="Add new file"
@ok="addFile"
>
<!-- If the checkbox below is checked we only need a simple input
for entering an URI, instead of a complete file upload dialogue -->
<div v-if="addNewFileURI">
<b-row>
<b-col md="2">
<b>Link:</b>
</b-col>
<b-col>
<b-form-input
v-model="uploadSourceURI"
type="url"
placeholder="Insert a HTTP(S) link here"
/>
</b-col>
</b-row>
</div>
<!-- If the checkbox is not checked, we provide a file selection
dialogue instead of a simple input for an URI -->
<div v-else>
<b-form-file
v-model="uploadSourceFile"
accept="audio/*"
placeholder="Choose a file..."
drop-placeholder="Drop file here..."
/>
</div>
<hr>
<!-- This checkbox determines whether there is just a simple input
or a file selection dialogue (see above) -->
<div align="center">
<b-form-checkbox
v-model="addNewFileURI"
value="true"
unchecked_value="false"
>
Download from remote source instead of uploading a file
</b-form-checkbox>
</div>
</b-modal>
<!-- Modal: Edit meta information
This modal lets the user change a file's meta information (artist,
album, title). When the user hits OK, the saveFile method is called.
-->
<b-modal
id="modal-edit-file"
title="Edit meta information"
size="lg"
@ok="saveFile"
>
<!-- Input element: Artist -->
<b-row>
<b-col md="2">
Artist:
</b-col>
<b-col>
<b-form-input
v-model="temp.artist"
type="text"
/>
</b-col>
</b-row>
<!-- Input element: Album -->
<b-row>
<b-col md="2">
Album:
</b-col>
<b-col>
<b-form-input
v-model="temp.album"
type="text"
/>
</b-col>
</b-row>
<!-- Input element: Title -->
<b-row>
<b-col md="2">
Title:
</b-col>
<b-col>
<b-form-input
v-model="temp.title"
type="text"
/>
</b-col>
</b-row>
<hr>
<!-- As additional orientation we provide the source this file was
uploaded/imported from. This might help the user to manage all files
and set the meta information accordingly.
-->
<b-row>
<b-col md="2">
Sourced from:
</b-col>
<b-col>{{ temp.uri }}</b-col>
</b-row>
</b-modal>
<!-- Now for the files. If there are none yet, just show an alert and
a button for uploading/importing a new file -->
<div
v-if="files.length === 0"
align="center"
>
<b-alert
show
variant="warning"
>
There are no files for this show yet.
</b-alert>
<b-button
v-b-modal.modal-add-file
variant="success"
>
Upload or add a file
</b-button>
</div>
<!-- If there already are files for this show we'll show the button
for uploading/importing a new file and then a table with all files -->
<div v-else>
<!-- This is the button -->
<div
align="center"
style="padding-bottom: 1.5em;"
>
<b-button
v-b-modal.modal-add-file
variant="success"
>
Upload or add a file
</b-button>
</div>
<!-- We also import the modal for showing file import logs here -->
<app-modalFileManagerLog ref="appModalFileManagerLog" />
<!-- And here comes the table -->
<b-table
ref="filesTable"
striped
:fields="filesTableFields"
:items="files"
>
<!-- The first two columns in the table (Index & Artist) are filled
in automatically, because we do not use these fields for displaying
upload/import progress information.
-->
<!-- Column: Album
This column displays either the album meta information or, in case
the file is just being uploaded/imported, a spinner visualising an
ongoing import.
-->
<template v-slot:cell(metadata.album)="data">
<span v-if="data.item.source.import.state === 'aborted'"><b-badge variant="danger">Error:</b-badge></span>
<span v-else-if="data.item.source.import.state !== 'done'"><img
src="../assets/radio.gif"
width="24"
alt="loading data"
></span>
<span v-else>{{ data.value }}</span>
</template>
<!-- Column: Title
This column displays either the title meta information or, in case
the file is just being uploaded/imported, the current phase
of the ongoing import (fetching or normalizing).
-->
<template v-slot:cell(metadata.title)="data">
<span v-if="data.item.source.import.state === 'done'">{{ data.value }}</span>
<span v-else-if="data.item.source.import.state === 'aborted'">import was aborted</span>
<span v-else-if="data.item.source.import.progress !== undefined">{{ data.item.source.import.progress.step }} :</span>
</template>
<!-- Column: Duration
This column displays either the duration of the audio file, or, if
the file is just being uploaded/imported, the current progress.
-->
<template v-slot:cell(duration)="data">
<!-- In case the import is already done just print a pretty duration -->
<div v-if="data.item.source.import.state === 'done'">
{{ prettyNanoseconds(data.value) }}
</div>
<!-- If the import was aborted show some error info -->
<div v-else-if="data.item.source.import.state === 'aborted'">
{{ data.item.source.import.error }}
</div>
<!-- Or print the progress for ongoing imports. We use the variant
prop of the b-progress to display the bar in different colours -
either the info variant for the fetching phase or the success
variant for the normalizing phase -->
<div v-else-if="data.item.source.import.progress !== undefined">
<b-progress
:value="data.item.source.import.progress.progress"
:max="1"
show-progress
:variant="data.item.source.import.progress.step === 'fetching' ? 'info' : 'success'"
animated
/>
</div>
</template>
<!-- Column: Size
This column displays the size of the audio file, if the file is
already fully imported. Otherwise we'll just leave it empty.
-->
<template v-slot:cell(size)="data">
<span v-if="data.item.source.import.state === 'done'">{{ prettyFileSize(data.value) }}</span>
</template>
<!-- Column: Actions
This column displays the available button for actions the user can
take on this file (e.g. editing and deleting).
-->
<template v-slot:cell(actions)="data">
<b-button-group size="sm">
<b-button @click="editFile(data.item.id)">
Edit
</b-button>
<b-button
v-b-tooltip.hover
title="Show import log"
@click="$refs.appModalFileManagerLog.show(data.item.show, data.item.id)"
>
&#128220;
</b-button>
<b-button
variant="danger"
@click="deleteFile(data.item.id)"
>
Delete
</b-button>
</b-button-group>
</template>
</b-table>
</div>
<!-- End of files table -->
</div>
<files />
</div>
<!-- All the UI for creating and editing playlists is only shown if the user
......@@ -571,19 +304,19 @@
</template>
<script>
import { mapGetters } from 'vuex'
import axios from 'axios'
import filesize from 'filesize'
import prettyDate from '../mixins/prettyDate'
import modalFileManagerLog from './FileManagerModalLog.vue'
import showSelector from './ShowSelector.vue'
import jumbotron from './filemanager/Jumbotron.vue'
import files from './filemanager/Files.vue'
export default {
// include the modal for displaying file import logs
components: {
'app-modalFileManagerLog': modalFileManagerLog,
'show-selector': showSelector,
'jumbotron': jumbotron,
'files': files,
},
// generic functions that we want to use from our mixins folder
......@@ -602,17 +335,10 @@ export default {
uploadInterval: null,
// some flags to signal if API data is already fully loaded
loaded: {
shows: false,
files: false,
loadedLocal: {
playlists: false
},
// variables used by file upload/import modal
addNewFileURI: false,
uploadSourceURI: '',
uploadSourceFile: null,
// and for adding and editing the playlists we need some temporary stuff
playlistEditor: {
id: null,
......@@ -622,31 +348,12 @@ export default {
newStreamURL: null
},
// we need this for the modal to edit a file's meta information
temp: {
id: null,
artist: '',
album: '',
title: ''
},
// for formatting the buttons - this way we could customize it later
button: {
files: 'info',
playlists: 'outline-info'
},
// configuration of the files table, which will use the files array as data
filesTableFields: [
{ key: 'id', label: 'Index' },
{ key: 'metadata.artist', label: 'Artist' },
{ key: 'metadata.album', label: 'Album' },
{ key: 'metadata.title', label: 'Title' },
{ key: 'duration', label: 'Duration' },
{ key: 'size', label: 'Size' },
{ key: 'actions', label: 'Actions', class: 'text-right' },
],
// configuration of the playlists table, which will use the playlists array as data
playlistsTableFields: [
{ key: 'id', label: 'Index' },
......@@ -665,37 +372,27 @@ export default {
}
},
computed: {
loaded () {
return {
shows: this.$store.state.shows.loaded.shows,
files: this.$store.state.files.loaded.files,
playlists: this.loadedLocal.playlists
}
},
...mapGetters({
selectedShow: 'shows/selectedShow',
})
},
// Right after this component is set up, we want to fetch all available shows
// from the AuRa tank module. This works quite similar to the ShowManager.
// We also want to load the files and playlists as soon as the shows are
// loaded.
created () {
// when we enter this module, we want to load all shows of the current user
// before we search for corresponding shows in the tank
var uri = process.env.VUE_APP_API_STEERING_SHOWS
// only the superuser should see all shows and therefore files and playlists
// normal users should only see their own shows
if (!this.$parent.user.steeringUser.is_superuser) {
uri += '?owner=' + this.$parent.user.steeringUser.id
}
// now make the API call to fetch the shows
axios.get(uri, {
withCredentials: true,
headers: { 'Authorization': 'Bearer ' + this.$parent.user.access_token }
}).then(response => {
if (response.data.length === 0) {
alert('There are no shows associated with your account!')
return
}
this.shows = response.data
this.currentShowID = this.shows[0].id
this.currentShow = 0
this.loaded.shows = true
this.switchShow(this.currentShow)
}).catch(error => {
this.$log.error(error.response.status + ' ' + error.response.statusText)
this.$log.error(error.response)
alert('Error: could not fetch shows from steering. See console for details.')
this.$store.dispatch('shows/fetchShows', {
callback: () => { this.showHasSwitched() }
})
},
......@@ -900,302 +597,19 @@ export default {
}
},
// To start modifying the meta information for a file we have to set our
// temporary data (which will be used to check if anything changed) and
// then open the editing modal
editFile: function (id) {
var file = this.getFileById(id)
this.temp.id = file.id
this.temp.artist = file.metadata.artist
this.temp.album = file.metadata.album
this.temp.title = file.metadata.title
this.temp.uri = file.source.uri
this.$bvModal.show('modal-edit-file')
},
// Once the OK button is hit in the file editing modal, we have to check
// if anything changed and then send an appropriate metadata obecjt to
// the AuRa tank API to update it
saveFile: function (){
var file = this.getFileById(this.temp.id)
// we only want to send a PATCH request if some metadata actually changed
if (this.temp.artist !== file.metadata.artist || this.temp.album !== file.metadata.album || this.temp.title !== file.metadata.title ) {
// if a metadata property was in use before and now shall be emptied
// we cannot just omit the property, but have to explicitly send null
var metadata = {
artist: this.temp.artist ? this.temp.artist : null,
album: this.temp.album ? this.temp.album : null,
title: this.temp.title ? this.temp.title : null
}
var uri = process.env.VUE_APP_API_TANK + 'shows/' + this.shows[this.currentShow].slug + '/files/' + file.id
// TODO: add mechanism to indicate the running patch request in the files table
axios.patch(uri, metadata, {
withCredentials: true,
headers: { 'Authorization': 'Bearer ' + this.$parent.user.access_token }
}).then(response => {
this.getFileById(this.temp.id).metadata = response.data.metadata
}).catch(error => {
this.$log.error(error.response.status + ' ' + error.response.statusText)
this.$log.error(error.response)
alert('Error: could not save metadata info to file. See console for details.')
})
}
},
// Deletes a file with a specific ID calling the AuRa tank API and afterwards
// fetching a fresh list of files from it.
deleteFile: function (id) {
let uri = process.env.VUE_APP_API_TANK + 'shows/' + this.shows[this.currentShow].slug + '/files/' + id
// TODO: add mechanism to indicate the running delete request in the files table
axios.delete(uri, {
withCredentials: true,
headers: { 'Authorization': 'Bearer ' + this.$parent.user.access_token },
}).then(() => {
this.$log.debug(`Fetching files for show ${this.shows[this.currentShow].slug} (ID: ${id})`)
this.fetchFiles(this.shows[this.currentShow].slug)
}).catch(error => {
// if there was a 409 Conflict response it means, that this file is
// still used in one or more playlists.
if (error.response.status === 409) {
let pls = error.response.data.detail.playlists.length
let msg = 'Cannot delete file. Still used in ' + pls + ' playlists:\n\n'
for (let pl of error.response.data.detail.playlists) {
msg += 'ID: ' + pl.id
if (pl.description) {
msg += ' (' + pl.description + ')'
}
msg += '\n'
}
msg += '\nIf you want to delete the file, remove it from those playlists first.'
alert(msg)
} else {
this.$log.error(error.response.status + ' ' + error.response.statusText)
this.$log.error(error.response)
alert('Error: could not delete file. See console for details.')
}
})
},
// With this function we add a new file in the AuRa tank by calling its API.
// Depending on wheter we add a remote file which tank then imports by itself,
// or if we want to upload a local file, we source-uri has to look different.
// And for uploading a local file this is just the first step. Afterwards the
// actual upload has to be started with the startUpload function.
addFile: function () {
var uri = process.env.VUE_APP_API_TANK + 'shows/' + this.shows[this.currentShow].slug + '/files'
if (this.addNewFileURI) {
// TODO: add mechanism to indicate the running post request in the files table
axios.post(uri, { 'source-uri': this.uploadSourceURI }, {
withCredentials: true,
headers: { 'Authorization': 'Bearer ' + this.$parent.user.access_token }
}).then(() => {
this.fetchFiles(this.shows[this.currentShow].slug)
if (this.uploadInterval === null) {
this.uploadInterval = setInterval(() => { this.fetchImports(this.shows[this.currentShow].slug) }, 250)
}
}).catch(error => {
this.$log.error(error.response.status + ' ' + error.response.statusText)
this.$log.error(error.response)
alert('Error: could not add the new remote import. See console for details.')
})
} else if (this.uploadSourceFile) {
// TODO: add mechanism to indicate the running post request in the files table
axios.post(uri, { 'source-uri': encodeURI(encodeURI('upload://' + this.uploadSourceFile.name)) }, {
withCredentials: true,
headers: { 'Authorization': 'Bearer ' + this.$parent.user.access_token }
}).then(response => {
this.startUpload(response.data.id)
this.fetchFiles(this.shows[this.currentShow].slug)
if (this.uploadInterval === null) {
this.uploadInterval = setInterval(() => { this.fetchImports(this.shows[this.currentShow].slug) }, 250)
}
}).catch(error => {
this.$log.error(error.response.status + ' ' + error.response.statusText)
this.$log.error(error.response)
alert('Error: could not add the new file upload. See console for details.')
})
} else {
alert('Something is wrong. You have choosen to upload a file, but the corresponding file object does not exist.')
}
},
// When a new file was added with the addFile function we can start an upload
// fetching the import endpoint of this file and then call the upload
// function, which atually puts the file onto the server.
startUpload: function (id) {
var uri = process.env.VUE_APP_API_TANK + 'shows/' + this.shows[this.currentShow].slug + '/files/' + id + '/import'
axios.get(uri, {
withCredentials: true,
headers: { 'Authorization': 'Bearer ' + this.$parent.user.access_token },
params: {'wait-for': 'running'}
}).then(
this.upload(id)
).catch(error => {
this.$log.error(error.response.status + ' ' + error.response.statusText)
this.$log.error(error.response)
alert('Error: could not start the file upload. See console for details.')
})
},
// Upload a file to the AuRa tank API - given it was created with the addFile
// and started with the startUpload methods.
upload: function (id) {
/*
* NOTE: there is no npm package for flow.js and importing it manually did not
* work so far. therefore this is commented out and we are using the simple
* upload method, until there is a nice npm package for flow.js or somone
* resolves this issue otherwise
var flow = new Flow({
target: process.env.VUE_APP_API_TANK + 'shows/' + this.shows[this.currentShow].slug + '/files/' + id + '/upload',
chunkSize: 100 * 1024,
prioritizeFirstAndLastChunk: true
})
flow.on('fileSuccess', function(file, message) {
this.$log.error(file, message)
})
flow.on('fileError', function(file, message) {
this.$log.error(file, message)
alert('Error: could not upload your file. See console for details.')
})
flow.addFile(this.uploadSourceFile)
flow.upload()
*/
var uri = process.env.VUE_APP_API_TANK + 'shows/' + this.shows[this.currentShow].slug + '/files/' + id + '/upload'
axios.put(uri, this.uploadSourceFile, {
withCredentials: true,
headers: {
'Authorization': 'Bearer ' + this.$parent.user.access_token,
'Content-Type': 'application/octet-stream'
}
}).then(() => {
this.$log.info('Sucessfully uploaded file.')
// now we start polling for the import progress
// the fetchImports function has to make sure to deactivate the interval
// again, when all running imports are done (in this first raw version;
// ideally we should refine this so that every single file gets updated independently)
//this.uploadInterval = setInterval(() => { this.fetchImports(this.shows[this.currentShow].slug) }, 100)
}).catch(error => {
if (error.response.status === 500 && error.response.data.error === 'ffmpeg returned 1') {
this.$log.error(error.response.status + ' ' + error.response.statusText)
this.$log.error(error.response)
// if we use a file format that is not supported by ffmpeg, we should find
// the second to last line should notify us about invalid data
let ffmpegError = error.response.data.detail[error.response.data.detail.length - 2]
if (ffmpegError.line === 'pipe:: Invalid data found when processing input') {
// in this case we can make the error message in the files table more specific
alert('Error: import aborted. The audio data format of your file is not valid!')
} else {
alert('Error: ffmpeg could not processs your file! See console for details.')
}
} else {
this.$log.error(error.response.status + ' ' + error.response.statusText)
this.$log.error(error.response)
alert('Error: could not finish the file upload/import. See console for details.')