Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
AURA
dashboard
Commits
6678349f
Commit
6678349f
authored
May 19, 2020
by
jackie / Andrea Ida Malkah Klaura
Browse files
move file listing and show selector to own components
parent
9296781e
Changes
4
Hide whitespace changes
Inline
Side-by-side
src/components/FileManager.vue
View file @
6678349f
<
template
>
<b-container>
<show-selector
title=
"Files & playlists"
/>
<show-selector
title=
"Files & 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)"
>
📜
</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
+=
'
\n
If 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.
'
)