mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-07-30 15:55:26 +02:00
Add audiobook uploader
This commit is contained in:
parent
6cb253598b
commit
3dfd7ea035
8 changed files with 336 additions and 6 deletions
258
client/pages/upload/index.vue
Normal file
258
client/pages/upload/index.vue
Normal file
|
@ -0,0 +1,258 @@
|
|||
<template>
|
||||
<div id="page-wrapper" class="page p-6" :class="streamAudiobook ? 'streaming' : ''">
|
||||
<main class="container mx-auto h-full max-w-screen-lg p-6">
|
||||
<article class="max-h-full overflow-y-auto relative flex flex-col bg-primary shadow-xl rounded-md" @drop="drop" @dragover="dragover" @dragleave="dragleave" @dragenter="dragenter">
|
||||
<h1 class="text-xl font-book px-4 pt-4 pb-2"><span class="text-error pr-4">(Experimental)</span>Audiobook Uploader</h1>
|
||||
|
||||
<div class="flex my-2 px-6">
|
||||
<div class="w-1/2 px-2">
|
||||
<ui-text-input-with-label v-model="title" label="Title" />
|
||||
</div>
|
||||
<div class="w-1/2 px-2">
|
||||
<ui-text-input-with-label v-model="author" label="Author" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex my-2 px-6">
|
||||
<div class="w-1/2 px-2">
|
||||
<ui-text-input-with-label v-model="series" label="Series" note="(optional)" />
|
||||
</div>
|
||||
<div class="w-1/2 px-2">
|
||||
<div class="w-full">
|
||||
<p class="px-1 text-sm font-semibold">Directory <em class="font-normal text-xs pl-2">(auto)</em></p>
|
||||
<ui-text-input :value="directory" disabled class="w-full font-mono text-xs" style="height: 42px" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section v-if="showUploader" class="h-full overflow-auto p-8 w-full flex flex-col">
|
||||
<header class="border-dashed border-2 border-gray-400 py-12 flex flex-col justify-center items-center relative h-40" :class="isDragOver ? 'bg-white bg-opacity-10' : ''">
|
||||
<p v-show="isDragOver" class="mb-3 font-semibold text-gray-200 flex flex-wrap justify-center">Drop em'</p>
|
||||
<p v-show="!isDragOver" class="mb-3 font-semibold text-gray-200 flex flex-wrap justify-center">Drop your audio and image files or</p>
|
||||
|
||||
<input ref="fileInput" id="hidden-input" type="file" multiple :accept="inputAccept" class="hidden" @change="inputChanged" />
|
||||
<ui-btn @click="clickSelectAudioFiles">Select files</ui-btn>
|
||||
<p class="text-xs text-gray-300 absolute bottom-3 right-3">{{ inputAccept.join(', ') }}</p>
|
||||
</header>
|
||||
</section>
|
||||
<section v-else class="h-full overflow-auto px-8 pb-8 w-full flex flex-col">
|
||||
<p v-if="!hasValidAudioFiles" class="text-error text-lg pt-4">* No valid audio tracks</p>
|
||||
|
||||
<div v-if="validImageFiles.length">
|
||||
<h1 class="pt-8 pb-3 font-semibold sm:text-lg text-gray-200">Cover Image(s)</h1>
|
||||
<div class="flex">
|
||||
<template v-for="file in validImageFiles">
|
||||
<div :key="file.name" class="h-28 w-20 bg-bg">
|
||||
<img :src="file.src" class="h-full w-full object-contain" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="validAudioFiles.length">
|
||||
<h1 class="pt-8 pb-3 font-semibold sm:text-lg text-gray-200">Audio Tracks</h1>
|
||||
|
||||
<table class="text-sm tracksTable">
|
||||
<tr class="font-book">
|
||||
<th class="text-left">Filename</th>
|
||||
<th class="text-left">Type</th>
|
||||
<th class="text-left">Size</th>
|
||||
</tr>
|
||||
<template v-for="file in validAudioFiles">
|
||||
<tr :key="file.name">
|
||||
<td class="font-book">
|
||||
<p class="truncate">{{ file.name }}</p>
|
||||
</td>
|
||||
<td class="font-sm">
|
||||
{{ file.type }}
|
||||
</td>
|
||||
<td class="font-mono">
|
||||
{{ $bytesPretty(file.size) }}
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div v-if="invalidFiles.length">
|
||||
<h1 class="pt-8 pb-3 font-semibold sm:text-lg text-gray-200">Invalid Files</h1>
|
||||
<table class="text-sm tracksTable">
|
||||
<tr class="font-book">
|
||||
<th class="text-left">Filename</th>
|
||||
<th class="text-left">Type</th>
|
||||
<th class="text-left">Size</th>
|
||||
</tr>
|
||||
<template v-for="file in invalidFiles">
|
||||
<tr :key="file.name">
|
||||
<td class="font-book">
|
||||
<p class="truncate">{{ file.name }}</p>
|
||||
</td>
|
||||
<td class="font-sm">
|
||||
{{ file.type }}
|
||||
</td>
|
||||
<td class="font-mono">
|
||||
{{ $bytesPretty(file.size) }}
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
<footer v-show="!showUploader" class="flex justify-end px-8 pb-8 pt-4">
|
||||
<ui-btn :disabled="!hasValidAudioFiles" color="success" @click="submit">Upload Audiobook</ui-btn>
|
||||
<button id="cancel" class="ml-3 rounded-sm px-3 py-1 hover:bg-white hover:bg-opacity-10 focus:shadow-outline focus:outline-none" @click="cancel">Cancel</button>
|
||||
</footer>
|
||||
|
||||
<div v-if="processing" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-50 flex items-center justify-center z-20">
|
||||
<ui-loading-indicator text="Uploading..." />
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Path from 'path'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
processing: false,
|
||||
title: null,
|
||||
author: null,
|
||||
series: null,
|
||||
acceptedAudioFormats: ['.mp3', '.m4b', '.m4a'],
|
||||
acceptedImageFormats: ['image/*'],
|
||||
inputAccept: ['image/*, .mp3, .m4b, .m4a'],
|
||||
isDragOver: false,
|
||||
showUploader: true,
|
||||
validAudioFiles: [],
|
||||
validImageFiles: [],
|
||||
invalidFiles: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
streamAudiobook() {
|
||||
return this.$store.state.streamAudiobook
|
||||
},
|
||||
hasValidAudioFiles() {
|
||||
return this.validAudioFiles.length
|
||||
},
|
||||
directory() {
|
||||
if (!this.author || !this.title) return ''
|
||||
if (this.series) {
|
||||
return Path.join('/audiobooks', this.author, this.series, this.title)
|
||||
} else {
|
||||
return Path.join('/audiobooks', this.author, this.title)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
reset() {
|
||||
this.title = ''
|
||||
this.author = ''
|
||||
this.series = ''
|
||||
this.cancel()
|
||||
},
|
||||
cancel() {
|
||||
this.validAudioFiles = []
|
||||
this.validImageFiles = []
|
||||
this.invalidFiles = []
|
||||
if (this.$refs.fileInput) {
|
||||
this.$refs.fileInput.value = ''
|
||||
}
|
||||
this.showUploader = true
|
||||
},
|
||||
inputChanged(e) {
|
||||
if (!e.target || !e.target.files) return
|
||||
var _files = Array.from(e.target.files)
|
||||
if (_files && _files.length) {
|
||||
this.filesChanged(_files)
|
||||
}
|
||||
},
|
||||
drop(evt) {
|
||||
console.log('Dropped event', evt)
|
||||
this.isDragOver = false
|
||||
this.preventDefaults(evt)
|
||||
const files = [...evt.dataTransfer.files]
|
||||
this.filesChanged(files)
|
||||
},
|
||||
dragover(evt) {
|
||||
console.log('Dragged over', evt)
|
||||
this.isDragOver = true
|
||||
this.preventDefaults(evt)
|
||||
},
|
||||
dragleave(evt) {
|
||||
console.log('Dragged leave', evt)
|
||||
this.isDragOver = false
|
||||
this.preventDefaults(evt)
|
||||
},
|
||||
dragenter(evt) {
|
||||
this.isDragOver = true
|
||||
this.preventDefaults(evt)
|
||||
},
|
||||
preventDefaults(e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
},
|
||||
filesChanged(files) {
|
||||
console.log('FilesChanged', files)
|
||||
this.showUploader = false
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
var file = files[i]
|
||||
var ext = Path.extname(file.name)
|
||||
|
||||
if (this.acceptedAudioFormats.includes(ext)) {
|
||||
this.validAudioFiles.push(file)
|
||||
} else if (file.type.startsWith('image/')) {
|
||||
file.src = URL.createObjectURL(file)
|
||||
this.validImageFiles.push(file)
|
||||
} else {
|
||||
this.invalidFiles.push(file)
|
||||
}
|
||||
}
|
||||
},
|
||||
clickSelectAudioFiles() {
|
||||
if (this.$refs.fileInput) {
|
||||
this.$refs.fileInput.click()
|
||||
}
|
||||
},
|
||||
submit() {
|
||||
if (!this.title || !this.author) {
|
||||
this.$toast.error('Must enter a title and author')
|
||||
return
|
||||
}
|
||||
this.processing = true
|
||||
|
||||
var form = new FormData()
|
||||
form.set('title', this.title)
|
||||
form.set('author', this.author)
|
||||
form.set('series', this.series)
|
||||
|
||||
var index = 0
|
||||
var files = this.validAudioFiles.concat(this.validImageFiles)
|
||||
files.forEach((file) => {
|
||||
form.set(`${index++}`, file)
|
||||
})
|
||||
|
||||
this.$axios
|
||||
.$post('/upload', form)
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
this.$toast.error(data.error)
|
||||
} else {
|
||||
this.$toast.success('Audiobook Uploaded Successfully')
|
||||
this.reset()
|
||||
}
|
||||
this.processing = false
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed', error)
|
||||
this.$toast.error('Oops, something went wrong...')
|
||||
this.processing = false
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
Loading…
Add table
Add a link
Reference in a new issue