mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-08-03 09:44:41 +02:00
Adding download zip file, fix local cover art for m4b download
This commit is contained in:
parent
a7c538193c
commit
a56b3a8096
14 changed files with 625 additions and 113 deletions
|
@ -1,28 +1,54 @@
|
|||
<template>
|
||||
<div class="w-full h-full overflow-hidden overflow-y-auto px-4 py-6">
|
||||
<p class="text-center text-lg mb-4 py-8">Preparing downloads can take several minutes and will be stored in <span class="bg-primary bg-opacity-75 font-mono p-1 text-base">/metadata/downloads</span>. After the download is ready, it will remain available for 60 minutes, then be deleted.<br />Download will timeout after 15 minutes.</p>
|
||||
<div class="w-full border border-black-200 p-4 my-4">
|
||||
<!-- <p class="text-center text-lg mb-4 pb-8 border-b border-black-200">
|
||||
<span class="text-error">Experimental Feature!</span> If your audiobook is made up of multiple audio files, this will concatenate them into a single file. The file type will be the same as the first track. Preparing downloads can take anywhere from a few seconds to several minutes and will be stored in
|
||||
<span class="bg-primary bg-opacity-75 font-mono p-1 text-base">/metadata/downloads</span>. After the download is ready, it will remain available for 10 minutes then get deleted.
|
||||
</p> -->
|
||||
<p class="text-center text-lg mb-4 pb-8 border-b border-black-200">
|
||||
<span class="text-error">Experimental Feature!</span> If your audiobook has multiple tracks, this will merge them into a single M4B audiobook file.<br />Preparing downloads can take several minutes and will be stored in <span class="bg-primary bg-opacity-75 font-mono p-1 text-base">/metadata/downloads</span>. After the download is ready, it will remain available for 60 minutes, then be
|
||||
deleted.
|
||||
</p>
|
||||
|
||||
<div class="flex items-center">
|
||||
<p class="text-lg">{{ isSingleTrack ? 'Single Track' : 'M4B Audiobook File' }}</p>
|
||||
<div>
|
||||
<!-- <p class="text-lg">{{ isSingleTrack ? 'Single Track' : 'M4B Audiobook File' }}</p> -->
|
||||
<p class="text-lg">M4B Audiobook File <span class="text-error">*</span></p>
|
||||
<p class="max-w-xs text-sm pt-2 text-gray-300">Generate a .M4B audiobook file with embedded cover image and chapters.</p>
|
||||
</div>
|
||||
<div class="flex-grow" />
|
||||
<div>
|
||||
<p v-if="singleAudioDownloadFailed" class="text-error mb-2">Download Failed</p>
|
||||
<p v-if="singleAudioDownloadReady" class="text-success mb-2">Download Ready!</p>
|
||||
<p v-if="singleAudioDownloadExpired" class="text-error mb-2">Download Expired</p>
|
||||
<a v-if="isSingleTrack" :href="`/local/${singleTrackPath}`" class="btn outline-none rounded-md shadow-md relative border border-gray-600 px-4 py-2 bg-primary">Download Track</a>
|
||||
<ui-btn v-else-if="!singleAudioDownloadReady" :loading="singleAudioDownloadPending" :disabled="tempDisable" @click="startSingleAudioDownload">Start Download</ui-btn>
|
||||
<ui-btn v-else @click="downloadWithProgress">Download</ui-btn>
|
||||
<p v-if="singleDownloadStatus === $constants.DownloadStatus.FAILED" class="text-error mb-2">Download Failed</p>
|
||||
<p v-if="singleDownloadStatus === $constants.DownloadStatus.READY" class="text-success mb-2">Download Ready!</p>
|
||||
<p v-if="singleDownloadStatus === $constants.DownloadStatus.EXPIRED" class="text-error mb-2">Download Expired</p>
|
||||
|
||||
<!-- <a v-if="isSingleTrack" :href="`/local/${singleTrackPath}`" class="btn outline-none rounded-md shadow-md relative border border-gray-600 px-4 py-2 bg-primary">Download Track</a> -->
|
||||
<ui-btn v-if="singleDownloadStatus !== $constants.DownloadStatus.READY" :loading="singleDownloadStatus === $constants.DownloadStatus.PENDING" :disabled="tempDisable" @click="startSingleAudioDownload">Start Download</ui-btn>
|
||||
<div v-else>
|
||||
<ui-btn @click="downloadWithProgress(singleAudioDownload)">Download</ui-btn>
|
||||
<p class="px-0.5 py-1 text-sm font-mono text-center">Size: {{ $bytesPretty(singleAudioDownload.size) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full border border-black-200 p-4 my-4">
|
||||
<div class="flex items-center">
|
||||
<div>
|
||||
<p v-if="totalFiles > 1" class="text-lg">Zip {{ totalFiles }} Files</p>
|
||||
<p v-else>Zip 1 File</p>
|
||||
<p class="max-w-xs text-sm pt-2 text-gray-300">Generate a .ZIP file from the contents of the audiobook directory.</p>
|
||||
</div>
|
||||
|
||||
<div class="flex-grow" />
|
||||
<div>
|
||||
<p v-if="zipDownloadStatus === $constants.DownloadStatus.FAILED" class="text-error mb-2">Download Failed</p>
|
||||
<p v-if="zipDownloadStatus === $constants.DownloadStatus.READY" class="text-success mb-2">Download Ready!</p>
|
||||
<p v-if="zipDownloadStatus === $constants.DownloadStatus.EXPIRED" class="text-error mb-2">Download Expired</p>
|
||||
|
||||
<ui-btn v-if="zipDownloadStatus !== $constants.DownloadStatus.READY" :loading="zipDownloadStatus === $constants.DownloadStatus.PENDING" :disabled="tempDisable" @click="startZipDownload">Start Download</ui-btn>
|
||||
<div v-else>
|
||||
<ui-btn @click="downloadWithProgress(zipDownload)">Download</ui-btn>
|
||||
<p class="px-0.5 py-1 text-sm font-mono text-center">Size: {{ $bytesPretty(zipDownload.size) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full flex items-center justify-center absolute bottom-4 left-0 right-0 text-center">
|
||||
<p class="text-error text-lg">* <strong>Experimental:</strong> Merging multiple .m4b files may have issues. <a href="https://github.com/advplyr/audiobookshelf/issues" class="underline text-blue-600" target="_blank">Report issues here.</a></p>
|
||||
</div>
|
||||
|
||||
<div v-if="isDownloading" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-50 z-50 flex items-center justify-center">
|
||||
<div class="w-80 border border-black-400 bg-bg rounded-xl h-20">
|
||||
|
@ -55,7 +81,7 @@ export default {
|
|||
}
|
||||
},
|
||||
watch: {
|
||||
singleAudioDownloadPending(newVal) {
|
||||
singleDownloadStatus(newVal) {
|
||||
if (newVal) {
|
||||
this.tempDisable = false
|
||||
}
|
||||
|
@ -71,20 +97,14 @@ export default {
|
|||
singleAudioDownload() {
|
||||
return this.downloads.find((d) => d.type === 'singleAudio')
|
||||
},
|
||||
singleAudioDownloadPending() {
|
||||
return this.singleAudioDownload && this.singleAudioDownload.isPending
|
||||
singleDownloadStatus() {
|
||||
return this.singleAudioDownload ? this.singleAudioDownload.status : false
|
||||
},
|
||||
singleAudioDownloadFailed() {
|
||||
return this.singleAudioDownload && this.singleAudioDownload.isFailed
|
||||
zipDownload() {
|
||||
return this.downloads.find((d) => d.type === 'zip')
|
||||
},
|
||||
singleAudioDownloadReady() {
|
||||
return this.singleAudioDownload && this.singleAudioDownload.isReady
|
||||
},
|
||||
singleAudioDownloadExpired() {
|
||||
return this.singleAudioDownload && this.singleAudioDownload.isExpired
|
||||
},
|
||||
zipBundleDownload() {
|
||||
return this.downloads.find((d) => d.type === 'zipBundle')
|
||||
zipDownloadStatus() {
|
||||
return this.zipDownload ? this.zipDownload.status : false
|
||||
},
|
||||
isSingleTrack() {
|
||||
if (!this.audiobook.tracks) return false
|
||||
|
@ -93,11 +113,34 @@ export default {
|
|||
singleTrackPath() {
|
||||
if (!this.isSingleTrack) return null
|
||||
return this.audiobook.tracks[0].path
|
||||
},
|
||||
audioFiles() {
|
||||
return this.audiobook ? this.audiobook.audioFiles || [] : []
|
||||
},
|
||||
otherFiles() {
|
||||
return this.audiobook ? this.audiobook.otherFiles || [] : []
|
||||
},
|
||||
totalFiles() {
|
||||
return this.audioFiles.length + this.otherFiles.length
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
startZipDownload() {
|
||||
// console.log('Download request received', this.audiobook)
|
||||
|
||||
this.tempDisable = true
|
||||
setTimeout(() => {
|
||||
this.tempDisable = false
|
||||
}, 1000)
|
||||
|
||||
var downloadPayload = {
|
||||
audiobookId: this.audiobook.id,
|
||||
type: 'zip'
|
||||
}
|
||||
this.$root.socket.emit('download', downloadPayload)
|
||||
},
|
||||
startSingleAudioDownload() {
|
||||
console.log('Download request received', this.audiobook)
|
||||
// console.log('Download request received', this.audiobook)
|
||||
|
||||
this.tempDisable = true
|
||||
setTimeout(() => {
|
||||
|
@ -112,10 +155,10 @@ export default {
|
|||
}
|
||||
this.$root.socket.emit('download', downloadPayload)
|
||||
},
|
||||
downloadWithProgress() {
|
||||
var downloadId = this.singleAudioDownload.id
|
||||
downloadWithProgress(download) {
|
||||
var downloadId = download.id
|
||||
var downloadUrl = `${process.env.serverUrl}/api/download/${downloadId}`
|
||||
var filename = this.singleAudioDownload.filename
|
||||
var filename = download.filename
|
||||
|
||||
this.isDownloading = true
|
||||
|
||||
|
|
|
@ -127,39 +127,62 @@ export default {
|
|||
this.$store.commit('user/setSettings', user.settings)
|
||||
}
|
||||
},
|
||||
downloadToastClick(download) {
|
||||
console.log('Downlaod ready toast click', download)
|
||||
// if (!download || !download.audiobookId) {
|
||||
// return console.error('Invalid download object', download)
|
||||
// }
|
||||
// var audiobook = this.$store.getters['audiobooks/getAudiobook'](download.audiobookId)
|
||||
// if (!audiobook) {
|
||||
// return console.error('Audiobook not found for download', download)
|
||||
// }
|
||||
// this.$store.commit('showEditModalOnTab', { audiobook, tab: 'download' })
|
||||
},
|
||||
downloadStarted(download) {
|
||||
var filename = download.filename
|
||||
this.$toast.success(`Preparing download for "${filename}"`)
|
||||
|
||||
download.isPending = true
|
||||
download.status = this.$constants.DownloadStatus.PENDING
|
||||
download.toastId = this.$toast(`Preparing download "${download.filename}"`, { timeout: false, draggable: false, closeOnClick: false, onClick: this.downloadToastClick })
|
||||
this.$store.commit('downloads/addUpdateDownload', download)
|
||||
},
|
||||
downloadReady(download) {
|
||||
var filename = download.filename
|
||||
this.$toast.success(`Download "${filename}" is ready!`)
|
||||
download.status = this.$constants.DownloadStatus.READY
|
||||
var existingDownload = this.$store.getters['downloads/getDownload'](download.id)
|
||||
|
||||
download.isPending = false
|
||||
if (existingDownload && existingDownload.toastId !== undefined) {
|
||||
download.toastId = existingDownload.toastId
|
||||
this.$toast.update(existingDownload.toastId, { content: `Download "${download.filename}" is ready!`, options: { timeout: 5000, type: 'success', onClick: this.downloadToastClick } }, true)
|
||||
} else {
|
||||
this.$toast.success(`Download "${download.filename}" is ready!`)
|
||||
}
|
||||
this.$store.commit('downloads/addUpdateDownload', download)
|
||||
},
|
||||
downloadFailed(download) {
|
||||
var filename = download.filename
|
||||
this.$toast.error(`Download "${filename}" is failed`)
|
||||
download.status = this.$constants.DownloadStatus.FAILED
|
||||
var existingDownload = this.$store.getters['downloads/getDownload'](download.id)
|
||||
|
||||
download.isFailed = true
|
||||
download.isReady = false
|
||||
download.isPending = false
|
||||
var failedMsg = download.isTimedOut ? 'timed out' : 'failed'
|
||||
|
||||
if (existingDownload && existingDownload.toastId !== undefined) {
|
||||
download.toastId = existingDownload.toastId
|
||||
this.$toast.update(existingDownload.toastId, { content: `Download "${download.filename}" ${failedMsg}`, options: { timeout: 5000, type: 'error', onClick: this.downloadToastClick } }, true)
|
||||
} else {
|
||||
console.warn('Download failed no existing download', existingDownload)
|
||||
this.$toast.error(`Download "${download.filename}" ${failedMsg}`)
|
||||
}
|
||||
this.$store.commit('downloads/addUpdateDownload', download)
|
||||
},
|
||||
downloadKilled(download) {
|
||||
var filename = download.filename
|
||||
this.$toast.error(`Download "${filename}" was terminated`)
|
||||
|
||||
var existingDownload = this.$store.getters['downloads/getDownload'](download.id)
|
||||
if (existingDownload && existingDownload.toastId !== undefined) {
|
||||
download.toastId = existingDownload.toastId
|
||||
this.$toast.update(existingDownload.toastId, { content: `Download "${download.filename}" was terminated`, options: { timeout: 5000, type: 'error', onClick: this.downloadToastClick } }, true)
|
||||
} else {
|
||||
console.warn('Download killed no existing download found', existingDownload)
|
||||
this.$toast.error(`Download "${download.filename}" was terminated`)
|
||||
}
|
||||
this.$store.commit('downloads/removeDownload', download)
|
||||
},
|
||||
downloadExpired(download) {
|
||||
download.isExpired = true
|
||||
download.isReady = false
|
||||
download.isPending = false
|
||||
download.status = this.$constants.DownloadStatus.EXPIRED
|
||||
this.$store.commit('downloads/addUpdateDownload', download)
|
||||
},
|
||||
initializeSocket() {
|
||||
|
|
|
@ -47,6 +47,7 @@ module.exports = {
|
|||
|
||||
// Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins
|
||||
plugins: [
|
||||
'@/plugins/constants.js',
|
||||
'@/plugins/init.client.js',
|
||||
'@/plugins/axios.js',
|
||||
'@/plugins/toast.js'
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "1.1.7",
|
||||
"version": "1.1.8",
|
||||
"description": "Audiobook manager and player",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
|
14
client/plugins/constants.js
Normal file
14
client/plugins/constants.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
const DownloadStatus = {
|
||||
PENDING: 0,
|
||||
READY: 1,
|
||||
EXPIRED: 2,
|
||||
FAILED: 3
|
||||
}
|
||||
|
||||
const Constants = {
|
||||
DownloadStatus
|
||||
}
|
||||
|
||||
export default ({ app }, inject) => {
|
||||
inject('constants', Constants)
|
||||
}
|
|
@ -13,6 +13,9 @@ export const state = () => ({
|
|||
})
|
||||
|
||||
export const getters = {
|
||||
getAudiobook: (state) => id => {
|
||||
return state.audiobooks.find(ab => ab.id === id)
|
||||
},
|
||||
getFiltered: (state, getters, rootState) => () => {
|
||||
var filtered = state.audiobooks
|
||||
var settings = rootState.user.settings || {}
|
||||
|
|
|
@ -6,6 +6,9 @@ export const state = () => ({
|
|||
export const getters = {
|
||||
getDownloads: (state) => (audiobookId) => {
|
||||
return state.downloads.filter(d => d.audiobookId === audiobookId)
|
||||
},
|
||||
getDownload: (state) => (id) => {
|
||||
return state.downloads.find(d => d.id === id)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue