Commit 00732b10 authored by jackie / Andrea Ida Malkah Klaura's avatar jackie / Andrea Ida Malkah Klaura
Browse files

Merge branch 'feature-playlist' into develop

parents d93865c2 0b7c2033
VUE_APP_API_STEERING = http://127.0.0.1:8000/api/v1/ VUE_APP_API_STEERING = http://127.0.0.1:8000/api/v1/
VUE_APP_API_STEERING_SHOWS = http://127.0.0.1:8000/api/v1/shows/ VUE_APP_API_STEERING_SHOWS = http://127.0.0.1:8000/api/v1/shows/
VUE_APP_API_TANK = http://127.0.0.1:4000/api/v1/
VUE_APP_OIDC_CLIENT_ID = 078036 VUE_APP_OIDC_CLIENT_ID = 078036
VUE_APP_API_STEERING_OIDC_URI = http://localhost:8000/openid VUE_APP_API_STEERING_OIDC_URI = http://localhost:8000/openid
VUE_APP_API_STEERING_OIDC_EXPIRE_NOTIFICATION = 120 VUE_APP_API_STEERING_OIDC_EXPIRE_NOTIFICATION = 120
......
...@@ -5,6 +5,10 @@ ...@@ -5,6 +5,10 @@
VUE_APP_API_STEERING = http://YOUR.STEERING.DOMAIN/api/v1/ VUE_APP_API_STEERING = http://YOUR.STEERING.DOMAIN/api/v1/
VUE_APP_API_STEERING_SHOWS = http://YOUR.STEERING.DOMAIN/api/v1/shows/ VUE_APP_API_STEERING_SHOWS = http://YOUR.STEERING.DOMAIN/api/v1/shows/
# aura/tank REST API settings
# ===========================
VUE_APP_API_TANK = http://YOUR.TANK.DOMAIN/api/v1/
# Open ID Connect settings # Open ID Connect settings
# ======================== # ========================
......
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -9,20 +9,21 @@ ...@@ -9,20 +9,21 @@
}, },
"dependencies": { "dependencies": {
"axios": "^0.18.0", "axios": "^0.18.0",
"bootstrap-vue": "^2.0.0-rc.11", "bootstrap-vue": "^2.0.0-rc.19",
"oidc-client": "^1.6.1", "filesize": "^4.1.2",
"vue": "^2.5.22", "oidc-client": "^1.7.1",
"vue-router": "^3.0.1" "vue": "^2.6.10",
"vue-router": "^3.0.6"
}, },
"devDependencies": { "devDependencies": {
"@vue/cli-plugin-babel": "^3.4.0", "@vue/cli-plugin-babel": "^3.6.0",
"@vue/cli-plugin-eslint": "^3.4.0", "@vue/cli-plugin-eslint": "^3.6.0",
"@vue/cli-service": "^3.4.0", "@vue/cli-service": "^3.6.0",
"babel-eslint": "^10.0.1", "babel-eslint": "^10.0.1",
"copy-webpack-plugin": "^4.6.0", "copy-webpack-plugin": "^4.6.0",
"eslint": "^5.8.0", "eslint": "^5.16.0",
"eslint-plugin-vue": "^5.0.0", "eslint-plugin-vue": "^5.2.2",
"vue-template-compiler": "^2.5.21" "vue-template-compiler": "^2.6.10"
}, },
"eslintConfig": { "eslintConfig": {
"root": true, "root": true,
...@@ -33,7 +34,9 @@ ...@@ -33,7 +34,9 @@
"plugin:vue/essential", "plugin:vue/essential",
"eslint:recommended" "eslint:recommended"
], ],
"rules": {}, "rules": {
"no-console": "off"
},
"parserOptions": { "parserOptions": {
"parser": "babel-eslint" "parser": "babel-eslint"
} }
......
<template> <template>
<b-container align="center"> <b-container>
<h1>Dateien und Playlists verwalten</h1> <b-row>
<br /><br /> <b-col>
<div style="border: 5px dotted #5c3566;"> <h3>Dateien und Playlists</h3>
<br /><br /> </b-col>
<p><b>By the mighty witchcraftry of the mother of time!</b></p> <b-col align="right">
<p>This feature is not implemented yet.</p> <b-dropdown id="ddshows" text="Sendereihe auswählen" variant="info">
<br /><br /> <b-dropdown-item v-for="(show, index) in this.shows" :key="show.id" v-on:click="switchShow(index)">{{ show.name }}</b-dropdown-item>
</b-dropdown>
</b-col>
</b-row>
<hr />
<b-jumbotron>
<template slot="header">
<span v-if="loaded.shows">
{{ shows[currentShow].name }}
</span>
<span v-else>Shows are being loaded</span>
</template>
<template slot="lead">
<span v-if="loaded.shows">{{ shows[currentShow].short_description }}</span>
</template>
<hr />
<div align="center">
<b-button-group>
<b-button size="lg" :variant="button.files" @click="switchMode('files')">Files</b-button>
<b-button size="lg" :variant="button.playlists" @click="switchMode('playlists')">Playlists</b-button>
</b-button-group>
</div>
</b-jumbotron>
<div v-if="mode === 'files'">
<div v-if="!loaded.files">
<b-row>
<b-col align="center">
<img src="../assets/radio.gif" alt="loading data" />
</b-col>
</b-row>
</div>
<div v-else>
<b-modal id="modal-add-file" title="Add new file" @ok="addFile">
<div v-if="addNewFileURI">
<b-row>
<b-col md="2">
<b>Link:</b>
</b-col>
<b-col>
<b-form-input type="url" v-model="uploadSourceURI" placeholder="Insert a HTTP(S) link here"></b-form-input>
</b-col>
</b-row>
</div>
<div v-else>
<b-form-file v-model="uploadSourceFile" accept="audio/*" placeholder="Choose a file..." drop-placeholder="Drop file here..."></b-form-file>
</div>
<hr>
<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>
<b-modal id="modal-edit-file" title="Edit meta information" size="lg" @ok="saveFile">
<b-row>
<b-col md="2">Artist:</b-col>
<b-col><b-form-input v-model="temp.artist" type="text"></b-form-input></b-col>
</b-row>
<b-row>
<b-col md="2">Album:</b-col>
<b-col><b-form-input v-model="temp.album" type="text"></b-form-input></b-col>
</b-row>
<b-row>
<b-col md="2">Title:</b-col>
<b-col><b-form-input v-model="temp.title" type="text"></b-form-input></b-col>
</b-row>
<hr />
<b-row>
<b-col md="2">Sourced from:</b-col>
<b-col>{{ temp.uri }}</b-col>
</b-row>
</b-modal>
<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 variant="success" v-b-modal.modal-add-file>Upload or add a file</b-button>
</div>
<div v-else>
<div align="center" style="padding-bottom: 1.5em;">
<b-button variant="success" v-b-modal.modal-add-file>Upload or add a file</b-button>
</div>
<b-table ref="filesTable" striped :fields="filesTableFields" :items="files">
<template slot="metadata.album" slot-scope="data">
<span v-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>
<template slot="metadata.title" slot-scope="data">
<span v-if="data.item.source.import.state === 'done'">{{ data.value }}</span>
<span v-else-if="data.item.source.import.progress !== undefined">{{ data.item.source.import.progress.step }} :</span>
</template>
<template slot="duration" slot-scope="data">
<div v-if="data.item.source.import.state === 'done'">{{ prettyNanoseconds(data.value) }}</div>
<div v-else-if="data.item.source.import.progress !== undefined">
<div v-if="data.item.source.import.progress.step === 'fetching'">
<b-progress :value="data.item.source.import.progress.progress" :max="1" show-progress variant="info" animated></b-progress>
</div>
<div v-else>
<b-progress :value="data.item.source.import.progress.progress" :max="1" show-progress variant="success" animated></b-progress>
</div>
</div>
</template>
<template slot="size" slot-scope="data">
<span v-if="data.item.source.import.state === 'done'">{{ prettyFileSize(data.value) }}</span>
</template>
<template slot="actions" slot-scope="data">
<b-button-group size="sm">
<b-button @click="editFile(data.item.id)">Edit</b-button>
<b-button variant="danger" @click="deleteFile(data.item.id)">Delete</b-button>
</b-button-group>
</template>
</b-table>
</div>
</div>
</div>
<div v-if="mode === 'playlists'">
<div v-if="!loaded.playlists">
<b-row>
<b-col align="center">
<img src="../assets/radio.gif" alt="loading data" />
</b-col>
</b-row>
</div>
<div v-else>
<div v-if="playlists.length === 0" align="center">
<b-alert show variant="warning">There are no playlists for this show yet.</b-alert>
<b-button variant="success" @click="notYetImplemented">Create a playlist</b-button>
</div>
<div v-else>
<b-table striped :items="playlistsTable" />
</div>
</div>
</div> </div>
</b-container> </b-container>
</template> </template>
<script> <script>
export default {} import axios from 'axios'
import filesize from 'filesize'
import prettyDate from '../mixins/prettyDate'
export default {
data () {
return {
shows: [], // an array of objects describing our shows (empty at load, will be populated on created())
currentShow: 0, // index of the currently selected show in our shows array
currentShowID: 0, // actual id of the currently selected show
files: [],
playlists: [],
mode: 'files',
addNewFileURI: false,
uploadSourceURI: '',
uploadSourceFile: null,
uploadInterval: null,
loaded: {
shows: false,
files: false,
playlists: false
},
temp: {
id: null,
artist: '',
album: '',
title: ''
},
button: {
files: 'info',
playlists: 'outline-info'
},
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' },
]
}
},
mixins: [ prettyDate ],
computed: {
playlistsTable: function (){
var arr = []
for (var i in this.files) {
arr.push({
id: this.files[i].id,
other_fields: 'not yet implemented',
updated: this.files[i].updated,
actions: '...'
})
}
return arr
}
},
methods: {
notYetImplemented: function () {
alert('By the mighty witchcraftry of the mother of time!\n\nThis feature is not implemented yet.')
},
prettyFileSize: function (s) {
return filesize(s)
},
getFileById: function (id) {
for (var i in this.files) {
if (this.files[i].id === id) {
return this.files[i]
}
}
return null
},
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')
},
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 => {
console.log('Error:')
console.log(error)
alert('Error: could not save metadata info to file. See console log for details.')
})
}
},
deleteFile: function (id) {
var 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.fetchFiles(this.shows[this.currentShow].slug)
).catch(error => {
console.log('Error:')
console.log(error)
alert('Error: could not delete file. See console log for details.')
})
},
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 => {
console.log('Error:')
console.log(error)
alert('Error: could not add the new remote import. See console log 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('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 => {
console.log('Error:')
console.log(error)
alert('Error: could not add the new file upload. See console log for details.')
})
} else {
alert('Something is wrong. You have choosen to upload a file, but the corresponding file object does not exist.')
}
},
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 => {
console.log('Error:')
console.log(error)
alert('Error: could not start the file upload. See console log for details.')
})
},
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) {
console.log(file, message)
})
flow.on('fileError', function(file, message) {
console.log(file, message)
alert('Error: could not upload your file. See console log 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(() => {
console.log('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 => {
console.log('Error:')
console.log(error)
alert('Error: could not start the file upload. See console log for details.')
})
},
switchShow: function (index) {
// set the current show and its ID to whatever we want to switch to now
this.currentShow = index
this.currentShowID = this.shows[this.currentShow].id
this.fetchShow(this.shows[this.currentShow].slug)
},
switchMode: function (mode) {
if (this.mode !== mode) {
this.mode = mode
for (var b in this.button) {
if (b === mode) this.button[b] = 'info'
else this.button[b] = 'outline-info'
}
}
},
fetchImports: function (slug){
var uri = process.env.VUE_APP_API_TANK + 'shows/' + slug + '/imports'
axios.get(uri, {
withCredentials: true,
headers: { 'Authorization': 'Bearer ' + this.$parent.user.access_token }
}).then(response => {
// if all imports are done, we will receive an empty result set and stop
// polling the server again. now we can also refetch all file statuses.
if (response.data.results === null) {
clearInterval(this.uploadInterval)
this.uploadInterval = null
this.fetchShow(slug)
} else {
for (var i in response.data.results) {
var f = this.getFileById(response.data.results[i].id)
if (f) {
f.source.import.progress = {
progress: response.data.results[i].progress.progress,
step: response.data.results[i].progress.step
}
}
var p = '[import]'
p += ' id: ' + response.data.results[i].id
p += ', progress: ' + response.data.results[i].progress.progress
p += ', step: ' + response.data.results[i].progress.step
console.log(p)
this.$refs.filesTable.refresh()
}
}
}).catch(error => {
console.log(error)
alert('There was an error fetching current imports. See console for details.')
})
},
fetchShow: function (slug) {
this.fetchFiles(slug)
this.fetchPlaylists(slug)
},
fetchFiles: function (slug) {
this.loaded.files = false
var uri = process.env.VUE_APP_API_TANK + 'shows/' + slug + '/files'
axios.get(uri, {
withCredentials: true,
headers: { 'Authorization': 'Bearer ' + this.$parent.user.access_token }
}).then(response => {
// we don't have to check separately, if there are files, because tank
// always provides an empty array if there are no files (or even if there is no corresponding show)
this.files = response.data.results
this.loaded.files = true
}).catch(error => {
alert('There was an error fetching files from tank: ' + error)
})
},
fetchPlaylists: function (slug) {
this.loaded.playlists = false
var uri = process.env.VUE_APP_API_TANK + 'shows/' + slug + '/playlists'
axios.get(uri, {
withCredentials: true,
headers: { 'Authorization': 'Bearer ' + this.$parent.user.access_token }
}).then(response => {
// we don't have to check separately, if there are playlists, because tank
// always provides an empty array if there are no playlists (or even if there is no corresponding show)
this.playlists = response.data.results
this.loaded.playlists = true
}).catch(error => {
alert('There was an error fetching playlists from tank: ' + error)
})
}
},
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 now 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 => {
alert('There was an error fetching shows from steering: ' + error)
})
}
}
</script> </script>
<style> <style>
div.filelistbox {
border: 1px solid #e9ecef;
border-radius: 0.3rem;
padding: 1rem 2rem;
}
.stateNew {
color: red;
font-weight: bold;
}
.stateRunning {
color: darkgreen;
}
.stateUndefined {
color: orange;
font-weight: bold;
}
</style> </style>
...@@ -68,6 +68,13 @@ export default { ...@@ -68,6 +68,13 @@ export default {
duration += 'sec' duration += 'sec'
} }
return duration return duration
},
prettyNanoseconds: function(ns) {
var sec_total = ns / 1000 / 1000 / 1000
var hours = Math.floor(sec_total / 3600)
var minutes = Math