mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	This commit is contained in:
		
							parent
							
								
									0c168b3da4
								
							
						
					
					
						commit
						04f92c33c2
					
				| @ -6,6 +6,7 @@ npm-debug.log | ||||
| /config | ||||
| /audiobooks | ||||
| /audiobooks2 | ||||
| /media/ | ||||
| /metadata | ||||
| dev.js | ||||
| test/ | ||||
|  | ||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -4,6 +4,7 @@ node_modules/ | ||||
| /config/ | ||||
| /audiobooks/ | ||||
| /audiobooks2/ | ||||
| /media/ | ||||
| /metadata/ | ||||
| test/ | ||||
| /client/.nuxt/ | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| <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"> | ||||
|       <span class="material-icons cursor-pointer text-4xl" @click="show = false">close</span> | ||||
|     </div> | ||||
| @ -35,10 +35,6 @@ | ||||
| import ePub from 'epubjs' | ||||
| 
 | ||||
| export default { | ||||
|   props: { | ||||
|     value: Boolean, | ||||
|     url: String | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       book: null, | ||||
| @ -63,12 +59,34 @@ export default { | ||||
|   computed: { | ||||
|     show: { | ||||
|       get() { | ||||
|         return this.value | ||||
|         return this.$store.state.showEReader | ||||
|       }, | ||||
|       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() { | ||||
|       return this.$store.getters['user/getToken'] | ||||
|     } | ||||
| @ -116,7 +134,7 @@ export default { | ||||
|     init() { | ||||
|       this.registerListeners() | ||||
| 
 | ||||
|       console.log('epub', this.url) | ||||
|       console.log('epub', this.url, this.epubEbook, this.ebooks) | ||||
|       // var book = ePub(this.url, { | ||||
|       //   requestHeaders: { | ||||
|       //     Authorization: `Bearer ${this.userToken}` | ||||
|  | ||||
| @ -14,11 +14,16 @@ | ||||
|           <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="!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"> | ||||
|                 <span class="material-icons" :style="{ fontSize: playIconFontSize + 'rem' }">play_circle_filled</span> | ||||
|               </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"> | ||||
|               <span class="material-icons" :style="{ fontSize: sizeMultiplier + 'rem' }">edit</span> | ||||
| @ -34,9 +39,14 @@ | ||||
|           </div> | ||||
| 
 | ||||
|           <!-- 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> --> | ||||
|             <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 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() { | ||||
|       return this.audiobook.numEbooks | ||||
|     }, | ||||
|     hasTracks() { | ||||
|       return this.audiobook.numTracks | ||||
|     }, | ||||
|     isSelectionMode() { | ||||
|       // return this.$store.getters['getNumAudiobooksSelected'] | ||||
|       return !!this.selectedAudiobooks.length | ||||
|     }, | ||||
|     selectedAudiobooks() { | ||||
| @ -150,11 +162,23 @@ export default { | ||||
|       return this.userProgress ? !!this.userProgress.isRead : false | ||||
|     }, | ||||
|     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() { | ||||
|       return this.audiobook.isMissing | ||||
|     }, | ||||
|     isIncomplete() { | ||||
|       return this.audiobook.isIncomplete | ||||
|     }, | ||||
|     hasMissingParts() { | ||||
|       return this.audiobook.hasMissingParts | ||||
|     }, | ||||
| @ -163,6 +187,7 @@ export default { | ||||
|     }, | ||||
|     errorText() { | ||||
|       if (this.isMissing) return 'Audiobook directory is missing!' | ||||
|       else if (this.isIncomplete) return 'Audiobook has no audio tracks & ebook' | ||||
|       var txt = '' | ||||
|       if (this.hasMissingParts) { | ||||
|         txt = `${this.hasMissingParts} missing parts.` | ||||
| @ -211,6 +236,9 @@ export default { | ||||
|         e.preventDefault() | ||||
|         this.selectBtnClick() | ||||
|       } | ||||
|     }, | ||||
|     clickReadEBook() { | ||||
|       this.$store.commit('showEReader', this.audiobook) | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| <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"> | ||||
|     <div v-if="showM4bDownload" class="w-full border border-black-200 p-4 my-4"> | ||||
|       <div class="flex items-center"> | ||||
|         <div> | ||||
|           <p class="text-lg">M4B Audiobook File <span class="text-error">*</span></p> | ||||
| @ -44,7 +44,7 @@ | ||||
|       </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> | ||||
|     </div> | ||||
| 
 | ||||
| @ -89,6 +89,9 @@ export default { | ||||
|     audiobookId() { | ||||
|       return this.audiobook ? this.audiobook.id : null | ||||
|     }, | ||||
|     _audiobook() { | ||||
|       return this.audiobook || {} | ||||
|     }, | ||||
|     downloads() { | ||||
|       return this.$store.getters['downloads/getDownloads'](this.audiobookId) | ||||
|     }, | ||||
| @ -120,6 +123,9 @@ export default { | ||||
|     }, | ||||
|     totalFiles() { | ||||
|       return this.audioFiles.length + this.otherFiles.length | ||||
|     }, | ||||
|     showM4bDownload() { | ||||
|       return !this._audiobook.isMissing && !this._audiobook.isIncomplete && this._audiobook.tracks.length | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| <template> | ||||
|   <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="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> | ||||
| @ -36,6 +37,8 @@ | ||||
|           </tr> | ||||
|         </template> | ||||
|       </table> | ||||
|     </template> | ||||
|     <div v-else class="flex my-4 text-center justify-center text-xl">No Audio Tracks</div> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| @ -94,6 +97,9 @@ export default { | ||||
|     }, | ||||
|     showDownload() { | ||||
|       return this.userCanDownload && !this.isMissing | ||||
|     }, | ||||
|     hasTracks() { | ||||
|       return this.audiobook.tracks.length | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|  | ||||
| @ -11,7 +11,12 @@ | ||||
|     <div class="flex-grow" /> | ||||
|     <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="!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> | ||||
| </template> | ||||
| 
 | ||||
| @ -27,7 +32,8 @@ export default { | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       mouseover: false | ||||
|       mouseover: false, | ||||
|       isDeleting: false | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
| @ -54,12 +60,29 @@ export default { | ||||
|     editClick() { | ||||
|       this.$emit('edit', this.library) | ||||
|     }, | ||||
|     deleteClick() { | ||||
|       if (this.isMain) return | ||||
|       this.$emit('delete', this.library) | ||||
|     }, | ||||
|     scan() { | ||||
|       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() {} | ||||
|  | ||||
| @ -8,7 +8,7 @@ | ||||
|     </div> | ||||
| 
 | ||||
|     <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> | ||||
|     <modals-edit-library-modal v-model="showLibraryModal" :library="selectedLibrary" /> | ||||
|   </div> | ||||
| @ -38,27 +38,6 @@ export default { | ||||
|       await this.$store.dispatch('libraries/fetch', 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() { | ||||
|       this.selectedLibrary = null | ||||
|       this.showLibraryModal = true | ||||
|  | ||||
| @ -7,6 +7,7 @@ | ||||
|     <app-stream-container ref="streamContainer" /> | ||||
|     <modals-libraries-modal /> | ||||
|     <modals-edit-modal /> | ||||
|     <app-reader /> | ||||
|     <!-- <widgets-scan-alert /> --> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "audiobookshelf-client", | ||||
|   "version": "1.4.3", | ||||
|   "version": "1.4.4", | ||||
|   "description": "Audiobook manager and player", | ||||
|   "main": "index.js", | ||||
|   "scripts": { | ||||
|  | ||||
| @ -57,7 +57,7 @@ | ||||
|                   </template> | ||||
|                 </div> | ||||
|               </div> | ||||
|               <div class="flex py-0.5"> | ||||
|               <div v-if="tracks.length" class="flex py-0.5"> | ||||
|                 <div class="w-32"> | ||||
|                   <span class="text-white text-opacity-60 uppercase text-sm">Duration</span> | ||||
|                 </div> | ||||
| @ -65,7 +65,7 @@ | ||||
|                   {{ durationPretty }} | ||||
|                 </div> | ||||
|               </div> | ||||
|               <div class="flex py-0.5"> | ||||
|               <div v-if="tracks.length" class="flex py-0.5"> | ||||
|                 <div class="w-32"> | ||||
|                   <span class="text-white text-opacity-60 uppercase text-sm">Size</span> | ||||
|                 </div> | ||||
| @ -73,16 +73,20 @@ | ||||
|                   {{ sizePretty }} | ||||
|                 </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 class="flex-grow" /> | ||||
|           </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' : ''"> | ||||
|             <p class="leading-6">Your Progress: {{ Math.round(progressPercent * 100) }}%</p> | ||||
|             <p class="text-gray-400 text-xs">{{ $elapsedPretty(userTimeRemaining) }} remaining</p> | ||||
| @ -92,13 +96,13 @@ | ||||
|           </div> | ||||
| 
 | ||||
|           <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> | ||||
|               {{ streaming ? 'Streaming' : 'Play' }} | ||||
|             </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> | ||||
|               Missing | ||||
|               {{ isMissing ? 'Missing' : 'Incomplete' }} | ||||
|             </ui-btn> | ||||
| 
 | ||||
|             <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> | ||||
| 
 | ||||
|           <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" /> | ||||
| 
 | ||||
| @ -150,7 +154,7 @@ | ||||
|       </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <app-reader v-if="showExperimentalFeatures" v-model="showReader" :url="epubUrl" /> | ||||
|     <!-- <app-reader v-if="showExperimentalFeatures" v-model="showReader" :url="epubUrl" /> --> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| @ -175,7 +179,6 @@ export default { | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       showReader: false, | ||||
|       isRead: false, | ||||
|       resettingProgress: false, | ||||
|       isProcessingReadUpdate: false | ||||
| @ -230,6 +233,12 @@ export default { | ||||
|     isMissing() { | ||||
|       return this.audiobook.isMissing | ||||
|     }, | ||||
|     isIncomplete() { | ||||
|       return this.audiobook.isIncomplete | ||||
|     }, | ||||
|     showPlayButton() { | ||||
|       return !this.isMissing && !this.isIncomplete && this.tracks.length | ||||
|     }, | ||||
|     missingParts() { | ||||
|       return this.audiobook.missingParts || [] | ||||
|     }, | ||||
| @ -313,16 +322,15 @@ export default { | ||||
|     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() { | ||||
|       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() { | ||||
|       return this.$store.getters['user/getToken'] | ||||
|     }, | ||||
| @ -365,7 +373,7 @@ export default { | ||||
|   }, | ||||
|   methods: { | ||||
|     openEbook() { | ||||
|       this.showReader = true | ||||
|       this.$store.commit('showEReader', this.audiobook) | ||||
|     }, | ||||
|     toggleRead() { | ||||
|       var updatePayload = { | ||||
|  | ||||
| @ -7,6 +7,7 @@ export const state = () => ({ | ||||
|   streamAudiobook: null, | ||||
|   editModalTab: 'details', | ||||
|   showEditModal: false, | ||||
|   showEReader: false, | ||||
|   selectedAudiobook: null, | ||||
|   playOnLoad: false, | ||||
|   developerMode: false, | ||||
| @ -111,6 +112,14 @@ export const mutations = { | ||||
|   setShowEditModal(state, 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) { | ||||
|     state.developerMode = val | ||||
|   }, | ||||
|  | ||||
							
								
								
									
										11
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										11
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "audiobookshelf", | ||||
|   "version": "1.4.1", | ||||
|   "version": "1.4.3", | ||||
|   "lockfileVersion": 1, | ||||
|   "requires": true, | ||||
|   "dependencies": { | ||||
| @ -411,15 +411,6 @@ | ||||
|       "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", | ||||
|       "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": { | ||||
|       "version": "1.0.6", | ||||
|       "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "audiobookshelf", | ||||
|   "version": "1.4.3", | ||||
|   "version": "1.4.4", | ||||
|   "description": "Self-hosted audiobook server for managing and playing audiobooks", | ||||
|   "main": "index.js", | ||||
|   "scripts": { | ||||
| @ -8,7 +8,7 @@ | ||||
|     "start": "node index.js", | ||||
|     "client": "cd client && npm install && npm run generate", | ||||
|     "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" | ||||
|   }, | ||||
|   "bin": "prod.js", | ||||
| @ -26,7 +26,6 @@ | ||||
|     "axios": "^0.21.1", | ||||
|     "bcryptjs": "^2.4.3", | ||||
|     "command-line-args": "^5.2.0", | ||||
|     "cookie-parser": "^1.4.5", | ||||
|     "date-and-time": "^2.0.1", | ||||
|     "epub": "^1.2.1", | ||||
|     "express": "^4.17.1", | ||||
|  | ||||
| @ -24,6 +24,9 @@ class BackupManager { | ||||
|     this.scheduleTask = null | ||||
| 
 | ||||
|     this.backups = [] | ||||
| 
 | ||||
|     // If backup exceeds this value it will be aborted
 | ||||
|     this.MaxBytesBeforeAbort = 1000000000 // ~ 1GB
 | ||||
|   } | ||||
| 
 | ||||
|   get serverSettings() { | ||||
| @ -191,6 +194,7 @@ class BackupManager { | ||||
|   } | ||||
| 
 | ||||
|   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`) | ||||
|     var metadataBooksPath = this.serverSettings.backupMetadataCovers ? Path.join(this.MetadataPath, 'books') : null | ||||
| 
 | ||||
| @ -233,6 +237,7 @@ class BackupManager { | ||||
| 
 | ||||
|   async removeBackup(backup) { | ||||
|     try { | ||||
|       Logger.debug(`[BackupManager] Removing Backup "${backup.fullPath}"`) | ||||
|       await fs.remove(backup.fullPath) | ||||
|       this.backups = this.backups.filter(b => b.id !== backup.id) | ||||
|       Logger.info(`[BackupManager] Backup "${backup.id}" Removed`) | ||||
| @ -263,6 +268,15 @@ class BackupManager { | ||||
|         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)
 | ||||
|       archive.on('warning', function (err) { | ||||
|         if (err.code === 'ENOENT') { | ||||
| @ -279,6 +293,16 @@ class BackupManager { | ||||
|         Logger.error(`[BackupManager] Archiver error: ${err.message}`) | ||||
|         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
 | ||||
|       archive.pipe(output) | ||||
|  | ||||
| @ -143,18 +143,21 @@ class Scanner { | ||||
|       forceAudioFileScan = true | ||||
|     } | ||||
| 
 | ||||
|     // ino is now set for every file in scandir
 | ||||
|     // inode is required
 | ||||
|     audiobookData.audioFiles = audiobookData.audioFiles.filter(af => af.ino) | ||||
| 
 | ||||
|     // REMOVE: No valid audio files
 | ||||
|     // TODO: Label as incomplete, do not actually delete
 | ||||
|     if (!audiobookData.audioFiles.length) { | ||||
|       Logger.error(`[Scanner] "${existingAudiobook.title}" no valid audio files found - removing audiobook`) | ||||
| 
 | ||||
|       await this.db.removeEntity('audiobook', existingAudiobook.id) | ||||
|       this.emitter('audiobook_removed', existingAudiobook.toJSONMinified()) | ||||
| 
 | ||||
|       return ScanResult.REMOVED | ||||
|     // No valid ebook and audio files found, mark as incomplete
 | ||||
|     var ebookFiles = audiobookData.otherFiles.filter(f => f.filetype === 'ebook') | ||||
|     if (!audiobookData.audioFiles.length && !ebookFiles.length) { | ||||
|       Logger.error(`[Scanner] "${existingAudiobook.title}" no valid book files found - marking as incomplete`) | ||||
|       existingAudiobook.setLastScan(version) | ||||
|       existingAudiobook.isIncomplete = true | ||||
|       await this.db.updateAudiobook(existingAudiobook) | ||||
|       this.emitter('audiobook_updated', existingAudiobook.toJSONMinified()) | ||||
|       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
 | ||||
| @ -219,14 +222,15 @@ class Scanner { | ||||
|       await audioFileScanner.scanAudioFiles(existingAudiobook, newAudioFiles) | ||||
|     } | ||||
| 
 | ||||
|     // If after a scan no valid audio tracks remain
 | ||||
|     // TODO: Label as incomplete, do not actually delete
 | ||||
|     if (!existingAudiobook.tracks.length) { | ||||
|       Logger.error(`[Scanner] "${existingAudiobook.title}" has no valid tracks after update - removing audiobook`) | ||||
| 
 | ||||
|       await this.db.removeEntity('audiobook', existingAudiobook.id) | ||||
|       this.emitter('audiobook_removed', existingAudiobook.toJSONMinified()) | ||||
|       return ScanResult.REMOVED | ||||
|     // After scanning audio files, some may no longer be valid
 | ||||
|     //   so make sure the directory still has valid book files
 | ||||
|     if (!existingAudiobook.tracks.length && !ebookFiles.length) { | ||||
|       Logger.error(`[Scanner] "${existingAudiobook.title}" no valid book files found after update - marking as incomplete`) | ||||
|       existingAudiobook.setLastScan(version) | ||||
|       existingAudiobook.isIncomplete = true | ||||
|       await this.db.updateAudiobook(existingAudiobook) | ||||
|       this.emitter('audiobook_updated', existingAudiobook.toJSONMinified()) | ||||
|       return ScanResult.UPDATED | ||||
|     } | ||||
| 
 | ||||
|     var hasUpdates = hasUpdatedIno || hasUpdatedLibraryOrFolder || removedAudioFiles.length || removedAudioTracks.length || newAudioFiles.length || hasUpdatedAudioFiles | ||||
| @ -269,8 +273,9 @@ class Scanner { | ||||
|   } | ||||
| 
 | ||||
|   async scanNewAudiobook(audiobookData) { | ||||
|     if (!audiobookData.audioFiles.length) { | ||||
|       Logger.error('[Scanner] No valid audio tracks for Audiobook', audiobookData.path) | ||||
|     var ebookFiles = audiobookData.otherFiles.map(f => f.filetype === 'ebook') | ||||
|     if (!audiobookData.audioFiles.length && !ebookFiles.length) { | ||||
|       Logger.error('[Scanner] No valid audio files and ebooks for Audiobook', audiobookData.path) | ||||
|       return null | ||||
|     } | ||||
| 
 | ||||
| @ -279,8 +284,9 @@ class Scanner { | ||||
| 
 | ||||
|     // Scan audio files and set tracks, pulls metadata
 | ||||
|     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 | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -37,6 +37,8 @@ class Audiobook { | ||||
| 
 | ||||
|     // Audiobook was scanned and not found
 | ||||
|     this.isMissing = false | ||||
|     // Audiobook no longer has "book" files
 | ||||
|     this.isInvalid = false | ||||
| 
 | ||||
|     if (audiobook) { | ||||
|       this.construct(audiobook) | ||||
| @ -70,6 +72,7 @@ class Audiobook { | ||||
|     } | ||||
| 
 | ||||
|     this.isMissing = !!audiobook.isMissing | ||||
|     this.isInvalid = !!audiobook.isInvalid | ||||
|   } | ||||
| 
 | ||||
|   get title() { | ||||
| @ -175,7 +178,8 @@ class Audiobook { | ||||
|       audioFiles: this._audioFiles.map(audioFile => audioFile.toJSON()), | ||||
|       otherFiles: this._otherFiles.map(otherFile => otherFile.toJSON()), | ||||
|       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, | ||||
|       hasInvalidParts: this.invalidParts ? this.invalidParts.length : 0, | ||||
|       // 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, | ||||
|       chapters: this.chapters || [], | ||||
|       isMissing: !!this.isMissing | ||||
|       isMissing: !!this.isMissing, | ||||
|       isInvalid: !!this.isInvalid | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| @ -220,15 +226,16 @@ class Audiobook { | ||||
|       sizePretty: this.sizePretty, | ||||
|       missingParts: this.missingParts, | ||||
|       invalidParts: this.invalidParts, | ||||
|       audioFiles: (this.audioFiles || []).map(audioFile => audioFile.toJSON()), | ||||
|       otherFiles: (this.otherFiles || []).map(otherFile => otherFile.toJSON()), | ||||
|       ebooks: (this.ebooks || []).map(ebook => ebook.toJSON()), | ||||
|       audioFiles: this._audioFiles.map(audioFile => audioFile.toJSON()), | ||||
|       otherFiles: this._otherFiles.map(otherFile => otherFile.toJSON()), | ||||
|       ebooks: this.ebooks.map(ebook => ebook.toJSON()), | ||||
|       numEbooks: this.hasEpub ? 1 : 0, | ||||
|       tags: this.tags, | ||||
|       book: this.bookToJSON(), | ||||
|       tracks: this.tracksToJSON(), | ||||
|       chapters: this.chapters || [], | ||||
|       isMissing: !!this.isMissing | ||||
|       isMissing: !!this.isMissing, | ||||
|       isInvalid: !!this.isInvalid | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -16,11 +16,12 @@ function getPaths(path) { | ||||
|   }) | ||||
| } | ||||
| 
 | ||||
| function isAudioFile(path) { | ||||
| function isBookFile(path) { | ||||
|   if (!path) return false | ||||
|   var ext = Path.extname(path) | ||||
|   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
 | ||||
| @ -36,17 +37,18 @@ function groupFilesIntoAudiobookPaths(paths, useAllFileTypes = false) { | ||||
|     return pathsA - pathsB | ||||
|   }) | ||||
| 
 | ||||
|   // Step 2.5: Seperate audio files and other files (optional)
 | ||||
|   var audioFilePaths = [] | ||||
|   // Step 2.5: Seperate audio/ebook files and other files (optional)
 | ||||
|   //              - Directories without an audio or ebook file will not be included
 | ||||
|   var bookFilePaths = [] | ||||
|   var otherFilePaths = [] | ||||
|   pathsFiltered.forEach(path => { | ||||
|     if (isAudioFile(path) || useAllFileTypes) audioFilePaths.push(path) | ||||
|     if (isBookFile(path) || useAllFileTypes) bookFilePaths.push(path) | ||||
|     else otherFilePaths.push(path) | ||||
|   }) | ||||
| 
 | ||||
|   // Step 3: Group audio files in audiobooks
 | ||||
|   var audiobookGroup = {} | ||||
|   audioFilePaths.forEach((path) => { | ||||
|   bookFilePaths.forEach((path) => { | ||||
|     var dirparts = Path.dirname(path).split(Path.sep) | ||||
|     var numparts = dirparts.length | ||||
|     var _path = '' | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user