FileManager.vue 19.1 KB
Newer Older
1
<template>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
  <b-container>
    <b-row>
      <b-col>
        <h3>Dateien und Playlists</h3>
      </b-col>
      <b-col align="right">
        <b-dropdown id="ddshows" text="Sendereihe auswählen" variant="info">
          <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>
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
        <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>
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
        <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>
82
83
        <div v-if="files.length === 0" align="center">
          <b-alert show variant="warning">There are no files for this show yet.</b-alert>
84
          <b-button variant="success"  v-b-modal.modal-add-file>Upload or add a file</b-button>
85
86
        </div>
        <div v-else>
87
88
89
          <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>
90
          <b-table ref="filesTable" striped :fields="filesTableFields" :items="files">
91
            <template slot="metadata.album" slot-scope="data">
92
93
94
              <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>
95
            <template slot="metadata.title" slot-scope="data">
96
97
              <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>
98
            </template>
99
            <template slot="duration" slot-scope="data">
100
              <div v-if="data.item.source.import.state === 'done'">{{ prettyNanoseconds(data.value) }}</div>
101
102
103
104
105
106
107
108
              <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>
109
110
            </template>
            <template slot="size" slot-scope="data">
111
              <span v-if="data.item.source.import.state === 'done'">{{ prettyFileSize(data.value) }}</span>
112
113
114
115
116
117
118
119
            </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>
120
        </div>
121
122
123
124
125
126
127
128
129
130
131
132
      </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>
133
134
135
136
137
138
139
        <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>
140
      </div>
141
    </div>
142
  </b-container>
143
144
145
</template>

<script>
146
import axios from 'axios'
147
148
import filesize from 'filesize'
import prettyDate from '../mixins/prettyDate'
149
150
151
152
153
154
155

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
156
157
      files: [],
      playlists: [],
158
      mode: 'files',
159
160
161
      addNewFileURI: false,
      uploadSourceURI: '',
      uploadSourceFile: null,
162
      uploadInterval: null,
163
164
165
166
167
      loaded: {
        shows: false,
        files: false,
        playlists: false
      },
168
169
170
171
172
173
      temp: {
        id: null,
        artist: '',
        album: '',
        title: ''
      },
174
175
176
      button: {
        files: 'info',
        playlists: 'outline-info'
177
178
179
180
181
182
183
184
185
186
      },
      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' },
      ]
187
188
    }
  },
189
  mixins: [ prettyDate ],
190
191
192
193
194
195
196
197
198
199
200
201
202
203
  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
    }
  },
204
  methods: {
205
206
207
    notYetImplemented: function () {
      alert('By the mighty witchcraftry of the mother of time!\n\nThis feature is not implemented yet.')
    },
208
209
210
    prettyFileSize: function (s) {
      return filesize(s)
    },
211
212
213
214
215
216
217
218
    getFileById: function (id) {
      for (var i in this.files) {
        if (this.files[i].id === id) {
          return this.files[i]
        }
      }
      return null
    },
219
    editFile: function (id) {
220
221
222
223
224
225
226
227
228
229
      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)
230
      // we only want to send a PATCH request if some metadata actually changed
231
      if (this.temp.artist !== file.metadata.artist || this.temp.album !== file.metadata.album || this.temp.title !== file.metadata.title ) {
232
233
234
        // 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 = {
235
          artist: this.temp.artist ? this.temp.artist : null,
236
237
238
          album: this.temp.album ? this.temp.album : null,
          title: this.temp.title ? this.temp.title : null
        }
239
        var uri = process.env.VUE_APP_API_TANK + 'shows/' + this.shows[this.currentShow].slug + '/files/' + file.id
240
        // TODO: add mechanism to indicate the running patch request in the files table
241
242
243
244
245
246
247
248
249
250
251
        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.')
        })
      }
252
253
254
    },
    deleteFile: function (id) {
      var uri = process.env.VUE_APP_API_TANK + 'shows/' + this.shows[this.currentShow].slug + '/files/' + id
255
      // TODO: add mechanism to indicate the running delete request in the files table
256
257
258
259
      axios.delete(uri, {
        withCredentials: true,
        headers: { 'Authorization': 'Bearer ' + this.$parent.user.access_token },
      }).then(
260
        this.fetchFiles(this.shows[this.currentShow].slug)
261
262
263
264
265
266
      ).catch(error => {
        console.log('Error:')
        console.log(error)
        alert('Error: could not delete file. See console log for details.')
      })
    },
267
    addFile: function () {
268
269
      var uri = process.env.VUE_APP_API_TANK + 'shows/' + this.shows[this.currentShow].slug + '/files'
      if (this.addNewFileURI) {
270
271
272
273
        // 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 }
274
        }).then(() => {
275
          this.fetchFiles(this.shows[this.currentShow].slug)
276
277
278
279
          if (this.uploadInterval === null) {
            this.uploadInterval = setInterval(() => { this.fetchImports(this.shows[this.currentShow].slug) }, 250)
          }
        }).catch(error => {
280
281
282
283
          console.log('Error:')
          console.log(error)
          alert('Error: could not add the new remote import. See console log for details.')
        })
284
      } else if (this.uploadSourceFile) {
285
        // TODO: add mechanism to indicate the running post request in the files table
286
287
288
289
290
        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)
291
          this.fetchFiles(this.shows[this.currentShow].slug)
292
293
294
          if (this.uploadInterval === null) {
            this.uploadInterval = setInterval(() => { this.fetchImports(this.shows[this.currentShow].slug) }, 250)
          }
295
296
297
298
299
300
        }).catch(error => {
          console.log('Error:')
          console.log(error)
          alert('Error: could not add the new file upload. See console log for details.')
        })
      } else {
301
        alert('Something is wrong. You have choosen to upload a file, but the corresponding file object does not exist.')
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
      }
    },
    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'
        }
346
347
348
349
350
351
352
      }).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)
353
354
355
356
357
      }).catch(error => {
        console.log('Error:')
        console.log(error)
        alert('Error: could not start the file upload. See console log for details.')
      })
358
    },
359
360
361
362
    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
363
      this.fetchShow(this.shows[this.currentShow].slug)
364
365
366
367
368
369
370
371
372
373
    },
    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'
        }
      }
    },
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
    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.')
      })
    },
408
409
410
411
412
    fetchShow: function (slug) {
      this.fetchFiles(slug)
      this.fetchPlaylists(slug)
    },
    fetchFiles: function (slug) {
413
414
415
416
417
418
419
420
421
422
423
424
425
      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)
      })
426
427
428
429
    },
    fetchPlaylists: function (slug) {
      this.loaded.playlists = false
      var uri = process.env.VUE_APP_API_TANK + 'shows/' + slug + '/playlists'
430
431
432
433
434
435
436
437
438
439
440
      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)
      })
441
442
443
444
    }
  },
  created () {
    // when we enter this module, we want to load all shows of the current user
445
    // before we search for corresponding shows in the tank
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
    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)
    })
  }
}
471
472
473
</script>

<style>
474
475
476
477
478
div.filelistbox {
  border: 1px solid #e9ecef;
  border-radius: 0.3rem;
  padding: 1rem 2rem;
}
479
480
481
482
483
484
485
486
487
488
489
.stateNew {
  color: red;
  font-weight: bold;
}
.stateRunning {
  color: darkgreen;
}
.stateUndefined {
  color: orange;
  font-weight: bold;
}
490
</style>