Abort backup if it is getting too large #89, support for ebook only book folders #75

This commit is contained in:
advplyr 2021-10-10 16:36:21 -05:00
parent 0c168b3da4
commit 04f92c33c2
18 changed files with 258 additions and 149 deletions

View File

@ -6,6 +6,7 @@ npm-debug.log
/config /config
/audiobooks /audiobooks
/audiobooks2 /audiobooks2
/media/
/metadata /metadata
dev.js dev.js
test/ test/

1
.gitignore vendored
View File

@ -4,6 +4,7 @@ node_modules/
/config/ /config/
/audiobooks/ /audiobooks/
/audiobooks2/ /audiobooks2/
/media/
/metadata/ /metadata/
test/ test/
/client/.nuxt/ /client/.nuxt/

View File

@ -1,5 +1,5 @@
<template> <template>
<div v-if="value" class="w-screen h-screen fixed top-0 left-0 z-50 bg-white text-black"> <div v-if="show" class="w-screen h-screen fixed top-0 left-0 z-50 bg-white text-black">
<div class="absolute top-4 right-4 z-10"> <div class="absolute top-4 right-4 z-10">
<span class="material-icons cursor-pointer text-4xl" @click="show = false">close</span> <span class="material-icons cursor-pointer text-4xl" @click="show = false">close</span>
</div> </div>
@ -35,10 +35,6 @@
import ePub from 'epubjs' import ePub from 'epubjs'
export default { export default {
props: {
value: Boolean,
url: String
},
data() { data() {
return { return {
book: null, book: null,
@ -63,12 +59,34 @@ export default {
computed: { computed: {
show: { show: {
get() { get() {
return this.value return this.$store.state.showEReader
}, },
set(val) { set(val) {
this.$emit('input', val) this.$store.commit('setShowEReader', val)
} }
}, },
selectedAudiobook() {
return this.$store.state.selectedAudiobook
},
libraryId() {
return this.selectedAudiobook.libraryId
},
folderId() {
return this.selectedAudiobook.folderId
},
ebooks() {
return this.selectedAudiobook.ebooks || []
},
epubEbook() {
return this.ebooks.find((eb) => eb.ext === '.epub')
},
epubPath() {
return this.epubEbook ? this.epubEbook.path : null
},
url() {
if (!this.epubPath) return null
return `/ebook/${this.libraryId}/${this.folderId}/${this.epubPath}`
},
userToken() { userToken() {
return this.$store.getters['user/getToken'] return this.$store.getters['user/getToken']
} }
@ -116,7 +134,7 @@ export default {
init() { init() {
this.registerListeners() this.registerListeners()
console.log('epub', this.url) console.log('epub', this.url, this.epubEbook, this.ebooks)
// var book = ePub(this.url, { // var book = ePub(this.url, {
// requestHeaders: { // requestHeaders: {
// Authorization: `Bearer ${this.userToken}` // Authorization: `Bearer ${this.userToken}`

View File

@ -14,11 +14,16 @@
<cards-book-cover :audiobook="audiobook" :author-override="authorFormat" :width="width" /> <cards-book-cover :audiobook="audiobook" :author-override="authorFormat" :width="width" />
<div v-show="isHovering || isSelectionMode" class="absolute top-0 left-0 w-full h-full bg-black rounded" :class="overlayWrapperClasslist"> <div v-show="isHovering || isSelectionMode" class="absolute top-0 left-0 w-full h-full bg-black rounded" :class="overlayWrapperClasslist">
<div v-show="!isSelectionMode && !isMissing" class="h-full flex items-center justify-center"> <div v-show="showPlayButton" class="h-full flex items-center justify-center">
<div class="hover:text-gray-200 hover:scale-110 transform duration-200" @click.stop.prevent="play"> <div class="hover:text-gray-200 hover:scale-110 transform duration-200" @click.stop.prevent="play">
<span class="material-icons" :style="{ fontSize: playIconFontSize + 'rem' }">play_circle_filled</span> <span class="material-icons" :style="{ fontSize: playIconFontSize + 'rem' }">play_circle_filled</span>
</div> </div>
</div> </div>
<div v-show="showReadButton" class="h-full flex items-center justify-center">
<div class="hover:text-gray-200 hover:scale-110 transform duration-200" @click.stop.prevent="clickReadEBook">
<span class="material-icons" :style="{ fontSize: playIconFontSize + 'rem' }">auto_stories</span>
</div>
</div>
<div v-if="userCanUpdate" v-show="!isSelectionMode" class="absolute cursor-pointer hover:text-yellow-300 hover:scale-125 transform duration-50" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="editClick"> <div v-if="userCanUpdate" v-show="!isSelectionMode" class="absolute cursor-pointer hover:text-yellow-300 hover:scale-125 transform duration-50" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="editClick">
<span class="material-icons" :style="{ fontSize: sizeMultiplier + 'rem' }">edit</span> <span class="material-icons" :style="{ fontSize: sizeMultiplier + 'rem' }">edit</span>
@ -34,9 +39,14 @@
</div> </div>
<!-- EBook Icon --> <!-- EBook Icon -->
<div v-if="showExperimentalFeatures && hasEbook" class="absolute rounded-full bg-blue-500 w-6 h-6 flex items-center justify-center bg-opacity-90" :style="{ bottom: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }"> <div
v-if="showSmallEBookIcon"
class="absolute rounded-full bg-blue-500 flex items-center justify-center bg-opacity-90 hover:scale-125 transform duration-200"
:style="{ bottom: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem`, width: 1.5 * sizeMultiplier + 'rem', height: 1.5 * sizeMultiplier + 'rem' }"
@click.stop.prevent="clickReadEBook"
>
<!-- <p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">EBook</p> --> <!-- <p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">EBook</p> -->
<span class="material-icons text-white text-base">auto_stories</span> <span class="material-icons text-white" :style="{ fontSize: sizeMultiplier * 1 + 'rem' }">auto_stories</span>
</div> </div>
<div v-show="!isSelectionMode" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full" :class="userIsRead ? 'bg-success' : 'bg-yellow-400'" :style="{ width: width * userProgressPercent + 'px' }"></div> <div v-show="!isSelectionMode" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full" :class="userIsRead ? 'bg-success' : 'bg-yellow-400'" :style="{ width: width * userProgressPercent + 'px' }"></div>
@ -90,8 +100,10 @@ export default {
hasEbook() { hasEbook() {
return this.audiobook.numEbooks return this.audiobook.numEbooks
}, },
hasTracks() {
return this.audiobook.numTracks
},
isSelectionMode() { isSelectionMode() {
// return this.$store.getters['getNumAudiobooksSelected']
return !!this.selectedAudiobooks.length return !!this.selectedAudiobooks.length
}, },
selectedAudiobooks() { selectedAudiobooks() {
@ -150,11 +162,23 @@ export default {
return this.userProgress ? !!this.userProgress.isRead : false return this.userProgress ? !!this.userProgress.isRead : false
}, },
showError() { showError() {
return this.hasMissingParts || this.hasInvalidParts || this.isMissing return this.hasMissingParts || this.hasInvalidParts || this.isMissing || this.isIncomplete
},
showReadButton() {
return !this.isSelectionMode && this.showExperimentalFeatures && !this.showPlayButton && this.hasEbook
},
showPlayButton() {
return !this.isSelectionMode && !this.isMissing && !this.isIncomplete && this.hasTracks
},
showSmallEBookIcon() {
return !this.isSelectionMode && this.showExperimentalFeatures && this.hasEbook
}, },
isMissing() { isMissing() {
return this.audiobook.isMissing return this.audiobook.isMissing
}, },
isIncomplete() {
return this.audiobook.isIncomplete
},
hasMissingParts() { hasMissingParts() {
return this.audiobook.hasMissingParts return this.audiobook.hasMissingParts
}, },
@ -163,6 +187,7 @@ export default {
}, },
errorText() { errorText() {
if (this.isMissing) return 'Audiobook directory is missing!' if (this.isMissing) return 'Audiobook directory is missing!'
else if (this.isIncomplete) return 'Audiobook has no audio tracks & ebook'
var txt = '' var txt = ''
if (this.hasMissingParts) { if (this.hasMissingParts) {
txt = `${this.hasMissingParts} missing parts.` txt = `${this.hasMissingParts} missing parts.`
@ -211,6 +236,9 @@ export default {
e.preventDefault() e.preventDefault()
this.selectBtnClick() this.selectBtnClick()
} }
},
clickReadEBook() {
this.$store.commit('showEReader', this.audiobook)
} }
} }
} }

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="w-full h-full overflow-hidden overflow-y-auto px-4 py-6"> <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> <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"> <div v-if="showM4bDownload" class="w-full border border-black-200 p-4 my-4">
<div class="flex items-center"> <div class="flex items-center">
<div> <div>
<p class="text-lg">M4B Audiobook File <span class="text-error">*</span></p> <p class="text-lg">M4B Audiobook File <span class="text-error">*</span></p>
@ -44,7 +44,7 @@
</div> </div>
</div> </div>
<div class="w-full flex items-center justify-center absolute bottom-4 left-0 right-0 text-center"> <div v-if="showM4bDownload" 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> <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>
@ -89,6 +89,9 @@ export default {
audiobookId() { audiobookId() {
return this.audiobook ? this.audiobook.id : null return this.audiobook ? this.audiobook.id : null
}, },
_audiobook() {
return this.audiobook || {}
},
downloads() { downloads() {
return this.$store.getters['downloads/getDownloads'](this.audiobookId) return this.$store.getters['downloads/getDownloads'](this.audiobookId)
}, },
@ -120,6 +123,9 @@ export default {
}, },
totalFiles() { totalFiles() {
return this.audioFiles.length + this.otherFiles.length return this.audioFiles.length + this.otherFiles.length
},
showM4bDownload() {
return !this._audiobook.isMissing && !this._audiobook.isIncomplete && this._audiobook.tracks.length
} }
}, },
methods: { methods: {

View File

@ -1,5 +1,6 @@
<template> <template>
<div class="w-full h-full overflow-y-auto overflow-x-hidden px-4 py-6"> <div class="w-full h-full overflow-y-auto overflow-x-hidden px-4 py-6">
<template v-if="hasTracks">
<div class="w-full bg-primary px-4 py-2 flex items-center"> <div class="w-full bg-primary px-4 py-2 flex items-center">
<div class="h-7 w-7 rounded-full bg-white bg-opacity-10 flex items-center justify-center"> <div class="h-7 w-7 rounded-full bg-white bg-opacity-10 flex items-center justify-center">
<span class="text-sm font-mono">{{ tracks.length }}</span> <span class="text-sm font-mono">{{ tracks.length }}</span>
@ -36,6 +37,8 @@
</tr> </tr>
</template> </template>
</table> </table>
</template>
<div v-else class="flex my-4 text-center justify-center text-xl">No Audio Tracks</div>
</div> </div>
</template> </template>
@ -94,6 +97,9 @@ export default {
}, },
showDownload() { showDownload() {
return this.userCanDownload && !this.isMissing return this.userCanDownload && !this.isMissing
},
hasTracks() {
return this.audiobook.tracks.length
} }
}, },
methods: { methods: {

View File

@ -11,7 +11,12 @@
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn v-show="mouseover && !libraryScan && canScan" small color="bg" @click.stop="scan">Scan</ui-btn> <ui-btn v-show="mouseover && !libraryScan && canScan" small color="bg" @click.stop="scan">Scan</ui-btn>
<span v-show="mouseover && !libraryScan && showEdit && canEdit" class="material-icons text-xl text-gray-300 hover:text-gray-50 ml-4" @click.stop="editClick">edit</span> <span v-show="mouseover && !libraryScan && showEdit && canEdit" class="material-icons text-xl text-gray-300 hover:text-gray-50 ml-4" @click.stop="editClick">edit</span>
<span v-show="!libraryScan && mouseover && showEdit && canDelete" class="material-icons text-xl text-gray-300 ml-3" :class="isMain ? 'text-opacity-5 cursor-not-allowed' : 'hover:text-gray-50'" @click.stop="deleteClick">delete</span> <span v-show="!libraryScan && mouseover && showEdit && canDelete && !isDeleting" class="material-icons text-xl text-gray-300 ml-3" :class="isMain ? 'text-opacity-5 cursor-not-allowed' : 'hover:text-gray-50'" @click.stop="deleteClick">delete</span>
<div v-show="isDeleting" class="text-xl text-gray-300 ml-3 animate-spin" :class="isMain ? 'text-opacity-5 cursor-not-allowed' : 'hover:text-gray-50'" @click.stop="deleteClick">
<svg viewBox="0 0 24 24" class="w-6 h-6">
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
</svg>
</div>
</div> </div>
</template> </template>
@ -27,7 +32,8 @@ export default {
}, },
data() { data() {
return { return {
mouseover: false mouseover: false,
isDeleting: false
} }
}, },
computed: { computed: {
@ -54,12 +60,29 @@ export default {
editClick() { editClick() {
this.$emit('edit', this.library) this.$emit('edit', this.library)
}, },
deleteClick() {
if (this.isMain) return
this.$emit('delete', this.library)
},
scan() { scan() {
this.$root.socket.emit('scan', this.library.id) this.$root.socket.emit('scan', this.library.id)
},
deleteClick() {
if (this.isMain) return
if (confirm(`Are you sure you want to permanently delete library "${this.library.name}"?`)) {
this.isDeleting = true
this.$axios
.$delete(`/api/library/${this.library.id}`)
.then((data) => {
this.isDeleting = false
if (data.error) {
this.$toast.error(data.error)
} else {
this.$toast.success('Library deleted')
}
})
.catch((error) => {
console.error('Failed to delete library', error)
this.$toast.error('Failed to delete library')
this.isDeleting = false
})
}
} }
}, },
mounted() {} mounted() {}

View File

@ -8,7 +8,7 @@
</div> </div>
<template v-for="library in libraries"> <template v-for="library in libraries">
<modals-libraries-library-item :key="library.id" :library="library" :selected="currentLibraryId === library.id" :show-edit="true" @edit="editLibrary" @delete="deleteLibrary" @click="clickLibrary" /> <modals-libraries-library-item :key="library.id" :library="library" :selected="currentLibraryId === library.id" :show-edit="true" @edit="editLibrary" @click="clickLibrary" />
</template> </template>
<modals-edit-library-modal v-model="showLibraryModal" :library="selectedLibrary" /> <modals-edit-library-modal v-model="showLibraryModal" :library="selectedLibrary" />
</div> </div>
@ -38,27 +38,6 @@ export default {
await this.$store.dispatch('libraries/fetch', library.id) await this.$store.dispatch('libraries/fetch', library.id)
this.$router.push(`/library/${library.id}`) this.$router.push(`/library/${library.id}`)
}, },
deleteLibrary(library) {
if (library.id === 'main') return
// if (confirm(`Are you sure you want to permanently delete user "${user.username}"?`)) {
// this.isDeletingUser = true
// this.$axios
// .$delete(`/api/user/${user.id}`)
// .then((data) => {
// this.isDeletingUser = false
// if (data.error) {
// this.$toast.error(data.error)
// } else {
// this.$toast.success('User deleted')
// }
// })
// .catch((error) => {
// console.error('Failed to delete user', error)
// this.$toast.error('Failed to delete user')
// this.isDeletingUser = false
// })
// }
},
clickAddLibrary() { clickAddLibrary() {
this.selectedLibrary = null this.selectedLibrary = null
this.showLibraryModal = true this.showLibraryModal = true

View File

@ -7,6 +7,7 @@
<app-stream-container ref="streamContainer" /> <app-stream-container ref="streamContainer" />
<modals-libraries-modal /> <modals-libraries-modal />
<modals-edit-modal /> <modals-edit-modal />
<app-reader />
<!-- <widgets-scan-alert /> --> <!-- <widgets-scan-alert /> -->
</div> </div>
</template> </template>

View File

@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "1.4.3", "version": "1.4.4",
"description": "Audiobook manager and player", "description": "Audiobook manager and player",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

View File

@ -57,7 +57,7 @@
</template> </template>
</div> </div>
</div> </div>
<div class="flex py-0.5"> <div v-if="tracks.length" class="flex py-0.5">
<div class="w-32"> <div class="w-32">
<span class="text-white text-opacity-60 uppercase text-sm">Duration</span> <span class="text-white text-opacity-60 uppercase text-sm">Duration</span>
</div> </div>
@ -65,7 +65,7 @@
{{ durationPretty }} {{ durationPretty }}
</div> </div>
</div> </div>
<div class="flex py-0.5"> <div v-if="tracks.length" class="flex py-0.5">
<div class="w-32"> <div class="w-32">
<span class="text-white text-opacity-60 uppercase text-sm">Size</span> <span class="text-white text-opacity-60 uppercase text-sm">Size</span>
</div> </div>
@ -73,16 +73,20 @@
{{ sizePretty }} {{ sizePretty }}
</div> </div>
</div> </div>
<!--
<p v-if="narrator" class="text-base">
<span class="text-white text-opacity-60">By:</span> <nuxt-link :to="`/library/${libraryId}/bookshelf?filter=authors.${$encode(author)}`" class="hover:underline">{{ author }}</nuxt-link>
</p> -->
<!-- <p v-if="narrator" class="text-base"><span class="text-white text-opacity-60">Narrated by:</span> {{ narrator }}</p>
<p v-if="publishYear" class="text-base"><span class="text-white text-opacity-60">Publish year:</span> {{ publishYear }}</p>
<p v-if="genres.length" class="text-base"><span class="text-white text-opacity-60">Genres:</span> {{ genres.join(', ') }}</p> -->
</div> </div>
<div class="flex-grow" /> <div class="flex-grow" />
</div> </div>
<!-- Alerts -->
<div v-show="showExperimentalReadAlert" class="bg-error p-4 rounded-xl flex items-center">
<span class="material-icons text-2xl">warning_amber</span>
<p class="ml-4">Book has no audio tracks but has valid ebook files. The e-reader is experimental and can be turned on in config.</p>
</div>
<div v-show="showEpubAlert" class="bg-error p-4 rounded-xl flex items-center mt-2">
<span class="material-icons text-2xl">warning_amber</span>
<p class="ml-4">Book has valid ebook files, but the experimental e-reader currently only supports epub files.</p>
</div>
<div v-if="progressPercent > 0 && progressPercent < 1" class="px-4 py-2 mt-4 bg-primary text-sm font-semibold rounded-md text-gray-200 relative max-w-max" :class="resettingProgress ? 'opacity-25' : ''"> <div v-if="progressPercent > 0 && progressPercent < 1" class="px-4 py-2 mt-4 bg-primary text-sm font-semibold rounded-md text-gray-200 relative max-w-max" :class="resettingProgress ? 'opacity-25' : ''">
<p class="leading-6">Your Progress: {{ Math.round(progressPercent * 100) }}%</p> <p class="leading-6">Your Progress: {{ Math.round(progressPercent * 100) }}%</p>
<p class="text-gray-400 text-xs">{{ $elapsedPretty(userTimeRemaining) }} remaining</p> <p class="text-gray-400 text-xs">{{ $elapsedPretty(userTimeRemaining) }} remaining</p>
@ -92,13 +96,13 @@
</div> </div>
<div class="flex items-center pt-4"> <div class="flex items-center pt-4">
<ui-btn v-if="!isMissing" :disabled="streaming" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="startStream"> <ui-btn v-if="showPlayButton" :disabled="streaming" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="startStream">
<span v-show="!streaming" class="material-icons -ml-2 pr-1 text-white">play_arrow</span> <span v-show="!streaming" class="material-icons -ml-2 pr-1 text-white">play_arrow</span>
{{ streaming ? 'Streaming' : 'Play' }} {{ streaming ? 'Streaming' : 'Play' }}
</ui-btn> </ui-btn>
<ui-btn v-else color="error" :padding-x="4" small class="flex items-center h-9 mr-2"> <ui-btn v-else-if="isMissing || isIncomplete" color="error" :padding-x="4" small class="flex items-center h-9 mr-2">
<span v-show="!streaming" class="material-icons -ml-2 pr-1 text-white">error</span> <span v-show="!streaming" class="material-icons -ml-2 pr-1 text-white">error</span>
Missing {{ isMissing ? 'Missing' : 'Incomplete' }}
</ui-btn> </ui-btn>
<ui-btn v-if="showExperimentalFeatures && epubEbook" color="info" :padding-x="4" small class="flex items-center h-9 mr-2" @click="openEbook"> <ui-btn v-if="showExperimentalFeatures && epubEbook" color="info" :padding-x="4" small class="flex items-center h-9 mr-2" @click="openEbook">
@ -141,7 +145,7 @@
</div> </div>
</div> </div>
<tables-tracks-table :tracks="tracks" :audiobook="audiobook" class="mt-6" /> <tables-tracks-table v-if="tracks.length" :tracks="tracks" :audiobook="audiobook" class="mt-6" />
<tables-audio-files-table v-if="otherAudioFiles.length" :audiobook-id="audiobook.id" :files="otherAudioFiles" class="mt-6" /> <tables-audio-files-table v-if="otherAudioFiles.length" :audiobook-id="audiobook.id" :files="otherAudioFiles" class="mt-6" />
@ -150,7 +154,7 @@
</div> </div>
</div> </div>
<app-reader v-if="showExperimentalFeatures" v-model="showReader" :url="epubUrl" /> <!-- <app-reader v-if="showExperimentalFeatures" v-model="showReader" :url="epubUrl" /> -->
</div> </div>
</template> </template>
@ -175,7 +179,6 @@ export default {
}, },
data() { data() {
return { return {
showReader: false,
isRead: false, isRead: false,
resettingProgress: false, resettingProgress: false,
isProcessingReadUpdate: false isProcessingReadUpdate: false
@ -230,6 +233,12 @@ export default {
isMissing() { isMissing() {
return this.audiobook.isMissing return this.audiobook.isMissing
}, },
isIncomplete() {
return this.audiobook.isIncomplete
},
showPlayButton() {
return !this.isMissing && !this.isIncomplete && this.tracks.length
},
missingParts() { missingParts() {
return this.audiobook.missingParts || [] return this.audiobook.missingParts || []
}, },
@ -313,16 +322,15 @@ export default {
ebooks() { ebooks() {
return this.audiobook.ebooks return this.audiobook.ebooks
}, },
showEpubAlert() {
return this.ebooks.length && !this.epubEbook && !this.tracks.length
},
showExperimentalReadAlert() {
return !this.tracks.length && this.ebooks.length && !this.showExperimentalFeatures
},
epubEbook() { epubEbook() {
return this.audiobook.ebooks.find((eb) => eb.ext === '.epub') return this.audiobook.ebooks.find((eb) => eb.ext === '.epub')
}, },
epubPath() {
return this.epubEbook ? this.epubEbook.path : null
},
epubUrl() {
if (!this.epubPath) return null
return `/ebook/${this.libraryId}/${this.folderId}/${this.epubPath}`
},
userToken() { userToken() {
return this.$store.getters['user/getToken'] return this.$store.getters['user/getToken']
}, },
@ -365,7 +373,7 @@ export default {
}, },
methods: { methods: {
openEbook() { openEbook() {
this.showReader = true this.$store.commit('showEReader', this.audiobook)
}, },
toggleRead() { toggleRead() {
var updatePayload = { var updatePayload = {

View File

@ -7,6 +7,7 @@ export const state = () => ({
streamAudiobook: null, streamAudiobook: null,
editModalTab: 'details', editModalTab: 'details',
showEditModal: false, showEditModal: false,
showEReader: false,
selectedAudiobook: null, selectedAudiobook: null,
playOnLoad: false, playOnLoad: false,
developerMode: false, developerMode: false,
@ -111,6 +112,14 @@ export const mutations = {
setShowEditModal(state, val) { setShowEditModal(state, val) {
state.showEditModal = val state.showEditModal = val
}, },
showEReader(state, audiobook) {
console.log('Show EReader', audiobook)
state.selectedAudiobook = audiobook
state.showEReader = true
},
setShowEReader(state, val) {
state.showEReader = val
},
setDeveloperMode(state, val) { setDeveloperMode(state, val) {
state.developerMode = val state.developerMode = val
}, },

11
package-lock.json generated
View File

@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf", "name": "audiobookshelf",
"version": "1.4.1", "version": "1.4.3",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -411,15 +411,6 @@
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
"integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg=="
}, },
"cookie-parser": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.5.tgz",
"integrity": "sha512-f13bPUj/gG/5mDr+xLmSxxDsB9DQiTIfhJS/sqjrmfAWiAN+x2O4i/XguTL9yDZ+/IFDanJ+5x7hC4CXT9Tdzw==",
"requires": {
"cookie": "0.4.0",
"cookie-signature": "1.0.6"
}
},
"cookie-signature": { "cookie-signature": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",

View File

@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf", "name": "audiobookshelf",
"version": "1.4.3", "version": "1.4.4",
"description": "Self-hosted audiobook server for managing and playing audiobooks", "description": "Self-hosted audiobook server for managing and playing audiobooks",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
@ -8,7 +8,7 @@
"start": "node index.js", "start": "node index.js",
"client": "cd client && npm install && npm run generate", "client": "cd client && npm install && npm run generate",
"prod": "npm run client && npm install && node prod.js", "prod": "npm run client && npm install && node prod.js",
"build-win": "npm run build-prep && pkg -t node12-win-x64 -o ./dist/win/audiobookshelf .", "build-win": "pkg -t node12-win-x64 -o ./dist/win/audiobookshelf .",
"build-linux": "build/linuxpackager" "build-linux": "build/linuxpackager"
}, },
"bin": "prod.js", "bin": "prod.js",
@ -26,7 +26,6 @@
"axios": "^0.21.1", "axios": "^0.21.1",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"command-line-args": "^5.2.0", "command-line-args": "^5.2.0",
"cookie-parser": "^1.4.5",
"date-and-time": "^2.0.1", "date-and-time": "^2.0.1",
"epub": "^1.2.1", "epub": "^1.2.1",
"express": "^4.17.1", "express": "^4.17.1",

View File

@ -24,6 +24,9 @@ class BackupManager {
this.scheduleTask = null this.scheduleTask = null
this.backups = [] this.backups = []
// If backup exceeds this value it will be aborted
this.MaxBytesBeforeAbort = 1000000000 // ~ 1GB
} }
get serverSettings() { get serverSettings() {
@ -191,6 +194,7 @@ class BackupManager {
} }
async runBackup() { async runBackup() {
// Check if Metadata Path is inside Config Path (otherwise there will be an infinite loop as the archiver tries to zip itself)
Logger.info(`[BackupManager] Running Backup`) Logger.info(`[BackupManager] Running Backup`)
var metadataBooksPath = this.serverSettings.backupMetadataCovers ? Path.join(this.MetadataPath, 'books') : null var metadataBooksPath = this.serverSettings.backupMetadataCovers ? Path.join(this.MetadataPath, 'books') : null
@ -233,6 +237,7 @@ class BackupManager {
async removeBackup(backup) { async removeBackup(backup) {
try { try {
Logger.debug(`[BackupManager] Removing Backup "${backup.fullPath}"`)
await fs.remove(backup.fullPath) await fs.remove(backup.fullPath)
this.backups = this.backups.filter(b => b.id !== backup.id) this.backups = this.backups.filter(b => b.id !== backup.id)
Logger.info(`[BackupManager] Backup "${backup.id}" Removed`) Logger.info(`[BackupManager] Backup "${backup.id}" Removed`)
@ -263,6 +268,15 @@ class BackupManager {
Logger.debug('Data has been drained') Logger.debug('Data has been drained')
}) })
output.on('finish', () => {
Logger.debug('Write Stream Finished')
})
output.on('error', (err) => {
Logger.debug('Write Stream Error', err)
reject(err)
})
// good practice to catch warnings (ie stat failures and other non-blocking errors) // good practice to catch warnings (ie stat failures and other non-blocking errors)
archive.on('warning', function (err) { archive.on('warning', function (err) {
if (err.code === 'ENOENT') { if (err.code === 'ENOENT') {
@ -279,6 +293,16 @@ class BackupManager {
Logger.error(`[BackupManager] Archiver error: ${err.message}`) Logger.error(`[BackupManager] Archiver error: ${err.message}`)
reject(err) reject(err)
}) })
archive.on('progress', ({ fs: fsobj }) => {
if (fsobj.processedBytes > this.MaxBytesBeforeAbort) {
Logger.error(`[BackupManager] Archiver is too large - aborting to prevent endless loop, Bytes Processed: ${fsobj.processedBytes}`)
archive.abort()
setTimeout(() => {
this.removeBackup(backup)
output.destroy('Backup too large') // Promise is reject in write stream error evt
}, 500)
}
})
// pipe archive data to the file // pipe archive data to the file
archive.pipe(output) archive.pipe(output)

View File

@ -143,18 +143,21 @@ class Scanner {
forceAudioFileScan = true forceAudioFileScan = true
} }
// ino is now set for every file in scandir // inode is required
audiobookData.audioFiles = audiobookData.audioFiles.filter(af => af.ino) audiobookData.audioFiles = audiobookData.audioFiles.filter(af => af.ino)
// REMOVE: No valid audio files // No valid ebook and audio files found, mark as incomplete
// TODO: Label as incomplete, do not actually delete var ebookFiles = audiobookData.otherFiles.filter(f => f.filetype === 'ebook')
if (!audiobookData.audioFiles.length) { if (!audiobookData.audioFiles.length && !ebookFiles.length) {
Logger.error(`[Scanner] "${existingAudiobook.title}" no valid audio files found - removing audiobook`) Logger.error(`[Scanner] "${existingAudiobook.title}" no valid book files found - marking as incomplete`)
existingAudiobook.setLastScan(version)
await this.db.removeEntity('audiobook', existingAudiobook.id) existingAudiobook.isIncomplete = true
this.emitter('audiobook_removed', existingAudiobook.toJSONMinified()) await this.db.updateAudiobook(existingAudiobook)
this.emitter('audiobook_updated', existingAudiobook.toJSONMinified())
return ScanResult.REMOVED return ScanResult.UPDATED
} else if (existingAudiobook.isIncomplete) { // Was incomplete but now is not
Logger.info(`[Scanner] "${existingAudiobook.title}" was incomplete but now has book files`)
existingAudiobook.isIncomplete = false
} }
// Check for audio files that were removed // Check for audio files that were removed
@ -219,14 +222,15 @@ class Scanner {
await audioFileScanner.scanAudioFiles(existingAudiobook, newAudioFiles) await audioFileScanner.scanAudioFiles(existingAudiobook, newAudioFiles)
} }
// If after a scan no valid audio tracks remain // After scanning audio files, some may no longer be valid
// TODO: Label as incomplete, do not actually delete // so make sure the directory still has valid book files
if (!existingAudiobook.tracks.length) { if (!existingAudiobook.tracks.length && !ebookFiles.length) {
Logger.error(`[Scanner] "${existingAudiobook.title}" has no valid tracks after update - removing audiobook`) Logger.error(`[Scanner] "${existingAudiobook.title}" no valid book files found after update - marking as incomplete`)
existingAudiobook.setLastScan(version)
await this.db.removeEntity('audiobook', existingAudiobook.id) existingAudiobook.isIncomplete = true
this.emitter('audiobook_removed', existingAudiobook.toJSONMinified()) await this.db.updateAudiobook(existingAudiobook)
return ScanResult.REMOVED this.emitter('audiobook_updated', existingAudiobook.toJSONMinified())
return ScanResult.UPDATED
} }
var hasUpdates = hasUpdatedIno || hasUpdatedLibraryOrFolder || removedAudioFiles.length || removedAudioTracks.length || newAudioFiles.length || hasUpdatedAudioFiles var hasUpdates = hasUpdatedIno || hasUpdatedLibraryOrFolder || removedAudioFiles.length || removedAudioTracks.length || newAudioFiles.length || hasUpdatedAudioFiles
@ -269,8 +273,9 @@ class Scanner {
} }
async scanNewAudiobook(audiobookData) { async scanNewAudiobook(audiobookData) {
if (!audiobookData.audioFiles.length) { var ebookFiles = audiobookData.otherFiles.map(f => f.filetype === 'ebook')
Logger.error('[Scanner] No valid audio tracks for Audiobook', audiobookData.path) if (!audiobookData.audioFiles.length && !ebookFiles.length) {
Logger.error('[Scanner] No valid audio files and ebooks for Audiobook', audiobookData.path)
return null return null
} }
@ -279,8 +284,9 @@ class Scanner {
// Scan audio files and set tracks, pulls metadata // Scan audio files and set tracks, pulls metadata
await audioFileScanner.scanAudioFiles(audiobook, audiobookData.audioFiles) await audioFileScanner.scanAudioFiles(audiobook, audiobookData.audioFiles)
if (!audiobook.tracks.length) {
Logger.warn('[Scanner] Invalid audiobook, no valid tracks', audiobook.title) if (!audiobook.tracks.length && !audiobook.ebooks.length) {
Logger.warn('[Scanner] Invalid audiobook, no valid audio tracks and ebook files', audiobook.title)
return null return null
} }

View File

@ -37,6 +37,8 @@ class Audiobook {
// Audiobook was scanned and not found // Audiobook was scanned and not found
this.isMissing = false this.isMissing = false
// Audiobook no longer has "book" files
this.isInvalid = false
if (audiobook) { if (audiobook) {
this.construct(audiobook) this.construct(audiobook)
@ -70,6 +72,7 @@ class Audiobook {
} }
this.isMissing = !!audiobook.isMissing this.isMissing = !!audiobook.isMissing
this.isInvalid = !!audiobook.isInvalid
} }
get title() { get title() {
@ -175,7 +178,8 @@ class Audiobook {
audioFiles: this._audioFiles.map(audioFile => audioFile.toJSON()), audioFiles: this._audioFiles.map(audioFile => audioFile.toJSON()),
otherFiles: this._otherFiles.map(otherFile => otherFile.toJSON()), otherFiles: this._otherFiles.map(otherFile => otherFile.toJSON()),
chapters: this.chapters || [], chapters: this.chapters || [],
isMissing: !!this.isMissing isMissing: !!this.isMissing,
isInvalid: !!this.isInvalid
} }
} }
@ -197,10 +201,12 @@ class Audiobook {
hasMissingParts: this.missingParts ? this.missingParts.length : 0, hasMissingParts: this.missingParts ? this.missingParts.length : 0,
hasInvalidParts: this.invalidParts ? this.invalidParts.length : 0, hasInvalidParts: this.invalidParts ? this.invalidParts.length : 0,
// numEbooks: this.ebooks.length, // numEbooks: this.ebooks.length,
numEbooks: this.hasEpub ? 1 : 0, ebooks: this.ebooks.map(ebook => ebook.toJSON()),
numEbooks: this.hasEpub ? 1 : 0, // Only supporting epubs in the reader currently
numTracks: this.tracks.length, numTracks: this.tracks.length,
chapters: this.chapters || [], chapters: this.chapters || [],
isMissing: !!this.isMissing isMissing: !!this.isMissing,
isInvalid: !!this.isInvalid
} }
} }
@ -220,15 +226,16 @@ class Audiobook {
sizePretty: this.sizePretty, sizePretty: this.sizePretty,
missingParts: this.missingParts, missingParts: this.missingParts,
invalidParts: this.invalidParts, invalidParts: this.invalidParts,
audioFiles: (this.audioFiles || []).map(audioFile => audioFile.toJSON()), audioFiles: this._audioFiles.map(audioFile => audioFile.toJSON()),
otherFiles: (this.otherFiles || []).map(otherFile => otherFile.toJSON()), otherFiles: this._otherFiles.map(otherFile => otherFile.toJSON()),
ebooks: (this.ebooks || []).map(ebook => ebook.toJSON()), ebooks: this.ebooks.map(ebook => ebook.toJSON()),
numEbooks: this.hasEpub ? 1 : 0, numEbooks: this.hasEpub ? 1 : 0,
tags: this.tags, tags: this.tags,
book: this.bookToJSON(), book: this.bookToJSON(),
tracks: this.tracksToJSON(), tracks: this.tracksToJSON(),
chapters: this.chapters || [], chapters: this.chapters || [],
isMissing: !!this.isMissing isMissing: !!this.isMissing,
isInvalid: !!this.isInvalid
} }
} }

View File

@ -16,11 +16,12 @@ function getPaths(path) {
}) })
} }
function isAudioFile(path) { function isBookFile(path) {
if (!path) return false if (!path) return false
var ext = Path.extname(path) var ext = Path.extname(path)
if (!ext) return false if (!ext) return false
return globals.SupportedAudioTypes.includes(ext.slice(1).toLowerCase()) var extclean = ext.slice(1).toLowerCase()
return globals.SupportedAudioTypes.includes(extclean) || globals.SupportedEbookTypes.includes(extclean)
} }
// Input: array of relative file paths // Input: array of relative file paths
@ -36,17 +37,18 @@ function groupFilesIntoAudiobookPaths(paths, useAllFileTypes = false) {
return pathsA - pathsB return pathsA - pathsB
}) })
// Step 2.5: Seperate audio files and other files (optional) // Step 2.5: Seperate audio/ebook files and other files (optional)
var audioFilePaths = [] // - Directories without an audio or ebook file will not be included
var bookFilePaths = []
var otherFilePaths = [] var otherFilePaths = []
pathsFiltered.forEach(path => { pathsFiltered.forEach(path => {
if (isAudioFile(path) || useAllFileTypes) audioFilePaths.push(path) if (isBookFile(path) || useAllFileTypes) bookFilePaths.push(path)
else otherFilePaths.push(path) else otherFilePaths.push(path)
}) })
// Step 3: Group audio files in audiobooks // Step 3: Group audio files in audiobooks
var audiobookGroup = {} var audiobookGroup = {}
audioFilePaths.forEach((path) => { bookFilePaths.forEach((path) => {
var dirparts = Path.dirname(path).split(Path.sep) var dirparts = Path.dirname(path).split(Path.sep)
var numparts = dirparts.length var numparts = dirparts.length
var _path = '' var _path = ''